This is a proof of concept for hooking events that are normally inaccessible from python. It assumes you've set up an unrestricted python interpreter for 2142 and installed ctypes
. The hooking is done dynamically through python, rather than by statically modifying the executable.
- The code here is for 64 bit systems, it will not work as is on 32 bit
- The offsets may be different depending on which 2142 patch you're working with
- If you want to add new events, you'll need to reverse engineer the relevant parts of the server to find the offsets
- You'll also need to be able to write assembly
So we want to get some python code executed whenever the user selects an option in the comm rose. This might not be the best example, since iirc there's a thread on bfeditor.org about doing this in bf2 by modifying the HUD files to trigger a python remotecommand event or something. But afaik there's no way to do it purely through python, so it should work fine to exemplify the process for hooking events.
I'm not going to go too in depth here, and will mostly just be stating my findings, but if you want to know more about the process of reversing you can contact me about it.
Anyway, the handleRadioMessageReceivedEvent
function seemed like a good place to insert the hook (the linux builds of the server are compiled with debug symbols). The second argument to this function is a pointer to a RadioMessageReceivedEvent
class instance. The byte at offset 0x28
in this class contains the player ID, and the byte at offset 0x2a
contains the selection ID.
The primary selection IDs are as follows. There's a few more that I was too lazy to find, but that would be trivial to do.
0xd0 : Roger
0xd1 : Negative
0xd2 : Thanks
0xd3 : Sorry
0xdb : Spot
0xe4 : Medic
0xe5 : Backup
0xe7 : Ammo
0xec : Go, go, go!
0xef : Follow
When handleRadioMessageReceivedEvent
gets called, we want our code to run first. The general idea is that we're going to allocate some memory, write our hook code to it, and then overwrite the first few instructions of the above function with a jmp
into our hooking code. Once our code is done, it will run whatever instructions were overwritten and then jump back into the function.
The handleRadioMessageReceivedEvent
function begins with the following instructions.
mov [rsp-0x20], rbp
mov [rsp-0x18], r12
mov rbp, rsi
mov [rsp-0x28], rbx
We want to patch the function to look like this.
mov rax, <address of our code>
jmp rax
; <extra byte or two>
mov [rsp-0x28], rbx
So when this function is called, instead of normal operation it will first jump into our code before continuing through its normal path.
To do this with ctypes
, we first want to get a reference to the memory where the function is located. The following function will return a python object that can be used to read from and write to an arbitrary address. It's basically a mutable string object.
def getbuf(addr, size):
return (ctypes.c_char*size).from_address(addr)
Before we can use this to patch in the jump though, we need to change the permission for the page where the function is mapped. Normally this page is mapped r-x
, so a write to it will cause a segfault.
libc = ctypes.cdll.LoadLibrary('libc.so.6')
libc.mprotect(addr & -0x1000, size, 7) # change permissions to rwx
# < write patch here >
libc.mprotect(addr & -0x1000, size, 5) # restore original permissions
When a RadioMessage
event occurs, it will first jump to our hook code. The first thing we need to do is save register state. This can be as simple as pushing all the relevant registers onto the stack (perhaps a more robust method would be using getcontext
and setcontext
).
Once that's done, we can do whatever we want here.
Once our hook is finished, we need to restore the register state. Pop off the registers that were pushed earlier, or use setcontext
. Then execute whichever instructions were overwritten with the original jump. Finally jump back into the function at the next instruction after the overwritten ones.
Basically, it will look something like this.
; <save register state>
; your code goes here
; <restore register state>
; run instructions that were overwritten with the original jmp
mov [rsp-0x20], rbp
mov [rsp-0x18], r12
mov rbp, rsi
; jump back to hooked function
mov rax, 0x45762d
jmp rax
This code then needs to be mapped into memory. mmap
via ctypes
is probably the best way to do this. Once it's mapped, we can change the placeholder bytes in the jmp
patch to point to it.
So we've got assembly code executing for the event. But we wanted python.
There are a couple ways to do this, but I'm going to show the easy way for now (which is probably also the less correct way, but it works well enough). The 2142 server imports a bunch of functions from the python library, one of which is PyRun_SimpleString
. This function takes one argument, a string, and runs it as python code.
lea rdi, [rip+cmd] ; load address of cmd string into rdi (first argument to function call)
mov rax, 0x407d60 ; address of PyRun_SimpleString import
call rax
cmd:
db "import host; host.rcon_invoke('game.sayall test')",0
Now we've got python executing for the event, but we still don't know which player triggered it or which selection they made. Again, there are several ways of doing this. For now, I just have the assembly code writing whatever values are needed to a constant offset in the mapped page and then call back into a python function that will extract that values and propagate the event.
lea rax, [rip] ; get address of instruction
and rax, -0x1000 ; get page-aligned address
mov [rax+0xf00], rsi ; save rsi to offset 0xf00 in page
; (rsi is pointing to a RadioMessageReceivedEvent class instance)
Below you'll see the full source for the process described above. If anything is unclear or you have questions, let me know and I can update this document as necessary.
Could I use this to manually trigger sounds by the server? Or do I even need this for that?