声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责。
unidbg是什么?
unidbg 是一款基于 unicorn 和 dynarmic 的逆向工具,一个标准的 java 项目,可以黑盒调用Android和 iOS 中的 so 文件,无论是黑盒调用 so 层算法,还是白盒 trace 输出 so 层寄存器值变化都是一把利器~ 尤其是动态 trace 方面堪比 ida trace。
github 地址 https://github.com/zhkl0228/unidbg
unidbg项目的由来
由于现在的大多数 app 把签名算法已经放到了 so 文件中,所以要想破解签名算法,必须能够破解 so 文件。但是我们知道,C++ 的逆向远比 Java 的逆向要难得多了,所以好多时候是没法破解的,那么这个时候还可以采用 hook 的方法,直接读取程序中算出来的签名,但是这样的话,需要实际运行这个应用,需要模拟器或者真机,效率又不是很高。
unidbg 就是一个很巧妙地解决方案,他不需要直接运行 app,也无需逆向 so 文件,而是通过在 app 中找到对应的 JNI 接口,然后用 unicorn 引擎直接执行这个 so 文件,所以效率也比较高。
更重要的是它可以和springBoot一起做成web服务。
在安卓开发中,可以在Native层利用反射进行Java层方法的调用。它再和NDK动态注册进行配合能实现让逆向人员在so文件逆向中找不到与加密函数直接相关的函数名。这样能拦住一部分安卓逆向人员。
下面主要介绍静态注册和动态注册。
静态注册
原理
根据函数名将Java代码中的native方法与so中的JNI方法一一对应,当Java层调用so层的函数时,如果发现其上有JNIEXPORT和JNICALL两个宏定义声明时,就会将so层函数链接到对应的native方法上。
而native方法和so方法对应规则是:以字符串“Java”为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来就是对应的JNI函数名了。
public class MainActivity extends AppCompatActivity{
...
public native String stringFromJNI(); //Java 层Native方法
...
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplicationndk_MainActivity_stringFromJNI( // so层方法
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
CPP代码预览
新建一个Android项目,项目名为myapplicationndk,包名为com.example.myapplicationndk,选择语言为Java,并选择项目模板为“Native C++”,如下图所示:
图片 1.png (50.39 KB, 下载次数: 0)
下载附件
图片1
2024-3-17 20:51 上传
图片 2.png (28.11 KB, 下载次数: 0)
下载附件
图片2
2024-3-17 20:52 上传
项目创建完成后,其工程结构目录如下:
图片 3.png (20.18 KB, 下载次数: 0)
下载附件
图片3
2024-3-17 20:52 上传
这里主要关注的是cpp文件夹,这是系统自动生成的,该文件夹下包含一个CMakeLists.txt,其文件内容也是系统自动生成的,其内容如下:
add_library( # Sets the name of the library.
myapplicationndk
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp)
其中myapplicationndk是由native-lib.cpp文件编译后生成的so文件名,该文件名在MainActivity.java中作为参数被加载到应用中:
public class MainActivity extends AppCompatActivity {
// 应用启动时加载native-lib.so库
static {
System.loadLibrary("myapplicationndk");
}
...
}
在MainActivity.java中定义一个native方法myFunc(),该方法包含一个int型的参数,且返回值类型为String,如下图所示:
图片 4.png (35.67 KB, 下载次数: 0)
下载附件
2024-3-17 20:53 上传
虽然报错了但是没有多大的问题,是因为我们还没有注册这个方法。将鼠标移动到报错处,按下快捷键Alt+Shift+Enter。如下所示:
图片 5.png (23.04 KB, 下载次数: 0)
下载附件
2024-3-17 20:53 上传
操作结束,在native层自动创建了对应的JNI方法,如下所示:
图片 6.png (23.45 KB, 下载次数: 0)
下载附件
2024-3-17 20:53 上传
可以看出,上图中的方法上方有JNIEXPORT和JNICALL两个宏定义声明,且其命名符合native方法和so方法的对应规则。其中JNIEnv类型代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。jobject thiz代表该native方法的类实例或者这个native方法的类的class对象实例。然后实现其函数功能:
图片 7.png (31.89 KB, 下载次数: 0)
下载附件
2024-3-17 20:53 上传
在MainActivity.java中调用native方法:
图片 8.png (23.29 KB, 下载次数: 0)
下载附件
2024-3-17 20:54 上传
将以上程序运行到模拟器或者真机上,可以看到在程序启动以后会弹出如下图所示的提示:
图片 9.png (20.61 KB, 下载次数: 0)
下载附件
2024-3-17 20:54 上传
优点
缺点
动态注册
原理
在调用System.loadLibrary()时会在so层调用一个名为JNI_OnLoad()的函数,我们可以提供一个函数映射表,再在JNI_Onload()函数中通过JNI中提供的RegisterNatives()方法来注册函数。这样Java就可以通过函数映射表来调用函数,而不必通过函数名来查找对应函数。
实现流程
[ol]
[/ol]
其中JNINativeMethod结构体如下所示:
typedef struct {
const char* name; // native方法名
const char* signature; // 方法签名,例如()Ljava/lang/String;
void* fnPtr; // 函数指针
} JNINativeMethod;
这里关注一下第二个参数,其类型为字符串,由一对小括号和若干签名符号组成,其中括号内写传入参数的签名符号,没有参数则不写,括号外写返回参数的签名符号。
[table]
[tr]
[td]签名符号[/td]
[td]C/C++[/td]
[td]java[/td]
[/tr]
[tr]
[td]V