
NX하나만 걸려있다.
코드 분석부터 진행하겠다.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
void get_shell() {
system("/bin/sh");
}
int main(int argc, char *argv[]) {
char buf[0x80];
initialize();
read(0, buf, 0x80);
printf(buf);
exit(0);
}
buf[0x80] 바이트만큼 선언굉장히 간단한 소스코드다.
printf() 실행 시 포맷스트링을 입력하면 그대로 주소로 출력하는 취약점을 활용한 문제이다.
호출 프롤로그
lea eax,[ebp-0x80] ; buf
push eax ; printf(format = buf)
call printf@plt
printf 호출 시 스택 상태는 다음과 같다.
[esp] : return addr (printf 복귀지)
[esp+0x4] : format 포인터 = &buf[0]
[esp+0x8] : 첫 번째 '가변 인자' 슬롯 = buf[4..7] 바이트
[esp+0xc] : 두 번째 '가변 인자' 슬롯 = buf[8..11] 바이트
...
즉, buf의 4바이트 단위 조각들이 그대로 printf의 가변 인자 슬롯이 된다.
이 구조 덕분에 “포맷스트링(문자열)”은 buf[0]부터 시작되어 %...를 해석하고,
가변 인자 참조(%n, %k$hn)는 buf의 다음 4바이트 슬롯들을 그대로 사용한다.
따라서 포맷 문자열은 깨끗한 ASCII로 구성하고,
쓰고 싶은 주소(예: exit@GOT, exit@GOT+2)는 buf의 뒤쪽 가변 슬롯 위치(예: 10번째, 11번째 슬롯)에
원시 4바이트 값으로 배치하면 된다.
포맷 문자열 끝에는 \\x00로 종료(문자열 종료)해도, 가변 인자 슬롯의 “메모리 값”은 그대로 남아 있으므로
%10$hn 처럼 포지셔널(직접 인덱스) 지정자를 쓰면 그 슬롯을 정상적으로 참조할 수 있다.
printf(buf) 호출 시, 가변 인자(varargs)는 콜러의 스택에서 [esp+8]부터 읽히며 이것이 정확히 buf의 0,4,8… 오프셋과 1:1 매칭된다.
즉, buf의 각 4바이트 덩어리가 %1$..., %2$..., … 에 해당한다.