app版本:4.54
抓包工具:Charles
反汇编工具:JEB、JADX
inject:frida
查壳:360加固
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
{"Encrypt":"NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii+GOZz1l+A5V9FPOSMf47jbE\n010Kk+PbNyEDRjj1zY76jXa7VyHLkjxpqsrJYht6LX1PcVabK8oBp/fiOE4l2lC5JVjqx/JI7CJm\neUXVXkgJ6rgPne3WCJUYU+ztDNEi+mvECeOktUk0KxqBbPzuJj3LKsW5Ux080rWm4NZWHxPFbZYl\nIs2IRcs=\n"}
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
=
[
0x64
,
0x65
,
0x78
,
0x0a
,
0x30
,
0x33
,
0x35
,
0x00
];
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
-
U
-
f com.dodonew.online
-
l dumpdex.js
-
-
no
-
pause
|
将脱壳后的dex推出:
其中第一个为加壳程序;
第二个为IjkMediaPlayer和rx库,IjkMediaPlayer是基于FFmpeg的Android多媒体播放器库,大佬们可自行百度了解;
第三个为应用程序界面信息dex;
第四个为应用程序逻辑代码。
既然是分析登陆逻辑,那肯定是在第四个dex中分析啦!
jadx每次生成的参数名称会有所出入,各位在对照这这份教程进行分析的时候只需把握整体步骤即可。
将第四个文件拖入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>
map
,
int
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,入口点定位无误。
继续分析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
i
=
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)
|
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():
"""获取请求签名"""
s
=
'equtype=ANDROID&loginImei=Androidnull&timeStamp='
+
timeStamp
+
'&userPwd=12334&username=123456789&key=sdlkjsdljf0j2fsjk'
return
get_md5_mes(s).upper()
def
get_Encrypt():
"""获取加密后的请求参数"""
s
=
'{"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 为比较早期的对称加密算法,加密与解密就是一个对称的过程。
请求是 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":{}}
因为我在这给的账号和密码本就是错误的,所以提示账号或密码错误一点问题没有。
至此完结。
更多【一个被众多逆向人员凌辱的app --> 某du牛】相关视频教程:www.yxfzedu.com