0*01 概述
本文介绍个人学习 pwn 过程中的一些总结, 包括常用方法, 网上诸多教程虽然有提供完整的 exp, 但并未解释 exp 为什么是这样的, 比如 shellcode 写到哪里去了(这关系到跳转地址),ROP 链怎么选择的. 对于 pwn, 本人也是新手, 其中有总结错误的, 欢迎各位大佬指正.
文中用到的测试程序都在: https://github.com/silience/pwn
0*02 PWN 常用的基本知识
首先拿到一个 PWN 程序, 可以先使用 file 命令, 判断是 32 位还是 64 位.
可以使用 objdump 读取 plt 和 got 表, plt 和 got 网上都有详细的介绍, 再此不再赘述.
这边要提一下数据在寄存器中的存放顺序, 这个在格式化字符串漏洞中要格外注意, 特别是 64 位, 32 位的先后顺序是 eax->edx->ecx->ebx,64 位的先后顺序是 rdi->rsi->rdx->rcx->r8->r9.
刚开始学习的时候, 个人经常把 pop 和 push 经常搞反, 因此在此把这两个指令的介绍说一下: push [reg]/[num] 是将 reg 寄存器中的值或是数字 num 压入堆栈中, 而 pop [reg]是将堆栈栈顶的值弹出到 reg 寄存器中, 并将这个值从堆栈中删去.
有时候要查看寄存器中的值, 可以用到如下命令:
print $esp: 打印 esp 的值
x/10x $esp: 打印出 10 个从 esp 开始的值
x/10x $esp-4: 打印出 10 个从偏移 4 开始的值
x/10gx $esp: 以 64 位格式打印
下面先使用 hello 练练手, 首先使用 IDA 的 F5 大法可以看到内部有个 getshell 函数, 可以直接跳转到该函数 getshell.
使用工具 pade 可以很方便的计算出偏移量, pattern create 100.
pattern offset 0*41284141, 计算出偏移量为 22.
查看汇编代码, 获取 getshell 的地址, 也就是要跳转的地址.
最后得到完整的 exp 如下.
0*03 shellcode
生成方式
1, 在 shellcode 数据库网站找一个 shellcode, http://shell-storm.org/shellcode/
2, 使用 kali 的 msfvenon 生成 shellcode, 如命令 msfvenon -p Linux/x86/exec CMD=/bin/sh -f python
3, 使用 pwntools 自带的函数如 asm(shellcraft.sh())
但有时候不知道 shellcode 写到哪里去了, 在回答这个问题前, 要提一下 bss 段, data 段, text 段, 堆 (heap), 栈(stack) 的一些区别.
1,bss 段 (bss segment) 通常是指用来存放程序中未初始化的全局变量的一块内存区域, bss 段属于静态内存分配.
2,data 段: 数据段 (data segment) 通常是指用来存放程序中已初始化的全局变量的一块内存区域, 数据段属于静态内存分配.
3,text 段: 代码段 (code segment/text segment) 通常是指用来存放程序执行代码的一块内存区域. 这部分区域的大小在程序运行前就已经确定, 并且内存区域通常属于只读(某些架构也允许代码段为可写, 即允许修改程序). 在代码段中, 也有可能包含一些只读的常数变量, 例如字符串常量等.
4, 堆(heap): 堆是用于存放进程运行中被动态分配的内存段, 它的大小并不固定, 可动态扩张或缩减. 当进程调用 malloc 等函数分配内存时, 新分配的内存就被动态添加到堆上(堆被扩张); 当利用 free 等函数释放内存时, 被释放的内存从堆中被剔除(堆被缩减).
5, 栈 (stack): 栈又称堆栈, 是用户存放程序临时创建的局部变量, 也就是说我们函数括弧 "{}" 中定义的变量(但不包括 static 声明的变量, static 意味着在数据段中存放变量). 除此以外, 在函数被调用时, 其参数也会被压入发起调用的进程栈中, 并且待到调用结束后, 函数的返回值也会被存放回栈中. 由于栈的先进先出(FIFO) 特点, 所以栈特别方便用来保存 / 恢复调用现场.
下面以 ret2shellcode, 同样使用 IDA 看下代码, 很明显, shellcode 写入到 bss 段.
使用命令 readelf -S ret2shellcode 查看获取 bss 段地址为 0x0804a040.
还必须保证 bss 段有可执行权限, shellcode 才能运行, 可用 gdb 调试的 vmmap 命令查看, 发现 bss 段可读可写可执行. 范围是 0x0804a000 到 0x0804b000,bss 段地址 0x0804a040 在这个区间, 且必须保证 shellcode 长度不超过这个区间即可, 但到目前为止, shellcode 具体地址依然不知道.
这时可以去调用它的函数 strncpy 前查看汇编代码, 一般通过 push 或者 move 进行参数传递, 参数传递顺序是从右到左, 可以定位到 shellcode 地址 0x804a80.
最后 exp 如下.
shellcode 地址的位置其实是一个坑. 因为正常的思维是使用 gdb 调试目标程序, 然后查看内存来确定 shellcode 的位置. 但当你真的执行 exp 的时候你会发现 shellcode 压根就不在这个地址上! 这是为什么呢? 原因是 gdb 的调试环境会影响 buf 在内存中的位置, 虽然我们关闭了 ASLR, 但这只能保证 buf 的地址在 gdb 的调试环境中不变, 但当我们直接执行的时候, buf 的位置会固定在别的地址上. 怎么解决这个问题呢? 有两种方法, 一种是 开启 core dump 这个功能, 另外一种是使用 GDB 的 attach 功能.
可以使用 level1 练手, 有时 checksec 显示 PIE 关闭.
其实用 ldd 会发现, 地址依然会随机变化.
可使用命令 echo 0> /proc/sys/kernel/randomize_va_space 关掉整个 Linux 系统的 ASLR 保护, 再进行调试; 开启 core dump 这个功能, 开启之后, 当出现内存错误的时候, 系统会生成一个 core dump 文件在 tmp 目录下. 然后我们再用 gdb 查看这个 core 文件就可以获取到 buf 真正的地址了.
- ulimit -c unlimited
- sudo sh -c 'echo"/tmp/core.%t"> /proc/sys/kernel/core_pattern'
0*04 格式化字符串漏洞
这要讲一下字节序.
大端就是: 存储最高有效字节在最小的地址(网络传输文件存储常用).
小端就是: 存储最低有效字节在最小的地址(计算机内部存储).
帮助记忆的法子: 小端就是存储先存最小有效字节, 大端就是先存最大有效字节.
printf 函数的格式化字符串常见的有 %d,%f,%c,%s(用于读取内存数据),%x(输出 16 进制数, 前面没有 0x),%p(输出 16 进制数, 前面带有 0x);%n 是一个不经常用到的格式符, 它的作用是把前面已经打印的长度写入某个内存地址, 用于修改内存, 除了 %n, 还有 %hn,%hhn,%lln, 分别为写入目标空间 4 字节, 2 字节, 1 字节, 8 字节.
去读内存, 假如当偏移量为 5 时:
./a.out "`printf"\0x78\x56\x34\x12"`.%08x.%08x.%08x.%08x.%08s"
或者直接使用读取地址 0*12345678 的内容:
./a.out "`printf"\0x78\x56\x34\x12"`.%5\$s"
比如要将跳转地址 0x0804a048 改 data 为 0*12345678, 可使用 %hhn; 因为使用的是小段序, 高字节保存在高地址.
所以 poc 如下, 偏移量要从 6 开始, 应为 \ x4b\xa0\x04\x08 保存在偏移地址 6.
./a.out "`printf"%18c%6\$hhn"."%34c%7\$hhn"." %34c%8\$hhn "."%34c%9\$hhn "."\x4b\xa0\x04\x08"."\x4b\xa0\x04\x08"."\x4b\xa0\x04\x08"."\x4b\xa0\x04\x08"
但是为什么依次是 %18c,%34c,%34c,%34c; 第一个是 0*12, 很简单, 变成十进制就是 18; 第二个是 0*34, 十进制 52, 第二次总写入数包括第一次的, 即 18+34=52; 后面两次依此类推.
实际使用中, 可以直接使用 pwntools 的函数 fmtstr_payload, 或者 fmt_str(offset,size,addr,target)(其中 offset 表示要覆盖的地址最初的偏移, size 表示机器字长, addr 表示将要覆盖的地址, target 表示我们要覆盖为的目的变量值)直接覆盖.
可以以湖湘杯 2017 的 pwn200 进行练手, 使用 IDA, 发现很明显的格式化字符串漏洞.
首先输入 AAAA.%X.%X.%X.%X.%X.%X.%X.%X, 可以发现在第七个 %X 输出 41414141,A 的 ascii 码是 41(有时是 61616161,a 的 ascii 码 61, 因为程序把输入转换成小写), 可知偏移量是 7, 首先使用 %s 获取 puts 函数的真实地址, 然后计算出 system 的真实地址, 后面再利用函数 fmtstr_payload, 将 atoi 的地址替换为 system 地址, 当执行 atoi 时, 就会这些 system 函数, 从而获取 shell.
0*05 libc
libc 中提供了大量的函数, gdb 调试时可直接使用如下命令获取地址, 如果未提供, 可以去网站 http://libcdb.com/ 下载对应的文件.
可依次执行以下命令, 快速 getshell.
- print system# 获取 system 函数地址.
- print __libc_start_main
- find 0xb7e393f0, +2200000, "/bin/sh"# 获取参数 "/bin/sh" 的地址
以 level2 为例, exp 如下, 利用链: 偏移数据 + system 地址 + 返回地址 + 参数地址, 本例是通过 system 获得 shell, 不需要做其他操作, 所以返回地址可以随便写.
0*06 ROP
Rop 链顺序, 首先是跳转地址, 比如要调用的内置函数 write 泄露出 system 地址, 然后是返回地址(如果泄露的地址要重复使用, 则返回地址是 write 地址或者它前面的地址), 再就是传递的参数是从右往左入栈.
以 ret2syscall 为例, rop 链构造如下: 因为要调用 execve("/bin/sh",NULL,NULL), 该系统函数的调用号为 0xb, 因此首先要将 0xb 给 eax 寄存器, 可使用 ROPgadget -binary ret2syscall -only "pop|ret" | grep "eax" 进行查找.
因为函数 execve 有三个参数, 接着可以使用命令.
ROPgadget -binary ret2syscall -only "pop|pop|pop|ret" | grep "ebx", 不能选包含 esi(esi 是下条指令执行地址)或者 ebp(栈基址寄存器).
使 ROPgadget -binary ret2syscall -string '/bin/sh', 可查找参数 / bin/sh 的地址.
最后再跳转到 int 0*80 的地址就可执行对应的系统调用, 也就是 execve 函数, 可通过 ROPgadget -binary ret2syscall -only 'int', 找 int 0*80 的地址.
最后完整的 exp 如下.
来源: http://www.tuicool.com/articles/FVjaM3n