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

어셈블리 언어를 이용한 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 관련글 쓰기


abex' crackme #1

analysis 2009/02/28 22:54

* 출처 : http://www.simples.co.kr/RCEZone



Level


초급



abstract


abex’s crackme #1 샘플을 크랙합니다.



Goal

아주 간단한 crackme 샘플을 분석하여 디버거와 디스어셈 코드에 익숙해집니다.
단순한 크랙이 목표가 아니라 디스어셈 코드와 디버깅에 익숙해지는 것이 목표입니다.



Prologue

crackme 라는 프로그램은 말 그대로 crack 연습 목적으로 작성되어 공개된 프로그램입니다.
리버싱을 처음 시작할때 간단한 crackme 를 분석해보면 실제로 디버거와 디스어셈 코드에 익숙해지는 효과가 있습니다.

abex' crackme 는 매우 간단해서 초보자에게 알맞고, 아주 유명하기 때문에 해외 사이트뿐만 아니라
국내에도 abex' crackme 를 설명한 사이트가 많이 있습니다.

자신이 크랙한 방법과 남들이 했던 방법을 비교할 수 있어서 좋겠죠?

* 상용 프로그램(웹하드, 게임, 기타)을 크랙해달라는 요청은 사양합니다.



VirusTotal 검사

만약 다운 받은 crackme 파일이 바이러스라면 곤란하겠죠?
먼저 www.virustotal.com 으로 검사해봅니다.

새로운 파일을 분석하기 전에 VirusTotal 을 이용하는 것은 좋은 습관입니다.


<Fig. 1>

파일에 별 문제는 없어 보이는군요.



abex' crackme #1

디버깅을 시작하기 전에 먼저 파일을 실행시켜서 어떤 프로그램인지 살펴봅니다.


<Fig. 2>

위 그림과 같이“Make me think your HD is a CD-Rom.” 메시지 박스가 출력됩니다.
처음에 전 이 영어 문장이 이해가지 않았습니다. @@~

뒤의 CD-Rom 단어를 보고 HD 가 HDD(하드 디스크 드라이브)를 의미 한다는걸 간신히 추측했지요.
선택의 여지가 없으니 일단 [확인]을 눌러봅니다.


<Fig. 3>

다시 'Error' 메시지 박스가 출력되면서 종료되는군요.

아직 abex 가 원하는게 뭔지 (뭘 어떻게 크랙하라는 건지) 정확히 이해할 수 없네요.
코드를 직접 보고 원하는 걸 파악해야 겠습니다.

* crackme 샘플은 보통 serial key 를 맞추는 것이 대부분인데 abex #1 은 조금 특이합니다.



Start debugging

OllyDbg 를 실행시켜 파일의 디스어셈 코드를 살펴봅니다.


<Fig. 4>

EP 코드가 매우 짧습니다.
지난번에 분석한 "HelloWorld.exe" 와는 너무나 차이가 나네요.

이것은 바로 Assembly 로 만들어진 실행 파일이기 때문에 그렇습니다.

VC++, VC, Delphi 등의 개발툴을 사용하면 자신이 작성한 소스코드외에
컴파일러가 stub code 를 추가시키기 때문에 디스어셈을 하면 복잡하게 보입니다.

하지만 Assembly 언어로 작성하면 어셈 소스가 곧 디스어셈 코드가 됩니다.
군더더기 없는 직관적인 코드(EP 에 main 함수가 바로 나타남)가 바로 Assembly 언어로 개발했다는 증거입니다.



코드 분석

코드가 매우 짧으므로 눈으로 슥~ 훑어 보겠습니다.

<Fig. 4> 에서 코멘트 부분의 붉은색으로 표시된 Win32 API 함수위주로 살펴봅니다.

MessageBox(“Make me think your HD is a CD-Rom.”)
GetDriveType(“C:\\”)
...
MessageBox(“Nah... This is not a CD-ROM Drive!”)
MessageBox(“OK, I really think that your HD is a CD-ROM! :p”)
ExitProcess()

 
Windows 프로그래밍을 해보셨으면 위 함수들의 의미를 잘 아실겁니다.

코드를 보니 이제 제작자의 의도가 명확하게 이해됩니다.

GetDriveType() 함수로 C 드라이브의 타입을 얻어오는데 (당연히 HDD 타입이 리턴되겠죠.)
이걸 조작해서 CD-ROM 타입으로 인식하도록 만들어서
위에 있는 파란 글씨로 표시된 메시지 박스가 출력되도록 하면 되는거였습니다.

Crackme 첫시간이므로 라인별 상세분석을 해보겠습니다.

00401000    PUSH 0                 ; Style = MB_OK|MB_APPLMODAL
00401002    PUSH 402000            ; Title = "abex' 1st crackme"
00401007    PUSH 402012            ; Text = "Make me think your HD is a CD-Rom."
0040100C    PUSH 0                 ; hOwner = NULL
0040100E    CALL 00401061          ; MessageBoxA
                                   ; => 함수내부에서 ESI = FFFFFFFF 로 세팅됩니다.

00401013    PUSH 402094            ; RootPathName = "c:\\"

00401018    CALL 00401055          ; GetDriveTypeA
                                   ; => 리턴값(EAX)은 3 입니다.

0040101D    INC ESI                ; => ESI = 0

0040101E    DEC EAX                ; => EAX = 2
0040101F    JMP SHORT 00401021     ; => 의미 없는 JMP 명령 (garbage code)
00401021    INC ESI                ; => ESI = 1
00401022    INC ESI                ; => ESI = 2
00401023    DEC EAX                ; => EAX = 1

00401024    CMP EAX,ESI            ; => EAX(1) 와 ESI(2) 비교

00401026    JE SHORT 0040103D      ; => JE(Jump if Equal) 조건 분기 명령
                                   ; => 두 값이 같으면 40103D 로 점프, 틀리면 그냥 밑으로 진행
                                   ; => 40103D 주소는 제작자가 원하는 메시지 박스 출력 코드

00401028    PUSH 0                 ; Style = MB_OK|MB_APPLMODAL

0040102A    PUSH 402035            ; Title = "Error"
0040102F    PUSH 40203B            ; Text = "Nah... This is not a CD-ROM Drive!"
00401034    PUSH 0                 ; hOwner = NULL
00401036    CALL 00401061          ; MessageBoxA
0040103B    JMP SHORT 00401050

0040103D    PUSH 0                 ; Style = MB_OK|MB_APPLMODAL

0040103F    PUSH 40205E            ; Title = "YEAH!"
00401044    PUSH 402064            ; Text = "Ok, I really think that your HD is a CD-ROM! :p"
00401049    PUSH 0                 ; hOwner = NULL
0040104B    CALL 00401061          ; MessageBoxA

00401050    CALL 0040105B          ; ExitProcess
                                   ; => 프로세스 종료
                                   ; => 파라미터를 넘기지 않았음. (현재 스택에 있는 값을 리턴함.)

위 코드에서 사용된 어셈블리 명령어는 별로 어렵지 않기 때문에
제가 파란색으로 표시한 comment 만 보셔도 쉽게 이해할 수 있으실 겁니다.

* 위 코드에서 사용된 어셈블리 명령어 설명

PUSH : 스택에 값을 입력
CALL : 지정된 주소의 함수를 호출
INC : 값을 1 증가 (C 언어의 ++ 연산자)
DEC : 값을 1 감소 (C 언어의 -- 연산자)
JMP : 지정된 주소로 점프 (C 언어의 goto 명령)
CMP : 주어진 두 개의 operand 비교
          SUB 명령어와 동일하나 operand 값이 변경되지 않고 EFLAGS 레지스터만 변경됨
          (두 operand 의 값이 동일하다면 SUB 결과는 0 → ZF = 1 로 세팅됨)
JE : 조건 분기 (Jump if equal)
       ZF = 1 이면 점프



패치

단순 패치가 목적이라면 401026 주소 명령어(JE SHORT 0040103D)를
'Assemble' 기능 [Space] 으로 JMP 0040103D 로 변경합니다.

즉, 조건 분기(JE) 명령어를 점프(JMP) 명령어로 바꾸는 것입니다.
(CMP 명령어의 결과에 상관없이 무조건 점프해버립니다.)

파일로 만들고 싶을땐 'Copy to executable' 메뉴를 사용하시면 됩니다.
(OllyDbg 기능 설명은 'HelloWorld.exe' 참고)



스택에 파라미터 전달 방법

위 코드에서 눈여겨 보아야 할 것은 함수 호출 시 스택에 파라미터를 전달하는 방법입니다.

아래에 00401000 ~ 0040100E 주소 사이의 명령어를 보시면 MessageBoxA() 함수를 호출하기 전에
4 번의 PUSH 명령어를 사용하여 필요한 parameter 를 역순으로 입력하고 있습니다.

00401000    PUSH 0                 ; Style = MB_OK|MB_APPLMODAL
00401002    PUSH 402000            ; Title = "abex' 1st crackme"
00401007    PUSH 402012            ; Text = "Make me think your HD is a CD-Rom."
0040100C    PUSH 0                 ; hOwner = NULL
0040100E    CALL 00401061          ; MessageBoxA


위 코드를 C 언어로 구현하면 아래와 같습니다.

MessageBox(NULL, "Make me think your HD is a CD-Rom.", "abex' 1st crackme", MB_OK|MB_APPLMODAL);

실제 C 언어 소스 코드에서 함수에 넘기는 parameter 의 순서가
Assembly 언어에서는 역순으로 넘어간다는 것을 알고 계시면 됩니다.

그런데 왜 역순으로 입력할까요 ?

이 내용을 이해하시려면 스택 메모리 구조(FILO:First In Last Out)를 떠올리시면 됩니다.

"스택은 FILO 구조이기 때문에 파라미터를 역순으로 넣어주면,
 받는 쪽(MessageBoxA 함수내부)에서 올바른 순서로 꺼낼 수 있습니다."

디버거를 이용해서 EIP = 0040100E 시점까지 진행한 다음 스택을 보시면 아래와 같습니다.


<Fig. 5>

IA-32 에서 스택은 아래로 자라기(스택에 값을 입력하면 ESP 값은 작아짐) 때문에
디버거에서 스택을 보면 <Fig. 5> 에서처럼 MessageBoxA() 함수의 첫번째 파라미터가
스택의 제일 위에 보이고, 마지막(네번째) 파라미터가 아래에 쌓여있게 됩니다.
(PUSH 명령 순서를 따라가 보시면 이해하기 쉽습니다.)

0012FFB4    00000000    hOwner = NULL (1st param)
0012FFB8    00402012    Text = "Make me think your HD is a CD-Rom." (2nd param)
0012FFBC    00402000    Title = "abex' 1st crackme" (3rd param)
0012FFC0    00000000    Style = MB_OK|MB_APPLMODAL (4th param)

따라서 MessageBoxA() 함수 내부에서는 POP 명령을 써서 파라미터를 꺼내게 될 텐데,
스택의 FILO 구조에 따라서 첫번째 파라미터 부터 꺼낼 수 있게 되는 것입니다.

즉, 파라미터를 꺼내서 사용하는 MessageBoxA() 함수 입장에서는 스택에 파라미터의 순서대로 들어 있는 셈이 되는 것입니다.

* 실제로는 함수 내부에서 POP 명령이 아니라 EBP 레지스터를 이용하여 파라미터를 직접 엑세스 하게 됩니다.
   여기서는 개념을 설명드리기 위해서 POP 명령을 얘기한 것 입니다.

* 중요한 것은 함수를 호출할 때 파라미터는 역순으로 입력한다는 것입니다.

* 나중에 'Stack Frame' 을 설명할 때 스택에 대해서 좀 더 자세히 다루도록 하겠습니다.




배운 내용

VirusTotal (www.virustotal.com)
간단한 Assembly 명령어 (PUSH, CALL, JMP, INC, DEC, CMP, JE)
Assembly 언어로 된 파일의 크랙
함수 호출 시 스택을 이용한 파라미터 전달 방법



앞으로 배워야 할 내용

Stack Frame



Epilogue

매우 간단한 크랙이지만 처음 접하시는 분들을 위해서 상세한 설명을 하였습니다.

다시 한번 강조하지만 크랙은 그냥 리버싱 처음 배울 때 디버깅 연습용(?)으로만 사용하는 것이 좋습니다.

핵심 원리를 파악하여 고급 리버싱을 할 수 있는 기초를 마련하는데 힘을 쏟아야 할 것 입니다.

감사합니다.

'analysis' 카테고리의 다른 글

키로거(KeyLogger) 분석 (3)  (4) 2009/05/03
키로거(KeyLogger) 분석 (2)  (51) 2009/05/03
키로거(KeyLogger) 분석 (1)  (14) 2009/04/24
Lena's Reversing for Newbies #10 (2)  (7) 2009/03/18
Lena's Reversing for Newbies #10 (1)  (16) 2009/03/17
abex' crackme #2 (2)  (27) 2009/03/01
abex' crackme #2 (1)  (31) 2009/02/28
abex' crackme #1  (34) 2009/02/28
"Hello World!" - 내 생애 첫 디버깅 (3)  (39) 2009/02/28
"Hello World!" - 내 생애 첫 디버깅 (2)  (5) 2009/02/28
"Hello World!" - 내 생애 첫 디버깅 (1)  (47) 2009/02/26

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

  1. 오오 2009/10/29 20:35 댓글주소 | 수정 | 삭제 | 댓글

    이런 좋은강좌에 댓글이 하나도없다니.. 다들 보고 그냥 가시나보군
    감사합니다~

  2. 베리굿 2009/11/07 23:11 댓글주소 | 수정 | 삭제 | 댓글

    감사합니다.^^ 초보들도 이해하기 쉬운강좌네요 ㅎㅎ

  3. 놀람. 2009/11/13 23:26 댓글주소 | 수정 | 삭제 | 댓글

    이렇게 자세한 강좌는 처음보네요. 매번 간단하게 jmp 로 넘기면 됩니다.에서 스택에 대해 나오고 오 감동입니다. 감사합니다.

  4. 2010/01/29 13:47 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

  5. 감사 2010/02/13 13:50 댓글주소 | 수정 | 삭제 | 댓글

    감사합니다. 그런데 처음에


    ESI가 FFFFFFFF로 셋팅된다고 하셨는데 저것은 어떻게 아나요?
    본격적인 작업전에 한번 실행해보면서 옆에 레지스터 화면을 보고 아는건가요?

    • ReverseCore 2010/02/14 10:24 댓글주소 | 수정 | 삭제

      안녕하세요.

      MessageBox() API 내부에서 ESI = FFFFFFFF 로 세팅하는 것입니다. 디버거에서 한번 StepOver[F8] 하면 금방 아실 수 있지요.

      이처럼 Win32 API 함수들 내부에서 레지스터 값이 변경되기 때문에 어셈블리 프로그래밍을 할 때 주의해야 하지요.

      감사합니다.

  6. 리버싱만세! 2010/04/13 00:10 댓글주소 | 수정 | 삭제 | 댓글

    초보자용으로 설명 드린거 감사하게 보고 갑니다^^ 다른것도 있어서
    많이 들르러 올게요~

  7. 비빅 2010/07/20 02:37 댓글주소 | 수정 | 삭제 | 댓글

    좋은 정보 항상 잘 보고 있습니다^^ 감사합니다^^

  8. 리버싱입문자 2010/12/16 11:04 댓글주소 | 수정 | 삭제 | 댓글

    아직 많이많이 부족하지만,,, 정말
    하나하나 알아가는 기분입니다.

    감사합니다 ^^

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

      네, 바로 그겁니다.

      리버싱이란것은 하루에 아주 조금씩 천천히...

      대신 꾸준히 앞으로 전진하는 것이지요... ^^

  9. 2011/01/10 14:15 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

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

      안녕하세요.

      고급언어든 저급언어든 실행되기 위해서 기계어로 변환되는것은 같지요.

      STUB 코드는 빌더(ex:VC++, VB, Delphi, etc) 에서 추가시키는 코드이며, 빌더만의 독특한 코드로 채워집니다. 같은 C++ 컴파일러라도 VC++ 과 BC++ 이 만들어내는 Stub 코드는 서로 다른 모양이지요.

      저는 VC++ 을 사용하는데요, Stub이 무조건 들어갑니다.

      참고로 제가 사용하는 MASM32 어셈블러에는 Stub 코드가 추가되지 않습니다.

      감사합니다.

  10. 2011/01/10 16:43 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

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

      Makefile 을 이용하여 VC++ 컴파일러, 링커를 쓰시면...
      그래도 역시 비슷하게 생성되겠죠?
      물론 설정에 따라 생긴 모습은 틀려지겠지만 말이죠~

  11. Reverser_H 2011/01/20 00:17 댓글주소 | 수정 | 삭제 | 댓글

    처음에 리턴값이(EAX)가 3인이유와
    ESI가 0인 이유를 잘 모르겠네요;

    (f8)을 이용해서 실행시키면서 register값을
    확인하면 EAX의 리턴값이 3이고 ESI의 값이 0인것을
    알수 있습니다만

    이렇게 실행하지 않고 단지 소스코드만보고
    알수있나요?
    제가 초보라서;;; 궁금해서 질문드립니다.

    • reversecore 2011/01/26 00:05 댓글주소 | 수정 | 삭제

      안녕하세요.

      당연히 디버깅 해서 알아낸 값이지요. ^^~

      API 에 익숙한 사람이라면 GetDriveType() 의 파라미터를 보고 리턴값 정도는 예측할 수 있겠지요~

      감사합니다.

  12. Apple 2011/02/17 16:39 댓글주소 | 수정 | 삭제 | 댓글

    쓰신 글을 따라 배워 보고 있습니다.

    감사합니다.

  13. 감자 2011/02/21 20:46 댓글주소 | 수정 | 삭제 | 댓글

    garbage code가 있는 이유가 궁금하네요.. 단순히 디버깅을 방해하기 위한건가요??

  14. reverse 2011/03/11 01:02 댓글주소 | 수정 | 삭제 | 댓글

    일단 강좌 감사합니다.ㅎㅎㅎ

    질문 몇가지 남깁니다 ^*^

    1.그전에 0040101F주소값에서 바로 0040103D로 점프해버리면 안되나요?

    2. 그렇게해도 결과는 같은데,, JE를 바꿔주는거랑 어떤 큰 차이점이나 문제점이 있게 되는건가요?ㅎ

    3. CMP 부분을 수정을 통하여 JE 조건 분기가 1이 되도록 해서
    조건을 만족 시키는 방법도 있는데,
    어떻게 EAX, ESI 부분을 저 시점에서 수정 되도록 해야 할지
    모르겠네요. 명령어 부분을 지우는 방법은 없나요?

    4.ESI 부분이 증가 되는 부분을 빼버리면 될것 같은데,
    그리고 ESI 값이 0이고 1이고 2고 그런건 어디 부분에서
    분석이 가능 한건가요?

    • reversecore 2011/03/16 22:57 댓글주소 | 수정 | 삭제

      안녕하세요.

      1. 됩니다. 더 좋은 위치에요~ ^^ 사실 가장 좋은 위치는 401000 이지요. 여기에 JMP 코드를 설치하면 무조건 됩니다. ^^

      2. 레지스터 값이 변경되는 것 말고는 없습니다.

      3. 디버거에서 레지스터 값을 직접 고치셔도 됩니다. 명령어를 지우는 방법은 NOP 로 채우시는 겁니다. 1 byte 의미 없는 명령이기 때문에 코드에 전혀 영향을 끼치지 않습니다.

      4. 디버깅을 하다보면 레지스터 창에서 해당 값이 변하는 걸 쉽게 보실 수 있습니다. 직접 연산하지 않아도 되니 편리하지요.

      감사합니다.

  15. 야호 2011/03/31 17:09 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요.. 늦게 시작하는 직장인입니다
    너무 좋은 사이트를 발견해서 너무 행복하고요...빨리 책을 보고싶습니다..

    오늘 강좌에서 JMP 를 쓰지않고 00401023 을 NOP 처리하여, 패치를 완성하여 exe 파일로 저장하였습니다. 그런데 윈도우에서 exe 파일을 실행하면, 또다시 NO 가 됩니다. 해당파일을 디비거로 올려보면, 정확히 NOP 로 수정이 되어었고요, F9를 이용해 실행하면, YES 가 되어, 정상 Crack되었습니다. 왜 윈도우상에서 exe 파일 실행된 결과값과, 디버거에서의 결과가 틀린지 궁금합니다.
    단지 1바이트 코드만을 수정햇을뿐인데..... 이게 정상인가요??? ㅜㅜ

    답변 부탁드립니다.

    • Elephunk 2011/03/31 22:09 댓글주소 | 수정 | 삭제

      ollydbg 에서 esi레지스터 를 fffffff 로초기화 시키기 때문에

      ollydbg 에서 실행한 결과와 실제 결과가 달라집니다

      이점을 이용해서 ollydbg 를 탐지해 낼수가 있지요

      실제로 이것이 적용된 패킹 프로그램도 있구요

      ollydbg 2.0 에서 수정되었으니 2.0 사용해보세요

    • reversecore 2011/04/06 01:25 댓글주소 | 수정 | 삭제

      안녕하세요.

      Elephunk님, 항상 좋은 답변 감사합니다. ^^

      하나 추가하고 싶은 부분은요~
      이 샘플에서만큼은 그 패치 위치가 좋지 않았다는 것입니다.

      Win 7 에서는 OllyDbg 의 ESI 초기값이 0 이기 때문에 잘 됩니다.
      XP 에서는 Elephunk 님 말씀대로 ESI 가 -1 이라서 안되구요.

      되도록이면 그런 영향을 덜 받을 수 있는 조건 분기 명령어가 제격입니다.

      어디까지나 이건 연습용이기 때문에 테스트로 그러신거겠죠? 그래도 그런 호기심으로 다른 사실을 알게 되셨구요. 리버싱에 도움되는 좋은 습관을 가지셨네요~ ^^

      감사합니다.

  16. 금빛 star 2011/04/27 10:37 댓글주소 | 수정 | 삭제 | 댓글

    좋은 강좌 감사글없이 지나치는건 죄악일것 같아서요..
    감사 10000번
    근데 질문입니다
    우의 예제 assembly로 작성했다고 하셨는데 그럼 masm등을 이용했다는건가요, 혹 그렇다면 assembly에서도 저렇게 api를 호출할수 있는가요?
    무식한넘 감히 질문합니다.

  17. 빵뎅이 2011/05/26 17:37 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 요즘 리버싱 공부하느라 거의 매일 오고 있어요 ㅎ
    좋은 글 일단 감사합니다..
    그리고 질문이 있는데요 MessageBoxA 함수 내부에서 ESI가 -1 이 세팅된다고 하셨는데, 저는 401013에 BP 걸고 실행하면 ESI가 0이 되서 나오네요.. 궁금해서 F7로 함수 내부로 들어갔는데, ESI가 -1로 세팅이 되긴하는데 어느 순간 0으로 바뀌더군요.
    제가 Win7 이라 올리 2.0 을 쓰는데 그래서 그럴까요?