리버서들이 가장 많이 사용하는 도구인 디버거(Debugger)에 대해 이야기 해보도록 하겠습니다.


리버싱 현업에서 사용되는 Debugger 들을 간략히 정리해 보겠습니다. 


1. OllyDbg



OllyDbg 는 사용이 편리하고 가볍고 빠른 무료 Debugger 입니다. 도저히 무료라고 보기 힘든 다양한 기능과 많은 Plugin 을 통한 확장성으로 인하여 수 많은 리버서들의 열광적인 지지를 얻고 있습니다. 

OllyDbg 는 리버싱 초보부터 전문가까지 폭넓게 사용되는 가장 인기 있는 디버거입니다.


<그림 1 - OllyDbg>

OllyDbg 의 장점으로는 가볍고 빠르며 상당히 다양한 기능과 많은 옵션을 제공한다는 것입니다. 또한 PlugIn 기능을 통한 확장성을 제공합니다. 가장 사용자가 많고 OllyDbg 를 이용한 리버싱 강좌가 많이 있기 때문에 초보자도 쉽게 배울 수 있습니다. 그리고 무료로 제공된다는 점도 큰 장점입니다.

단점으로는 개인이 혼자서 개발한 거라 업데이트와 후속 제품의 개발 주기가 늦다는 것입니다. 다행히 최근에 OllyDbg 2.0 Final 버전이 릴리즈 되었습니다. 비록 겉모습은 이전 버전과 동일하지만 내부 코드는 완전히 새롭게 프로그래밍 하여 속도와 정확성 등이 크게 향상되었다고 하네요.


2. IDA Pro



Hex-rays 사의 IDA Pro 는 현재 최고의 Disassembler & Debugger 라고 말할 수 있습니다. 과거에는 Disassembler 성격이 강했으나, 수많은 업데이트를 통해 Debugger 기능 또한 막강해 졌습니다. 

수 많은 다양한 기능들을 설명하는 전문 서적이 따로 존재할 정도로 엄청난 기능을 자랑합니다. 또한 Decompiler Plugin 등을 추가로 장착하면 리버싱이 말할 수 없이 편리해 집니다. 그만큼 가격도 비싸지요.

많은 리버싱 전문가들이 IDA Pro 를 주력으로 삼으면서 리버싱 전문 툴로써의 입지를 탄탄히 굳히고 있습니다.

장점으로는 다 써보지도 못할 정도로 다양한 기능과 충실한 업데이트를 들 수 있습니다. 다만 가격이 비싸고 사용법이 비교적 복잡하며 초기 로딩 시간이 좀 걸린다는 것을 단점으로 들 수 있겠습니다. 


<그림 2 – IDA Pro>


3. WinDbg



WinDbg 는 DOS 시절 16 bit 디버거인 debug.exe 의 Windows 버전입니다. 


<그림 3 – Debug.exe>

콘솔 화면에서 키보드만으로 디버깅을 하는 debug.exe 의 사용자 인터페이스를 그대로 가져왔습니다. 저는 이런 스타일의 프로그램을 매우 좋아하지만 반대로 거부감을 갖는 분들도 상당수 계십니다.

유저 모드 디버깅(User Mode Debugging) 분야에서는 사용자 편리성이 뛰어난 OllyDbg 나 IDA Pro 가 꽉 잡고 있습니다. 따라서 WinDbg 는 주로 커널 모드 디버깅(Kernel Mode Debugging)에 주로 사용됩니다. 전설적인 커널 디버거인 SoftICE 의 후속 제품 개발이 중단된 이후 커널 디버깅 분야에서 사실상 독보적인 존재가 되어 버렸습니다. (경쟁 제품이 없는 상태입니다.)

역사가 오래 된 만큼 기능도 다양하고 사용 방법에 대한 전문 서적이 여러 권 나와 있습니다.


<그림 4 - WinDbg>

장점은 커널 디버깅이 가능하다는 것과 Microsoft 에서 직접 만든 디버거라는 점입니다. 시스템 파일에 대한 심볼(Symbol)을 직접 다운 받을 수 있어서 시스템 내부 구조체(Undocumented 포함) 및 API 에 대한 정보를 얻을 수 있습니다. 또한 Windows OS 의 덤프 파일을 읽어 들여 분석할 수 있기 때문에 시스템 크래쉬(Crash)가 발생했을 때 원인을 분석해 낼 수 있습니다. 

단점으로는 다른 디버거들에 비해 좀 떨어지는 사용자 환경과 편의성을 꼽을 수 있겠습니다. (예를 들어 코드에 직접 주석을 입력할 수 없고, Disassembly 코드에서 호출되는 API 이름도 잘 표시되지 않는 등의 불편함이 있습니다.)

그래도 커널 드라이버 개발과 유지보수에 필수적으로 사용되는 디버거 입니다. 리버싱에서는 커널 드라이버 파일 분석에 주로 사용됩니다. 최근에는 대부분 WinDbg 와 VirtualPC(또는 VMWare) 조합으로 커널 디버깅을 수행합니다. 향후 여러분들의 리버싱 실력이 쌓인 후 커널 드라이버(예: Rootkit) 등을 분석할 때 자주 사용될 것입니다.

+---+

지금까지 리버싱 현업에서 사용되는 대표적인 디버거들에 대해서 간략히 살펴보았습니다.

제 경우에는 유저 모드 디버깅에 OllyDbg 1.10 를 사용하고, 커널 모드 디버깅에는 WinDbg 를 사용합니다.

ReverseCore

'tool' 카테고리의 다른 글

리버싱 현업에서 사용되는 디버거(Debugger)들  (20) 2010/09/29
InjDll.exe – DLL Injection/Ejection 전용 도구  (33) 2010/03/15
(개인) 즐겨찾기  (0) 2009/06/02
Process Explorer - 최고의 작업 관리자  (0) 2009/05/03
www.virustotal.com  (2) 2009/03/20
www.google.com  (2) 2009/03/06

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

  1. reversecore 2010/09/30 19:25 댓글주소 | 수정 | 삭제 | 댓글

    - 봉이님 댓글

    ㅁㅁ님!
    제가 리버싱 공부하면서, 안티리버싱, 악성코드 분석에 관심이 생겨서 인터넷 곳곳 돌아 봤는데요..
    안랩 연구원들 분께서 고려대, 서울여대 악성코드 분석 강의를 하시는 것 같더라구요..
    관련 강의자료를 구할수 있을까요?
    꼭 부탁드립니다.

    • reversecore 2010/09/30 19:26 댓글주소 | 수정 | 삭제

      안녕하세요~

      ^^ 제 실명을 거론 하셔서 제가 약간 가렸습니다. 양해바랍니다.

      강의에 대해서 잘 알고 계시군요?

      해당 자료는 수강생 이외에는 비공개 입니다.

      참고로 저도 한 섹션을 맡았습니다. ^^

  2. reversecore 2010/09/30 19:25 댓글주소 | 수정 | 삭제 | 댓글

    - 봉이님 댓글

    core님이 ㅁㅁ님 맞죠?
    혹시 core님 이름이 틀렸을수도 있으니, 제가 위에 표현한 ㅁㅁ님은 core님입니다.

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

      ^^ 네 제가 그님(?) 맞습니다.

      봉이님께서는 혹시 저를 아시는 분인가요?
      너무 친숙하게 물어보셔서요~ ^^
      (비밀 댓글로 답해주시면 됩니다.)

  3. 2010/10/01 15:39 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

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

      안녕하세요.~

      네, 그러셨군요. ^^

      리버싱을 공부하고 싶으시다는 열의가 느껴집니다.

      죄송하지만 강의 자료를 제공해 드릴 수는 없습니다. (제가 작성한 거라도 불가능합니다.)

      그 대신 공부하시다가 막히시는 부분이 있을때 질문 올려 주시면 제 능력껏 답변 달아 드리겠습니다.

      감사합니다.

  4. 김종현 2010/10/02 23:40 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요.
    처음 인사드리는 김종현입니다.
    테스트중 이상한 현상이 있어 문의 드립니다.

    아래와 같이 정말 간단한 DLL을 만들어 Injection하였는데
    이상하게 iexplore의 자식 프로세스들에서는 ::CreateFile()로
    파일이 생성되지 않는데
    그 이유가 심히 궁금하고
    해결방법에 대하여 고수님들께 조언을 구합니다.

    #include "stdafx.h"
    #include <stdio.h>
    #include <tchar.h>

    /////////////////////////////////////////////////////////////////////////////
    // _Inject

    class _Inject
    {
    public:
    static BOOL IsValidModule(LPCTSTR module_name)
    {
    static LPCTSTR valid_module_name_list[] =
    {
    //_T("notepad.exe"),
    _T("iexplore.exe"),
    NULL
    };
    for (int i=0; valid_module_name_list[i]; i++)
    {
    if (!_tcsicmp(module_name, valid_module_name_list[i])) return TRUE;
    }
    return FALSE;
    }
    }; //_Inject

    static BOOL IsValidCurrentModule()
    {
    TCHAR full_name[MAX_PATH];
    HMODULE module_handle = ::GetModuleHandle(NULL);
    ::GetModuleFileName(module_handle, full_name, MAX_PATH);
    TCHAR* module_name = full_name;
    for (int i=0; full_name[i]; i++)
    {
    if (full_name[i] == '\\') module_name = full_name + i + 1;
    if (full_name[i] == '/') module_name = full_name + i + 1;
    }
    if (!_Inject::IsValidModule(module_name)) return FALSE;
    LPCTSTR file_name = _T("c:\\zzz.txt");
    HANDLE file_handle = ::CreateFile(file_name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (file_handle != INVALID_HANDLE_VALUE) ::CloseHandle(file_handle);
    else
    {
    TCHAR text[256];
    _stprintf(text, _T("ERROR: Cannot create file \"%s\"!!!\nPID: %d"), file_name, ::GetCurrentProcessId());
    ::MessageBox(NULL, text, module_name, MB_OK);
    }
    return TRUE;
    }

    BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
    {
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    if (IsValidCurrentModule())
    {
    }
    break;
    case DLL_THREAD_ATTACH:
    break;
    case DLL_THREAD_DETACH:
    break;
    case DLL_PROCESS_DETACH:
    break;
    }
    return TRUE;
    }

    EXTERN_C __declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
    {
    return CallNextHookEx(NULL, code, wParam, lParam);
    }

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

      안녕하세요.

      iexplore.exe 의 자식 프로세스라면...
      7 버전 이후의 탭으로 생성되는 서브 프로세스를 말씀하시는 건지요?

      제 경우에는 정상적으로 c:\\zzz.txt 파일이 생성되었습니다.
      (WinXP SP3 & Win7)

      혹시 다른 경우를 말씀하시는 거라면 다시 질문 올려주세요~ ^^

      감사합니다.

    • 김종현 2010/10/04 12:22 댓글주소 | 수정 | 삭제

      테스트해 주셔서 감사합니다.
      그런데 정작 중요한 환경을 알려 드리지 않았네요.
      제가 테스트한 IE는 버전8이고
      말씀하신데로 탭으로 생기는 서브프로세스에서는
      전부 문제가 발생하고 있습니다.
      유독 부모프로세스만 문제 없구요.

      정확한 정보는 gmail로 다시 보내드리니
      IE8에서 다시 한번 더 봐 주시길 부탁드립니다.

    • reversecore 2010/10/05 21:21 댓글주소 | 수정 | 삭제

      네, 제 테스트 환경과 동일하시네요~

      전 보통 최신 .NET Framework 가 설치되지 않은 환경에서의 실행을 보장하기 위해 /MT 옵션으로 빌드합니다.

      제가 테스트한 파일을 김종현님 email 로 보내드릴테니 한번 실행해 보시고 결과를 알려 주시기 바랍니다. ^^

      * 보안 프로그램등에서 차단당하는 경우도 있으니 참고하시기 바랍니다.

      감사합니다.

  5. lain32 2010/10/04 19:53 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 :D
    계속해서 좋은글을 많이 올려주셔서 제가 기초가 부실한 부분들이 있었는데
    많은 도움을 받고 있습니다.
    그리고 정말 자세하게 설명하셔서 너무나도 많은 도움이 되고 있구요.
    이렇게 좋은 자료들을 열정적으로 공개해주셔서 너무 감사드립니다.

    힘내시라고 감사에 글을 적어봤습니다. ㅎㅎ

  6. 2011/02/17 22:47 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

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

      안녕하세요.

      아주 멋진 크랙미 사이트를 만드셨군요~

      구경 잘 했습니다.

      제 경우에 블로그 홍보를 따로 해본 적은 없습니다. ^^

      이곳에 방문 하시는 분들은 대부분 google 과 naver 검색으로 찾아오시는 분들입니다.

      감사합니다.

    • 2011/02/18 23:02 댓글주소 | 수정 | 삭제

      비밀댓글입니다

  7. 2011/09/02 12:01 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

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

      안녕하세요.

      비슷한 시간에 디버거를 만드신 다는 질문이 여러개 등록되어있군요. ^^

      OllyDbg 정도의 범용 디버거는 매우 높은 수준의 개발 실력과 시스템에 대한 이해가 필요하다고 생각하시면 됩니다.

      다만 특정 상황에서만 간단히 사용한다면... (Disassembler 도 필요없고 GUI 도 필요없고... 단순한 명령어만 디버깅한다고 했을때... ) 개발 난이도는 상당히 낮출 수 있습니다.

      커널 디버거는 저도 아직 어떻게 만들어야 할지 어렴풋이 감만 잡고 있는 수준입니다. ^^~

      감사합니다.

  8. 비밀 2011/12/21 15:59 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요? ^-^
    책 원고가 완성되었다고 하셨는데.. 개인적으로. 가능하다면.
    Immunity Debugger 에 대해서도 설명을 넣으면 좋을것 같습니다 :)
    제가 알기로 OllyDbg 를 만들던 분이 Immunity 로 이직하시면서 Python 기반으로 개발하신걸로 알고 있습니다. 물론 무료입니다 :)
    http://debugger.immunityinc.com

    수고하세요~

    • reversecore 2012/01/11 07:53 댓글주소 | 수정 | 삭제

      안녕하세요.

      Immunity Debugger 의 python 스크립트는 아주 좋습니다. 저도 많이 써봤지요.

      근데 OllyDbg 개발자는 다른 직업이 있고 그냥 취미로 OllyDbg 를 만든걸로 알고있는데요.

      Immunity Debugger 는 그냥 OllyDbg 클론이구요. 제가 잘 못 알고 있나요?

      감사합니다.

  9. 2012/02/09 20:13 댓글주소 | 수정 | 삭제 | 댓글

    이뮤니티 디버거 같은경우 올리디버거와 같지만 디버거 자체의 라이브러리가 좋다고 말할수 있을거 같아요. 거기다 파이썬 코드로 스크립트 가능하니까 윗분은 뭐 그런 부분을 집어 달라는게 아닐까요?



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

어셈블리 언어를 이용한 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 을 소개하는 이유는 리버싱에 대해 좀 더 깊이있는 공부를 해보자는 뜻입니다.

      감사합니다.



Windows 7 (& Vista) 에 맞는 InjectDll.exe 를 개발하고 소스 코드를 살펴보겠습니다.

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


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



지난 내용 정리


시작하기 전에 먼저 지난 시간에 소개해 드린 내용을 짧게 정리해 보겠습니다.

Windows 7 (or Vista) 에서는 Session 관리 정책이 변경됨에 따라서 kernel32!CreateRemoteThread() API 내부 구현이 변경되었습니다. 

☞ 참고 : Session in Windows 7

그 결과 CreateRemoteThread() 를 사용한 DLL Injection 기술이 Windows 7 (or Vista) 의 서비스 프로세스(Session 0) 에게는 정상적으로 동작하지 않습니다. 

Kernel32!CreateRemoteThread() 를 디버깅 한 결과 원인은 API 내부에서 리모트 스레드를 생성할 때 suspend 모드로 생성하는데, 만약 리모트 프로세스가 Session 0 라면 resume 시키지 않고 그냥 에러를 리턴하기 때문이었습니다. 

* 참고!
리모트 스레드를 생성할 때 일단 suspend 모드로 생성시킨 후 resume 시키는 구현 방법은 예전 XP 때부터 사용되던 방식입니다.

Kernel32!CreateRemoteThread() API 내부에서 호출되는 ntdll!ZwCreateThreadEx() API 의 파라미터를 조작 하거나 에러 조건 분기를 강제로 변경하면 정상적으로 리모트 스레드가 생성되면서 DLL Injection 이 성공하는 것을 확인 하였습니다.



InjectDll.exe


Windows 7 에서는 kernel32!CreateRemoteThread() 를 호출하는 것보다는 ntdll!ZwCreateThreadEx() API 를 직접 호출하는 것이 더 좋은 방법임을 알게 되었습니다.


* 주의!
ntdll!ZwCreateThreadEx() API 는 Vista 이후부터 추가된 API 이므로 XP 이하에서는 계속 CreateRemoteThread() 를 사용해야 합니다.

새로 알게 된 지식을 바탕으로 InjectDll.exe 프로그램을 새롭게 만들었습니다.

새롭게 프로그래밍한 InjectDll() 함수를 살펴보겠습니다.

typedef DWORD (WINAPI *PFNTCREATETHREADEX)
    PHANDLE                 ThreadHandle,
    ACCESS_MASK             DesiredAccess,
    LPVOID                  ObjectAttributes,
    HANDLE                  ProcessHandle,
    LPTHREAD_START_ROUTINE  lpStartAddress,
    LPVOID                  lpParameter,
    BOOL                    CreateSuspended,
    DWORD                   dwStackSize,
    DWORD                   dw1, 
    DWORD                   dw2, 
    LPVOID                  Unknown 
); 

BOOL IsVistaOrLater()
{
    OSVERSIONINFO osvi;

    ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
    osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

    GetVersionEx(&osvi);

    // 커널 버전이 6 이상인지 확인!
    if( osvi.dwMajorVersion >= 6 )
        return TRUE;

    return FALSE;
}

BOOL MyCreateRemoteThread
(HANDLE hProcess, LPTHREAD_START_ROUTINE pThreadProc, LPVOID pRemoteBuf)
{
    HANDLE      hThread = NULL;
    FARPROC     pFunc = NULL;

    // OS 가 Vista 이상인지 확인!
    if( IsVistaOrLater() )    // Vista, 7, Server2008
    {
        pFunc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCreateThreadEx");
        if( pFunc == NULL )
        {
            printf("GetProcAddress(\"NtCreateThreadEx\") failed!!! [%d]\n",
                   GetLastError());
            return FALSE;
        }

        // NtCreateThreadEx() 호출
        ((PFNTCREATETHREADEX)pFunc)(&hThread,
                                    0x1FFFFF,
                                    NULL,
                                    hProcess,
                                    pThreadProc,
                                    pRemoteBuf,
                                    FALSE,
                                    NULL,
                                    NULL,
                                    NULL,
                                    NULL);
        if( hThread == NULL )
        {
            printf("NtCreateThreadEx() failed!!! [%d]\n", GetLastError());
            return FALSE;
        }
    }
    else                    // 2000, XP, Server2003
    {
        hThread = CreateRemoteThread(hProcess, 
                                     NULL, 
                                     0, 
                                     pThreadProc, 
                                     pRemoteBuf, 
                                     0, 
                                     NULL);
        if( hThread == NULL )
        {
            printf("CreateRemoteThread() failed!!! [%d]\n", GetLastError());
            return FALSE;
        }
    }

    if( WAIT_FAILED == WaitForSingleObject(hThread, INFINITE) )
    {
        printf("WaitForSingleObject() failed!!! [%d]\n", GetLastError());
        return FALSE;
    }

    return TRUE;
}

BOOL InjectDll(DWORD dwPID, char *szDllName)
{
    HANDLE hProcess = NULL;
    LPVOID pRemoteBuf = NULL;
    FARPROC pThreadProc = NULL;
    DWORD dwBufSize = strlen(szDllName)+1;

    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
    {
        printf("OpenProcess(%d) failed!!! [%d]\n", 
                dwPID, GetLastError());
        return FALSE;
    }

    pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, 
                                MEM_COMMIT, PAGE_READWRITE);

    WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName, 
                       dwBufSize, NULL);

    pThreadProc = GetProcAddress(GetModuleHandle(L"kernel32.dll"), 
                                 "LoadLibraryA");

    if( !MyCreateRemoteThread(hProcess, (LPTHREAD_START_ROUTINE)pThreadProc, pRemoteBuf) )
    {
        printf("CreateRemoteThread() failed!!! [%d]\n", GetLastError());
        return FALSE;
    }

    VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);

    CloseHandle(hProcess);

    return TRUE;
}


InjectDll() 함수의 변경된 사항은 바로 kernel32!CreateRemoteThread() 를 직접 호출하지 않고MyCreateRemoteThread() 사용자 함수를 호출 한다는 것입니다.

MyCreateRemoteThread() 함수 내부에서 OS 버전을 구해서 Vista 이상이라면 ntdll!NtCreateThreadEx() 를 호출하고, XP 이하라면 kernel32!CreateRemoteThread() 를 호출하도록 하였습니다.

간단한 코드이므로 쉽게 이해하실 수 있으실 겁니다.


* 참고!
유저 모드에서 ntdll.dll 라이브러리의 NtCreateThreadEx() 와 ZwCreateThreadEx() API 는 사실 같은 함수입니다. (두 함수의 시작 주소는 동일합니다.) 커널 모드(ntoskrnl.exe) 에서는 두 함수가 서로 틀려집니다. 향후 커널 모드 디버깅을 설명할 때 다시 설명 드리겠습니다. 일단 유저 모드에서는 NtXXX() 와 ZwXXX() 는 같다는 것만 기억해 주시기 바랍니다.



테스트

dummy.dll 파일은 지난 시간에 사용된 것과 동일한 파일로써 인젝션이 성공하면 디버그 로그를 출력하는 기능을 가지고 있습니다.

테스트를 위해서 적당한 서비스 프로세스를 골라 봅니다.


<Fig. 1>

그리고 아래와 같이 InjectDll.exe 를 실행시켜 주세요.


<Fig. 2>

Process Explorer 로 svchost.exe (PID:612) 를 확인해 보시면 dummy.dll 이 인젝션 되어 있는 걸 확인 하실 수 있습니다.


<Fig. 3>

이제 서비스 프로세스(Session 0) 에도 무리 없이 DLL Injection 을 할 수 있게 되었습니다. 

* 참고!
ntdll!NtCreateThreadEx() API 는 undocument API 입니다. 따라서 MicroSoft 에서 사용을 권장하지 않을뿐더러 시스템의 안정성을 보장할 수 없습니다. 제가 테스트 해본 바로는 잘 동작하였지만 언제든지 MS 에서 패치를 시킬 가능성도 있습니다. 따라서 업무용으로 이 방법을 따라 하시는 분들께서는 꼭 이러한 사항을 염두에 두시기 바랍니다.

이상으로 Windows 7 에서 DLL Injection 하는 방법에 대한 설명을 마치도록 하겠습니다.


ReverseCore

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

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

  1. Tracked from ♡바다.. 의 일상이야기 2010/08/10 12:02 삭제

    Subject: CopyPwd.exe 파일을 Windows Server 2008 에서도 사용해보자!!

    오랫만에 포스팅을 하는 듯 합니다. 서버 관리자 분들이면 꽤나 많이 사용하시는 소프트웨어가 CopyPwd.exe 파일입니다. 파일명에서도 보듯이 사용자의 패스워드를 해쉬값으로 변환해서 txt 파일로 저장 및 복원해주는 서버 관리자 분들에게는 없어서는 안될 아주 좋은 Command 창 소프트웨어 입니다. CopyPwd.exe 제조사 방문하기 하지만!! 제가 이번에 Windows Server 2008 을 설치하면서 난관에 봉착했습니다. 그것은 이 Cop..
  1. 철이 2010/02/23 04:12 댓글주소 | 수정 | 삭제 | 댓글

    항상 감사합니다.

  2. 늅늅 2010/03/01 12:51 댓글주소 | 수정 | 삭제 | 댓글

    잘 보고 있습니다!!

    혹시 책으로 언제쯤 발간하실 예정이신가요?~

    너무 기대되네요!!

    • reversecore 2010/03/01 23:32 댓글주소 | 수정 | 삭제

      늅늅님, 안녕하세요.

      제 책을 기다리시는 건가요? ^^

      목표는 금년내에 출판하는 것입니다.

      감사합니다.

  3. L4c0 2010/03/11 11:35 댓글주소 | 수정 | 삭제 | 댓글

    와 출간 계획이 있으셨군요. 빨리나오기를.. ^^

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

      L4c0님, 안녕하세요.

      책쓰는게 정말 쉬운일이 아니라는걸 느끼고 있답니다.

      기술 서적을 쓰시는 분들은 정말 존경합니다. ^^

      감사합니다.

  4. 허훈 2010/03/21 04:17 댓글주소 | 수정 | 삭제 | 댓글

    InjectDll 함수에서
    hThread를 왜 선언만 하고 바로
    CloseHandle하셨는지요??

  5. 아이졸려 2010/05/26 13:40 댓글주소 | 수정 | 삭제 | 댓글

    항상 좋은 글 감사합니다.
    정말 많이 배우고 있습니다.
    Window 7 DLL Injection을 실습중인데, 설명하신대로 잘 되지 않아서 문의드립니다.
    일단 Session 0이 아닌 것은 잘 됩니다. 그런데 Session 0일경우는 새로운 방법도 에러가 나고요.
    것도 CreateRemoteThread가 아닌, OpenProcess에서 나고 있습니다.
    그리고 "The token does not have the specified privilage"라는 메시지도 뜨고요.
    무엇이 문제일까요?
    오늘도 좋은 하루 보내세요~

    • reversecore 2010/05/27 01:33 댓글주소 | 수정 | 삭제

      안녕하세요.

      가령 session 0 의 svchost.exe 에 인젝션 시도하면 OpenProcess() 에서 에러가 발생한다는 말씀이시죠?

      제 경우에는 7 - 32bit 버전에서는 문제가 없고, 64bit 버전에서는 NtCreateThreadEx() 에서만 에러가 발생합니다.

      사용하시는 OS 버전, 인젝션 시도하는 프로세스명 등을 알려주시면 답변에 도움이 될것 같습니다.

      감사합니다.

    • 아이졸려 2010/05/28 11:48 댓글주소 | 수정 | 삭제

      문제는 Windows 7 UAC인것 같습니다.
      지금 자세히 그림을 보니, 명령어창을 관리자 권한으로 여셨네요.^^
      저는 일반 사용자 권한으로 열어서 실행하다 보니 이런 문제가 생긴 것 같습니다.
      그런데 일반 사용자모드에서 관리자 권한으로 상승시키고 DLL Injection을 할 수는 없는 것인지 궁금합니다.
      좋은 답변 감사드립니다.
      오늘도 즐거운 하루보내세요.

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

      안녕하세요.

      아~ 그랬군요.
      UAC 를 평소 끄고 사용하는지라 생각도 못하고 있었습니다.
      그리고 전 항상 관리자로만 접속합니다.

      그런 부분에 대해서는 미처 고려해보지 않았네요.
      소중한 정보 감사드립니다. ^^

      그리고 Windows XP/7 에서는 로그인 유저마다 고유 Session 이 있기 때문에 일반적으로는 다른 Session 의 프로세스에게 DLL Injection 시킬 수 없습니다.

      강제로 권한을 상승 시킨후 DLL Injection 을 하면 어찌 될지 모르겠네요. (제가 요즘 차분히 앉아서 테스트를 할 수 있는 상황이 아니라서요... ㅠㅜ 제 대신 해주시면 매우 감사 하겠습니다.)

      감사합니다.

  6. LinkC 2010/06/12 16:19 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요, Reversecore님의 강의는 항상 감사하게 잘 보고 있습니다 :D

    다름이 아니라, XP나 7 32bit 에서는 성공했는데

    64bit에서 NtCreateThreadEx 에서 권한이 없다면서 실패하고 있습니다

    위 덧글을 보면 저만 그런게 아닌 모양인데요

    혹시 원인을 발견하셨는지 여쭙고 싶어서 댓글 답니다

    32bit 와 64bit 정책 적용 방식이 다를거 같진 않은데

    권한이 없다고 뜨는건 의외네요

    물론 관리자 권한으로 실행도 해봤지만 같은 증상이 뜹니다

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

      안녕하세요.

      네, 64bit 에서 잘 안된다고 하신분들이 계신데요.

      제가 64 bit OS 를 설치해서 테스트 해본 후 답변드리겠습니다.

      감사합니다.

    • LinkC 2010/08/03 19:30 댓글주소 | 수정 | 삭제

      계속 미루다 64bit injection 을 해봤습니다

      32bit process 에서는 32 bit dll만

      64bit process 에서는 64 bit dll 만

      injection이 가능하더군요

      64bit os라고 64bit process만 돌아가는게

      아니니까 , global api hooking 을 하고자 할때는

      64bit용, 32bit 용으로 나눠

      32bit injection program, 32bit dll

      64bit injection program, 64bit dll

      이렇게 제작해야 올바르게 작동하는 걸 확인했습니다 :D

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

      안녕하세요.

      와~ 좋은 정보 감사합니다. ^^
      저도 64 bit 를 더 공부한 후 테스트 해보겠습니다.

      감사합니다.

  7. 이남 2011/05/11 12:54 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요...

    강의내용 잘 보았습니다. 저 헌데 csrss에 전혀 inject되지 않는 이유가 무얼까요? 제 체계는 Win7입니다. inject success통보문 뜨는데도 되지 않거던요..
    의견 부탁드립니다.

    • reversecore 2011/05/13 20:23 댓글주소 | 수정 | 삭제

      안녕하세요.

      csrss.exe 프로세스는 시스템 핵심 프로세스로서 DLL Injection 을 안하는 것이 좋습니다.

      위 포스트의 InjectDll.exe 에는 버그가 있어서 csrss.exe 에 인젝션 성공했다고 나타나지만 실제로는 실패한 것입니다.

      http://www.reversecore.com/76 <- 여기서 제공되는 InjDll.exe 를 사용해 보시면 실패라고 정확히 나타납니다.

      아래 API 호출 중 한 군데에서 에러가 발생합니다.

      OpenProcess(), VirtualAllocEx(), WriteProcessMemory(), CreateRemoteThread()

      감사합니다.

  8. 안랩의전설 2011/10/10 16:38 댓글주소 | 수정 | 삭제 | 댓글

    잘 계시죵?

    오홀~ 이런것까정 해 보셨다니.. 써프롸이즈~

    smss.exe 에 인젝션 해보니.. 시스템이 퍽~하며 재부팅하는군요

    시스템 프로세스는 화이트 리스트로 빼 주는 기능도 추가해야 되겠군요

    좀 더 좋은 정보를 위해서 멀티스레드 환경에서는 JMP 패치시에 코드 크래쉬가 일어날 수 있는데

    윈도우의 핫패치 방법을 이용하는것도 간단히 소개해 주신다면.. 다른 분들께서

    더욱 많은 도움이 될 듯 합니다..

    그럼 이만.. 총총~

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

      안녕하세요. 잘 지내고 있답니다. ^^

      네, 개념 이해용 예제 코드 말고 상업용 프로그램에 사용할 수 있는 전문 DLL Injection 강좌도 기획하고 있습니다.

      제 예제 코드를 현업에서 사용하시는 분들이 꽤 많으시더군요. 좀 더 안정적인 코드를 제공해 드려야 할 것 같아요.

      조만간 책이 완료되면 다시 블로그 작업을 진행할 계획입니다.

      감사합니다.

  9. 인젝션 2011/10/13 14:02 댓글주소 | 수정 | 삭제 | 댓글

    잘 보고 갑니다~

    정리 완전 잘 하셨네요 ㅎ

    블로그에 소중한 정보들 잘 보도록 하겠습니다.!

  10. choiks 2011/10/15 20:44 댓글주소 | 수정 | 삭제 | 댓글

    질문이 있습니다!!!제가 OS관에서 완전 초보인데,,,

    SetPrivilege(SE_DEBUG_NAME, TRUE);

    // InjectDll.exe <PID> <dll_path>
    if( argc != 3 )
    {
    printf("usage : %s <PID> <dll_path>\n", argv[0]);
    return 1;
    }
    이부분 까지 시행이 되고
    "usage:argv[0]문이 실행이 되고 되고 그이후 명령문
    if( !InjectDll((DWORD)atoi(argv[1]), argv[2]) )
    {
    printf("InjectDll() failed!!!\n");
    return 1;
    }
    등이 실행이 안되는데 왜그런걸까요??

  11. jinjin 2011/10/16 09:23 댓글주소 | 수정 | 삭제 | 댓글

    choiks님
    안녕하셰요.
    글 보고 싱크되는것 있어 보니 printf다음에 return이 있어 그러는것 같아요.

  12. choiks 2011/10/16 10:36 댓글주소 | 수정 | 삭제 | 댓글

    그래도 안되네여... 제가 정말 왕초본데 ㅠㅠ
    if( argc !=3)
    {
    printf("usage : %s <PID> <dll_path> \n",arg[0]);
    return 1;
    }
    여기 까지 실행이 되서
    usage : DllInjgection.exe <PID> <DLL_PATH>
    이런 문장이 나오는데 PID랑 DLL_PATH를 어떡해 설정해 주면 될까요??

    아그리고 cmd명령오로 제가 컴파일해서 생성한 DLL_Injection.exe를 쳤더니 내부 또는 외부 명령, 실행할수있는 프로그램,또는 배치 파일이 아닙니다 라고 뜨네요 ㅠ.ㅠ

  13. jinjin 2011/10/17 20:24 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요.
    아그멘트값설정하는것이지요.
    컴맨드프롬프트창에서 EXE의 파일경로를 주어야되는데 예를 들어 "D:\test\Dllinjection.exe"라고 주고 space건을 누르고 계속해서 인젝션시킬 프로세스 아이디를 주어야지요.
    PID는 taskmanager에서 볼수 있지요.
    Next로서 DLL_PATH값을 앞에서 처럼주면 되요.
    예제로 "C:\test\Dllinjection.exe" 5764 "C:\test\Me.dll"
    안녕하세요.

  14. eee 2011/10/30 14:59 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요!^^ 정말 왕초보인데 64bit 에서 64 bit 컴파일 해서

    인젝션 성공 메세지까지 받았어요 . 그런데 process explorer 에서 dll을 검색하니까 검색이 안되요

    인젝션에 성공하면 dll 검색하면 인젝션된 프로세스들이 나와야 하는데 안나와요.

    어떻게 된건지 모르겟어요.ㅠㅠ 고수님들 도와주세요! ^^;

  15. 안녕하세요^^. 2011/11/02 18:59 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요.
    덕분에 모든 32비트환경에서 잘작동하는거 확인했어요 ㅎㅎ.
    하지만 윈7 64비트는안되는데
    윗분님들 말씀보시니 64비트 환경에서 컴파일해야하는데
    64비트를 따로 깔수가없는 환경이라 그런데 다른방법은 없나요 ㅜㅠ?

  16. 구미호 2011/11/14 19:53 댓글주소 | 수정 | 삭제 | 댓글

    dll injection 시켜서 인젝션 당한 프로그램을 렉걸리게 하고싶은데 어떻게하지요?
    1초정도 정지시키거든
    1초정도 순간 렉을 발생시키고 싶은데
    sleep 쓰면 dll에만 적용되고 프로세스에는 적용이 안되서...
    어떻게 소스를 짜면될까요?>

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

      안녕하세요.

      Target 프로세스의 실행을 방해하신다구요? 악성코드 수준입니다. ^^~

      실행중인 모든 스레드를 SUSPEND 모드로 바꿔버려도 되구요... 아니면 무한루프 스레드를 1000개쯤 만들어 버리세요. 엄청난 과부하가 걸릴정도로요...

  17. 팔극진 2012/01/03 14:54 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요~
    강의 보다가 궁금한 점이 있어서 이렇게 질문드립니다.

    ((PFNTCREATETHREADEX)pFunc)(인자값....);

    : 위에 보면요 ntcreatethreadex 의 주소값을 다시 함수 형변환하는데요~

    왜 형변환을 해줘야 하는건가요?

    그리고, CreateSuspended 값 0x1fffff 로 바꿀때

    함수만 호출해서 저렇게 형변환 해서 바꾸면 되는건가요?

    저 부분이 헷갈리네요 ㅜㅠ

    너무 초보라서 아무리 찾아도 모르겠습니다.

    조언 좀 부탁드립니다.

    수고하세요~

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

      안녕하세요.

      형변환은 C 언어의 특징이고요.
      컴파일 에러를 피하기 위해서 넣어주는 겁니다.
      개발자를 돕기 위해서 저렇게 엄격한 타입 체크를 해주는 거랍니다. 고마운거죠.

      어셈블리라면 형변환 자체가 필요없습니다. 다만 모든 책임은 개발자가 지는 것이죠. 어디서 에러가 발생했는지 찾는게 매우 까다롭죠. ^^

      감사합니다.



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

 DLL Injection in Windows 7 (1)



디버깅 2


OllyDbg 를 재실행시켜 InjectDll.exe 의 CreateRemoteThread() 호출 코드까지 옵니다. (아래 그림 참조)


<Fig. 1>

스택에 저장된 CreateRemoteThread() API 의 파라미터를 보겠습니다.


<Fig. 2>

위 그림의 중요 파라미터들에 대한 설명은 다음과 같습니다.

(1) svchost.exe (PID : 3148) 의 프로세스 핸들
(2) kernel32!LoadLibraryA() API 주소
(3) svchost.exe 의 프로세스 메모리에 할당한 버퍼 주소 ("c:\work\dummy.dll")

이제 <Fig. 1> 에서 StepIn (F7) 명령으로 kernel32!CreateRemoteThread() API 내부로 들어가 보겠습니다.


<Fig. 3>

Kernel32!CreateRemoteThread() 는 내부적으로 kernelbase!CreateRemoteThreadEx() 를 호출합니다. 

* kernelbase.dll 은 Vista 부터 추가된 DLL 파일로써 kernel32.dll 의 wrapper 역할을 담당합니다.

이때의 스택에 저장된 파라미터를 살펴보겠습니다.


<Fig. 4>

kernelbase!CreateRemoteThreadEx() 의 파라미터들은 kernel32!CreateRemoteThread() 와 거의 동일하고 lpAttributeList 파라미터가 추가되었습니다.

다시 kernelbase!CreateRemoteThreadEx() 코드 내부로 들어갑니다. 스크롤을 조금 내려보면 아래와 같이 ntdll!ZwCreateThreadEx() 호출 코드가 나타납니다.


<Fig. 5>

스택에 저장된 파라미터를 살펴보겠습니다.


<Fig. 6>

파라미터 개수가 더 많아졌습니다. <Fig. 2> 와 <Fig. 6> 을 비교해보면 중요 파라미터 (1) ~ (3) 이 그대로 전달된 것을 확인할 수 있습니다.

ntdll!ZwCreateThreadEx() API 의 내부를 더 파고 들어가면 결국 "SYSENTER" 명령어를 만나서 커널 모드로 진입하게 됩니다. 유저 모드 디버깅으로는 더 이상 진행할 수 없습니다. (커널 모드 디버깅에 대해서는 향후 자세히 설명하도록 하겠습니다.)

사실 kernelbase!CreateRemoteThreadEx() 와 ntdll!ZwCreateThreadEx() 는 모두 Vista 이후에 추가된 API 입니다. (XP 이하에서는 존재하지 않는 API 입니다.)

참고로 XP 에서는 kernel32!CreateRemoteThread() 내부에서 바로 ntdll!ZwCreateThread() 를 호출합니다. XP 와 7 에서 kernel32!CreateRemoteThread() API 의 호출 흐름을 아래 그림에 나타내었습니다.


<Fig. 7>

따라서 새로 추가된 이 API 때문에 Session 0 에서 실행되는 서비스 프로세스들에게 DLL Injection 이 실패한다고 볼 수 있겠습니다.

kernelbase!CreateRemoteThreadEx() 는 kernel32!CreateRemoteThread() 의 wrapper 라고 한다면 문제의 원인은 ntdll!ZwCreateThreadEx() 에 있을 것 같습니다. ntdll!ZwCreateThreadEx() 는 undocumented API 이므로 MSDN 에서는 함수 정의를 찾을 수 없고, Google 검색으로 찾아야 합니다.

typedef struct 
{
    ULONG    Length; 
    ULONG    Unknown1; 
    ULONG    Unknown2; 
    PULONG   Unknown3; 
    ULONG    Unknown4; 
    ULONG    Unknown5; 
    ULONG    Unknown6; 
    PULONG   Unknown7; 
    ULONG    Unknown8; 
}UNKNOWN, *PUNKNOWN; 


DWORD ZwCreateThreadEx
    PHANDLE                ThreadHandle,
    ACCESS_MASK            DesiredAccess,
    POBJECT_ATTRIBUTES     ObjectAttributes,
    HANDLE                 ProcessHandle,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID                 lpParameter,
    BOOL CreateSuspended,
    DWORD                  dwStackSize,
    DWORD                  dw1, 
    DWORD                  dw2, 
    PUNKNOWN               pUnknown 
);

* XP 이하에서는 지원되지 않음

Google 검색을 하던 중에 아래와 같은 글을 발견하였습니다.


글 내용을 요약하면 Vista 이후의 OS 에서 DLL Injection 을 할 때는 CreateRemoteThread() 대신에 ZwCreateThreadEx() 를 직접 호출하면 잘 된다는 것입니다. 제가 테스트 해보니 Session 에 상관없이 성공하였습니다. (관련 소스는 다음편에서 설명합니다.)

이 방법에서 사용된 파라미터와 <Fig. 6> 의 파라미터를 비교하니 바로 7 번째 파라미터인 CreateSuspended 항목에서 차이를 보였습니다. 즉, ZwCreateThreadEx() 를 직접 호출하여 성공한 경우에는 CreateSuspended 파라미터가 FALSE (0) 인데 반해, CreateRemoteThread() API 내부 에서 호출되는 ZwCreateThreadEx() 호출에서는 CreateSuspended 파라미터가 TRUE (1) 입니다.

* XP 이전부터 CreateRemoteThread() API 의 내부 구현 알고리즘은 일단 suspend 모드로 thread 를 생성한 후 resume 시키는 방법을 사용해 왔습니다. (CreateSuspended = 1)



CreateRemoteThread() 성공 시키는 방법 #1


위와 같이 ZwCrateThreadEx() API 의 CreateSuspended 파라미터의 차이점을 이용한다면 Windows 7 의 서비스 프로세스에 CreateRemoteThread() API 호출도 성공 시킬 수 있을 것 같습니다.

다시 디버깅을 새로 시작하여 위의 <Fig. 5> 까지 진행한 후 스택에 저장된 CreateSuspended 파라미터 값을 1 에서 0 으로 변경합니다.


<Fig. 8>

이 상태로 ZwCreateThreadEx() 호출을 StepOver(F8) 로 넘어가면 아래 그림과 같이 dummy.dll 이 인젝션 됩니다.


<Fig. 9>

DbgView 로 보면 아래 그림과 같이 dummy.dll 의 DllMain() 함수에서 찍은 디버깅 로그가 나타납니다.


<Fig. 10>



CreateRemoteThread() 성공 시키는 방법 #2


kernelbase!CreateRemoteThreadEx() 를 좀 더 디버깅 해보면 몇 가지 사실을 더 알 수 있습니다.

<Fig. 5> 에서 ZwCreateThreadEx() 를 그대로(CreateSuspended 수정 안함 = TRUE) StepOver(F8) 하면 아래 그림과 같이 첫 번째 파라미터 pThreadHandle 에 값이 세팅됩니다. 


<Fig. 11>

스레드 핸들이 생성되었다는 말은 스레드가 정상적으로 생성되었다는 얘기입니다. 즉, CreateRemoteThread() 호출에서도 리모트 스레드 "생성"만큼은 성공했다고 볼 수 있습니다.

하지만 이 리모트 스레드가 정상 동작하지 않았던 이유는 이후에 호출될 ntdll!ZwResumeThread() API 가 실패했거나, 아니면 아예 호출되지 못했거나 둘 중의 하나가 될 것입니다. (suspend 모드로 생성하였으니 resume 해줘야 스레드가 정상적으로 실행됩니다.)

디버깅을 더 진행해서 ZwResumeThread() API 호출 부분까지 따라가 보겠습니다. 아래 그림은 kernelbase!CreateRemoteThreadEx() API 코드의 끝 부분입니다.


<Fig. 12>

위 그림을 보시면 758EBD33 주소의 ntdll!CsrClientCallServer() API 가 호출된 이후에 그 아래쪽의 조건 분기 명령어(CMP/JL)에 의해 ZwResumeThread() API 가 호출되지 않고 점프하는 것을 확인할 수 있습니다. 

디버거에서 ntdll!CsrClientCallServer() API 호출 후에 저 조건 분기를 조작해서 ZwResumeThread() 를 호출하도록 해주면 DLL Injection 이 성공합니다.

Intel IA-32 Reference 에 의하면 JL 명령어는 SF != OF 인 경우 점프하도록 되어 있으므로 아래 그림과 같이 S Flag 를 마우스 더블 클릭하여 변경합니다. 


<Fig. 13>

DLL 인젝션이 정상적으로 되었는지 직접 확인해 보시기 바랍니다.


+---+

지금까지 Windows 7 에서 서비스 프로세스에 DLL Injecion 할 때 kernel32!CreateRemoteThread() API 호출이 왜 실패하는지 알아보았습니다. 또한 kernel32!CreateRemoteThread() API 를 디버깅해서 결국 DLL Injection 이 성공하도록 실습해봤습니다. (디버깅을 이용하는 방법이라서 범용적으로 사용하기에는 불편합니다.)

다음 시간에는 Windows 7 (or Vista) 뿐만 아니라 XP 에서도 범용적으로 사용할 수 있는 InjectDll.exe 를 만들어 보도록 하겠습니다.


ReverseCore

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

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

  1. 늅늅 2010/02/22 17:58 댓글주소 | 수정 | 삭제 | 댓글

    앗 1등!!!
    선리플 후 감상 입니다!!!

  2. 비셔스 2010/02/22 20:50 댓글주소 | 수정 | 삭제 | 댓글

    좋은 정보 감사합니다!!

  3. 철이 2010/02/22 20:55 댓글주소 | 수정 | 삭제 | 댓글

    정말로 실력이 부럽습니다..........
    ㅜㅜ 언제쯤 이런 강의를 적을수있을가요 ㅜㅜ

  4. 철이 2010/02/23 04:40 댓글주소 | 수정 | 삭제 | 댓글

    당연히 칭찬이죠 ㅜㅜ 존경 ㅜㅜ

  5. upx 2010/02/23 17:35 댓글주소 | 수정 | 삭제 | 댓글

    저도 강의보고 놀래고있습니다..
    어떻게 저렇게 설명을 저는 죽었다 깨어나면 할라나요.^^

  6. 허훈 2010/03/21 15:39 댓글주소 | 수정 | 삭제 | 댓글

    음,, 저는 일단 2가지 방법을 수행해도 절대 성공하지 않는군요

    과정은 똑같지만 결과가 다르게 나오네요.

    좀더 분석해봐야겠습니다.

    • reversecore 2010/03/22 09:29 댓글주소 | 수정 | 삭제

      허훈님, 안녕하세요.

      위의 설명대로 해도 잘 안되셨나 보네요.

      각 API 의 리턴값과 에러 코드를 확인해서 저에게 알려주시면 저도 좀 확인해 보겠습니다.

      감사합니다.

  7. 너굴히 2010/09/06 15:27 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 reversecore님^^
    Vista나 Win7의 경우 최근에는 64bit OS를 많이 사용하고 있는데요.
    이 경우 dll injection 시 고려해야 할 점은 뭐가 있을까요?
    좋은 글 감사합니다~

    @저는 어플 개발만 10년 했는데 low level을 공부하면서 다시금 개발에 흥미를 느끼고 있는 프로그래머입니다 :)

  8. 2011/09/11 22:28 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

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

      안녕하세요.

      ZwCreateThreadEx() 의 함수 파라미터를 문의하시는 것인가요?


      DWORD ZwCreateThreadEx
      (
      PHANDLE ThreadHandle,
      ACCESS_MASK DesiredAccess,
      POBJECT_ATTRIBUTES ObjectAttributes,
      HANDLE ProcessHandle,
      LPTHREAD_START_ROUTINE lpStartAddress,
      LPVOID lpParameter,
      BOOL CreateSuspended,
      DWORD dwStackSize,
      DWORD dw1,
      DWORD dw2,
      PUNKNOWN pUnknown
      );

      이걸 참고하시기 바랍니다.


      감사합니다.



Windows 7 (& Vista) 에서 DLL Injection 하는 방법에 대한 설명입니다.
새로 변경된 Session 정책에 의하여 기존 CreateRemoteThread() API 를 이용한 DLL Injection 방법이 일부 프로세스들에게 제대로 동작하지 않습니다. 정확한 증상을 알아보고 문제 발생 원인과 해결 방법에 대하여 알아보도록 하겠습니다.

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



DLL Injection


예전에 DLL Injection 에 대해서 연재를 한 적이 있습니다.


또한 API Hooking 연재를 하면서 DLL Injection 기법을 사용하였습니다.


위에서 소개한 DLL Injection 기법은 CreateRemoteThread() API 를 사용하는 방법으로써 Windows XP, 2000 에서는 아주 정확하게 잘 동작합니다.
하지만 Windows 7 (& Vista) 에서 약간 다르게 동작합니다. 정확하게 말하면 기존 CreateRemoteThread() API 를 이용한 방법으로는 7 (& Vista) 의 서비스(Service) 프로세스에 DLL Injection 이 되지 않습니다. 

그 이유는 7 (& Vista) 에서 적용된 Session 관리 정책의 변경 때문입니다. (아래 링크 참고!)

간단한 프로그램을 사용하여 DLL Injection 실패 상황을 재현해 보도록 하겠습니다.



InjectDll.exe & dummy.dll


실습용 파일과 소스 코드입니다.

InjectDll.cpp 소스 코드에서 핵심 함수인 InjectDll() 를 아래에 표시하였습니다.

BOOL InjectDll(DWORD dwPID, char *szDllName)
{
    HANDLE hProcess = NULL;
    HANDLE hThread = NULL;
    LPVOID pRemoteBuf = NULL;
    FARPROC pThreadProc = NULL;
    DWORD dwBufSize = strlen(szDllName)+1;

    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
    {
        printf("[ERROR] OpenProcess(%d) failed!!! [%d]\n", 
        dwPID, GetLastError());
        return FALSE;
    }

    pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, 
                                MEM_COMMIT, PAGE_READWRITE);

    WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName, 
                       dwBufSize, NULL);

    pThreadProc = GetProcAddress(GetModuleHandle(L"kernel32.dll"), 
                                 "LoadLibraryA");

    hThread = CreateRemoteThread(hProcess, NULL, 0, 
                                 (LPTHREAD_START_ROUTINE)pThreadProc, 
                                 pRemoteBuf, 0, NULL);
    if( hThread == NULL )
    {
        printf("[ERROR] CreateRemoteThread() failed!!! [%d]\n", GetLastError());
        return FALSE;
    }

    WaitForSingleObject(hThread, INFINITE);

    VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

지금까지 익히 보아오던 DLL Injection 의 전형적인 소스 코드에다가 리턴값 체크를 추가하였습니다.


그리고 아래는 dummy.dll 파일의 소스 코드(dummy.cpp)입니다.

#include "windows.h"
#include "tchar.h"

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    TCHAR   szPath[MAX_PATH]    = {0,};
    TCHAR   szMsg[1024]         = {0,};
    TCHAR   *p                  = NULL;

    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH : 
            GetModuleFileName(NULL, szPath, MAX_PATH);
            p = _tcsrchr(szPath, L'\\');
            if( p != NULL )
            {
                _stprintf_s(szMsg, 1024 - sizeof(TCHAR), 
                            L"Injected in %s(%d)", 
                            p + 1,                          // Process Name
                            GetCurrentProcessId());         // PID
                OutputDebugString(szMsg);
            }
            
            break;
    }

    return TRUE;
}

DllMain() 함수는 아주 간단합니다. dummy.dll 파일이 프로세스에 정상적으로 인젝션 되었다면 디버그 메시지(프로세스 이름, 프로세스 ID)를 출력하는 것입니다.



테스트


Process Explorer 를 실행시킨 후 Session 0 의 svchost.exe (PID : 3148) 프로세스와 Session 1 의notepad.exe(PID : 2172) 프로세스에 각각 dummy.dll 파일을 인젝션 시켜보도록 하겠습니다.

 
<Fig. 1>

위에서 첨부한 InjectDll.exe 와 dummy.dll 파일을 적당한 폴더에 다운받은 후 아래 그림과 같이 실행합니다. 

 
<Fig. 2>

Session 1 의 notepad.exe (2172) 프로세스에는 정상적으로 인젝션 되었지만, Session 0 의 svchost.exe (3148) 프로세스에는 인젝션이 실패(error code = 8)하였습니다. 

Process Explorer 에서 dummy.dll 모듈을 검사해봅니다.


<Fig. 3>

위 그림에서 보시는 것처럼 Session 1 의 notepad.exe (2172) 프로세스에게만 정상적으로 dummy.dll 파일이 인젝션 되었습니다.



디버깅


<Fig. 2> 에서 Session 0 의 svchost.exe (3148) 프로세스에 인젝션 할 때 CreateRemoteThread() API 함수 호출에서 실패하였고, 그 때의 에러 코드는 8 입니다.

OllyDbg 를 이용해서 InjectDll.exe 파일을 디버깅 해보겠습니다.
InjectDll.exe 파일을 파라미터("3148 c:\work\dummy.dll")를 입력해서 Open 합니다.


<Fig. 4>

CreateRemoteThread() API 호출 시에 에러가 발생하는 것을 알았으니, Search for \ All intermodular calls 메뉴를 이용해서 해당 API 호출 코드에 바로 BP 를 설치합니다.


<Fig. 5>

* 참고!
Windows 7 에서 InjectDll.exe 프로세스의 Base Address 는 랜덤하게 변경됩니다.

디버거를 Run 시키면 <Fig. 5> 의 BP 에 멈추게 됩니다.


<Fig. 6>

여기서 그대로 F8 (StepOver) 로 진행하면 아래 그림과 같이 LastErr = ERROR_NOT_ENOUGH_MEMORY (8) 가 찍힙니다.


<Fig. 7>

에러 메시지도 낯설고 아직은 뭐가 문제인지 잘 모르겠군요.

다음에 이어지는 글에서 InjectDll.exe 의 CreateRemoteThread() 를 좀 더 디버깅 해보겠습니다.



ReverseCore

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

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

  1. eew 2010/02/20 03:12 댓글주소 | 수정 | 삭제 | 댓글

    기대기대

  2. 철이 2010/02/20 05:20 댓글주소 | 수정 | 삭제 | 댓글

    정말로 많이 배웁니다. 빨리 연재 해주세요..;

  3. 늅늅 2010/02/20 23:11 댓글주소 | 수정 | 삭제 | 댓글

    dll injection 관련해서 새로운 글이 올라왔군요~!
    자주 컴터를 할 수 없어서 감이 팍팍 떨어지는데, 오랫만에 다시 코딩을 해봐야겠네요

    좋은 자료 잘 보고 갑니다 :)

  4. 퉁퉁 2011/10/13 09:29 댓글주소 | 수정 | 삭제 | 댓글

    이번 과제로 dll injection이 나왔는데 너무 어렵네요..ㅠㅠ

    어떤 실행파일을 실행하면 20초후에 어떤 주소값이 나오는데 그 주소에는 멀티쓰레드를 이용하여 만들어진 어떤 문자열이 저장되어있대요..

    저장된 문자열을 탈취하여 확인하기위해 실행중인 다른 process에 thread를 생성하여 dll을 injection 하는 공격으로 문자열을 확인하는 건데..

    위에 설명을 참고해서 짜면되겠..........ㅈ..요..? ㅠㅠㅠㅠㅠㅠㅠ

  5. eee 2011/10/30 03:08 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요^^ 이쪽에는 초보입니다.

    64bit win7 에서 64bit 로 컴파일햇는데 에러 코드 1300 이 나와요 ㅠㅠ

    도와주세요!



후킹 코드를 디버깅하면서 후킹된 IAT 메모리 영역을 확인해 보겠습니다.

또한 프로세스에 인젝션(Injection)된 DLL 파일을 디버깅하는 법을 살펴보도록 하겠습니다.



<IAT Hooking 코드>

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

API Hooking – 계산기, 한글을 배우다. (1)
API Hooking – 계산기, 한글을 배우다. (2)
API Hooking – 계산기, 한글을 배우다. (3)



인젝션(Injection)된 DLL 의 디버깅


계산기(calc.exe) 프로세스에 인젝션(Injection)된 hookiat.dll 을 디버깅 하겠습니다.

DLL Injection 에 대한 더 자세한 내용은 아래 글을 참고해 주세요.
DLL Injection – 다른 프로세스에 침투하기 (1)

인젝션(Injection)된 DLL 의 디버깅 방법에 대한 더 자세한 내용은 아래 글을 참고해 주세요.
키로거(KeyLogger) 분석 (2)

계산기 프로그램을 실행하신 후 Process Explorer 로 PID 값을 확인합니다.

<Fig. 1>

이제 OllyDbg를 calc.exe 프로세스에 attach 시킵니다.

<Fig. 2>


* 참고!
OllyDbg 2.0 beta 2 를 사용합니다. 기존 1.10 버전은 Injection DLL을 디버깅 할 때 약간의 버그가 있어서 debuggee 프로세스가 종료되는 경우가 있습니다.


Attach 가 잘 되었으면 아래 그림과 같이 OllyDbg 의 옵션을 수정하여 DLL 파일(hookiat.dll)이 Injection 될 때 제어가 디버거에게 넘어오도록 합니다.

<Fig. 3>

"Pause on new module (DLL)" 옵션을 체크하면 debuggee 프로세스에 DLL 이 로딩(injection 포함) 될 때마다 제어가 debugger 에게 넘어옵니다.

이제 준비를 마쳤으니 OllyDbg 를 Run [F9] 시켜서 계산기 프로세스가 정상적으로 실행되도록 합니다.

InjectDll.exe 에 파라미터를 적절히 주고 실행하면 계산기 프로세스에 hookiat.dll 이 인젝션 됩니다. (아래 그림 참고)


<Fig. 4>


calc.exe 프로세스에 “DLL Load” 이벤트가 발생하였으므로, OllyDbg 에 해당 이벤트가 통지되고 <Fig. 3> 의 옵션에 따라서 아래 그림처럼 hookiat.dll 의 EP(EntryPoint) 에서 실행이 멈추게 됩니다.


<Fig. 5>


* 로딩된 DLL 의 EP 에 자동으로 멈추는 것은 OllyDbg 2.0 에서 지원되는 기능입니다. 이전 버전에서는 EP 가 아닌 다른 코드 위치(ntdll.dll 영역)에서 실행이 멈춥니다.

이제 <Fig. 3> 의 “Pause on new module (DLL)” 옵션을 해제(uncheck)하고 DllMain() 코드를 찾아 보겠습니다.
 
디버거에서 hookiat.dll 의 DllMain() 함수로 쉽게 찾아가는 방법은 DllMain() 에서 사용되는 문자열 혹은 API 를 검색하는 것입니다. (물론 하나 하나 StepIn 명령으로 따라가셔도 좋습니다.)

아래에 DllMain() 코드를 표시하였습니다.
(자세한 코드 설명은 API Hooking – 계산기, 한글을 배우다. (3) 포스트를 참고하세요.)

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH : 
            // original API 주소 저장
            g_pOrgFunc = GetProcAddress(GetModuleHandle(L"user32.dll"),
                                                        "SetWindowTextW");
            // # hook
            //   user32!SetWindowTextW() 를 hookiat!MySetWindowText() 로 후킹

            hook_iat("user32.dll", g_pOrgFunc, (PROC)MySetWindowTextW);
            break;

        case DLL_PROCESS_DETACH :
            // # unhook
            //   calc.exe 의 IAT 를 원래대로 복원

            hook_iat("user32.dll", (PROC)MySetWindowTextW, g_pOrgFunc);
            break;
    }

    return TRUE;
}

<Code 1>

코드에서 사용되는 "user32.dll" 과 "SetWindowTextW" 문자열로 DllMain() 을 찾아보겠습니다.

<Fig. 6>

위 그림을 보시면 "user32.dll" 문자열은 2 군데가 있고, "SetWindowTextW" 문자열은 1 군데가 있습니다. "SetWindowTextW" 문자열이 참조되는 코드 주소 1000113E 로 가봅니다.

<Fig. 7>

위 그림에서 빨간 테두리의 디스어셈 코드는 <Code 1> 의 C 언어 코드와 정확히 일치합니다. 따라서 이 부분이 DllMain() 입니다. (참고로 DllMain() 의 시작 주소는 10001130 입니다.)

여기까지가 프로세스에 인젝션된 DLL 을 디버깅하는 방법에 대한 설명입니다. (이외에도 여러 가지 꼼수가 있습니다만 위에 소개한 방법이 정석이라고 할 수 있습니다.)



DllMain()


앞에서 구한 DllMain() 함수 위치부터 디버깅을 시작해 보겠습니다. (위의 <Code 1> 과 비교하면서 보시면 좀 더 쉽게 파악할 수 있습니다.)

DllMain() 에서 조금만 디버깅해 내려가면 아래와 같은 코드가 나타납니다.

<Fig. 8>

10001160 주소의 CALL 10001090 명령이 바로 hook_iat() 함수 호출 부분입니다. 함수 파라미터는 역순으로 스택에 저장되므로 각 파라미터의 의미는 주석으로 쓰여진 부분 그대로 입니다. (<Code 1> 과 <Fig. 8> 를 함께 비교해서 보세요.)

한가지 특이한 사항은 <Code 1> 에서 보면 hook_iat() 함수는 파라미터가 3 개입니다. 그러나 <Fig. 8> 에서 hook_iat() 는 파라미터가 2 개뿐입니다. (Stack Window 참조)

잘 보시면 hook_iat() 의 첫 번째 파라미터인 "user32.dll" 문자열이 생략된 걸 알 수 있습니다. 이것은 VC++ 2008 컴파일러의 코드 최적화 기능이 적용되어서 문자열 주소 (4 byte 상수) 는 함수 파라미터로 넘기지 않고 hook_iat() 함수 내에 하드코딩 해버린 것입니다.

앞으로 여러분이 직접 작성한 프로그램을 디버깅 하실 때 위와 같은 코드 최적화 기능을 자주 보게 되실 겁니다.



hook_iat()


실제로 IAT 를 후킹하는 핵심 함수인 hook_iat() 입니다.


# user32.dll 의 IMAGE_IMPORT_DESCRIPTOR 찾기

<Fig. 9>

위 그림에서 빨간색으로 표시된 부분의 코드는 PE Header 에서 어떻게 IMAGE_IMPORT_DESCRIPTOR Table (이하 IID Table) 을 찾아내는지 보여줍니다. (IID 를 PEView 에서는  Import Directory Table 로 표시합니다.)

위 코드(100010A1 ~ 100010AD)에서는 어셈블리 명령어 4 개만을 써서 IID Table 을 따라갑니다.

아직 PE Header 구조에 익숙하지 않으신 분들 또는 위와 같은 어셈블리 코드를 처음 접하신 분들께서는 코드의 내용이 잘 이해되지 않으실 겁니다.

이건 숙련도의 문제이기 때문에 계속해서 보시면 점차 나아집니다. 나중에는 [EDI+3C], [EDI+EAX+80] 등의 코드만 봐도 이게 대충 IID Table 을 따라가는 코드라는걸 알 수 있게 됩니다.


100010CA 주소의 CALL 100075CA 명령어는 stricmp() 함수 호출 코드입니다. IID Table 을 훑으면서 IID.Name 항목과 "user32.dll" 문자열을 비교하여 user32.dll 에 해당하는 IID 를 찾습니다.


# IAT 에서 SetWindowTextW API 위치 찾기

user32.dll 에 해당하는 IID 를 찾았다면 이제는 IAT(Import Address Table) 에서 SetWindowTextW API 를 찾는 코드가 이어집니다. (아래 그림 참고)

우리는 그 위치의 내용을 바꿔 쳐서 API 를 후킹할 겁니다. 

<Fig. 10>

100010E0 주소의 CMP DWORD PTR DS:[ESI], EBP 명령어에서 ESI 값은 user32.dll 의 IAT(Import Address Table) 의 시작 주소(010010A4) 입니다. 그리고 EBP 의 값은 SetWindowTextW 의 주소(77D0960E) 입니다.

즉, <Fig. 10> 의 코드는 루프를 돌면서 IAT 를 따라 내려가다가 01001110 주소의 SetWindowTextW 주소값(77D0960E) 을 찾는 내용입니다.

* 참고!
OllyDbg 의 Memory Window (<Fig. 10> 의 빨간 테두리 부분) 의 보기 옵션을 “Integer -> Address” 로 변경하면 <Fig. 10> 과 같이 [Address & API Name] 형식으로 볼 수 있습니다.


# IAT Hooking

이제 실제로 IAT 를 후킹하는 코드 입니다.

<Fig. 11>

10001117 주소의 MOV DWORD PTR DS:[ESI], EDX 명령이 바로 앞에서 구한 IAT 에서 SetWindowTextW 위치(01001110)에 후킹 함수 MySetWindowTextW 주소(10001000)를 덮어쓰는 코드입니다.

01001110 주소는 calc.exe 프로세스에서 user32.dll 의 IAT 영역입니다. 원래 이 위치에는 SetWindowTextW 주소값(77D0960E) 이 저장 되어 있었습니다. (<Fig. 10> 참고)

10001117 주소의 명령어에 의해서 user32!SetWindowTextW 주소값(77D0960E)이 hookiat!MySetWindowTextW 주소값(10001000)으로 변경됩니다. (<Fig. 11> 참고)

이제부터 calc.exe 프로세스내의 코드에서 (IAT 를 통해) user32!SetWindowTextW() API 를 호출하면 실제로는 hookiat!MySetWindowTextW() 가 호출됩니다.



MySetWindowTextW()


앞에서 IAT 후킹이 완료되었으므로 OllyDbg 를 실행[F9] 시켜서 계산기(calc.exe) 프로세스가 정상적으로 동작하게 만듭니다.

calc.exe 프로세스에서 user32!SetWindowTextW() API 를 호출하는 코드에 BP를 설치하여 실제로 hookiat!MySetWindowTextW() 함수가 호출되는 상황을 디버깅 해보겠습니다.

먼저 user32!SetWindowTextW() 호출 코드에 BP 를 설치합니다. OllyDbg 의 “Search for – All intermodular calls” 기능을 쓰면 아래와 같은 다이알로그가 나타납니다.

<Fig. 12>

calc.exe 프로세서에서 user32!SetWindowTextW() API 를 호출하는 위치는 총 2 곳입니다. 2 곳 모두 BP 를 설치합니다. (실제로는 01002628 주소의 명령어가 우리가 찾는 위치입니다.)

그리고 계산기 프로그램에 숫자 '1' 을 입력하면 (위에서 설치한) 01002628 주소의 BP 에 걸립니다. (아래 그림 참고)

<Fig. 13>

01001110 주소에는 원래 user32!SetWindowTextW() 주소(77D0960E)가 있었으나, 후킹된 이후에는 <Fig. 13> 에서 보듯이 hookiat!MySetWindowTextW() 주소(10001000)가 있습니다.

MySetWindowTextW() 함수 안으로 디버깅해 들어가면 아래 그림과 같은 코드가 나타납니다.

<Fig. 14>

MySetWindowTextW() 함수의 기능은 "숫자" 문자열을 "한글" 문자열로 변환한 후 원래 함수인 user32!SetWindowTextW() API 를 호출하는 것입니다.

1000107D 주소의 CALL DWORD PTR DS:[1000B6B8] 명령어가 바로 user32!SetWindowTextW() API 를호출하는 코드입니다. 1000B6B8 주소는 hookiat.dll 에서 .data 섹션의 전역변수(g_pOrgFunc) 를 의미합니다. DllMain() 에서 이곳에 미리 SetWindowTextW 주소를 저장해 두었습니다. (<Code 1> & <Fig. 8> 참고)

이것으로써 calc.exe 프로세스에 인젝션 된 hookiat.dll 을 디버깅을 완료하였습니다.

+---+

총 4 회에 걸쳐서 API Hooking 기법 중에서 "Dll Injection" 과 "IAT Hooking" 을 사용해서 계산기 프로세스를 후킹해 보았습니다.

다음 번에는 API Code 를 직접 패치하는 후킹 기법에 대해서 알아보겠습니다. 또한 시스템에서 실행되는 모든 프로세스들과 이후에 실행되는 모든 프로세스들에 대해서 전부 API Hooking 시키는 기술인 global Hooking 기법에 대해서 살펴보겠습니다.

재미 있으실 겁니다. 많이 기대해 주세요~ ^^

감사합니다.

ReverseCore

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

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

  1. 김대성 2009/11/27 02:27 댓글주소 | 수정 | 삭제 | 댓글

    정말 재밌게읽었습니다.... 아래서 부터 읽고 올라오다보니 새로운글이있네요 ㅎ 시간가는줄모르고 읽었습니다. 리버싱에 관심갖고 리버스엔지니어링비밀을파헤치다 책을 샀는데 초심자에게는 어려운중급책이더군요.. 그래서 실망하고있던차에 이렇게 좋은 사이트를 찾게되어서 너무기쁩니다 ^^

  2. 냥냥 2009/11/27 07:03 댓글주소 | 수정 | 삭제 | 댓글

    요즘... 낮생활로 바꿔가고 있어요 ㅠ_ㅠ... 1등은 힘드네요..ㅎㅎㅎ
    항상좋은 자료감사합니다~..~ 저도 블로그 만들었어요 .. ㅋㄷ

  3. Sun2Day 2009/11/27 13:45 댓글주소 | 수정 | 삭제 | 댓글

    항상 재미있는 글 감사합니다 '-'/

    다음 글도 기대하고 있습니다 +_+(번뜩)

  4. 늅늅 2009/11/30 09:36 댓글주소 | 수정 | 삭제 | 댓글

    아 맨날 늦어 -_-; 오늘도 한주의 시작은 이걸로 시작합니다 ㅎㅎ
    주인장님 어서 이 내용들로 책 내주세요~ 너무 잘 나와있는데..많은 이들에게 도움이 될 거 같아요~

  5. mAn1aS 2009/12/23 10:48 댓글주소 | 수정 | 삭제 | 댓글

    아 그런데 궁금한게 있습니다.
    rand 같은경우는 IAT에 포함되어 있지 않더라구여..
    detour에서도 찾질 못하는데, rand 같은 api는 어떻게 찾아서 후킹해야 할까요..
    메모리에 call 되는부분 주소를 수동으로 직접 알아서 코드 패치 할 순 있지만, 범용적이지 않은 것 같구..

    범용적으로 rand 를 후킹할 방이 있을까요?

    rand 쓰는 테스트 프로그램에 아래와 같이 rand의 주소를 얻어모면 정상적으로 얻어오는데...
    static int (WINAPIV * TrueRand)(void) = rand;

    dll에 위 주소를 얻게하고, rand 테스트 프로그램에 삽입하면, 엉뚱한 주소가 나오네요-0-

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

      mAn1aS님, 안녕하세요.

      IAT 에 없는 API 를 후킹하는 방법은 지금 진행되고 있는 강좌 - "API Hooking - '스텔스' 프로세스" 를 참고하시면 될것 같습니다.

      API 시작 코드를 직접 패치하는 방식입니다.

      msvcrt!rand() API 를 후킹 하시면 되겠네요.

      그리고 API 주소를 얻을 때는 GetProcAddress() 를 이용하시면 됩니다.

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

      감사합니다.



API Hooking 기법 중에서 DLL Injection 기법을 설명 드리겠습니다.

프로세스에 Injection 된 DLL 파일은 IAT(Import Address Table)후킹(hooking)하여 프로세스에서 호출 되는 특정 API 의 기능을 변경시킵니다.

Windows 계산기(calc.exe) 프로세스에 dll 을 삽입시켜 IAT의 user32!SetWindowTextW() API 주소를 후킹합니다. SetWindowTextW() API 가 후킹당한 계산기는 계산결과를 숫자가 아닌 한글로 출력하게 됩니다.


<SetWindowTextW() 가 후킹된 계산기 프로세스>

* 본문 내용과 관련된 정보는 아래와 같습니다.

PE(Portable Executable) File Format (1)
- PE(Portable Executable) File Format (6)
DLL Injection – 다른 프로세스에 침투하기
API Hooking – 리버싱의 ‘꽃’



Tech Map



<Fig. 1>

위 그림은 API Hooking TechMap 에서 "DLL Injection 을 통한 IAT 후킹 기법"빨간색으로 보여줍니다.

이 방식의 장점은 동작 원리와 구현이 비교적 간단하다는 것입니다. (원하는 API 를 사용자 DLL 에 재정의 하고 프로세스에 Injection 시키면 됩니다.)

단점으로는 후킹을 원하는 API 가 대상 프로세스의 IAT 에 존재하지 않다면 사용할 수 없다는 것입니다. 즉, 프로그램 코드 상에서 동적으로 DLL 을 로딩해서 사용하는 API 의 경우는 이 방법으로 후킹할 수 없습니다.



대상 API 선정


작업 목표를 설정한 후 API 후킹 기법을 사용하겠다고 결정하였다면, 그 다음으로 중요한 작업은 후킹 대상 API 를 선정하는 작업입니다.

초보자 분들께서는 어쩌면 이 부분에 어려움을 겪으실 수 있겠습니다.
왜냐하면 후킹을 원하는 기능을 제공하는 API 가 뭔지 알아야 하기 때문입니다.

예를 들면 파일 생성은 kernel32!CreateFile() API, 레지스트리 생성은 advapi32!RegCreateKeyEx() API, 네트워크 접속은 ws2_32!connect() API 등에서 담당합니다.

개발/리버싱 경험이 많다면 원하는 API 를 쉽게 떠올릴 수 있지만, 그렇지 않다면 검색이 필요합니다. 공개된 API 말고 공개되지 않은 undocumented API 를 후킹해야 하는 상황도 있기 때문에 검색은 필수입니다. 검색이 잘 안될 때는 일단 경험(또는 직관)에 의존하여 선택한 후 검증 작업을 거치면 됩니다.

적절한 API 를 선택하기 전에 먼저 작업 목표를 설정합니다.
이번 포스트의 실습 목표는 "계산기의 텍스트 에디터에 표시되는 모든 숫자를 한글로 변경" 하기입니다.

PE View 등의 유틸리티를 이용해서 계산기(calc.exe)에서 import 하는 API 를 확인합니다.

<Fig. 2>

위 그림을 보시면 2개의 API 가 눈에 띕니다. 바로 SetWindowTextW(), SetDlgItemTextW() 입니다.

두 API 모두 텍스트 에디터에 글씨를 써주는 역할을 합니다. 그런데 SetDlgItemTextW() 는 내부적으로 다시 SetWindowTextW() 를 호출하기 때문에 여기서는 SetWindowTextW() 를 후킹하면 될 것이라고 일단 가정합니다.

SetWindowTextW() API 의 정의를 보시겠습니다.

BOOL SetWindowText(     
    HWND hWnd,
    LPCTSTR lpString
);

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

이 API 는 파라미터로 윈도우 핸들(hWnd)과 문자열 포인터(lpString)를 받습니다.
우리가 딱 원하는 파라미터(lpString)가 있네요. 실제 후킹할 때는 저 문자열(lpString)을 살펴보고 숫자를 한글로 변경하면 될 것 같습니다.

*참고!
API 이름 뒤 ‘W’ 의 의미는 해당 API 의 “Wide character” 버전을 의미합니다. 이와 대응해서 같은 이름으로 끝에 ‘A’ 가 붙은 API 들이 있는데 이는 “ASCII character” 버전을 의미합니다.
예) SetWindowTextA(), SetWindowTextW()

OllyDbg 로 위 가정을 "검증" 해보겠습니다.

<Fig. 3>

위 그림과 같이 Search for - All intermodular calls 명령을 사용하여 계산기(calc.exe) 코드 내에서 호출되는 모든 SetWindowTextW() API 에 대해서 BP 를 설치한 후 실행합니다.

실행시키자마자 아래 그림과 같이 BP 에 걸립니다.

<Fig. 4>

SetWindowTextW() API 의 lpString 파라미터는 스택(ESP+4) 에 있는 7FB5C 입니다. (OllyDbg 에서는 "Text" 라고 표시해 주는군요.)

7FB5C 주소에 가면 "0. " 문자열이 wide character 형식으로 저장되어 있습니다. 이 문자열은 계산기에 있는 텍스트 창의 초기값입니다.

이 상태에서 그대로 실행해 보겠습니다.

<Fig. 5>

위 <Fig. 5> 그림과 같이 계산기(calc.exe)가 정상적으로 실행되고 텍스트 창에 <Fig. 4> 에서 보았던 "0. " 문자열이 나타납니다. (". " 은 계산기에서 자동으로 붙여주는 문자열입니다.)

좀 더 디버깅을 하기 위해서 계산기에 숫자를 입력해 보겠습니다.
숫자 7 을 입력 하였습니다. 이미 BP 가 걸려있기 때문에 <Fig. 4> 와 동일한 위치에서 멈춥니다.

<Fig. 6>

위의 그림에서는 Text 파라미터의 값이 7F978 로 <Fig. 4> 의 7FB5C 값과는 틀립니다만, 7F978 주소에 지금 입력한 “7“ 이 보입니다. (끝에 있는 “. “ 문자열은 계산기에서 자동으로 추가해주는 문자열입니다.)

테스트를 위해서 이 숫자 “7” 을 한글 “칠” 로 바꿔 보겠습니다.

한글 “칠” 에 해당하는 wide character 는 0xCE60 입니다.

이 값을 아래와 같이 7F978 주소에 덮어쓰겠습니다.

<Fig. 7>

Intel Architecture 계열은 Little Endian 표기법에 따라서 역순(60CE)으로 써줘야 한다는 걸 기억하세요.

위와 같이 SetWindowTextW() API 의 lpString(또는 Text) 파리미터 내용을 변경한 후 실행하면 아래 그림과 같이 숫자 "7" 이 한글 "칠" 로 변경됩니다.

<Fig. 8>

이것으로써 SetWindowTextW() API 에 대한 검증이 완료되었습니다.


다음 포스트에서 IAT 후킹의 동작 및 구현 원리를 알아보고, 계산기의 SetWindowText() API 의 IAT 후킹 소스 코드에 대해서 살펴보도록 하겠습니다.


API Hooking - 계산기, 한글을 배우다. (2)


+---+

ReverseCore


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

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

  1. 냥냥 2009/11/10 01:37 댓글주소 | 수정 | 삭제 | 댓글

    이번에도 제가 1등인가요..?! > <// ㅎㅎ
    항상 감사합니다~

  2. 늅늅 2009/11/10 09:53 댓글주소 | 수정 | 삭제 | 댓글

    2등이군요.

    이런데서 등수놀이 할 줄이여 -.-;;

    text 로만 보면 어려울 수 있는 내용을 그림과 함께 잘 설명해 주셔서 감사합니다 ^^

  3. 이승철 2009/11/10 17:38 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요!@ 이승철입니다.

    다름이 아니라 리플을 읽어 보니깐 오프라인 강의나 책을 출판 하신다고 하시는데

    혹시 어디에 사세요? 서울 사시나요? 전 부산이라서 ㅜㅜ

    출판 하게 되신다면 윤성우강사님처럼 친철한 강의 원하는데

    • ReverseCore 2009/11/10 22:39 댓글주소 | 수정 | 삭제

      이승철님, 안녕하세요.

      전 서울입니다. ^^

      출판, 강연은 아직 먼 미래의 일이구요...

      음... 저도 친절하고 싶어요 ^^

  4. safscx 2009/11/10 21:52 댓글주소 | 수정 | 삭제 | 댓글

    굿

  5. 2009/11/11 20:57 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

  6. 뿡뿡이 2009/11/12 00:25 댓글주소 | 수정 | 삭제 | 댓글

    우와...

    리버싱 공부 시작한지 이제 3일된 초보자 입니다....

    관련 자료들 찾던중...이렇게 재미있고 괜찮은 강좌가 있눈 사이트를 발견한게

    정말 행운이라고 생각합니다!!!! 아직 초보라 강좌 내용은 잘모르지만..

    그래도!! 1시간 가량 정말 잼있게 읽었습니다..ㅎㅎㅎ

    앞으로도 자주 들러볼께요!!

    좋은 강좌 많이 해주세용 ㅋㅋㅋ

  7. 부자봉스 2009/11/12 00:36 댓글주소 | 수정 | 삭제 | 댓글

    참 잘 보고 있습니다.. 지금 시작이지만 좋은 공부가 될거 같네요

  8. 부자봉스 2009/11/13 02:00 댓글주소 | 수정 | 삭제 | 댓글

    일일이 답변주시고 감사합니다

  9. 쭈욱 2010/01/20 16:51 댓글주소 | 수정 | 삭제 | 댓글

    정성껏 작성하신 글 잘 봤습니다. 감사합니다~
    앞으로도 계속 도움이 될거 같네요. 후킹은 첨이라서 얼떨떨하네요.

  10. 경서기 2010/02/19 17:29 댓글주소 | 수정 | 삭제 | 댓글

    후킹... 다른 글은 어려운 말로 나를 괴롭혔는데...
    여기 글 보고... 한번 해봐야겠다는 의욕이 생기네요 ^^
    좋은 감사합니다.

    • reversecore 2010/02/19 23:52 댓글주소 | 수정 | 삭제

      안녕하세요.

      제 글을 읽고 의욕이 생기셨다니 기쁘네요. ^^

      후킹을 공부하시다가 질문이 있으시면 올려주세요~

      감사합니다.

  11. 시간의흔적 2010/03/26 15:53 댓글주소 | 수정 | 삭제 | 댓글

    정말 글을 잘 읽고 있습니다 ^^ 넘넘 감사드리구요..
    눈으로만 보다가 실제로 해보니 궁금한 점이 생겨서 질문을 들려요..
    만약 칠. -> HEX code로 바꿀때.. 다른 툴을 쓰시는 건지 아님 OllyDbg를 쓰시는건지 ^^;;
    넘 기본적인 질문인가요?? OllyDbg를 첨써봐서 ^^
    지금 열심히 따라하고 있습니다 ㅎㅎ..

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

      시간의흔적님, 안녕하세요.

      OllyDbg 는 아니구요.
      예전에 프로그래머로 일할때 이미 알고 있던 내용입니다.

      간단히 VC++ 에서 아래와 같이 프로그래밍하신 후 디버깅 하시면 해당 값을 알 수 있습니다.

      wchar_t wc = L"칠";

      따라 하시다가 다른 궁금한 내용 있으시면 질문 올려주세요.

      감사합니다.

    • Ezbeat 2010/04/11 14:12 댓글주소 | 수정 | 삭제

      저도 그거때문에 고생한 적이 있어요~!
      댓글 보고 바로 프로그램으로 만들어봤어요 ^^;

      http://ezbeat.tistory.com/174
      항상 디버거로 열어서 보기 귀찮으시다면.. 써주세요
      ㅜ ㅜㅋㅋ

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

      Ezbeat님, 안녕하세요.

      답변 감사드리고요,
      좋은 프로그램 만들어 주셔서 감사합니다.

  12. 시간의흔적 2010/03/29 11:40 댓글주소 | 수정 | 삭제 | 댓글

    아 그렇군요 ^^ OllyDbg에서 안되길래.. 다른 프로그램을 쓰시는줄 알았습니다.
    감사합니다~

  13. 안정현 2010/06/28 11:59 댓글주소 | 수정 | 삭제 | 댓글

    search-무슨무슨 모듈콜 이거있자나요 ㅋ 아무것도안뜨는건 무슨현상이죵 ㅜ

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

      음... 질문내용이 잘 이해가 안되는데요...
      OllyDbg 에서 해당 명령을 내리면 아무런 창이 안보인 다는 말씀이신가요?
      해당 윈도우가 작게 숨어있던지 다른 윈도우 밑에 깔려있는것은 아닐까요?

  14. 궁금이 2011/01/26 06:39 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요. 작년에 reversecore.com에서 크랙미 리버싱 강좌를 보다가 어셈블리가 어렵고 눈에 안들어와서 접었다가

    각종 후킹 방법들을 배우다 보니 다시 이곳에 오게되었습니다. 아직 몇개 읽어보지는 못했지만 이 곳 만큼 자료가 많고 정리 잘 된곳도 없다는 생각이 드네요. 감사드립니다..

    사실 처음에 어셈블리만 거의 나오길래 쉽게 포기했었는데 나중에보니까 제 좁은소견으로는 주로 쓰이는 후킹 기법 몇가지만 알아도 제가 원하는 바를 이룰수가 있는 것 같더라구요.(맞나요?)

    TCP/IP프로그래밍과 MS에서 출판한 Windows Internals를 보려고 하고 있습니다만 Windows Internals를 보는 것이 리버싱에 좀 도움이 될까요? 고맙습니다..

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

      안녕하세요.

      칭찬 감사합니다.

      Windows Internals 요? 리버서에게 그만큼 좋은 책은 없을겁니다. 끝까지 읽기가 너무 힘들어서 문제이지요.

      서점에서 한번 훑어 보신 후에 너무 어렵다고 생각되시면 나중에 실력이 좋아진 후 구입하셔도 될 것 같습니다.

      제 주변에 드라이버 개발자들도 고개를 설레설레 젓는 책입니다. ^^

      감사합니다.

  15. Ch3ongDY 2011/06/27 19:32 댓글주소 | 수정 | 삭제 | 댓글

    좋은글 감사합니다.



UPack 으로 압축된 메모장(notepad_upack.exe) 파일을 디버깅하여 OEP 로 가보겠습니다. UPack 은 PE Header 를 독특하게 변경하는 게 문제가 될 뿐 Anti-Debugging 기법은 없으므로 디버깅 자체는 어렵지 않습니다.

UPack 의 PE Header 변경 기법에 대해서는 이전의 제 글을 참고하시기 바랍니다.

UPack 상세 분석 - PE Header 완전 정복 (1)
UPack 상세 분석 - PE Header 완전 정복 (2)
UPack 상세 분석 - PE Header 완전 정복 (3)
UPack 상세 분석 - PE Header 완전 정복 (4)



OllyDbg 실행 에러



UPack 은 IMAGE_OPTIONAL_HEADER 에서 NumberOfRvaAndSizes 값을 A 로 변경(기본값 10)하기 때문에 OllyDbg 의 초기 검증 과정에서 아래와 같은 에러 메시지를 출력합니다.


<Fig. 1>

크리티컬한 에러가 아니므로 [확인] 버튼을 눌러서 넘어갑니다.

위 에러 때문에 OllyDbg 는 EP 로 가지 못하고 아래 그림과 같이 ntdll.dll 영역에서 멈춰버립니다.


<Fig. 2>

어디까지나 OllyDbg 의 버그(혹은 엄격한 PE 체크)때문에 이런 현상이 발생한 것이므로 강제로 EP 를 설정해줘야 합니다.

먼저 EP 가 어디인지 알아야겠죠? Stud_PE 를 이용하여 확인해 보겠습니다.


<Fig. 3>

위 그림을 보면 ImageBase 가 01000000 이고, EP 의 RVA 값이 1018 입니다.
따라서 EP 의 VA 값은 01001018 입니다.

OllyDbg 의 Code 윈도우에서 01001018 로 이동 한 후 "New origin here" 명령을 이용하여 강제로 EIP 를 변경시킵니다.


<Fig. 4>

"New origin here" 명령을 실행하면 경고 메시지 박스가 뜨는데 [확인]을 눌러주면, 이제부터 정상적인 디버깅이 가능합니다.



Decoding Loop


모든 Packer 에는 Decoding Loop 가 존재합니다. 압축/해제 알고리즘 자체가 많은 조건 분기와 루프로 구성되어 있다는 걸 알고 계시다면 Decoding Loop 가 왜 그리 복잡하게 보이는지 이해하실 수 있을 것입니다.

이러한 Decoding Loop 를 디버깅할 때는 조건 분기를 적절히 건너뛰어서 루프를 탈출해야 합니다. 경우에 따라서는 한눈에 루프가 파악되지 않을 수도 있습니다. 레지스터를 잘 보면서 어떤 주소에 값을 쓰고 있는지 잘 살펴야 합니다. (사실 이건 많은 경험이 필요한 부분입니다.)

UPack 은 2nd Section 에 압축된 원본 데이터가 존재하고 이 데이터를 Decoding Loop 를 돌면서 1st Section 에 압축해제 시킵니다.

EP 코드부터 디버깅을 시작해 보겠습니다.


<Fig. 5>

첫 두 명령은 010011B0 주소에서 4 byte 를 읽어서 EAX 에 저장하는 명령입니다. EAX 는 0100739D 값을 가지는데 이는 원본 notepad 의 OEP (Original Entry Point) 입니다.

사실 이 값이 OEP 인 것을 알고 있다면 Hardware BP 를 설치하고 실행[F9] 하면 정확히 OEP 에서 멈추게 됩니다. (리버서들은 보통 BP 설치 후 실행한다는 말을 'BP 걸고 달린다' 라고 표현합니다.)

우리는 디버깅 실력 향상이 목표이므로 정석대로 계속 진행하겠습니다. (익숙해 지시면 BP 걸고 달리세요~)

조금 진행하다 보면 아래와 같은 함수 호출 코드가 나타납니다.


<Fig. 6>

이때 ESI 값은 0101FCCB 이고 이게 바로 decode() 함수의 주소입니다. 앞으로 이 함수가 반복적으로 호출될 것입니다.

decode() 함수를 살짝 살펴보겠습니다.

 
<Fig. 7>

이 부분만 봐서는 뭐 하는 코드인지 아직 감이 안 오실 겁니다.

StepIn[F7] 을 계속 하다 보면 아래와 같은 코드를 만나게 됩니다.


<Fig. 8>

0101FE57 과 0101FE5D 주소에는 EDI 값이 가리키는 곳에 뭔가를 쓰는 명령어가 있습니다. 이때 EDI 값은 1st Section 내의 주소를 가리킵니다.

즉, 압축을 해제 한 후 실제 메모리에 쓰는 명령어 들입니다.
1001FE5E 와 0101FE61 주소에는 CMP/JB 명령어를 통해서 EDI 값이 01014B5A 가 될 때까지 계속 루프를 돌게 됩니다. ([ESI+34] = 01014B5A)

0101FE61 주소가 바로 Decoding Loop 의 끝부분입니다.

실제로 이곳에 BP 를 걸고 달리시면 EDI 가 가리키는 주소에 어떤 값들이 쓰여지는 것을 볼 수 있습니다.



IAT 세팅


일반적인 Packer 에서는 Decoding Loop 가 끝나면 원본 파일에 맞게 IAT 를 새롭게 구성합니다. UPack 도 같은 과정을 거칩니다. 아래 그림을 봐주세요.


<Fig. 9>

UPack 이 Import 하는 2개의 함수 LoadLibraryA 와 GetProcAddress 를 이용하여 루프를 돌면서 원본 notepad 의 IAT 를 구성합니다. (notepad 에서 Import 하는 함수들의 실제 메모리 주소를 얻어서 원본 IAT 영역에 쓰는 것입니다.)

이 과정이 끝나면 0101FEAF 주소의 RETN 명령어로 드디어 OEP 로 갑니다.


<Fig. 10>

수고하셨습니다. 익숙해 질 때 까지 몇 번 반복해 보시기 바랍니다.

이것으로 UPack 의 모든 설명을 마치겠습니다.

질문은 댓글이나 메일을 이용해 주세요~


ReverseCore

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

  1. HS 2009/09/21 12:34 댓글주소 | 수정 | 삭제 | 댓글

    ㅋ.. 예전에 MUP 자체에 푹 빠져서... 이것저것 풀어보려고 애썼던 기억이 납니다..^^;
    요즘에는 MUP 는 거의 시도를 안하고 있는데.. 귀찮은 것도 있지만...
    애초에 메모리에 올려버리면 풀린 코드들이 보이니까.. MUP 에 대한 매력이 반감되었다고나 할까요;ㅋ...

    ps.. 사실 Themida 라는 거대한 장벽에 가로막힌 후에 흥미를 잃어버린게 컸던거 같아요;ㅋ

    • reversecore 2009/09/21 15:25 댓글주소 | 수정 | 삭제

      HS님, 안녕하세요 ^^

      네, 맞습니다.
      MUP 할일이 별로 없는게 사실이죠.
      게임 혹은 보안 분야 업무에서나 어쩌다 가끔 하게되지요.

      Themida, EXECryptor, SVKP 를 비롯한 많은 Protector 류들은 정말 어려울 뿐더러 분석가에게 너무나 가혹한(?) 노가다를 요구합니다.
      (엄청난 쓰레기 코드, 끝없는 루프, 무지막지한 call depth, 곳곳에 anti-debugging, 그리고 한번 실수하면 다시 처음부터...)

      그런데 그게 리버싱의 '재미'인거 같애요.
      시도하고 실패하고의 반복인거죠. ^^

  2. reversecore 2009/09/22 17:30 댓글주소 | 수정 | 삭제 | 댓글

    실수로 애자몽님의 댓글을 삭제 해버렸네요. ㅠㅠ
    죄송합니다.
    (제 댓글을 삭제한건데 왜 애자몽님의 글이 삭제될까요???)

    애화몽님 께서 <Fig. 6> 와 설명이 맞지 않다는 문의를 해주셨습니다.

    ---------------------

    올바른 지적 감사드립니다. ^^
    제가 다른 그림을 올렸었군요.
    지금 수정 하였습니다.

    디코딩 코드 찾는 법은 먼저 디코딩 루프를 찾아서 코드 중에 메모리에서 값을 읽어들여 산술연산 후 값을 다시 메모리에 쓰는 코드가 있다면 그곳이 바로 디코딩 코드입니다.
    루프내에서 반복적으로 호출될 것입니다.

  3. 애화몽 2009/09/22 23:24 댓글주소 | 수정 | 삭제 | 댓글

    설명 감사합니다.
    '애자몽' ㅠ.ㅠ

  4. 애화몽 2009/09/23 10:59 댓글주소 | 수정 | 삭제 | 댓글

    MUP를 OEP 찾는 요령만 배워서요 ^^;
    따라하기식..
    과정 이해에 큰 도움이 되는 좋은 강좌 입니다.
    감사합니다.

    • reversecore 2009/09/23 14:27 댓글주소 | 수정 | 삭제

      네, 제가 찾아봐도 "어디에 BP 걸고 달려라" 식의 글들뿐이었습니다. 원리에 대한 설명은 아직 많이 없더라구요.

  5. 애화몽 2009/09/23 17:47 댓글주소 | 수정 | 삭제 | 댓글

    네... 알겠습니다.

  6. 2009/09/25 20:59 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

  7. hyun 2010/08/26 02:31 댓글주소 | 수정 | 삭제 | 댓글

    어떤 패커인지는 모르겠습니다.
    peid가 검출은 못하구요..
    올리로 열어보면 글자도 낱개로 보이고 함수도 안보여서 패킹되어있는거같습니다.
    mup로 언패킹을 하려고 했는데 리빌더 까지 하면 실행은 안되고
    Don't know how to continue because memory at address 73CE1C28 is not readable. Try to change EIP or pass exception to program
    이렇게 구문이 나오고 실행이 되지않습니다.
    도저히 모르겠어요.. 도와주세요..

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

      안녕하세요.

      unpack 을 도와 달라는 말씀이신가요?

      해당 파일을 보내주시면 한번 봐드릴께요.

      확장자를 exex/dllx 등으로 변경하여 아래 메일 주소로 보내주세요.

      reversecore@gmail.com

      * 저도 protector 류를 만나면 크게 뾰족한 방법이 없습니다. 몇 달씩 매달리는 수 밖에요... ^^

      감사합니다.

  8. tintin 2011/06/26 02:59 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 Pe파일 기본부터 지금까지 달려왔습니다
    블로그 정말 유익한거같네요^^ 국내에서는 구하기 힘든자료들이 와우 ㅋㅋ

    질문은 이렇습니다

    이번강좌에서 notepad 의 OEP를 구해서
    언팩해봤습니다

    그런데... 언팩을 해도 처음의 정상notepad 랑 구조가 다릅니다...헤더랑 섹션이랑 겹쳐잇고...dos stub가 없고..

    완벽하게 처음의 notepad 로 언팩 할순업나요..?

    • reversecore 2012/01/11 07:44 댓글주소 | 수정 | 삭제

      안녕하세요.

      패커의 종류에 따라서 원본과 완벽히 동일하게 할 수 있는 경우도 있습니다.

      하지만 대부분은 압축이 풀리고 실행이 가능하다 수준으로 언패킹 하지요. 왜냐하면 원본 파일의 정보를 싹 지우기 때문에 해주고 싶어도 해줄 수 없는 경우가 많거든요. 그리고 안정성 문제 때문에 100% 완벽하게 복구하기는 힘들지요.

      감사합니다.

  9. redbit 2011/12/27 10:33 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 ㅎ
    오랫만에 왔는데 여전하시군요~ 책 축하드립니다 ..ㅋ 아직 이른가요??
    군대가서 잘 못들어왔는데 한번에 정독하고 갑니다 ㅎ

  10. albye 2012/01/05 04:21 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 주인장님 새해복 많이 받으세요.
    다름이 아니라 obsidium 1.3.0.4로 패킹된 프로그램을 스크립트를 이용해 언패킹중인데
    자세한건 http://coolsoft2.com/919634 해당 링크의 글을 읽어주시면 감사하겠습니다. (__) 꾸벅

    해당 프로그램의 언패킹 관련해서 파일 제작자이신 twitter.com/a4slayer님의 허락을 받아두었습니다. 가능하시다면 언패킹좀 해주실 수 있는지.,, 파일과 스크립트는 위에 적으신 reversecore@gmail.com로 보냈습니다. 시간 있으실때 봐주시면 감사하겠습니다.
    로 보

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

      안녕하세요.

      ImpRec 을 이용한 언패킹을 해달라는 말씀이신가요?

      ^^ 저는 그런 툴을 이용한 단순 언패킹은 예전부터 거부감이 들어서 사용하지 않습니다. ImpRec 도 한번 구경만 해봤지 쓰는법도 몰라요.

      제가 좋아하는건 그 패커의 동작 원리를 이해하면서 디버거로 하나하나 따라가는 것입니다. 중간에 너~무 힘들어서 때려치는 경우도 많구요. ^^

      감사합니다.

  11. albye 2012/01/14 03:24 댓글주소 | 수정 | 삭제 | 댓글

    답장 감사합니다.
    제가 쓴글에 어폐가 있었던듯 합니다. imprec로 언패킹을 했으면 하는게 아니라 해당 스크립트에
    스크립트를 돌리고 imprec로 iat를 복구하라고 써있어서 써본건대 의사전달에 오류가 있었네요..ㅠㅠ 저 그럼 디버거 툴을 이용하셔서 언패킹을 해주실 수 있으신지 더미다처럼 코드 가상화기술도 들어가 있어서 지금에 제 실력으로는 도저히 못풀겠더군요..
    http://www.stares.co.kr/10350 여기에 좀 더 자세하게 써놨는데 한 번 읽어주시면 감사하겠습니다.