一 前言
抓包工具: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+A5V9FPOSMf47jbE010Kk+PbNyEDRjj1zY76jXa7VyHLkjxpqsrJYht6LX1PcVabK8oBp/fiOE4l2lC5JVjqx/JI7CJmeUXVXkgJ6rgPne3WCJUYU+ztDNEi+mvECeOktUk0KxqBbPzuJj3LKsW5Ux080rWm4NZWHxPFbZYlIs2IRcs="}
2.3 Response
2v+DC2gq7RuAC8PE5GZz5wH3/y9ZVcWhFwhDY9L19g9iEd075+Q7xwewvfIN0g0ec/NaaF43/S0=
多次抓包僅 Encrypt 參數變化,需要分析的就是它了。
三 脫殼
對脫殼流程有不明白的可參考我之前寫的文章:[原創]ART環境下dex加載流程分析及frida dump dex方案。
上腳本,手機端啟動fs后執行即可,脫殼的dex會在/data/data/com.dodonew.online目錄下:
function find_hook_fun() {
var fun_Name = "";
var libart = Module.findBaseAddress('libart.so'); //查找基地址
var exports = Module.enumerateExportsSync("libart.so");
for(var i=0; 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解析
將脫殼后的dex推出:

其中第一個為加殼程序;

第二個為IjkMediaPlayer和rx庫,IjkMediaPlayer是基于FFmpeg的Android多媒體播放器庫,大佬們可自行百度了解;

第三個為應用程序界面信息dex;

第四個為應用程序邏輯代碼。

既然是分析登陸邏輯,那肯定是在第四個dex中分析啦!
五 協議分析
jadx每次生成的參數名稱會有所出入,各位在對照這這份教程進行分析的時候只需把握整體步驟即可。
5.1 入手點定位
將第四個文件拖入jadx等待加載完成,搜 "Encrypt" 結果還挺多:

挺好定位 com.dodonew.online.http.JsonRequest 類中存在
addRequestMap(Map, int) void 方法和 paraMap(Map) void 方法, 兩方法中都有進行參數存放操作。
第一個方法 addRequestMap 翻譯以下:添加請求的 Map,可疑,跟進去看看:
public void addRequestMap(Map 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();
}
}
看這兩句代碼:
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函數代碼,看代碼:
String str = System.currentTimeMillis() + "";
map.put("timeStamp", str);
獲取時間戳,然后將時間戳添加進 Map 中,再調用:
RequestUtil.paraMap(map, Config.BASE_APPEND, "sign");
跟進RequestUtil.paraMap函數看看:
public static String paraMap(Map map, String str, String str2) {
try {
Set 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是入參參數二,向上跟一下是個固定值:
public static final String BASE_APPEND = "sdlkjsdljf0j2fsjk";
經過一系列操作完后對值進行 md5,md5 得到的值就是 sign 的值,hook 看看那些值需進行 md5:
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 算法:
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函數代碼,看代碼:
String encodeDesMap = RequestUtil.encodeDesMap(RequestUtil.paraMap(map, Config.BASE_APPEND, "sign"), this.desKey, this.desIV);
其中this.desKey, this.desIV,猜測為des算法,先hook看看數據,hook代碼:
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 方法查看:
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 進行操作,跟進看看:
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 進行操作:
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(secKey);
對 desKey 進行了 MD5 加密,然后才傳進去進行 DES 加密,加密模式 CBC 填充方式 PKCS5Padding。再看:
public String encrypt64(byte[] data) throws Exception {
return Base64.encodeToString(this.enCipher.doFinal(data), 0);
}
對加密后的數據又進行了一次 Base64 編碼,這回清楚了,再進行還原:
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數據也成功拿到,接下來就是模擬請求了。
六 模擬請求
前面該分析的也都分析好了,寫代碼這種事情相信各位佬隨手拈來,我就不在講解了,直接上代碼,是在不明白,代碼中的注釋也很全:
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 解密算法分析
對于返回結果是密文也是預料之中的,des 為比較早期的對稱加密算法,加密與解密就是一個對稱的過程。
請求是 addRequestMap 有 request 那么就會有 response,而且這個方法就在我們找到的 addRequestMap 上方:
public Response> 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 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));
}
留意:
parsed = RequestUtil.decodeDesJson(parsed, this.desKey, this.desIV);
hook 它看看:
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":{}}
因為我在這給的賬號和密碼本就是錯誤的,所以提示賬號或密碼錯誤一點問題沒有。
GoUpSec
數緣信安社區
商密君
上官雨寶
信息安全與通信保密雜志社
X0_0X
RacentYY
嘶吼專業版
信息安全與通信保密雜志社
中國信息安全
007bug