首页
论坛
课程
招聘
[原创]搜狗搜索 app so 加解密分析
2021-11-9 00:13 17713

[原创]搜狗搜索 app so 加解密分析

2021-11-9 00:13
17713

仅供学习研究 。请勿用于非法用途,本人将不承担任何法律责任。

目录

const 小白龙_龙哥 = function () {}

const a = 龙哥是全能的傻宝,每天都会换一种姿势来分享各种骚操作
const b = 10月份开始准备密码学课程,11月份准备更完 aes 密码学系列课程
const c = 除了密码学,还有各种 unidng 骚操作,反 unidbg,反反 unidbg,全是出自傻宝
const d = 赶紧点击加入星球吧,买到就是赚到 !
return 无敌的龙哥,全能的傻宝

前言

最近在学习 so 加解密逆向分析,算法还原,就拿星主分析过的一个样本来练手,那个星主?
当然是 call 小白龙_龙哥();

样本

搜狗搜索 app 版本 7.9.6.6 主要分析微信公众号文章请求参数的加解密

一、简单分析

图片描述

 

以上是抓包截图,请求跟响应都是加密串,主要是分析这些

0x1 jadx

图片描述

 

jadx-gui 打开之后,开始搜索,这里因为请求没啥关键字,搜索 url 即可,出来一个结果点进去看看

 

图片描述

 

这里调用了两个函数 encrypt dencrypt 应该就是加解密了,跳转到声明函数的地方

 

图片描述

 

进来发现全部是 navite 函数,这里加载了一个 so 文件,navite 的实现应该就在这里
使用 ida 打开看一下

0x2 ida

图片描述

 

打开 ida -> exports 窗口,搜索 java 发现这里是静态注册的函数,刚好对应 java 层的四个函数
先分析到这里,下篇文章在分析 so 具体的算法实现,下面使用 frida hook 验证一下

0x3 frida hook

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
function hook() {
    var ScEncryptWall = Java.use('com.sogou.scoretools.ScEncryptWall');
 
    ScEncryptWall.decrypt.implementation = function (a) {
        console.log('decrypt.a: ', a);
 
        var res = this.decrypt(a);
        console.log('decrypt.res: ', res);
        console.log('decrypt.res: ', Java.use('java.lang.String').$new(res));
 
        return res;
    }
 
    ScEncryptWall.encrypt.implementation = function (a, b, c) {
        console.log('encrypt.a: ', a);
        console.log('encrypt.b: ', b);
        console.log('encrypt.c: ', c);
 
        var res = this.encrypt(a, b, c);
        console.log('encrypt.res: ', res);
 
        return res;
    }
}
 
 
function main() {
    Java.perform(function () {
        hook();
    })
}
 
 
setImmediate(main);

图片描述

 

图片描述

 

结果全部 hook 出来了,跟抓包结果对比是一样的,在使用 unidbg 跑起来

0x4 unidbg

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
package com.xiayu.sougou;
 
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.hook.hookzz.IHookZz;
import com.github.unidbg.hook.xhook.IxHook;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.XHookImpl;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.sun.jna.Pointer;
 
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
 
public class SouGou extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
 
    public String apkPath = "/Users/admin/Desktop/android/file/sougou-7.9.6.6.apk";
    public String soPath = "unidbg-android/src/test/resources/test_so/sougou/libSCoreTools-7.9.6.6.so";
 
    private static LibraryResolver createLibraryResolver() {
        return new AndroidResolver(23);
    }
 
    private static AndroidEmulator createARMEmulator() {
        return AndroidEmulatorBuilder.for32Bit().build();
    }
 
    public SouGou() {
        emulator = createARMEmulator();
        final Memory memory = emulator.getMemory();
        memory.setLibraryResolver(createLibraryResolver());
 
        vm = emulator.createDalvikVM(new File(apkPath));
        vm.setVerbose(true);
        DalvikModule dm = vm.loadLibrary(new File(soPath), false);
        vm.setJni(this);
 
        dm.callJNI_OnLoad(emulator);
        module = dm.getModule();
    }
 
    public void encrypt() {
        List<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv());
        list.add(0);
 
        list.add(vm.addLocalObject(new StringObject(vm, "http://app.weixin.sogou.com/api/searchapp1")));
        list.add(vm.addLocalObject(new StringObject(vm, "type=2&ie=utf8&page=2&query=python&select_count=0&usip=1")));
        list.add(vm.addLocalObject(new StringObject(vm, "")));
        Number number = module.callFunction(emulator, 0x9CA0 + 1, list.toArray())[0];
 
        System.out.println("encrypt: " + vm.getObject(number.intValue()).getValue().toString());
    }
 
    public static void main(String[] args) throws IOException {
        SouGou sougou = new SouGou();
        sougou.encrypt();
        sougou.destroy();
    }
 
    private void destroy() throws IOException {
        emulator.close();
    }
}

以上就全部代码跑起来

 

图片描述

 

图片描述

 

代码跑起来但是结果为空,应该是哪里有问题
回去看下 so 发现有个 init 函数,应该就是初始化,先调用 init

1
2
3
4
5
6
7
8
9
10
11
public void init() {
    DvmClass Context = vm.resolveClass("android/content/Context");
 
    List<Object> list = new ArrayList<>(10);
    list.add(vm.getJNIEnv());
    list.add(0);
    list.add(vm.addLocalObject(Context.newObject(null)));
    Number number = module.callFunction(emulator, 0x9564 + 1, list.toArray())[0];
 
    System.out.println("init: " + number.intValue());
}

图片描述

 

在运行结果就正常出来了, 接着在来分析下 rsa base64 的加密逻辑

二、so rsa base64

0x1

图片描述

 

也就是这里点进去

 

图片描述

 

一共五个参数,前两个是默认的,分别是 JNIEnv jObject or jClass,后面三个就是我们传进来的
上面是做了一些字符串初始化然后调用了 j_Sc_EncryptWallEncode 这个函数,点进去看看

 

图片描述

 

进来之后有三个函数需要我们关注,前两经过分析都不是很重要,主要分析最后一个

 

图片描述

 

要感谢代码符号没去掉,才能看到里面密密麻麻的算法

 

图片描述

 

在往下分析,就看到我们熟悉字符串,不正是前面请求包的请求参数吗,这里就是全部算法的加密逻辑了

0x2 rsa

图片描述

 

前面是一些常规的判断赋值操作

v10 = operator new(48u);
这一行就是创建一个大小 48字节 的内存,v10 就只内存地址

EncryptWall::WallKey::WallKey(v10);
这里调用一个函数,把内存地址进去,点进去看看

0x3 EncryptWall::WallKey::WallKey

图片描述

 

点进来逻辑也比较简单,两个 do while 一共循环 48 次,刚好把 48字节大小的 内存填空

linux 下的 lrand48() 函数
随机生成 0 - 2 ^ 31 之间的长整数

 

Tips: 继续往下

v12 = RSA_Encrypt(v10 + 16, 32u, &v60, &v59);
这里就是调用 rsa 加密函数,四个参数分别是 v10 的首地址加上 1632数字v60 的地址v59 的地址

0x4 RSA_Encrypt

图片描述

n_crypto::SetSignPubKey
这里是生成 public_key,模数,指数都传进去了

n_crypto::PublicEnc(v8, v9, v6, &v11, v4);
调用这个函数开始加密,一共有五个参数
分别是 1:32字节的地址2:32数字3:128字节大小的内存4:内存地址5:80字节大小的内存地址

 

Tips: rsa 分析结束

0x5 base64Encode

图片描述

 

这里就是对 rsa 的加密结果,再来一层 base64 编码,稍后可以验证下是否是标准的 base64

0x6 unidbg

算法分析的差不多了,在使用 unidbg 的 console debugger 跟 hook 功能,验证下数据,不熟悉的可以看下上面的推荐文章

0x7 unidbg unicorn inline hook

EncryptWall::WallKey::WallKey 函数看看生成的啥数据

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
public void hookUnicornWallKey() {
    emulator.getBackend().hook_add_new(new CodeHook() {
        @Override
        public void onAttach(Unicorn.UnHook unHook) {
        }
 
        @Override
        public void detach() {
        }
 
        @Override
        public void hook(Backend backend, long address, int size, Object user) {
            if (address == (module.base + 0xB332)) {
                System.out.println("Hook By Unicorn hookUnicornWallKey");
                RegisterContext ctx = emulator.getContext();
                Pointer input1 = ctx.getPointerArg(0);
                Inspector.inspect(input1.getByteArray(0, 0x100), " 参数1");
            }
            if (address == (module.base + 0xB336)) {
                RegisterContext ctx = emulator.getContext();
                Inspector.inspect(ctx.getPointerArg(0).getByteArray(0, 0x100), " Unicorn hook hookUnicornWallKey");
            }
        }
    }, module.base + 0xB332, module.base + 0xB336, null);
}

图片描述

 

运行结果可以看到,刚好是 48字节 的数据

接着分别 hook rsa base64 函数

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
public void hookUnicornRsa() {
    emulator.getBackend().hook_add_new(new CodeHook() {
        @Override
        public void onAttach(Unicorn.UnHook unHook) {
        }
 
        @Override
        public void detach() {
        }
 
        @Override
        public void hook(Backend backend, long address, int size, Object user) {
            if (address == (module.base + 0xB34E)) {
                RegisterContext ctx = emulator.getContext();
                Pointer input1 = ctx.getPointerArg(0);
                Inspector.inspect(input1.getByteArray(0, 0x100), " hookUnicornRsa 参数1");
            }
            if (address == (module.base + 0xB352)) {
                RegisterContext ctx = emulator.getContext();
                Inspector.inspect(ctx.getPointerArg(0).getByteArray(0, 0x100), " Unicorn hook hookUnicornRsa");
            }
        }
    }, module.base + 0xB34E, module.base + 0xB352, null);
}
 
public void hookUnicornBase64Encode() {
    emulator.getBackend().hook_add_new(new CodeHook() {
        @Override
        public void onAttach(Unicorn.UnHook unHook) {
        }
 
        @Override
        public void detach() {
        }
 
        @Override
        public void hook(Backend backend, long address, int size, Object user) {
            if (address == (module.base + 0xB372)) {
                RegisterContext ctx = emulator.getContext();
                Pointer input1 = ctx.getPointerArg(0);
                Inspector.inspect(input1.getByteArray(0, 0x100), " hookUnicornBase64Encode 参数1");
            }
            if (address == (module.base + 0xB376)) {
                RegisterContext ctx = emulator.getContext();
                Inspector.inspect(ctx.getPointerArg(0).getByteArray(0, 0x100), " Unicorn hook hookUnicornBase64Encode");
            }
        }
    }, module.base + 0xB372, module.base + 0xB376, null);
}

代码写完跑起来

 

图片描述

rsa 的第一个参数,是 32 个字节,也就是前面随机生成 48 字节数据的后 32个字节数据,返回值是 128 个字节

 

图片描述

base64 的第一个参数,刚好是前面 rsa 的返回值

 

图片描述

使用 CyberChef 网站来验证,是否是标准的 base64 ,结果不出所料,就是标的
rsa base64 就分析完了,下一篇文章在分析 zip + aes 算法

三、so zip aes

0x1 so 分析

图片描述

v46 = sub_B178(s, v17, v11, v57);
先来看看这段代码,v46 刚好是 u 参数,调用 sub_B178 函数获取的

 

图片描述

 

点进来可以看到一些算法,分别是 zip_compress aes base64

aes 算法了解的同学应该知道需要关注那些内容,不了解的可以 call 龙哥 () 跟着一起学习密码学
咱们需要关注的是
1、使用的密码长度:AES-128 or AES-192 or AES-256
2、明文是啥
3、密钥 key 是啥
4、加密模式是啥:ECB or CBC,如果非 ECB,那么 IV 是啥
5、填充方式是啥:No or PKCS5 or PKCS7

 

Tips: 下面一个一个来分析,先来看看 sub_B178 函数的入参是啥

 

图片描述

1、s 是参数 a1
2、v17 是 s 的长度
3、v11 是 48字节大小的首地址加 16 的地址
3、v57 48字节大小的地址

 

Tips: 进入 sub_B178 函数继续分析

 

图片描述

这里的 zip_compress 需要注意,是用的是 deflate 压缩算法
其他的 zlib gzip 实际在压缩的时候会添加上头和尾,使用这两个算法需要去掉头和尾才是真正的数据
参考链接 gzip,deflate,zlib辨析

 

继续分析 AES_Encrypt 函数,Base64Encode 函数可以暂时忽略,看下参数

1、v10 = v9 也就是 v9 的内存地址
2、v11 是 zip_compress 函数的结果
3、&v16 地址
4、v5 参数 a3,也就是前面 48字节的 后 32位
5、32u 32数字
6、v4 参数 a4 前面的 48字节
7、16u 16数字

 

Tips: 参数分析完,点击去继续分析

 

图片描述

先分析 n_crypto::SetEncKeySym(&v20, v10, 8 * a5); 这一行代码,看像是密钥相关函数
1、&v20 char 类型的地址
2、v10 是参数 a4
3、8 x a5,a5 是参数 a5 8 x a5 = 256

 

图片描述

SetEncKeySym 函数点进来,看到这个判断,就可以判断是 AES-256, 在AES-256中,密钥是32字节
这里的 a2 就是参数3,而参数1 = a4 = 32 字节,这里的参数1 应该就是密钥

 

图片描述

 

再往下分析 padding 填充方式,目前看不出来是啥方式,不过通过参数 16u 可以猜测这个应该是 block 大小,Pcks#5 默认是 block = 5, Pcks#7 可变的 1-255,这里应该是 Pcks#7 填充,后面可以验证下

 

Tips: 继续分析 n_crypto::EncSym 函数

 

图片描述

先来看下参数
1、&v21 是个内存地址。v21 在上面有初始化,a6 就是前面的 48字节,这里应该是取前 16 个字节,猜测:前 16 是 IV,后面 32 是密钥
2、v9 内存地址
3、v18 内存地址
4、v14 内存地址
5、&v20 应该是密钥的内存地址

 

图片描述

 

EncSym 函数点进去,通过符号可以发现是 CBC 加密模式

 

图片描述

 

前面的猜测确实正确,CBC 模式下的 IV 在运算会先跟密钥进行异或,这里应该就是异或的逻辑了

0x2 unidbg

又到了 unidbg debugger hook 阶段,验证一些数据

0x3 unidbg console debugger zip_compress

先在 0xB1B0 下个断点

 

图片描述

 

运行代码,成功断下来,查看 mr0 的数据,就是我们传进来的 url

 

图片描述

 

图片描述

 

输入 n 指令单步执行,在查看 r6 的值,使用 cyberchef 转成 bas64 稍后再使用 python 来验证

 

图片描述

 

图片描述

 

这里为啥看 r6 的值呢,看下 so 就明白了 operator new[] 申请一块内存,存到 r6 寄存器,zip_compress 的加密结果存到了 v9 里也就是 r6

 

图片描述

 

使用 python zlib 模块,头和尾分别是 2-4 个字节去掉即可

0x4 unidbg console debugger aes

最后再来看下 aes cbc 使用的啥填充模式,0x12970 下断点

 

图片描述

 

查看 mr0 的值,第三行后面 11 都是填充的,模式就是 pkcs#7

四、response 解密

前面分析了,参数加密的逻辑,下面就来分析下 response 的解密

0x1

图片描述

 

解密逻辑就是这个 decrypt 函数,点进去

 

图片描述

 

需要一个参数,就是我们传进来的加密字符串然后调用 j_Sc_EncryptWallDecode 函数完成解密

 

图片描述

 

这里先有个 if 判断,看看 byte_3A0C0 这是啥

 

图片描述

 

table 键查看汇编代码,这里点击去

 

图片描述

 

这个变量是在加密逻辑返回的,所以后面在使用 unidbg 调用的时候还需要先调用加密逻辑才行

 

图片描述

 

进到 EncryptWall::DecryptHttpRequest3 函数看一下,一大堆密密麻麻的解密函数,这函数一共有 6 个参数,先来分析一下

1、v4 = dword_3A0C4 = 也就是加密逻辑的返回值
1、v2 = 传进来的加密串
1、v6 = 加密字符串的长度
1、&v8 = 地址
1、0 = 0
1、v7 = 变量

0x2

这里分析下 else 的逻辑,经过分析得知这里走的是 else 分支

 

图片描述

v9 = (4 a3);*

v9 = 4 * 字符串的长度

v10 = operator new[](4 a3);*

v10 = 内存地址,大小是 字符串长度的 3 倍

v14 = n_crypto::Decode_Base64(v10, v9, v11, v13);

这里是 base64 decode 逻辑

v8 = AES_Decrypt(v12, v14, v20, v7 + 16, 32u, v7, 16u);

这里是 aes 解密,参数有 7 个分析一下
1、v12 = 内存地址
2、v14 = base64 解密结果
3、v20 = v14
4、v7 + 16 = 参数1 + 16
5、32u = 32 字节
6、v7 = 参数一
7、16u = 16 字节

AES_Decrypt

分析下 AES_Decrypt 的逻辑,看看 key iv 分别是啥

 

图片描述

 

有了前面分析加密逻辑的经验这里看起来大概就了解了

_aeabi_memcpy(&v16, a6, 16);

v16 = 内存地址,a6 = 参数6,16 = 16个字节,这里应该是从 a6 里取出前 16 个字节的数据存到 v16 里

n_crypto::SetDecKeySym(&v15, v11, 8 a5);*

这个应该是生成 key 的逻辑
v15 = 内存地址,v11 = 参数4, 8 a5 = 8 32 = 256,这里应该是 AES-256 解密

n_crypto::DecSym(&v16, v9, v8, v7, &v15);

这里开始解密
v16 = 内存地址,值是 16 个字节数据,v9 = 内存地址,v8 = 参数1,v7 = 参数2,v15 = 应该是 key,这里可以猜测 v16 = iv

 

AES Decrypt 的解密逻辑就分析到这里

0x3 zip_uncompress

最后在来看下 zip_uncompress 解压缩数据的逻辑

 

图片描述

zip_uncompress(v8 + 1, v20 - 4, v15, v18, 0);*
主要看这行代码
v8 = aes 解密结果,v8 + 1 也就是地址 + 1,16位的1地址 = 2字节,32位的1地址 = 4字节,64位的1地址 = 8字节
所以这里是 aes 解密结果 去掉前 四个字节 在 解压缩

 

Tips: so 解密逻辑分析完了,下面使用 unidbg 验证一些数据

0x4

先使用 unidbg 把 解密函数跑起来

0x5 unidbg

1
2
3
4
5
6
7
8
9
public void decrypt() {
    List<Object> list = new ArrayList<>(10);
    list.add(vm.getJNIEnv());
    list.add(0);
    list.add(vm.addLocalObject(new StringObject(vm, "加密字符串")));
    Number number = module.callFunction(emulator, 0x9DA0 + 1, list.toArray())[0];
 
    System.out.println("decrypt: " + new String((byte[]) vm.getObject(number.intValue()).getValue()));
}

图片描述

 

代码跑起来,发现报错了,具体因为啥,这里先不说我们继续往下分析

0x6 unidbg console debugger

使用 console debugger 下端点调试

 

图片描述

 

现在分析 byte_3A0C0 看看加密逻辑返回的是个啥

 

图片描述

 

0xA35E 地址下端点,查看 r6 的值,前 48个字节 的数据事不是有些熟悉,没错正是加密时随机生成的数据
48个 字节的 前 16个aes 加密的 iv,后 32个aes 加密的 key,所以猜测 aes 解密时用的也是这个 key iv,下面来验证一下

 

图片描述

 

0x1158C 下端点,查看 r1 寄存器的数据,前 32个 字节刚好是,encrypt 返回的结果

v11 = v4 = 参数4 = 解密逻辑结果 + 16

 

图片描述

 

0x115A6 下端点,查看 r0 寄存器的数据,前 16个 字节的数据也正好是 encrypt 返回的结果

现在就真想大白了,参数加密用的 key iv,跟响应解密用的是同一套,如果不对的话,解密结果就不对
经过测试每次执行程序 加密逻辑的结果都是一样,这里随机生成的 key iv 应该也是固定的,就拿着这个加密结果去请求一次,获取到响应结果再来调用解密函数

 

图片描述

 

加解密的 key iv 对应起来之后,decrypt 函数也就正常执行了

0x7 python

再来看看 使用 python 解密

 

图片描述

 

这里在 zlib.decompress 报错了

 

图片描述

 

加个参数 -15 就可以了: 参考文章

zlib 库可以解以下的格式
deflate 需要加上 wbits = -zlib.MAX_WBITS
zlib 需要加上 wbits = zlib.MAX_WBITS
gzip 需要加上 wbits = zlib.MAX_WBITS | 16

最后

整个搜狗搜索 app 的加解密文章就算写完了
原文地址: 搜狗搜索 app so 加解密分析


【公告】看雪团队招聘安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

收藏
点赞8
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回