C++Builder Programming Forum
C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
C++빌더 포럼
Q & A
FAQ
팁&트릭
강좌/문서
자료실
컴포넌트/라이브러리
메신저 프로젝트
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
볼랜드포럼 광고 모집

C++빌더 강좌/문서
C++Builder Programming Tutorial&Docments
[186] 빌더에서의 글로벌 후킹과 공유메모리 사용 방법.
김태선 [cppbuilder] 23062 읽음    2009-02-27 06:22
1. preface

C++빌더의 막강한 편리함을 써 본 개발자들은 큰 감탄을 하게 됩니다.
하지만 항상 개발자들이 아쉬워하는 것은 자료가 풍족하지 않다는 것입니다.
그래서 노가다성 코드가 많긴 해도 자료 많은 VC를 쓰는게 낫다는 생각이 들기도 하죠.

C++빌더는 이미 많은 기능이 갖추어져 있으나 몰라서 못 쓰는 경우가 많습니다.
빌더를 제대로 알면 알수록 역시 잘 만들어진 개발도구라는 사실을 새삼 알게 되는데,
공유메모리 문제도 그러합니다.

보통 공유메모리를 쓰는 표준적인 방법으로 널리 쓰이는 것은 FileMapping 을 이용하는 방법입니다.
이 방법이 가장 무난하고 OS가 제공하는 표준이기 때문에 이를 쓰는 것이 좋습니다.
이에 관한 자료는 워낙 쉽고 또 검색하면 잘 나오므로 여기서는 다루지 않습니다.

그런데 VC로 만들어진 후킹 소스 등을 보면 공유 메모리를 이용하는 경우가 종종 있는데
보통 아래와 같은 형식입니다.

// 공유 메모리에 올릴 변수를 선언합니다.
#pragma data_seg(".shared")
    HHOOK             _hHook = NULL;
    HWND            _hTarget = NULL;
#pragma data_seg()

// 공유메모리의 영역에 대한 퍼미션과 링커의 처리를 명시 합니다.
#pragma comment(linker,"/SECTION:.shared.RWS")

이 코드가 빌더에서도 그대로 컴파일 되었으면 좋겠지만, 컴파일 되지 않습니다.
#pragma data_seg(".shared")
#pragma data_seg()
는 에러 없이 컴파일이 되는 듯 보이도, 실지로는 컴파일 되지 않습니다.
원래 #pragma 는 컴파일러에 대한 지시자로 정의되지 않는 항목은 무시하기 때문입니다.
그렇지만
#pragma comment(linker,"/SECTION:.shared.RWS")
는 명확하게 에러를 내는데, 이는 comment 에 대한 인자는, 빌더에서는 lib, exestr, user 로
정해져 있기 때문에 링커의 옵션을 지정한 linker는 처리할 수 없기 때문에 에러를 내는 것입니다.

왜 이를 빌더에서 구현하지 않았는가 하면
이는 표준이 아닐뿐 아니라, 링커에서 어떤 식으로 처리해 최종적으로 결과물 파일을 만들어 낼지는
VC 개발툴을 만든 MS사 마음대로 했기 때문입니다.

하지만 빌더에서도 매우 편리하게 공유메모리를 설정할 수 있습니다.
그러면 빌더에서는 공유메모리를 어떻게 만들 수 있을까요?
(아래 내용에 쓰인 소스는 모두 첨부한 예제 파일에 포함되어 있는 것입니다.
단 Hook.DLL이라고 표현된 것은 예제 파일에 GHook.DLL로 되어 있습니다.)



2. 빌더에서 공유메모리 사용하기.

빌더는 VC에서 특정 메모리 섹션을 공유메모리로 지정하는 것이 아니라
하나의 모듈을 공유메모리로 지정합니다.
즉 하나의 유닛에 선언된 변수는 모두 공유 메모리에 올라갑니다.

아래는 글로벌 후킹 예제에 쓰인 data_seg.cpp 파일 내용 입니다.
//---------------------------------------------------------------------------
// 아래 2줄은 소스 선두에 있어야 한다.
#pragma option -zRSHARESEG    // sets segment name for this source file to SHARESEG
#pragma option -zTSHARECLS    // sets class name for segment to SHARECLS
/*
    여기서 선언된 데이타 형은  data_seg 로 처리된다.
    이를 가능하게 하는 것이 위 2줄 선언이다.
*/

int  g_Handle = 0;
//---------------------------------------------------------------------------

이는  g_Handle 변수를 공유메모리 영역에 올리겠다는 뜻입니다.

그리고 DLL이 로드될 때 위 모듈에 있는 변수들이 공유메모리 영역에 올라 갈수 있도록
.DEF 파일에 명시를 해줍니다.
dll.def 파일의 내용입니다.

; hookdll.cpp 에서 쓰이는 공유메모리 영역에 대한 처리.
LIBRARY HOOKDLL

SEGMENTS
    SHARESEG CLASS "SHARECLS" SHARED


이렇게만 처리해 주면 됩니다.

이렇게 하면 공유 메모리에 대한 설정은 끝입니다.
이제 실지로 공유메모리 영역에 있는 변수를 핸들링(제어)하면 되는 것입니다.
위 내용은 공유 메모리 영역 설정할때
항상 쓰이는 공식 같으니 그냥 파일을 두었다가 필요한 프로젝트에 복사해서 쓰면  아주 간편합니다.



3. 글로벌 후킹 Global Hooking

후킹에는 로컬 후킹과 글로벌 후킹이 있는데, 보통 로컬 후킹은
메시지 핸들러를 서브클래싱 해서 구현하며, 글로벌 후킹은 윈도 API가 제공하는 후킹 함수를 씁니다.
로컬 후킹은 이미 서브클래싱 형태로 많이 접하므로 여기서 설명하지 않고,
글로벌 후킹만 설명하겠습니다. 빌더용으로 설명된 내용이 그간에 없었고,
VC나 델파이쪽 후킹 자료도 보면 명확한 내용을 설명하는 경우가 거의 없었습니다.
특히 왜 후킹에 공유메모리를 이용해야 하는지 명확한 설명은 거의 보기가 어렵더군요.

저도 여기서 후킹에 대한 모든 것을 설명하기는 곤란한데,
이는 저도 필요할때 필요한 부분을 공부해서 쓰기 때문에 전반에 대한 지식은 전하기 곤란하고
다만, 기초적인 키보드 후킹만 해 보겠습니다.

키보드 후킹은 간단해 보여도 사실 키로그 같은 엄청나게 치명적인 해킹툴까지도 만들수 있는
기본 기술이고, 또 의외로 필요한 곳에 활용하면 유용하기도 하며, 조금만 확장하면 마우스나
기타 메시지 후킹을 덧 붙이는 것도 어렵지 않기 때문에,
가장 중요한 기본 프레임웍을 만들어 보겠습니다.

글로벌 키보드 후킹을 위해 윈도  API가 제공하는 함수는 아래와 같습니다.

후킹 시작
g_hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyHook, g_hInst, 0);
후킹 종료
UnhookWindowsHookEx(g_hHook);

SetWindowsHookEx API를 불러 주어 훅을 설치하면,
KeyHook 프로시져를 키입력이나 마우스 입력 등이 발생하면 호출해 줍니다.

LRESULT    CALLBACK  KeyHook(int code, WPARAM wParam, LPARAM lParam)
{
    if (code == HC_ACTION)
    {
        PostMessage(g_Handle, WM_USER_SENDKEY, wParam, lParam);
    }
    return CallNextHookEx(g_hHook, code, wParam, lParam);
}

KeyHook 프로시져는 대략 위와 같이 구성할 수 있습니다.

이 KeyHook 프로시져는 OS 가 호출하기 때문에 DLL 형태이어야 합니다.
즉 Hook.DLL(여기서는 명칭을 Hook.dll이라고 임의로 정한다)과 같은 형식이어야 하는데, 이는
SetWindowsHookEx API를 호출하면 KeyHook 가 있는 DLL 이 메모리로 로드되게 됩니다.

이때 중요한 것은 원래 DLL이 단 한부만 메모리에 올라가는 것이 비해
후킹 DLL은 완전히 따로 메모리에 올라가게 됩니다.
즉 SetWindowsHookEx API를 호출하면 DLL이 별도로 메모리에 올라가게 되며
이때 DLL 속의 메모리 내용은 DLL이 처음 로드되어 초기화 되기 이전의 상태로 됩니다.

보통 위 SetWindowsHookEx API는 Hook.DLL 내에 존재하므로,
이를 기준으로 다시 설명하면 Hook,DLL을 쓰기 위해 후킹 어플리케이션에서 LoadLibrary로 Hook.DLL을
메모리로 로드하면 Hook.DLL이 메모리에 한 부가 존재하게 되고 여기서는 필요한 객체는
모두 초기화 됩니다.

그리고 SetWindowsHookEx API를 호출하여 훅을 설치하면 Hook.DLL이 새로운 메모리
다시 올라가게 됩니다. 이는 OS가 KeyHook 프로시져를 호출해야 하기 때문입니다.
물론 처음 올라간 Hook.DLL 내의 KeyHook 프로시져도 호출해 주지만
두번째 올라간 Hook.DLL 내의 KeyHook 프로시져도 OS가 호출해 주게 됩니다.

이렇게 하는 이유는 어플리케이션에 의해 로딩된 첫번째 Hook.DLL은 그 어플리케이션에서
발생한 메시지를 KeyHook 프로시져에 전달하고,
그외 프로그램에서 발생한 메시지는 두번째 로딩된 Hook.DLL 내의 KeyHook 프로시져에 전달합니다.
그래서 2개의 Hook.DLL이 메모리에 올라가 로컬과 로컬외의 글로벌 모두 키를 잡아 들일 수 있게 되는 것입니다.

이렇게 Hook.DLL이 2부가 올라가는데, DLL 내의 KeyHook 프로시져에서 잡은 메시지를 어플리케이션에
전달할 때 문제가 생기게 됩니다.
첫번째 올라간 Hook.DLL은 어플리케이션과 같은 메모리 영역에 로딩되므로
메시지를 받을 윈도의 핸들을 전달하여 세팅하는 것이 어렵지 않는데,
두번째 올라가 OS에 의해 로딩되는 Hook.DLL은 OS 메모리 영역에 로딩되므로,
어플리케이션과 단절된다는 문제가 발생합니다.

가령 메시지를 받을 어플리케이션의 핸들을 Hook.DLL에 전달해도
첫번째 로딩된 Hook.DLL은 받아 처리하는게 문제가 없는데,
OS에 의해 두번째 로딩된 Hook.DLL은 OS 영역에 있고 오직 KeyHook 프로시져만 동작하기 때문에
어플리케이션에서 어떻게 해볼 도리가 없습니다.

그래서 공유메모리 영역을 설정해 여기에 필요한 데이타를 세팅하고,
두번째 로딩된 Hook.DLL에서 이 데이타를 가져다 쓰게 하는 것입니다.



4. Hook.DLL
메인 소스
//---------------------------------------------------------------------------
#include <windows.h>
#include <stdio.h>
#include "TRACE.h"

#ifdef __DLL__
#define DLL_API extern "C" __declspec(dllexport)
#else
#define DLL_API extern "C" __declspec(dllimport)
#endif

//---------------------------------------------------------------------------
// C 스타일의 파일 제어 클래스
//      - 자동으로 파일이 닫히게 하기
//      - fp 를 생략할 수 있다.

class TFILE
{
public:
    FILE    *fp;
public:
    TFILE(char *filename, char *mode)
    {
        fp = fopen(filename, mode);
    }
    ~TFILE()
    {
        if (fp) fclose(fp);
    }
    int fseek(int ptr, int whence)
    {
        return ::fseek(fp, ptr, whence);
    }
    int GetFileSize()
    {
        int  cur = fseek(0, SEEK_CUR);
        fseek(0, SEEK_END);
        int  filesize = ::ftell(fp);
        fseek(cur, SEEK_SET);
        return filesize;
    }
    int fread(void *ptr, int size, int pcs)
    {
        return ::fread(ptr, size, pcs, fp);
    }
    int fwrite(void *ptr, int size, int pcs)
    {
        return ::fwrite(ptr, size, pcs, fp);
    }
    char*  fgets(char *buf, int len)
    {
        return ::fgets(buf, len, fp);
    }
    int  fputs(char *buf)
    {
        return ::fputs(buf, fp);
    }
    int  fflush()
    {
        return ::fflush(fp);
    }
    // 필요한 것은 아래에 더 추가해서 사용.
    ;
};
//---------------------------------------------------------------------------

#define WM_USER_SENDKEY        WM_USER + 9999

HHOOK         g_hHook;
HINSTANCE     g_hInst;
extern HWND    g_Handle;       // 메시지를 받을 핸들.  공유메모리 상에 존재하는 객체.

TFILE        g_logfile("key_log.txt", "a");


BOOL WINAPI  DllMain(HINSTANCE hInst, DWORD reason, LPVOID reserved)
{
    g_hInst = hInst;

    return TRUE;
}

// 키보드 후킹 프로시저
//
LRESULT    CALLBACK  KeyHook(int code, WPARAM wParam, LPARAM lParam)
{
    if (code == HC_ACTION)
    {
        // debug
        char  buf[100];
        char  c = wParam & 0xff;
        wsprintf(buf, "key=%04X[%c] %08X\n", wParam, c > 0 ? c : '.', lParam);
        g_logfile.fputs(buf);    // local  hook 결과만 기록.
        TRACE(buf);                // global hook 모니터.

        // application으로 데이타 전송.
        PostMessage(g_Handle, WM_USER_SENDKEY, wParam, lParam);
    }
    return CallNextHookEx(g_hHook, code, wParam, lParam);
}

// Hook 설치
//        handle은 메시지가 발생했을때 그 메시지를 받을 윈도를 지정한다.
//
DLL_API bool WINAPI SetHook(HWND handle)
{
    bool  ret = false;
    if (!g_hHook)
    {
        g_hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyHook, g_hInst, 0);
        if (g_hHook)
        {
            g_Handle = handle;
            ret = true;
        }
    }
    return ret;
}

// Hook 제거
//
DLL_API bool WINAPI  FreeHook()
{
    bool  ret = false;
    if (g_hHook)
    {
        ret = UnhookWindowsHookEx(g_hHook);
        if (ret)
            g_hHook = NULL;
    }
    return ret;
}
//---------------------------------------------------------------------------

TFILE 클래스는 Log 기록용이라 그냥 본론과 상관없으니 무시해도 좋습니다.
DLL 파일이고 어플리케이션에서 이 기능 제어를 하게 하기 위해 함수를 export 해줘야 하는데,

훅 설치
DLL_API bool WINAPI SetHook(HWND handle)
혹 제거
DLL_API bool WINAPI  FreeHook()

두 개의 함수로 충분합니다.
훅 설치시 글로벌 후킹 메시지를 받을 윈도의 핸들을 정해 줄수 있게 합니다.

메모리에 올라간 2개의 Hook.DLL이 동일한 윈도로 메시지를 날려야 하기 때문에
바로 이 윈도 핸들 값을 담은 변수를 공유 메모리에 둡니다.
그것이 2. 빌더에서 공유메모리 사용하기 파트에 있는 내용입니다.

위 소스에 보면 TRACE로 들어온 내용을 보는 것이 있는데
이는 필자가 만든 TraceWindow 디버깅 창으로 디버깅 메시지를 보내는 역할을 합니다.
또 로그 파일로 기록하는 내용이 들어 있는데,
로그 파일에는 로컬 훅 내용만 기록되고 글로벌 훅 내용은 기록되지 않습니다.
반면 TRACE 로 내용을 보낸 TraceWindow 디버깅 창에는 글로벌 후킹 결과가 모두 기록됩니다.
이는 로그 파일을 핸들링하는 클래스가 공유 메모리 영역에 있지 않기 때문에
이러한 차이가 생기는 것입니다. 보다 정확하게 말해, 클래스의 코드는 올라가지만
OS에 의해 두번째 Hook.DLL이 로드될때 객체에 대한 초기화가 진행되지 않기 때문에, 동작을 하지 않는 것입니다.
위에서 자세하게 왜 이런 문제가 생기는지 설명했으므로 이 정도로 이에 관한 설명은 마칩니다.


5. 후킹 어플리케이션
다음은  후킹 어플리케이션의 메인 소스인데,
내용이 아주 간단하므로 소스만 봐도 알수 있습니다.
//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop
#include "TRACE.h"

#define WM_USER_SENDKEY        WM_USER + 9999

#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;



typedef bool (WINAPI *FSetHook)(HWND handle);
typedef bool (WINAPI *FFreeHook)();

HINSTANCE  dll_inst = NULL;

//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
}
//---------------------------------------------------------------------------

void __fastcall TForm1::FormCreate(TObject *Sender)
{
    TRACE("keyboard hook 시작합니다...");
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
    // Hook DLL 로드
    dll_inst = LoadLibrary("..\\dll\\GHook.dll");
    if(dll_inst == NULL)
    {
        ShowMessage("훅 DLL을 로드할 수 없습니다.");
        return;
    }
    // hook 함수 찾기.
    FSetHook  SetHook  = (FSetHook) GetProcAddress(dll_inst, "SetHook");
    if (SetHook == NULL)
    {
        ShowMessage("DLL에 필요한 훅 메소드를 찾을 수 없습니다.");
        FreeLibrary(dll_inst);
        dll_inst = NULL;
        return;
    }
    // 훅 설치.
    if (SetHook(Handle))
    {
        Caption = "start";
    }
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
    Unhook();
}
//---------------------------------------------------------------------------

void    TForm1::WM_User_SendKey(short key, UINT keystate)
{
    char  c = key & 0xff;
    char  buf[100];
    wsprintf(buf, "APP key=%04X[%c] %08X", key, c > 0 ? c : '.', keystate);
    Memo1->Lines->Add(buf);
}
//---------------------------------------------------------------------------

void    TForm1::Unhook()
{
    if (!dll_inst)
        return;
    FFreeHook  FreeHook = (FFreeHook) GetProcAddress(dll_inst, "FreeHook");
    if (FreeHook == NULL)
    {
        ShowMessage("필요한 훅 메소드를 찾을 수 없습니다.");
    }
    else
    {
        FreeHook();
    }
    FreeLibrary(dll_inst);
    dll_inst = NULL;
}
//---------------------------------------------------------------------------

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
    Unhook();
}
//---------------------------------------------------------------------------

Hook.DLL에서 메시지가 잡히면
다시 위의 폼으로 메시지를 전달하게 되는데,
이를 잡기 위해 메시지 핸들러를 만들어야 합니다.
필자는 아래처럼 그냥 Dispatch 메시지 핸들러를 override 하는 방법으로 메시지를 핸들링 했습니다.
매크로를 쓰나 아래처럼 실지 코드를 넣나 사실은 같은 것입니다.
//---------------------------------------------------------------------------

#ifndef Unit1H
#define Unit1H
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:    // IDE-managed Components
    TButton *Button1;
    TButton *Button2;
    TMemo *Memo1;
    TEdit *Edit1;
    void __fastcall FormCreate(TObject *Sender);
    void __fastcall Button1Click(TObject *Sender);
    void __fastcall Button2Click(TObject *Sender);
    void __fastcall FormDestroy(TObject *Sender);
private:    // User declarations
    //
    void WM_User_SendKey(short key, UINT keystate);
    //
    virtual void __fastcall Dispatch(void *Message)
    {
        TMessage  *msg = (PMessage)Message;
        switch (msg->Msg)
        {
            case WM_USER_SENDKEY :
                WM_User_SendKey(msg->WParamLo, msg->LParam);
                return;
        }
        TForm::Dispatch(Message);
    }
public:        // User declarations
    __fastcall TForm1(TComponent* Owner);
    void    Unhook();

};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif


이렇게 하면 메모리에 올라간 두개의 DLL로 부터
키보드 메시지는 모두 TForm1 으로 오게 됩니다.

여기서 키보드 메시지는 키보드 값과 키의 상태에 관한
값이 전달되므로 이를 적절히 처리해 주어야 합니다.


위 예제는 모두 빌더6로 만들어 테스트 하였습니다.


밤 샜더니 졸려서 이만..
장성호 [nasilso]   2009-02-27 13:43 X
감사합니다.
DCAS [simulica]   2009-03-02 23:07 X
잘 읽고 갑니다.
은진 아빠 [squid01]   2009-03-20 17:43 X
^^ 좋은글 감사합니다.
풀버전 [cns5690]   2009-04-28 11:02 X
잘 읽었습니다. 이제막 배우기시작한 초본데요~
혹시 이렇게하여 C++빌더와 VC6.0이나 VC2005에서 짠 프로그램과도 메모리 공유가 되는건가요?
파일매핑 방식보다 속도가 빠른가요?
감사합니다.
풀버전 [cns5690]   2009-04-29 10:20 X
빌더와 VC에서 모두 메모리 공유가 되는군요 ^^
페이지파일에 파일매팡하는방식과 이방식과의 속도차이가 궁금하군요.
확인하시면 댓글 부탁드립니다.
감사합니다.
박목월 [godori21]   2009-06-15 15:24 X
#pragma option -zRSHARESEG   
#pragma option -zTSHARECLS  
무료버전엔 이게 사용안되는데 원래그런가요?

+ -

관련 글 리스트
186 빌더에서의 글로벌 후킹과 공유메모리 사용 방법. 김태선 23062 2009/02/27
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.