王者营di-巅峰榜sdkEncodeParam算法逆向

admin 2024年10月22日12:58:38评论53 views字数 8578阅读28分35秒阅读模式
王者营di-巅峰榜sdkEncodeParam算法逆向

最近在打开王者营地rank榜单的时候,看到一个msdkEncodeParam参数,每次加密都会变化,出于对其算法实现的好奇,于是出于安全研究的想法,看了其实现过程,以为是很简单的标准算法,作者搞了周六一天才完全还原出来,希望其中的过程对大家提高安全意识有帮助。

王者营di-巅峰榜sdkEncodeParam算法逆向
王者营di-巅峰榜sdkEncodeParam算法逆向

1.初步分析参数

2.主动调用

3.算法逆向分析

4.还原的算法代码

1.初步分析参数

王者营di-巅峰榜sdkEncodeParam算法逆向

王者营di-巅峰榜sdkEncodeParam算法逆向

 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

王者营di-巅峰榜sdkEncodeParam算法逆向

家人们,对这种处理也有办法;ps:汇编小知识,sp指针在一个函数中,开辟堆栈使用的,正常的情况下,不会在函数内部进行改变。另一个知识,一个字符串在内存中存储是连续的,

王者营di-巅峰榜sdkEncodeParam算法逆向

王者营di-巅峰榜sdkEncodeParam算法逆向

通过上面的两张图,我们可以定位到key的初始地址为[SP,#0x50+var_34]按照tap键转换后的为[SP,#0x50+var_34]。

王者营di-巅峰榜sdkEncodeParam算法逆向

王者营di-巅峰榜sdkEncodeParam算法逆向

也就是说这个v20目前的值为[SP,#0x50+var_38]这里面的值取出来的,我们需要知道谁给他写入的

王者营di-巅峰榜sdkEncodeParam算法逆向

王者营di-巅峰榜sdkEncodeParam算法逆向

其实这个操作跟写pc端外挂找数据基础地址有异曲同工之妙。

根据上面的操作我们就可以通过汇编追溯到其来源,就可以得到下图的这个dowhille循环中的三个未知量

王者营di-巅峰榜sdkEncodeParam算法逆向

然后我们还原这个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]);    }

王者营di-巅峰榜sdkEncodeParam算法逆向

至于上面的这个还原就更简单了,

至此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主动调用的结果

王者营di-巅峰榜sdkEncodeParam算法逆向

c代码还原的

王者营di-巅峰榜sdkEncodeParam算法逆向

还没结束,这个还原的是内层的代码。还有最外层的算法代码需要还原

最外层算法

oi_symmetry_encrypt2算法名字叫这个,目前我们无法从特征看出有啥特点。

王者营di-巅峰榜sdkEncodeParam算法逆向

直到看到了这个,发现肯定是有填充的,且算法有十个初始值。

可能读者说了,你怎么那么确定呢?因为见到的太多了,后面代码分析过程中我们会怀疑往待加密块增加额外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个字节的长度块。

王者营di-巅峰榜sdkEncodeParam算法逆向

通过hook这里发现,每次加密的八个字节最后面都是七个0,

然后找到了其十的来源。

经过分析:

其待加密密文为: 1bit +pad + salt(2字节) +  input + 7个0

然后这个长度一定符合8的倍数的,

然后逐个送进teaEcb进行加密,当然了这也是马后炮,因为还原的时候ida配合frida进行还原的。

王者营di-巅峰榜sdkEncodeParam算法逆向

王者营di-巅峰榜sdkEncodeParam算法逆向

总结起来:1bit +pad + salt(2字节) + input + 7个0
分成n个八字节的等份

对第一个等份data(8bit) ^ = a(0x0)result = encrypt(data)result(8bit)^=b(0x0)同时更新aba = result(8bit)b = data(8bit)对第二个等份。。。。一直进行循环第n个等份,必然会出现有一个input的最后一个字节,和七个0byte对其再按照上面的加密过程加

这个算法就分析完了,作者在写这篇文章的时候,实现的比较臃肿,如果按照上面的这种思路实现,可能会更优雅一些。

4.还原完的算法代码

#include <stdio.h>#include <cstdlib>#include <time.h>#define DELTA 0x9e3779b9void 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;}

那么试试算法吧:

王者营di-巅峰榜sdkEncodeParam算法逆向

如果读者想要获取精彩内容可以关注我朋友:

我是BestToYou,分享工作或日常学习中关于Android、iOS逆向及安全防护的一些思路和一些自己闲暇时刻调试的一些程序,文中若有错误或者不足的地方,恳请大家联系我批评指正。

王者营di-巅峰榜sdkEncodeParam算法逆向

扫码加我为好友

原文始发于微信公众号(二进制科学):王者营di-巅峰榜sdkEncodeParam算法逆向

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月22日12:58:38
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   王者营di-巅峰榜sdkEncodeParam算法逆向https://cn-sec.com/archives/1851181.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息