MIDI 메시지를 통한 원격 코드 실행
(psi3.ru)- 오래된 Yamaha PSR-E433 신시사이저의 SWL01U 칩을 리버스 엔지니어링해, USB-MIDI SysEx 메시지만으로 RAM에 코드를 쓰고 LCD에 Bad Apple 영상을 띄움
- JTAG IDCODE
0x3f0f0f0f와 OpenOCD/GDB 실험으로 칩이 ARM7TDMI 코어처럼 동작함을 확인하고, 내부 64KiB ROM과 외부 16MiB 플래시 펌웨어를 덤프함 - 펌웨어 안에는 MIDI SysEx 위에서 동작하는 숨겨진 셸이 있었고,
login과 비밀번호#0000뒤에 메모리 읽기/쓰기 명령을 사용할 수 있었음 - 임의 메모리 쓰기 명령으로 RAM에 ARM 코드를 주입한 뒤 스택의 반환 주소를 덮어, JTAG나 UART 없이 MIDI 파일 재생만으로 코드 실행이 가능해짐
- LCD 출력은 CGRAM 제어, 태스크 테이블 복사, 디스플레이 태스크 비활성화, 셸 콜백 교체를 거쳐 프레임당 전송량을 6732바이트에서 92바이트로 줄이며 개선됨
Yamaha PSR-E433 내부 조사
- 대상 장비는 오래 사용하던 Yamaha PSR-E433 신시사이저였고, 메인 보드에는
DMLCD표기와 함께 두 개의 플래시 칩, RAM 칩,YAMAHA SWL01U칩이 있었음 - SWL01U에 대한 공개 정보는 거의 없었고, 온라인에서 찾은 글 하나만 SuperH CPU 코어 기반일 수 있다고 언급했음
- 비슷한 모델인 E443 서비스 매뉴얼에는 SWL01U 핀아웃이 있었고,
TESTN,PROTN, 두 개의 양방향 UART, JTAG 테스트 포인트가 표시돼 있었음 - 초기 접근은 네 갈래였음
TESTN,PROTN핀을 조작해 부팅 모드 변화 확인- UART Tx 핀에 납땜해 출력 확인
- JTAG로 칩 식별 코드 읽기
- 플래시 칩을 분리해 펌웨어 덤프
TESTN을 활성화하면 신시사이저가 부팅하지 않았고,PROTN은 동작에 변화를 주지 않았음- 사용되지 않는 UART Tx 핀에 직접 납땜했지만,
TESTN/PROTN네 가지 조합 모두에서 출력은 없었음
JTAG로 드러난 ARM7TDMI 동작
- JTAG는 벤더별 구현에 따라 세부 회로 설명이 필요하지만, 거의 모든 장치가 지원하는 IDCODE 읽기를 OpenOCD로 먼저 시도함
- OpenOCD는 IDCODE를
0x3f0f0f0f로 보고했고, 이 값은 STMicroelectronics STR7xxx 또는 Atmel SAM7xxx 계열 같은 ARM7 기반 마이크로컨트롤러와 연결될 수 있어 보였음 - SWL01U를 OpenOCD의
arm7tdmi타깃으로 지정하자 정상적으로 연결됐고, 하드웨어에 2개의 breakpoint/watchpoint 유닛이 있다고 표시됨 - GDB로 실행을 멈추고 재개하면 보드 전류가 예측 가능하게 바뀌었음
- 실행 중 약 115mA
- 일시정지 중 약 98mA
- 이 전류 변화는 실제로 ARM7TDMI 코어를 멈추고 재개하고 있다는 강한 신호가 됨
ROM과 플래시 펌웨어 덤프
- ARM7TDMI 문서에 따르면 reset vector는 주소
0에 있고, GDB로 주소0을 읽자ldr pc, [pc, #24]형태의 점프 명령이 보였음 - 주소
0에서 16MiB를 덤프해 Cutter로 열었지만, 문자열이 64KiB마다 반복됐음- 예:
SWL01U Internal이0x0000bfd0,0x0001bfd0,0x0002bfd0등에 반복됨
- 예:
- 반복 패턴과 문자열 때문에 이 덤프는 외부 플래시가 아니라 칩 내부 메모리로 판단됐고, SWL01U에는 64KiB ROM이 있는 것으로 정리됨
- reset vector의 점프 대상은
0x02000000이었고, 이 주소에서 다시 16MiB를 덤프하자 반복은 없었음 - 외부 플래시 덤프에는 신시사이저 사용 중 볼 수 있는 문자열이 들어 있었음
GrandPnoTr1 will be OverWritten!BogiWogi
- 확인된 메모리 배치는 다음과 같음
- 내부 ROM:
0x00000000, 64KiB - 외부 플래시:
0x02000000, 16MiB - 부팅 시 ROM이 즉시 외부 플래시로 제어를 넘김
- 내부 ROM:
Ghidra로 찾은 숨겨진 셸
- Cutter만으로는 분석이 부족해 Ghidra로 전환했고, 문자열과 xref를 따라가며 펌웨어 구조를 파악함
help,?,info,ver같은 문자열이 가까운 주소에 모여 있었고, 각 문자열은 명령 이름과 함수 포인터의 쌍으로 보이는 배열에 연결돼 있었음- 명령 처리 함수는 상태 머신 형태였고,
login문자열과Passwd Error문자열을 통해 로그인 절차가 있는 셸임을 확인함 - 셸 입력 처리는 256바이트 원형 버퍼를 순회하며 문자 단위로 처리하고,
\r문자를 만나면 명령을 실행하는 구조였음 - 로그인 흐름은 다음과 같았음
login입력 시passwd?출력- 비밀번호
#0000입력 시login OK - 이후 명령 실행 가능
- 확인된 셸 명령에는 다음이 포함됨
logout,help,?,info,verstack,perf-on,perf-off,perf-dispd,dp,d xxxxx,d/s xxxxxm ADDRESS DATA,m/b ADDRESS DATA,m/w ADDRESS DATA,m/l ADDRESS DATA
info명령은 다음 정보를 반환했음DevelopName PSR-E433DevelopNumber #3341Main DevelopNumber #3341Make data & time MAY 16 2012 19:00:57J/E Select English
USB-MIDI SysEx 위에서 동작하는 셸
- 셸 출력 함수는 각 바이트를 상위/하위 4비트 니블로 나눠 별도 바이트에 담고, 앞뒤에 고정 헤더와 footer를 붙였음
>프롬프트에 해당하는 패킷 구조는 다음과 같았음- 헤더:
F0 43 73 01 52 19 00 00 - payload:
03 0E 02 00 - footer:
F7
- 헤더:
- MIDI SysEx 메시지는
0xF0으로 시작해 제조사 ID와 payload를 거쳐0xF7로 끝나며, payload에는 MSB가 0인 바이트만 담을 수 있음 - 헤더의
0x43은 Yamaha 제조사 ID였고, 셸 패킷 구조는 Yamaha SysEx 메시지 형식과 맞아떨어졌음 - 신시사이저의 USB 디스크립터에는 MIDI 인터페이스만 있었고, 별도 시리얼 포트는 없었음
- Python 스크립트로 터미널과 셸 프로토콜 사이를 변환하자 USB-MIDI를 통해 셸과 대화할 수 있었음
MIDI Shellcode로 코드 실행
- 셸의
m/l AAAAAAAA DDDDDDDD\r명령은 32비트 메모리 쓰기를 수행하며, 주소와 데이터를 16진수 ASCII로 전달함 - 4바이트 payload를 쓰는 과정에서도 실제 전송량은 크게 불어났음
- 명령 바이트는 각각 두 개의 4비트 니블 바이트로 변환됨
- SysEx 메시지에 9바이트가 추가됨
- 3바이트마다 4바이트 USB-MIDI 패킷으로 감싸짐
- 4바이트 쓰기에 신시사이저로 72바이트를 보내야 함
- 에코와 프롬프트까지 포함하면 총 396바이트가 오감
- 사용되지 않는 것처럼 보이는 RAM 영역을 찾아 ARM 어셈블리 코드를 배치했고, 스택 반환 주소를 덮어 해당 코드를 실행함
- 첫 payload는 펌웨어 내부의 문자열 출력 함수를 호출해 LCD의 8글자 텍스트 영역에
HeloWrld를 표시했음 - 이 방식은 JTAG나 UART 없이도 동작하며, 메시지를 MIDI 파일에 넣어 재생하는 것만으로 실행할 수 있었음
- PSR-E433 펌웨어 1.02용 MIDI 파일도 제공됐지만, 다른 Yamaha 장치나 다른 펌웨어 버전의 PSR-E433에서 재생하면 예측 불가능하게 동작할 수 있다고 경고함
Bad Apple을 LCD에 표시하기
- Yamaha PSR-E433의 LCD 컨트롤러는 ML9040A이며, 기본적으로 점 행렬 텍스트 문자를 처리하는 구조였음
- LCD에는 점 행렬 영역뿐 아니라 음표 표기, 7세그먼트 영역, 코드 표기, 하단 키보드 표시 영역도 있었음
- ML9040A에는 세 가지 메모리가 있었음
- DDRAM: 표시할 문자 데이터를 호스트가 기록
- CGROM: 문자 코드를 그래픽 패턴으로 변환
- CGRAM: 호스트가 최대 8개의 사용자 정의 문자를 정의
- 펌웨어는 CGRAM을 조작해 점 행렬 아래의 비텍스트 표시 요소를 제어했고, 이 경로를 이용해 사용자 정의 그래픽을 표시할 수 있었음
- 임의 데이터를 LCD 컨트롤러로 보내는 펌웨어 함수를 찾아 CGRAM에 체크 패턴을 올렸지만, 펌웨어가 계속 CGRAM을 갱신해 곧 덮어썼음
RAM 조작으로 디스플레이 갱신 제어
- 플래시를 직접 덮어쓰면 장치를 벽돌로 만들 위험이 있어, 모든 실험은 전원 재시작으로 되돌릴 수 있도록 RAM 조작으로 제한함
- 펌웨어에는 원시적인 RTOS처럼 보이는 구조가 있었고, 플래시에는 64개 태스크의 콜백 함수, 스택, 속성을 정의하는 전역 테이블이 있었음
- 부팅 시 플래시 펌웨어가 ROM에 태스크 테이블 위치를 알려주고, ROM은 그 위치를 내장 SRAM의 전역 변수에 저장했음
- 태스크 테이블을 RAM으로 복사한 뒤 ROM이 새 테이블을 사용하도록 바꾸면, 플래시를 수정하지 않고 태스크 콜백을 교체할 수 있었음
- 디스플레이 갱신 태스크의 콜백을 기본 idle 콜백으로 바꿔, 펌웨어가 CGRAM을 계속 덮어쓰지 못하게 만들었음
전송 효율과 화면 깨짐 개선
- Bad Apple 첫 구현은 동작했지만, 전송 효율이 낮아 프레임률이 매우 낮고 화면 아티팩트가 있었음
- 프레임마다 CGRAM 64바이트와 32비트 반환 주소 덮어쓰기만 보내도, 실제 전송량은 70바이트 payload당 6732바이트였음
- 낮은 효율의 주요 원인은 두 가지였음
- 데이터가 셸 명령 형태로 감싸져야 함
- 신시사이저가 명령을 문자마다 큰 SysEx 패킷으로 에코함
- 셸 태스크 콜백을 직접 만든 콜백으로 교체해 raw 데이터를 받고 응답하지 않도록 만들자, 명령 래핑과 에코 오버헤드를 제거할 수 있었음
- 추가 패킹 최적화 뒤 프레임당 전송량은 6732바이트에서 92바이트로 줄었고, 이는 73배 감소였음
- 남은 아티팩트는 LCD 통신과 패널 버튼/LED 스캔이 같은 8개 GPIO 라인을 공유하기 때문에 발생했음
- 최종 구현은 LCD에 직접 쓰지 않고, LCD/패널 멀티플렉싱 태스크가 패널 스캔을 마친 뒤 원하는 데이터를 보내도록 요청해 깨짐을 피했음
최종 동작 절차와 남은 분석 대상
- MIDI로 LCD 영상을 표시하는 최종 절차는 다음과 같음
- 셸에 로그인
- 셸의 메모리 쓰기 명령으로 실행 코드를 RAM에 기록
- 스택 반환 주소를 덮어 RAM 코드를 실행
- 태스크 테이블을 RAM으로 복사
- 새 태스크 테이블들이 서로를 가리키도록 수정
- ROM이 새 태스크 테이블을 사용하도록 변경
- 디스플레이 태스크 콜백을 기본 idle 콜백으로 교체
- 셸 태스크 콜백을 자체 콜백으로 교체
- 자체 콜백에서 MIDI 데이터를 언팩해 LCD/패널 멀티플렉싱 태스크로 전달
- MIDI로 영상 프레임을 공급
- SWL01U의 MMIO 영역 이해는 아직 제한적이며, 메인 ARM 코어와 별개인 DSP도 남은 분석 대상임
- 관련 자료
댓글과 토론
Hacker News 의견들
-
SuperH는 Sega 32X, Sega Saturn, Sega Dreamcast에도 들어갔고, HP Jornada 같은 초기 Pocket PC 일부에도 쓰였음
다만 대부분의 Pocket PC는 ARM 기반이었음- 산업용에서도 많이 쓰였고, Mitsubishi는 Lancer Evolution을 포함한 일부 차량의 ECU에 이 칩을 사용했음
-
전제가 터무니없을 정도인데 실제로 해낸 게 놀라움
“또 다른 패킹 최적화”를 언급했는데, 프레임을 어떻게 전송하는지 궁금함
도트 매트릭스가 7x5 문자 8개라면 총 280비트, 즉 프레임당 7비트 그룹 40개인데, 전송에는 그 두 배 공간을 쓰는 것처럼 보임
제어 데이터 때문에 낭비되는 건지, 아니면 전송 방식이 약간 비최적인 건지 궁금함- 도트 매트릭스는 실제로 5x8 문자 8개라서 총 320비트이고, 이 320비트를 셸 프로토콜에서 쓸 수 있는 바이트당 4비트에 패킹하고 있음
여기에 패킷 헤더와 푸터 9바이트가 추가됨
글에는 92라고 쓴 것 같은데 계산을 잘못한 듯함
전체 7비트를 쓰는 방법을 찾는 게 너무 어려워서, 원래 방식과 비교하면 최적해보다 아주 조금 나쁜 정도의 해법을 택했음
정확한 알고리즘이 궁금하면 아직 정리되지 않은 코드지만 이 파일들을 보면 됨: https://github.com/portasynthinca3/swl01u/blob/master/fun/bi..., https://github.com/portasynthinca3/swl01u/blob/master/fun/ba...
- 도트 매트릭스는 실제로 5x8 문자 8개라서 총 320비트이고, 이 320비트를 셸 프로토콜에서 쓸 수 있는 바이트당 4비트에 패킹하고 있음
-
“리버스 엔지니어링은 경험이 많지 않다”는 말이 이 정도라면, 나머지 사람들은 어디쯤에 있는 건지 모르겠음
- 많이 알지만 아무것도 모른다는 걸 자각하는 건 경험치 4단계쯤 같음
1단계는 새롭고 열정적이지만 아직 모른다는 걸 아는 상태, 2단계는 “나는 신이다”, 3단계는 “나는 바보다” 단계임 - 특히 뛰어난 비전문 엔지니어들이 이런 말을 자주 함
- 많이 알지만 아무것도 모른다는 걸 자각하는 건 경험치 4단계쯤 같음
-
“세계 최초 MIDI 셸코드”라고 했지만, 주요 플랫폼 대부분에서 MIDI 셸코드는 20년도 더 전부터 있었음: https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=midi
- 버퍼 오버플로는 많지만, 그 취약점들에 대해 실제로 셸코드를 작성한 사람이 있었는지는 별개임
-
당연히 SysEx였음
표준 MIDI에서 SysEx는 Python에서 인라인 어셈블러 같은 존재임
거의 모든 MIDI 장치 안에는 문서화되지 않은 독점적인 것들이 숨어 있음- 음악 한 곡을 연주해서 원격 코드 실행을 일으킬 수 있으면 좋겠음
MIDI 키보드를 꽂고 Am6,9/G#를 연주했더니 루트 권한 터미널 창이 열리면 정말 멋질 듯함 - Google이 지난 몇 년간 만들고 있는 Chrome MIDI 지원에 어떤 SysEx 해킹을 구겨 넣을지 기대됨
- 이런 세계가 있는 줄 전혀 몰랐음
최근 MIDI 퍼징을 하려면 무엇이 필요한지 찾아봤고, 이를 위해 .mid 파일을 생성하는 자료는 찾았지만 원하던 것과는 조금 달랐음
대신 이쪽을 탐색해볼 만해 보임 - SysEx는 훌륭함
요즘 신시사이저들이 점점 덜 쓰는 것 같아 꽤 아쉬움, 특히 Roland
그래도 Behringer는 아직 꽤 잘 지원함
예를 들어 Deepmind는 이미 MIDI CC 범위도 좋은데, SysEx로 거의 100% 프로그래밍 가능함
- 음악 한 곡을 연주해서 원격 코드 실행을 일으킬 수 있으면 좋겠음
-
놀라운 연구임
실제 DNA/RNA 분자에 셸코드를 합성해서 DNA 시퀀싱 장비에서 원격 코드 실행을 보였던 2017년 연구가 조금 떠오름: https://www.usenix.org/conference/usenixsecurity17/technical...
“다음은 OSC인가”라고 말하려 했지만, 아직은 MIDI가 지배적인 듯함 -
전체 글을 읽는 걸 추천하지만 핵심 문장들은 이렇다고 봄
“이 [키보드 제조사] 미친 사람들이 USB 위의 MIDI SysEx 메시지 위에서 도는 셸을 만들어놨다”
“가장 흥미로운 명령은 임의 메모리 읽기/쓰기 명령이다. 원한다면 MIDI로 신시사이저 메모리를 들여다보고 찔러볼 수 있다”
“원한다면 이 메시지들을 MIDI 파일에 써서 다른 MIDI 파일처럼 신시사이저에서 재생할 수 있다. 어라, 좋은 생각이 떠오르는데…”
“펌웨어를 파고든 수많은 잠 못 이룬 밤 끝에, 임의 데이터를 LCD 컨트롤러로 보내는 함수를 발견했다”- 이제 진짜 질문은 키보드에서 실행 중인 코드를 바꿔서, 같은 모델의 다른 키보드가 이 MIDI 데이터를 받으면 감염을 시도하게 만들 수 있느냐임
어떤 면에서는 사물인터넷 악몽을 살짝 엿보는 셈임
거의 어떤 장치든 백도어가 있을 수 있고, 심지어 #0000 같은 멍청한 백도어일 수도 있음 - 이걸 MIDI 파일처럼 재생하면 결과물이 덥스텝일 것 같음
- MIDI로 신시사이저 메모리를 읽고 쓸 수 있다는 말은 쉬워 보이지만, SysEx에는 전달 보장도 없고 연결이나 세션 개념도 없어서 답답할 수 있음
패킷 손실이 생기는 게 완전히 정상임
- 이제 진짜 질문은 키보드에서 실행 중인 코드를 바꿔서, 같은 모델의 다른 키보드가 이 MIDI 데이터를 받으면 감염을 시도하게 만들 수 있느냐임
-
Bad Apple 명령 사이에 MIDI 음악을 끼워 넣어서 오디오도 자체 재생하게 할 수 있는지 궁금함
-
저장소 README에는 이미지 덤프가 들어 있다고 되어 있는데 실제로는 없음
이게 맞는 상태인지 궁금함- 실수였음
마지막 순간에 어셈블된 코드 조각을 제외하려고*.bin을.gitignore에 추가했는데, 덤프도 같이 제외된 듯함
몇 시간 안에 업로드할 예정임 - 그런 것 같음
해당 덤프들은 Yamaha 저작권으로 보호될 가능성이 있어서 오히려 좋은 판단일 수도 있음
- 실수였음
-
혹시 HN 독자 중 Armenia에 있는 사람이 있다면, Porta가 1월 10일 Hacker Embassy에서 이 주제로 발표함
꼭 와보면 좋겠음: https://t.me/hackerembassy/17