安卓逆向-unidbg文件读写

下雨天
10月17日发布 /正在检测是否收录...

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方法了,先不着急,看看是否有目标的文件存在了;

image-20251017145810507

  • 我们的目标就是来读取它俩,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"
    }
  • 最后读取到的结果就是这样的:

image-20251017150923316

  • 为了省事就不去切割第二部分字符串了,直接开始后模拟执行;

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");
        }
    }
}
  • 这才是正确的做法,继续运行,发现出值了;

image-20251017154813164

  • 但是有个点出现了差错,我们不是还有一个文件需要读吗,为何直接完成了执行,并且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);
    }
  • 在日常的样本里推荐打开全部日志,避免有所遗漏,但也会有弊端,日志会比较多;

image-20251017160652130

  • 我们需要补文件访问了,这里有两种方式补文件访问,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处理有三步,请不要忘记了,继续执行;

image-20251017161124882

  • 没问题了,这里想要和真机一样也是可以的,但是没必要;

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文件夹下右键即可创建;

image-20251017162755643

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

image-20251017162921101

  • 得到目标图片的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;
    }
}
  • 速速执行;

image-20251017164500110

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

image-20251017164541498

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

image-20251017165412823

  • 非常简单,直接就执行出了结果;但这里可以引出几个问题;

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

image-20251017170433405

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

    • Patch/Hook 这个不支持的SO所使用的函数;
    • 使用Unidbg VirtualModule;
  • 第一个很好理解,第二种方法就是我们前面使用的方式;它本质上也是Hook,只不过实现了SO中少数几个函数罢了;

image-20251017170748217

  • 碰巧有我们的目标,所以执行也就顺理成章;再看问题三,unidbg出现一个我不认识的报错,我应该怎样去溯源问题?
  • 我们的例子就很说明了这个问题,若有不理解的报错,打开tracecode看看哪里停下来,基本上都会找到对应的问题所在,再根据具体问题去patch或者hook;

6. 总结

  • 致谢:文章参考于白龙大佬分享在csdn的优秀文章,我仅做复现并记录下此文章;文章中所有观点均来自于原文;
  • By:下雨天 2025.10.17;
  • 逆向交流+vx:HeiYuKuaiDou23;
© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
OωO
取消