0x01 前言
app版本:4.54
抓包工具:Charles
反汇编工具:JEB、JADX
inject:frida
查壳:360加固
0x02 抓包
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 参数变化,需要分析的就是它了。
0x03 脱壳
对脱壳流程有不明白的可参考我之前写的文章:[原创]ART环境下dex加载流程分析及frida dump dex方案。
上脚本,手机端启动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
|
0x04 dex解析
将脱壳后的dex推出:
其中第一个为加壳程序;
第二个为IjkMediaPlayer和rx库,IjkMediaPlayer是基于FFmpeg的Android多媒体播放器库,大佬们可自行百度了解;
第三个为应用程序界面信息dex;
第四个为应用程序逻辑代码。
既然是分析登陆逻辑,那肯定是在第四个dex中分析啦!
0x05 协议分析
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> 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,入口点定位无误。
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 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)
|
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):
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":"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数据也成功拿到,接下来就是模拟请求了。
0x06 模拟请求
前面该分析的也都分析好了,写代码这种事情相信各位佬随手拈来,我就不在讲解了,直接上代码,是在不明白,代码中的注释也很全:
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):
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()
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'
desKey = bytes.fromhex(get_md5_mes( '65102933' ))
timeStamp = get_timeStamp()
login()
|
结果,与抓包结果一致,返回数据还是加密的:
2v+DC2gq7RuAC8PE5GZz5wH3/y9ZVcWhFwhDY9L19g9iEd075+Q7xwewvfIN0g0ec/NaaF43/S0=
0x07 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":{}}
因为我在这给的账号和密码本就是错误的,所以提示账号或密码错误一点问题没有。
至此完结。
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
最后于 2023-6-27 10:19
被行简编辑
,原因: