0. 起因
因工作或生活上的某些原因不得不使用某应用, 暂且记为 A 应用把可 A 应用设计得实在不人性化, 一个操作通常需要点击若干次屏幕, 点击一次还要 lodaing , 程序员说: 不能忍
于是开始着手改善软件体验
1. 初步计划
初步分析 A 应用实际上是一个 HTTP 客户端, 前端后台之间完全通过 HTTP 协议传输数据可使用 Fiddler 工具抓取数据包分析
分析发现之前所有那些繁杂的操作 (例如签到打卡 (虚构)), 其实只需要发送一个 HTTP 请求于是, 完全可以使用一段代码, 伪装成 A 应用向后端发请求, 完成相应的操作甚至可以将应用内常用的操作全部提取出来, 这样在上班的时候突然想起还没签到打卡, 直接跑一段程序就 OK, 甚至都不需要打开手机简直美滋滋, 我这样想着
以签到打卡操作为例
实际应用中并不存在签到打卡操作
2. 分析请求
使用 Fiddler 抓取请求如下:
- POST http://api.*****.com/v1/****/****/what
- // 请求头
- headers = {
- "Accept-Encoding": "gzip",
- "Accept-Language": "zh_CN",
- "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 7.0; MI 5 MIUI/8.1.25)",
- "Content-Type": "application/x-www-form-urlencoded",
- "Welove-UA": "[Device:MI5][OSV:7.0][CV:Android4.0.2][WWAN:0][zh_CN][platform:tencent][WSP:2]"
- }
- // 请求体 (表单)
- form = {
- "access_token": "562********358-2****************6",
- "app_key": "a*************4",
- "timestamp":"1522393966",
- "sig":"rTRa2PTiGiwkNVQUnSB0n2l6KrA=",
- }
使用 Postman 原样发送请求, 操作成功但一旦更改请求的参数, 服务端便会返回:
- {
- "result": 160,
- "error_msg": "sig 签名错误"
- }
操作失败!
回头看一眼请求体中的 sig 字段, 这个值
rTRa2PTiGiwkNVQUnSB0n2l6KrA=
一看就是一个用于校验的字符串, A 应用在构造完请之后, 根据 URL 和请求参数生成一个 sig 字段, 并附加到请求的参数里面, 后台接收到请求之后, 通过 sig 字段来校验请求的合法性这个设计一定程度上阻碍了我们伪装成 A 应用发请求
所以我们修改了请求体中的数据之后, 必然导致后台校验 sig 失败
如何能愉快的玩耍? 关键在于窥探 A 应用如何生成 sig 字段
3. Hack It
思路:
(1) 反编译应用, 静态分析代码, 找出生成 sig 的规则;
(2) 若静态分析又困难, 尝试动态调试 (运行时打印日志等)
3.1 反编译得到 smali
(1) 下载最新版本的 Apktook
(2) 获取 A 应用安装包, 命名为 t.apk
(3) 使用
java -jar apktool.jar d t.apk
反编译应用, 得到文件夹 t, 里面便是 A 应用的全部
文件夹 t 的目录结构如下:
Snipaste_2018-03-30_15-45-01.png
其中 smali 开头的文件夹里面, 是反编译之后的 smali 代码 (类似汇编代码)
可是 smali 代码不便于阅读, 能不能直接看到 Java 源码呢?
3.2 反编译得到 Java 源码
(1) 使用电脑上的压缩软件直接打开 t.apk,
(2) 解压出压缩包. dex 后缀的所有文件,
(3) 使用 dex2jar 工具将 dex 文件转化为 jar 文件
(4) 使用 jd-gui 工具打开 jar 文件, 即可查看源码
注: 使用 Apktool 反编译之后的文件夹 t, 可使用 Apktool 回编译成 apk 文件, 经签名之后, 可再次安装到 Android 设备上运行
3.3 定位关键代码
使用 jd-gui 打开 dex2jar 转化之后的 jar 文件, 场面大概是这个样子:
Snipaste_2018-03-30_16-01-57.png
反编译的目的是找到 A 应用生成 sig 的规则, 可即使我们得到了他的代码, 如何能在混淆之后的浩如烟海的代码之中, 找到生成 sig 的那几行呢?
在 jd-gui 中搜索 字符串 "sig", 经过层层删选, 锁定了某个名为 a 类中的 a 方法:
Snipaste_2018-03-30_16-08-09.png
其中关键的是, 判断 paramMap 中是否包含 key 为 sig 的值, 若没有, 就调用 e.a() 生成一个, 于是, sig 的生成规则, 就看 e.a() 这个方法了,
Snipaste_2018-03-30_16-08-09.png
点开一看, 事情似乎异常明朗了:
(1)e.a() 方法掉用了重载方法来生成 sig
(2) 在重载的 a 方法里面, 采用 HmacSHA1 加密算法, 密钥为 8b5b********d1f,
(3) 加密之后的内容在通过 Base64 编码, 得到最后的 sig
可加密的内容是什么呢?
加密的内容是
paramString1.doFinal
方法的参数, 即 paramArrayOfByte, 追踪一下这个参数, 看到
b(paramString1,paramString2,paramMap).getBytes()
于是又进入 b 方法:
Snipaste_2018-03-30_16-46-52.png
这个方法做的事情似乎复杂了很多, 大致是:
(1) 将 paramMap 中的数据按 key 排序, 并用 & 连接成一个字符串,
(2) 经某种处理之后将 paramString1 和 paramString2 和第一步中的字符串连接, 并返回
由前面的参数跟踪分析可知 paramString2 值是 "POST", 由此大胆猜测, paramString1 是请求地址, paramMap 是请求体
如何验证?
3.4 动态调试代码, 彻底搞清楚 sig 的生成规则
思路: 在 smali 代码找到 3.3 中关键代码对应的部分, 在关键的地方加上打印 log 的代码, 然后回编译成 apk, 重新运行程序进行操作, 便可以在日志看到我们感兴趣的内容
这里我们 log 一下 b 方法的返回值
回到 3.1 中得到的 smali 代码, 找到 a(String,String,Map) 方法
- .method public static a(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/String;
- .locals 1
- //... 省略了部分代码
- //...
- // 这里调用了 b 方法
- invoke-static {p0, p1, p2}, Lcom/xxxx/xxxx/k/e;->b(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/String;
- // 方法的返回值赋给 v0 寄存器
- move-result-object v0
- invoke-virtual {v0}, Ljava/lang/String;->getBytes()[B
- move-result-object v0
- invoke-static {p0, p1, v0}, Lcom/xxxx/xxxx/k/e;->a(Ljava/lang/String;Ljava/lang/String;[B)Ljava/lang/String;
- move-result-object v0
- return-object v0
- .end method
所以我们在这里加上一段代码打印出 v0 寄存器的值就 ok 了, 代码如下
- // 这里调用了 b 方法
- invoke-static {p0, p1, p2}, Lcom/xxxx/xxxx/k/e;->b(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/String;
- // 方法的返回值赋给 v0 寄存器
- move-result-object v0
- const-string v1, "I got sig"
- // 打印 v0
- invoke-static {v1, v0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
然后回编译:
java -jar apktool.jar b t
然后签名:
"jarsigner.exe" -keystore your_key_store -signedjar t_signed.apk t\dist\t.apk username
然后手机连接电脑, 安装签名之后的应用:
adb install t_signed.apk
然后监控手机端日志:
adb logcat | grep "I got sig"
手机运行应用, 可以看到有日志输出:
Snipaste_2018-03-30_17-30-20.png
由此得到了 b 方法的返回值,
POST&http://api.xxxxxxxxx.com/v1/app/startup&app_key=ac5f34563a4344c4&imei=861945033465836&mac=02%3A00%3A00%3A00%3A00%3A00
解码之后发现和之前的猜想一致: 该值由请求方式请求地址请求参数拼接而成
4. sig 的生成规则是什么?
现在可以梳理一下 sig 的生成规则了
(1) 获得请求方式 method,
(2) 获得请求地址请求 url,
(3) 获得请求参数表 param,
(4)param 按 key 排序, 并使用 key=value 的形式, 用 & 拼接得到字符串 paramStr,
(5) 将 method,url,paramStr 进行 url 编码, 并用 & 拼接, 得到字符串 unsig,
(6) 使用 HmacSHA1 算法, 密钥 8b5b********d1f, 对 unsig 加密, 得到字节数组 dsig,
(7)Base64 编码 dsig, 得到字符串 sig
5. 为任意请求生成 sig
又能愉快的玩耍了, 抄起 Python 写一个为任意请求生成 sig 的方法, 便于后续使用:
- from hashlib import sha1
- import hmac
- import base64
- from urllib import parse
- def sig_gen(method, url, param):
- result = method + '&'
- result = result + parse.quote(url) + '&'
- param_keys = param.keys()
- param_keys = sorted(param_keys)
- param_str = ''
- for i, key in enumerate(param_keys):
- param_str = param_str + key + '=' + str(param[key])
- if i < len(param_keys) - 1:
- param_str = param_str + '&'
- result = result + parse.quote(param_str)
- result = result.replace('/','/')
- return sig(result)
- def sig(raw):
- sign = hmac.new(b'8b5b********d1f',raw.encode('utf-8'),sha1).digest()
- return base64.b64encode(sign).decode('utf-8')
来源: http://www.jianshu.com/p/d0732c9319b2