
Tenda AC15 라우터 펌웨어 재호스팅 및 원격 명령 실행 (CVE-2020-10987) 익스플로잇 재현에 대한 분석 보고서
이 글은 AC15 (V15.03.05.19) 펌웨어의 웹서버를 QEMU로 에뮬레이션하고, 호스트 브라우저에서 접근 가능하게 만든 후, 취약한 /goform/setUsbUnload 핸들러(CVE-2020-10987)를 실행하여 에뮬레이션된 rootfs 내에서 명령어 실행을 얻는 과정을 설명합니다.
squashfs 파일 시스템을 추출(또는 획득)하고 리버싱 시작먼저 펌웨어 이미지를 가져와야 합니다. AC15 V15.03.05.19에 대한 이미지 다운로드를 찾을 수 없었지만, 이미 추출된 squashfs 파일 시스템이 있는 GitHub 저장소를 찾을 수 있었습니다.
올바른 이미지 파일이 있었다면 binwalk를 사용하여 다음과 같이 추출할 수 있었을 것입니다.```
user@computer $ binwalk -e AC15_V15.03.05.19.bin
64 0x40 TRX firmware header, little endian, image size: 6778880 bytes, CRC32: 0x80AD82D6, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x1A488C, rootfs offset: 0x0 92 0x5C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4177792 bytes 1722572 0x1A48CC Squashfs filesystem, little endian, version 4.0, compression:xz, size: 5052332 bytes, 848 inodes, blocksize: 131072 bytes, created: 2017-04-19 16:18:08
user@computer $ cd _AC15_V15.03.05.19.bin.extracted
올바른 이미지 파일은 없지만 이미 파일 시스템이 추출된 저장소가 있으므로, 저장소를 복제하면 됩니다.```
git clone https://github.com/lapinpt/Tenda-AC15-Firmware-V15.03.05.19-9061
VR라는 디렉토리를 만들고 이 리포지토리를 그 안에 클론했습니다. 제 rootfs는 $HOME/VR/Tenda-AC15-Firmware-V15.03.05.19-9061/rootfs에 있습니다.
자, 이제 AC15 V15.03.05.19 펌웨어를 확보했습니다. 리버싱을 통해 내부를 살펴보겠습니다.
리버싱에는 Ghidra 11.4.2를 사용하겠습니다.
타겟 바이너리인 rootfs/bin/httpd로 이동하여 Ghidra에 로드합시다.
formsetUsbUnload 함수로 가면 다음을 볼 수 있습니다.```C
uVar1 = FUN_0002bd4c(param_1,"deviceName",&DAT_000f4bdc);
doSystemCmd("cfm post netctrl %d?op=%d,string_info=%s",0x33,3,uVar1);
FUN_0002c6cc(param_1,"HTTP/1.0 200 OK\r\n\r\n");
FUN_0002c6cc(param_1,"{"errCode":0}");
FUN_0002cc14(param_1,200);
return;
이것이 취약점입니다. `deviceName` 매개변수가 `doSystemCmd`에 직접 전달되어 원하는 명령을 보낼 수 있습니다.
이제 원래 라우터 하드웨어가 아닌 qemu를 사용하여 이 펌웨어를 재호스팅할 예정이므로 일부 프로그램이 존재하지 않는 장치에 접근하려고 시도하여 시작이 중단될 것입니다.
우리의 목표는 웹서버(`httpd`)를 익스플로잇하는 것이므로 전체 시작(`/rootfs/etc_ro/init.d/rcS`)이 아닌 해당 바이너리 재호스팅에만 집중했습니다. _돌이켜보면 이것이 옳은 선택이었는지 확신할 수 없습니다._
그래서 `rcS`를 살펴볼 때 `rcS`가 수행하는 작업 중 `httpd`가 필요로 하는 것이 무엇인지 찾고 싶었습니다. 파일의 끝 부분에서 다음을 볼 수 있습니다:```bash
cfmd &
echo '' > /proc/sys/kernel/hotplug
udevd &
logserver &
rcS는 나머지 스택이 시작되기 직전에 cfmd를 백그라운드에서 시작합니다.
더 많은 연구와 취약한 함수가 명령을 전송하는 방식을 살펴본 결과, 다음과 같은 결론에 도달했습니다.
httpd는 cfm post를 생성합니다.cfm 클라이언트는 UNIX 도메인 소켓(예: /var/cfm_socket)을 통해 cfmd와 통신합니다.cfmd의 InitServer 루틴에서 다음을 볼 수 있습니다.```C
unlink("/var/cfm_socket");
strncpy(sa_unix.sun_path, "/var/cfm_socket", ...);
bind(fd, (sockaddr*)&sa_unix, 0x6e);
listen(fd, 5);
UIX 소켓을 생성하고 수신 대기합니다.
전체적인 작동 방식은 핸들러가 각 클라이언트로부터 고정된 0x7e0바이트 프레임을 읽는 것입니다 (`RecvMsg`/`SendMsg`). 처음 4바이트는 명령 코드이며, 그 다음에는 512바이트 키 버퍼와 1500바이트 값 버퍼가 있습니다 (핸들러에서 스택 객체 크기를 볼 수 있습니다). opcode에 따라 분기하여 ACK 코드로 응답합니다:
- `2` -> 가져오기: `GetCfmValue(key, value)` 그런 다음 응답 코드 `3`
- `0` -> 설정: `SetCfmValue(key, value)` 그런 다음 응답 코드 `1`
- `0x11` -> 해제: `UnSetCfmValue(key)` 그런 다음 응답 코드 `0x12`
- `10` -> 커밋: `SaveCfm2Flash()` 그런 다음 `0x10` (성공) 또는 `0xB` (오류) 응답
따라서 `cfmd`를 에뮬레이트하기 위해 간단한 스크립트 `cfm_stub`를 만들었습니다.```C
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define SOCK_PATH "/var/cfm_socket"
// minimal UNIX-domain server that httpd expects.
// Replies with an IP string when it sees the key it asks for.
int main(void) {
int s = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = {0};
if (s < 0) { perror("socket"); return 1; }
unlink(SOCK_PATH);
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path)-1);
if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
if (listen(s, 5) < 0) { perror("listen"); return 1; }
for (;;) {
int c = accept(s, NULL, NULL);
if (c < 0) { if (errno==EINTR) continue; perror("accept"); break; }
char buf[1024]; ssize_t n = read(c, buf, sizeof(buf));
if (n > 0) {
// In some builds httpd asks for "lan.webiplansslen" etc.
// Any non-empty reply that looks like an IP keeps init happy.
const char *reply = "192.168.0.1";
write(c, reply, strlen(reply));
}
close(c);
}
close(s);
return 0;
}
다음 부분 이후에 이것을 컴파일하는 방법을 보여드리겠습니다.
다음 헬퍼 파일은 hooks.so입니다. httpd와 cfm에서 사용되는 몇몇 함수들이 존재하지 않는 하드웨어와 상호작용하려고 시도합니다.
다음은 프로그램이 시작할 때 가정하는 사항입니다.
/dev/nvram)가 존재하며 적절한 기본값을 반환합니다.다음 함수들이 문제가 되므로 LD_PRELOAD=/hooks.so로 패치해야 합니다.
get_flash_type() -> 4를 반환하면 코드는 파일 기반 경로(cfm_file_init)를 사용하고, 그렇지 않으면 MTD와 통신을 시도합니다 (이 MTD는 없습니다).get_cfm_blk_size_from_cache() (또는 변형 j_get_cfm_blk_size_from_cache)는 설정 블록 크기 계산에 사용됩니다.bcm_nvram_get) 호출은 실패해서는 안 됩니다. 그렇지 않으면 스택이 "NVRAM 파괴됨"으로 인식하고 복원/재부팅 로직으로 진행합니다.load_l7setting_file() 및 restore_power()와 같은 추가 루틴은 성공할 것으로 예상되지만 존재하지 않는 하드웨어/파일을 건드립니다.다음은 hooks.c입니다. 원본은 azeria-labs에 크레딧을 제공합니다.```C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>
int j_get_cfm_blk_size_from_cache(const int i) { puts("j_get_cfm_blk_size_from_cache called....\n"); return 0x20000; // 128 KiB block — what the file path expects }
int get_flash_type() { puts("get_flash_type called....\n"); return 4; // force file-backed CFM init, not MTD }
int load_l7setting_file() { puts("load_l7setting_file called....\n"); return 1; // pretend Layer-7 settings loaded OK }
int restore_power(int a, int b) { puts("restore_power called....\n"); return 0; // success (don’t touch RF/power hardware) }
char *bcm_nvram_get(char *key) { char *value = NULL;
if (strcmp(key, "et0macaddr") == 0) { value = strdup("DE:AD:BE:EF:CA:FE"); // any valid MAC works } if (strcmp(key, "sb/1/macaddr") == 0) { value = strdup("DE:AD:BE:EF:CA:FD"); } if (strcmp(key, "default_nvram") == 0) { value = strdup("default_nvram"); // signals “nvram is OK” }
printf("bcm_nvram_get(%s) == %s\n", key, value); return value; }
이제 이 파일들을 컴파일하고 우리 펌웨어에 배치해 봅시다. 크로스 컴파일을 위해 Bootlin의 사전 구축된 uClibc 툴체인을 사용할 수 있습니다.```bash
wget https://toolchains.bootlin.com/downloads/releases/toolchains/armv5-eabi/tarballs/armv5-eabi--uclibc--stable-2020.08-1.tar.bz2
tar xjf armv5-eabi--uclibc--stable-2020.08-1.tar.bz2
export PATH="$PWD/armv5-eabi--uclibc--stable-2020.08-1/bin:$PATH"
ls armv5-eabi--uclibc--stable-2020.08-1/bin | grep gcc
다음과 같은 내용을 보게 될 것입니다.```bash arm-buildroot-linux-uclibcgnueabi-gcc arm-buildroot-linux-uclibcgnueabi-gcc-9.3.0 arm-buildroot-linux-uclibcgnueabi-gcc-9.3.0.br_real arm-buildroot-linux-uclibcgnueabi-gcc-ar arm-buildroot-linux-uclibcgnueabi-gcc.br_real arm-buildroot-linux-uclibcgnueabi-gcc-nm arm-buildroot-linux-uclibcgnueabi-gcc-ranlib arm-linux-gcc arm-linux-gcc-9.3.0 arm-linux-gcc-9.3.0.br_real arm-linux-gcc-ar arm-linux-gcc.br_real arm-linux-gcc-nm arm-linux-gcc-ranlib
이제 다음을 사용하여 컴파일할 수 있습니다```bash
arm-buildroot-linux-uclibcgnueabi-gcc -shared -fPIC -Os -ldl -Wl,-soname,hooks.so -o hooks.so hooks.c
arm-buildroot-linux-uclibcgnueabi-gcc -Os -s -o cfm_stub cfm_stub.c
그런 다음 마지막으로 이들을 펌웨어에 설치합니다.``` $FIRM = "$HOME/VR/Tenda-AC15-Firmware-V15.03.05.19-9061/rootfs" install -m 0644 ./hooks.so "$FIRM/hooks.so" install -D -m 0755 ./cfm_stub "$FIRM/usr/sbin/cfm_stub"
## ARM 게스트 시스템 구축
qemu 풀 시스템을 사용하여 arm 게스트를 설정해 보겠습니다.
이 게스트 시스템을 위한 디렉토리를 `~/qsys`에 만들었습니다.