- Texts.com 팀은 자사와 비슷한 독립형 데스크톱 앱인 macOS용 Meta Messenger를 분석하려 했지만, 인증서 핀닝 때문에 프록시 기반 MITM 분석이 막힘
- Meta의 인증서 핀닝은 앱이 허용한 인증서만 신뢰하게 만들어, 사용자가 만든 인증 기관으로 요청을 가로채고 복호화하는 방식을 차단함
- Frida 기반 동적 계측은 Messenger에서 크래시와 배포 복잡도가 커서, 팀은 더 작고 재현 가능한 바이너리 패치를 선택함
- Hopper 분석 결과
IsUsingSandbox()가 true를 반환하도록 4바이트를 바꾸면, custom sandbox 사용 시 SSL 검증을 끄는 코드 경로를 탈 수 있었음
- 패치한 실행 파일로 원본 바이너리를 교체하고 서명을 처리하자 프록시 도구에서 헤더, 응답 본문, 요청 정보를 확인할 수 있게 됨
Texts.com이 Messenger를 분석한 이유
- Texts.com에서 Meta 플랫폼 프로젝트를 맡은 Batuhan İçöz는 macOS용 Messenger 앱이 자사 모델과 가까운 독립형 데스크톱 앱이라 분석 가치가 있다고 판단함
- 네트워크 요청 가로채기는 진입 장벽이 낮아 앱 동작을 이해하는 첫 단계로 활용하기 좋음
- 하지만 Meta는 앱에 인증서 핀닝을 적용해 보안 모델을 강화하고, 사용자가 자기 자신을 대상으로 수행하는 MITM 분석까지 막음
인증서 핀닝이 막는 것
- 프록시 클라이언트로 요청을 가로채려면 사용자가 만든 인증 기관을 설정하고 신뢰해야 함
- 해당 인증 기관이 발급한 인증서를 통해 요청 정보를 가로채고 복호화할 수 있음
- 서비스가 인증서 핀닝을 구현하면 앱은 특정 인증 기관이 발급한 인증서만 받아들임
- 이 경우 사용자가 만든 인증서는 유효하지 않으므로 요청을 가로챌 수 없음
패치 전 상태와 목표
- 인증서 핀닝을 끄지 않으면 모든 요청이 “Internal Error”를 반환함
- 프록시 소프트웨어에는 “SSL Handshake Failed”가 표시되고 요청 생명주기가 끝까지 진행되지 않음
- 이 상태에서는 요청 내용을 추론하기 어려움
- 목표는 네트워크 디버깅 도구에서 요청, 응답, 헤더를 직접 읽을 수 있게 만드는 것임
실패한 우회안과 최종 선택
- 과거에 동작했던 한 방법은 바이너리 안의 URL 문자열을 TLS를 구현하지 않는 자체 호스팅 엔드포인트로 바꾸는 방식임
- 이 엔드포인트가 클라이언트와 서버 사이에서 요청과 응답을 전달함
- Messenger처럼 큰 앱보다는 작은 앱에 더 적합함
- Frida 같은 동적 계측 라이브러리도 후보였지만 Messenger에서는 안정성이 낮았음
- 훅을 걸 때 크래시가 잦았음
- 오버헤드 때문에 문제가 되는 지점을 찾기 어려웠음
- 실행에 필요한 환경과 도구 구성이 있어 팀원에게 배포하기 복잡함
- 수년간 유지해 온 Frida 스크립트도 시도함
- 이 스크립트는 일반적인 인증서 핀닝 라이브러리와 우회 방식에 쓰였고 대부분의 앱에서 동작함
- Meta 앱군은 이 “대부분”에 포함되지 않았음
- 결국 팀원에게 쉽게 전달할 수 있는 바이너리 패치로 인증서 핀닝을 완전히 끄는 방식을 택함
Hopper로 찾은 패치 지점
- Messenger를 다운로드해 애플리케이션 폴더로 옮긴 뒤
/Applications/Messenger.app/Content/MacOS/Messenger의 컴파일된 ARM 바이너리를 Hopper로 가져옴
- Hopper는 컴파일된 바이너리를 디스어셈블, 디컴파일, 재컴파일, 디버그, 시각화할 수 있음
- 바이너리와 참조 로딩 후
certificate, ssl, pinning 같은 용어를 검색함
"SSL pinning verification failed for host:" 문자열이 분석 출발점이 됨
- 컴파일된 바이너리는 과도하게 수정하면 크래시가 날 수 있어, 가능한 한 작은 변경만 적용하는 전략을 사용함
- 이상적인 변경은 불리언 값 뒤집기, 조건문 반전, 몇 개 명령어 수정처럼 영향 범위가 좁은 패치임
IsUsingSandbox()를 항상 true로 만들기
- 제어 흐름 그래프로 실행 흐름을 시각화하고 연결된 참조를 따라 올라감
"Using custom sandbox -> turn off SSL verification" 문자열을 찾음
- 이 플래그를 결정하는 함수의 참조를 파일에서 검색했고, 프로시저 상단에서 해당 참조를 확인함
IsUsingSandbox() 함수에서 반환값이 할당되는 지점을 추적함
w0 레지스터는 w19에서 이동된 뒤 반환됨
w19는 원래 load byte 명령으로 할당됨
w19를 로드하지 않고 항상 true로 설정하면 IsUsingSandbox()가 true를 반환함
- 앞서 찾은 문자열대로라면 custom sandbox 사용 시 SSL 검증이 꺼지므로, 이 변경으로 인증서 핀닝이 비활성화됨
Original
ARM: ldrb w19, [sp, #0x40 + var_20]
HEX: F3 83 40 39
Rewritten
ARM: mov w19, #1
HEX: 33 00 80 52
- 이 교체는 hexadecimal mode에서 애플리케이션의 바이트 코드를 직접 수정해 수행함
실행 결과와 재서명
- Hopper의 “Produce New Executable” 옵션으로 새 실행 파일을 내보냄
- 실행 파일의 서명을 제거한 뒤, 원래 Messenger 바이너리를 새 바이너리로 교체함
- Messenger를 다시 실행하자 프록시 도구에서 헤더, 응답 본문, 기타 요청 정보가 보임
- 전체 바이너리 크기 97,477,728바이트 중 4바이트 수정만으로 요청 가로채기가 가능해짐
- iOS에서 유사한 접근을 보고 싶다면 Hassan Mostafa의 2020년 Instagram 인증서 핀닝 우회 글을 볼 수 있음
- 해당 글은 탈옥한 iPhone에서 조건 분기 명령을 뒤집어 Instagram 인증서 핀닝을 해제한 사례임
- 컴파일한 바이너리는 Batuhan에게 전달됨
- Batuhan은 서명 인증서를 확보하고 설치한 뒤 애플리케이션에 서명함
- 이후 자신의 시스템에서 해당 바이너리를 사용해 자신의 요청을 볼 수 있었음
codesign --force --deep -s CERTNAME_OR_ID /Applications/Messenger.app