Search

'Class member function'에 해당되는 글 1건

  1. 2011.07.15 Detours Hooking Framework - 3-1. Hooking class member function

이전 글

Detours Hooking Framework 1 - Hooking Function

Detours Hooking Framework 2 - Hooking API

 

잠시 복습

첫번째 Detours Hooking Framework 글에선 일반 Function을 Hooking하는 법을 살펴보았고 두번째 글에서는 Detours로 API를 후킹하는 법에 대해 알아보았다. Detours가 훌륭한 것은 동일한 방식으로 다른 분류의 Hooking을 할 수 있다는 사실이다. 사용자의 입장에선 내부적으로 어떤 일이 벌어지는지 신경쓸 필요가 없다. 조금만 신경쓰면 된다. 잠시 이전에 Detours가 어떻게 Hooking을 했는지 복습해보자. 일반 함수의 경우 함수 심볼 위치에 있는 명령어를 사용자가 원하는 명령어로 변경해줌으로써 이 일이 가능했다.

위를 보면 TestFunction: 심볼 값에 위치한 명령이 jmp TestFunction에서 jmp HookingFunction으로 변경된 것을 확인할 수 있다. 또한 Dynamic Link된 API를 Hooking할 때는 좀더 복잡한 Code Patch방식을 사용한다는 것이 확인되었다. 만약 해당 API Function의 주소가 A라면 해당 주소에 jmp HookingFunction이라는 명령어가 들어가는 식이었다. 하지만 근본적인 방식은 특정 메모리 주소에 Jmp HookingFunction이라는 명령어를 쓰는 방식으로 동일했다. 그리고 변경되기 전 메모리 주소의 명령어들은 다른 메모리 주소로 옮겨 진다. 이것은 현대의 컴퓨터가 폰노이만 머신, 즉 Code와 Data가 동시에 같은 메모리상에 존재하기 때문에 가능한 것이었다.(VirtualProtect함수로 해당 메모리 주소에 WRITE속성을 추가하고 memcpy로 명령어를 쓰는 것으로 이루어진다.) 당연히 후킹을 위해선 반드시 후킹하고 싶은 함수 또는 API의 메모리 주소를 알아야한다.

 

Class member function call을 디벼보자.

class member function을 후킹하려고 한다. 먼저 후킹이 가능하려면 해당 member function의 주소를 알아내야 한다. 그런데 그것만으로 될까?


#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <detours.h>

class TestClass{
private :
    int mInt;
public :
    TestClass() : mInt(0){}
    TestClass(int initValue) : mInt(initValue){}
    int TestFunction(int iValue);
};

int TestClass::TestFunction(int iValue){
    return mInt + iValue;
}

int main(int argc, char** argv){
    TestClass a(10),b(20);
    int result1 = a.TestFunction(10);
    int result2 = b.TestFunction(10);
    return 0;
}


위와 같은 초 단순한 예를 한번 생각해보자. 후킹을 하기 위해서 해당 함수의 메모리 주소를 알아야한다. 그렇다면 여기서 a.TestFunction 함수와 b.TestFunction 함수의 메모리 주소는 같을까 다를까? 같다면 하나의 작업으로 여러 Instance의 TestFunction을 후킹할 수 있을 것이고 다르다면 Instance마다 TestFunction을 후킹해야한다. 이를 확인하기 위해 a.TestFunction 함수 콜과 b.TestFunction 함수 콜이 Assembly로 어떻게 컴파일 되는지 살펴보자.


    int result1 = a.TestFunction(10);
008E1612  push        0Ah  
008E1614  lea         ecx,[a]  
008E1617  call        TestClass::TestFunction (8E111Dh)  
008E161C  mov         dword ptr [result1],eax  
    int result2 = b.TestFunction(10);
008E161F  push        0Ah  
008E1621  lea         ecx,[b]  
008E1624  call        TestClass::TestFunction (8E111Dh)  
008E1629  mov         dword ptr [result2],eax  


위 어셈블리를 통해 알 수 있듯이 a.TestFunction 함수와 b.TestFunction은 같은 주소 값을 가진다. 이것은 동일한 클래스의 Instance가 여러개 존재하더라도 해당 클래스의 Function은 같은 메모리 주소에 있다는 것을 의미한다. 그렇다면 해당 클래스 멤버 함수는 자신이 어떤 Instance에 속해 실행되는지 어떻게 알 수 있을까? 옳지, call 명령 전의 lea ecx, [a]에 해답이 있을 것 같다. lea ecx, [a]명령이 완료되면 a변수의 주소가 ECX 레지스터에 들어가게 된다. 아하, TestClass::TestFunction은 이 ECX 레지스터에 있는 a의 주소를 통해 정확한 mInt의 값을 얻어 내는 것이구나! a.TestFunction(10)으로 들어가 확인해보자.


int TestClass::TestFunction(int iValue){
008E1590  push        ebp      //Stack Frame을 사용하기 위해 ebp를 Stack에 저장한다. 
008E1591  mov         ebp,esp  //현 esp 값을 ebp에 저장한다. 
008E1593  sub         esp,0CCh //함수내에 사용될 스택의 크기를 0x0CC만큼 확보한다. 
008E1599  push        ebx      //ebx 레지스터의 값을 스택에 저장한다. 
008E159A  push        esi      //esi 레지스터의 값을 스택에 저장한다. 
008E159B  push        edi      //edi 레지스터의 값을 스택에 저장한다. 
008E159C  push        ecx      //ecx 레지스터의 값을 스택에 저장한다. 
008E159D  lea         edi,[ebp-0CCh]  //여기서 부터 rep stos까지의 명령은 하나의 명령 set으로 보면된다. 
008E15A3  mov         ecx,33h         //ecx에 0x33 값을 넣는다.  
008E15A8  mov         eax,0CCCCCCCCh  //eax에 0xcccccccc값을 넣는다. 
008E15AD  rep stos    dword ptr es:[edi]  
//rep stos dword ptr es:[edi]는 edi, ecx, eax와 연관된다. 
//보통 함수의 첫 시작에 불리는 이 명령어 집합은 스택에 초기값을 넣는 것에 활용된다. 
//이 명령은 edi의 주소 값에 eax값을 넣는다. 그리고 그 다음에는 edi+4의 주소 값에 eax값을 넣는다. 
//다음에는 edi+8의 주소 값에 eax값을 넣는다. 이것을 총 ecx내의 수 만큼 반복한다. 즉 0x33번 반복한다.(0x33*4 = 0xcc)
008E15AF  pop         ecx  //ecx값을 복구한다.
008E15B0  mov         dword ptr [ebp-8],ecx  //ecx값을 ebp-8의 주소에 넣는다. 
    return mInt + iValue;
008E15B3  mov         eax,dword ptr [this] //this 주소 값을 eax에 넣는다.   
008E15B6  mov         eax,dword ptr [eax]  //eax의 주소의 value값을 eax에 넣는다. 
008E15B8  add         eax,dword ptr [iValue]  //eax에 iValue값을 더해 넣는다. 
}
008E15BB  pop         edi  //edi값 복구  
008E15BC  pop         esi  //esi값 복구
008E15BD  pop         ebx  //ebx값 복구
008E15BE  mov         esp,ebp  //esp = ebp
008E15C0  pop         ebp  //ebp값 복구
008E15C1  ret         4  //esp = esp +4, 함수 호출했던 주소로 가기 위해 또한번 pop. 


 

Stack

음... Stack관련 명령만 대해 먼저 정리하자. 먼저 알아야 할 것은 함수가 불리기 전의 ESP값과 함수가 불린 후의 ESP 값이 동일해야 한다는 사실이다. 그리고 Stack은 함수 내 변수 공간을 위해 활용된다. 이 Stack의 변화를 보기 위해 Stack관련 명령어들만 정리한 다음 그림을 보기 바란다.

이 과정으로 ESP가 함수 호출이 끝나면서 자연스럽게 복귀됨을 알 수 있다. 이 assembly와 같이 EBP 레지스터를 이용해 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법을 Stack Frame기법이라 한다. 보통 이 과정은

push EBP
mov EBP, ESP
...
mov ESP, EBP, 
pop EBP
ret 

로 압축된다. 이 stack frame을 이용하면 함수 호출 깊이가 깊어져도 스택을 완벽하게 관리할 수 있다. 더 관심있으신 분들은 검색해보시길~ 

 

 

ECX, 그리고 EBP-8 == this

하여간 여기서 중요한 것은 Stack Frame이 어떻게 동작하는가가 아니다. 우리가 궁금한 것은 어떻게 TestClass::TestFunction이 자신의 Instance의 mInt변수값을 알아 낼 수 있는가였다. 이 부분에 해당하는 코드만 따로 보자.


main함수 내에서

    int result1 = a.TestFunction(10);
01041612  push        0Ah  
01041614  lea         ecx,[a]  
01041617  call        TestClass::TestFunction (104111Dh)  
0104161C  mov         dword ptr [result1],eax  

 

TestClass::TestFunction 내에서

010415B0  mov         dword ptr [ebp-8],ecx  
    return mInt + iValue;
010415B3  mov         eax,dword ptr [this]  
010415B6  mov         eax,dword ptr [eax]  
010415B8  add         eax,dword ptr [iValue]  
}


위 부분이 궁금한 부분을 압축한 코드라 할 수 있다. 보자. 함수 호출전에 ecx값에 a변수의 주소가 들어갔었다. TestFunction내에서 ebp-8의 주소에 ecx값을 넣고 있다. 이때 ebp 에는 -12값이 들어있으므로 -20에 ecx값, 곧 a의 변수 값을 넣고 있다. 이 다음에 나오는 코드가 mov eax, dword ptr [this]이다. 이것은 무엇일까?

놀랍게도 이것은 mov eax, dword [ebp-8]과 동일한 의미를 지닌다. 중요한 포인트인데, 바로 클래스 멤버 함수의 ebp-8에는 해당 클래스의 instance의 주소가 들어가게 된다는 사실이다. 이것은 일종의 약속이다. 그리고 해당 instance의 주소에는 클래스 멤버 변수들의 값이 들어있다. 이 경우 해당 instance의 주소에 mInt값이 들어있으므로 mov eax, dword ptr[eax]로 해당 멤버 변수의 값을 eax에 로드할 수 있는 것이다. 그렇다면 만약 TestClass에 int mInt2라는 변수가 추가되어있다면 어떨까? 그리고 이 변수는 어떻게 클래스 함수에서 접근할까? 딩동댕! 다음과 같은 꼴일 것이다.

mov eax, dword ptr[this+4]

 

 

Class Member Function Pointer

비밀들이 벗겨지고 있다. 위에서 설명한 이유 때문에 일반 Function Pointer로 함수 멤버 함수를 호출할 수 없는 것이다. 예를 들어 TestClass::TestFunction의 주소를 얻어와 이것을 일반 Function Pointer로 만든 후 해당 Function Pointer를 호출해보자.


TestClass a(10),b(20);
typedef int (*FPointer)(int value);
PVOID functionAddress = (PVOID)(&(PVOID&)TestClass::TestFunction);
FPointer pointer = (FPointer)functionAddress;
pointer(10);


위 코드는 정상적으로 빌드가 되고 pointer에 TestClass::TestFunction의 주소 값이 정상적으로 들어가지만 Run-time Exception을 발생시킨다. 그리고 이것은 너무나 당연한 결과이다.


00B41621  push        0Ah  
00B41623  call        dword ptr [pointer]  


class의 Instance를 넘겨주는 명령이 빠져있는 것을 볼 수 있다. 이와 반대로 Class Member Function Pointer를 활용해보자.


typedef int (TestClass::*PFClassMember)(int);
PFClassMember classMemberFP = &TestClass::TestFunction;
(a.*classMemberFP)(10);


    int result3 = (a.*classMemberFP)(10);
0107161B  push        0Ah  
0107161D  lea         ecx,[a]  
01071620  call        dword ptr [classMemberFP]  

ecx에 변수 instance a의 주소값을 넘겨주고 있다. 이로써 해당 함수 콜이 정상적으로 동작할 수 있다.

 

 

결론

일반 함수 콜과 클래스 멤버 함수 콜이 어떻게 다른지 기초적인 부분을 알아보았다. 다음 포스트에서는 이 TestFunction 클래스 함수를 후킹해보자! 

 

**혹시 이 포스트에서 제가 잘못 알고 있는 부분이 있다면 꼭 태클해주세요. 감사합니다.

신고