最近在打开王者营地rank榜单的时候,看到一个msdkEncodeParam参数,每次加密都会变化,出于对其算法实现的好奇,于是出于安全研究的想法,看了其实现过程,以为是很简单的标准算法,作者搞了周六一天才完全还原出来,希望其中的过程对大家提高安全意识有帮助。
⊙1.初步分析参数
⊙2.主动调用
⊙3.算法逆向分析
⊙4.还原的算法代码
1.初步分析参数
2.主动调用
我们只想获得其如何加密过程的话有两个函数需要我们去分析还原,一个是
NewTea::oi_symmetry_encrypt2
另一个是其上个函数内部的
NewTea::TeaEncryptECB
teaEncry 主动调用,及其还原
function call_tea_ecb() {
Java.perform(function() {
var libnative = Module.findBaseAddress("libkings-tea.so");
console.log("libnative-lib.so base address: " + libnative);
var TeaEncryptECB = new NativeFunction(
libnative.add(0xAC1), // 函数的偏移地址,需要替换为实际的偏移地址
'void', // 函数返回类型
['pointer', 'pointer', 'pointer'] // 函数参数类型
);
var pInBuf = Memory.alloc(8);
var pKey = Memory.alloc(16);
var pOutBuf = Memory.alloc(8);
Memory.writeByteArray(pInBuf, [0x96,0x4B, 0x03,0x5F,0xBD,0xA5,0x6A,0xE5]);
Memory.writeByteArray(pKey, [0x33, 0x5e, 0x74, 0x76, 0x42, 0x72, 0x6a, 0x5b, 0x48, 0x38, 0x3e, 0x33, 0x3e, 0x3a, 0x71, 0x6b]
);
TeaEncryptECB(pInBuf, pKey, pOutBuf);
console.log(hexdump(pOutBuf));
});
}
function hook_tea_ecb(){
var libnative = Module.findBaseAddress("libkings-tea.so");
Interceptor.attach(libnative.add(0xAC1), {
onEnter: function (args) {
console.log(hexdump(args[0]));
console.log(hexdump(args[1]));
this.result = args[2]
},
onLeave: function (retval) {
console.log(hexdump(this.result));
}
});
}
还原这个函数是静态的(也就是说固定的参数和key放进去就会出现固定的加密值)
NewTea::oi_symmetry_encrypt2
function call_encrypt2(){
Java.perform(function() {
var libnative = Module.findBaseAddress("libkings-tea.so");
console.log("libnative-lib.so base address: " + libnative);
var NewTea_oi_symmetry_decrypt2 = new NativeFunction(
libnative.add(0xD39), // 函数的偏移地址,需要替换为实际的偏移地址
'void', // 函数返回类型
['pointer', 'int', 'pointer', 'pointer', 'pointer']
);
var pInBuf = Memory.alloc(8); // Allocate 8 bytes of memory
var pKey = Memory.alloc(16); // Allocate 8 bytes of memory
var pOutBuf = Memory.alloc(32); // Allocate 8 bytes of memory
var pOutBufLen = Memory.alloc(Process.pointerSize); // Allocate memory for an int pointer
Memory.writeByteArray(pInBuf, [0x96, 0x4B, 0x03, 0x5F, 0xBD, 0xA5, 0x6A, 0xE5]);
Memory.writeByteArray(pKey, [0x33, 0x5e, 0x74, 0x76, 0x42, 0x72, 0x6a, 0x5b, 0x48, 0x38, 0x3e, 0x33, 0x3e, 0x3a, 0x71, 0x6b]);
NewTea_oi_symmetry_decrypt2(pInBuf, 8, pKey, pOutBuf, pOutBufLen);
console.log(hexdump(pOutBuf));
});
}
function hook_encrypt2(){
var libnative = Module.findBaseAddress("libkings-tea.so");
Interceptor.attach(libnative.add(0xD39), {
onEnter: function (args) {
console.log("encry2——--intput")
console.log(hexdump(args[0]));
this.result = args[3]
},
onLeave: function (retval) {
console.log("encry2——--result")
console.log(hexdump(this.result));
}
});
}
3.逆向分析
首先经过上方的主动调用,发现内部的函数ecb在输入的值和key固定的情况下,所以只是外部的变化;
TeaEncryptECB
家人们,对这种处理也有办法;ps:汇编小知识,sp指针在一个函数中,开辟堆栈使用的,正常的情况下,不会在函数内部进行改变。另一个知识,一个字符串在内存中存储是连续的,
通过上面的两张图,我们可以定位到key的初始地址为[SP,#0x50+var_34]按照tap键转换后的为[SP,#0x50+var_34]。
也就是说这个v20目前的值为[SP,#0x50+var_38]这里面的值取出来的,我们需要知道谁给他写入的
其实这个操作跟写pc端外挂找数据基础地址有异曲同工之妙。
根据上面的操作我们就可以通过汇编追溯到其来源,就可以得到下图的这个dowhille循环中的三个未知量
然后我们还原这个dowhile循环
特征:要进行16次循环,目前经过我们上面的操作,没有已知量。
我们人工模拟下:
还原的时候我们要区别变值和不变值,变值就是在这个dowhile循环中,前面使用的值,在后面要进行覆盖,这个就是变值
先刨除循环的语句 --v15; 前面dowhile并没对那次循环用到这个v15
把我们已知的量填充上去
v16 = pInBuf | (v8 << 16) | (v7 << 24) | (v9 << 8);
v6 = ((Bv14 + v16) ^ (Bpkey[0] + 16 * v16) ^ (Bpkey[1]+ (v16 >> 5)))
((v4 << 16) | (v13 << 24) | (v5 << 8) | v6);
v13 = v6 >> 24;
v4 = v6 >> 16;
v5 = v6 >> 8;
pInBuf = (v16 + ((Bv14 + v6) ^ (Bpkey[2] + 16 * v6) ^ (Bpkey[3] + (v6 >> 5))));
v14 += DELTA; //v14只被读取,每次加一个常量,
v7 = (unsigned int)pInBuf >> 24;
v8 = (unsigned int)pInBuf >> 16;
v9 = (unsigned int)pInBuf >> 8;
现在开始第一轮还原:
v16 = 为pinbuf的高四字节;变值会更新
所以我们先用c语言还原出来
v1 = pInBuf[7] | pInBuf[6] << 8 | pInBuf[5] << 16 | pInBuf[4] << 24;
那么第一轮 v16 = v1
v6 = ((Bv14 + v16) ^ (Bpkey[0] + 16 * v16) ^ (Bpkey[1]+ (v16 >> 5)))只要v16和v4固定了其实, + ((v4 << 16) | (v13 << 24) | (v5 << 8) | v6);这个初始值用一次
那么这个只用一次的值我们定义为 v0 = pInBuf[3] | pInBuf[2] << 8 | pInBuf[1] << 16 | pInBuf[0] << 24;
pInBuf = (v16 + ((Bv14 + v6) ^ (Bpkey[2] + 16 * v6) ^ (Bpkey[3] + (v6 >> 5))));这个值前面都已经算出来了,
然后更新值 v7 v8 v9
这个时候其实可以还原这部分的dowhile了
v0 = pInBuf[3] | pInBuf[2] << 8 | pInBuf[1] << 16 | pInBuf[0] << 24;
v1 = pInBuf[7] | pInBuf[6] << 8 | pInBuf[5] << 16 | pInBuf[4] << 24;
for(i = 0; i < 4; i++) {
k[i] = pKey[i*4 + 3] | pKey[i*4 + 2] << 8 | pKey[i*4 + 1] << 16 | pKey[i*4] << 24;
}
for(i = 0; i < 16; i++) {
sum += DELTA;
v0 += ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
v1 += ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
}
至于上面的这个还原就更简单了,
至此teaEcb算法为
void TeaEncryptECB(uint8_t *pInBuf, uint8_t *pKey, uint8_t *pOutBuf) {
uint32_t v0, v1, sum = 0;
uint32_t k[4];
int i;
v0 = pInBuf[3] | pInBuf[2] << 8 | pInBuf[1] << 16 | pInBuf[0] << 24;
v1 = pInBuf[7] | pInBuf[6] << 8 | pInBuf[5] << 16 | pInBuf[4] << 24;
for(i = 0; i < 4; i++) {
k[i] = pKey[i*4 + 3] | pKey[i*4 + 2] << 8 | pKey[i*4 + 1] << 16 | pKey[i*4] << 24;
}
for(i = 0; i < 16; i++) {
sum += DELTA;
v0 += ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
v1 += ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
}
pOutBuf[3] = v0 & 0xFF;
pOutBuf[2] = (v0 >> 8) & 0xFF;
pOutBuf[1] = (v0 >> 16) & 0xFF;
pOutBuf[0] = (v0 >> 24) & 0xFF;
pOutBuf[7] = v1 & 0xFF;
pOutBuf[6] = (v1 >> 8) & 0xFF;
pOutBuf[5] = (v1 >> 16) & 0xFF;
pOutBuf[4] = (v1 >> 24) & 0xFF;
}
frida主动调用的结果
c代码还原的
还没结束,这个还原的是内层的代码。还有最外层的算法代码需要还原
最外层算法
oi_symmetry_encrypt2算法名字叫这个,目前我们无法从特征看出有啥特点。
直到看到了这个,发现肯定是有填充的,且算法有十个初始值。
可能读者说了,你怎么那么确定呢?因为见到的太多了,后面代码分析过程中我们会怀疑往待加密块增加额外bit的操作。
v7 = (nInBufLen + 10) % 8;
说明整个填充完之后必须是8的倍数,且最小未加密前的长度为16,也就说如果传进去6个字节,那么这种情况就是16个字节。
ps:小插曲,在arm64上的so
if ( nInBufLen + 10 == ((nInBufLen + 10 ) & 0xFFFFFFF8) )
v11 = 0;
else
v11 = 8 - (nInBufLen + 10 - (v6 & 0xFFFFFFF8));
算填充是这么算的,那么他们为啥不同呢?
在arm64这种方式利用了,如果一个数是八的倍数,那么其二进制最后的三个bit一定为0,在这种情况下,一个数是不是8的倍数只要求出来和0xFFFFFFF8与之后,就能得到最接近这个数的8的倍数的值,如果这样与运算之后,得到的是其本身,那么肯定就是8的倍数了。
在还原其他填充类型的算法的时候,还发现 v7 = (nInBufLen + 10) & 7; 也能达到取8余数的目的,这三种一般一出现可能就是求余数。
接下来我们向下分析,v28[0] = lrand48() & 0xF8 | v7; 这个就是额外必须增加的长度 是1,那么另外九个呢
相当于低三位存储了填充的长度,高五位生成的随机数,我们刚才在分析teaecb的时候得到了个信息,每次只能加密8个bit的长度,
又从外部分析到也是8个字节的长度块。
通过hook这里发现,每次加密的八个字节最后面都是七个0,
然后找到了其十的来源。
经过分析:
其待加密密文为: 1bit +pad + salt(2字节) + input + 7个0
然后这个长度一定符合8的倍数的,
然后逐个送进teaEcb进行加密,当然了这也是马后炮,因为还原的时候ida配合frida进行还原的。
总结起来:1bit +pad + salt(2字节) + input + 7个0
分成n个八字节的等份
对第一个等份
data(8bit) ^ = a(0x0)
result = encrypt(data)
result(8bit)^=b(0x0)
同时更新ab
a = result(8bit)
b = data(8bit)
对第二个等份
。。。。一直进行循环
第n个等份,必然会出现有一个input的最后一个字节,和七个0byte
对其再按照上面的加密过程加
这个算法就分析完了,作者在写这篇文章的时候,实现的比较臃肿,如果按照上面的这种思路实现,可能会更优雅一些。
4.还原完的算法代码
void TeaEncryptECB(uint8_t *pInBuf, uint8_t *pKey, uint8_t *pOutBuf) {
uint32_t v0, v1, sum = 0;
uint32_t k[4];
int i;
v0 = pInBuf[3] | pInBuf[2] << 8 | pInBuf[1] << 16 | pInBuf[0] << 24;
v1 = pInBuf[7] | pInBuf[6] << 8 | pInBuf[5] << 16 | pInBuf[4] << 24;
for(i = 0; i < 4; i++) {
k[i] = pKey[i*4 + 3] | pKey[i*4 + 2] << 8 | pKey[i*4 + 1] << 16 | pKey[i*4] << 24;
}
for(i = 0; i < 16; i++) {
sum += DELTA;
v0 += ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
v1 += ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
}
pOutBuf[3] = v0 & 0xFF;
pOutBuf[2] = (v0 >> 8) & 0xFF;
pOutBuf[1] = (v0 >> 16) & 0xFF;
pOutBuf[0] = (v0 >> 24) & 0xFF;
pOutBuf[7] = v1 & 0xFF;
pOutBuf[6] = (v1 >> 8) & 0xFF;
pOutBuf[5] = (v1 >> 16) & 0xFF;
pOutBuf[4] = (v1 >> 24) & 0xFF;
}
void NewTea_oi_symmetry_encrypt2(const uint8_t *pInBuf, int nInBufLen, uint8_t *pKey, uint8_t * pOutBuf , int *pOutBufLen) {
// 初始化部分
int padding = (nInBufLen + 10) % 8;
if (padding != 0) padding = 8 - padding;
uint8_t data[8];
//设置随机数种子,不随机会出现问题气死人
srand48(time(NULL));
// 对第一个字节做特殊处理,用于存储填充长度信息
data[0] = (lrand48() & 0xF8) | padding;
//这个记录data的下标
int buffi = 1;
//填充随机字节
while(padding--){
data[buffi++] = lrand48() & 0xff;
}
uint8_t prev[8];
for(int s = 0; s<8; s++) prev
展开收缩= 0;uint8_t * tmp = prev;
*pOutBufLen =0;
int salti = 1;
int counti;
int countj;
while (salti <3)
{
if(buffi < 8){
data[buffi++] = lrand48() & 0xff;
++salti;
}
if(buffi ==8){
for (counti = 0; counti < 8; counti++){
data[counti ] ^= tmp[counti];
}
TeaEncryptECB(data, pKey, pOutBuf);
for (countj = 0; countj< 8; countj++) {
pOutBuf[countj] ^= prev[countj];
}
for(int q = 0; q < 8; q++) {
printf("%02x", pOutBuf[q]);
}
for (counti= 0; counti < 8; counti++) prev[counti] = data[counti];
tmp = pOutBuf;
buffi =0;
*pOutBufLen += 8;
pOutBuf += 8;
}
}
while (nInBufLen){
if(buffi < 8){
data[buffi++] = *pInBuf;
pInBuf ++ ;
nInBufLen --;
}
if(buffi == 8){
for (counti = 0; counti < 8; counti++){
data[counti ] ^= tmp[counti];
}
TeaEncryptECB(data, pKey, pOutBuf);
for (countj = 0; countj< 8; countj++) {
pOutBuf[countj] ^= prev[countj];
}
printf("n");
for(int q = 0; q < 8; q++) {
printf("%02x", pOutBuf[q]);
}
printf("n");
for (counti= 0; counti < 8; counti++) prev[counti] = data[counti];
tmp = pOutBuf;
buffi =0;
*pOutBufLen += 8;
pOutBuf += 8;
}
}
int countm=1 ;
while(countm <= 8){
if(buffi < 8){
data[buffi++] = 0;
++countm;
}
if(buffi == 8){
for (counti = 0; counti < 8; counti++){
data[counti ] ^= tmp[counti];
}
TeaEncryptECB(data, pKey, pOutBuf);
for (countj = 0; countj< 8; countj++) {
pOutBuf[countj] ^= prev[countj];
}
for(int q = 0; q < 8; q++) {
printf("%02x", pOutBuf[q]);
}
printf("n");
for (counti= 0; counti < 8; counti++) prev[counti] = data[counti];
tmp = pOutBuf;
buffi =0;
*pOutBufLen += 8;
pOutBuf += 8;
}
}
}
int main() {
uint8_t in[8] = {0x96,0x4B, 0x03,0x5F,0xBD,0xA5,0x6A,0xE5};
uint8_t key[16] = {0x33, 0x5e, 0x74, 0x76, 0x42, 0x72, 0x6a, 0x5b, 0x48, 0x38, 0x3e, 0x33, 0x3e, 0x3a, 0x71, 0x6b};
uint8_t out1[24] = {0};
int * outlen = new int(0);
NewTea_oi_symmetry_encrypt2(in, 8, key, out1, outlen);
printf("%dn", *outlen);
for(int i = 0; i < *outlen; i++) {
printf("%02x", out1[i]);
}
printf("n");
delete outlen;
return 0;
}
那么试试算法吧:
如果读者想要获取精彩内容可以关注我朋友:
我是BestToYou,分享工作或日常学习中关于Android、iOS逆向及安全防护的一些思路和一些自己闲暇时刻调试的一些程序,文中若有错误或者不足的地方,恳请大家联系我批评指正。
扫码加我为好友
原文始发于微信公众号(二进制科学):王者营di-巅峰榜sdkEncodeParam算法逆向
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论