作者: evilpan
原文鏈接: https://evilpan.com/2023/01/30/android-iptables/

本文介紹一種在 Andorid 中實現單應用、全局、優雅的抓包方法。

此文于去年端午節編寫,由于種種原因,當時藏拙并未發布。現刪除一些敏感信息后分享出來,希望對各位有所啟發。

背景

昨天在測試一個 Android APK 的時候發現使用 WiFi 的 HTTP 代理無法抓到包,在代理的日志中沒有發現任何 SSL Alert,因此可以判斷不是證書問題;另外 APP 本身仍可以正常收發數據,這說明代理設置被應用繞過了。

根據我們前一篇文章(終端應用安全之網絡流量分析)中所介紹的,遇到這種情況時就可以使用路由抓包方法,確保接管所有流量。但是因為端午放假被封印在家,且用于抓包的樹莓派放在了公司,因此只有另謀他路。

本來接著考慮裝個 DroidProxy 去試一下,但突然間靈光一閃,為什么不直接用 iptables 去修改流量呢?于是,就有了這篇小記。

iptables 101

iptables 應該大家都不會陌生,說起來這也是我入門 “黑客” 時就接觸的命令,因為我的網絡安全入門第一戰就是使用 aircrack 去破解鄰居的 WiFi 密碼。多年以前還寫過一篇Linux內核轉發技術,介紹 iptables 的常用操作,但當時年幼無知,很多概念自己并沒有完全理解。其實介紹 iptables 最好的資料就是官方的 man-pages,因此這里也就不做一個無情的翻譯機器人了,只簡單介紹一些關鍵的概念。

basic

首先是我們作為系統管理員最為關心的命令行參數,在坊間流傳的各類防火墻、WiFi 熱點、流控 shell 腳本中,充斥著各種混亂而難以理解的 iptables 命令,但實際上其命令行參數非常優雅,可以概況為以下表述:

iptables [-t table] {-A|-C|-D} chain rule-specification

rule-specification = [matches...] [target]
match = -m matchname [per-match-options]
target = -j targetname [per-target-options]

一個 table 中有多個 chain,除了內置的 chain,用戶也可以自己新建(比如 DOCKER 鏈)。常用的 table 及其包含的 chain 有以下這些:

  • filter
  • INPUT
  • FORWARD
  • OUTPUT
  • nat
  • PREROUTING
  • INPUT
  • OUTPUT
  • POSTROUTING
  • mangle
  • PREROUTING
  • OUTPUT
  • INPUT
  • FORWARD
  • POSTROUTING
  • raw
  • PREROUTING
  • OUTPUT

其中有的表比其他表包含更多的 chain,這是其定位決定的。正如其名字而言,filter 主要用于流量過濾,nat 表主要用于網絡地址轉換,mangle 表用于數據包修改,而 raw 表則用于網絡包更早期的配置。除此之外還有 security 表用于權限控制,不過用得不多。

雖然看起來各個表各司其職,但實際中也沒有強制的差異。比如 mangle 表雖然用來修改流量,但也可以用來做網絡地址轉換,filter 表也是同理。在日常中設置 iptables 規則的時候主要考慮的是數據包的時序,而這和 chain 的關系更大一些。

上面提到的這些常見 chain,不管在哪個表中,其含義都是類似的:

  • INPUT: 表示數據包從遠端發送到本地;
  • OUTPUT: 表示數據包在本地生成,并準備發送到遠端;
  • PREROUTING: 接收到數據包的第一時間,在內核進行路由之前;
  • POSTROUTNG: 表示數據包準備離開的前一刻;
  • FOWARD: 本機作為路由時正要準備轉發的時刻;

table 結合對應的 chain,網絡數據包在 iptables 中的移動路徑如下圖所示:

extensions

對于 iptables 而言重點無疑是其中的規則定義,上文提到的參數無非就是將自定義的規則加入到對應 CHAIN 之中,比如 -A 是將規則插入到鏈的末尾(append),-I 是插入到鏈的頭部(insert),-D 是刪除對應規則(delete),等等。

而規則又分為兩個部分,即數據包匹配以及匹配之后的操作,分別通過 -m-j 來指定。這其中就引入了成百的命令行參數,以至于社區還就此產生了不少段子:

Overheard: “In any team you need a tank, a healer, a damage dealer, someone with crowd control abilities and another one who knows iptables”

— Jérome Petazzoni (@jpetazzo) June 27, 2015

不過實際上社區對 iptables 的抱怨更多是在多用戶系統中規則配置沖突以及由此引發的艱難調試之旅,在沒有沖突的情況下,配置規則也是比較簡單的。定義 iptables 規則的參考主要是 iptables-extensions(8),其中定義了一系列 匹配拓展(MATCH EXTENSIONS) 以及 目標拓展(TARGET EXTENSIONS)

match

先看匹配拓展,一般我們使用 iptables 都是根據 ip 或者端口進行匹配,比如 -m tcp --dport 22。但其中也有一些比較有趣的匹配規則,比如上一篇文章中介紹過的 Android 單應用抓包方法:

$ iptables -A OUTPUT -m owner --uid-owner 1000 -j CONNMARK --set-mark 1
$ iptables -A INPUT -m connmark --mark 1 -j NFLOG --nflog-group 30 
$ iptables -A OUTPUT -m connmark --mark 1 -j NFLOG --nflog-group 30 
$ dumpcap -i nflog:30 -w uid-1000.pcap

用到了兩個匹配拓展,一個是 owner 拓展,使用 --uid-owner 參數表示創建當前數據包的應用 UID。但是這樣只能抓到外發的包,而服務器返回的包由于并不是本地進程創建的,因此沒有對應的 UID 信息,因此 owner 拓展只能應用于 OUTPUT 或者 POSTROUTING 鏈上。為了解決這個問題,上面使用了另一個拓展 connmark,用來匹配 tcp 連接的標志,這個標志是在第一條命令中的外發數據中進行設置的。

還有個值得一提的匹配拓展是 bpf,支持兩個參數,可以使用 --object-pinned 直接加載編譯后的 eBPF 代碼,也可以通過 --bytecode 直接指定字節碼。直接指定的字節碼格式類似于 tcpdump -ddd 的輸出結果,第一條是總指令數目。

例如以下 bpf 指令 (ip proto 6):

4               # number of instructions
48 0 0 9        # load byte  ip->proto
21 0 1 6        # jump equal IPPROTO_TCP
6 0 0 1         # return     pass (non-zero)
6 0 0 0         # return     fail (zero)

實際調用時候需用用逗號分隔每條指令,且不支持注釋等其他符號:

iptables -A OUTPUT -m bpf --bytecode '4,48 0 0 9,21 0 1 6,6 0 0 1,6 0 0 0' -j ACCEPT

對于其他遇到的匹配拓展,可以在官方文檔中查看其詳細用法。

target

target 表示數據包匹配之后要執行的操作,一般使用大寫表示。標準操作有 ACCEPT/DROP/RETURN 這三個,其他都定義在 target extensions 即目標拓展中。

比如我們前面提到的 CONNMARK 就是其中一個拓展,其作用是對當前鏈接進行打標,這樣 TCP 請求的返回數據也會帶上我們的標記。類似的還有 MARK 拓展,表示對當前數據包設置標志,主要用于后續 table/chain 的識別。

前面用到的另一個拓展是 NFLOG,表示 netfilter logging,規則匹配后內核會將其使用對應的日志后端進行保存,通常與 nfnetlink_log 一起使用,通過多播的方式將獲取到的數據包發送到 netlink 套接字中,從而可以讓用戶態的抓包程序獲取并進行進一步分析。

其他常用的拓展還有 SNAT/DNAT 用于修改數據包的源地址和目的地址,LOG 可以使內核 dmesg 打印匹配的數據包信息,TRACE 可以使內核打印規則信息用于調試分析等。

Android Proxy

復習完 iptables 的基礎后,我們繼續回到文章開頭的問題,有什么辦法可以在不設置代理的基礎上代理所有流量呢?

這個問題可以從兩方面去考慮,即:

  1. 如何匹配目標數據包;
  2. 匹配之后如何轉發到代理地址;

第一個問題比較簡單,我們需要匹配從本地發出的,目的端口是 80/443 的 tcp 流量,因此匹配規則可以寫為:

-p tcp -m tcp --dport 443

在不確定目標 web 服務器端口的情況下,可以將 dport 指定為 0:65535,對所有端口都進行劫持轉發;當然也可以直接不寫 match,默認就是匹配所有 tcp 包。不過可以稍微過濾一下目的地址,比如 ! -d 127.0.0.1,以免本地的 RPC 請求也被誤攔截。

或者,更優雅的方案是使用 multiport 來一次性指定多個端口:

-m multiport --dports 80,443

第二個問題,既然我們需要將流量轉發到代理工具,那么可以選擇透明代理模式,上篇文章也有提到過。因此一個最簡單的方法是使用 DNAT 修改目的地址。查閱文檔可知,DNAT 只能用在 nat 表中的 PREROUTINGOUTPUT 鏈。再根據上文中的流程圖,如果代理地址在本地,那只能使用 OUTPUT、如果是遠程地址,那么兩個鏈任選一個即可。

綜上所述,假設 HTTP 透明代理監聽在 127.0.0.1:8080,那么可以直接用以下方法設置代理并進行抓包:

iptables -t nat -A OUTPUT -p tcp ! -d 127.0.0.1 -m multiport --dports 80,443 -j DNAT --to-destination 127.0.0.1:8080

更進一步

通過這么一條 iptables 命令,配合上透明代理就可以實現全局的 HTTPS 抓包了。所以就這樣了嗎?回憶一下之前我們其實是可以通過 owner target 去進行 UID 匹配的,只不過之前是使用 NFLOG 配合 tcpdump 進行抓包。因此我們其實也可以通過類似的方式實現基于 UID 的透明代理。

轉發規則并沒有太大變化,只需要在匹配規則上新增一個約束。

iptables -t nat -A OUTPUT -p tcp ! -d 127.0.0.1 -m owner --uid-owner 2000 -m multiport --dports 80,443 -j DNAT --to-destination 127.0.0.1:8080

這樣,不需要額外的路由抓包設備,甚至不需要引入 VPN Service 等其他應用,只需要一行命令即可實現針對單個 Android 應用的全局 HTTP/HTTPS 抓包。

總結

本文主要介紹了 iptables 規則的配置方法,并且實現了一種在 Android 中全局 HTTP(S) 抓包的方案,同時借助 owner 拓展實現應用維度的進一步過濾,從而避免手機中其他應用的干擾。

相比于傳統的 HTTP 代理抓包方案,該方法的優勢是可以實現全局抓包,應用無法通過禁用代理等方法繞過;而相比于 Wireshark 等抓包方案,該方法基于透明代理,因此可以使用 BurpSuite、MITMProxy 等成熟的 HTTP/HTTPS 網絡分析工具來對流量進行快速的可視化、攔截/重放,以及腳本分析等操作,這些優勢是傳統抓包方案所無法比擬的。

參考鏈接


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2044/