首页
社区
课程
招聘
[原创]启明X辰样本分析报告
2023-3-5 14:42 26553

[原创]启明X辰样本分析报告

2023-3-5 14:42
26553

写在前面

这是在面试启明X辰时,公司要求分析的 CreakMe,这里做一份记录。

样本分析报告

分析报告 分析环境
分析人 刘XX
时间 2023年2月28日
平台 x86 模拟器

样本信息

样本 分析环境
样本名称 20230221.demo.apk
MD5值 2A006D76B461ECE94596D46B3C9B3072
SHA1值 CF7810FA84DCCFA7F4F21847188E133A4C80CBA7
CRC32 6D81EAF7

分析目标

  1. 分析密码生成(generate1)过程以及check校验过程。
  2. 分析密码生成(generate2)过程以及check校验过程。
  3. 分析密码生成(generate3)过程以及check校验过程。
  4. 总结在分析过程中哪些难题, 最后通过什么方式解决的。

信息整理

整理一下各个按钮的 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

按钮 generate1

  1. 分析linstener g,其主要就是第16行的内容的 MainActivity.this.G 方法和 com.test.pac.demo.a.a.a 方法
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. 分析方法 G
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. 分析方法 com.test.pac.demo.a.a.a(),这个方法较为简单,就是取绝对值后转成16进制,然后再拼接成新的字符串
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. 总结一下,generate1按钮绑定 linstener g(),随机生成16个字节,将该16个字节 AES 加密,随后随机数再与密文重新组合一下:bArr3 = bArr2[8-15] + aes(明文) + bArr2[0-7],最后将每个字节取绝对值并转为16进制字符,输出到屏幕。

按钮 check1

  1. 分析linstener d,可以轻易判断出 obj 为 username,obj2 为 password,需要重点分析的就是第15行的内容的 com.test.pac.demo.a.a.b 方法和 MainActivity.this.D 方法
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. 分析方法 com.test.pac.demo.a.a.b

这个方法起初看起来有点复杂,一直以为是个加密的方法,但仔细观察,这其实就是个十六进制字符串转十进制的方法,将传入的十六进制字符串转换成了对应的十进制字节数组

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;
}
  1. 分析方法 MainActivity.this.D

方法D 由两个方法组成,分别是方法F 和方法E,而方法D只是简单的判断方法E的返回值真假

1
2
3
public int D(byte[] bArr, byte[] bArr2) {
    return E(F(bArr), bArr2) ? 1 : 0;
}
  1. 分析方法F

根据之前分析过的方法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. 分析方法E,其实都可以直接猜出来,E方法一定是在比较解密出来的明文和从控件获得的password
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;
}
  1. 总结一下,check1 按钮完全就是 generate1的逆向操作,只要是 generate1 设置的 password,check1 是必然成功的。

按钮 generate2

按钮 generate2的分析较为困难,主要问题在于 OpenSSL 库函数较为生疏,而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的长度

分析函数 MainActivity_M1

这个函数较为简单,通过函数 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;
}

分析函数 sub_46660

这个函数就是 generate2 按钮的主体逻辑了,首先将该函数分为四个部分依次分析:

  • hmac_ctx_hash (函数一)
  • evp_encrypt(函数二)
  • 二次加密 encrypt_data (函数三)
  • encrypt_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
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
}

分析函数 hmac_ctx_hash(部分一)

这个函数总的来讲就是不断的将求得的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);
}

分析函数 evp_encrypt(部分二)

这个函数的总的来说就是使用上一层传入的 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);
}

二次加密 encrypt_data(部分三)

这一部分主要就是通过 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次

分析函数 encrypt_4(部分四)

这个函数主要分为三个部分:

  • 初始化数组 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);

函数 sub_46660 总结

  • 生成一串随机数作为明文和key
  • 调用函数 hmac_ctx_hash 得到随机明文的 hash
  • 调用函数 evp_encrypt 以上一步的 hash 作为 key,得到 user_name 的密文
  • 对用户名长度做了一些校验,跳过了一堆 SSE 指令(感谢手下留情)
  • 通过 hash 第二次加密 encrypt_data 得到 encrypt_data2
  • 调用函数 encrypt_4 第三次加密,得到最终的 dest
  • dest 即为回传至屏幕的 password

按钮 check2

由于已经详细的分析了 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 中,只需要将其逆运算取出来即可使用。

初始化函数 _init

在分析按钮 generate3 时很多数据静态分析时并不存在,所以这里还需要分析 _init 函数,该函数地址并没有被 IDA 分析出来,可以通过以下两个办法找到:

  • 使用 readelf 工具查看

    .\readelf.exe -d C:\Users\Administrator\Desktop\libnative-lib.so
    
  • 使用 IDA 动态调试,在被写入数据的地址下内存写入断点

分析函数 sub_41BA0

这个函数就是 _init 函数,其主要分为三个函数,分别是 set_buff1、 set_buff2、 set_buff3

分析函数 set_buff1

这个函数首先将 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_48840

该函数主要是一些范围的判断,主要还是调用函数 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;
}
分析函数 sub_488E0

这里就是 buff1 真正调用的函数,作用是初始化 buff1 空间中的数据

  • buff1_60_16[0-3] 存放着申请堆空间大小+1,也就是49
  • buff1_60_16[4-7] 存放着字符串"3390fd362dfdda0030d5737632d3d213"的长度,也就是32
  • buff1_60_16[8-11] 存放着字符串"3390fd362dfdda0030d5737632d3d213"的指针
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"的指针
}

分析函数 set_buff2

这个函数比较浅,主要就是计算随机数的 hash,然后初始化 buff2 的内存数据,结果是:

  • buff2[0-3] = 19aac+imagebase
  • buff2[4-19] = 0
  • buff2[20-531] = const_data ^ 0x0036
  • buff2[532-595] = 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
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_buff3

基本上和 set_buff2 如出一辙,调用了一个新的函数 get_hash2,不过和之前求hash函数的也是基本一致,这里直接列出 buff3 内存布局:

  • buff3[0-3] = 19ca8c + image_base(根据后面的分析,这个就是按钮 generate3 中调用的函数)
  • buff3[4-67] = hash
  • buff3[68-83] = 0
  • buff3[84-595] = const_data ^ 0x0042
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;
}

按钮 generate3

可以轻易发现,主要是调用了 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)));
        }
    }

分析函数 MainActivity_getbt

这里的 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

函数 fun3 的大致逻辑如下:

  • 通过循环异或位运算计算 buff3 中的数据得到 第一次的 hash1
  • 通过 username 与 常数计算出一个32字节的 key
  • 使用 key 计算 hash1 的hash 得到 hash2
  • 循环异或位运算计算key 的 hash 得到 hash3
  • 调用函数 sub_425F0 得到一段数据
  • 调用函数 sub_43070 修改内存数据并得到一个影响密码长度的值
  • 不断变换交换上面得到的数据,最终将数据拷贝到 password
  • 返回password长度
分析函数 sub_425F0

主要功能为设置 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;
}
分析函数 sub_43070

主要功能为设置 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
}
分析函数 get_hash3_and_change_fun3

这个函数总的来讲就是不断的修改 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;
}
分析函数 sub_43990

这个函数主要就是在一个循环中通过一系列的变换,不断的在修改 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;
}

generate3 按钮总结

利用启动应用时初始化的数据,不断的求 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;                                   // 返回拷贝数据的长度
}

按钮 check3

可以轻易发现,主要是调用了 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();
    }
}

分析函数 MainActivity_getck

可以发现主要就是在调用函数 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;
}

分析函数 sub_43280

这个函数是解密的核心函数,这里总结一下流程:

  • 依次按照加密的流程逆向运算对 password 进行解密
  • 调用了一个加密过程中未出现过的函数 sub_43860 加密password
  • 调用函数 sub_424E0 解密 password
  • 函数 sub_424E0 解密失败,返回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
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;
}

分析函数 sub_43860

没有找到这个函数对应的加密流程,应该是导致解密失败的主要原因

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;
}

分析函数 sub_424E0

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;                                   // 返回失败
}

check3 按钮总结

和 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


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-1-12 22:27 被简单的简单编辑 ,原因:
收藏
点赞13
打赏
分享
最新回复 (11)
雪    币: 4
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
shenhao 2023-3-5 19:17
2
0
分析的详细
雪    币: 477
活跃值: (1412)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_foyotena 2023-3-6 09:40
3
0
考的密码学和数学
雪    币: 4436
活跃值: (3459)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
木志本柯 2023-3-6 10:18
4
0
一般他们要你逆向这个用多久 需要当天在公司逆完吗?
雪    币: 3906
活跃值: (5533)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
badboyl 2 2023-3-6 10:45
5
0
考验的就是耐心啊
雪    币: 0
活跃值: (232)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
老弟来了 2023-3-7 16:37
6
0

附上,第三题验证失败原因,以及打印补丁位置。


armv8文件在0x6ACB0处修改为EB 04 00 54=>27 00 00 14


改成无条件跳转,不进行字节位移,完成修复,可以正常效验了。


上传的附件:
雪    币: 859
活跃值: (69)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_威郑天 2023-3-8 13:07
7
0
楼主能分享APP吗,我也想分析一下。
雪    币: 5583
活跃值: (3857)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
简单的简单 2023-3-8 21:48
8
0
wx_威郑天 楼主能分享APP吗,我也想分析一下。
帖子的最后有附带样本
雪    币: 5583
活跃值: (3857)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
简单的简单 2023-3-8 22:01
9
0
木志本柯 一般他们要你逆向这个用多久 需要当天在公司逆完吗?
给了七天时间,让拿回去分析,我也用了七天时间
雪    币: 5583
活跃值: (3857)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
简单的简单 2023-3-8 22:01
10
0
badboyl 考验的就是耐心啊
样本分析就是比耐心
雪    币: 5583
活跃值: (3857)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
简单的简单 2023-3-8 22:02
11
0
老弟来了 附上,第三题验证失败原因,以及打印补丁位置。armv8文件在0x6ACB0处修改为EB 04 00 54=&gt;27 00 00 14。改成无条件跳转,不进行字节位移,完成修复,可以正常效验 ...

跪拜大佬

最后于 2023-3-20 14:45 被简单的简单编辑 ,原因:
雪    币: 4636
活跃值: (2841)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
任蝶飞 2023-3-20 03:58
12
0
建议样本上传附件,避免丢失
游客
登录 | 注册 方可回帖
返回