Flutter 앱에 Burp를 물렸는데 HTTP history가 텅 비어 있다면, 설정을 잘못한 게 아닙니다. 구조상 안 잡히는 게 정상입니다. 실제 상용 앱을 분석하며 정리한 재현 가능한 절차입니다.
Flutter는 후자입니다. 이 구분이 진단 방향을 정합니다. 인증서 우회 도구를 아무리 붙여도 트래픽 자체가 안 오면 소용이 없습니다.
dio든 http든 결국 dart:io의 HttpClient를 씁니다. 그런데 HttpClient.findProxy의 기본 반환값은 항상 "DIRECT" 입니다.
Wi-Fi 설정의 수동 프록시는 CFNetwork(iOS)나 JVM system property(Android)를 읽는 계층에만 적용됩니다. Dart 소켓은 그 계층을 통째로 지나칩니다. 프록시를 아무리 정확히 설정해도 Dart 트래픽은 거기로 가지 않습니다.
그래서 기기 설정이 아니라 네트워크 경로에서 꺾어야 합니다.
아이폰이 처음부터 Mac을 목적지로 알고 접속하게 만드는 방식이 가장 안정적이었습니다. DNS로 유도합니다.
아이폰 ──DNS 질의──▶ Mac:53 (dnsmasq) "api.target.io = 192.168.1.14"
아이폰 ──TLS:443───▶ Mac:443 ──pf rdr──▶ Burp:8888 ──SNI──▶ 실서버
그 외 트래픽 ───────▶ 공유기 직행 (인터넷 정상)
ARP 스푸핑도, IP 포워딩도, NAT도 필요 없습니다. 탈옥도 불필요하고, 아이폰에서 바뀌는 건 DNS 한 줄뿐입니다.
dnsmasq, pf(macOS 기본), tcpdumpbrew install dnsmasq
수동 절차는 뒤에 정리했습니다. 먼저 바로 쓸 수 있는 스크립트입니다. 세 파일을 같은 디렉터리에 두면 됩니다.
domains.txtBurp로 유도할 도메인 목록입니다. 앱마다 다르니 아래 "도메인 뽑기"를 참고해 채우세요.
# 한 줄에 하나, # 은 주석
api.target.io
cdn.target.io
api.thirdparty.io
# 동작 검증용
example.com
burp-ios.sh#!/bin/bash
# ============================================================
# burp-ios.sh — 아이폰 트래픽을 Burp 로 흘리는 원클릭 스크립트
# 사용법: sudo ./burp-ios.sh start [아이폰IP]
# sudo ./burp-ios.sh stop
# sudo ./burp-ios.sh status
# ============================================================
set -uo pipefail
PORT="${BURP_PORT:-8888}"
DIR="$(cd "$(dirname "$0")" && pwd)"
DOMAINS="$DIR/domains.txt"
CONF="/tmp/burp-ios-dnsmasq.conf"
PIDF="/tmp/burp-ios-dnsmasq.pid"
SAVED="$DIR/.last-phone-ip"
DNSMASQ="$(command -v dnsmasq || echo /opt/homebrew/opt/dnsmasq/sbin/dnsmasq)"
red() { printf "\033[31m%s\033[0m\n" "$*"; }
grn() { printf "\033[32m%s\033[0m\n" "$*"; }
ylw() { printf "\033[33m%s\033[0m\n" "$*"; }
[ "$(id -u)" -eq 0 ] || { red "sudo 로 실행하세요: sudo $0 $*"; exit 1; }
UP="$(route -n get default 2>/dev/null | awk '/interface:/{print $2}')"
MACIP="$(ipconfig getifaddr "$UP" 2>/dev/null)"
[ -n "$MACIP" ] || { red "업링크 IP 를 찾을 수 없습니다 (인터페이스=$UP)"; exit 1; }
# ---------- start ----------
start() {
PHONE="${1:-}"
[ -z "$PHONE" ] && [ -f "$SAVED" ] && PHONE="$(cat "$SAVED")"
if [ -z "$PHONE" ]; then
red "아이폰 IP 가 필요합니다: sudo $0 start 192.168.1.12"
echo " 아이폰: 설정 → Wi-Fi → ⓘ → IP 주소"
exit 1
fi
echo "$PHONE" > "$SAVED"
# 1. Burp 리스너 확인
if ! lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
red "❌ $PORT 포트에 리스너가 없습니다. Burp 를 먼저 실행하세요."
echo " Proxy → Proxy settings → Add listener"
echo " Port: $PORT / Bind: All interfaces"
echo " Request handling → Support invisible proxying ✅ (필수)"
exit 1
fi
grn "✅ Burp 리스너 확인 (*:$PORT)"
# 2. dnsmasq 설정 생성
[ -x "$DNSMASQ" ] || { red "❌ dnsmasq 가 없습니다: brew install dnsmasq"; exit 1; }
{
echo "port=53"
echo "listen-address=$MACIP"
echo "bind-interfaces"
echo "no-resolv"
echo "server=1.1.1.1"
echo "server=8.8.8.8"
echo "log-queries"
grep -vE '^\s*(#|$)' "$DOMAINS" | while read -r d; do
echo "address=/$d/$MACIP"
done
} > "$CONF"
N=$(grep -c '^address=' "$CONF")
# 3. dnsmasq 기동
[ -f "$PIDF" ] && kill "$(cat "$PIDF")" 2>/dev/null
pkill -f "burp-ios-dnsmasq.conf" 2>/dev/null
"$DNSMASQ" -C "$CONF" -x "$PIDF" 2>/dev/null
sleep 1
if dig +short +time=2 +tries=1 @"$MACIP" example.com >/dev/null 2>&1; then
grn "✅ dnsmasq 기동 ($N 개 도메인 유도)"
else
red "❌ dnsmasq 응답 없음 — 53 포트 점유 확인: sudo lsof -nP -iUDP:53"
exit 1
fi
# 4. pf 규칙 ★ 목적지 IP 는 유지하고 포트만 변경 (127.0.0.1 로 하면 마샨 드롭)
[ -f /etc/pf.conf.burp-backup ] || cp /etc/pf.conf /etc/pf.conf.burp-backup
cp /etc/pf.conf.burp-backup /etc/pf.conf
cat > /etc/pf.anchors/burp <<EOF
rdr pass on $UP inet proto tcp from $PHONE to $MACIP port 443 -> $MACIP port $PORT
rdr pass on $UP inet proto tcp from $PHONE to $MACIP port 80 -> $MACIP port $PORT
EOF
awk '
/^rdr-anchor "com.apple\/\*"/ {
print; print "rdr-anchor \"burp\"";
print "load anchor \"burp\" from \"/etc/pf.anchors/burp\""; next }
{print}' /etc/pf.conf > /tmp/pf.new && mv /tmp/pf.new /etc/pf.conf
pfctl -f /etc/pf.conf 2>&1 | grep -viE 'ALTQ|Use of -f|present in the main|See /etc/pf.conf|^$' || true
pfctl -e 2>&1 | grep -viE 'ALTQ|already enabled|^$' || true
grn "✅ pf 리다이렉트 (443/80 → $MACIP:$PORT)"
echo
ylw "── 아이폰에서 설정 ──"
echo " 설정 → Wi-Fi → ⓘ → DNS 구성 → 수동"
echo " 기존 항목 모두 삭제 후 $MACIP 하나만 추가"
echo " IP 구성 = 자동, 라우터 = 공유기 그대로, HTTP 프록시 = 끔"
echo " → 설정 후 Wi-Fi 를 껐다 켜세요 (DNS 캐시 비우기)"
echo
echo " CA 미설치 시: Safari 로 http://$MACIP:$PORT 접속 → CA Certificate 다운로드"
echo " 설정 → 일반 → 정보 → 인증서 신뢰 설정 → PortSwigger CA 토글 ON (필수)"
echo
echo " DNS 질의 로그: tail -f /var/log/system.log | grep dnsmasq"
echo " 패킷 관찰 : sudo $DIR/watch.sh"
}
# ---------- stop ----------
stop() {
[ -f "$PIDF" ] && kill "$(cat "$PIDF")" 2>/dev/null && rm -f "$PIDF"
pkill -f "burp-ios-dnsmasq.conf" 2>/dev/null
[ -f /etc/pf.conf.burp-backup ] && cp /etc/pf.conf.burp-backup /etc/pf.conf
rm -f /etc/pf.anchors/burp
pfctl -f /etc/pf.conf 2>&1 | grep -viE 'ALTQ|Use of -f|present in the main|See /etc/pf.conf|^$' || true
pfctl -d 2>&1 | grep -viE 'ALTQ|not enabled|^$' || true
grn "✅ 종료 및 원복 완료"
ylw " 아이폰: 설정 → Wi-Fi → ⓘ → DNS 구성 → 자동 으로 되돌리세요"
}
# ---------- status ----------
status() {
echo "업링크 : $UP ($MACIP)"
echo -n "Burp : "; lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1 && grn "LISTEN *:$PORT" || red "없음"
echo -n "dnsmasq : "; pgrep -f "burp-ios-dnsmasq.conf" >/dev/null && grn "실행 중" || red "중지"
echo -n "DNS 응답 : "; dig +short +time=2 +tries=1 @"$MACIP" example.com 2>/dev/null | head -1
echo "pf : $(pfctl -s info 2>/dev/null | head -1)"
echo "--- 리다이렉트 규칙 ---"
pfctl -a burp -s nat -v 2>/dev/null | grep -viE 'ALTQ' | sed 's/^/ /'
echo "--- 아이폰 연결 ---"
lsof -nP -iTCP:"$PORT" 2>/dev/null | grep ESTABLISHED | sed 's/^/ /' || echo " 없음"
}
case "${1:-}" in
start) shift; start "${1:-}" ;;
stop) stop ;;
status) status ;;
*) echo "사용법: sudo $0 {start [아이폰IP] | stop | status}"; exit 1 ;;
esac
watch.sh패킷을 실시간으로 관찰합니다. 성공/실패 판정에 씁니다.
#!/bin/bash
# 아이폰 관련 패킷 실시간 관찰
# sudo ./watch.sh <아이폰IP>
PHONE="${1:-$(cat "$(dirname "$0")/.last-phone-ip" 2>/dev/null)}"
[ -z "$PHONE" ] && { echo "사용법: sudo $0 <아이폰IP>"; exit 1; }
UP=$(route -n get default | awk '/interface:/{print $2}')
echo "▶ $PHONE 의 DNS/443/8888 패킷 ($UP). Ctrl+C 로 종료."
exec tcpdump -i "$UP" -n "host $PHONE and (port 53 or port 443 or port 8888)"
chmod +x burp-ios.sh watch.sh
sudo ./burp-ios.sh start 192.168.1.12 # 아이폰 IP (다음부터 생략 가능)
sudo ./burp-ios.sh status
sudo ./burp-ios.sh stop # 완전 원복
start 하나로 Burp 리스너 확인 → dnsmasq 설정 생성·기동 → pf 규칙 적용까지 끝나고, 아이폰에서 할 설정을 출력해 줍니다.
스크립트가 뭘 하는지 알고 쓰는 게 낫습니다. 단계별로 정리합니다.
APK에서 libapp.so를 꺼내 문자열을 봅니다. --obfuscate 없이 빌드됐다면 그대로 나옵니다.
unzip -o app.apk -d out
strings -n 10 out/lib/arm64-v8a/libapp.so \
| grep -oE 'https?://[a-zA-Z0-9._-]+' | sort -u
여기서 나온 호스트가 domains.txt가 됩니다. 같은 방법으로 사용 중인 pub 패키지도 확인할 수 있습니다.
strings -n 10 libapp.so | grep -oE '^package:[a-z0-9_]+' | sort | uniq -c | sort -rn
HTTP 클라이언트가 dio인지 http인지 확인하면, 프록시가 왜 무시되는지도 미리 알 수 있습니다. 정적 분석을 먼저 하면 동적 분석 시간이 확 줄어듭니다.
ipconfig getifaddr en0 # Mac IP, 예: 192.168.1.14
아이폰 IP는 설정 → Wi-Fi → 연결된 네트워크 ⓘ. 예: 192.168.1.12
대상 도메인만 Mac IP로 응답하고, 나머지는 정상 포워딩합니다. 그래서 인터넷이 끊기지 않습니다.
port=53
listen-address=192.168.1.14
bind-interfaces
no-resolv
server=1.1.1.1
log-queries
address=/api.target.io/192.168.1.14
address=/cdn.target.io/192.168.1.14
sudo /opt/homebrew/opt/dnsmasq/sbin/dnsmasq --keep-in-foreground -C ~/burp/dnsmasq.conf
포그라운드로 두면 아이폰이 뭘 조회하는지 실시간으로 보입니다. 다른 창에서 확인:
dig +short @192.168.1.14 api.target.io # → 192.168.1.14 (유도됨)
dig +short @192.168.1.14 www.google.com # → 실제 IP (정상 포워딩)
Burp는 일반 사용자 권한이라 443에 바인딩할 수 없습니다. pf로 포트만 바꿔줍니다.
sudo tee /etc/pf.anchors/burp >/dev/null <<'RULES'
rdr pass on en0 inet proto tcp from 192.168.1.12 to 192.168.1.14 port 443 -> 192.168.1.14 port 8888
rdr pass on en0 inet proto tcp from 192.168.1.12 to 192.168.1.14 port 80 -> 192.168.1.14 port 8888
RULES
/etc/pf.conf를 백업하고, rdr-anchor "com.apple/*" 바로 아래에 두 줄을 넣습니다. pf는 앵커 순서가 엄격합니다.
sudo cp /etc/pf.conf /etc/pf.conf.bak
sudo vi /etc/pf.conf
rdr-anchor "com.apple/*"
rdr-anchor "burp" ← 추가
load anchor "burp" from "/etc/pf.anchors/burp" ← 추가
sudo pfctl -f /etc/pf.conf
sudo pfctl -e
sudo pfctl -a burp -s nat # 규칙 확인
Proxy → Proxy settings → Proxy listeners → Add
8888이 체크가 핵심입니다. 일반 프록시는 클라이언트가 CONNECT api.target.io:443으로 목적지를 알려줍니다. 그런데 투명하게 리다이렉트된 연결에는 그 과정이 없고 TLS 핸드셰이크가 곧바로 날아옵니다.
이 옵션을 켜야 Burp가 TLS의 SNI(HTTP면 Host 헤더)를 읽어 진짜 목적지를 알아냅니다. 안 켜면 Burp가 그 요청을 자기한테 온 것으로 처리해서 랜딩 페이지를 반환하고, history는 계속 비어 있습니다.
CA 설치 — Safari로 접속하면 Burp 페이지가 뜹니다.
http://192.168.1.14:8888
우측 상단 CA Certificate → 다운로드 → 설정 앱의 프로파일 설치.
그리고 반드시 이 단계까지:
설정 → 일반 → 정보 → 맨 아래 인증서 신뢰 설정 → PortSwigger CA 토글 ON
프로파일만 설치하고 이 토글을 안 켜면 전부 실패합니다. iOS 프록시 실패의 상당수가 여기서 납니다.
DNS 변경 — 설정 → Wi-Fi → ⓘ → DNS 구성 → 수동
기존 서버를 모두 지우고 192.168.1.14 하나만 추가합니다.
IP 구성 자동 (그대로)
라우터 공유기 (그대로)
DNS 구성 수동 → 192.168.1.14
HTTP 프록시 끔
마지막으로 Wi-Fi를 껐다 켜서 DNS 캐시를 비웁니다. iCloud 개인정보 보호 릴레이가 켜져 있으면 꺼야 합니다.
sudo ./watch.sh 192.168.1.12
SYN-ACK가 돌아오는지만 보면 됩니다.
.12 > .14.443: [S] SYN
.14.443 > .12: [S.] SYN-ACK ← 없으면 아래 함정 확인
.12 > .14.443: length 253 TLS ClientHello
.14.443 > .12: length 2274 Burp 인증서
.12 > .14.443: length 833 암호화된 요청 ← 인증서를 수락했다는 뜻
인증서를 받은 뒤에 요청을 보냈다면 CA 신뢰가 걸린 겁니다. 거부했다면 TLS alert 후 즉시 끊깁니다.
lsof -nP -iTCP:8888 | grep ESTABLISHED
127.0.0.1로 하면 안 됩니다❌ rdr ... to 192.168.1.14 port 443 -> 127.0.0.1 port 8888
✅ rdr ... to 192.168.1.14 port 443 -> 192.168.1.14 port 8888
127.0.0.0/8은 정의상 한 기기 안에서만 도는 주소입니다. 이 주소가 목적지인 패킷이 Wi-Fi 같은 실제 인터페이스로 들어오는 건 논리적으로 불가능합니다. 정상이라면 출발지를 위조한 공격이거나 설정 오류죠. 그래서 커널은 이런 마샨(martian) 패킷을 조용히 버립니다.
1. 아이폰 패킷이 en0로 들어옴 목적지: 192.168.1.14:443
2. pf rdr 이 목적지를 재작성 목적지: 127.0.0.1:8888
3. 커널: "루프백인데 en0로 들어왔네?"
4. 마샨 판정, 드롭. 에러 로그도 없음.
Burp는 *:8888로 리스닝하므로 Mac 자기 LAN IP로 들어와도 정상적으로 받습니다. 목적지 IP는 그대로 두고 포트만 바꾸세요.
sudo pfctl -a burp -s nat -v
여기 나오는 Packets는 규칙 매칭 횟수지 전달 성공 횟수가 아닙니다. 위 마샨 케이스에서는 2단계까지 성공했으니 카운터가 올라갑니다. 그런데 3단계에서 버려집니다.
실제 도달 여부는 lsof -i:8888이나 tcpdump로 따로 봐야 합니다. 이걸 혼동하면 "트래픽은 오는데 Burp가 이상하다"고 엉뚱한 곳을 파게 됩니다.
| 증상 | 원인 |
|---|---|
| Burp에 아무것도 안 뜸 | invisible proxying 미설정 |
| SYN만 반복, SYN-ACK 없음 | rdr 목적지가 127.0.0.1 |
| TLS 에러 | 아이폰 CA 신뢰 토글 누락 |
| DNS 질의가 안 옴 | iCloud 개인정보 보호 릴레이 ON / DNS 캐시 |
sudo ./burp-ios.sh stop
수동으로 했다면:
sudo pfctl -d
sudo cp /etc/pf.conf.bak /etc/pf.conf
# dnsmasq 창에서 Ctrl+C
아이폰: 설정 → Wi-Fi → ⓘ → DNS 구성 → 자동
이 글의 대상 앱은 기본 설정 그대로였습니다. 그럼에도 인터셉트에 시간이 걸린 건 Flutter의 구조 덕이지 앱이 방어를 해서가 아닙니다. 실제로 막고 싶다면:
IOHttpClientAdapter의 SecurityContext에 서버 인증서만 신뢰하도록 지정. 이게 있었다면 위 방법은 통하지 않습니다.--obfuscate 빌드 — 심볼이 남아 있으면 strings 한 번에 아키텍처와 엔드포인트가 전부 드러납니다.networkSecurityConfig 실제 적용 확인 — 파일만 넣고 매니페스트에서 참조를 안 하면 아무 효과가 없습니다. 실제로 그런 앱을 봤습니다. 배포 전 apktool로 한 번 확인할 가치가 있습니다.