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

分析

img_2

可以看到符号并没有抹除.

我们进入 AES_ECB_encrypt 看看

img_3

可以看到 sub_20B14 被调用了三次, 这显然是 轮密钥加 的操作.
第一轮 与 key0 做异或
第2到10轮 与 key1-9 做异或
第11轮 与 key10 做异或

进入到 sub_20BAC, 看看实现

img_4

再看看 byte_16CD5

img_5

这明显是一个查表的操作, 而且 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F 明显是AES 的 S盒, 专用于字节替换.

再来看看 sub_20C3C 的实现.

img_6

这看起来似乎是循环左移的操作.

那 sub_20D2C 大概率是 列混淆的操作了.

a1 大概率是 state.

我们试试在倒数第二轮修改state 的某个值.

img_7

可以看到 i== 10 就退出了, 我们期望 i==9 时候, 修改 state 的值.

这里介绍一个ida插件, 可以生成 frida 代码

ida 插件 gen_frida

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hookwbShiftRows(){
var count = 0;
var base_addr = Module.findBaseAddress("libaes.so");
//var real_addr = base_addr.add(0x20C3C)
var real_addr = base_addr.add(0x20D2C)
Interceptor.attach(real_addr, {
onEnter: function (args) {
count += 1;
if(count%9 === 0){
//console.log(hexdump(args[0]))
args[0].add(randomNum(0,15)).writeS8(randomNum(0, 0xff));
//args[0].add(randomNum(0,15)).writeU8(randomNum(0, 0xff));
}
}
});
}

函数名没来得及改, hook循环左移, 但不影响, 我们实际hook的是列混淆函数

一共是九次列混淆, 最后一轮计算中并没有列混淆的操作. 所有当 i==9 时, 注入一个字节. 在哪里注入? 在arg[0] + offset 的位置注入无符号字节, 0-255之间

我们怎么来主动 call 呢?

答曰 : 从java 层或者 native层主动call
我这里从native层没有找到好的时机, 因为 aes的第一个入参是 &ctx, 这是个结构体. 所以我尝试从 java层多次call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Java.perform(function () {
// 替换为你实际的类名
var TargetClass = Java.use("com.example.aes.MainActivity");

// 构造 byte[] 参数
function stringToByteArray(str) {
var bytes = Java.array('byte', Array.from(str, c => c.charCodeAt(0)));
return bytes;
}

var data = stringToByteArray("1111222233334444");
var key = stringToByteArray("1234567890123456");

for(var i=0;i<100;i++){
var result = TargetClass.openssl_encrypt(data, key);
// console.log("Encrypted result: " + result);
console.log(result);
}

});

下面给出完整的 dfa 注入错误的代码

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

function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}

function randomNum(minNum,maxNum){
if (arguments.length === 1) {
return parseInt(Math.random() * minNum + 1, 10);
} else if (arguments.length === 2) {
return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
} else {
return 0;
}
}

function hookwbShiftRows(){
var count = 0;
var base_addr = Module.findBaseAddress("libaes.so");
//var real_addr = base_addr.add(0x20C3C)
var real_addr = base_addr.add(0x20D2C)
Interceptor.attach(real_addr, {
onEnter: function (args) {
count += 1;
if(count%9 === 0){
//console.log(hexdump(args[0]))
args[0].add(randomNum(0,15)).writeS8(randomNum(0, 0xff));
//args[0].add(randomNum(0,15)).writeU8(randomNum(0, 0xff));
}
}
});
}


function hook_0x491A0() {
let base_addr = Module.findBaseAddress("libaes.so");

Interceptor.attach(base_addr.add(0x20964), {
onEnter(args) {
console.log(`call 0x491A0 arg0:${args[0]} arg1:${args[1]}`);
}
});
}



function bufferToHex (buffer) {
return [...new Uint8Array (buffer)]
.map (b => b.toString (16).padStart (2, "0"))
.join ("");
}

function dfa(){


hookwbShiftRows();
Java.perform(function () {
// 替换为你实际的类名
var TargetClass = Java.use("com.example.aes.MainActivity");

// 构造 byte[] 参数
function stringToByteArray(str) {
var bytes = Java.array('byte', Array.from(str, c => c.charCodeAt(0)));
return bytes;
}

var data = stringToByteArray("1111222233334444");
var key = stringToByteArray("1234567890123456");

for(var i=0;i<100;i++){
var result = TargetClass.openssl_encrypt(data, key);
// console.log("Encrypted result: " + result);
console.log(result);
}

});

}

然后执行 frida 注入

img_8

将结果复制到文件 aes_dfa_result.txt 中, 第一行添加正确的加密的结果.

1
2
3
4
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,代表这是一个解密过程

跑一下 看看结果

img_9

可以看到 最后一轮也就是第11轮的密钥已经出来了, 197DD69D902A88D60447D21405FC374C

已知AES ECB 的某一轮密钥, 可以推导出其他任意一轮密钥,(共11轮密钥), 包括初始密钥, 也可以推导出.

我们使用 Stark 工具来推导出任意一轮密钥

1
2
$ git clone https://github.com/SideChannelMarvels/Stark.git
$ make -j8
1
2
# 从0开始计数. 10代表第11轮
./aes_keyschedule 197DD69D902A88D60447D21405FC374C 10

看看结果

img_10

cyberchef 看看

img_11

可以看到 AES ECB 的密钥已经出来了. 算法并没有魔改

而且这个样本使用的就是标准的 tiny-AES-c 的算法
感兴趣可以自己研究研究

1
git clone https://github.com/kokke/tiny-AES-c.git

案例

点我下载 案例.apk

源码下载

点我下载 apk源码