Jump to content
Tuts 4 You

Is there any difference on offset between call & jmp on x64?


CreateAndInject

Recommended Posts

CreateAndInject
Posted

See the following code, Way 1 can work on both x86 & x64, but Way 2 can only work on x86.

Why does Way 2 fail? How to fix Way 2?

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace HookTest
{
    unsafe class Program
    {
        public static string MyReadLine() => "test";

        [DllImport("kernel32")]
        public static extern bool VirtualProtect(IntPtr address, int size, int newProtect, out int oldProtect);

        static void Main()
        {
            var oldMethod = typeof(Console).GetMethod("ReadLine");
            var newMethod = typeof(Program).GetMethod("MyReadLine");
            RuntimeHelpers.PrepareMethod(oldMethod.MethodHandle);
            var oldAddress = oldMethod.MethodHandle.GetFunctionPointer();
            var newAddress = newMethod.MethodHandle.GetFunctionPointer();
            VirtualProtect(oldAddress, 20, 0x40, out _);

            // Way 1 : jmp offset
            // Marshal.WriteByte(oldAddress, 0xE9); // jmp
            // Marshal.WriteInt32(oldAddress, 1, checked((int)((long)newAddress - (long)oldAddress - 5))); // offset

            // Way 2 : call offset + ret
            Marshal.WriteByte(oldAddress, 0xE8); // call 
            Marshal.WriteInt32(oldAddress, 1, checked((int)((long)newAddress - (long)oldAddress - 5))); // offset
            Marshal.WriteByte(oldAddress, 5, 0xC3); // ret

            string str = Console.ReadLine();
            Console.WriteLine(str);
            Console.ReadKey();
        }
    }
}

 

CreateAndInject
Posted (edited)
1 hour ago, fearless said:

The 2nd way only has 4 bytes (32bits) length to "jump" to, so of course in x64 you would need 8 bytes (64bits) to properly "jump" to the location when using call. In x86 it of course works fine as thats expected that the E8 call instruction byte is followed by the 32bit value. Also check out short/near jumps and this: https://stackoverflow.com/questions/5757866/what-does-short-jump-mean-in-assembly-language

Don't agree. Both E8(call) & E9(jmp) need 4 bytes rather than 8 bytes even if on x64. You can debug in any debugger.

Edited by CreateAndInject
Posted

Hi

Maybe it's because your call destroying stack !!!!

you can use "push xyz, ret" instead (xyz = destination address).

CreateAndInject
Posted (edited)
1 hour ago, h4sh3m said:

Hi

Maybe it's because your call destroying stack !!!!

you can use "push xyz, ret" instead (xyz = destination address).

I already succeed by `jmp offset` and `push address + ret` on both x86 and x64 before, now, I want to know why `call offset + ret` failed on x64 and how to fix it.

Edited by CreateAndInject
Posted (edited)

What do you exactly mean with "Way 2 fail" ? What does exactly happen (or doesn't)?
If you mean that the called routine crashes or gives wrong results, you should pay attention to stack requirements.

Waiting for the experts, of course.

However, as far as I can tell, on x64 usually you have to reserve 0x20 bytes on the stack (add rsp,0x20) before calling a routine. In other words the callee expects it has 4 [additional] qwords at the top of the stack when called. 

Hope this helps.

Regards,
Tony

 

Edited by tonyweb
  • Like 1
Posted
Quote

// Way 2 : call offset + ret

Marshal.WriteByte(oldAddress, 0xE8); // call

Marshal.WriteInt32(oldAddress, 1, checked((int)((long)newAddress - (long)oldAddress - 5))); // offset

Marshal.WriteByte(oldAddress, 5, 0xC3); // ret

0x400000 call 0xE8 0x11223344

0x400005 ret 0xC3

The return belongs in the Detour proc
 

call Detour

...

proc Detour

   nop

ret

 

Or use ...

mov rax, Detour

call rax // or jmp rax

CodeExplorer
Posted

Tiny changes to code:
 

        public static string MyReadLine()
        {
        return "test";
        }

           Just for seeing what's going on:

            Console.WriteLine("oldAddress="+oldAddress.ToString("X8"));
            Console.WriteLine("newAddress="+newAddress.ToString("X8"));
            long diff = (long)newAddress-(long)oldAddress;
            Console.WriteLine(diff.ToString("X8"));

difference was not to big so it can be handled by 32 bits value. 0xE8 + 4 bytes should be just fine for x64 also.
I don't know why it isn't working!

000007FEE01F0B20 | 48 83 EC 28                               | sub rsp,28                                                       |
000007FEE01F0B24 | E8 67 C7 FF FF                            | call mscorlib.ni.7FEE01ED290                                     |
000007FEE01F0B29 | 4C 8B D8                                  | mov r11,rax                                                      |
000007FEE01F0B2C | 49 8B 03                                  | mov rax,qword ptr ds:[r11]                                       |
000007FEE01F0B2F | 48 8B 90 B8 00 00 00                      | mov rdx,qword ptr ds:[rax+B8]                                    |
000007FEE01F0B36 | 49 8B CB                                  | mov rcx,r11                                                      |
000007FEE01F0B39 | 48 8B C2                                  | mov rax,rdx                                                      |
000007FEE01F0B3C | 48 83 C4 28                               | add rsp,28                                                       |
000007FEE01F0B40 | 48 FF E0                                  | jmp rax                                                          |

it will becomes after patches:

000007FEE01F0B20 | E8 0B B5 E3 1F                            | call 7FF0002C030                                                 |
000007FEE01F0B25 | C3                                        | ret                                                              |
000007FEE01F0B26 | C7                                        | ???                                                              |
000007FEE01F0B27 | FF                                        | ???                                                              |
000007FEE01F0B28 | FF 4C 8B D8                               | dec dword ptr ds:[rbx+rcx*4-28]                                  |
000007FEE01F0B2C | 49 8B 03                                  | mov rax,qword ptr ds:[r11]                                       |

kernelbase.dll
000007FEFD9931F0 | 66 90                                     | nop                                                              |
000007FEFD9931F2 | CC                                        | int3                                                             |
000007FEFD9931F3 | C3                                        | ret                                                              |

If I attach x64dbg to the program it just gives an kernelbase.dll int3 exception.

 

 

CreateAndInject
Posted

I was told `call offset + ret` is completely equal to `jmp offset`.

This seems right on x86, I'm puzzle why x64 is different.

Is `offset` here has different meaning between x86 & x64 when it's negative number? Can it also work on x64 when the offset is positive number? 

Posted

call means put current address + 5 (size of the e8 xx xx xx xx opcode) on the stack and jump to the code...

the ret would clean it up upon return (which it probably wont reach) , the call messed up the params passed by adding another to it... the return address of the call...

they are NOT the same

stack on hitting your code looks like this (example with 3 params)

param3
param2
param1

so a jmp keeps that intact

if you call its like this

param3
param2
param1
returnaddress

as for your e8 offset confusion...

the opcode is e8 xx xx xx xx, where xx xx xx xx is a signed dword, so the range is current offset +/- 2 gigs ish....

if the address is out of range you need to get creative with jmp qword ptr / call qword ptr

  • Like 1
CreateAndInject
Posted
2 hours ago, evlncrn8 said:

call means put current address + 5 (size of the e8 xx xx xx xx opcode) on the stack and jump to the code...

the ret would clean it up upon return (which it probably wont reach) , the call messed up the params passed by adding another to it... the return address of the call...

they are NOT the same

stack on hitting your code looks like this (example with 3 params)

param3
param2
param1

so a jmp keeps that intact

if you call its like this

param3
param2
param1
returnaddress

as for your e8 offset confusion...

the opcode is e8 xx xx xx xx, where xx xx xx xx is a signed dword, so the range is current offset +/- 2 gigs ish....

if the address is out of range you need to get creative with jmp qword ptr / call qword ptr

But why `call offset + ret` always work on x86?

Posted (edited)
1 hour ago, CreateAndInject said:

But why `call offset + ret` always work on x86?

did you tried all type of functions ?!

functions that have more than 3 parameters (specially in stdcall calling convention) ?

Edited by h4sh3m
CreateAndInject
Posted
13 hours ago, h4sh3m said:

did you tried all type of functions ?!

functions that have more than 3 parameters (specially in stdcall calling convention) ?

Thanks, you're right.

`call offet + ret` can only work if there're less than 3 parameters AND on x86

So `call offset + ret` != `jmp offset` (`jmp offset` allways work)

So it's coincidence which `call offet + ret` can work if there're less than 3 parameters AND on x86?

Posted

seems you're testing just with fastcall/cdecl calling conventions, this method will not work even for functions with 1 parameter in stdcall calling convention.

CreateAndInject
Posted
1 hour ago, h4sh3m said:

seems you're testing just with fastcall/cdecl calling conventions, this method will not work even for functions with 1 parameter in stdcall calling convention.

Thanks, I remove this method, and keep `jmp offset` and `push address + ret`.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...