1P by GN⁺ | ★ favorite | 댓글 1개
  • 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

댓글과 토론

Hacker News 의견들
  • 비슷한 길로 가다가 디컴파일/수정/재컴파일까지 하려는 순간 포기했음
    이 정도면 집념이고, 실제로 몇 시간이 들었는지 궁금함. 나는 중단 기준을 정해두고 그대로 지켰음

    • 이 글은 원래 Texts.com 내부 글이었고, 몇 주 전 나도 정확히 같은 접근을 해보다가 정해둔 시간 한도에 걸려 포기했다는 부분은 공유용으로 다듬으면서 뺐음
      처음에는 2시간 동안 여러 명령어를 바꿔보다가 포기했음. 이후 리버스 엔지니어 “Hassan Mostafa”(cyclon3)가 예전에 같은 방식으로 성공한 글, 즉 iOS의 Instagram에 Hopper Disassembler를 적용한 글을 보고 그날 밤 다시 시도했지만 실패했음. 같은 명령어도 찾아서 바꿔봤음
      그러다 그만두기로 했고, 몇 주 뒤 약간의 앙금이 남은 상태에서 즉흥적으로 다시 시도했더니 샌드박스 함수를 찾은 뒤 30분쯤 만에 끝냈음
  • eBPF를 쓰면 TLS 암호화 전에 데이터를 읽을 수 있어 보임: Debugging with eBPF Part 3: Tracing SSL/TLS connections https://blog.px.dev/ebpf-openssl-tracing/

    • 편리한 방식이고, Frida 같은 것으로 TLS 송수신 함수를 후킹하는 다른 방법도 거의 확실히 가능함. 다만 인증서 고정 우회가 되면 연구자가 Burp Suite나 mitmproxy 같은 기존 도구로 트래픽을 흘려보낼 수 있다는 장점이 있음
      실제 앱 트래픽을 가로채는 프록시로 라우팅하면 목적에 따라 시간이 크게 줄어듦. 예를 들어 인증/세션 설정 이후에야 발생하는 요청의 매개변수 하나를 자동으로 바꾸고 싶다면, 초기 과정을 모두 수행하는 클라이언트를 새로 작성하거나 eBPF 필터로 수정 로직을 짜는 것보다 앱이 알아서 진행하게 두고 프록시에서 한 군데만 바꾸는 편이 훨씬 빠름
    • 참고로 이 방식은 가장 널리 쓰이는 Rust TLS 라이브러리인 rustls 를 정적으로 링크한 Rust 프로그램에는 통하지 않음
  • 아주 영리한 방식임. 다만 샌드박스 모드에서도 인증서 고정을 강제할 수는 있었을 것 같음
    대학 때 Snapchat을 중간자 공격으로 들여다보려 했는데, 거기도 인증서 고정을 써서 결국 못 풀었던 기억이 남

    • 나도 같은 걸 시도했고, 앱을 패치해서 요청을 가로채는 데까지는 성공했지만 요청 서명을 담당하는 공유 객체를 리버스 엔지니어링하려다 포기했음
      진입점조차 찾지 못했음. 비교적 작은 소셜 미디어 앱치고는 2015년에 이미 보안이 말도 안 되게 강했음
    • 사용자가 바이너리를 수정할 수 있다면 인증서 고정을 강제하기는 근본적으로 어려움
      샌드박스 모드에서 인증서 고정을 썼더라도, 고정 인증서 검사를 제거하는 다른 방법이 있었을 가능성이 큼
    • 맞음. 소비자 함수 안에서 샌드박스 플래그 함수의 출력만 true로 할당하게 만들면 구현할 수도 있었겠지만, 이 경우에는 지금 방식으로도 잘 동작했음 :)
    • 모바일 앱에서 이걸 시도했다가 막힌 사람이 이렇게 많다는 게 웃김
      이제는 아니지만
  • 이 글을 보니 +Orc 시절이 떠오름. 원치 않는 분기를 찾아 NOP 처리하는 법 같은, 당시에는 흔했던 지식이 많이 사라진 것 같음
    요즘은 배울 기술이 훨씬 많으니 그럴 만도 함
    [1]: https://en.m.wikipedia.org/wiki/Old_Red_Cracker

    • +Orc나 Fravia(RIP) 언급을 보면 늘 향수가 느껴짐
      그래도 NOP 패치를 하는 사람은 아직 많다고 봄. 다만 복잡도가 더 올라갔을 뿐임. DRM을 깨거나 임의의 모바일 앱을 헥스 편집기 등으로 조사하는 사람들은 계속 있음
      요즘 프로그램은 더 복잡해서 시작하기는 어려워졌지만, 동시에 필요한 지식은 더 접근하기 쉬워졌음
  • Meta 앱 트래픽을 가로채고 싶다면 굳이 그렇게 할 필요는 없음
    https://www.facebook.com/whitehat/bugbounty-education/261571...

    • 이건 Android에서만 동작함. 우리는 Android 앱을 가로채는 데 관심이 없었음
  • 이런 수정을 어렵게 만드는 데 런타임 바이너리 체크섬이 도움이 됐을까 궁금함
    모바일 앱에서는 표준 관행이 아닌가? iOS나 Android SDK가 이런 기능을 제공하나? 공식 릴리스 프로세스와 연결되고, 각자의 탈옥되지 않은 플랫폼에서 강제되는 식일 것 같음
    기본적인 질문이긴 한데, 최종 해법이 바이너리 몇 바이트를 수정하는 것이라서 막을 수 있어 보였음

    • macOS(데스크톱)이지 iOS(모바일)가 아님
    • 어차피 바이너리를 수정하면 재서명해야 하므로 같은 효과가 남
      탈옥되지 않은 플랫폼에서는 보통 개발자 인증서로 이걸 함
  • Meta, 적어도 Messenger의 리버스 엔지니어링 방어는 꽤 느슨해 보임
    고급 난독화까지 가지 않더라도, 프로덕션 빌드에서 IsUsingSandbox()를 통째로 제거하는 건 쉬웠을 것 같음

    • 내가 거기서 일할 때 기준으로는 리버스 엔지니어링 방어가 목표였던 적이 없었음
      인증서 고정은 공격자가 변조하기 어렵게 만들기 위한 것이지, 사용자가 하기 어렵게 만들려는 게 아니었음
    • Meta 앱은 프로덕션 빌드에도 디버그 메뉴가 통째로 들어 있음. 글쓴이가 찾은 문자열도 그런 메뉴의 일부일 가능성이 큼
  • 처음 앱을 크랙했을 때는 당연히 실패할 거라고 생각했는데, 알고 보니 이런 식으로 수정하기 쉬운 JNE/JEZ 지점을 찾는 일이 생각보다 쉬웠음
    잘못 고르더라도 원본 파일로 되돌리고 다른 지점을 시도하면 됨
    이런 건 AI가 자동으로 쉽게 할 수 있을 것 같음. 후보 지점 여러 곳에서 JEZ/JNZ를 뒤집어보고 앱을 실행한 뒤 잔소리 화면이 뜨는지만 보면 되니까

    • 이건 딱히 AI 문제가 아니라 퍼징에 가까움
      실패 조건이 잘 정의되어 있다면 결국 후보를 줄이면 됨
      AI가 Denuvo 같은 걸 무시도(0-shot)로 깨줄 수 있다면 그건 또 다른 얘기겠지만
    • 그런 도구는 이미 90년대에도 있었음. AI가 아니라 그냥 무차별 대입이었음
  • 이렇게 큰 회사의 애플리케이션이 왜 완전히 난독화되어 있지 않고, 수정된 바이너리 실행을 막는 보호 장치도 충분히 넣지 않는지 궁금함

    • Facebook 앱에서 처음 이 결정을 했던 입장에서 말하면, 그럴 가치가 없음
      충분히 숙련되었거나 동기가 강한 개인/집단/정부라면 결국 뚫음. 클라이언트 바이너리를 배포한다는 게 원래 그런 성격임
      막으려고 시간과 돈을 엄청 쓸 수도 있음. 예전에 Pinterest가 자체 언어와 가상 머신을 배포하려 했는데 나는 반대했음. 아니면 클라이언트 코드는 기본적으로 이미 침해됐다고 받아들이고, 로직을 서버에 두고 넘어가면 됨
      인증서 고정은 거의 공짜에 가깝고 “이 키 이상이어야 탑승 가능” 같은 장치임. 안전한 건 아니지만 잡다한 시도는 걸러냄
    • 난독화에는 비용이 들고, 인증서 고정은 리버스 엔지니어링 방지보다는 사용자에게 불리한 중간자 공격을 더 어렵게 만드는 목적에 가까움
      물론 리버스 엔지니어링에도 영향을 주긴 하지만, 그건 덤에 가까움
      결국 코드는 사용자 기기에서 실행되고, 사용자는 코드가 하는 일을 관찰할 수 있으므로 언제든 역난독화가 가능함. 한 사람이 풀어서 결과를 공유하면 복제도 아주 쉬워짐. 난독화가 쓸모없다는 뜻은 아니지만, 너무 많은 시간을 들일 대상은 아님
    • 모바일/프런트엔드 앱에서는 그렇게 해도 소용이 없음
      공격자가 기기에 물리적으로 접근할 수 있다면 그 시점에서 막을 방법은 없음. 할 수 있는 일은 과정을 더 귀찮게 만들어서 짜증나 포기하길 바라는 것뿐임
    • 우선순위와 비용 대비 효과 문제일 가능성이 큼
      난독화는 이 실험의 결과에 거의 영향을 주지 못했을 것이고, 접근 방식이 동적 계측을 조금 더 많이 쓰는 쪽으로 바뀌었을 수는 있음. 내가 본 가장 효과적인 난독화는 VM 난독화였지만, 성능 영향이 상당함. 난독화는 정상적인 디버깅도 더 어렵게 만듦
      수정된 바이너리 방지는 시스템 수준에서 이뤄지고, 애플리케이션 수준에서도 구현 가능하며 흔하기도 함. 하지만 그 기능 자체도 우회될 수 있고, 보안 검사가 끝난 뒤 Frida 같은 동적 계측 라이브러리로 수정할 수도 있음
      Meta 입장에서 리버스 엔지니어들과 고양이쥐 게임을 벌이는 건 최선이 아닐 것 같음
    • 왜 모든 은행이 Fort Knox처럼 보안 처리되어 있지 않겠음?
  • 글에서 사용한 프록시 도구가 뭔지 궁금함. 실행 중일 때 모든 애플리케이션 트래픽을 거기로 라우팅하나?
    멍청한 질문이면 미안함

    • 좋은 질문임. 글에서 쓴 건 Proxyman
      macOS에서는 모든 애플리케이션 트래픽을 거기로 라우팅하고, 기기에 자체 서명 인증서를 설치한 뒤 프록시에 연결하면 iOS 기기도 프록시할 수 있음