2024-11-30
전력 사용량 기반 IoT 피드백 퍼징
졸업 프로젝트로 전력 사용량을 피드백 신호로 쓰는 IoT 퍼저를 3인 팀으로 설계했다. 아이디어는 단순하다 — 커버리지 계측을 넣기 어려운 블랙박스 IoT 기기에서, 실행 계측 대신 기기가 소비하는 전력을 곁눈질(side-channel)해서 "지금 새로운 코드 경로가 실행됐는가?"를 판단하는 퍼징 피드백으로 삼자는 것이다.
제가 맡은 부분은 전력 데이터를 Quantization·비트맵으로 정량화해 커버리지 피드백으로 연결하는 단계였고, 결론부터 말하면 이 부분에서 벽을 만났다. 그 과정과 이유를 정리한다.
왜 전력인가
공유기 같은 IoT 기기는 자원이 제한적이라 강력한 보안 메커니즘을 넣기 어렵고, Mirai 봇넷처럼 대규모 피해로 이어지곤 한다. 취약점을 찾는 대표적 자동화 기법이 퍼징인데, 문제는 이런 기기가 대부분 소스도 심볼도 없는 블랙박스라는 점이다.
- AFL 계열의 커버리지 가이드 퍼징은 바이너리 계측이 전제이다.
- 펌웨어를 QEMU로 에뮬레이션하려 해도 주변장치·NVRAM 의존성 때문에 잘 뜨지 않는 경우가 많다.
- 그래서 실기기 대상 HTTP 퍼징은 대개 커버리지 피드백 없는 무작위(black-box) 퍼징에 머문다.
여기서 전력 소비를 커버리지의 대용(proxy)으로 쓰자는 것이 프로젝트의 가설이었다. 실행되는 코드 경로가 달라지면 CPU·주변장치의 전력 파형도 달라질 테니, 그 차이를 잡아내면 계측 없이도 "새로운 동작"을 감지할 수 있지 않겠냐는 것이다.
기존 연구(Side-Channel Aware Fuzzing, Powertrace-based Fuzzing of CAN 등)도 사이드 채널을 퍼징에 쓴 사례가 있었지만, 실제 IoT 공유기에 전력 기반 퍼징을 적용한 선례는 찾지 못했다.
먼저: 기존 HTTP 퍼저로 1-Day를 트리거해 보다
바닥부터 만들기 전에, 실기기에서 알려진 취약점(1-Day)을 실제로 트리거할 수 있는지 기존
퍼저들로 확인했다. 타깃은 TP-Link TL-WR940N의 httpd에 존재하는 CVE-2022-24355로
잡았다. 사용해 본 도구와 한계:
| 퍼저 | 한계 |
|---|---|
| wfuzz | 취약 헤더를 기본으로 안 넣음 → 명시적 헤더 지정 필요. 기본 wordlist에 트리거 경로 없음 |
| http-fuzzer | 템플릿 자동 생성이 없어 취약 헤더를 직접 템플릿에 작성해야 함 |
| httpfuzz | 헤더 자동 추가/삭제 없음 → 취약 헤더를 직접 지정해야 함 |
공통적으로 취약점을 트리거하는 URI 경로 /loginFs/passwd가 기본 wordlist에 없어서, 사용자
정의 wordlist와 헤더를 직접 넣어주지 않으면 트리거되지 않았다. 즉 "입력(무엇을 보낼지)의
질"이 병목이라는 것이 분명해졌고, 이게 우리 퍼저의 첫 설계 방향이 된다.
퍼저 설계 ① — Filtered Strings로 입력의 질을 올리기
무작위 입력 대신, 펌웨어에서 실제로 존재하는 경로를 뽑아 씨앗(seed)으로 쓰기로 했다.
httpd 바이너리를 strings로 훑어 URI 후보를 추출하고, 블랙리스트로 잡음을 걸러 wordlist를
만들었다.
import os
PATH = "~/httpd"
with open("blacklist.txt", "r") as bl:
blacklist = set(line.strip() for line in bl)
stream = os.popen('strings {} | grep -E "/[a-zA-Z0-9_/.-]+"'.format(PATH))
with open("filtered_strings.txt", "w") as f:
for line in stream:
for part in line.split():
if part.startswith("/") and all(item not in part for item in blacklist):
f.write(part + "\n")
여기서 현실적인 문제들이 튀어나왔다.
- 의미 없는 URI로 GET을 보내도
200이 떴는데, invalid 경로가/로 redirect되어 그런 것이었다. → Burp Suite로 invalid 응답을 덤프해 응답 패턴으로 유효/무효 경로를 전처리로 구분하도록 자동화했다. %s%d같은 포맷 스트링이 섞이거나 경로에*이 들어간 경우 처리./기준 앞뒤 문자열을 따로 저장해 조합 다양성을 확보.
httpd를 분석해 보니 libcgic 대신 자체 라이브러리를 쓰고, httpAliasConfAdd로 alias를 다루는
구조라는 것도 이때 파악했다. "펌웨어에서 유효한 입력을 저비용으로 뽑아내는" 방법론을
확보한 게 이 파트의 성과이다.
퍼저 설계 ② — Mutator와 템플릿 자동 생성
씨앗을 다양화하기 위해 삽입·삭제·변경 기반 mutator를 구현해 HTTP 요청의 헤더·쿠키·본문을
동적으로 변형했다. 요청 생성 자체는 http-fuzzer
(Go text/template 기반)를 토대로 하되, 기존 도구의 최대 약점이었던 템플릿 수작업을
없애기 위해 filtered strings로부터 템플릿을 자동 생성하도록 붙였다.
전체 퍼저 파이프라인은 이렇게 정리된다.
Seed ─┬─ 펌웨어 strings
├─ Burp Suite 로그
└─ 알려진 HTTP 헤더(72종)
│
Preprocess ── 경로 검증(HTTP 응답 확인) → Valid 경로 추출
│
Template 자동 생성 → Mutation / Generation
│
Monitor ── HTTP 응답 확인 → 유효 입력 저장
전력 기반 Request 탐지 (LSTM) — 여기까진 성공
전력 파형은 Monsoon High Voltage Power Monitor(HVPM) 로 수집했다. 전원 → 측정기 → 공유기 순으로 연결해 측정기가 공유기에 전력을 공급하고, 9V로 TP-Link WR840N을 정상 구동시켰다.
수집한 전력 시계열을 LSTM에 학습시켜 IDLE 상태 vs Request 발생 상태를 구분했다. 단순 RNN과 비교했을 때 LSTM 쪽이 실측–예측 파형이 훨씬 잘 겹쳤다. 핵심 아이디어는 IDLE만 학습시킨 모델의 예측 오차(MSE) 를 이상 탐지 신호로 쓰는 것이었다.
- 정상(IDLE) 예측 MSE:
0.196 × 1e-05 - 이상(Request 발생) 예측 MSE:
2.307 × 1e-05→ 약 10배 이상 급증
Request가 들어오면 오차가 튀고, IDLE로 돌아오면 오차가 다시 가라앉아 연속 이벤트 추적도 가능했다. 즉 "공유기에 요청이 들어와 뭔가를 처리했다"는 사실 자체는 전력만으로 잡아낼 수 있었다. 여기까지가 이 프로젝트에서 실제로 된 부분이다.
그리고 벽 — Quantization·비트맵으로 커버리지를 만들 수 있는가
제 담당은 그다음이었다. "Request가 들어왔다"를 넘어서, 그 전력 파형을 정량화해서 "이번 입력이 새로운 코드 경로를 실행했다"는 커버리지 신호로 바꿔야 퍼저의 피드백 루프가 완성된다. AFL의 edge coverage 비트맵을 참고해, 전력 파형을 구간별로 Quantization해서 비트맵을 만들고, 비트맵이 새로 채워지면 "커버리지 증가"로 판단하려 했다.
문제는 전력 파형의 분해능이 코드 경로 차이를 담아내기엔 부족했다는 점이다.
- IDLE과 Request는 확연히 구분됐지만, 서로 다른 Request 사이의 미세한 경로 차이는 전력 파형에서 안정적으로 분리되지 않았다. "가만히 있어도 값이 튀는" 노이즈가 신호를 덮었다.
- AFL식 비트맵을 그대로 씌워도 커버리지가 늘었는지 여부를 판단할 신뢰할 만한 경계를 잡지 못했다.
- 결정적으로 실시간 측정 API 세팅에서 시간을 크게 잃었다. Monsoon
monsoon파이썬 모듈이 USB 백엔드를 못 잡아(usb.core.NoBackendError: No backend available) Windows에서는 블루스크린까지 났고, 결국 VMware Ubuntu에서 USB Arbitration으로 우회해 겨우 붙였다. 이 삽질에서 일정이 밀렸다.
결과적으로 전력 → 커버리지 정량화는 실패했고, 퍼저에 전력 기반 피드백을 완전히 통합하지 못했다. 실패를 인정하되, 왜 안 됐는지(분해능·노이즈·계측 신뢰도) 를 분석해 개선 방향을 남기는 데 집중했다.
CVE-2022-24355 — 퍼저로 크래시는 트리거했다
피드백 루프는 못 닫았지만, 우리가 만든 퍼저(filtered strings 자동 템플릿 + mutation)로
CVE-2022-24355 크래시를 실제로 트리거하는 데는 성공했다. 이 취약점은 httpd의
httpRpmFs()가 호출하는 함수에서 발생한다. 디컴파일된 핵심 루프는 이렇다.
sVar3 = strlen(param_1);
pcVar2 = param_1 + sVar3;
do { // '.'을 만날 때까지 뒤에서 앞으로 이동
pcVar8 = pcVar2;
pcVar2 = pcVar8 + -1;
} while (*pcVar8 != '.'); // 확장자('.')가 없으면 버퍼 밖으로 계속 감
입력 경로에 .이 없으면 이 do-while이 문자열 경계를 넘어 계속 뒤로 읽어 나가고, 이후
확장자를 대문자로 복사하는 루프에서 스택 버퍼를 넘겨 크래시가 난다. 크래시 때마다 공유기가
죽어서, 스마트 플러그로 원격 재부팅을 걸어 테스트를 자동화했다.
회고 — 무엇이 됐고 무엇이 안 됐나
- 된 것: 펌웨어 문자열 기반으로 입력의 질을 끌어올리는 방법론, 템플릿 자동 생성 퍼저, LSTM 기반 Request 탐지(전력만으로 요청 발생 감지), 1-Day PoC 크래시 트리거.
- 안 된 것: 전력 파형을 코드 커버리지로 정량화하는 피드백 루프. 전력의 분해능과 계측 노이즈가 근본적인 벽이었다.
제 파트가 실패한 파트라 아쉬움이 크지만, "블랙박스 기기에서 사이드 채널을 커버리지로 쓴다"는 아이디어의 경계가 어디인지를 직접 부딪혀 확인한 경험이었다. AFL 비트맵을 그대로 가져오는 것으로는 부족하고, 전력 신호에 맞는 다른 정량화·노이즈 제거 기법이 필요하다는 게 이 프로젝트가 남긴 가장 분명한 교훈이다.
목표 기기: TP-Link TL-WR940N / TL-WR840N · 계측: Monsoon HVPM · 모델: LSTM(TensorFlow/Keras)