美文网首页
细数监控安卓应用数据包的若干种姿势

细数监控安卓应用数据包的若干种姿势

作者: CatusWoong | 来源:发表于2019-10-24 10:48 被阅读0次

需求:反病毒中心捕获到一个APT样本,需要分析出里面所有与服务器交互的数据

那么我们就需要思考了,如果我们想找到所有的数据,必然要知道安卓应用都有哪些发包的方式,所以在开发安卓应用的时候都有哪些发包的接口呢?

首先是WebView,这个控件用于访问网页,严格说起来这个并不属于我们今天要讨论的范围,因为使用WebView进行数据传输的样本我还没有遇到过,但是毕竟行为监控嘛,这里给它算上

创建一个WebView对象,简单的设置加载一个页面

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        WebView webView = findViewById(R.id.webview);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("http://www.baidu.com");
    }
}

使用Burp对其进行抓包,我们可以看到请求包如下

GET / HTTP/1.1
Host: couplee.wang
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Linux; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,en-US;q=0.8
X-Requested-With: com.wnagzihxa1n.webview
Connection: close

返回包

HTTP/1.1 200 OK
Server: GitHub.com
Content-Type: text/html; charset=utf-8
Last-Modified: Fri, 11 Oct 2019 04:56:49 GMT
ETag: W/"5da00b91-b16"
Access-Control-Allow-Origin: *
Expires: Fri, 18 Oct 2019 11:01:24 GMT
Cache-Control: max-age=600
X-Proxy-Cache: MISS
X-GitHub-Request-Id: 130E:3503:1F5A24:2A6FED:5DA9992C
Content-Length: 2838
Accept-Ranges: bytes
Date: Fri, 18 Oct 2019 10:51:24 GMT
Via: 1.1 varnish
Age: 0
Connection: close
X-Served-By: cache-ams21038-AMS
X-Cache: MISS
X-Cache-Hits: 0
X-Timer: S1571395884.456750,VS0,VE96
Vary: Accept-Encoding
X-Fastly-Request-ID: 5909fbbaded5725d2e4a7534f082dd93b039e78b

......

对于这种接口我们对其进行监控很简单,直接勾住对应的接口即可,因为WebView有两个加载页面的方法,除了上述提到的loadUrl,还有一个是postUrl,这个方法是是POST方式,使用Xposed来实现的代码如下:

XposedHelpers.findAndHookMethod("android.webkit.WebView", lpparam.classLoader, "loadUrl", String.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        String url = (String) param.args[0];
        Log.e(TAG, url);
    }
});

XposedHelpers.findAndHookMethod("android.webkit.WebView", lpparam.classLoader, "loadUrl", String.class, Map.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        String url = (String) param.args[0];
        Map data = (Map) param.args[1];
        Log.e(TAG, url + ":" + data.toString());
    }
});

XposedHelpers.findAndHookMethod("android.webkit.WebView", lpparam.classLoader, "postUrl", String.class, byte[].class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        String url = (String) param.args[0];
        String data = new String((byte[]) param.args[1]);
        Log.e(TAG, url + ":" + data);
    }
});

接下来就是今天正式的部分了,最常见的就是HTTP,安卓SDK提供了两种方式

第一种就是HttpURLConnection,官方推荐这种方式

HttpURLConnection httpURLConnection = null;
URL url = new URL(address);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("GET");

然后把设置请求参数等几个API勾住就行

XposedHelpers.findAndHookMethod("java.net.URL", lpparam.classLoader, "openConnection", new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        URL url = (URL) param.thisObject;
        Log.e(TAG, url.toString());
    }
});

XposedHelpers.findAndHookMethod("java.net.URL", lpparam.classLoader, "openConnection", java.net.Proxy.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        URL url = (URL) param.thisObject;
        Log.e(TAG, url.toString() + ":" + ((Proxy) param.args[0]).toString());
    }
});

XposedHelpers.findAndHookMethod("java.net.URLConnection", lpparam.classLoader, "setRequestProperty", String.class, String.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, (String) param.args[0] + ":" + (String) param.args[1]);
    }
});

XposedHelpers.findAndHookMethod("java.net.URLConnection", lpparam.classLoader, "addRequestProperty", String.class, String.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, (String) param.args[0] + ":" + (String) param.args[1]);
    }
});

第二种方式是HttpClient,这个相对麻烦些,我们放到之后的一篇文章里详细讲

接下来是今天的重头戏Socket,Socket连接是不能通过Burp抓到包的,但是可以通过WireShark来抓,WireShark捕获安卓包的方式很简单,只需要本机开启无线网络,让手机连接无线,然后监控这个无线网卡就行,如果WireShark里没有无线网卡接口,可以下载WinPcap再打开WireShark,就会出现无线网卡接口

如果觉得配置WireShark来抓APP的TCP包很麻烦,在安卓上还可以使用tcpdump来抓包,我们来模拟一下安卓应用与服务器使用Socket通信的过程

以下所有的网络行为请务必使用多线程,不然在主线程运行不仅会阻塞,而且会暴露你是个菜鸟

服务端

创建一个服务端程序,监听Mac本地23333端口

reader = new InputStreamReader(socket.getInputStream());
bufReader = new BufferedReader(reader);
String str = null;
StringBuffer stringBuffer = new StringBuffer();
while ((str = bufReader.readLine()) != null) {
    sb.append(str);
}
System.out.println("收到客户端数据:" + stringBuffer.toString());
socket.shutdownInput();
os = socket.getOutputStream();
os.write("Here is Server.".getBytes());
os.flush();
socket.shutdownOutput();

客户端

创建一个Socket对象,往外写一段数据

socket = new Socket(address, port);
OutputStream os = socket.getOutputStream();
os.write(msg.getBytes());
os.flush();
socket.shutdownOutput();

接下来等待服务端回传数据,收到回传数据就发送给Handler处理,我们这里就直接让它日志输出就行,也可以不处理

InputStream is = socket.getInputStream();
reader = new InputStreamReader(is);
bufReader = new BufferedReader(reader);
String s = null;
final StringBuffer sb = new StringBuffer();
while ((s = bufReader.readLine()) != null) {
    sb.append(s);
}
sendMsg(0, sb.toString());

准备好服务端和客户端两个程序之后,我们开始准备抓包环境,将tcpdump发送到手机上并给执行权限,不带参数直接执行,运行我们的客户端就可以捕获到TCP交互的数据包

➜  ~ adb push tcpdump /data/local/tmp
tcpdump: 1 file pushed. 7.8 MB/s (2025444 bytes in 0.247s)

shell@hammerhead:/data/local/tmp $ su
root@hammerhead:/data/local/tmp # chmod 777 tcpdump
root@hammerhead:/data/local/tmp # ./tcpdump                                    
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on wlan0, link-type EN10MB (Ethernet), capture size 262144 bytes
02:55:38.590653 IP 172.27.35.2.51503 > 172.27.35.3.23333: Flags [S], seq 246093200, win 65535, options [mss 1460,sackOK,TS val 6339521 ecr 0,nop,wscale 6], length 0
02:55:39.404075 IP 172.27.35.3.23333 > 172.27.35.2.51503: Flags [S.], seq 3736934881, ack 246093201, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 776168464 ecr 6339521,sackOK,eol], length 0
02:55:39.404201 IP 172.27.35.2.51503 > 172.27.35.3.23333: Flags [.], ack 1, win 1369, options [nop,nop,TS val 6339602 ecr 776168464], length 0
02:55:39.405824 IP 172.27.35.2.51503 > 172.27.35.3.23333: Flags [P.], seq 1:21, ack 1, win 1369, options [nop,nop,TS val 6339602 ecr 776168464], length 20
02:55:39.405930 IP 172.27.35.2.51503 > 172.27.35.3.23333: Flags [F.], seq 21, ack 1, win 1369, options [nop,nop,TS val 6339602 ecr 776168464], length 0
02:55:39.416261 IP 172.27.35.3.23333 > 172.27.35.2.51503: Flags [.], ack 1, win 2058, options [nop,nop,TS val 776168669 ecr 6339602], length 0
02:55:39.416371 IP 172.27.35.3.23333 > 172.27.35.2.51503: Flags [.], ack 21, win 2058, options [nop,nop,TS val 776168669 ecr 6339602], length 0
02:55:39.418701 IP 172.27.35.3.23333 > 172.27.35.2.51503: Flags [.], ack 22, win 2058, options [nop,nop,TS val 776168673 ecr 6339602], length 0
02:55:39.418799 IP 172.27.35.3.23333 > 172.27.35.2.51503: Flags [P.], seq 1:74, ack 22, win 2058, options [nop,nop,TS val 776168673 ecr 6339602], length 73
02:55:39.418851 IP 172.27.35.3.23333 > 172.27.35.2.51503: Flags [F.], seq 74, ack 22, win 2058, options [nop,nop,TS val 776168673 ecr 6339602], length 0
02:55:39.419119 IP 172.27.35.2.51503 > 172.27.35.3.23333: Flags [.], ack 74, win 1369, options [nop,nop,TS val 6339604 ecr 776168673], length 0
02:55:39.419298 IP 172.27.35.2.51503 > 172.27.35.3.23333: Flags [.], ack 75, win 1369, options [nop,nop,TS val 6339604 ecr 776168673], length 0

那么我们使用参数抓包并把数据包写到指定文件,这里如果没有权限导出到电脑,需要给777权限

root@hammerhead:/data/local/tmp # ./tcpdump -i any -p -vv -s 0 -w capture.pcap  
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
12 packets captured
12 packets received by filter
0 packets dropped by kernel
root@hammerhead:/data/local/tmp # chmod 777 capture.pcap                       

➜  ~ adb pull /data/local/tmp/capture.pcap .      
/data/local/tmp/capture.pcap: 1 file pulled. 0.3 MB/s (1145 bytes in 0.004s)

使用WireShark打开捕获到的数据包,这里我们结合数据包来看下TCP的相关协议及通讯过程

TCP里有个说法叫作三次握手,用于创建连接,第一次握手 如下,由我们的客户端172.27.35.2发往服务端172.27.35.3SYN1表示这是发起连接请求,Seq的值为0,正常情况下这个值是一个随机值,但是这里WireShark为了方便分析,以相对偏移来显示,所以是0

服务器收到连接请求,进行第二次握手,注意Seq也是0,但是这里的0和上面那个包的0不是一个意思,这个表示服务端产生的随机值的偏移,然后ACK被设置为1,计算方式是客户端发过来的包里的Seq字段加一,表示接下来希望收到客戶端哪个序列号的包

客户端收到服务端的返回包,进行第三次握手,告诉服务端我这里已经收到消息,准备开始进行数据传输,Seq1表示客户端当前发包的序列号,Ack1表示希望收到对方的包序列号

三次握手到这里结束,以上的部分由下面这句代码来完成

socket = new Socket(address, port);

接下来我们进行数据发送

OutputStream os = socket.getOutputStream();
os.write(msg.getBytes());
os.flush();

这里我传输了一句Here is wnagzihxa1n.,可以看到Data字段里包含了这句话

当我们发送完数据后,使用方法shutdownOutput()shutdownInput()是结束单向连接,比如我这里使用了shutdownOutput(),就是结束了客户端往服务端的输出流,服务端的输出流不受影响,但是此时Socket并没有关闭连接,是处于连接状态的,如果使用的是outputStream.close(),那就是直接关闭Socket连接了,所以对这部分不熟悉的同学可以注意下

socket.shutdownOutput();

FIN字段为1,表示断开,此时客户端主动断开,会开始四次挥手

后面服务器开始往客户端发数据,客户端等待接收数据代码如下

InputStream is = socket.getInputStream();
reader = new InputStreamReader(is);
bufReader = new BufferedReader(reader);
String str = null;
final StringBuffer stringBuffer = new StringBuffer();
while ((str = bufReader.readLine()) != null) {
    stringBuffer.append(s);
}

既然服务端收到了客户端的包,那么肯定是要发一个ACK确认回去的,上面客户端发的包数据长度是20,所以Seq变成了21

然后响应用户发的FIN包,这个标志和SYN一样

发完ACK包后,开始发送服务端往客户端的数据

发完包后,客户端收到数据,给服务端发一个ACK,因为上一个包数据长度是73,所以Seq74

最后服务端发出关闭输出流的包

整个挥手由客户端发出最后一个ACK结束

上面就是一个精简版的APP与服务器进行Socket通信的大致过程,从上述的代码我们可以看到几个可以勾住的点:

  1. 创建Socket连接
  2. 往外发数据,OutputStream.write()

使用代码就是如下所示:

XposedHelpers.findAndHookConstructor(InetSocketAddress.class, String.class, int.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, param.args[0] + ":" + param.args[1]);
    }
});

XposedHelpers.findAndHookMethod("java.io.OutputStream", lpparam.classLoader, "write", byte[].class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        byte[] data = (byte[]) param.args[0];
        Log.e(TAG, new String(data));
    }
});

为什么只写一个OutputStream呢?因为只有一个参数的会补充两个参数来调用下面这个API

public void write(byte b[]) throws IOException {
    write(b, 0, b.length);
}

在安卓开发里,其实Socket还可以使用另外两个API来创建连接,上面是直接使用构造函数,下面这种方式是先创建Socket对象,然后再进行连接

socket.connect(new InetSocketAddress(address, port));
socket.connect(new InetSocketAddress(address, port), 6000);

而第一个API,本质上也是补充了第二个参数,再去调用第二个API,它的实现代码如下:

public void connect(SocketAddress endpoint) throws IOException {
    connect(endpoint, 0);
}

那么这一个API的钩子对照着写就行

XposedHelpers.findAndHookMethod("java.net.Socket", lpparam.classLoader, "connect", SocketAddress.class, int.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, ((SocketAddress) param.args[0]).toString());
    }
});

但是大家可以思考一下,这种方式的缺点是什么?

我们在简单的环境下,按照上面的方式是可以正常打印数据的,但是如果在一个SDK里,将这个Socket对象封装了起来并全局存储,在调用方法OutputStream.write()前,使用了IO进行文件的读写,这也是会被勾住打印出来的,从而造成干扰,这个问题的解决方法需要从系统底层来思考,之后我会单独写一篇文章来分析

还有一种是SocketChannel

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(addr, port));
ByteBuffer byteBuffer = ByteBuffer.allocate(0x20);
socketChannel.read(byteBuffer);
socketChannel.close();

勾住其连接Socket的代码如下:

XposedHelpers.findAndHookMethod("java.nio.channels.SocketChannel", lpparam.classLoader, "open", InetSocketAddress.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, ((InetSocketAddress) param.args[0]).toString() + ":" + param.args[1]);
    }
});

除了主动连接别人,APP也可以当服务端,开个端口等着数据传过来

ServerSocket serverSocket = new ServerSocket();

这个API有四种初始化方式

public ServerSocket() throws IOException
public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

我们以最后一个构造函数为例来实现钩子,如果构造函数未传入端口等信息,那么就需要调用bind()绑定端口

XposedHelpers.findAndHookConstructor(ServerSocket.class, int.class, int.class, InetAddress.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, ((InetAddress) param.args[0]).toString());
    }
});

XposedHelpers.findAndHookMethod("java.net.ServerSocket", lpparam.classLoader, "bind", SocketAddress.class, int.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, ((SocketAddress) param.args[0]).toString());
    }
});

最后就剩下UDP了,这个相对简单的多

XposedHelpers.findAndHookConstructor(DatagramSocket.class, SocketAddress.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, ((SocketAddress) param.args[0]).toString());
    }
});

XposedHelpers.findAndHookMethod("java.net.DatagramSocket", lpparam.classLoader, "createSocket", int.class, InetAddress.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, ((InetAddress) param.args[1]).toString() + ":" + param.args[0]);
    }
});

XposedHelpers.findAndHookMethod("java.net.DatagramSocket", lpparam.classLoader, "bind", SocketAddress.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Log.e(TAG, ((SocketAddress) param.args[0]).toString());
    }
});

XposedHelpers.findAndHookMethod("java.net.DatagramSocket", lpparam.classLoader, "send", DatagramPacket.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        DatagramPacket datagramPacket = (DatagramPacket) param.args[0];
        Log.e(TAG, datagramPacket.getAddress() + ":" + datagramPacket.getPort() + " - " + new String(datagramPacket.getData()));
    }
});

所以要监控一个病毒所有的网路行为就是如上的方式,但是你会遇到很多奇奇怪怪的问题,其实病毒还好,一般来说逻辑都不复杂,所有的代码都是开发者自己写的,很少碰到那种用了大量的第三方库来实现病毒功能的

难办的是那种大型的APP,功能巨复杂,揉了几十个SDK在内部的那种,比如我现在要分析某IM应用,我想跟踪Socket创建,我会发现它是在一个第三方SDK里进行创建的,这就需要我们掌握这个SDK的文档,而且是熟练掌握,不然后面会很棘手

本文描述的一些方法只是做了一个大概的描述,实际场景需要根据具体情况进行调整,比如优化输出格式,筛选钩子的条件等

欢迎大家关注我的公众号:大土豆的菜栏

相关文章

网友评论

      本文标题:细数监控安卓应用数据包的若干种姿势

      本文链接:https://www.haomeiwen.com/subject/xqjrvctx.html