Finishing the SLAE exam gave me quite come confidence with the Assembly language. Initially I though to myself that I've learned most of what I needed to write programs from scratch on 32-bit and 64-bit x86 systems.
Unfortunately, that wasn't the case. Although on Linux systems it's quite easy to turn assembly code into a fully-functional ELF executable, on Windows the story goes a little bit different.
Since I prefer a pratical approach when learning, I chose to start with something less boring than a Hello World program: an operative system! Yeah...
Jokes aside, I decided to start with something like something that would be useful for real-life exploits: a system reboot. Oftentimes, when you perform changes to vulnerable services (e.g., Unquoted Path) you need to restart them. If you don't have the required privileges you can't restart the service, but usually you can restart the whole system.
To make things harder more interesting, I decided to write the shellcode for 64-bit systems, since that's what I'm using most of the time.
We need to know a few things:
global _start
section .text
_start:
xor rax, rax
inc rax
The first step to assemble this code is to have an assembler. Since I'm already used to NASM, we can it on Windows too; you can download it from this link.
To assemble the program:
nasm -f win64 -o test.o test.nasm
Finally, we need to link it. On Windows systems with Visual Studio installed there's a useful tool named link.exe
which can help us.
However, before doing that you need to set some environment variables. Luckily, Visual Studio already prepared some scripts to simplify the process. In this case, the script is named "x64 Native Tools Command Prompt for VS 2022", you can find it through Windows Search.
After that, you should be able to link the object file and obtain a Portable Executable:
link /entry:_start /subsystem:console test.o
Next, we need to clarify some point before actually writing the shellcode, specifically the call convention to use.
In the page below, it's specified that "[...] the first four arguments are placed onto the registers. That means RCX
, RDX
, R8
, R9
for integer, struct or pointer arguments (in that order), and XMM0
, XMM1
, XMM2
, XMM3
for floating point arguments. Additional arguments are pushed onto the stack (right to left)".
To call a specific function from a DLL you have to:
Follows an example:
extern _MessageBoxA@16
global _main
section .text
; ...
call _MessageBoxA@16
When you import external references, you need to pass the correct library (usually .lib
) to link.exe
.
You should also pay attention to the so-called shadow-space:
In the Microsoft x64 calling convention, it is the caller's responsibility to allocate 32 bytes of "shadow space" on the stack right before calling the function (regardless of the actual number of parameters used), and to pop the stack after the call.
The shadow space is used to spill
RCX
,RDX
,R8
, andR9
, but must be made available to all functions, even those with fewer than four parameters.
If you add 32 bytes of space before a function call, it may not work properly. As an example, the function LookupPrivilegeValueA
won't work if you don't allocate the required space on the stack.
Another thing you should bear in mind is stack alignment. According to the documentation (and some people arguing on stackoverflow), the Windows x64 ABI requires you to have aligned RSP by 16 bytes before a function call:
The quote is taken from here.
To align the stack, I placed the following instruction at the very beginning of my code:
_start:
; stack alignment
and rsp, 0xfffffffffffffff0
First things first, we need to find the function for restarting the Windows system. According to the official documentation, its name should be InitiateSystemShutdownA and it accepts 5 parameters:
BOOL InitiateSystemShutdownA(
[in, optional] LPSTR lpMachineName,
[in, optional] LPSTR lpMessage,
[in] DWORD dwTimeout,
[in] BOOL bForceAppsClosed,
[in] BOOL bRebootAfterShutdown
);
Based on the previous notes about the call convention, we should use the following registers:
RCX
for the parameter lpMachineNameRDX
for the parameter lpMessageR8
for the parameter dwTimeoutR9
for the parameter bForceAppsClosedThe final code should be similar to this:
extern InitiateSystemShutdownA
global _start
section .text
_start:
; lpMachineName -> NULL
xor rcx, rcx
; lpMessage -> NULL
mov rdx, rcx
; dwTimeout -> 0
mov r8, rdx
; bForceAppsClosed -> true (force all apps to close)
mov r9, r8
dec r9
; bRebootAfterShutdown -> true
; (reboot instead of shutdown only)
push r9
sub rsp, 0x20
call InitiateSystemShutdownA
add rsp, 0x20
To compile the program I added the library advapi32.lib
to the link
command, otherwise it won't know where the function InitiateSystemShutdownA
resides.
nasm -f win64 -o test.o test.nasm
link /entry:_start /subsystem:console test.o advapi32.lib
If you attempt to run the program, you'll quickly notice that it doesn't work very well, or better said, it doesn't at all. Monitoring the API calls with API Monitor, I found out that the call to the previous function throws the error Access is denied, which means the process doesn't have the required privileges.
Moreover, if you were to run the program as Administrator, it still wouldn't work. This is caused by the token SeShutdownPrivilege
(for the SE_SHUTDOWN_NAME privilege), which is disabled by default.
Monitoring the API calls
As a matter of fact, this apprach is already described in the official documentation provided by Microsoft.
Based on the previous article, we need to perform the following operations to enabled the shutdown privilege:
OpenProcessToken
LookupPrivilegeValue
AdjustTokenPrivileges
To retrieve the handle to the current process' token, you can use the function OpenProcessToken, which accepts three parameters:
BOOL OpenProcessToken(
[in] HANDLE ProcessHandle,
[in] DWORD DesiredAccess,
[out] PHANDLE TokenHandle
);
To invoke it, I wrote the following assembly procedure:
_start:
; stack alignment
and rsp, 0xfffffffffffffff0
_getProcessToken:
xor rcx, rcx
mov rdx, rcx
mov r8, rcx
; ProcessHandle -> -1 (current process)
dec rcx
; DesiredAccess -> TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY
add dl, 0x28
; TokenHandle -> pointer to output handle
mov r8, process_token_handle
sub rsp, 0x20
call OpenProcessToken
add rsp, 0x20
Instead of calling GetCurrentProcess
to retrieve the process handle of the current process, I used the pseudo handle -1. You can find more information about this at the following link.
In the previous snippet, the symbol process_token_handle
is a variabled defined in the .bss
section:
section .data
shutdown_privilege_name: db "SeShutdownPrivilege", 0x0
section .bss
token_privileges_struct: resb 0x10,
process_token_handle: resb 0x8
As mentioned before, a LUID is a locally-unique identifier. In this case, we need the local identifier of the shutdown privilege, so we can reference it later.
Luckily, there's a convenient function named LookupPrivilegeValue
that can help us.
There are two versions of it: LookupPrivilegeValueA
and LookupPrivilegeValueW
. The former is the ANSI version (think of z), while the latter uses unicode strings (utf16-le). Since I prefer working with utf-8, the choice is pretty obvious.
Based on the documentation provided by Microsoft, the function accepts three parameters:
BOOL LookupPrivilegeValueA(
[in, optional] LPCSTR lpSystemName,
[in] LPCSTR lpName,
[out] PLUID lpLuid
);
The assembly code looks like this:
_getPrivilegeLuid:
; lpSystemName -> NULL
xor rcx, rcx
; lpName -> pointer to string "SeShutdownPrivilege"
lea rdx, [shutdown_privilege_name]
; lpLuid -> pointer to output LUID struct
mov r8, token_privileges_struct + 0x4
sub rsp, 0x20
call LookupPrivilegeValueA
add rsp, 0x20
As you can see, I allocated 32 bytes of space on the stack before calling the function.
Once we have the LUID of the privilege we want to enable, we can call the function AdjustTokenPrivileges to set it. The latter accepts six parameters:
BOOL AdjustTokenPrivileges(
[in] HANDLE TokenHandle,
[in] BOOL DisableAllPrivileges,
[in, optional] PTOKEN_PRIVILEGES NewState,
[in] DWORD BufferLength,
[out, optional] PTOKEN_PRIVILEGES PreviousState,
[out, optional] PDWORD ReturnLength
);
You can refer to the this link if you're curious about the purpose of each parameter.
Combining this information, I came up with the assembly code below:
_enableShutdownPrivilege:
; set TOKEN_PRIVILEGES.PrivilegeCount
mov DWORD [token_privileges_struct], 0x1
; set TOKEN_PRIVILEGES.LUID_AND_ATTRIBUTES.Attributes
mov DWORD [token_privileges_struct + 0xc], 0x2
; TokenHandle
mov rcx, QWORD [process_token_handle]
; DisableAllPrivileges -> false
xor rdx, rdx
; PTOKEN_PRIVILEGES
mov r8, token_privileges_struct
; BufferLength -> 0
mov r9, rdx
; PreviousState and ReturnLength -> 0
push rdx
push rdx
sub rsp, 0x20
call AdjustTokenPrivileges
add rsp, 0x20
The first mov
operation sets the value of the PrivilegeCount
field of the TOKEN_PRIVILEGES
structure. In this case, we're only enabling one privilege, hence the counter should be set to 1.
The second mov
operation sets the field Attributes of the LUID_AND_ATTRIBUTES
structure to the value 0x2 i.e., SE_PRIVILEGE_ENABLED
.
Putting the pieces together, you should have a fully-functional reboot shellcode. Follows the commands for the compilation:
nasm -f win64 -o test.o test.nasm
link /LARGEADDRESSAWARE:NO /entry:_start /subsystem:console test.o advapi32.lib kernel32.lib
I think the Hello World example would have been a better choice for an introduction to shellcoding on Windows... but I learned a few things, so it's good.
Anyways, I hope you find it useful.