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) |
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(): """获取请求签名""" 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":{}}
因为我在这给的账号和密码本就是错误的,所以提示账号或密码错误一点问题没有。
至此完结。