블로그 목록으로 돌아가기

GOTROOT / Research

공홈 펌웨어, 뜯기 전에 읽어야 할까? 뜯고 읽어야 할까? | GOTROOT

공식 펌웨어를 먼저 받아 분석하는 방식이 있고, 바로 UART를 연결해 접근을 시도하는 방식도 있습니다. 지난 시도에서는 UART 연결에는 성공했으나, Magic Key를 몰라 더 이상 진행하지 못했습니다. 이번에는 펌웨어를 먼저 받아 Magic Key 추출을 시도해봤습니다. 과연 성공했을까요?

GOTROOT ROOT LAB 2026. 8. 19.

들어가며

지원이 끊긴 기기는 그대로 방치됩니다. 패치도 없고, 공지도 없습니다. 누군가의 거실에, 누군가의 사무실 한쪽에 꽂힌 채로. 1편에서 UART 콘솔은 매직키에 막혔습니다. 이번엔 방향을 바꿨습니다. 물리 장비 대신 소프트웨어로 접근했습니다. 공식 사이트에서 펌웨어를 받아 직접 분석했고, 예상보다 많은 게 나왔습니다.

image

[AI로 생성된 공유기 이미지]


1. 1편에서 이어서

1편 마지막에서 이런 메시지가 나왔습니다.

press magic key to change default setting ...

UART는 연결됐고, 부트 로그도 나왔습니다. 그런데 이 한 줄 앞에서 막혔습니다. 특정 바이트 시퀀스를 정확한 타이밍에 입력해야만 콘솔이 열리는 구조였습니다. 그 키를 몰랐습니다. 파이썬 스크립트로 자동화해서 시도했습니다. 안 됐습니다. 그 키는 어디 있을까요. 공유기가 스스로 확인하는 값이라면, 펌웨어 바이너리 안에 있어야 합니다. IPTIME 공식 사이트에서 A2003NS-MU 펌웨어를 받아 리버싱했습니다.

이번 편에서 확인할 3가지입니다.

  1. 펌웨어 안에 매직키가 있는가? (리버싱 분석)

  2. SPI Flash를 직접 읽을 수 있는가? (CH341A 덤프)

  3. 덤프한 데이터에서 유의미한 정보가 나오는가? (결과 분석)


2. 리버싱이란 무엇인가

"완성된 자물쇠를 분해해서 내부 구조를 파악하는 것"

리버싱(Reverse Engineering)은 완성된 제품을 분해해 구조를 파악하는 과정입니다. 소프트웨어에서는 기계어로 된 바이너리를 분석해 원래 로직을 읽어내는 작업입니다. 자물쇠를 분해해 내부 구조를 보는 것과 같습니다.

image

[AI로 생성된 자물쇠 분해도 이미지]


목표: 기계어로 된 바이너리를 분석해 원래 로직을 파악하는 방법을 이해합니다.
-> 완성된 프로그램 안에 어떤 코드가 들어있는지 읽어낼 수 있을까요?
image

[이해를 돕기 위해 AI로 생성된 이미지]

제조사가 소스코드를 공개하지 않아도, 바이너리만 있으면 리버싱으로 안에 뭐가 들었는지 확인할 수 있습니다.

실제로 분석하기 위한 도구(IDA Pro)를 사용합니다.


3. IDA Pro

IDA Pro는 기계어를 어셈블리어로, 더 나아가 의사코드로 변환해주는 리버싱 도구입니다. Hex-Rays 플러그인 디컴파일러를 얹으면 C에 유사한 코드도 볼 수 있습니다.

목표: IDA Pro를 사용해 ARM/MIPS 바이너리를 디컴파일하는 방법을 이해합니다.
-> 기계어를 사람이 읽을 수 있는 코드로 변환할 수 있을까요?
(외계어를 한국어로 번역해주는 사전과 같습니다.)
image

[이해를 돕기 위해 AI로 생성된 이미지]

그러면 통째로 바이너리를 넘겨주면 편하겠네요. 생각하실 수 있습니다. 한번 통째로 넣어볼까요?

(설명을 위한 예시일 뿐이므로 실제는 binwalk -e, file, readelf 도구를 사용해서도 확인이 가능합니다. 해당 부분에서는 왜 MIPS, ARM, x86인지를 알아야 하는지 입니다.)

image

[IDA가 아키텍처를 자동으로 식별하지 못하는 상황]

바이너리 헤더(ELF/PE헤더) 값이 없는 Raw 데이터인 .bin 파일은 IDA가 아키텍처를 자동으로 식별하지 못하는 상태입니다.

(ELF, PE(EXE/DLL), Mach-O 같은 일반적인 실행 파일에는 파일 맨 앞에 "나는 MIPS 아키텍처용 프로그램이고, Endian은 Big-Endian이다"라고 적혀 있는 표준 헤더(Header)가 존재합니다. IDA는 이 헤더를 읽고 아키텍처를 자동으로 맞춰줍니다)

image

[IDA가 인식할 때 아키텍처]


"이 파일의 데이터를 가상 메모리의 몇 번지 주소에 올려서 해석을 시작할까?"라고 물어보는 설정 단계입니다. IDA에서 MIPS 여부를 판별하는 것이 목적이기 때문에 기본 값으로 진행합니다.

image

[데이터로 뭉개져서 .BYTE 형식으로 보이는 상황]

Entry Point 및 파일시스템 포맷을 확인 및 읽지 못하는 상황입니다. 단축키 "P"를 사용해서 IDA가 해당 영역을 하나의 함수로 묶어서 Functions으로 나타내줍니다.

image

[IDA가 해당 영역을 하나의 함수로 묶어서 Function 변환]

CLV, BPL, BRK 값이 익숙하지 않습니다. 왜냐하면 저희가 처음에 IDA가 아키텍처를 제대로 분간 못하고, 아래 프로세스 타입을 m65816 으로 가정하고 디컴파일을 했기 때문입니다.

image

[Auto 디컴파일이 되지 않고 수동으로 아키텍처 설정하게 안내한 이미지]


MIPS / ARM / x86

binwalk -e로 뜯어보나, 다른 도구들을 사용해서 뜯어보았을 경우 MIPS로 판별이 났습니다. 그런데 위처럼 m65816으로 오해하고 진행할 경우 제대로 된 분석이 불가능합니다. 그래서 아래와 같이 MIPS / ARM / x86 등 아키텍처를 확인해야 합니다.

(바이너리 헤더(ELF/PE 헤더)가 없는 Raw .bin 파일은 IDA가 아키텍처를 자동 식별하지 못합니다. ELF, PE, Mach-O 같은 실행 파일에는 파일 앞에 아키텍처와 Endian 정보가 담긴 표준 헤더가 있지만, Raw 데이터에는 없습니다.)

MIPS 어셈블리
=> jal, lw, sw, jr 등
ARM 어셈블리
=> LDR, STR, BL, MOV, ADD 등
x86 어셈블리
=> mov, push, pop, call, jmp, lea

각 아키텍처 명령어 구조는 godbolt.org에서 확인할 수 있습니다.

image

[MIPS, x86, ARM 아키텍처 별 어셈블리 명령어 비교 이미지]


단일 실행 파일 분석기

IDA는 단일 실행 파일 분석기이지, 펌웨어 패키지 풀이 도구가 아닙니다.

image

[단일 실행 파일 분석기 이해를 돕기 위해 AI로 생성된 이미지]

통 펌웨어(.bin)를 통째로 로드하면 부트로더, 커널 이미지, RFS가 뭉쳐진 상태로 들어옵니다. IDA는 단일 실행 파일 분석기이지, 펌웨어 패키지 풀이 도구가 아닙니다. RFS를 먼저 추출하고, 그 안의 실행 파일(BusyBox 또는 bin, sbin 내 바이너리)을 binwalk, readelf 등으로 아키텍처를 파악한 뒤 넘기는 방식을 씁니다. (연구팀 또한 위와 같은 방식으로 진행하였습니다.)

자, 그러면 IDA Pro를 통해 MIPS인지, ARM인지, x86인지, 그리고 왜, 어떻게 확인해야 하는지를 살펴보았습니다. 아키텍처를 확정했다면, 다음 단계는 무엇일까요? 바로 분석입니다. 여기서 핵심적인 질문은 "어떤 파일을 분석해야 하는가, 그리고 왜 분석해야 하는가?" 입니다.


RFS란?

펌웨어는 부트로더, 커널, 루트 파일시스템(RFS)으로 구성됩니다. 지금 분석하는 대상은 일반적인 펌웨어가 아니라 임베디드 리눅스입니다. 일반 펌웨어(RTOS)는 OS 기능, 드라이버, 응용프로그램이 하나의 바이너리 안에 뭉쳐 동작하지만, 임베디드 리눅스 펌웨어는 부트로더, 커널, RFS 세 레이어로 나뉩니다. RFS는 리눅스 OS가 동작하는 데 필요한 모든 파일과 환경의 집합입니다.

image

[이해를 돕기 위해 AI로 생성된 이미지]

RFS는 리눅스 커널이 부팅을 마치고 최초로 마운트하는 최상위 디렉터리 "/"입니다.


UART 방식 vs Unpack 방식

RFS를 보는 방법은 두 가지입니다.

여기서 조금 혼동이 올 수도 있었습니다. 차이는 단순합니다. RFS가 메모리에 올라가 실제로 동작하고 있는가, 아닌가입니다.

image

[이해를 돕기 위해 AI로 생성된 이미지]

UART는 동작 중인 OS에 쉘을 통해 직접 명령어를 입력해 덤프하는 방식입니다. Unpack은 펌웨어를 정적으로 추출해 RFS 내 단일 ELF 바이너리를 IDA에 로드하는 방식입니다.


단일 ELF를 보는 이유

Unpack이 끝나면 RFS 안에서 분석 대상인 /usr/sbin/httpd 같은 프로그램을 찾게 됩니다.

image

[이해를 돕기위해 AI로 생성된 이미지]

위의 이미지처럼 펌웨어 전체를 분석하는 게 아니라, 타겟이 되는 단 하나의 실행 파일을 추출해 분석하는 것입니다.


포맷 형식?

운영체제마다 실행 파일 포맷이 정해져 있습니다. Windows의 실행 파일 형식이 .exe인 것처럼요.

image

[Windows 실행 파일 형식 .exe]

image

[Linux 실행 파일 형식 .elf]

Windows

Linux

notepad.exe

/usr/bin/nano

calc.exe

/usr/bin/bc

cmd.exe

/bin/bash

리눅스에서 독립 실행 파일은 ELF, Windows에서는 PE(.exe)입니다. 리눅스 명령어처럼 보이지만 실제로는 바이너리 파일입니다.

image

임베디드 리눅스의 표준 실행 파일 형식도 ELF이기 때문에, 분석 대상은 ELF 파일입니다.


4. 공식 사이트에서 펌웨어를 받았습니다.

이제 펌웨어를 왜 다운 받으면 좋은지 확인이 끝났습니다.

그러면 공식 홈페이지에서 펌웨어를 직접 다운로드합니다.

고객지원-다운로드 | EFM - ipTIME

image

[공식 사이트 펌웨어 다운로드 방법]

image

[공식 사이트 펌웨어 다운로드 방법-1]

목표: 제조사 공식 사이트에서 펌웨어를 다운로드하고 내부 구조를 파악합니다.
-> 공개된 펌웨어 파일 안에 어떤 것들이 들어있을까요?
(택배 상자를 받기 전에 배송 목록을 먼저 확인하는 과정입니다.)

다운로드 후 binwalk -e로 내부 구조를 확인합니다.

image

[WARNING 메세지 이미지]

image

[--preserve-symlinks 실행 이미지]

WARNING 메시지는 디렉토리 트래버셜 방어용으로 binwalk가 걸어둔 것이라 무시해도 됩니다. 펌웨어 내부에 host의 /etc/shadow를 가리키는 악성 심링크가 심어져 있을 경우, binwalk를 그대로 사용하면 추출 단계에서 악성 심링크가 만들어져 사용자 PC의 실제 파일을 읽거나 덮어쓸 수 있습니다. 이를 막기 위해 추출 디렉토리 밖을 가리키는 심링크 타겟을 자동으로 /dev/null로 변경합니다. (CVE-2021-4287 취약점)

목적은 RFS 추출입니다. 바이너리 파일 내 RFS 위치만 확인하고 나머지는 버립니다. 필요한 구간만 바이트 단위로 잘라내는 dd 도구를 사용했습니다.

image

[dd 실행하여 RFS 분리된 이미지]


offset과 File Size는 "위에 binwalk -e 빨간 네모를 확인해서 입력"합니다.

image

[내부 구조 파악하기 위해 unsquashfs 실행이미지]

image

[내부 구조 확인 가능 이미지]

unsquashfs로 내부를 풀면 bin, usr, lib, sbin 등 리눅스 파일시스템 구조와 바이너리 파일들이 나옵니다.

(/etc의 내용이 /tmp/etc로 dangling 심볼릭 링크가 걸려 있어 직접 볼 수는 없지만, /default/etc가 존재해 부팅 과정에서 내용이 복사될 것으로 봤습니다. 임베디드 장비는 부팅 후 일반적으로 linuxrc가 실행된 뒤 /etc/init.d 또는 /etc/inittab의 스크립트나 바이너리를 실행합니다. 이 과정이 끝나면 커널은 /sbin/init을 PID 1로 실행합니다. 이 펌웨어의 linuxrc는 /bin/busybox로 심볼릭 링크되어 있었고, 실행해도 아무 작업을 하지 않았습니다. /etc에 init.d, inittab, 특별한 스크립트도 없었습니다.)

/sbin/init이 실제 서비스 시작을 담당한다고 판단하고, 이를 전제로 다음 분석을 진행합니다.


5. 바이너리에서 매직키를 찾습니다.

공유기 부팅 순서상 커널이 제일 먼저 실행하는 건 /sbin/init입니다.

목표: ghidra로 init 바이너리를 분석해 매직키 시퀀스를 추출합니다.
-> 제조사가 하드코딩해 둔 값이 바이너리 안에 남아있을까요?
(설계도 안에 자물쇠 조합 번호가 적혀있는지 확인하는 과정입니다.)

/sbin/ 폴더 내 주요 바이너리입니다.

/sbin/init

커널이 제일 먼저 실행하는 프로세스

/sbin/inittime

부팅 초기 타이밍 관련

/sbin/lighttpd

웹서버

/sbin/servd

서비스 데몬

image

[init, inittime 이미지]

앞서 IDA Pro로 확인한 내용에 이어, 이번엔 Ghidra로 /sbin/init을 분석합니다.

image

[ghidra로 init 파일 실행 후 의심 문자열 확인 이미지]

/sbin/init 내부에는 tmpfs 설정에 쓰이는 /tmp/etc, /tmp/var 같은 문자열들이 있습니다. pre-auth 상태에서 노출되는 CGI 서비스를 찾으려면 웹서버가 어디서 뜨고 설정을 어떻게 하는지 봐야 합니다. init 자체에서 웹서버를 띄우는 명령은 없었습니다. init이 다른 프로세스를 실행하고, 그 프로세스에서 웹서버를 띄우는 구조로 봤습니다. exec 계열 함수를 찾아 어떤 프로세스를 실행하는지 추적하는 방향으로 갔습니다. 문자열을 훑다 보면 일반적인 시스템 문자열과 다른 #notenoughmineral^ 같은 값도 눈에 띕니다.

추적 순서는 "exec 계열 함수 → 실행 바이너리 → 웹서버 설정 → CGI 위치"입니다.


이 문자열이 어디서 참조되는지(xref)를 확인해야 의미를 파악할 수 있습니다.

image

[init 바이너리 내 run_shell() 디컴파일러를 활용하여 소스코드 분석한 이미지]

runshell() 함수가 보이지 않아 press magic key의 값이라고 확정하기는 어렵습니다.


로그와 코드로 재분석

init에서 string 검색 시 magic key 문자열은 없었지만 #notenoughmineral^ 값은 확인됐습니다. 부트 로그를 기준으로 구간을 나눠 분석합니다.

[부트로더]

Booting...

init_ram

DRAM Type: DDR2

DRAM frequency: 533MHz

found w25q128 # 플래시 칩 확인

---Realtek RTL8197F boot code at 2017.07.05

Delay 1 second.

Magic Number: DEFAUL 00000000

Check Firmware(00040000) ... OK

Jump to image start=0x80a00000 # 부트로더 끝

# 하드웨어 초기화 후 플래시에서 커널을 찾아 메모리에 올림

[리눅스 커널]

Uncompressing Linux... done

Linux version 3.10.90 ...

CPU revision is: 00019385 (MIPS 24Kc)

Memory: 91504k/131072k available

Serial: 8250/16550 driver

eth0 added. / eth1 added.

squashfs: version 4.0 VFS: Mounted root (squashfs filesystem) #RFS 마운트. 커널 끝

Freeing unused kernel memory

# 커널이 드라이버, 메모리, 네트워크, 파일시스템을 초기화하고 RFS를 마운트함

[RFS]

Checking System Requirements ... OK

alias:a2003nm version:12.162 #/sbin/init 실행됨

FTM is disable in SAVE header

Default Configuration

==================================================

press magic key to change default setting ...

LAN MAC : 70:5D:CC:22:52:C8

WAN MAC : 70:5D:CC:22:52:C9

Bridge Init #inittime 끝, init 계속

device wlan1-va0 is not a slave of br0

Start Wireless Helper

Restart HTTPD #웹서버 시작

Kill httpd / Start httpd

Enable HW NAT

Turn ON All LED

# /sbin/init이 PID 1로 실행되면서 나머지 프로세스를 모두 띄움

/sbin/init이 PID 1로 실행되면서 웹서버를 포함한 나머지 프로세스를 모두 띄웁니다. init, inittime, lighttpd가 모두 /sbin/ 폴더 안에 있고, init이 전체 서비스 시작을 총괄합니다.


inittime을 확인하던 중 init 바이너리에서 run_shell()을 발견했습니다.

image

[init 바이너리 코드 내에 inittime, run_shell() 이미지]

image

[inittime 바이너리 코드 내에 로그 상 magic key 부분 이미지]

inittime 바이너리 내에서 매직키 출력 직후 키 입력을 기다리는 코드를 찾았습니다.

image

[magic key 확인 이미지]

매직키 위치를 확인한 뒤 콘솔 진입을 시도합니다.


6. 콘솔 진입 시도

펌웨어에서 뽑은 매직키 값으로 실제 콘솔 진입을 시도했습니다.

목표: 추출한 매직키를 정확한 타이밍에 전송해 UART 콘솔 진입을 시도합니다.
-> 매직키를 알아도 타이밍이 맞지 않으면 콘솔이 열릴까요?
(비밀번호를 알아도 문이 열리는 시간이 따로 있을 수 있습니다.)

결론적으로 콘솔 진입에 시도하였으나,

Default Configuration

enable 0 interval

=================================================================

press magic key to change default setting ...

  LAN MAC : 00:00:CC:22:00:C8

  WAN MAC : 00:00:CC:22:00:C9

Bridge Init

여기서 멈췄습니다. 진행이 너무 빨라 키보드 입력이 불가능했습니다. 파이썬으로 타이밍을 맞춰 시도했지만 넘어가지 못했습니다.

image

[magic key 입력 파이썬 스크립트 원문 제공 X 이미지]

매직키를 확보해도 타이밍을 맞추지 못하면 콘솔은 열리지 않습니다. UART로는 덤프가 불가능한 상황입니다. 다음 방법은 SPI 통신으로 펌웨어를 직접 추출하는 것입니다.


마치며

매직키는 찾았습니다. 펌웨어 안에 있었습니다. inittime 바이너리가 출력 직후 키 입력을 기다리는 구조였고, 그 값은 하드코딩되어 있었습니다. 그런데 콘솔은 열리지 않았습니다. 키를 알아도 타이밍이 맞지 않으면 의미가 없었습니다. 파이썬으로 자동화해서 시도했습니다. 그래도 넘어서지 못했습니다. 그래서 방향을 바꿨습니다. UART가 막혀 있다면 Flash를 직접 읽으면 됩니다. 다음 편에서는 CH341A로 SPI Flash를 직접 덤프합니다. 다음 편에서 이어집니다.

image