When you execute some shellcode in a new thread, things are simple:
You can simply write your shellcode doing whatever you want it to do and finish it with a return.
On Windows, you execute it with CreateThread (or CreateRemoteThread from another program) then you can simply use WaitForSingleObject with the thread handle and voila.
Unfortunately with thread hijacking, this is impossible, because we interrupted a thread that was busy doing operations that we don’t know with a stack in a state that we don’t know either, which means that executing a return instruction would make the thread return to the last return address on the stack and mess up completely its operations.
We have to be clever.
I took a (brief) look around, and it seems that many people go for a solution with a byte set to a certain value to signal that the execution is complete before maintaining the thread in its current state with whatever solution.
I did not really like this solution, since it means that we have to read the target process memory, and therefore we need a process handle with enough rights to read the memory.
Instead I wanted a solution that would use only what we already have for the thread hijacking, especially the thread handle.
We can detect whether the hijacked thread has completed execution with the information that we already have:
- We can design our shellcode to enter a very small infinite loop at the end of its operations (with for example JMP REL8 -2)
- We know the address at which the shellcode starts
- We know the size of the shellcode and therefore the address at which the shellcode ends
- We can get the address at which the instruction pointer is only with our thread handle that we already needed for the thread hijacking
So we simply finish our shellcode with our bytes infinite loop (JMP REL8 -2, opcodes: 0xEB 0xFE) and in our hijacking program we simply do an infinite loop that executes GetThreadContext while the instruction pointer has not reached our infinite loop.
When this is done, we could simply re-suspend the thread, reset its context to its initial state before we hijacked it, and resume it.
I have done a proof of concept program for x64 shellcode that I pushed on GitHub if you are interested.