Android开发部落Android安全-逆向Android开发

记一次逆向 Android 的经历

2018-03-30  本文已影响653人  王不哈

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%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')
上一篇下一篇

猜你喜欢

热点阅读