这是在面试启明X辰时,公司要求分析的 CreakMe,这里做一份记录。
分析报告 | 分析环境 |
---|---|
分析人 | 刘XX |
时间 | 2023年2月28日 |
平台 | x86 模拟器 |
样本 | 分析环境 |
---|---|
样本名称 | 20230221.demo.apk |
MD5值 | 2A006D76B461ECE94596D46B3C9B3072 |
SHA1值 | CF7810FA84DCCFA7F4F21847188E133A4C80CBA7 |
CRC32 | 6D81EAF7 |
整理一下各个按钮的 id 与对应的linstener
text | id | button | linstener |
---|---|---|---|
generate1 | btn_login | this.s = button | g |
GENERATE2 | btn_gen2 | this.t = button2 | e |
generate3 | btn_gen3 | this.u = button3 | f |
check1 | btn_check | this.v = button4 | d |
check2 | btn_check2 | this.w = button5 | b |
check3 | btn_check3 | this.x = button6 | c |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class
g implements View.OnClickListener {
g() {
}
@Override
/
/
android.view.View.OnClickListener
public void onClick(View view) {
String obj
=
MainActivity.this.y.getText().toString();
MainActivity.this.z.getText().toString();
if
(obj.length() !
=
16
) {
Toast.makeText(MainActivity.this.getApplicationContext(),
"用户名长度必须为16字节"
,
0
).show();
return
;
}
MainActivity.this.v.setEnabled(true);
MainActivity.this.w.setEnabled(false);
MainActivity.this.x.setEnabled(false);
MainActivity.this.z.setText(com.test.pac.demo.a.a.a(MainActivity.this.G(obj.getBytes())));
}
}
|
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
|
public byte[] G(byte[] bArr) {
try
{
byte[] bArr2
=
new byte[
16
];
/
/
随机生成
16
个字节
new Random().nextBytes(bArr2);
/
/
实例化一个加密对象
Cipher cipher
=
Cipher.getInstance(
"AES/CBC/NoPadding"
);
/
/
传入密钥和随机数
cipher.init(
1
, new SecretKeySpec(
"6SvMO4msTk1OqA8n"
.getBytes(),
"AES"
), new IvParameterSpec(bArr2));
/
/
传入明文加密并得到返回的密文
byte[] doFinal
=
cipher.doFinal(bArr);
/
/
AES 加密后的数据长度是不变的,这里其实就是创建了一个长度
32
的字节数组
byte[] bArr3
=
new byte[doFinal.length
+
16
];
/
/
从最开始申请的随机数的下标
8
位开始拷贝,拷贝
8
个长度,放到 barr3 的起始位置
System.arraycopy(bArr2,
8
, bArr3,
0
,
8
);
/
/
把 AES 加密后的全部数据拷贝到 barr3,从barr3的下标
8
开始,其实就是接着上边结束位置
System.arraycopy(doFinal,
0
, bArr3,
8
, doFinal.length);
/
/
从最开始申请的随机数的下标
0
位开始拷贝,拷贝
8
个长度,从barr3的下标 doFinal.length
+
8
开始,其实就是接着上边结束位置
System.arraycopy(bArr2,
0
, bArr3, doFinal.length
+
8
,
8
);
/
/
小结一下,bArr3 的组成如下所示:
/
/
bArr3
=
bArr2[
8
-
15
]
+
aes(明文)
+
bArr2[
0
-
7
]
return
bArr3;
} catch (Exception e2) {
e2.printStackTrace();
return
null;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
public static String a(byte[] bArr) {
StringBuffer stringBuffer
=
new StringBuffer();
for
(byte b : bArr) {
/
/
取绝对值后转成
16
进制
String hexString
=
Integer.toHexString(b &
255
);
if
(hexString.length()
=
=
1
) {
hexString
=
'0'
+
hexString;
}
stringBuffer.append(hexString.toUpperCase());
}
return
stringBuffer.toString();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class
d implements View.OnClickListener {
d() {
}
@Override
/
/
android.view.View.OnClickListener
public void onClick(View view) {
Context applicationContext;
String
str
;
String obj
=
MainActivity.this.y.getText().toString();
/
/
username
String obj2
=
MainActivity.this.z.getText().toString();
/
/
password
if
(obj.length() !
=
16
) {
Toast.makeText(MainActivity.this.getApplicationContext(),
"用户名长度必须为16字节"
,
0
).show();
return
;
}
if
(MainActivity.this.D(com.test.pac.demo.a.a.b(obj2), obj.getBytes()) >
0
) {
applicationContext
=
MainActivity.this.getApplicationContext();
str
=
"verify success!"
;
}
else
{
applicationContext
=
MainActivity.this.getApplicationContext();
str
=
"verify failed!"
;
}
Toast.makeText(applicationContext,
str
,
0
).show();
}
}
|
这个方法起初看起来有点复杂,一直以为是个加密的方法,但仔细观察,这其实就是个十六进制字符串转十进制的方法,将传入的十六进制字符串转换成了对应的十进制字节数组
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public static byte[] b(String
str
) {
if
(
str
.length() <
1
) {
return
null;
}
byte[] bArr
=
new byte[
str
.length()
/
2
];
for
(
int
i
=
0
; i <
str
.length()
/
2
; i
+
+
) {
int
i2
=
i
*
2
;
/
/
i2
=
0
2
4
6
8
10
...
int
i3
=
i2
+
1
;
/
/
i3
=
1
3
5
7
9
11
...
/
/
一个
16
位的十六进制数由两个字符表示,高位字符转成十进制后乘以
16
+
低位字符转成十进制
bArr[i]
=
(byte) ((Integer.parseInt(
str
.substring(i2, i3),
16
)
*
16
)
+
Integer.parseInt(
str
.substring(i3, i2
+
2
),
16
));
}
return
bArr;
}
|
方法D 由两个方法组成,分别是方法F 和方法E,而方法D只是简单的判断方法E的返回值真假
1
2
3
|
public
int
D(byte[] bArr, byte[] bArr2) {
return
E(F(bArr), bArr2) ?
1
:
0
;
}
|
根据之前分析过的方法G,其实可以直接判断这显然是方法G的逆运算,也就是AES的解密方法,该方法返回了解密后的明文字节数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
private byte[] F(byte[] bArr) {
try
{
byte[] bArr2
=
new byte[
16
];
/
/
把密文的
0
-
7
位拷贝到bArr2的
8
-
15
位
System.arraycopy(bArr,
0
, bArr2,
8
,
8
);
/
/
把密文的末端
8
位拷贝到bArr2的
0
-
7
位
System.arraycopy(bArr, bArr.length
-
8
, bArr2,
0
,
8
);
/
/
实例化一个加密对象
Cipher cipher
=
Cipher.getInstance(
"AES/CBC/NoPadding"
);
/
/
设置模式为解密,同时传入密钥和随机数的密文
cipher.init(
2
, new SecretKeySpec(
"6SvMO4msTk1OqA8n"
.getBytes(),
"AES"
), new IvParameterSpec(bArr2));
/
/
申请
16
个字节的空间
byte[] bArr3
=
new byte[bArr.length
-
16
];
/
/
密文的下标
8
开始拷贝,拷贝
16
个,也就是将密文的[
7
-
23
]位拷贝到bArr3
System.arraycopy(bArr,
8
, bArr3,
0
, bArr.length
-
16
);
/
/
返回解密后的明文
return
cipher.doFinal(bArr3);
} catch (Exception e2) {
e2.printStackTrace();
return
null;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
private boolean E(byte[] bArr, byte[] bArr2) {
if
(bArr.length
=
=
0
|| bArr2.length
=
=
0
|| bArr.length !
=
bArr2.length) {
return
false;
}
for
(
int
i
=
0
; i < bArr.length && i < bArr2.length; i
+
+
) {
if
(bArr[i] !
=
bArr2[i]) {
System.out.println(
"different"
);
return
false;
}
}
return
true;
}
|
按钮 generate2的分析较为困难,主要问题在于 OpenSSL 库函数较为生疏,而OpenSSL的所以这里先补充一下本次分析中遇到的数据结构和函数知识。
数据结构与函数 | 功能说明 | |
---|---|---|
HMAC_CTX_init | 初始化一个 HMAC_CTX | |
HMAC_CTX | 是一个上下文,保存状态数据和中间计算 | |
EVP_sha256 | 返回 sha256 的 EVP_MD | |
EVP_MD | 用来存放摘要算法信息以及各种计算函数。 | |
HMAC_Init_ex | 初始化HAMC_CTX上下文结构,key为秘钥,len为秘钥长度,md为计算hash | |
HMAC_Update | 向HMAC上下文输入字节流 | |
HMAC_Final | 生成最终的HMAC串,成功时len更新为HMAC的长度 |
这个函数较为简单,通过函数 sub_46660 向 dest 中写入数据,回传到屏幕作为密码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
jbyteArray __cdecl Java_com_test_pac_demo_MainActivity_M1(JNIEnv
*
env, jobject a2, BYTE
*
bytes_)
{
jbyteArray v4;
/
/
esi
int
v6;
/
/
[esp
-
14h
] [ebp
-
1030h
]
unsigned
int
user_name_length;
/
/
[esp
+
0h
] [ebp
-
101Ch
]
jbyte
*
byte_user_name;
/
/
[esp
+
4h
] [ebp
-
1018h
]
char dest[
4096
];
/
/
[esp
+
8h
] [ebp
-
1014h
] BYREF
unsigned
int
v10;
/
/
[esp
+
1008h
] [ebp
-
14h
]
v10
=
__readgsdword(
0x14u
);
user_name_length
=
(
*
env)
-
>GetArrayLength(env, bytes_);
byte_user_name
=
(
*
env)
-
>GetByteArrayElements(env, bytes_,
0
);
memset(dest,
0
, sizeof(dest));
v6
=
sub_46660((
int
)byte_user_name, user_name_length, dest);
v4
=
(
*
env)
-
>NewByteArray(env, v6);
(
*
env)
-
>SetByteArrayRegion(env, v4,
0
, v6, dest);
(
*
env)
-
>ReleaseByteArrayElements(env, bytes_, byte_user_name,
0
);
return
v4;
}
|
这个函数就是 generate2 按钮的主体逻辑了,首先将该函数分为四个部分依次分析:
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
|
int
__cdecl sub_46660(BYTE
*
byte_user_name, unsigned
int
user_name_length, char
*
dest)
{
......
canary
=
__readgsdword(
0x14u
);
/
/
开启canary保护
memset(&key_data[
32
],
0
,
32
);
v3
=
-
32
;
do
{
v4
=
lrand48();
/
/
产生一个正的长整型随机数
key_data[v3
+
+
+
64
]
=
v4
+
v4
/
255
;
/
/
为 key_data[
32
-
63
] 位赋值随机数
}
while
( v3 );
memset(key_data,
0
,
32
);
v5
=
-
32
;
do
{
v_eax
=
lrand48();
key_data[v5
+
+
+
32
]
=
v_eax
+
v_eax
/
255
;
/
/
为 key_data[
0
-
31
] 位赋值随机数
}
while
( v5 );
memset(
hash
,
0
, sizeof(
hash
));
hmac_ctx_hash(&key_data[
32
], key_data,
hash
);
/
/
参数
1
:data 参数
2
:key 参数
3
:dest
strcpy((char
*
)const_key,
"123456awxzcdfqwqt2wetbwerw"
);
memset(encrypt_data,
0
, sizeof(encrypt_data));
evp_encrypt((
int
)
hash
, (
int
)&
hash
[
32
],
12
, const_key,
27
, (
int
)byte_user_name, user_name_length, encrypt_data);
/
/
加密用户名
memset(encrypt_data2,
0
, sizeof(encrypt_data2));
*
(__m128i
*
)hash_temp
=
_mm_load_si128((const __m128i
*
)&
hash
[
48
]);
/
/
从
hash
的第
48
个字节开始加载
128bits
,也就是加载
16
个字节
__memcpy_chk((
int
)encrypt_data2, (
int
)encrypt_data, user_name_length
+
16
,
0x2800
);
if
( !(((
int
)(user_name_length
+
16
) <
0
) ^ __OFADD__(
16
, user_name_length) | (user_name_length
=
=
-
16
)) )
/
/
用户名长度为
16
,所以
1
^
0
|
0
=
1
/
/
__OFADD__ 测试两数相加后是否溢出
{
v_esi
=
0
;
v8
=
user_name_length
+
16
;
/
/
v8
=
32
if
( user_name_length >
=
4294967280
)
/
/
/
/
检查用户名长度是否大于
4294967280
goto LABEL_15;
if
( user_name_length
+
15
>
15
)
/
/
检查用户名长度是否大于
0
,此处跳转至LABEL15,感谢手下留情
goto LABEL_15;
v_esi
=
v8 &
0xFFFFFFF0
;
si128
=
_mm_load_si128((const __m128i
*
)(&(&off_1A25DC)[
-
11535
]
+
1
));
v10
=
_mm_load_si128((const __m128i
*
)(&off_1A25DC
-
92275
));
v11
=
_mm_load_si128((const __m128i
*
)(&(&off_1A25DC)[
-
11534
]
+
1
));
v12
=
_mm_load_si128((const __m128i
*
)(&off_1A25DC
-
92267
));
v13
=
0
;
v34
=
*
(__m128i
*
)hash_temp;
v31
=
*
(__m128i
*
)(&(&off_1A25DC)[
-
11533
]
+
1
);
v33
=
*
(__m128i
*
)(&off_1A25DC
-
92259
);
v32
=
_mm_load_si128((const __m128i
*
)(&(&off_1A25DC)[
-
11532
]
+
1
));
......
while
( v_esi !
=
v13 );
if
( v8 !
=
v_esi )
{
LABEL_15:
do
{
v28
=
(v_esi
+
1
) ^ hash_temp[v_esi &
0xF
];
/
/
hash48[
0
-
F] 分别与
1
进行异或,也就是说src每一位如果是
/
/
奇数:减一
/
/
偶数:不变
encrypt_data2[v_esi]
=
v28 ^ (((unsigned __int8)(encrypt_data[v_esi]
+
v28) >>
4
) | (
16
*
(encrypt_data[v_esi]
+
v28)));
/
/
/
/
encrypt_data[i]
+
v28 看成一个整体结果记为 res
/
/
res转成了无符号类型所以是逻辑右移,左侧用零补齐
/
/
res乘以
16
等价于逻辑左移
4
位
/
/
其实就是把res的高四位和低四位颠倒了一下
/
/
最后再和v28异或
}
while
( v8 !
=
+
+
v_esi );
/
/
循环
32
次
}
}
memset(s,
0
,
960u
);
*
(_OWORD
*
)hash_temp
=
*
(_OWORD
*
)
hash
;
*
(_OWORD
*
)&hash_temp[
16
]
=
*
(_OWORD
*
)&
hash
[
16
];
*
(_OWORD
*
)&hash_temp[
32
]
=
*
(_OWORD
*
)&
hash
[
32
];
*
(_OWORD
*
)&hash_temp[
48
]
=
*
(_OWORD
*
)&
hash
[
48
];
/
/
hash_temp[
0
-
63
]
=
hash
[
0
-
63
]
__memcpy_chk((
int
)s, (
int
)encrypt_data2, user_name_length
+
16
,
960
);
/
/
这里把密文进行了一次拷贝,但看起来后续没有用到
*
(_OWORD
*
)v39
=
0LL
;
return
encrypt_4(hash_temp, user_name_length
+
80
, (
int
)v39,
16
, dest);
/
/
第三次加密得到最终的dest
}
|
这个函数总的来讲就是不断的将求得的hash作为key再次求hash,在分析这个函数时遇到的问题主要在于作者使用了 HMAC_Update 并且还总是在参数中不该传0的传0,无法确定是否对上下文产生了影响,解决方法是花了一些时间在 VS 中搭了一下环境,写了一个简单的 demo 进行简单的测试。
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
|
unsigned
int
__cdecl hmac_ctx_hash(BYTE
*
data, BYTE
*
key, BYTE
*
res)
{
int
md;
/
/
esi
__int64 v4;
/
/
xmm1_8
__int64 v5;
/
/
xmm0_8
int
len
;
/
/
[esp
+
8h
] [ebp
-
154h
] BYREF
char v8;
/
/
[esp
+
Eh] [ebp
-
14Eh
] BYREF
char v9;
/
/
[esp
+
Fh] [ebp
-
14Dh
] BYREF
BYTE hash3[
32
];
/
/
[esp
+
10h
] [ebp
-
14Ch
] BYREF
BYTE hash2[
32
];
/
/
[esp
+
30h
] [ebp
-
12Ch
] BYREF
int
ctx[
52
];
/
/
[esp
+
50h
] [ebp
-
10Ch
] BYREF
BYTE hash1[
40
];
/
/
[esp
+
120h
] [ebp
-
3Ch
] BYREF
unsigned
int
canary;
/
/
[esp
+
148h
] [ebp
-
14h
]
canary
=
__readgsdword(
0x14u
);
memset(hash1,
0
,
32
);
HMAC_CTX_init((
int
)ctx);
/
/
初始化一个 HMAC_CTX
md
=
EVP_sha256();
/
/
返回 sha256 的 EVP_MD
HMAC_Init_ex((
int
)ctx, (
int
)key,
32
, md,
0
);
/
/
初始化HAMC_CTX上下文结构,key为秘钥,
len
为秘钥长度,md为计算
hash
的函数集合
HMAC_Update((
int
)ctx, (
int
)data,
0
);
/
/
向HMAC上下文输入字节流,加密并输出,传入长度为
0
/
/
经过 demo 验证,该条指令不会对上下文产生影响
len
=
0
;
HMAC_Final((
int
)ctx, (
int
)hash1, (
int
)&
len
);
/
/
生成最终的HMAC串,成功时
len
更新为HMAC的长度。
memset(hash2,
0
, sizeof(hash2));
v9
=
1
;
HMAC_CTX_init((
int
)ctx);
HMAC_Init_ex((
int
)ctx, (
int
)hash1,
32
, md,
0
);
/
/
把 hash1 作为 key
HMAC_Update((
int
)ctx, (
int
)data,
0
);
HMAC_Update((
int
)ctx, (
int
)&v9,
1
);
/
/
输入字节流
1
,长度为
1
len
=
0
;
HMAC_Final((
int
)ctx, (
int
)hash2, (
int
)&
len
);
/
/
得到第二次的
hash
memset(hash3,
0
, sizeof(hash3));
v8
=
2
;
HMAC_CTX_init((
int
)ctx);
HMAC_Init_ex((
int
)ctx, (
int
)hash1,
32
, md,
0
);
/
/
把第一次的
hash
作为 key 再次加密
HMAC_Update((
int
)ctx, (
int
)hash2,
32
);
/
/
输入字节流 hash2,长度为
32
HMAC_Update((
int
)ctx, (
int
)&v8,
1
);
/
/
输入字节流
2
,长度为
1
len
=
0
;
HMAC_Final((
int
)ctx, (
int
)hash3, (
int
)&
len
);
/
/
得到第三次的
hash
v4
=
*
(_QWORD
*
)&hash2[
8
];
/
/
v4
=
hash2[
8
-
15
]
*
(_QWORD
*
)res
=
*
(_QWORD
*
)hash2;
/
/
res[
0
-
7
]
=
hash2[
0
-
7
]
*
((_QWORD
*
)res
+
1
)
=
v4;
/
/
res[
8
-
15
]
=
hash2[
8
-
15
]
*
((_QWORD
*
)res
+
2
)
=
*
(_QWORD
*
)&hash2[
16
];
/
/
res[
16
-
23
]
=
hash2[
16
-
23
]
*
((_QWORD
*
)res
+
3
)
=
*
(_QWORD
*
)&hash2[
24
];
/
/
res[
24
-
31
]
=
hash2[
24
-
31
]
*
((_QWORD
*
)res
+
7
)
=
*
(_QWORD
*
)&hash3[
24
];
/
/
res[
56
-
63
]
=
hash3[
24
-
31
]
*
((_QWORD
*
)res
+
6
)
=
*
(_QWORD
*
)&hash3[
16
];
/
/
res[
48
-
55
]
=
hash3[
16
-
31
]
v5
=
*
(_QWORD
*
)hash3;
*
((_QWORD
*
)res
+
5
)
=
*
(_QWORD
*
)&hash3[
8
];
/
/
res[
40
-
47
]
=
hash3[
8
-
15
]
*
((_QWORD
*
)res
+
4
)
=
v5;
/
/
res[
32
-
39
]
=
hash3[
0
-
7
]
return
__readgsdword(
0x14u
);
}
|
测试demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include "openssl/hmac.h"
#include "openssl/evp.h"
#include "openssl/aes.h"
#pragma comment(lib, "libcrypto.lib")
int
main()
{
unsigned char key[
32
]
=
{
0
};
unsigned char data[
32
]
=
{
0
};
unsigned char
hash
[
32
]
=
{
0
};
unsigned
int
len
=
0
;
for
(
int
i
=
0
; i <
32
; i
+
+
)
{
key[i]
=
i;
data[i]
=
i
*
2
;
}
HMAC_CTX
*
ctx
=
HMAC_CTX_new();
const EVP_MD
*
md
=
EVP_sha256();
HMAC_Init_ex(ctx, key,
32
, md,
0
);
/
/
HMAC_Update(ctx, data,
0
);
/
/
该条语句注释未产生任何影响
HMAC_Final(ctx,
hash
, &
len
);
}
|
这个函数的总的来说就是使用上一层传入的 hash 作为 key,加密用户名得到 encrypt_data,然后将 encrypt_data分段赋值给最终的 res_encrypt_data,分析这个函数的问题也还是在于作者使用了 EVP_EncryptUpdate 这个不熟悉的函数,并且还总是在参数中不该传0的传0,无法确定是否对上下文产生了影响,解决方法是花了一些时间在 VS 中搭了一下环境,测试了各种情况的结果。
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
|
int
__cdecl evp_encrypt(
int
hash
,
int
iv,
int
iv_len,
BYTE
*
const_key,
int
const_key_length,
int
byte_user_name,
int
user_name_length,
char
*
res_encrypt_data)
{
int
ctx;
/
/
esi
int
cipher;
/
/
eax
size_t v10;
/
/
eax
__int64 v11;
/
/
xmm1_8
int
v13;
/
/
[esp
+
0h
] [ebp
-
41Ch
] BYREF
size_t encrypt_data_len;
/
/
[esp
+
4h
] [ebp
-
418h
] BYREF
BYTE encrypt_data[
1024
];
/
/
[esp
+
8h
] [ebp
-
414h
] BYREF
unsigned
int
v16;
/
/
[esp
+
408h
] [ebp
-
14h
]
v16
=
__readgsdword(
0x14u
);
ctx
=
EVP_CIPHER_CTX_new();
/
/
创建加密上下文
cipher
=
EVP_aes_256_gcm();
/
/
选择一种加密算法
EVP_EncryptInit_ex(ctx, cipher,
0
,
0
,
0
);
/
/
初始化密码上下文ctx
EVP_CIPHER_CTX_ctrl(ctx,
9
, iv_len,
0
);
/
/
# define EVP_CTRL_AEAD_SET_IVLEN 0x9
/
/
设置向量IV的长度
EVP_EncryptInit_ex(ctx,
0
,
0
,
hash
, iv);
/
/
使用
hash
作为key初始化ctx
EVP_EncryptUpdate(ctx,
0
, (
int
)&encrypt_data_len, (
int
)const_key, const_key_length);
/
/
加密 const_key,让人困惑的是输出地址为
0
,
/
/
无法确定是否会对上下文产生影响,经过 demo 测试最终确定加密结果被丢弃
EVP_EncryptUpdate(ctx, (
int
)encrypt_data, (
int
)&encrypt_data_len, byte_user_name, user_name_length);
/
/
加密用户名
memcpy(res_encrypt_data, encrypt_data, encrypt_data_len);
/
/
dest[
0
-
15
]
=
encrypt_data[
0
-
15
]
/
/
将第二次EncryptUpdate的结果拷贝到dest中,而且经过动态调试得知,encrypt_data_len
=
16
EVP_EncryptFinal_ex(ctx, (
int
)encrypt_data, (
int
)&v13);
/
/
块对齐
EVP_CIPHER_CTX_ctrl(ctx,
16
,
16
, (
int
)encrypt_data);
/
/
# define EVP_CTRL_AEAD_GET_TAG 0x10 获取标签
v10
=
encrypt_data_len;
v11
=
*
(_QWORD
*
)&encrypt_data[
8
];
/
/
v11
=
encrypt_data[
8
-
15
]
*
(_QWORD
*
)&res_encrypt_data[encrypt_data_len]
=
*
(_QWORD
*
)encrypt_data;
/
/
dest[
16
-
23
]
=
encrypt_data[
0
-
7
]
*
(_QWORD
*
)&res_encrypt_data[v10
+
8
]
=
v11;
/
/
dest[
24
-
31
]
=
v11
=
encrypt_data[
8
-
15
]
EVP_CIPHER_CTX_free(ctx);
/
/
释放上下文
return
encrypt_data_len
+
16
;
/
/
返回加密结果的长度
+
16
}
|
测试demo
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
|
#include "openssl/hmac.h"
#include "openssl/evp.h"
#include "openssl/aes.h"
#pragma comment(lib, "libcrypto.lib")
int
main()
{
unsigned char key[
32
]
=
{
0
};
unsigned char iv[
32
]
=
{
0
};
unsigned char encrypt_data[
32
]
=
{
0
};
unsigned char user_name[
16
]
=
{
'a'
,
'b'
,
'c'
,
'd'
,
'a'
,
'b'
,
'c'
,
'd'
,
'a'
,
'b'
,
'c'
,
'd'
,
'a'
,
'b'
,
'c'
,
'd'
};
const unsigned char
*
const_key
=
(const unsigned char
*
)
"123456awxzcdfqwqt2wetbwerw"
;
int
len
=
0
;
for
(
int
i
=
0
; i <
32
; i
+
+
)
{
key[i]
=
i;
iv[i]
=
i;
}
EVP_CIPHER_CTX
*
ctx
=
EVP_CIPHER_CTX_new();
const EVP_CIPHER
*
cipher
=
EVP_aes_256_gcm();
EVP_EncryptInit_ex(ctx, cipher,
0
, key, iv);
EVP_CIPHER_CTX_ctrl(ctx,
9
,
12
,
0
);
/
/
EVP_EncryptUpdate(ctx,
0
, &
len
, const_key,
27
);
/
/
该条语句注释未产生任何影响
len
=
0
;
EVP_EncryptUpdate(ctx, encrypt_data, &
len
, user_name,
16
);
len
=
0
;
EVP_EncryptFinal_ex(ctx, encrypt_data, &
len
);
}
|
这一部分主要就是通过 hash 第二次加密 encrypt_data 得到 encrypt_data2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
*
(__m128i
*
)hash_temp
=
_mm_load_si128((const __m128i
*
)&
hash
[
48
]);
/
/
从
hash
的第
48
个字节开始加载
128bits
,也就是加载
16
个字节
......
do
{
v28
=
(v_esi
+
1
) ^ hash_temp[v_esi &
0xF
];
/
/
hash48[
0
-
F] 分别与
1
进行异或,也就是说src每一位如果是
/
/
奇数:减一
/
/
偶数:不变
encrypt_data2[v_esi]
=
v28 ^ (((unsigned __int8)(encrypt_data[v_esi]
+
v28) >>
4
) | (
16
*
(encrypt_data[v_esi]
+
v28)));
/
/
encrypt_data[i]
+
v28 看成一个整体结果记为 res
/
/
res转成了无符号类型所以是逻辑右移,左侧用零补齐
/
/
res乘以
16
等价于逻辑左移
4
位
/
/
其实就是把res的高四位和低四位颠倒了一下
/
/
最后再和v28异或
}
while
( v8 !
=
+
+
v_esi );
/
/
循环
32
次
|
这个函数主要分为三个部分:
初始化数组 v21、v22,需要注意的是 v21 与 v22 在内存中相邻
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
_BYTE v21[
256
];
/
/
[esp
+
10h
] [ebp
-
21Ch
] BYREF
_BYTE v22[
264
];
/
/
[esp
+
110h
] [ebp
-
11Ch
] BYREF
unsigned
int
v23;
/
/
[esp
+
218h
] [ebp
-
14h
]
v23
=
__readgsdword(
0x14u
);
memcpy(dest, hash_temp, user_name_length_add_80);
/
/
注意此时dest被赋值为hash_temp.最后一步的化简会用到
memset(v22,
0
,
256
);
memset(v21,
0
, sizeof(v21));
for
( i
=
0
; i !
=
256
;
+
+
i )
{
v21[i]
=
i;
/
/
v21 依次赋值
1
2
3
4
5
6
...
v22[i]
=
v39[i
%
c_16];
/
/
v22 依次填充
0
,因为v39全是
0
}
v_dest
=
dest;
|
第一次循环加密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
v6
=
dest;
v7
=
0
;
v8
=
-
256
;
do
{
v9
=
(unsigned __int8)v22[v8];
v10
=
v9
+
v7
+
(unsigned __int8)v22[v8
+
256
];
v10
%
=
256
;
v11
=
v21[v10];
v21[v10]
=
v9;
v22[v8
+
+
]
=
v11;
v7
=
v10;
}
while
( v8 );
|
这个循环的赋值有点混乱,利用两个相邻的数组越界赋值,所以这里我们先进行一个初步整理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
unsigned char v21[
256
]
=
{
0
};
for
(
int
i
=
0
; i <
256
; i
+
+
)
v21[i]
=
i;
int
v7
=
0
;
int
v8
=
0
;
int
v9
=
0
;
int
v10
=
0
;
char v11
=
0
;
for
(
int
i
=
0
; i <
256
; i
+
+
)
{
v9
=
v21[i];
v10
=
v9
+
v7;
v10
%
=
256
;
v11
=
v21[v10];
v21[v10]
=
v9;
v21[i]
=
v11;
v7
=
v10;
}
|
这样看依然不是很清晰,所以进一步整理:
这时可以看出是 v10 每次加上v21[i],避免v10作为下标时越界又与256取余,随后利用一个中间变量交换v21[v10]和v21[i]的值,实际上这一部分的结果是固定的。
1
2
3
4
5
6
7
8
9
|
int
v_tmp
=
0
;
for
(
int
i
=
0
; i <
256
; i
+
+
)
{
v10
+
=
v21[i];
v10
%
=
256
;
v_tmp
=
v21[v10];
v21[v10]
=
v21[i];
v21[i]
=
v_tmp;
}
|
第二次循环加密
整个循环中不好分析的难点在于 v15 = v12 + 1 - ((v12 + ((unsigned int)((v12 + 1) >> 31) >> 24) + 1) & 0xFFFFFF00);
这里做下该语句的简要分析:
(v12+1)>>31
因为v12是有符号类型,也就是说该结果不是0就是0xffffffff, 关键在于v12的最高位判断
而 v12 的值来自于 v15,纵观整个循环 v15 又来自 v12,看似互相套娃但由于v15和v12的初值都为0
所以在循环量较小的时候 v12 的高位必然为0,所以 ((v12 + ((unsigned int)((v12 + 1) >> 31) >> 24) + 1) & 0xFFFFFF00) = 0
那么看似复杂的第一行代码可以直接简化为 v15 = v12 + 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
v12
=
0
;
v13
=
0
;
v14
=
user_name_length_add_80;
do
{
v15
=
v12
+
1
-
((v12
+
((unsigned
int
)((v12
+
1
) >>
31
) >>
24
)
+
1
) &
0xFFFFFF00
);
v16
=
(unsigned __int8)v21[v15];
v13
=
(v16
+
v13)
%
256
;
v17
=
v14;
v18
=
v21[v13];
v21[v13]
=
v16;
v21[v15]
=
v18;
*
v_dest
+
+
^
=
v21[(unsigned __int8)(v21[v13]
+
v18)];
v14
=
v17
-
1
;
v19
=
v17
=
=
1
;
v12
=
v15;
}
|
随后将代码进一步进行化简,执行流程就更为清晰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
char tmp
=
0
;
do
{
/
/
取出 v13 和 v15,v13
+
=
v21[v15],为避免越界v13作为下标越界,模
256
取余数
v15
+
+
;
v13
=
(v21[v15]
+
v13)
%
256
;
/
/
交换 v21[
13
] 和 v21[
15
]
tmp
=
v21[v13];
v21[v13]
=
v21[v15];
v21[v15]
=
tmp;
/
/
v21[
13
]
+
v21[
15
] 作为下标
id
,为避免越界,类型强转为unsigned char使其不超过
255
/
/
*
hash_temp ^
=
v21[
id
]; hash_temp
+
+
;
*
hash_temp
+
+
^
=
v21[(unsigned char)(v21[v13]
+
v21[v15])];
/
/
v14是 user_name_length_add_80
=
0x60
v14
-
-
;
}
while
(v14!
=
0
);
|
由于已经详细的分析了 generate2, 简单观察便可知check2只是其流程的逆运算,这里附上一些分析代码片段:
代码片段1
1
2
3
4
5
6
7
8
9
10
|
v21
=
encrypt_4(pass_word, pass_word_len, (BYTE
*
)&v32,
16
, v33);
/
/
/
/
encrypt_4 是已经分析过的,但在解密时重新调用了一遍,这时再分析一遍该函数可以发现
/
/
核心算法是和一串固定的字节数组异或,这里第二次异或可以还原之前的值,作为解密
v23
=
v19;
*
(_QWORD
*
)v34
=
*
(_QWORD
*
)v33;
/
/
这里可以看到将 encrypt_4 的结果 v33 赋值给 v34,显然这就是对应加密时的逆运算
*
(_QWORD
*
)&v34[
8
]
=
*
(_QWORD
*
)&v33[
8
];
*
(_QWORD
*
)&v34[
16
]
=
*
(_QWORD
*
)&v33[
16
];
*
(_QWORD
*
)&v34[
24
]
=
*
(_QWORD
*
)&v33[
24
];
*
(_QWORD
*
)&v34[
32
]
=
*
(_QWORD
*
)&v33[
32
];
*
(_QWORD
*
)&v34[
40
]
=
*
(_QWORD
*
)&v33[
40
];
|
代码片段2
1
2
3
4
5
6
7
|
do
{
v15
=
v7 ^ v24[((_BYTE)v7
-
1
) &
0xF
];
/
/
这里也是,重新审视一下这个循环算法,就是异或,这里第二次调用作为解密算法
v5[v7
-
1
]
=
__ROL1__(v15 ^ v33[v7
+
63
],
4
)
-
v15;
-
-
v7;
}
while
( v7 >
0
);
|
代码片段3
1
2
3
4
5
6
|
v21
=
v6
-
80
;
v16
=
Decrypt((
int
)v34, (
int
)&v34[
32
],
12
, (
int
)v31,
27
, (
int
)&v5[v20
-
16
],
16
, (
int
)v5, v6
-
80
, v24);
/
/
解密数据,解密后的数据放在v24中
v4
=
0
;
if
( v16 && v21
=
=
user_name_len && !memcmp(v24, user_name, user_name_len) )
/
/
校验用户名和解密后的数据,一致返回
1
return
1
;
|
代码片段4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
int
__cdecl Decrypt(
int
key,
int
a2,
int
a3,
int
a4,
int
a5,
int
a6,
int
a7,
int
a8,
int
a9, void
*
dest)
{
int
ctx;
/
/
esi
int
v11;
/
/
eax
int
v12;
/
/
edi
size_t v14;
/
/
[esp
+
4h
] [ebp
-
418h
] BYREF
char src[
1024
];
/
/
[esp
+
8h
] [ebp
-
414h
] BYREF
unsigned
int
v16;
/
/
[esp
+
408h
] [ebp
-
14h
]
v16
=
__readgsdword(
0x14u
);
ctx
=
EVP_CIPHER_CTX_new();
v11
=
EVP_aes_256_gcm();
EVP_DecryptInit_ex(ctx, v11,
0
,
0
,
0
);
/
/
大致浏览一下参数与和之前的基本一致,只是调用的是解密函数
EVP_CIPHER_CTX_ctrl(ctx,
9
, a3,
0
);
EVP_DecryptInit_ex(ctx,
0
,
0
, key, a2);
if
( a5 )
EVP_DecryptUpdate(ctx,
0
, &v14, a4, a5);
EVP_DecryptUpdate(ctx, src, &v14, a8, a9);
memcpy(dest, src, v14);
/
/
这里也是一样,解密后的数据放到最后一个参数地址中
EVP_CIPHER_CTX_ctrl(ctx,
17
, a7, a6);
v12
=
EVP_DecryptFinal_ex(ctx, src, &v14);
EVP_CIPHER_CTX_free(ctx);
return
v12;
}
|
check2 按钮总结
分析完 check2 按钮后,发现相比较 generate2 缺少一个流程,那就是求用户名 hash 作为key,于是回溯了一下这个 key,通过追溯发现,这个 key 已经通过两个异或加密函数加密在了 password 中,只需要将其逆运算取出来即可使用。
在分析按钮 generate3 时很多数据静态分析时并不存在,所以这里还需要分析 _init 函数,该函数地址并没有被 IDA 分析出来,可以通过以下两个办法找到:
使用 readelf 工具查看
1
|
.\readelf.exe
-
d C:\Users\Administrator\Desktop\libnative
-
lib.so
|
使用 IDA 动态调试,在被写入数据的地址下内存写入断点
这个函数就是 _init 函数,其主要分为三个函数,分别是 set_buff1、 set_buff2、 set_buff3
这个函数首先将 19ca9c + image_base 存到 buff1_60[0-3] 中,根据后面的分析结果这里存的是一个函数地址,之后在 buff1_60[28-43] 中填入了一些随机数,随后调用了 sub_48840,并传入了一个字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
BYTE
*
__cdecl set_buff1(BYTE
*
buff1)
{
*
(_DWORD
*
)buff1
=
&off_19CA9C;
/
/
buff1_60[
0
-
3
]
=
19ca9c
+
image_base
*
((_DWORD
*
)buff1
+
5
)
=
0
;
/
/
buff1_60[
16
-
27
]
=
0
*
((_DWORD
*
)buff1
+
4
)
=
0
;
*
((_DWORD
*
)buff1
+
6
)
=
0
;
*
((_DWORD
*
)buff1
+
12
)
=
0
;
/
/
buff1_60[
44
-
59
]
=
0
*
((_DWORD
*
)buff1
+
11
)
=
0
;
*
((_DWORD
*
)buff1
+
14
)
=
0
;
*
((_DWORD
*
)buff1
+
13
)
=
0
;
v1
=
lrand48();
/
/
buff1_60[
28
-
43
]
=
rand()
buff1[
28
]
=
v1
+
v1
/
255
;
v2
=
lrand48();
......
buff1[
42
]
=
v15
+
v15
/
255
;
v16
=
lrand48();
buff1[
43
]
=
v16
+
v16
/
-
16777216
;
return
sub_48840(buff1
+
16
,
"3390fd362dfdda0030d5737632d3d213"
,
32u
);
/
/
/
/
buff1[
0
-
4
]
19ca9c
+
image_base
/
/
buff1[
16
-
19
] 存放着申请堆空间大小
+
1
,也就是
49
/
/
buff1[
20
-
23
] 存放着字符串
"3390fd362dfdda0030d5737632d3d213"
的长度,也就是
32
/
/
buff1[
24
-
27
] 存放着字符串
"3390fd362dfdda0030d5737632d3d213"
的指针
/
/
buff1[
28
-
43
] 存放着随机数
}
|
该函数主要是一些范围的判断,主要还是调用函数 sub_488E0,并传入了一个字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
BYTE
*
__cdecl sub_48840(BYTE
*
buff1_16, void
*
src, size_t len_32)
{
v3
=
*
buff1_16;
v4
=
10
;
if
( (v3 &
1
) !
=
0
)
/
/
0
&
1
!
=
0
判断失败
v4
=
(
*
(_DWORD
*
)buff1_16 &
0xFFFFFFFE
)
-
1
;
if
( v4 >
=
len_32 )
/
/
10
>
=
32
判断失败
{
......
}
else
{
if
( (v3 &
1
) !
=
0
)
/
/
判断失败
v5
=
*
((_DWORD
*
)buff1_16
+
1
);
else
v5
=
v3 >>
1
;
/
/
v5
=
0
sub_488E0(buff1_16, v4, len_32
-
v4, v5,
0
, v5, len_32, src);
/
/
/
/
sub_488e0(buff1_16,
10
,
22
,
0
,
0
,
0
,
32
,
"3390fd362dfdda0030d5737632d3d213"
)
/
/
buff1_16[
0
-
3
] 存放着申请堆空间大小
+
1
,也就是
49
/
/
buff1_16[
4
-
7
] 存放着字符串
"3390fd362dfdda0030d5737632d3d213"
的长度,也就是
32
/
/
buff1_16[
8
-
11
] 存放着字符串
"3390fd362dfdda0030d5737632d3d213"
的指针
}
return
buff1_16;
}
|
这里就是 buff1 真正调用的函数,作用是初始化 buff1 空间中的数据
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
|
/
/
sub_488e0(buff1_60_16,
10
,
22
,
0
,
0
,
0
,
32
,
"3390fd362dfdda0030d5737632d3d213"
)
BYTE
*
__cdecl sub_488E0(BYTE
*
buff1_60_16,unsigned
int
a2,unsigned
int
a3,
int
a4,size_t n,
int
a6,size_t a7,void
*
src)
{
if
(
0xFFFFFFEE
-
a2 < a3 )
/
/
这里注意a2的类型是无符号类型,
-
18
会转换成一个非常大的正数,所以判断失败
sub_48A20();
if
( (
*
buff1_16 &
1
) !
=
0
)
/
/
buff1_16
=
=
0
,判断失败
v8
=
(BYTE
*
)
*
((_DWORD
*
)buff1_16
+
2
);
else
v8
=
buff1_16
+
1
;
/
/
v8
=
buff1_17
v18
=
v8;
/
/
v18
=
buff1_17
v9
=
-
17
;
/
/
v9
=
-
17
if
( a2 <
=
0x7FFFFFE6
)
/
/
10
<
=
0x7ffffff6
,判断成功
{
v10
=
a2
+
a3;
/
/
v10
=
32
; a2
=
10
; a3
=
22
if
( a2
+
a3 <
2
*
a2 )
/
/
10
+
22
<
10
*
2
=
false, 判断失败
v10
=
2
*
a2;
v9
=
11
;
/
/
v9
=
11
if
( v10 >
=
0xB
)
/
/
32
>
=
0xb
,判断成功
v9
=
(v10
+
16
) &
0xFFFFFFF0
;
/
/
v9
=
48
}
v20
=
v9;
/
/
v20
=
48
buff_48
=
(char
*
)operator new(v9);
/
/
申请了
48
个字节的空间
buff_48_
=
buff_48;
if
( n )
memcpy(buff_48, v18, n);
buff_48__
=
buff_48_;
v13
=
a6;
/
/
v13
=
0
v14
=
a7;
/
/
v14
=
a7
=
32
if
( a7 )
{
memcpy(&buff_48_[n], src, a7);
/
/
buff_48[
0
-
31
]
=
"3390fd362dfdda0030d5737632d3d213"
v13
=
a6;
/
/
v13
=
0
v14
=
a7;
/
/
v14
=
32
}
v15
=
a4
-
v13;
/
/
v15
=
0
-
0
=
0
if
( a4
-
v13 !
=
n )
/
/
0
-
0
!
=
0
该判断失败
{
memcpy(&buff_48__[n
+
v14], &v18[n
+
a6], v15
-
n);
v14
=
a7;
}
if
( a2 !
=
10
)
/
/
10
!
=
10
判断失败
{
operator delete(v18);
v14
=
a7;
}
result
=
buff1_16;
*
((_DWORD
*
)buff1_16
+
2
)
=
buff_48__;
/
/
buff1_16[
8
-
11
]
=
buff_48;注意这里是把申请空间的地址放在了这里
/
/
buff1_16[
8
-
11
] 存放了一个堆空间的指针,该指针指向字符串
"3390fd362dfdda0030d5737632d3d213"
*
(_DWORD
*
)buff1_16
=
v20 |
1
;
/
/
buff1_16[
0
-
3
]
=
0x00000031
/
/
小端存放 buff1_16[
0
-
3
]
=
{
0x31
,
00
,
00
,
00
}
v17
=
v14
+
v15;
/
/
v17
=
32
*
((_DWORD
*
)buff1_16
+
1
)
=
v17;
/
/
buff1_16[
4
-
7
]
=
0x00000032
/
/
buff1_16[
4
-
7
]
=
{
0x20
,
0
,
0
,
0
}
buff_48__[v17]
=
0
;
/
/
字符串
"3390fd362dfdda0030d5737632d3d213"
后添加
0
确保截断
return
result;
/
/
总结一下:
/
/
buff1_16[
0
-
3
] 存放着申请堆空间大小
+
1
,也就是
49
/
/
buff1_16[
4
-
7
] 存放着字符串
"3390fd362dfdda0030d5737632d3d213"
的长度,也就是
32
/
/
buff1_16[
8
-
11
] 存放着字符串
"3390fd362dfdda0030d5737632d3d213"
的指针
}
|
这个函数比较浅,主要就是计算随机数的 hash,然后初始化 buff2 的内存数据,结果是:
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
|
unsigned
int
__cdecl set_buff2(BYTE
*
buff2)
{
v31
=
__readgsdword(
0x14u
);
*
(_DWORD
*
)buff2
=
&off_19CAAC;
/
/
buff2[
0
-
3
]
=
19aac
+
imagebase
*
((_DWORD
*
)buff2
+
2
)
=
0
;
/
/
buff2[
4
-
19
]
=
0
*
((_DWORD
*
)buff2
+
1
)
=
0
;
*
((_DWORD
*
)buff2
+
4
)
=
0
;
*
((_DWORD
*
)buff2
+
3
)
=
0
;
memcpy(dest, byte_151F3A, sizeof(dest));
/
/
把固定地址的数据拷贝至dest,这里暂时将这串数据记作const_data
memcpy(buff2
+
20
, dest,
512u
);
/
/
buff2[
20
-
531
]
=
const_data
RAND_bytes(v29,
32
);
/
/
生成
32
字节大小的随机数
v1
=
v29[
3
];
/
/
v1
=
v29[
3
]
v28[
3
]
=
v29[
3
];
v2
=
v29[
2
];
/
/
v2
=
v29[
2
]
v28[
2
]
=
v29[
2
];
v3
=
v29[
1
];
/
/
v3
=
v29[
1
]
v28[
1
]
=
v29[
1
];
v28[
0
]
=
v29[
0
];
/
/
v28[
0
-
3
]
=
v29[
0
-
3
]
*
(_QWORD
*
)(buff2
+
532
)
=
v29[
0
];
/
/
buff2[
532
-
539
]
=
v29[
0
]
*
(_QWORD
*
)(buff2
+
540
)
=
v3;
/
/
buff2[
540
-
547
]
=
v3
*
(_QWORD
*
)(buff2
+
548
)
=
v2;
/
/
buff2[
548
-
555
]
=
v2
*
(_QWORD
*
)(buff2
+
556
)
=
v1;
/
/
buff2[
556
-
563
]
=
v1
*
(_QWORD
*
)(buff2
+
588
)
=
v28[
3
];
/
/
buff2[
588
-
595
]
=
v1
v4
=
v28[
1
];
*
(_QWORD
*
)(buff2
+
564
)
=
v28[
0
];
/
/
buff2[
564
-
571
]
=
v29[
0
]
*
(_QWORD
*
)(buff2
+
580
)
=
v28[
2
];
/
/
buff2[
580
-
587
]
=
v2
*
(_QWORD
*
)(buff2
+
572
)
=
v4;
/
/
buff2[
572
-
579
]
=
v3
/
/
小结一下:
/
/
buff2[
532
-
563
]
=
buff2[
564
-
595
] 是两组相同的随机数
v5
=
-
64
;
v6
=
0
;
do
v6
=
*
(_WORD
*
)&buff2[
2
*
(buff2[v5
+
+
+
596
] ^ HIBYTE(v6))
+
20
] ^ (v6 <<
8
);
while
( v5 );
/
/
观察这个公式,buff2[v5
+
+
+
596
]指定就是最后的
64
个随机字节,v5
+
+
依次取出,这里将这个值记作rand[i]
/
/
观察buff2[
2
*
(rand[i] ^ HIBYTE(v6))
+
20
],对照当前 buff2 的内存布局,这指的就是之前的 const_data
/
/
也明白了buff2的空间为什么是
596
这个奇怪的大小,它的组成是
20
+
256
*
2
+
64
/
/
需要注意的是最后使用了
*
(WORD
*
)&,也就是 const_data 被当做WORD取出
/
/
/
/
小结一下:
/
/
随机字节和v6异或也是随机的,就记为随机
/
/
循环将 const_data 以 WORD 类型随机取出,再和上一次结果的高位八位异或
/
/
总的来讲就是得到了一个随机数 v6
v27
=
v6;
*
(_WORD
*
)((char
*
)v28
+
5
)
=
v6;
/
/
这里修改了v28的第
5
-
6
字节为v6
hmac_ctx_hash((BYTE
*
)v28, (BYTE
*
)v29, buff2
+
532
);
/
/
这是一个已经分析过的函数,
hash
和data没有关系
/
/
把随机数 v29 作为key,
hash
结果放在buff2[
532
-
595
]中
v7
=
-
32
;
v8
=
0
;
do
v8
=
*
(_WORD
*
)&buff2[
2
*
(
*
((unsigned __int8
*
)v29
+
v7
+
+
) ^ HIBYTE(v8))
+
20
] ^ (v8 <<
8
);
while
( v7 );
/
/
这个循环和上边差不多,无非是随机数在v29这个随机字节数组中取,最终得到一个随机数 v8
v9
=
lrand48()
%
12
;
/
/
v9 是个
0
-
11
范围内的随机数
*
(_WORD
*
)&buff2[v9
+
532
]
=
v8;
/
/
随机数 v8 被随机的写在 buff2[
532
-
544
]中,注意 v8 是 WODD 类型
*
(_WORD
*
)&buff2[v9
+
534
]
=
v27;
/
/
一个
0
被随机的写在 buff2[
534
-
546
]中,注意
0
是 WODD 类型
v10
=
_mm_xor_ps(
*
(__m128
*
)(buff2
+
36
), (__m128)xmmword_148490);
*
(__m128
*
)(buff2
+
20
)
=
_mm_xor_ps(
*
(__m128
*
)(buff2
+
20
), (__m128)xmmword_148490);
/
/
buff2[
20
-
35
] ^
=
0x00360036003600360036003600360036
*
(__m128
*
)(buff2
+
36
)
=
v10;
/
/
buff2[
36
-
51
] ^
=
0x00360036003600360036003600360036
v11
=
_mm_xor_ps(
*
(__m128
*
)(buff2
+
68
), (__m128)xmmword_148490);
*
(__m128
*
)(buff2
+
52
)
=
_mm_xor_ps(
*
(__m128
*
)(buff2
+
52
), (__m128)xmmword_148490);
/
/
/
/
发现了规律,其实就是,buff2[
20
-
532
]
2
字节一组与
0x0036
异或,并更新buff2中的值
/
/
在这里总结一下:
/
/
buff2[
0
-
3
]
=
19aac
+
imagebase
/
/
buff2[
4
-
19
]
=
0
/
/
buff2[
20
-
531
]
=
const_data ^
0x0036
/
/
buff2[
532
-
595
]
=
hash
......
v25
=
_mm_xor_ps(
*
(__m128
*
)(buff2
+
516
), (__m128)xmmword_148490);
*
(__m128
*
)(buff2
+
500
)
=
_mm_xor_ps(
*
(__m128
*
)(buff2
+
500
), (__m128)xmmword_148490);
*
(__m128
*
)(buff2
+
516
)
=
v25;
return
__readgsdword(
0x14u
);
}
|
基本上和 set_buff2 如出一辙,调用了一个新的函数 get_hash2,不过和之前求hash函数的也是基本一致,这里直接列出 buff3 内存布局:
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
|
unsigned
int
__cdecl set_buff3(BYTE
*
buff3)
{
v32
=
__readgsdword(
0x14u
);
*
(_DWORD
*
)buff3
=
&off_19CA8C;
/
/
buff3[
0
-
3
]
=
19ca8c
+
image_base
*
((_DWORD
*
)buff3
+
18
)
=
0
;
/
/
buff3[
68
-
83
]
=
0
*
((_DWORD
*
)buff3
+
17
)
=
0
;
*
((_DWORD
*
)buff3
+
20
)
=
0
;
*
((_DWORD
*
)buff3
+
19
)
=
0
;
memcpy(dest, &unk_1485A0, sizeof(dest));
/
/
把固定地址的数据拷贝至dest,这里暂时将这串数据记作const_data
memcpy(buff3
+
84
, dest,
512u
);
/
/
buff3[
84
-
595
]
=
const_data
RAND_bytes(key,
32
);
/
/
生成
32
字节大小的随机数
v1
=
*
(_QWORD
*
)&key[
24
];
/
/
v1
=
key[
24
-
31
]
*
(_QWORD
*
)&data[
24
]
=
*
(_QWORD
*
)&key[
24
];
v2
=
*
(_QWORD
*
)&key[
16
];
/
/
v2
=
v32[
16
-
23
]
*
(_QWORD
*
)&data[
16
]
=
*
(_QWORD
*
)&key[
16
];
v3
=
*
(_QWORD
*
)&key[
8
];
/
/
v3
=
key[
8
-
15
]
*
(_QWORD
*
)&data[
8
]
=
*
(_QWORD
*
)&key[
8
];
*
(_QWORD
*
)data
=
*
(_QWORD
*
)key;
/
/
data[
0
-
31
]
=
key[
0
-
31
]
*
(_QWORD
*
)(buff3
+
4
)
=
*
(_QWORD
*
)key;
/
/
buff3[
4
-
11
]
=
key[
0
-
7
]
*
(_QWORD
*
)(buff3
+
12
)
=
v3;
/
/
buff3[
12
-
19
]
=
v3
*
(_QWORD
*
)(buff3
+
20
)
=
v2;
/
/
buff3[
20
-
27
]
=
v2
*
(_QWORD
*
)(buff3
+
28
)
=
v1;
/
/
buff3[
28
-
35
]
=
v1
*
(_QWORD
*
)(buff3
+
60
)
=
*
(_QWORD
*
)&data[
24
];
/
/
buff3[
60
-
67
]
=
v1
v4
=
*
(_QWORD
*
)&data[
8
];
*
(_QWORD
*
)(buff3
+
36
)
=
*
(_QWORD
*
)data;
/
/
buff3[
36
-
43
]
=
key[
0
-
7
]
*
(_QWORD
*
)(buff3
+
52
)
=
*
(_QWORD
*
)&data[
16
];
/
/
buff3[
52
-
59
]
=
v2
*
(_QWORD
*
)(buff3
+
44
)
=
v4;
/
/
buff3[
44
-
51
]
=
v3
/
/
小结:
/
/
buff3[
4
-
35
]
=
buff3[
36
-
67
] 是两组相同的随机数
v5
=
-
64
;
v6
=
0
;
do
v6
=
*
(_WORD
*
)&buff3[
2
*
(buff3[v5
+
+
+
68
] ^ HIBYTE(v6))
+
84
] ^ (v6 <<
8
);
/
/
这里和 setbuff2 基本一致,总的来讲就是得到了一个随机数 v6
while
( v5 );
v28
=
v6;
*
(_WORD
*
)&data[
5
]
=
v6;
/
/
data 的第
5
-
6
字节被赋值为v6
get_hash2(v27, (
int
)data, (
int
)key, buff3
+
4
);
/
/
buff3[
4
-
67
]
=
hash
v7
=
-
32
;
v8
=
0
;
do
v8
=
*
(_WORD
*
)&buff3[
2
*
(key[v7
+
+
] ^ HIBYTE(v8))
+
84
] ^ (v8 <<
8
);
/
/
通过 key 得到一个随机的 v8
while
( v7 );
v9
=
lrand48()
%
30
;
/
/
v9 一个范围在
0
-
29
的随机数
*
(_WORD
*
)&buff3[v9
+
4
]
=
v8;
/
/
buff3[
4
-
31
] 随机位置被赋值 v8
*
(_WORD
*
)&buff3[v9
+
6
]
=
v28;
/
/
buff3[
6
-
35
]随机位置被赋值v6
v10
=
_mm_xor_ps(
*
(__m128
*
)(buff3
+
100
), (__m128)xmmword_1483D0);
/
/
buff3[
84
-
595
]
2
字节一组与
0x0042
异或,并更新buff3中的值
/
/
总结:
/
/
buff3[
0
-
3
]
=
19ca8c
+
image_base
/
/
buff3[
4
-
67
]
=
hash
/
/
buff3[
68
-
83
]
=
0
/
/
buff3[
84
-
595
]
=
const_data ^
0x0042
......
v25
=
_mm_xor_ps(
*
(__m128
*
)(buff3
+
580
), (__m128)xmmword_1483D0);
*
(__m128
*
)(buff3
+
564
)
=
_mm_xor_ps(
*
(__m128
*
)(buff3
+
564
), (__m128)xmmword_1483D0);
*
(__m128
*
)(buff3
+
580
)
=
v25;
return
__readgsdword(
0x14u
);
}
|
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
|
int
sub_41BA0()
{
buff1
=
(BYTE
*
)operator new(
60u
);
set_buff1(buff1);
/
/
buff1[
0
-
4
]
19ca9c
+
image_base
/
/
buff1[
16
-
19
] 存放着申请堆空间大小
+
1
,也就是
49
/
/
buff1[
20
-
23
] 存放着字符串
"3390fd362dfdda0030d5737632d3d213"
的长度,也就是
32
/
/
buff1[
24
-
27
] 存放着字符串
"3390fd362dfdda0030d5737632d3d213"
的指针
/
/
buff1[
28
-
43
] 存放着随机数
dword_1A50B0
=
(
int
)buff1;
/
/
buff1 的地址被记录在
1a50b0
+
imagebase中
buff2
=
(BYTE
*
)operator new(
596u
);
set_buff2(buff2);
/
/
buff2[
0
-
3
]
=
19aac
+
imagebase
/
/
buff2[
4
-
19
]
=
0
/
/
buff2[
20
-
531
]
=
const_data1 ^
0x0036
/
/
buff2[
532
-
596
]
=
随机字节
dword_1A50B4
=
(
int
)buff2;
/
/
buff2 的地址被记录在
1a50b4
+
imagebase中
buff3
=
(BYTE
*
)operator new(
596u
);
set_buff3((
int
)buff3);
/
/
buff3[
0
-
3
]
=
19ca8c
+
image_base
/
/
buff3[
4
-
67
]
=
hash
/
/
buff3[
68
-
83
]
=
0
/
/
buff3[
84
-
595
]
=
const_data2 ^
0x0042
dword_1A50B8
=
(
int
)buff3;
/
/
buff3 的地址被记录在
1a50b8
+
imagebase中
dword_1A50C0
=
dword_1A50B0;
result
=
dword_1A50B4;
dword_1A50C4
=
dword_1A50B4;
dword_1A50C8
=
(
int
)buff3;
/
/
buff1、buff2、buff3的地址又依次被记录在
/
/
1a50c0
、
1a50c4
、
1a50c8
中也就是说有两处位置记录这三个堆空间
return
result;
}
|
可以轻易发现,主要是调用了 MainActivity.this.getbt 方法
1
2
3
4
5
6
7
8
9
10
11
12
|
class
f implements View.OnClickListener {
f() {
}
@Override
/
/
android.view.View.OnClickListener
public void onClick(View view) {
String obj
=
MainActivity.this.y.getText().toString();
MainActivity.this.z.getText().toString();
......
MainActivity.this.z.setText(com.test.pac.demo.a.a.a(MainActivity.this.getbt(obj.getBytes(),
3
)));
}
}
|
这里的 fun3 就是 buff3 的前四个字节指向的地址(19ca8c + image_base),整个按钮 generate3 的真正核心逻辑自此真正展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
jbyteArray __cdecl Java_com_test_pac_demo_MainActivity_getbt(JNIEnv
*
env, jobject a2, void
*
user_name_,
int
num_3)
{
v12
=
__readgsdword(
0x14u
);
user_name_len
=
(
*
env)
-
>GetArrayLength(env, user_name_);
user_name
=
(BYTE
*
)(
*
env)
-
>GetByteArrayElements(env, user_name_,
0
);
dword_1A50BC[
0
]
=
dword_1A50BC[num_3];
fun3
=
(
int
(__cdecl
*
*
*
)(_DWORD, BYTE
*
,
int
, char
*
))dword_1A50BC[
0
];
memset(password,
0
, sizeof(password));
len
=
(
*
*
fun3)(fun3, user_name, user_name_len, password);
v6
=
(
*
env)
-
>NewByteArray(env,
len
);
(
*
env)
-
>SetByteArrayRegion(env, v6,
0
,
len
, password);
(
*
env)
-
>ReleaseByteArrayElements(env, user_name_, (jbyte
*
)user_name,
0
);
return
v6;
}
|
函数 fun3 的大致逻辑如下:
主要功能为设置 dest 区域的内存数据
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
|
int
__cdecl sub_425F0(
int
a1,
int
hash
,
int
iv,
int
num_12,
int
username_reverse,
int
username_reverse_len,
int
key_hash,
int
key_hash_len,char
*
dest)
{
int
ctx;
/
/
esi
int
cipher;
/
/
eax
size_t v11;
/
/
eax
__int64 v12;
/
/
xmm1_8
int
v14;
/
/
[esp
+
0h
] [ebp
-
41Ch
] BYREF
size_t
len
;
/
/
[esp
+
4h
] [ebp
-
418h
] BYREF
BYTE outbuff[
1024
];
/
/
[esp
+
8h
] [ebp
-
414h
] BYREF
unsigned
int
v17;
/
/
[esp
+
408h
] [ebp
-
14h
]
v17
=
__readgsdword(
0x14u
);
ctx
=
EVP_CIPHER_CTX_new();
cipher
=
EVP_aes_256_gcm();
EVP_EncryptInit_ex(ctx, cipher,
0
,
0
,
0
);
/
/
初始化 ctx
EVP_CIPHER_CTX_ctrl(ctx,
9
, num_12,
0
);
/
/
# define EVP_CTRL_AEAD_SET_IVLEN 0x9
/
/
设置向量IV的长度为
12
EVP_EncryptInit_ex(ctx,
0
,
0
,
hash
, iv);
/
/
使用
hash
作为 key 初始化 ctx
EVP_EncryptUpdate(ctx,
0
, (
int
)&
len
, username_reverse, username_reverse_len);
/
/
输出地址为
0
,也就是说username没有参与加密
EVP_EncryptUpdate(ctx, (
int
)outbuff, (
int
)&
len
, key_hash, key_hash_len);
memcpy(dest, outbuff,
len
);
/
/
dest[
0
-
1
]
=
encrydata[
0
-
1
]
EVP_EncryptFinal_ex(ctx, outbuff, &v14);
EVP_CIPHER_CTX_ctrl(ctx,
16
,
16
, (
int
)outbuff);
/
/
获取
16
字节长度的 tag
v11
=
len
;
/
/
v11
=
2
v12
=
*
(_QWORD
*
)&outbuff[
8
];
/
/
v12
=
tag[
8
-
15
]
*
(_QWORD
*
)&dest[
len
]
=
*
(_QWORD
*
)outbuff;
/
/
dest[
2
-
9
]
=
tag[
0
-
7
]
*
(_QWORD
*
)&dest[v11
+
8
]
=
v12;
/
/
dest[
10
-
17
]
=
tag[
8
-
15
]
EVP_CIPHER_CTX_free(ctx);
/
/
总结:
/
/
dest[
0
-
1
]
=
encrydata[
0
-
1
]
/
/
dest[
2
-
9
]
=
tag[
0
-
7
]
/
/
dest[
10
-
17
]
=
tag[
8
-
15
]
return
len
+
16
;
}
|
主要功能为设置 src 区域的内存数据,并返回一个影响密码长度的值,另外我记着在分析这个函数的时候遇到了阻碍 IDA 分析的一连串 nop,我的解决方法是把 nop 改为 mov eax, eax ,然后依次使用快捷键 u 、c、p、f5,重新识别。
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
|
size_t __cdecl sub_43070(_DWORD
*
fun3, _BYTE
*
password, size_t num_20, _BYTE
*
src)
{
v22
=
__readgsdword(
0x14u
);
do
v4
=
get_hash3_and_change_fun3(fun3);
/
/
这里把结果的低两字节放到了v4中
/
/
此处通过位移修改了 fun3 中的数据
while
( (((_BYTE)v4
-
8
) &
0xC0
)
=
=
0
);
/
/
循环判断 v4
-
8
后[
2
-
3
]位的值是否为 C
/
/
直到不为C结束
*
(_WORD
*
)src
=
v4;
src[
2
]
=
BYTE1(num_20);
/
/
src[
2
]
=
n的高位
=
0
src[
3
]
=
num_20;
/
/
src[
3
]
=
20
memcpy(src
+
4
, password, num_20);
/
/
src[
4
-
23
]
=
password
v5
=
(unsigned __int16)(num_20
+
4
);
/
/
v5
=
24
v6
=
32
-
num_20;
/
/
v6
=
12
if
(
300
-
v5 >
32
-
num_20 )
v6
=
300
-
v5;
/
/
v6
=
276
v16
=
1399
-
(v6
+
v5);
/
/
v16
=
1099
v7
=
get_hash3_and_change_fun3(fun3);
/
/
hash3
v8
=
0
;
v18
=
v6
+
v7
%
v16
-
3
*
(((v6
+
v7
%
v16)
/
3
) &
0xFFFFFFF0
);
/
/
v18
=
276
+
v7
%
16
-
3
*
((
276
+
v7
%
1099
)
/
3
) &
0xfffffff0
v9
=
v18
+
32
;
/
/
v9
=
v18
+
32
memset(s,
0
, sizeof(s));
v17
=
v18
+
32
;
do
{
v10
=
v9
-
v8;
if
( v10 <
8
)
{
v19
=
get_hash3_and_change_fun3(fun3);
/
/
这里再次计算
hash
memcpy(&s[v8], &v19, v10);
/
/
将
hash
复制到数组 s 中
}
else
{
v11
=
get_hash3_and_change_fun3(fun3);
/
/
再次计算
hash
v19
=
v11;
*
(_DWORD
*
)&s[v8
+
4
]
=
HIDWORD(v11);
/
/
设置数组 s 的内容
*
(_DWORD
*
)&s[v8]
=
v11;
}
v8
+
=
8
;
/
/
v8
+
=
8
v9
=
v18
+
32
;
}
while
( v17 > v8 );
/
/
循环直到 v17 <
=
v8 结束
v12
=
num_20
+
v18;
memcpy(&src[num_20
+
4
], s, v17);
/
/
这里将 s 拷贝到 src
v13
=
*
(_QWORD
*
)&src[num_20
+
20
+
v18];
v20
=
*
(_QWORD
*
)&src[num_20
+
28
+
v18];
v19
=
v13;
sub_43990((
int
)fun3, (
int
)(src
+
2
), num_20
+
v18
+
18
, (unsigned
int
*
)&src[num_20
+
20
+
v18]);
/
/
通过一系列的变换,修改了src中的值
v14
=
v20;
*
(_QWORD
*
)&src[v12
+
20
]
=
v19;
/
/
src[
20
+
v18
+
20
]
=
src[num_20
+
20
+
v18]
*
(_QWORD
*
)&src[v12
+
28
]
=
v14;
/
/
src[
20
+
v18
+
28
]
=
src[num_20
+
28
+
v18]
return
num_20
+
v18
+
36
;
/
/
总结:
/
/
首先修改fun3的数据并计算出一个hash1,放到src[
0
-
1
]
/
/
再次修改fun3的数据并计算出一个hash2,通过hash2和一些常数的计算
/
/
得到贯穿整个函数的v18,在
43
-
60
行的代码中,反复修改fun3的数据并计算
hash
/
/
来设置数组 s, 随后将数组 s拷贝到 src 中,此时函数先将src的[
40
+
v18]到[
40
+
v18]保存了一份
/
/
调用函数 sub_43990 中通过一系列的位移变换,再次修改src中的数据
/
/
然后将保存的数据进行恢复,返回值为 v18
+
56
}
|
这个函数总的来讲就是不断的修改 fun3 区域内的数据并最后计算出了一个 hash
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
|
__int64 __cdecl sub_43D00(_DWORD
*
fun3)
{
v1
=
fun3[
17
];
v2
=
fun3[
18
];
if
(
*
(_QWORD
*
)(fun3
+
17
) )
{
v3
=
fun3[
20
];
v4
=
fun3[
19
];
}
else
{
gettimeofday(&tv,
0
);
v5
=
tv.tv_usec ^ (
*
(unsigned __int64
*
)&tv >>
9
) ^ HIDWORD(v31) ^ (HIDWORD(v31) >>
26
) ^ ((tv.tv_usec ^ (unsigned
int
)(
*
(unsigned __int64
*
)&tv >>
9
)) >>
17
);
v6
=
tv.tv_sec ^ (tv.tv_sec <<
23
) ^ v31 ^ (v31 >>
26
) ^ ((
*
(_QWORD
*
)&tv ^ (unsigned __int64)(
*
(_QWORD
*
)&tv <<
23
)) >>
17
);
v7
=
v31 ^ ((_DWORD)v31 <<
23
);
LODWORD(v31)
=
HIDWORD(v31) ^ (v31 >>
9
);
v8
=
((unsigned
int
)v31 >>
17
) ^ (v5 >>
26
) ^ v5 ^ v31;
......
LODWORD(v10)
=
v25 ^ (v25 <<
23
);
v4
=
v1 ^ (__PAIR64__(v2, v1) >>
26
) ^ v10 ^ (v10 >>
17
);
v3
=
v2 ^ (v2 >>
26
) ^ HIDWORD(v10) ^ (HIDWORD(v10) >>
17
);
fun3[
17
]
=
v1;
fun3[
18
]
=
v2;
fun3[
19
]
=
v4;
fun3[
20
]
=
v3;
}
v26
=
v2 ^ (__PAIR64__(v2, v1) >>
9
);
HIDWORD(v27)
=
(v3 >>
26
) ^ v26 ^ v3 ^ (v26 >>
17
);
HIDWORD(v28)
=
v26;
LODWORD(v28)
=
v1 ^ (v1 <<
23
);
LODWORD(v27)
=
(__PAIR64__(v3, v4) >>
26
) ^ (v28 >>
17
) ^ v4 ^ v28;
fun3[
17
]
=
v4;
fun3[
18
]
=
v3;
*
(_QWORD
*
)(fun3
+
19
)
=
v27;
return
__PAIR64__(v3, v4)
+
v27;
/
/
#define __PAIR__(high, low) (((unsigned long)(high)<<sizeof(high)*8) | low)
/
/
其实就是拼接成
64
位
8
字节的宏
/
/
接下来这些位运算居然还有奇数是最棘手的,想不到化简的办法
/
/
但可以知道,结果是fun3[
20
]和fun[
19
]拼接的值加上 v27
/
/
v27 这些位运算得来的,而整个函数其实就是在做一件事:
/
/
/
/
通过这些位移修改了 fun3[
68
-
80
]区间内的数据
/
/
/
/
/
/
/
/
v1
=
fun3[
17
];
/
/
/
/
v2
=
fun3[
18
];
/
/
/
/
v3
=
fun3[
20
];
/
/
/
/
v4
=
fun3[
19
];
/
/
/
/
v26
=
fun3[
18
] ^ (__PAIR64__(fun3[
18
], fun3[
17
]) >>
9
);
/
/
HIDWORD(v27)
=
(fun3[
20
] >>
26
) ^ v26 ^ fun3[
20
] ^ (v26 >>
17
);
/
/
HIDWORD(v28)
=
v26;
/
/
LODWORD(v28)
=
fun3[
17
] ^ (fun3[
17
] <<
23
);
/
/
LODWORD(v27)
=
(__PAIR64__(fun3[
20
], fun3[
19
]) >>
26
) ^ (v28 >>
17
) ^ fun3[
19
] ^ v28;
/
/
fun3[
17
]
=
fun3[
19
];
/
/
fun3[
18
]
=
fun3[
20
];
/
/
*
(_QWORD
*
)(fun3
+
19
)
=
v27;
/
/
return
__PAIR64__(fun3[
20
], fun[
19
])
+
v27;
}
|
这个函数主要就是在一个循环中通过一系列的变换,不断的在修改 src 中的数据
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
|
int
__cdecl sub_43990(
int
a1,
int
src_2,
int
a3, unsigned
int
*
a4)
{
......
do
{
v46
=
v42 ^ (__PAIR64__(v42, v43) >>
9
);
v47
=
v44;
v48
=
v43 ^ (v43 <<
23
);
v49
=
v45;
v55
=
v44;
v44 ^
=
v46 ^ (v46 >>
17
) ^ (v44 >>
26
);
v58
=
v45;
v45 ^
=
v48 ^ (__PAIR64__(v46, v48) >>
17
) ^ (__PAIR64__(v47, v45) >>
26
);
*
(_WORD
*
)(src_2
+
v60)
=
__PAIR16__(
((unsigned __int16)(v49
+
v45) >>
8
) ^
*
(_BYTE
*
)(src_2
+
v60
+
1
),
(v58
+
v45) ^
*
(_BYTE
*
)(src_2
+
v60));
*
(_BYTE
*
)(src_2
+
v60
+
2
) ^
=
(v49
+
v45) >>
16
;
*
(_BYTE
*
)(src_2
+
v60
+
3
) ^
=
(v49
+
v45) >>
24
;
v50
=
v55
+
__CFADD__(v58, v45)
+
v44;
*
(_WORD
*
)(src_2
+
v60
+
4
) ^
=
v50;
*
(_BYTE
*
)(src_2
+
v60
+
6
) ^
=
BYTE2(v50);
*
(_BYTE
*
)(src_2
+
v60
+
7
) ^
=
HIBYTE(v50);
v60
+
=
8
;
v43
=
v58;
v42
=
v55;
}
while
( v60 < a3 );
}
return
a3;
}
|
利用启动应用时初始化的数据,不断的求 hash、加密、更新数据区的内容,最终计算出 password
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
|
size_t __cdecl sub_42940(_DWORD
*
fun3, BYTE
*
user_name,
int
user_name_len, char
*
password)
{
v73
=
__readgsdword(
0x14u
);
v4
=
-
64
;
v5
=
0
;
do
v5
=
*
((_WORD
*
)fun3
+
(
*
((unsigned __int8
*
)fun3
+
v4
+
+
+
68
) ^ HIBYTE(v5))
+
42
) ^ (v5 <<
8
);
while
( v4 );
/
/
又见到了这个算法,这个算法已经出现了不止一次
/
/
不过我突然觉着这个算法可能没有我之前分析的那么简单
/
/
因为我发现对于整个算法来讲,最重要的是v5的高位
/
/
参与算法的并不是当前v5的低位,而是上一次 v5 的低位
/
/
这种情况可以将v5拆成两个uint8,以便更好的分析算法
/
/
这里使用 res 记录当前v5高位,old_data 记录上次 v5 低位
/
/
/
/
这里对这个算法进行重新分析:
/
/
将 v5 转为 uint8 类型后整个算法逐渐变的更加清晰
/
/
首先通过公式
id
=
(data[i] ^ v5)
*
2
+
84
得到
id
作为下一次从 data 中取数据的下标
/
/
随后通过
id
+
1
将高位取出,再和上一次的 v5 低位异或得到该轮循环的值
/
/
注意:这种情况最终的结果将不再是 uint16 类型,因为这里是为了更清晰的分析算法
/
/
如果需要,可以添加一个一直指向 res 的指针,最终取出 uint16 的数据
/
/
通过之前分析 buff3 的内存结构可以知道,data 的数据是应用启动时生成的一段随机
hash
/
/
这里返回的 v5 如果应用不重启,那么这个值是是固定的,如果应用重启这个值就会变化
/
/
/
/
int
id
=
0
;
/
/
unsigned char res
=
0
;
/
/
unsigned char old_data
=
0
;
/
/
/
/
for
(
int
i
=
4
; i <
68
; i
+
+
)
/
/
{
/
/
id
=
(data[i] ^ res)
*
2
+
84
;
/
/
res
=
data[
id
+
1
] ^ old_data;
/
/
old_data
=
data[
id
];
/
/
}
v62
=
v5;
/
/
v61
=
v5
*
((_WORD
*
)password
+
1
)
=
v5;
/
/
password[
2
-
3
]
=
v5
if
( user_name )
{
if
( user_name_len >
0
)
/
/
判断用户名长度大于
0
{
v6
=
0
;
if
( (unsigned
int
)user_name_len <
0x10
)
/
/
判断用户名长度小于
16
goto LABEL_33;
v6
=
user_name_len &
0xFFFFFFF0
;
/
/
v16
=
16
v7
=
0
;
/
/
v7
=
0
si128
=
_mm_load_si128((const __m128i
*
)(&(&off_1A25DC)[
-
11539
]
+
1
));
/
/
si128
=
0F0F
0F0F
0F0F
0F0F
0F0F
0F0F
0F0F
0F0F
v59
=
_mm_load_si128((const __m128i
*
)(&off_1A25DC
-
92307
));
/
/
v59
=
9F9F
9F9F
9F9F
9F9F
9F9F
9F9F
9F9F
9F9F
v9
=
_mm_load_si128((const __m128i
*
)(&(&off_1A25DC)[
-
11538
]
+
1
));
/
/
v9
=
3030
3030
3030
3030
3030
3030
3030
3030
v10
=
_mm_load_si128((const __m128i
*
)(&off_1A25DC
-
92299
));
/
/
v10
=
5757
5757
5757
5757
5757
5757
5757
5757
v11
=
_mm_load_si128((const __m128i
*
)(&(&off_1A25DC)[
-
11537
]
+
1
));
/
/
v11
=
0909
0909
0909
0909
0909
0909
0909
0909
do
{
v12
=
_mm_loadu_si128((const __m128i
*
)&user_name[v7]);
/
/
将 username 加载到寄存器 v12 中
/
/
v13
=
_mm_and_si128(_mm_srli_epi16(v12,
4u
), si128);
/
/
username分为
8
组
16bit
的数据,对它们分别逻辑右移
4
位,高位补零
/
/
再与上
0F0F
0F0F
0F0F
0F0F
0F0F
0F0F
0F0F
0F0F
/
/
v13
=
username>>
4
^si128
/
/
v14
=
_mm_cmpeq_epi8(_mm_min_epu8(v12, v59), v12);
/
/
username 和
9F9F
9F9F
9F9F
9F9F
9F9F
9F9F
9F9F
9F9F
的最小值
/
/
而字符串必然小于
9F
,所以v14
=
FFFFFFFFFFFFFFFF
/
/
v15
=
_mm_or_si128(_mm_andnot_si128(v14, _mm_add_epi8(v13, v10)), _mm_and_si128(_mm_or_si128(v13, v9), v14));
/
/
/
/
v15
=
(!v14 & (v13
+
v10))|((v13|v9)&v14)
/
/
注意 andnot 会将 v14 反转,后边的 v14 不再是ffff而是
0
/
/
稍微化简即可得到 v15
=
v13|v9
v16
=
_mm_and_si128(v12, si128);
/
/
v16
=
username & si128
v17
=
_mm_cmpeq_epi8(_mm_subs_epu8(v16, v11), (__m128i)
0LL
);
/
/
(v16分组减v11) 和
0
比较,v16 每个字节不会超过
0f
, v11 每个字节是
09
/
/
所以 v17 为 ff 和
00
组成
v18
=
_mm_or_si128(_mm_andnot_si128(v17, _mm_add_epi8(v16, v10)), _mm_and_si128(_mm_or_si128(v16, v9), v17));
/
/
通过动态调试得知,v18
=
username
*
(__m128i
*
)&key[
2
*
v7
+
16
]
=
_mm_unpackhi_epi8(v15, v18);
/
/
将 v15 和 v18 的低
64
位数以
8
位为单位进行交错
/
/
key[
16
-
31
]
=
username>>
4
^si128 低
64
位分组交错 username
*
(__m128i
*
)&key[
2
*
v7]
=
_mm_unpacklo_epi8(v15, v18);
/
/
key[
0
-
15
]
=
username>>
4
^si128 高
64
位分组交错 username
v7
+
=
16
;
}
while
( v6 !
=
v7 );
/
/
看似循环,实际只执行一次
/
/
小结:
/
/
key[
0
-
15
]
=
(username
8bit
分组右移四位) ^ si128 再高
64
位分组交错 username
/
/
key[
16
-
31
]
=
(username
8bit
分组右移四位) ^ si128 再低
64
位分组交错 username
/
/
if
( v6 !
=
user_name_len )
/
/
判断不成立
{
......
}
}
}
HMAC_CTX_init(ctx);
md
=
EVP_sha256();
HMAC_Init_ex(ctx, key,
32
, md,
0
);
HMAC_Update((
int
)ctx, (
int
)&v62,
2
);
/
/
这里的 v61 就是每次重启应用才会改的那个值,计算这个值的
hash
len
=
0
;
HMAC_Final((
int
)ctx, (
int
)
hash
, (
int
)&
len
);
/
/
得到
hash
v26
=
-
32
;
key_hash_
=
0
;
do
key_hash_
=
*
((_WORD
*
)fun3
+
((unsigned __int8)v71[v26
+
+
] ^ HIBYTE(key_hash_))
+
42
) ^ (key_hash_ <<
8
);
while
( v26 );
/
/
越界访问 key,通过(key[i] ^ v27)
*
2
+
84
得到
id
/
/
作为下一次从 data 中取数据的下标,最终得到 v27
key_hash
=
key_hash_;
/
/
v76
=
v27
uesrname_reverse
=
0LL
;
v28
=
user_name;
if
( user_name_len >
0
)
{
v29
=
0
;
if
( (unsigned
int
)user_name_len <
=
0x1F
)
goto LABEL_21;
/
/
跳转到 LABEL_21
......
v28
=
user_name;
if
( v29 !
=
user_name_len )
{
LABEL_21:
v35
=
user_name_len
-
v29;
/
/
v35
=
16
uesrname_reverse_
=
&ctx[v29
-
16
];
/
/
v36
=
__int128 v69
=
0
do
*
uesrname_reverse_
+
+
=
v28[
-
-
v35];
/
/
将 username 倒序存入 username_reverse 中
while
( v35 );
}
}
username_
=
(
int
)v28;
memset(dest,
0
, sizeof(dest));
sub_425F0(v57, (
int
)
hash
, username_,
12
, (
int
)&uesrname_reverse,
16
, (
int
)&key_hash,
2
, dest);
/
/
总结:
/
/
dest[
0
-
1
]
=
encrydata[
0
-
1
]
/
/
dest[
2
-
9
]
=
tag[
0
-
7
]
/
/
dest[
10
-
17
]
=
tag[
8
-
15
]
*
(_WORD
*
)password
=
*
(_WORD
*
)dest;
/
/
password[
0
-
1
]
=
dest[
0
-
1
]
v38
=
*
(_QWORD
*
)&dest[
2
];
*
(_QWORD
*
)(password
+
12
)
=
*
(_QWORD
*
)&dest[
10
];
/
/
password[
12
-
19
]
=
dest[
10
-
17
]
*
(_QWORD
*
)(password
+
4
)
=
v38;
/
/
password[
4
-
11
]
=
dest[
2
-
9
]
memset(password_,
0
,
0x400u
);
/
/
v77 被清零,v65也被越界清空
v65
=
*
((_DWORD
*
)password
+
4
);
/
/
v64
=
password[
16
-
19
]
v39
=
*
(_QWORD
*
)password;
/
/
v39
=
password[
0
-
7
]
*
(_QWORD
*
)&password_[
8
]
=
*
((_QWORD
*
)password
+
1
);
/
/
v77[
8
-
15
]
=
password[
8
-
15
]
*
(_QWORD
*
)password_
=
v39;
/
/
v77[
0
-
7
]
=
password[
0
-
7
]
v40
=
sub_43070(fun3, password_,
0x14u
, src);
/
/
/
/
首先修改fun3的数据并计算出一个hash1,放到src[
0
-
1
]
/
/
再次修改fun3的数据并计算出一个hash2,通过hash2和一些常数的计算
/
/
得到贯穿整个函数的v18(这个值最终决定了password长度),
/
/
在
43
-
60
行的代码中,反复修改fun3的数据并计算
hash
/
/
来设置数组 s, 随后将数组 s拷贝到 src 中,此时函数先将src的[
40
+
v18]到[
40
+
v18]保存了一份
/
/
调用函数 sub_43990 中通过一系列的位移变换,再次修改src中的数据
/
/
然后将保存的数据进行恢复,返回值为 v18
+
56
memset(v72,
0
,
256
);
memset(v71,
0
, sizeof(v71));
for
( i
=
0
; i !
=
256
;
+
+
i )
{
v71[i]
=
i;
/
/
初始化v71
=
0
1
2
3
4
5
6
...
v72[i]
=
*
((_BYTE
*
)fun3
+
(i &
0x3F
)
+
4
);
/
/
使用 fun3 的数据初始化 v72
}
v42
=
0
;
v43
=
-
256
;
do
{
v44
=
(unsigned __int8)v72[v43];
v45
=
v44
+
v42
+
(unsigned __int8)v72[v43
+
256
];
v45
%
=
256
;
v46
=
v71[v45];
v71[v45]
=
v44;
v72[v43
+
+
]
=
v46;
/
/
不断变换交换 v71 和 v72 的数据
v42
=
v45;
}
while
( v43 );
v58
=
v40;
if
( v40 >
0
)
/
/
v40
=
上个函数中的v18
+
56
判断成立
{
v47
=
0
;
v48
=
0
;
src_
=
src;
v50
=
v58;
do
{
v51
=
v47
+
1
-
((v47
+
((unsigned
int
)((v47
+
1
) >>
31
) >>
24
)
+
1
) &
0xFFFFFF00
);
v52
=
(unsigned __int8)v71[v51];
v48
=
(v52
+
v48)
%
256
;
v53
=
v50;
v54
=
v71[v48];
v71[v48]
=
v52;
/
/
变换 v71 中的数据
v71[v51]
=
v54;
*
src_
+
+
^
=
v71[(unsigned __int8)(v71[v48]
+
v54)];
/
/
循环将 v71 的数据异或再赋值给src
v50
=
v53
-
1
;
v55
=
v53
=
=
1
;
v47
=
v51;
}
while
( !v55 );
}
memcpy(password, src, v58);
/
/
将 src 的数据拷贝到 password
return
v58;
/
/
返回拷贝数据的长度
}
|
可以轻易发现,主要是调用了 MainActivity.this.getck 方法,通过其返回值判断成功失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class
c implements View.OnClickListener {
c() {
}
@Override
/
/
android.view.View.OnClickListener
public void onClick(View view) {
Context applicationContext;
String
str
;
String obj
=
MainActivity.this.y.getText().toString();
String obj2
=
MainActivity.this.z.getText().toString();
if
(obj.length() !
=
16
) {
Toast.makeText(MainActivity.this.getApplicationContext(),
"用户名长度必须为16字节"
,
0
).show();
return
;
}
if
(MainActivity.this.getck(obj.getBytes(), com.test.pac.demo.a.a.b(obj2)) >
0
) {
applicationContext
=
MainActivity.this.getApplicationContext();
str
=
"verify success!"
;
}
else
{
applicationContext
=
MainActivity.this.getApplicationContext();
str
=
"verify failed!"
;
}
Toast.makeText(applicationContext,
str
,
0
).show();
}
}
|
可以发现主要就是在调用函数 dword_1A50BC[0] + 4 而这个地址其实就是在 _init 初始化的 [19ca8c + image_base]+4(函数sub_43280)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
int
__cdecl Java_com_test_pac_demo_MainActivity_getck(JNIEnv
*
a1,
int
a2, void
*
username_, void
*
password_)
{
username_len
=
(
*
a1)
-
>GetArrayLength(a1, username_);
username
=
(
*
a1)
-
>GetByteArrayElements(a1, username_,
0
);
password_len
=
(
*
a1)
-
>GetArrayLength(a1, password_);
password
=
(
*
a1)
-
>GetByteArrayElements(a1, password_,
0
);
v8
=
(
*
(
int
(__cdecl
*
*
)(
int
, jbyte
*
, jsize, jbyte
*
, jsize))(
*
(_DWORD
*
)dword_1A50BC[
0
]
+
4
))(
dword_1A50BC[
0
],
username,
username_len,
password,
password_len);
(
*
a1)
-
>ReleaseByteArrayElements(a1, username_, username,
0
);
(
*
a1)
-
>ReleaseByteArrayElements(a1, password_, password,
0
);
return
v8;
}
|
这个函数是解密的核心函数,这里总结一下流程:
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
|
int
__cdecl sub_43280(BYTE
*
fun3, BYTE
*
username,
int
username_len, char
*
password,
int
password_len)
{
result
=
0
;
if
( username_len
=
=
16
)
{
memset(password_encrypt,
0
,
256
);
memset(v71,
0
,
256
);
......
v15
=
v13
+
1
-
((v13
+
((unsigned
int
)((v13
+
1
) >>
31
) >>
24
)
+
1
) &
0xFFFFFF00
);
v16
=
(unsigned __int8)v71[v15];
v14
=
(v16
+
v14)
%
256
;
v17
=
v71[v14];
v71[v14]
=
v16;
v71[v15]
=
v17;
password[v12
+
+
] ^
=
v71[(unsigned __int8)(v71[v14]
+
v17)];
/
/
/
/
上面的代码在 sub_42940 是分析过的
/
/
注意和 password 进行异或的值都是通过
/
/
fun3 初始化 v71、
72
在通过相同的位运算计算出来的
/
/
也就是说这里就是在对 password 进行解密
v13
=
v15;
}
while
( password_len !
=
v12 );
if
( password_len >
=
32
)
{
v19
=
password;
sub_43990((
int
)fun3, (
int
)(password
+
2
), password_len
-
18
, (unsigned
int
*
)&password[password_len
-
16
]);
/
/
/
/
这里的第二次调用 sub_43990 也是同样的道理
/
/
也是异或相同的值对 password 进行解密
v20
=
(unsigned __int8)password[
3
];
v21
=
16
*
(unsigned __int8)password[
2
];
v22
=
v20
+
v21
=
=
0
;
v23
=
v20
+
v21;
v18
=
0
;
if
( !v22 )
{
v24
=
v20
+
16
*
(unsigned __int8)password[
2
];
v25
=
0
;
if
( v24 <
0x20
)
goto LABEL_17;
/
/
动态调试得知此处跳转至 LABEL_17
v25
=
v24 &
0xFFFFFFE0
;
......
if
( v24 !
=
v25 )
{
LABEL_17:
v29
=
v24
-
v25;
v30
=
&v19[v25
+
4
];
do
{
*
(v30
-
4
)
=
*
v30;
/
/
v30 就是 password,在函数 sub_42940 中也是
/
/
存在这段代码的,这里还是对password进行解密
+
+
v30;
-
-
v29;
}
while
( v29 );
}
v18
=
v23;
/
/
v20
=
(unsigned __int8)password[
3
];
/
/
v21
=
16
*
(unsigned __int8)password[
2
];
/
/
v22
=
v20
+
v21
=
=
0
;
/
/
v23
=
v20
+
v21;
/
/
/
/
v18
=
password[
2
]
*
16
+
password[
3
]
}
}
else
{
v18
=
0
;
}
}
v31
=
v18;
/
/
v31
=
password[
2
]
*
16
+
password[
3
]
/
/
/
/
回去翻看了一下 sub_43070 的分析过程,发现这个值是固定的
=
20
/
/
src[
2
]
=
BYTE1(num_20);
/
/
src[
2
]
=
n的高位
=
0
/
/
src[
3
]
=
num_20;
/
/
src[
3
]
=
20
memset(password_encrypt,
0
, sizeof(password_encrypt));
v32
=
sub_43860(v47, password, v31, password_encrypt, COERCE_FLOAT(
120
));
/
/
这里出现了一个在加密过程中没有出现过的函数
/
/
这个函数将password加密的数据存放到 password_encrypt 中
/
/
参数 v31 应该是欲加密的长度为
20
,返回值 v32
=
v31
=
20
result
=
0
;
if
( v32
=
=
20
)
{
if
( username )
{
v33
=
_mm_loadu_si128((const __m128i
*
)username);
si128
=
_mm_load_si128((const __m128i
*
)&xmmword_148380);
......
*
(__m128i
*
)&key[
16
]
=
_mm_unpackhi_epi8(v39, v42);
*
(__m128i
*
)key
=
_mm_unpacklo_epi8(v39, v42);
/
/
这段循环之前在 sub_42940 也是分析过的
/
/
显然这就是把用户名变换一下,然后当做下面求
hash
的key使用
/
/
key[
0
-
15
]
=
(username
8bit
分组右移四位) ^ si128 再高
64
位分组交错 username
/
/
key[
16
-
31
]
=
(username
8bit
分组右移四位) ^ si128 再低
64
位分组交错 username
}
HMAC_CTX_init(ctx);
v43
=
EVP_sha256();
HMAC_Init_ex(ctx, key,
32
, v43,
0
);
HMAC_Update((
int
)ctx, (
int
)&password_encrypt[
2
],
2
);
/
/
将 password_encrypt[
2
-
3
] 传入
v49
=
0
;
HMAC_Final((
int
)ctx, (
int
)
hash
, (
int
)&v49);
/
/
得到
hash
v44
=
-
32
;
v45
=
0
;
do
v45
=
*
(_WORD
*
)&fun3[
2
*
(key[v44
+
+
+
32
] ^ HIBYTE(v45))
+
84
] ^ (v45 <<
8
);
while
( v44 );
/
/
这个算法之前也分析过,把 key[i]
*
2
*
v45 当做
id
/
/
求得fun3数据区的
hash
v48
=
_mm_shuffle_epi8(_mm_loadu_si128((const __m128i
*
)username), (__m128i)xmmword_1483E0);
/
/
/
/
xmmword_1483E0
=
102030405060708090A0B0C0D0E0Fh
/
/
对 username 的顺序进行变换,传入下边的 sub_424e0
/
/
不过这个参数并不影响结果
memset(v71,
0
, sizeof(v71));
result
=
sub_424E0(
/
/
EVP_DecryptFinal_ex 失败,result
=
0
v46,(
int
)
hash
,(
int
)username,
12
,(
int
)&v48,
16
,(
int
)&password_encrypt[
4
],
16
,(
int
)password_encrypt,
2
,v71);
if
( !result || v45 !
=
*
(_WORD
*
)v71 )
return
0
;
/
/
返回
0
}
}
return
result;
}
|
没有找到这个函数对应的加密流程,应该是导致解密失败的主要原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
int
__cdecl sub_43860(
int
a1, char
*
password,
int
password_len, void
*
dest,
float
a5)
{
v5
=
&v13[
-
((
4
*
password_len
+
15
) &
0xFFFFFFF0
)];
memcpy(v5, password, password_len);
/
/
v5
=
password
if
( password_len >
=
4
)
{
*
(_DWORD
*
)&v13[
12
]
=
-
LODWORD(a5);
/
/
v13 中记录了两个值, v13[
16
] 中记录的是长度除以
4
v6
=
0
;
*
(_DWORD
*
)&v13[
16
]
=
password_len
/
4
;
if
( (unsigned
int
)(password_len
/
4
) <
=
7
)
goto LABEL_6;
/
/
动态调试此处跳转 LABEL_6
......
while
(
*
(_DWORD
*
)&v13[
16
] !
=
v6 )
{
LABEL_6:
*
(_DWORD
*
)&v5[
4
*
v6]
=
(
*
(_DWORD
*
)&password[
4
*
v6] << v13[
12
]) | (
*
(_DWORD
*
)&password[
4
*
v6] >> SLOBYTE(a5));
+
+
v6;
/
/
通过这个公式循环修改 v5 中的数据,循环次数为长度的四分之一
}
}
memcpy(dest, v5, password_len);
/
/
将修改后的数据拷贝到dest
return
password_len;
}
|
EVP_DecryptFinal_ex 解密失败,这里我查阅了一下 openssl 源码,结合动态调试返回的地方,对应在 EVP_R_INVALID_OPERATION 处返回,正好源码里有注释,翻译了一下是:防止解密时意外使用加密上下文。 应该是解密的数据不对导致的失败返回 0。
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
|
int
__cdecl sub_424E0(
int
a1,
int
hash
,BYTE
*
username,
int
num_12,
int
a5,
int
num_16_,
BYTE
*
password_encrypt_4,
int
num_16,BYTE
*
password_encrypt,
int
num_2,BYTE
*
dest)
{
ctx
=
EVP_CIPHER_CTX_new();
cipher
=
EVP_aes_256_gcm();
EVP_DecryptInit_ex(ctx, cipher,
0
,
0
,
0
);
/
/
使用 chiper 初始化 ctx
EVP_CIPHER_CTX_ctrl(ctx,
9
, num_12,
0
);
/
/
设置向量长度
EVP_DecryptInit_ex(ctx,
0
,
0
,
hash
, username);
/
/
使用
hash
作为 key,使用username作为iv
if
( num_16_ )
EVP_DecryptUpdate(ctx,
0
, &v15, a5, num_16_);
/
/
输出地址为
0
,无意义
EVP_DecryptUpdate(ctx, src, &v15, password_encrypt, num_2);
/
/
将 password_encrpt 前两个字节输入并解密,解密数据放到src中
memcpy(dest, src, v15);
/
/
拷贝解密数据到 dest 中
EVP_CIPHER_CTX_ctrl(ctx,
0x11
, num_16, (
int
)password_encrypt_4);
/
/
# define EVP_CTRL_AEAD_SET_TAG 0x11
/
/
设置 password_encrypt[
4
]作为 TAG
v13
=
EVP_DecryptFinal_ex(ctx, src, &v15);
/
/
此处在动态调试的时候发现解密失败,跟进看了一下
/
/
/
/
*
a3
=
0
;
/
/
v3
=
*
a1;
/
/
if
( (
*
(
*
a1
+
18
) &
0x10
) !
=
0
)
/
/
{
/
/
savedregs
=
0
;
/
/
v16
=
(
*
(v3
+
24
))();
/
/
if
( v16 <
0
)
/
/
{
/
/
return
0
;
/
/
会在此处返回
/
/
}
/
/
/
/
查阅了一下 openssl 源码,这里对应的应该是下边这一句
/
/
翻译一下是:防止解密时意外使用加密上下文,应该是解密的数据不对
/
/
失败就也挺合理,因为前面调用了一个password_encrypt 是调用了
/
/
一个和 check_bt 无法逆向对应的一个函数
/
/
/
/
/
*
Prevent accidental use of encryption context when decrypting
*
/
/
/
if
(ctx
-
>encrypt) {
/
/
ERR_raise(ERR_LIB_EVP, EVP_R_INVALID_OPERATION);
/
/
return
0
;
/
/
}
/
/
EVP_CIPHER_CTX_free(ctx);
return
v13;
/
/
返回失败
}
|
和 generate3 相比,check3 的代码量少了许多,并没有像check2那样完全逆向流程解密,导致验证失败,而且我思考了一下,注册机应该是写不了的,因为最开始在 _init 函数中初始化的数据是随机的。
本次样本分析共历时七天,总的来讲主要集中在 OpenSSL 函数、SSE指令集的用法的细节问题,但最终经过网络搜索、动态调试和搭建实验环境调试 demo 予以解决。
[1] OpenSSL之EVP(一)——数据结构及源码结构介绍
https://blog.csdn.net/scuyxi/article/details/60365001
[2] OpenSSL学习之一:HMAC算法分析
https://blog.csdn.net/KXue0703/article/details/120795546
[3] SSE指令集优化学习:双线性插值
https://blog.csdn.net/djzhao/article/details/78408198
[4] OpenSSL中文手册之EVP库详解
https://blog.csdn.net/liao20081228/article/details/76285896
[5] Intel白皮书SSE2相关指令查询
https://software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=SSE2&text=_mm_cmpeq_epi32&expand=773
最后附上样本
http://gofile.me/6J4EF/mzW5XxJtC
更多【记一次面试启明X辰的样本分析报告】相关视频教程:www.yxfzedu.com