ARM exploitation for IoT – Episode 1

根据作者的安排,文章分为ARM逆向、构造shellcode、最终的exploitation。本篇介绍和ARM逆向相关的知识点。

原文链接:https://quequero.org/2017/07/arm-exploitation-iot-episode-1/
关于文中实验的说明:由于编辑人员时间和资源的限制,暂时没有实际验证。如果后续有条件的话,会实际验证一下。
关于翻译的说明:遣词造句的功夫还欠缺火候,如有错误,烦请指出。

动机和简介

在前几周的一个会议上,我注意到针对ARM平台的漏洞利用课程的价格有点贵,我觉得我可以自己来写一些教程,让那些无法支付昂贵学费的人也能学习这些相关技能。本课程分为三个章节。
我仅仅是在做出自己的贡献,这些文章是不能与现场课程相媲美的。

内容将划分如下:

第1章:逆向ARM应用程序
第2章:ARM shellcode
第3章:ARM漏洞利用

逆向ARM应用程序

环境:树莓派3
我选择了一个比较便宜和容易配置的硬件环境,当然了,安卓也是一个不错的选择。

硬件:教程所用硬件的型号:
Raspberry Pi 3 Model B ARM-Cortex-A53

软件:该教程所使用的软件信息:

root@raspberrypi:/home/pi# cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 8 (jessie)"
NAME="Raspbian GNU/Linux"
VERSION_ID="8"
VERSION="8 (jessie)"
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

root@raspberrypi:/home/pi#cat /etc/rpi-issue
Raspberry Pi reference 2017-03-02
Generated using pi-gen, https://github.com/RPi-Distro/pi-gen, f563e32202fad7180c9058dc3ad70bfb7c09f0fb, stage2

如何安装教程中所用操作系统
如何配置ssh远程连接

编译器

对于所有代码(C,C ++,汇编),我们将使用Gnu编译器集合(GCC),Raspbian操作系统自带。GCC的版本是

root@raspberrypi:/home/pi/arm/episode1# gcc --version
gcc (Raspbian 4.9.2-10) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

另外需要说明的是,GCC的ARM汇编指令根其他编译器不一样,我建议你看看这些指令说明

源码

教程的代码都在我的github上

编译选项

对于编译选项必须要知道和理解,在本节中,我们将看到3个不同的选项,并且对于每个选项将进行一个实际的例子。
我们用下面的例子作为所有选项演示的例子(文件:compiler_options.c)

#include <stdio.h>
#include <string.h>
static char password[] = "compiler_options";

int main()
{
    char input_pwd[20]={0};
    fgets(input_pwd, sizeof(input_pwd), stdin);
    int size = sizeof(password);
    if(input_pwd[size] != 0)
    {
        printf("The password is not correct! \n");
        return 0;
    }
    int ret = strncmp(password, input_pwd, size-1);

    if (ret==0)
    {
        printf("Good done! \n");
    }
    else
    {
        printf("The password is not correct! \n");
    }
    return 0;
}

调试符号

选项-g产生存储在可执行文件中的调试信息(符号表)。编译我们的例子(compiler_options.c),比较带“-g”选项和不带“-g”选项所产生可执行文件的大小。

root@raspberrypi:/home/pi/arm/episode1# gcc -o compiler_options compiler_options.c
root@raspberrypi:/home/pi/arm/episode1# ls -l
total 12
-rwxr-xr-x 1 root root 6288 Jun 14 20:21 compiler_options
-rw-r--r-- 1 root root 488 Jun 14 19:41 compiler_options.c
root@raspberrypi:/home/pi/arm/episode1# gcc -o compiler_options compiler_options.c -g
root@raspberrypi:/home/pi/arm/episode1# ls -l
total 16
-rwxr-xr-x 1 root root 8648 Jun 14 20:21 compiler_options
-rw-r--r-- 1 root root 488 Jun 14 19:41 compiler_options.c

我们可以看到,在第二种情况下,文件较大,这意味着有一些信息已添加到ELF文件中。
我们可以使用不同的方法查看可执行文件中的调试信息,我们用readelf程序,加上-S选项(显示section的头)。

root@raspberrypi:/home/pi/arm/episode1# readelf -S compiler_options | grep debug
[27] .debug_aranges PROGBITS 00000000 0007f2 000020 00 0 0 1
[28] .debug_info PROGBITS 00000000 000812 000318 00 0 0 1
[29] .debug_abbrev PROGBITS 00000000 000b2a 0000da 00 0 0 1
[30] .debug_line PROGBITS 00000000 000c04 0000de 00 0 0 1
[31] .debug_frame PROGBITS 00000000 000ce4 000030 00 0 0 4
[32] .debug_str PROGBITS 00000000 000d14 000267 01 MS 0 0 1

您可以看到包含以DWARF调试格式存储的调试信息的所有部分,这是GCC编译器使用的默认调试格式。
可以用objdump查看section的详细数据

root@raspberrypi:/home/pi/arm/episode1# objdump --dwarf=info ./compiler_options
…
Abbrev Number: 14 (DW_TAG_variable)
DW_AT_name : (indirect string, offset: 0x8a): password
DW_AT_decl_file : 1
DW_AT_decl_line : 4
DW_AT_type : <0x2eb>
DW_AT_location : 5 byte block: 3 70 7 2 0 (DW_OP_addr: 20770)
Abbrev Number: 16 (DW_TAG_variable)
DW_AT_name : (indirect string, offset: 0x215): stdin
DW_AT_decl_file : 5
DW_AT_decl_line : 168
DW_AT_type : <0x26b>
DW_AT_external : 1
DW_AT_declaration : 1
Abbrev Number: 0

上面显示的是debug_info section的信息,这些信息在调试的时候被调试器使用。

删除所有符号表和重定位信息

使用GCC编译器,我们可以删除所有的符号表和重定位信息,这样做的选项是-s。

root@raspberrypi:/home/pi/arm/episode1# gcc -o compiler_options compiler_options.c
root@raspberrypi:/home/pi/arm/episode1# readelf --sym compiler_options
Symbol table '.dynsym' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
2: 00000000 0 FUNC GLOBAL DEFAULT UND fgets@GLIBC_2.4 (2)
3: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.4 (2)
4: 00020788 4 OBJECT GLOBAL DEFAULT 24 stdin@GLIBC_2.4 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND strncmp@GLIBC_2.4 (2)
6: 00000000 0 FUNC GLOBAL DEFAULT UND abort@GLIBC_2.4 (2)
7: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.4 (2)
Symbol table '.symtab' contains 115 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00010134 0 SECTION LOCAL DEFAULT 1
2: 00010150 0 SECTION LOCAL DEFAULT 2
...
112: 00000000 0 FUNC GLOBAL DEFAULT UND strncmp@@GLIBC_2.4
113: 00000000 0 FUNC GLOBAL DEFAULT UND abort@@GLIBC_2.4
114: 00010318 0 FUNC GLOBAL DEFAULT 11 _init

正如我们所看到的那样。symtab具有许多局部符号,这些不是运行程序所必需的,因此可以删除此部分。

root@raspberrypi:/home/pi/arm/episode1# gcc -o compiler_options compiler_options.c -s
root@raspberrypi:/home/pi/arm/episode1# readelf --sym compiler_options
Symbol table '.dynsym' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
2: 00000000 0 FUNC GLOBAL DEFAULT UND fgets@GLIBC_2.4 (2)
3: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.4 (2)
4: 00020788 4 OBJECT GLOBAL DEFAULT 24 stdin@GLIBC_2.4 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND strncmp@GLIBC_2.4 (2)
6: 00000000 0 FUNC GLOBAL DEFAULT UND abort@GLIBC_2.4 (2)
7: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.4 (2)

使用-s选项进行编译后,函数名和一些其他的信息被移除,逆向起来就有点困难了。在第三章,我们将看到其余一些对于漏洞利用来说很重要的编译选项。

ARM Hello World

我们将首先编写一个简单的hello world程序,我们将以两种不同的方式来实现:

1.使用Raspbian系统调用
2.使用libc函数

使用Raspbian系统调用

首先,通过Raspbian系统调用来编写一个简单的Hello World程序(源文件:rasp_syscall.s)

.data
string: .asciz "Hello World!\n"
len = . - string
.text
.global _start
_start:
    mov r0, #1      @ stdout
    ldr r1, =string @ string address
    ldr r2, =len    @ string length
    mov r7, #4      @ write syscall
    swi 0           @ execute syscall
_exit:
    mov r7, #1      @ exit syscall
    swi 0           @ execute syscall

然后汇编、链接

root@raspberrypi:/home/pi/arm/episode1# as -o rasp_syscall.o rasp_syscall.s
root@raspberrypi:/home/pi/arm/episode1# ld -o rasp_syscall rasp_syscall.o

注意:如果我们直接GCC编译的话,会出错:

root@raspberrypi:/home/pi/arm/episode1# gcc -o rasp_syscall rasp_syscall.s
/tmp/ccChPTEP.o: In function `_start':
(.text+0x0): multiple definition of `_start'
/usr/lib/gcc/arm-linux-gnueabihf/4.9/../../../arm-linux-gnueabihf/crt1.o:/build/glibc-g3vikB/glibc-2.19/csu/../ports/sysdeps/arm/start.S:79: first defined here
/usr/lib/gcc/arm-linux-gnueabihf/4.9/../../../arm-linux-gnueabihf/crt1.o: In function `_start':
/build/glibc-g3vikB/glibc-2.19/csu/../ports/sysdeps/arm/start.S:119: undefined reference to `main'
collect2: error: ld returned 1 exit status

编译器会提示我们找不到main函数:

undefined reference to `main'

因为我们确实没有在程序中实现main函数。关于GCC汇编,我们会在接下来的例子中看到。执行链接后的程序:

root@raspberrypi:/home/pi/arm/episode1# ./rasp_syscall
Hello World!

再试试用gdb加载程序:

root@raspberrypi:/home/pi/arm/episode1# gdb -q ./rasp_syscall
Reading symbols from ./rasp_syscall...(no debugging symbols found)...done.
(gdb) info files
Symbols from "/home/pi/arm/episode1/rasp_syscall".
Local exec file:
`/home/pi/arm/episode1/rasp_syscall', file type elf32-littlearm.
Entry point: 0x10074
0x00010074 - 0x00010094 is .text
0x00020094 - 0x000200a2 is .data
(gdb) b *0x00010074
Breakpoint 1 at 0x10074
(gdb) r
Starting program: /home/pi/arm/episode1/rasp_syscall
Breakpoint 1, 0x00010074 in _start ()
(gdb) x/7i $pc
=>    0x10074 <_start>: mov r0, #1
    0x10078 <_start+4>: ldr r1, [pc, #16] ; 0x10090 <_exit+8>
    0x1007c <_start+8>: mov r2, #14
    0x10080 <_start+12>: mov r7, #4
    0x10084 <_start+16>: svc 0x00000000
    0x10088 <_exit>: mov r7, #1
    0x1008c <_exit+4>: svc 0x00000000

我们可以看到.text段的指令。地址0x10078处的指令会将pc+16的值给r1寄存器。也就是r1=0x10080+16=0x10090。
看一下这个地址放着什么数据:

(gdb) x/14c *(int*)0x10090
0x20094: 72 'H' 101 'e' 108 'l' 108 'l' 111 'o' 32 ' ' 87 'W' 111 'o'
0x2009c: 114 'r' 108 'l' 100 'd' 33 '!' 10 '\n' 0 '\000'

使用libc函数

这次用printf实现Hello world。但是还要做一些调整,比如说要将全局的_start符号换成_main符号,我会在接下来详细介绍。源码文件

.data
string: .asciz "Hello World!\n"
.text
.global main
.func main
main:
    stmfd sp!, {lr}    @ save lr
    ldr r0, =string    @ store string address into R0
    bl printf          @ call printf
    ldmfd sp!, {pc}    @ restore pc
_exit:
    mov lr, pc         @ exit

编译器用新定义的符号(.global main,.func main,main:)告诉libc库程序的main函数在哪里。
像上文那样执行汇编、链接:

root@raspberrypi:/home/pi/arm/episode1# as -o libc_functions.o libc_functions.s
root@raspberrypi:/home/pi/arm/episode1# ld -o libc_functions libc_functions.o
ld: warning: cannot find entry symbol _start; defaulting to 00010074
libc_functions.o: In function `main':
(.text+0x8): undefined reference to `printf'

但是好像出错了,找找原因。汇编器和链接器只是GCC编译器很小的组成部分,而libc库由GCC来提供给程序使用(当然手动链接一些共享库也是可行的),所以直接汇编、链接的话程序会找不到printf函数。
来看看gcc怎么编译程序:

root@raspberrypi:/home/pi/arm/episode1# gcc -o libc_functions libc_functions.s

用gdb加载:

root@raspberrypi:/home/pi/arm/episode1# gdb -q ./libc_functions
Reading symbols from ./libc_functions...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x10420
(gdb) r
Starting program: /home/pi/arm/episode1/libc_functions
Breakpoint 1, 0x00010420 in main ()
(gdb) info proc mappings
process 2023
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x10000 0x11000 0x1000 0x0 /home/pi/arm/episode1/libc_functions
0x20000 0x21000 0x1000 0x0 /home/pi/arm/episode1/libc_functions
0x76e79000 0x76fa4000 0x12b000 0x0 /lib/arm-linux-gnueabihf/libc-2.19.so
0x76fa4000 0x76fb4000 0x10000 0x12b000 /lib/arm-linux-gnueabihf/libc-2.19.so
0x76fb4000 0x76fb6000 0x2000 0x12b000 /lib/arm-linux-gnueabihf/libc-2.19.so
0x76fb6000 0x76fb7000 0x1000 0x12d000 /lib/arm-linux-gnueabihf/libc-2.19.so
0x76fb7000 0x76fba000 0x3000 0x0
0x76fba000 0x76fbf000 0x5000 0x0 /usr/lib/arm-linux-gnueabihf/libarmmem.so
0x76fbf000 0x76fce000 0xf000 0x5000 /usr/lib/arm-linux-gnueabihf/libarmmem.so
0x76fce000 0x76fcf000 0x1000 0x4000 /usr/lib/arm-linux-gnueabihf/libarmmem.so
0x76fcf000 0x76fef000 0x20000 0x0 /lib/arm-linux-gnueabihf/ld-2.19.so
0x76ff1000 0x76ff3000 0x2000 0x0
0x76ff9000 0x76ffb000 0x2000 0x0
0x76ffb000 0x76ffc000 0x1000 0x0 [sigpage]
0x76ffc000 0x76ffd000 0x1000 0x0 [vvar]
0x76ffd000 0x76ffe000 0x1000 0x0 [vdso]
0x76ffe000 0x76fff000 0x1000 0x1f000 /lib/arm-linux-gnueabihf/ld-2.19.so
0x76fff000 0x77000000 0x1000 0x20000 /lib/arm-linux-gnueabihf/ld-2.19.so
0x7efdf000 0x7f000000 0x21000 0x0 [stack]
0xffff0000 0xffff1000 0x1000 0x0 [vectors]
可以看到libc共享库(libc-2.19.so)被加载到进程空间了。让我们看看程序源码:
(gdb) x/5i $pc
=>  0x10420 <main>: stmfd   sp!, {lr} 
    0x10424 <main+4>:  ldr r0, [pc, #8]    ; 0x10434 <_exit+4> 
    0x10428 <main+8>:  bl  0x102c8 
    0x1042c <main+12>: ldmfd   sp!, {pc} 
    0x10430 <_exit>:   mov lr, pc

在地址0x10428处是对printf函数的调用,从更详细的实现细节来说,0x10428处的指令只是PLT(过程链接表)的入口,每个PLT在包含函数真实地址的GOT段中都有对应的表项。来看看细节。
当我们使用GCC编译程序时,libc不包含在二进制文件(libc_functions)中,但是libc将动态链接到这个二进制文件。我们可以使用ldd查看从这个二进制文件引用的动态库:

root@raspberrypi:/home/pi/arm/episode1# ldd libc_functions
linux-vdso.so.1 (0x7eeb1000)
/usr/lib/arm-linux-gnueabihf/libarmmem.so (0x76fe6000)
libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0x76e9f000)
/lib/ld-linux-armhf.so.3 (0x54b6d000)

我们可以看到libc是二进制文件所必需的,如果你运行ldd otehrs的时候你可以注意到,libc的地址是不同的,这是因为ASLR被启用。我们用IDA打开二进制文件

在0x10428位置有对printf函数的调用,我们可以注意到我们没有到达libc

但是我们在PLT部分,在0x102D0行,我们可以看到跳转(LDR PC,[…])到存储在另一个位置的地址

我们进入GOT部分,可以看到这里存储的是一个外部符号的地址。
使用gdb进行调试的时候,我们可以在0x10428地址设置一个断点(在main函数中调用printf函数)

Breakpoint 2, 0x00010428 in main ()Breakpoint 2, 0x00010428 in main ()
(gdb) x/i $pc
=> 0x10428 <main+8>: bl 0x102c8

继续执行stepi命令

如果我们继续执行几个指令,我们将达到ld二进制文件中包含的dl_runtime_resolve函数

ldd是一个动态的链接器,它的功能就是确定程序应用了哪些libc库。
关于本节更详细的介绍在http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/

逆向工程介绍

在本节中,我只在分析algorithm_reversing程序的时候罗列一下的源码,其余程序的源码不再特意贴上来。

逆向算法

我们从一个真正简单的程序开始,它接收一个消息,这个消息由一个简单的算法处理,并输出另一个消息。本练习的目的是了解所使用的算法,使得输出消息是字符串“Hello”。

strIN -------[algorithm]-------strOUT
strOUT = Hello

源码

.data
.balign 4
    info: .asciz "Please enter your string: "
    format: .asciz "%5s"
.balign 4
    strIN: .skip 5
    strOUT: .skip 5
    val: .byte 0x5
    output: .asciz "your input: %s\n"
.text
.global main
.extern printf
.extern scanf

main:
    push {ip, lr}      @ push return address + dummy register
    ldr r0, =info      @ print the info
    bl printf
    ldr r0, =format
    ldr r1, =strIN
    bl scanf
    @ parsing of the message
    ldr r5, =strOUT
    ldr r1, =strIN
    ldrb r2, [r1]
    ldrb r3, [r1,#1]
    eor r0, r2, r3
    str r0, [r5]
    ldrb r4, [r1,#2]
    eor r0, r4, r3
    str r0, [r5,#1]
    add r2, #0x5
    str r2, [r5,#2]
    ldrb r4, [r1,#3]
    eor r0, r3, r4
    str r0, [r5,#3]
    ldrb r2, [r1,#4]
    eor r0, r2, r4
    str r0, [r5,#4]
    @ print of the final string
    ldr r0, =strOUT    @ print num formatted by output string.
    bl printf
    pop {ip, pc}       @ pop return address into pc

编译:

root@raspberrypi:/home/pi/arm/episode1# gcc -o algorithm_reversing algorithm_reversing.s

调试它:

root@raspberrypi:/home/pi/arm/episode1# gdb -q ./algorithm_reversing
Reading symbols from ./algorithm_reversing...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x10450
(gdb) r
Starting program: /home/pi/arm/episode1/algorithm_reversing
Breakpoint 1, 0x00010450 in main ()
(gdb) x/10i $pc
=>    0x10450 <main>: push    {r12, lr} 
    0x10454 <main+4>:  ldr r0, [pc, #92]   ; 0x104b8 <main+104> 
    0x10458 <main+8>:  bl  0x102ec 
    0x1045c <main+12>: ldr r0, [pc, #88]   ; 0x104bc <main+108> 
    0x10460 <main+16>: ldr r1, [pc, #88]   ; 0x104c0 <main+112> 
    0x10464 <main+20>: bl  0x10304 
    0x10468 <main+24>: ldr r5, [pc, #84]   ; 0x104c4 <main+116> 
    0x1046c <main+28>: ldr r1, [pc, #76]   ; 0x104c0 <main+112> 
    0x10470 <main+32>: ldrb    r2, [r1] 
    0x10474 <main+36>: ldrb    r3, [r1, #1]

执行到0x10454,这条指令的意思是

r0=*(pc+92)

看看pc+92处存放的是什么数据:

(gdb) x/x 0x104b8
0x104b8 <main+104>:   0x00020668

存放的是一个地址,这个地址位于数据段:

(gdb) x/s 0x20668
0x20668: "Please enter your string: "

存放的是printf的第一个参数。
在地址0x10464处调用scanf函数,r0参数是格式化字符串地址,r1参数是接收输入的地址。

(gdb) i r $r0 $r1
r0 0x20683 132739
r1 0x20688 132744
(gdb) nexti

从源码中能看出,需要输入5个字符:

format: .asciz "%5s"
strIN: .skip

然后我们可以输入字符串“ABCDE”

(gdb) nexti
Please enter your string: ABCDE

执行0x10468和0x1046c两条指令,将输出字符串地址给r5,将输入字符串地址给r1,接着执行0x10470(算法部分)处的指令

(gdb) x/18i $pc 
=>    0x10470 <main+32>:  ldrb    r2, [r1] 
    0x10474 <main+36>: ldrb    r3, [r1, #1] 
    0x10478 <main+40>: eor r0, r2, r3 
    0x1047c <main+44>: str r0, [r5] 
    0x10480 <main+48>: ldrb    r4, [r1, #2] 
    0x10484 <main+52>: eor r0, r4, r3 
    0x10488 <main+56>: str r0, [r5, #1] 
    0x1048c <main+60>: add r2, r2, #5 
    0x10490 <main+64>: str r2, [r5, #2] 
    0x10494 <main+68>: ldrb    r4, [r1, #3] 
    0x10498 <main+72>: eor r0, r3, r4 
    0x1049c <main+76>: str r0, [r5, #3] 
    0x104a0 <main+80>: ldrb    r2, [r1, #4] 
    0x104a4 <main+84>: eor r0, r2, r4 
    0x104a8 <main+88>: str r0, [r5, #4] 
    0x104ac <main+92>: ldr r0, [pc, #16]   ; 0x104c4 <main+116> 
    0x104b0 <main+96>: bl  0x102ec 
    0x104b4 <main+100>:    pop {r12, pc} 

我们来看看下面的指令(参见行内注释)

0x10470 <main+32>: ldrb    r2, [r1]        ; r2 <- *r1
0x10474 <main+36>: ldrb    r3, [r1, #1]  ; r3 <-*(r1+1)
0x10478 <main+40>: eor r0, r2, r3     ; r0=r2 xor r3
0x1047c <main+44>: str r0, [r5]        ; r0 -> *r5

继续执行0x10480地址(用nexti),并检查r0,r2和r3寄存器的内容

(gdb) i r $r0 $r2 $r3
r0 0x3 3
r2 0x41 65
r3 0x42 66

计算过程是

*r5 = r2 xor r3

也就是说输出字符串第一个字节为

byte1strOut = byte1strInput xor byte2strInput

我们继续从地址0x10480分析

(gdb) x/8i $pc 
=>    0x10480 <main+48>:  ldrb    r4, [r1, #2] 
    0x10484 <main+52>: eor r0, r4, r3 
    0x10488 <main+56>: str r0, [r5, #1] 
    0x1048c <main+60>: add r2, r2, #5 
    0x10490 <main+64>: str r2, [r5, #2] 
    0x10494 <main+68>: ldrb    r4, [r1, #3] 
    0x10498 <main+72>: eor r0, r3, r4 
    0x1049c <main+76>: str r0, [r5, #3]

我们来看看下面的指令(参见行内注释)

0x10480 <main+48>: ldrb    r4, [r1, #2]   ; r4 <- *(r1+2) 
0x10484 <main+52>: eor r0, r4, r3      ; r0=r4 xor r3
0x10488 <main+56>: str r0, [r5, #1]   ; r0 -> *(r5+1)

我们来看看0x1048c指令,看看寄存器r0,r3和r4的内容

(gdb) i r $r0 $r3 $r4
r0 0x1 1
r3 0x42 66
r4 0x43 67
计算过程是
*(r5+1) = r4 xor r3

也就是说输出字符串第二个字节为

byte2strOut = byte2strInput xor byte3strInput

继续,让我们分析这两个指令

0x1048c <main+60>:    add r2, r2, #5 
0x10490 <main+64>:    str r2, [r5, #2] 

计算过程是

*(r5+2) = r2 + 0x5

也就是说输出字符串第三个字节为

byte3outStr = byte1strInput + 0x5

接下来计算输出字符串第四个字节

0x10494 <main+68>:    ldrb    r4, [r1, #3] 
0x10498 <main+72>:    eor r0, r3, r4 
0x1049c <main+76>:    str r0, [r5, #3]

计算过程是

*(r5+3) = r3 xor r4

也就是说输出字符串第四个字节为

byte4strOut = byte2strInput xor byte4strInput

最后是输出字符串第五个字节

0x104a0 <main+80>:    ldrb    r2, [r1, #4] 
0x104a4 <main+84>:    eor r0, r2, r4 
0x104a8 <main+88>:    str r0, [r5, #4]

计算过程是

*(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

将等式左边的符号替换成我们想要的”Hello”字符串

‘H’ = 0x48 = byte1strInput xor byte2strInput
‘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 (,)

该算法似乎已经解决了,我们来试试一下

root@raspberrypi:/home/pi/arm/episode1# ./algorithm_reversing
Please enter your string: g/JC,
Hello

逆向一个简单的加载器

这个新程序是一个简单的加载程序,它的任务是加载指令到内存中,并执行这条指令此练习的目的是打印以下传出的消息:“WIN”。您必须在调试过程中更改xor key的值来打印“WIN”字符串
程序名称是:loader_reversing

root@raspberrypi:/home/pi/arm/episode1# file loader_reversing
loader_reversing: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped
root@raspberrypi:/home/pi/arm/episode1# strings loader_reversing
Andrea Sindoni @invictus1306
aeabi
.symtab
.strtab
.shstrtab
.text
.data
.ARM.attributes
loader_reversing.o
mystr
code
_loop
_exit
_bss_end__
__bss_start__
__bss_end__
_start
__bss_start
__end__
_edata
_end

用IDA打开文件

我们可以在_start例程中看到系统调用被调用(地址0x10090),系统调用号为0xc0(mmap syscall)让我们详细分析一下

mov r4, #0xffffffff  @file descriptor
ldr r0, =0x00030000  @address
ldr r1, =0x1000      @size of the mapping table
mov r2, #7           @prot
mov r3, #0x32        @flags
mov r5, #0           @offset
mov r7, #192         @syscall number
swi #0 @ mmap2(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, -1, 0)

在mmap系统调用之后,我们可以看到新的分配区域(0x30000)

(gdb) info proc mappings
process 2405
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x10000 0x11000 0x1000 0x0 /home/pi/arm/episode1/loader_reversing
0x20000 0x21000 0x1000 0x0 /home/pi/arm/episode1/loader_reversing
0x30000 0x31000 0x1000 0x0
0x76ffd000 0x76ffe000 0x1000 0x0 [sigpage]
0x76ffe000 0x76fff000 0x1000 0x0 [vvar]
0x76fff000 0x77000000 0x1000 0x0 [vdso]
0x7efdf000 0x7f000000 0x21000 0x0 [stack]
0xffff0000 0xffff1000 0x1000 0x0 [vectors]
0x10098处的指令    .text:00010098 LDR R1, =code将变量code的地址给r1,来看看code存放的数据
(gdb) i r $r1
r1 0x200f1 131313
(gdb) x/10x 0x200f1
0x200f1: 0xe93f7c56 0xe25fe45e 0xe1b2745b 0xe3b21468
0x20101: 0xe3b20454 0xe3b264c0 0xe0302453 0xe49f2457
0x20111: 0xe2501448 0xe0302453

这些字节似乎不像arm代码,然后继续执行0x100A4指令

.text:000100A4 LDR R2, [R1,R4]

r2=*(r1+r4),其中r4第一次参与计算时候等于0。看下一条指令

.text:000100A8 EOR R2, R2, R6

r2和r6异或,此时r2的值为0x56,r6,为0x12345。xor操作的结果存储在r2中,在下一条指令中将r2的值保存到mmap分配的0x30000地址开始的区域(注意r0是mmap系统调用的返回值)

.text:000100AC STR R2, [R0,R4]

该循环用于解密代码变量的所有字节,为了解密,我们将使用gdb(之后我们也将使用IDA做这个),然后在地址0x100BC设置断点,并查看地址0x30000

(gdb) b *0x100bc 
Breakpoint 3 at 0x100bc 
(gdb) c 
Continuing. 
Breakpoint 3, 0x000100bc in _loop () 
(gdb) x/24i 0x30000 
    0x30000: push {r11, lr} 
    0x30004: sub sp, sp, #8 
    0x30008: mov r4, sp 
    0x3000c: mov r2, #62 ; 0x3e 
    0x30010: mov r3, #2 
    0x30014: mov r5, #150 ; 0x96 
    0x30018: eor r1, r2, r5 
    0x3001c: str r1, [sp], #1 
    0x30020: sub r2, r2, #30 
    0x30024: eor r1, r2, r5 
    0x30028: str r1, [sp], #1 
    0x3002c: add r2, r2, #7 
    0x30030: subs r3, r3, #1 
    0x30034: bne 0x30024 
    0x30038: mov r0, #1 
    0x3003c: mov r3, #10 
    0x30040: str r3, [sp], #1 
    0x30044: mov r1, r4 
    0x30048: mov r2, #4 
    0x3004c: mov r7, #4 
    0x30050: svc 0x00000000 
    0x30054: add sp, sp, #4 
    0x30058: pop {r11, pc} 
    0x3005c: andeq r0, r0, r0

您可以看到我们有新的ARM指令我们也可以使用一个简单的idc脚本解密指令

auto i, t;
auto start=0x200f1;
for (i=0;i<=0x5C;i=i+4) 
{ 
    t = Dword(start)^0x123456;
    PatchDword(start,t);
    start=start+4;
}

我们来分析解密出来的代码

=> 0x30004:  sub sp, sp, #8 
    0x30008: mov r4, sp 
    0x3000c: mov r2, #62 ; 0x3e 
    0x30010: mov r3, #2 
    0x30014: mov r5, #150    ; 0x96 
    0x30018: eor r1, r2, r5 
    0x3001c: str r1, [sp], #1 
    0x30020: sub r2, r2, #30 
    0x30024: eor r1, r2, r5 
    0x30028: str r1, [sp], #1 
    0x3002c: add r2, r2, #7 
    0x30030: subs    r3, r3, #1 
    0x30034: bne 0x30024 
    0x30038: mov r0, #1 
    0x3003c: mov r3, #10 
    0x30040: str r3, [sp], #1 
    0x30044: mov r1, r4 
    0x30048: mov r2, #4 
    0x3004c: mov r7, #4 
    0x30050: svc 0x00000000 
    0x30054: add sp, sp, #4 
    0x30058: pop {r11, pc} 

在前五个指令(从0x30004到0x30014)之后,sp-=8(局部变量),r4=sp,r2=0x3e,r3=0x2,r5=0x96。

(gdb) i r $r2 $r3 $r4 $r5 $sp 
r2 0x3e 62 
r3 0x2 2 
r4 0x7efff7b0 2130704304 
r5 0x96 150 
sp 0x7efff7b0 0x7efff7b0

在接下来的两个指令(0x30018和0x3001c)中,r1=r2 xor r5=0xa8中,该值保存在堆栈上,并且sp增加1在0x3001c(str r1,[sp],#1)的指令之后

(gdb) x/x 0x7efff7b0 
0x7efff7b0: 0x000000a8
(gdb) i r $sp
sp 0x7efff7b1 0x7efff7b1

在地址0x30020,寄存器R2由值减去0X1E,执行后

(gdb) i r $r2
r2 0x20 32

现在在指令0x30024有一个简单的循环

=> 0x30024:  eor r1, r2, r5 
    0x30028: str r1, [sp], #1 
    0x3002c: add r2, r2, #7 
    0x30030: subs r3, r3, #1 
    0x30034: bne 0x30024 

每次循环,都是同通过r2和r5进行xor操作,并且总是将xor操作的结果存储到堆栈中,从而增加1(sp)。我们可以看到,循环次数有r3决定,R3的初始值是2,每次循环减1,则循环执行只有2次。当循环结束时,我们到达地址0x30038,让我们看看内容在0x7efff7b0(局部变量)

(gdb) x/4bx 0x7efff7b0 
0x7efff7b0: 0xa8 0xb6 0xb1 0x00

可以看到栈上存放了三个字节的数据,此时sp的值是

(gdb) i r $sp    
sp 0x7efff7b3 0x7efff7b3

执行0x3003c处的两个指令后,另一个字节存储到堆栈指针中

0x3003c: mov r3, #10 
0x30040: str r3, [sp], #1

在0x30040的指令之后,局部变量(0x7efff7b0)的内容是 (gdb) x/4bx 0x7efff7b0 0x7efff7b0: 0xa8 0xb6 0xb1 0x0a如果我们继续,调用write系统调用

0x30038: mov r0, #1  @ fd: stdout
...
0x30044: mov r1, r4  @ buf: r4 (the buffer stored at 0xbefff7e0;)
0x30048: mov r2, #4  @ count: len of the buffer
0x3004c: mov r7, #4  @ write syscall number
0x30050: svc 0x00000000

在write系统调用之后,这是结果

(gdb) nexti 

但是我们希望将WIN字符串作为结果,然后如本节开头所示,我们必须更改xor key以便计算出正确的值:

0x57 0x49 0x4e

我们可以看看0x30018的第一个xor指令

0x30018: eor r1, r2, r5

R5寄存器包含XOR key,我们要改变它,以便有

r1 = r2 xor r5 = 0x57

r2的值为0x3e,则r5寄存器(xor键)的值应为0x69

(gdb) set $r5=0x69
(gdb) i r $r5
r5 0x69 105

另外对于另外两次xor指令我们用相同的key,那么问题就解决了。

(gdb) c
Continuing.
WIN

基本反调试技术

这是本章最后一个例子,目的是了解算法并绕过一些基本的反调试技术,使得输出的消息是字符串“Good”。
程序:anti_dbg

root@raspberrypi:/home/pi/arm/episode1# file anti_dbg
anti_dbg: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.32, BuildID[sha1]=7028a279e2161c298caeb4db163a96ee2b2c49f3, not stripped

用调试器运行程序:

root@raspberrypi:/home/pi/arm/episode1# gdb -q ./anti_dbg
Reading symbols from ./anti_dbg...(no debugging symbols found)...done
(gdb) r
Starting program: /home/pi/arm/episode1/anti_dbg
You want debug me?
[Inferior 1 (process 2497) exited normally]

调试失败,即使我们使用strace/ltrace命令,也会是同样的结果。用IDA打开看看

分析这个指令

ldr r2, =aAd

将aAd变量地址给r2,看看aAd的内容

将变量的内容以字节形式显示,以便更好的观察

随后的两条指令将0x10988开始的四个字节放到局部变量var_C中,var_10局部变量包含了三个字节

这三个字节通过如下指令存放到var_10变量中

LDRH R1, [R2]                        @ load an halfword (2 byte) into R1
LDRB R2, [R2,#(unk_109CE – 0x109CC)] @ load the next byte(0x44) into r2
STRH R1, [R3]                        @ store into *R3 the first two bytes (0x22, 0x41)
STRB R2, [R3,#2]                     @ store the last byte 0x44 into *(R3+2)

现在我们有两个数组,第一个(var_C)包含4个元素

0x7, 0x2f, 0x2f, 0x24

第二个(var_10)包含3个元素

0x22, 0x41, 0x44

先来看看main函数逻辑,然后看看flag变量的作用

从上图可以看出,如果标志变量等于1,执行红线后的代码,否则执行绿色行(loc_10858)后的代码(flag!= 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 = address of the array with 4 elements
var_8 = 0 index (first iteration)

在执行add指令后,r3的值为

r3 = 0x7 + 0x40 = 0x47

我们可以创建一个简单的idc脚本来计算第一个数组的所有元素(var_C)

看看脚本输出结果

我们来看看flag != 1,或者说是loc_10864,这个时候是循环var_10数组,只有三个元素,循环下标index = r3。来看关键指令:

ADD R3, R3, #0x20

跟上面一样,我们可以创建一个用于解析最终字符串的idc脚本

输出结果

这次的结果不是想要的我们最终需要打印”Good”,所以,现在我们需要做的就是找出怎么改变flag的值。
我们还可以注意到,在main函数中,没有检查调试器是否存在,也没有”You want debug me?”的字符串。
查看一下falg变量的引用情况

从上图中,我们可以看到一个叫做ptrace_capt的函数,这个函数在main之前执行(你可以通过gdb在该函数上下断点来验证),为了更深入理解,我们来看一下.ctors(或者.init_array)段,这个段记录了一个函数地址列表,这个列表里面的函数将会在程序主函数执行前/之后后被调用,我们的例子是在执行前被调用。

查看ptrace_capt函数

很好,我们看到ptrace检查,这是一个非常简单的检查

if(ptrace(PTRACE_TRACEME, 0, 0, 0) < 0)
{
    printf("You want debug me?\n");
    exit(0);
}

我们很快可以看到,如何轻松地绕过这个检查。继续并分析loc_10690中的代码

我们可以总结一下流程:

1.打开文件password.raw

fopen("password.raw", "r")

2.计算大小

.text:000106B4 LDR R0, [R11,#var_10] ; load the file descriptor into r0
.text:000106B8 MOV R1, #0            ; offset
.text:000106BC MOV R2, #2            ; SEEK_END
.text:000106C0 BL fseek              ; seek to end of file
.text:000106C4 LDR R0, [R11,#var_10] ; load the file descriptor into r0
.text:000106C8 BL ftell              ; size

3.验证文件大小是否小于6

.text:000106E4 LDR R3, [R11,#var_14]
.text:000106E8 CMP R3, #6
.text:000106EC BLS loc_106F
.text:000106F0 MOV R0, #0
.text:000106F4 BL exit

如果文件大小小于6,我们到达loc_10700。否则程序结束

如果我们继续下去,我们可以看到它是一个循环

看看fgetc做了什么

.text:00010700 LDR R0, [R11,#var_10] ; load into r0 the file descriptor
.text:00010704 BL fgetc
.text:00010708 STR R0, [R11,#var_18  ; save r0 into the local variable var_18
after we have the function feof
.text:0001070C LDR R0, [R11,#var_10] ; load into r0 the file descriptor
.text:00010710 BL feof
.text:00010714 MOV R3, R0            ; mov the reterun value into r3
.text:00010718 CMP R3, #0            ; compare r3 with 0
.text:0001071C BEQ loc_10750         ; associated with the stream is not set (r3=0) branch to loc_10750

如果r3等于0(表示没有到文件末尾)

.text:00010750 loc_10750 ; CODE XREF: ptrace_capt+D0#j
.text:00010750 SUB R3, R11, #-var_1C  ; r3 = address of var_1C
.text:00010754 LDR R0, [R11,#var_18]  ; r0 ← *(r11+var_18)
.text:00010758 LDR R1, [R11,#var_8]   ; r1 ← *(r11+var_8)
.text:0001075C MOV R2, R3             ; r2 = r3
.text:00010760 BL sub0

var_18是读取的字符,第一次循环中的var_8(index)的值为0.然后我们有

sub0(var_18, var_8, &var_1C);

在下图中我们可以看到sub0函数的代码

翻译成一个伪C代码:

if(var_C==0 || var_C==2)
{
    //loc_1060C
    *var_10=var_8|0x55;
}
else
{
    //loc_10620
    *var_10=var_8^0x69 | var_8<<3;
}

当函数sub0返回时,执行以下代码(记住var_1C包含返回的值)

.text:00010764 LDR R3, [R11,#var_1C]
.text:00010768 LDR R2, [R11,#var_C]
.text:0001076C ADD R3, R2, R3
.text:00010770 STR R3, [R11,#var_C]
.text:00010774 LDR R3, [R11,#var_8]
.text:00010778 ADD R3, R3, #1
.text:0001077C STR R3, [R11,#var_8]
.text:00010780 B loc_10700

我们可以写相应的伪C代码

var_C = var_1C + var_C;
var_8++; //increment the index

当r3 != 0(到达文件的末尾)

.text:00010720 LDR R3, [R11,#var_C]
.text:00010724 LDR R2, =0x997
.text:00010728 CMP R3, R2
.text:0001072C BNE loc_10740
.text:00010730 LDR R3, =flag
.text:00010734 MOV R2, #1
.text:00010738 STR R2, [R3]
.text:0001073C B loc_10784
.text:00010740 loc_10740 ; CODE XREF: ptrace_capt+E0#j
.text:00010740 LDR R3, =flag
.text:00010744 MOV R2, #2
.text:00010748 STR R2, [R3]
.text:0001074C B loc_10784

在这种情况下,我们也可以编写伪C代码

if (var_C==0x997)
{
    flag=1;
}
else
{
    //loc_10740
    flag=2;
}

从上面的代码中看到如何改变flag变量的值我们必须首先创建password.raw文件,并在文件中写入5个字符

#vim password.raw
bbbbb

我使用vim与删除新行(LF)

:set noendofline binary

运行程序

我们需要使用gdb运行它,顺便过掉ptrace。

#gdb ./3b

然后我们可以设置一个断点在0x10678,并修改r3的值,以绕过ptrace检查

现在我们可以继续使用gdb进行分析,我的策略非常简单,我只想改变最后一个字节,检查flag是否等于1(var_C = 0x997)。我在文件中写道

|-|-|-|-|-|
|b|b|b|b|b|
|1|2|3|4|5|

我只想改变第五个字节使得var_C = 0x997。为了做到这一点,我们需要在第4步中知道var_C的值。

从上图可以看出,索引为3(交互4),var_C的值为0x724。让我们尝试改变第五个字节使得var_C = 0x977我写了一个简单的python脚本来改变第五个字节

num = 0x997-0x724
for c in range (0x20,0x7f):
    ref = c^0x69 | (c<<3)
    if (ref==num):
        print "The number is " + hex(c)
print "End!"

运行python脚本

#python antDgbAlgho.py
The number is 0x4a
End!

我们得到第五个字节的正确值,现在我们可以修改文件password.raw

#vim password.raw
bbbbJ

记住删除新行(LF)的设置

:set noendofline binary

启动程序

输出想要的结果”Good”

© 2017 物联网安全技术研究 All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero