CYBERSECURITY, IDENTITÄTSMANAGEMENT UND MULTI-FAKTOR-AUTHENTIFIZIERUNG

PwnAdventure3 – Reverse Engineering Binary

Lesezeit: 14 Minuten

Players usually explore, interact and understand the game through the classical user interfaces. With Pwn Adventure 3, the inputs are the mouse and keyboard where the user can move and interact with the Pwnie Island world. The output is the rendered 3D graphics and the HUD interface. However, sometime, the output is not enough for the user to fully understand exactly what he has to do in order to finish a quest. Those kind of „secrets“ are often used by developers to increase the difficulty of the game and/or to force the player to explore, try and discover new things in the game. Considering the game was released for a 2-days CTF, we don’t have much time. We want to be the first one to finish all quests. That’s where Reverse Engineering comes in handy.

Luckily, Pwn Adventure 3 client’s game logic comes with the server’s game logic. Since most of the verifications are done on the server side, reversing only the client wouldn’t have given much internal information.

In this tutorial I will assume that you already have some basic knowledge in x64 assembly and jump straight to an example: the egg finder.

Egg Finder (write-up)

The quest start when you pick up your first egg. For this, we can use our proxy developed in a previous blog post to spawn right in front of an egg:

def parse(p_out):
    
    p_in = ""

    def newspawn(opcode, x, y, z):
        return opcode + struct.pack("=HfffHHH", 0, x, y, z, 0, 0, 0)
    
    if len(p_out) == 22 and p_out[2:4] == "\x00\x00" and p_out[16:] == "\x00\x00\x00\x00\x00\x00":
        
        # First egg location
        x = 24770.0
        y = 69466.0
        z =  2727.0
        
        p_out = newspawn(p_out[:2], x, y, z)

    return (p_in, p_out)

Once picked up, the quest „Egg Finder“ start. The description is the following: „Find all of the Golden Eggs“.

Some eggs are really complicate to reach if not simply impossible. Let’s use our proxy again to fake our location and pick up all the eggs. For this, we need the location of each eggs as well as the element identifiers. All this information is listed in one of the first packet sent to the client when the user join the game (spawning packet).

Based on the provided information, we can edit the proxy as follow:

def parse(p_out):
    
    p_in = ""

    def getme(item):
        return "ee" + struct.pack("I", item)

    def loc(x, y, z):
        return "mv" + struct.pack("fffHHHBB", x, y, z, 0, 0, 0, 0, 0)

    def chat(msg):
        return "#*" + struct.pack("H", len(msg)) + msg
    
    if chat("egg1") in p_out:
        p_out += loc(-25045.0, 18085.0, 260.0)
        p_out += getme(11)
    if chat("egg2") in p_out:
        p_out += loc(-51570.0, -61215.0, 5020.0)
        p_out += getme(12)
    if chat("egg3") in p_out:
        p_out += loc(24512.0, 69682.0, 2659.0)
        p_out += getme(13)
    if chat("egg4") in p_out:
        p_out += loc(60453.0, -17409.0, 2939.0)
        p_out += getme(14)
    if chat("egg5") in p_out:
        p_out += loc(1522.0, 14966.0, 7022.0)
        p_out += getme(15)
    if chat("egg6") in p_out:
        p_out += loc(11604.0, -13130.9023438, 411.0)
        p_out += getme(16)
    if chat("egg7") in p_out:
        p_out += loc(-72667.0, -53567.0, 1645.0)
        p_out += getme(17)
    if chat("egg8") in p_out:
        p_out += loc(48404.0, 28117.0, 704.0)
        p_out += getme(18)
    if chat("egg9") in p_out:
        p_out += loc(65225.0, -5740.0, 4928.0)
        p_out += getme(19)
    if chat("BallmerPeakEgg") in p_out:
        p_out += loc(-2778.0, -11035.0, 10504.0)
        p_out += getme(20)

    return (p_in, p_out)

Once we join the game with our character, we open the chat interface and start typing „egg1“, then „egg2“, etc. Sometimes, we got disconnected. This bug has already been reported in our previous post. This is most likely due to sudden jump between different locations which crash the server. Anyway, we just have to reconnect and continue the enumeration of the eggs.

At the end of the process, I noticed that there is one egg that cannot be picked up, the „Ballmer Peak Egg“. I spawned directly at the location and I couldn’t find the egg. I tried to fake other location around the item, but nothing worked. I then decided to have a look at the binary with IDA Pro to see what might prevent me from picking up that egg.

The binary to reverse is the game logic library located at:

  • Linux: PwnAdventure3/client/PwnAdventure3_Data/PwnAdventure3/PwnAdventure3/Binaries/Linux/libGameLogic.so
  • macOS: /Applications/Pwn\ Adventure\ 3.app/Contents/PwnAdventure3/PwnAdventure3.app/Contents/MacOS/GameLogic.dylib

Note: For those who doesn’t want to spend +1000€ for a licence of IDA, you can also use the disassembler Hopper, which allows you to reverse x64 in its demo (free) version. Or you can also get Binary Ninja, a really good disassembler much cheaper than IDA (~90€), developed by those who brought you Pwn Adventure 3, Vector35

Once loaded in IDA, in the „Functions window„, I „Modify filters…“ and searched for „BallmerPeakEgg“ and found the following functions:

The function BallmerPeakEgg::CanUse() looks particularly interesting as this function returns whether or not the user (an object IPlayer sent in argument) is allow to use it (or pick up). Let’s have a closer look at the function.

Here, we want to find what conditions should be met in order to have the function returning True (1). Usually, the returned value is stored in the EAX register. If we start from the end, we can see that var_1 is stored in AL, then AL is AND’ed with 0x01, and finally, AL is copied in EAX. Therefore, we need to find when var_1 is set to 0x01 (True).

This happen in the block loc_20C3C1. This block is executed if CALL qword ptr [rcx+0D0h] returns 0x01. The problem is that we don’t know exactly to what function is pointing qword ptr [rcx+0D0h]. For this, we can attach GDB on our local server and set a breakpoint to the CALL qword ptr [rcx+0D0h].

Note: This is possible only if you can run a testing server instance locally. If not, then you will have to reverse the Class IPlayer.

In our server, we execut the command „ps a“ to find the PID for ./PwnAdventure3Server. We then switch user to root, and attach gdb to the appropriate PID:

$ ps a
  PID  TTY    STAT TIME  COMMAND
  2334 pts/18 Sl+   0:00 ./MasterServer
  2410 pts/17 Sl+  11:08 ./PwnAdventure3Server
  2414 pts/17 Sl+  10:56 ./PwnAdventure3Server
  2416 pts/17 Sl+  12:35 ./PwnAdventure3Server
  3232 pts/1  Ss    0:00 bash
  3603 pts/1  R+    0:00 ps a
$ sudo su
# gdb -p 2410
(gdb) break BallmerPeakEgg::CanUse
(gdb) continue

Once we join the game again and type „BallmerPeakEgg“ so the proxy can change our location and attempt to pick up the egg, GDB pauses at the break point. We can see at which address the CALL qword ptr [rcx+0D0h] is located, set a breakpoint and continue.

(gdb) set disassembly-flavor intel
(gdb) x/20i $rip
=> 0x7f3c6ff26374: mov rdi,QWORD PTR [rbp-0x18]
   0x7f3c6ff26378: mov QWORD PTR [rbp-0x20],rdi
   0x7f3c6ff2637c: mov rdi,rsi
   0x7f3c6ff2637f: mov rsi,QWORD PTR [rbp-0x20]
   0x7f3c6ff26383: call 0x7f3c6fe3e9a0 <_ZN10ItemPickup6CanUseEP7IPlayer@plt>
   0x7f3c6ff26388: test al,0x1
   0x7f3c6ff2638a: jne 0x7f3c6ff26399 <BallmerPeakEgg::CanUse(IPlayer*)+57>
   0x7f3c6ff26390: mov BYTE PTR [rbp-0x1],0x0
   0x7f3c6ff26394: jmp 0x7f3c6ff263c5 <BallmerPeakEgg::CanUse(IPlayer*)+101>
   0x7f3c6ff26399: lea rsi,[rip+0x21049] # 0x7f3c6ff473e9
   0x7f3c6ff263a0: mov rax,QWORD PTR [rbp-0x18]
   0x7f3c6ff263a4: mov rcx,QWORD PTR [rax]
   0x7f3c6ff263a7: mov rdi,rax
   0x7f3c6ff263aa: call QWORD PTR [rcx+0xd0] <=======
   0x7f3c6ff263b0: test al,0x1
   0x7f3c6ff263b2: jne 0x7f3c6ff263c1 <BallmerPeakEgg::CanUse(IPlayer*)+97>
   0x7f3c6ff263b8: mov BYTE PTR [rbp-0x1],0x0
   0x7f3c6ff263bc: jmp 0x7f3c6ff263c5 <BallmerPeakEgg::CanUse(IPlayer*)+101>
   0x7f3c6ff263c1: mov BYTE PTR [rbp-0x1],0x1
   0x7f3c6ff263c5: mov al,BYTE PTR [rbp-0x1]
(gdb) break *0x7f3c6ff263aa
(gdb) info breakpoint
1 breakpoint keep y 0x00007f3c6ff26374
2 breakpoint keep y 0x00007f3c6ff263aa
(gdb) del 1
(gdb) continue
(gdb) step
Player::HasPickedUp (this=0x72ed9e0, name=0x7f3c6ff473e9 "BallmerPeakSecret") at Player.cpp:864
(gdb) finish
Value returned is $1 = false

Once at the second break point, we step into so we can find the address and the symbolic name where [rcx+0D0h] is pointing to, i.e. Player::HasPickedUp(), with the third argument „BallmerPeakSecret“.

Based on the name of the function and the arguments, we can guess that at some point, our player has to pick up the item „BallmerPeakSecret“ in order to pick up the Ballmer Peak Egg.

Let see where else the string „BallmerPeakSecret“ is used. In IDA (same for Hopper), we need to select the string and press X in order to find all the cross-references.

The function BallmerPeakPoster::Damage() is also using the string „BallmerPeakSecret“. Let’s have a look at it. The string is used in the block loc_1B722. I highlighted in green all the block through which the execution flow should go in order reach the winning block (loc_1B722).

Entry block –True–> loc_1BF64E –True–> loc_1BF668 –True–> loc_1BF67B -> loc_1BF6B5 -> loc_1BF6CD –False–> loc_1BF722

We are going to move block by block and find all the required conditions to meet in order to reach loc_1BF22. Here again, let’s attach GDB on the PwnAdventure3Server process and set a breapoint on BallmerPeakPoster::Damage.

(gdb) break BallmerPeakPoster::Damage
(gdb) continue

Now we need to trigger this function. Based on the name, we can assume that this function should trigger when we try to damage the Ballmer Peak Poster. According the spawning packet, the Ballmer Peak Poster is located at (-6101.0, -10956.0, 10636.0).

We can use the proxy to spawn nearby, and start shooting at it with our Great Balls of Fire.

def parse(p_out):
    
    p_in = ""

    def newspawn(opcode, x, y, z):
        return opcode + struct.pack("=HfffHHH", 0, x, y, z, 0, 0, 0)
    
    if len(p_out) == 22 and p_out[2:4] == "\x00\x00" and p_out[16:] == "\x00\x00\x00\x00\x00\x00":
        
        # Ballmer Peak location
        x =  -6791.0
        y = -11655.0
        z =  10528.0
        
        p_out = newspawn(p_out[:2], x, y, z)

    return (p_in, p_out)

Once the projectile hit the poster, gdb paused at the break point.

In this first entry block, instigator should be different than 0 if we want to move to the next green block. Instigator is set with the register RSI, so we just need check if $rsi == 0.

(gdb) x/x $rsi
0x5e0fcb0: 0x230c2980

RSI is indeed not equal to 0, so we move to the right block (loc_1BF64E). Here, I assume that any time we trigger this function by shooting at the poster, the instigator will always be different than 0 since we are the one who trigger that function, therefore, there is an instigator.

In this next block, we have to make sure that CALL word ptr [rcx+20h] returns 1. But where is [rcx+20h] pointing to?

(gdb) set disassembly-flavor intel
(gdb) x/30i $rip
[...]
   0x7f8622dcd64e: mov rax,QWORD PTR [rbp-0x10]
   0x7f8622dcd652: mov rcx,QWORD PTR [rax]
   0x7f8622dcd655: mov rdi,rax
   0x7f8622dcd658: call QWORD PTR [rcx+0x20]
   0x7f8622dcd65b: test al,0x1
   0x7f8622dcd65d: jne 0x7f8622dcd668
   0x7f8622dcd663: jmp 0x7f8622dcd741
   0x7f8622dcd668: cmp QWORD PTR [rbp-0x18],0x0
[...]
(gdb) break *0x7f8622dcd658
(gdb) break *0x7f8622dcd668
(gdb) continue
(gdb) step
Player::IsPlayer (this=0x5e0fcb0)
(gdb) finish
Value returned is $1 = true
(gdb) continue

CALL word ptr [rcx+20h] is actually a call to Player::IsPlayer(). Since the function is triggered by us, the instigator (sent as argument with the register RAX) is indeed a player. We then move to the next block (loc_1BF668).

Here, the variable item sent as argument to the function BallmerPeakPoster::Damage is compare with 0. I assume that the item is either the projectile or the weapon/spell used.

(gdb) x/xg $rbp-0x18
0x7fffd46c5988: 0x0000000005bc8290

The item is not equal to 0, we move to the next block (loc_1BF67B). So far we met all the condition by default.

In order to move to the next final green block (loc_1BF722), AL should be different than 1 (AL == 0). Starting from the end, AL is set with var_59. var_59 is the return value of CALL __ZStneIcSt11char_traitsIcESaIcEEbRKSbIT_T0_T1_EPKS3_. Once de-mangled, this name give the following name:

_bool std::operator!=<char, std::char_traits<char>, std::allocator<char> >(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, char const*)

std::operator compares the contents of a string with another string or a null-terminated array of CharT. The argument sent to this function (basically the two compared string) are sent with the registers RSI and RDI. RDI contains the string „CowboyCoder“, while RDI is set with [rbp+__lhs].

Note that here we have an inequality comparison (!=). Meaning that if the string are different, the function will return True, while if the strings are the same, the function will return False.

Let’s break at the comparison call an check the value of RSI:

(gdb) x/30i $rip
[...]
 0x7f8622dcd6b5: lea rdi,[rbp-0x28]
 0x7f8622dcd6b9: lea rsi,[rip+0x6a6b6] # 0x7f8622e37d76
 0x7f8622dcd6c0: call 0x7f8622d1afb0 <_ZStneIcSt11char_traitsIcESaIcEEbRKSbIT_T0_T1_EPKS3_@plt>
 0x7f8622dcd6c5: mov BYTE PTR [rbp-0x59],al
 0x7f8622dcd6c8: jmp 0x7f8622dcd6cd
[...]
(gdb) break *0x7f8622dcd6c0
(gdb) continue
(gdb) x/s $rsi
0x7f8622e37d76: "CowboyCoder"
(gdb) x/xg $rdi
0x7fffd46c5978: 0x0000000005db9d38
(gdb) x/s 0x0000000005db9d38
0x5db9d38: "GreatBallsOfFire"

GreatBallsOfFire was the weapon I used to shoot at the poster. If I trace where this string is coming from, I noticed that the CALL RCX, is actually a CALL <WEAPON>::GetName(). For instance, when using the Great Balls of Fire, I call to the function GreatBallsOfFire::GetName().

So in order to reach that last green block (loc_1BF722) that is using the string „BallmerPeakSecret“, I should shoot at the poster with a „CowboyCoder“. If you finished the Unbearable revenge quest, you should have enough money to buy a Cowboy Coder to Major Payne in Town.

Since the analysis thus far might have taken a few time, your client will most likely disconnected. What I would recommend now is to restart both PwnAdventure3Server and the MasterServer, then attach gdb on the new process and set new break point at BallmerPeakPoster::Damage. You can now login back with the client.

Once the Cowboy Coder purchased (or looted), go back to the poster and fire! Now let’s analyse this last block.

Let’s set a break point at the CALL word ptr [rcx+250h] instruction and see where [rcx+250h] is pointing to.

(gdb) x/30i $rip
[...]
0x7f8622dcd722: lea rsi,[rip+0x6dcc0] # 0x7f8622e3b3e9
 0x7f8622dcd729: mov rax,QWORD PTR [rbp-0x10]
 0x7f8622dcd72d: mov QWORD PTR [rbp-0x48],rax
 0x7f8622dcd731: mov rax,QWORD PTR [rbp-0x48]
 0x7f8622dcd735: mov rcx,QWORD PTR [rax]
 0x7f8622dcd738: mov rdi,rax
 0x7f8622dcd73b: call QWORD PTR [rcx+0x250]
 0x7f8622dcd741: add rsp,0x60
 0x7f8622dcd745: pop rbp
 0x7f8622dcd746: ret 
[...]
(gdb) break *0x7f8622dcd73b
(gdb) continue
(gdb) step
Player::MarkAsPickedUp (this=0x619e010, name=0x7f8622e3b3e9 "BallmerPeakSecret")

This is exactly what we needed, i.e. putting „BallmerPeakSecret“ as picked up. Now if you go outside on the balcony in Ballmer Peak, we should see the final Ballmer Peak Egg. Or we can simply type „BallmerPeakEgg“, and the proxy will take care of picking the final egg and you should receive from the server the flag.

Flag: „The fortress of Anorak is all yours“

Fire and Ice (write-up)

Another quest that required some reverse engineering is the Fire and Ice quest. Here is the description: „Kill Magmarok“. Quite straight forward. In order to initiate the quest, you simply need to enter the Lava Cave. Once again, you can use our dedicated proxy:

def parse(p_out):
    
    p_in = ""

    def newspawn(opcode, x, y, z):
        return opcode + struct.pack("=HfffHHH", 0, x, y, z, 0, 0, 0)
    
    if len(p_out) == 22 and p_out[2:4] == "\x00\x00" and p_out[16:] == "\x00\x00\x00\x00\x00\x00":
        
        # Lava Cave location
        x = 50876.0
        y = -5243.0
        z =  1645.0
        
        p_out = newspawn(p_out[:2], x, y, z)

    return (p_in, p_out)

You will spawn right in front of Magmarok, a fire monster. Your goal now is to kill him. While trying to, you will notice that once he has reached around half his life bar, Magmarok will start a spell (that last around 4 seconds) that will regenerate completely his life. You should also notice that ice spells seem to hurt him more than bullet and Static Link. Finally, another interesting point is that the Great Balls of Fire seem to generate life instead of hurting him.

After using multiple weapon, even with multiple users, it seems not possible to kill that beast. Magmarok will eventually always get back to full health bar.

Here again, we will need to look into the binary in order to understand the logic behind Magmarok health. Using IDA, in the „Functions window„, I „Modify filters…“ and searched for „Magmarok::“ and found the following functions:

Here, I’m particularly interested in Magmarok::Damage.

void __cdecl Magmarok::Damage(Magmarok *this, IActor *instigator, IItem *item, int32_t dmg, DamageType type)

The function seem quite small. Below is my translation in pseudo-code. I provided the addresses to the related opcode so you can follow the instruction in the binary and understand the purpose:

FIRE = 1
ICE = 2

if (weaponType == FIRE) # 0x13AF4F
{
    healthFactor = magmarok.currentHealth / 10000   # 0x13AF87 
    healthMultiplier = healthFactor ^ 3             # 0x13AFA2
    intendedHealing = healthMultiplier * weaponDmg  # 0x13AFC0
    intendedHealing = intendedHealing * 4           # 0x13AFC4
    
    maximumHealing = 10000 - magmarok.currentHealth # 0x13AFD3
 
    if (intendedHealing > maximumHealing) # 0x13AFDC
    {
        intendedHealing = maximumHealing  # 0x13AFE8
    }

    weaponDmg = 0 - intendedHealing # 0x13AFF3
}
else 
{
    if (weaponType != ICE) # 0x13AFFB
    {
        # If not ice nor fire 
        weaponDmg = weaponDmg / 2 # 0x13B019
    }
}


if (magmarok.something == 1) # 0x13B027
{
    if (weaponDmg <= 0)      # 0x13B034
    {
        if (magmarok.something == 1)    # 0x13B060
        {
            if (weaponType != ICE)      # 0x13B06D
            {
                weaponDmg = weaponDmg * 4 # 0x13B07D
            }
        }
    }
    else
    {
        weaponDmg = weaponDmg / 2 # 0x13B052
    }
}


damage(magmarok, finalDmg, weaponType) # 0x13B09E

What we can see here is that fire items (spell/weapon) will give negative damages (which means it will give more health to Magmarok), ice items damages give normal damage and the other type of damages will be divided by two.

The fact that we can heal Magmarok when using fire spell is interesting as this could lead to an integer overflow:

If the variable has a signed integer type, a program may make the assumption that a variable always contains a positive value. An integer overflow can cause the value to wrap and become negative, which violates the program’s assumption and may lead to unexpected behavior.

However, this does not seem to be possible due to the following if statement:

maximumHealing = 10000 - magmarok.currentHealth

if (intendedHealing > maximumHealing)
{ 
    intendedHealing = maximumHealing
}

It means we cannot heal Magmarok more than its Maximum health, i.e. 10000HP. We need to find another way.

We noticed when attacking Magmarok that, once it reach about half his health bar, Magmarok start a spell that regenerates its full health back.  Let’s have a look at the function responsible for that healing process. For this, I used IDA and searched for the string „heal“ (Alt + T).

It finds 239 occurrences of „heal“. I then filter the finding to list only the function that contains „magmarok“. I end up with 3 functions:

  • Magmarok::Damage
  • Magmarok::Tick
  • Magmarok::GetMaxHealth

We already reversed the Magmarok::Damage and the function Magmarok::GetMaxHealth simply return the hard coded value 0x2710. So let’s focus on Magmarok::Tick.

Magmarok::Tick function is a „real time“ function that is triggered regularly in order to update the state and action of Magmarok. Below is my pseudo-code translation:

[...]

if (magmarok.healing == False) # 0x13B122
{
    if (magmarok.health > 0)   # 0x13B32F
    {
        if (magmarok.health < 5000) # 0x13B343
        {
            magmarok.healing = True # 0x13B357
            updateState(magmarok, "Healing") # 0x13B3CD
        }
    }
}
else
{
    if (magmarok.health > 0) # 0x13B166
    {
        newHealth = magmarok.health + 4975    # 0x13B182
        sendHealthUpdate(magmarok, newHealth) # 0x13B1B5
        triggerEvent(magmarok, "Heal")        # 0x13B227
        triggerEvent(magmarok, "Healing")     # 0x13B2A9
    }
}

[...]

Here we can clearly see that when Magmarok’s health drops below 5000, the monster switch to „healing“ mode, which at the next tick, will grant him 4975 health point (HP).

This might not seem obvious, but we could to use this healing regeneration to exploit two integer overflows in order to kill Magmarok. Earlier, we mentioned that we couldn’t overflow Magmarok’s health because of the following statement:

maximumHealing = 10000 - magmarok.currentHealth # 0x13AFD3

if (intendedHealing > maximumHealing) # 0x13AFDC
{ 
    intendedHealing = maximumHealing  # 0x13AFE8
}

However, now we know a way to have Magmarok’s health higher than 10000HP. Why is that so interesting to have Magmarok’s health higher then 10000HP? Because the if comparison @0x13AFDC is actually a JBE, which compares unsigned values. So if we manage to have Magmarok’s health at 10001 for instance, the subtraction @0x13AFD3 will result with a -1, which in binary is represented as 64 time 1’s.

0000000000000000000000000000000000000000000000000000000000000010 = 2
0000000000000000000000000000000000000000000000000000000000000001 = 1
0000000000000000000000000000000000000000000000000000000000000000 = 0
1111111111111111111111111111111111111111111111111111111111111111 = -1
1111111111111111111111111111111111111111111111111111111111111110 = -2

Since JBE compares unsigned values, maximumHealing (= -1) is not seen at a negative value, but as a very big positive value: pow(2, 64) – 1. This means as soon as we have magmarok.health > 10000, we will be able to generate more health thanks to fire spells for ever.

We know that whenever Magmarok’s health drop below 5000HP, it will change its „healing“ state to true, and at the next „tick“, it will gain 4975HP. Unfortunately, 5000 + 4975 is lower than 10000. However, the delta time between two ticks are long enough (around 4s) for us to shoot multiple times Magmarok with Great Balls of Fire to get its health above 5025HP so that at the next tick, its health goes above 10000HP.

Since we don’t have much time between two ticks, it is important to be quite accurate with Magmarok’s health. If we drop too much below 5000HP, we won’t have enough time to get above 5025HP.

In order to ease the process, we could add in our proxy a debug message that tells us what is Magmarok’s current health status:

def parse(p_out):
 
    p_in = ""

    if "++" in p_out:
        posi = p_out.index("++")
        actor,health = struct.unpack("ii", p_out[posi+2 : posi+10])
        print "HEALTH: " + str(actor) + " - " + str(health) + "hp"

    return (p_out, p_in)

Once we have Magmarok’s health above 10000HP, we just need to keep shooting with the Great Balls of Fire until we finally overflow his health point to a negative number and kill him.

Flag: Some bosses just roll over and die

Conclusion

Reverse engineering binary allows you to see what exactly is executed by the computer and  thus understanding the logic behind. Sometimes, the binary can be obfuscated, which makes the task for the analyst more complicate, but the entire logic will eventually be disclosed by the binary (since anyway, the computer has to executed it at some point).

Here, the reversed functions were actually functions executed by the server. If the server logic was not embedded in the client logic binary, we wouldn’t be able to see the code behind the Ballmer Peak Egg and Magmarok. I guess we would have eventually shoot the Ballmer Peak Poster with a Cowboy Coder, but that would have been out a luck and most likely after lots of time spent on the game. Here the reverse engineering of the binary allows us to avoid wasting our time with guessing but doing exactly what the game is expecting us to do in order to finish the quest. The reverse exercise also allowed us to highlight some glitch (integer overflow) that allowed us to finish the quest designed as „impossible“ otherwise.

Resources

Final version of our proxy: https://github.com/Foxmole/PwnAdventure3/blob/master/pwn3proxy.py

IDA Pro: https://www.hex-rays.com/products/ida/

Binary Ninja: https://binary.ninja

Hopper: https://www.hopperapp.com


Blog series: IntroductionReverse Engineering Network ProtocolPwn Adventure 3 Network ProtocolBuilding a Wireshark ParserAsynchronous Proxy in PythonIntercepting Packets – Reverse Engineering BinaryPatching BinaryHooking shared library

Von |2017-07-11T12:00:29+00:0011. Juli, 2017 um 12:00 Uhr|KEYIDENTITY|Noch keine Kommentare

Über den Autor:

Manuela Kohlhas