对于App
的安全分析、协议接口分析、渗透测试或者内容爬取的过程中,第一步就是首先要抓到包,如果连包都抓不到,可能分析都无法开始了。
为了能抓到包,无数安全研究人员使出浑身解数,我们可以按照OSI七层模型或TCP/IP四层模型,将这些方法进行粗略的分类:
可能有读者并不清楚TCP/IP四层模型,这里做个非常精简的介绍,一般而言:
- 我们在谈论MAC地址/ARP的时候,我们聊的就是链路层;
- 我们在谈论IP地址/路由器的时候,我们聊的就是网络层;
- 我们在谈论连接某个端口的时候,我们聊的就是传输层;
- 我们在谈论发送数据的内容的时候,我们聊的就是应用层;
- 应用层/Application:基于中间人的HTTP(S)抓包
- 该方法继承于网页端的抓包,只不过对抗性全面强化;在设计网站时无法控制客户端,但是App确是可以被厂商全面控制的;
- 在客户端校验服务器证书的情况下,需要将抓包软件(推荐Charles)的证书置于手机根证书目录下,推荐Magisk插件Move Certificates;
- 在服务器验证客户端证书的情况下,还需要在App中dump出证书导入到Charles中,这就涉及到证书密码和证书的解密;
- App使用特定API,绕过WIFI代理进行通信→ 使用VPN将所有流量导入到Charles → App还会检测VPN,发现即断网 → 需要hook过VPN检测;
- App使用SSL pinning,只信任自己的证书 → 从数十种框架中找到hook点并绕过 → App进行了代码混淆 → 反混淆并hook绕过,而反混淆总是让人倒吸一口凉气。。。
- 由于厂商可以全面控制客户端,因此可以使用小众协议,比如WebSocket、Protobuf,甚至自己写协议,比如腾讯的JceStruct,此时除了自己分析协议字段别无他法。
- 传输层/Transport:App使用纯Socket通信
- 比如某应用的数据采用点对点纯Socket的tcp通信,此时只有dump其通信流量,分析其raw data,结合源码分析字段构成;
- 某厂商开创性地提出了自建代理长连通道的网络加速方案,App中绝大部分的请求通过CIP通道中的TCP子通道与长连服务器通信,长连服务器将收到的请求代理转发到业务服务器,对于业务来讲大大提高了效率,但是对于逆向来说却加大了抓包的难度。
也幸亏其SDK中包含了降级方案,可以hook某些关键函数实现降级到HTTP,给了安全研究员一口饭吃。 - 更有大厂已经在通讯标准演进的路线上大步快跑,在目前HTTP/2都没有普及的情况下,受益于相比于网页端而言、App客户端全面可控的优势,提前迈入HTTP/3时代,在性能优化的KPI上一骑绝尘而去,从内核、算法、传输层网络库和服务端全部自研。
面对连抓包工具都没有提供支持的kQUIC,逆向分析者只能说欲哭无泪。同样还是幸亏SDK中包含了plan B
降级方案,可以通过hook来进行降级,安全研究员续命一秒钟。
- 网络层/Network:一般而言鲜有App可以更改设备的IP地址
从以上的分析也可以看出:
1 2 |
1. App在开发过程中,以App自己的权限,可以用代码实现到的最底层为传输层,也就用Socket接口,进行纯二进制的收发包,此处包括Java层和Native层。 2. 除了少数开发实力雄厚甚至过剩的大厂,掌握着纯二进制收发包的传输层创新、或者自定义协议的技术之外,占绝对数量``*``*``绝大多数``*``*``的App厂商采用的还是传统的HTTP``/``SSL方案。 |
---|
而且占绝对数量中绝大多数的App,其实现HTTP/SSL的方案也是非常的直白,那就是调用系统的API,或者调用更加易用的网络框架,比如访问网站的Okhttp框架,播放视频的Exoplayer,异步平滑图片滚动加载框架Glide,对于非网络库或协议等底层开发者来说,这些才应当是普罗大众安卓应用开发者的日常。
所以我们在对Java
层Socket
接口进行trace
之后打调用栈,即可清晰地得出从肉眼可见的视频、到被封装成HTTP包、再到进入SSL进行加解密,再通过Socket
与服务器进行通信的完整过程。
只要开发者使用了应用层框架,即无法避免的使用了系统的Socket进行了收发,如果是HTTP
则直接走了Socket
,没有加解密、直接是明文,将内容dump下来即可;如果走了HTTPS
,那么HTTP包还要“裹上”一层SSL,通过SSL的接口进行收发,SSL
则将加密后和解密前的数据走Socket
与服务器进行通信,明文数据只有SSL
库自己知道。
因此想要得到SSL加密前和解密后的HTTP数据的话,就要对SSL库有深入的研究,而像这种大型的、历史悠久的基础库,研究它的人是非常多的;比如谷歌就有研究员对OpenSSL
的收发包接口进行了深入的研究,并对其收发包等接口使用frida进行hook,提取明文HTTP数据,最终的成品为ssl_logger项目;因为这种库一般作为互联网世界架构的基础设施,所以其应用非常广泛,这也是为何当其暴漏出“心脏滴血”漏洞时,几乎影响到所有互联网设备的原因,不管是Linux
、Macos/iOS
、还是安卓,使用的都是OpenSSL
,刚刚我们trace
到的SSLInputStream.read
函数,充其量只是OpenSSL
库在Java
层的一个包装器罢了。
而又有来自阿里的巨佬,在使用的过程中,进一步优化了该项目的JS脚本,修复了在新版frida上的语法错误,并在原项目只支持Linux
和macOS
的基础上,增加了对iOS
和Android
的支持,最终的成品就是frida_ssl_logger项目。
该项目的完成度已经非常高,其核心原理就是对SSL_read
和SSL_write
进行hook
,得到其收发包的明文数据。
[Process.platform == "darwin" ? "*libboringssl*" : "*libssl*", ["SSL_read", "SSL_write", "SSL_get_fd", "SSL_get_session", "SSL_SESSION_get_id"]], // for ios and Android
[Process.platform == "darwin" ? "*libsystem*" : "*libc*", ["getpeername", "getsockname", "ntohs", "ntohl"]]
并将明文数据使用RPC
传输到电脑上,使用hexdump
在python
的控制台进行输出:
if verbose:
src_addr = socket.inet_ntop(socket.AF_INET,
struct.pack(">I", p["src_addr"]))
dst_addr = socket.inet_ntop(socket.AF_INET,
struct.pack(">I", p["dst_addr"]))
print("SSL Session: " + p["ssl_session_id"])
print("[%s] %s:%d --> %s:%d" % (
p["function"],
src_addr,
p["src_port"],
dst_addr,
p["dst_port"]))
hexdump.hexdump(data)
或者保存至pcap
文件,以供后续进一步分析。
def log_pcap(pcap_file, ssl_session_id, function, src_addr, src_port,
dst_addr, dst_port, data):
"""Writes the captured data to a pcap file.
Args:
pcap_file: The opened pcap file.
ssl_session_id: The SSL session ID for the communication.
function: The function that was intercepted ("SSL_read" or "SSL_write").
src_addr: The source address of the logged packet.
src_port: The source port of the logged packet.
dst_addr: The destination address of the logged packet.
dst_port: The destination port of the logged packet.
data: The decrypted packet data.
"""
t = time.time()
if ssl_session_id not in ssl_sessions:
ssl_sessions[ssl_session_id] = (random.randint(0, 0xFFFFFFFF),
random.randint(0, 0xFFFFFFFF))
client_sent, server_sent = ssl_sessions[ssl_session_id]
if function == "SSL_read":
seq, ack = (server_sent, client_sent)
else:
seq, ack = (client_sent, server_sent)
for writes in (
# PCAP record (packet) header
("=I", int(t)), # Timestamp seconds
("=I", int((t * 1000000) % 1000000)), # Timestamp microseconds
("=I", 40 + len(data)), # Number of octets saved
("=i", 40 + len(data)), # Actual length of packet
# IPv4 header
(">B", 0x45), # Version and Header Length
(">B", 0), # Type of Service
(">H", 40 + len(data)), # Total Length
(">H", 0), # Identification
(">H", 0x4000), # Flags and Fragment Offset
(">B", 0xFF), # Time to Live
(">B", 6), # Protocol
(">H", 0), # Header Checksum
(">I", src_addr), # Source Address
(">I", dst_addr), # Destination Address
# TCP header
(">H", src_port), # Source Port
(">H", dst_port), # Destination Port
(">I", seq), # Sequence Number
(">I", ack), # Acknowledgment Number
(">H", 0x5018), # Header Length and Flags
(">H", 0xFFFF), # Window Size
(">H", 0), # Checksum
(">H", 0)): # Urgent Pointer
pcap_file.write(struct.pack(writes[0], writes[1]))
pcap_file.write(data)
if function == "SSL_read":
server_sent += len(data)
else:
client_sent += len(data)
ssl_sessions[ssl_session_id] = (client_sent, server_sent)
由于完成度已经相当高了,在构建安卓应用层抓包通杀脚本时,应当尽可能复用其已经实现好的“基础设施”,只要为其再补上明文数据即可,而这明文数据从哪里来?根据多轮trace
可以得知,明文数据的收发包接口,正是由java.net.SocketOutputStream.socketWrite0
和java.net.SocketInputStream.socketRead0
这两个API
负责的,当然其实二者还有很多上层调用的接口,在选择分析的接口时,应尽量选择离native
层更近的、并且在更多安卓版本上适用的,比如这两个API在安卓7、8、9、10上是通用和不变的,以降低工作量。
最后的任务就是与SSL_read
和SSL_write
一样,根据收发的函数、找到收发的IP地址和端口,而正好两个API均有socket
的实例域,提供了收发包的IP地址和端口信息。
最终就是取出这些信息,构造与SSL
一样发给电脑即可,需要注意的是Java
的[B
需要手动转化成JavaScript
的ByteArray
还是略微复杂的。
f (Java.available) {
Java.perform(function () {
Java.use("java.net.SocketOutputStream").socketWrite0.overload('java.io.FileDescriptor', '[B', 'int', 'int').implementation = function (fd, bytearry, offset, byteCount) {
var result = this.socketWrite0(fd, bytearry, offset, byteCount);
var message = {};
message["function"] = "HTTP_send";
message["ssl_session_id"] = "";
message["src_addr"] = ntohl(ipToNumber((this.socket.value.getLocalAddress().toString().split(":")[0]).split("/").pop()));
message["src_port"] = parseInt(this.socket.value.getLocalPort().toString());
message["dst_addr"] = ntohl(ipToNumber((this.socket.value.getRemoteSocketAddress().toString().split(":")[0]).split("/").pop()));
message["dst_port"] = parseInt(this.socket.value.getRemoteSocketAddress().toString().split(":").pop());
var ptr = Memory.alloc(byteCount);
for (var i = 0; i < byteCount; ++i)
Memory.writeS8(ptr.add(i), bytearry[offset + i]);
send(message, Memory.readByteArray(ptr, byteCount))
return result;
}
Java.use("java.net.SocketInputStream").socketRead0.overload('java.io.FileDescriptor', '[B', 'int', 'int', 'int').implementation = function (fd, bytearry, offset, byteCount, timeout) {
var result = this.socketRead0(fd, bytearry, offset, byteCount, timeout);
var message = {};
message["function"] = "HTTP_recv";
message["ssl_session_id"] = "";
message["src_addr"] = ntohl(ipToNumber((this.socket.value.getRemoteSocketAddress().toString().split(":")[0]).split("/").pop()));
message["src_port"] = parseInt(this.socket.value.getRemoteSocketAddress().toString().split(":").pop());
message["dst_addr"] = ntohl(ipToNumber((this.socket.value.getLocalAddress().toString().split(":")[0]).split("/").pop()));
message["dst_port"] = parseInt(this.socket.value.getLocalPort());
if (result > 0) {
var ptr = Memory.alloc(result);
for (var i = 0; i < result; ++i)
Memory.writeS8(ptr.add(i), bytearry[offset + i]);
send(message, Memory.readByteArray(ptr, result))
}
return result;
}
})
}
One more thing,虽然直接调用native层Socket的应用框架几乎没有;但是Javs层的Socket API是可以进一步下沉到C层的Socket,以支援so文件的socket抓包。以java.net.SocketOutputStream.socketWrite0
举例,其native层的实现为JNIEXPORT void JNICALL 55SocketOutputStream_socketWrite0(JNIEnv *env, jobject this,jobject fdObj,jbyteArray data,jint off, jint len)
(地址),其核心为一句话int n = NET_Send(fd, bufP + loff, llen, 0);
,进一步追踪NET_Send
可以在linux_close.cpp
文件中找到其实现(地址),本质上也是libc
的send、sendto、recv、recvfrom
这些,因此可以直接hook这些接口,捕获该进程的所有通信流量。
int NET_Read(int s, void* buf, size_t len) {
BLOCKING_IO_RETURN_INT( s, recv(s, buf, len, 0) );
}
int NET_ReadV(int s, const struct iovec * vector, int count) {
BLOCKING_IO_RETURN_INT( s, readv(s, vector, count) );
}
int NET_RecvFrom(int s, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen) {
socklen_t socklen = *fromlen;
BLOCKING_IO_RETURN_INT( s, recvfrom(s, buf, len, flags, from, &socklen) );
*fromlen = socklen;
}
int NET_Send(int s, void *msg, int len, unsigned int flags) {
BLOCKING_IO_RETURN_INT( s, send(s, msg, len, flags) );
}
int NET_WriteV(int s, const struct iovec * vector, int count) {
BLOCKING_IO_RETURN_INT( s, writev(s, vector, count) );
}
int NET_SendTo(int s, const void *msg, int len, unsigned int
flags, const struct sockaddr *to, int tolen) {
BLOCKING_IO_RETURN_INT( s, sendto(s, msg, len, flags, to, tolen) );
}
int NET_Accept(int s, struct sockaddr *addr, int *addrlen) {
socklen_t socklen = *addrlen;
BLOCKING_IO_RETURN_INT( s, accept(s, addr, &socklen) );
*addrlen = socklen;
}
int NET_Connect(int s, struct sockaddr *addr, int addrlen) {
BLOCKING_IO_RETURN_INT( s, connect(s, addr, addrlen) );
}
#ifndef USE_SELECT
int NET_Poll(struct pollfd *ufds, unsigned int nfds, int timeout) {
BLOCKING_IO_RETURN_INT( ufds[0].fd, poll(ufds, nfds, timeout) );
}
#else
int NET_Select(int s, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout) {
BLOCKING_IO_RETURN_INT( s-1,
select(s, readfds, writefds, exceptfds, timeout) );
}
只是如果hook native层的这些接口的话,会混进openssl/boringssl的经过加密的流量,届时会比较难以区分,所以其实duck不必下降到native层,Java层的通信足以覆盖99%以上的场景(这个百分比是我估计的)。
最终也就是现在的效果:r0capture:安卓应用层抓包通杀脚本,地址:https://github.com/r0ysue/r0capture
- 仅限安卓平台,测试安卓7、8、9、10 可用 ;
- 无视所有证书校验或绑定,不用考虑任何证书的事情;
- 通杀TCP/IP四层模型中的应用层中的全部协议;
- 通杀协议包括:Http,WebSocket,Ftp,Xmpp,Imap,Smtp,Protobuf等等、以及它们的SSL版本;
- 通杀所有应用层框架,包括HttpUrlConnection、Okhttp1/3/4、Retrofit/Volley等等;
用法
- Spawn 模式:
$ python3 r0capture.py -U -f com.qiyi.video
- Attach 模式,抓包内容保存成pcap文件供后续分析:
$ python3 r0capture.py -U com.qiyi.video -p iqiyi.pcap
建议使用Attach模式,从感兴趣的地方开始抓包,并且保存成pcap文件,供后续使用Wireshark进行分析。
PS:用来抓注册包,效果尤佳。
更多短视频数据实时采集接口,请查看文档: TiToData
免责声明:本文档仅供学习与参考,请勿用于非法用途!否则一切后果自负。