进入Unidbg的世界

概述

Unidbg是一个基于Unicorn的逆向工具,可以黑盒调用Android和iOS中的so文件.这使逆向人员无须了解so内部算法原理,只需主动调用so中的函数,传入所需的参数,补全运行所需的环境,即可得到需要的结果.

对于Android逆向来说,Unidbg有以下几个特点:

  • 模拟JNI调用的API,因而可以调用JNI_OnLoad函数
  • 支持JavaVM和JNIEnv
  • 支持模拟系统调用指令
  • 支持ARM32和ARM64
  • 支持基于Dobby的Inline Hook
  • 支持基于xHook的GOT Hook
  • Unicorn后端支持简单的控制台调试器、GDB Stub、指令跟踪和内存读写跟踪
  • 支持Dynarmic

其官方地址如下:

https://github.com/zhkl0228/unidbg

模拟执行初探

测试Apk

java层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.example.luodst;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.example.luodst.databinding.ActivityMainBinding;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'luodst' library on application startup.
    static {
        System.loadLibrary("luodst");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        TextView tv = binding.sampleText;
        findViewById(R.id.btn_md5).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv.setText(md5("XiaLuoHun"));
            }
        });

    }

    public native String md5(String data);
}

Native层:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <jni.h>
#include <string>

void md5(const uint8_t *initial_msg, size_t initial_len, uint8_t *digest);

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_luodst_MainActivity_md5(JNIEnv *env, jobject thiz, jstring data) {
    char *msg = const_cast<char *>(env->GetStringUTFChars(data, 0));
    int len = strlen(msg);
    uint8_t result[16];
    for (int i = 0; i < 1000000; i++) {
        md5((uint8_t*)msg, len, result);
    }
    char  res[32] = {0};
    for (int i = 0; i < 16; i++)
        sprintf(res+i*2, "%2.2x", result[i]);

    return env->NewStringUTF(res);
}

// Constants are the integer part of the sines of integers (in radians) * 2^32.
const uint32_t k[64] = {
        0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee ,
        0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501 ,
        0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be ,
        0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821 ,
        0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa ,
        0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8 ,
        0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed ,
        0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a ,
        0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c ,
        0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70 ,
        0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05 ,
        0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665 ,
        0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039 ,
        0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1 ,
        0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1 ,
        0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 };

// r specifies the per-round shift amounts
const uint32_t r[] = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
                      5,  9, 14, 20, 5,  9, 14, 20, 5,  9, 14, 20, 5,  9, 14, 20,
                      4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
                      6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21};

// leftrotate function definition
#define LEFTROTATE(x, c) (((x) << (c)) | ((x) >> (32 - (c))))

void to_bytes(uint32_t val, uint8_t *bytes)
{
    bytes[0] = (uint8_t) val;
    bytes[1] = (uint8_t) (val >> 8);
    bytes[2] = (uint8_t) (val >> 16);
    bytes[3] = (uint8_t) (val >> 24);
}

uint32_t to_int32(const uint8_t *bytes)
{
    return (uint32_t) bytes[0]
           | ((uint32_t) bytes[1] << 8)
           | ((uint32_t) bytes[2] << 16)
           | ((uint32_t) bytes[3] << 24);
}

void md5(const uint8_t *initial_msg, size_t initial_len, uint8_t *digest) {

    // These vars will contain the hash
    uint32_t h0, h1, h2, h3;

    // Message (to prepare)
    uint8_t *msg = NULL;

    size_t new_len, offset;
    uint32_t w[16];
    uint32_t a, b, c, d, i, f, g, temp;

    // Initialize variables - simple count in nibbles:
    h0 = 0x67452301;
    h1 = 0xefcdab89;
    h2 = 0x98badcfe;
    h3 = 0x10325476;

    //Pre-processing:
    //append "1" bit to message
    //append "0" bits until message length in bits ≡ 448 (mod 512)
    //append length mod (2^64) to message

    for (new_len = initial_len + 1; new_len % (512/8) != 448/8; new_len++)
        ;

    msg = (uint8_t*)malloc(new_len + 8);
    memcpy(msg, initial_msg, initial_len);
    msg[initial_len] = 0x80; // append the "1" bit; most significant bit is "first"
    for (offset = initial_len + 1; offset < new_len; offset++)
        msg[offset] = 0; // append "0" bits

    // append the len in bits at the end of the buffer.
    to_bytes(initial_len*8, msg + new_len);
    // initial_len>>29 == initial_len*8>>32, but avoids overflow.
    to_bytes(initial_len>>29, msg + new_len + 4);

    // Process the message in successive 512-bit chunks:
    //for each 512-bit chunk of message:
    for(offset=0; offset<new_len; offset += (512/8)) {

        // break chunk into sixteen 32-bit words w[j], 0 ≤ j ≤ 15
        for (i = 0; i < 16; i++)
            w[i] = to_int32(msg + offset + i*4);

        // Initialize hash value for this chunk:
        a = h0;
        b = h1;
        c = h2;
        d = h3;

        // Main loop:
        for(i = 0; i<64; i++) {

            if (i < 16) {
                f = (b & c) | ((~b) & d);
                g = i;
            } else if (i < 32) {
                f = (d & b) | ((~d) & c);
                g = (5*i + 1) % 16;
            } else if (i < 48) {
                f = b ^ c ^ d;
                g = (3*i + 5) % 16;
            } else {
                f = c ^ (b | (~d));
                g = (7*i) % 16;
            }

            temp = d;
            d = c;
            c = b;
            b = b + LEFTROTATE((a + f + k[i] + w[g]), r[i]);
            a = temp;

        }

        // Add this chunk's hash to result so far:
        h0 += a;
        h1 += b;
        h2 += c;
        h3 += d;

    }

    // cleanup
    free(msg);

    //var char digest[16] := h0 append h1 append h2 append h3 //(Output is in little-endian)
    to_bytes(h0, digest);
    to_bytes(h1, digest + 4);
    to_bytes(h2, digest + 8);
    to_bytes(h3, digest + 12);
}

符号调用

Unidbg支持两种调用so中函数的方式:符号调用和地址调用.

符号调用的示例代码如下:

1
2
3
4
5
6
7
8
private String call_md5() {
  //执行JNI方法
  DvmObject obj = ProxyDvmObject.createObject(vm,this);
  //DvmObject obj = vm.resolveClass("com.example.luodst.MainActivity").newObject(null);
  DvmObject dvmObject = obj.callJniMethodObject(emulator, "md5(Ljava/lang/String;)Ljava/lang/String;", "XiaLuoHun");
  String retval = (String) dvmObject.getValue();
  return retval;
}

若使用ProxyDvmObject创建调用so的代理对象obj,需保证在Unidbg中新建的测试类和so中的Native方法所在的类一致.

md5方法的返回值为String类型,故应使用callJniMethodObject()方法来调用md5方法,同时使用DvmObject对象来接收该返回值.除了基本类型之外,其余的类型都要返回DvmObject对象.

地址调用

地址调用的示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private String call_addr() {
  //构造函数参数
  Pointer jniEnv = vm.getJNIEnv();
  DvmObject obj = ProxyDvmObject.createObject(vm,this);
  StringObject data = new StringObject(vm, "XiaLuoHun");

  //构造为参数列表
  List<Object> args = new ArrayList<>();
  args.add(jniEnv);
  args.add(vm.addLocalObject(obj));
  args.add(vm.addLocalObject(data));

  //调用so函数 md5函数在so中的偏移为0x1C68
  Number number = module.callFunction(emulator, 0x1C68, args.toArray());
  //根据Number中存储的对象hash取得DvmObject对象
  DvmObject<?> object = vm.getObject(number.intValue());
  String value = (String) object.getValue();
  return  value;
}

在使用符号调用方式时,Unidbg帮助我们完成了很多操作,例如拼接函数名、填充参数等.但在使用地址调用方式时,这些操作需要我们自己来完成.

上述在构造第三个参数需要注意的是,在Unidbg中,当传入参数为非指针和Number类型时,需要将其定义为DvmObject对象并添加到VM中.而对于常用的String类,Unidbg对其做了相应的封装,这里新建了一个StringObject对象来作为第三个参数.

上述在调用so中的函数后获得的返回值为Number类型,这与实际的返回值String类型不相符.在Unidbg中对于基本类型或者布尔类型,可以直接通过Number获取相应的值.而对于Object对象来说,Number中实际存储的是Object对象的hash值,因此需要先通过vm.getObject()方法来获取DvmObject对象,之后即可通过getValue()方法来获取对象中储存的值,并将其强制转换为String类型后输出.

模拟执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.example.luodst;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.DynarmicFactory;
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.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.sun.jna.Pointer;

import java.awt.*;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AbstractJni {

    public static void main(String[] args) {
        MainActivity mainActivity = new MainActivity();
        String result = mainActivity.call_addr();
        System.out.println(result);
    }

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private MainActivity() {
        //1.创建Android模拟器实例
        emulator = AndroidEmulatorBuilder
                .for64Bit()
                .addBackendFactory(new DynarmicFactory(true))
                .build();

        //2.获取操作内存的接口
        Memory memory = emulator.getMemory();
        //3.设置Android SDK 版本
        LibraryResolver resolver = new AndroidResolver(23);
        memory.setLibraryResolver(resolver);
        //4.创建虚拟机
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/example/luodst/files/app-debug.apk"));
        //5.是否打印日志
        vm.setVerbose(false);
        //6.设置jni
        vm.setJni(this);
        new AndroidModule(emulator,vm).register(memory);
        //7.加载目标so文件
        DalvikModule dm = vm.loadLibrary("luodst", true);
        //8.将so文件对应的Module存入成员变量
        module = dm.getModule();
        //9.调用JNI_OnLoad
        dm.callJNI_OnLoad(emulator);
    }

    private String call_md5() {
        //执行JNI方法
        DvmObject obj = ProxyDvmObject.createObject(vm,this);
        //DvmObject obj = vm.resolveClass("com.example.luodst.MainActivity").newObject(null);
        DvmObject dvmObject = obj.callJniMethodObject(emulator, "md5(Ljava/lang/String;)Ljava/lang/String;", "XiaLuoHun");
        String retval = (String) dvmObject.getValue();
        return retval;
    }

    private String call_addr() {
        //构造函数参数
        Pointer jniEnv = vm.getJNIEnv();
        DvmObject obj = ProxyDvmObject.createObject(vm,this);
        StringObject data = new StringObject(vm, "XiaLuoHun");

        //构造为参数列表
        List<Object> args = new ArrayList<>();
        args.add(jniEnv);
        args.add(vm.addLocalObject(obj));
        args.add(vm.addLocalObject(data));

        //调用so函数 md5函数在so中的偏移为0x1C68
        Number number = module.callFunction(emulator, 0x1C68, args.toArray());
        //根据Number中存储的对象hash取得DvmObject对象
        DvmObject<?> object = vm.getObject(number.intValue());
        String value = (String) object.getValue();
        return  value;
    }
}

修补执行环境

测试Apk

java层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package com.example.luodst;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.example.luodst.databinding.ActivityMainBinding;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'luodst' library on application startup.
    static {
        System.loadLibrary("luodst");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        TextView tv = binding.sampleText;
        findViewById(R.id.btn_md5).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv.setText(md52("XiaLuoHun".getBytes()));
            }
        });

    }

    private String md5Java(byte[] data) {
        try {
            MessageDigest digest = MessageDigest.getInstance("md5");
            digest.update(data);
            byte[] digest1 = digest.digest();
            return byte2Hex(digest1);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String byte2Hex(byte[] data){
        StringBuilder sb = new StringBuilder();
        for (byte b : data){
            String s = Integer.toHexString(b & 0xFF);
            if (s.length() < 2){
                sb.append("0");
            }
            sb.append(s);
        }
        return sb.toString();
    }

    public native String md52(byte[] data);
}

Native层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_luodst_MainActivity_md52(JNIEnv *env, jobject thiz, jbyteArray data) {
    jclass MessageDigest = env->FindClass("java/security/MessageDigest");
    jmethodID getInstance = env->GetStaticMethodID(MessageDigest,"getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;");
    jobject digest = env->CallStaticObjectMethod(MessageDigest,getInstance,env->NewStringUTF("md5"));

    jmethodID update = env->GetMethodID(MessageDigest, "update", "([B)V");
    env->CallVoidMethod(digest,update,data);

    jmethodID dig = env->GetMethodID(MessageDigest,"digest", "()[B");
    jobject result = env->CallObjectMethod(digest,dig);

    jclass MainActivity = env->FindClass("com/example/luodst/MainActivity");
    jmethodID bytes2Hex = env->GetMethodID(MainActivity,"byte2Hex","([B)Ljava/lang/String;");
    jobject string_result = env->CallObjectMethod(thiz,bytes2Hex,result);
    return static_cast<jstring>(string_result);
}

模拟执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.example.luodst;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.DynarmicFactory;
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.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;

import java.io.File;

public class MainActivity extends AbstractJni {

    public static void main(String[] args) {
        MainActivity mainActivity = new MainActivity();
        String result = mainActivity.call_md5();
        System.out.println(result);
    }

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private MainActivity() {
        //1.创建Android模拟器实例
        emulator = AndroidEmulatorBuilder
                .for64Bit()
                .addBackendFactory(new DynarmicFactory(true))
                .build();

        //2.获取操作内存的接口
        Memory memory = emulator.getMemory();
        //3.设置Android SDK 版本
        LibraryResolver resolver = new AndroidResolver(23);
        memory.setLibraryResolver(resolver);
        //4.创建虚拟机
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/example/luodst/files/app-debug.apk"));
        //5.是否打印日志
        vm.setVerbose(false);
        //6.设置jni
        //vm.setJni(this);
        new AndroidModule(emulator, vm).register(memory);
        //7.加载目标so文件
        DalvikModule dm = vm.loadLibrary("luodst", true);
        //8.将so文件对应的Module存入成员变量
        module = dm.getModule();
        //9.调用JNI_OnLoad
        dm.callJNI_OnLoad(emulator);
    }

    private String call_md5() {
        //执行JNI方法
        DvmObject obj = ProxyDvmObject.createObject(vm, this);
        //DvmObject obj = vm.resolveClass("com.example.luodst.MainActivity").newObject(null);
        DvmObject dvmObject = obj.callJniMethodObject(emulator, "md52([B)Ljava/lang/String;", "XiaLuoHun".getBytes());
        String retval = (String) dvmObject.getValue();
        return retval;
    }

}

使用上述Unidbg代码进行模拟调用时,会报下述错误.

1
2
3
4
5
java.lang.IllegalStateException: Please vm.setJni(jni)
	at com.github.unidbg.linux.android.dvm.Hashable.checkJni(Hashable.java:8)
	at com.github.unidbg.linux.android.dvm.DvmClass.getStaticMethodID(DvmClass.java:101)
	at com.github.unidbg.linux.android.dvm.DalvikVM64$110.handle(DalvikVM64.java:1787)
	at com.github.unidbg.linux.ARM64SyscallHandler.hook(ARM64SyscallHandler.java:121)

由于模拟执行的函数中有JNI操作,而不同的so调用的JNI方法有所不同,Unidbg无法为其一一实现,缺失的部分需要我们自己来实现,因此上述提示我们需要设置vm.setJni()来实现缺失的JNI接口部分.

当我们调用了vm.setJni()方法时,再次运行,会报下述错误.

1
2
3
4
5
java.lang.UnsupportedOperationException: java/security/MessageDigest->update([B)V
	at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:990)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callVoidMethodV(DvmMethod.java:229)
	at com.github.unidbg.linux.android.dvm.DalvikVM64$59.handle(DalvikVM64.java:1078)

我们需要重写报错的callVoidMethodV方法,为Unidbg补全缺失的update()方法的具体实现,使之可以继续执行.这个补齐JNI的过程叫做"补环境".

具体的修补代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void callVoidMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
  switch (signature) {
      case "java/security/MessageDigest->update([B)V": {
          MessageDigest messageDigest = (MessageDigest) dvmObject.getValue();
          int intArg = vaList.getIntArg(0);
          Object obj = vm.getObject(intArg).getValue();
          messageDigest.update((byte[]) obj);
          return;
      }
  }
  super.callVoidMethodV(vm, dvmObject, signature, vaList);
}

当执行到上述方法时,参数vm为虚拟机,dvmObject为调用该函数的对象,signature为函数的签名,vaList为函数的参数列表.

在执行messageDigest.update()方法时,我们需要先取得messageDigest对象,dvmObject是调用该方法的对象,因此可以通过dvmObject.getValue()方法来获取messageDigest对象.

update()方法的参数为byte[],而在JNI中没有对象的概念.因此Unidbg为非基本类型维护了一个Map引用,通过getIntArg()方法取得Map中的key值,然后通过getObject()方法从VM虚拟机中取得该对象,最后通过getValue()方法取得实际对象的值.

修补完update()方法后继续运行,会出现下述错误.

1
2
3
4
5
java.lang.UnsupportedOperationException: java/security/MessageDigest->digest()[B
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)
	at com.github.unidbg.linux.android.dvm.DalvikVM64$32.handle(DalvikVM64.java:559)

具体的修补代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
  switch (signature) {
      case "java/security/MessageDigest->digest()[B":{
          MessageDigest messageDigest = (MessageDigest) dvmObject.getValue();
          byte[] digest = messageDigest.digest();
          DvmObject<?> object = ProxyDvmObject.createObject(vm, digest);
          vm.addLocalObject(object);
          return object;
      }
  }
  return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

步骤和上述大致相同,多了一个将运算得到的结果digest先代理创建为DvmObject对象,然后在添加到VM虚拟机的操作.

所有的非基本类型、包装类型都需要添加到VM虚拟机的Map映射中,否则在JNI中无法找到该引用.最后将运算得到的结果当做函数返回值返回.

上述修补完后,再次运行会报下述错误.

1
2
3
4
5
java.lang.UnsupportedOperationException: com/example/luodst/MainActivity->byte2Hex([B)Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.example.luodst.MainActivity.callObjectMethodV(MainActivity.java:88)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

具体的修补代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
  switch (signature) {
      case "java/security/MessageDigest->digest()[B":{
          MessageDigest messageDigest = (MessageDigest) dvmObject.getValue();
          byte[] digest = messageDigest.digest();
          DvmObject<?> object = ProxyDvmObject.createObject(vm, digest);
          vm.addLocalObject(object);
          return object;
      }
      case "com/example/luodst/MainActivity->byte2Hex([B)Ljava/lang/String;":{
          int intArg = vaList.getIntArg(0);
          Object object = vm.getObject(intArg).getValue();
          String s = byte2Hex((byte[]) object);
          StringObject stringObject = new StringObject(vm, s);
          vm.addLocalObject(stringObject);
          return stringObject;
      }
  }
  return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

public String byte2Hex(byte[] data){
  StringBuilder sb = new StringBuilder();
  for (byte b : data){
      String s = Integer.toHexString(b & 0xFF);
      if (s.length() < 2){
          sb.append("0");
      }
      sb.append(s);
  }
  return sb.toString();
}

再次运行,成功输出结果如下:

1
0fd61e6c11dccc2fbf41b630b4e7d339

Unidbg Hook

Unidbg支持多种Hook框架,如HookZz、Dobby、xHook、whale等.下面我们使用HookZz来完成对64位so程序的Hook

测试Apk

java层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.luodst;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import com.example.luodst.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'luodst' library on application startup.
    static {
        System.loadLibrary("luodst");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        TextView tv = binding.sampleText;
        findViewById(R.id.btn_md5).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv.setText(String.valueOf(add(10, 5)));
            }
        });

    }
    public native int add(int a, int b);
}

Native层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_luodst_MainActivity_add(JNIEnv *env, jobject thiz, jint a, jint b) {
    if(a < 0){
        a = -a;
    }
    if(b < 0){
        b = -b;
    }
    return a + b;
}

HookZz

HookZz的wrap方法只是在函数的头、尾来添加并执行额外的逻辑,并不影响函数原来的代码.示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
HookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.base + 0x1648, new WrapCallback<HookZzArm64RegisterContextImpl>() {
    @Override
    //在函数执行之前执行的回调函数
    public void preCall(Emulator<?> emulator, HookZzArm64RegisterContextImpl ctx, HookEntryInfo info) {
        System.out.println(String.format("X2: %d, X3: %d",ctx.getIntArg(2),ctx.getIntArg(3)));
        //将值保存到ctx中,方便在postCall方法中获取
        ctx.push(1000);
    }
    @Override
    //在函数执行之后执行的回调函数
    public void postCall(Emulator<?> emulator, HookZzArm64RegisterContextImpl ctx, HookEntryInfo info) {
        //从ctx中取得保存的值
        int arg = ctx.pop();
        ctx.setXLong(0, arg);
        super.postCall(emulator, ctx, info);
    }
});

HookZz的replace是替换掉函数,让函数不执行.当第三个参数为true时,表示执行原函数,此时类似于wrap方法.示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
HookZz hook = HookZz.getInstance(emulator);
hook.replace(module.base + 0x1648, new ReplaceCallback() {
    @Override
    public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
        System.out.println(String.format("Before X2: %d, X3: %d",context.getIntArg(2),context.getIntArg(3)));
        emulator.getBackend().reg_write(Unicorn.UC_ARM64_REG_X2,5);
        System.out.println(String.format("After  X2: %d, X3: %d",context.getIntArg(2),context.getIntArg(3)));
        emulator.getBackend().reg_write(Unicorn.UC_ARM64_REG_X0,1000);
        //如果不想执行原函数,可直接返回LR寄存器中的地址
        return HookStatus.RET(emulator, context.getLR());
        //return super.onCall(emulator, context, originFunction);
    }
    @Override
    public void postCall(Emulator<?> emulator, HookContext context) {
        super.postCall(emulator, context);
    }
}, false);

onCall()方法是在函数执行之前执行,postCall()方法是在函数执行结束后执行.postCall()方法默认是不执行的,需要配置第三个参数enablePostCall为true来启用.

Unidbg Hook完整代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.example.luodst;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.*;
import com.github.unidbg.hook.xhook.IxHook;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.XHookImpl;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import unicorn.Unicorn;

import java.io.File;

public class MainActivity extends AbstractJni {

    public static void main(String[] args) {
        MainActivity mainActivity = new MainActivity();
        mainActivity.hook();
        mainActivity.call_func();
    }

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private MainActivity() {
        //1.创建Android模拟器实例
        emulator = AndroidEmulatorBuilder
                .for64Bit()
                .addBackendFactory(new DynarmicFactory(true))
                .build();

        //2.获取操作内存的接口
        Memory memory = emulator.getMemory();
        //3.设置Android SDK 版本
        LibraryResolver resolver = new AndroidResolver(23);
        memory.setLibraryResolver(resolver);
        //4.创建虚拟机
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/example/luodst/files/app-debug.apk"));
        //5.是否打印日志
        vm.setVerbose(false);
        //6.设置jni
        vm.setJni(this);
        new AndroidModule(emulator, vm).register(memory);
        //7.加载目标so文件
        DalvikModule dm = vm.loadLibrary("luodst", true);
        //8.将so文件对应的Module存入成员变量
        module = dm.getModule();
        //9.调用JNI_OnLoad
        dm.callJNI_OnLoad(emulator);
    }

    private void call_func() {
        //执行JNI方法
        DvmObject obj = ProxyDvmObject.createObject(vm, this);
        //DvmObject obj = vm.resolveClass("com.example.luodst.MainActivity").newObject(null);
        int nRet = obj.callJniMethodInt(emulator, "add(II)I", 10, 5);
        System.out.println(nRet);
    }
    public void hook(){
        HookZz hookZz = HookZz.getInstance(emulator);
        hookZz.wrap(module.base + 0x1648, new WrapCallback<HookZzArm64RegisterContextImpl>() {
            @Override
            //在函数执行之前执行的回调函数
            public void preCall(Emulator<?> emulator, HookZzArm64RegisterContextImpl ctx, HookEntryInfo info) {
                System.out.println(String.format("X2: %d, X3: %d",ctx.getIntArg(2),ctx.getIntArg(3)));
                //将值保存到ctx中,方便在postCall方法中获取
                ctx.push(1000);
            }
            @Override
            //在函数执行之后执行的回调函数
            public void postCall(Emulator<?> emulator, HookZzArm64RegisterContextImpl ctx, HookEntryInfo info) {
                //从ctx中取得保存的值
                int arg = ctx.pop();
                ctx.setXLong(0, arg);
                super.postCall(emulator, ctx, info);
            }
        });
    }
}

Dobby

Dobby的各个方法与接口和HookZz差不多.示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Dobby dobby = Dobby.getInstance(emulator);
dobby.replace(module.base + 0x1648, new ReplaceCallback() {
    @Override
    public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
        emulator.getBackend().reg_write(Unicorn.UC_ARM64_REG_X0,1000);
        //如果不想执行原函数,可直接返回LR寄存器中的地址
        return HookStatus.RET(emulator, context.getLR());
        //return super.onCall(emulator, context, originFunction);
    }
    @Override
    public void postCall(Emulator<?> emulator, HookContext context) {
        super.postCall(emulator, context);
    }
}, false);

xHook

xHook仅能实现符号表的Hook,优点是稳定.示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
IxHook ixHook = XHookImpl.getInstance(emulator);
ixHook.register("libc.so", "strlen", new ReplaceCallback() {
    @Override
    public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
        System.out.println("Enter");
        return super.onCall(emulator, context, originFunction);
    }
    @Override
    public void postCall(Emulator<?> emulator, HookContext context) {
        //emulator.getBackend().reg_write(Unicorn.UC_ARM64_REG_X0,1000);
        super.postCall(emulator, context);
    }
}, true);
//需要调用refresh方法来刷新后才能Hook
ixHook.refresh();
注意
上述传入的函数名称是原始名称,而不是经过IDA解释后的名称.

Unidbg Patch

测试Apk

同Unidbg Hook测试Apk

Patch

如果想要在Unidbg中修改程序的逻辑代码,可以利用Patch技术.

下面展示将下述so中的ADD指令修改为SUB指令.

IDAADD指令

可通过下述网站来讲汇编指令转换为机器码.

https://armconverter.com/

Arm指令

Patch代码如下:

1
2
3
4
5
public void patch(){
  UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x16B4);
  byte[] code = new byte[]{(byte) 0x00, 0x01, 0x09, 0x4B};
  pointer.write(code);
}

也可以使用Keystone将汇编指令自动转换为机器码.

1
2
3
4
5
6
7
8
public void patch2(){
  UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x16B4);
  Keystone keystone = new Keystone(KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian);
  String s = "SUB W0, W8, W9";
  byte[] machineCode = keystone.assemble(s).getMachineCode();
  //System.out.println(Arrays.toString(machineCode));
  pointer.write(machineCode);
}

完整的Unidbg代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.example.luodst;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.DynarmicFactory;
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.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneMode;

import java.io.File;

public class MainActivity extends AbstractJni {

    public static void main(String[] args) {
        MainActivity mainActivity = new MainActivity();
        mainActivity.patch2();
        mainActivity.call_func();
    }

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private MainActivity() {
        //1.创建Android模拟器实例
        emulator = AndroidEmulatorBuilder
                .for64Bit()
                .addBackendFactory(new DynarmicFactory(true))
                .build();

        //2.获取操作内存的接口
        Memory memory = emulator.getMemory();
        //3.设置Android SDK 版本
        LibraryResolver resolver = new AndroidResolver(23);
        memory.setLibraryResolver(resolver);
        //4.创建虚拟机
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/example/luodst/files/app-debug.apk"));
        //5.是否打印日志
        vm.setVerbose(false);
        //6.设置jni
        vm.setJni(this);
        new AndroidModule(emulator, vm).register(memory);
        //7.加载目标so文件
        DalvikModule dm = vm.loadLibrary("luodst", true);
        //8.将so文件对应的Module存入成员变量
        module = dm.getModule();
        //9.调用JNI_OnLoad
        dm.callJNI_OnLoad(emulator);
    }

    private void call_func() {
        //执行JNI方法
        DvmObject obj = ProxyDvmObject.createObject(vm, this);
        //DvmObject obj = vm.resolveClass("com.example.luodst.MainActivity").newObject(null);
        int nRet = obj.callJniMethodInt(emulator, "add(II)I", 10, 5);
        System.out.println(nRet);
    }

    public void patch(){
        UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x16B4);
        byte[] code = new byte[]{(byte) 0x00, 0x01, 0x09, 0x4B};
        pointer.write(code);
    }
    public void patch2(){
        UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x16B4);
        Keystone keystone = new Keystone(KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian);
        String s = "SUB W0, W8, W9";
        byte[] machineCode = keystone.assemble(s).getMachineCode();
        //System.out.println(Arrays.toString(machineCode));
        pointer.write(machineCode);
    }
}

Unidbg Debugger

Unidbg还对Hook进行了封装,提供了调试器功能,可以让我们很方便地在控制台进行调试.除了提供Console Debugger,还支持GDB与IDA协助调试.

测试Apk

同Unidbg Hook测试Apk

Debugger

在使用调试器功能时,需要使用默认的Unicorn后端.调用调试器的相关代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//attach方法的参数如下:
public enum  DebuggerType {
    /**
     * console debugger
     */
    CONSOLE,

    /**
     * gdb server
     */
    GDB_SERVER,

    /**
     * ida android server v7.x
     */
    ANDROID_SERVER_V7
}

//附加调试器,并添加相应地址的断点
emulator.attach(DebuggerType.CONSOLE).addBreakPoint(module.base + 0x1648);

//或者添加一个回调
emulator.attach(DebuggerType.CONSOLE).addBreakPoint(module.base + 0x1648, new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        //修改寄存器等操作
        
        //返回false则继续断下,返回true则不断下
        return false;
    }
});

当attach方法的参数为空时,默认使用Console Debugger

使用Console Debugger进行调试的界面如下:

UnidbgDebugger

调试指令如下:

1
2
3
4
5
6
7
8
9
help
c: 继续
n: 单步步过
s: 单步步进
d: 显示反汇编
mr0-mr7, mfp, mip, msp [size]: 查看指定寄存器内存
mr0s-mr7s: 将寄存器指向的内存空间数据当做字符串来读取
m(address) [size]: 查看指定地址内存
//---

Unidbg Debugger完整代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.example.luodst;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.DebuggerType;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.*;
import com.github.unidbg.hook.xhook.IxHook;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.XHookImpl;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import unicorn.Unicorn;

import java.io.File;

public class MainActivity extends AbstractJni {

    public static void main(String[] args) {
        MainActivity mainActivity = new MainActivity();
        mainActivity.call_func();
    }

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private MainActivity() {
        //1.创建Android模拟器实例
        emulator = AndroidEmulatorBuilder
                .for64Bit()
                .addBackendFactory(new Unicorn2Factory(true))
                .build();

        //2.获取操作内存的接口
        Memory memory = emulator.getMemory();
        //3.设置Android SDK 版本
        LibraryResolver resolver = new AndroidResolver(23);
        memory.setLibraryResolver(resolver);
        //4.创建虚拟机
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/example/luodst/files/app-debug.apk"));
        //5.是否打印日志
        vm.setVerbose(false);
        //6.设置jni
        vm.setJni(this);
        new AndroidModule(emulator, vm).register(memory);
        //7.加载目标so文件
        DalvikModule dm = vm.loadLibrary("luodst", true);
        //8.将so文件对应的Module存入成员变量
        module = dm.getModule();
        //9.调用JNI_OnLoad
        dm.callJNI_OnLoad(emulator);
    }

    private void call_func() {
        emulator.attach(DebuggerType.CONSOLE).addBreakPoint(module.base + 0x1648, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                //修改寄存器等操作

                //返回false则继续断下,返回true则不断下
                return false;
            }
        });

        //执行JNI方法
        DvmObject obj = ProxyDvmObject.createObject(vm, this);
        //DvmObject obj = vm.resolveClass("com.example.luodst.MainActivity").newObject(null);
        int nRet = obj.callJniMethodInt(emulator, "add(II)I", 10, 5);
        System.out.println(nRet);
    }
}

参考链接

<Unidbg逆向工程 原理与实践>


相关内容

0%