포맷 스트링#
printf는 포맷 스트링을 사용하여 값을 원하는 형식으로 출력해요.
printf("%d\n", 123);
printf("%s\n", "Hello");
printf("%p\n", address);c대표적인 형식 지정자는 다음과 같아요.
| 형식 지정자 | 설명 |
|---|---|
%d | 정수 출력 |
%x | 16진수 출력 |
%s | 주소가 가리키는 문자열 출력 |
%p | 주소 출력 |
%n | 지금까지 출력한 문자 수를 메모리에 저장 |
인자의 위치를 직접 지정할 수도 있어요.
%7$ptext위 포맷 스트링은 일곱 번째 인자를 주소 형태로 출력해요.
FSB#
포맷 스트링 버그(Format String Bug, FSB)는 사용자 입력을 printf의 포맷 스트링으로 직접 사용했을 때 발생해요.
char format[0x100];
scanf("%255s", format);
printf(format);c사용자가 %p, %s, %n 등을 입력하면 printf가 레지스터와 스택의 값을 인자로 해석해요.
%p → 레지스터 또는 스택 값 출력
%s → 해당 값을 주소로 사용하여 메모리 읽기
%n → 해당 값을 주소로 사용하여 메모리 쓰기text따라서 포맷 스트링 버그를 이용하면 주소를 유출하거나 메모리의 값을 읽고 쓸 수 있어요.
임의 주소 읽기#
%s는 인자를 문자열이 저장된 주소로 사용해요.
%7$stext일곱 번째 인자 위치에 읽고 싶은 주소가 있다면 해당 주소의 데이터를 문자열로 출력해요.
페이로드 뒤에 주소를 직접 넣는 방식도 사용할 수 있어요.
payload = b"%7$s"
payload = payload.ljust(8, b"a")
payload += p64(target_address)py정확한 인자 번호는 실행 환경과 스택 구조에 따라 달라져요. 일반적으로 %p를 반복하여 입력값이 나타나는 위치를 찾아요.
AAAA.%1$p.%2$p.%3$p...text임의 주소 쓰기#
%n은 현재까지 출력된 문자 수를 인자로 받은 주소에 저장해요.
int num;
printf("AAAA%n", &num);cAAAA는 네 글자이므로 num에는 4가 저장돼요.
Width를 사용하면 원하는 수만큼 문자를 출력할 수 있어요.
%31337c%8$ntext위 포맷 스트링은 31337개의 문자를 출력한 뒤 여덟 번째 인자가 가리키는 주소에 31337을 기록해요.
기록하는 크기는 길이 지정자로 조절할 수 있어요.
%hhn → 1바이트
%hn → 2바이트
%n → 4바이트text큰 값을 기록할 때는 %hhn을 사용하여 한 바이트씩 나누어 쓰는 방법을 사용할 수 있어요.
예를 들어 0xdeadbeef는 리틀 엔디언 환경에서 다음 순서로 저장돼요.
낮은 주소
ef be ad de
높은 주소text따라서 각각의 바이트를 target, target + 1, target + 2, target + 3에 기록하면 돼요.
방지 방법#
그냥 포맷 스트링을 쓰면 해결돼요.
printf(user_input); // 취약
printf("%s", user_input); // 안전c문자열만 출력한다면 puts나 fputs를 사용할 수도 있어요.
puts(user_input);
fputs(user_input, stdout);c포맷 스트링을 고정하고 사용자 입력은 별도의 인자로 전달하는 것이 가장 중요해요.
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg4c/dn4BitGH1/xNjKoKEp97I2b
eU57QXvkDBEdNNrEMAAAATYmxvZy5pbW55YS5uZy9wb3N0cwAAAAAAAAAGc2hhNTEyAAAA
UwAAAAtzc2gtZWQyNTUxOQAAAECyhgad3dpzvi/czJk5py52oUHvnQfugEbZbIV1mwzJEW
33sOR4mW89euaiPldAzE/zELf2jZXQJdIG7a8ys5oP
-----END SSH SIGNATURE-----