- Author:ZERO-A-ONE
- Date:2021-01-20
From the video "Vulnerability Bank | A Brief Talk on Malicious Code Countermeasures", it mainly starts from two aspects:
- Counter disassembly
- Anti-debug
1. Counter disassembly (static)
1.1 Basic concepts
- Assembly language: The CPU executes machine code. In order to facilitate memory, mnemonics are used to replace the opcodes of machine instructions in assembly language.
- Disassembly: the process of converting object code into assembly code
- Anti-disassembly technology: is to use some specially structured code or data in the program to make the disassembly tool produce incorrect assembly code
1.2 Disassembly algorithm
1.2.1 Linear disassembly algorithm
- Linear scan is a basic disassembly algorithm that traverses the binary data of a file from the beginning to the end and translates it into assembly language. The algorithm is simple and easiest to implement, and it is also easy to cause analysis errors
- Use libdisasm disassembly library to quickly realize disassembly
- http://bastard.sourceforge.net/libdisasm.html
Sample code:
char buffer[BUF_SIZE];
int position = 0;
while(position < BUF_SIZE){
x86_insn_t insn; //初始化一个结构体
int size=x86_sisasm(buffer,BUF_SIZE,0,position,&insn); //填充insn结构体
if(size != 0){
char disassembly_line[1024];
x86_format_insn(&insn,disassembly_line,1024,intel_syntax); //接收反汇编的结果
printf("%s\n",disassembly_line);
position += size;
}else{
position++;
}
}
x86_cleanup();
1.2.2 Frustrating the linear disassembly algorithm
In the executable file, in addition to the instruction code, there is also the data required for the program to run. If you insert useless data bytes in the code, it may cause the disassembler to parse the error
As shown on the left side of the figure above, it is easy to find in this code
call near ptr 15FF2A71h
There is a big problem, because this is a very large address in the memory, it is impossible to jump to this location, it can be considered that there is a disassembly error. As shown on the right is the correct disassembly result
1.2.3 Disassembly algorithm for code flow
- Code flow-oriented disassembly is a more advanced disassembly algorithm. The disassembler will not blindly disassemble the entire file. It will check every instruction and build an address list that needs to be disassembled. This algorithm is widely used in commercial disassemblers, such as IDA
1.2.4 Comparison of two disassembly algorithms
When the linear disassembler encounters the jmp instruction, regardless of the code logic, it will disassemble it according to the byte sequence. As shown in the figure, the malicious code author can use this feature to hide some sensitive strings
When the code flow-oriented disassembler encounters the jmp instruction, it will calculate the position to jump to, thereby treating the code that will not be executed as data
1.3 Anti-disassembly algorithm
1.3.1 Defects of code flow-oriented algorithms
The main method of disassembly technology against malicious code implementation is the use of a disassembler selection algorithm and algorithm loopholes assumptions:
- Conditional branch makes the code flow-oriented disassembler choose one of the two branches from true or false to disassemble first, and most code flow-oriented disassemblers will give priority to the false branch.
- Most disassemblers will disassemble the byte immediately following the call call first, followed by the position byte of the call call
1.3.2 Jump instructions to the same target
There are the following disassembly code instructions:
74 03 jz short near ptr loc_4011C4+1
75 01 jnz short near ptr loc_4011C4+1
loc_4011C4: ;CODE XREF:sub_4011C0
; sub_4011C0+2j
E8 58 C3 90 90 call near ptr 90D0D521h
Both the jz and jnz jumps point to the same address, which is equivalent to the unconditional jump jmp instruction, but after seeing the jnz, the disassembler will still give priority to decompiling the false branch by default, despite the fact that this branch will never be executed
Starting from E8 is loc_4011C4, which means that jz and jnz should skip E8 and start execution from 58. However, because of the priority of the decompiler to decompile the false branch, E8 is mistakenly regarded as part of the instruction code and decompiled Become a call instruction, the correct disassembly code should be:
74 03 jz short near ptr loc_4011C5
75 01 jnz short near ptr loc_4011C5
;------------------------------------------------
E8 db 0E8h
;------------------------------------------------
loc_4011C5: ;CODE XREF:sub_4011C0
; sub_4011C0+2j
58 pop eax
C3 retn
1.3.3 Jump instructions with fixed conditions
Jump instructions are always fixed, resulting in jump instructions always jump to the true branch, because the disassembler will prioritize the false branch, but the false branch code will conflict with the true branch code
33 C0 xor eax,eax
74 01 jz short near ptr loc_4011C4+1
loc_4011C4: ;CODE XREF: 004011C2j
;DATA XREF: .rdata:004020AC0
E9 58 C3 68 94 jmp near ptr 94A8D521h
The correct disassembly code should be:
33 C0 xor eax,eax
74 01 jz short near ptr loc_4011C4+1
;------------------------------------------------
E9 db 0E9h
;------------------------------------------------
loc_4011C5: ;CODE XREF: 004011C2j
;DATA XREF: .rdata:004020AC0
58 pop eax
C3 retn
1.3.4 Anti-anti-disassembly
The two methods introduced before are because the existence of rogue bytes causes the disassembler to generate incorrect disassembly code, thereby translating data bytes into instruction bytes, so you can manually perform data and instruction conversions. In IDA In, button C can convert data into instructions, and button D can convert instructions into data
The red mark in IDA indicates that there is a disassembly error
There is a way to make use of rogue bytes that cannot be ignored. All bytes are part of the instruction, and some bytes belong to two or more instructions at the same time during execution. At present, there is no disassembler in the industry that can A single byte is represented as part of two instructions
1.3.5 Invalid disassembly-entry case
- In the 4-byte sequence, the first instruction is a 2-byte jmp instruction, and the jump target of the jmp instruction is his second byte, so that the second byte FF belongs to the jmp-1 instruction. It belongs to the inc eax instruction again
- If the disassembler uses FF as part of the jmp instruction, it cannot be displayed as the beginning byte of the inc eax instruction
- This 4-byte sequence is essentially a complex NOP sequence, which can be inserted almost anywhere in the program, thereby breaking the disassembly chain. When encountering such a byte sequence, the above 4 bytes can be converted into data and ignored
is equivalent to:
JMP -1
INC EAX
DEX EAX
1.3.6 Invalid disassembly-advanced case
- This sequence is equivalent to xor eax eax, but the obfuscated disassembly code will not recognize the retn return instruction, which will cause the function to fail to end normally, resulting in a large number of code errors.
- The solution is to carefully analyze the machine code and correctly analyze the actual data modification operations
is equivalent to:
MOV ax,05EBh
XOR eax,eax
JZ -6
JMP 5
Real Code
This code in IDA will be disassembled into:
66 B8 EB 05 mov ax,5EBh
31 C0 xor eax,eax
74 FA jz short near ptr sub_4011C0+2
loc_4011C8:
E8 58 C3 90 90 call near ptr 98A80525h
After correct modification, it should read:
66 byte_4011C0 db 66h
88 db 0B8h
EB db 0E8h
05 db 5
;------------------------------------------------
31 C0 xor eax,eax
;------------------------------------------------
74 db 74h
FA db 0FAh
E8 db 0E8h
;------------------------------------------------
58 pop eax
C3 retn
2. Anti-debugging (dynamic)
2.1 Anti-debugging technology
- Malicious code uses anti-debugging technology to identify whether it is currently being debugged. After identification, it usually changes the execution path or modifies itself to crash the program, thereby increasing the debugging time and complexity
2.2 Using Windows API
-
IsDebuggerPresent: Query the IsDebugger flag in the process environment block (PEB). It returns 0 if it is not debugged, and returns non-zero if it is debugged (the details of PEB will be introduced later)
-
CheckRemoteDebuggerPresent: Similar to IsDebuggerPresent, the difference is that it can also detect whether other processes are being debugged
-
NtQueryInformationProcess: used to extract the information of a given process, the first parameter is the process handle, the second parameter is the information type, if the second parameter is set to ProcessDebugPort (value 0x7), return whether the process is debugged
-
FindWindowA: detect window name
-
OutputDebugString: Display a string in the debugger
-
Use the SetLastError function to set any error code. If the process is not debugged, calling OutputDebugString will fail, and the system will reset the error code
-
DWORD errorValue = 12345; SetLastError(errorValue); OutputDebugString("Test for Debugger"); if(GetLastError() == errorValue){ ExitProcess(); } else{ ... }
-
2.3 Registry check
- The following is the commonly used location of the debugger in the registry:,
HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\AeDebug
by default, it is set toDr.Wastson
, if it is modified to OllyDbg, the malicious code may determine that it is being debugged
2.4 Process Environment Block (PEB)
- Windows maintains the PEB structure for each running process. The structure contains all user mode parameters related to the process, including the debugging status of the process.
- Specific reference: https://bbs.pediy.com/thread-52398.html
The PEB structure is as follows:
typedef struct _PEB {
// Size: 0x1D8
000h UCHAR InheritedAddressSpace;
001h UCHAR ReadImageFileExecOptions;
002h UCHAR BeingDebugged; //Debug运行标志
003h UCHAR SpareBool;
004h HANDLE Mutant;
008h HINSTANCE ImageBaseAddress; //程序加载的基地址
00Ch struct _PEB_LDR_DATA *Ldr //Ptr32 _PEB_LDR_DATA
010h struct _RTL_USER_PROCESS_PARAMETERS *ProcessParameters;
014h ULONG SubSystemData;
018h HANDLE ProcessHeap;
01Ch KSPIN_LOCK FastPebLock;
020h ULONG FastPebLockRoutine;
024h ULONG FastPebUnlockRoutine;
028h ULONG EnvironmentUpdateCount;
02Ch ULONG KernelCallbackTable;
030h LARGE_INTEGER SystemReserved;
038h struct _PEB_FREE_BLOCK *FreeList
03Ch ULONG TlsExpansionCounter;
040h ULONG TlsBitmap;
044h LARGE_INTEGER TlsBitmapBits;
04Ch ULONG ReadOnlySharedMemoryBase;
050h ULONG ReadOnlySharedMemoryHeap;
054h ULONG ReadOnlyStaticServerData;
058h ULONG AnsiCodePageData;
05Ch ULONG OemCodePageData;
060h ULONG UnicodeCaseTableData;
064h ULONG NumberOfProcessors;
068h LARGE_INTEGER NtGlobalFlag; // Address of a local copy
070h LARGE_INTEGER CriticalSectionTimeout;
078h ULONG HeapSegmentReserve;
07Ch ULONG HeapSegmentCommit;
080h ULONG HeapDeCommitTotalFreeThreshold;
084h ULONG HeapDeCommitFreeBlockThreshold;
088h ULONG NumberOfHeaps;
08Ch ULONG MaximumNumberOfHeaps;
090h ULONG ProcessHeaps;
094h ULONG GdiSharedHandleTable;
098h ULONG ProcessStarterHelper;
09Ch ULONG GdiDCAttributeList;
0A0h KSPIN_LOCK LoaderLock;
0A4h ULONG OSMajorVersion;
0A8h ULONG OSMinorVersion;
0ACh USHORT OSBuildNumber;
0AEh USHORT OSCSDVersion;
0B0h ULONG OSPlatformId;
0B4h ULONG ImageSubsystem;
0B8h ULONG ImageSubsystemMajorVersion;
0BCh ULONG ImageSubsystemMinorVersion;
0C0h ULONG ImageProcessAffinityMask;
0C4h ULONG GdiHandleBuffer[0x22];
14Ch ULONG PostProcessInitRoutine;
150h ULONG TlsExpansionBitmap;
154h UCHAR TlsExpansionBitmapBits[0x80];
1D4h ULONG SessionId;
} PEB, *PPEB;
The members closely related to anti-debugging technology are as follows:
002h UCHAR BeingDebugged;
00Ch struct _PEB_LDR_DATA *Ldr;
018h HANDLE ProcessHeap;
068h LARGE_INTEGER NtGlobalFlag;
Next, explain the above 4 PEB members separately
2.4.1 Checking the BeingDebugged flag
- When the program is running, fs:[30h] points to the PEB base address, and the program can check the BeginDebugger flag to confirm whether it is being debugged
- When the process is in the debugging state, the value of BeingDebugged is set to 1, and when the process is running in the non-debugging state, its value is set to 0. So we can determine the running process of our program by judging the value of this member
The test code is as follows:
int main()
{
charresult=0;
__asm
{
moveax,fs:[0x30];//获取PEB的地址。
moval,BYTE PTR [eax+2];
movresult,al;//得到BeingDebugged成员的值。
}
if(result==1)
printf("isdebugging\n");
else
printf("notdebugging\n");
system("pause");//为了观察方便,添加的。
return 0;
}
For assembly code:
mov eax,dword ptr fs:[30h]
mov ebx,byte ptr [eax+2]
test ebx,ebx
jz NoDebuggerDetected
or:
push dword ptr fs:[30h]
pop edx
cmp byte ptr [edx+2],1
je DebuggerDetected
2.4.2 Check ProcessHeap logo
- ProcessHeap is an attribute of PEB that is not disclosed by Microsoft. It is located at 0x18 of PEB structure. ProcessHeap contains ForceFlags. This attribute value can determine whether the current process is debugged. The offset is 0x10 in XP and 0x44 in Win7.
- ProcessHeap is a pointer to the HEAP structure. The two members Flags and Force Flags in the HEAP structure have offsets of 0xC and 0x10, respectively. When the process is running normally, the value of Heap.Flags is 0x2, HEAP.ForceFlags The value of the member is 0x0. When the process is in the debug state, these values will change
The test code is as follows:
int main()
{
intresult=0;
__asm
{
moveax,fs:[0x30]; //PEB地址
moveax,[eax+0x18];//ProcessHeap成员
moveax,[eax+0x10];//ForceFlags成员
movresult,eax;
}
if(result!=0)
printf("isdebugging\n");
else
printf("notdebugging\n");
system("pause");
return 0;
}
The assembly code can also be:
mov eax,large fs:30h
mov eax,dword ptr [eax+18h]
cmp dword ptr ds:[eax+10h],0
jne DebuggerDetected
2.4.3 Checking the NtGlobalFlag flag
- Since the startup process in the debugger is different from the startup process in the normal mode, the way they create the memory heap is also different. NTGlobalFlag is also undisclosed, and the PEB offset is 0x68. When the value is 0x70, it means that the program is started from the debugger.
The test code is as follows:
int main()
{
intresult=0;
__asm
{
moveax,fs:[0x30]; //PEB地址
moveax,[eax+0x68];//NtGlobalFlag成员
movresult,eax;
}
if(result==0x70)
printf("isdebugging\n");
else
printf("notdebugging\n");
system("pause");
return 0;
}
The assembly code is as follows:
mov eax,large fs:30h
cmp dword ptr ds:[eax+68h],70h
jz DebuggerDetected
2.4.4 Checking the Ldr flag
When debugging a process, some special marks will appear in its heap memory, indicating that it is being debugged. The most eye-catching of these signs is that the unused heap memory area is filled with OxFEEEFEEE. We can use this feature to determine whether the process is being debugged. The PEB.Ldr member points to a _PEB_LDR_DATA structure, and this structure is created in the heap memory area, so we can scan this area to determine whether the process is in a debugging state
The detection code is as follows:
int main()
{
LPBYTEpLdr;
DWORDpLdrSig[4]={
0xEEFEEEFE,0xEEFEEEFE,0xEEFEEEFE,0xEEFEEEFE};
__asm
{
moveax,fs:[0x30]; //PEB地址
moveax,[eax+0xC];//Ldr
movpLdr,eax;
}
__try
{
while(1){
if(!memcmp(pLdr,pLdrSig,sizeof(pLdrSig)){
printf("is debuggig\n");
break;}
else{
pLdr++;}
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("notdebugging\n");
}
system("pause");
return 0;
}