App逆向案例-dudu牛

App逆向案例-dudu牛

在模拟器中安装该app后,首先运行app熟悉页面及其功能,下图为登录页面

登录接口

抓包分析

我们先使用Charles对该app抓取http请求数据包

可以看到该登录接口请求包中的”Encrypt”参数和相应包都是进行了加密处理,多次更改登录信息发现参数变化,我们来分析下该接口的加密处理。

脱壳处理

我们使用jadx工具对该apk包进行反编译分析,可以看到该包做了混淆处理,无法有效阅读代码,并且里面的”qihoo.util”可以初步判断使用的360加固

我们也可以使用查壳工具来判断出使用的360加固

接下来我们就需要去对其进行脱壳处理得到原代码,这里使用frida-dexdump搭配frida进行脱壳。首先启动安装在模拟器上的frida server

然后在命令行窗口上使用frida-dexdum得到程序脱壳后的dex文件

1
frida-dexdump -FU

可以看到嘟嘟牛在线有四个dex文件,这些为脱壳后的dex文件

同样使用BlackDex32在模拟器上对嘟嘟牛在线进行脱壳处理得到脱壳后文件

反编译分析

我们使用jadx-gui对脱壳后的dex文件进行反编译,如下图所示

搜索文本”Encrypt”作为关键字字符串进行查询,由于该字符串是在登录接口http报文中涵盖的字符串,所以可以判断出源代码位于com.dodonew.online.http.JsonRequest类中存在addRequestMap方法中有该字符串

跟进该函数进一步分析逻辑关系

该函数中值得注意的是定义了一个encrypt字符串变量,这就是生成的Encrypt字符串,入口点定位正确

1
2
3
4
5
6
7
8
String time = System.currentTimeMillis() + DeviceConfig.f7371b;
if (addMap == null) {
addMap = new HashMap();
}
addMap.put("timeStamp", time);
String encrypt = RequestUtil.encodeDesMap(RequestUtil.paraMap(addMap, Config.BASE_APPEND, "sign"), this.desKey, this.desIV);
JSONObject obj = new JSONObject();

先获取系统时间戳,将其添加进addmap中,随后调用RequestUtil.paraMap将addMap和Config.BASE_APPEND作为参数。可以看到BASE_APPEND为一固定值字符串 “sdlkjsdljf0j2fsjk”

我们跟进RequestUtil.paraMap函数进一步分析

该函数先将addMap中的键提取出来,然后定义一个List集合用来存放键值信息,使用sort()对该list进行排序。将排序后的键值按序保存在builder中,最后添加 “sdlkjsdljf0j2fsjk”这一字符串,对builder进行计算md5值保存在sign这一字符串中。

还原加密

上面初步分析了对字符串计算md5值,接下来我们来分析判断对哪些值进行md5计算。

下面编写一个Frida脚本记录java类的操作,获取com.dodonew.online.util.utils类的对象,然后返回其计算md5值后的字符串信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

将其保存为test.js文件,使用frida命令运行启动脚本

1
frida -U -l test.js -f com.dodonew.online

等程序运行后,输入手机号码和密码信息点击登录,可看到脚本中返回需要计算md5的参数和计算后的md5值

返回信息如下:

1
2
3
4
[SM-G988N::com.dodonew.online ]-> md5 is called, string: equtype=ANDROID&loginImei=Android351564715931416&timeStamp=1691317099396&userPwd=12345&username=18148411953&key=sdlkjsdljf0j2fsjk

md5 ret value is
7b9841d0d0c5d23c9271200e9f950b32

我们使用在线工具计算字符串md5值正确,对照一致,符合标准md5值算法

我们继续来看下面的代码,encodeDesMap函数还有this.deskey和this.desIVl两个参数,猜测使用了des算法

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

跟进该函数,先调用了DesSecurity函数对deskey和desIV进行操作,再调用了encrypt64函数对加密的数据进行Base64编码

跟进DesSecurity函数,调用InitCipher函数对deskey和desIV进行操作

InitCipher函数中首先对deskey进行md5加密,然后传进去进行DES加密,使用的加密模式是CBC填充方式PKCS5Padding

我们先找出deskey和desIV参数,下面代码为hook代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

使用frida运行该脚本得到deskey和desiv这两个值65102933和32028092

1
2
3
4
5
6
 encodeDesMap is called, data: {"equtype":"ANDROID","loginImei":"Android351564715931416","sign":"4AC813019A9C77EF6498B3221DCCC17F","timeStamp":"1691322190860","userPwd":"12345","username":"18200411953"}, desKey: 65102933, desIV: 32028092

encodeDesMap ret value is NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii7mUQQsI1NkwwCI4HVgshTMq
i6557JDHstrEdSW11iu/vvUpskEuxayIZPFIFNCu4Botd21NW6//bTU+cPCqMHADoYHmZWF4QshO
SBlYcRIP9PkfH1EZE0gjBqIGmjhZrzr1C7RPND6PYF8q2udN4T9bznMt6Qn9mAf719HDgepvcBCm
m09qHaE=

了解了整个加密过程,我们编写程序来还原下加密代码

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
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):

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'

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

data = '{"equtype":"ANDROID","loginImei":"Android351564715931416","sign":"4AC813019A9C77EF6498B3221DCCC17F","timeStamp":"1691322190860","userPwd":"12345","username":"18200411953"}'

print(des_encrypt(data, desKey, desIV))

可以看到得出的结果刚好符合,还原成功。

模拟请求

我们来梳理下整个加密流程

  • 首先使用md5计算请求签名(equtype、loginImei、timeStamp、userPwd、username、key)保存为sign
  • 对签名及其他字符串进行des加密
1
2
3
取deskey前8字节作为加密密钥
取desIV使用CBC模式PAD_PKCS5进行加密
将机密后的字符串进行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
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
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加密

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():#获取请求签名

s = 'equtype=ANDROID&loginImei=Androidnull&timeStamp=' + timeStamp + '&userPwd=admin&username=18148411953&key=sdlkjsdljf0j2fsjk'

return get_md5_mes(s).upper()

def get_Encrypt():#获取加密后的请求参数

s = '{"equtype":"ANDROID","loginImei":"Androidnull","sign":"' + get_sign() + '","timeStamp":"' + timeStamp + '","userPwd":"admin","username":"18148411953"}'

return des_encrypt(s, desKey, desIV)

def login():#构造http request请求

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()

可以看到模拟请求的响应报文和app登录接口响应报文中内容一样

返回数据还是加密的

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

DES算法是对称加密算法,可以在JsonRequest类中可以看到有个response方法

注意下面这行代码,其中用到了this.deskey和this.desIV这两个参数

1
str = RequestUtil.decodeDesJson(str, this.desKey, this.desIV);

下面我们来具体看看decodeDesJson方法用到的数据,使用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
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)

运行该脚本,在登录接口中输入手机号码和密码点击登录,可以看到返回的加密信息和解密后的JSON字符串

返回信息如下:

1
{"code":-1,"message":"账号或密码错误","data":{}}

至此将该app登录接口模拟请求成功,由于该接口未做防撞库,可使用字典对手机号和密码进行爆破

注册接口

注册界面如下,有手机号码、登录密码和验证码三个输入参数

抓包分析

我们先使用Charles对该app抓取获取验证码http请求数据包,这里使用的测试用例为

1
2
电话号码:18148411953
登录密码:admin

抓取的请求和响应报文如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /api/user/regCode HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.2; SM-G988N Build/NRD90M)
Host: api.dodovip.com
Accept-Encoding: gzip
Content-Length: 151
Connection: keep-alive

{"Encrypt":"VcoXz6dO\/thZ4\/m4PTyZPX+0J3iLNxRy+URRTGi9\/LI0AUSg\/R6SaZXENfK9uOm8nXrss2dC5Nbi\neItA\/j2wmRE33fd4nCDaFtCJ3Wda8vxfwA8AuGaqLkCt5dJ3bQhU\n"}


respond:
2v+DC2gq7Rs5bcSNXbyEx5NU077kz30XWVKcFfbDbqfwNfIfy/H7WfFaOoMFRieB4vJKmUSdrviL
ucFs2ZbC+89cZLVR2q86OhBJqXwkedVPzhVeu2r3xpKnonbCMovs

下面为注册时的http请求及其响应报文,测试用例为

1
2
3
电话号码:18148411953
登录密码:admin
验证码:123456

抓取的请求和响应报文如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /api/user/activateAccount HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.2; SM-G988N Build/NRD90M)
Host: api.dodovip.com
Accept-Encoding: gzip
Content-Length: 285
Connection: keep-alive

{"Encrypt":"NIszaqFPos1vd0pFqKlB42Np5itPxaNH\/\/FDsRnlBfgL4lcVxjXii7mUQQsI1NkwwCI4HVgshTOW\nqpHE8q4N8iEDukVF0C9p4xJVE7Km+zijcNlqK038YX5CRUp5A8oy+UVl7otgB3TAD65sXwkMhCwS\nwY0iPkIY7Cr\/lMsVHISUXF6xAPs+xApDcTW804\/ICXm\/gfrzyecRtMgjtowLUWI6I2Nll12kOQdw\n4dO6FM1wbqY2T72Z\/wh\/4e653Twn\n"}

respond:
2v+DC2gq7RsNlVI9b1yN862HwQjTZO7NoQVzr8hRlcBfVp8rq9IPMznCRmxEVw9jsS/SHTe8nzCD
gX+C9o0F2w==

反编译分析

在jadx中搜索关键字符串”activateAccount”,可查询到RegisterActivity类的register函数

值得注意的是onClick这个函数,这个函数使用switch分支来判断获取的按钮id来执行相应的逻辑,btn_get_code按钮为获取验证码,btn_register按钮为注册账号,btn_found_pwd为忘记密码

首先来看获取验证码btn_get_code这个分支代码,先使用checkMobile函数检查电话号是否符合要求,若符合则调用getCode函数向指定电话发送验证码信息

1
2
3
4
5
6
7
8
case C0689R.C0688id.btn_get_code /*2131558565*/:
String mobile = this.etMobile.getText() + DeviceConfig.f7371b.trim();
if (checkMobile(mobile)) {
getCode(mobile);
return;
1
}
return;

注册功能btn_register代码中通过checkInput函数判断用户输入的phone、pwd、code这三个参数,若都符合则调用register函数进行注册

1
2
3
4
5
6
7
8
9
case C0689R.C0688id.btn_register /*2131558653*/:
String phone = this.etMobile.getText() + DeviceConfig.f7371b.trim();
String pwd = this.etPwd.getText() + DeviceConfig.f7371b.trim();
String code = this.etCode.getText() + DeviceConfig.f7371b.trim();
if (checkInput(phone, pwd, code)) {
register(phone, pwd, code);
return;
}
return;

我们跟进checkInput函数,首先依次检查电话号码、密码和验证码是否为空,若为空则提示错误

继续跟进register函数,构造para参数属性(moblie、pwd、code、config.equtype、application.devid),然后调用requestNetwork函数通过”user/activateAccount”路由接口进行请求。

继续跟进requestNetwork函数

创建JsonRequest对象,当符合指定路由的时候设置用户和手机号码参数调用注册组件,然后调用addRequestMap函数和DodonewOnlineApplication.addRequest函数

1
2
3
his.request.addRequestMap(para);
DodonewOnlineApplication.addRequest(this.request, this);

这里调用的addRequestMap函数和登录接口不同,重载只有一个参数,将useDes设置为真,调用paraMap函数

继续跟进paraMap函数,构造addMap2参数(timeStamp、userid、imei),然后调用RequestUtil.encodeDesMap函数

下面getCode函数为获取验证码功能,与注册功能不同的是para构造参数只有mobile这一参数

前面以及对“注册“和”获取验证码“这两个按钮事件进行了分析,接下来我们分析”找回密码”按钮事件,这里直接开启了FindPasswordActivity

1
startActivity(new Intent(this,FindPasswordActivity.class));

跟进FindPasswordActivity类,里面的click函数为重要逻辑代码

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
switch (this.position) {
case TTransportException.f7557b /*1*/:
this.mobile = this.step1.getMobile();
if (Utils.isMobileNO(this.mobile)) {
this.isAgain = false;
sendMobileCode(this.mobile);
return;
}
showToast("\u8bf7\u8f93\u5165\u6b63\u786e\u7684\u624b\u673a\u53f7.");
return;
//验证手机号并发送验证码
case TTransportException.f7558c /*2*/:
String code = this.step2.getCode();
if (TextUtils.isEmpty(code)) {
showToast("\u8bf7\u8f93\u5165\u9a8c\u8bc1\u7801.");
return;
} else {
verificationCode(this.mobile, code);
return;
}
//验证判断信息
case TTransportException.f7559d /*3*/:
String newPwd = this.step3.getNewPwd();
if (TextUtils.isEmpty(newPwd)) {
showToast("\u8bf7\u8f93\u5165\u65b0\u5bc6\u7801");
return;
} else {
modifyNewPwd(this.mobile, newPwd, this.phoneCode.getToken());
return;
}
//修改密码

接下来就和登录接口流程是一样的

还原加密

注册功能

编写js脚本获取输入处理的参数以及经过encodeDesMap函数后的加密数据

1
2
3
4
5
6
encodeDesMap is called, data: {"equtype":"ANDROID","loginImei":"Android351564715931416","mobileCode":"123456","phone":"18148411953","sign":"DF249AC067B2097E07BDFD4329B95D4B","timeStamp":"1691735134838","userPwd":"admin"}, desKey: 65102933, desIV: 32028092

encodeDesMap ret value is NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii7mUQQsI1NkwwCI4HVgshTOW
qpHE8q4N8iEDukVF0C9p4xJVE7Km+zijcNlqK038YX5CRUp5A8oy+UVl7otgB3QAcgbw2mrGvsUi
kMJr1V+9/Lvs7Bgj8JWrl6vLlvqcknHWaYN4O+pJBSKxY83p40f/SNvHAwGsZvNA0f436GxGDHv7
+o8Z/gt9MsZTK5oyQJy3Ogma7mbW2

该app的所有http请求加密过程都是同一个加密过程,根据编写加密脚本来验证加密数据,执行结果如下,完全符合

找回密码

获取验证码时只对手机号码、时间戳和固定的字符串计算md5值

判断验证时对验证码、手机号码、时间戳和固定的字符串计算MD5值

模拟请求

模拟请求代码和上面登录接口代码一样,只需修改进行计算md5值的字符串和进行DES加密的字符串即可,这里只对返回信息进行Hook解密

获取验证码返回信息

1
{"code":1,"message":"验证码已发送","data":{"message":"验证码已发送","code":1}}

注册返回信息

1
{"code":0,"message":"验证码错误或者已过期","data":{}}
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信