세 플랫폼 모두에서 네이티브 연결 지점을 찾아냈다. 디바이스 등록이 시작되는 위치, 보안 SDK가 요청을 가로채는 계층, 네이티브 인터셉터가 나가는 패킷을 어떻게 다시 쓰는지, 그리고 JNI가 RegisterNatives로 런타임에 동적 등록하는 메서드까지 추적했다. Frida 스크립트를 붙이자 로그는 쏟아졌고, 모든 훅은 안정적으로 걸렸다. 그런데 명령 카탈로그를 열어 세 플랫폼에서 실제 호출 가능한 네이티브 앱 엔드포인트를 세어 보니 결과는 0개였다.
같은 프로젝트의 다른 플랫폼은 달랐다. 그곳에는 11개가 있었다. 현장 검증을 마쳤고, 코드로 이관돼 일급 명령으로 동작 중인 기능들이다.
차이는 훅 기법에 있지 않았다. 훅은 모두 걸렸고 연결 구조도 깔끔하게 그려졌다. 문제는 많은 사람이 놓치는 하나의 기준선이다. 훅이 걸린 것과 기능을 실제로 쓸 수 있는 것 사이에는 증거의 단계가 있다. 이 글에서는 그 기준을 어떻게 세울지, 반쯤 만든 엔드포인트를 내보내는 것보다 "아직 사용 불가"라고 표시하는 편이 왜 더 저렴한지, 그리고 그 과정에서 모델을 어디에 써야 하는지를 다룬다.
훅 성공은 기능 구현 완료가 아니다
앱을 리버싱하다 보면 "훅이 걸렸다"는 순간을 결승선처럼 여기기 쉽다. 대상 메서드에 스크립트가 붙고, 로그에는 인자와 반환값, 호출 스택이 찍힌다. "안으로 들어왔다"는 감각은 분명 강하다. 그리고 꽤 자주 잘못된 확신이 된다.
하지만 명령 카탈로그가 받아들이는 것은 "내부에 들어갔다"는 사실이 아니다. 정상적인 입력을 넣었을 때, 다운스트림이 바로 소비할 수 있는 비어 있지 않고 올바른 구조의 페이로드를 이 명령이 안정적으로 내보내는지가 기준이다. 둘 사이의 거리는 생각보다 멀다.
전형적인 상황은 이렇다. 모든 훅이 걸리고 연결도 완성돼 있다. 로그에서는 디바이스 등록 요청이 나가는 모습도 보이고, 보안 SDK가 값을 계산하는 과정도 보이며, 네이티브 인터셉터가 서명 헤더를 추가하는 것도 확인된다. 겉으로는 완벽해 보인다. 그러나 실제 기기에서 실행하면 디바이스 등록은 값이 0인 디바이스 ID를 반환하거나, 상세 조회 엔드포인트는 빈 본문으로 돌아온다. 연결은 열렸지만 데이터가 없다.
이 시점에 가진 것은 특정 버전의 앱 내부 동작을 관찰할 수 있는 프로브 묶음이다. 호출 가능한 엔드포인트는 아니다. 전자를 후자인 것처럼 배포하면, 이후 작업자 모두를 위한 지뢰를 묻는 셈이다.
11개와 0개를 가른 기준
두 플랫폼군을 나란히 놓고 보면 기준선이 드러난다.
대조 플랫폼(동영상 커뮤니티 앱) | 상위 콘텐츠 앱 3개 | |
|---|---|---|
훅 연결 | 식별 완료 및 실기기 검증 | 전부 식별 완료, 모든 훅 성공 |
실제 설치 상태 | 0이 아닌 디바이스 ID 확보 | 값이 0인 디바이스 ID / 실제 디바이스 프로필 부재 |
비어 있지 않은 응답 | 구조화된 상세 데이터 | 빈 상세 데이터 / 빈 본문 |
명령 카탈로그 등록 수 | 11 | 0 |
대조 플랫폼의 11개는 단순히 "더 좋은 훅"이 아니다. 각각 다음 섹션의 네 가지 증거 기준을 모두 통과한 뒤 일급 Python 명령으로 이관됐고, 구조화된 검증 증거도 함께 보관했다.
세 플랫폼의 작업이 멈춰 있던 것은 아니다. 보안 SDK의 인터셉션 계층, 네이티브 요청 인터셉터, JNI가 동적으로 등록한 메서드 묶음까지 모두 파악했고 Frida 훅도 유지하고 있다. 다만 실제 설치 상태를 통과하지 못하고, 디바이스 등록에서 0이 아닌 ID를 얻지 못하는 한 그 뒤의 응답은 모두 비어 있다. 그래서 네이티브 앱 명령은 정직하게 0개로 남아 있으며, 현재 쓸 수 있는 경로는 별개의 Web 또는 브라우저 전송 방식뿐이다.
전체 장부로 보면 이번 모바일 기능 묶음은 32개다. 이 중 23개는 실제 구현으로 이어졌고, 나머지 9개는 "실제 디바이스 프로필 대기" 상태에 머물러 있다. 어느 것도 성급하게 카탈로그에 넣지 않았다. 이 9는 실패 수가 아니다. 연결은 이해했지만 증거가 아직 부족하다는 범위를 정확히 표시하는 숫자다.
카탈로그 등록 전 통과해야 할 네 가지 증거
비교를 풀어 보면 앱 기능이 명령 카탈로그에 들어가기 위해서는 네 가지 조건을 모두 충족해야 한다. 하나라도 빠지면 등록하지 않는다.
첫째, 실제 설치 상태. 요청은 업스트림이 인정하는 0이 아닌 설치 ID에서 나와야 한다. 에뮬레이터 환경이나 깨진 프로필에서 훅이 걸렸다는 사실은 증거가 되지 않는다. 디바이스 등록이 값이 0인 ID를 돌려준다면 바로 이 관문에서 막힌 것이다. 이후 연결이 아무리 완전해도 출력은 비어 있다. 세 플랫폼이 공통으로 멈춘 지점도 여기였다.
둘째, 비어 있지 않은 응답. 요청이 열려 있다고 데이터가 있는 것은 아니다. 요청은 나가고 상태 코드는 200인데 본문은 비어 있을 수 있다. 이 "성공한 빈 응답"은 오류보다 위험하다. 예외 발생 여부만 보는 검사를 전부 통과해 버리기 때문이다. 기준은 다운스트림에 바로 전달할 수 있는 구조화되고 완전하며 비어 있지 않은 데이터다.
셋째, 완전한 오류 분류. 성숙한 기능이라면 실패했을 때 단순히 실패했다고 던지는 것이 아니라 원인을 알려야 한다. 페이지 없음, 로그인 안 됨, 런타임 미준비, 빈 응답은 실패 방식이 모두 다르다. 하나의 오류로 뭉개지 말고 구별 가능한 오류 코드로 나눠야 한다. "실패했다"고만 말하는 명령은 호출자가 재시도할지, 다시 인증할지, 건너뛸지를 결정할 수 없으므로 카탈로그에 넣을 준비가 되지 않았다.
넷째, 닫힌 형태의 재현 가능한 테스트. 한 번 운 좋게 성공한 것은 기능이 아니다. 같은 입력과 같은 흐름이 반복 실행돼야 하며, 그 성공은 저장된 구조화 검증 증거로 고정돼야 한다. 한 번은 되고 다음번에는 빈 응답이 온다면 아직 연결을 통제하는 것이 아니다. 그저 한 번 런타임 상태가 맞아떨어졌을 뿐이다.
네 조건이 모두 닫혀야 명령 카탈로그에 등록한다. 하나라도 열려 있으면 그 기능은 엔드포인트가 아니라 연구 자산이다. 이 두 단어의 차이가 이 글의 핵심이다.
Frida 로그가 폭주할 때는 모델 티어를 나눠 써라
네 가지 기준 중 어디에서 연결이 막혔는지 판단하는 원재료는 Frida가 남기는 트레이스다. 문제는 Frida 트레이스가 쉽게 폭증한다는 점이다. 메서드 수십 개에 훅을 붙이고 하나의 전체 흐름을 돌리면 수만 줄에서 수십만 줄이 쌓이는 일이 흔하다. 그 대부분은 폴리필, 하트비트, 무관한 비즈니스 모듈에서 나온 잡음이다.
이 로그 전체를 한 모델에 넣고 "왜 이 훅은 데이터를 못 받았나"라고 물으면 뭉뚱그린 추측이 돌아오기 쉽다. 컨텍스트가 클수록 서로 무관한 호출 구간 두 개를 이어 붙일 가능성도 커진다. 올바른 방법은 로그를 청크로 나누고, 작업 성격에 따라 서로 다른 모델 티어에 맡기는 것이다.
이 작업은 "가장 강한 모델 하나를 모든 일에 쓰는" 사례가 아니라 티어를 조합해야 하는 전형적인 경우다. 네 작업이 모델에 요구하는 능력은 서로 다르다.
Frida 로그 처리 단계 | 필요한 역량 | 선택 | model id |
|---|---|---|---|
전체 호출 체인 트레이스를 한 번에 읽기 | 긴 컨텍스트, 전체 체인을 함께 읽는 능력 | Kimi K3 |
|
수만 줄 라벨링(디바이스 등록 / 네트워크 / 암호화 / 잡음) | 저렴한 비용, 높은 동시성의 수천 회 호출 | Claude Sonnet 5 |
|
연결이 네 가지 기준 중 어디에서 막혔는지 판단 | 강한 추론 능력, 결론을 내릴 수 있는 판단력 | Claude Opus 5 |
|
두 트레이스의 분기 지점을 비교해 응답이 빈 이유 설명 | 중간 수준의 추론 기반 귀속, 특정 로그 행에 근거한 설명 | GPT-5.6 Sol |
|
특히 라벨링 단계가 중요하다. 수만 줄 중 잡음 행을 걷어 내고 디바이스 등록, 암호화, 네트워크 관련 행만 남기는 순수한 반복 작업인데, 물량이 너무 많아 사람이 처리하기 어렵다. 이런 "매우 높은 빈도로 수행하는 단순 판단"이야말로 저비용 티어의 역할이다. 이를 추론 티어에 올리는 것은 비용 낭비일 뿐이다. 로그를 수백 줄의 라벨링된 요약으로 압축한 뒤 추론 티어에 넘겨 "어느 기준에서 막혔는가"를 판단하게 하면 비용과 정확도가 모두 맞아떨어진다.
이 분할의 효과는 직접 한 번 돌려 보면 확인할 수 있다.
하나의 연결에서 트레이스 구간을 잘라 낸다. 수천 줄에서 수만 줄 정도면 된다.
먼저
claude-sonnet-5로 청크별 라벨링을 수행해 잡음 행을 제외하고 디바이스 등록, 암호화, 네트워크 분류만 남긴다.라벨링된 요약을
claude-opus-5에 넣고, "이 연결이 현재 네 가지 기준 중 어디에서 막혔는지와 근거가 되는 행"을 출력하게 한다.대조군으로는 동일한 원시 트레이스 전체를 단일 모델에 넣고 같은 질문을 한다.
확인할 것은 하나다. 뭉뚱그린 추측을 내놓는가, 아니면 특정 기준과 특정 행을 정확히 짚는가. 그 차이가 선택 기준이 된다.
훅은 버전에 묶인 관찰 프로브이지 서명기가 아니다
세 플랫폼의 훅을 유지하면서도 명령 카탈로그 밖에 단단히 두는 이유가 있다. 훅은 프로브이고 서명기가 아니다. 둘은 본질적으로 다른 대상이다.
훅은 예를 들어 32.x처럼 특정 앱 빌드에 묶여 있다. 의존하는 심볼, 오프셋, 메서드 레이아웃 모두 그 버전에 속한다. 업스트림이 새 버전을 내놓으면 전부 이동하고 훅은 즉시 죽는다. 본질적으로 소모적이고 버전 종속적이다. 훅이 답하는 질문은 "이 버전이 지금 내부에서 무엇을 하는가"라는 관찰의 질문이다. 계측 도구가 맡아야 할 역할도 정확히 이것이다. Frida 문서는 Interceptor를 런타임 호출을 관찰하고 다시 쓰기 위한 수단으로 설명한다. 함수가 어떤 방식으로 호출되는지와 인자가 무엇인지는 볼 수 있지만, 입력을 받아 서명을 계산하는 배포 가능한 기능 자체는 아니다.
엔드포인트 카탈로그에 들어갈 서명기는 정반대여야 한다. 안정적이고, 재현 가능하며, 독립적으로 동작하고, CI에서 검증할 수 있어야 한다. "입력을 주면 올바른 서명을 계산한다"는 재사용 가능한 기능의 질문에 답해야 한다. 훅으로 서명 알고리즘의 뼈대를 선명하게 볼 수 있더라도, 그것은 algorithm-fingerprinting 단계일 뿐이다. 독립형 서명기가 되려면 정제와 차분 검증의 전체 흐름이 아직 남아 있다.
이는 정제 사다리의 순서도 설명한다. 기능은 먼저 순수 Python 직접 연결을 목표로 해야 하고, 그다음에는 로컬 Node/V8에서 최소한의 서명 조각을 실행하는 방식으로 후퇴하며, 정말 마지못해 수동 브라우저 브리지를 받아들여야 한다. 훅은 아직 이 사다리 위에조차 없다. 구현 이전의 "연구" 단계에 있다. 버전 종속 관찰 프로브를 서명기로 등록하는 일은 반쯤 만들어진 "연구" 결과물에 "구현"이라는 간판을 다는 것과 같다.
연구 자산을 썩지 않게 보관하는 법
훅을 엔드포인트 카탈로그에 넣지 않는다고 버리라는 뜻은 아니다. 연결 지식과 샘플, 검증 증거가 들어 있는 연구 자산이며, 여기에 이미 적지 않은 노력이 들어갔다. 이를 삭제하는 것은 순손실이다. 핵심은 제대로 보관하는 것이다. 그렇지 않으면 두 달 뒤에는 작업한 사람조차 어디까지 진행됐는지 알 수 없다.
앱 연구 자산에는 적어도 다음 네 가지를 기록해야 한다.
전송 방식. 연결이 네이티브 프로토콜, 로컬 Node/V8 서명, 수동 브라우저 브리지 중 어디에서 동작하는지 기록한다. 이는 나중에 어느 수준까지 정제할 수 있는지를 결정한다.
증거 단계. 네 가지 기준 중 몇 개를 통과했는지 적는다. "연결 식별 완료"인지, "0이 아닌 설치 상태는 확보했지만 응답은 비어 있음"인지, "비어 있지는 않지만 재현 불가"인지 명확히 한다. 다음 작업자가 사용 가능 상태까지 무엇이 남았는지 즉시 알 수 있다.
앱 버전 / 빌드. 훅이 어느 버전에 묶여 있는지 남긴다. 이 정보가 없으면 업스트림이 업데이트됐을 때 내가 잘못 작성한 것인지, 빌드가 바뀐 것인지 구분할 방법이 없다.
샘플 요약. 이번 실행에서 입력과 출력이 어떤 형태였는지 비식별화한 복사본으로 남긴다. 연구를 다시 시작할 때 가장 빠르게 맥락을 되찾게 해주는 기준점이다.
이 네 가지를 기록하면 명령 수 0개에 머문 연결도 만료된 로그 더미가 아니라 계속 발전시킬 수 있는 자산이 된다. 동시에 최악의 두 가지 처리를 막는다. 훅을 삭제하고 연구 자체가 없었던 일처럼 만드는 것, 혹은 사용 가능한 척 카탈로그에 억지로 넣는 것이다. 특히 후자는 비용이 크다. "호출은 되는 것처럼 보이지만 실제로는 빈 응답을 반환하는" 엔드포인트는 카탈로그를 신뢰하는 모든 다운스트림 호출자에게 비용을 전파한다. 그들은 이를 바탕으로 통합을 작성하고 재시도를 구현하며, 빈 데이터에 한 번 당한 뒤에는 카탈로그 전체를 믿지 않게 된다. 반면 "연구 자산, 명령 0개"라고 솔직하게 표시한 공백은 README 한 줄로 끝나는, 한 번뿐인 비용이다.
빈 껍데기는 공백보다 비싸다. 이 사례만큼 그 사실이 선명한 곳도 드물다.
키 하나로 로그 분할의 마찰을 줄이기
네 번째 섹션의 로그 분할로 돌아가 보자. 긴 컨텍스트 티어는 전체 트레이스를 읽고, 저비용 티어는 대량 라벨링을 하며, 추론 티어는 분류를 맡고, 중간 추론 티어는 원인 귀속을 담당한다. 여러 공급사의 네 티어, 네 SDK, 네 인증 방식, 네 가지 오류 형식이 필요하다. Frida 로그를 네 클라이언트에 나눠 처리하려 하면 대부분은 계산기를 두드린 뒤 수지가 맞지 않는다고 판단한다. 결국 하나의 모델로 수십만 줄의 트레이스를 붙잡고 씨름하게 되고, 비용을 태우거나 끝까지 처리하지 못한다.
AIReiter는 이 마찰을 없앤다. 키 하나와 OpenAI 호환 인터페이스 하나로 네 티어를 모두 사용할 수 있고, 요청 본문의 model 필드만 바꾸면 된다.
# 로그 라벨링: 저비용 티어로 높은 동시성의 수천 회 호출
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"messages": [{"role": "user", "content": "<one chunk of trace + labeling instruction>"}]
}'
# 기준 분류: 나머지는 그대로 두고 추론 티어로 전환
# "model": "claude-opus-5"
# 전체 호출 체인 읽기: 긴 컨텍스트 티어
# "model": "kimi-k3"
# 응답 차이 원인 귀속:
# "model": "gpt-5.6-sol"
이미 OpenAI SDK를 사용 중이라면 base_url을 https://aireiter.com/api/v1로 지정하면 된다. 나머지는 바꿀 필요가 없다. Anthropic SDK에서는 같은 키로 POST /api/v1/messages를 호출한다.
이 글에서 다루는 작업은 비용 구조가 한쪽으로 크게 기울어져 있고, 할인도 바로 그 기울어진 구간에 적용된다. 하나의 연결 트레이스는 수만 줄에서 수십만 줄이고, 라벨링은 청크마다 수행되며, 플랫폼 하나만 해도 claude-sonnet-5 호출이 수백에서 수천 번에 이른다. 비용의 대부분이 여기서 발생한다. 전체 트레이스를 읽는 긴 컨텍스트 호출도 입력당 수십만 토큰이므로 또 하나의 큰 비용 덩어리다. 분류와 귀속은 단가가 더 높지만 호출 수는 적다. Claude 30% 할인은 가장 비용이 큰 라벨링과 기준 분류에 정확히 적용된다. GPT 반값 할인은 귀속 티어에 적용된다. 긴 컨텍스트 읽기는 같은 키로 호출 가능한 Kimi K3에서 실행한다.
가입 없이 사용해 보기: 트레이스 한 구간을
claude-sonnet-5에 맡겨 라벨링하고, 이어서claude-opus-5로 분류해 보자. 연동하기 전에 어느 기준에서 막혔는지 정확히 짚어내는지 확인할 수 있다.
마무리
앱 리버스 엔지니어링에서 훅이 걸렸다는 것은 "이 버전이 내부에서 무엇을 하는지 볼 수 있다"는 관찰 능력을 얻었다는 뜻이다. 반면 엔드포인트 카탈로그가 원하는 것은 "입력을 주면 비어 있지 않은 올바른 데이터를 안정적으로 내놓는다"는 호출 능력이다. 그 사이에는 실제 설치 상태, 비어 있지 않은 응답, 오류 분류, 재현 가능한 테스트라는 네 가지 증거 기준이 놓여 있다.
세 플랫폼 모두 연결을 완전히 식별했고 훅도 전부 걸렸지만 명령 수가 0개라는 것은 실패가 아니다. 실제 설치 상태를 통과하지 못했으므로 정직하게 0에서 멈추고, 훅을 연구 자산으로 보관한 뒤 디바이스 프로필을 확보했을 때 다시 진행하는 규율이다.
이 흐름에서 모델의 역할은 분명하다. 수십만 줄의 트레이스를 나누고, 라벨링하고, 분류하고, 원인을 귀속해 "이 연결은 어디에서 막혔는가"를 로그 읽기에 쓰던 반나절에서 몇 분으로 압축한다. 하지만 "기준을 통과했는가"라는 최종 판단은 차분 테스트에서 "가설이 맞는가"를 판단하는 일과 마찬가지로 모델에 맡길 수 없다. 이는 스스로 정한 네 가지 기준과 반복해서 재현되는 증거가 결정한다. 전체 리버스 엔지니어링 워크플로에서 모델을 제자리에 두는 방법은 4단계 개요에서 다룬다.