最近在尝试逆向方向相关的探索,针对某款运动APP的登录协议进行了分析,此文记录一下分析过程与结果,仅供学习研究,使用的工具较少,内容也比较简单,新手项,大佬请跳过。针对密码登录模块进行分析,随便输入一个手机号与密码,后续使用抓包工具分析,针对登录协议的几个字段从学习角度还是值得看下实现逻辑的。
首先能看到请求头里面有sign字段,针对该字段进行分析:
sign: b61df9a8bce7a8641c5ca986b55670e633a7ab29
整体长度为40,常用的MD5长度为32,第一反应不太像,但是也有可能md5以后再拼接其它字段,sha1散列函数的长度是40,正好吻合。那我们就一一验证,先看下是否有MD5的痕迹,直接写脚本frida试着跑下。 脚本内容比较明确,针对MD5的Init、Update、Final分别hook打印看下输入与输出,下面给到关键代码:
很幸运,在打印中明显看到了sign相关的内容打印,但是缺少sign的后面一部分,那就明确sign值的构成为32(md5)+8,先看下md5的数据构造过程。
b61df9a8bce7a8641c5ca986b55670e6 33a7ab29
通过打印可以明确的看到,sign的MD5由三部分数据组成,分别为:bodyData+Url+Str,body数据也可从Charles获取到。
{"body":"5gJEXtLqe3tzRsP8a/bSwehe0ta3zQx6wG7K74sOeXQ6Auz1NI1bg68wNLmj1e5Xl7CIwWelukC445W7HXxJY6nQ0v0SUg1tVyWS5L8E2oaCgoSeC6ypFNXV2xVm8hHV"}
/account/v4/login/password
V1QiLCJhbGciOiJIUzI1NiJ9
到这里有一个疑问,数据的第三部分:V1QiLCJhbGciOiJIUzI1NiJ9,该值是固定的字符串还是每次都变化的?猜测应该是固定的字符串,作为MD5的Salt值来使用,我们再次请求验证一下。
新的sign值为:131329a5af4ecb025fb5088615d5e5c526dbd1a3,通过脚本打印的数据能确认第三部分为固定字符串。
MD5({"body":"12BcOSg50nLxdbt++r7liZpeyWAVpmihTy8Zu8BmpA6a1hqdevS5PPYwnbtpjN05xgeyReSihh9idyfriR6qx1Fbo8AA0k8HQt6gJ3spWITI21GhLTzh9PDUkgjCtrEK"}/account/v4/login/passwordV1QiLCJhbGciOiJIUzI1NiJ9)
接下来我们针对Sign的尾部数据进行分析,单纯盲猜或者挂frida脚本已经解决不了问题了,我们用IDA看下具体的实现逻辑,当然上面的MD5分析也可以直接从IDA反编译入手,通过搜索sign关键字进行定位,只是我习惯先钩一下脚本,万一直接命中就不用费时间去分析了...
通过MD5的脚本打印,我们也能看到相关的函数调用栈,这对于我们快速定位也提供了很大的方便。我们直接搜索[KEPPostSecuritySign kep_signWithURL: body:]方法,可以看到明显的字符串拼接的痕迹,IDA还是比较智能的,已经识别出了MD5的salt值。
通过分析,定位到[NSString kep_networkStringOffsetSecurity]函数,在内部进行了字符串的处理,在循环里面进行了各种判断以及移位操作,不嫌麻烦的话可以分析一下逻辑,重写一下处理流程。
我这边处理比较暴力,发现kep_networkStringOffsetSecurity是NSString的Catetory,那就直接调用验证一下吧,使用frida挂载以后,找到NSString类,调用方法传入md5之后的值,然后就会发现经过该函数,神奇的sign值就给到了。
分析完sign以后,观察到还有一个x-ads的字段,按照惯例,先用脚本试着钩一下,经常采用的加密大致就是DES、AES或RC4这些算法。
针对 AES128、DES、3DES、CAST、RC4、RC2、Blowfish等加密算法进行hook,脚本的关键代码如下:
查看脚本的输出日志,直接命中了AES128的加密算法,并且输出的Base64数据完全匹配,只能说运气爆棚。
拿到对应的key跟iv,尝试解密看下也是没问题的。x-ads分析结束,都不用反编译看代码:)
最后看下sign值的组成部分,body数据是怎么计算的,抱着试试的想法,直接用x-ads分析得到的算法以及对应的key、iv进行解密:
{
"body": "5gJEXtLqe3tzRsP8a/bSwXDiK0VslZZZyOEj1jBDBhtYTGGdWltuIjLbzwZ2OxMcb3mFX7bJtgH3WlqGET5W34P4dTEIDhLH6FkT3HSLaDnEXYHvEl9IZRQKf19wMG/t"
}
这次说不上什么运气爆棚了...只能说开发者比较懒或者安全意识有点差了,使用了AES-CBC模式,iv都不改变一下的...
这次分析整体来看,没什么技术含量,大部分都是脚本直接解决了,从结果来看,也是使用的常规的加密、签名算法,这也从侧面给我们安全开发提个醒,是不是可以有策略性的改变一下,比如我们拿MD5来看下都可以做哪些改变。
https://opensource.apple.com/source/ppp/ppp-37/ppp/pppd/md5.c.auto.html
首先针对MD5Init,我们可以改变它的初始化数据:
其次针对Transform我们也可以改变其中的某几个数据:
简单的变形以后,即使脚本能hook到对应的函数,但是想直接脱机调用结果还是不可以的,此时就要不得不进行反编译分析或者动态调试,此时配合代码混淆、VMP等静态防护手段,再加上反调试等安全手段,对于攻击的门槛也相应的提高。
Interceptor.attach(Module.findExportByName(
"libcommonCrypto.dylib"
, g_funcName), {
onEnter:
function
(args) {
console.log(g_funcName +
" begin"
);
var
len = args[1].toInt32();
console.log(
"input:"
);
dumpBytes(args[0], len);
this
.md = args[2];
},
onLeave:
function
(retval) {
console.log(g_funcName +
" return value"
);
dumpBytes(
this
.md, g_funcRetvalLength);
console.log(g_funcName +
' called from:\n'
+
Thread.backtrace(
this
.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join(
'\n'
) +
'\n'
);
}
});
Interceptor.attach(Module.findExportByName(
"libcommonCrypto.dylib"
, g_updateFuncName), {
onEnter:
function
(args) {
console.log(g_updateFuncName +
" begin"
);
var
len = args[2].toInt32();
console.log(
"input:"
);
dumpBytes(args[1], len);
},
onLeave:
function
(retval) {
console.log(g_updateFuncName +
' called from:\n'
+
Thread.backtrace(
this
.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join(
'\n'
) +
'\n'
);
}
});
Interceptor.attach(Module.findExportByName(
"libcommonCrypto.dylib"
, g_finalFuncName), {
onEnter:
function
(args) {
finalArgs_md = args[0];
},
onLeave:
function
(retval) {
console.log(g_finalFuncName +
" return value"
);
dumpBytes(finalArgs_md, g_funcRetvalLength);
console.log(g_finalFuncName +
' called from:\n'
+
Thread.backtrace(
this
.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join(
'\n'
) +
'\n'
);
}
});
Interceptor.attach(Module.findExportByName(
"libcommonCrypto.dylib"
, g_funcName), {
onEnter:
function
(args) {
console.log(g_funcName +
" begin"
);
var
len = args[1].toInt32();
console.log(
"input:"
);
dumpBytes(args[0], len);
this
.md = args[2];
},
onLeave:
function
(retval) {
console.log(g_funcName +
" return value"
);
dumpBytes(
this
.md, g_funcRetvalLength);
console.log(g_funcName +
' called from:\n'
+
Thread.backtrace(
this
.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join(
'\n'
) +
'\n'
);
}
});
Interceptor.attach(Module.findExportByName(
"libcommonCrypto.dylib"
, g_updateFuncName), {
onEnter:
function
(args) {
console.log(g_updateFuncName +
" begin"
);
var
len = args[2].toInt32();
console.log(
"input:"
);
dumpBytes(args[1], len);
},
onLeave:
function
(retval) {
console.log(g_updateFuncName +
' called from:\n'
+
Thread.backtrace(
this
.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join(
'\n'
) +
'\n'
);
}
});
Interceptor.attach(Module.findExportByName(
"libcommonCrypto.dylib"
, g_finalFuncName), {
onEnter:
function
(args) {
finalArgs_md = args[0];
},
onLeave:
function
(retval) {
console.log(g_finalFuncName +
" return value"
);
dumpBytes(finalArgs_md, g_funcRetvalLength);
console.log(g_finalFuncName +
' called from:\n'
+
Thread.backtrace(
this
.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join(
'\n'
) +
'\n'
);
}
});
var
handlers = {
CCCrypt: {
onEnter:
function
(args) {
var
operation = CCOperation[args[0].toInt32()];
var
alg = CCAlgorithm[args[1].toInt32()].name;
this
.options = CCoptions[args[2].toInt32()];
var
keyBytes = args[3];
var
keyLength = args[4].toInt32();
var
ivBuffer = args[5];
var
inBuffer = args[6];
this
.inLength = args[7].toInt32();
this
.outBuffer = args[8];
var
outLength = args[9].toInt32();
this
.outCountPtr = args[10];
if
(
this
.inLength < MIN_LENGTH ||
this
.inLength > MAX_LENGTH){
return
;
}
if
(operation ===
"kCCEncrypt"
) {
this
.operation =
"encrypt"
console.log(
"***************** encrypt begin **********************"
);
}
else
{
this
.operation =
"decrypt"
console.log(
"***************** decrypt begin **********************"
);
}
console.log(
"CCCrypt("
+
"operation: "
+
this
.operation +
", "
+
"CCAlgorithm: "
+ alg +
", "
+
"CCOptions: "
+
this
.options +
", "
+
"keyBytes: "
+ keyBytes +
", "
+
"keyLength: "
+ keyLength +
", "
+
"ivBuffer: "
+ ivBuffer +
", "
+
"inBuffer: "
+ inBuffer +
", "
+
"inLength: "
+
this
.inLength +
", "
+
"outBuffer: "
+
this
.outBuffer +
", "
+
"outLength: "
+ outLength +
", "
+
"outCountPtr: "
+
this
.outCountPtr +
")"
);
console.log(
"key: "
);
dumpBytes(keyBytes, keyLength);
console.log(
"IV: "
);
dumpBytes(ivBuffer, keyLength);
var
isOutput =
true
;
if
(!SHOW_PLAIN_AND_CIPHER &&
this
.operation ==
"decrypt"
) {
isOutput =
false
;
}
if
(isOutput){
console.log(
"In buffer:"
);
dumpBytes(inBuffer,
this
.inLength);
}
},
onLeave:
function
(retVal) {
if
(
this
.inLength < MIN_LENGTH ||
this
.inLength > MAX_LENGTH){
return
;
}
var
isOutput =
true
;
if
(!SHOW_PLAIN_AND_CIPHER &&
this
.operation ==
"encrypt"
) {
isOutput =
false
;
}
if
(isOutput) {
console.log(
"Out buffer:"
);
dumpBytes(
this
.outBuffer, Memory.readUInt(
this
.outCountPtr));
}
console.log(
'CCCrypt called from:\n'
+
Thread.backtrace(
this
.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join(
'\n'
) +
'\n'
);
}
},
};
if
(ObjC.available) {
console.log(
"frida attach"
);
for
(
var
func
in
handlers) {
console.log(
"hook "
+ func);
Interceptor.attach(Module.findExportByName(
"libcommonCrypto.dylib"
, func), handlers[func]);
}
}
else
{
console.log(
"Objective-C Runtime is not available!"
);
}
var
handlers = {
CCCrypt: {
onEnter:
function
(args) {
var
operation = CCOperation[args[0].toInt32()];
var
alg = CCAlgorithm[args[1].toInt32()].name;
this
.options = CCoptions[args[2].toInt32()];
var
keyBytes = args[3];
var
keyLength = args[4].toInt32();
var
ivBuffer = args[5];
var
inBuffer = args[6];
this
.inLength = args[7].toInt32();
this
.outBuffer = args[8];
var
outLength = args[9].toInt32();
this
.outCountPtr = args[10];
if
(
this
.inLength < MIN_LENGTH ||
this
.inLength > MAX_LENGTH){
return
;
}
if
(operation ===
"kCCEncrypt"
) {
this
.operation =
"encrypt"
console.log(
"***************** encrypt begin **********************"
);
}
else
{
this
.operation =
"decrypt"
console.log(
"***************** decrypt begin **********************"
);
}
console.log(
"CCCrypt("
+
"operation: "
+
this
.operation +
", "
+
"CCAlgorithm: "
+ alg +
", "
+
"CCOptions: "
+
this
.options +
", "
+
"keyBytes: "
+ keyBytes +
", "
+
"keyLength: "
+ keyLength +
", "
+
"ivBuffer: "
+ ivBuffer +
", "
+
"inBuffer: "
+ inBuffer +
", "
+
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)