这篇文章上次修改于 1682 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
Android应用防重放攻击总结
背景
很多APP采用多种安全策略来防止大黑阔的攻击,采用的手段有:
通信层面:使用HTTPS方式传输通信内容
代码层面:使用加固工具对APP自身进行加密,亦或使用OLLVM或者Proguard插件对APP混淆
若上面两层防护手段都被突破了,还有一道也就是本文想要说明的一种防护方法——防重放策略。
防重放策略介绍
防重放策略一
sign=md5(token+userid+timestamp)
用户凭证加上userid
加上时间戳生成sign
值,太糙了,仅凭时间戳就想达到防重放的目的,no,no,no,no,程序猿xgg太天真了...
防重放策略二
for (int i = 4608000; i <= 4608903; i++) {
Map hashMap = new HashMap();
String vin=String.valueOf(i);
hashMap.put("userid",vin);
String xx = getSignCheckContentV1(hashMap, URL_ALL_ILLEGAL_LIST, PASSWORD);
}
public static String getSignCheckContentV1(Map<String, Object> map, String str, String str2) {
if (map == null || str2.equals("")) {
return null;
}
map.put("rl_key", str2);
StringBuffer stringBuffer = new StringBuffer();
ArrayList arrayList = new ArrayList(map.keySet());
Collections.sort(arrayList);
for (int i = 0; i < arrayList.size(); i++) {
String str3 = (String) arrayList.get(i);
Object obj = map.get(str3);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(str3);
stringBuilder.append("-");
stringBuilder.append(obj);
stringBuffer.append(stringBuilder.toString());
}
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.append(str);
stringBuilder2.append(stringBuffer.toString());
return generateMd5(stringBuilder2.toString());
}
hashmap
是一个叫userid
的东西,URL_ALL_ILLEGAL_LIST
是APP定义好的接口地址,外加一个授权密码
防重放策略三
String getLastLocation = "location/public/location/getLastLocation_appkey=123123456_nonce=" + nonce + "_signt=" + signt + "_token=" + token + "_uniqueId=" + vin + "_l5k123123123131311231312zmy";
System.out.println(getLastLocation);
String getLastLocationMd5 = d.generateMd5(URLEncoder.encode(getLastLocation, "UTF-8")).toLowerCase();
URL+NONCE+SIGNT+TOKEN+UNIQUEID+VIN+STR复杂度够了,但是被我们知道了计算逻辑一样跑不了被绕过
防重放策略四
概述及样例
请求地址
http://e710888d3ccb4638a723ff8d03837095-cn-qingdao.aliapi.com/demo/post
请求方法
POST
请求体
FormParam1=FormParamValue1&FormParam2=FormParamValue2
//HTTP Request Body
请求头部
Host: e710888d3ccb4638a723ff8d03837095-cn-qingdao.aliapi.com
Date: Mon, 22 Aug 2016 11:21:04 GMT
User-Agent: Apache-HttpClient/4.1.2 (java 1.6)
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
//请求体类型,请根据实际请求体内容设置。
Accept: application/json
//请求响应体类型,部分 API 可以根据指定的响应类型来返回对应数据格式,建议手动指定此请求头,如果不设置,部分 HTTP 客户端会设置默认值 */*,导致签名错误。
X-Ca-Request-Mode: debug
//是否开启 Debug 模式,大小写不敏感,不设置默认关闭,一般 API 调试阶段可以打开此设置。
X-Ca-Version: 1
// API 版本号,目前所有 API 仅支持版本号『1』,可以不设置此请求头,默认版本号为『1』。
X-Ca-Signature-Headers: X-Ca-Request-Mode,X-Ca-Version,X-Ca-Stage,X-Ca-Key,X-Ca-Timestamp
//参与签名的自定义请求头,服务端将根据此配置读取请求头进行签名,此处设置不包含 Content-Type、Accept、Content-MD5、Date 请求头,这些请求头已经包含在了基础的签名结构中,详情参照请求签名说明文档。
X-Ca-Stage: RELEASE
//请求 API 的 Stage,目前支持 TEST、PRE、RELEASE 三个 Stage,大小写不敏感,API 提供者可以选择发布到哪个 Stage,只有发布到指定 Stage 后 API 才可以调用,否则会提示 API 找不到或 Invalid Url。
X-Ca-Key: 60022326
//请求的 AppKey,请到 API 网关控制台生成,只有获得 API 授权后才可以调用,通过云市场等渠道购买的 API 默认已经给 APP 授过权,阿里云所有云产品共用一套 AppKey 体系,删除 ApppKey 请谨慎,避免影响到其他已经开通服务的云产品。
X-Ca-Timestamp: 1471864864235
//请求的时间戳,值为当前时间的毫秒数,也就是从1970年1月1日起至今的时间转换为毫秒,时间戳有效时间为15分钟。
X-Ca-Nonce:b931bc77-645a-4299-b24b-f3669be577ac
//请求唯一标识,15分钟内 AppKey+API+Nonce 不能重复,与时间戳结合使用才能起到防重放作用。
X-Ca-Signature: FJleSrCYPGCU7dMlLTG+UD3Bc5Elh3TV3CWHtSKh1Ys=
//请求签名。
CustomHeader: CustomHeaderValue
//自定义请求头,此处仅作为示例,实际请求中根据 API 定义可以设置多个自定义请求头。
请求签名说明文档
系统级 Header
- 【必选】X-Ca-Key:AppKey。
- 【必选】X-Ca-Signature:签名字符串。
- 【可选】X-Ca-Timestamp:API 调用者传递时间戳,值为当前时间的毫秒数,也就是从1970年1月1日起至今的时间转换为毫秒,时间戳有效时间为15分钟。
- 【可选】X-Ca-Nonce:API 调用者生成的 UUID,结合时间戳防重放。
- 【可选】Content-MD5 当请求 Body 非 Form 表单时,可以计算 Body 的 MD5 值传递给云网关进行 Body MD5 校验。
- 【可选】X-Ca-Stage请求 API 所属 Stage,目前仅支持 TEST 、PRE 和 RELEASE,默认RELEASE,若您调用的 API 不在线上环境,请一定要指定该参数的值,否则会报 URL 错误。
签名校验
组织参与签名计算的字符串
String stringToSign= HTTPMethod + "\n" + Accept + "\n" + //建议显示设置 Accept Header。当 Accept 为空时,部分 Http 客户端会给 Accept 设置默认值为 */*,导致签名校验失败。Content-MD5 + "\n" Content-Type + "\n" + Date + "\n" + Headers + Url
HTTPMethod 为全大写,如 POST。
Accept、Content-MD5、Content-Type、Date 如果为空也需要添加换行符”n”,Headers如果为空不需要添加”n”。
Content-MD5
Content-MD5 是指 Body 的 MD5 值,只有当 Body 非 Form 表单时才计算 MD5,计算方式为:
String content-MD5 = Base64.encodeBase64(MD5(bodyStream.getbytes("UTF-8")));
bodyStream 为字节数组。
Headers
Headers 是指参与 Headers 签名计算的 Header 的 Key、Value 拼接的字符串,建议对 X-Ca 开头以及自定义 Header 计算签名,注意如下参数不参与 Headers 签名计算:X-Ca-Signature、X-Ca-Signature-Headers、Accept、Content-MD5、Content-Type、Date。
- Headers 组织方法:
先对参与 Headers 签名计算的 Header的Key 按照字典排序后使用如下方式拼接,如果某个 Header 的 Value 为空,则使用 HeaderKey + “:” + “n”参与签名,需要保留 Key 和英文冒号。
String headers =HeaderKey1 + ":" + HeaderValue1 + "\n"\+HeaderKey2 + ":" + HeaderValue2 + "\n"\+...HeaderKeyN + ":" + HeaderValueN + "\n"
将 Headers 签名中 Header 的 Key 使用英文逗号分割放到 Request 的 Header 中,Key为:X-Ca-Signature-Headers。
Url
Url 指 Path + Query + Body 中 Form 参数,组织方法:对 Query+Form 参数按照字典对 Key 进行排序后按照如下方法拼接,如果 Query 或 Form 参数为空,则 Url = Path,不需要添加 ?,如果某个参数的 Value 为空只保留 Key 参与签名,等号不需要再加入签名。
String url =Path +"?" +Key1 + "=" + Value1 +"&" + Key2 + "=" + Value2 +..."&" + KeyN + "=" + ValueN
注意这里 Query 或 Form 参数的 Value 可能有多个,多个的时候只取第一个 Value 参与签名计算。
计算签名
Mac hmacSha256 = Mac.getInstance("HmacSHA256");byte[] keyBytes = secret.getBytes("UTF-8");hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, "HmacSHA256"));String sign = new String(Base64.encodeBase64(hmacSha256.doFinal(stringToSign.getBytes("UTF-8")),"UTF-8"));
secret 为 APP 的密钥。
传递签名
将计算的签名结果放到 Request 的 Header 中,Key为:X-Ca-Signature。
破解之道
破解方法很简单,只要寻找到计算sign
值的方法一把梭哈,即可知晓绕过方法。翻译成人话就是:我们只要hook最终执行计算sign的方法即可。
这里hook的工具有:frida,那女老少都喜欢的hook工具~
啥自己写hook脚本太麻烦了,还有基于frida的更简单化的操作工具objection
具体操作自己看手册去吧~
我认为师傅们的学习能力还是很强的~
结语
说了这么多,感觉有股内味了….是软广的味道….. 不过frida+objection是真香啊~请自行脑补情景~~
就以上例子来看,其实防重放无论怎么防只要防重放的逻辑被搞定那么他的防重放策略都是不安全的。(捂脸,这句好像是废话,我就是为了水点字数,不然通篇都是代码都没几行字)
Java层的防重放太容易被搞了,那么放在native层呢?要想知道native层的防重放分析案例或者本文所述的实际分析案例,那么请留言,这样我就知道大家的意思了~我会择机再水一篇文章
文中有啥不当,错误的地方,各位巨佬请尽情的畅言——(喷我~)
没有评论