Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
8cryptdo — 8BitDo 펌웨어 암호화에 대한 리버스 엔지니어링 문서 및 도구 | Kitploit
도구/GitHubGitHub/aeromodes/8cryptdo
Embedded Systems SecurityEncryption/Decryption ToolsIoT SecurityReverse EngineeringCryptographyHardware & IoT SecurityBinary AnalysisPapers & ResearchFirmware Analysis
GitHubaeromodes/8cryptdo

8cryptdo

8BitDo 펌웨어 암호화에 대한 리버스 엔지니어링 문서 및 도구

291일 전아직 검토되지 않음

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
저장소 보기

8CryptDo

8BitDo가 여러 GD32 기반 제품에 사용하는 펌웨어 암호화에 대한 리버스 엔지니어링 문서 및 도구입니다.

이 문서에서 설명하는 알고리즘을 적용한 복호화 및 재암호화 도구는 8cryptdo.py를 참조하세요.

이 도구는 잠재적으로 맞춤형 펌웨어를 생성할 수 있는 가능성을 제공합니다. 그러나 이 저장소에는 공식 펌웨어 데이터가 포함되어 있지 않습니다. 최신 펌웨어 파일을 다운로드하는 스크립트는 fwupd/8bitdo-firmware 저장소에서 찾을 수 있으며, 일부 구형 펌웨어 바이너리도 보관되어 있습니다.

헤더

펌웨어 .dat 파일을 겉으로 보면 28바이트의 평문 헤더가 존재합니다.

root@kitploit:~
import struct

raw = open("sn30-v2_07.dat", "rb").read()
version, addr, payload_len = struct.unpack("<III", raw[:12])
# version     = 207        -> firmware == v2.07
# addr        = 0x08003400 -> destination (probably)
# payload_len = 99328      -> section payload length in bytes

최근 펌웨어 파일에는 이 뒤에 알 수 없는 32비트 값이 위치합니다. 헤더의 나머지 부분은 0입니다.

키 없는 체이닝 계층

특정 펌웨어 파일에서는 패턴이 나타납니다. 각 워드를 다음 워드에 섞는 계층이 있습니다.

root@kitploit:~
def rotr(x, r):
    return ((x >> r) | (x << (32 - r))) & 0xFFFFFFFF

dechained = [words[0]]
for i in range(1, len(words)):
    dechained.append(words[i] ^ rotr(words[i - 1], 3))

이 체이닝은 128워드 블록 경계마다 초기화됩니다. 블록의 첫 번째 워드(pos = 0)에는 선행 항이 없으며, pos = 1..127은 이전 워드에서 체인됩니다.

키 없는 계층은 보안을 추가하지 않습니다. 이를 벗겨내면 더 깔끔한 중간 상태(dechained)가 드러나며, 여기서 평문을 숨기는 유일한 요소는 키스트림뿐입니다.

root@kitploit:~
P[i] = dechained[i] ^ keystream[i]

키스트림 인수분해

fwupd/8bitdo-firmware 저장소에는 구형 펌웨어 카탈로그가 있습니다. 이 펌웨어들은 모두 체이닝 계층을 가지고 있는 것으로 보입니다.

체이닝 계층을 제거한 후 일부를 서로 XOR하면 긴 0의 연속과 읽을 수 있는 구조가 드러납니다.

root@kitploit:~
plaintext_diff = [a ^ b for a, b in zip(dechained_a, dechained_b)]

만약 동일한 키스트림이 둘 다에 사용된다면...

root@kitploit:~
a = dechained_a[i] ^ dechained_b[i]
b = (P_a[i] ^ keystream[i]) ^ (P_b[i] ^ keystream[i])
c = P_a[i] ^ P_b[i]
assert a == b == c

키스트림이 상쇄됩니다.

이것은 전형적인 *다중 사용 일회용 패드(many-time pad)*입니다. 이러한 키스트림은 한 번만 사용할 때만 안전합니다. 어떤 이유에서인지 8BitDo는 제품별 키스트림뿐만 아니라 여러 제품에 걸쳐 단일 키스트림을 사용하기로 결정했습니다. 이는 암호의 보안을 상당히 약화시켰고 여기서의 나머지 분석을 가능하게 만들었습니다.

키스트림 읽기

두 평문이 XOR되어 있다는 것을 알게 되면, 하나의 일부를 추측하고 추측값을 빼서 다른 하나를 읽을 수 있습니다. 이렇게 하면 해당 지점의 keystream을 복구할 수 있습니다.

root@kitploit:~
keystream[i] = dechained[i] ^ P[i]
for fw in all_fw:
    P[fw][i] = dechained[fw][i] ^ keystream[i]

문자열을 사용하면 가장 쉬웠겠지만, 여기서 가장 생산적인 기법은 ARM 명령어의 버전 간 재배치였습니다. 하나의 함수가 두 펌웨어 버전에서 이동된 위치에 나타날 수 있습니다. 한 버전에서 키스트림이 알려진 곳에서 다른 버전의 공유 코드를 정렬하여 새로운 키스트림 값을 수확할 수 있습니다.

이를 활용하여 알려진 키스트림 워드를 약 1,500개에서 수천 개로 늘렸으며, 이는 펌웨어의 약 18%를 읽을 수 있게 한 것입니다.

키스트림 규칙

더 큰 keystream 샘플을 확보하자 패턴이 보이기 시작했습니다. 16워드 간격의 위치들이 거의 일정한 양만큼 증가했고, 각 바이트는 예측 가능한 크기만큼 이동했습니다.

시행착오를 통해 블록 내 모든 키스트림 워드가 다음 공식을 따른다는 것을 발견했습니다.

root@kitploit:~
STEP  = 0x92A753FA
A_MUL = 0x80000301
ROT   = 18
UINT32_MAX  = 0xFFFFFFFF

def rotr(x, r):
    r &= 31
    return ((x >> r) | (x << (32 - r))) & UINT32_MAX if r else x

def block_base(block):
    return (block * A_MUL + block // 2) & UINT32_MAX

def keystream(block, pos, block_key):
    counter = (block_base(block) + pos * STEP) & UINT32_MAX
    mask = rotr(block_key, ROT * pos)
    return counter ^ mask

블록 내에서 간단한 카운터(block_base(block) + pos*STEP)를 진행시키고, block_key에서 시작하여 매 단계마다 18비트씩 회전하는 마스크와 XOR합니다. 전체 128워드 블록은 하나의 32비트 숫자 block_key에 의해 결정됩니다.

알려진 워드 하나가 전체 블록을 해제합니다. 공식을 재배열하면 알려진 키스트림 워드로부터 block_key를 얻을 수 있습니다.

root@kitploit:~
def block_key_from_known(block, pos, known_keystream):
    counter = (block_base(block) + pos * STEP) & UINT32_MAX
    return rotr(known_keystream ^ counter, -ROT * pos & 31)

시드 테이블

각 블록의 키는 어디에서 왔을까요? 이는 256개 항목의 시드 테이블에서 가져온 두 개의 16비트 절반으로 인수분해됩니다.

root@kitploit:~
def block_key_of(block, seed_table):
    hi = seed_table[block]
    lo = seed_table[block ^ 0xF9]
    return (hi << 16) | lo

시드 테이블 자체는 특별한 공식이 없는 것으로 보이며, 아마도 각 장치의 부트로더 어딘가에 저장된 512개의 상수 바이트일 것입니다. 그러나 0xF9와의 XOR은 아름다운 부작용을 제공합니다. 바로 *거울 법칙(mirror law)*입니다. 블록과 그 파트너 block ^ 0xF9는 동일한 두 테이블 항목을 서로 교환하여 구성됩니다.

root@kitploit:~
mirror = block_key_of(block ^ 0xF9, seed_table)
assert mirror == rotr(block_key_of(block, seed_table), 16)

이것은 이전에 알려지지 않았던 영역을 해독했습니다. 전체 32비트 block_key를 맹목적으로 추측하는 대신, 두 개의 16비트 절반을 추측하고 어떤 선택이 두 블록을 그럴듯한 문자열이나 ARM 명령어로 바꾸는지 점수화할 수 있습니다.

이로써 이 특정 암호를 사용하는 모든 펌웨어 데이터의 100% 커버리지를 얻었습니다.

남은 작업

이 암호는 2020년경의 대부분의 8BitDo 제품과 여전히 GD32 SoC를 사용하는 현재 제품(SN30 Pro 포함)을 포괄하는 것으로 보입니다.

M30과 Zero 2 같은 일부 장치는 다중 섹션으로 보이며, 일부 펌웨어 데이터는 이 암호를 사용하고 다른 펌웨어는 다른 암호화 방식을 사용합니다.

더 새로운 장치, 적어도 다른 SoC를 사용하는 장치들은 더 강력한 펌웨어 암호화 방식을 가진 것으로 보입니다. 일부 순진한 분석에 따르면 공유될 가능성이 있지만 확실하지 않습니다. 추가 조사가 필요합니다.

도구 다운로드