OBJECT_HEADER에서 OBJECT_HEADER_NAME_INFO를 어떻게 얻을까 다양한 방법으로 구글링해보다가 ObQueryNameInfo(PVOID pObject) 함수가 있다는 것을 발견했습니다. 이 함수를 쓰면 POBJECT_HEADER_NAME_INFO 주소를 리턴하더군요. 어떻게 얻나 궁금해서 windbg로 까봤습니다.
0: kd> u ObQueryNameInfo
nt!ObQueryNameInfo:
fffff803`6987b920 0fb641ea movzx eax,byte ptr [rcx-16h]
fffff803`6987b924 4883c1d0 add rcx,0FFFFFFFFFFFFFFD0h
fffff803`6987b928 a802 test al,2
fffff803`6987b92a 7416 je nt!ObQueryNameInfo+0x22 (fffff803`6987b942)
fffff803`6987b92c 83e003 and eax,3
fffff803`6987b92f 488d154aa55a00 lea rdx,[nt!ObpInfoMaskToOffset (fffff803`69e25e80)]
fffff803`6987b936 0fb60410 movzx eax,byte ptr [rax+rdx]
fffff803`6987b93a 482bc8 sub rcx,rax
fffff803`6987b93d 488bc1 mov rax,rcx
fffff803`6987b940 c3 ret
먼저 인자에서 0x16을 뺀것을 포인터 참조해서 저장하고, 그 다음은 0x30만큼 빼고 주소를 저장하네요. (인자로는 DRIVER_OBJECT 0xffffc28f1318ce30 를 줘봤습니다. )
일단 0x30만큼 뺀 것은 OBJECT_HADER 구조체였습니다. (대충 감으로 때려맞춤...)
드라이버를 서비스 형태로 등록한 경우 DRIVER_OBJECT가 할당되며, 이 구조체로부터 드라이버 정보가 담긴 LDR_DATA_TABLE_ENTRY 구조체에 다다를 수 있다.
hidusb.sys를 샘플로 잡고 windbg에서 DRIVER_OBJECT 구조체를 살펴보면 0x28 오프셋에 DriverSection이 있는데 이는 LDR_DATA_TABLE_ENTRY 구조체를 가리킨다.
0x0 오프셋 구조체에 다음&이전 드라이버의 구조체 주소가 담겨있다.
다음&이전 구조체를 가리키는 Flink, Blink는 LDR_DATA_TABLE_ENTRY로 표현된다.
아래와 같이 Flink 값의 구조체를 살펴보면 다음 드라이버를 가리키는 것을 확인할 수 있다.
여기서, 숨기고자 하는 드라이버의 Flink와 Blink를 자신의 LDR_DATA_ENTRY를 가리키게 하고, 이전 드라이버 구조체의 Flink 를 다음 드라이버 구조체의 Blink와 이으면 LDR_DATA_TABLE_ENTRY 트레버싱이나 ZwQuerySystemInformation()의 드라이버 목록에서 사라지게 된다.
PLDR_DATA_TABLE_ENTRY PsLoadedModuleList;
void GetPsLoadedModuleList(PLDR_DATA_TABLE_ENTRY currentEntry)
{
while (TRUE)
{
if (NULL == currentEntry->DllBase)
{
PsLoadedModuleList = currentEntry;
break;
}
currentEntry = (PLDR_DATA_TABLE_ENTRY)currentEntry->InLoadOrderLinks.Flink;
}
}
void HideDriver(PCWSTR szFileName)
{
PLDR_DATA_TABLE_ENTRY prevEntry, nextEntry, Entry;
Entry = (PLDR_DATA_TABLE_ENTRY)PsLoadedModuleList->InLoadOrderLinks.Flink;
while (Entry != PsLoadedModuleList)
{
if (0 == wcscmp((PCWSTR)Entry->BaseDllName.Buffer, szFileName))
{
prevEntry = (PLDR_DATA_TABLE_ENTRY)Entry->InLoadOrderLinks.Blink; // 이전 노드
nextEntry = (PLDR_DATA_TABLE_ENTRY)Entry->InLoadOrderLinks.Flink; // 다음 노드
prevEntry->InLoadOrderLinks.Flink = Entry->InLoadOrderLinks.Flink; // 이전 노드의 다음을 내 다음으로 바꿈
nextEntry->InLoadOrderLinks.Blink = Entry->InLoadOrderLinks.Blink; // 다음 노드의 이전을 내 이전로 바꿈
// 내 노드의 앞, 뒤를 나 자신으로 바꿈
// 바꾸지 않는다면 드라이버 서비스를 stop할때 BSOD발생 (KERNEL SECURITY CHECK FAILURE)
Entry->InLoadOrderLinks.Flink = (PLIST_ENTRY)Entry;
Entry->InLoadOrderLinks.Blink = (PLIST_ENTRY)Entry;
break;
}
Entry = (PLDR_DATA_TABLE_ENTRY)Entry->InLoadOrderLinks.Flink;
}
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath)
{
UNREFERENCED_PARAMETER(pRegistryPath);
pDriverObject->DriverUnload = DriverUnload;
PLDR_DATA_TABLE_ENTRY pCurrentTableEntry = (PLDR_DATA_TABLE_ENTRY)pDriverObject->DriverSection;
GetPsLoadedModuleList(pCurrentTableEntry);
HideDriver(L"hidusb.sys");
}
int main()
{
VIRTUALIZER_START
int a = 0x12345678;
VIRTUALIZER_END
printf("%d", a);
}
빌드한 바이너리를 디버거에 붙여보면 아래와 같은 형태가 됩니다.
가상화되는 부분은 0x411900의 7바이트입니다.
이 바이너리를 Code Virtualizer에 넣고 protect 버튼을 누르면, 코드 가상화 및 난독화 기능을 수행합니다.
프로그램의 동작 순서는 아래와 같습니다.
암/복호화 구조체 생성 → KEY 초기화(with time seed) → 레지스터 인덱스 생성 → 핸들러 구조체 생성 → 핸들러에 복호화 코드 추가 → 핸들러 난독화 → 원본 어셈블리어 파싱 및 가상 CPU가 읽을 수 있는 코드로 변환 → 구조체1,2,3 생성 → Prolog & Epilog 핸들러 추가 → fake 핸들러 추가 → 다음 핸들러를 가리키기 위한 핸들러 번호 및 명령어에 대한 데이터 암호화 → 파일에 저장
1) 코드 가상화
Code Virtualizer는 코드 가상화를 위해 가상화에 필요한 정보가 담긴 3개의 구조체를 생성합니다.
첫 번째 구조체 생성을 위해 가장 먼저 하는 일은 가상화 대상의 어셈블리어 코드를 기반으로 아래와 같은 텍스트 형태의 어셈블리어를 생성합니다.
section .code base 0000000h code
MOV dword ptr [EBP + 0fffffff8h], 012345678h
PUSH 000411919h // 복귀를 위해 Code Virtualizer가 추가
RET
LOAD DWORD 0x12345678 가상 CPU는 일반 CPU와 명령어를 읽는 구조가 다르므로, 이를 가상 CPU가 인식할 수 있는 문법으로 변환합니다. (0x41EFB8 함수)
MOVE ADDR, DWORD PTR [0xF0000030] // F0000030는 EBP에 대한 범용 레지스터 변수를 의미. 추후 다룸
ADD ADDR, 0xFFFFFFF8
STORE DWORD PTR [ADDR]
LOAD 0x411919h
RET
이를 가지고 가상 CPU의 명령어, 접미어가 담긴 구조체.1을 생성합니다.
구조체.1는 14바이트입니다. x64 파일인 경우 4바이트 명령어 데이터가 추가되어 18바이트가 되지만 다루지 않겠습니다.
0x00 1바이트 : 가상 CPU 명령어 번호
0x01 2바이트 : 미사용 (0x00, 0x00)
0x03 1바이트 : 0x80일 경우 명령어에 상대 주소가 있음을 의미함
0x04 4바이트 : 명령어에 대한 접미어 번호
0x08 4바이트 : 명령어에 대한 데이터
0x0C 2바이트 : 미사용 (0x00, 0x00)
가상 CPU의 명령어는 52개, 접미어는 36개가 있습니다. 아래는 예제에서 사용하는 일부입니다.
Reversing Tip
전역변수 0x70A810에 구조체.3 주소가 저장되어 있습니다.
0x606D23 함수에서 구조체.3에 값을 복사하는 작업을 수행합니다.
구조체.3은 24바이트입니다.
0x00 (2바이트) : 핸들러 번호
0x02 (4바이트) : 구조체.2 메모리 주소
0x06 (4바이트) : 암호화 된 코드 메모리 주소 (Code Virtualizer.exe에서의 주소)
0x0A (1바이트) : 명령어에 대한 데이터를 몇 바이트를 읽을 것인가? 읽을 필요가 없는 경우 0 (즉, 핸들러에 LODS 명령어가 포함되어 있지 않은 경우), LODS가 포함되어 있다면 핸들러에 따라 1(BYTE), 2(WORD), 4(DWORD), 8(QWORD)
0x0B (1바이트) : 구조체.2에 상대주소가 있거나 접미어가 FLAGS인 경우 TRUE
0x0C (4바이트) : 명령어에 대한 데이터
0x10 (4바이트) : 명령어에 대한 x64 확장 데이터 (x86에선 0x00 0x00 0x00 0x00)
0x14 (4바이트) : 암호화 된 데이터 (target.exe)
핸들러에 대한 설명이 필요한 경우, 2절을 미리 읽고 오시는 것을 추천합니다.
0x60802B 함수에서 구조체.3을 만드는데, 구조체.2의 0x11, 0x12, 0x27에 따라 필요한 핸들러의 갯수와 각 핸들러마다 구조체.3의 0x00, 0x0A, 0x0B, 0x0C를 지정합니다.
만약 명령어에 상대 주소가 있는 경우(구조체.2 0x27), 0x0C는 본래의 값에 특수한 연산을 취한 값을 가지게 됩니다.
target의 ".v-lizer" 섹션에 구조체.3의 핸들러 번호와 명령어 데이터를 담은 바이너리를 저장하는데, 키 및 암/복호화 구조체를 가지고 암호화(SUB/ADD/XOR)를 한 후 저장합니다.
키의 경우 최초로 설정된 값은 target에서의 암호화 된 바이너리가 저장된 address이며, 각 핸들러 실행 시마다 계속 변하게 됩니다. 체인 형태로 되어 있어 이전 핸들러가 실행되어야만 다음 핸들러의 키 값을 알 수 있습니다.
복호화를 위해 Code Virtualizer는 target의 각 핸들러(150개 + NEXT핸들러)에 복호화 코드를 추가합니다. 복호화는 암호화의 역순으로 합니다. 가령, 암호화 시에 1.SUB → 2.ADD → 3.ADD를 수행했다면, 복호화 시에 3.SUB → 2.SUB → 1.ADD를 수행하는 코드를 추가합니다.
복호화 코드는 핸들러에 LODS명령어(암호화 된 바이너리를 꺼내오는)가 있을 경우에만 추가됩니다.
Reversing Tip
암호화 로직 생성 : 0x6087BB
복호화 로직 생성 : 0x6081A9
LODS가 읽어들이는 바이트 수(1/2/4)에 따라 동일 사이즈로 암/복호화를 하게 됩니다.
LODS 명령어 바로 밑에 4개의 복호화 명령어가 추가됩니다. 이는 암호화 시 생성되는 암/복호화 구조체([0x70A870])와 밀접하게 연관되어 있습니다.
암/복호화 구조체는 2가지 경우에 사용됩니다.
암호화 : 핸들러 번호 암호화 (NEXT핸들러 구조체 사용), 각 핸들러에서 처리되는 명령어에 대한 데이터 1/2/4바이트 암호화
복호화 : NEXT 핸들러에 복호화 로직 추가 (NEXT 핸들러 구조체 사용), 각 핸들러에 복호화 로직 추가
암/복호화 구조체에는 4개의 암/복호화 명령어 타입과 2개의 랜덤키가 정의되어 있습니다.
0x00 (2바이트) : 핸들러 번호 (0x0~0x161, NEXT 핸들러의 경우 0x0으로 정의되어 있음)
0x02 (1바이트) : 명령어.3 에 대한 암,복호화 타입
0x03 (1바이트) : 명령어.2 에 대한 암,복호화 타입
0x04 (1바이트) : 명령어.1 에 대한 암,복호화 타입
0x05 (1바이트) : 명령어.4 에 대한 암,복호화 타입
0x06 (4바이트) : 명령어.3 에 대한 랜덤 키
0x0A (4바이트) : 명령어.2 에 대한 랜덤 키
암/복호화 구조체의 명령어.1~4 타입 및 랜덤키는 키 체인(다음 키는 이전 키의 값에 영향을 받음)에 의해 생성됩니다. (0x606A40 함수에서 생성) 레지스터 변수의 인덱스 생성 시에도 동일한 키 체인이 적용됩니다.
최초 키 생성 시 SEED는 GetLocalTime() 으로 생성하는데(0x653134 함수), 암/복호화 구조체 생성 이후에 SEED를 생성하므로 최초 실행 시의 암/복호화 구조체엔 항상 동일한 SEED(0x3039)가 적용되어 아래 예제의 값은 최초 실행 시에는 고정이 됩니다.
example
0000 01 01 00 01 32A55896 10F3261D // NEXT 핸들러 및 핸들러 번호 전용
0000 00 00 00 00 62D34738 64B7CA2B // 이하 각 핸들러 및 데이터 암/복호화에 사용
0001 00 00 00 01 7D1840D9 04F96246
0002 02 02 01 00 2B2AFD01 3A7C55C9
0003 01 01 02 02 7021F1B5 1BC3CFF7
0004 01 01 02 01 78076014 0D986F9A
0006 01 02 01 02 20E473F0 6F4C262F
0007 02 02 01 00 44AD5FB7 60BFF50B
0009 02 01 02 01 7C0DFEFA 6C75F611
이하 생략
명령어 1~4의 타입에 따라 명령어가 다르게 추가됩니다. ebx는 키입니다.
타입
명령어1
명령어2
명령어3
명령어4
0
add eax, ebx(Key)
add eax, RandomKey
add eax, RandomKey
add ebx(Key), eax
1
sub eax, ebx(Key)
sub eax, RandomKey
sub eax, RandomKey
sub ebx(Key), eax
2
xor eax, ebx(Key)
xor eax, RandomKey
xor eax, RandomKey
xor ebx(Key), eax
(암호화 로직에 추가되는 명령어. LODS로 읽은 바이트 수에 따라 al/ax/eax, bl/bx/ebx)
타입
명령어1
명령어2
명령어3
명령어4
0
sub eax, ebx(Key)
sub eax, RandomKey
sub eax, RandomKey
add ebx(Key), eax
1
add eax, ebx(Key)
add eax, RandomKey
add eax, RandomKey
sub ebx(Key), eax
2
xor eax, ebx(Key)
xor eax, RandomKey
xor eax, RandomKey
xor ebx(Key), eax
(복호화 로직에 추가되는 명령어. LODS로 읽은 바이트 수에 따라 al/ax/eax, bl/bx/ebx)
구조체를 생성후 각 핸들러에 복호화 로직을 추가합니다.
0x3번 핸들러를 예로 들어봅니다.
LODS WORD PTR DS:[ESI]
MOVZX EAX,AX
PUSH AX
구조체는 0003 01 01 02 02 7021F1B5 1BC3CFF7가 사용되었다고 가정하면 생성되는 복호화 핸들러는 아래와 같습니다.