Yet another PwnAdventure3 Writeup

Server Setup

Basically follow this video/this post.

Here what I did:

Install Debian 20.04 Server in VirtualBox 2Gb Ram 10Gb Drive (Edit: which is not enough) Setup the Network correctly, I’m using a host only adapter

If you want to use docker, don’t install the snap docker from the server installer.
It conflicts with docker-compose or something like that.

sudo apt update
sudo apt upgrade

sudo apt install
sudo apt install docker-compose

git clone
cd PwnAdventure3
tar -xvf pwn3.tar.gz

docker-compose build
docker-compose up

If you want to change the keylayout, use loadkeys en (with your language code); resets after every reboot.

I couldn’t even unzip the pwn3.tar.gz. So I upgraded the hard drive to 50 gigs. In VirtualBox: File > Virtual Media Manager… > PwnAdventureServer.vdi > Properties You would need to resize the partition too. You can do that with Gparted/pghidra arted.

Note: The docker-compose build step took a time for me and was stuck at Building Init for like 5-10min. Just wait. It’ll do something (hopefully).

Client Setup

On Windows Insert into /etc/hosts (on Windows %WINDIR%/System32/drivers/etc/hosts) master.pwn3 game.pwn3

Change the server.ini in PwnAdventure3\PwnAdventure3_Data\PwnAdventure3\PwnAdventure3\Content\Server



Launch the game, create a account and you should be good to go.
The db is not persistent. So just pause the vm.


I’m using Ghidra for static analysis, Cheat Engine for dynamic analysis and ReClass.NET for getting structs.

Let’s do stuff

Okay you start in this cave and have to escape. There is a bush blocking the way out and you can find a fireball in the same cave.
Collect the spell and fire it. It reduces mana! So let’s look for that value.


Attach Cheat Engine, look for 100, shoot a fireball and look for a decreased value. I’d recommend setting hotkeys for decreased & increased values.
After a few times I found 4 addresses all containing the same value.

I found no connection to the player though. Let’s look at something different instead.

I fucked up here and ignored this, but you can find the player here too: One address has this write:

542C45E6 - 89 82 2C010000  - mov [edx+0000012C],eax <<


And EDX is our player

Current Item

Look at current selected item in toolbar.
Search for Unknown Number, Decreased, Increased etc.
Found the value (it’s indexed, starts at 0). Look at writes.
Found one in GameLogic.dll:

56613E8D - 8B 8E B8010000  - mov ecx,[esi+000001B8]
56613E93 - 89 96 80010000  - mov [esi+00000180],edx <<

esi => Player?
ReClass.NET shows RTTI:

<DATA>GameLogic.dll.56658440 Player : Actor : IActor : IPlayer

Player Class

Let’s look at the class then. Here a few things I found:

  • 0x79: Player Name
  • 0x90: Team Name
  • 0xF4: Level Name (Contains ‘LostCave’)
  • 0x12C: Our mana (it’s even the address we found before)
  • 0x180: currentItem

If we have a look at the vtable in Ghidra and look where it’s referenced:

void __thiscall InitPlayer(void *this,undefined param_1)
  FUN_00002560(local_30,(int **)"Player",(int *)&IMAGE_DOS_HEADER_00000000.e_crlc);
  local_8 = 0;
  if (0xf < local_1c) {
  *(undefined4 *)((int)this + 0x70) = 0x6f438;
  *(undefined4 *)this = 0x78440;
  *(undefined4 *)((int)this + 0x70) = 0x78334;
  *(undefined4 *)((int)this + 0x8c) = 0xf;
  *(undefined4 *)((int)this + 0x88) = 0;
  *(undefined *)((int)this + 0x78) = 0;

If we have a look where that is referenced, we can see 4 functions and before each call 01xdc get’s alloced.

  this = operator_new(0x1dc);
  local_8 = 0;
  if (this == (void *)0x0) {
    local_34 = (int *)0x0;
  else {
    local_34 = (int *)InitPlayer(this,0);

Now we have the Player Class size.

Out of the cave

Let’s go out of the cave to get some more items and info.

We get into the severs and see a few rats. After being attacked, we can see that our health at player+0x30 changes.

I also just noticed that all of the strings are probably std::string’s or some other implementation. We can see that after the char[16], we have a size value and after that a capacity value.

struct String
    char[16] val;
    unsigned int size;
    unsigned int capacity;

Or something like that.


Out of the severs are a few bears that drop Items, namely Pistol, Shotgun and Rifle Ammunition. After a few item count searches I found the address. If we look at the writes it leads us back to the 2nd Player vtable at [10].

After some digging around Player + 0xBC is probably the inventory and 0xC0 the itemCount. I also noticed that Player + 0x158 is a array with all the hotbar items.

The inventory is structured weird. It took some time and graph drawing but it’s probably a tree.

struct InventorySlot
    InventorySlot* leftChild;
    InventorySlot* parent;
    InventorySlot* rightChild;
    int val;
    Item* item;
    Item* item2;
    int count;

Here is a script to print out the inventory as a tree. Player address needs to be given.

function toHex(int)
         return string.format("%02X", int)

player = 0x3D59B050
invOffset = 0xBC
inventory = readPointer(player + invOffset)
print("Inventory: " .. toHex(inventory))
print("ItemCount: " .. readInteger(player + invOffset + 4))

firstChild = readPointer(inventory + 0x4)

function printNode(cur, i)
         local left = readPointer(cur)
         --parent = readPointer(cur + 0x4) -- We shouldn't need to print this, as we probs come from there
         local right = readPointer(cur + 0x8)
         if left and left ~= inventory then
            printNode(left, i + 1)

         val = toHex(readInteger(cur + 0xC))
         count = readInteger(cur + 0x18)
         countStr = ""
         if count then
            count = ", Count: " .. count
         print(string.rep("-",i) .. "Item [" .. toHex(cur) .. "]: Val: " .. val .. count)

         if right and right ~= inventory then
            printNode(right, i + 1)

printNode(firstChild, 0)


I got some bear quest while trying to change my money and I found a pointer at Player + 0x18C.
RTTI shows:

<DATA>GameLogic.dll.54308650 Quest : IQuest ...

With ReClass I found:

  • 0x4 (String): questID (probably some internal quest string)
  • 0x1C (char*): questName
  • 0x3C: QuestState* (Name showed in RTTI)


  • 0x8 (String): stateName (‘Initial’)
  • 0x20 (char*): infoText (shows what to do ‘Solve the puzzles in Fort Blox’)


Right under the Quest pointer are 2 floats with the values 200.f and 420.f.
Changing the first made me fast as fuck boi. Probably the max speed.
I thought the second one was the acceleration, but it’s the jump height.

With these I should be able to find the position. They are accessed through the vtable, but the call stems from PwnAdventure3.exe. Uhh I didn’t analyze that yet.

While that is analyzing, let’s try finding a static pointer to our player.

Achieving Persistence

The InitPlayer function we found at the beginning is called in InitLocal, which is apparently a export. A parameter is an ILocalPlayer pointer and apparently Player + 0x1B8 gets set to that pointer.

Inside the function DAT_97d7c gets initialized too and the vtable is set to the LocalWorld vtable.
If we have a look in ReClass, we see that it’s indeed a World, but not the LocalWorld, but ClientWorld.
If we have a look at the cross references to the ClientWorld vtable, we see the Export InitClient. In there DAT_97e48 is used:

  if (DAT_97e48 == 0) {
    iVar2 = 0;
  else {
    iVar2 = DAT_97e48 + -0x70;

-0x70? Isn’t that our IPlayer vtable offset? (The 2nd vtable inside our Player Class).

A look in ReClass shows that this is indeed a pointer to our IPlayer object.

Part 1?

This is probably enough for one post…