安全公司-* * * *-面试题:_ 安卓逆向分析分享
1 总览:
a) 查看清单文件AndroidManifest注册的各种信息(入口类、权限、广播、服务等)
l 发现只有入口类信息:
Ø com.test.pac.demo.MainActivity
b) 安装程序查看大致行为
l 输入UserName(长度为16)
l PassWord 可以输入,也可以点击Generate 生成
l 点击Check按钮验证PassWord是否正确
确认目标:分析PassWord生成算法并写出该算法程序,最后验证逆向的算法是否正确。
2 逆向过程:
2.1 配置.so 调试环境
为了更好的观察数据变化,需要配置.so调试环境
l 配置动态分析so 文件的环境
手机没有root,额尝试了也root不了,提示没有匹配机型,动态调试so文件失败。。。。。。
嗯,浪费了很多时间被多篇博客误导了;原来可以直接使用IDA 直接附加夜神模拟器进程动态调试so文件,配置参考:
https://cloud.tencent.com/developer/article/1357098;但是其中附加方法我变了一下,原文在我机器上行不通;效果如下图:
2.2 分析关键点
使用反编译工具Jadx 查看反编译代码,确定关键调用:
分析:
1> 首先根据长度信息和控件之间的对应关系,可以确定f7997x为UserName编辑框控件、f7996w为PassWord编辑框控件。
2> 进一步可以确定getck() 是关键校验算法,getbt() 关键密码生成算法。
2.3 密码校验函数getck()
l 查看该函数信息
分析:
可以看到getck()是一个native 层的函数。PrtUtil.m7581a() 对 输入的PassWord 进行了数据转换:从左到右每两个字符转换成一个16进制数值,再强转此数值为一个字符。例如: “31323368656c6c6f”—》“123hello”
l 进入so 文件后,发现getck() 函数有4个参数,但是实际是2个参数,,然后就有点小懵(以前还没有分析过)
l 重新设定a1 参数类型为JNIEnv* 类型,就清晰很多了
再通过查阅网上资料,发现a2 代表返回类型,a3 、a4 分别是传入的参数。
l 继续分析 getck()
a) SSE向量运算-转换UserName为其Ascii串
分析:
Ø 进入getck() 函数之后,该函数对UserName 通过一系列SSE2向量运算计算出一个数据(供后面计算摘要使用);经过分析,这里应该是优化后的代码;实际只是求UserName串的Ascii串。
例如:
“321cba” 的Ascii:"333231636261"
--> 16进制串如下:
"\x33\x33\x33\x32\x33\x31\x36\x33\x36\x32\x36\x31"
所以此处的C代码暂定为:
unsignedchar * GetAsAs(char * Dest,intLen)
{
unsignedchar * Ret = (unsignedchar *)malloc(Len * 2);
memset(Ret, 0, Len * 2);
for (int i = 0; i < Len*2; i+=2)
{
unsignedchar Tmp = Dest[i/2]>>4& 0x0f|0x30;
Ret[i] = Tmp;
Ret[i + 1] = Dest[i/2]& 0x0f | 0x30;
}
return Ret;
}
b) HMAC_sha256 签名
分析:
Ø 然后,将前面计算的数据(UserName的Ascii串的HMAC 摘要签名;并将签名信息作为参数(参数还有用户名、常见字符表、密码起始地址+4、密码起始地址)作为参数传入sub_34CC0() –》这里我重命名KeyFunc_KKKKK()函数进一步计算。
l 详细分析sub_34cc0() / KeyFunc_KKKKK() 函数
分析:
Ø 可以看到;此函数只是一些OpenSSL函数的调用;简述流程就是创建Ctx对象,使用AES_256_gcm引擎;以UserName作为IV初始向量、前面计算的HMACResult签名作为密钥,解密出密文。(Aes_256_gcm 算法的的参考链接: https://www.cnblogs.com/0616--ataozhijia/p/11271433.html)
l 最后以EVP_DecryptFinal_ex() 返回值作为这次getck() 函数成功与否的标识
分析:
Ø 而EVP_DecryptFinal_ex() 成功就返回一个正数,不然就标识着失败;这也验证了反编译出的源码中的调用返回结果的判别:getck()>0 == { success;};
2.4 密码生成函数getbt()
l 查看该函数信息:
分析:
Ø 可以看到getbt() 也是一个native层的函数;而PrtUtil.m7580a()是将目标字符串转换成其Ascii16进制格式字符串,例如“123hello”—》“31323368656c6c6f”。特殊情况:如果字符Ascii的16进制<0x10,就会填充一个0;例如:字符Ascii的16进制为0x8 则--> 08
l getbt()函数中使用到的全局168240处的64Bytes数据分析
看到getbt() 一开始就使用了一个全局变量;一全局的8个qword = 64Bytes 的值,其起始地址168240,就分析了一下,如下:
分析:
Ø 这8个qword = 64 Bytes的值是由一开始随机产生32 Bytes,还经过了一些计算(3层HMAC计算,后面有分析)来填充到这64Bytes的区域的。在JNI_Onload 的时候会调用这个函数。一旦程序加载成功JNI_Onload调用之后;在程序运行过程中,这64Bytes 的数值就是稳定的。(暂时认为这个时候每次程序运行都是不相等的,所以想要编写注册机的话,需要每次动态读取这一块内存,加入密码计算过程)。(也可以使用调试查看数据,但毕竟不适合非专业人员使用)
Ø 这时候注册机应该会涉及到Android的跨进程内存读写:
(参考:https://blog.csdn.net/mldxs/article/details/14486827)
Ø 进一步分析根据32Byres随机数据填充该168240位置64Bytes的算法
分析:
n 使用了3层 HMAC 签名算法,计算出的签名值,用来填充168240开始的64字节区域。
l 接着计算通过167004位置的cipher表映射+计算出的特征值
分析:
Ø 结合后面的代码,这里的HIWORD(V45) = w_FeatureDigit就是密码的偏移2处的2Byte(这里w_FeatureDigit是重命名的),查看了 上下文,这个特征值在后面HMACMD计算、密码的数据都用到了;所以这块随机数据处理后的值,目前看来不能缺少。
(前面验证函数有详细分析,这里就不赘述了)
l 计算HMAC SHA256签名
分析:
Ø 这里面HMAC就使用到了 前面计算了那64Bytes 的特征值(WORD)。
l 计算UserName的Ascii串在全局映射表中计算出的特征:
l 创建一份常见字符串文本(从后面代码看,是用来作为AAD数据)
l 调用过程函数35210() 加密
分析:
Ø 加密了UserName的Ascii串经过Map_167004计算的特征值的密文2Byte并传出;然后将16Byte 的tag值也传出。
l 最后组合密码
分析:
Ø 根据刚才传出的区域EncryptRegion,组合v45为起始的密码区域。
3 总结归纳编写注册机
3.1 总结密码的构成分析
解释:
该密码 由3部分组成:
Ø 第一部分:是由UserName的Ascii 串经过 全局的映射表(167004h)计算后的的2Bytes 特征值,使用Aes_256_gcm 加密后的密文2Byte值。
Ø 第二部分:原始数据是JNI_Onload里面调用函数生成32Bytes的随机值复制两份,然后经过3层HMAC运算结果填充到168240h处的64Byte;经167004h处的映射表计算,得出的2Byte值。
Ø 第三部分:是经过AES_256_gcm 加密算法后的16Byte Tag值
3.2 总结需读进程内存的数据
Ø 全局64Bytes数据(168240h起始的)
解释:此区域数据,每次程序启动调用JNI_Onload函数后,会调用里面的
35520()函数产生随机32Bytes数据,然后调用35330()函数将这32Bytes的的数据进行3层HMAC_SHA256签名计算。将各层MD签名值赋值到168240h起始的64Bytes。
Ø 全局CipherTable (167004h 起始的,也可以手工从镜像中提取,但是都到这一步了,顺道就提取了)
解释:此表在注册机中,在计算中提供了一种映射关系;每次程序启动
调用JNI_Onload函数的会在168240h起始的64Bytes位置产生新的数据,而映射表,在计算中会因为不同的Bytes值,得到不同的特征值,供后面的HMAC签名运算和密码生成运算。
3.3 注册机编写思路
(1) 通过进程读取内存数据(168240h起始的64bytes,也可顺带把那个全局映射表也取出来)
https://blog.csdn.net/mldxs/article/details/14486827
步骤:
a) 通过获取libnative-lib.so 所在模块的数据区段位置,来定位该数据,如下图:
分析:该模块具有很明显的特征 :所属libnative-lib.so并且具有rw权限。全局64Bytes 数据距离该区段起始位置的偏移为:1240h;全局映射表所在偏移:4h。
b) 编写程序获取数据(此程序在linux环境或者Cygwin中编译好,放到Android中运行)
(2) 编写小函数:将UserName字符串转换成其Ascii串的Ascii串
如:“123” –》“333133323333”
代码:
unsignedchar * GetAsAs(char * Dest,intLen) { unsignedchar * Ret = (unsignedchar *)malloc(Len * 2); memset(Ret, 0, Len * 2); for (int i = 0; i < Len*2; i+=2) { unsignedchar Tmp = Dest[i/2]>>4 &0x0f|0x30; Ret[i] = Tmp; Ret[i + 1] = Dest[i/2] & 0x0f | 0x30; } return Ret; }
// 后面就扣取代码了
(3) 根据跨进程获取到的全局映射表(167004h)、全局64Bytes数据(168240h)
计算映射特征值,代码:
(4) 计算 HMAC、AES_256_gcm
(需要安装配置openssl )
代码:
(6) 得出结果
3.4 代码
注: 这里 还没有使用 进程内存读写,使用了读取出来 ,应该就可以使用下面的代码了
// OpenSSL_DE_EN.cpp : 此文件包含"main" 函数。程序执行将在此处开始并结束。
1 // 2 3 4 5 #include"pch.h" 6 7 #include<iostream> 8 9 #include<openssl/ssl.h> 10 11 #include<openssl/err.h> 12 13 #include<Windows.h> 14 15 #pragmacomment(lib,"libssl.lib") 16 17 #pragmacomment(lib,"libcrypto.lib") 18 19 unsignedchar g_Bytes[64]; 20 21 unsignedlonglongint g_4QW[4]; 22 23 24 25 // 获取UserName 的Ascii 26 27 unsignedchar * GetAsAs(char * Dest, intLen) 28 29 { 30 31 unsignedchar * Ret = (unsignedchar *)malloc(Len * 2); 32 33 memset(Ret, 0, Len * 2); 34 35 for (int i = 0; i < Len * 2; i += 2) 36 37 { 38 39 unsignedchar Tmp = Dest[i / 2] >> 4& 0x0f | 0x30; 40 41 Ret[i] = Tmp; 42 43 Ret[i + 1] = Dest[i / 2]& 0x0f | 0x30; 44 45 } 46 47 return Ret; 48 49 } 50 51 // 加密算法 52 53 54 55 size_t__cdecl Encrypt_sub_35210(unsignedchar* key, unsignedchar * IV_, intIV_len, unsignedchar * UsualChars_AAD, intAAD_Len, unsignedchar * plainText, intplain_Len_2, void *EncryptRegion) 56 57 { 58 59 int CtxObj_; // esi 60 61 int Len_ = 0; 62 63 int v13; 64 65 unsignedchar * cipherText = (unsignedchar *)malloc(0x400); 66 67 EVP_CIPHER_CTX* CtxObj = EVP_CIPHER_CTX_new(); // 创建一个 Ctx 68 69 EVP_EncryptInit_ex(CtxObj, EVP_aes_256_gcm(), 0, 0,0);// 绑定Aes 引擎 70 71 EVP_CIPHER_CTX_ctrl(CtxObj, 9, IV_len, 0); // 设置IV 的长度 72 73 EVP_EncryptInit_ex(CtxObj, 0, 0, key, IV_); // 初始化Key密钥 和IV初始化向量 74 75 EVP_EncryptUpdate(CtxObj, 0, &Len_, UsualChars_AAD, AAD_Len);// 提供任意AAD 数据;此方式可调用多次 76 77 EVP_EncryptUpdate(CtxObj, cipherText, &Len_, plainText, plain_Len_2);// 提供需要被加密的文本,以及接收加密后的文本 78 79 memcpy(EncryptRegion, cipherText, Len_); // 拷贝 前一步2Byte 密文 80 81 EVP_EncryptFinal_ex(CtxObj, cipherText, &v13);// 加密! 82 83 EVP_CIPHER_CTX_ctrl(CtxObj, 16, 16, cipherText);// 获取Aes_256_gcm 此次的 Tag 16Byte 84 85 int v10 = Len_; 86 87 *(unsignedlonglong *)((char *)EncryptRegion + Len_ + 8) = *((unsignedlonglong *)&cipherText+1);// 这两步将16Byte 的tag 值输出到EnCryptRegion 88 89 *(unsignedlonglong *)((char *)EncryptRegion + v10) = *(unsignedlonglong *)&cipherText;; 90 91 EVP_CIPHER_CTX_free(CtxObj); 92 93 free(cipherText); 94 95 return 0; 96 97 } 98 99 100 101 102 103 int main() 104 105 { 106 107 printf("请输入 那64Bytes 数据:\n"); 108 109 for (int i = 0; i < 4; i++) 110 111 { 112 113 scanf_s("%i64x", g_4QW + i); 114 115 } 116 117 118 119 EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); 120 121 std::cout <<"Hello World!\n"; 122 123 124 125 WORD v45[10]; 126 127 unsignedchar UserName_Bytes[32]; 128 129 //getCiphertTable 130 131 unsignedlonglong w_167004_MapTable_[1024]; // 当程序JNI_Onload()运行后,这随机值就稳定了;这时候输入的UserName 后Generate的密码就是稳定的。 132 133 //get 64Bytes 134 135 unsignedlong data64Bytes; // 通过引用查看 这是一个 随机值,赋值所在函数35520() ;在程序启动JNI_Onload调用的时候会调用35520()函数进行初始化。 136 137 unsignedlonglong *p64Bytes = (unsignedlonglong *)&g_4QW; // 但是重新进入程序,输入相同的UserName会发现这个值不是稳定的。就是因为JNI_ONload里面调用了35520()进行了随机数据赋值。 138 139 int count_64 = 64; 140 141 short w_FeatureDigit = 0; 142 143 do // 计算出 程序JNIEnv_Onload程序的时候随机出的数据64bytes 通过167004处的全局cipher表;找出的随机数据的特征值 144 145 { // 根据 随机出的数据;查167004处的cipher表计算出特征值 146 147 w_FeatureDigit = w_167004_MapTable_[*(unsignedchar *)p64Bytes ^ ((unsignedint)w_FeatureDigit >> 8)] ^ (w_FeatureDigit << 8); 148 149 p64Bytes = (unsigned__int64 *)((unsignedchar *)p64Bytes + 1); 150 151 --count_64; 152 153 } while (count_64); 154 155 short w_Rand64Bytes_FeatureDigit = w_FeatureDigit; 156 157 v45[1] = (WORD)w_FeatureDigit; 158 159 int v32 = 0; 160 161 162 163 // 获取UserName 的Ascii 164 165 char Name[] = { "123456789abcdefg" }; 166 167 unsignedchar * AscData = GetAsAs(Name, 0x10); 168 169 HMAC_CTX *Ctx = HMAC_CTX_new();// 创建一个 Ctx 170 171 HMAC_Init_ex(Ctx, AscData, 32, EVP_sha256(), NULL);// 创建一个 摘要引擎 EVP_sha256 172 173 HMAC_Update(Ctx, (constunsignedchar *)&w_Rand64Bytes_FeatureDigit, 2);// 此函数 可以重复调用,将数据块加入到 计算结果中 174 175 unsignedint len1 = 0; 176 177 unsignedchar * result = (unsignedchar*)malloc(sizeof(char) * 0x20); 178 179 HMAC_Final(Ctx, result, &len1); 180 181 int v26 = 32; // v45-32 的位置 刚好是 前面计算UserName 的Ascii串 182 183 int v27 = 0; // int16 - Word 184 185 do 186 187 v27 = w_167004_MapTable_[*((unsigned__int8 *)AscData + --v26) ^ ((unsignedint)v27 >> 8)] ^ (v27 << 8); 188 189 while (v26); 190 191 short UserNameMapFeature = v27; 192 193 unsignedchar * UsualCharNum = (unsignedchar *)malloc(0x25); 194 195 memcpy(UsualCharNum, "abcdefghijklmnopqrstuvwxyz0123456789", 0x25u);// 准备AES_GCM的AAD数据 196 197 unsignedchar * EncryptRegion_ = (unsignedchar *)malloc(0x400); 198 199 memset(&EncryptRegion_, 0, 0x400u); 200 201 Encrypt_sub_35210( 202 203 result, 204 205 (unsignedchar *)UserName_Bytes, 206 207 12, 208 209 UsualCharNum, 210 211 37, 212 213 (unsignedchar *)UserNameMapFeature, 214 215 2, 216 217 EncryptRegion_); 218 219 v45[0] = *(WORD *)EncryptRegion_; // 密码的前2Byte 220 221 *(unsignedlonglong *)&v45[2] = *(unsignedlonglong *)(EncryptRegion_ + 4); 222 223 *(unsignedlonglong *)&v45[6] = *(unsignedlonglong *)(EncryptRegion_ + 12);// 这两步 是复制密码的tag 域16Byte/ / v45、v46、v47 这三个连续的一片区域就是 我们的密码 224 225 226 227 } 228 229
参考资料:
[1] so文件调试环境搭建:
https://cloud.tencent.com/developer/article/1357098
[2] Intel白皮书SSE2相关指令查询:
[3] HMAC 摘要签名计算:
https://www.xuebuyuan.com/3193335.html
http://gmssl.org/docs/evp-api.html
[4] OpenSSL AES_256_gcm加解密讲解
https://s0www0openssl0org.icopy.site/docs/man1.1.1/man3/EVP_CipherFinal_ex.html
http://gmssl.org/docs/evp-api.html
[5] AES 的多种加解密模式
https://www.cnblogs.com/0616--ataozhijia/p/11271433.html
[6] Android 跨进程读取内存数据
https://blog.csdn.net/mldxs/article/details/14486827
[7] VS2017 使用OpenSSL库