首页
社区
课程
招聘
app逆向--马蜂窝参数分析
发表于: 2天前 749

app逆向--马蜂窝参数分析

2天前
749

声明

特别声明:本文章中所有内容仅供学习交流,不可用于任何商业用途和非法用途,否则后果自负,如有侵权,请联系作者立即删除!由于本人水平有限,如有理解或者描述不准确的地方,还望各位大佬指教!!

前言

今天我们要分析的app是马蜂窝。样本下载

1
https://www.wandoujia.com/apps/492643/history_v1039

抓包分析

第一步抓包分析。通过抓包发现oauth_signature和zzzghostsigh是加密的。而这个zzzghostsigh的长度是40位的,猜测应该是SHA1加密。oauth_signature_method为HMAC-SHA1,由此可以初步判断oauth_signature的加密方式为HMAC-SHA1。图片
没有壳,直接jadx打开apk,搜索zzzghostsigh,java层不多说,里面有涉及到java的反射,最终找到下图这里
图片
其中xPreAuthencode是生成zzzghostsigh的方法,xAuthencode是生成oauth_signature的方法。接下来,我们打开ida进入到so。
在导出函数里搜索java,发现没搜到,函数是动态加载的。
图片

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
package com.mafengwo;
 
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.debugger.Debugger;
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;
 
public class mfwAlgo extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
 
    public mfwAlgo() {
        emulator = AndroidEmulatorBuilder
                .for64Bit()
                .addBackendFactory(new Unicorn2Factory(true))
                .setProcessName("com.mfw.roadbook")
                .build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File("mfw.apk"));
        vm.setJni(this);
        vm.setVerbose(true);
        DalvikModule dm = vm.loadLibrary(new File("libmfw.so"), true);
        module = dm.getModule();
        dm.callJNI_OnLoad(emulator);
    }
 
 
    public static void main(String[] args) {
        mfwAlgo mfw = new mfwAlgo();
    }
}

运行,我们要分析的两个函数的地址也打印出来了
图片

zzzghostsigh分析

先静态分析一下伪代码,看看能不能获取有用的信息
图片
没有找到什么有用的信息,sub_3C9C4还是签名校验函数,后面是一大串的运算然后给v36赋值。那我们就换个路子,用unidbg把汇编执行流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
65
66
67
68
69
70
package com.mafengwo;
 
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.debugger.Debugger;
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.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
 
public class mfwAlgo extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
 
    public mfwAlgo() {
        emulator = AndroidEmulatorBuilder
                .for64Bit()
                .addBackendFactory(new Unicorn2Factory(true))
                .setProcessName("com.mfw.roadbook")
                .build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File("mfw.apk"));
        vm.setJni(this);
        vm.setVerbose(true);
        DalvikModule dm = vm.loadLibrary(new File("libmfw.so"), true);
        module = dm.getModule();
        dm.callJNI_OnLoad(emulator);
    }
 
    public void xPreAuthencode() {
        String traceFile = "mfw_trace.txt";
        PrintStream traceStream = null;
        try {
            traceStream = new PrintStream(new FileOutputStream(traceFile), true);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        //核心 trace 开启代码,也可以自己指定函数地址和偏移量
        emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);
        System.out.println("xPreAuthencode start");
 
        List<Object> list = new ArrayList<Object>();
        list.add(vm.getJNIEnv());   // 第一个参数是env
        DvmObject<?> thiz = vm.resolveClass("com/mfw/tnative/AuthorizeHelper").newObject(null);
        list.add(vm.addLocalObject(thiz)); // 第⼆个参数,实例⽅法是jobject,静态⽅法是jclass,直接填0,⼀般⽤不到。
        DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
        list.add(vm.addLocalObject(context));
        list.add(vm.addLocalObject(new StringObject(vm, "123456")));
        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();
        System.out.println("result=" + result);
    }
 
 
    public static void main(String[] args) {
        mfwAlgo mfw = new mfwAlgo();
        mfw.xPreAuthencode();
    }
}

通过模拟执行出来的结果为:result=2f45b7dd81d48343c79ddbc14cddb756d7353190
去CyberChef上看看是不是标准的SHA1加密,发现对不上;没关系,继续往下分析
图片
在trace里搜索一下前八位0x2f45b7dd,发现这些正是加密结果,我们跳转到0x3f360
图片
发现大量的运算,我们hook一下这个sub_3E1D0函数的入参和出参
图片
这参数1不就是我们的入参吗,记住mx0的内存地址,继续查看结果。使用blr下个临时断点
图片
这不就是结果的小端序吗。我们来到sub_3E1D0函数初始化赋值的地方,可以看到对参数1进行取值
图片
相等于

1
2
3
4
5
v18 = 0x67452301
v19 = 0xEFCDAB89
V20 = 0x98BADCFE
v21 = 0x5E4A1F7C
v22 = 0x10325476

仔细一看,这好像是SHA1的魔值,下面这个是标准的魔值

1
2
3
4
5
A = 0x67452301
B = 0xEFCDAB89
C = 0x98BADCFE
D = 0x10325476
E = 0xC3D2E1F0

通过对比,发现魔改了第4和5,我们在标准的SHA1算法修改一下魔值,对比一下生成的结果

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
# 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 0 <= t <= 19:
                    f = (b & c) | ((~b) & d)
                    k = 0x5A827999
 
                elif 20 <= t <= 39:
                    f = b ^ c ^ d
                    k = 0x6ED9EBA1
 
                elif 40 <= t <= 59:
                    f = (b & c) | (b & d) | (c & d)
                    k = 0x8F1BBCDC
 
                elif 60 <= t <= 79:
                    f = b ^ c ^ d
                    k = 0xCA62C1D6
 
                S0 = madd(ROL4(a, 5), f, e, k, W[t])
                print(f'num: {t},' + '0x%08x' % k + ',0x%08x' % S0)
                S1 = ROL4(b, 30)
 
                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'12345'
    s0 = sha1(s).hexdigest()
    print(s0)

发现也对不上,说明该样本对SHA1算法进行了魔改,并不是只修改了IV。
SHA1和MD5采用了相同的结构,每512比特分组需要一轮运算,我们的输入长度不超过一个分组的长度,所以只用考虑一轮运算。一轮运算是80步,每隔20步是一种模式。
我们打印出每一步正常情况下应该得到的结果,然后对比tracecode
对比之前我们先看看伪代码,伪代码里面出现了比较多的数字,我们把它摘抄出来
图片

1
2
3
4
hex(1518500249) = 0x5a827999
hex(1859775393) = 0x6ed9eba1
hex(-1894007588 & 0xFFFFFFFF) = 0x8f1bbcdc
hex(-899497514 & 0xFFFFFFFF) = 0xca62c1d6

可以看到这些数字是算法源码里面的k值,我们在tracecode里搜索0x5a827999
图片
图片
这是1-3轮的运算结果,跟正常得到的是一样的,我们继续往下看
图片
发现在第16轮之后就找不到了,我们回到伪代码中查看下一个k值是什么
图片
下一个是0x6ed9eba1
图片
我们搜索一下看看出现了多少次
图片
出现了4次,也就是说第17-20轮的k值是0x6ed9eba1,继续往下看
图片
图片
按照这个思路第21-40轮的k值是0x8f1bbcdc
图片
图片
第41-60轮的k值又用回0x5a827999
图片
图片
最后20轮的k值跟源码一样是0xca62c1d6
样本的80轮运算为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for t in range(0, 80):
    if 0 <= t <= 15:
        f = (b & c) | ((~b) & d)
        k = 0x5A827999
 
    elif 16 <= t <= 19:
        f = b ^ c ^ d
        k = 0x6ED9EBA1
 
    elif 20 <= t <= 39:
        f = (b & c) | (b & d) | (c & d)
        k = 0x8F1BBCDC
 
    elif 40 <= t <= 59:
        f = (b & c) | ((~b) & d)
        k = 0x5A827999
 
    elif 60 <= t <= 79:
        f = b ^ c ^ d
        k = 0xCA62C1D6

修改后的加密结果

1
2
69276ad7 3eea0449 9de68070 ec5535d6 3fa7c266  (修改80轮后得到的结果)
69276ad7 3eea0449 26c5f358 6375c2ee 3fa7c266  (unidbg得到的结果)

可以发现中间两段对不上,我们再次回到伪代码里
图片
在函数的末尾我们发现a1的顺序是4-2-3-1-0。我们在最后一次轮换中也按这个顺序,也就是c和d调换顺序

1
2
3
4
if t == 79:
    a, b, d, c, e = S0, a, S1, c, d
else:
    a, b, c, d, e = S0, a, S1, c, d

现在完全对的上了。

oauth_signature分析

通过抓包oauth_signature_method为HMAC-SHA1,由此可以初步判断oauth_signature的加密方式为HMAC-SHA1。先跳转到0x3998c看看伪代码
图片
看起来sub_3B168就是对明文进行加密,点进去看看,里面有很多操作看不懂,看不懂没关系,我们hook一下这个函数的入参和出参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void xAuthencode() {
        List<Object> list = new ArrayList<Object>();
        list.add(vm.getJNIEnv());   // 第一个参数是env
        DvmObject<?> thiz = vm.resolveClass("com/mfw/tnative/AuthorizeHelper").newObject(null);
        list.add(vm.addLocalObject(thiz)); // 第⼆个参数,实例⽅法是jobject,静态⽅法是jclass,直接填0,⼀般⽤不到。
        DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
        list.add(vm.addLocalObject(context));
        list.add(vm.addLocalObject(new StringObject(vm, "cookies")));
        list.add(vm.addLocalObject(new StringObject(vm, "")));
        list.add(vm.addLocalObject(new StringObject(vm, "com.mfw.roadbook")));
        list.add(0);  // false填0
        Number number = module.callFunction(emulator, 0x3998c, list.toArray());
        String result = vm.getObject(number.intValue()).getValue().toString();
        System.out.println("result=" + result);
    }

 这个函数被调用了很多次,我们现查看它的调用次数

1
2
3
4
5
6
7
8
9
10
11
12
public void hookDebugger() {
    Debugger debugger = emulator.attach();
    debugger.addBreakPoint(module.base + 0x3B168, new BreakPointCallback() {
        int num = 0;
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            num += 1;
            System.out.println("num次:" + num);
            return true;
        }
    });
}

图片
一共调用了5次,并且在第3次之后明文才被取出来
图片
图片
图片
图片
这里,先说一下标准的HMAC-X的加密流程

  1. 将密钥用 0x00 填充达到分组长度
  2. 将第一步填充后的密钥和 0x36 做异或运算
  3. 第二步得到的密钥后拼接明文
  4. 对第三步得到的数据做对应加密
  5. 将第一步填充后的密钥和 0x5c 做异或运算
  6. 将第四步所得结果附加到第五步所得数据的末尾
  7. 对第六步所得数据做加密,得到最终结果

上面的截图跟加密流程对比,基本上可以确定是HMAC-SHA1加密
图片跟unidbg模拟执行出来是对的上的,到此这个样本的两个参数已经分析出来了。

结言

算是第二篇app的文章了,这里的源码已经放在了星球,欢迎大家跟我一起讨论,同时星球会丢更多的一些辅助工具。
图片


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

最后于 2天前 被Harden13编辑 ,原因: 错别字
收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//