IDA动态调试破解EXE文件与分析APK流程
Posted Tr0e
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IDA动态调试破解EXE文件与分析APK流程相关的知识,希望对你有一定的参考价值。
文章目录
前言
在前一篇文章:JEB动态调试与篡改攻防世界Ph0en1x-100 中介绍了如何借助 JEB 调试工具对 APK 的 smali 源码进行调试分析,本文主要来看如何使用 IDA 来调试 android 中的 native 源码,因为现在一些 app,为了安全或者效率问题,会把一些重要的功能放到 native 层的 so 库文件,这种情况下,靠 JEB 调试 smali 源码就无法满足分析破解 APK 的需求了,所以本文将学习记录下如何借助 IDA 调试工具来动态调试 so 文件,从而让提升破解成功率。IDA 不仅可以对 APK 进行动态调试,同时也可以对 ipa、exe、elf 等不同平台下的二进制可执行文件进行调试分析,本文还将演示 IDA 动态调试破解 exe 可执行文件。
IDA调试基础
下文演示 IDA 动态调试 APK 时,仍将以 JEB动态调试与篡改攻防世界Ph0en1x-100 文章中涉及的 攻防世界 Ph0en1x-100.apk 为例,调试的目标是在内存中定位并获取到 getFlag() 函数的值。在正式进行调试之前,先来看看 IDA 动态调试的一些基础知识和技巧。
1.1 JNI函数转换
使用 IDA 打开 libphcm.so 文件:
关键窗口简介:
功能窗口 | 介绍 |
---|---|
Function Window | 对应的so函数区域,可以使用 ctrl+f 进行函数的搜索定位 |
IDA View | 对应的 so 中代码指令视图,可以查看具体函数对应的 arm 指令代码 |
Hex View | 对应的 so 的十六进制数据视图,查看 arm 指令对应的数据等 |
1、定位到 Java_com_ph0en1x_android_1crackme_MainActivity_getFlag 函数处:
使用 F5 快捷键可以将 arm 指令转化成可读的 C 语言伪代码,帮助分析:
2、以上 C 语言伪代码还不够清爽,可以进一步进行 JNI 函数方法名还原,增加代码可读性。一般 JNI 函数方法名首先是一个指针加上一个数字,比如v 3+676,然后将这个地址作为一个方法指针进行方法调用,并且第一个参数就是指针自己,比如 (v3+676)(v3…)。这实际上就是我们在 JNI 里经常用到的 JNIEnv 方法,因为 IDA 并不会自动的对这些方法进行识别,所以当我们对 so 文件进行调试的时候经常会见到却搞不清楚这个函数究竟在干什么(因为这个函数实在是太抽象了)。解决方法非常简单,只需要对 JNIEnv 指针做一个类型转换即可,我们可以选中 a1 变量,然后按一下 y 快捷键:
然后将类型声明修改为 JNIEnv*
并保存:
查看一下效果(注意与上面原先的伪代码进行对比),代码瞬间清晰很多:
3、有人( 貌似是看雪论坛上的)还总结了所有 JNIEnv 方法对应的数字,地址以及方法声明:
JNI 是 Java Native Interface 的缩写,它提供了若干的 API 实现了Java 和 其他语言的通信(主要是 C/C++)。通俗来说,就是 JAVA 调用 C/C++ 函数的接口。如果你要想调用 C系列的函数,你就必须遵守这样的约定。详情可参见:安卓逆向15:JNI 和 NDK。
1.2 快捷键使用
1、Shirt+F12 快捷键,快速打开 so 中所有的字符串内容窗口(有时候字符串是一个非常重要的信息,特别是对于破解的时候,可能就是密码或者是密码库信息):
2、Ctrl+S 快捷键:有两个用途,用途一是在正常打开 so 文件的 IDA View 视图的时候,可以查看 so 对应的 Segement 信息:
可以快速得到一个段的开始位置和结束位置,不过这个位置是相对位置,不是 so 映射到内存之后的位置(关于 so 中的段信息,不了解的可以参看这篇文章:Android中so文件格式详解)。
用途二是当在调试页面的时候,Ctrl+s 可以快速定位到我们想要调试的 so 文件映射到内存的地址: 因为一般一个程序,肯定会包含多个 so 文件的,比如系统的 so 就有好多的,一般都是在 /system/lib 下面,当然也有我们自己的 so 文件。so 文件中对应的不同 Segement 信息被映射到内存中的,一般是代码段,数据段等,因为我们需要调试代码,所以我们只关心代码段,代码段有一个特点就是具有执行权限 x,所以我们只需要找到权限中有 x 的那段数据即可。
3、G 快捷键:在 IDA 调试页面的时候,我们可以使用 G 键快速跳转到指定地址(getFlag()函数)的内存位置:
这里的跳转地址是可以算出来的,比如此处想跳转到 getFlag 函数然后下断点,那么我们可以使用上面说到的 Ctrl+s 查找到 so 文件的内存开始的基地址 B37B77BA
,但本例是 LOAD 的地址 B37B5000
(原因我还不知道):
然后可以双开 IDA,使用 IDA View 窗口定位查看 getFlag 函数对应的相对地址00000EE0
:
二者相加就是 getFlag() 函数在此调试过程中的内存绝对地址B37B5EE0
:
一般这里的基地址只要程序没有退出,在运行中,那么它的值就不会变,因为程序的数据已经加载到内存中了,基地址不会变的,除非程序退出,又重新运行把数据加载内存中了,同时相对地址是永远不会变的,只有在修改 so 文件的时候,文件的大小改变了,可能相对地址会改变,其他情况下不会改变,相对地址就是数据在整个 so 文件中的位置。
【注意】有时候我们发现跳转到指定位置之后,看到的全是 DCB 数据:
这时候我们选择函数地址,点击 P 键就可以看到对应的 arm 指令源码了:
4、调试快捷键
1.3 ARM指令集
IDA 打开 so 文件之后,看到的是纯种的汇编指令代码,所以这就要求我们必须会看懂汇编代码,就类似于我们在调试 Java 层代码的时候一样,必须会 smali 语法。庆幸的是这两种语法都不是很复杂,所以我们知道一些大体的语法和指令就可以了。下面我们来看看 ARM 指令中的寻址方式、寄存器、常用指令,看完这三个知识点,我们就会对 ARM 指令有一个大体的了解。
ARM 指令中的寻址方式
寻址方式 | 解释 | 示例 |
---|---|---|
立即数寻址 | 操作数本身包含在指令中,只要取出指令也就取到了操作数。这个操作数叫做立即数,对应的寻址方式叫做立即寻址 | MOV R0,#64 ;R0 ← 64 |
寄存器寻址 | 利用寄存器中的数值作为操作数,也称为寄存器直接寻址 | ADD R0,R1, R2 ;R0 ← R1 + R2 |
寄存器间接寻址 | 把寄存器中的值作为地址,再通过这个地址去取得操作数,操作数本身存放在存储器中 | LDR R0,[R1] ;R0 ←[R1] |
寄存器偏移寻址 | 在寄存器寻址得到操作数后再进行移位操作,得到最终的操作数 | MOV R0,R2,LSL #3 ;R0 ← R2 * 8 ,R2的值左移3位,结果赋给R0 |
寄存器基址变址寻址 | 又称为基址变址寻址,它是在寄存器间接寻址的基础上扩展来的。它将寄存器(该寄存器一般称作基址寄存器)中的值与指令中给出的地址偏移量相加,从而得到一个地址,通过这个地址取得操作数 | LDR R0,[R1,#4] ;R0 ←[R1 + 4],将R1的内容加上4形成操作数的地址,取得的操作数存入寄存器R0中 |
多寄存器寻址 | 可以一次完成多个寄存器值的传送 | LDMIA R0,R1,R2,R3,R4 ;R1←[R0],R2←[R0+4],R3←[R0+8],R4←[R0+12] |
堆栈寻址 | 使用堆栈指针(Stack Pointer, SP)指示当前的操作位置,堆栈指针总是指向栈顶 | STMFD SP! ,{R1-R7, LR} ;将R1-R7, LR压入堆栈,满递减堆栈 |
ARM中的寄存器
R0-R3:用于函数参数及返回值的传递
R4-R6, R8, R10-R11:没有特殊规定,就是普通的通用寄存器
R7: 栈帧指针(Frame Pointer).指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址。
R9: 操作系统保留
R12:又叫IP(intra-procedure scratch )
R13:又叫SP(stack pointer),是栈顶指针
R14:又叫LR(link register),存放函数的返回地址。
R15:又叫PC(program counter),指向当前指令地址。
ARM中的常用指令含义
ADD 加指令
SUB 减指令
STR 把寄存器内容存到栈上去
LDR 把栈上内容载入一寄存器中
.W 是一个可选的指令宽度说明符。它不会影响为此指令的行为,它只是确保生成 32 位指令。Infocenter.arm.com的详细信息
BL 执行函数调用,并把使lr指向调用者(caller)的下一条指令,即函数的返回地址
BLX 同上,但是在ARM和thumb指令集间切换。
CMP 指令进行比较两个操作数的大小
ARM指令简单代码段分析
#include <stdio.h>
int func(int a, int b, int c, int d, int e, int f)
int g = a + b + c + d + e + f;
return g;
对应的 ARM 指令:
add r0, r1 将参数a和参数b相加再把结果赋值给r0
ldr.w r12, [sp] 把最底的一个参数f从栈上装载到r12寄存器
add r0, r2 把参数c累加到r0上
ldr.w r9, [sp, #4] 把参数e从栈上装载到r9寄存器
add r0, r3 累加d累加到r0
add r0, r12 累加参数f到r0
add r0, r9 累加参数e到r0
IDA调试APK
了解完 IDA 动态调试的前置知识,下面开始步入正题,对 攻防世界 Ph0en1x-100.apk 进行动态调试,调试的目标是在内存中定位并获取到 so 层 getFlag() 函数的值。
2.1 IDA调试步骤
1、对于没有反调试机制保护的 APK 的调试步骤:
1)adb push android_server(IDA的dbgsrv目录下) /data/local/tmp/android_server(这个目录是可以随便放的)
2) adb shell
3) su(手机一定要有root权限)
4) cd /data/local/tmp
5) chmod 777 android_server (给android_server可执行权限)
6)./android_server 对本地设备端口进行监听
6)再开一个cmd 转发端口:adb forward tcp:23946 tcp:23946
7)使用IDA连接上转发的端口,查看设备的所有进程,找到需要调试的进程进行Attach
8)设置断点,动静结合方式(基地址+相对地址)确定函数地址进行调试。
2、对于有反调试机制的 APK 的调试步骤(后面会单独学习反调试):
1)启动android_server
2)端口转发adb forward tcp:23946 tcp:23946
3)adb shell am start -D -n 包名/类名;出现 Debugger 的等待状态
(说明:以启动模式启动,是停在加载so文件之前,包名可以在 Androidmanifest 文件中找到)
4)打开IDA,附加上对应的进程之后,设置IDA中的load so时机,即在debug options中设置;
5)运行命令:jdb -connect com.sun.jdi.SocketAttach:hostname=localhost, port=8700
6)点击IDA运行按钮,或者F9快捷键。
【注意】 IDA 动态调试 APK 时需要使用真机才行,因为模拟器多数是基于 X86 架构的 CPU 指令,而 IDA 支持的是 ARM 架构的 CPU 指令。
2.2 Apk调试实例
1、运行 IDA 调试的服务端程序并转发端口:
2、IDA 加载 libphcm.so 文件并在功能菜单栏的 debugger 中选择并创建如下调试器:
3、配置 debugger,如下:
4、开始调试前可以先设置断点(快捷键 F2),由于调试的目的是获取内存中 getFlag() 函数的返回结果,所以可以设置断点如下(应注意到伪代码也可以设置断点):
按 Tab 快捷键可以快速切换到对应的 ARM 指令(IDA View-A):
5、经几次测试发现应将断点设置在 pop 指令上方的 ADD 指令,才能获取到想要的 getFlag() 函数计算结果的内存值,故取消原来伪代码的断点,在指令窗口设置断点如下:
6、断点设置完毕,开始附加手机对应应用的进程,在手机运行 APP 后在 Debugger 工具栏中选择 Attach to process…
,选中进程:
弹出的告警信息选择 same 即可:
7、附加进程完毕,可以关闭以下两个不常用的窗口(模块窗口和线程窗口):
8、此时手机上的 APP 将暂停在调试前设置的程序的入口 Suspend process entry point,无法正常运作程序输入字符串:
可以点击 F9 或者 IDA 上该按钮,恢复程序运行:
9、APP 恢复运行后并未在断点处暂停,因为尚未触发 getFlag() 函数,在输入框输入任意字符后点击 go 按键,发现程序将成功在刚才设置的断点处暂停下来:
10、将 Hex View-1 内存值的窗口显示更改为如下选项(可选择跟踪查指定寄存器在内存中存放的数值的变化):
窗口数值变化:
11、不断按 F8 单步步过,继续运行程序,最后可以发现熟悉的目标字符串出现了:
完整的应当如下:
至此就完成了简单的 APK 动态调试,观察到了本题中想要的内存值。
【注意】内存值窗口、指令窗口、寄存器窗口的值均可以在调试中进行篡改!具体可参见:IDA调试修改内存数据。
IDA调试EXE
下面继续演示下如何利用 IDA 调试 exe 可执行文件,这相对于调试 APK 来说,准备步骤会简单很多。程序下载地址,目标程序运行后要求输入正确的 Flag,显然需要逆向获得:
3.1 IDA静态分析
1、将 T6.exe 拖进 IDA,使用 Ctrl+F 在函数窗口搜索 main 函数:
2、快捷键 Shift + F12,打开字符串窗口,搜索“CTFer”:
3、可以定位发现 mian_0 才是真正的主函数,查看其伪代码:
结合程序逻辑,观察发现第 8 行的 sub_459511 应该是 printf
函数,第 9 行的 sub_459521 应该是 scanf()
函数,可以将其进行全局修改:
修改完以后整段伪代码的逻辑就很清晰了:
可以看到 str2 是我们的输入,str1 是经过一系列计算后的一个固定值(也就是我们想要的 Flag),废话不多说,当然是在第 12 行if ( !j__strcmp(Str1, Str2) )
处设置断点,查看程序运行中 str1 对应的内存值,即可获得 Flag。
3.2 IDA动态调试
1、设置完断点后,开始设置一个本地的 windows debugger:
2、接下来按如下按钮即可运行 exe 程序并进入调试模式:
3、输入任意字符串,程序将暂停在设置的断点处:
4、切换到汇编指令窗口,可以清晰地看到 ecx 寄存器将存放我们想要的 str1 的值:
所以将内存值窗口改为跟踪 ecx 寄存器的值:
5、切换后,惊喜来了,Flag 出现:
总结
本文学习了 IDA 动态调试的步骤、技巧,对 APK 和 EXE 文件进行了动态调试,获取了内存中对应的 Flag 的值,但是需要注意的是,对于 APK 来说,经常会有各种反调试的手段来防止他人调试程序,其防护手段及绕过检测常见的方法是逆向工作者所需要掌握的,后续我将继续补充、学习。
本文参考:
以上是关于IDA动态调试破解EXE文件与分析APK流程的主要内容,如果未能解决你的问题,请参考以下文章