# CCCamp CTF 2023
## Boss
> > The best kind of gamehacking: smash the overpowered boss enemy.> > George returned from his mountain vaccation and continues to roam the prehistoric earth, still causing troubles for CTF players. Get your revenge and beat him to death!>>  Author: 0x45da>> [`camp_gamedev-public.zip`](https://github.com/D13David/ctf-writeups/blob/main/cccampctf23/game_hacking/boss/../camp_gamedev-public.zip)
Tags: _game_
## SolutionWe start to explore the world a bit. At some point the green biome ends and some ice biome starts. The guardian warns us of some mean dinos in the icy land. This sounds like the right place to find the `overpowered boss enemy`.
![](https://raw.githubusercontent.com/D13David/ctf-writeups/main/cccampctf23/game_hacking/boss/images/location.png)
Since the boss enemy really is overpowered, there is no way to defeat him with the default player strength and health. So lets analyze the code a bit, and see what we can do.
The game has a `client-server` architecture. This basically expludes all easy local hacking of properties, since the server will most likely keep track of the game state. So lets see what happens when the player attacks a enemy.
src/client/client/game/entities/player@146```pythondef _update_move(self, dt: float) -> None:    game: Game = cast("Game", client.scene_manager.current_scene)
    self.velocity_x = 0    self.velocity_y = 0
    keys = cast(Dict[int, bool], client.scene_manager.keys.data)    controller = client.scene_manager.controller
    # ....
    if client.game_state.session_type == SessionType.SESSION_TYPE_NORMAL:        match keys:            case {key.SPACE: True}:                if self.can_attack():                    self._attacking = True                    self._last_attack = time.time()                    self._find_attack_enemy()
            case _:                pass    # ....```
So, in `_update_move` the player input is handled. When pressing `space` the player checks if he can attack and then tries to find a enemy to attack.
src/client/client/game/entities/player@261```pythondef can_attack(self):        return time.time() > self._last_attack + 0.600  # One attack is 600ms```
This looks interesting. The 600ms timeout is calculated locally, so we can try to remove it for faster attack rate. This might be something the server counters, but it does not hurt. So we change this to always return `true`.
src/client/client/game/entities/player@123```pythondef _find_attack_enemy(self):    direction = self.rotation_to_directionproto()    self.sprites[Activity.ACTIVITY_ATTACKING][        direction    ].frame_index = 0  # Reset attack animation to start    self.sprite = self.sprites[Activity.ACTIVITY_ATTACKING][direction]
    objects_in_range: List[Enemy] = client.game_state.get_objects_in_range(        distance_squared=400    )
    enemy_obj = None    # print("Obj in range: " + str(len(objects_in_range)))
    if len(objects_in_range) == 0:        return
    enemy_obj = objects_in_range[0]  #  TODO: Differentiate between enemies
    client.global_connection.attack_enemy(        time=datetime.now(), uuid=enemy_obj.uuid, damage=10    )```
Next the game tries to check for attackeable enemies within `20` units radius. If a candidate is found the client notifies the server about the attack with a fixed damage of `10`.
src/server/server/server_runner@402```python@authenticatedasync def _attack_enemy(self, uuid: str, message: AttackEnemy) -> None:    # ....
    if not server.game_state.fight_manager.is_plausible_attack(        user=user, enemy=enemy, attack_msg=message    ):        print("not plausible attack")        return  # TODO: Not plausible message to client?
    if not server.game_state.fight_manager.can_take_damage_enemy(        enemy_uuid=enemy.uuid, cooldown_ticks=30    ):  # TODO: Dynamic cooldown for attacks        print("Enemy can't get damage again, cooldown not reached")        return  # TODO: Not plausible message to client?
    await enemy.take_damage(message.damage)
    # ....```
The server checks for various things like if user and enemy are valid. But also if the attack properties are `plausible` and if the enemy can take damage.
src/server/server/game/fight@34```pythondef can_take_damage_enemy(self, enemy_uuid: str, cooldown_ticks: int = 30) -> bool:    # User not in list => Store and allow to take damage    if not enemy_uuid in self.user_damage_cooldowns:        self.enemy_damage_cooldowns[enemy_uuid] = time.time()        return True
    last_damage_timestamp = self.enemy_damage_cooldowns[enemy_uuid]    ticks_passed = (time.time() - last_damage_timestamp) * SERVER_TICK_RATE
    # Cooldown did not pass yet    if ticks_passed < cooldown_ticks:        return False
    self.enemy_damage_cooldowns[enemy_uuid] = time.time()    return True
def is_plausible_attack(self, user: User, enemy: Enemy, attack_msg: AttackEnemy):    # Check if attack direction is plausible:    # TODO
    # Check if damage is plausible    if attack_msg.damage > 11:        return False
    if attack_msg.damage < 0:        return False
    # Check if enemy is in range of weapon    dist_x = (user.coords.x - enemy.x) ** 2    dist_y = (user.coords.y - enemy.y) ** 2    max_attack_distance = 400  # TODO: Find good value, maybe dynamic from weapon    if dist_x + dist_y > max_attack_distance:        return False
    attack_timestamp = time.mktime(attack_msg.time.timetuple())
    if not enemy.uuid in self.enemy_damage_cooldowns:        self.enemy_damage_cooldowns[enemy.uuid] = attack_timestamp        return True
    # TODO: Get dynamic from current user weapon    weapon_cooldown_ticks = 30
    last_damage_timestamp = self.enemy_damage_cooldowns[enemy.uuid]
    ticks_passed = (attack_timestamp - last_damage_timestamp) * SERVER_TICK_RATE    if ticks_passed > weapon_cooldown_ticks:        self.enemy_damage_cooldowns[enemy.uuid] = time.mktime(            attack_msg.time.timetuple()        )
        return True    return False```
Ok, changing the damage to a high value will not work, since everything damage value greater `11` will be recognized as `implausible`. Also the player distance to the enemy is checked, so no range weapons are allowed. Also the attack cooldown is checked here, so we cannot increase the hit rate for the client.
This looks all save, more or less at least. So lets see what happens when the enemy takes damage. For base entities just the health is decreased.
src/server/server/game/entity/enemy@70```pythonasync def take_damage(self, damage: int) -> None:    self.health -= damage```
There is a specialization `BossPatrolEnemy` that puts a flag in the players inventory when the enemy dies. But nothing interesting that can be done here, except we have a hint for what to watch out in code to find other places where flags are added to the players inventory.
src/server/server/game/entity/enemy@246```pythonclass BossPatrolEnemy(PatrolEnemy):    def __init__(self, *args: Any, **kwargs: Any) -> None:        super().__init__(*args, **kwargs)
    async def take_damage(self, damage: int) -> None:        self.health -= damage
        if self.user_uuid is None:            return
        if self.health <= 0:            success = await server.game_state.give_item(                user_id=self.user_uuid,                item=ITEMS["flag_boss"],                once=True,            )            if success:                await server.global_server.update_self(self.user_uuid)```
But we still cannot defeat the boss. So lets check what happens when the player takes damage. Lets check the `PatrolEnemy` in server code.
src/server/server/game/entity/enemy@225```pythonclass PatrolEnemy(Enemy):    patrol_path: NPCPath    speed: float
    def __init__(        self, path: Polygon | None, speed: float, *args: Any, **kwargs: Any    ) -> None:        super().__init__(*args, **kwargs)
        self.speed = speed
        self.patrol_path = NPCPath(path=path, speed=speed, loop=True)
        self.state_machine.add_state(PatrolState(self))        self.state_machine.add_state(AggroState(self))        self.state_machine.add_state(AttackingState(self))        self.state_machine.add_state(DeadState(self))
        self.state_machine.change_state("patrol")```
The enemies behaviour is implemented with a simple state machine. As per default the enemy is in `patrol state`. If the player is moving to close the enemy changes from `patrol` to `aggro state` and tries to reach the player until being in attack range. If the player is reached the enemy goes from `aggro` to `attack state` and sends a `GiveDamage` packet to the client after a attack happened.
src/server/server/game/entity/enemy@192```pythonif time.time() > self.last_attack + AttackingState.ATTACK_SPEED:    await server.global_server.give_damage(        self_obj.uuid, 7, include_user_ids=[self_obj.user_uuid]    )    self.last_attack = time.time()    self_obj.last_attack = time.time()```
The client then recieves the message and sends back a `AcknowledgeDamage` damage packet with the amount of acknowledged damage.
src/client/client/networking/network@153```pythoncase {"giveDamage": inner_message}:    self.acknowledge_damage(damage=inner_message["damage"])```
src/server/server/server_runner@380```python@authenticatedasync def _acknowledge_damage(self, uuid: str, message: AcknowledgeDamage) -> None:    user_uuid = server.game_state.peer_sessions[self.peer].user_id    user = server.game_state.get_user(uuid=user_uuid)
    if user is None:        return
    response = ServerMessage(uuid=uuid)
    if (        server.game_state.fight_manager.can_take_damage_user(            username=user.username, cooldown_ticks=30        )        and message.damage > 0    ):        user.health -= message.damage
        await self.network_queue.put((response, None))
        await server.global_server.update_self(user.uuid)```
The server then receives the `AcknowledgeDamage` package checks if the user can take damage and if the damage taken is greater `0` and decreases the user healt... So, we easily can turn on `god mode` by just not acknowledgeing damage (sending `0` for `message.damage`)?
Now we indeed can handle this boss 
![](https://raw.githubusercontent.com/D13David/ctf-writeups/main/cccampctf23/game_hacking/boss/images/boss.png)
and after a while the boss drops the flag.
![](https://raw.githubusercontent.com/D13David/ctf-writeups/main/cccampctf23/game_hacking/boss/images/flag.png)
Flag `ALLES!{L3ss_annoying_th3n_qualifier_Ge0rge_right?}`