首页
社区
课程
招聘
最新版shield纯算分析
2024-5-6 09:55 3530

最新版shield纯算分析

2024-5-6 09:55
3530

前言

版本是8.32.0,2024/04/19发布的,侵权系删!!!
抓包就不抓了,每个包headers里面都有一个shield,格式是XYAAAAAQAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG434NfuFQTKRBzI2yneNkH53+rOYKz8Mi1cx+2KY3QQwYRWWLN7/z1S011Oc8PZAcZStVDuw6DCul1soQ
这个参数这个版本搜不到,它是通过okhttp3的Interceptor添加上去的,添加的位置在so层.这个app有点特殊,不能说太多,关键位置都打码了.此篇是删减版

hook

既然是加密就得确认入参,先打印看看有哪些Interceptor,需要以spawn的形式启动,新版有libmsaoaidsec.so的frida反调试,拆开来是msa open aid security 不是读msao,很多人读错了,网上自行寻找过frida检测的方法,我这里不介绍.

hook Interceptor

hook验证发现是在com.xxxxx.shield.http.xxxHttpInterceptor类的intercept方法把shield添加上去的,传入的时候头部没有shield,下一个拦截器y62.b里出现了,所以是这个位置

hook intercept

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
Java.perform(function (){
    let xxxHttpInterceptor = Java.use("com.xxxxx.shield.http.xxxHttpInterceptor");
    xxxHttpInterceptor["intercept"].overload('okhttp3.Interceptor$Chain', 'long').implementation = function (chain, j2) {
    var request = chain.request();
    console.log('1111111111111111111111:',j2)
    var url = request.url().toString()
    console.log('url:',url)
    var headerString = request.headers().toString();
    console.log('headers:',headerString)
    console.log('1111111111111111111111')
    let result = this["intercept"](chain, j2);
    return result;
};
})
Java.perform(function (){
    let xxxHttpInterceptor = Java.use("y62.b");
     xxxHttpInterceptor["intercept"].overload('okhttp3.Interceptor$Chain').implementation = function (chain) {
    console.log('22222222222222222222222')
    var request = chain.request();
    var url = request.url().toString()
    console.log('url:',url)
    var headerString = request.headers().toString();
    console.log('headers:',headerString)
    console.log('22222222222222222222222')
    let result = this["intercept"](chain);
    return result;
};
})

结果

有敏感信息,不展示

后续的unidbg中的入参来自这组hook的结果,这组是get请求,post请求自己hook一个,没什么区别,只是多传了组参数.
com.xxxxx.shield.http.xxxHttpInterceptor类下的intercept方法,是一个native方法,用的jadx1.5.0,感觉更新后反编译速度快了,不那么占内存了,搜索也不卡,快去试试吧!
图片不展示,有敏感信息
上面还有3个native方法,看名字的意思就知道初始化,用unidbg跑的话必须要先初始化,否则会跑不出结果或者异常结果,这得益于这个app没有把名字去掉可以看出来,有的没有这种init字眼,或者只有一个native方法,根据不同的入参数值来进行初始化,初始化和入参共一个函数,比如mt的和阿里的.网上也有unidbg的代码,我这里直接放代码了,后续用unidbg来还原算法

shield.so

先确认下是哪个so

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
var addrRegisterNatives = null;
 
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {
    var symbol = symbols[i];
    if (symbol.name.indexOf("art") >= 0 &&
        symbol.name.indexOf("JNI") >= 0 &&
        symbol.name.indexOf("RegisterNatives") >= 0 &&
        symbol.name.indexOf("CheckJNI") < 0) {
 
        addrRegisterNatives = symbol.address;
        console.log("RegisterNatives is at ", symbol.address, symbol.name);
        break
    }
}
if (addrRegisterNatives) {
    Interceptor.attach(addrRegisterNatives, {
        onEnter: function (args) {
            var env = args[0];        // jni对象
            var java_class = args[1]; // 类
            var class_name = Java.vm.tryGetEnv().getClassName(java_class);
            var taget_class = "com.xxxxx.shield.http.xxxHttpInterceptor";   //111 某个类中动态注册的so
            if (class_name === taget_class) {
                //只找我们自己想要类中的动态注册关系
                console.log("\n[RegisterNatives] method_count:", args[3]);
                var methods_ptr = ptr(args[2]);
                var method_count = parseInt(args[3]);
                for (var i = 0; i < method_count; i++) {
                    // Java中函数名字的
                    var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                    // 参数和返回值类型
                    var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                    // C中的函数内存地址
                    var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
                    var name = Memory.readCString(name_ptr);
                    var sig = Memory.readCString(sig_ptr);
                    var find_module = Process.findModuleByAddress(fnPtr_ptr);
                    // 地址、偏移量、基地址
                    var offset = ptr(fnPtr_ptr).sub(find_module.base);
                    console.log("name:", name, "sig:", sig,'module_name:',find_module.name ,"offset:", offset);
 
                }
            }
        }
    });
}
// 动态注册函数地址
// frida -U -f com.xingin.xxx -l hook_so_register.js 

结果

1
2
3
4
name: initializeNative sig: ()V module_name: libxyass.so offset: 0xbd8bc
name: intercept sig: (Lokhttp3/Interceptor$Chain;J)Lokhttp3/Response; module_name: libxyass.so offset: 0xbc6b4
name: initialize sig: (Ljava/lang/String;)J module_name: libxyass.so offset: 0xbc3c8
name: destroy sig: (J)V module_name: libxyass.so offset: 0xbc380

libxyass.so,这个版本只有64位的so,拉到ida64先看一下
在这里插入图片描述
都是seg段,加固了,用sodump dump下来再修复,之前星球发过这个工具
dump下来的是正常的,上面的"进度条"变蓝了
c

同时可以看到这个so真正的名字是libshield.so

unidbg还原算法

1
代码不公开

上面hook的结果中headers有很多参数,我测试过只有url,xy-common-params,xy-direction会影响最终结果,除了这些参数外,unidbg中补的环境也有影响结果的参数,deviceId和main_hmac.除此以外还有些小参数在xy-common-params会有体现,后面我们遇到的时候再说.

trace

先trace一份结果,后面遇到需要trace的地方看这份结果就行了,时机的话选择so刚加载进来的位置
在这里插入图片描述

1
2
3
4
5
6
7
8
String traceFile = "unidbg-android/src/test/java/com/xxx/trace/trace2.txt";
        PrintStream traceStream = null;
        try{
            traceStream = new PrintStream(new FileOutputStream(traceFile), true);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
emulator.traceCode(0x40000000,0x40000000+1245184).setRedirect(traceStream);

trace的结果10多万行,先放一边,这种入参那么多的肯定是从后往前推效果更好.
先从jni的日志中开始定位,0xbcf2c
在这里插入图片描述
在这里插入图片描述
这里newstringutf是将cstring转jstring,所以要追踪v89的来源,600多行直接找太麻烦,直接监控v89这块内存的写入
看下这处的汇编在这里插入图片描述
BCF2C执行完后跳到13a18执行,13a18应该是newstringutf,这样的话x1就是第一个参数,下断看下

1
2
3
4
public void HookByConsoleDebugger() {
    Debugger debugger = emulator.attach();
    debugger.addBreakPoint(module.base+0xBCF2C);
    }

在这里插入图片描述
地址是0x40461018,监控这块内存的写入,最终结果是134字节

1
emulator.traceWrite(0x40461018,0x40461018+134);

在这里插入图片描述
前两个字节是5859是XY拼接的,看后132字节就行了
在这里插入图片描述
[libc.so]0x1c1f8 pc指向libc的话一般用的是memcpy函数,直接看lr寄存器的位置就可以了,0x9f0f0
在这里插入图片描述
0x9f0f0的上一行memcpy,9F0EC下断看下内存地址,memcpy的第一个参数buffer,第二个源数据,第三个源数据长度,看第二个参数地址
在这里插入图片描述
继续监控0x4059a018
在这里插入图片描述
0x4bfac跳过去看一下
在这里插入图片描述
有base64 table,跳过去看了下是标准的,同时看到了v3 += 3LL;和v4 += 4;确实符合base64的是3字节转4字节数据,数据来源是a2,hook验证下是不是标准的,4BF44的第二个参数
在这里插入图片描述
在这里插入图片描述
验证了,是标准base64

第3个参数0x63是参数2的长度所以目标变成了追踪这0x63个字节,地址是0x40456098
在这里插入图片描述
还是之前的trace追踪方法
在这里插入图片描述
0x40593000,接着跟
在这里插入图片描述

前16字节

发现前16字节和后面字节的赋值位置不一样,先看前面的,0x497b4
在这里插入图片描述
来自v4,v4来自a1,函数是sub_49650,在49650下断
在这里插入图片描述
在这里插入图片描述
是个指针,小端序
在这里插入图片描述
跟踪0x40453196偏移16字节的位置
在这里插入图片描述
0x4930c
在这里插入图片描述
上面的两个01是固定在so里面的,看后面的两个0x53,都来自于v7,v7和a1有关,看下a1是什么
sub_4926C
在这里插入图片描述
一个指针,指向0x4058e010
在这里插入图片描述
(*(*a1 + 20LL) + *(*a1 + 24LL) + *(*a1 + 28LL) + 24);这行代码的结果是0x53怎么来的?
0x07+0x24+0x10+0x18=0x53,前3个数字就是上图中的20,24,28位置的值,还要继续追吗?其实改改入参的话会发现这个结果始终是0x53,为了严谨点还是跟过去看看吧.
只需要3个字节就可了

1
emulator.traceWrite(0x4058e024,0x4058e024+9);

在这里插入图片描述
0x49138
在这里插入图片描述
这个函数是sub_4908C,来自a2,a5,a7
a2和a5都是指针
在这里插入图片描述
长度是07,是build,在xy-common-params里面有
在这里插入图片描述
长度0x24,是deviceid
在这里插入图片描述
长度0x10,这个很重要,是后续魔改md5的最终结果,这个非常重要
除了这几个还有app_id,后续我不再这样跟了,出现的时候我说一声

后0x53字节

RC4

在这里插入图片描述
0x497dc,都是赋值的位置,我们要找最初生成的位置,和上面一样的跟踪,这里不重复上面的内容了,只需定位一次就可定位到,在0x5126c
在这里插入图片描述
在这里插入图片描述
一轮8个字节,一共10轮,再加后序添加的3个字节,入参是a3,先看一眼入参
在这里插入图片描述
也是0x53个字节,后16字节是前面说到的md5的结果,ECFAAF01是app_id,在xy-common-params中有,
和上面跟踪0x53一样的思路,01,02是固定在so里的,07,24,10就是我们上面跟踪的,中间的结果由build+deviceid拼接成.所以最终的目标就是后16个字节,魔改md5的结果.
等等,还有眼前的这83个字节转换呢!
这个是标准的RC4,先介绍下RC4
1 RC4是一种流加密算法,密钥长度可变。它加解密使用相同的密钥,因此也属于对称加密算法。
2 通过算法生成一个256字节的S-box。再通过算法每次取出S-box中的某一字节K.将K与明文做异或得到密文。
下面是我学RC4时的笔记可以参考一下,或者网上找篇文章学习下

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
总结
通过算法生成一个256字节的S-box。
再通过算法每次取出S-box中的某一字节K.
将K与明文做异或得到密文。
 
重点是打乱的s盒
1、先初始化状态向量S(256个字节,用来作为密钥流生成的种子1)
    按照升序,给每个字节赋值0,1,2,3,4,5,6.....,254,255
2、初始密钥(由用户输入),长度任意
    如果输入长度小于256个字节,则进行轮转,直到填满
    例如输入密钥的是1,2,3,4,5   ,  那么填入的是1,2,3,4,5,1,2,3,4,5,1,2,3,4,5........
3、开始对状态向量S进行置换操作(用来打乱初始种子1)
    j = 0;
   for (i = 0 ; i < 256 ; i++){
    j = (j + S[i] + T[i]) % 256;
    swap(S[i] , S[j]);
   }
4、最后是秘钥流的生成与加密,很多人在这里不是特别理解
    假设我的明文字节数是datalength=1024个字节(当然可以是任意个字节)
    i=0;
    j=0;
    while(datalength--){//相当于执行1024次,这样生成的秘钥流也是1024个字节
         i = (i + 1) % 256;
        j = (j + S[i]) % 256;
        swap(S[i] , S[j]);
        t = (S[i] + S[j]) % 256;
        k = S[t];这里的K就是当前生成的一个秘钥流中的一位
        //可以直接在这里进行加密,当然也可以将密钥流保存在数组中,最后进行异或就ok
        data[]=data[]^k; //进行加密,"^"是异或运算符
    }
    解密按照前面写的,异或两次就是原文,所以只要把密钥流重新拿过来异或一次就能得到原文了
    这样就完成了一次生成密钥流及加密的过程,这也是RC4的全部工作,是不是很简单呢?

我刚开始弄的时候没看出来RC4,虽然他一直在swap我也没想到,遇到的少,就是纯看汇编和c代码还原的,以及前面trace的文本,还原出来是查表法实现的

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
def swap(list, swap1, swap2):
    int1 = list.index(swap1)
    int2 = list.index(swap2)
    # 交换值
    list[int1], list[int2] = list[int2], list[int1]
    return list
def eightyRounds(i, result1, next, _str, a3):
    for j in range(8):
        mid = result1[i * 8 + j + 1]
        # print(hex(mid))
        _swap = result1[(mid + next) & 0xff]
        # print(hex(_swap))
        result1 = swap(result1, mid, _swap)
        res = a3[i * 8 + j] ^ result1[(_swap + mid) & 0xff]
        # print(f'结果{i+1}_{j+1}:', hex(res))
 
        _str += hex(res)[2:] if len(hex(res)[2:]) == 2 else '0' + hex(res)[2:]
        next = (mid + next) & 0xff
    return result1, next, _str
 
result1 = [0x7c, 0x2e, 0x0e, 0x3d, 0x09, 0x32, 0xe3, 0x1c, 0x87, 0xc9, 0xb3, 0x2d, 0xb1, 0x2b, 0xdd, 0x20, 0x0b, 0xc3
    , 0x28, 0x5c, 0x0f, 0x7b, 0x12, 0x53, 0x0c, 0x2f, 0x92, 0x23, 0x30, 0xce, 0x50, 0xd0, 0x40, 0x1f, 0x45, 0x33
    , 0x3b, 0x95, 0xb2, 0x15, 0xbc, 0x34, 0x89, 0xe6, 0xd5, 0x39, 0x68, 0x78, 0xf1, 0x80, 0x7f, 0xc1, 0xca, 0x3e
    , 0x71, 0x52, 0x7a, 0x5e, 0xe7, 0xa4, 0x37, 0xa5, 0x69, 0xd1, 0x4c, 0x73, 0xb6, 0x5b, 0xfe, 0x16, 0xcf, 0x55
    , 0xcb, 0xc0, 0xf9, 0x24, 0x10, 0xa8, 0x6b, 0xe0, 0x62, 0x8c, 0x63, 0xd9, 0xbd, 0xda, 0x31, 0xf3, 0x96, 0xe4
    , 0x75, 0x5d, 0xf7, 0x79, 0x22, 0x57, 0xde, 0xbf, 0x91, 0x86, 0xea, 0x85, 0x97, 0x4d, 0x83, 0x00, 0xa9, 0x84
    , 0x14, 0x27, 0x1a, 0x17, 0xed, 0xe1, 0x1b, 0x6f, 0x41, 0x59, 0xa2, 0x99, 0xfc, 0x4b, 0x1e, 0xa7, 0x74, 0xab
    , 0x7d, 0x88, 0x5a, 0x51, 0xe2, 0xbe, 0xa6, 0x77, 0x67, 0x58, 0x11, 0x0d, 0xa0, 0x8f, 0x08, 0x2a, 0x72, 0xd2
    , 0xc2, 0x9b, 0x36, 0xd7, 0xf2, 0x70, 0x35, 0x8e, 0xd3, 0x03, 0xb7, 0xb9, 0xc8, 0x2c, 0x93, 0xb0, 0xee, 0x8d
    , 0x07, 0xf8, 0x47, 0x8b, 0xe9, 0x90, 0x04, 0x46, 0x94, 0xd4, 0x49, 0x4e, 0xb5, 0xd6, 0xa1, 0xac, 0x4f, 0xbb
    , 0xdc, 0xff, 0x18, 0xba, 0xaf, 0xc4, 0x26, 0x6e, 0x9e, 0x6d, 0x3a, 0x5f, 0xc7, 0x82, 0x44, 0xae, 0xcd, 0x8a
    , 0xad, 0x3c, 0x38, 0x01, 0xa3, 0xdf, 0xc5, 0x05, 0x02, 0xf4, 0xf0, 0x06, 0x13, 0x3f, 0xef, 0x29, 0x64, 0xfd
    , 0x66, 0x25, 0x60, 0xeb, 0xe8, 0x61, 0x7e, 0xfa, 0x54, 0x9a, 0xfb, 0xd8, 0xf5, 0xb4, 0x48, 0x76, 0x43, 0x65
    , 0x6a, 0xec, 0x9c, 0x1d, 0xf6, 0x0a, 0xe5, 0x9f, 0x42, 0x6c, 0xc6, 0xb8, 0xcc, 0x9d, 0xaa, 0xdb, 0x98, 0x21
    , 0x81, 0x56, 0x4a, 0x19]
next = 0
_str = ''
a3 = [0x00, 0x00, 0x00, 0x01, 0xec, 0xfa, 0xaf, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00
    , 0x24, 0x00, 0x00, 0x00, 0x10, 0x38, 0x33, 0x32, 0x30, 0x36, 0x38, 0x39, 0x33, 0x39, 0x39, 0x32, 0x33, 0x65, 0x33
    , 0x63, 0x2d, 0x36, 0x63, 0x36, 0x65, 0x2d, 0x33, 0x38, 0x39, 0x31, 0x2d, 0x39, 0x34, 0x35, 0x61, 0x2d, 0x66, 0x64
    , 0x30, 0x33, 0x63, 0x34, 0x37, 0x39, 0x36, 0x65, 0x62, 0x33, 0xa9, 0xec, 0xec, 0xc3, 0xc7, 0x90, 0xc4, 0xe7, 0x78
    , 0x41, 0x37, 0x63, 0xf3, 0xf5, 0x3d, 0xab]
for i in range(10):
    result1, next, _str = eightyRounds(i, result1, next, _str, a3)
 
# 第81个字节
byte1 = a3[80] ^ result1[(result1[0x51] + result1[(result1[0x51] + next) & 0xff]) & 0xff]
 
next = result1[0x51] + next
_str += hex(byte1)[2:] if len(hex(byte1)[2:]) == 2 else '0' + hex(byte1)[2:]
result1 = swap(result1, result1[0x51], result1[(result1[0x51] + next) & 0xff])
 
# 第82个字节
byte2 = a3[81] ^ result1[(result1[0x52] + result1[(result1[0x52] + next) & 0xff]) & 0xff]
 
next = result1[0x52] + next
_str += hex(byte2)[2:] if len(hex(byte2)[2:]) == 2 else '0' + hex(byte2)[2:]
result1 = swap(result1, result1[0x52], result1[(result1[0x52] + next) & 0xff])
 
# 第83个字节
byte2 = a3[82] ^ result1[(result1[0x53] + result1[(result1[0x53] + next) & 0xff]) & 0xff]
 
_str += hex(byte2)[2:] if len(hex(byte2)[2:]) == 2 else '0' + hex(byte2)[2:]
print(_str)

这里就不带大家扣了,仔细点对着汇编来还原应该都可以,我们来看看这个256字节的表是怎么来的,能否还原最初始的秘钥
在使用RC4加密时,不应使用弱密钥。当密钥长度超过128位时,至今也没有有效的破解手段,所以还是看看能不能在so里找到秘钥,硬破解不一定能弄.
sub_511E0的入参就是256字节的表,不过中间隔了3个00
在这里插入图片描述
0xbffff090这块内存是栈内存

1
emulator.traceWrite(0xbffff099L,0xbffff090+5);

在这里插入图片描述
pc和lr寄存器指向的都是libc,这样不太好跟,看下调用栈,断下按bt
图片不展示,敏感信息
0x04956c
在这里插入图片描述
v21来自51698,这个函数传了13个字节,会不会是秘钥?
在这里插入图片描述

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
def swap(list, swap1, swap2):
    int1 = list.index(swap1)
    int2 = list.index(swap2)
    # 交换值
    list[int1], list[int2] = list[int2], list[int1]
    return list
j = 0;
 
S = [i for i in range(256)]
T = [0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61
     ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29
     ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a
     ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28
     ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a
     ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74
     ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64
     ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72
     ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74
     ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f
     ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73
     ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62
     ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72 ,0x74 ,0x28 ,0x29 ,0x3b
     ,0x73 ,0x74 ,0x64 ,0x3a ,0x3a ,0x61 ,0x62 ,0x6f ,0x72]
for i in range(256):
    j = (j + S[i] + T[i]) % 256
    swap(S,S[i], S[j]);
def hex_list(list):
    return [hex(i) for i in list]
print(hex_list(S))

扩展完和上面256字节的表对的上,这样秘钥也找到了,拿去加密验证下
在这里插入图片描述
验证了,是标准RC4,所有最终需要的参数就是后16字节,魔改MD5的结果

魔改md5

还是上面说的跟踪方法,这里不跟了,地址是0x539DC,这个函数一共跑5次,对应md5的updata函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
debugger.addBreakPoint(module.base+0x539DC, new BreakPointCallback() {
        int num = 0;
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext context = emulator.getContext();
            Pointer src1 = context.getPointerArg(0);
            Pointer src2 = context.getPointerArg(1);
            num+=1;
            System.out.println("num==================================="+num);
            Inspector.inspect(src1.getByteArray(0,0x70), "0x539DC onenter arg0 "+num);
            Inspector.inspect(src2.getByteArray(0,0x70), "0x539DC onenter arg1 "+num);
            emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
                @Override
                public boolean onHit(Emulator<?> emulator, long address) {
                    Inspector.inspect(src1.getByteArray(0,0x70), "0x539DC onleave arg0 "+num);
                    Inspector.inspect(src2.getByteArray(0,0x70), "0x539DC onleave arg1 "+num);
                    return true;
                }
            });
        return true;
        }
    });

标准hmac

输出内容太多了,就展示部分,这其实是个hmac md5,hamc方法是标准的,需要结合上面hook的日志来看,我这里直接说结果了,你们自己验证下.hmac去之前ks那篇又讲或者网上找些资料.
第一次 update 64字节的0x36的扩展秘钥得到context1
第二次 update 64字节的0x5c的扩展秘钥得到context2
这两次的魔数(iv)都是
a0 = 0x10325476
b0 = 0x98badcfe
c0 = 0xefcdab89
d0 = 0x67452301
和标准的不一样
第三次 魔数用的context1,入参用的url(去掉协议和问号) + xy-common-params + xy-direction + xy-platform-info,也对应着update过程
第四次 就是第三次的填充结果的updata,不满512分组要填充,看到有0x80想到填充,得到第一个md5的结果
第五次 魔数用的context2,入参是第四次得到的md5的结果,最终加密成A9 EC EC C3 C7 90 C4 E7 78 41 37 63 F3 F5 3D AB
单看一二和三四五就发现是标准hmac

md5魔改点

ida中c代码太长了,500行,我这里就截关键位置
1 k表
2 魔数
3 循环左移ida中的是循环右移,需要用32减去,同时循环左移的位数也改了,不是32减标准的,没规律
在这里插入图片描述
4 md5 64轮,每16轮算一小轮,第3小轮改了运算顺序
以下是一个标准的md5

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
import struct
def md5_1(message):
    def left_rotate(x, amount):
        return ((x << amount) | (x >> (32 - amount))) & 0xFFFFFFFF
 
    def F(x, y, z):
        return (x & y) | (~x & z)
 
    def G(x, y, z):
        return (x & z) | (y & ~z)
 
    def H(x, y, z):
        return x ^ y ^ z
 
    def I(x, y, z):
        return y ^ (x | ~z)
 
    def FF(a, b, c, d, x, s, ac):
        a = (a + F(b, c, d) + x + ac) & 0xFFFFFFFF
        return left_rotate(a, s) + b & 0xFFFFFFFF
 
    def GG(a, b, c, d, x, s, ac):
        a = (a + G(b, c, d) + x + ac) & 0xFFFFFFFF
        return left_rotate(a, s) + b & 0xFFFFFFFF
 
    def HH(a, b, c, d, x, s, ac):
        a = (a + H(b, c, d) + x + ac) & 0xFFFFFFFF
        return left_rotate(a, s) + b & 0xFFFFFFFF
 
    def II(a, b, c, d, x, s, ac):
        a = (a + I(b, c, d) + x + ac) & 0xFFFFFFFF
        return left_rotate(a, s) + b & 0xFFFFFFFF
 
    def pad_message(message): # 填充
        original_length_bits = len(message) * 8
        message += b'\x80'
        while (len(message) + 8) % 64 != 0:
            message += b'\x00'
        message += struct.pack('<Q', original_length_bits)
        return message
 
    a0 = 0x67452301
    b0 = 0xEFCDAB89
    c0 = 0x98BADCFE
    d0 = 0x10325476
    message = pad_message(message)
    chunks = [message[i:i + 64] for i in range(0, len(message), 64)]
    for chunk in chunks:
        words = struct.unpack('<16I', chunk)
        A, B, C, D = a0, b0, c0, d0
        # Round 1
        A = FF(A, B, C, D, words[0], 7, 0xD76AA478)
        D = FF(D, A, B, C, words[1], 12, 0xE8C7B756)
        C = FF(C, D, A, B, words[2], 17, 0x242070DB)
        B = FF(B, C, D, A, words[3], 22, 0xC1BDCEEE)
        A = FF(A, B, C, D, words[4], 7, 0xF57C0FAF)
        D = FF(D, A, B, C, words[5], 12, 0x4787C62A)
        C = FF(C, D, A, B, words[6], 17, 0xA8304613)
        B = FF(B, C, D, A, words[7], 22, 0xFD469501)
        A = FF(A, B, C, D, words[8], 7, 0x698098D8)
        D = FF(D, A, B, C, words[9], 12, 0x8B44F7AF)
        C = FF(C, D, A, B, words[10], 17, 0xFFFF5BB1)
        B = FF(B, C, D, A, words[11], 22, 0x895CD7BE)
        A = FF(A, B, C, D, words[12], 7, 0x6B901122)
        D = FF(D, A, B, C, words[13], 12, 0xFD987193)
        C = FF(C, D, A, B, words[14], 17, 0xA679438E)
        B = FF(B, C, D, A, words[15], 22, 0x49B40821)
        # Round 2
        A = GG(A, B, C, D, words[1], 5, 0xF61E2562)
        D = GG(D, A, B, C, words[6], 9, 0xC040B340)
        C = GG(C, D, A, B, words[11], 14, 0x265E5A51)
        B = GG(B, C, D, A, words[0], 20, 0xE9B6C7AA)
        A = GG(A, B, C, D, words[5], 5, 0xD62F105D)
        D = GG(D, A, B, C, words[10], 9, 0x02441453)
        C = GG(C, D, A, B, words[15], 14, 0xD8A1E681)
        B = GG(B, C, D, A, words[4], 20, 0xE7D3FBC8)
        A = GG(A, B, C, D, words[9], 5, 0x21E1CDE6)
        D = GG(D, A, B, C, words[14], 9, 0xC33707D6)
        C = GG(C, D, A, B, words[3], 14, 0xF4D50D87)
        B = GG(B, C, D, A, words[8], 20, 0x455A14ED)
        A = GG(A, B, C, D, words[13], 5, 0xA9E3E905)
        D = GG(D, A, B, C, words[2], 9, 0xFCEFA3F8)
        C = GG(C, D, A, B, words[7], 14, 0x676F02D9)
        B = GG(B, C, D, A, words[12], 20, 0x8D2A4C8A)
        # Round 3
        A = HH(A, B, C, D, words[5], 4, 0xFFFA3942)
        D = HH(D, A, B, C, words[8], 11, 0x8771F681)
        C = HH(C, D, A, B, words[11], 16, 0x6D9D6122)
        B = HH(B, C, D, A, words[14], 23, 0xFDE5380C)
        A = HH(A, B, C, D, words[1], 4, 0xA4BEEA44)
        D = HH(D, A, B, C, words[4], 11, 0x4BDECFA9)
        C = HH(C, D, A, B, words[7], 16, 0xF6BB4B60)
        B = HH(B, C, D, A, words[10], 23, 0xBEBFBC70)
        A = HH(A, B, C, D, words[13], 4, 0x289B7EC6)
        D = HH(D, A, B, C, words[0], 11, 0xEAA127FA)
        C = HH(C, D, A, B, words[3], 16, 0xD4EF3085)
        B = HH(B, C, D, A, words[6], 23, 0x04881D05)
        A = HH(A, B, C, D, words[9], 4, 0xD9D4D039)
        D = HH(D, A, B, C, words[12], 11, 0xE6DB99E5)
        C = HH(C, D, A, B, words[15], 16, 0x1FA27CF8)
        B = HH(B, C, D, A, words[2], 23, 0xC4AC5665)
        # Round 4
        A = II(A, B, C, D, words[0], 6, 0xF4292244)
        D = II(D, A, B, C, words[7], 10, 0x432AFF97)
        C = II(C, D, A, B, words[14], 15, 0xAB9423A7)
        B = II(B, C, D, A, words[5], 21, 0xFC93A039)
        A = II(A, B, C, D, words[12], 6, 0x655B59C3)
        D = II(D, A, B, C, words[3], 10, 0x8F0CCC92)
        C = II(C, D, A, B, words[10], 15, 0xFFEFF47D)
        B = II(B, C, D, A, words[1], 21, 0x85845DD1)
        A = II(A, B, C, D, words[8], 6, 0x6FA87E4F)
        D = II(D, A, B, C, words[15], 10, 0xFE2CE6E0)
        C = II(C, D, A, B, words[6], 15, 0xA3014314)
        B = II(B, C, D, A, words[13], 21, 0x4E0811A1)
        A = II(A, B, C, D, words[4], 6, 0xF7537E82)
        D = II(D, A, B, C, words[11], 10, 0xBD3AF235)
        C = II(C, D, A, B, words[2], 15, 0x2AD7D2BB)
        B = II(B, C, D, A, words[9], 21, 0xEB86D391)
 
        a0 = (a0 + A) & 0xFFFFFFFF
        b0 = (b0 + B) & 0xFFFFFFFF
        c0 = (c0 + C) & 0xFFFFFFFF
        d0 = (d0 + D) & 0xFFFFFFFF
 
    result = struct.pack('<4I', a0, b0, c0, d0)
    return result.hex()
 
 
# print(md5_1('1'.encode()))
print(md5_1(bytes.fromhex('31')))

魔改的md5

1
代码不公开

魔改点我上面说的也很清楚了,自己去还原一遍才知道md5的算法细节.

魔改AES

由上面的分析,最终需要逆向的参数就是hmac的秘钥
在这里插入图片描述
用扩展秘钥1异或0x36或秘钥2异或0x5c得到,扩展秘钥上面hook md5的代码可以得到,我没贴图.
追踪这64个字节,还是和上面一样的方法,最终生成的位置是0x52A5C
在这里插入图片描述
veorq_s8是两个16字节的数据逐字节异或,这里看汇编意思更清楚

在这里插入图片描述
异或的两个16进制来自X19和X24,hook看一下,注意这个hook需要在callinitialize这个native函数执行前调用,原因后面再说

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
debugger.addBreakPoint(module.base+0x52A5C,new BreakPointCallback(){
        int num = 0;
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            num+=1;
            System.out.println("num==================================="+num);
            Backend backend = emulator.getBackend();
            long x19 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X19).longValue();
            byte[] bytes19 = backend.mem_read(x19, 0x10);
            StringBuilder hexString19 = new StringBuilder();
            for (byte b : bytes19) {
                hexString19.append(String.format("%02X", b & 0xFF));
            }
            System.out.println("hexString19==="+hexString19);  //iv
 
            long x24 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X24).longValue();
            byte[] bytes24 = backend.mem_read(x24, 0x10);
            StringBuilder hexString21 = new StringBuilder();
            for (byte b : bytes24) {
                hexString21.append(String.format("%02X", b & 0xFF));
            }
            System.out.println("hexString24==="+hexString21);   //明文和iv异或的结果
 
        return true;
        }
    });

在这里插入图片描述
一共6轮,看下他们异或的结果
在这里插入图片描述
中间0x40字节就是我们需要的,前16字节不知道是什么,最后16字节都是0x10,应该能猜出这是pkcs7填充过的吧.网上有文章说是魔改了AES,来看下findcrypt能不能找到
在这里插入图片描述
AES最原始的名称就是RijnDael,之前有文章介绍过这个RijnDael,这里不说了.
也就是说我们hmac的秘钥是来自中间的0x40字节,这有点奇怪啊,为什么会要填充前的0x40字节?答案就是这0x60数据是AES解密出来的,以nopadding解密出来后面会带填充的值.这样的话加密结果也是0x60字节,我们需要找到这0x60字节,这是入参
在这里插入图片描述
获取了
看下jni的日志,解密的上几行,获取了DeviceId和main_hmac的值,这个main_hmac值来源于文件访问,最终源头是服务器返回的,看着像是base64过的,前面有一个base64是标准的,这个应该也会是,转成16进制看看
在这里插入图片描述
16进制的结果是0x60个字节,这一个就是加密的数据了,可以发现前16字节和第二轮的hexString19===EE1755BFE9D97ECE5D3215AF401FA9E7对的上,后面的错开排列也对的上,这意思也很明显,上一组的密文异或一个东西得到解密的"明文",这个明文打个引号,那不就是CBC模式吗?解密出的"明文"是最初的明文异或iv得到的,最初的iv是0x3101323404020861667A666607176639,后续的iv用上一轮加密的结果,CBC模式可以防止替换某段密文来达到替换明文的效果.这里其实看汇编也能看懂
,X19是iv,X24是明文异或iv的结果,他两异或就是最原始明文
这个是AES解密,密文知道了,iv知道了,key是39923e3c-6c6e-3891-945a-fd03c4796eb3,36字节,但是AES没有36字节的key,只有16,24和32字节的,跟踪下这个key的使用,跟踪jni日志中的GetStringUtfChars,0x17d10,一路往下跟,来到秘钥编排的位置sub_51884,上面我们说hook需要在callinitialize这个native函数执行前调用,原因就是在callinitialize函数已经完成了解密,如果在最终的callintercept函数调用就好hook不上.
在这里插入图片描述
这里可以看到只使用了前16字节,填充方式也确认了,基本的都确认了,但是这个是魔改AES,魔改了秘钥扩展部分,改了Rcon,s盒,既然是魔改AES,听说魔改程度很大,不建议对着代码标准AES来改,因为这个AES采用的是八个大的合并表,所以直接扣代码会比跟简单些,主要就是下断,跟汇编,之前zh的x-96用的也魔改的表合并AES,那篇有讲过这么扣代码,这里我就不说了,最多花个几天时间肯定能搞定.


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

最后于 2024-5-25 09:54 被杨如画编辑 ,原因: 更正
收藏
免费 6
打赏
分享
最新回复 (8)
雪    币: 20367
活跃值: (29937)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-5-6 10:41
2
1
感谢分享
雪    币: 1041
活跃值: (1465)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_fssslkzs 2024-5-6 11:01
3
0
tql
雪    币: 307
活跃值: (490)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
北冥鱼丶 2024-5-9 14:48
4
1
达到这个水平要多久,需要学啥
雪    币: 40
活跃值: (385)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chaego 2024-5-9 15:14
5
0
北冥鱼丶 达到这个水平要多久[em_4],需要学啥
需要学逆向
雪    币: 307
活跃值: (490)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
北冥鱼丶 2024-5-9 17:06
6
0
chaego 需要学逆向
so你的逆向学习路径太陡峭了,我看这个文章中用到的技术:frida使用、IDA操作、010editer操作、winHex操作、so文件格式的魔改、unidbg的使用、md5 rc4等加密或编码算法的精通,然后将之灵活组合……艹  这学东西太多了
雪    币: 398
活跃值: (426)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
杨如画 2024-5-9 19:57
7
0
北冥鱼丶 达到这个水平要多久[em_4],需要学啥
至少半年
雪    币: 1489
活跃值: (2011)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
huluxia 2024-5-9 23:49
8
0
膜拜,足够详细。
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_寻_412 2024-5-11 18:46
9
0
大佬,tql吧。非科班的就只能看看java层的加密,一碰到so就抓瞎
游客
登录 | 注册 方可回帖
返回