Post

프로세스 이미지 변조 (ImageNtHeader)

STATUS_INVALID_IMAGE_FORMAT (0xC000007B)

해킹툴 분석 중 특정 프로세스에 스레드 생성을 전부 차단해버리는 재밌는 녀석을 발견했습니다. 해킹툴이 적용되면 OpenProcess(PROCESS_ALL_ACCESS)를 통해 정상적인 핸들을 획득했더라도 CreateThread, RemoteCreateThread, NtCreateThread 등 전부 실패하며 STATUS_INVALID_IMAGE_FORMAT(0xC000007B)를 반환합니다.

해당 값은 MS-ERREF 문서에 나와있는데 이미지가 잘못되었음을 나타냅니다.

Return value/codeDescription
0xC000007B

STATUS_INVALID_IMAGE_FORMAT
{Bad Image} %hs is either not designed to run on
Windows or it contains an error. Try installing the
program again using the original installation media
or contact your system administrator or the software
vendor for support.

NtCreateThread

해당 오류는 syscall 단계에서 실패했기 때문에 커널 레벨에서 분석을 진행하였습니다.

리턴되는 값인 0xC000007B을 검색하면 여러 함수가 나오는데, 이 중에서 스레드와 관련된 함수를 우선적으로 분석하였습니다.

유저 레벨에서 스레드를 생성할 때, RtlImageNtHeaderEx 함수는 다음과 같은 순서대로 호출됩니다.

RingCall Stacks
3CreateThread
3CreateRemoteThread
3NtCreateThread
0PspCreateThread
0PspAllocateThread
0PspSetupUserStack
0RtlCreateUserStack
0RtlImageNtHeader
0RtlImageNtHeaderEx

RtlImageNtHeaderEx

해당 함수에서 STATUS_INVALID_IMAGE_FORMAT를 반환하는 코드는 다음과 같습니다.

해당 코드를 C++ 형태로 번역하면 대충 다음과 같습니다. 호출 시 파라미터로 프로세스 이미지를 전달받으며, DOS와 NT 헤더의 시그니처 값을 비교합니다. 지정된 값과 일치하지 않으면 STATUS_INVALID_IMAGE_FORMAT를 반환하며 스레드 생성 또한 실패하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NTSTATUS RtlImageNtHeaderEx(..., BYTE ProcessImage[], ...)
{
    ... 

    if (PIMAGE_DOS_HEADER(ProcessImage)->magic == "MZ")
    {
        LONG offset = PIMAGE_DOS_HEADER(ProcessImage)->e_lfanew;

        if (PIMAGE_NT_HEADERS(&ProcessImage[offset])->Signature == "PE")
        {
            // valid image
            return ...
        }
    }

    // invalid image
    return STATUS_INVALID_IMAGE_FORMAT;
}

PE Header

해킹툴이 적용된 대상 프로세스의 메모리를 덤프해봤습니다. 다음과 같이 프로세스 이미지의 DOS 및 NT를 포함하여 PE Header 전체가 0x00으로 초기화되어 있습니다. 이로 인하여 커널에서 RtlImageNtHeaderEx 호출이 실패하고 스레드 생성이 실패된 것으로 보입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2: kd> lmDvmdwm
Browse full module list
start             end                 module name
00007ff6`40d10000 00007ff6`40d2f000   dwm        (deferred)             
    Image path: C:\windows\system32\dwm.exe
    Image name: dwm.exe
    Browse all global symbols  functions  data
    Timestamp:        Thu May 13 11:29:41 2027 (6BE51595)
    CheckSum:         missing
    ImageSize:        0001F000
    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4
    Information from resource tables:

2: kd> db dwm.exe
00007ff6`40d10000  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`40d10010  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`40d10020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`40d10030  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`40d10040  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`40d10050  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`40d10060  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`40d10070  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

정상적인 경우에는 다음과 같이 ‘MZ’로 시작하는 메모리가 존재해야 합니다.

1
2
3
4
5
6
7
8
9
0: kd> db dwm.exe
00007ff6`40d30000  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ..............
00007ff6`40d30010  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ........@.......
00007ff6`40d30020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`40d30030  00 00 00 00 00 00 00 00-00 00 00 00 00 01 00 00  ................
00007ff6`40d30040  0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68  ........!..L.!Th
00007ff6`40d30050  69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f  is program canno
00007ff6`40d30060  74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20  t be run in DOS 
00007ff6`40d30070  6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00  mode....$.......

POC Code

분석한 내용을 바탕으로 해킹툴과 동일하게 스레드 생성을 방해하는 코드를 개발하였습니다. 전체 소스코드는 GitHub에 업로드 하였습니다. x64를 기준으로 제작되었기 때문에 x86 파일에 대해서는 따로 포팅 작업이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL ProtectThreadCreation(PVOID BaseAddr)
{
	DWORD OldProtect;
	if (!VirtualProtect(BaseAddr, PAGE_SIZE, PAGE_READWRITE, &OldProtect))
		return FALSE;

#ifndef REMOVE_ALL_HEADER
	PIMAGE_DOS_HEADER dos	= PIMAGE_DOS_HEADER(BaseAddr);
	PIMAGE_NT_HEADERS nt	= PIMAGE_NT_HEADERS(PBYTE(BaseAddr) + dos->e_lfanew);

	nt->Signature	= 0;
	dos->e_lfanew	= 0;
	dos->e_magic	= 0;
#else
	memset(BaseAddr, 0x00, PAGE_SIZE);
#endif // REMOVE_ALL_HEADER

	VirtualProtect(BaseAddr, PAGE_SIZE, OldProtect, &OldProtect);

	return TRUE;
}

반대로 해킹툴에 의해 이미 헤더가 지워진 상태라면, 다음과 같이 복원하여 스레드 생성이 가능합니다. NT 헤더는 DOS Stub이 가변성을 가지고 있으므로, 임의로 위치를 지정하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
// "MZ"
dos->e_magic = 0x5A4D;

if (!dos->e_lfanew)
{
    dos->e_lfanew = sizeof(IMAGE_DOS_HEADER);
}

nt = PIMAGE_NT_HEADERS(PBYTE(BaseAddr) + dos->e_lfanew);

// "PE"
nt->Signature	= 0x4550;

보호가 적용된 이후에 다음과 같이 치트엔진 스크립트를 통해 코드 인젝션을 시도했으나, 스레드 생성이 실패하였습니다.

1
2
3
4
5
6
7
// Auto Assemble
Alloc(CodeInjection, 0x1000)
CreateThread(CodeInjection)

CodeInjection:
mov byte ptr [CodeInjection+0x100], 0x01
ret

NtCreateThread가 실패하여 STATUS_INVALID_IMAGE_FORMAT(0xC000007B)을 반환합니다. 해당 방식은 외부로부터 생성하는 스레드를 차단한다는 장점이 존재하지만, 내부에서 생성하는 것조차 방해하기 때문에 활용하기에는 까다로울 수 있는 방식같습니다.

This post is licensed under CC BY 4.0 by the author.