Microsoft MVP성태의 닷넷 이야기
VC++: 114. C++ vtable의 가상 함수 호출 가로채기 [링크 복사], [링크+제목 복사],
조회: 13345
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 3개 있습니다.)
VC++: 112. C++의 가상 함수 테이블 (vtable)은 언제 생성될까요?
; https://www.sysnet.pe.kr/2/0/11167

VC++: 113. C++ 클래스 상속 관계의 vtable 생성 과정
; https://www.sysnet.pe.kr/2/0/11168

VC++: 114. C++ vtable의 가상 함수 호출 가로채기
; https://www.sysnet.pe.kr/2/0/11169




C++ vtable의 가상 함수 호출 가로채기

가상 함수 테이블(virtual function table)은,

C++의 가상 함수 테이블 (vtable)은 언제 생성될까요?
; https://www.sysnet.pe.kr/2/0/11167

C++ 클래스 상속 관계의 vtable 생성 과정
; https://www.sysnet.pe.kr/2/0/11168

알아본 바와 같이 일단 메모리에 올라오면 고정이고, (변경이 없다면) 재실행하는 경우에도 동일한 메모리 주소로 로딩됩니다. (ASLR인 경우에도 ImageBase를 고려한 변위는 동일하게 유지됩니다.) 이런 성질 덕분에, 가로채기 함수를 구현하는 것이 매우 쉽습니다. 다음의 코드로 예를 들어보겠습니다.

#include "stdafx.h"

class A
{
public:
    int n1 = 1;

    void func1() { n1++; }

    virtual void vfunc1() { }
};

class BonA : A
{
public:
    int n2 = 2;

    void func2() { n2++; }

    virtual void vfunc2() { }
};

int main()
{
    BonA *table = new BonA();

    return 0;
}

table 변수에 할당된 주소로부터 vtable의 포인터를 다음과 같이 구할 수 있습니다.

BonA *table = new BonA();

void **pvftable = (void **)(*(__int64 *)table);

또한 pvftable 변수를 이용해 다음과 같이 가상 함수의 주소를 차례로 구해올 수 있습니다.

for (int i = 0; i < 2; i ++)
{
    printf("vtable[%d] == 0x%x\n", i, pvftable[i]);
}

/*
출력 결과:

vtable[0] == 0x9a1361   // A::vfunc1의 주소
vtable[1] == 0x9a1131   // BonA::vfunc2의 주소
*/

만약 vtable[0]번 항목인 A::vfunc1을 가로채기하고 싶다면 다음과 같이 새로운 함수 주소를 넣어주면 됩니다.

pvftable[0] = (int *)&intercept_vfunc1;

그런데 실제로 위와 같이 코드를 실행하면 다음과 같은 예외가 발생합니다.

Exception thrown: write access violation.
**pvftable** was 0xC77B40. occurred

왜냐하면, vtable의 데이터 저장 위치가 .rdata 섹션이고 그 영역은 IMAGE_SCN_MEM_READ 속성만 지정되어 있으므로 쓰기 실패가 발생합니다. 따라서 이 작업을 성공시키려면 메모리 접근 속성을 변경해야 합니다.

DWORD dwAttr = PAGE_EXECUTE_READWRITE;
DWORD oldProtect = 0;

BOOL result = VirtualProtect(pvftable, 4, dwAttr, &oldProtect); // PAGE_EXECUTE_READWRITE 가능하게 바꾸고,

pvftable[0] = (void *)&intercept_vfunc1; // .rdata 섹션 영역에 쓰기 작업을 한 다음,

VirtualProtect(pvftable, 4, oldProtect, &dwAttr); // 다시 원복

자, 그럼 이제 intercept_vfunc1 함수에 대해 이야기해 볼까요? 대부분의 경우, 가로채기하는 함수는 전/후처리를 한 다음 원본 메서드를 호출하는 경우가 많습니다. 따라서 원본 가상 함수를 덮어 쓰기 전에 다음과 같이 별도로 주소를 복사해 놓기 마련입니다.

g_ptrOrgVFunc1 = (void *)pvftable[0];

또한, 가로채기 함수(이 글에서는 intercept_vfunc1)가 불필요한 prolog/epilog 코드를 가지지 못하도록 다음과 같이 naked 형식의 함수로 지정합니다.

int g_i = 0;
void *g_ptrOrgVFunc1 = nullptr;

__declspec(naked) void intercept_vfunc1()
{
    g_i++;

    __asm {
        jmp [g_ptrOrgVFunc1]
    }
}

다음의 소스 코드는 이 모든 것을 합친 완전한 예제를 보여줍니다.

#include "stdafx.h"
#include <Windows.h>

class A
{
public:
    int n1 = 1;

    void func1() { n1++; }

    virtual void vfunc1() 
    { 
        printf("%d\n", 50);
    }
};

class BonA : A
{
public:
    int n2 = 2;

    void func2() { n2++; }

    virtual void vfunc2() { }
};

int g_i = 0;
void *g_ptrOrgVFunc1 = nullptr;

__declspec(naked) void intercept_vfunc1()
{
    g_i++;

    __asm {
        jmp [g_ptrOrgVFunc1]
    }
}


int main()
{
    BonA *table = new BonA();

    // x86
    int **pvftable = (int **)(*(int *)table);

    g_ptrOrgVFunc1 = (void *)pvftable[0];

    DWORD dwAttr = PAGE_EXECUTE_READWRITE;
    DWORD oldProtect = 0;

    int ptrSize = sizeof(int *);
    BOOL result = VirtualProtect(pvftable, ptrSize, dwAttr, &oldProtect);

    pvftable[0] = (int *)&intercept_vfunc1;

    VirtualProtect(pvftable, ptrSize, oldProtect, &dwAttr);

    A *aInst = (A *)table;
    aInst->vfunc1();

    return 0;
}




naked로 하면 성능 영향을 최소화하면서 필요한 최소 코드를 넣을 수 있다는 장점은 있지만 아무래도 코딩하기가 불편하다는 단점이 있습니다. 그래서 다음과 같은 형식으로 preprocess에 필요한 코드를 별도의 함수로 만들어 호출하는 것도 가능합니다.

void preprocess_vfunc1()
{
    printf("g_i == %d\n", g_i);
}

__declspec(naked) void intercept_vfunc1()
{
    g_i++;
    preprocess_vfunc1();

    __asm {
        jmp [g_ptrOrgVFunc1]
    }
}

문제는, 이것이 인자를 갖는 경우에는 naked이기 때문에 인자에 따른 스택 구성을 잘 맞춰주어야 합니다. 가령, 인자 1개를 받는 가상 함수를 가로채고 싶다면 다음과 같이 전처리를 해줘야 합니다.

class A
{
public:
    int n1 = 1;

    void func1() { n1++; }

    virtual void vfunc1() 
    { 
        printf("%d\n", 50);
    }

    virtual void vfunc2(int arg1)
    {
        printf("%d\n", n1 + arg1);
    }
};

void *g_ptrOrgVFunc2 = nullptr;

void preprocess_vfunc2(int arg1)
{
    printf("g_i == %d, arg1 == %d\n", g_i, arg1);
}

__declspec(naked) void intercept_vfunc2() // 가로채기 함수는 naked 형식이므로 인자 지정이 필요 없음.
{
    g_i++;

    __asm {
        push ecx
        mov eax, [esp + 8]
        push eax
        call preprocess_vfunc2;
        add esp, 4
        pop ecx
    }

    __asm {
        jmp [g_ptrOrgVFunc2]
        ret
    }
}




참고로 위의 코드는 x64 빌드에서는 사용할 수 없습니다. 왜냐하면 naked 옵션과 함께 inline 어셈블리가 x64에서는 지원되지 않기 때문입니다. 그래도 x64의 경우 호출 규약이 단일로 통일된 덕분에 쉽게 가로채기를 구현할 수 있습니다.

아래의 코드는 x64/x86 상관없이 vtable 호출을 가로채는 코드를 보여줍니다.

#include "stdafx.h"
#include <Windows.h>

class A
{
public:
    int n1 = 1;

    void func1() { n1++; }

    virtual void vfunc1() 
    { 
        printf("%d\n", 50);
    }

    virtual void vfunc2(int arg1)
    {
        printf("%d\n", n1 + arg1);
    }
};

class BonA : A
{
public:
    int n2 = 2;

    void func2() { n2++; }

    virtual void vfunc3() { }
};

int g_i = 0;
void *g_ptrOrgVFunc1 = nullptr;
void *g_ptrOrgVFunc2 = nullptr;

void preprocess_vfunc1()
{
    printf("g_i == %d\n", g_i);
}

void preprocess_vfunc2(int arg1)
{
    printf("g_i == %d, arg1 == %d\n", g_i, arg1);
}

#if defined(_AMD64_)
typedef void(*VFunc1)(void *thisPtr);
typedef void(*VFunc2)(void *thisPtr, int arg1);

void intercept_vfunc1(void *thisPtr)
{
    g_i++;
    preprocess_vfunc1();

    VFunc1 vFunc = (VFunc1)g_ptrOrgVFunc1;
    vFunc(thisPtr);
}

void intercept_vfunc2(void *thisPtr, int arg1)
{
    g_i++;

    preprocess_vfunc2(arg1);

    VFunc2 vFunc = (VFunc2)g_ptrOrgVFunc2;
    vFunc(thisPtr, arg1);
}
#else
__declspec(naked) void intercept_vfunc1()
{
    g_i++;
    preprocess_vfunc1();

    __asm {
        jmp [g_ptrOrgVFunc1]
    }
}

__declspec(naked) void intercept_vfunc2()
{
    g_i++;

    __asm {
        push ecx
        mov eax, [esp + 8]
        push eax
        call preprocess_vfunc2;
        add esp, 4
        pop ecx
    }

    __asm {
        jmp [g_ptrOrgVFunc2]
        ret
    }
}

#endif
int main()
{
    BonA *table = new BonA();

#if defined(_AMD64_)
    // x64
    __int64 **pvftable = (__int64 **)(*(__int64 *)table);
#else
    // x86
    int **pvftable = (int **)(*(int *)table);
#endif


    for (int i = 0; i < 2; i ++)
    {
        printf("vtable[%d] == 0x%x\n", i, pvftable[i]);
    }

    g_ptrOrgVFunc1 = (void *)pvftable[0];
    g_ptrOrgVFunc2 = (void *)pvftable[1];

    DWORD dwAttr = PAGE_EXECUTE_READWRITE;
    DWORD oldProtect = 0;

    int ptrSize = sizeof(int *);

    BOOL result = VirtualProtect(pvftable, ptrSize * 2, dwAttr, &oldProtect);

#if defined(_AMD64_)
    pvftable[0] = (__int64 *)&intercept_vfunc1;
    pvftable[1] = (__int64 *)&intercept_vfunc2;
#else
    pvftable[0] = (int *)&intercept_vfunc1;
    pvftable[1] = (int *)&intercept_vfunc2;
#endif

    VirtualProtect(pvftable, ptrSize * 2, oldProtect, &dwAttr);

    A *aInst = (A *)table;

    aInst->vfunc1();
    aInst->vfunc2(5000);

    table->vfunc3();

    return 0;
}

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 5/16/2018]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 46  47  48  49  [50]  51  52  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12379정성태10/21/202010656.NET Framework: 955. .NET 메서드의 Signature 바이트 코드 분석 [1]파일 다운로드2
12378정성태10/15/202010120.NET Framework: 954. C# - x86/x64 환경에 따라 달라지는 P/Invoke 함수의 export 이름파일 다운로드1
12377정성태10/15/202011419디버깅 기술: 172. windbg - 파일 열기 시점에 bp를 걸어 파일명 알아내는 방법(Managed/Unmanaged)
12376정성태10/15/20208180오류 유형: 669. windbg - sos의 name2ee 명령어 실행 시 "Failed to request module list." 오류
12375정성태10/15/20209506Windows: 177. 윈도우 탐색기에서 띄우는 cmd.exe 창의 디렉터리 구분 문자가 'Yen(&#0165;)' 기호로 나오는 경우 [1]
12374정성태10/14/202014115.NET Framework: 953. C# 9.0 - (6) 함수 포인터(Function pointers) [1]파일 다운로드2
12373정성태10/14/20209425.NET Framework: 952. OpCodes.Box와 관련해 IL 형식으로 직접 코딩 시 유의할 점
12372정성태10/13/202011341.NET Framework: 951. C# 9.0 - (5) 로컬 함수에 특성 지정 가능(Attributes on local functions)파일 다운로드1
12371정성태10/13/202010033개발 환경 구성: 519. Visual Studio의 Ctrl+Shift+U (Edit.MakeUppercase) 단축키가 동작하지 않는 경우
12370정성태10/13/202010900Linux: 33. Linux - nmcli를 이용한 고정 IP 설정
12369정성태10/12/202013740Windows: 176. Raymond Chen이 한글날에 밝히는 윈도우의 한글 자모 분리 현상 [3]
12368정성태10/12/20209771오류 유형: 668. VSIX 확장 빌드 - The "GetDeploymentPathFromVsixManifest" task failed unexpectedly.
12367정성태10/12/202022516오류 유형: 667. Ubuntu - Temporary failure resolving 'kr.archive.ubuntu.com' [2]
12366정성태10/12/202011560.NET Framework: 950. C# 9.0 - (4) 원시 크기 정수(Native ints) [1]파일 다운로드1
12365정성태10/12/202010504.NET Framework: 949. C# 9.0 - (3) 람다 메서드의 매개 변수 무시(Lambda discard parameters)파일 다운로드1
12364정성태10/11/202011708.NET Framework: 948. C# 9.0 - (2) localsinit 플래그 내보내기 무시(Suppress emitting localsinit flag)파일 다운로드1
12363정성태10/11/202012618.NET Framework: 947. C# 9.0 - (1) 대상으로 형식화된 new 식(Target-typed new expressions) [2]파일 다운로드1
12362정성태10/11/20209393VS.NET IDE: 151. Visual Studio 2019에 .NET 5 rc/preview 적용하는 방법
12361정성태10/11/202010980.NET Framework: 946. C# 9.0을 위한 개발 환경 구성
12360정성태10/8/20208216오류 유형: 666. The type or namespace name '...' does not exist in the namespace 'Microsoft.VisualStudio.TestTools' (are you missing an assembly reference?)
12359정성태10/7/20209731오류 유형: 665. Windows - 재부팅 후 iSCSI 연결이 끊기는 문제
12358정성태10/7/20209749오류 유형: 664. Web Deploy 설치 시 "A newer version of Microsoft Web Deploy 3.6 was found on this machine." 오류 [3]
12357정성태10/7/20207827오류 유형: 663. 이벤트 로그 - The storage optimizer couldn't complete retrim on New Volume
12356정성태10/7/202022665오류 유형: 662. ASP.NET Core와 500.19, 500.21 오류 (0x8007000d)
12355정성태10/3/20207903오류 유형: 661. Hyper-V Linux VM의 Internal 유형의 가상 Switch에 대한 IP 연결이 되지 않는 경우
12354정성태10/2/202020754오류 유형: 660. Web Deploy (msdeploy.axd) 실행 시 오류 기록 [1]
... 46  47  48  49  [50]  51  52  53  54  55  56  57  58  59  60  ...