📜 ⬆️ ⬇️

Disabling the main application thread from the debugger and avoiding interception CreateFile ()

One of the standard tricks that make learning your application difficult is emulating the execution of API functions.

Consider a particular case.
For example, when you need to determine a critical place in an application, say, opening yourself to check the application checksum, BP is set on the API function CreateFile (), where we will wait for the input parameter with the path to our executable file, then in the debugger we will go to code that caused this function and proceed directly to the analysis.

You can do even more simply by using the Process Monitor utility for the authorship of the notorious Mark Russinovich and Bruce Cogswell.

This utility, absolutely calmly shows the full stack of calls, including return addresses of interest to us.
')
image

We can only run the debugger and install BP to the desired address.

image

True, this utility already shows the return address, and not the address of the call to the immediate function. But more about that later.

Imagine that the body of our application is already changed. The easiest way to bypass the application checksum check is to replace the lpFileName parameter in the captured CreateFile () function with the path to the unchanged application body. After this operation, you do not even need to study the mechanism for calculating the checksum, no matter what is used there, digital signature verification, MD5 hash or the banal CRC32. Since This algorithm will work with the body of the original application - all checks will be successfully passed.

Therefore, the task looks like this in the first approximation: to make it as difficult as possible to change the parameters of the called function. The use of mounted protections in this case will not save, because as a result, there will always be a call to the CreateFile () function, the body of which is difficult to put it under the protection of the virtual machine (to put it mildly, some external protections can self-emulate this call, but now it’s not about them).

One of the ways to bypass CreateFile () is to directly call the corresponding kernel function, bypassing kernel32-> kernelbase-> ntdll. The result of such a call can be seen in the picture:

image

A direct call is no longer about installing BP on any functions, since such calls are simply not available. Yes, we have on our hands the return address after the call, but the nuance is that the overwhelming majority of modern mounted protectors spread the code quite intelligently, and having a return code on the call does not mean that we can determine the place of the call itself to change this or that another parameter.

To implement this algorithm, let's see what happens when you call such a code (delphi7 + Windows 7 32bit):

hFile := CreateFile(PChar(ParamStr(0)), GENERIC_READ, FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); 


1. There is a transition to the import table
2. Transferring control to the function CreateFileA () of the library kernel32.dll
3. Transfer of control of the function CreateFileW () of the library kernel32.dll
4. Transferring control to the CreateFileW () function of the KernelBase.dll library
5. Transfer control function zwCreateFile () library ntdll.dll
6. Transferring control to the kernel

We want to get away from the first five points and execute item 6 directly.
To do this, we will be helped by the implementation of the ZwCreateFile function in the ntdll.dll library, the description of the parameters of the function itself and a small educational program :)

Consider the implementation of ZwCreateFile of the third ring (UserMode) under different systems.
Pay attention to the machine code instructions MOV.

Windows Vista - 32 bits (6.0.6002.18005)
 .text:77F343D4 public ZwCreateFile .text:77F343D4 ZwCreateFile proc near .text:77F343D4 B8 3C 00 00 00 mov eax, 3Ch .text:77F343D9 BA 00 03 FE 7F mov edx, 7FFE0300h .text:77F343DE FF 12 call dword ptr [edx] .text:77F343E0 C2 2C 00 retn 2Ch .text:77F343E0 ZwCreateFile endp 


Windows 7 - 32 bits (6.1.7601.17725)
 .text:77F055C8 public ZwCreateFile .text:77F055C8 ZwCreateFile proc near .text:77F055C8 B8 42 00 00 00 mov eax, 42h .text:77F055CD BA 00 03 FE 7F mov edx, 7FFE0300h .text:77F055D2 FF 12 call dword ptr [edx] .text:77F055D4 C2 2C 00 retn 2Ch .text:77F055D4 ZwCreateFile endp 


Windows 8 - 32 bits (6.2.8400.0)
 .text:6A21629C public ZwCreateFile .text:6A21629C ZwCreateFile proc near .text:6A21629C B8 64 01 00 00 mov eax, 164h .text:6A2162A1 E8 03 00 00 00 call sub_6A2162A9 .text:6A2162A6 C2 2C 00 retn 2Ch .text:6A2162A6 ZwCreateFile endp 


As you can see, the differences are quite minimal. What you should pay attention to is that a certain number is entered into the EAX register, and a function is called. This function is called KiFastSystemCall and looks something like this (depending on the OS):

 mov edx, esp sysenter ret 


Instead of SYSENTER there may be an INT 0x2E call, but this is no longer essential.
The implementation of this function under 64-bit systems is slightly different:

Windows 8 - 64 bits, 32-bit ntdll (6.2.8400.0)
 .text:6B2BF470 public ZwCreateFile .text:6B2BF470 ZwCreateFile proc near .text:6B2BF470 B8 53 00 00 00 mov eax, 53h .text:6B2BF475 64 FF 15 C0 00 00 00 call large dword ptr fs:0C0h .text:6B2BF47C C2 2C 00 retn 2Ch .text:6B2BF47C ZwCreateFile endp 


Since our code is 32-bit, and the system is 64-bit, here the FS: 0C0h gateway is already being called, which ultimately passes the execution of the native 64-bit function that looks like this:

Windows 8 - 64 bits, 64-bit ntdll (6.2.8400.0)
 .text:0000000180003110 public NtOpenFile .text:0000000180003110 NtOpenFile proc near .text:0000000180003110 4C 8B D1 mov r10, rcx .text:0000000180003113 B8 31 00 00 00 mov eax, 31h .text:0000000180003118 0F 05 syscall .text:000000018000311A C3 retn .text:000000018000311A NtOpenFile endp 


But, despite this nuance, the EAX register is initialized even in this case.

The number placed in EAX is an index from the KeServiceDescriptorTable table, through which the kernel determines which function to call at a given time. These indices are sewn directly into the NTDLL code, change from version to version (changing the table can occur even as a result of a minor patch), so we need to learn how to get them dynamically.

The following function will help us in this:
 type //  STD  TSDTIndex = ( sdtNtSetInformationThread, sdtZwOpenFile, sdtNtQueryObject, WOW64ReservedAddr); var FunctionSDTIndex: array [TSDTIndex] of DWORD = (0, 0, 0, 0); procedure InitSDTTable; const //  ,      ApiNames: array [TSDTIndex] of string = ( 'NtSetInformationThread', 'ZwOpenFile', 'NtQueryObject', '' ); const KSEG0_BASE = $80000000; MM_HIGHEST_USER_ADDRESS = $7FFEFFFF; MM_USER_PROBE_ADDRESS = $7FFF0000; MM_SYSTEM_RANGE_START = KSEG0_BASE; MustWrite = PAGE_READWRITE or PAGE_WRITECOPY or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY; OBJ_CASE_INSENSITIVE = $00000040; FILE_SYNCHRONOUS_IO_NONALERT = $00000020; FILE_READ_DATA = 1; var pSectionAddr, dwLength: DWORD; lpBuffer: TMemoryBasicInformation; pNtHeaders: PImageNtHeaders; ExportAddr: TImageDataDirectory; ProcessExport: Boolean; ImageBase: DWORD; IED: PImageExportDirectory; I: Integer; FuntionAddr: Pointer; NamesCursor: PDWORD; OrdinalCursor: PWORD; Ordinal: DWORD; CurrentFuncName: string; SDT: TSDTIndex; begin //    ,    NTDLL pSectionAddr := GetModuleHandle('ntdll.dll'); ImageBase := 0; ExportAddr.VirtualAddress := 0; ExportAddr.Size := 0; dwLength := SizeOf(TMemoryBasicInformation); //  WOW ,     , //      sysenter //  32-     asm push eax mov eax, fs:[$c0] mov I, eax pop eax end; FunctionSDTIndex[WOW64ReservedAddr] := I; _Write(Format('WOW64Reserved: %d', [FunctionSDTIndex[WOW64ReservedAddr]])); //      while pSectionAddr < MM_USER_PROBE_ADDRESS do begin //     if VirtualQuery(Pointer(pSectionAddr), lpBuffer, dwLength) <> dwLength then RaiseLastOSError; try //     -   if (lpBuffer.State = MEM_FREE) or (lpBuffer.State = MEM_RESERVE) then Continue; //    -   if (lpBuffer.Protect and PAGE_GUARD) = PAGE_GUARD then Continue; if (lpBuffer.Protect and PAGE_NOACCESS) = PAGE_NOACCESS then Continue; _Write(Format(' : %x', [pSectionAddr])); //  -       ? if PWord(lpBuffer.BaseAddress)^ = IMAGE_DOS_SIGNATURE then begin //     pNtHeaders := Pointer(Integer(lpBuffer.BaseAddress) + PImageDosHeader(lpBuffer.BaseAddress)^._lfanew); ExportAddr.VirtualAddress := 0; ExportAddr.Size := 0; ImageBase := DWORD(lpBuffer.BaseAddress); if (pNtHeaders^.Signature = IMAGE_NT_SIGNATURE) and (pNtHeaders^.FileHeader.Machine = IMAGE_FILE_MACHINE_I386) then begin _Write(' PE .'); //   -      ExportAddr := pNtHeaders.OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT]; if ExportAddr.VirtualAddress <> 0 then Inc(ExportAddr.VirtualAddress, ImageBase) else ExportAddr.Size := 0; end; _Write(Format('  : %x', [ExportAddr.VirtualAddress])); _Write(Format('  : %x', [ExportAddr.Size])); end; // ,         ProcessExport := False; if ExportAddr.Size <> 0 then if ExportAddr.VirtualAddress >= DWORD(lpBuffer.BaseAddress) then ProcessExport := ExportAddr.VirtualAddress + ExportAddr.Size < DWORD(lpBuffer.BaseAddress) + lpBuffer.RegionSize; //    -   if ProcessExport then begin if (ImageBase = 0) or (ExportAddr.VirtualAddress = 0) then Exit; IED := PImageExportDirectory(ExportAddr.VirtualAddress); _Write(Format(' : %s', [string(PAnsiChar(ImageBase + IED^.Name))])); // ,     ? if LowerCase(string(PAnsiChar(ImageBase + IED^.Name))) = 'ntdll.dll' then begin _Write('  '); // ,   ,      I := 1; NamesCursor := Pointer(ImageBase + DWORD(IED^.AddressOfNames)); OrdinalCursor := Pointer(ImageBase + DWORD(IED^.AddressOfNameOrdinals)); while I < Integer(IED^.NumberOfNames) do begin //       CurrentFuncName := string(PAnsiChar(ImageBase + PDWORD(NamesCursor)^)); for SDT := sdtNtSetInformationThread to sdtNtQueryObject do if ApiNames[SDT] = CurrentFuncName then begin //       Ordinal := OrdinalCursor^ + IED^.Base; //       FuntionAddr := Pointer(ImageBase + DWORD(IED^.AddressOfFunctions)); FuntionAddr := Pointer(ImageBase + PDWORD(DWORD(FuntionAddr) + (Ordinal - 1) * 4)^); //      MOV FuntionAddr := Pointer(DWORD(FuntionAddr) + 1); //  SDT   FunctionSDTIndex[SDT] := PDWORD(FuntionAddr)^; _Write(Format('  %s - SDT  %d', [CurrentFuncName, FunctionSDTIndex[SDT]])); end; Inc(I); Inc(NamesCursor); Inc(OrdinalCursor); end; end; ImageBase := 0; end; // ,     ? if FunctionSDTIndex[sdtNtSetInformationThread] <> 0 then if FunctionSDTIndex[sdtZwOpenFile] <> 0 then if FunctionSDTIndex[sdtNtQueryObject] <> 0 then Exit; finally //    ,    . Inc(pSectionAddr, lpBuffer.RegionSize); end; end; end; 


This function determines the load address of the NTDLL.DLL library, goes to the export table of this library, searches for records about the functions we need (in this example, NtSetInformationThread, ZwOpenFile and NtQueryObject are considered), determines their actual address in memory and reads the SDT index of the required function, based on machine code functions above. The results are placed in the FunctionSDTIndex array.

Now, having on hand the valid SDT indexes of the functions required for the example, let's consider directly the declaration of ZwOpenFile itself, which we are going to call.

 NTSTATUS ZwOpenFile( _Out_ PHANDLE FileHandle, _In_ ACCESS_MASK DesiredAccess, _In_ POBJECT_ATTRIBUTES ObjectAttributes, _Out_ PIO_STATUS_BLOCK IoStatusBlock, _In_ ULONG ShareAccess, _In_ ULONG OpenOptions ); 


Six parameters placed on the stack. The first and fourth follow the link, the third pointer. Well, well, we do a call:
  //      // ZwOpenFile // =========================================================================== _Write('  ZwOpenFile'); _Write('    '); SysCallArgument := FunctionSDTIndex[sdtZwOpenFile]; oa.Length := SizeOf(TObjectAttributes); oa.RootDirectory := 0; oa.ObjectName := @UnicodeStr; oa.Attributes := OBJ_CASE_INSENSITIVE; oa.SecurityDescriptor := nil; oa.SecurityQualityOfService := nil; UnicodeStr.Buffer := StringToOleStr('??' + ParamStr(0)); UnicodeStr.Length := Length(UnicodeStr.Buffer) * SizeOf(WideChar); UnicodeStr.MaximumLength := UnicodeStr.Length + SizeOf(WideChar); asm //    mov SAVED_EBP, ebp mov SAVED_ESP, esp //   //         push FILE_SYNCHRONOUS_IO_NONALERT // OpenOptions push FILE_SHARE_READ + FILE_SHARE_WRITE + FILE_SHARE_DELETE // ShareAccess lea eax, iosb //   OUT  IoStatusBlock push eax //    lea eax, oa // ObjectAttributes  ,   push eax //  -    push FILE_READ_DATA + SYNCHRONIZE // DesiredAccess lea eax, hFile //   OUT  FileHandle push eax //     //       , //     , //          movzx eax, IsWOW64 or eax, eax jz @32Bit //   64-  lea eax, @64bit push eax push eax mov eax, WOW64Addr push eax mov eax, SysCallArgument xor ecx, ecx lea edx, dword ptr ss:[esp+4*3] ret @64bit: add esp, 4 jmp @FINALIZE @32Bit: //   32-  (XP  ) lea eax, @FINALIZE push eax push eax movzx eax, NeedInt2E or eax, eax jnz @NT_CODE mov edx, esp mov eax, SysCallArgument sysenter @NT_CODE: //   W2K   pop eax lea edx, esp + 4 mov eax, SysCallArgument int $2E nop @FINALIZE: //   mov Status, eax //    mov ebp, SAVED_EBP mov esp, SAVED_ESP end; if Status <> 0 then hFile := 0; _Write(Format('  %x', [Status])); _Write(Format(' %d', [hFile])) 
;

Actually this task is completed.

Now the nuances: as you can see, the code can be called in three different ways.

SYSENTER, INT2E and WOW64 register. This is due to the features of the implementation of various operating systems and their bit depth. The second nuance is the preservation of registers EBP / ESP. This is due to the fact that after calling a function under 64-bit systems, the alignment on the stack is slightly different, so we restore it forcibly, so as not to destroy the application.

Calls to the other two functions will not be considered here, but in short - NtSetInformationThread disconnects the main application thread from the debugger. After calling it, trying to install BP will result in a different plan for errors. For example, Delphi 7 responds with such errors:

image

After that, it remains only to disrupt the process and start the environment again.

In the source code of the example, this call is disabled by the DISABLE_HIDEFROMDEBUGGER directive. To avoid, if you want to check its operation, comment out the declaration of this directive and recast the example.

The second function, NtQueryObject, shows how to work with the resulting file handle (just as an example).

The result of the demo application will be something like this:

image

Take an example here .

Well, as a postscript. This approach is not a panacea, but simply an attempt to show one of the approaches to building application security. Naturally, this code will not save you from a competent researcher. It is possible to bypass even this implementation variant at least in three ways on a vskidka:

1. driver parameter substitution
2. substitution of the path to the application in the process environment block
3. Replacing the result of the function on your own by embedding the adapter into your code at the return address of the function, which will be shown to us by the same Process Monitor.

But scaring off a novice software researcher will help, maybe even puzzling a more advanced specialist, and only distributing the program as source codes will save the professional;)

Source: https://habr.com/ru/post/177625/


All Articles