需求:反病毒中心捕获到一个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.3
,SYN
为1
表示这是发起连接请求,Seq
的值为0
,正常情况下这个值是一个随机值,但是这里WireShark为了方便分析,以相对偏移来显示,所以是0

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

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

三次握手到这里结束,以上的部分由下面这句代码来完成
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
,所以Seq
是74

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

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

上面就是一个精简版的APP与服务器进行Socket通信的大致过程,从上述的代码我们可以看到几个可以勾住的点:
- 创建Socket连接
- 往外发数据,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的文档,而且是熟练掌握,不然后面会很棘手
本文描述的一些方法只是做了一个大概的描述,实际场景需要根据具体情况进行调整,比如优化输出格式,筛选钩子的条件等

网友评论