hasherezade / pe_to_shellcode

Converts PE into a shellcode
https://www.youtube.com/watch?v=WQCiM0X11TA
BSD 2-Clause "Simplified" License
2.27k stars 423 forks source link

Generated shellcode is executable but not runnable with runshc64 #38

Closed Path-17 closed 12 months ago

Path-17 commented 1 year ago

Hi, I am running into some issues generating shellcode from my rust binary.

It works when run in a copy of the same process using a built-in shellcode runner command (VirtualAlloc, CreateThread), but when executed in the context of another process, either injected or using runshc64, it does not work.

I am thinking that it could be some kind of pre-main entrypoint that is relying on the current process' environment but I cannot figure out how to skip over it the past few days.

Have you run into this before?

For context the raw code can be found here https://github.com/Path-17/diet-c2/tree/main/implants/implant-v2/src

Thank you! I attached a copy of the executable (not the output of pe2shc.exe) as well

implant-v2.zip

Path-17 commented 1 year ago

To follow up, I found a solution that I think confirms my suspicion.

The default rust toolchain uses Visual Studio build tools (MSVC), switching to the gnu build tools fixes the issues I was coming across.

Just run "rustup toolchain install stable-x86_64-pc-windows-gnu" then set it to your default with "rustup default stable-gnu".

Testing further on the MSVC toolchain, even the simplest println!("Hello world") doesn't work with pe2shc.exe / runshc64.exe.

I will test further with side by side with a debugger injecting into notepad.exe to see what the cause of the crash is!

hasherezade commented 1 year ago

hi @Path-17 ! Thank you for reporting. It is true that sometimes the compiler choice affects whether or not the executable is convertible without any issues. For example: https://github.com/hasherezade/pe_to_shellcode/issues/29

I will look into your particular case soon, and see what was the immediate cause.

hasherezade commented 1 year ago

@Path-17 : I did some quick tests today, and it seems to me that the problem lies in the TLS. First I tried to load the runshc64.exe <shellcode_path> under the debugger, and run it. There is an access violation:

tls_err

This is at RVA = 8E80F in the implant. Seeing it in IDA:

fetch_from_tls

So it seems that the value couldn't be fetched from the TLS.

Another check I did, by tracing the original application vs the shellcodified version vs the original one with TLS removed. Those are the results (fragments of the tracelogs).

Shellcodified:

> 13377339000+d82;kernel32.GetCurrentThread
> 13377339000+d70;kernel32.GetModuleHandleA
> 13377310000+cb7;kernel32.GetProcAddress
GetProcAddress:
    Arg[0] = ptr 0x00007ffc88c70000 -> {MZ\x90\x00\x03\x00\x00\x00}
    Arg[1] = ptr 0x00000133773951a9 -> "SetThreadDescription"

> 13377310000+cdc;kernelbase.SetThreadDescription
SetThreadDescription:
    Arg[0] = 0xfffffffffffffffe = 18446744073709551614
    Arg[1] = ptr 0x0000013362bdbaa0 -> L"main"

> 13377288000+c96;kernel32.HeapFree
> 13377288000+c1b;ntdll.RtlAllocateHeap
> 1337733a000+8c5;vcruntime140.memmove
> 13377288000+c1b;ntdll.RtlAllocateHeap
> 13377288000+c96;kernel32.HeapFree
> 13377288000+c96;kernel32.HeapFree
> 1337730f000+d6f;kernel32.GetStdHandle
> 1337730f000+dd1;kernel32.GetConsoleMode
> 13377310000+208;kernel32.MultiByteToWideChar
> 13377310000+245;kernel32.WriteConsoleW
WriteConsoleW:
    Arg[0] = 0x0000000000000058 = 88
    Arg[1] = ptr 0x0000000fe21fd0e0 -> L"thread panicked while processing panic. aborting.
"

Original:

b9d82;kernel32.GetCurrentThread
b9d70;kernel32.GetModuleHandleA
90cb7;kernel32.GetProcAddress
GetProcAddress:
    Arg[0] = ptr 0x00007ffc88c70000 -> {MZ\x90\x00\x03\x00\x00\x00}
    Arg[1] = ptr 0x00007ff6a3ef51a9 -> "SetThreadDescription"

90cdc;kernelbase.SetThreadDescription
8c96;kernel32.HeapFree
8c1b;ntdll.RtlAllocateHeap
ba8c5;vcruntime140.memmove
8c1b;ntdll.RtlAllocateHeap
8c1b;ntdll.RtlAllocateHeap
23f4;kernel32.GetComputerNameExW
8c1b;ntdll.RtlAllocateHeap
240b;kernel32.GetComputerNameExW
8c1b;ntdll.RtlAllocateHeap
ba8c5;vcruntime140.memmove

Original with TLS directory removed:

b9d82;kernel32.GetCurrentThread
b9d70;kernel32.GetModuleHandleA
90cb7;kernel32.GetProcAddress
GetProcAddress:
    Arg[0] = ptr 0x00007ffc88c70000 -> {MZ\x90\x00\x03\x00\x00\x00}
    Arg[1] = ptr 0x00007ff65d2651a9 -> "SetThreadDescription"

90cdc;kernelbase.SetThreadDescription
SetThreadDescription:
    Arg[0] = 0xfffffffffffffffe = 18446744073709551614
    Arg[1] = ptr 0x000002375de09170 -> L"main"

8c96;kernel32.HeapFree
8c1b;ntdll.RtlAllocateHeap
ba8c5;vcruntime140.memmove
8c1b;ntdll.RtlAllocateHeap
8c96;kernel32.HeapFree
8c96;kernel32.HeapFree
8fd6f;kernel32.GetStdHandle

We can see that the shellcodified version, and the version with TLS directory removed failed at the same point of execution. So there is a strong clue that inability to properly execute TLS caused the issue.

Although pe_to_shellcode supports TLS in a way, but it is a very simplified version. The TLS is called only once, before the Entry Point is executed. This may not be enough in some cases. But implementing a proper TLS support is beyond the scope of this small loader, because it would require hooking of functions.

If it is possible, I would recommend you to try compiling it with TLS disabled. Maybe the GNU compiler already did it, and that helped? Please let me know your thoughts.

Path-17 commented 12 months ago

As far as I can tell there is no way to fully disable TLS with rust's compiler, I haven't found any flags for it.

I guess it is just a quirk of the implementation between GNU and MSVC that made the difference!

Thank you for your detailed explanation and work on this, I learned a lot from your explanation above :)