首先使用Burpsutie抓取账户密码的登录数据包:
可以看到数据包是类型是json格式,body字段中的参数是加密的。同时,在数据包头部中有Sign。这个Sign的长度是40,并不是MD5的 长度,但在没有别的头绪之前,先尝试对其进行MD5的追踪:
使用frida-trace -UF -i "CC_MD5"
修改frida默认脚本如下:
{
onEnter(log, args, state) {
log('CC_MD5()--arg[0]='+args[0].readUtf8String());
},
onLeave(log, retval, state) {
log('CC_MD5()--return--=');
var md5_digest = hexdump(retval,{length:16});
var hexified = " ";
var raw_array = md5_digest.split("n");
for (var a=0; a<raw_array.length; a++)
{
var line_array = raw_array[a].split(" ");
for (var b=1; b<line_array.length-1; b++)
{
if (line_array[b].length === 2)
{
hexified += line_array[b];
hexified = hexified.trim();
}
}
}
log(hexified+"n");
}
}
根据数据包中的Sign值在脚本输出内容中进行搜索:
可以看到这个sign的组成是由数据包+接口+V1QiLCJhbGciOiJIUzI1NiJ9组成。
多尝试抓几个登录的数据包发现,V1QiLCJhbGciOiJIUzI1NiJ9没有变化。
使用MD5对数据包+接口+V1QiLCJhbGciOiJIUzI1NiJ9进行编码:
e8c9fe8c23a7f8045b89f4e58c34e726
e8c9fe8c23a7f8045b89f4e58c34e726e31fc14c
还有后8位不清楚是什么。
对app进行反汇编,搜索V1QiLCJhbGciOiJIUzI1NiJ9看看:
发现是从+[KEPPostSecuritySign kep_signWithURL:body:]:函数方法进行调用:
根据我们之前分析的结果,在以下指令中,
add x8, x8, #0x58 ; 0x108a4c058@PAGEOFF, @"V1QiLCJhbGciOiJIUzI1NiJ9"
大致就是将body+接口+V1QiLCJhbGciOiJIUzI1NiJ9。
add x2, x2, #0x78 ; 0x108a4c078@PAGEOFF, @"%@%@%@%@"
这条指令将 x2 寄存器中的值与立即数 0x78 相加,并将结果再存回 x2 寄存器。这样,x2 寄存器现在指向的是一个具体的内存地址,这个地址与字符串 "@%@@%@@%@@%@" 相关。这个字符串是一个格式字符串,常用于 NSString 的格式化输出方法中。
然后排查sub_1081cf880、sub_108073780、 sub_1080737c0方法。
发现sub_108073780调用了一个kep_networkMd5的方法,它首先加载该方法的选择器到 x1 寄存器,接着加载 _objc_msgSend 的地址到 x16 寄存器,然后调用 _objc_msgSend 来执行方法调用。
因此判断这里就是将数据包+接口+V1QiLCJhbGciOiJIUzI1NiJ9进行MD5编码。
然后在sub_1080737c0中加载 x16 寄存器与 0x108694000 相关的页面地址。
大致就是将上一步MD5的值与刚刚000000010246da98进行拼接。我们尝试去调用kep_networkStringOffsetSecurity:这个方法:
接下来我们分析body的加密内容。根据加密的密文格式,可能使用AES加密算法,使用以下脚本看看是否更够hook到加密算法:
// color着色
var Color = {
RESET: "x1b[39;49;00m", Black: "0;01", Blue: "4;01", Cyan: "6;01", Gray: "7;01", Green: "2;01", Purple: "5;01", Red: "1;01", Yellow: "3;01",
Light: {
Black: "0;11", Blue: "4;11", Cyan: "6;11", Gray: "7;01", Green: "2;11", Purple: "5;11", Red: "1;11", Yellow: "3;11"
}
};
var LOG = function (input, kwargs) {
kwargs = kwargs || {};
var logLevel = kwargs['l'] || 'log', colorPrefix = 'x1b[3', colorSuffix = 'm';
if (typeof input === 'object')
input = JSON.stringify(input, null, kwargs['i'] ? 2 : null);
if (kwargs['c'])
input = colorPrefix + kwargs['c'] + colorSuffix + input + Color.RESET;
console[logLevel](input);
};
function base64encode(str) {
var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var out, i, len;
var c1, c2, c3;
len = str.length;
i = 0;
out = "";
while (i < len) {
c1 = str.charCodeAt(i++) & 0xff;
if (i == len) {
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt((c1 & 0x3) << 4);
out += "==";
break;
}
c2 = str.charCodeAt(i++);
if (i == len) {
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
out += base64EncodeChars.charAt((c2 & 0xF) << 2);
out += "=";
break;
}
c3 = str.charCodeAt(i++);
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
out += base64EncodeChars.charAt(c3 & 0x3F);
}
return out;
}
function bin2string(array){
var result = "";
for(var i = 0; i < array.length; ++i){
result+= (String.fromCharCode(array[i]));
}
return result;
}
Interceptor.attach(Module.findExportByName('libcommonCrypto.dylib', 'CCCrypt'), {
onEnter: function (args) {
// Save the arguments
this.operation = args[0]
this.CCAlgorithm = args[1]
this.CCOptions = args[2]
this.keyBytes = args[3]
this.keyLength = args[4]
this.ivBuffer = args[5]
this.inBuffer = args[6]
this.inLength = args[7]
this.outBuffer = args[8]
this.outLength = args[9]
this.outCountPtr = args[10]
console.log("[+] --------------------------------------------------------------");
// 不要在onLeave回溯堆栈,会产生误报!
console.log('tACCURATE Backtrace:nt' + Thread.backtrace(this.context,Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('nt'));
// console.log('tFUZZY Backtrace:nt' + Thread.backtrace(this.context,Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('nt'));
},
onLeave: function (retVal) {
if (this.operation == 0) {
// Show the buffers here if this an encryption operation
// console.log("[+] CCAlgorithm: " + this.CCAlgorithm);
if (this.CCAlgorithm == 0x0) {console.log("[+] CCAlgorithm: " + this.CCAlgorithm + " --> AES Encrypt");}
if (this.CCAlgorithm == 0x1) {console.log("[+] CCAlgorithm: " + this.CCAlgorithm + " --> DES Encrypt");}
if (this.CCAlgorithm == 0x2) {console.log("[+] CCAlgorithm: " + this.CCAlgorithm + " --> 3DES Encrypt");}
// enum {
// /* options for block ciphers */
// kCCOptionPKCS7Padding = 0x0001,
// kCCOptionECBMode = 0x0002
// /* stream ciphers currently have no options */
// };
// typedef uint32_t CCOptions;
if (this.CCOptions == 0x0 || this.CCOptions == 0x1) {console.log("[+] CCOptions: " + this.CCOptions + " --> mode CBC");}
if (this.CCOptions == 0x2 || this.CCOptions == 0x3) {console.log("[+] CCOptions: " + this.CCOptions + " --> mode ECB");} // kCCOptionPKCS7Padding | kCCOptionECBMode == 0x03
// if (this.CCOptions != 0x1 && this.CCOptions != 0x3) {console.log("[+] CCOptions: " + this.CCOptions);}
console.log(Memory.readByteArray(this.inBuffer, this.inLength.toInt32()));
try {
// console.log("[+] Before Encrypt: " + Memory.readUtf8String(this.inBuffer, this.inLength.toInt32()));
LOG("[+] Before Encrypt: " + Memory.readUtf8String(this.inBuffer, this.inLength.toInt32()), { c: Color.Gray });
} catch(e) {
// var ByteArray = Memory.readByteArray(this.inBuffer, this.inLength.toInt32());
// var uint8Array = new Uint8Array(ByteArray);
// // console.log("[+] uint8Array: " + uint8Array);
// var str = "";
// for(var i = 0; i < uint8Array.length; i++) {
// var hextemp = (uint8Array[i].toString(16))
// if(hextemp.length == 1){
// hextemp = "0" + hextemp
// }
// str += hextemp + " ";
// }
// console.log("[+] Before Encrypt: " + str); // 打印hex,非可见ascii范围
}
console.log(Memory.readByteArray(this.keyBytes, this.keyLength.toInt32()));
if (this.keyLength.toInt32() == 16) {console.log("[+] KEY Length --> 128");}
if (this.keyLength.toInt32() == 24) {console.log("[+] KEY Length --> 192");}
if (this.keyLength.toInt32() == 32) {console.log("[+] KEY Length --> 256");}
try {
// console.log("[+] KEY: " + Memory.readUtf8String(this.keyBytes, this.keyLength.toInt32()));
LOG("[+] KEY: " + Memory.readUtf8String(this.keyBytes, this.keyLength.toInt32()), { c: Color.Gray });
} catch(e) {
var ByteArray = Memory.readByteArray(this.keyBytes, this.keyLength.toInt32());
var uint8Array = new Uint8Array(ByteArray);
// console.log("[+] uint8Array: " + uint8Array);
var str = "";
for(var i = 0; i < uint8Array.length; i++) {
var hextemp = (uint8Array[i].toString(16))
if(hextemp.length == 1){
hextemp = "0" + hextemp
}
str += hextemp + " ";
}
// console.log("[+] KEY: " + str); // 打印hex,非可见ascii范围
LOG("[+] KEY: " + str, { c: Color.Gray });
}
if (this.CCOptions == 0x0 || this.CCOptions == 0x1) {
console.log(Memory.readByteArray(this.ivBuffer, 16));
try {
// console.log("[+] IV: " + Memory.readUtf8String(this.ivBuffer, this.keyLength.toInt32()));
LOG("[+] IV: " + Memory.readUtf8String(this.ivBuffer, 16), { c: Color.Gray });
} catch(e) {
var ByteArray = Memory.readByteArray(this.ivBuffer, 16);
var uint8Array = new Uint8Array(ByteArray);
var str = "";
for(var i = 0; i < uint8Array.length; i++) {
var hextemp = (uint8Array[i].toString(16))
if(hextemp.length == 1){
hextemp = "0" + hextemp
}
str += hextemp + " ";
}
// console.log("[+] IV: " + str); // 打印hex,非可见ascii范围
LOG("[+] IV: " + str, { c: Color.Gray });
}
}
// console.log(Memory.readByteArray(this.outBuffer, Memory.readUInt(this.outCountPtr))); // 打印hex,非可见ascii范围
var array = new Uint8Array(Memory.readByteArray(this.outBuffer, Memory.readUInt(this.outCountPtr)));
// console.log("[+] After Encrypt: " + base64encode(bin2string(array)));
LOG("[+] After Encrypt: " + base64encode(bin2string(array)), { c: Color.Gray });
console.log("[-] --------------------------------------------------------------n");
}
if (this.operation == 1) {
// Show the buffers here if this a decryption operation
// console.log("inLength: " + this.inLength.toInt32());
// console.log("outLength: " + this.outLength.toInt32());
// console.log("outCount: " + Memory.readUInt(this.outCountPtr)); //实际的outBuffer的有效长度
// console.log("[+] CCAlgorithm: " + this.CCAlgorithm);
if (this.CCAlgorithm == 0x0) {console.log("[+] CCAlgorithm: " + this.CCAlgorithm + " --> AES Decrypt");}
if (this.CCAlgorithm == 0x1) {console.log("[+] CCAlgorithm: " + this.CCAlgorithm + " --> DES Decrypt");}
if (this.CCAlgorithm == 0x2) {console.log("[+] CCAlgorithm: " + this.CCAlgorithm + " --> 3DES Decrypt");}
// enum {
// /* options for block ciphers */
// kCCOptionPKCS7Padding = 0x0001,
// kCCOptionECBMode = 0x0002
// /* stream ciphers currently have no options */
// };
// typedef uint32_t CCOptions;
if (this.CCOptions == 0x0 || this.CCOptions == 0x1) {console.log("[+] CCOptions: " + this.CCOptions + " --> mode CBC");}
if (this.CCOptions == 0x2 || this.CCOptions == 0x3) {console.log("[+] CCOptions: " + this.CCOptions + " --> mode ECB");} // kCCOptionPKCS7Padding | kCCOptionECBMode == 0x03
// if (this.CCOptions != 0x1 && this.CCOptions != 0x3) {console.log("[+] CCOptions: " + this.CCOptions);}
var array = new Uint8Array(Memory.readByteArray(this.inBuffer, this.inLength.toInt32()));
// console.log("[+] Before Decrypt: " + base64encode(bin2string(array)));
LOG("[+] Before Decrypt: " + base64encode(bin2string(array)), { c: Color.Gray });
console.log(Memory.readByteArray(this.keyBytes, this.keyLength.toInt32()));
if (this.keyLength.toInt32() == 16) {console.log("[+] KEY Length --> 128");}
if (this.keyLength.toInt32() == 24) {console.log("[+] KEY Length --> 192");}
if (this.keyLength.toInt32() == 32) {console.log("[+] KEY Length --> 256");}
try {
// console.log("[+] KEY: " + Memory.readUtf8String(this.keyBytes, this.keyLength.toInt32()));
LOG("[+] KEY: " + Memory.readUtf8String(this.keyBytes, this.keyLength.toInt32()), { c: Color.Gray });
} catch(e) {
var ByteArray = Memory.readByteArray(this.keyBytes, this.keyLength.toInt32());
var uint8Array = new Uint8Array(ByteArray);
// console.log("[+] uint8Array: " + uint8Array);
var str = "";
for(var i = 0; i < uint8Array.length; i++) {
var hextemp = (uint8Array[i].toString(16))
if(hextemp.length == 1){
hextemp = "0" + hextemp
}
str += hextemp + " ";
}
// console.log("[+] KEY: " + str); // 打印hex,非可见ascii范围
LOG("[+] KEY: " + str, { c: Color.Gray });
}
if (this.CCOptions == 0x0 || this.CCOptions == 0x1) {
console.log(Memory.readByteArray(this.ivBuffer, 16));
try {
// console.log("[+] IV: " + Memory.readUtf8String(this.ivBuffer, this.keyLength.toInt32()));
LOG("[+] IV: " + Memory.readUtf8String(this.ivBuffer, 16), { c: Color.Gray });
} catch(e) {
var ByteArray = Memory.readByteArray(this.ivBuffer, 16);
var uint8Array = new Uint8Array(ByteArray);
var str = "";
for(var i = 0; i < uint8Array.length; i++) {
var hextemp = (uint8Array[i].toString(16))
if(hextemp.length == 1){
hextemp = "0" + hextemp
}
str += hextemp + " ";
}
// console.log("[+] IV: " + str); // 打印hex,非可见ascii范围
LOG("[+] IV: " + str, { c: Color.Gray });
}
}
console.log(Memory.readByteArray(this.outBuffer, Memory.readUInt(this.outCountPtr)));
try {
// console.log("[+] After Decrypt: " + Memory.readUtf8String(this.outBuffer, Memory.readUInt(this.outCountPtr)));
LOG("[+] After Decrypt: " + Memory.readUtf8String(this.outBuffer, Memory.readUInt(this.outCountPtr)), { c: Color.Gray });
} catch(e) {
// var ByteArray = Memory.readByteArray(this.outBuffer, Memory.readUInt(this.outCountPtr));
// var uint8Array = new Uint8Array(ByteArray);
// // console.log("[+] uint8Array: " + uint8Array);
// var str = "";
// for(var i = 0; i < uint8Array.length; i++) {
// var hextemp = (uint8Array[i].toString(16))
// if(hextemp.length == 1){
// hextemp = "0" + hextemp
// }
// str += hextemp + " ";
// }
// console.log("[+] After Decrypt: " + str); // 打印hex,非可见ascii范围
}
console.log("[-] --------------------------------------------------------------n");
}
}
})
根据登录时候输入的手机号码,看看是否有登录数据包的加密信息:
可以看到,使用的加密算法是AES128,加密模式是CBC。加密前的数据包也输出了。
使用AES加密数据包:
输出的结果并不是刚刚burpsuite的密文,我们跟进一下刚刚输出的函数SecurityUtil:
它还有一个encodeBase64Data方法:
在进行AES加密之前,还有一个base64的编码:
在刚刚的基础上增加一个base64的编码:
成功将body的参数进行解密。
原文始发于微信公众号(赛哈文):某健身APP登录数据包逆向分析
- 我的微信
- 微信扫一扫
-
- 我的微信公众号
- 微信扫一扫
-
评论