Skip to content

Instantly share code, notes, and snippets.

@AndrewMarumoto
Last active February 20, 2023 18:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AndrewMarumoto/22e4edfe8dfab12e8513d41fcca8b700 to your computer and use it in GitHub Desktop.
Save AndrewMarumoto/22e4edfe8dfab12e8513d41fcca8b700 to your computer and use it in GitHub Desktop.
Adding new python hooks to Battlefield 2142

Overview

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.

Caveats

  • 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

Example: hooking comm rose events

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.

Reversing the RadioMessage event

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

Hooking the RadioMessage event

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.

Patching with ctypes

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

Setting up our hook

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.

Calling back to python

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

Passing arguments

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)

Example files

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.

import host
import math
import ctypes
import struct
import traceback
libc = ctypes.cdll.LoadLibrary('libc.so.6')
mmap = libc.mmap
mmap.restype = ctypes.c_ulong
def round_pagesize(size):
return int(math.ceil(size / float(0x1000))) * 0x1000
def getaddr(x):
return ctypes.addressof(x)
def getbuf(addr, size):
return (ctypes.c_char*size).from_address(addr)
def mkbuf(x=0x1000, rwx=False):
if rwx:
def alloc(size):
size = round_pagesize(size)
return getbuf(mmap(0, size, 7, 34, -1, 0), size)
else:
alloc = ctypes.create_string_buffer
if type(x) is int:
return alloc(x)
elif type(x) is str:
buf = alloc(len(x))
buf[:len(x)] = x
return buf
def patch(addr, new_code, orig_perms=5):
buf = getbuf(addr, len(new_code))
size = round_pagesize(len(new_code))
libc.mprotect(addr & -0x1000, size, 7)
buf[:] = new_code
libc.mprotect(addr & -0x1000, size, orig_perms)
return True
def example_callback():
if not hasattr(example_callback, 'buf'):
return
data = example_callback.buf[0xf00:]
event = struct.unpack('<Q', data[:8])[0]
event_data = getbuf(event, 0x50)
opt = ord(event_data[0x2a:][:1])
sender = ord(event_data[0x28:][:1])
radio = {
0xe5: 'backup',
0xdf: 'pickup',
0xd0: 'roger',
0xd1: 'negative',
0xd2: 'thanks',
0xd3: 'sorry',
0xe4: 'medic',
0xe7: 'ammo',
0xec: 'go',
0xdb: 'spot',
0xef: 'follow',
}
if opt in radio:
selection = radio[opt]
else:
selection = str(opt)
host.rcon_invoke('game.sayall %s_%d\n' % (selection, sender))
def example_hook():
handle_radiomessage = 0x457620
stub = '\x48\xB8\x41\x41\x41\x41\x41\x41\x41\x41\xFF\xe0'
hook = "\x57\x56\x52\x53\x55\x41\x54\x41\x55\x41\x56\x48\x8D\x05\x00\x00\x00\x00\x48\x25\x00\xF0\xFF\xFF\x48\x89\xB0\x00\x0F\x00\x00\x48\x8D\x3D\x2A\x00\x00\x00\x48\xC7\xC0\x60\x7D\x40\x00\xFF\xD0\x41\x5E\x41\x5D\x41\x5C\x5D\x5B\x5A\x5E\x5F\x48\x89\x6C\x24\xE0\x4C\x89\x64\x24\xE8\x48\x89\xF5\x48\xC7\xC0\x2D\x76\x45\x00\xFF\xE0"
cmd = 'from bf2 import dyn_patch; dyn_patch.example_callback();'
hook_buf = mkbuf(hook+cmd, 1)
example_callback.buf = hook_buf
patch(handle_radiomessage, stub.replace('A'*8, struct.pack('<Q', getaddr(hook_buf))))
def init():
try:
example_hook()
except:
a = open('errors', 'wb')
traceback.print_exc(file=a)
a.close()
; save register state
push rdi
push rsi
push rdx
push rbx
push rbp
push r12
push r13
push r14
; save pointer to the RadioMessageReceivedEvent instance so it can be accessed by python
lea rax, [rip]
and rax, -0x1000
mov [rax+0xf00], rsi
; get our python command into rdi as the argument for PyRun_SimpleString
lea rdi, [rip+cmd]
; call PyRun_SimpleString
mov rax, 0x407d60
call rax
; restore register state
pop r14
pop r13
pop r12
pop rbp
pop rbx
pop rdx
pop rsi
pop rdi
; run the instructions that were overwritten with our jmp
mov [rsp-0x20], rbp
mov [rsp-0x18], r12
mov rbp, rsi
; jump back into the hooked function
mov rax, 0x45762D
jmp rax
cmd:
@rkantos
Copy link

rkantos commented Feb 19, 2023

Could I use this to manually trigger sounds by the server? Or do I even need this for that?

@AndrewMarumoto
Copy link
Author

That should be possible. Going with the theme from above, when a client selects a message in the comm rose it sends an event to the server which then gets propagated forward to all the other clients so they can hear the message audio and see it in the chat. You'd need to reverse out how that propagation happens in the server code so you could emulate it and create fake events without needing the initial client event.

The process should be similar for other events/sounds like gunshots, vehicle sounds, flag captures, etc. Though you might actually have to spawn a bullet to get gunshot audio for example, depending on how the game implements it. Or for flag captures, the server might just send an event saying that flag ID: whatever just changed state to captured by X team, and so you'd need to temporarily spawn a control point at the location you want the sound to come from then immediately delete it after playing the sound... all of which could be sketchy and cause issues. So it may get messy with all the side effects lol

You shouldn't need to do any of the hooking stuff described here, but the technique would be similar in how you're interacting with memory using ctypes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment