查看原文
其他

物联网中的 ARM 漏洞利用

2017-08-24 kiyaa 看雪学院


一、市面上目前的hook和注入工具



发文动机


几周前我参加某个会议的时候,有个“物联网上的 ARM 漏洞利用课程”的议题我觉得很多干货,我也决定自己写一篇,给去不了现场的同学发些福利。我打算分为三个部分来写。


当然我的文章没办法和现场的 course 相比,我还是想为大家做一些微小的工作。


这三个部分是:


  • 第一部分:逆向 ARM 应用

  • 第二部分:编写 ARM shellcode

  • 第三部分:ARM 漏洞利用




一、逆向ARM应用


  • 环境

    树莓派3——我选了这个又便宜又好配置的环境,Android 也是个不错的选择。

  • 硬件

    具体型号:>树莓派3、型号B、ARM-Cortex-A53架构


  • 软件

    这是些软件信息,接下来三部分都会用到。


操作系统的安装请看:https://www.raspberrypi.org/documentation/installation/installing-images/linux.md


配置远程 ssh 请看:https://www.raspberrypi.org/documentation/remote-access/ssh/


▶ 编译器


我们用到的所有 C、C++、汇编代码都会用树莓派自带的 GCC 编译器。


版本如下:



还有一点是 GCC 的汇编指令和其他的编译器不同,最好先看一下这些指令:http://www.ic.unicamp.br/~celio/mc404-2014/docs/gnu-arm-directives.pdf


▶ 源码


本部分我用到的源码都放在这里了:https://github.com/invictus1306/ARM-episodes/tree/master/Episode1


编译选项


这一节我会介绍三个选项,并附带例子。


这是用到的源码:



调试符号


-g 选项会在编译时向可执行文件中加入调试信息(符号表)。

编译带 -g 和不带 -g 选项的两个 ELF 文件,比较大小:



第二个文件更大,意味着它被加入了其他的信息。用 readelf 命令的 -S 选项(查看节头)查看其调试信息:



这些调试信息以 GCC 默认的 DWARF 格式存储。用 objdump 查看:



去除符号表和重定位信息

选项为 -s。



可以看到 .symtab 有很多本地符号,这些符号运行时并不需要,可以把它们去掉。



用过 -s 选项后,函数名之类的信息已经去掉了,某个逆向小子的生活又艰难了一步。


在之后的第三部分我会介绍其他更复杂的编译选项。




ARM Hello World



我们以两种方式开始这个 hello world 的研究:


1. 使用树莓派系统调用

2. 使用 libc 函数


▶ 使用树莓派的系统调用



汇编并链接此程序:



注意:如果用 gcc,



会得到错误:


这是因为源程序里没有 main 函数,在另一种实现里我们会看到如何使用 gcc 编译。


执行:


接下来使用 gdb:


可以看到 .text 段中放着我们的代码。0x10078 处的指令代表将 0x10090 指向的值载入 r1。


▶ 使用 libc 函数


这次我们会使用 printf 函数,这里我们将程序中的 .global _start 改为  .global main。



编译器需要我们指定 global main, .func main, main: 等符号告诉 libc main 函数在哪。



汇编器和链接器只是 GCC 的一小部分,下面我们会用到 GCC 其他的特性来编译。


可以看到在进程的地址空间里加载了 libc 共享库(libc-2.19.so)。



0x10428 处调用了 printf 函数,0x10428 是个 PLT 入口,指向 GOT 中 printf 函数在运行时的真实地址。


GCC 编译时,libc 的函数并没有被编译到可执行文件中,而是通过动态链接到 libc 使之可用。用 ldd 命令查看程序引用的动态库。



可用看到 libc 是程序所需要的。多次查看如果 libc 的地址不同,是因为打开了 ASLR。用 IDA 打开:



0x10428 处调用 printf,双击并没有进入 libc。



而是到了 PLT 段,0x102D0 处通过 LDR PC, […] 修改 PC 跳转到了其他地址。



到达 GOT 段,这里存着外部符号的地址。


下面用 gdb 调试,断点下在 0x10428 处。


stepi 继续运行。



走几步到达 ld 库中的 dl_runtime_resolve 函数。



ld 是动态链接器,这里建立起了 libc 的外部引用环境。

更多细节参考 http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/。


逆向工程介绍

这部分对于要分析的程序我不会提供源码。


逆向算法


第一个例子是输入一个字符串,经过算法处理后会输出另一个字符串,我需要使输出字符串为“Hello”。



下面是源码(我只会提供这个例子的):



编译:



调试理解算法:



0x10454 代表 r0=*(pc+92)。查看 pc+92 的内容:



是数据段的地址,看一下内容:



0x20668 是 printf 函数的参数。
运行到 0x10464,r0 是格式的地址,r1 是输入字符串的地址。



从源码中可以看到输入字符串的长度为5:



输入 “ABCDE”:



0x10468 和 0x1046c 处的指令将输出字符串地址赋给 r5,输入字符串地址赋给 r1。运行至 0x10470:



注释如下:



运行到 0x10480 处查看 r0, r2, r3 的值:



即 *r5 = r2 xor r3。

可以用伪代码表示:byte1strOut = byte1strInput xor byte2strInput。 比如我们想要生成“Hello”,需要使 r0 为 0x48(H)。

接着看 0x10480,



注意看注释:



执行到 0x1048c,看一下 r0、r3、r4 的值:



即 *(r5+1) = r4 xor r3。伪代码表示:byte2strOut = byte2strInput xor byte3strInput。



意味着 *(r5+2) = r2 + 0x5。伪代码表示:byte3outStr = byte1strInput + 0x5。第 4 个字节:



就是 *(r5+3) = r3 xor r4。伪代码:byte4strOut = byte2strInput xor byte4strInput。第 5 个字节:



就是 *(r5+4) = r4 xor r2。伪代码:byte5strOut = byte4strInput xor byte5strInput。


整个算法合在一起:

byte1strOut = byte1strInput xor byte2strInput

byte2strOut = byte2strInput xor byte3strInput

byte3strOut = byte2strInput + 0x5

byte4strOut = byte2strInput xor byte4strInput

byte5strOut = byte4strInput xor byte5strInput


将输出字符替换:

‘H’ = 0x48 = byte1strInput xor byte2strInput

39 38338 39 15288 0 0 792 0 0:00:48 0:00:19 0:00:29 3085‘e’ = 0x65 = byte2strInput xor byte3strInput

‘l’ = 0x6c = byte1strInput + 0x5

‘l’ = 0x6c = byte2strInput xor byte4strInput

‘o’ = 0x6f = byte4strInput xor byte5strInput


推出应输入字符:

byte1strInput = 0x6c – 0x5 = 0x67 (g)

byte2strInput = 0x48 xor 0x67 = 0x2f (/)

byte3strInput = 0x2f xor 0x65 = 0x4a (J)

byte4strInput = 0x2f xor 0x6c = 0x43 (C)

byte5strInput = 0x43 xor 0x6f = 0x2c (,)


输入试试:



▶ 逆向一个简单的加载器


这个加载器的作用是把指令加载到内存里,当你打印消息时执行。我们这次要打印“WIN”。程序在这里。



用 IDA 打开:



在 _start 这儿可以看到 0x10090 处有系统调用,调用号是 0xc0(mmap)。


看下面我的注释分析:



mmap 后有一块新内存(0x30000)。



0x10098 处的指令 .text:00010098 LDR R1, =code 把某个变量的地址存入 r1,看一下:



这些看起来不像 arm 代码,接着看 0x100A4:


.text:000100A4 LDR R2, [R1,R4] 把 r1+r4 地址额值存入 r2,r1 是 code 变量,r4 表示索引,第一次值为 0。

.text:000100A8 EOR R2, R2, R6 r2 与 r6 异或,r6 的值是 0x123456,第一次 r2 的值是 0x56。异或的值存在 r2,在下条指令

.text:000100AC STR R2, [R0,R4] 中被写入 mmap 分配的地址 0x30000 处,注意 r0 是 mmap 的返回值。

循环的作用是解密 code 的所有字节,在 0x100BC 处下断点查看 0x30000 的值。


这些就是 ARM 指令了,也可以用 idc 脚本解密:

现在来分析解密的指令:


执行过 0x30004 到 0x30014 的 5 条指令后,栈指针向低地址处移动了 8 ,r4 是栈指针,r2 的值是 0x3e,r3 的值是 0x2,r5 的值是 0x96。



接下来两条指令(0x30018 和 0x3001c)r2 与 r5 异或的结果 0xa8 存入 r1,这个值写入了栈顶,栈指针向高地址移动了 1 。此时:



0x30020 处 r2 自减 0x1e,得到:



0x30024 处是一个循环:



每次循环都将 r2 和 r5 异或,结果存入栈顶,sp + 1。0x30030 处可以看到 r3 是循环索引,每次减1,初始值为 2,所以共循环两次。

循环结束时运行到 0x30038时, 0x7efff7b0 值为:



还有两个字节在栈顶存着,此时栈指针为:



0x3003c 处的两条指令将剩下的两个字节存入栈顶:



执行完 0x30040 后0x7efff7b0 值为:



往下看就是 write 的系统调用了:



write 过后:


我们想要的是“WIN”,这时候就要修改 xor 的 key,这样存入栈顶的才会是正确的 0x57 0x49 0x4e。


来看 0x30018 处的异或操作 0x30018: eor r1, r2, r5,r2 每次都变,所以 r5 是异或的 key,我们需要修改 r5 使得 r1 = r2 xor r5 = 0x57。


r2 的值是 0x3e,则 r5 应该是 0x69。


异或的 key 没有变过,这就直接继续执行就可以了。


▶ 基本的反调试技术


这是本章最后一个程序了,这次需要理解算法并绕过一些基本的反调试,使程序输出“Good”。在这里下载。


试着用调试器运行:


即使再用 strace/ltrace 命令也是同样的输出。
IDA 打开:


我们从 ldr r2, =aAd 开始分析。
aAd 是个变量:


把 Array 转为 data 更好理解:


0x10988 处的数组用 var_c 表示,还有另一个变量 var_10,aAd + 4 的值如下:


即 var_10 变量存着 0x1098C 处的数组。

看接下来的指令做了什么:


总结来说就是有两个数组:


4个元素的 var_c:  0x7, 0x2f, 0x2f, 0x24; 3个元素的 var_10:0x22, 0x41, 0x44。


下面有个 flag 变量,我们来看 main 函数中关于它的流程图:


flag为1,红色执行,不为1绿色执行。

flag为1,r3 为 0 随后与 3 比较。
flag不为1,r3 为 0 随后 与 2 比较。



flag 为 1,我们来到 loc_107F8,最关键的是这句 ADD R3, R3, #0x40,r3 的值是 r3 = *(var_C+var_8),var_C 和 var_8 分别是:
var_C = 4个元素的数组
var_8 = 0 (索引,第一次的值)
相加之后 r3 的值为 r3 = 0x7 + 0x40 = 0x47。


可以用个简单的 idc 脚本计算:


结果是 Good:


再来看flag不为1时的 loc_10864,这里的循环计算的是3个元素的数组,关键的是 ADD R3, R3, #0x20。


像之前一样,idc 脚本


结果是 Bad:


为了使程序输出“Good”,需要找到 flag 赋值的地方,而且刚刚并没有发现检测调试器的地方, “You want debug me?” 也没有出现过。


查看 flag 的交叉引用:


发现有个 ptrace_capt 的函数,在 main 函数前执行。可以在 .ctors (或者 .init_array) 段中发现其提供了一些列函数用来在程序开始/结束前执行。


来看 ptrace_capt 函数:


这里有个检查:


用调试器可以轻易绕过,先来看 loc_10690:


大致如下:

1. 只读模式打开 password.raw;


2. 计算大小


3. 验证大小是否小于 6


如果小于等于6,来到 loc_10700:


往下看发现这是个循环:


调用 fgetc 函数:


如果 r3 等于 0,则来到 loc_10750:


var_18 是读取的字符,var_8 是循环索引,sub0 的调用则为 sub0(var_18, var_8, &var_1C);。


看 sub0 函数:


C 代码:


sub0 返回时继续执行,var_1C 保存返回值:


这段用伪码表示即:


如果 r3 不等于 0,则来到:


C 代码表示:


终于找到了 flag 赋值的地方,而我们需要其值为 1。
新建 password.raw 文件,写入:


我用 vim 的设置删掉了换行:


运行:


现在用 gdb 运行去掉反调试:# gdb ./3b


在 0x10678 处下断,修改 r3 的值:


继续往下分析,我现在要使 var_C=0x997,flag 才能赋值为 1。现在文件里的内容是:


要修改第五个字节使得 var_C=0x997,就要知道第4次循环时 var_C 的值。


在 0x10774 下断点,


此时循环索引为 3(第4次),var_C 的值为 0x724,现在要改掉第 5 个字节的值,我用了 Python 来计算:


运行:


得到了第五个字节的值,修改它:


记得删掉换行 :set noendofline binary。


运行:


“Good” 就被打印了。





本文由看雪翻译小组 kiyaa 编译,来源quequero@andrea sindoni

转载请注明来自看雪社区



热门阅读


点击阅读原文/read,

更多干货等着你~



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存