21~30
21.铁人三项(第五赛区)_2018_rop
(1)分析
main
be_nice_to_people
vulnerable_function
有read漏洞,buf可以溢出
没有system和/bin/sh字符串,动态编译,堆栈保护也开了,基本确定是ret2libc方法
我们可以用main函数中的write函数泄露libc版本
(2)payload
1 |
|
22.bjdctf_2020_babyrop
(1)分析
main
init函数
明示ret2libc了,那么我们就用puts函数泄露libc版本
vuln函数
read函数漏洞,可溢出
64位,用ROPgadget找到pop_rdi_ret=0x400733
cyclic调试出偏移地址为40字节,即0x28
准备工作完成,接下来就是构造payload
(2)payload
1 |
|
21、22题总结
*ret2libc的攻击思路
发送了一个换行符,所以要减掉0xa
PLT可以称作内部函数表,而GOT称为全局函数表。这两个表是相对应的,也就是PLT上的某一个函数地址就对应GOT上的一个地址
在第一次执行的时候,GOT表中的数据为@plt函数中下一条指令的地址,上面提到过,已加粗。
puts_plt为plt表puts项内容,此处相当于调用puts()
栈溢出 ==> 调用puts、printf……函数(fun_plt_addr) ==> 返回main函数(根据被调用函数决定带几个参数) ==> 参数中包括泄露got表地址(fun_got_addr) ==> 接收got表中的内容(该函数的真实地址) ==> libc寻址找到后门函数地址 ==> 之前返回main函数了,再栈溢出一次 ==> 调用后门函数提权
PS :32位先调用函数,再设置函数参数;64位先在寄存器中设置好参数的值,最后再调用函数
ret2libc的payload模板
(1)32位
1 |
|
(2)64位
1 |
|
关于泄露函数真实地址的接收问题
在第八题,exp使用的是
io.recvuntil(b”\n”)
puts_addr = u64(io.recvline().strip().ljust(8,b’\0’))
而本题用的是
io.recv()
puts_addr=u64(io.recv(6).ljust(8,’\x00’))
还有
read_addr=u64(io.recvuntil(b’\x7f’).ljust(8,b’\x00’))
read_addr=u64(io.recv(8))
read_addr=u64(io.recv(6).ljust(8,b’\x00’))
那么我们来分析一下不同情况下怎么接收到正确的地址
*理解几个不同接收函数的作用
recv(numb=字节大小, timeout=default) : 接收指定字节数。
recvall() : 一直接收直到达到文件EOF。
recvline(keepends=True) : 接收一行,keepends为是否保留行尾的\n。
recvuntil(delims, drop=False) : 一直读到delims的pattern出现为止。
recvrepeat(timeout=default) : 持续接收直到EOF或timeout。
发送函数也顺带说了
send(data) : 发送数据。
sendline(data) : 发送一行数据,相当于在数据末尾加\n。
sendafter()
payload中接收时出现的一些参数解释:
[start:stop:step]
是一个切片操作,从start位置开始,stop-1位置结束,每隔step-1个字符获取一次,可以用负数表示从倒数第几个开始/结束,step默认为1,-1表示逆向获取字符
[0:4] 接收前4个字节。
[-6:] 从倒数第6个字节开始,到字符串末尾结束
[:-6] 从头开始,到倒数第7个元素为止,不包括倒数第6个元素
.ljust(8, b’\x00’) 是一个方法调用
它将接收的字节后用字节 ‘\x00’ 进行填充,以确保最终的字节序列总共有8个字节,用于补全字节。
32位
由于32位程序的函数地址为四个字节,所以一般的接收方式为
fun_addr=u32(io.recv(4)) #接收前4个
fun_addr=u32(io.recvuntil(b’\x7f’).ljust(4,b’\x00’)) #接收到\x7f即停止接收(第一个不可见字符),用空字符填充到4字节
64位
由于64位程序的函数地址为8个字节,但是地址里的值就只有6位,所以我们一般只接收6位有效字节,剩下的用空字符填充
fun_addr=u64(io.recv(6).ljust(8,b’\x00’)) #接收6个字节,用空字符填充到8位
fun_addr = u64(io.recvline().strip().ljust(8,b’\0’)) #接收一行,.strip()用于删除行末尾的换行符 ’\n‘,用空字符填充到8位
泄露不同函数的payload构造方法及解释(长期更新中)
write函数
puts函数
printf函数
23.bjdctf_2020_babystack2
(1)分析
main
看下主函数,对nbytes有限制,无法造成溢出,但是最近学的CSAPP启示了我,计算机采用补码表示负数,if语句判定的时候比较会自动将nbytes转化为有符号数比较,不管它的二进制表示,所以我们可以用负数(-1是最大的无符号数)来造成溢出,其实题目的强制转换提示的很明显了。
backdoor
后门函数直接给了
那么本题主要考点就是计算机对负数的补码表示,其余很简单
(2)payload
1 |
|
24.jarvisoj_fm
格式化字符串漏洞
(1)分析
main
可以看到main函数中直接就给了后门函数,同时read栈溢出漏洞也存在,但是需要x=4时执行,跟进查看,发现x在data段,那么溢出就没法覆盖x的值了,考虑其他方法,发现printf格式化字符串漏洞也存在,尝试采用printf任意读写的特性来修改x的值
可以看到参数在栈上存储的位置偏移为11
那么可以
payload=p32(x_addr)+”%11$n”
解释一下,首先传入x参数的地址,这个地址存放在栈上偏移为11的位置,利用%11$n
,定位到了偏移为11的位置,往这个位置写入数据,写入的数据由%11$n
前面的参数的长度决定,而我们的x参数的地址,正好是4位,不需要添a来补齐位数就可以直接利用,将x参数的地址的值改成了4,获取了shell
但是如果需要x的值为1怎么办?
(2)payload
1 |
|
25.ciscn_2019_es_2
栈迁移(栈劫持)
(1)分析
main
init
vul
无/bin/sh字符串,但是我们注意到了hack函数
进入pwndbg调试溢出字节大小
那么问题来了,栈大小不够payload构造怎么办?
这就是本题的重点———栈迁移(栈劫持)
这里放一下[大佬的分析]:https://www.cnblogs.com/ZIKH26/articles/15817337.html
(2)payload
这里放一下改过的exp
1 |
|
25题(栈迁移)总结
0x01 栈迁移原理及使用条件
栈迁移就是控制程序的执行流(这个换的地方既可以是bss段也可以是栈里面)可溢出的长度不够用,也就是说我们要么是没办法溢出到返回地址只能溢出覆盖ebp,要么是刚好溢出覆盖了返回地址但是受payload长度限制,没办法把参数给写到返回地址后面。总之呢,就是能够溢出的长度不够,没办法GetShell,所以我们才需要换一个地方GetShell。
使用栈迁移的条件:
1、要能够栈溢出,这点尤其重要,最起码也要溢出覆盖个ebp
2、你要有个可写的地方(就是你要GetShell的地方),先考虑bss段,最后再考虑写到栈中
0x02 **==ebp和ebp的内容是两码事(它们二者的关系就如同c语言中,指针p与 p的关系)==*
ebp是0xffe7a9e8,它的内容是0xffe7aa38,而这个内容也是一个地址,这个地址里面装的又是0x8059b50。ebp本身大部分时候都是一个地址(程序正常运行情况下),而ebp的内容可以是地址,也可以不是地址(程序正常运行下,ebp的内容也装的是地址,但如果你进行溢出的话,自然可以不装成地址)。我这里想强调的是ebp和ebp的内容这两者一定不能混为一谈
0x03 栈迁移的核心,就在于两次的leave;ret指令上面
第一次:
leave:
mov esp,ebp(此时ebp中的内容已被修改):esp到ebp的==位置==
pop ebp :ebp到了我们修改的那个地址,esp下移一位(+4),指向返回地址(事先被修改为leave ;ret的地址)
ret:
pop eip :esp中的内容被赋给eip(leave ;ret的地址)
第二次:
leave:
mov esp,ebp(此时ebp中的内容为修改到的地址中的内容(一般为aaaa,让esp下移一格)):esp到ebp的位置(我们修改的那个地址)
pop ebp :ebp到了0x41414141,esp下移一位,指向返回地址(system函数所在地址)
ret:
pop eip :esp中的内容被赋给eip(system函数的地址)
栈劫持攻击过程完成
总结一下原理,核心是利用两次的leave;ret,第一次leave ret;将ebp给放入我们指定的位置(这个位置的就是迁移后的所在位置),第二次将esp也迁移到这个位置,并且pop ebp之后,esp指向了下一个内存单元(此时这里放的就是system函数的plt地址)
0x04 payload攻击思路
那么我们攻击的思路是先找到ebp的位置(地址),然后栈溢出覆盖到ebp的地址,把它改成我们放置后门函数的地址(或利用程序中已有的后门函数),同时要将返回地址的地址覆盖成leave;ret指令的地址,接下来用ROP链完成攻击
0x05 例题分析
以上题为例,现在我们知道需要泄露ebp地址,在IDA或gdb中找后门函数地址(有时需通过之前学的方法自己构造),在ROPgadget中找leave;ret指令的地址
那么我们一步步分析
==正常的函数调用时会将主调函数调用前的ebp地址保存在被调函数的ebp中,以便函数调用完后将ebp恢复到原来的位置。==(详见栈的相关内容)
在栈中,覆盖ret(函数返回地址)后就是ebp的地址,所以我们需要先溢出覆盖返回地址,调用read函数读出ebp的内容(此题为main函数的ebp地址,因为main函数调用的vul函数),因为是printf来打印字符串该函数在未遇到终止符 ‘\0’ 时会一直输出。利用该特性可帮助我们泄露出栈上的地址,从而能计算出要劫持到栈上的准确地址。我们第一次read正好输入0x30个字符,那就没有地方在填上00了(read读入之后,会自动补充00),因此就可以把下面的ebp地址给打印出来了。然后第二个read用来填充我们构造的system函数以及参数(我们这次是转移到了栈中,也就是第一次read读入s的地方)
栈迁移的最后一个 pop eip 执行结束后, esp 将指向 aaaa 后的内容开始执行(aaaa填栈,不然程序会到system函数地址的下一个地址执行)
劫持目标地址即为缓冲区变量 s 的起始地址,配合偏移来表达/bin/sh的地址,可以数出来偏移为0x10,那么所有准备工作完成,payload及注释见上。
payload攻击方式图解
26.jarvisoj_tell_me_something
(1)分析
main
发现read函数漏洞可溢出
good_game
发现这里读取了flag,那就没啥可说的了。
(2)payload
1 |
|
27.[HarekazeCTF2019]baby_rop2
(1)分析
main
read漏洞
printf函数泄露函数地址
传参的时候需要用到寄存器,在64位x86架构中,传参寄存器的顺序如下:
- RDI (Destination Index): 用于传递第一个整数参数。
- RSI (Source Index): 用于传递第二个整数参数。
- RDX (Data Register): 用于传递第三个整数参数。
- RCX (Counter Register): 用于传递第四个整数参数。
- R8: 用于传递第五个整数参数。
- R9: 用于传递第六个整数参数。
(这些寄存器用于传递函数的前六个整数参数。如果函数需要传递更多参数,额外的参数通常会被压入栈中,或者使用浮点寄存器来传递浮点数参数。函数的返回值通常存储在RAX寄存器中。如果返回值是一个浮点数,则通常使用XMM0寄存器。)
printf函数的原型int printf( const char* format , [argument] … );
举个例子:print(’%s’,‘hello world’)
大概就是这样的用法,这边有两个参数要设置,所以我们要找到设置rdi,rsi寄存器的指令
首先要用rdi接收第一个参数,就是带有类似于%s这种格式的字符串,使用程序里自带的语句
然后用rsi接收read_got的地址作为第二个参数,这样printf泄露read函数地址就完成了。
后面为ret2libc通用步骤,无需多言。
(2)payload
1 |
|
28.pwn2_sctf_2016
(1)分析
main
vuln

说明:atoi (表示ascii to integer)是把字符串转换成整型数的一个函数,应用在计算机程序和办公软件中。 int atoi(const char *nptr) 函数会扫描参数nptr字符串,会跳过前面的空白字符(例如空格,tab缩进)等。
这里的v2变量是为int型的-1可以绕过if(v2>32)的判断,不确定行不行,我们再看看get_n函数
get_n
get_n()是以unsigned int来接收的v2,所以这里可以造成整数溢出
do_thing
发现int 0x80程序软中断
任务明确了,我们要让:
eax=0xb
ebx=/bin/sh 的地址
ecx=0
edx=0
结果竹篮打水一场空……
没有能改rax的,只能试着ret2libc了
攻击思路:
1、整数溢出绕过
2、栈溢出构造rop来ret2libc3,从而getshell
(2)payload
1 |
|
27、28题总结
均为ret2libc和printf函数泄露libc地址
关键在于找程序中printf函数的第一个参数和payload构造问题
0x01 在64位x86架构中,传参寄存器的顺序
传参的时候需要用到寄存器,在64位x86架构中,传参寄存器的顺序如下:
- RDI (Destination Index): 用于传递第一个整数参数。
- RSI (Source Index): 用于传递第二个整数参数。
- RDX (Data Register): 用于传递第三个整数参数。
- RCX (Counter Register): 用于传递第四个整数参数。
- R8: 用于传递第五个整数参数。
- R9: 用于传递第六个整数参数。
0x02 printf函数的原型
int printf( const char* format , [argument] … );
例:print(’%s’,‘hello world’)
这边有两个参数要设置,所以我们要找到设置rdi,rsi寄存器的指令
首先要设置第一个参数,就是带有类似于%s这种格式的字符串,在程序里找
0x03 payload中关于printf函数的ROP链构造
1 |
|
29.jarvisoj_level3
(1)分析
main
vulnerable_function
buf可溢出24字节
看看有没有后门
基本确定又是ret2libc
那么不用细说了,直接上模板打,LibcSearcher不行就换网站的libc版本打
(2)payload
1 |
|
30.ciscn_2019_s_3
ret2csu/SROP
(1)分析
main
vuln
gadgets
发现了将rax赋值为59的操作
传参方式:首先将系统调用号 传入 rax,然后将参数 从左到右 依次存入 rdi,rsi,rdx寄存器中,返回值存在rax寄存器
调用号:sys_read 的调用号 为 0 ;sys_write 的调用号 为 1;stub_execve 的调用号 为 59; stub_rt_sigreturn 的调用号 为 15
调用方式: 使用 syscall 进行系统调用
loc_4011FE 段全是 pop,所以这段代码可以将我们构造的栈中的值全部存入 rbx, rbp, r12, r13, r14, r15寄存器中
loc_4011E8 段会比较复杂一点:
首先三段 mov 指令将存储在 r15的值赋给 rdx,存储在 r14的值赋给 rsi,存储在 r13的值赋给 edi,此时 rdi 的高32位寄存器中值为0,所以我们也可以控制 rdi 的值。
然后 call 指令跳转到 r12寄存器存储的位置处(在 gadgets1中置 rbx=0)
rbx+1,判断是否与 rbp 相等,否则重新执行 gadgets2,这里我们为了不重新执行,将 rbp 置为1
(2)payload
1 |
|
30题(ret2csu+sigret/SROP)总结
ret2csu原理
_libc_csu_init 函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数大部分程序里都有。
简单来说此攻击方式就是利用 _libc_csu_init 函数中多个对寄存器的弹栈指令来修改我们攻击中所需的寄存器值完成exp的编写。
该函数中被利用的汇编指令如下:
那么我们找到程序溢出点后需要