unidbg文件读写
1. 前言
本篇分析的是自写demo,主要是为了熟悉unidbg对于文件读写的处理,会涉及到:
- Sharedpreference 读写;
- Assets 读写;
- 文件 读写;
- 在模拟执行之前需要手动的编写好目标应用的代码;
2. demo1设计
- 首先,了解一下Sharedpreferences的基本概念:
Sharedpreferences是Android平台上常用的存储方式,用来保存应用程序的各种配置信息,其本质是一个以“键-值”对的方式保存数据的xml文件,其文件保存在/data/data/selfPackage/shared_prefs目录下;
- 在APP刚启动时,我们新建两个Sharedpreference 文件,分别填入两个键值对,name-sana和location-china,首先先看基本的业务代码:
package com.unidbg.demo1;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.unidbg.demo1.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("demo1");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 步骤1:创建SharedPreferences对象
SharedPreferences sharedPreferences1= getSharedPreferences("one", Context.MODE_PRIVATE);
SharedPreferences sharedPreferences2= getSharedPreferences("two", Context.MODE_PRIVATE);
// 步骤2: 实例化SharedPreferences.Editor对象
SharedPreferences.Editor editor1 = sharedPreferences1.edit();
SharedPreferences.Editor editor2 = sharedPreferences2.edit();
// 步骤3:将获取过来的值放入文件
editor1.putString("name", "xiayutian");
editor2.putString("location", "china");
// 步骤4:提交
editor1.apply();
editor2.apply();
TextView tv = findViewById(R.id.sample_text);
Button btn = findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String string_data = stringFromJNI(getApplicationContext());
Log.i("sanaTag", string_data);
tv.setText(string_data);
}
});
}
public native String stringFromJNI(Context applicationContext);
}- 接下来的重点就是 stringFromJNI 这个native方法了,先不着急,看看是否有目标的文件存在了;

我们的目标就是来读取它俩,stringFromJNI 的主要逻辑就是干这事,但是实现的方式选取以下两种:
- 读取one.xml时,使用JNI去调用 java 层对SharedPreference操纵的API;
- 读取two.xml时,使用系统调用open打开这个xml;
- 既然Sharedpreference本质上是个xml文件,那么用native中原生的open函数去读可能会更隐蔽,可是open本质上也是通过底层系统调用open的方式实现,那我们直截了当通过系统调用open打开这个xml也是一样;
- 完整代码如下:
#include <jni.h>
#include <string>
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
__attribute__((naked))
long raw_syscall(long _number, ...) {
__asm__ __volatile__ ("MOV R12,SP\r\n"
"STMFD SP!,{R4-R7}\r\n"
"MOV R7,R0\r\n"
"MOV R0,R1\r\n"
"MOV R1,R2\r\n"
"MOV R2,R3\r\n"
"LDMIA R12,{R3-R6}\r\n"
"SVC 0\r\n"
"LDMFD SP!,{R4-R7}\r\n"
"mov pc,lr");
};
char *test_syscall(const char *file_path) {
char *result = "";
long fd = raw_syscall(5, file_path, O_RDONLY | O_CREAT, 400);
if (fd != -1) {
char buffer[0x100] = {0};
raw_syscall(3, fd, buffer, 0x100);
result = buffer;
raw_syscall(6, fd);
}
return result;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_unidbg_demo1_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */, jobject mycontext) {
// 反射Context类
jclass cls_Context = env->FindClass("android/content/Context");
// 反射Context类getSharedPreferences方法
jmethodID mid_getSharedPreferences = env->GetMethodID(cls_Context,
"getSharedPreferences",
"(Ljava/lang/String;I)Landroid/content/SharedPreferences;");
// 获取Context类MODE_PRIVATE属性值
// 执行反射方法
jobject obj_sharedPreferences = env->CallObjectMethod(mycontext,
mid_getSharedPreferences,
env->NewStringUTF("one"),
0);
jclass cls_SharedPreferences = env->FindClass("android/content/SharedPreferences");
//反射SharedPreferences类的getString方法
jmethodID mid_getString = env->GetMethodID(cls_SharedPreferences,
"getString",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
// 参数类型转换
jstring key_name = env->NewStringUTF("name");
// 参数类型转换
jstring default_value = env->NewStringUTF(" ");
// 执行反射方法
jstring key_value1 = (jstring) env->CallObjectMethod(obj_sharedPreferences,
mid_getString, key_name, default_value);
const char *c_key_value1 = env->GetStringUTFChars(key_value1, 0);
const char *path = "/data/data/com.unidbg.demo1/shared_prefs/two.xml";
const char *result = test_syscall(path);
char dest[1000];
strcpy(dest, c_key_value1);
strcat(dest, result);
return env->NewStringUTF(dest);
}- 编译的时候需要注意,只编译32位的so;
defaultConfig {
applicationId "com.unidbg.demo1"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
ndk{
abiFilters 'armeabi-v7a'
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}- 最后读取到的结果就是这样的:

- 为了省事就不去切割第二部分字符串了,直接开始后模拟执行;
3. 模拟执行demo1
- 直接调用stringFromJNI方法,基本的框架如下:
package com.baiLong.csdn_demo8;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
public class csdn_demo8 extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public csdn_demo8() {
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("src/test/java/com/baiLong/csdn_demo8/file/demo1.apk"));
vm.setVerbose(true);
vm.setJni(this);
DalvikModule dm = vm.loadLibrary(new File("src/test/java/com/baiLong/csdn_demo8/file/libdemo1.so"), false);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
csdn_demo8 demo = new csdn_demo8();
String res = demo.call();
System.out.println("res-->>" + res);
}
public String call() {
// 898
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数 实例方法是jobject 静态方法是jclazz 直接填0 一般用不到
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context
list.add(vm.addLocalObject(context));
Number number = module.callFunction(emulator, 0x898 + 1, list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
}- 产生第一个报错:
java.lang.UnsupportedOperationException: android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:419)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)- 这正是开发中的其中一步,按照基本逻辑补好;
case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;": {
String arg0 = (String) vaList.getObjectArg(0).getValue();
System.out.println("sana getSharedPreferences arg0-->> " + arg0);
return vm.resolveClass("android/content/SharedPreferences").newObject(arg0);
}- 在继续运行之前需要说明,为何这里给的是arg0,返回null行不行?
- 假设APP在JNI中从五个SharedPreferences里读了十五个键值对,并且不同xml的键名有重复,如果每次取SharedPreferences时我们都返回空对象,那后面怎么区分a.xml和b.xml里键名都是name的数据呢?
- 参数1是想要获取的SharedPreferences的名字,应该把它放对象里返回,这样就有了"标识性",继续执行;
java.lang.UnsupportedOperationException: android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:419)
at com.baiLong.csdn_demo8.csdn_demo8.callObjectMethodV(csdn_demo8.java:68)- 这个方法有参数,我们打印看看;
case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
String fileName = dvmObject.getValue().toString();
String key = (String) vaList.getObjectArg(0).getValue();
String value = (String) vaList.getObjectArg(1).getValue();
System.out.println("sana getString fileName-->> " + fileName);
System.out.println("sana getString key-->> " + key);
System.out.println("sana getString value-->> " + value);
}
/*
sana getString fileName-->> one
sana getString key-->> name
sana getString value-->>
*/- 很显然,分别是文件名和键值都是我们熟悉的,这里也可以发现,文件名是我们从上一个环境中传下来的,也是我们那么做的原因所在;返回值自然就应该是对应的值了,这里也就应该是xiayutian;
case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
String fileName = dvmObject.getValue().toString();
String key = (String) vaList.getObjectArg(0).getValue();
System.out.println("sana getString fileName-->> " + fileName);
System.out.println("sana getString key-->> " + key);
return new StringObject(vm, "xiayutian");
}- 这里是可以更加严谨的,做一个判断,如果读的是one这个文件再这么返回;
case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": {
String fileName = dvmObject.getValue().toString();
String key = (String) vaList.getObjectArg(0).getValue();
if (fileName.equals("one")) {
if (key.equals("name")) {
return new StringObject(vm, "xiayutian");
}
}
}- 这才是正确的做法,继续运行,发现出值了;

- 但是有个点出现了差错,我们不是还有一个文件需要读吗,为何直接完成了执行,并且unidbg并没有告诉我们任何这方面的消息,完整日志如下:
[15:46:02 714] INFO [com.github.unidbg.linux.AndroidElfLoader] (AndroidElfLoader:483) - libdemo1.so load dependency libandroid.so failed
JNIEnv->FindClass(android/content/Context) was called from RX@0x40000a09[libdemo1.so]0xa09
JNIEnv->GetMethodID(android/content/Context.getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;) => 0xa0a12d1f was called from RX@0x40000a2b[libdemo1.so]0xa2b
JNIEnv->NewStringUTF("one") was called from RX@0x40000aab[libdemo1.so]0xaab
sana getSharedPreferences arg0-->> one
JNIEnv->CallObjectMethodV(android.content.Context@4fccd51b, getSharedPreferences("one", 0x0) => android.content.SharedPreferences@60215eee) was called from RX@0x40000a65[libdemo1.so]0xa65
JNIEnv->FindClass(android/content/SharedPreferences) was called from RX@0x40000a09[libdemo1.so]0xa09
JNIEnv->GetMethodID(android/content/SharedPreferences.getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;) => 0x2c5b4190 was called from RX@0x40000a2b[libdemo1.so]0xa2b
JNIEnv->NewStringUTF("name") was called from RX@0x40000aab[libdemo1.so]0xaab
JNIEnv->NewStringUTF(" ") was called from RX@0x40000aab[libdemo1.so]0xaab
JNIEnv->CallObjectMethodV(android.content.SharedPreferences@60215eee, getString("name", " ") => "xiayutian") was called from RX@0x40000a65[libdemo1.so]0xa65
JNIEnv->GetStringUtfChars("xiayutian") was called from RX@0x40000ac9[libdemo1.so]0xac9
JNIEnv->NewStringUTF("xiayutian") was called from RX@0x40000aab[libdemo1.so]0xaab
res-->>xiayutian- 这是一种非常难受的体验,若非我们提前得知都以为补完了···这里为什么没有提示呢?回忆一下我们的方式,使用的是系统调用,但我们没有打开Unidbg中系统调用的日志显示,手动打开一下吧;
public static void main(String[] args) {
Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG);
// Logger.getLogger("com.github.unidbg.unix.UnixSyscallHandler").setLevel(Level.DEBUG);
// Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG);
// Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG);
// Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG);
// Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG);
csdn_demo8 demo = new csdn_demo8();
String res = demo.call();
System.out.println("res-->>" + res);
}- 在日常的样本里推荐打开全部日志,避免有所遗漏,但也会有弊端,日志会比较多;

- 我们需要补文件访问了,这里有两种方式补文件访问,1是Unidbg提供的rootfs虚拟文件系统,2是代码方式文件重定向;我个人比较青睐io重定向的方式;
package com.baiLong.csdn_demo8;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.file.ByteArrayFileIO;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
// 1 实现IOResolver
public class csdn_demo8 extends AbstractJni implements IOResolver {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
@Override
// 3 resolve方法中处理
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if ("/data/data/com.unidbg.demo1/shared_prefs/two.xml".equals(pathname)) {
return FileResult.success(new ByteArrayFileIO(oflags, pathname, "mytest".getBytes()));
}
return null;
}
public csdn_demo8() {
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("src/test/java/com/baiLong/csdn_demo8/file/demo1.apk"));
vm.setVerbose(true);
// 2 注意这里的处理,加上这一句
emulator.getSyscallHandler().addIOResolver(this);
vm.setJni(this);
DalvikModule dm = vm.loadLibrary(new File("src/test/java/com/baiLong/csdn_demo8/file/libdemo1.so"), false);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
}- 这里io处理有三步,请不要忘记了,继续执行;

- 没问题了,这里想要和真机一样也是可以的,但是没必要;
4. demo2设计
- 各类加密算法大部分都有密钥的存在,非对称加密算法还有公钥私钥之分,所以加密算法运算时需要传入密钥,但是直接参数方式传递很容易被分析者意识到这是密钥,有没有办法更隐蔽一些呢?
- 比如我们把密钥放在xml中,在native中读取它,就类似于demo1;
- 有没有更隐蔽一些些的呢,我们可以把密钥藏在资源文件的图片里,即在so里读取资源文件里的某张图片,以它的某部分或者整体的md5结果作为密钥?这是一个更好的方案;
- demo2就是这个方案的简单实现——native中读取资源文件的1.jpg,并求其md5值,返回JAVA层;
- 首先看java层代码:
package com.unidbg.demo2;
import androidx.appcompat.app.AppCompatActivity;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.widget.TextView;
import com.unidbg.demo2.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("demo2");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
tv.setText(setNativeAssetManager(getAssets()));
}
public native String setNativeAssetManager(AssetManager assetManager);
}- native层的代码比较复杂,主要是md5代码比较多;
#include <jni.h>
#include <string>
#include "android/asset_manager.h"
#include "android/asset_manager_jni.h"
#define MD5_LONG unsigned long
// 分组大小
#define MD5_CBLOCK 64
// 分块个数
#define MD5_LBLOCK (MD5_CBLOCK/4)
// 摘要长度(字节)
#define MD5_DIGEST_LENGTH 16
#define MD32_REG_T long
// 小端序
#define DATA_ORDER_IS_LITTLE_ENDIAN
// 四个初始化常量
#define INIT_DATA_A (unsigned long)0x67452301L
#define INIT_DATA_B (unsigned long)0xefcdab89L
#define INIT_DATA_C (unsigned long)0x98badcfeL
#define INIT_DATA_D (unsigned long)0x10325476L
// 循环左移以及取低64位
#define ROTATE(a,n) (((a)<<(n))|(((a)&0xffffffff)>>(32-(n))))
// 大小端序互转
#define HOST_c2l(c,l) (l =(((unsigned long)(*((c)++))) ), \
l|=(((unsigned long)(*((c)++)))<< 8), \
l|=(((unsigned long)(*((c)++)))<<16), \
l|=(((unsigned long)(*((c)++)))<<24) )
#define HOST_l2c(l,c) (*((c)++)=(unsigned char)(((l) )&0xff), \
*((c)++)=(unsigned char)(((l)>> 8)&0xff), \
*((c)++)=(unsigned char)(((l)>>16)&0xff), \
*((c)++)=(unsigned char)(((l)>>24)&0xff), \
l)
// 更新链接变量值
#define HASH_MAKE_STRING(c,s) do { \
unsigned long ll; \
ll=(c)->A; (void)HOST_l2c(ll,(s)); \
ll=(c)->B; (void)HOST_l2c(ll,(s)); \
ll=(c)->C; (void)HOST_l2c(ll,(s)); \
ll=(c)->D; (void)HOST_l2c(ll,(s)); \
} while (0)
// 四个初始化非线性函数,或者叫逻辑函数
#define F(b,c,d) ((((c) ^ (d)) & (b)) ^ (d))
#define G(b,c,d) ((((b) ^ (c)) & (d)) ^ (c))
#define H(b,c,d) ((b) ^ (c) ^ (d))
#define I(b,c,d) (((~(d)) | (b)) ^ (c))
// F函数,每隔16步/轮 换下一个
#define R0(a,b,c,d,k,s,t) { \
a+=((k)+(t)+F((b),(c),(d))); \
a=ROTATE(a,s); \
a+=b; };
#define R1(a,b,c,d,k,s,t) { \
a+=((k)+(t)+G((b),(c),(d))); \
a=ROTATE(a,s); \
a+=b;};
#define R2(a,b,c,d,k,s,t) { \
a+=((k)+(t)+H((b),(c),(d))); \
a=ROTATE(a,s); \
a+=b; };
#define R3(a,b,c,d,k,s,t) { \
a+=((k)+(t)+I((b),(c),(d))); \
a=ROTATE(a,s); \
a+=b; };
typedef struct MD5state_st1{
MD5_LONG A,B,C,D; // ABCD
MD5_LONG Nl,Nh; // 数据的bit数计数器(对2^64取余),Nh存储高32位,Nl存储低32位。这种设计是服务于32位处理器,MD5的设计就是为了服务于32位处理器的。
MD5_LONG data[MD5_LBLOCK];//数据缓冲区
unsigned int num;
}MD5_CTX; // 存放MD5算法相关信息的结构体定义
unsigned char cleanse_ctr = 0;
// 初始化链接变量/幻数
int MD5_Init(MD5_CTX *c){
memset (c,0,sizeof(*c));
c->A=INIT_DATA_A;
c->B=INIT_DATA_B;
c->C=INIT_DATA_C;
c->D=INIT_DATA_D;
return 1;
}
// md5 一个分组中的全部运算
void md5_block_data_order(MD5_CTX *c, const void *data_, unsigned int num){
const auto *data= static_cast<const unsigned char *>(data_);
unsigned MD32_REG_T A,B,C,D,l;
#ifndef MD32_XARRAY
unsigned MD32_REG_T XX0, XX1, XX2, XX3, XX4, XX5, XX6, XX7,
XX8, XX9,XX10,XX11,XX12,XX13,XX14,XX15;
# define X(i) XX##i
#else
MD5_LONG XX[MD5_LBLOCK];
# define X(i) XX[i]
#endif
A=c->A;
B=c->B;
C=c->C;
D=c->D;
// 64轮
// 前16轮需要改变分组中每个分块的
for (;num--;){
HOST_c2l(data,l); X( 0)=l; HOST_c2l(data,l); X( 1)=l;
/* Round 0 */
R0(A,B,C,D,X( 0), 7,0xd76aa478L); HOST_c2l(data,l); X( 2)=l;
R0(D,A,B,C,X( 1),12,0xe8c7b756L); HOST_c2l(data,l); X( 3)=l;
R0(C,D,A,B,X( 2),17,0x242070dbL); HOST_c2l(data,l); X( 4)=l;
R0(B,C,D,A,X( 3),22,0xc1bdceeeL); HOST_c2l(data,l); X( 5)=l;
R0(A,B,C,D,X( 4), 7,0xf57c0fafL); HOST_c2l(data,l); X( 6)=l;
R0(D,A,B,C,X( 5),12,0x4787c62aL); HOST_c2l(data,l); X( 7)=l;
R0(C,D,A,B,X( 6),17,0xa8304613L); HOST_c2l(data,l); X( 8)=l;
R0(B,C,D,A,X( 7),22,0xfd469501L); HOST_c2l(data,l); X( 9)=l;
R0(A,B,C,D,X( 8), 7,0x698098d8L); HOST_c2l(data,l); X(10)=l;
R0(D,A,B,C,X( 9),12,0x8b44f7afL); HOST_c2l(data,l); X(11)=l;
R0(C,D,A,B,X(10),17,0xffff5bb1L); HOST_c2l(data,l); X(12)=l;
R0(B,C,D,A,X(11),22,0x895cd7beL); HOST_c2l(data,l); X(13)=l;
R0(A,B,C,D,X(12), 7,0x6b901122L); HOST_c2l(data,l); X(14)=l;
R0(D,A,B,C,X(13),12,0xfd987193L); HOST_c2l(data,l); X(15)=l;
R0(C,D,A,B,X(14),17,0xa679438eL);
R0(B,C,D,A,X(15),22,0x49b40821L);
/* Round 1 */
R1(A,B,C,D,X( 1), 5,0xf61e2562L);
R1(D,A,B,C,X( 6), 9,0xc040b340L);
R1(C,D,A,B,X(11),14,0x265e5a51L);
R1(B,C,D,A,X( 0),20,0xe9b6c7aaL);
R1(A,B,C,D,X( 5), 5,0xd62f105dL);
R1(D,A,B,C,X(10), 9,0x02441453L);
R1(C,D,A,B,X(15),14,0xd8a1e681L);
R1(B,C,D,A,X( 4),20,0xe7d3fbc8L);
R1(A,B,C,D,X( 9), 5,0x21e1cde6L);
R1(D,A,B,C,X(14), 9,0xc33707d6L);
R1(C,D,A,B,X( 3),14,0xf4d50d87L);
R1(B,C,D,A,X( 8),20,0x455a14edL);
R1(A,B,C,D,X(13), 5,0xa9e3e905L);
R1(D,A,B,C,X( 2), 9,0xfcefa3f8L);
R1(C,D,A,B,X( 7),14,0x676f02d9L);
R1(B,C,D,A,X(12),20,0x8d2a4c8aL);
/* Round 2 */
R2(A,B,C,D,X( 5), 4,0xfffa3942L);
R2(D,A,B,C,X( 8),11,0x8771f681L);
R2(C,D,A,B,X(11),16,0x6d9d6122L);
R2(B,C,D,A,X(14),23,0xfde5380cL);
R2(A,B,C,D,X( 1), 4,0xa4beea44L);
R2(D,A,B,C,X( 4),11,0x4bdecfa9L);
R2(C,D,A,B,X( 7),16,0xf6bb4b60L);
R2(B,C,D,A,X(10),23,0xbebfbc70L);
R2(A,B,C,D,X(13), 4,0x289b7ec6L);
R2(D,A,B,C,X( 0),11,0xeaa127faL);
R2(C,D,A,B,X( 3),16,0xd4ef3085L);
R2(B,C,D,A,X( 6),23,0x04881d05L);
R2(A,B,C,D,X( 9), 4,0xd9d4d039L);
R2(D,A,B,C,X(12),11,0xe6db99e5L);
R2(C,D,A,B,X(15),16,0x1fa27cf8L);
R2(B,C,D,A,X( 2),23,0xc4ac5665L);
/* Round 3 */
R3(A,B,C,D,X( 0), 6,0xf4292244L);
R3(D,A,B,C,X( 7),10,0x432aff97L);
R3(C,D,A,B,X(14),15,0xab9423a7L);
R3(B,C,D,A,X( 5),21,0xfc93a039L);
R3(A,B,C,D,X(12), 6,0x655b59c3L);
R3(D,A,B,C,X( 3),10,0x8f0ccc92L);
R3(C,D,A,B,X(10),15,0xffeff47dL);
R3(B,C,D,A,X( 1),21,0x85845dd1L);
R3(A,B,C,D,X( 8), 6,0x6fa87e4fL);
R3(D,A,B,C,X(15),10,0xfe2ce6e0L);
R3(C,D,A,B,X( 6),15,0xa3014314L);
R3(B,C,D,A,X(13),21,0x4e0811a1L);
R3(A,B,C,D,X( 4), 6,0xf7537e82L);
R3(D,A,B,C,X(11),10,0xbd3af235L);
R3(C,D,A,B,X( 2),15,0x2ad7d2bbL);
R3(B,C,D,A,X( 9),21,0xeb86d391L);
A = c->A += A;
B = c->B += B;
C = c->C += C;
D = c->D += D;
}
}
// 传入需要哈希的明文,支持多次调用
int MD5_Update(MD5_CTX *c, const void *data_, size_t len){
const unsigned char *data= static_cast<const unsigned char *>(data_);
unsigned char *p;
MD5_LONG l;
size_t n;
if (len==0) return 1;
// 低位
l=(c->Nl+(((MD5_LONG)len)<<3))&0xffffffffUL;
if (l < c->Nl)
c->Nh++;
// 高位
c->Nh+=(MD5_LONG)(len>>29);
c->Nl=l;
n = c->num;
if (n != 0){
p=(unsigned char *)c->data;
if (len >= MD5_CBLOCK || len+n >= MD5_CBLOCK){
memcpy (p+n,data,MD5_CBLOCK-n);
md5_block_data_order(c,p,1);
n = MD5_CBLOCK-n;
data += n;
len -= n;
c->num = 0;
memset (p,0,MD5_CBLOCK);
}else{
memcpy (p+n,data,len);
c->num += (unsigned int)len;
return 1;
}
}
n = len/MD5_CBLOCK;
if (n > 0){
md5_block_data_order(c,data,n);
n *= MD5_CBLOCK;
data += n;
len -= n;
}
if (len != 0){
p = (unsigned char *)c->data;
c->num = (unsigned int)len;
memcpy (p,data,len);
}
return 1;
}
// 得出最终结果
int MD5_Final(unsigned char *md, MD5_CTX *c){
unsigned char *p = (unsigned char *)c->data;
size_t n = c->num;
p[n] = 0x80; /* there is always room for one */
n++;
if (n > (MD5_CBLOCK-8)){
memset (p+n,0,MD5_CBLOCK-n);
n=0;
md5_block_data_order(c,p,1);
}
memset (p+n,0,MD5_CBLOCK-8-n);
p += MD5_CBLOCK-8;
#if defined(DATA_ORDER_IS_BIG_ENDIAN)
(void)HOST_l2c(c->Nh,p);
(void)HOST_l2c(c->Nl,p);
#elif defined(DATA_ORDER_IS_LITTLE_ENDIAN)
(void)HOST_l2c(c->Nl,p);
(void)HOST_l2c(c->Nh,p);
#endif
p -= MD5_CBLOCK;
md5_block_data_order(c,p,1);
c->num=0;
memset (p,0,MD5_CBLOCK);
#ifndef HASH_MAKE_STRING
#error "HASH_MAKE_STRING must be defined!"
#else
HASH_MAKE_STRING(c,md);
#endif
return 1;
}
//清除加载的各种算法,包括对称算法、摘要算法以及 PBE 算法,并清除这些算法相关的哈希表的内容。
void OPENSSL_cleanse(void *ptr, size_t len){
unsigned char *p = static_cast<unsigned char *>(ptr);
size_t loop = len, ctr = cleanse_ctr;
while(loop--){
*(p++) = (unsigned char)ctr;
ctr += (17 + ((size_t)p & 0xF));
}
p= static_cast<unsigned char *>(memchr(ptr, (unsigned char) ctr, len));
if(p)
ctr += (63 + (size_t)p);
cleanse_ctr = (unsigned char)ctr;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_unidbg_demo2_MainActivity_setNativeAssetManager(JNIEnv *env, jobject thiz,
jobject asset_manager) {
AAssetManager *nativeasset = AAssetManager_fromJava(env, asset_manager);
AAsset *assetFile = AAssetManager_open(nativeasset, "1.png", AASSET_MODE_BUFFER);
size_t fileLength = AAsset_getLength(assetFile);
char *dataBuffer = (char *) malloc(fileLength);
//read file data
AAsset_read(assetFile, dataBuffer, fileLength);
//the data has been copied to dataBuffer2, so , close it
AAsset_close(assetFile);
// 初始化MD5的上下文结构体
MD5_CTX context = {0};
MD5_Init(&context);
// 传入待处理的内容以及内容的长度
MD5_Update(&context, dataBuffer, fileLength);
// 收尾和输出
// 输出的缓冲区
unsigned char dest[16] = {0};
MD5_Final(dest, &context);
// 结果转成十六进制字符串
int i = 0;
char szMd5[33] = {0};
for(i=0; i<16; i++){
sprintf(szMd5, "%s%02x", szMd5, dest[i]);
}
//free malloc
free(dataBuffer);
// 传回Java世界
return env->NewStringUTF(szMd5);
}- 需要创建一个assets目录,在main文件夹下右键即可创建;

- 将图片放进去即可,运行应用;

- 得到目标图片的md5值,我们的目标就是模拟执行它;
5. 模拟执行demo2
- 基本的框架如下:
package com.baiLong.csdn_demo8;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.file.ByteArrayFileIO;
import com.github.unidbg.memory.Memory;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class csdn_demo8_2 extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public csdn_demo8_2() {
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("src/test/java/com/baiLong/csdn_demo8/file/demo2.apk"));
vm.setVerbose(true);
vm.setJni(this);
DalvikModule dm = vm.loadLibrary(new File("src/test/java/com/baiLong/csdn_demo8/file/libdemo2.so"), false);
module = dm.getModule();
emulator.traceCode();
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.unix.UnixSyscallHandler").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG);
csdn_demo8_2 demo = new csdn_demo8_2();
String res = demo.call();
System.out.println("res-->>" + res);
}
public String call() {
// 1D30
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
DvmObject<?> assetManager = vm.resolveClass("android/content/res/AssetManager").newObject(null);// context
list.add(vm.addLocalObject(assetManager));
Number number = module.callFunction(emulator, 0x1D30 + 1, list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
}- 速速执行;

- 不出所料报大错了,我们去报错的位置看看,启动ida;

- AAssetManager_fromJava函数哪来的?为什么报错?这需要我们思考两个问题;
- 首先来回忆一下我们做了什么,在native层读取资源文件不比demo1的简单,demo1直接open系统调用就行;
- 我们去查一下这个 AAssetManager_fromJava 函数的相关信息,它的作用不难分析,与资源文件有关,它是由libandroid.so这个系统SO实现,但是我们并没有加载这个so;不过好在,可以使用虚拟模块实现;
// 添加这个就可以
new AndroidModule(emulator,vm).register(memory);
// 添加这个就可以- 在模拟执行的so加载前注册这个虚拟模块即可;

非常简单,直接就执行出了结果;但这里可以引出几个问题;
- 为什么Unidbg不内置支持所有系统SO的加载?
- 如果一个SO的依赖SO里包含Unidbg尚未支持的系统SO,那该怎么办?
- unidbg出现一个我不认识的报错,我应该怎样去溯源问题?
- 先讨论第一个问题,一部分原因是大部分SO中主要的依赖项,就是Unidbg已经支持的这些,即已经够用了;把Android系统中全部SO都成功加载进Unidbg虚拟内存中,既是很大的工作量,又会占用过多内存;

- 另一个更主要的原因是,比如libandroid.so,其依赖SO实在太多了,想顺利加载整个SO确确实实是个苦差事;
- 再看第二个问题,如果SO的依赖项中有Unidbg不支持的系统SO,怎么办?
- 首先,Unidbg会给予提示,其实在最开始的日志就会有体现,只是容易被忽略;其次,尽管SO加载了Unidbg不支持的SO,但有可能我们的目标函数并没有使用到这个系统SO,这种情况下就不用理会,当作不存在就行;
如果目标用到了这个so,就有两种处理办法:
- Patch/Hook 这个不支持的SO所使用的函数;
- 使用Unidbg VirtualModule;
- 第一个很好理解,第二种方法就是我们前面使用的方式;它本质上也是Hook,只不过实现了SO中少数几个函数罢了;

- 碰巧有我们的目标,所以执行也就顺理成章;再看问题三,unidbg出现一个我不认识的报错,我应该怎样去溯源问题?
- 我们的例子就很说明了这个问题,若有不理解的报错,打开tracecode看看哪里停下来,基本上都会找到对应的问题所在,再根据具体问题去patch或者hook;
6. 总结
- 致谢:文章参考于白龙大佬分享在csdn的优秀文章,我仅做复现并记录下此文章;文章中所有观点均来自于原文;
- By:下雨天 2025.10.17;
- 逆向交流+vx:HeiYuKuaiDou23;