Android(安卓)逆向实战
Android逆向实战
本文由 Luzhuo 编写,转发请保留该信息.
原文: https://blog.csdn.net/Rozol/article/details/88755130
反编译工具
相关工具
- apktool: Java编写, 且开源. 用于反编译apk, 能得到smali(Android虚拟机执行指令, 与dex可相互转化)源码和资源文件
- Github: https://github.com/iBotPeaches/Apktool
- 最新版下载: https://ibotpeaches.github.io/Apktool
- Jadx: 直接可视化apk的Java代码
- Github: https://github.com/skylot/jadx
- Xposed框架: hook应用, 能得到应用运行时数据, 并能修改运行时数据
- 工具 Xposed框架
- Xposed需要root权限, VirtualXposed不需要root权限
- IDA: 能动态调试java 和 so
另外还需要一些基础知识: smali语法、arm指令、NDK开发
apktool
反编译:
使用该工具最好是去Github下去源码, 放到Eclipse里执行, 一旦反编译报错可以立即修改源码解决问题.
1.下载源码: https://github.com/iBotPeaches/Apktool
2.Eclipse安装Gradle插件
- 安装Gradle: https://gradle.org
- 安装Gradle插件: Help -> Eclipse Marketplace -> 输入
buildship
-> 搜索 -> Install -> 重启Eclipse - 配置Gradle: Eclipse -> Project -> Properties -> Gradle -> Gradle user home配置Gradle路径 -> OK
3.导入项目
- File -> Import -> Existing Gradle Project(Gradle) -> 选择apktool -> Finish
- 如果你还没有配置Gradle的情况下导入了apktool, 那么你只需右键项目 -> Gradle -> Refresh Gradle Project 即可.
4.运行apktool
在brut.apktool包下的Main类下的main入口函数手动构建一个命令, 然后运行即可.
args = new String[]{"d", "C:\\apk\\xxx.apk", "-o", "C:\\apk\\xxx"};
回编译:
1.进行回编译:
args = new String[]{"b", "C:\\apk\\xxx1", "-o", "C:\\apk\\xxx_build.apk"};
2.签名:
jarsigner -verbose -keystore "C:\apk\case.key.jks" -signedjar "C:\apk\xxx_build_ok.apk" "C:\apk\xxx_build.apk" key0
-signedjar
参数:
- 1.签名后apk存放路径
- 2.未签名apk路径
- 3.证书别名
3.安装
逆向操作
打开调试开关
想要调试应用, 打开调试开关这是破解步骤的第一步.
不反编译的情况下:
Android配置信息放在 /system/build.prop
文件里
C:\Users\LZLuz>adb shell[email protected]:/ $ su[email protected]:/ # cat /system/build.prop |grep ro# begin build propertiesro.build.id=KOT49Hro.build.display.id=VIBEUI_V2.0_1512_7.17.1_ST_S658tro.build.version.incremental=VIBEUI_V2.0_1512_7.17.1_ST_S658tro.custom.build.version=VIBEUI_V2.0_1512_7.17.1_ST_S658tro.build.version.sdk=19ro.build.version.codename=RELro.build.version.release=4.4.2ro.build.date=Mon Feb 9 21:38:28 CST 2015
ro开头的属性表示只读, 不可修改
可以通过命令单独获取某值
[email protected]:/ # getprop ro.product.modelLenovo S658t
默认配置信息
[email protected]:/ # cat default.prop## ADDITIONAL_DEFAULT_PROPERTIES#ro.secure=1ro.allow.mock.location=0persist.mtk.aee.aed=onro.debuggable=0ro.adb.secure=1
这里的ro.debuggable=0
为0表示判断应用中的AndroidManifest.xml文件中manifest标签是否有android:debuggable="true"
属性, 有则可以调试; 若为1, 表示所有程序均可调试.
接下来我们想办法修改这个值, 改为1:
我们使用ptrace注入到init进程, 修改内存中的属性值, 只要该进程不重启就会一直有效, 即使重启了, 再操作一遍即可.
1.下载mprop工具
2.拷贝到设备中
adb push C:\Code\mprop /sdcard/
3.执行命令
C:\Users\LZLuz>adb shell[email protected]:/ $ su[email protected]:/ # cd sdcard/1|[email protected]:/sdcard # ./mprop2 ro.debuggable 1properties map area: b6f7e000-b6f9e00000000000 84 e1 00 00 00 89 00 00 50 52 4f 50 ab d0 6e fc .�......PROP��n�00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
4.查看是够修改成功
[email protected]:/sdcard # getprop ro.debuggable1
为1说明修改成功
5.使用DDMS查看可调试列表
AndroidStudio3.2找不到DDMS, 看来只能手动找了:
SDK -> tools -> monitor.bat
接着把adb进程重启下:
C:\Users\LZLuz>adb kill-serverC:\Users\LZLuz>adb start-server
DDMS显示了可调式的进程
并且可通过jdwp
命令查看可调式进程id:
C:\Users\LZLuz>adb jdwp2215229162627727256
通过dumpsys package
命令查看包信息:
adb shell dumpsys package me.luzhuo.hacker
反编译的情况下:
1.反编译
2.manifest标签添加 android:debuggable="true"
属性
3.回编译
4.签名
5.安装
以上步骤的具体执行, 见apktool介绍.
静态逆向
编写一个apk, 然后通过签名导出.
以下是用jadx-gui看到apk源码
package me.luzhuo.smalidemo;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.widget.TextView;public class MainActivity extends AppCompatActivity { /* Access modifiers changed, original: protected */ public void onCreate(Bundle bundle) { super.onCreate(bundle); setContentView((int) R.layout.activity_main); ((TextView) findViewById(R.id.tv_hello)).setText(getString("hello")); } private String getString(String str) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(str); stringBuilder.append("world!"); return stringBuilder.toString(); }}
我们的目的是针对getString(), 打印传入的参数, 打印运算后的结果, 返回我们期望的结果.
1.反编译apk生成smali代码
使用apktool进行反编译, 生成smali源码
.method private getString(Ljava/lang/String;)Ljava/lang/String; .locals 1 .line 19 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; const-string p1, "world!" invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p1 return-object p1.end method
2.修改smali代码
修改后的代码:
扩展了2个非参数寄存器, 用来存放"MyLog"和"AndroidSmali!",
然后打印了 参数p1 和 结果p1 的值,
最后定义了"AndroidSmali!"字符串给返回.
.method private getString(Ljava/lang/String;)Ljava/lang/String; .locals 3 const-string v3, "MyLog" # print param invoke-static {v3, p1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I .line 19 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; const-string p1, "world!" invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p1 # print result invoke-static {v3, p1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I # change result const-string v2, "AndroidSmali!" return-object v2.end method
3.回编译, 签名, 安装
如果安装时被告知是非法包, 并且用jadx-gui查看APK signature时, 包空指针, 这说明你的包本身就不能被安装, 你得通过签名导出的方式获得, 或者通过app市场下载的方式获得.
4.查看日志输出
使用命令adb logcat -s MyLog
即可.
C:\Users\LZLuz>adb logcat -s MyLog--------- beginning of crash--------- beginning of system--------- beginning of main03-09 19:35:41.829 5622 5622 I MyLog : hello03-09 19:35:41.829 5622 5622 I MyLog : helloworld!
动态逆向
smalidea地址
通过AndroidStudio3.2动态调试smali代码, 使用baksmali
插件.
1.安装插件
目前最新的插件是smalidea-0.05.zip
, 20170331更新的.
去Browse Repositories搜了下, 发现没有这个插件, 直接去官方下载然后安装即可.
2.打开调试
adb shellsucd sdcard/./mprop2 ro.debuggable 1getprop ro.debuggable
3.安装应用
安装apk, 不需要做任何修改
4.获得smali
用apk反编译apk, 得到smali代码, AndroidStudio中打开现有AndroidStudio项目(File -> Open -> 选择反编译后文件夹 -> New window)
右击smali目录, 把Mark Directory As
改成 Sources Root
5.配置调试
配置远程调试: Run -> Edit Configurations -> + -> Remote (如果8700被占, 可换成任意端口)
Host: localhostPort: 8700
获取目标进程
C:\Users\LZLuz>adb shell ps | findstr "com.example.simpleencryption"u0_a121 8938 140 558708 28800 ffffffff 00000000 S com.example.simpleencryption
可见目标进程为8938
启动应用并将JDWP服务转发到localhost (注:8700被占了, 我换成了8701)
adb forward tcp:8701 jdwp:8938
6.调试应用
打调试断点 -> 调试
调试结束清除端口绑定
adb forward --remove-all
动态调试比静态调试的好处是, 静态需要添加Log然后打包签名安装才能看到运算值, 而动态调试不需要修改apk直接运行调试就能看到值, 比静态调试简便了许多.
IDA逆向.so
IDA的基本使用的文章很早之前写过一篇
将ARM指令转成伪C
打开IDA -> new -> 选择.so文件打开
.text:000000000000059C ; jstring Java_me_luzhuo_idaso_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz).text:000000000000059C EXPORT Java_me_luzhuo_idaso_MainActivity_stringFromJNI.text:000000000000059C Java_me_luzhuo_idaso_MainActivity_stringFromJNI.text:000000000000059C ; DATA XREF: LOAD:0000000000000330↑o.text:000000000000059C.text:000000000000059C var_18 = -0x18.text:000000000000059C var_10 = -0x10.text:000000000000059C env = -8.text:000000000000059C var_s0 = 0.text:000000000000059C.text:000000000000059C ; __unwind {.text:000000000000059C SUB SP, SP, #0x30.text:00000000000005A0 STP X29, X30, [SP,#0x20+var_s0].text:00000000000005A4 ADD X29, SP, #0x20.text:00000000000005A8 ADRP X8, #[email protected] ; "Hello from C!".text:00000000000005AC ADD X8, X8, #[email protected] ; "Hello from C!".text:00000000000005B0 STUR X0, [X29,#env].text:00000000000005B4 STR X1, [SP,#0x20+var_10].text:00000000000005B8 STR X8, [SP,#0x20+var_18].text:00000000000005BC LDUR X8, [X29,#env].text:00000000000005C0 LDR X8, [X8].text:00000000000005C4 LDR X8, [X8,#0x538].text:00000000000005C8 LDUR X0, [X29,#env].text:00000000000005CC LDR X1, [SP,#0x20+var_18].text:00000000000005D0 BLR X8.text:00000000000005D4 LDP X29, X30, [SP,#0x20+var_s0].text:00000000000005D8 ADD SP, SP, #0x30.text:00000000000005DC RET.text:00000000000005DC ; } // starts at 59C.text:00000000000005DC ; End of function Java_me_luzhuo_idaso_MainActivity_stringFromJNI
按F5就成将其转成伪C代码, 可见其对jni.h的支持还是相当不错的
__int64 __fastcall Java_me_luzhuo_idaso_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz){ return ((__int64 (__fastcall *)(JNIEnv *, const char *))(*env)->NewStringUTF)(env, "Hello from C!");}
而原来的C代码是这样子的
jstring Java_me_luzhuo_idaso_MainActivity_stringFromJNI(JNIEnv* env, jobject thiz){ char* cstr = "Hello from C!"; return (*env)->NewStringUTF(env, cstr);}
调试.so步骤
目的: 通过调试获取方法的输入和输出内容.
1.打开调试
adb shellsucd sdcard/./mprop2 ro.debuggable 1getprop ro.debuggable
2.获取端口号
a.安装apk
b.运行apk
c.查看端口 (port:8700)(SDK -> tools -> monitor.bat)
3.IDA配置
a.把 IDA安装目录/dbgsrv/
下的android_server
文件拷贝手机/sdcard
目录下
adb push "C:\DevelopSoftware\IDA 7.0\dbgsrv\android_server" /sdcard/
b.运行./android_server
adb shellsucd sdcard./android_server
c. 连接端口
adb forward tcp:23946 tcp:23946
(这里的端口是固定的)
调试结束清除端口绑定
adb forward --remove-all
d.打开IDA -> Go -> Debugger -> Attach -> Remote ARMLinux/Android debugger -> 填写信息 -> OK
Hostname: 127.0.0.1Port: 23946
e.然后会弹出一个Choose process to attach to
对话框, android_server会帮你把所有进程信息给列出来, 按Ctrl+F进行搜索, 然后OK
4.开始调试
a.查找.so文件的基地址:
按Ctrl+S弹出choose segment to jump
, 然后按Ctrl+F搜索so文件名, 可见基地址为:5C035000.
b.获取函数地址:
再开一个IDA, 为了方便区分, 我们就叫他IDA2吧(之前那个就叫IDA), 选择new, 并打开.so文件.
找到要分析的函数Java_me_luzhuo_idaso_MainActivity_stringFromJNI
, 可以看到函数相对地址为:778
c.得到函数的绝对地址为:5C035778, 在IDA使用G键快速跳到这个绝对地址, 然后点击左边的小绿点下断点(F2), 再然后点击左上角的绿色三角形按钮运行(F9)
d.触发native代码运行, 按F8单步调试, 可以看到寄存器里的内容.
可见输入值为:hello from java!
长度为0x10, 换成十进制就是16.
可以点寄存器地址后面的跳转按钮, 跳到该数据所在的内存地址.
输出的结果为:ifmmp!gspn!kbwb"
解决反调试
当你用IDA调试时, 打了断点, 然后点击绿色小三角准备开始调试, 结果应用却退出了, 这说明该应用被加了反调试.
这是未被调试前, TracerPid: 0
C:\Users\LZLuz>adb shell[email protected]:/ $ ps | grep com.yaou0_a220 13387 140 560248 27988 ffffffff 00000000 S com.yaotong.crackme[email protected]:/ $ cat /proc/13387/statusName: yaotong.crackmeState: S (sleeping)Tgid: 13387Pid: 13387PPid: 140TracerPid: 0Uid: 10220 10220 10220 10220Gid: 10220 10220 10220 10220
这是被调试后, TracerPid: 11868
[email protected]:/ $ cat /proc/13387/statusName: yaotong.crackmeState: t (tracing stop)Tgid: 13387Pid: 13387PPid: 140TracerPid: 11868Uid: 10220 10220 10220 10220Gid: 10220 10220 10220 10220
可见反调试就是不断的监测自己应用进程的状态值(/proc/[pid]/status
), 如果TracerPid
非0则说明被调试了, 就结束应用.
我们要解决这个问题就要从以下两个方法找解决方案:
.init_array
: so最先加载段信息, 一般用来so解密JNI_OnLoad
: System.loadLibrary调用时执行, 没.init_array
早
1.打开调试
2.debug启动应用
用am的debug方式启动应用 (D要大写), 这样就不会执行static方法
adb shell am start -D -n com.yaotong.crackme/com.yaotong.crackme.MainActivity
3.设置Debugger Option
启动IDA(Go), 然后进行连接(前面连过很多次了, 这里就不细说了)
需要使它暂停在load前, 通过 Debugger -> Debugger Option 设置
4.运行IDA
a.点绿色小三角运行(F9)
b.运行attach命令
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
如果8700出现无法附加到目标 VM。
, 那么就选8700前面的那个端口号
5.下断点
a.在JNI_OnLoad方法下断点
按Ctrl+S获得libcrackme.so的地址为:5F707000.
JNI_OnLoad的相对地址为:00001B9C
JNI_OnLoad的绝对地址为:5F708B9C
按G跳到该位置, 并打上断点.
b.在libc的fopen方法下断点
按Ctrl+S找到libc.so, 打开它,
按Alt+T搜索fopen
关键字, 然后下断点 (会自动转为代码形式)
6.调试
进行单步调试, 我们发现BLX(执行) R7寄存器创建了一个线程,
这个线程执行的函数体是unk_5F62A6A4.
这里的unk_5F62A30C被重复执行, 这很可能就是反调试代码.
然后按F9运行到fopen处, LR是5F62A420, 我们点进去看看
不断的点上一层, 最后你会发现就是unk_5F62A30C调的他, 现在可以大概率认定它就是反调试代码.
然后继续单步执行, 看看fopen打开的是哪个文件
现在不用怀疑了, 他就是反调试代码.
7.修改so
我们发现BLX R7创建的线程调用unk_5F62A6A4函数就是用来反调试的, 那么只需不让它执行就行了.
5F708C58 BLX R7
的相对地址为: 5F708C58 - 5F707000 = 1C58,
打开IDA2, 按G跳到1C58.
切换为Hex View窗口, 将代码NOP, 也就是把1C58位置的值 37 FF 2F E1 改为 00 00 A0 E1
, 然后右键applay change
改完之后 edit -> patch program -> apply patches to input file…, 最后覆盖so文件重新打包签名运行即可.
脱壳逆向
加固分为 apk加固 和 so加固.
apk加固脱壳
apk被加固后, 代码在jadx-gui里都看不到了, 只有一个继承Application的壳, 虽然代码看不到了, 但是AndroidManifest.xml清单里的申明都还在.
解决思路: 不管源码被如何加固, 最终都需要被加载到内存中运行, 只需要找到dex在内存中的位置, dump出来即可.
1.打开调试
2.获取libdvm.so文件
libdvm.so文件在/system/lib
目录下
adb pull /system/lib/libdvm.so C:\apk\libdvm.so
3.debug启动应用
adb shell am start -D -n com.ali.tg.testapp/com.ali.tg.testapp.MainActivity
4.设置Debugger Option
启动IDA(Go), 然后连接, 再去设置Debugger Option
5.运行IDA
a.点绿色小三角运行(F9)
b.运行attach命令
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
6.下断点
a.打开IDA2获取libdvm.so的dvmDexFileOpenPartial()相对地址.
dvmDexFileOpenPartial(void const*, int, DvmDex **)
第1个参数为dex内存起始地址, 第2个参数为dex大小.
dvmDexFileOpenPartial的相对地址为: 00044F1C
地址为: 417CF000 + 00044F1C = 41813F1C
b.IDA按G, 输入地址打上断点
7.调试
按F9运行到断点出, 然后单步执行PUSH指令.
dump脚本代码块:
static main(void){ auto fp, dex_addr, end_addr; fp = fopen("C:\\apk\\dump.dex", "wb"); end_addr = r0 + r1; for ( dex_addr = r0; dex_addr < end_addr; dex_addr++ ) fputc(Byte(dex_addr), fp);}
按Shift+F2调出脚本, 把上面dump代码粘贴进去, run之后, 指定目录下生成dump.dex
文件, 直接用jadx-gui打开就能看到源码了.
也可以使用baksmali工具生成smali代码
java -jar baksmali-2.2.6.jar disassemble -o C:\apk\dexout\ dump.dex
本文总结
主要断点处 | 作用 | 位于so库 |
---|---|---|
JNI_OnLoad | 反调试 | xxx.so |
fopen | 反调试 | libc.so |
fgets | 反调试 | libc.so |
dvmDexFileOpenPartial | apk脱壳 | libdvm.so |
提供练习的资源文件在这: https://download.csdn.net/download/rozol/11051587
更多相关文章
- android中String与InputStream之间的相互转换方式
- Android(安卓)打测试包
- 工欲善其事必先利其器-Android(安卓)Studio技巧与插件(1)
- Android(安卓)如何检测一个服务是否还在运行?
- Android(安卓)源代码编后的目录分析
- Haisi3716C (海思)源代码 编译并烧写
- android 信息(mms)的故事(五)-- 发彩信
- 总结 使用PHP作为中间介来实现android链接远程数据库。
- Android(安卓)UI之代码动态设置ImageView的宽度和高度