目录
2.2 使用IDA进行启动调试,原理是在应用程序执行初始化函数之前挂载进入应用
1.从方便性来看,肯定是第2种方法更好,这里采用第2种修改全局系统属性方法
2.启动ddms(在android_studio界面下载SDK或者网上搜索单独下载SDK文件)
一、系统环境
OS: Windows_NT x64 10.0.19045
JADX:1.5.0
IDA:7.7.220118
python:3.8.10
Node.js: 18.17.1
frida :14.2.14
objection:1.11.0
vscode: 1.87.2
device:nexus 5x-8.1.2
二、详细分析
前言:
典型的 Crake me 应用,废话不多说,开整
正文:
1.静态分析
使用 Jadx 打开后可以看到 Java层 MainActivity 逻辑很简单,真正对输入密码进行校验的位置在加载的 libwolf.so 中
public class MainActivity extends AppCompatActivity {
private EditText editText = null;
static {
System.loadLibrary("wolf");
}
/* JADX INFO: Access modifiers changed from: protected */
@Override // android.support.v7.app.AppCompatActivity, android.support.v4.app.FragmentActivity, android.support.v4.app.BaseFragmentActivityDonut, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(C0225R.layout.activity_main);
setTitle("Hey Man! Crack me!");
this.editText = (EditText) findViewById(C0225R.id.editText);
}
public void click(View v) {
if (this.editText.getText().toString().equals("")) {
Toast.makeText(this, "Please Enter Your PassWord!", 0).show();
} else {
C0224NI.greywolf(this, this.editText.getText().toString());
}
}
/* renamed from: s1 */
public String m13s1() {
return "s1";
}
/* renamed from: s2 */
public String m14s2() {
return "s2";
}
}
public class C0224NI {
public static native void greywolf(Context context, String str);
}
上 IDA 对 libwolf.so 进行分析
先看看 greywolf 这个函数是不是静态注册,以 java 为关键字在函数窗口搜索没有找到,说明是动态注册
函数窗口搜索找到 JNI_OnLoad分析
伪代码虽然可以基本看到大概,但还是看着不太舒服,尝试将代码还原得更贴近源码,JNI_OnLoad动态注册得流程是固定的,直接搜索 JNI_Onload动态注册源码实现(百度的ai智能回答还可以)
比对源码对变量类型和名字重命名后发现除了 AD() 这个函数外,其他都是动态注册的正常内容
进入 AD() 函数内部通过字符串可以分析出这个函数的作用应该是反调试
通过上面这些字符串可以分析出至少存在两种反调试方法:双进程反调试 和 调试端口检测,后面动态调试时需要处理
目前先重点看RegisterNatives函数,dword_55058就是 函数指针数组的地址
点击 dword_55058之后发现这个数组在 .bss 段
.bss段的作用
.bss段的主要作用是存放程序中未初始化的全局变量和静态变量。这些变量在编译时没有明确赋初值,因此在程序加载到内存时,系统会自动将.bss段的内存空间清零,以保证变量在使用前具备确定的初值。
也就是说 dword_55058 这个数组在应用运行的时候才能确定值,那么这个赋值操作是在哪里进行的呢?
点击 dword_55058,按下 X 键查找引用
可以看到在 sub_13C4C 这个函数中被引用,进入sub_13C4C 后再次查找引用
最终来到 .init_array 段
关于为什么可以在 .init_array段中进行初始化,这涉及到 so 的加载原理,如果感兴趣可以看看下面几篇文章
.preinit_array,.init_array,.init和JNI_OnLoadhttps://www.cnblogs.com/revercc/p/16859449.html
so的装载与链接https://www.cnblogs.com/runope/p/13934175.html
Android9.0.0_r61 so加载https://blog.csdn.net/qq_37661242/article/details/130448221
一文了解 Java 中 so 文件的加载原理https://blog.csdn.net/allisonchen/article/details/128809224
再回到 sub_13C4C
可以看到 dword_55058,dword_5505C,dword_55060是一个以4字节大小连续的地址,猜测这里就是注册函数数组的地址,对应 methods[]
//本地方法的声明
static void native_method(JNIEnv *env, jobject obj);
//要注册的方法的签名
static const char *native_method_signature = "(Ljava/lang/Object;)V";
// 函数指针数组
static JNINativeMethod methods[] = {
{"nativeMethod", native_method_signature, (void*)native_method}
};
dword_55058 = "nativeMethod"
dword_5505C = native_method_signature
dword_55060 = (void*)native_method
dword_55058, dword_5505C这两个变量的值都是字符串,从上面代码中可以看到这两个值都是从 wolf_de 函数返回,这个函数是对字符串加密的函数
所以真正的功能函数就是 dword_55060 地址的 bc 函数,这个函数也就是 java层调用的 greywolf函数
结合动态注册函数参数规则将 bc 函数的参数进行还原
static void native_method(JNIEnv *env, jobject obj);
public static native void greywolf(Context context, String str);
代码中函数的命名可读性比较差,容易把人绕晕,通过跟进到函数内部分析后根据具体函数的功能将函数重新命名为可读性更高的名字,不知道是不是开发者故意的,函数内部存在多次跳转,对于这些只是跳转而没有实际功能的函数命名规则是每跳1层就在函数名末尾累加1
bc = native_greywolf
dc = check_pass1
j_jk = check_pass2
j_st = show_Message
dh = check_signature
wolf_s = wolf_signature
输入的password只在 check_pass1 和 check_pass2 中有使用,所以跟随 参数 [password] 重点分析这两个函数调用链
通过分析发现,check_pass2最终调用的是 check_pass1,而 check_pass1 最终调用的是 j_ds 函数
分析最终调用 ds 函数可以发现,传入的 password 参数在 sub_146E0 被使用,这个函数很可能就是对 password 进行处理判断的关键函数
到这里已经基本知道了应用的逻辑和关键点函数,但因为从静态分析无法知道 sub_146E0的第1个参数的值是多少,所以通过静态分析无法分析出真实的 flag,接下来使用动态分析来获取到真实的flag
2.动态分析
2.1 使用IDA进行附加调试,原理基于ptrace机制
#操作步骤
1.将IDA/dbgsrv文件夹下的 android-server 和 android-server64 push 到手机 /data/local/tmp 目录下,并修改文件执行权限 chmod 777 android-server64 (因为是64位系统,所以修改这个文件,32位系统就修改 android-server)
2.启动android-server
进入目录 cd /data/local/tmp/
执行命令 ./android-server
3.启动IDA,菜单项选择 调试器 >附加 >Remote ARM Linux/Android debugger
4.在弹出的窗口输入手机ip地址
查看手机ip地址方法:adb shell到手机上执行 ifconfig
5.从进程列表中选择要调试的进程
这时发现一个问题,居然有两个名字相同的进程,这里就是双进程保护反调试,原理简单来说就是父进程启动时创建1个子进程并对子进程进行附加调试,由于调试原理是独占式的,一个进程在同一时间只能被1个进程单独调试占用,所以这时对子进程无法调试,同时由于父进程处于调试状态也无法被调试
#验证一下是否和原理一致
进入设备:adb shell
获取Root权限:su
获得APP的进程ID:ps | grep 软件的包名
查看进程的信息及TracerPid值: cat /proc/进程ID/status11874是父进程,11889是子进程
可以看到子进程11889的 TracerPid 就是 父进程的进程ID
尝试调试子进程->无法附加
尝试调试父进程->无法进入父进程进程空间
如果想深入了解原理的话,可以看看这篇文件
从这里可以看到附加调试的局限性,附加调试步骤是在应用的反调试代码运行之后,所以要对抗反调试,就需要将调试步骤放在应用的反调试代码之前执行
经过前面的静态分析可以知道下调试断点的位置应该在 .init_array和 JNI_Onload这两个入口处
2.2 使用IDA进行启动调试,原理是在应用程序执行初始化函数之前挂载进入应用
这种调试模式需要先将apk主动启动并暂停,所以需要apk开启调试模式,要开启apk的调试模式有两种方式
1.修改apk包中的AndroidManifest.xml,增加debuggable=true属性,重新打包签名apk
2.修改全局系统debuggable=true属性,修改这个全局系统属性之后,不管apk中有没有debuggable这个属性都可以被调试
1.从方便性来看,肯定是第2种方法更好,这里采用第2种修改全局系统属性方法
#具体修改步骤(手机重启后失效)
1.下载mprop(下面的github是作者的,包含64位,上面的链接只有32位)
https://www.renyiwei.com/wp-content/uploads/2019/05/mprop170119.zip
https://github.com/wpvsyou/mprop
2.上传mprop到手机并设置权限
$ adb push ./mprop /data/local/tmp/
$ adb shell
$ su
$ cd /data/local/tmp/
$ chmod 755 mprop
3.设置debuggable属性
$ ./mprop ro.debuggable 1
#查看属性是否设置成功,为1表示成功
$ getprop ro.debuggable
#重启adb服务
$ stop;start
2.启动ddms(在android_studio界面下载SDK或者网上搜索单独下载SDK文件)
运行SDK目录下的 monitor.bat
我的目录是C:\Users\win\AppData\Local\Android\Sdk\tools\monitor.bat
3.上传IDA调试arm架构文件并修改权限
#在IDA\dbgsrv目录下找到 android_server 文件上传到手机
$ adb push ./android_server /data/local/tmp/as
#修改权限
$ adb shell
$ su
$ chmod 777 /data/local/tmp/as
#运行android_server(静态分析时发现默认端口23946被检测,所以要修改启动端口)
$ ./data/local/tmp/as -p 32090
4.启动调试
#启动终端进行调试端口转发
$ adb forward tcp:32090 tcp:32090
#查看AndroidManifest.xml文件获取主界面名称
#以Debug方式启动主界面
$ adb shell am start -D -n com.wolf.ndktest/.MainActivity
#启动IDA通过转发端口连接到设备待调试进程
#修改调试器设置
#勾选3个选项(在加载so模块的时候可以暂停住)
#快捷键F9运行进程
#运行 jdb 连接到进程(这个文件在JDK目录,如何安装JDK网上搜索)
开启终端进入 jdb目录执行下面的命令
$ ./jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8608
上面的端口号8608来源于 ddms中进程的端口
#回到IDA加载libwolf.so模块
快捷键F9运行程序,同时观察IDA输出窗口是否已经加载 libwolf.so,如果没有加载就继续F9直到 libwolf.so加载
在模块列表选中并双击进入 libwolf.so模块
找到JNI_OnLoad函数并选中双击
#添加断点后修改调试器设置恢复程序运行
取消勾选3项设置后F9运行程序
程序成功在JNI_OnLoad入口除暂停
5.突破反调试
通过之前的静态分析可以知道,JNI_OnLoad入口处的函数就是 反调试函数AD,所以这里直接将这个函数nop掉
6.获取flag
根据前面的静态分析结果,bc函数是关键功能函数,check_pass1函数是处理password的函数入口,ds函数是最终处理flag的位置,所以我们直接一步步进入到 ds函数
#具体步骤
1.先找到bc函数
&unk_CE120058就是注册函数指针地址
从CE120058到CE120063刚好12字节,对应注册函数指针数组3个成员变量,使用快捷键D将Byte数据转换成DWORD数据
前面两个地址的内容都是字符串
第3个地址是bc函数地址
选中CE0DF074位置使用快捷键C将Byte数据还原为程序代码,在函数开头加上断点然后F9运行程序
2.触发bc函数断点
此时apk界面从暂停状态恢复为可输入操作,输入password,点击VERIFY IT!
bc函数断点被触发
单步调试进入第2个函数,也就是check_pass1(接下来进入的函数命名都与静态分析一致,除了最后一个ds因为符号重名问题命名为ds2)
进入 check_pass1_2
进入check_pass1_2_3
进入check_pass1_2_3_4
进入check1_2_3_4_5
进入 j_ds
进入 ds
进入 ds2(等同于静态分析最后一个ds)
快捷键 F8调试运行到 sub_146E0观察参数
R0 的值 = aaabbb
R1的值 = hello5.1
aaabbb是输入的password,猜测 R1的值就是要找的真实的flag
到apk界面输入验证一下
密码正确,hello5.1就是真实的flag.
参考文章: