Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
blackbox-fuzzing — TL-WR902AC 라우터를 예시로 사용한 IoT 기기 퍼징 | Kitploit
도구/GitHubGitHub/otsmr/blackbox-fuzzing
IoT SecurityVulnerability AnalysisExploitationReverse EngineeringFuzzingBinary AnalysisPapers & ResearchLearning & EducationFirmware Analysis
GitHubotsmr/blackbox-fuzzing

blackbox-fuzzing

TL-WR902AC 라우터를 예시로 사용한 IoT 기기 퍼징

132179개월 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
저장소 보기
웹사이트

라우터 TL-WR902AC를 예로 한 IoT 기기 블랙박스 퍼징

이 문서는 제 학기 논문의 HTML 버전으로, PDF는 여기에서 내려받을 수 있다.

서론

퍼징은 소프트웨어에서 버그를 찾는 "가장 효과적인 방법 중 하나"가 되었다. 이러한 주장이나 이와 유사한 주장으로 많은 최신 퍼징 관련 논문이 시작된다 [google-scholar]. "Internet of Vulnerable Things"라는 주제에 대한 지난 학기 논문의 주요 목표는 메모리 관련 버그를 찾은 다음 이 취약점에 대한 익스플로잇을 작성하는 것이었다. 우리는 펌웨어를 리버싱하여 취약점을 찾을 수 있었지만, 메모리 관련 버그는 발견되지 않았다. 바이너리를 수작업으로 리버싱하여 버퍼 오버플로를 찾는 것은 시간이 많이 걸릴 뿐만 아니라 많은 경험도 필요하다. 한편 퍼징은 이러한 메모리 관련 취약점을 찾는 "가장 효과적인 방법"이 되는 것을 목표로 한다. 예를 들어, Google은 오픈소스 소프트웨어를 지속적으로 퍼징하는 OSS-Fuzz를 도입했으며, 이미 1,000개 프로젝트에서 10,000개 이상의 취약점을 발견했다 [oss-fuzz].

이번 학기 논문의 목표는 다시 메모리 관련 취약점을 찾는 것이지만, 이번에는 퍼징을 사용하는 것이다. 목표 취약점은 관리자 자격 증명을 알지 못해도 네트워크를 통해 익스플로잇할 수 있어야 한다. 이 논문은 이 목표를 달성하는 방법을 설명한다. 이를 위해 이 논문은 두 부분으로 나뉜다. 첫 번째 부분은 유력한 대상을 찾는 방법, 사용할 수 있는 도구, 그리고 좋은 퍼징 대상이 무엇으로 구성되어야 하는지에 초점을 맞춘다. 두 번째 부분에서는 바이너리의 특정 함수를 퍼징할 수 있는 하니스를 개발하고 디버깅하는 방법을 설명한다. 그런 다음 개발된 하니스는 AFL++에서 대상 함수를 퍼징하는 데 사용된다. 다음에서는 간단한 배경을 소개하고 IoT 기기 퍼징에 관한 현재 최신 기술 수준이 어떠한지 설명한다.

이 학기 논문과 관련하여 생성된 모든 파일은 GitHub에 전체 공개되어 있으며 다음 URL에서 접근할 수 있다: otsmr/blackbox-fuzzing.

최신 기술 동향

IoT 기기를 퍼징하는 것은 오픈소스 프로젝트를 퍼징하는 것만큼 쉽지 않다. 소스 코드가 독점인 경우가 많아, 최상의 퍼징 성능을 위해 소스 코드를 계측하는 그레이박스 퍼징이 불가능하다 [afl-persistent]. 또한 CPU 아키텍처가 퍼저에 의해 기본적으로 지원되지 않는 경우가 많아 QEMU [qemu]와 같은 에뮬레이터가 필요하며, 이는 퍼징 속도도 저하시킨다 [afl-persistent]. 또 다른 문제는 하드웨어 주변기기로, 이는 일반적인 접근 방식의 개발을 어렵게 만든다. "Embedded Fuzzing: A Review of Challenges, Tools, and Solutions" [embedded-fuzzing] 논문은 하드웨어 기반 임베디드 퍼징과 같은 다양한 퍼징 전략에 대한 개요를 제공한다. 이러한 전략 대부분은 대상 프로그램의 소스 코드를 필요로 한다. 예를 들어 AFL과 같은 퍼저의 소스 코드를 ARM 기반 IoT 기기로 포팅하여 IoT 하드웨어에서 퍼저를 실행하는 경우가 있다. 기기 하드웨어에서 퍼저를 실행하는 것도 성능 문제가 있는데, IoT 기기는 일반 데스크톱 CPU보다 느린 저사양 CPU를 갖는 경우가 많기 때문이다. 이 논문에서 제시하는 또 다른 접근 방식은 에뮬레이션 기반 임베디드 퍼징이다. 단일 대상 프로그램을 에뮬레이터에서 실행하여 커버리지 기반 퍼징을 수행하거나 전체 시스템을 에뮬레이터에서 실행하는 방식이다.

위에서 언급한 접근 방식은 모두 에뮬레이터를 사용하거나 소스 코드를 계측하여 바이너리를 직접 대상으로 한다. 이러한 접근 방식은 단일 IoT 기기를 위해 특별히 제작되어야 하는 퍼징 환경이 필요하며 일반화하기 어렵다. 이를 위해 연구자들은 IoTFuzzer라는 프로그램을 만들었는데, 이는 "펌웨어 이미지에 대한 접근 없이 메모리 손상 취약점을 찾는 것 [iotfuzzer]."을 목표로 하는 자동화된 퍼징 프레임워크를 지향한다. IoTFuzzers는 대부분의 IoT 기기에 이를 제어하는 모바일 앱이 있고, 그러한 앱에는 기기와 통신하는 데 사용되는 프로토콜에 대한 정보가 포함되어 있다는 관찰에 기반한다. 그런 다음 이 프로그램은 프로그램별 로직을 식별하고 재사용하여 테스트 케이스를 변형함으로써 IoT 대상을 효과적으로 테스트한다 [iotfuzzer].

배경

하니스

하니스는 퍼저가 제공한 입력을 처리하는 일련의 API 호출을 설명한다. 하니스가 필요 없는 경우가 많은 일반 애플리케이션과 달리, 재사용 가능한 함수를 구현하는 라이브러리는 올바른 매개변수와 올바른 순서로 호출되어야 하므로 여러 공유 함수 호출 간의 상태가 유지될 수 있다. 상태 머신을 구축하지 않고 라이브러리를 무작위로 퍼징하는 것은 성공할 가능성이 낮으며, 반대로 라이브러리 종속성이 적용되지 않으면 많은 오탐 크래시가 발생하게 된다. 예를 들어 퍼저가 버퍼 크기 검사를 건너뛰어 가짜 버퍼 오버플로가 발생하는 경우가 이에 해당한다.

이 논문에서는 일반 애플리케이션을 퍼징하지만, 소켓 사용과 멀티스레딩의 하드웨어 종속성 때문에 이들에 대해서도 하니스를 만들어야 한다. 하니스는 바이너리의 컨텍스트에서 로드되며 대상 프로그램의 내부 함수를 호출할 수 있다. 이에 대한 예는 코드 10에서 확인할 수 있다.

코퍼스

"corpus"라는 용어는 유효한 입력 샘플 또는 테스트 케이스를 설명하며, 퍼징 과정에서 새로운 입력 데이터를 생성하기 위한 기본 참조 자료 역할을 한다. 코드 10에서 이는 예를 들어 HTTP 요청이 될 수 있다. 그런 다음 퍼저는 이 코퍼스를 활용하여 변형되거나 다양화된 테스트 케이스를 생성하며, 다양한 입력 시나리오를 탐색함으로써 소프트웨어 취약점 탐지를 돕는다.

유력한 대상 찾기

블랙박스 퍼징에서 가장 시간이 많이 걸리는 부분은 펌웨어에서 잠재적으로 취약한 함수를 찾는 것이다. 첫 번째 단계는 예를 들어 네트워크를 통해 접근 가능하거나, 안전하지 않은 함수를 사용하거나, 버퍼 오버플로 보호 기능인 스택 카나리와 같은 보안 기능이 활성화되어 있지 않은 흥미로운 바이너리를 찾는 것이다. 지난 논문 ([iovt])에서 이미 대상 라우터에서 펌웨어를 추출하는 방법과 잠재적으로 위험한 바이너리를 찾는 방법을 설명했다. 이를 위해 EMBA [emba] 도구가 사용되었다. EMBA는 펌웨어에서 발견된 모든 바이너리를 strcpy와 같은 안전하지 않은 함수의 수, 네트워크 접근, 그리고 스택 카나리나 NX-Bit와 같은 보안 보호 기능을 기준으로 순위를 매긴다. 이러한 요소들은 버퍼 오버플로를 익스플로잇할 때 중요해지며, 코드 1에서 확인할 수 있다.

```txt [+] STRCPY - top 10 results: 235 : libcmm.so : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | 77 : wscd : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | Networking | [snip] 28 : httpd : common linux file: yes | RELRO | No Canary | NX enabled | No Symbols | Networking | 27 : cli : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | ```

코드 1: 함수 strcpy의 안전하지 않은 사용에 대한 EMBA 결과.

본 논문의 목표는 관리자 자격 증명 없이 네트워크를 통해 악용할 수 있는 메모리 취약점을 찾는 것이므로, 취약한 함수는 네트워크를 통해 호출 가능해야 하며 제공된 사용자 입력과 직접 상호작용해야 합니다. 그러나 네트워크 상호작용이 있다고 해서 해당 바이너리가 네트워크를 통해 직접 접근 가능하다는 의미는 아닙니다. 어떤 바이너리가 수신 대기 중인지 확인하려면 [iovt]에서 이미 구축한 UART 루트 셸을 사용할 수 있습니다.

```txt ~ # netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:20002 0.0.0.0:* LISTEN 1045/tmpd tcp 0 0 0.0.0.0:1900 0.0.0.0:* LISTEN 1034/upnpd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1027/httpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1224/dropbear udp 0 0 0.0.0.0:20002 0.0.0.0:* 1048/tdpd [...] ```

코드 2: UART 루트 셸을 사용하여 netstat 실행

바이너리 리버싱

가장 유망해 보이는 첫 번째 바이너리는 wscd입니다. 이 바이너리는 (libcmm.so 라이브러리를 제외하고) 가장 많은 안전하지 않은 strcpy 호출과 네트워크 상호작용을 가지며, wscd의 경우 이는 UPnP 장치에 연결하고 특정 포트에서 수신 대기하지 않음을 의미합니다. 나중에 살펴보겠지만, 퍼즈(fuzz)하기 쉬운 함수가 있어 이 바이너리가 이 논문에서 일반적인 절차를 설명하는 예제로 선택된 이유입니다. 리버싱 전에 UART 루트 셸을 사용하여 바이너리가 실행 중인지, 어떻게 시작되었는지 확인할 수 있습니다.

```txt $ ps PID USER VSZ STAT COMMAND 962 admin 1096 S wscd -i ra0 -m 1 -w /var/tmp/wsc_upnp/ 1018 admin 1080 S wscd_5G -i rai0 -m 1 -w /var/tmp/wsc_upnp_5G/ ```

코드 3: ps 명령을 사용하여 실행 중인 모든 프로그램 표시.

ps를 사용하면 바이너리가 실행 중인지뿐만 아니라 인수가 무엇인지도 확인할 수 있으며, 이는 잠재적 함수가 실제로 호출되는지 검증하는 데 중요합니다. 이러한 인수의 의미는 인수 없이 바이너리를 호출할 때 표시되는 CLI 도움말에서 확인할 수 있습니다.

```txt $ chroot root /qemu-mipsel-static /usr/bin/wscd Usage: wscd [-i infName] [-a ipaddress] [-p port] [-f descDoc] [-w webRootDir] -m UPnPOpMode -D [-d debugLevel] -h -i: Interface name this daemon will run wsc protocol(if not set, will use the default interface name - ra0) e.g.: ra0 -w: Filesystem path where descDoc and web files related to the device are stored e.g.: /etc/xml/ -m: UPnP system operation mode 1: Enable UPnP Device service(Support Enrolle or Proxy functions) 2: Enable UPnP Control Point service(Support Registratr function) 3: Enable both UPnP device service and Control Point services. [...] ```

코드 4: wscd 바이너리의 옵션.

코드 4에서 볼 수 있듯이 wscd는 "Enabled UPnP Device service"로 시작되는데, 이는 유망해 보입니다. 바이너리가 실제로 라우터에서 실행 중인지 확인한 후에는 Ghidra를 사용하여 바이너리를 분석하고 의심스러운 함수를 검색할 수 있습니다. 퍼징을 위해서는 파싱 함수가 특히 흥미로운데, 일반적으로 복잡하고, 파싱되는 입력에는 TCP 패킷이 페이로드의 길이를 포함하는 것처럼 포함된 데이터의 길이 필드가 있는 경우가 많기 때문입니다.

그림 1: Ghidra를 사용하여 파싱 함수를 검색.

파싱 함수의 또 다른 장점은 코드의 다른 부분과 상호작용하거나 네트워크를 통한 사용자 상호작용이 없는 경우가 많다는 것입니다. 따라서 파싱 함수는 바이너리를 수정하거나 다른 함수를 덮어쓰지 않고도 입력과 함께 직접 호출할 수 있으므로 퍼징이 가능합니다.

함수 퍼징을 시작하기 전에 해당 함수가 실제로 트리거되는지 확인해야 합니다. 함수는 사용자 제어 입력으로 호출될 때만 의미가 있기 때문입니다. 이를 위해 Ghidra를 사용하여 대상 함수에 대한 참조를 검색할 수 있습니다. parser_parse 함수의 경우 여러 가지 방법이 있습니다. 프로그램이 어떻게 시작되는지 알고 있기 때문에 호출을 코드 5에 표시된 단일 함수 호출 트리로 줄일 수 있습니다.

```c main() if ((WscUPnPOpMode & 1) != 0) // Argument -m 1 WscUPnPDevStart() UpnpDownloadXmlDoc() -> my_http_Download() -> http_Download() if (http_MakeMessage()) http_RequestAndResponse() http_RecvMessage() ```

코드 5: 함수 parser_parse의 호출 트리

대상 함수를 찾은 후에는 이제 함수를 퍼징하기 위한 퍼징 설정을 만들 수 있다. 이는 다음 부분에서 설명한다. 그러나 먼저 다른 유력한 함수들을 소개한다.

기타 잠재적 취약 함수들

본 논문에서는 여러 잠재적 바이너리를 수동으로 분석하여 의심스러운 함수를 찾았다. 다음은 발견된 다른 가능한 대상들에 대한 간략한 요약이다.

httpd 바이너리는 관리자 웹 인터페이스의 백엔드이다. 이 바이너리는 네트워크의 80번 포트에서 접근할 수 있다. httpd에서 흥미로운 함수 중 하나는 httpd_parser_main 함수이다. Ghidra를 사용하여 파서 구현을 훑어보는 동안 몇 가지 서로 다른 의심스러운 코드 부분을 식별할 수 있었다. 의심스러운 부분 중 하나는 Content-Type의 파싱이다. 다음에서 기본 HTTP 요청을 찾을 수 있다.```txt POST / HTTP/1.1\r\n Content-Type: multipart/form-data; boundary=X;\r\n Host: example.com\r\n \r\n \r\n DATA\r\n

root@kitploit:~
다음은 `httpd_parser_main` 함수의 코드 조각으로, 사용자가 제공한 HTTP 요청에서
`Content-Type`을 파싱합니다.


<div id="c6"></div>```c
// user_input_ptr points to
//  "Content-Type: multipart/form-data; boundary=X;\r\nHost: example.com\r\n..."
cursor = strstr(user_input_ptr,"multipart/form-data");

if (user_input_ptr == cursor) {
 cursor = strstr(user_input_ptr,"boundary=");
 user_input_ptr = cursor + 9;

 // user_input_ptr points now to "X;\r\nHost: example.com\r\n..."

 if (cursor != (char *)0x0) {

  do {
    while (cursor = user_input_ptr, *cursor == " ") {
      user_input_ptr = cursor + 1;
    }
    user_input_ptr = cursor + 1;
  } while (*cursor == "\t");

  // cursor points now to "X;\r\nHost: example.com\r\n..."

  // strchr returns a pointer to the first occurrence of ";" in the user request.
  // If ";" is not found, the function returns a null pointer.
  user_input_ptr = strchr(cursor, ";");
  if (user_input_ptr != (char *)0x0) {
    // The character ";" is replaced by an null byte to terminate the string
    *user_input_ptr = "\0";
    // cursor points now to "X\0\r\nHost: example.com\r\n..."
  }

  // DAT_00444050 global array from 0x00444050 to 0x0044414f (255 Bytes)
  strcpy(&DAT_00444050, cursor);
  // DAT_00444050 contains now "X"
 }
}

코드 6: parser_parse 함수의 호출 트리

이 코드의 취약점은 strcpy 함수 호출과 Content-Type이 세미콜론으로 끝난다는 가정에 있습니다. strcpy는 버퍼를 다음 널 바이트까지 복사하기 때문입니다. 코드 6에서 보듯이 널 바이트는 세미콜론을 찾았을 때만 추가됩니다. 세미콜론을 제거하면 다음 널 바이트는 입력 버퍼의 끝, 예를 들어 HTTP 요청의 끝에 위치하게 됩니다. 따라서 전역 변수 DAT_00444050이 오버플로되어 주소 0x0044414f를 넘어서는 데이터를 덮어쓸 수 있습니다. 도전적인 부분은 이 주소 너머에 덮어쓸 수 있는 흥미로운 전역 변수를 찾아야 한다는 점뿐만 아니라, strcpy 때문에 널 바이트를 사용할 수 없다는 점입니다. 그러나 이런 실수가 하나 있다면 더 찾을 수 있을 가능성이 높습니다.

tdpd 바이너리는 모바일 앱에서 사용되며 로컬 네트워크에서 UDP를 통해 접근할 수 있습니다. tdpd는 tmpd와 거의 동일한 기능을 가지고 있지만 대부분 호출되지 않습니다. 메인 함수는 UDP 포트를 통해 메시지를 수신하고 항상 라우터의 이름이나 모델과 같은 기본 정보로 응답합니다. 사용자가 제공한 입력과의 상호작용이 거의 없으므로 퍼징할 만한 가치가 없습니다.

또 다른 흥미로운 바이너리 쌍은 upnpd와 ushare입니다. 두 바이너리 모두 UPnP 메시지를 처리하므로 XML을 파싱해야 합니다. 바이너리에서 저작권 문자열을 찾을 수 있기 때문에 이 프로그램들은 TP-Link가 개발한 것이 아니라고 추정할 수 있습니다.```sh $ strings usr/bin/ushare | grep "(C)" Benjamin Zores (C) 2005-2007, for GeeXboX Team.

root@kitploit:~
두 바이너리는 오픈 소스 프로젝트 `pupnp` [\[pupnp\]](https://github.com/pupnp/pupnp/)와 동일한 함수들을 포함하는 공유 라이브러리 `libupnp.so`와 `libixml.so`를 로드합니다. 이 논문의 초점은 블랙박스 퍼징이므로 이 바이너리들은 무시합니다. 그러나 2021년에 `libixml.so`에서 메모리 누수가 발견되었기 때문에 [\[pupnp-mem-leak\]](https://github.com/pupnp/pupnp/issues/249), 이 라이브러리를 그레이박스 퍼징하는 것은 잠재력이 있을 수 있습니다.

바이너리 **tmpd**는 모바일 앱의 백엔드입니다. 흥미로운 점은 라우터와 모바일 앱이 사용자 정의 바이너리 프로토콜로 통신한다는 것입니다. 다음은 클라이언트에서 서버로 보내는 메시지를 보여줍니다.

<div id="c7"></div>```txt
00000000  01 00 05 00 00 08 00 00  00 00 00 17 50 7b 6e fe  |............P{n.|
00000010  01 01 02 00 00 00 00 00                           |........        |

코드 7: 모바일 앱에서 라우터로 전송되는 메시지.

바이너리 프로토콜을 이해하기 위해 tmpd 바이너리를 Ghidra로 리버스 엔지니어링했습니다. 이 정보를 바탕으로 코드 7의 메시지는 다음과 같이 분해할 수 있습니다:```txt 01 00 05 00 : Version 00 08 00 00 : Size (8 Bytes) 00 00 00 17 : Datatype 50 7b 6e fe : Checksum (CRC32) 01 01 : Options 02 00 : Function id 00 00 00 00 : Function parameters

root@kitploit:~
<p class="text-align: center">코드 8: 사용자 정의 바이너리 프로토콜 분해.</p>

이것은 그러한 바이너리 프로토콜이 파싱되어야 하기 때문에 유망해 보인다. 하지만 바이너리 프로토콜에서 가장 의심스러운 부분은 길이 필드가 아니라 함수 ID와 함수 매개변수의 사용이다.


<figure id="f2">
  <p><img src="https://assets.kitploit.com/production/public/readmes/48851/43cb089d7f85d30ba036234ff0fdb29b6a54dd4a3382b2bd68e065f9f6c75369.png" style="width:90.0%" /></p>
  <figcaption>
    <p style="text-align: center">그림 2: tmpd에서 함수 ID와 해당 매개변수를 파싱하는 역공학된 함수.</p>
  </figcaption>
</figure>

[그림 2](#f2)는 사용자 정의 프로토콜의 디컴파일된 파서 함수 일부를 보여준다. 16번째 줄에서 함수 ID가 추출되고, 그에 해당하는 함수가 29번째 줄에서 호출된다. 의심스러운 동작은 사용자 제어 입력 버퍼에서 검사 없이 추출된 매개변수로 함수가 호출된다는 점이다. 이제 [그림 3](#f3)에 표시된 점프 테이블에서 매개변수가 버퍼 인덱싱에 사용되거나 문자열로 해석되는 등 위험할 수 있는 함수를 찾으려고 시도할 수 있다. 시간이 많이 걸리는, 100개 이상의 함수를 수동으로 역공학하고 검색하는 대신, 이 작업을 자동으로 수행할 퍼저를 사용할 수 있다.

<figure id="f3">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/708cf59aee459b840ca6dbcac32948d2b80fb426c3d4cc745157eac4f9b7c28e.png"
style="width:90.0%" /></p>
<figcaption><p style="text-align: center">그림 3: tmpd에서 함수 ID와 해당 매개변수를 파싱하는 역공학된 함수.</p></figcaption>
</figure>

안타깝게도 `tmpd` 바이너리는 [코드 2](#c2)에서 보듯이 네트워크를 통해 로컬에서만 접근할 수 있다. 이 바이너리에 연결하기 위해 앱은 먼저 라우터에 SSH `direct-tcpip` 모드로 연결하는데, 이 모드는 패킷을 로컬 프로세스로 전달만 한다. 그리고 SSH 연결은 관리자 자격 증명으로 보호된다. 그러나 [\[iovt\]](https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/main/Internet_of_Vulnerable_Things.pdf)에 설명된 것처럼 앱이 서버 호스트 키를 전혀 확인하지 않기 때문에 SSH 연결은 쉽게 손상될 수 있다. 인터넷으로 라우팅되는 모든 패킷을 버림으로써, 중간자 공격을 수행하여 자격 증명을 탈취하는 동안 관리자가 라우터에 로그인하도록 속일 수 있다.

## AFL++ 및 QEMU를 사용한 퍼징

이 섹션에서는 앞서 발견한 함수 중 하나를 대상으로 하는 하네스를 개발한다. 하네스가 개발되면 최신 퍼저인 AFL++ [\[aflpp\]](https://github.com/AFLplusplus/AFLplusplus)를 사용하여 대상 함수를 퍼징한다. 바이너리가 `mipsel` 아키텍처용으로 컴파일되었기 때문에 에뮬레이터 QEMU를 사용하여 바이너리를 실행한다. 이 논문에서 사용된 기본 퍼징 설정은 Adam Van Prooyen의 블로그 게시물 "Firmware Fuzzing 101" [\[b101\]](https://www.mayhem.security/blog/firmware-fuzzing-101)에서 대부분 영감을 받았다.

### 퍼징 환경 

재현 가능한 퍼징 환경을 쉽게 만들기 위해 Docker가 가장 좋은 선택이다. 우리는 `mipsel` CPU 아키텍처용 크로스 컴파일러나 하네스 디버깅에 사용할 수 있는 `gdb-multiarch` 같은 필요한 모든 도구를 설치하는 Dockerfile을 만들었다.

또한 AFLplusplus를 다운로드하고 QEMU와 함께 컴파일했는데, QEMU는 계측되지 않은 바이너리를 afl-fuzz에서 실행할 수 있도록 약간 수정된 버전으로 빌드되었다.```docker
FROM debian:latest

RUN apt update && apt install -y \
      curl \
      vim \
      gcc-mipsel-linux-gnu \
      openssh-server \
      qemu-user-static \
      gdb-multiarch
# Qemu statics are installed at /usr/bin/qemu-mipsel-static

# Compiling AFL++
RUN apt install -y git make build-essential clang ninja-build pkg-config libglib2.0-dev libpixman-1-dev
RUN git clone https://github.com/AFLplusplus/AFLplusplus /AFLplusplus
WORKDIR /AFLplusplus
RUN make all
WORKDIR /AFLplusplus/qemu_mode
RUN CPU_TARGET=mipsel ./build_qemu_support.sh

RUN echo "#!/bin/bash\n\nsleep infinity" >> /entry.sh
RUN chmod +x /entry.sh

WORKDIR /share
ENTRYPOINT [ "/entry.sh" ]

필요한 도구를 설치하는 Dockerfile입니다.

그런 다음 docker build를 사용하여 이미지를 빌드할 수 있습니다.```sh docker build -t fuzz .

root@kitploit:~
When the image is built, it can be easily used with `docker run` which
then starts the container.```sh
docker run -d --rm -v $PWD/:/share --name fuzz fuzz

-d 옵션을 사용하면 컨테이너가 백그라운드에서 시작됩니다. docker exec를 사용하면 컨테이너 내부에서 여러 셸을 시작할 수 있으며, 이는 한 세션에서 QEMU를 사용하여 실행 파일을 시작하고 다른 세션에서 gdb-multiarch를 사용하는 데 유용합니다.```sh docker exec -it fuzz /bin/bash

root@kitploit:~
### main 함수 덮어쓰기

이전 섹션에서 강력한 퍼즈 타깃이 식별되었습니다. 문제는 바이너리를 실행할 때 소켓을 통해 TCP 패킷을 수신한 경우에만 `parser_parse` 함수가 호출되기 때문에 함수 호출에 도달할 수 없다는 것입니다. 이는 성능에 좋지 않을 뿐만 아니라 설정하기도 어렵습니다. 그래서 퍼저의 진입점은 일반적인 main 함수과 다른 위치에 있어야 합니다. 이를 위해 내부 함수에 접근할 수 있는 하니스를 주입할 수 있게 해주는 환경 변수 `LD_PRELOAD`를 사용할 수 있습니다. 런타임에 실행 파일에 필요한 공유 라이브러리 연결을 담당하는 `ld.so`의 매뉴얼 페이지에 설명된 대로, `LD_PRELOAD`는 "다른 공유 객체의 함수를 선택적으로 재정의하는 데" 사용할 수 있습니다
[\[man-pages\]](https://www.man7.org/linux/man-pages/man8/ld.so.8.html).

`__uClibc_main` 함수가 이 목적에 가장 적합합니다. 이 함수를 덮어쓰려면 동일한 이름의 함수를 포함하는 C 파일을 생성해야 합니다.```c
void __uClibc_main(void *main, int argc, char** argv) {
    // Harness code, e.g. call the function parser_append
    printf("My custom __uClibc_main was called!");
}

C 파일은 그런 다음 mipsel-linux-gnu-gcc를 사용하여 mipsel 아키텍처의 공유 객체로 크로스 컴파일할 수 있습니다. -fPIC 옵션은 "위치 독립 코드(Position Independent Code)"를 활성화합니다. 즉, 절대 주소 대신 상대 주소 지정을 사용하여 기계 코드가 특정 주소에 위치하는 것에 의존하지 않는다는 뜻입니다.```txt $ mipsel-linux-gnu-gcc parser_parse_hook.c -o parser_parse_hook.o -shared -fPIC

root@kitploit:~
그런 다음 새로 생성된 공유 라이브러리는 QEMU 명령에 환경 변수 `LD_PRELOAD`를
추가하여 로드할 수 있습니다.```txt
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o /usr/bin/wscd
My custom __uClibc_main was called!

chroot 명령을 사용하면 제공된 명령의 현재 디렉터리와 루트 디렉터리를 변경할 수 있습니다. 실행 파일 wscd가 펌웨어의 공유 라이브러리와 같은 다른 파일들을 열기 때문에 이는 유용합니다. QEMU에 -strace 인수를 추가하면 이 동작을 확인할 수 있습니다.```txt chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o -strace /usr/bin/wscd /corpus/notify.txt 38180 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|0x4000000,-1,0) = 0x7f7e7000 38180 stat("/etc/ld.so.cache",0x7ffffa48) = -1 errno=2 (No such file or directory) 38180 open("/parser_parse_hook.o",O_RDONLY) = 3 38180 fstat(3,0x7ffff920) = 0 38180 close(3) = 0 38180 munmap(0x7f7e6000,4096) = 0 38180 open("/lib/libpthread.so.0",O_RDONLY) = 3 38180 open("/lib/libc.so.0",O_RDONLY) = 3 [...]

root@kitploit:~
보시다시피 실행 파일은 호스트가 아닌 펌웨어의 `/lib/` 폴더에 있는 여러 라이브러리를 엽니다.

### 하네스 개발 및 디버깅

설정이 생성된 후, 이제 하네스 개발을 시작할 수 있습니다. 배경 섹션에서 설명한 바와 같이, 하네스는 퍼저와 대상 함수 사이의 드라이버입니다. 하네스는 AFL++가 파일에 저장하는 퍼즈 입력을 로드합니다. 파일 경로를 매개변수로 사용하여 하네스는 퍼징 대상을 호출합니다. 이 경우 대상은 `parser_append`가 됩니다. 함수는 주소를 사용하여 호출할 수 있습니다.

<div id="c10"></div>```c
void __uClibc_main(void *main, int argc, char** argv)
{
  // Verify that a filename is provided
  if (argc != 2) exit(1);

  // Create function pointer to the fuzz target
  int (*parser_request_init)(void *, int) = (void *) 0x00412564;
  int (*parser_append)(void *, void *, int) = (void *) 0x00412e98;

  // Open the fuzz input file
  int fd = open(argv[1], O_RDONLY);
  char fuzz_buf[2048 + 1];
  int fuzz_buf_len = read(fd, fuzz_buf, sizeof(fuzz_buf) - 1);
  if (fuzz_buf_len < 0) exit(1);
  fuzz_buf[fuzz_buf_len] = 0;

  // Call the target functions
  uint8_t parsed_data[220]; 
  parser_request_init(parsed_data, 8);
  int status = parser_append(parsed_data, fuzz_buf, fuzz_buf_len);
  printf("Response is %d\n", status);
  exit(0);
}

코드 10: 바이너리 wscd에서 퍼즈 타깃 `parser_append`를 사용하는 하네스 코드.

Code 10에서 볼 수 있듯이 parser_parse 함수는 직접 호출되지 않고 parser_append 함수를 사용하여 호출됩니다. 이 함수가 호출되기 전에 초기화 함수 parser_request_init를 호출해야 하며, 이 함수는 parser_parse 함수의 출력 구조체를 초기화합니다.

parser_parse의 경우 하네스를 설정하기가 매우 쉽지만, 다른 타깃은 httpd_parser_main 함수와 같은 더 정교한 하네스가 필요합니다. 예를 들어, 타깃을 호출하기 전에 http_init_main 함수를 호출해야 하며, 이 함수는 SIGSEGV로 끝납니다. 어디서 이 세그멘테이션 폴트가 발생하는지 확인하려면 gdb와 같은 디버거로 코드를 디버깅하는 것이 유용합니다. 이렇게 하려면 QEMU를 -g 옵션으로 시작할 수 있으며, 이 옵션은 지정된 포트에서 gdb-server를 생성합니다.```sh chroot root /qemu-mipsel-static -strace -g 1234 -E LD_PRELOAD="/httpd_parser_main.o" /usr/bin/httpd corpus/httpd/simple.txt

root@kitploit:~
바이너리가 `mipsel` 아키텍처에 있으므로 `gdb-multiarch`를 사용해야 합니다. gdb가
시작된 후, 다음 init 스크립트는 `sources <path to script>`를 사용하여 gdb로 로드할 수 있습니다.```sh
set solib-absolute-prefix /share/root/
file /share/root/usr/bin/httpd
target remote :1234
# break bevor fuzz target is called
# break __uClibc_main
break http_parser_main
display/4i $pc

chroot 때문에 스크립트는 먼저 절대 접두사 경로를 변경하여 바이너리가 공유 객체를 로드할 때 gdb가 파일을 찾을 수 있게 한다. 그런 다음 대상 파일을 설정한다. QEMU의 gdb-server는 파일 전송을 지원하지 않으므로, gdb는 대신 디스크에서 파일을 로드하려고 시도한다. gdb가 구성된 후, 스크립트는 target remote로 gdb-server에 연결하고 대상 함수의 시작 부분에 중단점을 생성한다. display를 사용하면 출력이 개선되어, 단계별 실행 시 다음 네 줄의 어셈블리가 표시된다. si를 사용하면 한 명령어씩 실행할 수 있다. 이는 기본 코퍼스를 사용할 때 하네스에 세그멘테이션 오류가 발생하는 경우 유용하며, 기본 코퍼스는 항상 작동해야 한다. 코드 11에서 볼 수 있듯이 바이너리에는 세그멘테이션 오류가 fprintf 함수에서 발생한다.

```sh (gdb) si 0x004059b0 in http_parser_makeHeader () 1: x/4i \$pc => 0x4059b0 : jalr t9 0x4059b4 : addiu a1,a1,16248 0x4059b8 : li v0,200 0x4059bc : lw gp,16(sp) (gdb) ni 0x7f56a8ac in fprintf () from /share/root/lib/libc.so.0 1: x/4i \$pc => 0x7f56a8ac \: bal 0x7f56db80 0x7f56a8b0 \: nop 0x7f56a8b4 \: lw ra,36(sp) 0x7f56a8b8 \: jr ra 0x7f56a8bc \: addiu sp,sp,40 (gdb) n Single stepping until exit from function fprintf, which has no line number information.

Program received signal SIGSEGV, Segmentation fault.

root@kitploit:~
<p style="text-align: center">코드 11: printf에서 발생한 세그멘테이션 폴트.</p>

이 오류를 조사하려면 Ghidra를 사용하여 함수가 어떤 매개변수로 호출되는지 알아낼 수
있습니다.```c
fprintf(
 *(FILE **)(iVar1 + 0x101c),
 "HTTP/1.1 %d %s\r\n",
 *(undefined4 *)(&DAT_0042ee68 + (uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 8),
 (&PTR_DAT_0042ee6c)[(uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 2]
);

The SIGSEGV는 아마도 첫 번째 매개변수가 파일 디스크립터가 아니라 null 포인터이기 때문에 발생한 것입니다. 여기서 iVar1은 httpd_parser_main 함수의 입력에 대한 참조일 뿐입니다. 이는 퍼징 입력이 0x101c 위치에 파일 디스크립터를 가져야 함을 의미합니다. 따라서 입력은 다음 구조체에 맞게 조정되어야 합니다.```c typedef struct { int _a; // 4 Bytes int _b; // 4 Bytes int socket; // 4 Bytes int ip; // 4 Bytes int mac; // 4 Bytes unsigned char body[0x1008]; // 0x101c - 4*5 = 0x1008 Bytes FILE * fd_out; // expected to be a valid file descriptor } HttpMainT;

root@kitploit:~
Because `fd_out`는 유효한 파일 디스크립터 포인터이기만 하면 되므로, 쉽게 `stdout`로 설정할 수 있습니다.
Executing the `httpd_parser_main` again will now produce a valid HTTP output.```c
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/httpd_parser_main.o \
    /usr/bin/httpd /httpd_corpus.txt

bind: No such file or directory
[ dm_shmInit ] 086:  shmget to exitst shared memory failed. Could not create shared memory.
rdp_getObj is called with: 4274932gdpr_getSystemGDPREntry Error
gdpr_getNewSystemGDPREntry OK
#Msg: getsockname error
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 24257
Set-Cookie: JSESSIONID=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; HttpOnly
Connection: close

<!DOCTYPE html>
[...]

The harness works now and can be used to fuzz the function using AFL++ which will be explained in the next section.

Generate corpus data

As mentioned in the background, a seed corpus describes valid input samples, which serves as a foundational reference for generating new input data during the fuzzing process.

These inputs are typically chosen to represent different aspects of the target programs. The seed corpus is used by a fuzzer to generate mutated or evolved test cases that are then run against the target software to uncover bugs, crashes, or other problems. This corpus plays an important role in directing the fuzzer to relevant areas of the program and increasing the probability of detecting vulnerabilities or unexpected behaviors. By providing a diverse and representative set of initial inputs, the seed corpus helps the fuzzer explore different paths in the target faster and thereby increases coverage.

When it comes to functions parsing network data, these inputs can be created by using Wireshark to record different packets.

For the function, httpd_parse_main four different corpora were created. Each targeting different paths in the binary. One example is the login request, which contains the username and password. For this corpus, the harness had to be modified because TP-Link uses (weak) cryptography to "protect" the password. For this, the password is encrypted in the browser using AES and then decrypted on the backend. Whereby the password is generated in the browser and then encrypted using RSA. Then the encrypted data is signed. Because a fuzzer can not create a signature or encrypt data, some functions were overwritten and now just decodes the data from base64. For this, the data were first extracted in plaintext from the browser using the debugger shown in Figure 4.

Figure 4: Extracting the data bevor encryption.

In the target, the function rsa_tmp_decrypt_bypart was then overwritten to replace the logic from decrypting the data to just decoding from base64.```c // Replacing the logic with b64_decode int rsa_tmp_decrypt_bypart(uint8_t *input, int input_len, uint8_t *output) { // other params just key data int (*b64_decode)(uint8_t *, int, uint8_t *, int) = (void *) 0x0040bf00; b64_decode(output, 0x1000, input, input_len); int * seqnumber = (int *) 0x00444db0; *seqnumber = 0x3ac28e29-input_len+12; return 0; // says it was okay }

root@kitploit:~
<p style="text-align: center">Code 12: 함수 rsa_tmp_decrypt_bypart는 이제 데이터를 복호화하는 대신 base64를
디코딩하기만 합니다.</p>

코퍼스를 실행하는 동안 대상 함수는 항상 "408
Request Timeout" 오류가 포함된 HTML 문서를 반환합니다. Ghidra와 GDB를 사용하여 문제를 확인할 수 있었습니다. 오류는 항상
`http_stream_fgets` 함수 호출 이후에 발생합니다. 문제가 되는 줄은 줄바꿈
문자 `\n`에 대한 검사였습니다.```c
if (((cVar1 == '\n') && (param_3 < pcVar4)) && (pcVar4[-1] == '\r')) {

이 조건은 모든 줄 바꿈 뒤에 캐리지 리턴이 와야 함을 강제합니다. 캐리지 리턴을 추가한 후, 생성된 모든 코퍼스가 작동했습니다.

타겟 퍼징

이전 섹션에서는 여러 하네스를 개발하고 QEMU를 사용하여 실행했습니다. 이 섹션에서는 QEMU를 AFL++로 대체하며, 생성된 코퍼스를 시드 입력으로 사용하여 타겟 함수를 퍼징합니다. "퍼징 환경" 섹션에서는 AFL++를 GitHub에서 이미 가져오고, AFL++ 제공 스크립트를 사용하여 패치된 버전의 QEMU를 빌드하는 도커 이미지가 생성되었습니다. 따라서 AFL++는 이제 -Q와 같은 다양한 매개변수를 받는 다음 명령으로 시작할 수 있습니다. -Q는 AFL++에게 패치된 버전의 QEMU를 사용하도록 지시합니다.```sh QEMU_LD_PREFIX=/share/root AFL_PRELOAD=/share/root/httpd_parser_main.o
/AFLplusplus/afl-fuzz -Q
-i /share/root/corpus/httpd/ -o /share/afl-out/httpd/
-- /share/root/usr/bin/httpd @@

root@kitploit:~
<p style="text-align: center">코드 13: 하니스와 <code>afl-fuzz</code>를 사용하여 바이너리 <code>httpd</code> 퍼징.</p>

이전과 달리 `chroot` 명령은 더 이상 필요하지 않으며 `QEMU_LD_PREFIX` 변수로 대체됩니다. 이 변수는 QEMU가 공유 객체를 검색할 위치를 알려줍니다. 또한 `LD_PRELOAD` 변수는 AFL 전용 버전인 `AFL_PRELOAD`로 대체됩니다. 명령의 마지막 인자는 두 개의 `@` 문자입니다. 이 문자들은 AFL++에 의해 퍼징 입력을 담고 있는 파일 경로로 대체됩니다. 실행되면 AFL++는 [그림 5](#f5)에 표시된 터미널 UI를 사용하여 진행 상황을 표시합니다.

<div id="f5"></div>
<figure>
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/ac4b12fcf84c2041c9edc2a0871c5bb89b576d976b2eba0a2d0f843e7e708795.png" style="width:95.0%" /></p>
<figcaption><p style="text-align: center">그림 5: AFL++의 상태 화면.</p></figcaption>
</figure>

`AFL++` 상태 화면은 현재 퍼징 프로세스에 대한 필수적인 통찰력을 제공합니다. `AFL++` 문서에는 상태 화면에서 사용되는 용어에 대한 훌륭한 개요가 있습니다 [\[afl-screen\]](https://aflplus.plus/docs/status_screen/). 다음 환경 변수들로 코퍼스를 디버깅할 때 UI를 비활성화할 수 있고, `AFL_DEBUG`를 사용하면 상세 로깅이 활성화되어 현재 퍼저 입력과 대상 프로그램의 `stdout`을 보여줍니다.```sh
export AFL_DEBUG=1 && export AFL_NO_UI=1
unset AFL_DEBUG && unset AFL_NO_UI

Figure 5에 표시된 것처럼 바이너리 퍼징은 꽤 오랜 시간이 걸릴 수 있다. 문서에 따르면 "며칠 또는 몇 주 동안 실행될 것으로 예상해야 하며" "일부 작업은 몇 달 동안 실행되도록 허용될 것"이라고 한다. 필요한 시간을 단축하려면 실행 속도가 100 execs/sec 이상이어야 한다. 예를 들어 타깃 httpd_main_parser를 퍼징했을 때 초기 실행 속도는 약 30/sec였다. 속도를 개선하기 위해 타깃 바이너리에서 속도 저하의 원인으로 보이는 의심 함수들을 검색했다. 의심 함수 중 하나는 rsa_gdpr_generate_key였다. RSA 키 생성이 느리기로 알려져 있기 때문이다. 해당 함수를 덮어쓴 후 속도는 초당 600회 실행으로 개선되었다.

퍼징을 중단할 시점을 판단하는 데 도움이 되는 지표 중 하나는 사이클 카운터이다. AFL++는 "퍼저가 오랫동안 어떤 활동도 관찰하지 못했을 때" 숫자를 녹색으로 강조하며, 이는 퍼저를 중단하라는 판단을 내리는 데 도움이 된다.

하지만 가장 흥미로운 숫자는 아마도 "total crashes"일 것이다. 이는 프로그램이 현재 퍼징 입력으로 인해 크래시된 횟수를 나타내며, 아마도 메모리 관련 버그일 가능성이 높다. 이것이 실제 버그인지 확인하려면 gdb를 다시 사용하여 버그의 위치를 찾을 수 있다.

결론

퍼징은 보안 취약점을 찾는 가장 효과적인 방법일 수 있다. 이 기말 보고서에서는 세 가지 서로 다른 함수를 퍼징했지만, 어떤 취약점도 발견되지 않았다. 블랙박스 퍼징 설정 자체는 그다지 복잡하거나 시간이 많이 걸리지 않지만, 강력한 타깃을 찾고 작동하는 하네스를 개발하는 것은 복잡하고 시간이 많이 걸린다. 대부분의 경우 하네스를 디버깅해야 하며, 그 후 바이너리의 기본 논리를 리버스 엔지니어링해야 하는데, 이 역시 오랜 시간이 걸린다.

도구 다운로드