# 一次app抓包引发的Android分析（续）

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2014-09-11T07:01:00Z
Canonical: https://wepostx.com/topics/569

> 乌云历史资料归档
>
> **原始作者：** Elegance
> **原始编号：** superkieran-wooyundrops:342
> **原始发布时间：** 2014-09-11 15:01
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

## 0x00起因

首先说明下，本文不是正统的《一次app抓包引发的Android分析记录》的续篇，只是分析的某APP和起因是一样的，故借题。然而本文所做的分析解决了《一次app抓包引发的Android分析记录》所留下的问题，故称为续。
移动应用已不像起初，burpuite代理改遍天下。交叉编译、传输加密、DEX加壳等防护方式开始再慢慢应用，我们已经很难再肆意的抓包改数据了。
文中所分析的APP也是，将加密算法交叉编译至so库中，对传输参数进行整体加密。分析arm汇编代码是一条路子，但已然令大部分人望而生畏。然而安全总是充满着各种奇思淫意，就算不走汇编，依旧能找到其它路子。
接下来通过这篇文章向各位分享下分析过程。

## 0x01整体思路

我们此次分析的目的是为了还原传输过程中的request和response，当然更重要的是要能控制request中的参数。为此我们需要了解参数是如何加密的。通过《一次app抓包引发的Android分析记录》我们知道加密函数是libGoblin.so中的e函数，但似乎通过它要还原出解密函数不是那么容易。
不过既然知道了参数是何函数进行加密，那我们只要再知道参数是什么样的格式，直接调用这个函数来加密伪造后的数据，而后抓包替换掉对应的参数不就可以了吗？
为此我采取的策略是在json数据进入加密函数之前将其截获，这样就能知道参数是以什么样的格式传递。知道了具体的参数格式，便可伪造数据抓包改包。

## 0x02解析request

首先来看下一个request，如下图参数都是加密的，本小节目的就是为了搞清楚下面这一串是什么东西。

![原文图片](/media/2016/06/5a0ea2368fcf5c509206b7edaa1c5b28)

万年不变的第一步，apktool反编译、dex2jar反编译。当然代码混淆了，不过这不影响我们分析。android发送网络请求主要采用JDK的HttpURLConnection和Apache的HttpClient，搜索跟这两个相关的函数和字段以及请求的URL。而这些特征函数和字段，通常是不会进行混淆的。Andbug动态跟踪亦可，本次通文采用静态代码分析，故如此！
通过AndroidManifest.xml了解app的结构，找到主包名等等。jd-gui查看java代码，request是POST请求，所以搜索post、HttpPost字眼看看：

![原文图片](/media/2016/06/431aad3b9b777255c561a18dcc6a0e61)

利用如上方式，反复通过搜索http请求方法及请求URL等特征缩小范围，最终定位到AbstractHttpRequest类，该类是抽象类并拥有很多有趣的方法。而CommonTask类扩展了AbstractHttpRequest类。通过阅读代码知道该类为关键类。《一次app抓包引发的Android分析记录》文中亦说明了，此处不再赘述！
而c参数被AbstractHttpRequest的chrome()处理，跟进该函数：

![原文图片](/media/2016/06/10b9085d2d6e90600328b1a762742fe3)

str为字符串常量“6000lex”和json数据一起进入NetworkParam的convertValue(),跟进：

![原文图片](/media/2016/06/f591c11c649bbd7a34fc1786b282ff94)

在convertValue()中最终调用了加密函数，因此我们在Goblin.e加密paramString1和paramString2之前将这两个参数通过log打印出来就知道请求的数据到底是什么东西了。

![原文图片](/media/2016/06/404c2a51dab3fdd319006dcb025ecc5a)

用文本编辑器打开com.xxx.net下的NetworkParam.smali定位到convertValue()函数,在Goblin.e处添加如下代码：

![原文图片](/media/2016/06/fa96824dd38c0d7348df52898205a1d4)

通过修改smali代码将加密前后的字符串打印出来，重打包签名后安装，查看logcat日志:

![原文图片](/media/2016/06/2afb837343d0de116f8aa658c2422e70)

如上图所示，现在我们已经知道了参数传递的格式了。然而日志中还出现了预料之外的数据。照上分析p1应该为常量“6000lex”，而图中却出现了时间戳。对比传输的数据（下图），发现上图第二个request-e是b参数的密文。而b的明文正是登陆信息。搜索convertValue发现b也是调用该函数加密，故导致此现象。

![原文图片](/media/2016/06/3ad1cba2819151757edaf3122848b2ce)

通过如上分析，最终确定了登陆时c和b格式分别为：

```text
c={“adid":"f854a2765b5dcc3e","cid":"C2487","gid":"5EFD7B7D-A648-2F40-9D53-D42F1BCCC468","ke":"1410276980456","ma":"","mno":"310260","model":"sdk","msg":"","nt":"burp","osVersion":"4.4_19","pid":"10010","sid":"352C4C51-F09F-C929-E03A-B5E311BA2808","t":"p_ucLogin","uid":"000000000000000","un":"","vid":"60001060"} b={"loginT":1,"paramJson":"","prenum":"","pwd":"xxxx","uname":"xxxx","userSecurity":{"communityCode":"0","imeiCode":"000000000000000","imsiCode":"310260000000000","osType":"14","stationId":"0","terminalType":"02"}}
```
由于c中的ke和b的加密因子一样。猜测服务端解密时，先通过“6000lex”解密出c，获取ke的值，再通过ke解密出b参数。
实际上上面那个猜想是正确的，而《一次app抓包引发的Android分析记录》中所做的猜想也是正确的。Goblin.d()就是解密函数。
《一次app抓包引发的Android分析记录》中所记录的：

![原文图片](/media/2016/06/0317189bc2f289109f8b97abe615f5ae)

利用Goblin.d()解密后为：

![原文图片](/media/2016/06/b6607a0b9d080157e5cf548df130c203)

至于《一次app抓包引发的Android分析记录》中为何解密会失败，下面章节会说明到！

## 0x03解析response

如下，response响应报文亦是乱七八糟的一堆。这样就算我们能改包了，也无法判断结果到底是否正确。因此在开始改包之前，必须先解密response。

![原文图片](/media/2016/06/fe9bd1b84e8d53f8ef596d2636fd5a83)

通过0x02的分析知道app是用Apache的HttpClient进行post请求。而HttpClient获取response报文是通过getEntity()函数。故直接搜索getEntity，有了0x02的分析，轻松定位到AbstractHttpRequest类的getResult方法，由于此处dex2jar反编译错误。所以直接分析smali代码。
打开AbstractHttpRequest.smali:

![原文图片](/media/2016/06/3d16b7a02f20be293fbf7dcbde5db85f)

![原文图片](/media/2016/06/c0e18dc5abb194994e43ffaadac30aa2)

getEntity()结果为v0，v0最终进入到dealWithResponse(),而dealWithResponse返回一个Object而不是String，所以继续跟进dealWithResponse():

![原文图片](/media/2016/06/51be250fe2db9de1ac810228fc6516e8)

dealWithResponse只有一个参数，故跟踪p1:

![原文图片](/media/2016/06/35d544da809d8676c28cc3143366c14b)

p1最终进行到parseProtoResponse()和buildHttpResultString()中,跟进parseProtoResponse()：

![原文图片](/media/2016/06/fdacfb5f4b3f5e8dd68222e4f16daa0c)

parseProtoResponse()中调用了Goblin中的函数，可能这个函数就是我们想要的。先放着。我们再看看buildHttpResultString()这个函数，此处直接jd-gui查看：

![原文图片](/media/2016/06/d661ab194e70574aaf4dd5b356d75e66)

buildHttpResultString()是个抽象类，具体代码在CommonTask、MultiTask、PollTask中实现。而这几个类中最终都是调用Response类的pareResponse()方法，而pareResponse()也调用了Goblin中的解密函数。parseProtoResponse()也是Response类一个方法。所以我们将解密响应报文的函数定位在这两个函数中。接下来就是验证下是否正确。
修改Response.smali中的parseProtoResponse()和pareResponse()，将解密后的结果通过日志打印出来：

![原文图片](/media/2016/06/17050b3814c2aa1f0fa525a32b88f394)

在parseProtoResponse()和pareResponse()中标记了几处，但最终只出现如上结果，故确认解密函数为pareResponse()。

## 0x04控制request

至此我们已经完全看到了request和response传输的明文内容，如下所示，一个完成的请求响应过程：

![原文图片](/media/2016/06/d8accb113b6d1c48f0e34c0472c5224d)

当然我们目的是为了测试，所以必须要能改request请求才可以。
尝试一：
既然我们知道了request的加密函数，那么在自己的APP中调用，加密完替换掉burp拦截到得数据即可。
新建一个app引用同样的Goblin类，进行加解密测试，测试代码如下：

![原文图片](/media/2016/06/dd71e54ffd56dc85b88f964251f65303)

加密可以加密，但实际上加密结果和原始的密文不一样。而解密却失败，失败的原因正和《一次app抓包引发的Android分析记录》中所做的测试一样。

![原文图片](/media/2016/06/ba09f9235214ec1233b3f83af8b4fa5a)

发生了JNI WARNING：NewStringUTF input is not valid Modified UTF-8错误，而这个错误是由于在JNI中，google修改了UTF8的标准，当正常UTF8中包含了不符合这个标准的字节时，checkJNI函数就会报这个错，导致应用崩溃。
在google中亦有关于此错误的报告：
https://code.google.com/p/android/issues/detail?id=64892
https://code.google.com/p/android/issues/detail?id=25386
然而直接将原始密文拿来解密却可以正常解码，所以问题出可能出在加密环节。折腾半天，没有发现适合我们这边的处理办法。但在测试过程中发现，在原始APP应用中通过修改smali代码可以正常加解密。难道so中还有检测环境？当然这个得查看arm汇编才知道。
既然如此，那直接改造原始app吧。
尝试二：
改造思路：在原始APP内部中拦截参数—>通过外部修改拦截到的参数—>放行参数，后续按正常流程进行
在原始app内部中拦截参数，只要在让参数再进入加密之前进入到我们控制的函数中即可。为了方便说明。我们先来看看如何通过外部修改内部拦截的参数。
借助android的广播机制，我们能实现实时的跟app进行交互。因此，也能实时的让外部跟app内部进行数据交互。新建一个myBroadcast类，代码如下：

```text
#!java public class myBroadcast extends BroadcastReceiver{ public static boolean sw = false; public static boolean swc = false; public static boolean swb = false; public static String datac = null; public static String datab = null; public static String kec = "6000lex"; // 接收广播 public void onReceive(Context context, Intent intent) { Log.i("broadcast-intent",intent.toString()); String action = intent.getAction(); if(action.equals("com.test.broadcast1")){ sw = intent.getBooleanExtra("sw", true); // 控制是否拦截 swc = intent.getBooleanExtra("swc", false);// 控制拦截c swb = intent.getBooleanExtra("swb",false); // 控制拦截b } else if(action.equals("com.test.broadcast2")){ datac = intent.getStringExtra("datac"); // 接收c datab = intent.getStringExtra("datab"); // 接收b } Log.v("receiver-data:sw:swc:swb|datac:datab",sw+":"+swc+":"+swb+"|"+datac+":"+datab); } // 延迟函数 public static void delay(){ try { Thread.currentThread(); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } // 修改函数 public static String alter(String ke,String json){ Log.v("json-data-1",ke+":"+json); while(sw){ delay(); if(ke.equals(kec)){ if(swc){ if(datac!=null){ json = datac; Log.v("json-data1-2",json); break; } } else{ break; } } else{ if(swb){ if(datab!=null){ json =datab; Log.v("json-data2-2",json); break; } } else{ break; } } } return json; } }
```
利用onReceive实时接收外部数据，利用alter函数检测外部是否发送了拦截指令。当收到拦截的指令时，调用delay()函数进行循环延时，直到接收到伪造的数据或者关闭拦截。
将myBroadcast类转化为smali代码。把myBroadcast.smali放在NetworkParam.smali同级目录下。并在AndroidManifest.xml中注册myBroadcast这个receiver，且将exproted设置为true。
即允许该receiver被外部访问。

![原文图片](/media/2016/06/6ae9f1439fbf82c7edae1c35eedc2c26)

这样我们就能在NetworkParam.smali中使用这个receiver
接下来，我们来修改NetworkParam.smali，使其接收我们的指令。在Goblin.e前后修改如下：

![原文图片](/media/2016/06/d238ef69d2ee715d98d33c232bba1aa9)

让p0进入alter()函数，修改后结果保存至v3，让v3替换p0进入到Goblin.e()中。“request-data-1”打印出原来的参数，“json-data-1”打印出进入alter函数中的参数。“request-data-2”打印出修改后的参数，“request-data-e”，打印出加密后的参数。
修改完后，重新打包、签名、安装，运行查看logcat日志：
1、 默认设置是不拦截，所以logcat日志中应该能完整得看到

```text
“request-data-1”—>“json-data-1”—>“request-data-2(不变)”—>“request-data-e”—>“response-data”
```
测试如下：

![原文图片](/media/2016/06/6fbd9fdd47ec9f43fb0ad756b8f1c778)

和预期一样
2、 设置拦截指令，即sw为true，拦截数据进入循环延迟并等待接收伪造的参数。所以logcat日志中应该只能看到“request-data-1”—>”json-data-1”,测试如下：
am命令发送广播：

![原文图片](/media/2016/06/0ad207b2ca2fcd491ef0108fd6a53ba3)

Logcat日志，如预期，数据被拦截，应用一直处于加载当中：

![原文图片](/media/2016/06/c40e140a4735bab9524065cc0a66a539)

3、发送伪造数据，按照设计。此时logcat日志中应该能看到

```text
“request-data-1”—>“json-data-1”—>“request-data-2(修改后)”—>“request-data-e”—>“response-data”
```
测试如下：
发送广播指令，拦截b参数,放行c参数：

![原文图片](/media/2016/06/7e6a270cd46eb0cdc4bcc156dd8e24ef)

Logcat日志显示如下，c(带600lex的)被放行，b(带时间戳的)被拦截：

![原文图片](/media/2016/06/3bf438eaaacb497ccde8675d3f6026dc)

am命令发送datab数据，将uname改为test222：

![原文图片](/media/2016/06/9403f209306492f2036770ef33911149)

结果如下，receiver接收到伪造的数据后，将原b参数进行修改后放行：

![原文图片](/media/2016/06/c7c576df3c2321a6bcd684fe2a24a96a)

和预期一样。

## 0x05结语

最终，我们实现了查看传输明文信息，并控制了request请求报文。即便它采用so加密。我们依旧可以尽情的测试了~

## Replies
