이전 강좌에서 이어지는 내용입니다. 

어셈블리 언어를 이용한 Code Injection (4)



# MessageBoxA() 파라미터 입력 1 – MB_OK

002D002C    6A 00         PUSH 0

PUSH 0 은 스택에 0 을 입력하는 명령입니다. 이 0 의 의미는 아래에서 호출될 MessageBoxA() API 의 네 번째 파라미터(uType)로 사용됩니다.

참고로 MessageBoxA() API 는 아래와 같이 4 개의 파라미터를 받습니다.

int WINAPI MessageBox(
  __in_opt  HWND hWnd,
  __in_opt  LPCTSTR lpText,
  __in_opt  LPCTSTR lpCaption,
  __in      UINT uType
);


* 참고
uType 값이 0 이면 MB_OK 를 의미하며, 단순히 OK(“확인”) 버튼 한 개만 보여주게 됩니다.


# MessageBoxA() 파라미터 입력 2 – "ReverseCore"

002D002E    E8 0C000000   CALL 002D003F
002D0033    52            PUSH EDX
002D0034    65:76 65      JBE SHORT 002D009C
002D0037    72 73         JB SHORT 002D00AC
002D0039    65:43         INC EBX
002D003B    6F            OUTS DX,DWORD PTR ES:[EDI]
002D003C    72 65         JB SHORT 002D00A3
002D003E    00E8          ADD AL,CH

이번에는 CALL 명령으로 코드 사이에 포함된 문자열 데이터 주소를 스택에 입력하는 기법을 소개하겠습니다. 이 역시 Assembly 프로그래밍 언어에서만 가능한 기법입니다.

위 2D0033 ~ 2D003E 주소 영역은 분명히 프로그램 코드 영역이지만 그 내용은 사실 "ReverseCore" 문자열 데이터 입니다. (붉은색 표시 부분) 즉, "ReverseCore" 문자열의 시작주소는 2D0033 입니다. 그리고 이 문자열은 MessageBoxA() API 의 세 번째 파라미터 (lpCaption)로 사용됩니다. 

함수 파라미터로 사용되려면 문자열 주소를 스택에 넣어줘야 하는데 과연 어떤 방식으로 입력할까요? 

2D002E 주소의 CALL 002D003F 명령을 디버깅으로 쫓아 들어가 보겠습니다. (StepIn [F7]) 그리고 아래 그림과 같이 스택 주소를 봐주시기 바랍니다.


<Fig. 13>

스택에 "ReverseCore" 문자열 시작 주소인 2D0033 이 입력되었습니다!!! MessageBoxA() 의 세 번째 파라미터가 입력된 셈이죠.

이 트릭은 CALL 명령어의 "동작원리"를 응용한 것입니다.

CALL 002D003F 명령을 수행하면 함수(2D003F) 가 종료된 후 돌아올 리턴 주소(2D0033)를 스택에 입력(PUSH)한 후 해당 함수 주소(2D003F)로 이동(JMP)합니다. 즉, CALL 명령어는 PUSH, JMP 명령어를 합쳐 놓은 것입니다. 

사실 2D003F 는 함수 형태가 아닙니다. RETN 명령어로 되돌아가는 형태가 아니란 얘기지요. 여기서의 CALL 002D003F 명령어는 바로 뒤에 이어지는 "ReverseCore" 문자열 주소를 스택에 입력하고 그 다음 코드 명령어로 가기 위해서 사용되고 있는 것입니다.

이해 되시나요? ^^ 재미있는 CALL 명령어 사용법입니다.


# MessageBoxA() 파라미터 입력 3 – "www.reversecore.com"

002D003F    E8 14000000   CALL 002D0058
002D0044    77 77         JA SHORT 002D00BD
002D0046    77 2E         JA SHORT 002D0076
002D0048    72 65         JB SHORT 002D00AF
002D004A    76 65         JBE SHORT 002D00B1
002D004C    72 73         JB SHORT 002D00C1
002D004E    65:636F 72    ARPL WORD PTR GS:[EDI+72],BP
002D0052    65:           PREFIX GS:
002D0053    2E:636F 6D    ARPL WORD PTR CS:[EDI+6D],BP
002D0057    006A 00       ADD BYTE PTR DS:[EDX],CH

역시 위 "ReverseCore" 문자열과 마찬가지로 MessageBoxA() API 의 두 번째 파라미터 lpText 문자열("www.reversecore.com")을 입력하는 명령입니다.

위 코드에서 빨간색으로 표시된 부분은 코드 명령어가 아니라 문자열 데이터("www.reversecore.com") 입니다. 

2D003F 주소의 CALL 002D0058 명령어는 (앞에서 설명 드린 바와 같이) 바로 뒤에 이어지는 문자열("www.reversecore.com") 데이터의 주소(2D0044)를 스택에 입력하고 그 다음 명령어 주소(2D0058)로 갑니다. (아래 그림 참고)


<Fig. 14>


# MessageBoxA() 파라미터 입력 4 – NULL

002D0058    6A 00         PUSH 0

MessageBoxA() API 의 첫 번째 파라미터인 hWnd 값을 입력합니다. 일반적으로는 메시지 박스가 소속된 윈도우 핸들을 입력하지만, 여기서는 NULL 을 입력하여 무소속(?) 메시지 박스가 출력되도록 만들겠습니다.


# MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK) 호출

002D005A    FFD0          CALL EAX

드디어 MessageBoxA() API 를 호출하는 CALL 명령어 입니다. 현재 EAX 레지스터에는 위에서 호출한 GetProcAddress() 에 의해서 리턴된 MessageBoxA() API 의 시작 주소(7793EA71)가 저장되어 있습니다. (<Fig. 12> 참고)

2D005A 주소의 CALL EAX 명령어까지 디버깅한 후 레지스터와 스택을 살펴보면 아래 그림과 같습니다.


<Fig. 15>

이 CALL EAX 명령어를 실행하면 메시지 박스가 나타날 것입니다.


<Fig. 16>


# ThreadProc() 리턴값 세팅

002D005C    33C0          XOR EAX,EAX

notepad.exe 프로세스에 인젝션된 코드(ThreadProc() 스레드 함수) 가 종료될 준비를 합니다. 스레드 함수의 리턴값을 0 으로 세팅하기 위해서 XOR EAX, EAX 명령어가 사용됩니다. 함수의 리턴값은 EAX 레지스터를 사용한다는 것을 기억 하시죠?

* 참고
XOR EAX, EAX 명령어는 EAX 레지스터를 0 으로 초기화하는 가장 쉽고 빠른 명령어 입니다. 디버깅하면서 많이 접하게 될 것입니다.


# Stack Frame 해제 및 함수 리턴

002D005E    8BE5          MOV ESP,EBP
002D0060    5D            POP EBP
002D0061    C3            RETN

ThreadProc() 함수 시작할 때 생성한 Stack Frame 을 해제 합니다. 그리고 RETN 명령으로 함수가 종료됩니다.

이 ThreadProc() 함수에서 Stack Frame 은 매우 중요합니다. 앞에서 설명 드린 "PUSH 를 이용한 스택에 문자열 넣는 기법" 에서 스택에 입력된 문자열을 일일이 POP 명령으로 힘들게 없앨 필요 없이 Stack Frame 해제 명령어 한방으로 가볍게 초기화 시킬 수 있습니다.

* 참고
Stack Frame 관련 내용은 아래 링크를 참고하세요~


+--+

Assembly 언어를 이용한 Code Injection 에 관한 설명을 마치도록 하겠습니다. C 언어 보다 더 자유로운 Assembly 언어를 사용하여 다양하고 창의적인 코드를 생성해 보시기 바랍니다. Assembly 초보자도 OllyDbg 의 "Assemble" 명령어를 이용하면 좀 더 쉽게 이용하실 수 있습니다.

위 실습을 다 끝내신 분께서는 한번 제 블로그 이미지에 있는 바이트 코드를 입력해서 실행해 보시기 바랍니다. ^^ 어떤 코드가 나타날까요? (OllyDbg 의 편집 기능과 New Origin Here 기능을 사용하면 되겠지요~)


ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

Trackback Address :: http://www.reversecore.com/trackback/88 관련글 쓰기

  1. 철이 2010/07/06 15:44 댓글주소 | 수정 | 삭제 | 댓글

    정말로 잘보았습니다 ㅎ

  2. sonickaka 2010/07/10 14:23 댓글주소 | 수정 | 삭제 | 댓글

    정말 좋은 포스팅 감사합니다 ^^
    코드 인젝션을 체계적으로 정리해볼수 있어서 너무 갚지네요.. ^^
    감사합니다~!

  3. 땅콩 2010/07/24 11:35 댓글주소 | 수정 | 삭제 | 댓글

    한번 꼭 다 봐야지 봐야지..하면서...계속 미루다가..방학이라서..
    매일보고 있습니다..ㅎㅎ (analysis..부분만..거의 다 본듯..)

    이렇게 좋은 자료를..날로 먹는거 같아서....

    암튼..너무 감사합니다...

  4. 2010/07/31 00:11 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

    • reversecore 2010/08/01 22:41 댓글주소 | 수정 | 삭제

      안녕하세요.

      비도덕적이고 불법적인 내용이 아니라면 제가 작은 도움을 드릴 수 도 있습니다.

      관련된 질문 있으시면 올려주세요~

      감사합니다.

  5. 2010/08/02 10:48 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

    • reversecore 2010/08/04 20:43 댓글주소 | 수정 | 삭제

      안녕하세요.

      PE Viewer 제작이야 말로 PE Header 공부의 완성판이라고 할 수 있지요. ^^

      제가 예전에 Assembly 로 개발한게 있긴 한데요... 소스를 공개할만한 수준은 아니라서요...

      그때 저는 Microsoft 의 Dumpbin 유틸리티를 참고해서 콘솔버전으로 만들었습니다.

      다른 소스는 가지고 있는게 없네요~

      요령이라고 할것도 없지만...
      그냥 편하게 Dumpbin 이나 PEView 같이 기본적인 정보를 나열하는 식으로 만드시면 됩니다.

      dbghelp.dll 에 기본적인 API 함수가 지원되기는 하지만 그보다는 파일 매핑을 사용해 직접 구현하시는 방법을 추천드립니다.
      (기본적인 구조체는 winnt.h 에 잘 정의되어 있구요...)

      감사합니다.

  6. gjuRlo 2010/08/04 10:06 댓글주소 | 수정 | 삭제 | 댓글

    8월 마소 강좌 코드 인젝션에 이끌러 이 홈페이지까지 왔다가 잘 보고 갑니다. 감사합니다~

  7. 땅콩 2010/08/06 13:19 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 ..공부하다가 막히는 부분이 있어서 .글을 올립니다.

    사실은 데브피아에 글도 올려봤는데..답변이..없어서...이렇게 실례를 무릅쓰고...ㅜㅜ

    요즘 디바이스를 공부중에 잇는 학생입니다.



    ssdt 후킹을 해서 계산기를 프로세스를 종료를 못하게 했습니다..

    (zwterminateprocess를 후킹했습니다..)



    그런데..sdtrestore라는 http://www.security.org.sg/code/sdtrestore.html 여기서 받은걸로는 탐지를 못하는데

    (sdtrestore의 원리는 ntoskrnl.exe에서 정보를 바로 읽어온다고 알고 있는데....)



    하지만....icesword는 탐지를 했습니다..요 방법이 아주 궁금해서 글을 올립니다..



    과연 icesword의 탐지 방법을 무엇인지......



    답변 부탁드립니다. ㅎㅎ



    (제 실행환경은 xp에 sp3 입니다.

    • reversecore 2010/08/09 11:31 댓글주소 | 수정 | 삭제

      안녕하세요.

      후킹 탐지 방법은 후킹 방법처럼 무수히 많이 있답니다.

      저도 icesword 를 본적이 있습니다. 세부 동작 원리까지는 자세히 모르지만 매우 치졸한(?) 방법을 많이 사용했을 것입니다. ^^

      무슨 뜻이냐면... 후킹 탐지 되지 않기 위해서 별의별 기발한 기법들이 동원되는데요... 이를 탐지하는 입장에서도 똑같이 희안하고 무식하지만 확실한 (full scan 같은) 방법들을 동원하는 것입니다.

      커널 후킹은 고급주제이며 향후 제 블로그에 자세히 다룰 예정입니다.

      감사합니다.

  8. 땅콩 2010/08/09 14:26 댓글주소 | 수정 | 삭제 | 댓글

    옙..답변 감사드립니다..........원리를 분석하고 싶은데..아직 내공이 너무 부족하네요..ㅜㅜ

    빨리 블로그에서 글을 보았으면..좋겠습니다..ㅎㅎ

    수고하세요..ㅎ

  9. 봉이 2010/08/24 13:47 댓글주소 | 수정 | 삭제 | 댓글

    안녕 하세요!
    저두 마소에서 관련글보다 여기 왔는데, 정말 글을 잘쓰시는 거 같아요!
    코드인젝션편 보면서 그림과 올리디버거 사용방법등이 적절하게 나와 이해하기 좋았습니다.
    앞으로도 좋은 내용 기대할게요 ^^

  10. 엘군 2011/07/05 15:50 댓글주소 | 수정 | 삭제 | 댓글

    전 코드 인젝션에 대해 전혀 모르던 학생입니다ㅎㅎ
    dll 로딩 원리를 알고싶어서 구글링 하다가 우연히 봤는데 왜이렇게 재밌게 보이는지..!!
    요즘은 영상처리쪽 공부중인데요ㅎ 공부할 마음없었는데 기회되면 이분야도 해보고 싶네요!ㅋ
    책도내신것같은데 꼭 봐봐야겠습니다
    #덧, 좋은글 올려주셔서 감사합니다



다른 프로세스에 인젝션된 코드를 디버깅하면서 동작 원리를 알아보도록 하겠습니다.

이전 강좌의 내용은 아래 링크를 참조하시기 바랍니다.




notepad.exe 디버깅
notepad.exe 프로세스에 어셈블리 언어로 제작한 코드를 인젝션 시키고 디버깅을 해보도록 하겠습니다.

Code Injection 기법의 디버깅 방법은 아래 링크를 참고하시기 바랍니다.

위의 글에서 소개된 방법에 따라 notepad.exe 에 인젝션된 코드를 OllyDbg 로 보면 아래 그림과 같습니다. 


<Fig. 1>

* 참고
위 코드의 시작 주소(2D0000)는 사용자 환경에 따라서 틀려집니다.

위 코드를 자세히 디버깅 해보겠습니다.


# Stack Frame 생성

002D0000    55            PUSH EBP                         ; # ThreadProc()
002D0001    8BEC          MOV EBP,ESP

전형적인 스택 프레임 생성 명령어 입니다. 이 명령어가 낯 설은 분들께서는 이 기회에 "55 8BEC" 하고 외워두시는 것도 좋습니다. 

스택 프레임을 생성하는 이유는 이후에 나오는 명령어들이 스택에 문자열들을 집어넣는 기법을 사용하기 때문에 위 ThreadProc() 함수가 종료될 때 스택을 깨끗이 정리하기 위해서입니다.


# THREAD_PARAM 구조체 포인터

002D0003    8B75 08       MOV ESI,DWORD PTR SS:[EBP+8]

스택 프레임이 생성된 이후에 [EBP+8] 이 의미하는 것은 함수로 넘어온 첫 번째 파라미터 입니다. 이 경우에는 THREAD_PARAM 구조체 포인터가 될 것입니다. 

아래에 THREAD_PARAM 구조체를 표시하였습니다. 구조체의 멤버는 2개의 함수 포인터인데 각각 “LoadLibraryA” 와 “GetProcAddress()” 의 포인터가 저장됩니다. (누가 이 포인터를 구해서 저장시켜 줬을까요? 네, 지난 강좌에서 소개한 CodeInjection2.exe 프로그램에서 구해서 notepad.exe 에 인젝션 시킨 후 스레드 실행할 때 파라미터로 넣어주었죠.)

typedef struct _THREAD_PARAM 
{
    FARPROC pFunc[2];               // LoadLibraryA(), GetProcAddress()
} THREAD_PARAM, *PTHREAD_PARAM;

위 2D0003 주소의 MOV ESI, DWORD PTR SS:[EBP+8] 명령어를 실행한 이후에 ESI 레지스터에 저장된 주소를 따라가서 확인해 보겠습니다.


<Fig. 2>

ESI 에 280000 주소가 저장되었으며 이 주소는 CodeInjection2.exe 에서 THREAD_PARAM 구조체를 위해 notepad.exe 프로세스 메모리 공간에 할당한 메모리 버퍼의 주소입니다.

* 참고
THREAD_PARAM 구조체 주소(2D0000)는 사용자 환경에 따라서 틀려집니다.

<Fig. 2> 의 메모리 윈도우를 보면 280000 주소에 두 개의 4 byte 값들이 저장된 걸 확인할 수 있습니다. 저 값들이 "LoadLibraryA" 와 "GetProcAddress" API 함수의 시작 주소일 것입니다. 좀 더 직관적으로 확인하기 위해서 OllyDbg 메모리 윈도우의 보기 옵션을 변경해 보겠습니다.

메모리 윈도우에 커서를 위치시킨 후 마우스 우측 메뉴의 "Long – Address" 항목을 선택해 주시기 바랍니다.


<Fig. 3>

위 메뉴 항목을 선택하면 OllyDbg 의 메모리 윈도우는 아래 그림과 같이 표시 형식이 변경됩니다.


<Fig. 4>

주소가 훨씬 더 직관적으로 표시되지요? 또한 친절하게 Comment 에 각 주소에 해당되는 API 이름을 표시해 주고 있습니다.


# "user32.dll" 문자열

002D0006    68 6C6C0000   PUSH 6C6C         ; “\0\0ll”
002D000B    68 33322E64   PUSH 642E3233     ; “d.23”
002D0010    68 75736572   PUSH 72657375     ; “resu”

위 코드는 스택(Stack)에 문자열을 저장하는 기법입니다. 스택에 직접 접근할 수 있는 Assembly 프로그래밍 언어에서만 가능한 독특한 기법이지요. 

2D0006 주소의 PUSH 6C6C 명령어는 스택에 00006C6C 값을 저장하라는 뜻입니다. 6C 는 ASCII 로 'l' 이지요. 즉, 이 명령은 "\0\0ll" 문자열을 스택에 집어 넣는 것입니다. 

그 밑의 2D000B 와 2D0010 주소의 PUSH 명령어도 각각 "d.23" 문자열과 "resu" 문자열을 입력하는 명령어 입니다.

x86 CPU 의 Little Endian 표기법과 스택의 거꾸로 자라는 특성 때문에 문자열을 뒤집어서 입력하는 것을 주의 깊게 보시기 바랍니다. 이것은 디버깅할 때 잘 알고 계셔야 하는 내용입니다.

2D0010 주소까지 디버깅 한 후 스택을 보면 아래 그림과 같습니다.


<Fig. 5>

이와 같은 PUSH 명령어를 이용하여 원하는 문자열을 스택에 입력할 수 있습니다. 또한 Code Injection 할 때 문자열 데이터를 따로 인젝션 하지 않고 코드에 포함시켜서 코드만 인젝션 시킬 수 있습니다.

* 참고
  - 문자열 데이타를 코드에 포함시키는 방법은 한가지가 더 있으며, 뒤에서 따로 소개합니다.
  - 32 bit OS 에서 PUSH 명령어는 한번에 최대 4 byte 크기의 데이터만 스택에 저장이 가능합니다. 


# "user32.dll" 문자열 파라미터 입력

002D0015    54             PUSH ESP

LoadLibraryA() API 는 파라미터로 로딩시킬 DLL 파일 이름 문자열 주소를 받습니다. 

HMODULE WINAPI LoadLibrary(
  __in  LPCTSTR lpFileName
);


위 <Fig. 5> 를 보시면 현재 ESP 의 값은 219FCD4 이며 이것은 "user32.dll" 문자열의 시작 주소입니다. 따라서 2D0015 주소의 PUSH ESP 명령어는 "user32.dll" 문자열 주소(219FCD4)를 스택에 입력하는 명령입니다. (아래 그림 참고)


<Fig. 6>


# LoadLibraryA("user32.dll") 호출

002D0016    FF16          CALL DWORD PTR DS:[ESI]         ; kernel32.LoadLibraryA

ESI 레지스터는 <Fig. 4> 에서 보다시피 280000 값을 가지며 이 주소에는 LoadLibraryA() API 의 시작 주소(772C2864)가 저장되어 있습니다. 아래 그림을 봐주시기 바랍니다.
 

<Fig. 7>

어셈블리 언어의 메모리 참조 문법이 생소하신 분들께서는 이번 기회에 확실히 익혀 두시기 바랍니다. 아래와 같은 간단한 전개식을 사용하면 쉽게 이해하실 수 있습니다. ([ ] 는 C 언어의 포인터 참조와 같은 개념입니다.)

[ESI] = [280000] = 772C2864 (address of kernel32.LoadLibraryA)

2D0016 주소의 CALL DWORD PTR DS:[ESI] 명령어를 실행하면 LoadLibraryA() API 가 호출되면서 파라미터로 입력된 "user32.dll" 이 로딩됩니다. notepad.exe 프로세스는 실행될 때 이미 user32.dll 를 로딩하였으므로 그 로딩 주소만 리턴합니다.


<Fig. 8>

함수의 리턴값은 EAX 에 저장되므로 위 <Fig. 8> 을 보시면 EAX = 778E0000 이 저장되었습니다.

OllyDbg 메뉴의 "View – Executable modules [ALT + E]" 항목을 선택하시면 아래 그림과 같이 프로세스 메모리에 로딩된 DLL 을 확인 할 수 있습니다.


<Fig. 9>

위 그림에서 user32.dll 의 로딩 주소가 778E0000 임을 확인 할 수 있습니다.


# "MessageBoxA" 문자열

002D0018    68 6F784100   PUSH 41786F         ; “\0Axo”
002D001D    68 61676542   PUSH 42656761       ; “Bega”
002D0022    68 4D657373   PUSH 7373654D       ; “sseM”

역시 PUSH 명령어를 이용해서 문자열 "MessageBoxA" 를 스택에 입력하는 명령어 입니다. (위의 "user32.dll" 문자열 입력과 동일합니다.)

2D0022 주소의 PUSH 명령까지 디버깅을 하고 나면 아래 그림과 같이 스택에 "MessageBoxA" 문자열이 저장됩니다.


<Fig. 10>


# GetProcAddress(hMod, "MessageBoxA") 호출

002D0027    54            PUSH ESP                          ; - “MessageBoxA”
002D0028    50            PUSH EAX                          ; - hMod (778E0000)
002D0029    FF56 04       CALL DWORD PTR DS:[ESI+4]       ; kernel32.GetProcAddress

현재 ESP 의 값은 0219FCC8 입니다. (<Fig. 10> 참고) 따라서 2D0027 주소의 PUSH ESP 명령어는 "MessageBoxA" 문자열 주소(0219FCC8) 를 스택에 입력하는 명령입니다. (이 문자열 주소는 2D0029 주소에서 호출되는 GetProcAddress() API 의 2nd 파라미터로 사용됩니다.)

그리고 현재 EAX 의 값은 778E0000 입니다. 이는 user32.dll 모듈의 로딩 주소이지요. (<Fig. 8> 참고) 따라서 2D0029 주소의 PUSH EAX 명령어는 user32.dll 의 시작 주소(hMod)를 스택에 입력하는 명령입니다. (이 문자열 주소는 2D0029 주소에서 호출되는 GetProcAddress() API 의 1st  파라미터로 사용됩니다.)

여기까지 디버깅을 진행한 후 스택의 모습은 아래 그림과 같습니다.


<Fig. 11>

ESI 레지스터의 값은 280000 입니다. 따라서 [ESI+4] 의 전개식은 다음과 같습니다. (<Fig. 4>, <Fig. 7> 참고)

[ESI+4] = [280004] = 772C1837 (address of kernel32.GetProcAddress)

따라서 2D0029 주소의 CALL DWORD PTR DS:[ESI+4] 명령어는 GetProcAddress(778E0000, "MessageBoxA") API 를 호출하는 것입니다. 이 CALL 명령어를 실행하면 user32.MessageBoxA() API 시작 주소가 EAX 레지스터에 저장됩니다. (사용자 환경에 따라서 이 주소는 틀려집니다. 제 경우에는 EAX = 7793EA71 입니다.)


<Fig. 12>

(분량이 많아 다음 강좌에 이어서 하겠습니다.)

Trackback Address :: http://www.reversecore.com/trackback/87 관련글 쓰기



어셈블리(Assembly) 언어로 생성한 코드를 가지고 Injector 를 만들어 보도록 하겠습니다.

이전 강좌의 내용은 아래 링크를 참조하시기 바랍니다.




ThreadProc() 함수의 Binary 코드 얻기
지난 강좌에서 생성한 asmtest_patch.exe 파일을 OllyDbg 로 열어 보겠습니다. 우리가 프로그래밍한 ThreadProc() 의 주소는 401000 입니다. 메모리 윈도우에서 401000 주소로 갑니다.

 
<Fig. 1>

ThreadProc() 함수는 401000 ~ 401061 의 주소 영역입니다. 위 그림과 같이 이 영역을 선택하신 후 마우스 우측 메뉴의 "Copy - To file" 항목을 선택하시기 바랍니다. (아래 그림 참고)


<Fig. 2>

이렇게 저장한 파일을 텍스트 에디터로 열어보겠습니다. (GVIM 또는 AcroEdit 추천합니다.)


<Fig. 3>

위 텍스트 파일의 내용은 Hex 값으로 표현된 ThreadProc() 함수로써, IA-32 OpCode (Operation Code) 명령어 입니다. 이 내용은 곧 상대방 프로세스에 인젝션 시킬 코드가 되는 것입니다.

위 텍스트 파일을 아래와 같이 편집합니다. 불필요한 부분을 제거하고 모든 바이트마다 "0x" 표시를 붙여주고 ',' 로 연결합니다. 텍스트 에디터의 편집 기능(열 선택, 문자열 변경)을 적절히 사용하시면 편리합니다.


<Fig. 4>

위 그림에서 편집된 텍스트 내용을 보면 마치 C 언어의 BYTE 배열처럼 보이지 않습니까? 이게 바로 인젝션 시킬 코드 버퍼입니다. (아래 설명되는 CodeInjection2.cpp 파일에서 사용됩니다.)

편집된 텍스트 파일을 첨부합니다.



CodeInjection2.cpp
Injector 프로그램의 소스 코드입니다. 위에서 텍스트 에디터로 만든 코드 버퍼는 아래 소스에서 g_InjectionCode 배열에 사용되었습니다.

* 참고!
아래 소스코드는 MS Visual C++ 2008 Express Edition 에서 개발되었으며, Windows 7 32bit 환경에서 테스트 되었습니다. 또한 설명의 편의를 위하여 리턴 값 체크와 에러 처리 코드는 생략되었습니다. 원본 소스 코드는 위에 첨부된 CodeInjection2.cpp 파일을 참고하시기 바랍니다.

typedef struct _THREAD_PARAM 
{
    FARPROC pFunc[2];               // LoadLibraryA(), GetProcAddress()
} THREAD_PARAM, *PTHREAD_PARAM;

// ThreadProc()
BYTE g_InjectionCode[] = 
{
    0x55, 0x8B, 0xEC, 0x8B, 0x75, 0x08, 0x68, 0x6C, 0x6C, 0x00,
    0x00, 0x68, 0x33, 0x32, 0x2E, 0x64, 0x68, 0x75, 0x73, 0x65,
    0x72, 0x54, 0xFF, 0x16, 0x68, 0x6F, 0x78, 0x41, 0x00, 0x68,
    0x61, 0x67, 0x65, 0x42, 0x68, 0x4D, 0x65, 0x73, 0x73, 0x54,
    0x50, 0xFF, 0x56, 0x04, 0x6A, 0x00, 0xE8, 0x0C, 0x00, 0x00,
    0x00, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x43, 0x6F,
    0x72, 0x65, 0x00, 0xE8, 0x14, 0x00, 0x00, 0x00, 0x77, 0x77,
    0x77, 0x2E, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x63,
    0x6F, 0x72, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x6A, 0x00,
    0xFF, 0xD0, 0x33, 0xC0, 0x8B, 0xE5, 0x5D, 0xC3
};

/*
// ThreadProc()
004010ED    55               PUSH EBP
004010EE    8BEC             MOV EBP,ESP
004010F0    8B75 08          MOV ESI,DWORD PTR SS:[EBP+8] 
004010F3    68 6C6C0000      PUSH 6C6C                      
004010F8    68 33322E64      PUSH 642E3233
004010FD    68 75736572      PUSH 72657375
00401102    54               PUSH ESP                       
00401103    FF16             CALL DWORD PTR DS:[ESI] 
00401105    68 6F784100      PUSH 41786F
0040110A    68 61676542      PUSH 42656761
0040110F    68 4D657373      PUSH 7373654D
00401114    54               PUSH ESP                      
00401115    50               PUSH EAX                      
00401116    FF56 04          CALL DWORD PTR DS:[ESI+4]   
00401119    6A 00            PUSH 0                        
0040111B    E8 0C000000      CALL 0040112C
00401120    <ASCII>                                         
0040112C    E8 14000000      CALL 00401145
00401131    <ASCII>                                         
00401145    6A 00            PUSH 0                         
00401147    FFD0             CALL EAX                       
00401149    33C0             XOR EAX,EAX                        
0040114B    8BE5             MOV ESP,EBP
0040114D    5D               POP EBP                            
0040114E    C3               RETN
*/

BOOL InjectCode(DWORD dwPID)
{
    HMODULE         hMod            = NULL;
    THREAD_PARAM    param           = {0,};
    HANDLE          hProcess        = NULL;
    HANDLE          hThread         = NULL;
    LPVOID          pRemoteBuf[2]   = {0,};

    hMod = GetModuleHandleA("kernel32.dll");

    // set THREAD_PARAM
    param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
    param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");

    // Open Process
    hProcess = OpenProcess(PROCESS_ALL_ACCESS,    
                           FALSE,                     
                           dwPID);                    

    // Allocation for THREAD_PARAM
    pRemoteBuf[0] = VirtualAllocEx(hProcess,       
                                   NULL,               
                                   sizeof(THREAD_PARAM), 
                                   MEM_COMMIT,            
                                   PAGE_READWRITE);       

    WriteProcessMemory(hProcess,                      
                       pRemoteBuf[0],                  
                       (LPVOID)&param,                 
                       sizeof(THREAD_PARAM),          
                       NULL);                           

    // Allocation for g_InjectionCode
    pRemoteBuf[1] = VirtualAllocEx(hProcess,           
                                   NULL,                   
                                   sizeof(g_InjectionCode), 
                                   MEM_COMMIT,              
                                   PAGE_EXECUTE_READWRITE); 

    WriteProcessMemory(hProcess,                        
                       pRemoteBuf[1],                     
                       (LPVOID)&g_InjectionCode,         
                       sizeof(g_InjectionCode),          
                       NULL);                             

    hThread = CreateRemoteThread(hProcess,             
                                 NULL,                    
                                 0,                       
                                 (LPTHREAD_START_ROUTINE)pRemoteBuf[1], 
                                 pRemoteBuf[0],          
                                 0,                       
                                 NULL);                   

    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

<코드 1>

위의 코드와 지난번 CodeInjection.cpp 의 코드와의 가장 큰 차이점은 인젝션 시키는 코드 내에 필요한 문자열 데이터를 같이 포함시킨 것입니다. 

따라서 _THREAD_PARAM 구조체에서 문자열 멤버가 사라졌습니다. 그리고 기존 C로 된 ThreadProc() 함수 대신 <Fig. 4> 의 OpCode 버퍼(g_InjectionCode)가 사용됩니다. (이 OpCode 버퍼를 생성하기 위해 OllyDbg 의 “Assembly” 명령을 사용했던 것을 기억하시죠?)

조금 더 정교하게 프로그래밍을 했다면 _THREAD_PARAM 구조체조차 필요 없도록 만들 수도 있습니다. 세부적인 구현 방법은 어디까지나 구현하는 사람의 마음입니다. 

중요한 것은 어셈블리 프로그래밍을 통해서 생성된 OpCode 버퍼를 Injector 소스코드에 사용하여 상대방 프로세스에 인젝션 시킨다는 것입니다.

예전에 설명 드린 CodeInjection.cpp 소스 코드와 비교해서 보시면 차이점을 더 명확히 이해할 수 있을 것입니다. (위 <코드 1> 의 세부 설명은 생략합니다. Code Injection 의 구현 방법에 대해서는 밑의 링크된 설명을 참고하시기 바랍니다.)



다음 강좌에서 실제로 상대방 프로세스에 인젝션 시킨 후 디버깅을 하면서 저 어셈블리 코드가 어떤 의미를 가지고 있는지 알아보도록 하겠습니다.

☞ 어셈블리 언어를 이용한 Code Injection (4)


ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

Trackback Address :: http://www.reversecore.com/trackback/86 관련글 쓰기

  1. 지나가던이 2010/07/03 08:01 댓글주소 | 수정 | 삭제 | 댓글

    좋은 글 감사합니다. 항상 잘 배우고 있습니다.
    전 그동안은 코드 인젝션 할땐 DLL 인젝션과 병행 해서, 인젝션 코드에서 DLL의 함수를 call 하도록 간단하게 만들었었는데, 이렇게 함수 코드를 직접 집어넣는 방법도 괜찮네요.

    • reversecore 2010/07/03 14:56 댓글주소 | 수정 | 삭제

      네, 상황에 맞게 적절히 선택해서 사용하시면 되는데요.
      보통은 DLL Injection 이 많이 쓰입니다.

      Code Injection 을 소개하는 이유는 리버싱에 대해 좀 더 깊이있는 공부를 해보자는 뜻입니다.

      감사합니다.



지난 강좌에 이어지는 내용입니다. 

어셈블리 언어를 이용한 Code Injection (1)




ThreadProc() 


Assembly 언어로 ThreadProc() 함수를 만들겠습니다. 지난 강좌에서 C 로 만든 ThreadProc() 과의 차이점은 Code 사이에 필요한 Data(문자열)를 포함 시키는 것입니다.

각자 아래 그림과 같이 입력해 주시기 바랍니다. 각 Assembly 명령어에 대한 설명은 뒤쪽에서 하겠습니다. (“Fill with NOP’s” 옵션은 uncheck 해주시고, 오타가 나면 그 주소에 가서 다시 입력하시면 됩니다.) 


<Fig. 5>

40102E 주소의 CALL 0040103F 명령어까지 잘 입력하셨나요? 그 다음에는 문자열을 입력합니다. Assemble 윈도우를 닫아주세요. OllyDbg의 코드 윈도우에서 401033 주소에 커서를 위치시킨 후 아래 그림과 같이 "Edit" 명령(단축키 : [Ctrl+E])을 내려줍니다.


<Fig. 6>

위 그림의 Edit 윈도우에서 "ASCII" 항목에 "ReverseCore" 를 입력합니다. 문자열은 반드시 NULL 로 끝나야 하므로 "HEX" 항목에서 00 값을 추가해 줍니다. ("Keep size" 옵션은 uncheck 로 해주세요.)

이와 같이 입력한 후 OllyDbg 에서 코드를 보면 아래 그림과 같습니다.


<Fig. 7>

위 그림에서 푸른색으로 반전된 영역이 바로 "ReverseCore" 문자열 영역입니다. 매우 이상한 명령어로 표시된 것을 볼 수 있습니다. 이렇게 표시되는 이유는 OllyDbg 의 Disassembler 가 문자열을 IA-32 명령어로 잘 못 해석한 것입니다. 이것은 어디까지나 Code 위치에 문자열을 입력한 저의 잘못(?)이지 OllyDbg 의 Disassembler 의 문제는 아닙니다.

* 참고
디버깅을 할 때 이와 같은 상황을 종종 부딪히게 됩니다. 또한 이를 이용한 Anti-Debugging 기법도 있습니다. 향후 Anti-Debugging 에 대해서 설명할 때 소개해 드리겠습니다.

위 <Fig. 7> 화면처럼 문자열을 선택한 상태에서 "Analysis" 명령(단축키 : Ctrl+A)을 내려보겠습니다. 

* 참고
OllyDbg 의 “Analysis” 명령은 코드를 다시 해석하라는 명령입니다. 주로 Unpack 된 코드를 재 해석할 때 많이 사용하게 됩니다.


<Fig. 8>

위 그림은 Analysis 명령이 수행된 이후에 코드의 모습입니다. 401033 주소의 "ReverseCore" 문자열은 잘 보입니다만, 401000 주소 이후의 명령어들은 잘 못 해석이 되었습니다. (OllyDbg 2.0 에서도 코드와 데이터를 100% 정확히 구별해서 보여주지는 못합니다. 사실 이게 문자열인지 명령어인지 판단하기 어렵습니다.)

위 <Fig. 8> 은 코드를 보기 어려우므로 마우스 우측 메뉴의 "Analysis – Remove analysis from module" 명령을 사용하여 코드를 원래대로 되돌립니다.


<Fig. 9>

위 Remove analysis 명령을 사용하면 다시 위의 <Fig. 7> 과 같은 형태의 코드로 보이게 됩니다.

이제 401033 주소의 "ReverseCore" 문자열 뒤의 40103F 주소부터 다시 입력할 차례입니다. (<Fig. 7>, <Fig. 8> 의 40102E 주소의 CALL 40103F 명령어 참고)


<Fig. 10>

그리고 401044 주소에 문자열("www.reversecore.com")을 입력합니다. (마지막에 NULL 입력을 잊지 마시구요.)


<Fig. 11>

401058 주소부터 아래와 같이 명령어를 입력합니다.


<Fig. 12>

이것으로써 ThreadProc() 코드 입력이 모두 완료되었습니다. 아래 그림에 전체 코드가 나타나 있으니 각자 오타를 점검해보시기 바랍니다.
 

<Fig. 13>

* 참고
401033, 401044 주소의 내용은 명령어가 아니고 문자열입니다. OllyDbg 에서 문자열을 명령어로 인식하여 이상하게 표시하고 있습니다.



Save File


위에서 생성한 코드를 잘 저장합니다. OllyDbg 의 코드 윈도우에서 마우스 우측 버튼 메뉴 중 "Copy to executable \ All modifications" 를 선택합니다. (아래 그림 참고)


<Fig. 14>

아래 그림과 같이 확인 메시지 창이 뜹니다. "Copy all" 을 선택해 주세요.


<Fig. 15>

마지막으로 변경 내용을 보여주는 창이 나타납니다. 마우스 우측 메뉴의 "Save file" 항목을 선택합니다. (아래 그림 참고)


<Fig. 16>

이후에 나타나는 파일 저장 다이알로그에서 적당한 파일 이름(asmtest_patch.exe)을 적어준 후 저장합니다. 

다음 강좌에서 asmtest_patch.exe 의 어셈블리 코드를 이용해서 Injector 를 만들어 보도록 하겠습니다.


* 참고 : 새롭게 소개된 OllyDbg 명령어

Assemble [Space]
Analysis [Ctrl+A]
New origin here [Ctrl+Gray *]


ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

Trackback Address :: http://www.reversecore.com/trackback/85 관련글 쓰기

  1. badcob 2010/07/18 01:27 댓글주소 | 수정 | 삭제 | 댓글

    이런 보석같은 글에 리플이 없는게 아쉬워서 몇글자 적어봅니다;
    해외 문서들을 보면서 궁금하고 또 상세한 설명이 아쉬웠던 부분들이
    이 블로그에 있는 글들로 정말 많이 채워지는 기분이 드네요.
    오늘도 잘 보고 물러갑니다. ( __)



어셈블리(Assembly) 프로그래밍 언어를 이용하여 Code Injection 기법에 사용되는 코드를 생성해 보도록 하겠습니다. 


Code Injection 관련 내용은 아래 강좌를 참고하시기 바랍니다.




Goal


이번 강좌에서는 OllyDbg 의 Assemble 기능을 이용하여 Assembly 언어로 인젝션 시킬 코드(ThreadProc() 함수)를 만들어 보도록 하겠습니다. Assembly 언어는 C 언어보다 훨씬 더 자유로운 (정형화 되지 않은) 코드를 생성할 수 있습니다. (예: 스택(Stack), 레지스터(Register) 를 직접 access 가능) 그리고 CodeInjection.cpp 소스코드를 수정하여 Assembly 언어로 제작된 ThreadProc() 함수를 notepad.exe 프로세스에 인젝션 시켜보도록 하겠습니다. 

이전 강좌에서 설명 드린 (C 언어로 생성된) ThreadProc() 과 어떤 차이점이 있는지 잘 살펴보시기 바랍니다.



Assembly 프로그래밍


C/C++ 언어의 대표적인 개발 툴은 MicroSoft Visual C++Borland C++ Builder 입니다. 

Assembly 언어에도 MASM(MicroSoft Macro Assembler), TASM(Borland Turbo Assembler), FASM(Flat Assembler) 등의 개발 툴(Assembler)이 있습니다. 

참고로 저는 C/C++ 언어는 MS Visual C++ 로 개발하고 Assembly 언어는 MASM 으로 개발합니다. 특히 MASM 은 다양한 Macro 함수와 라이브러리를 지원하기 때문에 거의 C 언어와 비슷한 수준으로 편리하게 프로그래밍을 할 수 있습니다.

Assembly 프로그래밍을 정식으로 하시려면 MASM 을 설치하고 프로그래밍 하시면 됩니다. 또는 Visual C++ 와 같은 C 언어 개발 툴에서 인라인 어셈블리(Inline Assembly)를 사용할 수 도 있습니다. 이런 방식은 개발자에게 잘 어울리는 방식입니다.

우리는 모두 리버서(Reverser)이기 때문에 이번 강좌에서는 리버서에게 좀 더 잘 어울리는 방법을 소개하겠습니다. 바로 OllyDbg 에서 지원하는 "Assemble" 기능을 이용해서 프로그래밍을 하는 것입니다. 

* 참고
OllyDbg 의 "Assemble" 기능은 간단한 Assembly 프로그래밍을 지원하는데, 이는 리버싱에 매우 유용하게 사용됩니다. (디버깅하면서 코드를 이리저리 수정할 일이 많기 때문이지요.)



OllyDbg 의 "Assemble" 명령


OllyDbg 의 “Assemble” 명령어를 이용하여 Assembly 프로그래밍을 해보겠습니다.

위에 첨부된 asmtest.exe 실행 파일을 OllyDbg 로 열어보겠습니다. (asmtest.exe 는 Assembly 프로그래밍 테스트 용도로 제작된 -아무런 기능이 없는- 실행 파일입니다.)

 
<Fig. 1>

위 그림과 같이 코드 섹션의 맨 윗부분(401000)을 봐주시기 바랍니다. OllyDbg 의 새로운 명령어를 소개해 드리겠습니다. 바로 EIP 를 원하는 주소로 바꿔버리는 기능입니다. 

OllyDbg 의 코드 윈도우에서 401000 주소에 커서를 위치시킨 후 마우스 우측 메뉴의 "New origin here [Ctrl+Gray*]" 항목을 선택해 주세요.


<Fig. 2>

아래 그림과 같이 EIP는 401000 주소로 변하게 됩니다.


<Fig. 3>

위 EIP 변경 기능은 디버깅에 유용하게 사용될 수 있으므로 잘 기억해 두시기 바랍니다.

* 참고!
"New origin here" 기능은 단순히 EIP 만 바꿔버리는 것이기 때문에 직접 디버깅을 해서 그 주소로 가는 것과는 틀립니다. 레지스터와 스택의 내용은 전혀 바뀌지 않기 때문입니다.

이제 401000 주소에서 "Assemble" 명령(단축키 : [Space])을 내리면 아래 그림과 같은 Assemble 입력 창이 나타납니다.


<Fig. 4>

이제부터 OllyDbg 에서 간단한 Assembly 프로그래밍을 할 수 있게 되었습니다.

* 참고
위 <Fig. 4> 에서 "Fill with NOP’s" 항목은 uncheck 해주시기 바랍니다. OllyDbg 의 Assemble 명령은 해당 주소에 사용자 코드를 입력하는 것입니다. 만약 이 항목이 check 되어 있으면 기존의 코드 보다 짧은 길이의 코드를 입력했을 때 남은 길이만큼 NOP (No Operation) 명령어를 채워 넣어서 전체적인 Code Alignment 를 맞춰주게 됩니다. 이번 강좌에서는 설명의 편의를 위해서 uncheck 상태로 진행하도록 하겠습니다.


(내용이 많아 다음 강좌에 이어서 하겠습니다.)



Trackback Address :: http://www.reversecore.com/trackback/84 관련글 쓰기


Code Injection 기법 (3)

study 2010/06/24 10:43

Code Injection 기법에 의해서 인젝션된 코드를 디버깅 하는 방법에 대해서 알아보도록 하겠습니다. 

이전 강좌의 내용은 아래 링크를 참조하시기 바랍니다.




Code Injection 디버깅 실습
1. notepad.exe 디버깅

OllyDbg 를 이용하여 notepad.exe 파일의 디버깅을 시작합니다. 아래 그림과 같이 실행[F9] 버튼을 선택해서 notepad.exe 를 "Running" 상태로 만들어주세요.


<Fig. 1>

2. OllyDbg 옵션 변경

Code Injection 은 상대방 프로세스에 새로운 스레드를 생성하는 기법이므로 아래와 같이 OllyDbg 의 옵션을 변경하면 인젝션된 스레드 코드 시작부터 디버깅이 가능합니다.


<Fig. 2>

이제부터 notepad.exe 프로세스에서 스레드가 생성된다면 해당 스레드 함수 시작코드에서 멈추게 됩니다.

3. CodeInjection.exe 실행

Process Explorer 를 이용하여 notepad.exe 프로세스의 PID 를 구합니다.


<Fig. 3>

PID 값을 실행 파라미터로 하여 CodeInjection.exe 를 실행합니다.

 
<Fig. 4>

4. 스레드 시작 코드

CodeInjection.exe 프로세스가 실행되어 코드 인젝션이 성공하면 아래 그림과 같이 인젝션된 스레드 코드 시작 위치에서 디버깅이 멈추게 됩니다.


<Fig. 5>

주의 하실점은 디버깅이 멈춘 것이지 EIP 가 이곳으로 세팅된 것이 아닙니다.

위 그림의 150000 주소에 BP 를 설치한 후 실행[F9] 시켜 주세요. 실행 제어가 정확히 BP 설치 주소(150000)에 멈추게 되고 이제부터 펀안하게 디버깅을 진행하시면 됩니다.

* 참고!
실행 환경에 따라서 위 주소는 다르게 표시될 수 있습니다.

OllyDbg 의 편리한 기능을 이용하여 인젝션된 코드를 디버깅 하는 방법에 대해서 살펴보았습니다.

다음 강좌에는 C 언어가 아닌 Assembly 언어를 이용해서 인젝션 코드를 만들어 보도록 하겠습니다. 많이 기대해 주세요~

☞ 어셈블리 언어를 이용한 Code Injection (1)

ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

Trackback Address :: http://www.reversecore.com/trackback/83 관련글 쓰기

  1. Acid 2010/06/24 11:18 댓글주소 | 수정 | 삭제 | 댓글

    1등 ^^
    며칠전에 우연히 이 사이트에 들어왔다가 지금까지 궁금했던 것들 한꺼번에 배우고 있습니다.
    앞으로도 좋은글 부탁드려요~

  2. 궁금 2011/06/02 15:44 댓글주소 | 수정 | 삭제 | 댓글

    windows 7에서는 "The token dows not have the specified privilege" 에러가 납니다
    특별한 이유가 있을까요?

    그래서 cmd.exe를 관리자로 실행하고 codeInjectio.exe 실행했더니 잘 됩니다
    관리자로 실행하지 않으면 코드인젝션은 되지 않는건가요?

    • reversecore 2011/06/03 21:08 댓글주소 | 수정 | 삭제

      안녕하세요.

      네, 말씀하신대로 Vista, 7 에서는 그러한 에러가 자주 발생합니다. XP 보다 보안성이 강화되었기 때문입니다.

      시스템 프로세스가 아닌 같은 유저 환경에서 실행한 프로세스에는 인젝션이 가능합니다.

      감사합니다.

  3. Andi 2011/08/09 10:00 댓글주소 | 수정 | 삭제 | 댓글

    좋은 정보 감사합니다.

  4. 디버거 2011/10/12 19:26 댓글주소 | 수정 | 삭제 | 댓글

    어느덧 Code Injection까지 무작정 따라 왔는데요.

    실습용 CodeInjection.exe로는 잘 되는데
    컴파일 해서 실행하면 에러가 나면서 notepad가 꺼집니다.

    환경은 Winxp SP3, VS6.0입니다.

    cpp파일을 그대로 컴파일 하고 좀 따라가보다 나니
    dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
    에서 dwSize가 5가 되는데 이것이 맞는건지 가르쳐주세요.

    • reversecore 2011/10/28 01:17 댓글주소 | 수정 | 삭제

      안녕하세요.

      dwSize 는 최소 ThreadProc() 함수 크기 이상으로 나와야 합니다. 아마 Debug 모드로 빌드 하신 것 같습니다. Release 모드로 빌드 해보시기 바랍니다.

      감사합니다.


Code Injection 기법 (2)

study 2010/06/23 00:15

Code Injection 실습 예제인 CodeInjection.exe 의 소스 코드(CodeInjection.cpp)를 살펴보도록 하겠습니다. 


이전 강좌의 내용은 아래 링크를 참조하시기 바랍니다.

Code Injection 기법 (1)



CodeInjection.cpp

* 참고!
CodeInjection.cpp 는 Visual C++ 2008 Express Edition 으로 개발되었으며 Windows 7 32bit 환경에서 테스트 되었습니다. 또한 Visual C++ 의 코드 최적화 기능을 사용하지 않고 빌드 하였습니다. (/Od) 

아래 소개되는 코드들은 설명의 편의를 위하여 에러 처리 부분을 생략하였습니다. 완전한 코드는 첨부된 CodeInjection.cpp 파일을 참고하시기 바랍니다.

main()

먼저 main() 함수를 살펴 보겠습니다.

int main(int argc, char *argv[])
{
    DWORD dwPID     = 0;

    if( argc != 2 )
    {
        printf("\n USAGE  : %s <pid>\n", argv[0]);
        return 1;
    }

    // code injection
    dwPID = (DWORD)atol(argv[1]);
    InjectCode(dwPID);

    return 0;
}

<코드 1 – main() 함수>

main() 함수의 역할은 InjectCode() 함수를 호출하는 것입니다. 이때 함수 파라미터로 상대방 프로세스의 PID 값을 넘겨줍니다.

ThreadProc()

이제 상대방 프로세스에 인젝션 시킬 코드(스레드 함수)를 살펴보겠습니다.

// Thread Parameter
typedef struct _THREAD_PARAM 
{
    FARPROC pFunc[2];               // LoadLibraryA(), GetProcAddress()
    char    szBuf[4][128];          // "user32.dll", "MessageBoxA", 
                                    // "www.reversecore.com", "ReverseCore"
} THREAD_PARAM, *PTHREAD_PARAM;

// LoadLibraryA()
typedef HMODULE (WINAPI *PFLOADLIBRARYA)
(
    LPCSTR lpLibFileName
);

// GetProcAddress()
typedef FARPROC (WINAPI *PFGETPROCADDRESS)
(
    HMODULE hModule,
    LPCSTR lpProcName
);

// MessageBoxA()
typedef int (WINAPI *PFMESSAGEBOXA)
(
    HWND hWnd,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType
);

// Thread Procedure
DWORD WINAPI ThreadProc(LPVOID lParam)
{
    PTHREAD_PARAM   pParam      = (PTHREAD_PARAM)lParam;
    HMODULE         hMod        = NULL;
    FARPROC         pFunc       = NULL;

    // LoadLibrary(“user32.dll”)
    //   pParam->pFunc[0] = kernel32!LoadLibraryA()
    //   pParam->szBuf[0] = “user32.dll”
    hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);

    // GetProcAddress(“MessageBoxA”)
    //   pParam->pFunc[1] = kernel32!GetProcAddress()
    //   pParam->szBuf[1] = “MessageBoxA”
    pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);

    // MessageBoxA(NULL, “www.reversecore.com”, “ReverseCore”, MB_OK)
    //   pParam->pFunc[1] = kernel32!GetProcAddress()
    //   pParam->szBuf[1] = “MessageBoxA”
    ((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);

    return 0;
}

<코드 2 – ThreadProc() 함수>

위 코드에서 실제로 인젝션 되는 부분은 ThreadProc() 함수입니다. 그 위의 typedef 문은 C 언어 문법을 위한 것이므로 인젝션 시킬 필요가 없습니다.

ThreadProc() 의 코드는 함수 포인터를 많이 사용해서 얼핏 복잡하게 보이지만 사실 내용은 아래와 같이 간단합니다.

hMod = LoadLibraryA(“user32.dll”);
pFunc = GetProcAddress(hMod, “MessageBoxA”);
pFunc(“www.reversecore.com”, “ReverseCore”);

<코드 2> 의 주석을 참조하시면 ThreadProc() 의 코드는 쉽게 이해가 가실 것입니다.

중요한 것은 ThreadProc() 코드의 개념입니다. 

Code Injection 기법의 핵심은 독립 실행 코드를 인젝션 시키는 것입니다. 

그러기 위해서 코드와 (코드에서 참조하는) 데이터를 같이 인젝션 시키는 것입니다. 그리고 인젝션 시키는 코드에서 역시 인젝션 시킨 데이터를 정확히 참조할 수 있도록 해야 합니다.

위 ThreadProc() 함수를 보시면 직접 API 를 호출 하지 않습니다. 또한 문자열도 직접 정의해서 사용하지 않습니다. 전부 스레드 파라미터로 넘어온 THREAD_PARAM 구조체에서 가져다 사용하고 있습니다.

만약 일반적인 프로그램이라면 ThreadProc() 의 코드는 아래와 같이 간단히 작성할 수 있습니다.

DWORD WINAPI ThreadProc(LPVOID lParam)
{
    MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK);

    return 0;
}

<코드 3 – 일반적인 프로그램에서의 ThreadProc()>

위 <코드 3>을 빌드하여 생성된 파일을 디버거로 보면 아래 그림과 같습니다.

 
<Fig. 1>

위 그림의 코드(10001000 ~ 10001018 영역)를 다른 프로세스에 그대로 인젝션 시킨다면 정상적으로 실행되지 않습니다. 그 이유는 코드에서 사용되는 10009290, 1000929C, 100080F0 주소의 내용이 상대방 프로세스에는 없기 때문입니다.

따라서 저 주소에 해당하는 문자열과 API 주소를 같이 인젝션 시켜야 합니다. 또한 <Fig. 1>의 코드 역시 그 인젝션된 데이터의 주소를 정확히 참조하도록 프로그래밍 되어야 합니다.

이와 같은 조건을 만족시키기 위해서 <코드 2> 의 ThreadProc() 함수는 THREAD_PARAM 구조체를 이용해서 2 개의 API 주소와 4 개의 문자열 데이터를 받아들입니다. 2 개의 API 는 바로 “LoadLibraryA()”“GetProcAddress()” 입니다. 이 2 개의 API 만 있으면 모든 라이브러리의 함수를 호출 할 수 있습니다. 

* 참고 사항

1. 위 실습 예제의 경우에 LoadLibraryA() 와 GetProcAddress() 의 주소 말고 MessageBoxA() 의 주소를 직접 전달하여 사용해도 됩니다. 하지만 정석은 LoadLibraryA() 와 GetProcAddress() 만을 전달한 후 이를 이용해서 필요한 DLL 을 로딩시켜 원하는 함수 주소를 직접 구하는 것입니다. 이 방식의 장점은 해당 라이브러리를 프로세스에 정확히 로딩시킨다는 것입니다. 가령 notepad.exe 프로세스에 윈도우 소켓 API 인 ws2_32!connect() 의 주소를 넘겨주면 에러가 발생할 것입니다. (notepad.exe 에 기본적으로 ws2_32.dll 이 로딩되지 않았으니까요.)

2. 대부분의 유저 모드 프로세스는 kernel32.dll 을 로딩하므로 LoadLibraryA(), GetProcAddress() 의 주소를 직접 넘기는 것은 크게 무리가 없습니다. 단, kernel32.dll 을 로딩하지 않는 시스템 프로세스(예: smss.exe)도 있으니 사전에 꼭 확인하시기 바랍니다.

3. Kernel32.dll 같은 시스템 라이브러리는 OS 가 부팅되어 있는 상태에서는 모든 프로세스에서 동일한 주소에 로딩되어 있습니다. OS 버전이 틀리거나 재부팅(Vista, 7 의 경우)을 하거나 하면 같은 모듈이라도 로딩 주소는 틀려집니다.

위 <코드 2> 의 내용을 디버거로 살펴보면 아래 그림과 같습니다.

 
<Fig. 2>

위의 <Fig. 2> 의 코드를 보시면 모든 중요한 데이터는 스레드 파라미터인 pParam 으로 받아서 사용하는 것을 알 수 있습니다. 즉, <Fig. 2> 의 ThreadProc() 함수는 독립 실행 코드라고 말 할 수 있습니다. 위의 <Fig. 2> 와 앞에서 소개한 <Fig. 1> 를 비교해 보시면 그 차이점을 확인하실 수 있습니다.

* 참고!
Visual C++ 2008 Express Edition 에서 프로젝트의 [Release/Debug] 모드와 [최적화] 옵션에 따라서 CodeInjection.cpp 파일은 <Fig. 2> 와는 다른 형태로 빌드 될 수 있습니다. 위 실습 예제는 Release 모드에서 최적화 옵션 사용 안함(/Od)으로 빌드 하였습니다. 

InjectCode()

아래는 Code Injection 기법의 핵심인 InjectCode() 함수입니다.

BOOL InjectCode(DWORD dwPID)
{
    HMODULE         hMod            = NULL;
    THREAD_PARAM    param           = {0,};
    HANDLE          hProcess        = NULL;
    HANDLE          hThread         = NULL;
    LPVOID          pRemoteBuf[2]   = {0,};
    DWORD           dwSize          = 0;

    hMod = GetModuleHandleA("kernel32.dll");

    // set THREAD_PARAM
    param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
    param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
    strcpy_s(param.szBuf[0], "user32.dll");
    strcpy_s(param.szBuf[1], "MessageBoxA");
    strcpy_s(param.szBuf[2], "www.reversecore.com");
    strcpy_s(param.szBuf[3], "ReverseCore");

    // Open Process
    hProcess = OpenProcess(PROCESS_ALL_ACCESS,  // dwDesiredAccess
                           FALSE,                   // bInheritHandle
                           dwPID);                  // dwProcessId

    // Allocation for THREAD_PARAM
    dwSize = sizeof(THREAD_PARAM);
    pRemoteBuf[0] = VirtualAllocEx(hProcess,        // hProcess
                                   NULL,            // lpAddress
                                   dwSize,          // dwSize
                                   MEM_COMMIT,      // flAllocationType
                                   PAGE_READWRITE); // flProtect

    WriteProcessMemory(hProcess,                    // hProcess
                       pRemoteBuf[0],               // lpBaseAddress
                       (LPVOID)&param,              // lpBuffer
                       dwSize,                      // nSize
                       NULL);                       // [out] lpNumberOfBytesWritten

    // Allocation for ThreadProc()
    dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
    pRemoteBuf[1] = VirtualAllocEx(hProcess,        // hProcess
                                   NULL,            // lpAddress
                                   dwSize,          // dwSize
                                   MEM_COMMIT,      // flAllocationType
                                   PAGE_EXECUTE_READWRITE); // flProtect

    WriteProcessMemory(hProcess,                    // hProcess
                       pRemoteBuf[1],               // lpBaseAddress
                       (LPVOID)ThreadProc,          // lpBuffer
                       dwSize,                      // nSize
                       NULL);                       // [out] lpNumberOfBytesWritten

    hThread = CreateRemoteThread(hProcess,          // hProcess
                                 NULL,              // lpThreadAttributes
                                 0,                 // dwStackSize
                                 (LPTHREAD_START_ROUTINE)pRemoteBuf[1],
                                 pRemoteBuf[0],     // lpParameter
                                 0,                 // dwCreationFlags
                                 NULL);             // lpThreadId

    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

<코드 4 - InjectCode() 함수>

위 코드는 DLL Injection 코드와 매우 유사합니다. 

InjectCode() 함수의 앞 부분은 THREAD_PARAM 구조체 변수를 세팅하고 있습니다. 이 값들은 상대방 프로세스에 인젝션 되어 ThreadProc() 스레드 함수에 파라미터로 전달될 것입니다.

* 참고!
Windows 7 에서 모든 프로세스에 로딩된 kernel32.dll 의 주소가 동일하므로 CodeInjection.exe 프로세스에서 구한 API(“LoadLibraryA”, “GetProcAddress”) 주소와 notepad.exe 프로세스에서 구한 API(“LoadLibraryA”, “GetProcAddress”) 주소가 서로 동일하다는 것을 기억하시기 바랍니다.

그리고 API 함수 호출이 이어지는데요, 핵심 API 함수들의 호출 흐름만 살펴보면 아래와 같습니다.

OpenProcess()

// data : THREAD_PARAM
VirtualAllocEx()
WriteProcessMemory()

// code : ThreadProc()
VirtualAllocEx()
WriteProcessMemory()

CreateRemoteThread()

위 코드의 핵심은 상대방 프로세스에 data 와 code 를 각각 메모리 할당하고 인젝션 시켜 준다는 것입니다. 

마지막으로 CreateRemoteThread() API 를 이용해서 원격 스레드를 실행시킵니다.

+---+

이로써 Code Injection 기법을 이용한 실습 예제 소스코드에 대한 설명을 마치도록 하겠습니다.

설명의 편의성을 위하여 매우 기초적인 실습 예제를 소개하였습니다. Code Injection 의 개념을 이해하시는데 어려움이 없을 거라 생각됩니다. 위 개념을 이해하셨다면 다양한 아이디어로 자신만의 Code Injection 기법을 연습해 보시기 바랍니다

* 참고!
제가 Code Injection 기법을 구현할 때 인젝션 시킬 코드 부분은 어셈블리 언어로 프로그래밍 합니다. 복잡한 것은 MASM 을 사용하고, 간단한 것은 OllyDbg 의 “Assemble” 명령을 사용합니다. (단축키 [Space]) 이렇게 만들어진 Hex 코드 버퍼를 위 InjectCode() 함수 내에서 상대방 프로세스에 인젝션 시켜줍니다. 이러한 방법은 좀 더 직관적인 인젝션 코드를 만드는데 도움이 됩니다. Code Injection 마지막 강좌에서 간단히 실습해보도록 하겠습니다.

다음 강좌에서는 Code Injection 기법을 디버깅 하는 방법에 대해서 알아보도록 하겠습니다.


ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

Trackback Address :: http://www.reversecore.com/trackback/82 관련글 쓰기

  1. Ezbeat 2010/06/23 06:58 댓글주소 | 수정 | 삭제 | 댓글

    와아~ 축구도 16강 진출해서 기쁜 마음에 들어와봤는데.. 글이 올라와서 있어서 더욱 기쁘네요!
    역시나 좋은 강좌 잘 봤습니다.

    제가 코드 인젝션을 응용해서 프로그램을 하나 만들어 볼건데.. 인젝션 시킬 Thread에서 API함수가 아닌 일반 함수(직접 만든 함수)를 사용해보려고 합니다.
    막상 구현해보려니.. 상당히 난해하군요..ㅠㅠ
    1. Thread에서 사용할 함수의 내부에서 사용된 모든 API함수 또한 넘겨진 구조체에 있는 값을 사용해야한다.
    2. 해당 함수도 인젝션을 시키고 들어간 주소를 구조체에 담아야한다..;;
    대충 이렇게 구현해야할 꺼같은데.. Code Injection을 사용해 API후킹을 한다면..-_-;;
    구현하는데 쫌 시간이 걸리겠군요..ㅠㅠ

    아 맞다.. 7월 5일날 입대(합격!)를 해야하는데 -_-;; 강좌를 다보고갈 수 있겠죠?? 하핫;;

    • reversecore 2010/06/23 21:18 댓글주소 | 수정 | 삭제

      원리는 동일하기 때문에 알고 계신 내용을 곰곰히 생각해 보시면 충분히 해결하실 수 있으실 겁니다.

      Code Injection 기법 설명이 끝나면 바로 이어서 Code Injection 을 이용한 API Hooking 에 대한 강좌를 진행할 예정입니다.

      원하시는 바를 꼭 이루시기 바랍니다.

      감사합니다.

  2. 궁금합니다.. 2011/01/01 21:01 댓글주소 | 수정 | 삭제 | 댓글

    궁금한게 있는데요!!

    컴파일을 하고나서 하면 CreateRemoteThread() fail : err_code = 5

    이렇게 뜨는데요.

    Error Lookup에서 찾아보니. 엑새스가 거부 되었습니다. 라고 나오는데

    어떻게 해야되나요 ? 관리자 권한으로도 실행해 보앗는데.. 안되네요!

    • reversecore 2011/01/03 01:03 댓글주소 | 수정 | 삭제

      안녕하세요.

      이전 글에 소개된 CodeInjection.exe 를 사용해도 제대로 동작하지 않는지요? (http://www.reversecore.com/attachment/cfile21.uf@20491A014C1F87C766AA91.exe)

      에러 메시지만 보면 권한이 없는것 같은데요. 위에 첨부된 CodeInjection.zip 코드 그대로 하신것 맞으시죠? 블로그 본문에 있는 코드는 실제 코드를 간략하게 편집한 것입니다. 따라서 권한 상승 코드는 보여주지 않았지요. 첨부된 소스 코드에 해당 코드가 포함되어 있습니다.

      잘 안되시면 사용하시는 실행환경을 좀 알려주시기 바랍니다.

      저도 좀 더 다양한 PC 환경에서 테스트 해보도록 하겠습니다.

      감사합니다.

  3. gg 2011/02/16 19:14 댓글주소 | 수정 | 삭제 | 댓글

    인젝션 함수에서 스레드 메모리 할당할때 스레드의 사이즈를 어떻게 구한건가요?
    (dword)인젝션함수 - (dword)스레드 라고 되어잇는데 이러면 단지 주소값을뺀건데
    자세한 설명부탁드림니다.

    • reversecore 2011/02/18 22:27 댓글주소 | 수정 | 삭제

      안녕하세요.

      네, 본문에는 문의하신 내용에 대한 설명이 부족하네요.

      인젝션 시킬 함수의 코드 크기를 구하면 되는건데요.

      일반적으로 C 언어 소스를 만들어 컴파일 하면 소스 순서대로 바이너리에 생성됩니다. 예를 들어 A(), B() C() 순으로 프로그래밍 되어있다면, 빌드된 바이너리에도 같은 순서대로 배치 된다는 뜻입니다.

      위 전체 코드를 받아보시면 ThreadProc() 함수 다음에 바로 InjectCode() 함수가 나타납니다.

      따라서 InjectCode - ThreadCode 는 ThreadCode() 함수의 크기로 볼 수 있는 것입니다.

      감사합니다.

  4. 2011/06/01 22:05 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

    • reversecore 2011/06/03 21:23 댓글주소 | 수정 | 삭제

      안녕하세요.

      개념은 잡으신것 같습니다. 실제로 그 의미를 정확히 이해하셔야 합니다. 왜? 정교하게 계산된 어셈블리 코드를 인젝션 해야 하는지? 왜? 함부로 API 호출 코드를 인젝션 하기 어려운지? 에대한 이유를 아셔야 합니다.

      감사합니다.

  5. 흠; 2011/07/18 22:53 댓글주소 | 수정 | 삭제 | 댓글

    .. 어렵군요 ! ㅠㅠ API 공부더하고와야하는가..

    • reversecore 2011/07/19 21:57 댓글주소 | 수정 | 삭제

      안녕하세요.

      위에 나온 내용은 사실 어렵습니다.

      기초 API Hooking 부터 차근차근 읽으시면 도움이 되실 것 같습니다.

      감사합니다.

  6. hyun 2011/09/22 23:23 댓글주소 | 수정 | 삭제 | 댓글

    정말 유익하게 보구 있습니다 ㅎㅎ

    악성코드를 치료하는 용도로 code injection 방법을 사용하려고 하는데요.
    악성 DLL이 특정 프로세스에 인젝션 되어 있는상태인데,
    제가 FreeLibrary를 호출하는 코드를 인젝트함으로써, 악성 DLL를 강제로 언로드 시킬수 있을까요?ㅎ

    • reversecore 2011/09/28 20:57 댓글주소 | 수정 | 삭제

      안녕하세요.

      악성코드의 자체 보호 기능이 없고, 다른 객체(파일, 네트워크, etc)를 열고 있지 않다면...
      강제로 인젝션 된 DLL 은 FreeLibrary() 로 다 내릴 수 있습니다.

      감사합니다.

  7. 저기요 2011/11/24 20:02 댓글주소 | 수정 | 삭제 | 댓글

    user32.dll 을 올렸는데 FreLibrary로 안내려줘도 괜찮은가요?
    또 VirtualAlloc으로 할당된 메모리도 해제해야 될거 같고
    메모장 종료되면 자동적으로 해제되는건가요?

    • reversecore 2011/11/30 01:47 댓글주소 | 수정 | 삭제

      안녕하세요.

      보통은 말씀하신대로 안쓰는 자원은 그때 그때 반환하는 것이 좋습니다.

      인젝션의 경우는 프로세스 종료 시 자동 해제되기 때문에 크게 신경쓰지 않는 것입니다. (필요하다면 이젝션 시켜서 자원을 반환하는 경우도 있습니다.)

      감사합니다.


Code Injection 기법 (1)

study 2010/06/22 00:25

Code Injection 기법에 대해 설명하고 실습 예제를 분석하겠습니다. DLL Injection 기법과 어떻게 다른지 비교 분석해보겠습니다.


* 본문 내용을 편하게 읽기 위해서는 아래의 배경지식이 필요합니다.




Code Injection


Code Injection 이란 상대방 프로세스에 독립 실행 코드를 삽입한 후 실행 시키는 기법입니다. 일반적으로 CreateRemoteThread() API 를 이용하여 원격 스레드 형태로 실행 시키므로 Thread Injection 이라고도 얘기합니다. 

아래 그림은 Code Injection 의 개념을 보여주고 있습니다.


<Fig. 1>

인젝션 대상이 되는 target.exe 프로세스에 코드와 데이터를 삽입합니다. 이때 코드의 형식은 스레드 프로시져(Thread Procedure) 형식으로 해주고, 코드에서 사용되는 데이터는 스레드의 파라미터로 전달해 주면 됩니다. 즉, 코드와 데이터를 각각 인젝션 시켜주는 것입니다.

이와 같이 개념은 간단한데, 구현에 있어서 주의해야 할 내용이 있습니다. Code Injection 구현의 주의 사항에 대해서 DLL Injection 과 비교하여 설명 드리겠습니다.




DLL Injection vs Code Injection


아래와 같이 간단한 코드가 있습니다. 코드의 내용은 윈도우 메시지 박스를 출력하는 것입니다.

DWORD WINAPI ThreadProc(LPVOID lParam)
{
    MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK);

    return 0;
}

DLL Injection 기법이라면 위 코드를 DLL 파일 형태로 만든 후 다른 프로세스에 인젝션 시키면 됩니다. 

OllyDbg 를 실행시켜 위 ThreadProc() 코드 영역을 살펴보겠습니다.

 
<Fig. 2>

위 그림의 코드에서 사용되는 주소를 주목하시기 바랍니다.

먼저 10001002 주소의 PUSH 10009290 명령어와 그 밑의 PUSH 1000929C 명령어에 사용된 10009290, 1000929C 주소를 보겠습니다. 이 명령어들은 MessageBoxA() API 에 사용될 문자열(“ReverseCore”, “www.reversecore.com”) 주소를 스택에 저장시킵니다.


<Fig. 3>

위 그림을 보시면 이 문자열들의 주소(10009290, 1000929C)는 DLL의 데이터 섹션 영역에 위치합니다. 

이번에는 <Fig. 2> 에서 1000100E 주소의 CALL DWORD PTR DS:[100080F0] 명령어에 사용된 100080F0 주소를 보겠습니다. 참고로 이 CALL 명령어는 바로 user32!MessageBoxA() API 호출 명령입니다.


<Fig. 4>

위 그림에서 100080F0 주소는 바로 DLL 의 IAT(Import Address Table) 영역임을 알 수 있습니다. (그 위로 다른 API 들의 주소를 확인 할 수 있습니다.)

이와 같이 DLL 의 코드에서 사용되는 모든 데이터는 DLL 의 데이터 영역에 위치합니다. 
따라서 DLL Injection 기법으로 DLL 을 통째로 상대방 프로세스 메모리에 삽입시키면 코드와 데이터가 같이 메모리에 존재하기 때문에 코드는 정상적으로 실행될 수 있습니다.

Code Injection 은 필요한 코드(<Fig. 2>)를 인젝션 시키는 것입니다. 하지만 코드에서 사용되는 데이터(<Fig. 3>, <Fig. 4>) 도 같이 인젝션 시켜줘야 정상적으로 코드의 실행이 가능해집니다. (또한 인젝션된 데이터의 주소를 코드에서 잘 알아볼 수 있도록 프로그래밍 해야 합니다.)

이러한 이유 때문에 DLL Injection 기법 보다 고려할 사항이 좀 더 많습니다.

아래의 실습 예제 코드를 보시면 더 확실히 파악하실 수 있습니다.



코드 인젝션을 사용하는 이유


코드 인젝션은 DLL 인젝션과 비교하여 기능은 비슷하면서 고려해야 할 사항은 더 많기 때문에 사용하기 불편하게 느껴질 수 있습니다. 과연 코드 인젝션의 장점은 무엇일까요?

1) 메모리를 적게 차지한다.

아주 작은 크기의 코드와 데이터를 인젝션 할 때는 DLL 로 만들어서 인젝션 시킬 필요가 없습니다. 간단히 코드 인젝션으로 구현하면 DLL 인젝션과 같은 기능을 제공하면서 메모리를 훨씬 적게 차지합니다.

2) 흔적을 찾기 어렵다.

DLL 인젝션은 해당 프로세스 메모리에 흔적을 남기기 때문에 간단히 인젝션 여부를 알 수 있습니다. 하지만 Code 인젝션은 쉽게 흔적을 남기지 않습니다. (물론 이 역시 알아낼 수 있는 방법이 있습니다.) 이 특징 때문에 악성코드에서도 코드 인젝션을 많이 사용합니다.

3) 기타

별도의 DLL 파일 없이 Code Injector 프로그램만 있으면 됩니다. 또한 처음에는 생소하지만 일단 익숙해 지게 되면 아주 쉽고 편리하게 구현이 가능합니다. 

간단히 정리하면 DLL Injection 은 규모가 크고 복잡한 일을 수행할 때 사용하고, Code Injection 은 규모가 작고 간단한 일을 수행할 때 사용합니다.



실습 예제(CodeInjection.exe)
 
실습 예제는 notepad.exe 프로세스에 간단한 코드를 인젝션 시켜서 메시지 박스를 출력하는 내용입니다.

* 참고!
CodeInjection.exe 는 Visual C++ 2008 Express Edition 으로 개발되었으며 Windows 7 32bit 환경에서 테스트 되었습니다. 

#1. notepad.exe 실행

notepad.exe 를 실행 시킨 후 Process Explorer 를 이용하여 notepad.exe 프로세스의 PID 를 확인합니다.


<Fig. 5>

제 테스트 환경에서 notepad.exe 의 PID 는 1896 입니다.

#2. CodeInjection.exe 실행

첨부된 CodeInjection.exe 파일을 실행시킵니다. 이때 실행 파라미터로 앞에서 구한 notepad.exe 의 PID 값을 입력합니다.


<Fig. 6>

#3. 메시지 박스 확인


<Fig. 7>

메시지 박스가 notepad.exe 윈도우의 밑에 깔려 있으므로 확인 하실 때 주의하시기 바랍니다.

다음 강좌에서 실습 예제의 소스 코드를 보면서 어떻게 구현되었는지 자세히 확인해 보도록 하겠습니다.


ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

Trackback Address :: http://www.reversecore.com/trackback/81 관련글 쓰기

  1. L4c0 2010/06/22 10:20 댓글주소 | 수정 | 삭제 | 댓글

    재밌어요!!ㅎㅎ ^^

  2. Ezbeat 2010/06/22 12:45 댓글주소 | 수정 | 삭제 | 댓글

    드디어 코드인젝션 강의가 시작됬군요!! 쭉 읽어보면서 빼먹거나 몰랐던 내용도 있을거 같아서 꼼꼼히 읽어보고 갑니다~! ^^ 다음 강의도 기대할께요~!

  3. 흠; 2011/07/18 22:00 댓글주소 | 수정 | 삭제 | 댓글

    CreateRemoteThread<> fail : err_code = 5
    인젝션 차단된거같은대 흐음... 좋아할 일이겠지? 흠

  4. 흠; 2011/07/18 22:15 댓글주소 | 수정 | 삭제 | 댓글

    Oh , 곰오디오보안이 노트페드보다 못하군요 ㅋ
    곰오디오는 잘되네요 ㅋ

    • reversecore 2011/07/19 21:58 댓글주소 | 수정 | 삭제

      안녕하세요.

      아마 대부분의 프로세스에서는 DLL Injection 방어 기법이 없기 때문에 그대로 인젝션 될 것입니다.

      감사합니다.

  5. 2011/10/21 19:17 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다



Global API Hooking 개념과 구현 방법에 대해서 알아보도록 하겠습니다.


<Fig - Global API Hooking>

본 내용은 이전 포스트에서 이어지는 내용입니다.

API Hooking – ‘스텔스’ 프로세스 (3)

* 참고!
모든 소스 코드는 MS Visual C++ 2008 Express Edition 으로 개발 되었으며, Windows XP SP3 & Windows 7 환경에서 테스트 되었습니다.



Global API Hooking


Global API Hooking 이란 1) 현재 실행중인 모든 프로세스2) 앞으로 실행될 모든 프로세스에 대해서 API Hooking 을 시키는 것입니다.

지난번에 설명한 예제 프로그램(HideProc.exe, stealth.dll)은 global hooking 이 아닙니다. 위에서 설명한 2) 번 조건이 만족되지 않기 때문입니다.

HideProc.exe 를 실행 하여 notepad.exe 프로세스를 은폐시켜도 이후에 Process Explorer (혹은 task manager) 를 실행시키면 이들 프로세스에서는 notepad.exe 프로세스를 볼 수 있습니다.

그 이유는 HideProc.exe 실행 이후에 생성된 프로세스들에게는 stealth.dll 파일이 (자동으로) 인젝션 되지 않기 때문입니다.

아래 링크를 참조하여 직접 실습해 보세요.

API Hooking – ‘스텔스’ 프로세스 (2)


이러한 문제를 해결하기 위한 다양한 방법이 있을 수 있습니다.
그 중에서 또 다른 API 를 Hooking 하여 Global API Hooking 을 구현하는 방법에 대해서 설명 드리겠습니다.


 
Kernel32!CreateProcess() API


새로운 프로세스가 생성되려면 kernel32!CreateProcess() API 를 사용해야 합니다. WinExec(), ShellExecute(), system() 등의 API 도 내부적으로는 CreateProcess() 를 호출합니다.

BOOL WINAPI CreateProcess(
  __in_opt     LPCTSTR lpApplicationName,
  __inout_opt  LPTSTR lpCommandLine,
  __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
  __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in         BOOL bInheritHandles,
  __in         DWORD dwCreationFlags,
  __in_opt     LPVOID lpEnvironment,
  __in_opt     LPCTSTR lpCurrentDirectory,
  __in         LPSTARTUPINFO lpStartupInfo,
  __out        LPPROCESS_INFORMATION lpProcessInformation
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms682425(VS.85).aspx

따라서 현재 실행중인 모든 프로세스에 stealth.dll 을 인젝션 하고, stealth.dll 에서 CreateProcess() API 를 후킹하면 이후 실행되는 프로세스에게도 자동으로 stealth.dll 을 인젝션 시키도록 만들 수 있습니다.

다시 설명 드리면 모든 프로세스는 부모 프로세스에서 (CreateProcess() 를 이용하여) 생성시켜주기 때문에 부모 프로세스의 CreateProcess() API 를 후킹하여 자식 프로세스에게 stealth.dll 을 인젝션 하도록 만들면 됩니다. (보통 부모 프로세스는 explorer.exe 가 될 것입니다.)

어떤가요? 좋은 아이디어 이지요?
이와 같이 Global API Hooking 의 개념은 어렵지 않습니다.

하지만 CreateProcess() API 를 후킹하면 아래와 같이 고려해야 할 사항들이 있습니다.

1) CreateProcess() API 를 후킹할 때는 kernel32!CreateProcessA(), kernel32!CreateProcessW() 두 개의 API 를 각각 후킹해야 합니다. (ASCII 버전과 UniCode 버전)

2) CreateProcessA(), CreateProcessW() 는 각각 내부적으로 CreateProcessInternalA(), CreateProcessInternalW() 를 호출합니다. 실제 MS 제품들 중에서 일부는 CreateProcessInternalA/W 를 직접 호출하기도 하지요. 따라서 좀 더 정확히 Global API Hooking 을 구현하기 위해서는 이 두 함수를 더 후킹해줘야 합니다.

3) 후킹 함수(NewCreateProcess) 는 원본 함수(CreateProcess) 를 호출 한 후 생성된 자식 프로세스에 대해서 API 를 후킹 해야 합니다. 따라서 아주 짧은 시간동안 자식 프로세스가 후킹 되지 않은 채로 실행될 수 있습니다.


많은 리버싱 선배님들에 의하여 kernel32!CreateProcess() 보다 더 후킹하기 좋은 함수가 발견 되었습니다.
바로 ntdll!ZwResumeThread() API 입니다.

NtResumeThread(
    IN    HANDLE    ThreadHandle,
    OUT   PULONG    SuspendCount OPTIONAL
);

* 유저 모드에서는 NtXXX 계열과 ZwXXX 계열은 동일합니다.

* 출처 : http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/Thread/NtResumeThread.html


ZwResumeThread() 는 프로세스가 생성 된 후 메인 스레드 실행 직전에 호출되는 함수입니다. 따라서 이 함수 하나만 후킹하면 자식 프로세스의 코드가 하나도 실행되지 않은 상태에서 API 를 후킹시킬 수 있습니다.

단점은 ZwResumeThread() 는 undocumented API 라서 언제 바뀔지 알 수 없으며, 그만큼 안정성을 보장 할 수 없습니다. 따라서 ZwResumeThread() 같은 undocumented API 를 후킹 할 때는 OS 가 패치되면서 변경될 수 있다는 것을 항상 염두에 두어야 합니다. 하위 버전에서는 잘 되던 후킹이 최신 버전에서는 갑자기 안 되는 일이 많기 때문입니다.



실습




* 참고
위 stealth2.dll 는 CreateProcess 후킹 버전입니다.
ZwResumeThread 후킹 버전을 원하시는 분께서는 따로 요청해 주시면 보내드리겠습니다.

실습을 간단히 하기 위해서 은폐 프로세스를 notepad.exe 로 고정하였습니다. 참고하시기 바랍니다.

#1. stealth2.dll 파일 -> %SYSTEM% 폴더에 복사


<Fig. 1>

실행 중인 모든 프로세스에 stealth2.dll 파일을 인젝션 시킬 예정입니다. 따라서 모든 프로세스에서 공통적으로 인식할 수 있는 path 인 %SYSTEM% 폴더에 stealth2.dll 파일을 복사합니다.

#2. HideProc2.exe –hide 실행


<Fig. 2>

기존 HideProc.exe 와 비교해서 실행 파라미터가 변경되었습니다. 은폐 프로세스 이름이notepad.exe 로 하드코딩 되어있습니다.
HideProc2.exe 를 –hide 옵션으로 실행시키면 이제부터 글로벌 후킹이 시작됩니다.

#3. ProcExp.exe & notepad.exe 실행

Process Explorer(혹은 작업관리자) 와 notepad 를 여러 개 실행 해 주세요.

 
<Fig. 3>

위 그림을 보시면 ProcExp.exe 와 notepad.exe 프로세스가 각각 2개씩 실행되고 있습니다.
하지만 ProcExp.exe 에서는 notepad.exe 프로세스가 은폐되어 있습니다.

추가로 ProcExp.exe 를 몇 개 더 실행해 보시기 바랍니다. 마찬가지로 새로 생성된 ProcExp.exe 프로세스에서도 notepad.exe 프로세스가 은폐되어서 보이지 않을 것입니다.

이것이 바로 Global API Hooking 의 효과입니다.

#4. HideProc2.exe –show 실행

Global API Hooking 을 해제 합니다.

 
<Fig. 4>

이제 Process Explorer(혹은 작업관리자) 에서 notepad.exe 프로세스가 보일 것입니다.



소스 코드


# HideProc2.cpp


HideProc2.cpp 는 기존 HideProc.cpp 에서 실행 파라미터를 줄인 것뿐이므로, 기존 설명을 참고하시면 되겠습니다.

API Hooking – ‘스텔스’ 프로세스 (3)


# stealth2.cpp


stealth2.cpp 는 기존 stealth.cpp 에서 은폐 프로세스 이름을 "notepad.exe" 로 하드 코딩 하였고, global hooking 을 위해서 CreateProcessA() API 와 CreateProcessW() API 를 후킹 하는 코드가 추가 되었습니다.


DllMain()

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    char            szCurProc[MAX_PATH] = {0,};
    char            *p = NULL;

    // HideProc2.exe 프로세스에는 인젝션 되지 않도록 예외처리

    GetModuleFileName(NULL, szCurProc, MAX_PATH);
    p = strrchr(szCurProc, '\\');
    if( (p != NULL) && !_stricmp(p+1, "HideProc2.exe") )
        return TRUE;

    // change privilege
 
   SetPrivilege(SE_DEBUG_NAME, TRUE);

    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH :
            // hook
            hook_by_code("kernel32.dll", "CreateProcessA",
                         (PROC)NewCreateProcessA, g_pOrgCPA);
            hook_by_code("kernel32.dll", "CreateProcessW",
                         (PROC)NewCreateProcessW, g_pOrgCPW);
            hook_by_code("ntdll.dll", "ZwQuerySystemInformation",
                         (PROC)NewZwQuerySystemInformation, g_pOrgZwQSI);
            break;

        case DLL_PROCESS_DETACH :
            // unhook
            unhook_by_code("kernel32.dll", "CreateProcessA",
                           g_pOrgCPA);
            unhook_by_code("kernel32.dll", "CreateProcessW",
                           g_pOrgCPW);

            unhook_by_code("ntdll.dll", "ZwQuerySystemInformation",
                           g_pOrgZwQSI);
            break;
    }

    return TRUE;
}

위 DllMain() 함수를 보시면 CreateProcessA, CreateProcessW 를 후킹하는 코드가 추가되었습니다.


NewCreateProcessA()

CreateProcessA() API 의 후킹 함수인 NewCreateProcessA() 코드를 살펴 보겠습니다. (NewCreateProcessW() 코드도 거의 동일합니다.)

BOOL WINAPI NewCreateProcessA(
    LPCTSTR lpApplicationName,
    LPTSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,
    LPCTSTR lpCurrentDirectory,
    LPSTARTUPINFO lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation
)
{
    BOOL bRet;
    FARPROC pFunc;

    // unhook

    unhook_by_code("kernel32.dll", "CreateProcessA", g_pOrgCPA);

    // original API 호출

    pFunc = GetProcAddress(GetModuleHandle("kernel32.dll"), "CreateProcessA");
    bRet = ((PFCREATEPROCESSA)pFunc)(lpApplicationName,
                                     lpCommandLine,
                                     lpProcessAttributes,
                                     lpThreadAttributes,
                                     bInheritHandles,
                                     dwCreationFlags,
                                     lpEnvironment,
                                     lpCurrentDirectory,
                                     lpStartupInfo,
                                     lpProcessInformation);

    // 생성된 자식 프로세스에 stealth2.dll 을 인젝션 시킴

    if( bRet )
        InjectDll2(lpProcessInformation->hProcess, STR_MODULE_NAME);

    // hook

    hook_by_code("kernel32.dll", "CreateProcessA",
                 (PROC)NewCreateProcessA, g_pOrgCPA);

    return bRet;

}

코드는 매우 단순합니다.
일단 후킹을 풀고 원본 함수를 실행해서 생성된 자식 프로세스에 stealth2.dll 을 인젝션 시킵니다.
그 후 다음 실행을 위해서 다시 후킹해 줍니다.

제 글을 꾸준히 읽어오셨다면 쉽게 이해하실 수 있으실 겁니다.

한가지 눈여겨 볼 사항은 인젝션 함수인 InjectDll2() 입니다.
기존 InjectDll() 함수는 프로세스 ID (PID) 를 이용하여 프로세스 핸들을 얻어 인젝션 시키는 방법이었습니다. (OpenProcess() API 이용)

하지만 위의 경우는 CreateProcessA() API 를 호출하면서 자연스럽게 자식 프로세스의 핸들(lpProcessInformaiton->hProcess)을 얻을 수 있습니다. 이 내용도 같이 참고하시면 좋을 것 같습니다.



+---+

지금까지 Global API Hooking 에 대해서 알아보았습니다.

시스템 전체 프로세스에 대해서 후킹을 하는 기술이기 때문에 예상치 못한 에러가 발생할 수 있습니다. 따라서 사전에 꼼꼼한 테스트가 필요합니다. 그리고 undocumented API 를 후킹할 때는 현재 OS 버전에서 예상대로 동작하는지 반드시 확인하셔야 합니다.


다음번에는 API Hooking 시리즈의 마지막 테마인 Code Injection 기법에 대해서 설명드리도록 하겠습니다.

많이 기대해 주세요.

ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

Trackback Address :: http://www.reversecore.com/trackback/68 관련글 쓰기

  1. kuaaan 2010/01/18 09:24 댓글주소 | 수정 | 삭제 | 댓글

    일반적으로 ANSI 타입의 API들은 내부적으로 W타입을 Wrapping하는데요... CreateProcess는 그렇지 않군요. 신기하네요.. ^^a

  2. pico 2010/01/23 16:03 댓글주소 | 수정 | 삭제 | 댓글

    좋은 글 많이 보고 갑니다. 계속 좋은 글 부탁드려요 ^^.. view on 열심히 눌렀습니다. ^^;;

  3. 고기 2010/01/25 11:21 댓글주소 | 수정 | 삭제 | 댓글

    항상 눈팅만 하다가 댓글 남깁니다.
    정말 감사해요~

  4. Ezbeat 2010/04/22 18:33 댓글주소 | 수정 | 삭제 | 댓글

    VS옵션이 다른지.. reversecore님께서 올리신 소스를 제 VS에서 컴파일 시키면 에러가 많이 뜨더군요. 그냥 오류나는것을 보면 hook_by_code의 첫번째 인자를 넘겨줄 때
    "ntdll.dll" 이것을 넘겨주는데 해당 타입은 char* 입니다. 하지만 함수 정의부분에서 보면 자료형을
    LPCTSTR로 받고 있더군요. char*과 TCHAR* 타입이 달라서 오류가 쫘좌작 ;; 뜨네요 ㅜㅜ

    VS에서 옵션을 어떻게 주고 컴파일을 하시는지 ;;

    • reversecore 2010/04/23 23:10 댓글주소 | 수정 | 삭제

      아하~

      제가 블로그 초반에는 VS 6 에서 작성된 예제를 가져다 쓰는 경우가 많아서 multibyte 옵션으로 하다가...

      최근에는 unicode 옵션으로 하고 있습니다.

      위 stealth2.cpp 는 multibyte 용으로 작성되었네요. ^^

      VC++ 2008 의 프로젝트 속성 다이알로그에서 "일반\문자 집합" = "멀티바이트 문자 집합 사용" 으로 변경 후 컴파일 하시면 잘 될 것입니다.

      만약에 잘 안되시면 다시 연락주세요.
      프로젝트 파일을 보내드리겠습니다.

      감사합니다.

  5. Ezbeat 2010/04/24 17:29 댓글주소 | 수정 | 삭제 | 댓글

    아 ^^; 전 유니코드 문자 집합 사용으로 되어있네요.. 음..컴파일은 되는데
    전 이 옵션으로 코딩하는게 습관이 들어서 ..( 윤성우님이 쓰신 시스템 프로그래밍 책을 공부해서;.. ) 유니코드 문자 집합 사용 옵션으로 하고
    소스짜도 큰 상관은 없죠?? ㅠ ㅠ

  6. ohsung 2010/05/10 10:39 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요~ 유용한 정보 감사드립니다. 아직 테스트는 해보지 않았는데요 혹시 이 dll이 전역적으로 훅킹을 한다면 시스템에 부하가 걸리는건 아닌지 궁금해서요. 모든 프로세스가 생성될때마다 dll이 인젝션이 된다면 메모리 사용량도 늘어나는건 아닌지요?

    • reversecore 2010/05/12 12:55 댓글주소 | 수정 | 삭제

      ohsung님, 안녕하세요.

      좋은 질문이십니다. ^^

      모든 후킹은 그만큼의 부하가 걸립니다. 다만 프로그래밍을 잘하면 그 부하가 미미하기 때문에 큰 문제는 없습니다만, 어디까지나 시스템의 안정성과 자원을 잘 고려하셔야 합니다.

      그리고 모든 프로세스에 DLL 이 인젝션 되면 말씀하신 대로 메모리 사용량이 그만큼 증가합니다. DLL 크기 * 인젝션 프로세스 개수 만큼 늘어나는 것은 아니구요. Windows 에서는 같은 DLL 의 경우 한번만 메모리에 전체를 로딩하고 프로세스 들에게는 매핑의 개념을 사용합니다. 쉽게 설명해서 코드 섹션은 다 같은 메모리를 보고 있게 매핑하고요, 데이타 섹션만 해당 프로세스 별로 새로 생성합니다.

      다른 질문 있으시면 올려주세요~

      감사합니다.

  7. booraik 2010/05/19 20:12 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 정리를 깔끔하게 해놓으셔서 항상 잘보고 있습니다^^

    reversecore님께서는 컴파일을 다 multibyte로 하시나봐요
    저는 unicode를 사용하는데 수정할 일이 한두군데가 아니네요..ㅜㅜ
    이상한데서 버그가 막 튀 나오고 ㅋ
    DllMain에서 DLL_PROCESS_ATTACH, DETACH에 있는
    hook_by_code를 모두 주석처리하면 인젝션까지는 되는데
    주석을 하지 않으면 인젝션조차도 안되네요 ㅎㅎ
    아무래도 이거 multibyte->unicode때문에 그런거 같은데
    혹시 unicode기반으로 작성하신 소스코드는 없으신지요?
    있다면 메일로좀 부탁드릴게요 ㅎㅎ

    • reversecore 2010/05/20 18:09 댓글주소 | 수정 | 삭제

      안녕하세요.

      최신 VC++ 에서는 디폴트 설정이 UniCode 라 몇몇 분들께서 컴파일 과정에 에러가 발생한다고 얘기를 해주셨습니다.

      위 소스에 대해서 조만간 UniCode 버젼도 같이 올려놓도록 하겠습니다.

      감사합니다.

  8. xss 2010/06/24 21:49 댓글주소 | 수정 | 삭제 | 댓글

    좋은글 올려주셔서 감사합니다. 그런데 어떤 경우는 CreateProcess를 이용하지 않고 프로세스 생성이 되는 경우가 있는것 같네요. 예를들어 cmd.exe로 어떤 프로그램을 실행시켰는데 Process Explorer로 보면 그 프로그램에는 stealth.dll 이 로드되지 않는게 관찰되네요. 이후에 올려주신 ZwResumeThread를 이용한 버젼으로도 테스트해보았지만 결과는 마찬가지입니다. 혹시 이점에 대해서 해결책을 알고 계신가요?

    • reversecore 2010/06/25 19:12 댓글주소 | 수정 | 삭제

      안녕하세요.

      네, 위에서 언급한 대로 CreateProcess() 함수 내부에서 CreateProcessInternal() 를 호출하는데...
      만약 CreateProcessInternal() 를 직접 호출한다면 그 자식 프로세스에게는 후킹이 되지 않을 것입니다.

      이럴때는 CreateProcessInternal() 까지 후킹을 하시거나 다른 API 를 후킹하시면 됩니다.

      Advanced Global API Hooking (http://www.reversecore.com/77) 를 참고하시면 위 문제에 대한 해결이 가능할 것입니다.

      감사합니다.

    • xss 2010/06/25 18:53 댓글주소 | 수정 | 삭제

      답변 감사드립니다만 말씀해주신대로 ZwResumeThread를 후킹하여도 같은 현상이 일어나고 있습니다.

    • reversecore 2010/06/26 00:25 댓글주소 | 수정 | 삭제

      cmd.exe 를 간단히 살펴보니 CreateProcessW() 를 사용합니다. 위 stealth2.dll 에서 CreateProcessW() 를 후킹하고 있는데, 버그가 있는지 잘 안되는군요. 좀 더 테스트해본후 수정해야 겠네요...

      ZwResumeThread() 후킹 방식은 잘 됩니다.

      http://www.reversecore.com/79

      위 글에 소개된 InjDll.exe 를 이용해서 모든 프로세스(*)에 redirect.dll 을 인젝션 시켜주세요. 그러면 글로벌 후킹이 걸리게 됩니다. 그리고 cmd.exe 에서 notepad.exe 등을 실행하면 redirect.dll 이 인젝션 되어 있는 것을 확인하실 수 있습니다.

      * ZwResumeThread() 후킹 예제로 만든게 아직 redirect.dll 밖에 없군요.

      잘 안되시면 다시 질문해 주세요~

      감사합니다.

  9. dear 2010/07/30 10:05 댓글주소 | 수정 | 삭제 | 댓글

    질문있습니다.
    언인젝션은 어떻게 하나요?.
    같은방법으로 FreeLibrary를 리모트스레드로 하면되나요?.
    그런데 FreeLibrary는 LoadLibrary하고 프로토타입이 틀린데...

  10. 김성현 2010/08/03 18:40 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요.
    이방법으로 dll인젝션을해 보았는데요.
    explorer.exe에 인젝션을해서 createprocess를 후킹하고있습니다.
    인젝션후에
    opencapture.exe라는 캡쳐 프로그램(델파이로만들어짐)을 실행시키면
    잘못된 연산으로 죽습니다.
    그러나 가끔 실행될때가 있습니다.
    dllmain에 attach/dettach 에 있는 로직을 다 지우고
    시도 해도 opencapture.exe가 실행할떄 잘못된 연산으로 죽네요.

    환경은
    WINDOWSXP SP3 32BIT입니다.
    디버거로 어셈블리를 따라가면,

    콜스택은
    > kernel32.dll!__SEH_prolog() + 0x1a バイト
    OpenCapture.exe!00406120()
    [下のフレームは間違っているか、または見つかりません。OpenCapture.exe に対して読み込まれたシンボルはありません。]
    ntdll.dll!ExecuteHandler2@20() + 0x26 バイト
    0012ffb0()

    그리고 역어셈블은
    ReleaseMutex@4:
    7C8024B7 mov edi,edi
    7C8024B9 push ebp
    7C8024BA mov ebp,esp
    7C8024BC push 0
    7C8024BE push dword ptr [ebp+8]
    7C8024C1 call dword ptr [__imp__NtReleaseMutant@8 (7C8014BCh)]
    7C8024C7 test eax,eax
    7C8024C9 jl _ReleaseMutex@4+19h (7C812891h)
    7C8024CF xor eax,eax
    7C8024D1 inc eax
    7C8024D2 pop ebp
    7C8024D3 ret 4
    __SEH_prolog:
    7C8024D6 push offset __except_handler3 (7C839AD8h)
    7C8024DB mov eax,dword ptr fs:[00000000h]
    7C8024E1 push eax
    7C8024E2 mov eax,dword ptr [esp+10h]
    7C8024E6 mov dword ptr [esp+10h],ebp
    7C8024EA lea ebp,[esp+10h]
    7C8024EE sub esp,eax
    7C8024F0 push ebx
    7C8024F1 push esi
    7C8024F2 push edi
    7C8024F3 mov eax,dword ptr [ebp-8]
    7C8024F6 mov dword ptr [ebp-18h],esp
    7C8024F9 push eax
    7C8024FA mov eax,dword ptr [ebp-4]
    7C8024FD mov dword ptr [ebp-4],0FFFFFFFFh
    7C802504 mov dword ptr [ebp-8],eax
    7C802507 lea eax,[ebp-10h]
    7C80250A mov dword ptr fs:[00000000h],eax
    7C802510 ret
    __SEH_epilog:

    7C8024F0 push ebx 에서 에러가 발생하는것으로 캐치됩니다.

    나중에 zwresumethread를 후킹하는 강좌도 테스트 해보겠지만,
    왜 됬다 안됬다 하는지 이유가 궁금합니다.
    (DLL이 인젝션되어서 실행될떄가 있고, 아예 실행하면 잘못된 연산으로 죽을떄가 있다는뜻입니다.)
    바쁘실텐데, 답변 부탁드립니다.
    그럼이만.

    • reversecore 2010/08/04 22:24 댓글주소 | 수정 | 삭제

      안녕하세요.

      와~ 콜스택까지 올려주셨네요~ ^^

      저도 OpenCapture 를 잘 사용하는데요.
      위의 실습 파일을 가지고 조만간 테스트 해서 결과를 알려드리겠습니다.

      기본적으로 됐다 안됐다 하는 것은 대부분 프로그램 안정성 때문입니다. 유효하지 않은 메모리를 참조할 때 그렇습니다. 콜스택에서kernel32 의 SEH 주소를 가리키는 것을 보면 거의 잘못된 메모리 참조 때문일 것입니다.

      * 사용하시는 OpenCapture 의 정확한 버전을 알면 도움이 되겠습니다.

      * 여담인데요...
      전 처음에 제가 아는 분과 성함이 똑같아서 깜짝 놀랐는데요...
      생각해보니 그 분은 저보다 훨씬 실력이 뛰어나신 분이라서요...
      답변을 달아주시면 몰라도 질문을 올리지는 않으실꺼라 생각했답니다. ㅋ~

      감사합니다.

    • reversecore 2010/08/04 22:31 댓글주소 | 수정 | 삭제

      제가 가진 XP SP3 에서 테스트 결과 충돌 증상은 없네요. (MS VirtualPC 환경입니다.)

      제 OpenCapture 버전은 1.4.3 입니다.

      나중에 리얼 환경에서 다시 테스트 해보도록 하겠습니다.

      * 사실 API 코드 패치 기반의 API 후킹은 그 자체로 좀 위험성이 있습니다. 프로세스 실행중에 동적으로 패치하는 방법이라서... 패치하려는 순간 다른 스레드가 그 코드를 실행하고 있다면??? 바로 충돌 나겠죠. Jeffrey Richter 는 자신의 책에서 이점을 분명히 밝혔죠. ^^

      감사합니다.

  11. WERT 2010/08/26 10:05 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요. 좋은 정보 많이 배우고 갑니다.

    열심히 코딩해서 실행을 시켜 보았는데요. 함수 후킹은 아니구 모든 프로세스에 Injection 까지만 했을 때 일부 프로세스에만 Injection이 되는 현상이 나타납니다.

    제 환경은 Win 7 64bit 또는 Win XP 32bit입니다. 혹시나 권한문제인가해서 둘다해보았지만 같은 현상이었습니다. dll은 dummy.dll을 사용했습니다. 이 포스팅에 있는 코드랑 다른 포스팅에 있는 Win 7 에서 RemoteThreadEx인가 사용하는 코드를 넣었습니다.

    인젝션되는 프로세스는 보면 여기 스텔스 2번포스팅의 Process Explorer에 보이는 것처럼 svchost같은데는 전혀 안들어가구요 비쥬얼스튜디오나 네이트온같은곳에만 인젝션이 들어가네요.. 도대체 왜 이런지 넘 궁금합니다;

    • reversecore 2010/08/28 00:36 댓글주소 | 수정 | 삭제

      WERT 님, 안녕하세요.

      svchost.exe 는 서비스로 동작하는 프로그램이지요.
      문의하신 글을 읽어보니 아마 특권(privilege) 문제가 아닌가 생각됩니다.

      http://www.reversecore.com/76

      위 포스트에 소개된 InjDll.exe 와 dummy.dll 을 가지고 테스트 해보시겠어요?

      InjDll.exe 에 특권을 조정하는 코드를 적용시켜 놓았습니다.

      관련 소스는 http://www.reversecore.com/75 에서 InjDll.cpp 의 SetPrivilege() 함수를 참고하시면 되겠습니다.

      감사합니다.

  12. L4c0 2010/09/03 15:32 댓글주소 | 수정 | 삭제 | 댓글

    여기있는 코드들을 조합해서 Dll Injection을 해보았는데요. 훅을 설치할 경우 JMP코드를 삽입하는데 여기에 있는 hook_by_code에서 사용하는 방식은 32비트 밖에는 안되는 것 같습니다.

    한가지 질문이 있는데 32Bit에서는 Jmp [절대주소] 방식으로 사용이 가능한데 64Bit에서는 Jmp를 쓰기위해서는 rax레지스터를 이용하거나 rsp를 이용하여야 하더라구요. 이런 방식이다보니 아무리 적어도 11byte쯤? 여러가지 방법이 있지만 대충 최소 그 정도 이상의 바이트를 필요로 하게 된다고 하는것 같습니다.

    그래서 뭔가 해볼려고 하는데 혹시 어셈코드에 대응되는 바이너리를 쉽게 만드는 방법이 있는지 궁금합니다; mov rax, 주소; jmp rax <-- 요러한 코드를 만든다고 했을 때 이에 대응되는 바이너리는 어떻게 하면 알 수 있는건가요? Windbg로 Attach시킨다음에 a커맨드를 이용해서 위의 코드를 넣어볼려고 했는데 rax레지스터를 쓰는게 안되더라구요.. eax는 되던데;; 그래서 훅이 되는건지 안되는건지 확인도 못해보고 -_ㅜ;

    하나 더 질문드리면.. 위처럼 훅으로 박아넣는 점프코드가 길어졌는데.. 이때 이 점프코드보다 길이가 짧은함수들은 훅이 절대 불가능한건가요??

  13. dngchn 2010/12/03 16:55 댓글주소 | 수정 | 삭제 | 댓글

    본 컬럼 테스트를 하고 있습니다.
    제가 타겟으로 삼은 프로그램은 실행 하다가 "WinExec"를 통해 다른 프로세스를 생성하고 죽습니다. (즉 자식을 낳고 바로 죽는 부모의 경우)
    위 글에서 보면 "WinExec, ShellExecute... 등 도 내부적으로는 CreateProcess를 호출한다고 하였는데, 제 타겟에 대해 이를 체크해보면 WinExec를 통해 자식 프로세스를 생성하므로, 결과적으로는 CreateProcess에서 훅을 걸어보면 걸리지 않습니다. (WinExec에다 걸면 잘 걸리구요.)
    그래서 제 생각에는 ZwResumeThread를 한번 사용해보고 싶습니다. 코드좀 주시면 감사하겠구요.
    또 하나 걸리는 부분은 부모프로세스가 WinExec를 부르고 바로 죽기때문에 부모프로세스에 훅을 걸어놓은 CreateProcess를 탐지 할 수 없는 것일 수도 있을것 같은데, 이에 대해 관리자님 생각의 어떠신지 조언 부탁드립니다.

    • reversecore 2010/12/10 16:04 댓글주소 | 수정 | 삭제

      안녕하세요. ReverseCore 입니다.

      답변이 늦어서 죄송합니다. @@~

      CreateProcess() 는 내부적으로 CreateProcessInternal() 을 호출합니다. 그리고 CreateProcessInternal() 내부에서 ntdll!ZwResumeThread() 를 호출하는 식입니다.
      (http://www.reversecore.com/78 글의 <Fig. 9> 밑의 설명을 참고하세요)

      WinExec() 는 CreateProcessInternal() 를 바로 호출합니다. 따라서 CreateProcess() 를 후킹해서는 효과가 없습니다.

      ZwResumeThread() 사용 코드는 http://www.reversecore.com/80 글을 참고하시면 될것 같습니다.

      감사합니다.

  14. kmslife 2011/04/15 18:02 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요.
    질문이 있어서 이렇게 글을 올립니다.
    첨부되어 있는 프로그램으로 실행은 잘 되는데요
    문제는 vs2008로 만들어진 프로그램들, 그리고 vs2008 툴 자체는 위 dll을 injection 시키면
    프로그램 자체가 실행되지 않습니다.
    vs2003이나, 그외 프로그램들은 잘 동작하는데 말이죠
    ASLR기능 인가 찾아 보았지만 제 pc의 환경은 xp sp3환경이어서 문제가 되지는 않을 것 같습니다.
    원래 vs2008관련되어서는 dll injection 기능이 동작하지 않는것인가요?
    동작하게 하려면 어떻게 해야 할지요?

    • reversecore 2011/04/20 10:41 댓글주소 | 수정 | 삭제

      안녕하세요.

      지금 제 환경은 Win7 64bit, WinXP SP3 32bit 인데요.

      말씀하신 VS2008 은 없어서 Visual C++ 2008 Express (VCExpress.exe 프로세스)에 테스트 해본 결과 정상동작 하네요.

      VS2008 로 제작된 파일에 인젝션 시키면 동작 안한다고 하셨는데요.
      그 파일을 제게 보내주시면 테스트 해보겠습니다.

      실행 파일을 zip 압축 하신 후 확장자를 zipx 로 변경하셔서 아래 메일로 보내주시기 바랍니다.

      reversecore@gmail.com

      감사합니다.

  15. jjengae 2011/09/28 20:06 댓글주소 | 수정 | 삭제 | 댓글

    스텔스 프로세스(2)에서 올려주셨던 HideProc.exe는 XP기반에서만 작동하는 것 같더라구요

    윈도우 7(Enterprise 32bit)에서 vmware-vmx.exe라는 프로세스를 숨기기 하고 싶은데

    이번에 포스팅에 올려주신 파일은 notepad.exe로 고정되어 있는것 같아서요

    제가 임의의 프로세스를 숨기려면 어떤 부분을 수정해서 어떻게 컴파일 해야 하는건가요?

    .exe와 .dll은 한번도 컴파일해보지 않아서...

    또, 수정하려면 무조건 Visual Studio가 설치되어 있어야 하는건가요?

    기초 지식 없이 이렇게 물어봐서 죄송합니다

    가능하시다면 jjengae@nate.com 으로 메일 부탁드릴게요

    • reversecore 2011/09/28 20:36 댓글주소 | 수정 | 삭제

      안녕하세요.

      위 예제에서 은폐프로세스 이름은 notepad.exe 로 하드코딩 되어 있습니다.

      소스코드에서 "notepad.exe" 문자열을 "vmware-vmx.exe" 문자열로 수정 후 빌드 하시면 됩니다.

      Visual C++ 을 한번도 사용해보지 않으셨다면 조금 어려움이 있을 수 있습니다.

      감사합니다.

  16. jjengae 2011/09/29 23:32 댓글주소 | 수정 | 삭제 | 댓글

    빌드를 해야된다고 하셔서 VS2008 설치했습니다.

    "notepad.exe"를 수정해야 한다고 하시는건 "stealth2.cpp"에 있는 내용을 수정하고 빌드해야 된다는 말씀이시죠?

    "HideProc2.cpp"는 "멀티 바이트 문자 집합" 설정하고 컴파일 했더니 debug폴더에 실행파일이 생성되더군요. 문제는 stealth2.cpp 파일입니다.

    여차여차해서 stealth2.cpp 파일을 컴파일 하려고 프로젝트 생성시에 응용프로그램 종류에서 콘솔 응용프로그램 대신 "DLL"로 셋팅하고 멀티바이트 문자 집합으로 설정했더니 오류가 나지 않더군요.

    근데 컴파일 후에 나타나는 창이 무엇인지 잘 모르겠습니다. "디버그 세션이 사용할 실행 파일"이라는 창이 뜨고 "실행 파일 이름"과 "프로젝트에 액세스할 수 있는 URL"을 입력하라고 하는데 그것을 무엇으로 해야할지 잘 모르겠습니다.

    요약하자면 stealth2.cpp 파일을 어떻게 .dll로 컴파일하는지 좀 알려주시면 감사하겠습니다.

    혹시 .dll로 컴파일하는 포스팅은 없는건가요? 시간 되신다면 vmware-vmx.exe로 적용하여 jjengae@nate.com으로 보내주시면 감사하겠습니다. ㅜㅜ

    도움 주시면 감사하겠습니다.

  17. jjengae_father CPP 2011/09/30 10:47 댓글주소 | 수정 | 삭제 | 댓글

    글 보고 메일 발송하였는데요.

    • jjengae 2011/09/30 11:38 댓글주소 | 수정 | 삭제

      관심 감사합니다.
      그런데 메일을 확인해보니 메일이 도착하질 않았네요..
      죄송하지만 다시한번 메일 보내주셨으면 감사하겠습니다

  18. jjengae 2011/10/01 14:31 댓글주소 | 수정 | 삭제 | 댓글

    제발 누가 stealth2.cpp dll로 빌드하는 방법좀 알려주실 수 없나요 ....

  19. jjengae_father CPP 2011/10/06 22:32 댓글주소 | 수정 | 삭제 | 댓글

    안녕하셰요.
    메일 확인하니 주소가 틀렸어요.
    VS키고 dll프로젝트를 하나 창조하지요.
    다음 dll main에서 stealth2.cpp를 코피하다놓고 빌드하면 되겠지요.

    • jjengae 2011/10/08 00:58 댓글주소 | 수정 | 삭제

      전에 보내셨던 메일이 어떤 내용이었는지는 모르겠지만
      보낸메일함에서 전달로 다시한번 보내주실 수 없나요?

      그리구 제가 일반 콘솔 프로젝트는 많이 해봤는데
      dll은 컴파일을 하면 이상한 창이 하나 뜨더라구요
      "디버그 세션이 사용할 실행 파일"이라고...
      실례가 안된다면 프로젝트 생성부터 과정을 좀 자세하게 설명좀 부탁드릴 수 있을까요?

  20. jjengage_father CPP 2011/10/11 20:20 댓글주소 | 수정 | 삭제 | 댓글

    안녕하셰요.
    VS를 켜시고 File/New/Project/Win32를 선택하시지요.
    dll옵션을 체크하시고 empty를 선택하세요.
    그렇게하군서 stealth2.cpp를 코피하다가 빌드하세요.
    dll컴파일을 하구선 loadlibrary를 사용하셰야 디버그를 할수잇어요.

  21. jjengae 2011/10/13 07:05 댓글주소 | 수정 | 삭제 | 댓글

    드디어 빌드를 완료했습니다.

    그런데 아래와 같은 증상이 나타나면서 프로세스는 사라지지 않는데, 무엇이 문제인가요 ?

    C:\Work>HideProc2.exe -hide stealth.dll
    OpenProcess(968) failed!!!
    OpenProcess(1572) failed!!!
    OpenProcess(2296) failed!!!

  22. jjengage_father CPP 2011/10/13 14:14 댓글주소 | 수정 | 삭제 | 댓글

    와 정말 장하시네요.
    근데 또 에러가 있군요.
    오~~~~그거 권한문제때문같아요.
    안녕하셰요.

  23. jjengae 2011/10/14 04:35 댓글주소 | 수정 | 삭제 | 댓글

    father님 항상 조언 감사합니다.
    덕분에 dll 빌드하는 방법을 알았네요.
    전 항상 컴파일만 돌릴생각만 하고 안된다고 생각했었는데 빌드를 해야했군요.

    reversecore님 이게 권한문제때문에 발생하는 에러인가요?
    만약 권한문제라면 권한 문제는 어떻게 해결해야 하나요?



실습 예제 프로그램의 소스를 분석하면서 "Code Patch" 방식의 API Hooking 에 대하여 알아 보도록 하겠습니다.

<stealth.cpp 코드 일부>


본 내용은 이전 포스트에서 이어지는 내용입니다.
API Hooking – '스텔스' 프로세스 (2)


* 참고!
모든 소스 코드는 MS Visual C++ 2008 Express Edition 으로 개발 되었으며, Windows XP SP3 환경에서 테스트 되었습니다.



HookProc.exe



HookProc.exe 는 실행중인 모든 프로세스에 특정 DLL 파일을 Injection/Ejection 시켜주는 프로그램 입니다. 기존의 InjectDll.exe 프로그램에 모든 프로세스를 검사하는 기능을 추가한 거라고 생각하시면 됩니다.

대부분의 인젝션 코드는 기존의 InjectDll.cpp 와 같습니다. 아래 링크를 참고해 주세요.
DLL Injection – 프로세스에 침투하기 (2)


# InjectAllProcess()

새로 추가된 InjectAllProcess() 함수에 대해서 설명하겠습니다. 이 함수에서 실행중인 모든 프로세스를 검색하여 각각 DLL Injection/Ejection 을 수행합니다.

BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath)
{
    DWORD                   dwPID = 0;
    HANDLE                  hSnapShot = INVALID_HANDLE_VALUE;
    PROCESSENTRY32          pe;

    // Get the snapshot of the system
    pe.dwSize = sizeof(PROCESSENTRY32);
    hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);

    // find process
    Process32First(hSnapShot, &pe);
    do
    {
        dwPID = pe.th32ProcessID;

        // 시스템의 안정성을 위해서
        // PID 가 100 보다 작은 시스템 프로세스에 대해서는
        // DLL Injection 을 수행하지 않는다.

        if( dwPID < 100 )
            continue;

        if( nMode == INJECTION_MODE )
            InjectDll(dwPID, szDllPath);
        else
            EjectDll(dwPID, szDllPath);
    }while( Process32Next(hSnapShot, &pe) );

    CloseHandle(hSnapShot);

    return TRUE;
}

CreateToolhelp32Snapshot() API 를 이용해서 시스템에 실행중인 모든 프로세스 리스트를 얻어내고 Process32First() 와 Process32Next() API 를 이용해서 PID 를 구합니다.

HANDLE WINAPI CreateToolhelp32Snapshot(
  __in  DWORD dwFlags,
  __in  DWORD th32ProcessID
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms682489(VS.85).aspx

BOOL WINAPI Process32First(
  __in     HANDLE hSnapshot,
  __inout  LPPROCESSENTRY32 lppe
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms684834(VS.85).aspx

BOOL WINAPI Process32Next(
  __in   HANDLE hSnapshot,
  __out  LPPROCESSENTRY32 lppe
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms684836(VS.85).aspx


* 주의!
미리 HideProc.exe 프로세스의 권한(특권)을 상승시켜 놓아야 전체 프로세스의 리스트를 정확하게 얻을 수 있습니다. HideProc.cpp 에서는 main() 함수에서 미리 _EnableNTPrivilege() 함수를 호출하고 이 함수 내부에서 AdjustTokenPrivileges() API 를 이용해서 권한을 얻습니다.

PID 를 구했으면 모드(INJECTION / EJECTION)에 따라 InjectDll() / EjectDll() 함수를 호출합니다.

한가지 특이한 점은 PID 값이 100 보다 작다면, 그 프로세스는 작업하지 않고 그냥 통과합니다. 그 이유는 시스템의 안정성을 위해서 시스템 프로세스(PID = 0, 4, 8, … ) 에게는 DLL Injection 하지 않는 것입니다. (이런 PID 값은 Windows XP 에 대해서 경험적으로 얻은 값으로써 다른 Windows 버전에서는 시스템 프로세스의 PID 값이 달라 질 수 있습니다.)



stealth.dll



# SetProcName()

먼저 export 함수인 SetProcName() 을 살펴보겠습니다.

// global variable (in sharing memory)
#pragma comment(linker, "/SECTION:.SHARE,RWS")
#pragma data_seg(".SHARE")
    char g_szProcName[MAX_PATH] = {0,};
#pragma data_seg()

// export function
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void SetProcName(LPCTSTR szProcName)
{
    strcpy_s(g_szProcName, szProcName);
}
#ifdef __cplusplus
}
#endif

위 코드를 보시면 “.SHARE” 이름의 공유 메모리 섹션을 만들고 g_szProcName 버퍼를 생성합니다. 그리고 export 함수인 SetProcName() 에 의해서 은폐 시키고 싶은 프로세스 이름을 g_szProcName 에 저장시킵니다. (SetProcName() 함수는 HookProc.exe 에서 호출됩니다.)

* 참고
g_szProcName 버퍼를 공유 메모리 섹션에 만들면 stealth.dll 이 모든 프로세스에게 인젝션 될 때 간단하게 은폐 프로세스 이름을 공유 시킬 수 있는 장점이 있습니다. (향후 프로그램을 더 발전 시켜서 동적으로 은폐 프로세스를 다른 걸로 변경시킬 수도 있습니다.)



# DllMain()

그럼 DllMain() 함수를 살펴볼까요?

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    char            szCurProc[MAX_PATH] = {0,};
    char            *p = NULL;

    // #1. 예외처리
    // 현재 프로세스가 HookProc.exe 라면 후킹하지 않고 종료
    GetModuleFileName(NULL, szCurProc, MAX_PATH);
    p = strrchr(szCurProc, '\\');
    if( (p != NULL) && !_stricmp(p+1, "HideProc.exe") )
        return TRUE;

    switch( fdwReason )
    {
        // #2. API Hooking
        case DLL_PROCESS_ATTACH :
        hook_by_code("ntdll.dll", "ZwQuerySystemInformation",
                     (PROC)NewZwQuerySystemInformation, g_pOrgBytes);
        break;

        // #3. API Unhooking
        case DLL_PROCESS_DETACH :
        unhook_by_code("ntdll.dll", "ZwQuerySystemInformation",
                       g_pOrgBytes);
        break;
    }

    return TRUE;
}

DllMain() 은 보시다시피 매우 간단합니다.

먼저 문자열 비교를 통해 프로세스 이름이 "HookProc.exe" 라면 API 후킹하지 않도록 예외처리를 해둡니다. 모든 프로세스를 정확히 검색하기 위해서 HookProc.exe 자신은 후킹 당하면 안되겠죠? (후킹 당하면 HookProc.exe 자신도 은폐 프로세스를 볼 수 없고, stealth.dll 을 인젝션 시킬 수 없기 때문입니다.)

그리고 DLL_PROCESS_ATTACH 이벤트에 hook_by_code() 함수로 API 를 후킹하고, DLL_PROCESS_DETACH 이벤트에 unhook_by_code() 함수로 API 후킹을 해제 시킵니다.


# hook_by_code()

코드 패치를 이용하여 API 를 후킹하는 hook_by_code() 함수 입니다.

BOOL hook_by_code(LPCTSTR szDllName, LPCTSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
{
    FARPROC pfnOrg;
    DWORD dwOldProtect, dwAddress;
    BYTE pBuf[5] = {0xE9, 0, };
    PBYTE pByte;

    // 후킹대상 API 주소를 구한다
    pfnOrg = (FARPROC)GetProcAddress(GetModuleHandle(szDllName), szFuncName);
    pByte = (PBYTE)pfnOrg;

    // 만약 이미 후킹되어 있다면 return FALSE
    if( pByte[0] == 0xE9 )
        return FALSE;

    // 5 byte 패치를 위하여 메모리에 WRITE 속성 추가
    VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    // 기존코드 (5 byte) 백업
    memcpy(pOrgBytes, pfnOrg, 5);

    // JMP 주소계산 (E9 XXXX)
    // => XXXX = pfnNew - pfnOrg - 5
    dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5;
    memcpy(&pBuf[1], &dwAddress, 4);

    // Hook - 5 byte 패치(JMP XXXX)
    memcpy(pfnOrg, pBuf, 5);

    // 메모리 속성 복원
    VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect);

    return TRUE;
}

hook_by_code() 함수 파라미터 소개입니다.

LPCTSTR szDllName  : [IN] 후킹하려는 API 가 포함된 DLL 파일 이름
LPCTSTR szFuncName : [IN] 후킹하려는 API 이름
PROC pfnNew        : [IN] 사용자가 제공한 후킹함수 주소
PBYTE pOrgBytes    : [OUT] 원본 5 byte 를 저장시킬 버퍼 – 나중에 unhook 에서 사용됨

동작원리에서 설명 드렸듯이 hook_by_code() 함수의 기능은 원본 API 코드 시작부분의 5 byte 를 "JMP XXXX" 명령어로 변경하는 것입니다.

참고 API Hooking – '스텔스' 프로세스 (1)

소스 코드가 간단하여 주석을 보시면 대부분 쉽게 이해가 되는 내용입니다만, 중간의 점프 주소 계산 부분은 리버싱에서 매우 중요한 내용이기 때문에 자세히 살펴보도록 하겠습니다.

Intel x86 (IA-32) Instruction Format 에 따르면 JMP 명령어의 Op Code(Operation Code) 는 'E9' 입니다. 그리고 뒤에 4 byte 값이 이어집니다.

즉, JMP 명령어의 Op Code 는 "E9 XXXX" 형태가 됩니다.

문제는 XXXX 의 값이 JMP 할 절대 주소 값이 아니라, 현재 JMP 명령어 에서부터 JMP 할 위치까지의 상대 거리입니다. 이것은 아래의 공식으로 구할 수 있습니다.

XXXX = 점프할 주소 – 현재 명령어 주소 - 5

마지막에 5 를 더 빼주는 이유는 JMP 명령어 자체 길이 5 byte 를 보정해 주는 것입니다.

예를 들어 402000 주소의 JMP 명령어에서 401000 주소로 가고 싶을 때는 "E9 00104000" 이라고 쓰는 것이 아니고, 위 계산 공식대로 적용해서 XXXX 값을 구해야 합니다.

XXXX = 401000 – 402000 – 5 = FFFFEFFB

따라서 이 JMP 명령어의 Op Code 는 "E9 FBEFFFFF" 입니다.

OllyDbg 의 [Assemble] 또는 [Edit] 기능으로 확인해 보시기 바랍니다.


<Fig. 1>

* 참고 1
JMP 말고 short JMP 명령이 있습니다. 말 그대로 짧은 거리를 점프 할 때 쓰이는 명령어로 Op Code 는 "EB X" 입니다. (명령어 크기 2 byte)
OllyDbg 에서 'EB' 명령어도 테스트 해보시기 바랍니다.

* 참고 2
위와 같이 상대 거리 를 계산해서 JMP 명령어를 써주는 것이 좀 불편해 보일 수 있습니다.
물론 다른 명령어를 써서 절대 주소로 JMP 할 수 도 있습니다.

예1) PUSH + RET 
  68 00104000    PUSH 00401000
  C3             RETN

예2) MOV + JMP
  B8 00104000    MOV EAX, 00401000
  FFE0           JMP EAX


* 참고 3
32bit 주소 계산 하실 때 Windows 계산기는 좀 불편합니다.
다양한 기능이 있는 32bit Calculator v1.7 by cybult 를 추천합니다.

* 향후 x86 Op Code Map 을 해석하는 방법에 대해서 글을 올리도록 하겠습니다.


실제로 hook_by_code() 함수에 의해서 ZwQuerySystemInformation() API 가 후킹 되기 전/후의 모습을 디버거를 통하여 살펴 보도록 하겠습니다. (해당 프로세스는 procexp.exe 입니다.)

먼저 API 후킹 전의 ZwQuerySystemInformation() API 코드 입니다.

<Fig. 2>

ZwQuerySystemInformation() 주소는 7C93D92E 이며, 명령어 코드는 아래와 같습니다.

7C93D92E    B8 AD000000     MOV EAX, 0AD


이제 stealth.dll 이 인젝션 되면서 hook_by_code() 함수에 의해서 API 가 후킹된 이후의 코드입니다.

<Fig. 3>

ZwQuerySystemInformation() 코드 시작 명령어가 아래와 같이 변경 되었습니다. (정확히 5 byte)

7C93D92E    E9 ED376C93     JMP 10001120

10001120 주소는 바로 후킹 함수 NewZwQuerySystemInformation() 의 주소입니다.
그리고 E9 뒤의 4 byte (936C37ED) 값은 바로 위에서 설명 드린 계산 공식에 의해서 구할 수 있습니다. (한번씩 직접 해보시기 바랍니다.)


# unhook_by_code() 

후킹을 해제하는 unhook_by_code() 코드 입니다.

BOOL unhook_by_code(LPCTSTR szDllName, LPCTSTR szFuncName, PBYTE pOrgBytes)
{
    FARPROC pFunc;
    DWORD dwOldProtect;

    // API 주소 구한다
    pFunc = GetProcAddress(GetModuleHandle(szDllName), szFuncName);

    // 원래 코드 (5 byte)를 덮어쓰기 위해 메모리에 WRITE 속성 추가
    VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    // Unhook
    memcpy(pFunc, pOrgBytes, 5);

    // 메모리 속성 복원
    VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

    return TRUE;
}


Unhook 의 동작원리는 원래 코드의 5 byte 를 복원해 주는 것입니다.
코드가 단순하므로 설명은 주석으로 대체 합니다.


# NewZwQuerySystemInformation()

마지막으로 후킹함수 NewZwQuerySystemInformation() 에 대해서 살펴볼 시간입니다.

하지만 그전에 먼저 ntdll!ZwQuerySystemInfomation() API 에 대해서 알아야 합니다.

NTSTATUS WINAPI ZwQuerySystemInformation(
  __in       SYSTEM_INFORMATION_CLASS SystemInformationClass,
  __inout    PVOID SystemInformation,
  __in       ULONG SystemInformationLength,
  __out_opt  PULONG ReturnLength
);

typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    BYTE Reserved1[48];
    PVOID Reserved2[3];
    HANDLE UniqueProcessId;
    PVOID Reserved3;
    ULONG HandleCount;
    BYTE Reserved4[4];
    PVOID Reserved5[11];
    SIZE_T PeakPagefileUsage;
    SIZE_T PrivatePageCount;
    LARGE_INTEGER Reserved6[6];
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;

* 출처 : http://msdn.microsoft.com/en-us/library/ms725506(VS.85).aspx

간단히 설명해서 SystemInformationClass 파라미터에 SystemProcessInformation (5) 으로 세팅하고 ZwQuerySystemInformation() API 를 호출하면 SystemInformation [inout] 파라미터에 SYSTEM_PROCESS_INFORMATION 구조체 단방향 연결 리스트(single linked list)의 시작 주소가 얻어집니다.

바로 이 구조체 연결 리스트에 실행중인 모든 프로세스들의 정보가 담겨 있습니다.

따라서 프로세스 은폐를 구현하려면 은폐하고 싶은 프로세스에 해당하는 리스트 멤버를 찾아서 리스트 연결을 끊어 버리면 됩니다.

아래 NewZwQuerySystemInformation() 코드를 살펴 보면서 실제로 어떤식으로 구현되었는지 알아보겠습니다.

NTSTATUS WINAPI NewZwQuerySystemInformation(
                SYSTEM_INFORMATION_CLASS SystemInformationClass,
                PVOID SystemInformation,
                ULONG SystemInformationLength,
                PULONG ReturnLength)
{
    NTSTATUS status;
    FARPROC pFunc;
    PSYSTEM_PROCESS_INFORMATION pCur, pPrev;
    char szProcName[MAX_PATH] = {0,};

    // 작업 전에 unhook
    unhook_by_code("ntdll.dll", "ZwQuerySystemInformation", g_pOrgBytes);

    // original API 호출
    pFunc = GetProcAddress(GetModuleHandle("ntdll.dll"),
                           "ZwQuerySystemInformation");
    status = ((PFZWQUERYSYSTEMINFORMATION)pFunc)
              (SystemInformationClass, SystemInformation,
              SystemInformationLength, ReturnLength);

    if( status != STATUS_SUCCESS )
        goto __NTQUERYSYSTEMINFORMATION_END;

    // SystemProcessInformation 인 경우만 작업함
    if( SystemInformationClass == SystemProcessInformation )
    {
        // SYSTEM_PROCESS_INFORMATION 타입 캐스팅
        // pCur 는 single linked list 의 head

        pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;

        while(TRUE)
        {
            // wide character => multi byte 변환
            WideCharToMultiByte(CP_ACP, 0, (PWSTR)pCur->Reserved2[1],
                                -1, szProcName, MAX_PATH, NULL, NULL);

            // 프로세스 이름 비교
            // g_szProcName = 은폐 하려는 프로세스 이름
            // (=> SetProcName() 에서 세팅됨)

            if(!_strcmpi(szProcName, g_szProcName))
            {
                // 연결 리스트에서 은폐 프로세스 제거
                if(pCur->NextEntryOffset == 0)
                    pPrev->NextEntryOffset = 0;
                else
                    pPrev->NextEntryOffset += pCur->NextEntryOffset;
            }
            else  
                pPrev = pCur;

            if(pCur->NextEntryOffset == 0)
                break;

            // 연결 리스트의 다음 항목
            pCur = (PSYSTEM_PROCESS_INFORMATION)
                    ((ULONG)pCur + pCur->NextEntryOffset);
        }
    }

__NTQUERYSYSTEMINFORMATION_END:

    // 함수 종료전에 다시 API Hooking
    hook_by_code("ntdll.dll", "ZwQuerySystemInformation",
                 (PROC)NewZwQuerySystemInformation, g_pOrgBytes);

    return status;
}

위 NewZwQuerySystemInformation() 함수의 구조를 간단히 설명 드리면 아래와 같습니다.

- 언훅(unhook) ZwQuerySystemInformation()
- ZwQuerySystemInformation() 호출
- SYSTEM_PROCESS_INFORMATION 구조체 연결 리스트를 검사하면서 은폐 프로세스 찾음
- 은폐 프로세스를 찾으면 리스트에서 제거
- 훅(hook) ZwQuerySystemInformation()


NewZwQuerySystemInformation() 코드의 중간쯤 while() 문을 보시면 SYSTEM_PROCESS_INFORMATION 구조체 연결 리스트를 검사하면서 프로세스 이름(pCur->Reserved2[1])을 비교합니다. (프로세스 이름은 Unicode 문자열이기 때문에 간단히 ASCII 로 변경 후 strcmp_i() 함수를 사용했습니다.)

...

// wide character => multi byte 변환
WideCharToMultiByte(CP_ACP, 0, (PWSTR)pCur->Reserved2[1],
-1, szProcName, MAX_PATH, NULL, NULL);

// 프로세스 이름 비교
// g_szProcName = 은폐 하려는 프로세스 이름
// (=> SetProcName() 에서 세팅됨)

if(!_strcmpi(szProcName, g_szProcName))
{
...

함수의 동작 원리만 이해하시면 (주석과 함께) 코드를 보시는데 어려움이 없으실 걸로 생각합니다.


다음 번에는 Global API Hooking 에 대해서 배워보도록 하겠습니다.

API Hooking – '스텔스' 프로세스 (4)


ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

Trackback Address :: http://www.reversecore.com/trackback/67 관련글 쓰기

  1. reversecore 2009/12/31 00:08 댓글주소 | 수정 | 삭제 | 댓글

    새해 복 많이 받으세요 ^^*

  2. 쵸밥 2009/12/31 06:22 댓글주소 | 수정 | 삭제 | 댓글

    코어님도 복 많이받으세요 ^^*

  3. Vice 2009/12/31 08:59 댓글주소 | 수정 | 삭제 | 댓글

    잘 읽고 갑니다 ^^
    코어님도 새해복 많이 받으세요~

  4. aaaa 2010/01/04 00:27 댓글주소 | 수정 | 삭제 | 댓글

    항상 눈팅만 했지 글은 처음남기네요.^^ 정말 좋은 정보 감사합니다.
    새해 복 많이 받으세요 ^^

  5. 뭉이 2010/01/04 17:59 댓글주소 | 수정 | 삭제 | 댓글

    좋은글 잘 읽었습니다. 코드 인라인패치 후킹이 이런거였구요..^^
    제가 아직 후킹에 대해서 지식이 짧습니다..그런데 궁금한 점이 있습니다.

    Zw나 Nt로 시작하는 커널 API를 전역 후킹하는데 SSDT후킹과 어떤 방법이 좋을까요?.
    SSDT후킹은 몇번 해봤습니다.
    코드패치해서 후킹하는 방법은 특정프로세스만 후킹할때 적당해 보이네요..
    원격지에 스레드를 생성하는데 모든 프로세스를 검색해서 각각 dll를 모두 일일이 인젝션한다는게.
    CPU부하가 크게 느껴집니다.

    • ReverseCore 2010/01/04 23:38 댓글주소 | 수정 | 삭제

      뭉이님, 안녕하세요.

      제 생각에는 둘 다 쓰기 편한 방법이라서 작업 용도에 따라서 적절히 골라 사용하시면 될 것 같습니다.

      DLL Injection 의 CPU 부하는 DLL 의 기능에 따라 좀 차이가 나겠지요? ^^

  6. 늅늅 2010/01/10 08:29 댓글주소 | 수정 | 삭제 | 댓글

    오랫만에 들려보네요~
    개인적으로 다른일을 하느라 요즘들어 이분야에 소홀해졌네요 ㅠ.ㅠ
    주된 밥벌이 수단이 다른걸로 바뀌다보니 여지껏 알아온 지식들에 대해 소홀해지고, 그러다보니 정말 순식간에 까먹게 되네요 ㅡㅡ;;

    • reversecore 2010/01/11 20:04 댓글주소 | 수정 | 삭제

      늅늅님, 안녕하세요.

      업무 분야가 바뀌셨나보군요.
      나중에라도 리버싱이 필요해 질때 잊지 말고 들러 주세요~ ^^

      감사합니다.

  7. reversecore 2010/01/13 20:16 댓글주소 | 수정 | 삭제 | 댓글

     

  8. mAn1aS 2010/01/15 15:53 댓글주소 | 수정 | 삭제 | 댓글

    드뎌 3탄이 나왔군요 좋은 글 보고 갑니다. 하나 궁금한 게 있는데요.

    dll로 삽입해서 메모리를 바꿀때 memcpy를 써야 하나요? WriteProcessMemory 써야하나요?
    코드 영역과 데이터 영역에 따라 맞는걸로 써야 할까요?
    VirtualProtect도 역시 코드 영역과 데이터 영역 모두 쓰기 권한을 꼭 주고, 값을 써야 하는지 궁금하네요^^

    참 제 홈피에 그때 문의 드렷던 rand 후킹관련해서 테스트 해보고 올리긴 했는데요.
    왜 전 IAT에 rand가 존재 하지 않을까요.

    새해 복 많이 받으세요^^
    - 새해 힘찬 마음으로 시작하고 있는데, 야근에 심신이 다시 지치고 있는 1인 다녀갑니다.

    • reversecore 2010/01/17 02:46 댓글주소 | 수정 | 삭제

      mAn1aS님, 안녕하세요.

      memcpy 는 현재 프로세스 메모리에만 사용할 수 있고, WriteProcessMemory 는 다른 프로세스 메모리에도 사용할 수 있습니다.

      물론 두 경우 모두 메모리에 "쓰기" 속성이 없다면VirtualProtect 를 이용해서 "쓰기" 속성으로 바꿔놓아야 하지요.

      mAn1aS님도 새해 복 많이 받으세요~

      감사합니다.

  9. 와우:) 2010/03/05 14:51 댓글주소 | 수정 | 삭제 | 댓글

    와우 ~ 덕분에 좋은 정보 많이 얻어갑니다 :)

  10. Ezbeat 2010/04/22 17:01 댓글주소 | 수정 | 삭제 | 댓글

    질문 하나 드려보겠습니다 ^^;

    소스를 보면 JMP해서 우리가 만든 함수로 점프를 하게 해주는데 그 때 함수 주소 구할때
    만든 함수 주소 - 오리지널 함수 주소 - 5
    이렇게 해주셨던데;; 왜 위 과정을 거쳐야만 제가 만든 함수 주소가 나오는 것일까요??;;

    • reversecore 2010/04/23 23:04 댓글주소 | 수정 | 삭제

      Ezbeat님, 안녕하세요.

      FAR JMP 명령어는 E9 XXXX 의 instruction 으로 되어있으며, 저 XXXX 가 의미하는 내용은 현재 주소로부터 점프하려는 주소와의 상대거리 입니다.

      또한 -5 가 의미하는 내용은 FAR JMP 명령어 길이(5 바이트)를 보정해 주는 것이지요.

      그냥 편하게 Intel 개발자들이 명령어를 설계할때 그렇게 설계했다고 이해하고 외워버리세요~ ^^

  11. Ezbeat 2010/04/24 17:25 댓글주소 | 수정 | 삭제 | 댓글

    아~;; 소스코드 수정해보면서 OPCODE봐보니..정말 상대거리군요.. 지금까지 왜 몰랐을까요..-_-;;ㅋ
    아무튼 하나하나 알아가니 재미있군요..ㅋㅋ 이제 reversecore님이 쓰신 모든 코드는 이해가 끝났습니다 ㅋㅋ ㅜ ㅜ

    여기서 DLL인젝션과 API후킹을 공부해서 어디서 응용해보지 하다가..ㅋ 올디에서 제 프로그램 Attach를 막아보자라는 생각으로
    DebugActiveProcess함수 후킹을 해서 성공했습니다 ;;
    뭐.. 구현은.. 올디가 Attach하려는 PID값과 제 프로세스 PID값 비교해서 같으면 PID값을 임의적으로 바꿔버리는 방법을 썼어요 ㅜㅜㅋ 화이팅입니다!! ㅜㅜ 글로벌 후킹은 또 잘 안되는군요..
    다시 해봐야겠습니다 흐엉~ 오늘은 토요일..

    • reversecore 2010/04/25 22:57 댓글주소 | 수정 | 삭제

      Ezbeat님, 안녕하세요.

      와~ 응용까지 성공하셨군요...

      리버싱에 대해 하나하나 배워가시고 재미까지 느끼셨다니...
      대단하세요~ 아무리 선생이 우수해도 배우는 사람이 노력하지 않으면 전혀 성과가 없기 마련인데, 열심히 하셨기 때문에 좋은 성과가 나타난것 같습니다.

      제가 이 블로그를 운영하는 목표가 Ezbeat 님덕에 이루어 졌군요.
      드디어 성공했습니다. ^^

      감사합니다. 화이팅~ ^^

  12. Somebody 2010/07/22 10:53 댓글주소 | 수정 | 삭제 | 댓글

    질문 있습니다.

    kernel32.dll이 export 하고 있는 API함수를 후킹하여 시작 주소를 수정하는 것은
    어느 프로세스에서 실행되든 프로세스가 영향을 받는지 궁금합니다.

    DLL의 경우 한 번만 메모리에 로딩되고 나머지 프로세스들은 그 DLL을 매핑하는 방식이라고 들어서 API함수가 후킹될 경우 모든 프로세스에 영향을 미칠 것 같은데 제 생각이 맞나요?

    만약 맞다고 한다면 이 함수를 후킹하고 있는 프로세스에서는 NewZwQuerySystemInformation 함수의 주소가 있으니 제대로 동작하겠지만, 다른 프로세스에서는 주소가 틀려지니 NewZwQuerySystemInformation 함수를 DLL Injection 방법으로 삽입한 다음에 사용해야 하는 건가요?

    • newbie 2010/07/22 18:53 댓글주소 | 수정 | 삭제

      안녕하세요 :)
      PE파일들은 CopyOnWrite방식을 사용하고 있습니다.
      즉 dll들이 메모리에 올라와있으면 모든 프로세스가 그것을 공유하지만
      한 프로세스가 그걸 수정해버리면 수정한 부분만 그 프로세스의 고유메모리 영역에 복사해줍니다.
      그럼 수정한 부분은 더 이상 공유되는 부분이 아닌 특정 프로세스의 영역이 되고 앞서 같은
      dll을 공유하고 있던 프로세스들은 아무런 영향을 받지 않게 됩니다.
      이해 되지 않는 부분이 있으면 reversecore님께서 좀더 자세한 설명을 해주시리라 생각됩니다 :)

    • reversecore 2010/07/28 16:55 댓글주소 | 수정 | 삭제

      ^^ newbie 님의 정확하고 친절한 답변 너무 감사합니다.

      명쾌하게 설명하셔서 저도 같이 배웠습니다.

      감사합니다. ^^

  13. 토터스 2010/11/17 21:03 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요? 좋은 정보 감사 드립니다.
    hook_by_code부분이 64bit에서 제대로 동작하기 위해서는 아무래도 점프 주소 계산 부분이 변경되어야 할 듯 싶은데, 어떻게 바꾸어야 할지 전혀 감이 없네요.

    혹시 관련 정보, 책등이 있으시면 알려주시면 너무 감사 하겠습니다.
    종종 들리겠습니다. 감사 합니다. ^^

    • reversecore 2010/11/18 16:30 댓글주소 | 수정 | 삭제

      안녕하세요.

      64bit Native Mode 에서 API 후킹을 말씀하시는거죠?

      확장된 주소체계에서 CALL rel32 형태의 명령어 패치로는 원하는 모듈 주소로 갈 수 없는게 사실입니다. FF 로 시작하는 CALL r/m64 형태의 명령어를 써야 하지요.

      아직 직접 관련 자료를 찾아보지는 않았습니다만... x64 가 꽤 보급된만큼 어느정도 자료는 있을꺼라 생각됩니다. ^^

      감사합니다.

  14. 안녕하세요 2011/01/20 04:20 댓글주소 | 수정 | 삭제 | 댓글

    지금만들고 있는 프로그램에서 후킹을 사용해야하는 부분이 있어서 블로그를 참조하고 있습니다
    그런데 코드를 보다가 궁금한점이 생겨서 몇가지만 질문하겠습니다
    코드를 보다보면 PROC, FARPROC 이런게 보이는데 선언을 찾아보니
    typedef int (_stdcall *PROC)()
    typedef int (_stdcall *FARPROC)()
    이렇게 되있더라구요
    이 둘의 차이점이 무엇인가요? 똑같은 선언임에도 불구하고 두가지로 선언되어있는 이유가 호환성 때문인거 같은데, 각각 어떤용도로 쓰이는 것인지 모르겠습니다 ㅠ
    그리고 ZwQuerySystemInformation를 가리키는 함수포인터가 FARPROC형으로 선언이되있던데 ZwQuerySystemInformation의 리턴형은 NTSTATUS이고 FARPROC의 리턴형은 int형인데 FARPROC으로 선언해준이유가 NTSTATUS 구조체를 가리키는 주소값이 어차피 int형이기 때문인가요?
    또, typedef int (_stdcall *PROC)() 이런 선언같은 경우는 파라메터의 종류는 상관없이 모든 함수를 전부 가리킬수 있는건가요? 예를들어
    int a(void);
    int b(int);
    int c(char,int,long);
    PROC d=a;
    PROC e=b;
    PROC f=c;
    이렇게해도 전부 괜찮은건가요?

    • reversecore 2011/04/13 00:27 댓글주소 | 수정 | 삭제

      안녕하세요.

      C언어 문법에 관련된 질문을 해주셨네요~

      FAR 혹은 L(LONG) 등의 접두어는 의미상 편하게 구분하기 위해서 정의하는 듯 합니다. 당연히 Windows API 설계자들이 정의한 것이겠구요... 나름대로 규칙을 만든 것이라고 봐야겠죠.

      C 언어 타입 캐스팅은 컴파일러 문법체크를 통과하기 위한 기교라고 할 수 있고요... 어셈으로 보면 자료형의 크기만 중요시 합니다. 포인터는 타입에 상관없이 무조건 4 byte (32bit 의 경우) 이런 식으로요.

      그리고 올려주신 코드로 해도 되는데요. VC++ 버전에 따라서 에러를 낼 소지가 있으므로 PROC d = (PROC)a; 식으로 해주시면 좋겠지요.

      감사합니다.

  15. 뤼챠드 2011/03/23 23:01 댓글주소 | 수정 | 삭제 | 댓글

    win32API( )를 myAPI( )로 hooking을 한 상태에서 win32API()을 호출해준다고 가정할때 해당 win32API( )를 unhook을 하고 호출하는 방식을 제시하셨는데요. 이경우에대한 문제점도 지적하셨고, 매번 myAPI함수 내에서 unhook() hook()을 호출해주는 코드는 좀 거추장 스럽다는 느낌이 들더라구요.
    그래서 곰곰히 생각을 해보니 해당 win32API( )의 code block을 통째로 어딘가에 copy를 해두었다가 호출하는 방식이면 어떨까요? 예상되는 문제는 win32API()의 code block의 크기과 주소가 바뀌면서 생기는 side-effect인데요. 일단 test를 해보려 하니 win32API( )의 code block의 크기를 구하는 방법을 잘 모르겠더군요.
    혹시 아이디어 있으십니까?

    • 뤼챠드 2011/03/25 14:22 댓글주소 | 수정 | 삭제

      훔 여기 저기 뒤져보니 실행 code block을 copy하는 방식은 아니고, 일종의 wrapper 함수를 만들어 그 안에서 my api를 호출하는 방식으로 사용하는 것 같군요. wrapper안에서 hook() unhook()을 내부적으로 처리하고 sharing 문제도 CRITICAL_SECTION을 써서 해결하면 되더라군요.
      그런데 myAPI()를 호출하기 전에 각종 parameter와 return 처리를 하기 위해 asm으로 잔뜩 먼가를 하는데 잘 이해가 안되는 군요.
      API마다 parameter가 다른데 이를 하나의 wrapper로 해결하려니 먼가 꽁수를 쓰는 것 같습니다. 필요하시면 우연히 얻는 sample을 공유해드리면 해독하는 것을 도와주시면 어떠실까요 ^^

    • reversecore 2011/04/13 00:47 댓글주소 | 수정 | 삭제

      안녕하세요.

      제가 소개한 방법(5 byte 패치)은 말그대로 일반적인 상황에서 잘 돌 수 있는 방법입니다.

      저거 외에 7 byte 패치 방법이 있는데요, 후킹 함수내에서 hook()/unhook() 을 할 필요가 없습니다. 그런데 모든 API 에서 가능한 것은 아닙니다. 특히 ntdll 에서 서비스하는 native API 들은 7 byte 후킹이 불가능 하지요.

      말씀하신 대로 API 코드를 통째로 copy 해두는 방법도 있습니다만, relocation 이슈를 해결해야 하는 상황이 발생합니다. (근데 이방법은 ntdll 의 native API 들한테는 잘 먹힙니다. 코드가 매우 단순하거든요.)

      즉, 범용적인 간단한 방법이 바로 위에서 소개한 5 byte 패치 방법이지요. ^^

      감사합니다.

  16. BM 2011/06/02 10:08 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요, 항상 올려주시는 글을 감사히 보며 배우는 초보입니다.

    질문이 두 가지 있는데요..

    hook_by_code와 unhook_by_code 에서 후킹할 API를 주소를 매번 구하는데요.. 이 과정을 처음에만 해주어서 얻은 주소를 계속 사용해도 문제가 없겠죠?? 괜찮을 것 같지만 혹시 제가 모르는 예외가 있을까 싶어서 여쭤봅니다.

    다른 질문은 바로 위 댓글과 관련된 내용인데요.. 글에서 소개하신 5 byte 패치 방법은 일반적인 상황에서 잘 돌 수 있는 방법이라고 하셨는데.. 이 방법이 안먹힐 경우는 어떤 경우인지 알 수 있을까요??

    또 그럴 경우에는 어떤 방법을 써서 후킹할 수 있는지 간단하게나마 알려주시면 감사하겠습니다. :)

    • reversecore 2011/06/03 21:17 댓글주소 | 수정 | 삭제

      안녕하세요.

      정확한 API 주소를 구하셨다면 저장해 놓고 사용하셔도 무방합니다.

      5 byte LONG JUMP 패치 방법은 범용적인 (잘 동작하는) 방법입니다. Win32 API 를 후킹할 때 이 방법이 안먹히는 경우는 아직 본 적이 없습니다. ^^

      2 byte SHORT JUMP 패치 방법도 있는데요. 점프 위치가 현재 기준 -128 ~ 128 으로 제한됩니다. 만약 Win32 API 라면 Kernel32.dll 등의 라이브러리 영역 내부입니다. 근처에 빈 영역이 거의 없기 때문에 의미있는 후킹 코드를 써놓기 어렵지요.

      7 byte 패치는 바로 2 byte SHORT JUMP 이후에 바로 5 byte LONG JUMP 를 하는 것입니다. kernel32.dll 의 코드를 잘 보시면 함수와 함수 사이에 최소 7 byte 의 빈 영역(NOP 또는 MOV EDI, EDI 명령어)을 보실 수 있습니다. 그곳에 7 byte 점프 코드를 설치하는 것이죠.

      감사합니다.

  17. 디버거 2011/10/13 11:55 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요.
    좋은 글 감사합니다.

    한가지 질문이 있는데요...

    VS6.0 에서 컴파일 하려고 하는데 NTSTATUS, SYSTEM_INFORMATION_CLASS 등이 문제네요.
    이런것 들은 도대체 어디에 있는 건지 알 수가 없는데 좀 가르쳐 주세요.

    행복하세요.

  18. jjengage_father CPP 2011/10/11 20:33 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요.
    디버거님, 답변 궁금하시갔는데요.
    NTSTATUS는 DDK를 태우면 되시는데요 VS2008,VS2010을 태우시고
    쓰시면 되세요.
    안녕하세요.

  19. 팔극진 2011/10/19 15:50 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 ^^
    덕분에 좋은 지식 많이 얻어가고 있습니다.
    실력이 부족해서 구글링이나 웹사이트 뒤지면서 공부 하는데요~
    아래 부분은 도저히 이해가 가지 않아서 질문 좀 드립니다.

    숨길프로세스 정보를 찾아서 비교한 다음 같으면 숨기는거 같은데요~
    !_strcmpi 하면은 같지 않으면 아래로 이동 하는것 같은데요~

    그리고, pCur->NextEntryOffset == 0 는 연결리스트 마지막인거 같은데요~
    아래 소스가 이해가 되지 않습니다. ㅜㅠ
    조언 좀 부탁드립니다. 수고하세요~

    if(!_strcmpi(szProcName, g_szProcName))
    {
    // 연결 리스트에서 은폐 프로세스 제거
    if(pCur->NextEntryOffset == 0)
    pPrev->NextEntryOffset = 0;
    else
    pPrev->NextEntryOffset += pCur->NextEntryOffset;
    }
    else
    pPrev = pCur;

    • reversecore 2011/10/28 00:41 댓글주소 | 수정 | 삭제

      안녕하세요.

      if(!_strcmpi(szProcName, g_szProcName)) 의 의미는 두 문자열이 같으면~ 이라는 뜻이구요.

      pCur->NextEntryOffset == 0 는 연결리스트의 마지막이 맞습니다. 현재 프로세스가 프로세스 연결 리스트의 마지막이라면 pPrev->NextEntryOffset = 0; 명령으로 앞의 프로세스를 연결리스트 마지막으로 설정하란 뜻입니다.

      감사합니다.

  20. 팔극진 2011/10/31 14:51 댓글주소 | 수정 | 삭제 | 댓글

    친절하신 답변 정말 감사드립니다. ^^
    수고하세요~!

  21. Midkiz 2011/11/07 11:00 댓글주소 | 수정 | 삭제 | 댓글

    ㅎㅎ 이거 꿀같은 자료입니다... 공부열심히 해서 뒤따라 가도록 하겠습니다

  22. gunny 2012/02/08 22:26 댓글주소 | 수정 | 삭제 | 댓글

    블로그 아주 잘보고있습니다!

    하나 의문점이있어서 댓글씁니다

    if(!_strcmpi(szProcName, g_szProcName))
    {
    // 연결 리스트에서 은폐 프로세스 제거
    if(pCur->NextEntryOffset == 0)
    pPrev->NextEntryOffset = 0;
    else
    pPrev->NextEntryOffset += pCur->NextEntryOffset;
    }

    위 코드에서

    pPrev->NextEntryOffset += pCur->NextEntryOffset;

    이것을

    pPrev->NextEntryOffset = pCur->NextEntryOffset;

    이렇게 해서 pPrev 의 다음리스트의 주소를 pCur의 다음 리스트의 주소로 바꿔야하지않을까요?
    현재 링크스리스트를 건너뛰는거라면...

    제가 착각하고있는건가요?ㅎㅎ
    어쨋든 제 프로젝트에 후킹기법이 많은 도움이되었습니다 감사합니다!