Skip to content

抖音数据采集教程,安卓应用层抓包总结

Notifications You must be signed in to change notification settings

afwu/20210111-180912-496

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

抖音数据采集教程,安卓应用层抓包总结

有抓包方式总结

对于App的安全分析、协议接口分析、渗透测试或者内容爬取的过程中,第一步就是首先要抓到包,如果连包都抓不到,可能分析都无法开始了。
 
为了能抓到包,无数安全研究人员使出浑身解数,我们可以按照OSI七层模型或TCP/IP四层模型,将这些方法进行粗略的分类:

可能有读者并不清楚TCP/IP四层模型,这里做个非常精简的介绍,一般而言:

  1. 我们在谈论MAC地址/ARP的时候,我们聊的就是链路层;
  2. 我们在谈论IP地址/路由器的时候,我们聊的就是网络层;
  3. 我们在谈论连接某个端口的时候,我们聊的就是传输层;
  4. 我们在谈论发送数据的内容的时候,我们聊的就是应用层;

 

  • 应用层/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地址
    • 科学上网软件、VPN可以改手机的路由表,因此可以用来抓包;
    • 可以自建路由器进行抓包,对手机完全无侵入、无感知,彻底搞定抓不到包!

      缺点是加密内容也无法还原,可以dump流量,却无法解密内容;在手机端连标准的SSL也解不开。
    • 也可以在手机上安装使用Kali Nethunter,在手机上直接跑Wireshark,接在4G流量卡上进行抓包,这种方式甚至可以抓到手机的流量卡的网卡包,应该是目前已知的唯一抓流量卡的方法。

应用层抓包通杀脚本

从以上的分析也可以看出:

1
2
1. App在开发过程中,以App自己的权限,可以用代码实现到的最底层为传输层,也就用Socket接口,进行纯二进制的收发包,此处包括Java层和Native层。
2. 除了少数开发实力雄厚甚至过剩的大厂,掌握着纯二进制收发包的传输层创新、或者自定义协议的技术之外,占绝对数量``*``*``绝大多数``*``*``的App厂商采用的还是传统的HTTP``/``SSL方案。

而且占绝对数量中绝大多数的App,其实现HTTP/SSL的方案也是非常的直白,那就是调用系统的API,或者调用更加易用的网络框架,比如访问网站的Okhttp框架,播放视频的Exoplayer,异步平滑图片滚动加载框架Glide,对于非网络库或协议等底层开发者来说,这些才应当是普罗大众安卓应用开发者的日常。
 
所以我们在对JavaSocket接口进行trace之后打调用栈,即可清晰地得出从肉眼可见的视频、到被封装成HTTP包、再到进入SSL进行加解密,再通过Socket与服务器进行通信的完整过程。
 

 
只要开发者使用了应用层框架,即无法避免的使用了系统的Socket进行了收发,如果是HTTP则直接走了Socket,没有加解密、直接是明文,将内容dump下来即可;如果走了HTTPS,那么HTTP包还要“裹上”一层SSL,通过SSL的接口进行收发,SSL则将加密后和解密前的数据走Socket与服务器进行通信,明文数据只有SSL库自己知道。
 

 
因此想要得到SSL加密前和解密后的HTTP数据的话,就要对SSL库有深入的研究,而像这种大型的、历史悠久的基础库,研究它的人是非常多的;比如谷歌就有研究员对OpenSSL的收发包接口进行了深入的研究,并对其收发包等接口使用frida进行hook,提取明文HTTP数据,最终的成品为ssl_logger项目;因为这种库一般作为互联网世界架构的基础设施,所以其应用非常广泛,这也是为何当其暴漏出“心脏滴血”漏洞时,几乎影响到所有互联网设备的原因,不管是LinuxMacos/iOS、还是安卓,使用的都是OpenSSL,刚刚我们trace到的SSLInputStream.read函数,充其量只是OpenSSL库在Java层的一个包装器罢了。
 


 
而又有来自阿里的巨佬,在使用的过程中,进一步优化了该项目的JS脚本,修复了在新版frida上的语法错误,并在原项目只支持LinuxmacOS的基础上,增加了对iOSAndroid的支持,最终的成品就是frida_ssl_logger项目
 
该项目的完成度已经非常高,其核心原理就是对SSL_readSSL_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传输到电脑上,使用hexdumppython的控制台进行输出:

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.socketWrite0java.net.SocketInputStream.socketRead0这两个API负责的,当然其实二者还有很多上层调用的接口,在选择分析的接口时,应尽量选择离native层更近的、并且在更多安卓版本上适用的,比如这两个API在安卓7、8、9、10上是通用和不变的,以降低工作量。
 
最后的任务就是与SSL_readSSL_write一样,根据收发的函数、找到收发的IP地址和端口,而正好两个API均有socket的实例域,提供了收发包的IP地址和端口信息。
 

 
最终就是取出这些信息,构造与SSL一样发给电脑即可,需要注意的是Java[B需要手动转化成JavaScriptByteArray还是略微复杂的。

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文件中找到其实现(地址),本质上也是libcsend、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


免责声明:本文档仅供学习与参考,请勿用于非法用途!否则一切后果自负。

About

抖音数据采集教程,安卓应用层抓包总结

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published