PWN-基础-ret系列
- 前面的示例程序中,在main函数都会存在shellcode。
- 接下来看在main函数中不存在shellcode的情况,而main函数以外的,自定义的函数却存在shellcode
前置知识
-
CPU可以读内存也可以读取寄存器,主要还是读取寄存器
-
有一种计算机体系结构,没有内存,CPU直接去读硬盘。但是硬盘的读取速度太慢,而CPU处理的速度很快。这就会影响效率。
- 此后就出现了内存,而目前我们要考虑的是CPU读取内存
- 在计算机发展的过程中,内存与CPU直接又出现了缓存、保护模式、虚拟内存
内存虚拟地址空间
偏移
栈
堆
函数传参
ret系列思路
- rip地址控制的是CPU要执行的指令,调用完函数是需要ret指令将地址返回到main函数里面去的。
- 此时我们通过栈溢出,修改压入栈中的返回地址,将返回地址修改成我们的后门函数。那么rip就会执向后门函数的执行程序。
- 这样就可以使得,在main函数中没有调用的后门函数得以执行,获取控制权,拿到flag
ret2text
例题1
-
ret指定函数,不带形参
-
示例程序(x86,而不是x64)
-
gcc编译指令
gcc question_4_1.c -m32 -fno-stack-protector -o question_4_1_x86
1 | #include <stdio.h> |
查看程序
- 查看保护
- 查看文件类型
- ELF:32-bit 32位的文件
运行程序
- 发现有一个输入点,没有输出点
拖入IDA
- 第一步查看到文件是32位的文件,应该拖入32位的ida文件去反汇编
- F5反编译,再查看函数,最后发现在main函数中没有执行后门函数
- SHIFT+F12,发现字符串里面有一个bin/sh
- 找到调用/bin/sh的函数
使用gdb动调
- 开始运行程序
- si步入进dofunc()函数
- 在调试中发现,输入点的输入的数据,存储在main函数地址的上方,这个时候就可以进行操作了
- 在ida中可以看出,main函数的起始地址在整个程序段中的相对地址,和func在整个程序段中的相对地址,然后再看gdb动调的实际地址,得到起始地址(这里gdb也有一个指令
p &func
,可以看到func函数的地址)
- 这时再计算func()函数的绝对地址
- gdb动调看看要输入多少个字符才能到返回地址所在的栈,这里也可以静态调试,用ida查看栈(这里ida更方便)
- 发现存储main函数的地址的栈,经过计算要填充0x14个a才能栈溢出到该栈。
- 尝试栈溢出,发现正常输入可能解决不了,直接用set修改执行地址看看
- 通过修改栈中的值就可以使得ret指令返回的不是main函数,而是func函数
exp
1 | from pwn import * |
作业1
-
例题1的进阶版
-
编译指令
gcc question_4_1.c -m32 -fno-stack-protector -no-pie -fomit-frame-pointer -o question_4_1_x86_sep
-
指令意思:
-m32
以32位模式编译,-fno-stack-protector
不要为局部变量启用堆栈保护,-no-pie
不要生成位置无关可执行文件,会禁用PIE的生成,从而生成传统的固定地址可执行文件,-formit-frame-pointer
用于控制是否在生成的代码中包含帧指针,启用该指令就不是ebp寻址了,而是esp寻址
-
代码
1 |
|
对比
- 为了更直观的能感受到作业1和例题1的区别,故使用gdb动调对这俩个程序进行对比
- 用三个不同点
- 寻址方式不同,question_4_1_x86中都是用ebp进行寻址的
question_4_1_x86
中
而question_4_1_x86_sep
- 返回main函数的地址存储所在的栈不同
question_4_1_x86
而在question_4_1_x86_sep
中main函数的返回地址就会在ebp的所指向栈的上面,而不是在ebp所指向栈的下面一个栈。
- 静态调试的栈
question_4_1_x86
而question_4_1_x86_sep
中没有s,直接到r
exp
1 | from pwn import * |
作业2
- 将b数组的长度由原来的8,该为23
- 由于一个栈存储16位数据,那么23是个质数,就会出现该数组存入栈中,但是
1 |
|
例题2
-
ret指定函数,带形参,x86,32位
-
gcc编译
-
程序源码
1 |
|
重点分析
- 在正常调用函数的时候都会有call
- 而call的指令有
- push
- jmp
- 而在非法调用函数的时候,是没有call指令的。直接用ret指令跳到非法调用的函数中。但是缺少了把rip寄存器压入栈中。这就会导致这种情况的出现
原本ebp下面一行应该是返回地址,但是却变成了参数的地址
所以在放我们想要返回非法函数的地址时,需要先将执行完func之后的返回地址压入栈中
作业3
- 将例题2源码中,read函数的读取字符数缩小
- x86,32位编译
1 |
|
例题3
- ret指定函数,代参数,64位传参模式
测试64位函数调用
- 64位函数调用模式测试代码
1 |
|
- 发现传参不再使用pop
- 还有在传递全局变量的时候是用段寄存器读取值
- 再对程序进行修改
1 |
|
-
使用ida反汇编会发现
-
当参数太多,寄存器不够用的时候。才会把一些参数通过压栈的方式进行传参。
-
而压栈是通过edi,rdi寄存器进行的
1 | mov edi, [rbp+var_4] |
ret2libc
-
ret2text中,通过栈溢出修改返回地址,可以调用到system函数,而调用函数的地址通常是可以通过IDA静态编译得到
-
而ret2libc中,编写的程序没有关于调用system的函数。使用IDA静态反汇编得不到system函数的地址。这时通常需要返回到libc库中的system的地址得到shellcode。
-
三个问题:
- 程序中没有system函数就一定没有system函数吗?
- 怎么计算libc的基地址
- 怎么泄露libc里面存在的某个函数的地址
前置知识
- 在了解ret2libc的原理之前,先要有一些前置知识。
链接
- 在gcc入门里面有介绍
地址
-
绝对地址
-
相对地址
-
偏移量
绝对地址
- 在以内存起点为0,程序在内存中的地址就是绝对地址,下图中所示的就是绝对地址
基地址:程序头在内存中的地址
相对地址
- 相对地址就是一个指令或一个函数等在程序段中的地址,通常把程序开头的地址看作是
0x000000
,相对该程序开头的距离多少
偏移量
- 在栈溢出偏移量就是,要输入多少个字符,才能溢出到返回地址。
- 而在程序中的偏移量,可以简单的理解为某一段在程序中的相对地址
计算libc库中某个函数的绝对地址
- 在已知,libc库中puts函数的绝对地址、相对地址,还有system函数的相对地址,怎么计算出system函数的绝对地址
plt表和got表
-
PLT(Procedure Linkage Table)程序链接表
-
GOT (Global Offset Table)全局偏移表
-
关于俩个表的使用及其调用过程
在调用函数的过程中,如果没有开启保护,那么就会进行如下操作
例题1
- 示例程序
- gcc编译指令
gcc question_5.c -fno-stack-protector -no-pie -o question_5_x64
1 |
|
分析
- 先查看一下该可执行文件的保护情况
动调
- 进行动态调试,步入到dofunc函数里面中的
call read@plt
指令中 - 会发现read函数将用户输入存储在栈中的位置,输入8个字节就到rbp所指向的位置,再输入8个字节就可以覆盖返回地址了
- 在原先的
ret2text
中,接下来要做的就是覆盖返回地址到后门函数中,但是现在程序中没有后门函数 - 这里的动调会用上新的指令
watch *0x404018
设置内存断点
- 由于源码中没有设置后门函数,我们就需要想办法找到
/bin/sh
和system
函数的地址,接下来就开始寻找。使用gdb指令search '/bin/sh'
和p &system
- 寻找后会发现,在libc.so.6中存在
/bin/sh
分析write函数
- 设置断点到
call write
指令,运行到该位置后,si
进入到write函数的内部,发现是在一个名为write@plt
函数中 - 下图可知,接下来write函数会跳转到
0x401030
处 - 而跳转后,接下去会继续跳转,跳转到
0x401020
处 - 之后又会跳转到
0x7ffff7fd8d30
处,一个名为 <_dl_runtime_resolve_xsavec>的函数
- ni下去,会发现程序会跳转到真正的write函数的内部
- si步入进去,会发现,write函数里面会有调用
syscall
- 之后就会输出
input:
这个字串了
- 使用
vmmap
指令查看write函数的地址 - 会发现write函数是在
0x7ffff7db3000 0x7ffff7f48000 r-xp 195000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
这个位置里面
- 这涉及到内存的一些知识
- 一般来说
write read printf puts
等函数都会放在红色区域,而现在的重点就放在libc.so.6
这个库中,前面查看system函数的地址也是放在这里的
- 对system进行反汇编,可以看到system函数的汇编代码,但是system函数里面不存在
/bin/sh
参数
总的来说:libc函数里面存在着一些函数比如 read printf puts write
甚至是重要的 system
还有 /bin/sh
字符串
分析程序进入write函数的过程
- 先步入到dofunc中要调用write函数的指令中,不入进去
- 在这个位置可以先查看一下got表,会发现got表还没有写write函数的地址,而里面的值存储的却是plt表的地址
- 从该地址开始,反汇编,然后会发现进入plt表,而plt表在函数又会跳转到got表中
- 这里使用
watch *404018指令
观察got表的值是否被修改,会发现程序结束了在plt表和got表之间反复横跳之后,got里面的值会被改变
- 说明在下图指令中的上一个指令got的值就被改变了
- 但是当调用完第一次write,输出
input:
后,再调用第二次write输出bye bye
的时候就不会出现第一次调用时那么一大堆的程序
漏洞思路
- 通过栈溢出和布置ROP链泄露got表中,write的地址,将write的值给输出到显示屏上,然后计算system和
/bin/sh
的地址,从而获取权限
例题2
-
32位的程序,函数调用的规则不同,不用构造rop链,32位的libc库自己去寻找
-
gcc编译指令
gcc question_5.c -m32 -fno-stack-protector -no-pie -o question_5_x86
-
程序源码
1 | #include <stdio.h> |
例题3
- 当题目没有给libc.so文件应该怎么办
ret2csu
介绍
-
在使用ida的时候注意到,有函数的名称叫做
libc_csu_init
和libc_csu_fini
-
这个函数是gcc在编译的过程中生成的,只要是正常的程序编译完都会有该函数,不同版本的gcc生成的指令有所差别
-
而
libc_csu_init
函数中结尾有个典型的特点,连续pop
,并且有调用函数,还能控制3个寄存器(这3个寄存器都涉及到函数的参数传递),这是一段非常好的构造ROP链的代码 -
红色框下方的汇编代码,只要不执行跳转,那么就可以走下去,利用pop传参,这就需要ROP链既要控制rbp,也要控制rbx
1 | add rbx,1 |
- 由此可以得到思路先ret到连续pop处,在ret到红框处,继续pop参数,
例题1
-
64位的程序,gcc编译
-
程序源码
1 |
|
ret2syscall
- syscall就是中断
- 64位是syscall,32位的是in0x80
介绍
-
当程序是静态编译的情况,并没有libc库的情况
-
静态编译指令
gcc question_5.c -fno-stack-protector -no-pie -static -o question_5_x64
gcc question_5.c -fno-stack-protector -no-pie -static -o question_5_x64
-
静态编译不再解释,在该篇博客gcc入门有介绍静态编译PWN入门 | iyheart的博客
-
静态编译的程序,调用时就不会跳来跳去,而是直接跳转的被调用程序的地址处
-
这种情况下就程序就没有system函数了
system函数的实现过程
-
既然没有system函数,那么就要另找出路了,这时就需要知道system函数具体是怎么实现的
-
在实行system函数的时候会跳转到俩个地方,一个是loc_522A0另一个是sub_51CD0,而在sub_51CD0的下方一点,有会看到字串**/bin/sh**
system反编译代码
- 下图为system函数的反编译代码,会更直观
- 最后会发现跳转到sys_execve函数就不会跳转了,查看这行代码的反汇编,会发现syscall
- 所以system函数核心是执行execve函数,而execve函数的核心又为syscall
execve函数
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
- execve函数有三个参数
- 第一个参数:需要执行文件的文件名
- 第二个参数:执行文件的参数
- 第三个参数:执行文件的环境
- 通常来说只输入可执行文件名,那么其他俩个参数都默认为0
- 这时当没有system函数时,就栈溢出返回到该函数,并且设置第一个参数为**/bin/sh**,第二个参数为0,第三个参数也为0,这样就可以得到shell
- 查看system函数源码
漏洞思路
- 在静态编译中寻找execve函数或者寻找syscall函数
- 查找发现例题1没有execve函数和字串,也没有syscall函数,但是有syscall的字串而且该字串在text段,而且syscall字串还不只一个,这就需要去花时间寻找正确的syscall
- 模仿execve调用syscall,最终执行/bin/sh
- 这时可以使用ROPgadget去寻找syscall,注意:寻找的syscall有时后面需要跟一个ret,要不然ROP链布置不了
布栈思路
1 | 重点:当调用syscall的时候 |
例题1
-
静态编译是不需要泄露地址的,直接ROP打就行
-
64位静态编译
1 |
|
-
先使用
ROPgadget --binary question_5_x64_static --only 'syscall'
查看syscall所在的地址 -
再使用
ROPgadget --binary question_5_x64_static --only 'pop|ret'
查看ROP链,需要的是rax rdi rsi rdx
例题2
- 32位,静态编译,源码同例题1。
- 主要也是传递参数的区别,系统调用号也是看eax的,32位的execve系统调用号是11也就是0xB,而参数传递的是用栈,但是参数最终会传递给寄存器。寄存器接受参数就是
第一个参数 ebx ,第二个参数ecx,第三个参数 edx
,系统中断也不是syscall,而是int0x80,十六进制指令为cd 80 - 有的时候32位也需要构造ROP链
ret2shellcode
-
shellcode大全:Shellcodes database for study cases (shell-storm.org)
-
shellcode生成工具alpha3:https://github.com/Skylined/alpha3
-
主要就是如何去构造shellcode
-
三个方向
- 长度
- 字符:字符的数量、可显字符串、不可显字符串
题目没有libc库
- 有些题目不会给libc库,这时就要借助在线的libc库或者离线的工具对libc库进行还原
libc-database
- 网址:libc-database
- 先构造ROP链,多泄露几个地址,就可以在该网站上检索出来libc库的版本
- 然后可以确定system的偏移量、/bin/sh的偏移量等
libc-database下载到本地
-
去github上下载项目:niklasb/libc-database: Build a database of libc offsets to simplify exploitation (github.com)
- 下载解压后将该文件放入Linux里面,然后进入该文件
libc-database-master
- 然后输入命令,就会将大量的libc文件下载到
libc-database-master
目录的db
文件夹里面 - 注意这个下载需要一两个小时,会比较慢,但是下载完总的文件大小没到1G,不是很大
1 | ./get ubuntu |
- 下载好后进入
db
文件夹
- 支持的命令
1 | find 用于根据符号和偏移查找libc版本,打印libc ID。 |
- 这里先泄露一个puts函数的libc地址,作为例子
数据溢出
通常为整数溢出和浮点数溢出
或者格式化字符串漏洞
- 数据存储格式
- 数据取值范围
- 数据基本运算
没有/bin/sh怎么办
- /bin/sh的机器码
自己写/bin/sh
- 利用Python脚本和栈溢出,写出/bin/sh再输入到栈里面,这样就有/bin/sh,调用system函数的时候就有参数了
- 调用的system函数后面一定要有ret,要不然调用不了第二次,就没办法获取权限
写/bin/sh的地方
- 向可写段写入/bin/sh,注意有align(对齐)是一定可以写的
怎么去写入/bin/sh
- 利用syscall的参数0去写入/bin/sh
程序找/sh
- 利用程序现成的字串,去找以sh结尾的字符串,必须是sh结尾,这样读到空字符就不会再往下读取了
程序中找$0
- $0对应的机器码是24 30
- 在24 30之后必须跟空字符,即
24 30 00