根据作者的安排,文章分为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
编译器
对于所有代码(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汇编指令根其他编译器不一样,我建议你看看这些指令说明
源码
编译选项
对于编译选项必须要知道和理解,在本节中,我们将看到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”