记一次逆向 Android 的经历

1,462 阅读7分钟
原文链接: mp.weixin.qq.com
戳上面的蓝字关注我们哦!

作者:王不哈链接:https://www.jianshu.com/p/d0732c9319b2声明:本文是 王不哈 原创,转发等请联系原作者授权。

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 文件,场面大概是这个样子:

反编译的目的是找到 A 应用生成 sig 的规则,可即使我们得到了他的代码,如何能在混淆之后的浩如烟海的代码之中,找到生成 sig 的那几行呢?

在 jd-gui 中搜索 字符串 "sig",经过层层删选,锁定了某个名为 a 类中的 a 方法:

其中关键的是,判断 paramMap中是否包含key为sig的值,若没有,就调用e.a()生成一个,于是,sig的生成规则,就看e.a()这个方法了,

点开一看,事情似乎异常明朗了:(1)e.a()方法掉用了重载方法来生成 sig(2)在重载的a方法里面,采用 HmacSHA1 加密算法,密钥为8b5bd1f,(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 v0const-string v1, "I got sig"//打印 v0invoke-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"

手机运行应用,可以看到有日志输出:

由此得到了 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 sha1import hmacimport base64from urllib import parsedef 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')