지난 글에서는 데이터 흐름과 분기 구조의 중요성을 정리했다.
특히 CALL 이후 TEST와 JZ/JNZ로 이어지는 구조가 인증 로직의 핵심이라는 점을 확인했다.
하지만 실제 분석을 진행하면서 한 가지 의문이 계속 남아 있었다.
왜 굳이 TEST EAX, EAX를 사용하는가?
겉보기에는 의미 없는 연산처럼 보였기 때문이다.
이번 글에서는 이 부분을 정리하면서, 분기 구조를 해석하는 방식을 한 단계 정리해본다.
1. TEST를 연산이 아닌 상태 확인으로 보기
처음에는 이 명령어를 단순한 AND 연산으로 이해했다.
하지만 이 방식으로 접근하면 의미가 흐려진다.
자기 자신과 AND를 수행하는 것은 결과적으로 값이 그대로 유지되기 때문이다.
리버싱 관점에서 TEST의 역할은 다음과 같이 정리하는 것이 맞다.
TEST는 값을 변경하지 않고, 해당 값이 0인지 아닌지를 검사하여 플래그를 설정하는 명령어이다.
즉, 이 명령어의 목적은 계산이 아니라 상태 기록이다.
2. 분기 명령어는 값을 보지 않는다
다음 코드에서 핵심은 TEST가 아니라 그 이후의 흐름이다.
jnz success
처음에는 EAX 값을 기준으로 분기가 이루어진다고 생각하기 쉽다.
하지만 실제로는 그렇지 않다.
분기 명령어는 레지스터 값을 직접 참조하지 않는다.
오직 플래그 상태만을 기준으로 판단한다.
동작 흐름은 다음과 같다.
- TEST 실행 → Zero Flag 설정
- JNZ 실행 → Zero Flag 확인 후 점프 여부 결정
결과는 다음과 같이 정리된다.
- EAX == 0 → ZF = 1 → 점프하지 않음
- EAX != 0 → ZF = 0 → 점프 수행
이 구조를 이해하면 분기 해석이 훨씬 명확해진다.
3. 분기를 해석하는 방식의 변화
이번 분석에서 가장 크게 바뀐 부분은 조건문을 보는 방식이다.
기존에는 다음과 같이 해석했다.
하지만 이 방식은 실제 분석 과정에서는 비효율적이다.
리버싱에서는 분기 명령어를 먼저 보고 해석하는 것이 훨씬 빠르다.
이 코드를 기준으로 해석하면 다음과 같다.
점프하는 방향이 성공 경로이다.
그렇다면 어떤 조건에서 점프하는가를 역으로 추적하면 된다.
JNZ는 ZF가 0일 때 점프한다.
따라서 EAX는 0이 아닌 값이어야 한다.
결론적으로 성공 조건은 EAX != 0이다.
이 방식으로 접근하면 조건문 해석 속도가 크게 빨라진다.
4. TEST와 CMP의 역할 차이
분석을 진행하면서 TEST와 CMP의 차이도 명확해졌다.
CMP는 특정 값과의 비교를 수행한다.
반면
TEST는 값의 존재 여부, 즉 0인지 아닌지를 확인한다.
정리하면 다음과 같다.
- CMP : 특정 값과 비교
- TEST : 상태 확인 (0 여부)
이 차이를 구분하는 것만으로도 코드 해석이 훨씬 단순해진다.
5. 리버싱의 핵심 관점
이번 분석을 통해 다시 확인한 점은 다음과 같다.
리버싱은 명령어 해석이 아니라 흐름 분석이다.
핵심 구조는 항상 동일하다.
조건 검사 → 분기 발생 → 성공 / 실패 경로
따라서 중요한 것은 각 명령어의 의미 자체가 아니라
어떤 조건에서 흐름이 갈리는지 파악하는 것이다.
결국 분석의 목표는 하나로 수렴한다.
어떤 입력이 성공 분기로 이어지는가
6. 이번 단계에서의 변화
초기에는 명령어 단위로 해석하려고 했다.
하지만 지금은 다음과 같은 방식으로 접근하고 있다.
- 전체 흐름 먼저 파악
- 분기 구조 확인
- 필요한 부분만 상세 분석
특히 TEST를 이해한 이후
조건문을 해석하는 속도와 정확도가 크게 개선되었다.
7. 다음 단계
현재까지는 정적 분석을 통해 전체 구조를 파악한 상태다.
다음 단계에서는 동적 분석을 진행할 예정이다.
- 실제 분기 흐름 확인
- 레지스터 값 변화 추적
- 조건 우회 가능성 검증
정적 분석으로 이해한 구조가 실제 실행에서도 동일하게 동작하는지 확인하는 과정이다.
마무리
처음에는 의미 없어 보였던 TEST EAX, EAX 한 줄이
프로그램의 분기 구조를 결정하는 핵심이었다.
이제는 단순히 코드를 읽는 것이 아니라
흐름과 조건을 중심으로 해석하는 단계로 넘어가고 있다.