32. CTF 크랙미 리버스 패턴 5분 요약
크랙미를 빠르게 푼다는 말은 무작정 디컴파일러의 결과를 읽는다는 뜻이 아니다. 먼저 프로그램이 무엇을 관찰하고, 어느 조건에서 실패하며, 비교값을 어디서 만들었는지를 좁히는 일이다. 아래 흐름은 허가받은 CTF 바이너리나 직접 만든 연습용 실행 파일에만 적용한다.
실패 분기에서 시작하기
실행 파일을 한 번 실행해 정상 입력과 오답 입력의 차이를 기록한다. strings -a ./crackme로 메시지를 확인한 뒤, Ghidra나 디버거에서 strcmp, strncmp, memcmp, strlen의 호출자를 찾는다. 다만 이런 함수가 없다고 검증 로직이 없는 것은 아니다. 컴파일러가 인라인화했거나 바이트 단위 루프로 바꿨을 수 있으므로, 실패 메시지로 이어지는 조건 분기와 그 직전의 플래그를 기준으로 거꾸로 올라간다.
file ./crackme
checksec --file=./crackme
strings -a -n 5 ./crackme | less
checksec의 결과는 풀이법을 결정하는 답이 아니라 관찰 조건이다. PIE와 ASLR이 켜져 있으면 주소를 고정해 생각하지 말고, 심볼이 제거돼 있으면 함수의 역할을 호출 규약과 데이터 흐름으로 확인한다.
반복되는 변환의 판별
입력 길이를 먼저 고정하는 코드는 흔하지만, 길이 검사는 정답 자체가 아니다. 루프의 인덱스가 입력 길이만큼 증가하는지, 각 반복에서 같은 키나 테이블을 사용하는지, 결과가 상수 배열과 비교되는지를 따로 기록해야 한다. 한 바이트 XOR은 역연산이 같으므로 비교 대상 expected[i]와 키 k를 확인했다면 input[i] = expected[i] ^ k로 검산할 수 있다. 키가 위치마다 바뀌면 키 스케줄이나 테이블 조회를 먼저 복원해야 한다.
테이블 치환에서는 배열의 이름보다 인덱스가 중요하다. table[input[i] & 0xff]처럼 입력을 직접 인덱스로 쓰는지, 인덱스가 섞이거나 회전되는지 확인한다. CRC처럼 보이는 코드는 다항식, 초기값, 반전 여부가 모두 결과에 영향을 주므로 “CRC다”라고 단정하지 말고 작은 입력으로 참조 구현과 대조한다. 해시를 역산하는 대신, 가능한 입력 공간이 작거나 검증 루틴에 논리적 결함이 있는지부터 판단하는 편이 정확하다.
동적 확인과 복원
정적 분석으로 세운 가설은 디버거에서 한 번 검증한다. Linux x86-64라면 함수 인자는 보통 rdi, rsi, rdx에 놓이지만, 실제 호출 지점의 ABI와 최적화를 확인해야 한다. memcmp 직전에 두 버퍼를 메모리 덤프하고, 입력 한 글자만 바꿔 비교값이 어떻게 달라지는지 관찰하면 변환 위치를 빠르게 찾을 수 있다. 자동화 스크립트는 정답을 “추측”하는 도구가 아니라, 복원한 규칙을 여러 샘플로 검산하는 도구로 쓴다.
마지막에는 찾은 입력을 프로그램에 넣는 것만으로 끝내지 않는다. 길이 경계, 비ASCII 바이트, 반복 문자 같은 반례를 넣어 분석한 조건이 실제 코드의 조건과 일치하는지 확인한다. 좋은 크랙미 분석은 패턴 이름을 맞히는 일이 아니라, 관찰한 분기와 복원한 함수가 서로 설명력을 갖는지 증명하는 일이다.