记一次aes dfa 逆向, unidbg 注入故障

接着上篇, 上篇使用的是 frida 注入故障, 我们这篇使用 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
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
package com.aes;

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.android.dvm.api.SystemService;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
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.utils.Inspector;
import com.sun.jna.Pointer;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import javax.crypto.Mac;
import java.io.File;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
//import com.github.unidbg.pointer.Pointer;


public class Signer extends AbstractJni implements IOResolver {
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
System.out.println("file open:"+pathname);
return null;
}

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

Signer() {
// 创建模拟器实例
emulator = AndroidEmulatorBuilder
.for64Bit() // 我选择分析 ARM64 的 SO
.setProcessName("com.example.aes") // 传入进程名
.build();
// 获取模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\aes\\aes.apk"));


vm.setJni(this); // 设置JNI
vm.setVerbose(true); // 打印日志
emulator.getSyscallHandler().addIOResolver(this);// 设置文件处理器
DalvikModule dm = vm.loadLibrary("aes", true); // 加载 libsigner.so,Unidbg 会到 apk 的 lib/arm64-v8a 下寻找。
module = dm.getModule(); //获取目标模块的句柄

dm.callJNI_OnLoad(emulator); // 调用目标 SO 的 JNI_OnLoad
// 构造调用目标函数的对象
NativeLibHelper = vm.resolveClass("com.example.aes.MainActivity").newObject(null);


}

public static void main(String[] args) {
Logger.getLogger(DalvikVM64.class).setLevel(Level.DEBUG);
Signer signer = new Signer();
signer.callNsign();
}
public void callNsign(){
// arg1
DvmObject context = vm.resolveClass("com.example.aes.Application", vm.resolveClass("android/content/Context")).newObject(null);
// arg2
ByteArray barr2 = new ByteArray(vm, "1111222233334444".getBytes());
// arg3
ByteArray barr3 = new ByteArray(vm, "1234567890123456".getBytes());
// arg4
String ret = (String) (NativeLibHelper.callJniMethodObject(emulator, "openssl_encrypt", barr2, barr3).getValue());
System.out.println("ret " + ret);
// Inspector.inspect(ret, "result");




// List<Object> list = new ArrayList<>(10);
// // arg1 env
// list.add(vm.getJNIEnv());
// // arg2 jobject/jclazz 一般用不到,直接填0liboasiscore.so
// list.add(0);
//
// DvmObject<?> p2 = vm.resolveClass("com.example.aes.Application", vm.resolveClass("android/content/Context")).newObject(null);
//
// byte[] p3 = "1111222233334444".getBytes();
// byte[] p4 = "1234567890123456".getBytes();
//
//
//// list.add(vm.addLocalObject(p2));
// list.add(vm.addLocalObject(new ByteArray(vm, p3)));
// list.add(vm.addLocalObject(new ByteArray(vm, p4)));
//
// Number number = module.callFunction(emulator, 0x207D8, list.toArray());
// System.out.println(number);
//// Number number = module.callFunction(emulator, 0xef7c, list.toArray())[0];
//
// System.out.println(vm.getObject(number.intValue()).getValue());

}

public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
}

注意哦, 静态注册的函数, 不需要context 这个函数, 否则会报错

1
java.lang.ClassCastException: class com.github.unidbg.linux.android.dvm.DvmObject cannot be cast to class com.github.unidbg.linux.android.dvm.array.ByteArray 

我们来看看运行结果.

img_12

可以看到结果已经出来了

我们先hook 列混淆函数看看, 我们使用的是 arm64 的so文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void hook20D2C(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);

hookZz.wrap(module.base + 0x20D2C, new WrapCallback<HookZzArm64RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public void preCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
// 类似于Frida args[0]
System.out.println(ctx.getXLong(0));
System.out.println(ctx.getXLong(1));
System.out.println(ctx.getXLong(2));
};

@Override
// 类似于 frida onLeave
public void postCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
}
});
}

看看结果

img_13

可以看到 hook 成功了

我们尝试打印一下入参 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void hook20D2C(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);

hookZz.wrap(module.base + 0x20D2C, new WrapCallback<HookZzArm64RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public void preCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
// 类似于Frida args[0]
Inspector.inspect(ctx.getXPointer(0).getByteArray(0, 0x10), "Arg1");

// System.out.println(ctx.getXLong(0));
// Inspector.inspect(ctx.getPointerArg(0), "result");
// System.out.println(ctx.getXLong(1));
// System.out.println(ctx.getXLong(2));
};

@Override
// 类似于 frida onLeave
public void postCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
}
});
}

看看结果

img_14

那如何patch 呢,在最后一次列混淆的时候,能不能hook 的时候就patch 呢?
答曰: 可以!

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
public static int count = 0;
public void hook20D2C(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);

hookZz.wrap(module.base + 0x20D2C, new WrapCallback<HookZzArm64RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public void preCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
// 类似于Frida args[0]
count += 1;
// Inspector.inspect(ctx.getXPointer(0).getByteArray(0, 0x10), "Arg1");


if(count % 9 == 0){
// ctx.getXPointer(0).setByte(randInt(0,16), (byte) randInt(0, 255));
ctx.getXPointer(0).setByte(0, (byte) 2);
}
};

@Override
// 类似于 frida onLeave
public void postCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
}
});
}

我们来跑一下, 看看将第一个字节改为 2 后, 结果变成了什么

img_15

可以看到 结果变成了 2ee457698cd1c4cc0da65a8591ba75bc

再来看看正常运行 不使用dfa 攻击, 正常的 AES ECB 的值是多少?
答曰: d4e457698cd1c4930da6cb8591f075bc, 可以看到 我们成功了,结果差几个字节而已.

我们来批量注入.

1
2
3
4
5
6
7
8
public static void main(String[] args) {
Logger.getLogger(DalvikVM64.class).setLevel(Level.DEBUG);
Signer signer = new Signer();
signer.hook20D2C();
for(int i=0; i<200; i++){
signer.callNsign();
}
}

运行看看

img_16

可以看到 多次注入结果已经打印出来了, 把日志copy 到文件中, 写个脚本批量匹配 ret, 或者改代码, 把结果保存到文件中. 我们选择后者.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void callNsign(){
// arg1
DvmObject context = vm.resolveClass("com.example.aes.Application", vm.resolveClass("android/content/Context")).newObject(null);
// arg2
ByteArray barr2 = new ByteArray(vm, "1111222233334444".getBytes());
// arg3
ByteArray barr3 = new ByteArray(vm, "1234567890123456".getBytes());
// arg4
String ret = (String) (NativeLibHelper.callJniMethodObject(emulator, "openssl_encrypt", barr2, barr3).getValue());
System.out.println("ret " + ret);
String path = "output.txt";
try {
Files.write(Paths.get(path), (ret + "\n").getBytes() , StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

然后我们在文件首行加入正确的密文. d4e457698cd1c4930da6cb8591f075bc

交给 phoenixAES 执行看看

1
2
3
4
5
import phoenixAES

# phoenixAES.crack_file('aes_128_dfa.txt', [], True, False, 3) # crack_file的第三个参数传入False,代表这是一个解密过程
# phoenixAES.crack_file('aes_dfa_result.txt', [], True, False, 3) # crack_file的第三个参数传入False,代表这是一个解密过程
phoenixAES.crack_file('output.txt', [], True, False, 3) # crack_file的第三个参数传入False,代表这是一个解密过程

img_17

呃, 好像结果并没有出来, why????

是因为太少了吗? 我们注入500次试试

img_18

果然 200次太少, 一辈子太长, 只求天长地久.

ok, AES ECB 已知某一轮密钥, 怎么推出其他密钥,甚至初始密钥?

答曰: Stark.

img_19

cyberchef 验证看看

img_20

yyds!!