26. 심볼 없는 바이너리에서 함수 윤곽 잡기
스트립된 바이너리를 어렵게 만드는 것은 명령어가 아니라 이름의 부재다. 함수가 무엇을 하는지 모른 채 디스어셈블리 한 화면을 읽으면, 분석은 금세 주소 수집으로 변한다. 목표는 모든 함수를 복원하는 것이 아니라, 질문에 필요한 경계를 세우고 그 경계가 맞는지 증명하는 것이다. 아래 절차는 승인된 샘플, CTF, 또는 소유한 프로그램에만 적용한다.
먼저 실행 모델을 고정한다
파일 형식과 아키텍처를 확인하고, PIE·RELRO·보호된 섹션 같은 조건을 기록한다. file sample과 readelf -h -S -l sample은 도구의 자동 추론을 검토할 기준을 준다. _start에서 런타임 초기화 코드를 따라갈 때는 __libc_start_main에 전달되는 함수 포인터가 실제 main 후보라는 점을 확인하되, libc 버전과 컴파일러에 따라 모양이 달라질 수 있음을 전제로 한다.
이름 대신 관찰 가능한 계약을 찾는다
문자열, 포맷 문자열, 오류 경로, 외부 함수 호출은 좋은 앵커다. strings -tx sample로 오프셋을 보고 분석기에서 해당 주소의 XREF를 찾으면, 문자열을 사용하는 함수와 그 호출자를 좁힐 수 있다. 상수만으로 알고리즘을 단정하지는 않는다. 0x9E3779B9가 보인다고 해서 곧바로 특정 암호를 선언하기보다, 반복 횟수·회전·메모리 접근과 함께 가설로 기록한다.
호출 그래프는 입출력 경계에서 안쪽으로 읽는다. 파일 열기, 소켓 송수신, 파싱, 검증, 출력처럼 역할이 다른 호출을 색으로 구분하고, 각 후보 함수에 parse_request 같은 임시 이름과 근거를 남긴다. 이름은 결론이 아니라 다음 분석자를 위한 주석이어야 한다.
경계는 여러 증거로 정정한다
자동 함수 인식이 놓치는 지점에서는 호출 대상, 분기 도달성, 스택 포인터 변화, 함수 반환 명령을 함께 본다. x86-64에서 전형적인 프롤로그가 없다는 사실만으로 함수가 아니라고 판단할 수 없고, 인라이닝 때문에 하나의 기능이 여러 블록에 흩어질 수도 있다. 의심스러운 경계는 디버거에서 실제 호출·복귀 주소와 대조한다.
정적 가설은 LLDB의 조건부 중단점이나 Frida의 읽기 전용 로깅으로 검증할 수 있다. 인자 값, 반환값, 분기 횟수처럼 질문에 직접 답하는 값만 수집하고, 비밀 데이터와 제3자 시스템의 트래픽은 기록하지 않는다. 좋은 결과는 함수 이름 목록이 아니라 “이 함수는 이 입력 계약을 받아 이 상태 변화를 만든다”는 설명이다.
판단 기준
분석을 멈출 시점은 모든 주소에 이름을 붙였을 때가 아니다. 요청한 동작을 설명하는 호출 경로가 재현되고, 다른 입력에서 관찰한 결과가 가설과 일치하며, 도구의 오탐 가능성이 문서화되었을 때다. 심볼은 사라질 수 있지만 계약과 관찰은 남는다.