앙 규무링~ 

 

 

DLL 인젝션

오늘은 DLL 인젝션의 동작 원리(?)에 대해서 좀 집고 넘어면 좋을 것 같아서 써본다

 

DLL 인젝션이란 ?

실행 중인 다른 프로세스에 특정 DLL 파일을 강제로 삽입하는 기법.

 KeyHook.dll을 인젝션!

어제 살펴봤던 것처럼 다른 프로세서의 메모리 공간으로 DLL을 강제 삽입 시키는 행위 입니다.

DLL에 관해서는 먼저 읽어봐야할 자료가 있습니다.

- 블랙펄 시큐리티 자료입니다.

 

[정상인이 쓴 DLL 인젝션을 사용한 API 후킹]

[PLT와 GOT 자세히 알기 1]

[PLT와 GOT 자세히 알기 2]

 

위의 글들을 읽었다는 가정 아래 DLL 인젝션을 할 때 절차를 확인해봅시다.

 

DLL injection 순서!

#1. 대상 프로세스의 핸들 구하기

#2. 대상 프로세스 메모리에 인젝션할 DLL 경로를 쓴다.

#3. LoadLibraryW() API 주소를 구한다.

#4. 대상 프로세스에 원격 스레드(Remote Thread)를 실행한다.

 

- 위의 방법이 가장 무난한 dll 인젝션 방법이라고 합니다. 그럼 하나씩 알아봅시다!

 

#1. 대상 프로세스의 핸들 구하기

 

'hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))'

 

프로세스에 DLL을 인젝션 하기 위해서는 대상 프로세스의 PID값이 필요합니다. 이 PID값을 OpenProcess() API를 통해서 알아오게 됩니다. 

3번째 파라미터인 dwPID 값을 사용해 알아내서 프로세스의 핸들을 획득할 수 있습니다.

 

#2. 대상 프로세스 메모리에 인젝션할 DLL 경로를 쓰기

 

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

 

위의 방식으로 프로세스의 핸들을 얻었다고 한다면, 프로세스에 로딩할 DLL 파일의 경로를 알려줘야 합니다.

어따가 로딩할 껀지 알려주기 위해서 VirtualAllocEx() API를 이용해 상대방 프로세스의 메모리 공간에 버퍼를 할당합니다.

 

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

 

앞에서 할당받은 버퍼주소 (pRemoteBuf)에다가 WriteProcessMemory() API를 이용하여 DLL 경로 문자열을 써줍니다.

이 WriteProcessMemory() 여깃 hProcess 핸들이 가리키는 상대 프로세스에 메모리 공간에 쓰는 것입니다.

 

정리 : VirtualAllocEx()로 메모리 공간을 확보하고, WriteProcessMemory()로 로딩될 dll 경로를 적어줌.

 

#3. LoadLibraryW() API 주소를 구하기

 

hMod = GetModuleHandle("kernel32.dll");

pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");

 

DLL 인젝션을 하기 위해서는 'kernel32.dll' 안의 LoadLibrary() 함수를 이용해야 합니다. 그러므로 GetModuleHandle()을 이용하여 커널.dll의 핸들을 획득한 후에 GetProcAddress()를 사용하여 LoadLibrary()의 주소를 알아오게 됩니다.

 

#4. 대상 프로세스에 원격 스레드 실행

 

'hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);'

 

모든 준비가 끄탄고 마지막으로 대상 프로세스(ex:notepad.exe)로 하여금 LoadLibraryW() API를 호출하도록 명령을 내리면 되는데... Window OS는 그러한 API를 제공하지 않습니다. 따라서 CreateRemoteThread() API를 사용해서 프로세스 내부에 스레드를 생성하고 그 스레드의 시작주소를 LoadLibraryW()의 주소로 채워넣어 실행시키는 방법을 사용합니다.

 

HANDLE CreateRemoteThread(
  HANDLE                 hProcess,				// 프로세스 핸들
  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  SIZE_T                 dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,	// 스레드 함수 주소
  LPVOID                 lpParameter,			// 스레드 파라미터 주소
  DWORD                  dwCreationFlags,
  LPDWORD                lpThreadId
);

myhack.dll 실습!

다음의 파일을 이용하여 dll 인젝션을 해보자!

 

InjectDll.exe
0.04MB
myhack.dll
0.03MB

첫 번째로 메모장을 이용해 실습할 것이므로 메모장의 PID를 구합니다.

PID = 5920!

그 다음 cmd를 오픈한뒤에 다음과 같이 실행 파라미터를 입력해서 InjectDll.exe를 실행합니다.

 

success!!

 

dll 인젝션이 성공하게 되면 하나의 index.html파일이 생성되게 되며, 페이지를 열게 되면 다음과 같습니다!

어디서 본 페이지죠 ㅎㅎ?

실습 끝!

 

은 아니고 이제 소스코드를 분석할 시간이다...

InjectDll.cpp
0.00MB
myhack.cpp
0.00MB

 

먼저 Myhack.cpp를 보자!

// Myhack.cpp

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

#pragma comment(lib, "urlmon.lib")

#define DEF_URL     	(L"http://www.naver.com/index.html")
#define DEF_FILE_NAME   (L"index.html")

HMODULE g_hMod = NULL;

DWORD WINAPI ThreadProc(LPVOID lParam)
{
    TCHAR szPath[_MAX_PATH] = {0,};

    if( !GetModuleFileName( g_hMod, szPath, MAX_PATH ) )
        return FALSE;
	
    TCHAR *p = _tcsrchr( szPath, '\\' );
    if( !p )
        return FALSE;

    _tcscpy_s(p+1, _MAX_PATH, DEF_FILE_NAME);

    URLDownloadToFile(NULL, DEF_URL, szPath, 0, NULL);

    return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    HANDLE hThread = NULL;

    g_hMod = (HMODULE)hinstDLL;

    switch( fdwReason )
    {
    case DLL_PROCESS_ATTACH : 
        OutputDebugString(L"<myhack.dll> Injection!!!");
        hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
        CloseHandle(hThread);
        break;
    }

    return TRUE;
}

먼저 Dllmain()을 봅시다! DLL이 로딩되어지면서(DLL_PROCESS_ATTACH)될 때 디버그 문자열을 출력합니다.

("myhack.dll injection!!!") 그 후 스레드를 실행합니다. ThreadProc()의 내용은 URLDownloadToFile() API를 이용해 사이트의 index.html 파일을 다운받게 합니다. 프로세스에 DLL 인젝션이 발생한 경우 해당 DLL의 DllMain() 함수가 호출되고 결국 URLDownloadToFile() API가 호출되게 되는 원리! - 잘읽어보십쇼

 

다음은 InjectDll.cpp의 소스코드를 보겠습니다.

 

// InjectDll.cpp

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

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) 
{
    TOKEN_PRIVILEGES tp;
    HANDLE hToken;
    LUID luid;

    if( !OpenProcessToken(GetCurrentProcess(),
                          TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, 
			              &hToken) )
    {
        _tprintf(L"OpenProcessToken error: %u\n", GetLastError());
        return FALSE;
    }

    if( !LookupPrivilegeValue(NULL,           // lookup privilege on local system
                              lpszPrivilege,  // privilege to lookup 
                              &luid) )        // receives LUID of privilege
    {
        _tprintf(L"LookupPrivilegeValue error: %u\n", GetLastError() ); 
        return FALSE; 
    }

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    if( bEnablePrivilege )
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    else
        tp.Privileges[0].Attributes = 0;

    // Enable the privilege or disable all privileges.
    if( !AdjustTokenPrivileges(hToken, 
                               FALSE, 
                               &tp, 
                               sizeof(TOKEN_PRIVILEGES), 
                               (PTOKEN_PRIVILEGES) NULL, 
                               (PDWORD) NULL) )
    { 
        _tprintf(L"AdjustTokenPrivileges error: %u\n", GetLastError() ); 
        return FALSE; 
    } 

    if( GetLastError() == ERROR_NOT_ALL_ASSIGNED )
    {
        _tprintf(L"The token does not have the specified privilege. \n");
        return FALSE;
    } 

    return TRUE;
}

BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
    HANDLE hProcess = NULL, hThread = NULL;
    HMODULE hMod = NULL;
    LPVOID pRemoteBuf = NULL;
    DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
    LPTHREAD_START_ROUTINE pThreadProc;

    // #1. dwPID 를 이용하여 대상 프로세스(notepad.exe)의 HANDLE을 구한다.
    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
    {
        _tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
        return FALSE;
    }

    // #2. 대상 프로세스(notepad.exe) 메모리에 szDllName 크기만큼 메모리를 할당한다.
    pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

    // #3. 할당 받은 메모리에 myhack.dll 경로("c:\\myhack.dll")를 쓴다.
    WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);

    // #4. LoadLibraryA() API 주소를 구한다.
    hMod = GetModuleHandle(L"kernel32.dll");
    pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
	
    // #5. notepad.exe 프로세스에 스레드를 실행
    hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);	

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

int _tmain(int argc, TCHAR *argv[])
{
    if( argc != 3)
    {
        _tprintf(L"USAGE : %s <pid> <dll_path>\n", argv[0]);
        return 1;
    }

    // change privilege
    if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
        return 1;

    // inject dll
    if( InjectDll((DWORD)_tstol(argv[1]), argv[2]) )
        _tprintf(L"InjectDll(\"%s\") success!!!\n", argv[2]);
    else
        _tprintf(L"InjectDll(\"%s\") failed!!!\n", argv[2]);

    return 0;
}

main() 부터 살펴봅시다. main()의 주요기능은 프로그램의 파라미터들에 대해서 체크한 후에 InjectDll()함수를 호출하는 것입니다. InjectDll() 함수가 바로 DLL 인젝션을 하는 주요 함수입니다!

그럼 호출되어지는 InjectDll() 함수는 대상 프로세스로 하여금 LoadLibrary("myhack.dll") API를 호출하도록 명령하는 기능을 가지고 있습니다.

 

-> 이 InjectDll()를 위의 주석과 절차에 따라서 한 줄씩 확인해보면 신상에 이로울것 같습니다!

'Reversing' 카테고리의 다른 글

리버싱 10  (0) 2019.04.11
리버싱 9  (0) 2019.04.10
리버싱 8  (0) 2019.04.09
리버싱 7  (0) 2019.04.05
리버싱 6  (2) 2019.04.04

 앙 귬귬띠 

 

 

DLL 인젝션

- 훅(Hook) 이란 ?

중간에서 오고가는 정보를 엿보거나 가로채기 위해 초소를 설치함. (갈고리를 건다)

이중에서 메세지 훅에 대해서 알아봅시다.

 

- 메세지 훅 ?

Window는 GUI를 제공하고 Event Driven으로 동작한다고 핟나. 키보드/마우스 등 어떠한 행위를 할 때 이벤트가 발생하고 Window는 미리 정의된 메세지를 해당 응용 프로그램으로 통보합니다. 아래 그림을 살펴봅시다.

 

https://yokang90.tistory.com/58

만약에 키보드에 입력 이벤트가 발생하면 WM_KEYDOWN 메세지가 OS queue에 추가됩니다. OS는 어느 응용 프로그램에서 이벤트가 발생한지 확인 후에 큐에서 꺼내서 Application message queue에 추가합니다. 응용 프로그램(메모장)은 자신의 app 큐를 모니터링 하다가 WM_KEYDOWN 메세지를 확인한 다음에 해당 event handler를 호출하게 됩니다.

 

위 그림과 같이 훅이 설치되어있으면 OS와 APP 큐 사잉 ㅔ설치된 훅체인을 통해 키보드 메세지 훅들을 응용 프로그램보다 먼저볼 수 있습니다. 

-> 저부분에서 가로채는것 뿐만 아니라 메세지 자체의 변경도 가능하며, 아래로 안내려 보낼수도 있다!

 

메세지 훅같은 경우에는 SetWindowsHookEx() API를 사용하면 간단하게 구현할 수 있다고 합니다.

HHOOK SetWindowsHookExA(
  int       idHook,		// hook type
  HOOKPROC  lpfn,		// hook procedure
  HINSTANCE hmod,		// 프로시저가 속해있는 DLL 핸들
  DWORD     dwThreadId		// hook을 걸고 싶은 thread ID
);

msdn - api 정의

훅 프로시저는 OS가 호출해주는 콜백 함수이며 훅을 걸때 DLL 내부에 존재해야 한다.

4번째 변수 dwThreadID를 0으로 줄경우 전역(Global) 훅이 설치되며 실행중인 모든 프로세스에 영향이 간다.

 

키보드 메세지 후킹 실습

- chrome.exe 프로세스의 키보드 메세지를 가로채서 입력을 받지 못하도록 하는 실습

HookMain.exe
0.04MB
KeyHook.dll
0.03MB

HookMain.exe를 실행하면 다음과 같은 창이 뜬다.

후킹중

이 상태로 chrome.exe를 실행하여 키보드 입력을 해봅니다.

-> 되지 않음

 

Keyhook.dll 인젝션 되어있는 모습
PID 매칭됌

chrome.exe의 메모리에 keyhook.dll이 인젝션 된것이 보인다. 키보드 이벤트 발생시 chrome.exe는 아무것도 받지 못하기 때문에 검색창에 입력이 되지않는다.

 

Hookmain.exe 디버깅

EP에서 시작!

먼저 우리가 봤었던 press 'q'를 찾아봅시다. (문자열 검색기능 이용)

찾음

40104D에서 찾을 수 있습니다. 여기가 main()의 안이겠죠 ?

 

main()

 

401000에 BP를 설치한 후에 디버깅을 위부터 차례대로 시작합니다.

401006의 주소에서 Loadlibrary("keyhook.dll")을 호출합니다.

이후에 40104B의 call ebx에서 keyhook.hookstart() 함수가 호출됨을 알 수 있습니다.

call 안으로 들어가봅시다.

지금 보이는 그림은 HookMain.exe 프로세스에 로딩된 Keyhook.dll의 HookStart() 함수 입니다. 여기서 SetWindowsHookEx의 첫 번째와 두 번째 인자값들을 알 수 있습니다.

 

먼저 API의 첫 번째 파라미터 값은 WH_KEYBOARD(2)입니다. 두 번째 파라미터(lpfn)의 값은 10001020 이며, 이 값이 바로 훅 프로시저의 주소라고 합니다. main()의 나머지 부분들은 'q'를 입력시 종료받는 부분입니다. (디버깅해보기)

 

이제 notepad.exe를 실행 시킨다음에 HookMain.exe를 실행시키고 키보드 입력을 받습니다.

(디버그 dll 로드 on)

다음과 같이 10000000의 주소에 keyhook.dll이 로드되는 걸 볼 수 있습니다. 해당 EP로 가봅시다.

아까 봤던 훅 프로시저의 주소 (10001020)에 BP를 걸면 키보드 입력 이벤트가 발생할 때마다 이곳에서 멈춰집니다.

Hookmain.cpp과 keyhook.cpp 소스코드를 첨부합니다.

출처 - reversecore

HookMain.cpp
0.00MB
KeyHook.cpp
0.00MB

// HookMain.cpp	출처 - reversecore

/dll 로더
#include "stdio.h"
#include "conio.h"
#include "windows.h"
 
#define DEF_DLL_NAME  "KeyHook.dll"
#define DEF_HOOKSTART  "HookStart"
#define DEF_HOOKSTOP  "HookStop"
 
typedef void(*PFN_HOOKSTART)();
typedef void(*PFN_HOOKSTOP)();
 
void main()
{
    HMODULE   hDll = NULL;
    PFN_HOOKSTART HookStart = NULL;
    PFN_HOOKSTOP HookStop = NULL;
    char   ch = 0;
 
    // KeyHook.dll 로드
    hDll = LoadLibrary("KeyHook.dll");
 
    // HookStrat, HookStop 함수의 주소를 얻어온다.
    HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
    HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);
 
    // 후킹 시작
    HookStart();
 
    // q가 입력될때 까지 로더는 아무런 기능을 하지않고 대기
    printf("press 'q' to quit!\n");
    while (getch() != 'q');
 
    // 후킹 종료
    HookStop();
 
    // KeyHook.dll 언로드
    FreeLibrary(hDll);
}
//keyhook.cpp

#include "stdio.h"
#include "windows.h"

#define DEF_PROCESS_NAME		"notepad.exe"

HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
	switch( dwReason )
	{
        case DLL_PROCESS_ATTACH:
			g_hInstance = hinstDLL;
			break;

        case DLL_PROCESS_DETACH:
			break;	
	}

	return TRUE;
}

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	char szPath[MAX_PATH] = {0,};
	char *p = NULL;

	if( nCode >= 0 )
	{
		// bit 31 : 0 => press, 1 => release
		if( !(lParam & 0x80000000) )
		{
			GetModuleFileNameA(NULL, szPath, MAX_PATH);
			p = strrchr(szPath, '\\');

            // 현재 프로세스 이름을 비교해서 만약 notepad.exe 라면 0 아닌 값을 리턴함
            // => 0 아닌 값을 리턴하면 메시지는 다음으로 전달되지 않음
			if( !_stricmp(p + 1, DEF_PROCESS_NAME) )
				return 1;
		}
	}

    // 일반적인 경우에는 CallNextHookEx() 를 호출하여
    //   응용프로그램 (혹은 다음 훅) 으로 메시지를 전달함
	return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

#ifdef __cplusplus
extern "C" {
#endif
	__declspec(dllexport) void HookStart()
	{
		g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
	}

	__declspec(dllexport) void HookStop()
	{
		if( g_hHook )
		{
			UnhookWindowsHookEx(g_hHook);
			g_hHook = NULL;
		}
	}
#ifdef __cplusplus
}
#endif

'Reversing' 카테고리의 다른 글

DLL 인젝션  (2) 2019.04.12
리버싱 9  (0) 2019.04.10
리버싱 8  (0) 2019.04.09
리버싱 7  (0) 2019.04.05
리버싱 6  (2) 2019.04.04

인라인 패치

이번 시간에는 인라인 코드 패치를 실습해보자!

 

간단히 말해서 '코드 케이브'를 이용해서 패치를 하는 기법입니다. 대상 프로그램이 압축 또는 암호화 되어있어서 직접 파일을 수정하기 어려울 때 '케이브'를 이용해서 패치를 진행합니다.

 

출처 : http://blog.naver.com/PostView.nhn?blogId=ktw1332&logNo=220407203753&parentCategoryNo=&categoryNo=26&viewDate=&isShowPopularPosts=false&from=section

우리가 패치하고 싶은 코드가 OEP영역에 암호화 되어있는 경우 복호화시 어떻게 될지 모르기 때문에 인라인 코드 패치를 진행하는 것이다.

 

실습을 통해서 인라인 패치에 대해서 알아보자!

unpackme#1.aC.exe
0.00MB

먼저 프로그램을 실행 시켜봤더니 다음과 같았다.

저 문구들을 패치하는게 이번 문제인 것 같다.

디버거로 오픈 시에 다음과 같이 암호화 되어있는걸 볼 수 있다.

올리 디버거

 

x32dbg

 

둘이좀 다르게 보여주는데 잘 모르겠다; 아무튼 올리말고 xdbg로 해보자!

일단 내가 찾던 메세지는 검색해본 결과 없고, 좀더 디버깅을 진행해보자!

 

디버깅을 진행하다보면 다음과 같은 코드를 볼 수 있다. 이 형태를 잘 기억해두자

-> 복호화 루프라고 한다.

복호화 루프

xor byte ptr ds:[ebx], 44 에서 특정영역(4010F5 ~ 401248)을 복호화합니다.

그리고 4010B0의 첫 번째 call로 들어가보면 또다른 2개의 복호화 루프를 볼 수 있습니다.

복호화 루프 2

4010C8의 xor에 의해서 401007 ~ 401085가 복호화 되고, 다시 4010DB의 xor로 4010F5 ~ 401248 영역이 복호화 됩니다. 복호화 루프 1과 동일한 범위로 2중 복호화가 진행.

(이거 ebx에서 시작해서 ecx 1씩 줄면서 루프돌리면서 ebx 1씩 증가하는거 관찰하샘)

 

그렇게 2개의 루프를 지나고 다음의 call 401039 명령을 만나게 됩니다.

안으로 들어가보면 다음과 같습니다.

여기서 눈여겨 볼 부분은 checksum 루프 입니다.

먼저 401041의 edx 0을 통해서 edx를 초기화 합니다. 후에 401046에 ADD 명령으로 특정 주소 영역(4010F5 ~ 401248)에서 4바이트 단위로 순차적으로 값을 읽어서 edx에 덧셈 연산으로 누적시켜버립니다!

 

이렇게 루프를 다돌고 탈출하면 edx 레지스터에 특정한 값이 저장되는데, 이게바로 checksum 값 !!!!

ecx=0 되고 edx가 checksum값이 됨 

 

이 checksum 영역은 위에서 설명했다시피 2중으로 암호화되어 있는 영역임. 바로 이영역에 우리가 패치하려는 문자열이 존재할 것 같음.

 

그 후에 내가나온 checksum 값을 비교하는 401062의 CMP/JE 명령을 통해서 서로 다르면 에러 메세지 출력 후에 종료, 같다면 401083의 JMP로 OEP로 가게 됩니다.

checksum 결과에 따른 조건 분기

이러한 checksum 계산 방법은 특정 영역 코드/데이터가 변조되지 않았음을 검증하는 용도로 많이 사용된다고 합니다.

-> 해당 영역에서 한 바이트만 변경되어도 checksum 값이 달라짐

데이터 변조하고 그럼 checksum 비교 부분까지 변조하면 되겠네 ??? 

 

어찌 됐건 OEP로 가보자!

쩜프! 쩜프! 해서 여기까지 도착하게 된다.

OEP !

해당 부분은 보시다시피 다이얼로그를 실행시킵니다. 40123E주소의 다이얼로그를 실행시키면 박스가 나타나게 되죠.

DialogBoxParam() API의 정의는 MSDN에서 찾아볼 수 있습니다.

DialogBoxParam()는 총 5개의 변수를 받습니다. 

INT_PTR DialogBoxParamA(
  HINSTANCE hInstance,
  LPCSTR    lpTemplateName,
  HWND      hWndParent,
  DLGPROC   lpDialogFunc,
  LPARAM    dwInitParam
);

네 ~ 이중에서 4번째 파라미터가 프로시저의 주소라고 합니다.

5개 중 바로 이 4번째 주소!

그래서 PUSH 4010F5로 가봅니다.

패치 문자열 발견!!!

이곳에 패치해야할 문자열이 있는걸 확인할 수 있었습니다. (40110A, 401123) 위치 확인

그럼 인라인 패치를 진행해보도록 합시다.

 

여태까지 한게 이렇게 된다고함https://flack3r.tistory.com/entry/%EC%9D%B8%EB%9D%BC%EC%9D%B8-%ED%8C%A8%EC%B9%98

 

그렇다면 ... 프로그램의 흐름을 좀 위의 그림을 통해서 살펴보고 가봅시다 어려움 ㅡㅡ

401000	[EP Code]
40109B		[Decode]
4010A3			XOR [B] with 44
4010C8			XOR [C] with 7
4010DB			XOR [B] with 11
401039			[A]
401046				Checksum [B]
401090				XOR [C] with 17
401083				JMP OEP

일단.. [EP Code]는 [Decode]를 호출하며 디코딩 부분에서는 [B] - [A] - [B] 순서로 디코딩 작업이 이루어짐.

그러면면서 암호가 해제된 [A] 영역의 코드를 실행하게 되는데,,, 디코딩 과정해서 아까봤다시피 edx로 checksum 값이 떨어지게 됨. 이 checksum값을 [A]영역에서 cmp/je 하며 쳌썸 변경여부를 판별함.

그런 다음에 [C] 영역을 디코딩하고 마지막으로 OEP(40121E)로 점프함!

 

이걸 직접 디버깅하면서 확인해보라고 하시지만 어렵다...

 

인라인 패치 실습

어찌됐건 우리는 [B]영역의 문자열을 패치해야 되는데,, 이중으로 암호화되어있음, 그리고 쳌썸까지 써가면서 데티ㅓ가 변조되었는지 비교하기 때문에 이럴때 인라인 패치를 하면 좀 수월하게 패치할 수 있다고 함.

 

작업 순서는 이렇게 됨

#1. 파일의 적절한 위치에 패치시킬 문자열 삽입 (null padding 구역)

#2. [A]영역의 JMP OEP를 내가 삽입시킨 Patch Code로 점프시킴

#3. 프로그램이 실행되어 [A]의 JMP Patch Code를 만나면 패치 코드에서 문자열을 변경한후 OEP로 JMP하면 끝!

(동굴 판다고 생각하면 쉽네요)

 

패치코드를 설치하는데에 있어서 일반적으로 3가지 정도의 방법을 제시해줌

1. 널패딩

2. 마지막 섹션을 확장해 공간을 확보한 후 설치

3. 새로운 섹션을 확장해 공간을 확보한 후 설치

-> PE 구조가 중요하구나...

 

책에서하는 1번 방법으로 실행해 보겠음. PE Viewr로 열어보자.

나는 PE를 배웠다

 

살펴보면 첫 번째 섹션은 일단 400 ~ 800까지이며 680의 영역까지 데이터가 들어가 있고 이는 메모리의 401000 ~ 401280의 영역에 대비되어짐. 즉 메모리의 401280 ~ 402000의 영역은 널 패딩 영역임을 알 수 있음.

 

HxD로 그럼 파일 옵셋을 보며 널패딩 구역에 해당하는 680구역을 보자!

 

정말 널패딩 영역임을 확인할 수 있다!

 

이곳에 패치 케이브를 설치하자!

먼저 앞서 다시 디버깅을 진행하면서 OEP(40121E)로 가자

쨘!

여기서 앞서 찾은 널 패딩이 680 ~ 800 이기 때문에 이걸 메모리랑 대응 시켰을때 401280 ~ 401400 입니다.

그렇다면 이부분에 패치코드를 다음과 같이 작성해보자 !

(olly랑 달라서 헷갈렷다능...)

 

kyumpack ㅋㅋㅋ

코드작성하고 40121E 원래의 OEP로 점프시켜주면 패치코드가 실행되겠져 ??!

마지막으로 앞서만든 패치코드가 실행되도록 401083 주소의 jmp oep(40121e)를 패치코드 주소로 변경합니다!

변경!

그리고 해당 부분은 XOR 7 암호화를 하고있습니다. 여기서 주의할 것은 현재보이는 'E9 96 01'은 암호화가 풀린 상태의 모습이고, 파일에서 실제로 암호화된 모습은 다음과 같습니다.

 

파일의 'EE 91 06'이 XOR 7 복호화를 통해서 'E9 96 01'로 바뀐 것입니다. 패치 코드 주소는 401280 이므로 JMP명령은 다음과 같습니다.

위에서 확인 누르면 ㅇㅇ

이 instruction의 'E9 F8 01'을 그대로 쓰면 안 되고 복호화를 고려해서 XOR 7을 수행한 후에 씁니다.

 

'E9 F8 01' -> 'EE FF 06'

끝입니다. 이렇게 해서 완성!

kyumpack.exe
0.00MB

=> 글자 짤린다 ㅜㅜ 사내 MDS 때문인거 같다ㅠㅠ

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Reversing' 카테고리의 다른 글

DLL 인젝션  (2) 2019.04.12
리버싱 10  (0) 2019.04.11
리버싱 8  (0) 2019.04.09
리버싱 7  (0) 2019.04.05
리버싱 6  (2) 2019.04.04

PE relocation 

PE 파일은 프로세스 가상 메모리에 로딩되어질 때 PE 헤더의 'ImageBase'에 로딩되어짐.

그런데 ImageBase에 이미 다른 파일이 배치되어있을 경우에는 같은 주소 공간에 두개의 다른 파일을 로딩할 수는 없음.

따라서 PE 재배치를 통해 다른 주소에 로딩되게 함.

 

- 이전 EXE의 경우 가장 먼저 메모리에 올라갔기 때문에 재배치 고려를 하지 않았지만 현재쓰고있는 윈도우 버전의 경우엔 'ASLR' 기능이 추가되어서 EXE가 실행 되어질 때마다 랜덤한 주소에 로딩되어짐.

* 실제 재배치 작업 동작 원리

 

#1. 프로그램의 하드코딩된 주소를 써치

Base reloaction table을 통해서 찾는다. RVA가 1420일경웨 PE View를 통해 해당 주소의 내용을 확인한다.

1420의 하드코딩된 주소 08C28309 값이 들어있다.

 

#2. 값을 읽은 후 ImageBase만큼 뺀다(VA -> RVA)

 

08C28309 - 0100000 = 07C28309

 

#3. 실제 로딩 주소를 더한다.(RVA -> VA)

 

07C28309 + 00AF0000 (실제 로딩된 주소) = 08718309

 

PE 로더는 하드코딩된 주소 08C28309를 위와 같은 과정을 거쳐 보정된 값(08718309)을 같은 위치에 덮어씀!

이 과정을 하나의 Image_Base_reloaction 구조체의 모든 옵셋에 반복해서 적용하면 RVA 1000~2000 주소 영역에 해당하는 모든 하드코딩 주소에 대해서 PE 재배치 작업이 수행되어지게 되는것임!

 

.reloc 섹션

exe 파일에서 base_relocation_table은 필수적이지 않음. 그러나 dll,sys는 거의 필수적임.

.reloc는 VC++에서 생성한 PE의 맨 마지막에 위치한다고 함.

 

reloc.exe의 .reloc 섹션을 제거하기 위해서 다음의 4가지 단계를 거쳐야 함.

 

1. .reloc 섹션 헤더 정리

2. .reloc 섹션 제거

3. Image_file_header 수정

4. Image_optional_header 수정

가즈아!

 

#1. .reloc 섹션 헤더 정리

.reloc은 offset 270에서 시작함. 해당 부분을 0으로 덮어씁니다.

이 부분을 0으로
헤더 제거 완료

#2. reloc 섹션 제거

위 그림을 살펴보면 reloc 섹션의 시작 옵셋은 C000 입니다. 이 부분부터 파일의 끝까지 삭제합니다.

삭제완료

#3. Image_file_header 수정

 

섹션을 하나 삭제 했기 때문에 헤더에 섹션 개수를 나타내주는 Number_of_sections 항목을 수정합니다.

5에서 4로 수정!

#4. Image_optional_header 수정

섹션을 삭제해 그 크기만큼 이미지 크기가 줄어들었으므로 이미지 크기도 조절해줘야함.

현재 size_of_image값 11000에서 얼마만큼 빼야하냐면 삭제한 섹션(.reloc)의 Virtual Size는 E40이였음 이걸 

Section Alignment에 맞게 확장하면 1000임 따라서 1000을 빼줘야함.

 

다음으로는 자신의 섹션을 추가해보는 연습을 해볼 것.

섹션 추가 연습

의문점 ? Size of Raw Data랑 Virtual Size의 값이 다른거는 알겠다. 실제 옵셋이랑 메모리에 로딩되었을 때랑 일반적으로 다르다고 하니까. 그런데 vitual size값은 어떻게 결정되는거지

 

위에사진에 디스크에서 200의 size인데 메모리에 올라간 경우 1B4라는 값을 가지는데 어떻게 계산되는지 모르겠당

 

 

'Reversing' 카테고리의 다른 글

리버싱 10  (0) 2019.04.11
리버싱 9  (0) 2019.04.10
리버싱 7  (0) 2019.04.05
리버싱 6  (2) 2019.04.04
PE file(3)  (0) 2019.04.03

실행 압축

먼저 압축을 3가지 방식으로 나눠봅니다.

 

#1. 비손실 압축

- 압축된 파일을 100% 원래대로 복원할 수 있다. (무결성 보장)

 

#2. 손실 압축

- 원래대로 복원할 수 없다.

- jpg, mp4 등에 사용되며 의도적인 손상을 주어서 압축률을 높임 (사람은 식별안됌)

 

#3. 실행 압축

- PE 파일을 대상으로 하는 압축

- 파일 내부에 압축해제 코드를 포함하고 있어, 실행되는 순간에 메모리에서 압축을 해제 후 실행시키는 기술.

- PE 파일을 실행 압축 파일로 만들어 주는 유틸을 '패커'라고 함.

 

* 패커의 사용 목적 : 크기를 줄일 수도 있지만 파일 내부의 코드와 리소스를 보호.

 

PE 프로텍터 : 단순히 압축을 목적이 아닌 안티 리버싱을 위해 다양한 기법들로 코드및 리소스를 보호함

 

notepad.exe를 UPX 패커를 이용해 패킹을 진행해보자 !

 

[UPX 다운로드]

내가 쓸 notepad_upx.exe

notepad_upx.exe
0.05MB

PE Viewer로 비교해보자!

notepad.exe와 notepad_upx.exe

 

음... 뭔가가 변화가 있는것 같다. UPX의 첫번째 섹션을 열어보면,

 

Virtual Size의 값은 1만인데 Raw Data Size값이 0이다.

이게 무슨 일이냐면 압축된 원본코드와 해제코드는 지금 섹션2에 존재합니다. 그런데 실행하게 될경우 이게 섹션 1에 해제 되어지는 것입니더.

 

그럼 디버깅!

 

notepad_upx.exe 디버깅

목표 : 코드를 하나씩 트레이싱 하면서 원본 코드를 찾아보za

 

먼저 원본코드의 구조를 확인하기 위해 원본파일인 notepad.exe를 열어보자.

 

먼저 010073B2의 주소에서 GetModuleHandleA() 함수를 호출해서 notepad.exe의 ImageBase를 구한다.

그 후에 010073B4와 010073C0에서 각각 MZ와 PE 시그니처를 비교합니다. 

 

이제 notepad_upx.exe를 열어보자!

 

짜짱!

01015330이 EP 주소입니다. 여기는 2번째 섹션의 끝부분 주소입니다. 실제 원본코드는 이 주소 위로 존재합니다.

하나씩 살펴볼까요 ~

 

먼저 PUSHAD를 통해 EAX~EDI 레지스터 값을 스택에 저장하고,

ESI, EDI레지스터를 각각 두 번째와 첫 번째 섹션의 시작주소로 설정합니다. (0101100, 01001000)

- 이렇게 esi와 edi가 동시에 세팅되면 esi -> edi 로 메모리 복사가 일어날 거라 추측.

esi에서 데이터를 읽어서 압축을 해제한 후 edi에 저장.

 

그럼 트레이싱을 시작하자 트레이싱 단축키! (* 루프를 만나면 루프를 확인하고 넘어간다)

- 총 4개의 루프를 만날 예정

[Ctrl + F8]을 누르면 정신없이 트레이싱이 시작되다가 어떤 짧은 루프(#1) 안에 갖히게된다 그부분에서 [F12]를 눌러서 해당 루프를 자세히 봐야한다

.

 

루프를 돌려보면 ECX는 회전마다 1씩감소하고 EDX와 EDI는 1씩 증가하는걸 볼 수있다. EDX에서 한바이트를 읽어서 EDI에 쓰고 있는 중이다. (ctrl+F8을 돌려 살펴보라!)

 

010153E6에 Bp를 걸고 루프를 탈출한 뒤 다시 트레이싱을 실행해보자.

그러다보면 2번째 커다란 루프를 만날 수 있다.

 

#2. decoding 루프.

-> 압축 해제 루프

두 번째 섹션 주소에서 차례대로 값을 읽어 첫 번째 섹션으로 값을 옮겨 써주는 작업 중...

ESI가 가르키는 두 번째 섹션(UPX1)의 주소에서 차례대로 값을 읽어서 적절한 연산을 거쳐 압축을 해제하며 EDI가 가리키는 첫 번째 섹션(UPX0)에 주소에 값을 써주는 과정입니다.

 

...

...

 

* AL(EAX)에는 압축해제된 데이터, EDI는 첫 번째 섹션의 주소

 

=> al은 1바이트라 inc 1씩 증가시켜 다음 주소를 가르키고 eax는 4바이트라서 add로 4바이트 증가시킨 경우이다.

뭔지 몰랐다능 ... 까묵

NULl이었던 UPX0 영역(01007000)에 데이터가 채워진 모습

그렇게 두 번째 루프를 지나기 위해 01015402에 BP를 걸고 다음 트레이싱을 진행합니다.

바로 밑에서 세 번째 루프를 발견할 수 있습니다.

이 부분은 원본 코드의 CALL/JMP 명령어의 주소를 복원시켜주는 코드라고 합니다. 그냥 알고 넘어가면 대나...

jne는 not equal, cmp 비교시 같지않으면 ZF는 0을 반환합니다.

 

이제 IAT 셋팅만 하면 UPX 압축해제 코드가 끝이나게 됩니다. 다시 트레이싱을 해봅시다.

마지막 루프 입니다! 이 부분이 IAT를 셋팅하는 루프입니다. 01015436 주소에서 EDI=01014000으로 세팅되며, 이곳은 두번째 섹션(UPX1) 영역입니다. 이곳에 원본 notepad.exe에서 사용되는 API 이름 문자열이 저장되어 있습니다.

 

 

 

'Reversing' 카테고리의 다른 글

리버싱 9  (0) 2019.04.10
리버싱 8  (0) 2019.04.09
리버싱 6  (2) 2019.04.04
PE file(3)  (0) 2019.04.03
PE File(2)  (0) 2019.04.03

PE IAT 실습

notepad.exe를 이용하여 PE 구조를 파악한다.

notepad.exe
0.06MB

먼저 IMAGE_OPTIONAL_HEADER의 DataDirectory 구조체의 배열 정보를 확인합니다.

RVA = 7604 인걸 알 수 있쥬?

실제 해당주소에 들어가 보면 앞쪽이 IMPORT Table의 RVA 뒤쪽이 Size입니다.

 

 

이후에 RVA를 알기 때문에 offset(RAW)을 구합니다.

7604 - 1000 + 0400 = 6A04 (text 섹션임)

파일에서 6A04을 보면 다음과 같습니다.

IID의 구조체 배열

블록으로 잡힌 부분이 전부 IID의 구조체 배열입니다.

RAW 값을 구해보면 다음과 같아집니다.

Description RAW
INT 00006D90
Time Date Stamp -
Forwarder Chain -
Name 00006EAC
IAT 000006C4

그럼 이제 하나씩 따라가 보자!

 

#1. Name 00006EAC

comdlg32.dll 문자열이 보인다!

 

#2. INT 00006D90

INT는 임포트 하는 함수의 정보가 담긴 구조체 포인터 배열입니다. 이 정보로 프로세스 메모리에 로딩된 라이브러리에서 해당 함수의 시작 주소를 알아낼 수 있습니다.

주소 값 하나하나가 각각의 IMAGE_IMPORT_BY_NAME 구조체를 가리키고 있습니다.

제일 첫 번째 배열 값인 7A7A를 따라가 봅시다.

 

#3. IMAGE_IMPORT_BY_NAME

RVA:7A7A를 RAW로 변환합시다.

7A7A+1000-0400 = 6E7A

 

제일 앞의 '..'(000F)는 라이브러리에서 함수의 고유번호라고 합니다. 이 Ordinal 뒤로 함수 이름 문자열이 보이죠?

여기까지 봤을 때 INT는 IMAGE_IMPORT_BY_NAME 구조체의 포인터 배열임을 알 수 있습니다.6

함수의 이름은 'PageSetupDlg\(?)' 이네요.

 

#4. IAT 000006C4

다음 그림에서의 블록영역이 'comdlg32.dll' 라이브러리에 해당하는 IAT 배열 영역입니다.

IAT의 첫 번째 원소값인 '76324906'은 의미 없는 값이고 메모리에 로딩될 시 정확한 주소값으로 대체됩니다.

 

그래서 ImageBase 01000000 + 000006C4 = 010006C4를 디버거로 열어보면

pagesetup 함수 시작이 나타나게 됩니다. 그값은 94269677 인데 실행과정에서 이값이 왜바뀌는지는 잘모르겠습니다.

=> OS에 따라서 바뀐다고 함.

 

아무튼 디버깅 실행후에 값을 010012C4에 값을 살펴보면 77962694 이고,

77962694로 바꼈음

77962694로 이동을 했을 시

짜잔!

이렇게 PageSetupdlg 함수의 시작부분을 확인할 수 있습니다.

 

헥사값이 바뀌는건 OS때문에 바뀌는거라고 생각됩니다...

어찌됐건 함수의 시작부분을 찾았다 !

 

PE EAT 실습

 

IAT를 실습했으니 이번엔 EAT를 실습해보자!

Windows OS에서 라이브러리는 다른 프로그램에서 불러 쓸 수 있도록 관련 함수를 모아놓은 파일을 의미.(SYS/DLL)

그 중 kernel32.dll 파일이 가장 핵심적인 라이브러리 파일이라고 할 수 있습니다.

 

EAT는 저번에도 봤었다시피 라이브러리에서 제공하는 함수를 다른 프로그램에서 가져다가 쓸 수 있도록 해주는 핵심 메커니즘 입니다. => EAT를 통해서만 해당 라이브러리에서 익스포트하는 함수의 시작 주소를 정확히 구별할 수 있음

 

앞의 IAT와 마찬가지로 IMAGE_OPTIONAL_HEADER의 DataDirectory에서 Export 정보를 저장해놓고 있음. 

 

kernel32.dll
1.17MB

해당 주소로 이동해보자. 00000168

IAT와 같이 앞쪽 4바이트가 RVA 뒤쪽 4바이트가 Size의 멤버임을 볼 수 있음.

 

RVA값이 262C 이므로 RAW를 계산해보면,

262C - 1000 + 0400 = '1A2C' 입니다.

 

다음으로 IMAGE_EXPORT_DIRECTORY의 구조체는 다음과 같습니다.

typedef struct _IMAGE_EXPORT_DIRECTORY {

DWORD Characteristics

DWORD TimeDateStamp

WORD MajorVersion

WORD MinorVersion

DWORD Name

DWORD Base

DWORD NumberOfFunctions

DWORD NumberOfNames

DWORD AddressOfFunctions // 함수 주소 배열(EAT)

DWORD AddressOfNames //함수명 배열

DWORD AddressOfNameOrdinals // 함수 서수 배열

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY

그럼 중요한 멤버들을 정리해보면,

항목 의미
NumberOfFunctions 실제 Export 함수 개수
NumberOfNames Export 함수 중에서 이름을 가지는 함수 개수 (<=NOF)
AddressOfFunctions Export 함수 주소 배열 (배열 원소 개수 = NOF)
AddressOfNames 함수 이름 주소 배열 (배열 원소 개수 = NON)
AddressOfNameOrdinals Ordinal 배열 (배열 원소 개수 = NON)

그렇쥬 ??

 

라이브러리에서 함수 주소를 얻는 API는 GetProcAddress() 입니다. 이 api가 EAT를 참조해서 원하는 API 주소를 획득함.

그럼 EAT를 정복하러 가즈아!

 

GetProcAddress() 동작 원리

1. AddressOfNames 멤버를 이용해 '함수 이름 배열'로 갑니다.

2. '함수 이름 배열'은 문자열 주소가 저장되어 있고, strcmp를 통하여 원하는 함수 이름을 찾습니다. (name_index)

3. AddressOfNameOrdinal 멤버를 통해 'ordinal 배열'로 갑니다.

4. 'ordinal 배열'에서 name_index로 해당 ordinal 값을 써치합니다.

5. AddressOfFunctions 멤버를 이용해 '함수 주소 배열(EAT)'로 갑니다.

6. '함수 주소 배열(EAT)'에서 아까 구한 ordinal을 배열 인덱스로 하여 원하는 함수의 시작주소를 얻습니다.

 

이 순서 그대로 실습 해보겠습니다.

 

#1. 함수 이름 배열

AddOfNames 멤버의 RVA = 3538

RAW = 3538 -1000+0400 = 2938 이를 Hex Editor로 살펴보면 다음과 같습니다.

 

 

#2. 원하는 함수 이름 써치

 

이 부분들이 이제 4바이트의 RVA로 이루어진 배열입니다. 배열 원소의 개수는 NumberOfName(3B9) 겠죠??

저 모든 RVA 값들을 하나하나 따라가면 함수 이름 문자열이 나오게 됩니다.

 

3번째 원소값을 볼까요? (4BB3 -> RAW : 3FB3)

보이시죠 AddAtom ㅎㅎ

* OS에 따라서 나뭇잎책과 값이 다를순 있으나 실습은 똑같음

 

'AddAtom'은 AddOfNames의 RVA들 중 3번째에 속해있고 배열 인덱스로는 '2'가 됩니다.

 

#3. Oridnal 배열

 

이제 'AddAtom' 함수의 Oridnal 값을 알아냅니다. AddressOfOrdinals 멤버값은 441C -> RAW : 381C 입니다.

다음과 같이 2바이트의 ordinal로 이루어진 배열이 나타나게 됩니다. 

 

#4. ordinal

 

아까 구했던 인덱스 '2'를 위의 Ordinal 배열에 적용시키면 Oridianl(2)를 구할 수 있습니다.

AddressOfNameOrdinal[index] = ordinal ( index = 2, ordinal = 2)

 

#5. 함수 주소 배열 - EAT

이제 마지막으로 AddAtom의 실제 함수 주소로 찾아갈 수 있습니다. AddressOfFunctions의 값은 2654 -> RAW : 1A54 입니다.

 

해당 주소를 보면 다음과 같이 4바이트 RVA 배열이 나타나게 됩니다. 이게 바로 함수의 주소들 입니다.

 

#6. AddAtom 함수 주소

 

함수 주소를 얻기위해 Ordinal을 index로 적용하면, 다음과 같이 RVA = 326D9를 얻을 수 있습니다.

현재 저의 kernel32.dll의 imagebase = 7C800000 이기 때문에 실제주소 VA = '7C8326D9' 입니다

이 값을 디버거에서 확인해보면 'AddAtom'이 잇겠지 ??

없다고 한다...

 

 

없어서 imagebase부터 살펴보니 pe viewer로 봤을때와 디버거에 올라갔을때 imagebase가 틀려짐을 확인했다.

 

틀려짐... kernel32.dll의 imagebase = 76FF0000 으로 바뀌었다.

여기에 RVA를 더해도 찾을수가 없었다.... 왜죠 ?

왜 여기가있죠????

모르겠다능  775e26d9

'Reversing' 카테고리의 다른 글

리버싱 8  (0) 2019.04.09
리버싱 7  (0) 2019.04.05
PE file(3)  (0) 2019.04.03
PE File(2)  (0) 2019.04.03
PE File(1)  (0) 2019.04.02

 앙 규무링^^ 

 

 

PE file(3)

이번에는 직접 실습을 통해서 PE file의 IAT 입력 순서를 볼 예정입니다 :3

PE file(1), (2)에 이어서 봐야 이해됨 그럼 출바아알

 

IMAGE_IMPORT_DESCRIPTOR

PE file은 자신이 어떠한 라이브러리를 IMPORT하고 있는지 위의 구조체에 명시하고 있습니더.

구조체의 코드는 다음과 같습니다.

 

typedef struct _IMAGE_IMPORT_DESCRIPTOR {

    union {

        DWORD   Characteristics;            

        DWORD   OriginalFirstThunk;       // INT(Import Name Table) address (RVA)

    };

    DWORD   TimeDateStamp;

    DWORD   ForwarderChain; 

    DWORD   Name;                         // library name string address (RVA)

    DWORD   FirstThunk;                   // IAT(Import Address Table) address (RVA)

} IMAGE_IMPORT_DESCRIPTOR;



typedef struct _IMAGE_IMPORT_BY_NAME {

    WORD    Hint;                         // ordinal

    BYTE    Name[1];                      // function name string

} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; 

일반적으로 여러 개의 라이브러리를 임포트하기 때문에 라이브러리 개수만큼 위 구조체의 배열 형식으로 존재하며, 구조체 배열의 마지막은 null로 끝납니다.

ㄹㅇ임

- 구조체 중요 멤버들(주소들은 RVA 값임)

항목 의미
OriginalFirstThunk INT(Import Name Table)의 주소
Name Library 이름 문자열의 주소
FirstThunk IAT(Import Address Table)의 주소

그렇다면 이제 PE 로더가 임포트 함수 주소를 IAT에 입력하는 기본적인 순서!

 

IAT 입력 순서 :3

1. IID의 Name 멤버를 읽어서 라이브러리의 이름 문자열을 찾아냄 (ex : 'KERNEL32.dll')

2. 해당 라이브러리를 LoadLibrary() 함수를 써서 로딩함. 

-> LoadLibrary('KERNEL32.dll')

3. IID의 OriginalFirstThunk 멤버를 읽어서 INT 주소를 얻음.

4. 그 INT 주소에서 배열의 값을 한개씩 읽어들이면서 해당 IMPORT_BY_NAME 주소를 얻어냄

5. IMPORT_BY_NAME의 hint/name 항목을 이용해 해당 함수의 시작주소를 얻음

-> GetProcAddress('GetCurrentThreadId')

6. IID의 FisrtThunk(IAT)를 읽어서 IAT의 주소를 얻어낸다.

7. 해당 IAT 배열 값에 위에서 구한 함수 주소를 입력.

8. INT가 NULL을 만날 때까지 위의 4~7 과정을 반복함.

 

자... 무슨말이냐... Name을 읽어 dll 이름을 획득 -> 그 이름을 LoadLibrary() 함수 안에 넣어서 dll 핸들 획득 -> IID에서 INT를 획득하고 거기서 하나씩 읽어내면서 IBN 획득 -> IBN의 hint/name과 dll핸들(아까획득)을 이용해 함수를 돌림

GetProcAddress() 여기서 내가 원하는 함수의 시작주소를 얻게됨 -> IID에서 IAT 주소를 획득 -> 끝으로 IAT 배열값 위에서 구해놨던 함수의 시작주소를 입력함 -> 이렇게 NULL을 만날때까지 모든 dll에 대해서 수행

 

휴... 끝

 

notepad.exe를 이용한 실습을 진행해보자.

-

 

 

'Reversing' 카테고리의 다른 글

리버싱 7  (0) 2019.04.05
리버싱 6  (2) 2019.04.04
PE File(2)  (0) 2019.04.03
PE File(1)  (0) 2019.04.02
리버싱 5  (0) 2019.04.02

 앙 귬모띠~ 

 

 

PE File(2)

어제에 이어서 PE File 공부를 마저해보자!

어제는 NT Header에 대해서 안에 멤버들을 들여보다가 Image File Header 까지 봤었다.

오늘은 Image Optional Header로 가자

 

우선 까먹었을것 같으니 

가즈아

 

어제껄 복습한다.

다음의 블록이 NT Heaer - image file header 부분

 

offset		value		desc
---------------------------------------
000000DC	014c		machine
000000DE	0004		numeber of sections
000000E0	559EA6FF	time date stamp
000000E4	00000000	offset to symbol table
000000E8	00000000	number of symbols
000000EC	00E0		sie of optional header
000000EE	0102		characteristic
						IMAGE_FILE_EXECUTABLE_IMAGE
						IMAGE_FILE_32BIT_MACHINE

view 확인결과 실제와 동일 ㅋ

NT Header - image file header까지 보았다.

이제 구조체중에 가장 큰 NT Header - Optional Header를 살펴보자.

 

[IMAGE_OPTIONAL_HEADER32]

typedef struct _IMAGE_OPTIONAL_HEADER {  
 
   /* Standard fields */
   WORD  Magic; /* 0x10b or 0x107 */ /* 0x00 */
   BYTE  MajorLinkerVersion;
   BYTE  MinorLinkerVersion;
   DWORD SizeOfCode;
   DWORD SizeOfInitializedData;
   DWORD SizeOfUninitializedData;
   DWORD AddressOfEntryPoint;        /* 0x10 */
   DWORD BaseOfCode;
   DWORD BaseOfData;
 
   /* NT additional fields */
   DWORD ImageBase;
   DWORD SectionAlignment;       /* 0x20 */
   DWORD FileAlignment;
   WORD  MajorOperatingSystemVersion;
   WORD  MinorOperatingSystemVersion;
   WORD  MajorImageVersion;
   WORD  MinorImageVersion;
   WORD  MajorSubsystemVersion;      /* 0x30 */
   WORD  MinorSubsystemVersion;
   DWORD Win32VersionValue;
   DWORD SizeOfImage;
   DWORD SizeOfHeaders;
   DWORD CheckSum;           /* 0x40 */
   WORD  Subsystem;
   WORD  DllCharacteristics;
   DWORD SizeOfStackReserve;
   DWORD SizeOfStackCommit;
   DWORD SizeOfHeapReserve;      /* 0x50 */
   DWORD SizeOfHeapCommit;
   DWORD LoaderFlags;
   DWORD NumberOfRvaAndSizes;
   IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; /* 0x60 */
 } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

그렇다면 몇가지 중요한(?) 멤버들에 대해서 살펴봅시다.

 

#1. Magic

Magic은 32비트인지 64비트인지 구분할 때 쓰인다고 한다

 

- IMAGE_OPTIONAL_HEADER32 : 10B

- IMAGE_OPTIONAL_HEADER64 : 20B

 

#2. AddressOfEntryPoint

AddressOfEntryPoint는 EP의 RVA 값을 가지고 있습니다.

최초로 프로그램이 실행되는 코드의 시작주소로 ★매우 중요한 값★을 가지고 있음.

 

#3. ImageBase('중요')

메모리에서 PE파일이 어디에 로딩이 되는지 메모리에 로딩되는 시작 주소를 알려주는 부분.

exe, dll은 user memory 영역인 0~7FFFFFFF 범위에 로딩되어지며, sys 파일은 kernal memory 영역인 80000000~FFFFFFFF에 로딩되어진다고 합니다.

- 일반적으로 개발도구들이 만들어내는 .exe은 00400000이고, .dll은 100000000 입니다.

 

#4. SectionAlignment, FileAlignment

PE file의 Body는 섹션으로 나뉘어져 있다. file에서의 섹션의 최소단위가 filealignment이고 메모리에서의 섹션의 최소단위가 sectionalignment이다. 이 둘은 같을 수도 다를수도 있습니다.(읍읍;;) 파일/메모리의 섹션 크기는 반드시 sectionalignment / filealignment의 배수가 되어야 합니다.

 

#5. SizeOfImage

SizeofImage는 PE가 메모리에 로딩되었을 때에 가상메모리에서 PE image가 차지하는 크기를 말합니다.

실행되기 전과 후가 다르다고 하네요!

 

#6. SizeOfHeader

SizeofHeader는 PE Header의 전체 크기를 나타냅니다. 이 값또한 filealignment의 배수여야 하며, 파일시작에서 SizeOfHeader의 옵셋만큼 떨어진 위치에 첫번째 섹션에 존재

(DoS + NT + Section Header)

#7. Subsystem

이걸 보고서 .sys인지 .exe인지 .dll인지 구분할수 있다고 함.

참고.

#8. NumberOfRvaAndSizes

NumberOfRvaAndSizes는 IMAEG_OPTIONAL_HEADER32 구조체의 마지막 멤버인 DataDictionary 배열의 개수를 나타냄. 이값을 보고 PE 로더가 배열의 크기를 인식함 (꼭 16이 아닐수 있음. 16은 구조체 정의에 명시된 숫자)

 

#9. DataDirectory

DataDirectory는 IMAGE_DATA_DIRECTORY 구조체의 배열로, 각 항목마다 정의된 값을 가짐.

#define IMAGE_DIRECTORY_ENTRY_EXPORT        0
#define IMAGE_DIRECTORY_ENTRY_IMPORT        1
#define IMAGE_DIRECTORY_ENTRY_RESOURCE      2
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION     3
#define IMAGE_DIRECTORY_ENTRY_SECURITY      4
#define IMAGE_DIRECTORY_ENTRY_BASERELOC     5
#define IMAGE_DIRECTORY_ENTRY_DEBUG         6
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT     7
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR     8   /* (MIPS GP) */
#define IMAGE_DIRECTORY_ENTRY_TLS           9
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG   10
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT  11
#define IMAGE_DIRECTORY_ENTRY_IAT           12  /* Import Address Table */
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT  13
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR    14
#define IMAGE_DIRECTORY_ENTRY Reserved Directory 15

EXPORT, IMPORT, RESOURCE, TLS를 기억할 것.

IAT와 EAT의 개념에 대해서 잠깐 정리를 해보면 

 

IAT : 내가 프로그램에서 어떠한 함수를 사용할지를 모아둠

EAT : 내가 라이브러리가 되어서 다른 프로그램에 제공하는 함수목록을 모아둠.

 

여기까지가 IMAGE_OPTIONAL_HEADER 입니다.

VIEW 밑에 짤렸;

NT Header 끝!!

 

섹션 헤더로 출발!

섹션 헤더

- 각 섹션의 속성을 정의한 것이 섹션 헤더라고 합니다.

PE 파일은 code, data, resouce 등을 각각의 섹션으로 나뉘어서 저장되어 집니다.

이유 ? 프로그램의 안정성을 위해서. 안나뉘어 있으면 데이터 쓰다가 overflow나면 code영역을 침범함 ㅋㅋ

 

섹션의 속성에는 file/memory에서의 시작 위치, 크기, 엑세스 권한 등이 있어야겠고, 각각의 섹션마다 특성, 접근권한을 다르게 설정할 필요가 있어짐.

종류 엑세스 권한
code 실행, 읽기 권한
data 비실행, 읽기, 쓰기 권한
resource 비실행, 읽기 권한

IMAGE_SECTION_HEADER

섹션 헤더는 각 섹션별 IMAGE_SECTION_HEADER 구조체의 배열로 되어 있음.

typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

리얼

여기서 알아야 할 중요 멤버들은 다음과 같습니다.

항목 의미
VirtualSize 메모리에서 섹션이 차지하는 크기
VirtualAddress 메모리에서 섹션의 시작 주소(RVA)
SizeOfRawData 파일에서 섹션이 차지하는 크기
PointerToRawData 파일에서 섹션의 시작 위치
Characteristics 섹션의 속성(bit OR)

VirtualAddress와 PointerToRawData는 아무 값이나 가질 수 없고, 위에서 정의된 Sectionalignment와 Filealignment에 맞게 설정되어짐.

VirtualSize와 SizeOfRawData는 일반적으로 서로 다른 값을 가짐.

즉 파일에서의 섹션크기와 메모리에 로딩되는 섹션의 크기는 서로 다릅니다!

 

해당 부분이 IMAGE_SECTION_HEADER 구조체 배열!

그리고 각각의 .text, .data, .rsrc의 멤버들의 내용은 같습니다.

 

* 여기서 잠깐 이미지란 ?

- PE 파일이 메모리에 로딩되어질 때 파일이 그대로 올라가는게 아니라 섹션 헤더에 정의된 대로 섹션 시작 주소, 섹션 크기 등에 맞춰서 올라가게 됨. 따라서 PE와 메모리에서의 PE는 서로 다른 형태이고, 메모리에 로딩된 상태의 PE를 '이미지' 라고 부는 것임.

 

RVA, RAW

프로그램이 시작되기전의 주소를 offset 또는 'RAW'라고 부릅니다.

빨강네모!

 

실행되기전의 주소를 말할때는 이 부분을 참고하면 됩니당.

아무튼 다음에 알아볼건 'RVA' 입니다.

RVA는 얼마만큼 떨어져 있는지를 의미합니다 기준점은 어디서요? VA에서요 ^^

 

VA(Virtual Address)는 메모리의 어딘가의 주소이기 때문에 offset과는 전혀다른 개념이에요

window도 memory map file을 사용하기 때문에 파일을 실행하게 되면 가상 메모리 공간에 프로그램이 올라가게 되는데 이 과정을 'mapping'이라고 하는듯.

 

그런데 이 mapping이 되는 위치가 바로 NT Header 중에서 IMAGE_OPTIONAL_HEADER32구조체 안에있는 ImageBase를 의미함. VA는 ImageBase를 기준으로 생성된다고 봐도 무관할 것 같다. 실제 메모리에서는 이 VA주소를 보고 프로그램을 실행시키게 된다.

 

* PE의 대부분 주소가 RVA인 이유 ?

- 프로그램은 하나만 실행되지 않는다. 동시에 여러개의 프로그램을 실행할 때 이미 해당주소에 프로그램이 있을경우 하나의 주소에 두개의 프로그램이 실행될 순 없고, window os는 ASLR을 쓰기 때문에 실행마다 주소가 변경된다 따라서 변경된 주소에서 '얼마만큼 떨어져 있느냐' RVA를 통해 시작주소가 바뀌어도 프로그램은 정상적으로 구동된다.

이를 PE relocation 이라고 한다.

 

* 퀴즈 :-3

RVA = 5000일 때, RAW = ?

 

일단 RVA - VA = RAW - PointerToRawData 이다. (매핑과정에서 주소가 바꼈더라도 떨어진 거리는 같으므로 ??)

RAW = RVA - VA + PointerToRawData

RAW = 5000 - 1000 + 400 = 4400 ^^ (위 그림 참고)

 

★ IAT

- IAT ? 프로그램이 어떤 라이브러리에서 어떤 함수를 사용하고 있는지를 기술한 테이블입니다.

- DLL ? '동적 연결 라이브러리', (프로그램에 library를 포함시키지 않고 별도의 DLL로 구성하여 필요할 때 불러씀)

 

자이언티 - 꺼내먹어요 ~

 

DLL의 로딩방식은 2가지 입니다. 

- Explicit Linking : 프로그램에서 사용되는 순간에 로딩하고 사용이 끝나면 메모리에서 해제

- Implicit Linking : 프로그램 시작과 같이 로딩되고 프로그램 종료시 메모리에서 해제

IAT는 후자의 메커니즘을 제공하는 역할을 합니다.

프로그램은 다른 모듈을 호출할 때 다음과 같은 형식으로 호출

GetCureentProcessId를 호출할 때 직접호출하지 않고 002410CC 주소에 있는 값을 가져와서 호출하는 방식임

제작자가 컴파일시에 실제주소가 002410CC에 올라오면서 실행됨

call dword ptr ds:[api 여기에 넣어라!]

 

'Reversing' 카테고리의 다른 글

리버싱 6  (2) 2019.04.04
PE file(3)  (0) 2019.04.03
PE File(1)  (0) 2019.04.02
리버싱 5  (0) 2019.04.02
리버싱 4  (0) 2019.03.29

앙 규문띻 

 

 

PE file

오늘은 PE file 구조에 대해서 공부해야겠다.

그전에 PE파일에 대해서 잘 정리해놓은 블랙펄 콘치님의 PE둥 PE둥 시리즈들을 먼저 보고오면 좋을듯하다. (짱짱;;)

 

[PE둥 PE둥 놀면서 훑어보는 PE(1)]

[PE둥 PE둥 놀면서 훑어보는 PE(2)]

[PE둥 PE둥 놀면서 훑어보는 PE(3)]

 

PE file 이란 ? 

- PE 포맷(Portable Executable)은 윈도우 운영 체제에서 사용되는 실행 파일, DLL, object 코드, FON 폰트 파일[1] 등을 위한 파일 형식이다 (위키)

 

즉 .exe 뿐만아니라 dll, sys같은 파일들도 전부 PE 파일이라고 명칭할 수 있다고 한다.

에그 복잡해;;;

이미지 - https://slimv.tistory.com/entry/PE-File-Format-0x01

 

먼저 헤더부터 살펴보자.

PE 파일의 헤더는 3가지로 구분됩니다.

1) DoS Header

2) NT Header

3) Section Header

 

DoS Header

DoS쓰지도않는데 DoS헤더가 왜있을까!

=> PE file format을 만들 당시 널리 사용되던 DOS파일에 대해 하위호환성을 고려해서 만들었음.

그 결과로 인해 PE 헤더의 제일 앞부분에 기존 DOS EXE Header를 확장시킨 IMAGE_DOS_HEADER 구조체가 존재.

 

그렇다면 구조체 내용을 보자!

typedef struct _IMAGE_DOS_HEADER {
     WORD  e_magic;      /* 00: MZ Header signature */
     WORD  e_cblp;       /* 02: Bytes on last page of file */
     WORD  e_cp;         /* 04: Pages in file */
     WORD  e_crlc;       /* 06: Relocations */
     WORD  e_cparhdr;    /* 08: Size of header in paragraphs */
     WORD  e_minalloc;   /* 0a: Minimum extra paragraphs needed */
     WORD  e_maxalloc;   /* 0c: Maximum extra paragraphs needed */
     WORD  e_ss;         /* 0e: Initial (relative) SS value */
     WORD  e_sp;         /* 10: Initial SP value */
     WORD  e_csum;       /* 12: Checksum */
     WORD  e_ip;         /* 14: Initial IP value */
     WORD  e_cs;         /* 16: Initial (relative) CS value */
     WORD  e_lfarlc;     /* 18: File address of relocation table */
     WORD  e_ovno;       /* 1a: Overlay number */
     WORD  e_res[4];     /* 1c: Reserved words */
     WORD  e_oemid;      /* 24: OEM identifier (for e_oeminfo) */
     WORD  e_oeminfo;    /* 26: OEM information; e_oemid specific */
     WORD  e_res2[10];   /* 28: Reserved words */
     DWORD e_lfanew;     /* 3c: Offset to extended header */
 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

크기는 총 40이다. 여기서 일단 살펴볼껀 2가지를 살펴보자. 

- e_magic

- e_lfanew

 

e_magic : DOS signature 4D5A를 나타낸다. ('MZ')

MZ 보이시죠 헤헿

PE의 스펙에 맞게 '4D5A'의 2바이트로 시작하며 'MZ'가 보입니다. 그 다음으로

e_lfanew : NT Header의 offset을 나타낸다.

빨간네모박스가 e_lfanew 값입니다. 000000D8 입니다. (리틀 엔디언)

실제 000000D8에 'PE'가 보이네요 [NT Header 시작부분]

이 값들은 PE header이기 때문에 변조하면 정상 실행되지 않습니다.

 

DoS Stub

다음으로 DoS Stub 입니다.

DoS Header와 NT Header 사이가 그렇다면 DoS Stub이 되겠죠 ??

이 부분입니다!

DOS Stub의 존재여부는 옵션이며 크기도 일정하지 않습니다. (없어도 파일실행과 상관없음!)

위그림에서 offset 40~4D(this 전) 까지는 16비트 어셈영역이셈 32비트 운영체제에서 실행안됌. (DOS환경에서 실행)

=> 즉 DoS환경과 window 환경 둘다 호환되는 실행파일을 만들 수 있다?!

 

NT Header

다음으로 NT Header의 구조체 Image_NT_HEADERS입니다.

 

typedef struct _IMAGE_NT_HEADERS {
  DWORD Signature;
  IMAGE_FILE_HEADER FileHeader;     /* 0x04 */
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;   /* 0x18 */
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;
  WORD  NumberOfSections;
  DWORD TimeDateStamp;
  DWORD PointerToSymbolTable;
  DWORD NumberOfSymbols;
  WORD  SizeOfOptionalHeader;
  WORD  Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

typedef struct _IMAGE_OPTIONAL_HEADER {  
 
   /* Standard fields */
   WORD  Magic; /* 0x10b or 0x107 */ /* 0x00 */
   BYTE  MajorLinkerVersion;
   BYTE  MinorLinkerVersion;
   DWORD SizeOfCode;
   DWORD SizeOfInitializedData;
   DWORD SizeOfUninitializedData;
   DWORD AddressOfEntryPoint;        /* 0x10 */
   DWORD BaseOfCode;
   DWORD BaseOfData;
 
   /* NT additional fields */
   DWORD ImageBase;
   DWORD SectionAlignment;       /* 0x20 */
   DWORD FileAlignment;
   WORD  MajorOperatingSystemVersion;
   WORD  MinorOperatingSystemVersion;
   WORD  MajorImageVersion;
   WORD  MinorImageVersion;
   WORD  MajorSubsystemVersion;      /* 0x30 */
   WORD  MinorSubsystemVersion;
   DWORD Win32VersionValue;
   DWORD SizeOfImage;
   DWORD SizeOfHeaders;
   DWORD CheckSum;           /* 0x40 */
   WORD  Subsystem;
   WORD  DllCharacteristics;
   DWORD SizeOfStackReserve;
   DWORD SizeOfStackCommit;
   DWORD SizeOfHeapReserve;      /* 0x50 */
   DWORD SizeOfHeapCommit;
   DWORD LoaderFlags;
   DWORD NumberOfRvaAndSizes;
   IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; /* 0x60 */
 } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

복잡;;;

 

먼저 IMAGE_NT_HEADERS 부터 봅시다...

typedef struct _IMAGE_NT_HEADERS {
  DWORD Signature;
  IMAGE_FILE_HEADER FileHeader;     /* 0x04 */
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;   /* 0x18 */
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

제일 첫번째 멤버로 Signature(50450000)을 가집니다 'PE'글자를 나타내며 이글자가 보이면 NT header의 시작이라고 볼 수 있습니다.

오오

다음으로 밑에 2개의 구조체가 있네요?

먼저 파일의 대략적인 속성을 나타내는 NT header - File header 입니다.

typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;
  WORD  NumberOfSections;
  DWORD TimeDateStamp;
  DWORD PointerToSymbolTable;
  DWORD NumberOfSymbols;
  WORD  SizeOfOptionalHeader;
  WORD  Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

이 구조체에서는 4가지 멤버가 중요합니다. (이 값이 정확하지 않으면 파일이 정상실행 되지않는다고 함.)

 

#1. Machine

Machine 넘버는 CPU 별로 고유한 값이며 32비트 Intel x86 호환 칩은 14C의 값을 가짐

=> 쉽게말해 이게 어떤 환경에 돌아가는지 알려줌 ^호^

014C면 intel 386 환경!

Machine의 넘버링은 아래를 참조하자!

/* These are the settings of the Machine field. */
#define IMAGE_FILE_MACHINE_UNKNOWN  0
#define IMAGE_FILE_MACHINE_I860     0x014d
#define IMAGE_FILE_MACHINE_I386     0x014c
#define IMAGE_FILE_MACHINE_R3000    0x0162
#define IMAGE_FILE_MACHINE_R4000    0x0166
#define IMAGE_FILE_MACHINE_R10000   0x0168
#define IMAGE_FILE_MACHINE_WCEMIPSV2    0x0169
#define IMAGE_FILE_MACHINE_ALPHA    0x0184
#define IMAGE_FILE_MACHINE_SH3      0x01a2
#define IMAGE_FILE_MACHINE_SH3DSP   0x01a3
#define IMAGE_FILE_MACHINE_SH3E     0x01a4
#define IMAGE_FILE_MACHINE_SH4      0x01a6
#define IMAGE_FILE_MACHINE_SH5      0x01a8
#define IMAGE_FILE_MACHINE_ARM      0x01c0
#define IMAGE_FILE_MACHINE_THUMB    0x01c2
#define IMAGE_FILE_MACHINE_ARMNT    0x01c4
#define IMAGE_FILE_MACHINE_ARM64    0xaa64
#define IMAGE_FILE_MACHINE_AM33     0x01d3
#define IMAGE_FILE_MACHINE_POWERPC  0x01f0
#define IMAGE_FILE_MACHINE_POWERPCFP    0x01f1
#define IMAGE_FILE_MACHINE_IA64     0x0200
#define IMAGE_FILE_MACHINE_MIPS16   0x0266
#define IMAGE_FILE_MACHINE_ALPHA64  0x0284
#define IMAGE_FILE_MACHINE_MIPSFPU  0x0366
#define IMAGE_FILE_MACHINE_MIPSFPU16    0x0466
#define IMAGE_FILE_MACHINE_AXP64    IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE  0x0520
#define IMAGE_FILE_MACHINE_CEF      0x0cef
#define IMAGE_FILE_MACHINE_EBC      0x0ebc
#define IMAGE_FILE_MACHINE_AMD64    0x8664
#define IMAGE_FILE_MACHINE_M32R     0x9041
#define IMAGE_FILE_MACHINE_CEE      0xc0ee

[Machine 넘버 참조]

 

#2. NumberOfSections

PE 파일은 코드, 데이터, 리소스가 각각의 세션에 나뉘어져서 저장됩니다.

이 멤버는 그 섹션의 개수를 나타낸다고 합니다. 이 값은 반드시 0보다 커야하며 정의된 섹션 개수와 실제 섹션이 다르면 실행 에러가 발생합니다.

 

위 그림에서 4개의 섹션을 가지고 있다고 합니다. PEview를 통해서 확인해볼까요?

놀랍군요! ㅋㅋㅋㅋㅋ

#3. SizeofOptionalHeader

IMAGE_NT_HEADERS 구조체체의 마지막 멤버는 IMAGE_OPTIONAL_HEADER32 입니다.

이 멤버는 바로 IMAGE_OPTIONAL_HEADER32 구조체의 크기를 나타냅니다.

PE32+의 경우에는  IMAGE_OPTIONAL_HEADER64 구조체를 활용합니다. (두개의 구조체 크기는 다름)

=> 이렇게 구조체의 범위를 미리 잡아놓고 그만큼의 데이터를 파일의 정보로 받아들임 ㅋ.

 

#4. Characteristics

파일의 속성을 나타내는 값으로, 실행 가능한 형태인지 혹은 DLL파일인지의 정보들이 bit OR 형식으로 조합됨

ex) '0102' = '0100' + '0002'

=> PE 파일이 어떤 속성을 가지고 있는지 조합된 숫자로 파악한다!

 

0002와 2000은 기억하도록 하자(실행가능과 DLL)

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  
                                                     // (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from 
                                                     // file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, 
                                                     // copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, 
                                                     // copy and run from the swap file.
#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

[Characteristic]

'Reversing' 카테고리의 다른 글

PE file(3)  (0) 2019.04.03
PE File(2)  (0) 2019.04.03
리버싱 5  (0) 2019.04.02
리버싱 4  (0) 2019.03.29
리버싱 3 Stackframe  (0) 2019.03.28

Crack me #2 알고리즘 알아내기

- x32dbg로 abex' crack me 씨리얼 생성 알고리즘을 파악해보자.

 

먼저 abex' crack me #2를 디버거로 오픈한다.

 

저번장에서 우린이미 프로그램을 크랙했었다. 이제 시리얼 생성 알고리즘을 파악할 시간이다.

 

1. Serial 생성 알고리즘 찾기

프로그램의 check 버튼을 누르면 시리얼과 일치하는지를 검사하도록 되어있다.

전에 봤던 조건분기문은 분명히 [Check] 버튼의 이벤트로 발생하는 event handler? 일 것이다.

그렇다면 함수의 시작 부분부터 쭉~ 보다보면 다음과 같은 코드를 볼 수 있다.

 

* (ID는 ReverseCore로 입력했다)

호호

위의 push, mov는 전형적인 스택프레임을 구성할때 보이는 모습이고, 이부분이 함수의 시작임을 직관적으로(?) ㅇㅇ

따라서 이부분이 함수의 시작임을 알 수 있음.

그래서 여기에 캠프차리고 디버깅을 시작함.

[F8]막 눌러보면서 변화를 살펴보자!

 

몇번의 시행착오를 겪다보면 다음과 같은 코드를 만나게됨

AX값이 0이면 바로밑의 jmp문을 수행

에러문구를 보니 Name에 4글자가 안되면 AX가 0이 안되나보다! 아무튼 4글자 이상의 Name을 넣고 점프를 해보면!

다음과 같은 곳으로 뛰게된다.

짜잔!

mov, push는 스택프레임을 생성할 때 보이는 전형적인 패턴임을 알고간다.

루프문의 구조는 다음과 같다.

 

MOV EBX, 4								; 4개만 돌림
...
CALL DWORD PTR DS:[&~__vbaVarForInit]		
TEST EAX,EAX								; if(EAX==0) [loop start] 0되면 루프나감 ^^
JE abexcme2-.004032A5						
...
CALL dWORD PTR DS:[&~__vbaVarForNext]		
JMP abexcme2-00403197							; loop end
MOV EAX, DWORD PTR SS;[EBP+8]

여기가 원하는 알고리즘을 생성하는 부분이다.

그렇다면 이제 암호화 방법에 대해서 알아보자! 밑으로 내리다보면 다음의 부분이있다.

첫번째 PUSH(004031F6)에서 Name에서 가져온 유니코드값을 넣는다. ('R')

이후에 그 유니코드를 ASCII 변환을 한다 (004031F7)

 

0040323D까지 실행한후에 스택을 살펴보자.

ECX : 0018F3C4

EAX : 0018F384

EDX : 0018F40C

 

[CPU 범용 레지스터 까묵;;]

 

[보안] CPU 범용 레지스터

CPU 범용 레지스터 (EAX,EBX,ECX,EDX,EBP,ESI,EDI,ESP) - CPU 범용 레지스터란 범용적으로 사용되는(?) 레지스터 입니다. 모든 레지스터들은 각각 4byte(32bit)의 크기를 가진다고 하네요! 8개의 레지스터에서 상..

kyumoonhan.tistory.com

그럼 이제 각 메모리주소를 살펴본다.

ECX
EAX
EDX

다음을보면 ECX에서 18F3CC는 결과저장용 버퍼이다.

EAX의 18F38C에 암호화키 (64),

EDX에서 18F414는 Name에서 문자열의 첫 ASII키 값(내가입력한 'R' 52) 이다.

 

이 후에 밑의 함수를 실행시키면 ECX 레지스터가 가리키는 버퍼에 암호화된 값이 저장된다.

52 + 64 = B6 이겠죠?
ECX 결과값을 보니 정답!

계산결과 B6은 보다시피 원본값('R')에 64를 더해서 생성한 값임을 알 수 있었다!

이후에 아래의 부분을 이용해 'B6'값을 유니코드로 변경한다.

마지막 함수 호출시 변환

함수 호출직후 EAX가 가르키는 버퍼(0018F3C4)를 살펴보자

B6이 성공적으로 들어감!

이렇게 4번을 돌리겠죠? 끝으로 생성된 문자열을 붙여주는 코드가 뒤에 있다.

 

문자열 붙이기

- vbaVarCat() : 문자열 이어붙이기

함수 호출시 : old(ECX) + add(EDX) = NEW serial(EAX) ^오^

 

이런식으로 알고리즘이 짜여져있다.

그렇게 마지막 루프를 실행하게 되면

 

Serial = old("B6C9DA") + add("C9") = "B6C9DAC9"

이렇게 암호화 알고리즘을 풀었다.

(Reve의 아스키값에 64씩 더헀네)

 

정리

1. Name 문자열을 앞에서 한 문자씩 4회 읽는다.

2. Ascii로 변환한다.

3. 변환된거에 64를 더한다.

4. 다시 유니코드로 변환한다.

5. 변환된 문자를 연결시킨다.

 

 

 

 

 

'Reversing' 카테고리의 다른 글

PE File(2)  (0) 2019.04.03
PE File(1)  (0) 2019.04.02
리버싱 4  (0) 2019.03.29
리버싱 3 Stackframe  (0) 2019.03.28
리버싱 2  (0) 2019.03.27

+ Recent posts