记一次逆向 Android 的经历
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
的目录结构如下:
其中
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 方法:
这个方法做的事情似乎复杂了很多,大致是:
(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%3A%2F%2Fapi.xxxxxxxxx.com%2Fv1%2Fapp%2Fstartup&app_key%3Dac5f34563a4344c4%26imei%3D861945033465836%26mac%3D02%253A00%253A00%253A00%253A00%253A00
解码之后发现和之前的猜想一致:该值由请求方式、请求地址、请求参数拼接而成。
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('/','%2F')
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')