Need help hooking these functions

Please post any questions about developing your plugin here. Please use the search function before posting!
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Need help hooking these functions

Postby iPlayer » Fri Apr 21, 2017 9:21 am

Hey there,

My idea is to make stock TF2 bots work on PLR maps as they would on PL maps. They work perfectly on Payload maps, but on Payload Race maps they just stand still and do nothing. And it's official behavior, as bots are announced to not support PLR maps.

On PL map, there's 1 bomb cart. One team pushes the cart to their enemies, the other one defends their base.
On PLR map, however, there're 2 bomb carts. Each team has to push and defend at the same time.

I want to make the bots forget defending and at least start pushing their cart.

There's a Sourcemod plugin that just spawns a flag (like on CTF maps), makes it invisible, parents it to the carts, hooks its touch method etc., just to make the bots approach their carts. But no, I want to fix the problem in the root.

To start with, I'm trying to hook CTFGameRules::GetPayloadToPush and CTFBotMainAction::Update - just because they seem interesting. No luck so far. Testing on Linux.

CTFGameRules::GetPayloadToPush
  • Regular (non-virtual function)
  • Linux symbol: _ZNK12CTFGameRules16GetPayloadToPushEi
  • Windows signature: 55 8B EC 56 8B F1 2A 2A 2A 2A 2A 2A 8B 01 2A 2A 2A 2A 2A 2A 83 F8 03



This is how I hook it:

Syntax: Select all

if PLATFORM == "windows":
GET_PAYLOAD_TO_PUSH_IDENTIFIER = b"\x55\x8B\xEC\x56\x8B\xF1\x2A\x2A\x2A\x2A\x2A\x2A\x8B\x01\x2A\x2A\x2A\x2A\x2A\x2A\x83\xF8\x03"
else:
GET_PAYLOAD_TO_PUSH_IDENTIFIER = "_ZNK12CTFGameRules16GetPayloadToPushEi"

server = find_binary('server')


# CTFGameRules::GetPayloadToPush(CTFGameRules *this, int)
get_payload_to_push = server[GET_PAYLOAD_TO_PUSH_IDENTIFIER].make_function(
Convention.THISCALL,
[DataType.POINTER, DataType.INT],
DataType.VOID
)


@PreHook(get_payload_to_push)
def pre_get_payload_to_push(args):
print("Pre-Hooked!", args[1])


@PostHook(get_payload_to_push)
def post_get_payload_to_push(args, ret_val):
print("Post-Hooked!", args[1])


Here's what happens:
If I only do a pre-hook - the function crashes (ESP not present) after executing pre-hook;
If I only do a post-hook - the function crashes after executing post-hook;
If I do both hooks - the function crashes after executing post-hook.

Also, as you may have noticed, I print the value of the args[1]. It's junk in a pre-hook, but is set to "3" in a post-hook. Invincible told me that could mean that that "int" argument is passed by reference and is meant to be changed inside of the function.
I tried different return types - VOID, POINTER, INT - same behavior. ESP not present.

CTFBotMainAction::Update
  • Virtual function, linux index is 47, windows index is 46
  • Linux symbol: _ZN16CTFBotMainAction6UpdateEP6CTFBotf
  • Windows signature: didn't even bother




At first I didn't notice this was a virtual, so I hooked it as a regular function - with a symbol. Pre-hook is executed, then "ESP not present". Didn't test post-hooks.

Then I realized it's a virtual, so I decided to obtain an instance of CTFBotMainAction and hook the function properly. Unfortunately, almost all methods of this class are virtual, and I had a hard time trying to get an instance of it.

Finally I've found a non-virtual thunk to CTFBotMainAction::ShouldAttack, but it's only called when bots see an enemy. That means that I will have to run a PL map (or any other map that bots actually support), obtain an instance of CTFBotMainAction, then build a virtual function CTFBotMainAction::Update using the vtable index, and only then I can set a hook and change the level to my PLR map.

Here's the plugin that does all this stuff:

Syntax: Select all

from core import PLATFORM
from memory import Convention, DataType, find_binary


if PLATFORM == "windows":
SHOULD_ATTACK_THUNK_IDENTIFIER = b""
BOT_MAIN_ACTION_UPDATE_INDEX = 46
else:
SHOULD_ATTACK_THUNK_IDENTIFIER = "_ZThn4_NK16CTFBotMainAction12ShouldAttackEPK8INextBotPK12CKnownEntity"
BOT_MAIN_ACTION_UPDATE_INDEX = 47


should_attack_thunk_hook_set = False
bot_main_action_update = None


server = find_binary('server')


# CTFBotMainAction::ShouldAttack(CTFBotMainAction *this, const INextBot *, const CKnownEntity *)
should_attack_thunk = server[SHOULD_ATTACK_THUNK_IDENTIFIER].make_function(
Convention.THISCALL,
[DataType.POINTER, DataType.POINTER, DataType.POINTER],
DataType.BOOL
)


def create_bot_main_action_update(pointer):
# CTFBotMainAction::Update(CTFBotMainAction *this, CTFBot *, float)
return pointer.make_virtual_function(
BOT_MAIN_ACTION_UPDATE_INDEX,
Convention.THISCALL,
[DataType.POINTER, DataType.POINTER, DataType.FLOAT],
DataType.POINTER
)


def pre_bot_main_action_update(args):
print("Pre-Hooked CTFBotMainAction::Update!")


def pre_should_attack_thunk(args):
print("Retrieving CTFBotMainAction instance...")
global bot_main_action_update
bot_main_action_update = create_bot_main_action_update(args[0] - 4)

print("Setting pre-hook on CTFBotMainAction::Update...")
bot_main_action_update.add_pre_hook(pre_bot_main_action_update)

print("Removing pre-hook from CTFBotMainAction::ShouldAttack thunk...")
should_attack_thunk.remove_pre_hook(pre_should_attack_thunk)
global should_attack_thunk_hook_set
should_attack_thunk_hook_set = False


def load():
global should_attack_thunk_hook_set
should_attack_thunk.add_pre_hook(pre_should_attack_thunk)
should_attack_thunk_hook_set = True


def unload():
global should_attack_thunk_hook_set
if should_attack_thunk_hook_set:
should_attack_thunk.remove_pre_hook(pre_should_attack_thunk)
should_attack_thunk_hook_set = False

global bot_main_action_update
if bot_main_action_update is not None:
bot_main_action_update.remove_pre_hook(pre_bot_main_action_update)
bot_main_action_update = None


By the way, you may notice that I used args[0] - 4 to get a pointer to the instance. It's because the thunk looks like this:


Eventually I got the instance and set the pre-hook on its Update method which was immediately called, I saw "Pre-Hooked CTFBotMainAction::Update!" in my console, then... ESP not present.

Any ideas?

Thanks.
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image
User avatar
Ayuto
Project Leader
Posts: 2193
Joined: Sat Jul 07, 2012 8:17 am
Location: Germany

Re: Need help hooking these functions

Postby Ayuto » Fri Apr 21, 2017 2:00 pm

You are always missing an argument (this pointer). E.g. here:

Syntax: Select all

# CTFGameRules::GetPayloadToPush(CTFGameRules *this, int)
get_payload_to_push = server[GET_PAYLOAD_TO_PUSH_IDENTIFIER].make_function(
Convention.THISCALL,
[DataType.POINTER, DataType.INT],
DataType.VOID
)

It should actually look like this:

Syntax: Select all

# CTFGameRules::GetPayloadToPush(CTFGameRules *this, int)
get_payload_to_push = server[GET_PAYLOAD_TO_PUSH_IDENTIFIER].make_function(
Convention.THISCALL,
[DataType.POINTER, DataType.POINTER, DataType.INT],
DataType.VOID
)
If you are using make_function and make_virtual_function you always have to pass the this pointer. If you are using the TypeManager or the data files, it's done automatically.
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Re: Need help hooking these functions

Postby iPlayer » Fri Apr 21, 2017 2:06 pm

Wait, what? I thought I did not forget them. Look at the declaration, CTFGameRules::GetPayloadToPush receives only two arguments, including this-pointer. And I declared two arguments.

EDIT: I just put IDA declarations in the comments. I know in the actual C++ code the this-pointer won't be explicit.
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image
User avatar
Ayuto
Project Leader
Posts: 2193
Joined: Sat Jul 07, 2012 8:17 am
Location: Germany

Re: Need help hooking these functions

Postby Ayuto » Fri Apr 21, 2017 4:31 pm

Okay, these functions are very weird. You might want to try this:

Syntax: Select all

import memory

from memory import DataType
from memory import Register
from memory import CallingConvention

from memory.hooks import PreHook
from memory.hooks import PostHook


class WeirdConvention(CallingConvention):
def get_registers(self):
return [Register.ESP]

def get_pop_size(self):
return 4

def get_argument_ptr(self, index, registers):
return registers.esp.address.get_pointer() + index * 4 + 4

def argument_ptr_changed(self, index, registers, arg_ptr):
pass

def get_return_ptr(self, registers):
pass

def return_ptr_changed(self, registers, return_ptr):
pass


server = memory.find_binary('server')

# CTFGameRules::GetPayloadToPush(int)
get_payload_to_push = server['_ZNK12CTFGameRules16GetPayloadToPushEi'].make_function(
WeirdConvention,
[DataType.POINTER, DataType.POINTER, DataType.INT],
DataType.VOID
)


@PreHook(get_payload_to_push)
def pre_get_payload_to_push(args):
print("Pre-Hooked!", args)
payload_ptr = args[0]
gamerules = args[1]
team = args[2]


@PostHook(get_payload_to_push)
def post_get_payload_to_push(args, ret_val):
print("Post-Hooked!", args)
payload_ptr = args[0]
gamerules = args[1]
team = args[2]
print(payload_ptr.get_int())
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Re: Need help hooking these functions

Postby iPlayer » Sun Apr 23, 2017 3:55 am

Okay, thanks to Ayuto and Invincible, I make steps towards enabling bots on PLR maps.

What I have learned is that CTFGameRules::GetPayloadToPush sets the payload inthandle to -1 on PLR maps. While on PL maps it's just a valid inthandle of the correspoing team_train_watcher entity.

What I'm trying to do now is to return a valid inthandle in GetPayloadToPush.

This is how I detect valid train watcher for the current round (the map usually includes 3 different sublevels a.k.a. rounds):

Syntax: Select all

train_watchers_by_team = {}


def find_trains(cp_names):
for train_watcher in EntityIter('team_train_watcher'):
for cp_index in range(8):
key = "linked_cp_{}".format(cp_index + 1)
cp_name = get_key_value_string_t(train_watcher, key)
if cp_name in cp_names:
break
else:
continue

team = train_watcher.get_key_value_int('TeamNum')
if team == 0:
train_watchers_by_team[2] = train_watcher.inthandle
train_watchers_by_team[3] = train_watcher.inthandle
else:
train_watchers_by_team[team] = train_watcher.inthandle


@OnEntityOutput
def listener_on_entity_output(output_name, activator, caller, value, delay):
if output_name != "OnStart":
return

if caller.classname != "team_control_point_round":
return

cp_names = get_key_value_string_t(caller, 'cpr_cp_names').split(' ')
find_trains(cp_names)
print("Found trains: ", train_watchers_by_team)


This is my PreHook of the GetPayloadToPush:

Syntax: Select all

@PreHook(get_payload_to_push)
def post_get_payload_to_push(args):
team = args[2]
if team not in train_watchers_by_team:
return

train_watcher_handle = train_watchers_by_team[team]
args[0].set_uint(train_watcher_handle)


This doesn't seem to work, however, because CTFBotPayloadPush::Update doesn't call CTeamTrainWatcher::GetTrainEntity afterwards. I tried to reproduce assembly instructions one by one in order to detect where it might jump off the route to the GetTrainEntity call.

Here's assembly snippet with my comments - I've marked all possible jumps with (1), (2) and (3).


Now here's that same PreHook, but now it tries to see in the future and reproduce the steps that CTFBotPayloadPush::Update will do:

Syntax: Select all

@PreHook(get_payload_to_push)
def post_get_payload_to_push(args):
team = args[2]
if team not in train_watchers_by_team:
return

train_watcher_handle = train_watchers_by_team[team]
args[0].set_uint(train_watcher_handle)

edx = eax = train_watcher_handle
eax >>= 12
edx &= 0xfff
edx <<= 4
edx += server['g_pEntityList'].get_pointer()
print("[edx + 8], eax ::", (edx + 8).get_int(), eax)

eax = (edx + 4).get_pointer()
train_watcher = make_object(Entity, eax)
print("Train watcher entity: ", train_watcher.target_name)


I get something like
[edx + 8], eax :: 1234 1234
Train watcher entity: blue_watcher_1

Printed (read "spammed") to my console, so it seems that there's no place where Update could jump off, because:
  • (1) I don't set the handle to -1
  • (2) [edx + 8] equals to eax
  • (3) [edx + 4] is not null

I don't get it. Where's the problem?
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Re: Need help hooking these functions

Postby iPlayer » Sun Apr 23, 2017 5:25 am

I forgot to return from PreHook to prevent original function from being called. This probably leads to my inthandle being overwritten by the original function call. I will test it shortly.
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image

Return to “Plugin Development Support”

Who is online

Users browsing this forum: No registered users and 17 guests