Featured image of post 某蜂窝app逆向分析

某蜂窝app逆向分析

样本地址: aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzQ5MjY0My9oaXN0b3J5X3YxMTA4

抓包分析

首先来抓一下登录 login 接口,这接口最明显

首先来看一下请求参数, 经过测试发现: oauth_nonce, oauth_signature, zzzghostsigh 这三个参数是变化的,所以需要处理的也就是这三个参数

java 层分析

首先进行搜索 oauth_nonce

从这儿可以看出直接就是随机数,直接可以本地生成
先来看一下 zzzghostsigh 这个参数

这个参数由 ghostSigh 这个函数生成,不断往里面跟,发现是由 xPreAuthencode 这个 native 函数生成的
同理: oauth_signature 是由 xAuthencode 这个 native 函数生成的

zzzghostsigh 参数的生成

先来搭建一个 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
package com.mfw;

import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.pointer.UnidbgPointer;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneMode;

public class Mfw extends AbstractJni{
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    Mfw() {
        emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.mfw.roadbook").build(); // 创建模拟器实例
        Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
        vm = emulator.createDalvikVM(new File("apks/mafengwo/mfw11.3.0.apk")); // 创建Android虚拟机
        DalvikModule dm = vm.loadLibrary(new File("apks/mafengwo/libmfw.so"), true); // 加载so到虚拟内存
        module = dm.getModule(); //获取本SO模块的句柄
        vm.setJni(this);
        vm.setVerbose(true);
        dm.callJNI_OnLoad(emulator);
    };

    

    public String xPreAuthencode(){
        List<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv()); // 第一个参数是env
        list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
        DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context
        list.add(vm.addLocalObject(context));
        list.add(vm.addLocalObject(new StringObject(vm, "PUT&https%3A%2F%2Fmapi.mafengwo.cn%2Frest%2Fapp%2Fuser%2Flogin%2F&_mfwencode%3DeyJjb250ZW50IjoiWGwvbEhjM00rb24rdjRmOW5GZUU4T0xEUGlTRkFaZ0J2Yk5jVDV4ZU41aVcyZ0xUNXJQb2FRRlE3UlY1dXprZWhReHI4VDRvZ1F0QTZwTzI3UWZ5amdjR1o1bkpWKzRxL0ovaEZPNG5namNcdTAwM2QiLCJrZXkiOiJWbVU3ZUowNk1IUnZONldoQVp0VzZWVS9xTFFqZExUTHByb3BrZS93MkxMb1lpUFBRNW0vTE94Q2RoUG1RVHd0Q3NFdTQxWXh4NHpqdVkwVjJTNEM1YVhJWlRRY0JXSzh4cDkwL1M1Vm1oYVZhaTVvV2pqc3h1a1RZL3ZxQjYwTGlZaXNsSE1hcVNHeUY2eXUwaHcxcFFMa2IvQnRWa0c0L1g3ejJKOU9WSGtCTnVFZVVsZUt6ZXV6eWVNdFEwSThhN3dOWFhSbjF2UEwxSVdodXoveW1ZNTZxSjBROEJMUTFWSWdseFUybG9GREtGa0RFdDZNdTZ6RG1UT2swT3RQR25tZzJhSlVmZ3BHWmNDQU50VWVGeXdsbUJhYTVGeENzS21CcVJRSXJ4ODRIZzFhVHpBd21QdytRMlQ2WExZdWppOThPWlQ5Wldsbm5MNnAzYTc4ZVFcdTAwM2RcdTAwM2QiLCJ2ZXJzaW9uIjoxfQ%253D%253D%26after_style%3Ddefault%26android_oaid%3D00000000-0000-0000-0000-000000000000%26app_code%3Dcom.mfw.roadbook%26app_ver%3D11.3.0%26app_version_code%3D1108%26brand%3Dgoogle%26channel_id%3DVivo%26dev_ver%3DD2504.0%26device_id%3D1a225b1ca66cb7fd%26device_type%3Dandroid%26hardware_model%3DPixel%25202%26has_notch%3D0%26mfwsdk_ver%3D20140507%26o_coord%3Dwgs%26oauth_consumer_key%3D5%26oauth_nonce%3Dd0cb3997-2965-4a93-b22d-4f51f00b4c6d%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1770890982%26oauth_version%3D1.0%26open_udid%3D1a225b1ca66cb7fd%26put_style%3Ddefault%26screen_height%3D1794%26screen_scale%3D2.88%26screen_width%3D1080%26shumeng_id%3DDUE13wluwiFe0hoViIxIC9zGaccdbGJuC100RFVFMTN3bHV3aUZlMGhvVmlJeElDOXpHYWNjZGJHSnVDMTAwc2h1%26sys_ver%3D10%26time_offset%3D480%26x_auth_mode%3Dclient_auth")));
        list.add(vm.addLocalObject(new StringObject(vm, "com.mfw.roadbook")));

        Number number = module.callFunction(emulator, 0x396c8, list.toArray());
        String result = vm.getObject(number.intValue()).getValue().toString();
        return result;
    }

    public static void main(String[] args) throws Exception {
        Mfw test = new Mfw();
        System.out.println(test.xPreAuthencode());
    }
}

但是当我进行调用的时候,出现了错误

这里面又 packageInfo,signature,我怀疑有可能是签名校验的问题,先把 so 拖到 ida 中进行分析
首先跳转到函数 0x396c8

在这儿发现了问题,他会对 v7 的值进行判断,非法签名, sub_3C9C4 应该就是签名校验的函数了
进行之后,发现它确实是在对包名进行校验,可以尝试在 unidbg 中进行 patch

patch 代码如下:

1
2
3
4
5
6
7
8
9
public void patch(){
	UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base+0x3C9C4);
	// 修正模式为 LittleEndian
	Keystone keystone = new Keystone(KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian);
	String s = "MOV W0, #1; RET";
	byte[] machineCode = keystone.assemble(s).getMachineCode();
	assert pointer != null;
	pointer.write(machineCode);
}

patch 过后,发现直接就能够得到结果了,还不需要补环境啊

接着来进行算法分析
来到 0x39964

v6 这个之前分析过, sub_3C9C4 就是一个签名校验函数,我通过 patch 返回的正确值,过掉的这个签名校验函数
v8 是输入的明文,v9 是明文的长度
接着通过 loc_3DEC4 对数据进行加密,所以这个位置毫无疑问的就是很重要的地方,需要去重点分析
在 ida 中,loc_3DEC4 是一个自动生成的代码位置的标签,命名规则:

  • loc:前缀表示 “location”(位置/标签)
  • 3DEC4:十六进制地址偏移量 在 IDA 中,其他的常见命名方式如下:
  • loc_ 代码位置
  • sub_ 函数/子程序
  • off_ 数据偏移量
  • dword_/qword_ 数据变量
  • unk_ 未知类型

跳进去之后有点看不懂,看一下汇编是咋样的

首先标准的函数入口,开辟内存空间,分配栈帧
接着加载 #xmmword_C0C30@PAGE 这里面的常量

然后下面 W8 寄存器通过逻辑左移赋值等操作,得到 0x10325467,在之前学习算法的时候知道,标准的 hash 算法,他的常数是 0x67452301,这个不太一样啊,很有可能是进行魔改了
接着往下看,知道他会跳转到 loc_3DF38,将常数加载到内存,接着就应该把明文进行扩展了
跳转到 loc_3DF38,看一下进行了哪些操作

CMN W9, #8 比较 W9+8,更新标志位
B.CC loc_3DF50 当进位标志位为 0 时发生跳转,如果没有溢出就跳转到 loc_3DF50 这个位置,如果溢出了,就递增 w8
跳转到 loc_3DF50,看一下相关的代码

使用 unidbg 来 hook 一下 x9 寄存器的值,w9 是 x9 寄存器的低四位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
long baseAddr = module.base;
emulator.attach().addBreakPoint(baseAddr+0x3DF54L, new BreakPointCallback() {
	@Override
	public boolean onHit(Emulator<?> emulator, long address) {
		int x9 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X9).intValue();
		System.out.printf("获取到x9寄存器的值为:0x%02x\n", x9);

		return true;
	}
});

发现每隔 64 次就不跳转到 loc_3DF84

而是跳转到 sub_3E1D0 这个子函数,去看一下这个函数到底再做什么操作?

这里的 a1 有 5 个元素,看起来跟 sha1 算法的 5 个常数对应,a2 貌似很少有被用到,同时 hook 一下这两个参数吧
hook a2,发现就是明文

hook a1,他是 sha1 算法的常量

按照小端排序,得到:

1
2
3
4
5
0x67452301
0xEFCDAB89
0x98BADCEF
0x5E4A1F7C
0x10325476

而我们正常的 sha1 算法常数是:

1
2
3
4
5
0x67452301,
0xEFCDAB89,
0x98BADCFE,
0x10325476,
0xC3D2E1F0

对比得知第四,第五个常数发生了变化
在之前学习算法的过程中,知道了 SHA1 算法常见的魔改点有常量,k 值,线性函数等
k 值呢?一般是下面这几个

1
2
3
4
0x5A827999
0x6ED9EBA1
0x8F1BBCDC
0xCA62C1D6

但是我发现他跟正常的 sha1 算法用到的 k 值不太对

1
2
3
4
5
0x5A827999 在运算中出现了16次即0到15轮
0x6ED9EBA1 在运算中出现了4次即16到19轮
0x8F1BBCDC 在运算中出现了20次即20到39轮
0x5A827999 在运算中出现了20次即40到59轮
0xCA62C1D6 在运算中出现了20次即60到79轮

我改了之后,他的部分值是对的,但是还有一部分值错误了,我加密的值是 0xnonce
但是发现算法模拟的跟 unidbg 生成的值不太一样啊

1
2
8a625bcf aced1023 8e8481d4 ba3325bd 5f825702 算法模拟的
8a625bcf aced1023 f4a3e33f 5413c452 5f825702 unidbg生成的值

也就是 c 和 d 的值不一样,那需要看一下这个地方到底做了什么事情

a[3],它放的是循环左移 30 位后的值,也就是原来 d 的值
a[2],它貌似并没有进行累加,还是上一次的循环左移 30 的结果,说明还是上一次的 c
改一下:

  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
131
# sha1-v1
import struct

bitlen = lambda s: len(s) * 8


def ROL4(x, n):
    x &= 0xffffffff
    return ((x << n) | (x >> (32 - n))) & 0xffffffff


def madd(*args):
    return sum(args) & 0xffffffff


class sha1:
    block_size = 64
    digest_size = 20

    def __init__(self, data=b''):
        if data is None:
            self._buffer = b''
        elif isinstance(data, bytes):
            self._buffer = data
        elif isinstance(data, str):
            self._buffer = data.encode('ascii')
        else:
            raise TypeError('object supporting the buffer API required')

        self._sign = None

    def update(self, content):
        if isinstance(content, bytes):
            self._buffer += content
        elif isinstance(content, str):
            self._buffer += content.encode('ascii')
        else:
            raise TypeError('object supporting the buffer API required')

        self._sign = None

    def copy(self):
        other = self.__class__.__new__(self.__class__)
        other._buffer = self._buffer
        return other

    def hexdigest(self):
        result = self.digest()
        return result.hex()

    def digest(self):
        if not self._sign:
            self._sign = self._current()
        return self._sign

    def _current(self):
        msg = self._buffer

        # standard magic number
        # A = 0x67452301
        # B = 0xEFCDAB89
        # C = 0x98BADCFE
        # D = 0x10325476
        # E = 0xC3D2E1F0

        A = 0x67452301
        B = 0xEFCDAB89
        C = 0x98BADCFE
        D = 0x5E4A1F7C
        E = 0x10325476

        msg_len = bitlen(msg) & 0xffffffffffffffff

        zero_pad = (56 - (len(msg) + 1) % 64) % 64
        msg = msg + b'\x80'
        msg = msg + b'\x00' * zero_pad + struct.pack('>Q', msg_len)

        for idx in range(0, len(msg), 64):
            W = list(struct.unpack('>16I', msg[idx:idx + 64])) + [0] * 64

            for t in range(16, 80):
                T = W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16]
                W[t] = ROL4(T, 1)

            a, b, c, d, e = A, B, C, D, E

            # main loop:
            for t in range(0, 80):
                if t <= 15:
                    f = (b & c) | ((~b) & d)
                    k = 0x5a827999

                elif  t <= 19:
                    f = b ^ c ^ d
                    k = 0x6ED9EBA1
                    

                elif  t <= 39:
                    f = (b & c) | (b & d) | (c & d)
                    k = 0x8F1BBCDC

                elif t<=59:
                    f = (b & c) | ((~b) & d)
                    k = 0x5a827999

                elif 60 <= t <= 79:
                    f = b ^ c ^ d
                    k = 0xCA62C1D6

                S0 = madd(ROL4(a, 5), f, e, k, W[t])
                S1 = ROL4(b, 30)
                if t ==79:
                    a, b, d, c, e = S0, a, S1, c, d
                else:
                    a, b, c, d, e = S0, a, S1, c, d
                

            A = madd(A, a)
            B = madd(B, b)
            C = madd(C, c)
            D = madd(D, d)
            E = madd(E, e)

        result = struct.pack('>5I', A, B, C, D, E)
        return result


if __name__ == '__main__':
    s = b'0xnonce'
    s0 = sha1(s).hexdigest()
    print(s0) # 8a625bcfaced1023f4a3e33f5413c4525f825702

这样结果就正常了

oauth_signature 的生成

调用函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public String xAuthencode(){
	List<Object> list = new ArrayList<>(10);
	list.add(vm.getJNIEnv());
	list.add(0);
	DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
	list.add(vm.addLocalObject(context));
	list.add(vm.addLocalObject(new StringObject(vm, "0xnonce")));
	list.add(vm.addLocalObject(new StringObject(vm,"")));
	list.add(vm.addLocalObject(new StringObject(vm, "com.mfw.roadbook")));
	list.add(vm.addLocalObject(DvmBoolean.valueOf(vm,true)));
	Number number = module.callFunction(emulator, 0x3998c, list.toArray());
	String result = vm.getObject(number.intValue()).getValue().toString();
	return result;
}

生成的值: NxLJNp+YzBa171KlJhiAKLhCQFg=
在之前我抓包的时候,下面就曾明晃晃的写了个 hmac-sha1,看来这个算法很可能跟 hmac-sha1 算法有关

到最后返回的值中,v32 是由 sub_3B694 这个子函数生成的
对这个地方 hook 一下,但是我发现我好像并没有发现有什么东西

好,那就换一种方式,在上面这些子函数中,都会用到一个 v38 的变量
从最开始的 sub_3A3F8 这个子函数开始看

 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
mx0

>-----------------------------------------------------------------------------<
[08:56:39 774]x0=unidbg@0xe4fff5c8, md5=2ca4bbdfa8fdf4c47ec1254be4233306, hex=0000000000000038cf5b628a2310edac3fe3a3f452c413540257825f000200000000000030786e6f6e63658000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000380000000080f6ffe400000000
size: 112
0000: 00 00 00 00 00 00 00 38 CF 5B 62 8A 23 10 ED AC    .......8.[b.#...
0010: 3F E3 A3 F4 52 C4 13 54 02 57 82 5F 00 02 00 00    ?...R..T.W._....
0020: 00 00 00 00 30 78 6E 6F 6E 63 65 80 00 00 00 00    ....0xnonce.....
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0060: 00 00 00 38 00 00 00 00 80 F6 FF E4 00 00 00 00    ...8............
^-----------------------------------------------------------------------------^
mx1

>-----------------------------------------------------------------------------<
[08:56:44 127]x1=RW@0x12453060, md5=8c8ee55660f1152f1e4ace04ed1e6f9a, hex=35633035333235643561653830623736633164323263663031656438363061652600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 35 63 30 35 33 32 35 64 35 61 65 38 30 62 37 36    5c05325d5ae80b76
0010: 63 31 64 32 32 63 66 30 31 65 64 38 36 30 61 65    c1d22cf01ed860ae
0020: 26 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    &...............
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
^-----------------------------------------------------------------------------^

参数 1 貌似是个不知名的字符串,不太确定是个啥
参数 2,看起来像密钥,因为 hmac-sha1 最开始进行初始化的时候就是对密钥进行处理
参数 3 是 0x21,可能是密钥的长度
继续往下面看,看一下 sub_3B168 这个子函数

 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
mx0

>-----------------------------------------------------------------------------<
[09:00:22 645]x0=unidbg@0xe4fff5c8, md5=57f1eabc91cb2d387ae6fc63977fa0d6, hex=0123456789abcdeffedcba9876543210f0e1d2c352c4135400000000000000000000000030786e6f6e63658000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000380000000080f6ffe400000000
size: 112
0000: 01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10    .#Eg........vT2.
0010: F0 E1 D2 C3 52 C4 13 54 00 00 00 00 00 00 00 00    ....R..T........
0020: 00 00 00 00 30 78 6E 6F 6E 63 65 80 00 00 00 00    ....0xnonce.....
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0060: 00 00 00 38 00 00 00 00 80 F6 FF E4 00 00 00 00    ...8............
^-----------------------------------------------------------------------------^
mx1

>-----------------------------------------------------------------------------<
[09:00:26 863]x1=unidbg@0xe4fff500, md5=53b7bc5d455a3879769da28f2284bac4, hex=03550603050403520357530e0654010055075204045550060753520e0006575310363636363636363636363636363636363636363636363636363636363636360020131200000000000000000000000020ddcd5c000000003a99d11e0000000016493f1f000000004016feff00000000
size: 112
0000: 03 55 06 03 05 04 03 52 03 57 53 0E 06 54 01 00    .U.....R.WS..T..
0010: 55 07 52 04 04 55 50 06 07 53 52 0E 00 06 57 53    U.R..UP..SR...WS
0020: 10 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36    .666666666666666
0030: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36    6666666666666666
0040: 00 20 13 12 00 00 00 00 00 00 00 00 00 00 00 00    . ..............
0050: 20 DD CD 5C 00 00 00 00 3A 99 D1 1E 00 00 00 00     ..\....:.......
0060: 16 49 3F 1F 00 00 00 00 40 16 FE FF 00 00 00 00    .I?.....@.......
^-----------------------------------------------------------------------------^

参数 1 看起来跟我输入的明文有关,可能是填充过后的值
参数 2 就是 hmac 标准的样式了,将密钥与 0x36,0x5c 进行异或操作,那我同样将这段与 0x36 异或回去,就能够得到密钥了 得到的刚好就是 5c05325d5ae80b76c1d22cf01ed860ae& 那可以尝试着来进行加密一下 37 打头的,那上面 sub_3B694 加密出来看不懂的那个字符串就是 hmac-sha1 加密的结果啊
好,继续往下面看
为什么得到的值,跟我们 unidbg 模拟出来的值不一样
往 sub_3B694 中看一下,发现刚好是在做 base64 的操作,尝试着进行编码一下

正好就能够得到加密值了