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: Introduction – Reverse Engineering Network Protocol – Pwn Adventure 3 Network Protocol – Building a Wireshark Parser – Asynchronous Proxy in Python – Intercepting Packets – Reverse Engineering Binary – Patching Binary – Hooking shared library