-
Unidbg补环境实战第一篇:补环境入门
-
为什么要补环境
-
Unidbg补环境的案例情景复现
-
模拟执行so
-
参数获取
-
unidbg 代码初始化
-
目标函数的调用
-
补环境说明
-
实战补环境
-
-
本章小节
-
Unidbg补环境实战第一篇:补环境入门
Unidbg
是一个基于unicorn
的逆向工具,可以黑盒调用安卓和iOS中的so文件。这使得逆向人员可以在无需了解so内部算法原理的情况下,主动调用so中的函数,让其中的算法“为我所用”,只需要传入所需的参数、补全运行所需的环境,即可运行出所需要的结果。及由此衍生的辅助分析、算法还原、SO
调试与逆向等等功能。
对于Android逆向来说,Unidbg的特点有以下几种:
-
模拟JNI调用的API,因此可以调用
JNI_OnLoad
函数。 -
支持
JavaVM
和JNIEnv
。 -
支持模拟系统调用指令。
-
支持
ARM32
和ARM64
。 -
支持基于
Dobby
的inline hook。 -
支持基于
xHook
的GOT hook。 -
unicorn
后端支持简单的控制台调试器,gdb stub,指令追踪和内存读写追踪。 -
支持
dynarmic
快速的后端。
为什么要补环境
使用unidbg最主要的问题就是补环境,补环境对于so的模拟执行太重要了,并且在这块很容易跌跟头。我们知道unidbg的作用是模拟执行so中的函数,也就是使用C/C++编写的函数,它处于Native层。而Native的函数是Java层的函数通过JNI调用起来的,
那么Native也可以通过JNI这座桥梁去调用Java层的函数。
在Native层调用Java层的函数的时候,unidbg中并没有这些函数的实现,那么这些so就无法正常的通过unidbg加载起来。所以我们需要手动的把Java层的函数补充起来,让Native层的函数去调用。
PS:以下所有分析均在
r0env2022
版安卓逆向环境下完成。r0env2022
版集成了Unidbg,打开终端输入unidbg
回车,就是一个安装好所有依赖包的可以直接跑项目的完整的Unidbg
运行环境。
PS2:案例涉及的代码及附件会放在我的Github中,地址:https://github.com/r0ysue/AndroidSecurityStudy
Unidbg补环境的案例情景复现
为了给大家讲清本章的内容,笔者开发了一个样本APK构造了一些环境问题。首先我们先熟悉一下这个APP。APP的打开后的界面如图1所示:
图1 样本放置的目录
屏幕中央显示了一串字符,感觉像是16进制。然后,我们使用Jadx-gui反编译APK,其中MainActivity.java的代码如下所示:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
// 检测文件
public native void detectFile();
// 检测是否有Hook
public native void detectHookTool();
// native函数,获取哈希结果
public native String getHash(String str);
// 加载 so
static {
System.loadLibrary("dogpro");
}
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
this.binding = inflate;
setContentView(inflate.mo170getRoot());
TextView tv = this.binding.sampleText;
detectFile();
detectHookTool();
// 获取Hash结果
String r1 = getHash(getApplicationContext().getPackageCodePath());
tv.setText(r1);
}
}
在onCreate中有两个检测,我们先不要理会,他暂时不会对我们本章的内容产生任何的影响。但是可以给大家看看他们做了什么事情:
-
detectFile
int __fastcall Java_com_example_dogpro_MainActivity_detectFile(_JNIEnv *a1)
{
int v2; // [sp+8h] [bp-30h]
int v3; // [sp+Ch] [bp-2Ch]
int v4; // [sp+14h] [bp-24h]
int MethodID; // [sp+18h] [bp-20h]
int v6; // [sp+1Ch] [bp-1Ch]
int v7; // [sp+20h] [bp-18h]
int Class; // [sp+24h] [bp-14h]// 反射去Java层找File类 Class = _JNIEnv::FindClass(a1, "java/io/File"); v7 = _JNIEnv::AllocObject(a1, Class); // 检测的路径 v6 = _JNIEnv::NewStringUTF(a1, "/sys/class/power_supply/battery/voltage_now"); MethodID = _JNIEnv::GetMethodID(a1, Class, "<init>", "(Ljava/lang/String;)V"); _JNIEnv::CallVoidMethod(a1, v7, MethodID, v6); v4 = _JNIEnv::GetMethodID(a1, Class, "exists", "()Z"); if ( (unsigned __int8)_JNIEnv::CallBooleanMethod(a1, v7, v4) ) _android_log_print(6, "lilac", byte_35D7); else _android_log_print(6, "lilac", byte_35F0); v3 = _JNIEnv::AllocObject(a1, Class); v2 = _JNIEnv::NewStringUTF(a1, "/data/local/tmp/nox"); _JNIEnv::CallVoidMethod(a1, v3, MethodID, v2); if ( (unsigned __int8)_JNIEnv::CallBooleanMethod(a1, v3, v4) ) return _android_log_print(6, "lilac", byte_361D); else return _android_log_print(6, "lilac", byte_3636);
}
首先检测了电池的相关信息,我们尝试去/sys/class/power_supply/battery/voltage_now
下查看,如图2所示:
图21-2 电池属性
其它文件表示的含义如下所示:
//电池充电状态
cat /sys/class/power_supply/battery/status
//电池电量
cat /sys/class/power_supply/battery/capacity
//电池运行状况
cat /sys/class/power_supply/battery/health
//显示电池温度
cat /sys/class/power_supply/battery/temp
//电池电压 mV
cat /sys/class/power_supply/battery/voltage_now
第二处检测的是/data/local/tmp/nox
,nox是夜神模拟器,模拟器创建的时候会在此路径有文件的创建。幸运的是,当检测到的时候,并不会有任何的操作,所以我们不予理会,这里只带领大家看看它是如何做检测的。
-
detectHookTool
int __fastcall Java_com_example_dogpro_MainActivity_detectHookTool(_JNIEnv *a1)
{
int v1; // r0
int v2; // r0
const char *StringUTFChars; // [sp+28h] [bp-A0h]
int ObjectClass; // [sp+34h] [bp-94h]
int ObjectArrayElement; // [sp+38h] [bp-90h]
int i; // [sp+3Ch] [bp-8Ch]
int ArrayLength; // [sp+40h] [bp-88h]
int v9; // [sp+44h] [bp-84h]
int v10; // [sp+48h] [bp-80h]
int v11; // [sp+4Ch] [bp-7Ch]
int MethodID; // [sp+50h] [bp-78h]
int Class; // [sp+54h] [bp-74h]
size_t n; // [sp+6Ch] [bp-5Ch]
size_t v16; // [sp+7Ch] [bp-4Ch]
char v17[24]; // [sp+80h] [bp-48h] BYREF
char v18[36]; // [sp+98h] [bp-30h] BYREF// 反射找到 Throwable => 异常处理 Class = _JNIEnv::FindClass(a1, "java/lang/Throwable"); MethodID = _JNIEnv::GetMethodID(a1, Class, "<init>", "()V"); v11 = _JNIEnv::NewObject(a1, Class, MethodID); // 获取异常堆栈 v10 = _JNIEnv::GetMethodID(a1, Class, "getStackTrace", "()[Ljava/lang/StackTraceElement;"); // 调用方法 v9 = _JNIEnv::CallObjectMethod(a1, v11, v10); ArrayLength = _JNIEnv::GetArrayLength(a1, v9); // 复制值,检测 Xposed 框架 strcpy(v18, "de.robv.android.xposed.XposedBridge"); // 复制值,检测 substrate 框架 strcpy(v17, "com.saurik.substrate"); for ( i = 0; i < ArrayLength; ++i ) { ObjectArrayElement = _JNIEnv::GetObjectArrayElement(a1, v9, i); ObjectClass = _JNIEnv::GetObjectClass(a1, ObjectArrayElement); // 每个堆栈中的信息反射获取类名 v1 = _JNIEnv::GetMethodID(a1, ObjectClass, "getClassName", "()Ljava/lang/String;"); v2 = _JNIEnv::CallObjectMethod(a1, ObjectArrayElement, v1); StringUTFChars = (const char *)_JNIEnv::GetStringUTFChars(a1, v2, 0); n = _strlen_chk(v18, 0x24u); // 比对 if ( !strncmp(StringUTFChars, v18, n) ) { _android_log_print(6, "lilac", "%s", StringUTFChars); _android_log_print(6, "lilac", byte_389E); } v16 = _strlen_chk(v17, 0x15u); if ( !strncmp(StringUTFChars, v17, v16) ) { _android_log_print(6, "lilac", "%s", StringUTFChars); _android_l