记一道Android-CrackMe

记一道Android-CrackMe

App 信息

Jadx 解包 app 得到文件

逻辑很清晰,得到文本框的内容转为字符串赋值给 data

接着调用本类下的 a 方法

b-1 方法检测字符串长度是否是 36 且必须在 A-Z a-z 0-9 范围内。

并且调用 a2 方在进行检测

native 层检测,库文件是:native-lib

查看 JNI_OnLoad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
__int64 v4; // x8
_QWORD v5[2]; // [xsp+0h] [xbp-30h] BYREF

v5[1] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
sub_1430((__int64)vm, (__int64)reserved);
v5[0] = 0LL;
if ( (*vm)->GetEnv(vm, (void **)v5, 65540LL) )
return -1;
v4 = (*(__int64 (__fastcall **)(_QWORD, const char *))(*(_QWORD *)v5[0] + 48LL))(v5[0], "com/j/swag/MainActivity");
if ( !v4 )
return -1;
else
// "b2"
return ((*(int (__fastcall **)(_QWORD, __int64, char **, __int64))(*(_QWORD *)v5[0] + 1720LL))(
v5[0],
v4,
off_12098,
1LL) >> 31) | 0x10004;
}

其中:

1
v4 = (*(__int64 (__fastcall **)(_QWORD, const char *))(*(_QWORD *)v5[0] + 48LL))(v5[0], "com/j/swag/MainActivity");

是在调用:FindClass 函数

1
2
3
4
5
((*(int (__fastcall **)(_QWORD, __int64, char **, __int64))(*(_QWORD *)v5[0] + 1720LL))(
v5[0],
v4,
off_12098,
1LL) >> 31) | 0x10004;

是在调用:RegisterNatives 函数

其中 RegisterNatives 函数的第 2 个参数表示为注册函数的函数签名信息。

所以我们看 off_12098 就可以找到目标函数。

在这里我们可以得到,这个函数的参数是一个字符串,返回值是布尔类型。

sub_11B4 就是函数的实现

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
__int64 __fastcall sub_11B4(__int64 a1, __int64 a2, __int64 a3)
{
unsigned __int8 *v5; // x21
_QWORD *v6; // x22
_QWORD *v7; // x0
unsigned __int64 v8; // x9
_DWORD *i; // x8
_DWORD *v10; // x10
_BYTE v13[4]; // [xsp+4h] [xbp-3Ch] BYREF
__int64 v14; // [xsp+8h] [xbp-38h]

v14 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v5 = (unsigned __int8 *)(*(__int64 (__fastcall **)(__int64, __int64, _BYTE *))(*(_QWORD *)a1 + 1352LL))(a1, a3, v13);
v6 = sub_D60(v5);
sub_F1C();
v7 = sub_10BC((__int64)v6, (__int64)&byte_12008);
v8 = 0LL;
for ( i = &unk_1674; ; i += 6 )
{
v10 = (_DWORD *)v7[v8];
if ( *(i - 3) != *v10
|| *(i - 2) != v10[1]
|| *(i - 1) != v10[2]
|| *i != v10[3]
|| i[1] != v10[4]
|| i[2] != v10[5] )
{
break;
}
if ( v8++ >= 5 )
{
(*(void (__fastcall **)(__int64, __int64, unsigned __int8 *))(*(_QWORD *)a1 + 1360LL))(a1, a3, v5);
return 1LL;
}
}
return 0LL;
}

可以改一下函数的数据类型,使得 IDA 能够正常识别到 JNI 函数

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
__int64 __fastcall sub_11B4(JNIEnv *a1, jobject a2, jstring *a3)
{
unsigned __int8 *v5; // x21
_QWORD *v6; // x22
_QWORD *v7; // x0
unsigned __int64 v8; // x9
_DWORD *i; // x8
_DWORD *v10; // x10
_BYTE v13[4]; // [xsp+4h] [xbp-3Ch] BYREF
__int64 v14; // [xsp+8h] [xbp-38h]

v14 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v5 = (unsigned __int8 *)(*a1)->GetStringUTFChars(a1, a3, v13);
v6 = sub_D60(v5);
sub_F1C(v6);
v7 = sub_10BC((__int64)v6, (__int64)&byte_12008);
v8 = 0LL;
for ( i = &unk_1674; ; i += 6 )
{
v10 = (_DWORD *)v7[v8];
if ( *(i - 3) != *v10
|| *(i - 2) != v10[1]
|| *(i - 1) != v10[2]
|| *i != v10[3]
|| i[1] != v10[4]
|| i[2] != v10[5] )
{
break;
}
if ( v8++ >= 5 )
{
(*a1)->ReleaseStringUTFChars(a1, a3, (const char *)v5);
return 1LL;
}
}
return 0LL;
}

v5 就是输入的字符串,由于 Java 层的字符串不能在 c 中直接使用,所以需要使用 GetStringUTFChars 转为 c 字符串。

sub_D60 对 v5 进行操作

函数有点长,函数功能就是对 v5 进行分割,组成一个 6*6 的矩阵,每 6 个元素为一组。

sub_F1C 对 v6 进行操作,v6 就是 sub_D60 函数的结果,是一个 6*6 的矩阵。

sub_F1C 也有点长,函数功能就是对这个 6*6 的矩阵 转置一下,我说白了就是把原来的行转为新的列。

例如:

{ 1, 2, 3, 4, 5, 6}, { 1, 7, 13, 19, 25, 31},

{ 7, 8, 9, 10, 11, 12}, { 2, 8, 14, 20, 26, 32},

{13, 14, 15, 16, 17, 18}, { 3, 9, 15, 21, 27, 33},

{19, 20, 21, 22, 23, 24}, { 4, 10, 16, 22, 28, 34},

{25, 26, 27, 28, 29, 30}, { 5, 11, 17, 23, 29, 35},

{31, 32, 33, 34, 35, 36} { 6, 12, 18, 24, 30, 36}

sub_10BC 函数对这个转置后的矩阵再一次变换

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
_QWORD *__fastcall sub_10BC(__int64 a1, __int64 a2)
{
_QWORD *v4; // x20
char *v5; // x21
void *x9_V8; // x0
__int64 v7; // x8
__int64 x9_V8_1; // x9
_DWORD *v9; // x10
__int64 n24; // x11

v4 = malloc(0x30uLL);
v5 = (char *)malloc(0x18uLL);
*v4 = v5;
v4[1] = malloc(0x18uLL);
v4[2] = malloc(0x18uLL);
v4[3] = malloc(0x18uLL);
v4[4] = malloc(0x18uLL);
x9_V8 = malloc(0x18uLL);
v7 = 0LL;
v4[5] = x9_V8; // x9=V8
while ( 1 )
{
v9 = *(_DWORD **)(a1 + 8 * v7);
for ( n24 = 0LL; n24 != 24; n24 += 4LL )
{
x9_V8_1 = a2 + 0x48; // x9=V8
*(_DWORD *)&v5[n24] = *(_DWORD *)(x9_V8_1 + n24 - 0x48) * *v9
+ *(_DWORD *)(x9_V8_1 + n24 - 0x30) * v9[1]
+ *(_DWORD *)(x9_V8_1 + n24 - 0x18) * v9[2]
+ *(_DWORD *)(x9_V8_1 + n24) * v9[3]
+ *(_DWORD *)(x9_V8_1 + n24 + 0x18) * v9[4]
+ *(_DWORD *)(x9_V8_1 + n24 + 0x30) * v9[5];
}
if ( ++v7 == 6 )
break;
v5 = (char *)v4[v7];
}
return v4;
}

看不懂,ai 启动!!!

不得不说有 ai 就是方便昂。这搁在以前肯定是做不出来的。

剩下的就是校验了,看看经过两次变换后的矩阵是否与预设的一致。

唯一需要注意的就是取密文的时候要往后 3 个 DWORD

从这里往下 36 个 DWORD。

反解的话交给 ai 就好了

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
import numpy as np
# 系数矩阵 C
C = np.array([
[262, 581, 156, 482, 548, 634],
[274, 174, 803, 964, 880, 220],
[903, 30, 182, 984, 861, 314],
[697, 354, 131, 549, 87, 396],
[265, 539, 793, 238, 705, 469],
[570, 410, 325, 606, 810, 470]
], dtype=np.int64)

R = np.array([
[302999, 215646, 248498, 397573, 404205, 258602],
[301119, 208961, 236950, 386884, 392903, 252558],
[324152, 225885, 248502, 411075, 413376, 271265],
[301955, 220516, 256600, 390644, 405407, 260647],
[302954, 218407, 251586, 387387, 402758, 259069],
[326574, 234419, 262070, 424569, 426884, 279006],
], dtype=np.int64)

# 计算 C 的逆矩阵(整数域,浮点近似)
C_inv_float = np.linalg.inv(C)
print("C⁻¹ (浮点):\n", C_inv_float)

# 逆 sub_10BC:M' = R × C⁻¹
M_prime_float = np.dot(R, C_inv_float)
print("\nM' (浮点):\n", M_prime_float)

# 尝试四舍五入到整数
M_prime = np.round(M_prime_float).astype(np.int64)
print("\nM' (整数,四舍五入):\n", M_prime)

# 逆 sub_F1C:M = (M')^T
M = M_prime.T
print("\nM (原始矩阵):\n", M)

# 转换为字符串
result = ''.join(chr(c) for row in M for c in row)
print(result)

得到字符串
ningdunbeictfkjbdccbsdcudbcusdcbsdiu

输入进去验证