算法逆向分析初识

hellojni_2.0.7.apk

目标:还原signs的加密算法。

点击SIGN2按钮后,下面这个签名会发生改变。

image-20250414145949279

通过定位,发现这个签名是由JNI函数Java_com_example_hellojni_HelloJni_sign2生成的。

进一步,对其中的sub_1CFF0进行hook,发现其第3个参数(从1开始算)便是签名。

image-20250414150401970

hook的代码如下,将其中的addr赋值为1CFF0即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function hook_find_target_address(addr){
var base = Module.findBaseAddress("libhello-jni.so");
console.log("\r\nlibhello-jni.so Baseaddr: " + base);

var target_addr = base.add(addr);

console.log("\r\nTarget Address: " + target_addr);

// hook并打印参数和返回值
Interceptor.attach(target_addr, {
onEnter(args){
this.arg0 = args[0];
this.arg1 = args[1];
this.arg2 = args[2];

console.log("\r\nsub_" + addr.toString(16) + "args: ");
console.log("\r\ninput_str: \r\n" + hexdump(this.arg0));
console.log("\r\ninput_str_length: " + this.arg1);
},
onLeave(retval){
console.log("\r\noutput_str: \r\n" + hexdump(this.arg2));
}
})
}

打印的结果如下图所示。

image-20250414150622333

为了方便分析,这里需要将input_str和input_str_length进行固定。

如图所示,我将输入字符串固定为”1234567890abcdefg”。

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
function hook_find_target_address(addr){
var base = Module.findBaseAddress("libhello-jni.so");
console.log("\r\nlibhello-jni.so Baseaddr: " + base);
var target_addr = base.add(addr);
console.log("\r\nTarget Address: " + target_addr);

// hook并打印参数和返回值
Interceptor.attach(target_addr, {
onEnter(args){
// 保存原始参数值以便打印
this.arg0 = args[0];
this.arg1 = args[1];
this.arg2 = args[2];

console.log("\r\nsub_" + addr.toString(16) + " args: ");
console.log("\r\n原始input_str: \r\n" + hexdump(this.arg0, { length: parseInt(this.arg1) }));
console.log("\r\ninput_str_length: " + this.arg1);

// 新字符串
var new_str = "1234567890abcdefg";

// 方法1:直接写入内存
Memory.writeUtf8String(args[0], new_str);

// 方法2:如果需要写入二进制数据而不是UTF8字符串
// var bytes = [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x00]; // "1234567890abcdef\0"
// Memory.writeByteArray(args[0], bytes);

var length = new_str.length;
args[1] = ptr(length);
this.args1 = args[1];

console.log("\r\n修改后的长度: " + this.args1);

console.log("\r\n修改后input_str: \r\n" + hexdump(args[0], { length: parseInt(this.args1) }));
},
onLeave(retval){
console.log("\r\noutput_str: \r\n" + hexdump(this.arg2, { length: parseInt(this.args1) }));
}
});
}
image-20250414153532777

在确保了输入字符串不变后,尝试使用IDA的trace,追踪函数sub_1CFF0。

先注入frida、再注入IDA server——似乎不按这个顺序,先注入IDA再注入Frida,Frida会注入失败。

我决定先退出Frida的hook,再进行追踪,因为我发现Frida要是已经hook了sub_1CFF0,在sub_1CFF0的函数开始的地方,会存在inline hook,跳转到Frida的跳床函数。

image-20250414153913051

通过IDA trace,走过的汇编指令会变成黄色。

image-20250414154156636

追踪到的内容如下图所示,第一栏的36EE是线程id,第二栏是地址,第三栏开始就是汇编指令了,如果汇编指令修改了寄存器,则会在汇编指令后面,显示寄存器被修改后的值。

image-20250414154332639

之后,我重新生成了一个trace记录,并记录了一下签名值。

image-20250414162840995

之后进行算法逆向。

已知算法的输入是input_str / input_str_length / output_str,分别代表着X0、X1、X2。

image-20250414164247587

因为X2代表着output_str的内存地址,所以追踪X2,判断有哪些指令对地址0x00000079E1720D90上的内容进行了修改。

image-20250414164543132

查找到了34个对0x00000079E1720D90进行读写的位置。

对其中一些指令进行还原,可以猜到。

X8代表:当前正在操作output的第X8个字节,也代表循环次数;

X5代表output的内存地址;

X29[var_64]是一个固定字节数组。

X29[var_68]也是一个固定字节数组。

至于var_6C,最后还是追踪到var_68上。

image-20250414190420796

做了一些补充。

image-20250415094520831

一直追踪[X29,#var_64]。

image-20250415094627344

而一直追踪[X29,#var_68],会发现需要追踪X2,继而追踪X2和X3,继而追踪X3和X7……

最后追踪到[X29,#var_88]。

image-20250415094751043

最后发现,var_88这个变量是一个指针,它将地址给了X2,由X2去获取全局变量,也就是图中的xmmword_79A31EE7B0。

image-20250415095055847

至此,可以将全局变量及涉及到的寄存器改写成c。

(解密的时候前8个字节和后8个字节的处理方式不同,可能还得逆,这里我直接贴别人逆好的代码)

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
#include <iostream>
#include <cstring>



void enc_function(const char* input_str, int input_len, char* result) {
const char* table_key1 = "9d9107e02f0f07984956767ab1ac87e5";
const unsigned char table_key2[] = {0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0xF, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A};
for (int i = 0; i < input_len; ++i) {
unsigned char X2 = input_str[i];
unsigned char key2 = table_key2[(i & 0xF) & 0xFFFFFFFF];
unsigned char W8 = 0xDA;
unsigned char W30 = 0x25;
unsigned char W2 = X2;
unsigned char W7 = W8 & (~W2);
W2 = W2 & 0x25;
W2 = W7 | W2;
unsigned char W3 = key2;
W7 = W8 & (~W3);
W3 = W3 & W30;
W3 = W7 | W3;
W2 = W2 ^ W3;
W3 = W2;

unsigned char key1 = table_key1[(i ^ 0xFFFFFFF8) & i ];
W2 = key1;
W7 = key2;
W30 = key2;
unsigned char W1 = W2 & (~W3);
W3 = W3 & (~W2);
unsigned char W5 = W30 & (~W2);
W2 = W2 & (~W30);
W1 = W1 | W3;
W2 = W5 | W2;
W1 = W1 + W7;
W3 = W1 & (~W2);
W1 = W2 & (~W1);
W1 = W3 | W1;
result[i] = W1;
}
}

bool test_eq(const char* buf1, const char* buf2, int buf_len) {
for (int i = 0; i < buf_len; ++i) {
if (buf1[i] != buf2[i]) {
return false;
}
}
return true;
}

int main() {
const char* input = "0123456789abcdef0123456789abcdef";
int len = strlen(input);
char* result = (char*)malloc(len);
memset(result, 0, len);
enc_function(input, len, result);
for (int i = 0; i < len; ++i) {
printf("%02x", (unsigned char)result[i]);
}
printf("\r\n%x", test_eq(result, result, len));
free(result);
return 0;
}