난독화된 서명 SDK를 처음 열면, 모든 줄이 알 수 없는 암호처럼 보인다. 하지만 전체 역공학 흐름을 한 번 끝까지 밟아 보면 대개 결론은 반대다. 코드의 90%는 이미 공개된 표준 알고리즘이다.
해시는 공개 해시이고, 인코딩은 공개 인코딩이며, 스트림 암호도 공개 스트림 암호인 경우가 많다. 이 부분을 처음부터 이해하려 애쓸 필요는 없다. 어떤 알고리즘인지 식별한 뒤 RFC나 검증된 레퍼런스 구현을 가져오면 바이트 하나까지 그대로 재현할 수 있다.
문제는 남은 10%다. 표준 알고리즘에 눈치채기 어려울 만큼 미세한 변경이 들어간 자리다. 고정이어야 할 라운드 수가 변수가 되어 있거나, 기본 상수가 인접한 값으로 바뀌어 있거나, 한 라운드의 워드 인덱스가 서로 뒤바뀌어 있거나, 키가 암호문 안에 숨겨져 있을 수 있다. 각각은 작은 차이지만 하나라도 놓치면 재구현 결과는 대상과 맞지 않는다. 다른 90%가 정확하기 때문에 오히려 어디가 틀렸는지도 알기 어렵다.
즉, 난독화 서명을 리버싱할 때 읽어야 할 것은 90%가 아니라 표준에서 벗어난 10%다. 이 글에서는 대형 모델에게 그 변형 지점을 찾게 하는 방법과, 왜 이 단계에서 모델의 역할을 대체하기 어려운지를 다룬다.
(전제 단계인 알고리즘 계열 식별, 즉 상수와 구조를 보고 "이건 ChaCha다", "이건 FNV다"를 알아보는 방법은 이전 글에서 다뤘다. 여기서는 이미 뼈대는 식별했다는 가정 아래, 변형 지점 추적으로 바로 들어간다.)
사람이 표준과의 차이를 가장 쉽게 놓치는 이유
패턴을 인식한 뒤에는 세부 확인을 멈추는 경향이 사람에게 강하게 있다. 대강 무엇인지 알아차리는 순간, 더 자세히 보지 않게 된다.
코드를 보다 ChaCha 특유의 상수가 나오고 구조도 익숙하면, 머릿속에서는 곧바로 "ChaCha20"이라고 분류한 뒤 다음으로 넘어간다. 모든 라운드 수, 모든 인덱스, 모든 상수의 하위 비트까지 일일이 확인하지는 않는다. 이미 알아봤다는 만족감이 주의를 꺼 버리기 때문이다.
난독화 작성자는 바로 이 지점을 노린다. 알고리즘 전체를 새로 만드는 일은 비용도 크고 버그 위험도 높다. 대신 표준 구현에 최소한의 변경만 가한다. 숫자 하나를 뒤집고, 인덱스 하나를 바꾸고, 단계 하나를 추가한다. 패턴 인식은 그냥 지나칠 정도로 작지만, 표준 구현을 그대로 옮긴 결과를 완전히 틀리게 만들기에는 충분한 변화다.
이 싸움은 주의력 측면에서 비대칭이다. 작성자는 변형 하나만 숨기면 되지만, 분석자는 모든 변형을 찾아야 한다. 그리고 사람은 그럴듯하게 맞아 보이는 것을 계속 의심하는 데 본질적으로 약하다.
역설적으로 모델은 여기서 강점이 있다. "이미 알아봤다"는 만족감 때문에 집중력이 풀릴 일이 없다. 프롬프트에서 변형을 찾으라고 명시하면, 적절한 지시가 주어진 한 중간에 느슨해지지 않고 항목별로 확인한다. 올바른 프롬프트 구성은 Section 5에서 다룬다. 먼저 변형의 네 가지 유형부터 보자.
현장에서 자주 만나는 변형 4가지
아래에서는 공개 표준 알고리즘을 기준으로 정상 구현의 모습과 변형된 구현의 모습을 비교한다. 실제 프로젝트에서는 이 네 유형이 하나의 서명 로직 안에 겹쳐 들어가는 일도 흔하다.
1. 고정값을 런타임 값으로 바꾸는 경우
표준 스트림 암호의 라운드 수는 고정돼 있다. ChaCha20은 이름 그대로 20라운드이며 바뀌지 않는다. 표준 구현은 모두 이 값을 하드코딩한다. RFC 8439의 서문도 이 문서가 20라운드 ChaCha만 다룬다고 명시한다. 8라운드와 12라운드 변형은 다른 곳에 정의돼 있다. 라운드 수는 원래 표준에 박혀 있는 상수다.
변형 방식은 간단하다. 고정 라운드 수를 키에서 동적으로 계산하게 만든다. quarter-round 로직 자체는 같지만, 실제 반복 횟수가 키의 특정 바이트에 따라 달라진다. 키가 바뀌면 라운드 수도 달라진다.
사람이 놓치는 이유도 단순하다. quarter-round 구조와 σ 상수를 보고 이미 "ChaCha20"이라고 판단했기 때문에, 20라운드를 그대로 복사한다. 루프 횟수를 제어하는 변수가 상수인지 계산식인지조차 확인하지 않는다. 지금까지 본 ChaCha에서는 언제나 상수였기 때문이다.
이 유형의 신호는 명확하다. 상수가 있어야 할 위치에 입력 의존적인 계산식이 있다. 반증 검토를 제대로 유도한 모델이라면 "표준 구현에서는 여기 값이 하드코딩되는데, 이 구현에서는 계산된다"는 점을 집중적으로 확인한다.
차분 테스트로는 키의 바이트 하나만 바꾼 두 입력을 의도적으로 실행해 보면 된다. 출력 차이가 단순히 바이트 하나의 영향보다 훨씬 크다면, 그 바이트는 키스트림에 XOR되는 수준이 아니라 라운드 수처럼 전역 파라미터에 영향을 주고 있을 가능성이 높다.
2. 기준 상수를 아주 가까운 값으로 교체하는 경우
FNV-1a 해시에는 공개된 매직 넘버 두 개가 있다. offset basis와 prime이다. 올바른 FNV-1a 구현은 반드시 이 정확한 두 값을 사용하며, 표준에도 공개돼 있다. 공동 저자인 Landon Curt Noll이 관리하는 FNV 레퍼런스 페이지에는 32비트 offset basis가 2166136261, prime이 16777619라고 한 자리까지 명시돼 있다.
변형은 기준 상수 하나를 표준과 아주 조금만 다른 인접 값으로 바꾸는 방식이다. XOR, 곱셈, 루프를 포함한 해시 구조 전체는 그대로다. 초기값 하나에만 거의 보이지 않는 수정이 들어간다.
네 유형 중 사람이 가장 놓치기 쉬운 경우다. FNV 특유의 구조를 보고 offset basis처럼 생긴 큰 상수를 발견하면 곧바로 "표준 FNV-1a"라고 결론 내린다. 이미 알아본 매직 넘버를 비트 단위로 다시 대조하는 사람은 드물다.
큰 숫자를 눈으로 비교하는 일은 신뢰하기 어렵기 때문에, 이 유형은 사실상 차분 테스트가 가장 잘 잡아낸다. 알려진 입력을 표준 구현과 대상 블랙박스에 각각 넣어 결과를 비교한다. 구조는 동일한데 출력만 다르다면, 원인은 기본 상수일 가능성이 매우 높다. 이후 의심되는 상수를 모델에 넘겨 표준값과 비교하게 하면 된다. 이 작업은 사람이 하는 것보다 기계가 훨씬 정확하다.
여기서 모델의 가치는 매우 구체적이다. 모델은 표준 FNV-1a offset basis를 마지막 비트까지 기억하지만, 사람은 그렇지 않을 가능성이 크다. "이 상수가 표준 FNV-1a offset basis와 정확히 같은가"라고 묻기만 하면 차이를 바로 짚어낼 수 있다.
3. 알고리즘 한 단계를 국소적으로 바꾸는 경우
ChaCha의 각 double round는 8개의 quarter round로 구성된다. 앞의 4개는 열(column)에, 뒤의 4개는 대각선(diagonal)에 작용한다. 각 quarter round가 어느 워드를 다루는지는 표준이 정한 고정 인덱스 집합이다. RFC 8439 §2.3에는 블록 함수와 각 라운드가 건드리는 상태 워드가 정확히 정의돼 있다.
변형은 특정 라운드에서 워드 인덱스 한두 개를 조용히 바꾸는 것이다. 대부분의 라운드는 표준 그대로이고, 중간 어딘가에서만 처리해야 할 워드가 다른 워드로 대체된다. 여전히 ChaCha처럼 보이고 오류 없이 실행되지만, 생성되는 키스트림은 표준 ChaCha와 완전히 달라진다.
사람이 놓치는 이유는 quarter-round 인덱스가 긴 숫자열이기 때문이다. (0,4,8,12)(1,5,9,13)…처럼 8개 묶음이 이어지면, 눈은 "열 라운드 다음 대각선 라운드가 있네" 정도만 확인하고 넘어간다. 각 묶음의 네 숫자가 표준 위치에 있는지까지 보지 않는다. 이미 보기 피곤한 인덱스열 속은 변경을 숨기기에 좋은 장소다.
이것도 모델이 한눈에 잡아내는 문제는 아니다. 라운드별 인덱스를 나열해 표준 ChaCha와 대조하라고 구체적으로 지시해야 한다. 기계적으로 확인해야 하는 작업이고, 바로 이런 종류에서 모델은 흐트러지지 않는 반면 사람은 흐트러진다. "표준 인덱스 vs. 실제 인덱스" 표를 출력하게 하면 변형 지점은 자연스럽게 드러난다.
차분 테스트에서는 앞선 두 유형을 먼저 배제한다. 라운드 수와 상수가 모두 맞는데도 출력이 다르다면 구조적 변경을 의심할 차례다. 라운드별 중간 상태를 덤프하고, 표준 ChaCha와 처음 갈라지는 라운드를 찾으면 수정된 지점을 확인할 수 있다.
4. 출력 안에 키 재료를 숨기는 경우
앞의 세 유형이 알고리즘을 바꾸는 방식이었다면, 이 유형은 데이터를 배치하는 방식을 바꾼다.
흔한 기법 중 하나는 암호화 키를 별도 채널로 전송하지 않고 암호문 내부에 쪼개 넣는 것이다. 수신자는 같은 규칙으로 이를 다시 꺼낸다. 더 교묘한 구현은 삽입 위치도 고정하지 않는다. 데이터 내용 자체에서 위치를 계산하므로 암호문이 달라지면 키가 숨겨진 곳도 달라진다. 가장 바깥 레이어에는 커스텀 알파벳 Base64와 마커 프리픽스 바이트가 감싸고 있을 수 있다.
사람이 놓치는 이유는 암호화 알고리즘을 푸는 데만 매달리기 때문이다. 사실 키는 해독할 필요조차 없고 이미 손에 든 암호문 안에 있는데, 어느 구간에 있는지 모를 뿐이라는 사실을 알아차리지 못한다. 초보자는 평문이 눈앞에 있는데도 그것을 "해독"하려 하며 이 지점에서 자주 막힌다.
이 유형의 신호는 데이터 블록 안에서 주변과 통계적 특성이 다른 구간이다. 키는 대체로 고엔트로피 난수 바이트다. 암호문 중간에 삽입되면 눈에 띄는 이질 구간을 형성한다. 모델은 "이 출력에서 나머지와 다른 바이트 분포를 보이는 구간이 어디인가"를 분석해 삽입된 재료의 경계를 찾는 데 도움을 줄 수 있다.
확인은 삽입 규칙을 발견했을 때 가능하다. 예를 들어 바이트 합계의 모듈러 연산으로 위치를 정한다면, 알려진 입력/출력 쌍 몇 개로 삽입 위치의 식을 역으로 추론한 뒤 정방향으로 검증한다. 모델은 소수의 샘플에서 규칙을 유도하는 데 도움을 줄 수 있지만, 최종 판정은 여전히 assert가 내려야 한다.
반증 검토가 추론 모델의 진짜 승부처다
네 유형을 살펴보면 공통점이 보인다. 알고리즘 계열을 알아보는 후보 판별은 쉽고, 표준과 다른 지점을 찾는 반증은 어렵다.
후보 판별은 대부분의 모델이 할 수 있다. ChaCha의 σ 상수나 FNV의 구조는 학습 데이터에서 충분히 접했기 때문이다. 모델 간 격차가 실제로 드러나는 곳은 반증 검토다. 방금 알아본 알고리즘을 계속 파고들며 "그래도 이 부분은 표준과 다르다"고 말할 의지와 능력이 있는가가 관건이다.
약한 모델은 이 단계에서 힘을 잃는다. "이건 ChaCha20"이라고 판별한 뒤 반증 검토에서도 "이 구현은 전형적인 quarter round를 사용하는 표준 ChaCha20 구조를 따른다"처럼 후보 결론을 다른 말로 반복하는 데 그친다. 이는 변형을 확인한 것이 아니라 후보를 되풀이한 것일 뿐이며, 다음 분석 단계를 안내하지 못한다.
강한 추론 모델의 반증 검토는 다르다. "후보는 ChaCha20이지만 표준 구현과 세 가지 차이가 있다. 첫째, 표준 ChaCha20은 라운드 수가 20으로 고정되지만 여기서는 키 의존 계산식이 이를 제어한다. 둘째, N 라운드의 워드 인덱스가 표준 대각선 라운드와 다르다. 셋째…"처럼 각각 검증 가능한 변형 지점을 지목한다. 이런 반증 검토 결과는 Stage 3에서 작성할 차분 테스트 체크리스트가 된다.
그래서 알고리즘 계열 식별에는 추론 티어를 투입할 가치가 있고, 실제 작업에 쓰기 전에 직접 테스트해 봐야 한다. 모델별 반증 품질 차이가 곧 불필요한 테스트 수와 우회 경로의 길이를 결정한다.
이 워크플로에서 내가 사용하는 네 가지 티어는 다음과 같다.
단계 | 필요한 역량 | 선택 모델 | model id |
|---|---|---|---|
분할 후 구조 매핑 | 긴 컨텍스트, 모듈 전체를 한 번에 읽는 능력 | Kimi K3 |
|
알고리즘 계열 식별 및 반증 검토 | 강한 추론력, 자기 결론에 반론을 제기하는 능력 | Claude Opus 5 |
|
대량 심볼 이름 변경 | 저렴한 비용, 높은 동시성 | Claude Sonnet 5 |
|
차이 원인 귀속 | 중간 수준의 추론력, 특정 바이트를 기준으로 설명하는 능력 | GPT-5.6 Sol |
|
이 글의 핵심은 두 번째 티어다. 내 말을 그대로 믿고 고르기보다 직접 시험해 보는 편이 좋다. 방법은 간단하다.
자신의 난독화 번들에서 리프 함수 2~3개를 고른다. 이 중 최소 하나는 답을 이미 알고 있는 함수로 골라 대조군으로 쓴다.
이전 글의 "후보 / 증거 / 반증" 3단 프롬프트를 사용해, 같은 입력을
claude-opus-5와gpt-5.6-sol에 각각 넣는다.반증 검토만 본다. 실제로 변형을 하나씩 점검하는가, 아니면 후보 결론을 다른 말로 반복하는가? 대조군에서는 각 모델이 변형을 몇 개나 찾아냈는가?
포착한 변형의 수와 품질을 선택 기준으로 삼는다.
한 번만 실행해도 차이가 보일 것이다. 어떤 벤치마크 리더보드보다 직접적이다.
문제는 모델이 아니라 전환 비용이다
세 벤더에 걸친 네 모델을 쓴다고 하면 부담스럽게 들린다. 순진한 방식으로는 SDK 세 개, 인증 방식 세 개, 오류 처리 세 개를 연결해야 한다. 대부분은 계산해 본 뒤 그만한 가치가 없다고 판단하고, 결국 하나의 모델을 모든 단계에 사용한다. 반증이 약한 티어로 알고리즘 계열을 식별한 채, 왜 그런지도 모른 채 수많은 우회로를 돌게 된다.
AIReiter는 이 레이어를 단순화한다. 키 하나와 OpenAI 호환 인터페이스 하나로 네 모델을 모두 쓸 수 있으며, 요청 본문의 model 필드만 바꾸면 전환이 끝난다.
# 알고리즘 계열 식별 + 반증 검토: 추론 티어
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<3단 프롬프트 + 리프 함수 + 상수>"}]
}'
# 대량 심볼 이름 변경: 필드 하나만 변경
# "model": "claude-sonnet-5"
# 차이 원인 귀속:
# "model": "gpt-5.6-sol"
이미 OpenAI SDK를 쓰고 있다면 base_url만 https://aireiter.com/api/v1로 지정하면 된다. 나머지는 바꿀 필요가 없다. Anthropic SDK를 사용 중이라면 같은 키로 POST /api/v1/messages를 호출하면 된다.
가격 기준으로 Claude 모델은 정가 대비 30% 할인, GPT 모델은 반값이다. 이 워크플로에서는 할인이 특히 중요한 구간에 적용된다. 알고리즘 계열 식별은 같은 함수를 두고 프롬프트를 여러 차례 반복하는 가장 호출 밀도가 높은 단계이며, 대량 심볼 이름 변경은 호출 수가 수백 건부터 시작한다. 이 두 단계가 비용의 대부분을 차지한다.
가입 없이 사용해 보기 — 몇 차례 직접 실행해 두 모델의 반증 검토 결과를 나란히 확인한 뒤 결정하면 된다.
마무리
난독화 서명 역공학의 현실은 이렇다. 대부분의 코드는 그대로 옮길 수 있는 표준 알고리즘이고, 실질적인 작업은 표준이 조용히 바뀐 지점을 추적하는 데 있다.
고정값의 변수화, 상수 교란, 구조 변경, 재료 삽입이라는 네 가지 변형은 공통된 특징을 지닌다. 사람의 패턴 인식은 지나칠 만큼 작지만, 재구현 전체를 실패하게 만들 만큼 크다. 그럴듯하게 맞아 보이는 것을 계속 의심하는 데 사람은 약하고, 잘 프롬프트된 모델은 სწორედ 그 지점에서 강점을 보인다.
다만 모델은 의심을 만들어 줄 뿐, 확인해 주지는 않는다. 모든 변형 지점 가설은 결국 차분 테스트가 되어야 한다. 이는 차분 테스트 글에서 다룬다. 모델은 "바뀌었을 수 있는 위치"의 목록을 주고, 실제로 바뀌었는지는 assert가 판정한다.
