graf
4. 펌웨어 디스어셈블 본문
소스코드 분석을 위한 준비가 필요하다.
일단 디스어셈블 가능한 툴을 먼저 찾아봐야겠다.
>python -m esptool image_info esp_backup.bin
esptool.py v4.9.0
File size: 2097152 (bytes)
Detected image type: ESP8266
Image version: 1
Entry point: 4010f29c
1 segments
Segment 1: len 0x00570 load 0x4010f000 file_offs 0x00000008 []
Checksum: d0 (valid)
지난번에 확인했던 image_info 결과다.
이걸 바탕으로 분석을 시도해보자.
1. 아키텍쳐 확인
Ada on the ESP8266
아키텍쳐는 찾았다.
xtensa라는 아키텍쳐가 쓰였다고 한다.
오래된 칩이라 그런지 자료가 너무 없다.
안 좋은 소식은 명령어에 커스텀이 가능하다고 한다.
커스텀돼서 해석 불가능한 명령어가 들어있는 최악의 상황을 생각해야한다.
그래도 단서가 나왔으니 일단 시도를 해보자.
2. capstone 설치
python3 -m venv xtensa_env
source xtensa_env/bin/activate
pip install --upgrade pip
git clone https://github.com/capstone-engine/capstone.git
cd capstone
git fetch --tags
git checkout 6.0.0-Alpha2
./make.sh
cd bindings/python
python3 setup.py install
capstone 설치
이거로 한번 해보자.
binwalk가 capstone을 쓰고 있는데 한번 설치를 해봤더니 둘이 호환이 안된다.
기존 환경을 건드리지 않도록 가상환경에서 진행했다.
from capstone import *
BINARY_PATH = "esp_backup.bin"
ENTRY_POINT = 0x4010f29c
SEGMENT_BASE = 0x4010f000
FILE_OFFSET = 0x08
ENTRY_OFFSET = ENTRY_POINT - SEGMENT_BASE
with open(BINARY_PATH, "rb") as f:
f.seek(FILE_OFFSET + ENTRY_OFFSET)
CODE = f.read(0x300)
md = Cs(CS_ARCH_XTENSA, 0)
md.detail = True
print(f"[+] Disassembling {BINARY_PATH} from 0x{ENTRY_POINT:x} ...\n")
for instr in md.disasm(CODE, ENTRY_POINT):
print("0x%x:\t%s\t%s" % (instr.address, instr.mnemonic, instr.op_str))

펌웨어 분석을 시도하고 처음으로 뭔가 명령어같은게 나왔다.
아키텍쳐가 생소하니 해석이 눈에 안 들어오네.
call + 0x1ac는 rip 기준으로 분기하는 건가?
일단 entrypoint를 0x4010f2c2+2+0x1ac로 바꾸고 다시 출력해보자.

entrypoint 변경

더 나왔다

근데 나오다가 여기서 끊겼다.
extui는 비트연산이라고 한다.
ret같은거면 이해를 하겠는데 왜 여기서 끊어진거지?
어쩌면 해석 불가능한 명령어가 있는걸지도 모르겠다.
다음 주소를 찾아보자.

명령어는 4바이트 뒤인 0x4010f4bc부터 다시 출력됐다.
ret에서 멈춘거 보니까 이건 멀쩡해보이는데
아까 extui 명령어의 길이가 몇이었는지 확인해봐야겠다.
from capstone import *
BINARY_PATH = "esp_backup.bin"
ENTRY_POINT = 0x4010f2c2 + 2 + 0x1ac # 0x4010f29c
SEGMENT_BASE = 0x4010f000
FILE_OFFSET = 0x08
ENTRY_OFFSET = ENTRY_POINT - SEGMENT_BASE
with open(BINARY_PATH, "rb") as f:
f.seek(FILE_OFFSET + ENTRY_OFFSET)
CODE = f.read(0x300)
md = Cs(CS_ARCH_XTENSA, 0)
md.detail = True
print(f"[+] Disassembling {BINARY_PATH} from 0x{ENTRY_POINT:x} ...\n")
for instr in md.disasm(CODE, ENTRY_POINT):
hex_bytes = ' '.join(f'{b:02x}' for b in instr.bytes)
print(f"0x{instr.address:x}:\t{hex_bytes:<12}\t{instr.mnemonic:<7} {instr.op_str:<20} ; {instr.size} byte")
바이트랑 명령어 길이 출력 추가 후 확인

명령어 크기가 3바이트로 나온다.
분명 공백이 4바이트였는데 그럼 남은 1바이트는 뭐지?
hexdump로 확인해보자.

이 부분이 extui다.

extui 다음에 오는 92 28 4f도 그대로 확인 되는데
저 사이에 낀 cc 때문에 해석이 멈춘거다.
저게 대체 뭐지?
그냥 패딩 같은건가?
어이가 없네.
일단 다음으로 계속 넘어가보자.

0x4010f4bc는 ret에서 멈췄다.

다음은 0x4010f561부터 명령어가 나오기 시작했다.
이후는 전부 aa로 채워져있었다.

이 부분이다.

이후부터는 계속 다시 해봐도 해석이 이어지질 않는다.
그러고보니 헤더에서 나온 세그먼트 길이가 0x570이었지.
이제 코드 영역은 끝났다고 보면 될 것 같다.
3. ghidra extension 설치
capstone은 해석도 제대로 안 될 뿐더러 가독성도 너무 안좋다.
ghidra에 xtensa용 extension을 설치해서 해석해봐야겠다.
사실 저번에 시도하려다가 실패했었는데 이제 방법이 없다.
만약 이걸 성공하면 디컴파일까지 가능할지도 모른다.
기드라 10.2 버전으로 진행
cd C:\Users\baekj\Desktop\ghidra_10.2_PUBLIC\Ghidra\Processors
git clone https://github.com/yath/ghidra-xtensa.git
cd C:\Users\baekj\Desktop\ghidra_10.2_PUBLIC\Ghidra\Processors\ghidra-xtensa
set GHIDRA_DIR=C:\Users\baekj\Desktop\ghidra_10.2_PUBLIC
gradle buildExtension
사용한 버전 정보
ghidra 10.2
gradle 8.6
java 17
기드라 내에서 git clone하고 빌드까지 진행

성공하면 dist 안에 zip 파일이 생긴다.
extension 파일을 옮기고 찍은거라 위에 zip파일은 이거랑 상관 없다.

extension에서 xtensa 추가


이제 language 선택에서 xtensa를 추가할 수 있다.

디컴파일까지 잘 된다.
Xtensa + Ghidra + Java + Gradle
진짜 욕나오는 조합이다.
이거 하나 때문에 하루를 날렸다.
패키지 찾는 것도 빡센데 호환이 하나도 안돼서 에러가 너무 많이 나더라.
그래도 성공해서 다행이다.
자료도 없어서 정말 울고싶었다.

참조하는 위치까지 다 나온다.
기드라가 이렇게 반가운건 처음이다.

여기선 함수 주소가 offset으로만 나온다.
entrypoint도 찾지 못한 것 같다.
직접 entrypoint의 오프셋을 찾아보자.

이게 아까 봤던 capstone으로 entry point부터 해석한 결과다.
hexdump -C esp_backup.bin | grep "50 0d f0 00 92 a0 a0"
바이트가 일치하는 곳을 찾자.

0x2a3이 진입점의 offset이다.

근데 기드라에서는 아무것도 안 나온다..
바이트는 일치하는데 뭐지?
내가 뭔가 해석을 잘못하고 있던건가?
설마 이거 그냥 데이터 영역은 아니겠지?
일단 원래 계획대로 문자열에서 타고 올라가보자.

private key loaded 문자열을 찾았다.

근데 참조하는 곳이 안 나온다.

지금 보니까 문자열을 제대로 못 찾는다.
strings에서는 분명 나왔었는데 검색하니까 No matches found가 뜬다.

직접 오프셋 찾아서 가면 데이터가 나오긴 한다.

진입점 이후에 나오는 코드들도 해석이 제대로 되는 곳이 없다.
알아보니 기드라의 extension은 esp32 기준이라
esp8266은 해석이 부정확할 수 있다고 한다.
이것도 안되겠다. 또 다른 방법을 찾아보자.
4. lx106 툴체인 설치
esp8266은 esp32와 다르다.
lx106 프로세서에 기반한 분석을 해야한다.
mkdir -p ~/esp
cd ~/esp
wget https://dl.espressif.com/dl/xtensa-lx106-elf-gcc8_4_0-esp-2020r3-linux-amd64.tar.gz
tar xvzf xtensa-lx106-elf-gcc8_4_0-esp-2020r3-linux-amd64.tar.gz
echo 'export PATH="$PATH:$HOME/esp/xtensa-lx106-elf/bin"' >> ~/.profile
source ~/.profile
xtensa-lx106-elf-objdump --version
xtensa용 툴체인 설치
esp8266이 lx106 프로세서를 활용한다고 하니
xtensa-lx106-elf-XXX 이게 8266용 명령어일거다.
xtensa-lx106-elf-objdump -D -b binary -m xtensa -EL --start-address=0x4010eff8 --adjust-vma=0x4010eff8 esp_backup.bin > disasm.txt
capstone으로 나오는 것과 주소를 맞춰줬다.

결과가 나온다.
0x4010f29c: f8 f1 l32i.n a15, a1, 0x3c ; 2 byte
0x4010f29e: 12 c1 50 addi a1, a1, 0x50 ; 3 byte
0x4010f2a1: 0d f0 ret.n ; 2 byte
0x4010f2a3: 00 92 a0 addx4 a9, a2, a0 ; 3 byte
0x4010f2a6: a0 90 11 slli a9, a0, 6 ; 3 byte
0x4010f2a9: c0 02 61 xsr a12, lcount ; 3 byte
0x4010f2ac: 27 f2 61 bbsi a2, 0x12, . +0x65 ; 3 byte
0x4010f2af: 26 fd 01 beqi a13, 0x100, . +5 ; 3 byte
0x4010f2b2: 0c 92 movi.n a2, 9 ; 2 byte
0x4010f2b4: 29 0f s32i.n a2, a15, 0 ; 2 byte
0x4010f2b6: 0c 02 movi.n a2, 0 ; 2 byte
0x4010f2b8: 22 4f 04 s8i a2, a15, 4 ; 3 byte
0x4010f2bb: 0c 02 movi.n a2, 0 ; 2 byte
0x4010f2bd: 05 da ff call0 . -0x25c ; 3 byte
0x4010f2c0: 8b 2f addi.n a2, a15, 8 ; 2 byte
0x4010f2c2: 85 1a 00 call0 . +0x1ac ; 3 byte
아까 캡스톤으로 뽑았던 코드
4010f29c: f1f8 l32i.n a15, a1, 60
4010f29e: 50c112 addi a1, a1, 80
4010f2a1: f00d ret.n
4010f2a3: a09200 addx4 a9, a2, a0
4010f2a6: 1190a0 slli a9, a0, 6
4010f2a9: 6102c0 excw
4010f2ac: 61f227 bbsi a2, 18, 0x4010f311
4010f2af: 01fd26 beqi a13, 0x100, 0x4010f2b4
4010f2b2: 920c movi.n a2, 9
4010f2b4: 0f29 s32i.n a2, a15, 0
4010f2b6: 020c movi.n a2, 0
4010f2b8: 044f22 s8i a2, a15, 4
4010f2bb: 020c movi.n a2, 0
4010f2bd: ffda05 call0 0x4010f060
4010f2c0: 2f8b addi.n a2, a15, 8
4010f2c2: 001a85 call0 0x4010f46c
4010f2c5: f2cc bnez.n a2, 0x4010f2d8
4010f2c7: 120c movi.n a2, 1
4010f2c9: 044f22 s8i a2, a15, 4
4010f2cc: 024c movi.n a2, 64
그리고 디스어셈블된 코드다.
해석은 대체로 비슷한 것 같은데
가장 큰 차이점은 캡스톤과 달리 해석이 멈추는 부분이 없다는거다.
그래도 제대로 해석된건지는 다시 확인이 필요하다.

아까 capstone에서 해석이 멈췄던 부분을 확인해봤다.
0x92cc가 하나의 명령어로 해석됐다.
코드를 봐도 특별히 어색한 부분은 없다.
툴체인으로 해석한 명령어가 대체로 더 정확한 것 같다.
근데 문제는 영역의 구분 없이 모든 데이터를 명령어로 해석했다는 점이다.
capstone은 코드가 연결이 안되고
ghidra는 해석 자체가 제대로 안되는데
objdump는 진짜 코드와 가짜 코드가 섞여있다.
돌겠네 진짜
문자열이 들어가있던 영역을 확인해보자.
$ strings -t x esp_backup.bin | grep private
70f82 private key loaded
>>> hex(0x4010f000 + 0x70f82)
'0x4017ff82'
0x4017ff82 근처에 있을거다.
>>> print("private key loaded".encode("utf-8").hex())
70726976617465206b6579206c6f61646564

여기부터다.
마지막에 64 00으로 널바이트까지 제대로 있다.
주소는 0x4017ff7a이다.
cat disasm.txt | grep "0x4017ff7a"
참조하는게 있나 확인해봤지만 나오는게 없다.
아마 세그먼트 별로 로드되는 주소가 달라서 부정확하게 나오는 것 같다.
세그먼트에 대한 조사가 선행되어야 할 것 같다.
'RSW-725R > 분석 자료 (공개용)' 카테고리의 다른 글
| 6. 복호화 로직 분석 (0) | 2026.04.22 |
|---|---|
| 5. 세그먼트 탐색 및 추출 (0) | 2026.04.22 |
| 3. 동적 분석 (0) | 2026.04.22 |
| 2. UART 연결 및 펌웨어 추출 (0) | 2026.04.22 |
| 1. 하드웨어 분석 (0) | 2026.04.22 |