Imagine that you have some executable memory, with for example an infinite loop using a short jump like that:
NOP
JMP REL8 0xFE (-0x2)
This generate the following shellcode:
0x90, 0xEB, 0xFE
Now, what would happen if another process or another thread was modifying the destination of the short jump?
Would it be possible that the instruction executes while the destination is BEING overwritten?
0xFE is 1111 1110 is binary.
Could it happen that this short jump sometimes jumps of 0x1E (0001 1110) because it is currently being overwritten to 0x00 and is not entirely overwritten?
Is that possible?
Note that I am interested only in knowing the possibility of an incomplete overwrite of a single byte, no more than that.
To verify that I have made a C++ program that executes a shellcode in a new thread with the infinite loop I showed above.
The main thread modifies the destination of the short jump to the furthest short jump (JMP REL8 +127).
At that address, the shellcode modify itself, restoring the destination of the JMP to -0x2 to make it an infinite loop again, then right after that there is another small shellcode that calls GetSystemTime filling a SYSTEMTIME struct before jumping back to the infinite loop.
Meanwhile, the main thread checks in a while(true) loop is the SYSTEMTIME struct has been filled, meaning that the shellcode of the other thread has executed correctly, and when it is filled, it resets it to 0 with SecureZeroMemory before restarting the operation (modifying the destination of the short jump to make the other thread get the SYSTEMTIME).
A JMP REL8 can jump from -128 to +127, and the only why it could crash is if the destination is -1 (0xFF) since it would set the next instruction to execute to 0xFF which won’t work.
Therefore, I have placed my JMP REL8 surrounded by NOPs and a single 0xCC (breakpoint) at the end.
This means that if at any moment the destination is being read while incompletely overwritten, the shellcode will start executing NOPs until arriving on the breakpoint, and I will know that an incomplete destination has been read and executed.
The last case would be if the incomplete overwriting of the destination byte led to the execution of JMP REL8 0xFF (-1) which would place the instruction pointer on 0xFF and it would crash because it is not a valid opcode just by itself.
I have ran my program for about an hour and it never crashed or hit a breakpoint, therefore, I think that this is not possible.
If someone can confirm, or better has the explanation of why this cannot happen, this would be much appreciated.
The code of this program can be found on my GitHub feel free to experiment with it.
Why does it matter?
Thread hijacking is rather straightforward: you suspend a thread, save its context, set its context to execute your shellcode, resume the thread and wait for it to finished executing.
You can be acknowledged of its completion for example by ending your thread with an infinite loop and reading a value in memory with ReadProcessMemory that your shellcode updated to let know your other hijacking process that it is done, or you can check if the instruction pointer is now in the instructions of the infinite loop.
Once you know that the thread completed, you can once again suspend the thread, set its context to the initial context and resume so it can go back doing whatever it was doing before.
Granted that you have correctly cleared after your operations, there won’t be any problem.
Now, this is okay for one-time execution, but if you want to execute stuff several thousands of time per seconds all these operations will slow you down quite a lot.
Instead, granted that your target process has a thread that you can hijack permanently without problem, you could use this system for the thread control, avoiding entirely to suspend, resume and change the thread context.
The thread stays in its infinite loop, you update your shellcode, then change the single byte of the destination of the jump to release the execution from the loop, the shellcode resets the destination to -0x2, execute your operations, and goes back to the infinite loop.
I am going to try that in action in a new bypass, I’ll keep you updated.