티스토리 뷰

원본 : https://blog.0xbadc0de.be/archives/67


소스는 따로 올리지 않았기 때문에 원본들어가서 받으면 됩니다.

---------------------------------------------------------------------------------------------------------------------------

소개



  휴일동안, C++로 완전히 코딩된 프로그램을 리버싱하고 분석하는 시간을 가졌다. 오로지 IDA만을 정보 출처로 사용하여 C++ 코드 원형을 심각하게 연구한건 이것이 처음이다.


다음은 흥미있는 함수 내부를 분석하기 시작했을 때, Hex-rays로부터 얻은 샘플이다.

v81 = 9;
v63 = *(_DWORD *)(v62 + 88);
if ( v63 )
{
   v64 = *(int (__cdecl **)(_DWORD, _DWORD, _DWORD,
   _DWORD, _DWORD))(v63 + 24);
   if ( v64 )
     v62 = v64(v62, v1, *(_DWORD *)(v3 + 16), *(_DWORD
     *)(v3 + 40), bstrString);
}
 
cs


  심볼의 이름을 추가하고, 클래스들을 식별한 후, hex-rays에 정보를 설정하여 정확하고 확실히 이해할 수 있는 결과를 얻는 것이 이번 목표이다.

padding = *Dst;
if ( padding < 4 )
  return -1;
buffer_skip_bytes(this2->decrypted_input_buffer, 5u);
buffer_skip_end(this2->decrypted_input_buffer, padding);
if ( this2->encrypt_in != null )
{
  if ( this2->compression_in != null )
  {
    buffer_reinit(this2->compression_buffer_in);
    packet_decompress(this2,
      this2->decrypted_input_buffer,
      this2->compression_buffer_in);
    buffer_reinit(this2->decrypted_input_buffer);
    avail_len = buffer_avail_bytes(this2->compression_buffer_in);
    ptr = buffer_get_data_ptr(this2->compression_buffer_in);
    buffer_add_data_and_alloc(this2->decrypted_input_buffer, ptr, avail_len);
  }
}
packet_type = buffer_get_u8(this2->decrypted_input_buffer);
*len = buffer_avail_bytes(this2->decrypted_input_buffer);
this2->packet_len = 0;
return packet_type;
 
cs


  물론, Hex-rays는 이름을 정해주지 않을 것이기 때문에 코드를 의미를 알고, 코드에 의미를 부여해야 할 것이다. 최소한 Class에 이름을 부여한다면 큰 도움이 될 수 있을 것이다.


  여기 있는 모든 샘플은 Visual Studio나 Gnu C++로 컴파일되었다. 서로 호환되지 않더라도, 결과가 비슷하기 때문에 자신에게 맞게 수정해서 사용하면 된다.


C++ 프로그램의 구조


  OOP가 어떻게 동작하는지를 가르쳐 주는 것이 목표도 아니다. OOP 프로그램이 어떻게 동작하고 구현되는지 큰 그림을 그려가며 살펴볼 것이다.


  클래스(Class) = data 구조체 + code(메소드)


  디스어셈블러의 결과로 메소드가 나타났을 때, data 구조체는 소스 코드에서만 보는 것이 가능하다.


  객체(Object) = memory 할당 + data + 가상 함수(virtual functions)


  객체는 Class의 인스턴스(=예시, instantiation)이며, IDA에서 관찰할 수 있다. 객체는 메모리를 필요로 하기 때문에 new() (또는 스택 할당) 연산자 호출, 생성자와 소멸자 호출을 볼 수 있을 것이다. 이 때 멤버 변수(내부 객체)에 접근하는 것을 볼 수 있고, 가상 함수를 호출할 수도 있다.


  가상 함수는 프로그램이 브레이크 포인트(Break Point)를 실행하지 않으며, 실행 중에 어떤 코드가 실행되고 디스어셈블되는지 알기 어렵다.


  멤버 변수는 C(구조체)내부에 매칭되고, IDA는 구조체를 선언하기에 매우 적합한 툴이며, hex-rays는 디스어셈블리를 매우 잘 처리한다. 우선, 비트와 바이트가 있는 곳으로 돌아가도록 하자,


객체(Object) 생성

int __cdecl sub_80486E4()
{
  void *v0; // ebx@1
  v0 = (void *)operator new(8);
  sub_8048846(v0);
  (**(void (__cdecl ***)(void *))v0)(v0);
  if ( v0 )
    (*(void (__cdecl **)(void *))(*(_DWORD *)v0 + 8))(v0);
  return 0;
}
 
cs


  위는 C++로 컴파일된 작은 테스트 프로그램을 디컴파일한 결과이다. 여기서 new(8)을 볼 수 있는데, 이것은 8바이트의 변수를 나타내는 것이 아니라, 객체의 크기가 8바이트임을 나타낸다.


  new()의 바로 뒤에 호출되는 sub_8048846 함수는 해당 포인터를 매개 변수로 사용하기 때문에 생성자임을 알 수 있다.


  이 다음의 함수 호출은 조금 독특하다. 함수를 호출하기 전에, v0에 이중 포인터로 역참조를 수행한다. 이것은 가상 함수(virtual function)를 호출하는 모습이다.


  모든 다형성을 가진 객체에는 변수들 중 vtable이라고 불리는, 특수한 포인터를 가진다. 이 테이블은 모든 가상 메소드(virtual methods)의 주소를 포함하고 있어서, C++ 프로그램이 필요로 할 때 호출할 수 있다. 컴파일러로 테스트한 결과, vtable은 항상 객체의 첫 번째 요소이며, 하위 클래스에서도 항상 같은 위치에 위치하고 있다(테스트하지는 않았지만, 다중 상속의 경우에도 마찬가지이다).


  IDA로 마법을 부려보도록 하자.


심볼(symbols) 이름 재설정


  함수의 이름을 클릭한 후, << n >>을 누르면, 의미있는 이름으로 바꿀 수 있다. 아직은 Class가 어떤 동작을 수행하는지 알 수 없기 때문에, Class의 이름을 << class1 >>로 이름을 바꾼다. Class가 어떤 동작을 하는지 이해할 때 까지는 이 규칙을 사용하는 것이 좋다. class1을 파기 전에 다른 Class들을 발견할 수도 있으므로, 클래스 이름은 계속해서 바꿔나가면 된다.

int __cdecl main()
{
  void *v0; // ebx@1
  v0 = (void *)operator new(8);
  class1::ctor(v0);
  (**(void (__cdecl ***)(void *))v0)(v0);
  if ( v0 )
    (*(void (__cdecl **)(void *))(*(_DWORD *)v0 + 8))(v0);
  return 0;
}
 
cs


구조체 생성


  IDA의 << structures >> 창은 매우 유용하다. Shift-F9를 누르게 되면 나타난다. QT IDA 버전에서 IDA의 오른쪽에 창을 옮겨 놓으면, 디컴파일 창과 Structures 창을 같이 볼 수 있기 때문에 좋다.


  << insert >>를 누르면, 새로운 Structure인 << class1 >>을 만들 수 있다. 이 구조체는 8 바이트 길이이므로, << d >> 키를 사용하여 2개의 << dd >> 필드가 생길 때 까지 필드를 추가한다. 첫 번째는 vtable이기 때문에 vtable로 이름을 변경한다.


  이제 함수에 타이핑한 정보를 추가할 것이다. v0를 마우스 오른쪽 클릭하고, << Convert to struct * >>을 클릭하고, << class1 >>을 선택한다. 또는 << y >>를 누르고, 자료형을 << class1 * >>으로 변경하면 같은 결과를 얻을 수 있다.


  12 바이트의 새 Structure를 만든 후, 이것을 "class1_vtable"이라고 정한다. 이 상태에서, vtable이 얼마나 큰지 알 수 없지만, 구조체의 크기를 변경하는 것은 매우 쉽다. class1의 선언에서 << vtable >>을 클릭한 후, << y >>를 입력하면 된다. 이제  class1_vtable*(객체의 포인터로 선언)하고, pseudocode 뷰를 새로고침하면, 다음과 같은 화면을 볼 수 있다.


  이제 남은 메소드를 << method1 >>과 << method3 >>으로 이름을 변경한다. Method3은 확실하게 소멸자다. 프로그래밍 규약과 컴파일러 사용에 따라, 첫 번째 메소드는 종종 소멸자이지만, 여기서는 반대이다. 우선, 생성자를 분석할 것이다.


생성자 분석


int __cdecl class1::ctor(void *a1)
{
  sub_80487B8(a1);
  *(_DWORD *)a1 = &off_8048A38;
  return puts("B::B()");
}
 
cs


  << a1 >>은 우리가 이미 알고있는 정보(class1 자신)를 타이핑해서 설정할 수 있다. puts() 호출은 현재 생성자를 보고 있다는 것이 맞을을 나타내고, 여기서 class의 이름을 알아낼 수 있다.


  << sub_80487B8() >>는 생성자에서 직접 호출된다. 이 함수는 class1의 정적 메소드이거나, 부모 Class의 생성자일 수도 있다.


  << off_8048A48 >>는 class1의 vtable이다. 잘 살펴보면 vtable(Xref를 통해 다음 포인터를 보면 됨)의 크기와 << class1 >>의 가상 메소드(virtual methods) 목록을 확인할 수 있다. 이를 통해 << class1_mXX >>로 이름을 변경할 수 있지만, 일부 메소드의 경우, 다른 Class들과 공유될 수 있다.


  vtable 자체에 대해서 정보를 타이핑해서 설정하는 것도 가능하다(예를 들어, << y >>, << class1_vtable >>과 같이). 하지만 IDA의 클래식 정보를 잃어버려, 볼 수 없기 때문에 권장하지는 않는다.


생성자 내 이상한 호출


int __cdecl sub_80487B8(int a1)
{
  int result; // eax@1
  *(_DWORD *)a1 = &off_8048A50;
  puts("A::A()");
  result = a1;
  *(_DWORD *)(a1 + 4= 42;
  return result;
}
 
cs


  생성자에서 << sub_80487b8() >>함수를 호출하면 가상 함수 테이블(vtable) 포인터가 vtable 멤버에 저장되며, puts()를 통해 또 다른 생성자가 존재하는 것을 알 수 있는데, 여기서 동일한 타입의 함수를 드러낸다.


  현재 class1을 다루는 것이 아니기 때문에 << a1 >> 인자에 << class1 >>에 대한 값을 입력하면 안 된다. 이것은 << class2 >>라는 새로운 Class의 발견이며, 이 Class는 class1의 슈퍼 클래스이다. class1에서 수행했던 작업을 동일하게 수행해보자. 여기서 한 가지 문제점은 해당 Class의 멤버 크기를 알지 못 한다는 점인데, 이것을 알아내는 방법은 2가지다.

  • class2::ctor의 xref를 확인한다. new() 연산자(instance 생성) 후에 생성된 녀석을 직접 호출하고 있다면, 이를 통해 Class 멤버의 크기를 아는 것이 가능하다.
  • vtable의 메소드를 확인해서, 접근하는 멤버 중 가장 높은 곳에 위치한 멤버가 어떤 것인지 추측해서 확인한다.

  << class2::ctor >>은 첫 4바이트(vtable) 이후의 4바이트에 접근하여, 42라는 값을 세팅한다. 자식 클래스 << class1 >>은 8바이트 long이므로, 이것은 << class2 >>이다.


모든 하위 클래스에 동일한 절차를 수행하여, 부모 클래스(상위 클래스)부터 자식 클래스(하위 클래스)까지 가상 함수(virtual functions)에 이름을 지정한다.


소멸자에 대한 연구


  main으로 함수 이름을 지정했던 위치로 돌아가도록 하자. v0 객체가 memory leak이 일어나기 전, 마지막 호출 부분을 보면 class2의 3번째 가상 메소드(virtual method)를 호출한다. 이것을 연구해보도록 하자.

if ( v0 )
  ((void (__cdecl *)(class1 *))
    v0->vtable->method_3)(v0);
 
cs


void __cdecl class1::m3(class1 *a1)
{
  class1::m2(a1);
  operator delete(a1);
}
 
void __cdecl class1::m2(class1 *a1)
{
  a1->vtable = (class1_vtable *)&class1__vtable;
  puts("B::~B()");
  class2::m2((class2 *)a1);
}
 
void __cdecl class2::m2(class2 *a1)
{
  a1->vtable = (class2_vtable *)&class2__vtable;
  puts("A::~A()");
}
 
cs


  여기서 class1::m3은 class1::m2를 호출하는 class1의 메인 소멸자인 것을 볼 수 있다. 이 소멸자는 vtable을 << class1 >>의 상태로 설정함으로써, << class1 >> 컨텍스트가 잘 동작한다는 것을 보장해준다. 이 후, vtable을 << class2 >> 컨텍스트로 설정하여, << class2 >>의 소멸자를 호출한다. 가상 소멸자는 항상 모든 Class에 대해서 호출될 수 있어야 하기 때문에, 이러한 방식은 전체 Class 계층을 처리하는데 사용할 수 있다.


왜 같은 필드를 정의하는 구조체가 2개 있을 수 있는가?


  C와 함께 OOP를 할 때, 모든 서브클래스에는 선언된 여러 필드가 생기기 때문에 정확히 동일한 문제가 발생할 수 있다. 다음은 필드의 재정의를 방지하기 위한 방법들이다.

  • 각 클래스를 위해, classXX_members, classXX_vtable, classXX 구조체 정의
  • classXX 포함
    • vtable(classXX_vtable * 자료형)
    • classXX-1_members(슈퍼 클래스의 members)
    • classXX_members이 있는 경우
  • classXX_vtable 포함
    • classXX-1_vtable
    • classXX의 vptrs이 있는 경우


  이상적으로, 메인 클래스부터 시작해서 가장자리에 있는 자식 클래스로 끝나는게 좋다. 다음의 예제는, 샘플의 << solution >>이다.

00000000 class1          struc ; (sizeof=0x8)
00000000 vtable          dd ?                    ; offset
00000004 class2_members  class2_members ?
00000008 class1          ends
00000008
00000000 ; ----------------------------------------------00000000
00000000 class1_members  struc ; (sizeof=0x0)
00000000 class1_members  ends
00000000
00000000 ; ----------------------------------------------00000000
00000000 class1_vtable   struc ; (sizeof=0xC)
00000000 class2_vtable   class2_vtable ?
0000000C class1_vtable   ends
0000000C
00000000 ; ----------------------------------------------00000000
00000000 class2          struc ; (sizeof=0x8)
00000000 vtable          dd ?                    ; offset
00000004 members         class2_members ?
00000008 class2          ends
00000008
00000000 ; ----------------------------------------------00000000
00000000 class2_vtable   struc ; (sizeof=0xC)
00000000 method_1        dd ?                    ; offset
00000004 dtor            dd ?                    ; offset
00000008 delete          dd ?                    ; offset
0000000C class2_vtable   ends
0000000C
00000000 ; ----------------------------------------------00000000
00000000 class2_members  struc ; (sizeof=0x4)
00000000 field_0         dd ?
00000004 class2_members  ends
00000004
 
cs


int __cdecl main()
{
  class1 *v0; // ebx@1
  v0 = (class1 *)operator new(8);
  class1::ctor(v0);
  ((void (__cdecl *)(class1 *)) v0->vtable->class2_vtable.method_1)(v0);
  if ( v0 )
    ((void (__cdecl *)(class1 *)) v0->vtable->class2_vtable.delete)(v0);
  return 0;
}
 
int __cdecl class1::ctor(class1 *a1)
{
  class2::ctor((class2 *)a1);
  a1->vtable = (class1_vtable *)&class1__vtable;
  return puts("B::B()");
}
 
class2 *__cdecl class2::ctor(class2 *a1)
{
  class2 *result; // eax@1
  a1->vtable = (class2_vtable *)&class2__vtable;
  puts("A::A()");
  result = a1;
  a1->members.field_0 = 42;
  return result;
}
 
cs


요약

  • 새로운 Class를 찾으면, 상징적인 이름을 주고, 진짜 이름을 알아내기 전에 전체적인 tree부터 풀어나가는게 좋음
  • 상위 클래스부터 자식클래스로 올라가야 함
  • 먼저, 생성자와 소멸자를 확인하고, new() 연산자나 정적 메소드에 대한 참조를 확인
  • 컴파일된 파일에서 동일 클래스의 메소드는 보통 가까운 위치에 있음, 관련 클래스(상속)은 멀리 떨어져 있을 수 있으며, 가끔 생성자가 자식 클래스 생성자나 인스턴스화 위치에 인라인됨
  • 매우 거대한 상속 구조체를 리버싱하는데 시간을 소비하고 싶다면, struct inclusion trick을 사용하여 변수에 이름을 한 번만 지정
  • Hex-ray의 타이핑 시스템은 매우 강력한 기능이므로 애용
  • 순수 가상 함수는 비슷한 vtable을 가진 여러 Class들을 찾을 수 있지만,  공통된 코드가 없기 때문에 헬


'Reversing' 카테고리의 다른 글

Windows stub code와 main 함수(IDA 사용)  (0) 2017.10.19
[Windows Driver] Reversing  (0) 2017.10.12
C++ Reversing  (0) 2017.10.03
Ida Plugin 리스트 및 사용법  (0) 2017.10.03
[Android] Main Activity 검색 스크립트  (2) 2017.08.23
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30