본문 바로가기

Let's Study/Hacking Technique

Assembly-> C Handray(2)

Assembly-> C Handray(1)에서 했던 내용을 약간 응용해서 Codegate 2013 vuln 400 문제를 handray 할텐데요.

assembly언어를 C로 handray하는 건 끈기와 인내(?)가 필요한 작업이기 때문에 일부분만 하겠습니다.


이전 글을 읽으시고 핸드레이를 연습하시는 분이라면 이 글은 어셈블리어가 눈에 익숙해지신 분들만 읽어주셨으면 좋겠습니다.



코드가 너무 길어서 일일히 설명해드릴 수가 없고(어차피 노가다이기 때문에 핸드레이 좀 해보신 분들은 이해하실 수 있을겁니다.) Ida의 힘을 빌려 함수와 변수명을 정의하는 과정을 보여드립니다.



일단 이 문제는 이렇게 생겼습니다.





핸드레이에 익숙하신 분은 Ida-pro를 이용해서 코드분석을 빨리 하시는 것도 좋은 방법이지만, 배우는 입장이시라면 Ida를 안쓰고 gdb만 이용해서 핸드레이를 해주셨으면 합니다.


전 이걸 아이다를 안쓰고 핸드레이 했을 때, 하루정도 걸렸습니다. 이왕 공부 시작한거, 아이다에 의지하지 않고 흐름을 읽고싶어서 보고 보고 또 본 것 같은데, 아이다를 꼭 써야되는 상황이 온다면, 함수정의용으로만 쓰시거나 아이다가 분석한 C코드와 자신이 핸드레이한 C코드를 비교해보는 것도 좋은 방법입니다. 아이다가 틀릴 수도 있는데 어지간해서는 거의 다 맞더라고요.


그럼, 시작합니다.


제 기억에 이 문제는 main함수가 없습니다. 그러므로 gdb에서 info function으로 gmon start를 찾으시거나 아이다로 열어서 gmon start를 찾습니다.




함수에서 __libc_start_main을 찾으셨다면 이 함수의 첫번째 인자로 들어가는 주소 0x8049177이 프로그램이 시작하는 곳입니다.

이 함수를 타고 들어가봅니다.




 이 함수를 main이라 정의하겠습니다. 

main의 처음부분을 보시면 단순하게 변수에 값을 넣고 setvbuf 함수가 쓰인 것을 볼 수 있습니다.


int v1 = 0;

int v2 = 0x7a69; 

int v3 = 0x7a69;

int d_804b090 = 0;  (ds:가 붙은 것은 전역변수이지만 아직 변수의 이름을 제대로 모르기 때문에 d_804b090으로 표현합니다.)

setvbuf(d_804b090, 0, 2, 0);


setvbuf() 밑에 보시면 첫 번째 인자와 두 번째 인자가 있는데 그 밑의 함수가 nop 처리된 걸로 보아 esp + 0x4와 esp는 크게 의미 없는 값입니다.


이 코드를 아이다로 열어서 분석을 해봅니다.


void __cdecl sub_8049177() //위에서 설명하였으므로 이 부분은 

= void main()   //main으로 바꾸도록 하겠습니다.

{

  int v0; // eax@2

  char *v1; // [sp+4h] [bp-1Ch]@1

  int v2; // [sp+14h] [bp-Ch]@1

  signed int v3; // [sp+18h] [bp-8h]@1

  signed int v4; // [sp+1Ch] [bp-4h]@1


  v2 = 0;

  v3 = 31337;

  v4 = 31337;

  dword_804B090 = 0; //어떤 기능을 하는지 모르는 전역변수 1

  dword_804B08C = 0; //어떤 기능을 하는지 모르는 전역변수 2


  setvbuf(stdout, 0, 2, 0);

  v1 = &byte_8049152;

  sub_8048A15(); //8048A15 함수

  while ( 1 )

  {

    while ( 1 )

    {

      puts("1. Write");

      puts("2. Read");

      puts("3. Exit");

      printf("=> ", v1);

      v0 = getchar();

      dword_804B088 = v0;

      if ( v0 != 50 )

        break;

      sub_8048B7C(&v2);

    }

    if ( v0 == 51 )

      sub_8048D57();

    if ( v0 == 49 )

    {

      if ( sub_8048B2A((int)&v2) == 1 )

        puts("Wrong");

      else

        sub_8048A93((int)&v2);

    }

    else

    {

      puts("Wrong");

    }

  }

}


main함수를 보면 여러 가지 함수가 있습니다. 이제부터 모르는 함수는 파란색으로 표시를 하고 모르는 전역변수는 녹색으로 표시를 하도록 하겠습니다. 그리고 구조체는 빨간색으로 표시를 하겠습니다.


main 함수에서는 간단히 write, read, exit를 입력받아서 그에 맞는 함수를 출력해주는 것 외에는 없어보입니다. 이 사실을 알고 있다면 sub_8048a15() 함수는 단순한 출력함수로 추측할 수 있고 sub_8048b7c(&v2)는 '2'를 눌렀을 때 실행되는 함수, read인 것을 알 수 있고 sub_8048d57()는 exit, sub_8048b2a((int)&v2)sub_8048a93((int)&v2)는 write 관련 함수인 것을 추측할 수 있습니다. sub_8048a15를 쫒아들어가보면 


__cdecl sub_8048A15()

{

  puts(" _______________________________ ");

  puts("/==============================/ ");

  puts("|     Onetime Board Console    | ");

  puts("/------------------------------/ ");

  puts("|          | WELCOME |         | ");

  puts("|__________|_________|_________| ");

  puts("|          W  a  i   t         | ");

  puts("++++++++++++++++++++++++++++++++ ");

  return sleep(0);

}


역시 단순한 put을 해주는 함수임을 알 수 있습니다. 나중에 헷갈리지 않게 Ida로 함수 이름을 print_start로 수정해줍니다.

그 다음, '2'를 입력했을 때 call되는 sub_8048b7c(&v2)로 갑니다.


int __cdecl sub_8048B7C(int *a1)

{

  int result; // eax@2

  int v2; // [sp+1Ch] [bp-Ch]/@1


  v2 = *a1;

  system("/usr/bin/clear");

  if ( v2 )

  {

    printf("\t| %s| %-20s | %-20s \n", "number", "author", "title");

    while ( v2 )

   { 

      puts("\t----------------------------------------------");

      printf("\t| %5d | %-20s | %-20s \n", *(_DWORD *)(v2 + 24), *(_DWORD *)(v2 + 28), *(_DWORD *)(v2 + 32));

      puts("\t----------------------------------------------");

      v2 = *(_DWORD *)(v2 + 4);

    }

    result = sub_8048C31(a1);

  }

  else

  {

    result = puts("Wrong");

  }

  return result;

}


sub_8048b7c로 넘어가보면 2. read로 넘어갔을 때 볼 수 있는 화면이 보입니다. 알아보기 쉽게 read_sha(여러분이 이해하기 편한 이름)로 바꿔줍니다. 그리고 빨간색으로 표시한 부분은 구조체이지만 아직 구조체가 어떻게 정의되어 있는지 알 수 없습니다. 그런데 위의 printf("| %s| %-20s | %-20s ", "number", "author", "title");

이 문장을 봤을 때 각각  *(_DWORD *)(v2 + 24), *(_DWORD *)(v2 + 28), *(_DWORD *)(v2 + 32)  


typedef Struct Node

{

unknown *a;

unknown *(a + 1)

unknown *(a + 2)

unknown *(a + 3)

unknown *(a + 4)

unknown *(a + 5)

int number;

char* author;

char* title;

}

로 표현할 수 있습니다.


이제 read_sha에서 쓰인 sub_8048c31로 들어가봅니다.


int __cdecl sub_8048C31(int *a1)

{

  int result; // eax@1

  int i; // [sp+14h] [bp-14h]@1

  int v3; // [sp+18h] [bp-10h]@3

  signed int v4; // [sp+1Ch] [bp-Ch]@1


  v4 = 0;

  printf(": ");

  __isoc99_scanf("%d", &dword_804B088);

  result = *a1;

  for ( i = *a1; i; i = *(_DWORD *)(i + 4) )

  {

    if ( *(_DWORD *)(i + 24) == dword_804B088 )

   { 

      v3 = *(_DWORD *)(i + 20);

      puts("\t\t===================================");

      printf("\t\t|| %d || %-20s || %-20s \n", *(_DWORD *)(i + 24), *(_DWORD *)(i + 28), *(_DWORD *)(i + 32));

      puts("\t\t===================================");

      printf("\t\t|content | %s\n", *(_DWORD *)(i + 36));

      puts("\t\t===================================");

      v4 = 1;

      while ( v3 )

      {

        puts("\t\t\t");

        printf("\t\t\t====> %s\n", *(_DWORD *)(v3 + 12));

        v3 = *(_DWORD *)(v3 + 24);

      }

      sub_8049013(i);

    }

    result = *(_DWORD *)(i + 4);

  }

  if ( !v4 )

    result = puts("Wrong");

  return result;

}


이 함수에서 2. read로 넘어갔을 때 나오는 "Number: "가 보이므로 sub_8048c31(int *a1)는 read_number로 바꿉니다.


printf("\tNumber: ");

  __isoc99_scanf("%d", &dword_804B088);에서 dword_80b8088이 값을 입력받는 것을 보니 이 변수는 값을 입력받는 변수임을 알 수 있습니다. d_input으로 이름을 바꾸고 진행을 합니다.


printf("\t|content | %s\n", *(_DWORD *)(i + 36));


이 문장에서는 content가 i + 36에 위치한 것으로 보아 *(i + 9)는 char* content입니다. 

while ( v3 )

     { 

        puts("\t\t\t|");

        printf("\t\t\t|====> %s\n", *(_DWORD *)(v3 + 12));

        v3 = *(_DWORD *)(v3 + 24);

      }

여기서는, v3일 때 *(_DWORD *)(v3 + 12)의 값을 출력하라는 말인데 

 이 부분은 



댓글을 다는 부분입니다.

그리고 그 밑에  v3 = *(_DWORD *)(v3 + 24); 이 부분이 있는데 만약 이 부분이 제가 정의했던 struct Node* -> int number부분이라면 말이 안되기 때문에 v3은 새로운 구조체라고 볼 수 있습니다. 댓글을 출력하고 다음 노드로 넘어가라는 의미로 보이므로 

Typedef struct Reply{

unknown *a

unknown *(a + 1)

unknown *(a + 2)

*char reply_content

unknown *(a + 4)

unknown *(a + 5)

struct Reply *next;

}Reply; 로 표현할 수 있습니다.


... 이런식으로 쭉 함수를 진행하다보면 두 개의 구조체와 그 구조체에서 쓰인 함수포인터들이 정의가 되는데 여기서 알아야 될 부분인 함수 몇 개만 찍어서 정리를 하겠습니다.


int init_sha(Node *n)

{

        int result=0;


        d_number++;

        n->reply_count = 0;

        n->next = 0;

        n->prev = 0;

        n->fill_func = write_sha;

        n->del_func = init_free;

        n->replies = 0;

        n->number = d_number;

        n->author = (char*)malloc(256);

        n->title = (char*)malloc(256);

        n->status = 0xdeadbeaf;

        n->unknown = rand();


        return result;

}


 구조체의 값들을 초기화시켜주는 부분입니다. 여기서 write_sha 함수와 init_free 함수를 각각 함수포인터 fill_func, del_func로 사용하겠다고 선언이 되어있습니다. 이 초기화된 값들을 보면 struct node의 길이가 어느정도인지 예상을 할 수 있는 부분입니다.


int read_delete(Node* n)

{

        struct Reply* tmp;


        if (n->reply_count <= 0)

        n->prev->next = n->next;

        n->next->prev = n->prev;

        if (n->status == 0xdeadbeef)

        {

                for (tmp = n->replies; tmp->next; tmp = tmp->next)

                {

                        tmp->func1 = delete_set;

                        tmp->del_rep_func = free_sha;

                }

        }

        n->del_func(n);

        else{

                puts("Cannot Deleted. There's at least one or more replies on it");

        }

        return 0;        

}


더블링크드리스트 부분입니다. 여기서 해당 number의 노드를 지우고 이전 prev와 다음 next를 연결해주는 부분입니다.


이렇게 복원을 진행하면 약 450줄 정도의 코드가 완성됩니다. 어떻게 핸드레이 하느냐에 따라 다르지만요.




지금까지 C코드만 핸드레이했는데, java나 c++, mfc 전부 컴파일러에 따라 어셈구조가 다 다르기 때문에 여러가지 언어를 핸드레이 해 보시는게 좋습니다.