← 홈으로

2026 · 개인 · 비공개

myfan

BLE 서큘레이터 프로토콜 리버스 엔지니어링

역할
분석·구현 전부
기간
2026.08
상태
해독 완료 · 대체 앱 사용 중
스택
Kotlin, Android, BLE, Reverse Engineering, Python
링크

배경

르젠 BLDC 서큘레이터 LZEF-DC290은 순정 앱으로만 조작됩니다. 앱이 무겁고 위젯도 없어서, 본인 소유 기기와의 상호운용을 목적으로 프로토콜을 해독하고 대체 앱을 만들었습니다. 순정 앱을 복제하거나 배포하지 않습니다.

밝혀낸 것

  • 접속형이 아니라 브로드캐스트형: 순정 앱은 BLE 연결도 스캔도 하지 않습니다. 명령을 광고 패킷의 제조사 데이터(0xFFF0)에 실어 약 200ms 뿌리고 끝냅니다. 선풍기는 듣기만 하므로 BLE 스캐너로는 영원히 찾을 수 없습니다.
  • 프레임 구조: 평문 33바이트 중 뒤 18바이트만 전송: sync 3 · 주소 5 · 헤더 2 · 명령 1 · 기기 ID 4 · 체크섬 1 · CRC16 2.
  • 화이트닝 2단: BLE 표준 LFSR(x⁷+x⁴+1)을 시드 63과 37로 두 번 거는데, 두 시드가 상수라 결국 고정 마스크 한 번의 XOR로 축약됩니다. 인코딩과 디코딩이 같은 연산입니다.
  • 비표준 CRC16: poly 0x1021·init 0xFFFF이지만 주소는 역순, 페이로드는 비트반전해서 넣습니다. 1차 분석에서 560가지 파라미터 조합을 전수 대입했는데 전부 빗나간 이유였습니다.
  • “세션 논스”는 논스가 아니었다: 앱을 껐다 켤 때마다 바뀌던 4바이트는 앱이 난수로 만든 기기 ID였습니다. 그 사이에 다른 기기를 등록하고 지웠기 때문에 바뀐 것이었습니다. 난수도 세션도 없어서 같은 (명령, 기기 ID)는 항상 같은 18바이트입니다.
  • 기기 삭제는 브로드캐스트: cmdType 15는 기기 ID를 무시하고 전파가 닿는 모든 기기에 갑니다. 대체 앱에서는 넣지 않았습니다.

검증

캡처한 63프레임 전부 재인코딩 왕복 일치. 인코더는 Python(tools/rf297l.py), 안드로이드 쪽은 실제 캡처 프레임과 대조하는 단위 테스트로 검증합니다.

대체 앱

Kotlin, 의존성 3개, 권한은 BLUETOOTH_ADVERTISE 하나. BluetoothLeAdvertiser.startAdvertising() 한 번이면 끝이라 연결·페어링·재연결·GATT 캐시가 전부 필요 없습니다. 홈화면 위젯 5종(명령 하나 · 전원 토글 · 풍량 · 전원+풍량 등). R8은 위젯 프로바이더가 이름으로 부르는 메서드를 지울 수 있어 껐습니다.