一个被众多逆向人员凌辱的app --> 某du牛

一 前言

app版本:4.54
抓包工具:Charles
反汇编工具:JEB、JADX
inject:frida
查壳:360加固

二 抓包

2.1 Headers

POST: /api/user/login HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Dalvik/2.1.0 (Linux; U; Android 8.1.0; Pixel 2 XL Build/OPM4.171019.021.R1)
Host: api.dodovip.com
Accept-Encoding: gzip
Content-Length: 262
Connection: keep-alive

2.2 Text

{"Encrypt":"NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii+GOZz1l+A5V9FPOSMf47jbE\n010Kk+PbNyEDRjj1zY76jXa7VyHLkjxpqsrJYht6LX1PcVabK8oBp/fiOE4l2lC5JVjqx/JI7CJm\neUXVXkgJ6rgPne3WCJUYU+ztDNEi+mvECeOktUk0KxqBbPzuJj3LKsW5Ux080rWm4NZWHxPFbZYl\nIs2IRcs=\n"}

2.3 Response

2v+DC2gq7RuAC8PE5GZz5wH3/y9ZVcWhFwhDY9L19g9iEd075+Q7xwewvfIN0g0ec/NaaF43/S0=

多次抓包仅 Encrypt 参数变化,需要分析的就是它了。

三 脱壳

上脚本,手机端启动fs后执行即可,脱壳的dex会在/data/data/com.dodonew.online目录下: 

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

function find_hook_fun() {

    var fun_Name = "";

    var libart = Module.findBaseAddress('libart.so');   //查找基地址

    var exports = Module.enumerateExportsSync("libart.so");

    for(var i=0; i<exports.length; i++){

        if(exports[i].name.indexOf("OpenMemory") !== -1){

            fun_Name = exports[i].name;

            console.log("导出模块名: " + exports[i].name + "\t\t偏移地址: "+ (exports[i].address - libart - 1));

            break;

        }else if(exports[i].name.indexOf("OpenCommon") !== -1){

            fun_Name = exports[i].name;

            console.log("导出模块名: " + exports[i].name + "\t\t偏移地址: "+ (exports[i].address - libart - 1));

            break;

        }

    }

    return fun_Name;

}

function DexFileVerifier(Verify){

    var magic_03x = true;

    var magic_Hex = [0x640x650x780x0a0x300x330x350x00];

    for(var i = 0; i < 8; i++){

        if(Memory.readU8(ptr(Verify).add(i)) !== magic_Hex[i]){

            if(Memory.readU8(ptr(Verify).add(i)) === 0x37 || 0x38){

                console.log('new dex');

            }else{

                magic_03x = false;

                break;

            }

        }

    }

    return magic_03x;

}

function dump_Dex(fun_Name, apk_Name){

    if (fun_Name !== ''){

        var hook_fun = Module.findExportByName("libart.so", fun_Name);

        Interceptor.attach(hook_fun, {

            onEnter: function (args) {

                var begin = 0;

                var dex_flag = false;

                dex_flag = DexFileVerifier(args[0]);

                if(dex_flag === true){

                    begin = args[0];

                }

                if(begin === 0){

                    dex_flag = DexFileVerifier(args[1]);

                    if(dex_flag === true){

                        begin = args[1];

                    }

                }

                if(dex_flag === true){

                    console.log("magic : " + Memory.readUtf8String(begin));

                    var address = parseInt(begin,16+ 0x20;

                    var dex_size = Memory.readInt(ptr(address));

                    console.log("dex_size :" + dex_size);

                    var dex_path = "/data/data/" + apk_Name + "/" + dex_size + ".dex";

                    var dex_file = new File(dex_path, "wb");

                    dex_file.write(Memory.readByteArray(begin, dex_size));

                    dex_file.flush();

                    dex_file.close();

                }

            },

            onLeave: function (retval) {

            }

        });

    }else{

        console.log("Error: no hook function.");

    }

}

var fun_Name = find_hook_fun();

var apk_Name = 'com.dodonew.online'

dump_Dex(fun_Name, apk_Name);

// frida --f com.dodonew.online -l dumpdex.js --no-pause

四 dex解析

将脱壳后的dex推出:

 其中第一个为加壳程序;

 第二个为IjkMediaPlayer和rx库,IjkMediaPlayer是基于FFmpeg的Android多媒体播放器库,大佬们可自行百度了解;

 第三个为应用程序界面信息dex;

 第四个为应用程序逻辑代码。

既然是分析登陆逻辑,那肯定是在第四个dex中分析啦!

五 协议分析

jadx每次生成的参数名称会有所出入,各位在对照这这份教程进行分析的时候只需把握整体步骤即可。

5.1 入手点定位

将第四个文件拖入jadx等待加载完成,搜 "Encrypt" 结果还挺多:

挺好定位 com.dodonew.online.http.JsonRequest 类中存在
addRequestMap(Map<String, String>, int) void 方法和 paraMap(Map<String, String>) void 方法, 两方法中都有进行参数存放操作。
第一个方法 addRequestMap 翻译以下:添加请求的 Map,可疑,跟进去看看:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public void addRequestMap(Map<String, String> mapint i) {

    String str = System.currentTimeMillis() + "";

    if (map == null) {

        map = new HashMap<>();

    }

    map.put("timeStamp"str);

    String encodeDesMap = RequestUtil.encodeDesMap(RequestUtil.paraMap(map, Config.BASE_APPEND, "sign"), this.desKey, this.desIV);

    JSONObject jSONObject = new JSONObject();

    try {

        jSONObject.put("Encrypt", encodeDesMap);

        this.mRequestBody = jSONObject + "";

    } catch (JSONException e) {

        e.printStackTrace();

    }

}

看这两句代码:

1

2

3

String encodeDesMap = RequestUtil.encodeDesMap(RequestUtil.paraMap(map, Config.BASE_APPEND, "sign"), this.desKey, this.desIV);

jSONObject.put("Encrypt", encodeDesMap);

第一句中生成的encodeDesMap就是Encrypt,入口点定位无误。

5.2 md5 算法分析

继续分析addRequestMap函数代码,看代码:

1

2

String str = System.currentTimeMillis() + "";

map.put("timeStamp"str);

获取时间戳,然后将时间戳添加进 Map 中,再调用:

1

RequestUtil.paraMap(map, Config.BASE_APPEND, "sign");

跟进RequestUtil.paraMap函数看看:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

public static String paraMap(Map<String, String> map, String str, String str2) {

    try {

        Set<String> keySet = map.keySet();

        StringBuilder sb = new StringBuilder();

        ArrayList arrayList = new ArrayList();

        for (String str3 : keySet) {

            arrayList.add(str3 + "=" + map.get(str3));

        }

        Collections.sort(arrayList);

        for (int = 0; i < arrayList.size(); i++) {

            sb.append((String) arrayList.get(i));

            sb.append("&");

        }

        sb.append("key=" + str);

        map.put(str2, Utils.md5(sb.toString()).toUpperCase());

        String json = new GsonBuilder().serializeNulls().create().toJson(sortMapByKey(map));

        Log.w(AppConfig.DEBUG_TAG, json + "   result");

        return json;

    } catch (Exception e) {

        e.printStackTrace();

        return "";

    }

}

首先将 Map 中的键提取出来存入 Set 中,再定义一个 List 集合用来存放键值信息,and 进行 sort 排序,
其中有处:sb.append("key=" + str); str是入参参数二,向上跟一下是个固定值:

1

public static final String BASE_APPEND = "sdlkjsdljf0j2fsjk";

经过一系列操作完后对值进行 md5,md5 得到的值就是 sign 的值,hook 看看那些值需进行 md5:

1

2

3

4

5

6

7

8

9

10

11

12

function main() {

    Java.perform(function () {

        var Utils = Java.use("com.dodonew.online.util.Utils");

        Utils["md5"].implementation = function (string) {

            console.log('md5 is called' + ', ' + 'string: ' + string);

            var ret = this.md5(string);

            console.log('md5 ret value is ' + ret);

            return ret;

        };

    });

}

setImmediate(main)

hook 结果:

md5 is called, string: equtype=ANDROID&loginImei=Androidc0b30f35fc9535b5&timeStamp=1687772161410&userPwd=12334&username=123456789&k
ey=sdlkjsdljf0j2fsjk
md5 ret value is e888bef28d91b42fc10cf91540ec057b

试着 python 还原下看看是不是标准 md5 算法:

1

2

3

4

5

6

7

8

9

10

from hashlib import md5

def get_encode_mes(mes):

    new_md5 = md5()

    new_md5.update(mes.encode(encoding='utf-8'))

    return new_md5.hexdigest()

if __name__ == '__main__':

    print(get_encode_mes('equtype=ANDROID&loginImei=Androidc0b30f35fc9535b5&timeStamp=1687772161410&userPwd=12334&username=123456789&k

ey=sdlkjsdljf0j2fsjk'))

结果:e888bef28d91b42fc10cf91540ec057b,对照一致,标准md5算法。

5.3 des 加密算法分析
继续分析addRequestMap函数代码,看代码:

1

String encodeDesMap = RequestUtil.encodeDesMap(RequestUtil.paraMap(map, Config.BASE_APPEND, "sign"), this.desKey, this.desIV);

其中this.desKey, this.desIV,猜测为des算法,先hook看看数据,hook代码:

1

2

3

4

5

6

7

8

9

10

11

12

function main() {

    Java.perform(function () {

        var RequestUtil = Java.use("com.dodonew.online.http.RequestUtil");

        RequestUtil["encodeDesMap"].overload('java.lang.String''java.lang.String''java.lang.String').implementation = function (data, desKey, desIV) {

            console.log('encodeDesMap is called' + ', ' + 'data: ' + data + ', ' + 'desKey: ' + desKey + ', ' + 'desIV: ' + desIV);

            var ret = this.encodeDesMap(data, desKey, desIV);

            console.log('encodeDesMap ret value is ' + ret);

            return ret;

        };

    });

}

setImmediate(main)

hook 结果:

encodeDesMap is called, data: {"equtype":"ANDROID","loginImei":"Androidc0b30f35fc9535b5","sign":"0FAFB81829C15EF86EBD30E214675BBC",
"timeStamp":"1687772424834","userPwd":"12334","username":"123456789"}, desKey: 65102933, desIV: 32028092
encodeDesMap ret value is NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii+GOZz1l+A5V9FPOSMf47jbE
010Kk+PbN/jjSVvUEnMkBeVQY2tdy+to9cUXg0XyzdSi3Wehubi6R5t5NLiRanFipatR61mx4ISH
B/wjHUkmAFDl2b3zZIYs2UMZhz4YfC4HgFeRqA/9X1+m1LNZQYUkOLl/HqD5GFDgdRel9stq/g+8
ZB8fY84=

在此吃了个亏,直接用 hook 出来的 desKey、desIV 进行加密,怎么搞都不对,后面发现它还进行了操作,还是太年轻了。跟进 encodeDesMap 方法查看:

1

2

3

4

5

6

7

8

9

public static String encodeDesMap(String data, String desKey, String desIV) {

    try {

        DesSecurity ds = new DesSecurity(desKey, desIV);

        return ds.encrypt64(data.getBytes("UTF-8"));

    } catch (Exception e) {

        e.printStackTrace();

        return "";

    }

}

先调用 DesSecurity(desKey, desIV); 对 desKey、desIV 进行操作,跟进看看:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public DesSecurity(String key, String iv) throws Exception {

    if (key == null) {

        throw new NullPointerException("Parameter is null!");

    }

    InitCipher(key.getBytes(), iv.getBytes());

}

private void InitCipher(byte[] secKey, byte[] secIv) throws Exception {

    MessageDigest md = MessageDigest.getInstance("MD5");

    md.update(secKey);

    DESKeySpec dsk = new DESKeySpec(md.digest());

    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");

    SecretKey key = keyFactory.generateSecret(dsk);

    IvParameterSpec iv = new IvParameterSpec(secIv);

    this.enCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");

    this.deCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");

    this.enCipher.init(1, key, iv);

    this.deCipher.init(2, key, iv);

}

查看其构造方法,调用 InitCipher 方法对 desKey、desIV 进行操作:

1

2

MessageDigest md = MessageDigest.getInstance("MD5");

md.update(secKey);

对 desKey 进行了 MD5 加密,然后才传进去进行 DES 加密,加密模式 CBC 填充方式 PKCS5Padding。再看:

1

2

3

public String encrypt64(byte[] data) throws Exception {

    return Base64.encodeToString(this.enCipher.doFinal(data), 0);

}

对加密后的数据又进行了一次 Base64 编码,这回清楚了,再进行还原:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

from pyDes import CBC, PAD_PKCS5, des

from hashlib import md5

import base64

def get_md5_mes(mes):

    new_md5 = md5()

    new_md5.update(mes.encode(encoding='utf-8'))

    return new_md5.hexdigest()

def des_encrypt(data, desKey, desIV):

    """DES 加密 :param data: 原始字符串 :param desKey: 取加密密钥 8 位 :return: 加密后字符串, base64"""

    key = desKey[:8]  # 只需前八字节

    ds = des(key, CBC, desIV, pad=None)

    en = ds.encrypt(data.encode(), padmode = PAD_PKCS5)

    return base64.b64encode(en).decode()

  

if __name__ == '__main__':

    desIV = '32028092'

    # 需转换成 byte 的 hex 值 用 hexstr 来创建 bytes 对象

    desKey = bytes.fromhex(get_md5_mes('65102933'))

    data = '{"equtype":"ANDROID","loginImei":"Androidc0b30f35fc9535b5","sign":"0FAFB81829C15EF86EBD30E214675BBC","timeStamp":"1687772424834","userPwd":"12334","username":"123456789"}'

    print(des_encrypt(data, desKey, desIV))

执行结果:

NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii+GOZz1l+A5V9FPOSMf47jbE010Kk+PbN/jjSVvUEnMkBeVQY2tdy+to9cUXg0XyzdSi3Wehubi6R5t5NLiRanFipatR61mx4ISHB/wjHUkmAFDl2b3zZIYs2UMZhz4YfC4HgFeRqA/9X1+m1LNZQYUkOLl/HqD5GFDgdRel9stq/g+8ZB8fY84=

对照其hook结果一直,还原成功,至此整个协议就分析完成了,Encrypt数据也成功拿到,接下来就是模拟请求了。

六 模拟请求

前面该分析的也都分析好了,写代码这种事情相信各位佬随手拈来,我就不在讲解了,直接上代码,是在不明白,代码中的注释也很全:

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

from pyDes import CBC, PAD_PKCS5, des

from hashlib import md5

import requests

import base64

import time

def get_md5_mes(mes):

    """获取字符串的MD5摘要"""

    new_md5 = md5()

    new_md5.update(mes.encode(encoding='utf-8'))

    return new_md5.hexdigest()

def des_encrypt(data, desKey, desIV):

    """DES加密

    :param data: 原始字符串

    :param desKey: 加密密钥,取前8字节

    :return: 加密后的字符串,base64编码

    """

    key = desKey[:8]  # 只需前八字节

    ds = des(key, CBC, desIV, pad=None)

    en = ds.encrypt(data.encode(), padmode=PAD_PKCS5)

    return base64.b64encode(en).decode()

def get_timeStamp():

    """获取时间戳(毫秒级)"""

    return str(int(time.time() * 1000))

def get_sign():

    """获取请求签名"""

    = 'equtype=ANDROID&loginImei=Androidnull&timeStamp=' + timeStamp + '&userPwd=12334&username=123456789&key=sdlkjsdljf0j2fsjk'

    return get_md5_mes(s).upper()

def get_Encrypt():

    """获取加密后的请求参数"""

    = '{"equtype":"ANDROID","loginImei":"Androidnull","sign":"' + get_sign() + '","timeStamp":"' + timeStamp + '","userPwd":"12334","username":"123456789"}'

    return des_encrypt(s, desKey, desIV)

def login():

    """登录函数"""

    url = "http://api.dodovip.com/api/user/login"

    header = {

    "Host""api.dodovip.com",

    "Cache-Control""public, max-age=0",

    'Content-Type''application/json; charset=utf-8',

    'User-Agent'"Dalvik/2.1.0 (Linux; U; Android 11; M2012K11AC Build/RQ3A.211001.001)",

    }

    data = {

        'Encrypt': get_Encrypt()

    }

    res = requests.post(url, headers=header, json=data)

    print(res.text)

if __name__ == '__main__':

    desIV = '32028092'

    # 需转换成 byte 的 hex 值 用 hexstr 来创建 bytes 对象

    desKey = bytes.fromhex(get_md5_mes('65102933'))

    timeStamp = get_timeStamp()

    login()

结果,与抓包结果一致,返回数据还是加密的:

2v+DC2gq7RuAC8PE5GZz5wH3/y9ZVcWhFwhDY9L19g9iEd075+Q7xwewvfIN0g0ec/NaaF43/S0=

七 des 解密算法分析

对于返回结果是密文也是预料之中的,des 为比较早期的对称加密算法,加密与解密就是一个对称的过程。
请求是 addRequestMap 有 request 那么就会有 response,而且这个方法就在我们找到的 addRequestMap 上方:

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

public Response<RequestResult<T>> parseNetworkResponse(NetworkResponse response) {

    String parsed;

    try {

        parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));

    } catch (UnsupportedEncodingException e) {

        parsed = new String(response.data);

    }

    if (this.useDes) {

        parsed = RequestUtil.decodeDesJson(parsed, this.desKey, this.desIV);

    }

    Log.w(AppConfig.DEBUG_TAG, parsed);

    RequestResult<T> res = (RequestResult) this.mGson.fromJson(parsed, this.typeOfT);

    res.response = parsed;

    if (this.useDes) {

        try {

            JSONObject object = new JSONObject(parsed);

            if (object.has("code")) {

                String code = object.getString("code");

                if (code.equals(a.e)) {

                    if (object.has(MapTilsCacheAndResManager.AUTONAVI_DATA_PATH)) {

                        res.response = object.getString(MapTilsCacheAndResManager.AUTONAVI_DATA_PATH);

                    }

                else if (code.equals("-10")) {

                    this.mHandler.sendEmptyMessage(0);

                }

            }

        } catch (Exception e2) {

            e2.printStackTrace();

        }

    }

    return Response.success(res, HttpHeaderParser.parseCacheHeaders(response));

}

留意:

1

parsed = RequestUtil.decodeDesJson(parsed, this.desKey, this.desIV);

hook 它看看:

1

2

3

4

5

6

7

8

9

10

11

12

function main() {

    Java.perform(function () {

        var RequestUtil = Java.use("com.dodonew.online.http.RequestUtil");

        RequestUtil["decodeDesJson"].implementation = function (json, desKey, desIV) {

            console.log('decodeDesJson is called' + ', ' + 'json: ' + json + ', ' + 'desKey: ' + desKey + ', ' + 'desIV: ' + desIV);

            var ret = this.decodeDesJson(json, desKey, desIV);

            console.log('decodeDesJson ret value is ' + ret);

            return ret;

        };

    });

}

setImmediate(main)

结果:

decodeDesJson is called, json: 2v+DC2gq7RuAC8PE5GZz5wH3/y9ZVcWhFwhDY9L19g9iEd075+Q7xwewvfIN0g0ec/NaaF43/S0=, desKey: 65102933, desIV: 32028092
decodeDesJson ret value is {"code":-1,"message":"账号或密码错误","data":{}}

因为我在这给的账号和密码本就是错误的,所以提示账号或密码错误一点问题没有。
至此完结。

猜你喜欢

转载自blog.csdn.net/qq_64428978/article/details/131553027
du
今日推荐