PlaidCTF 2020: Bonzi Scheme

rev (300 points, 12 solves)

Writeup for PlaidCTF 2020’s “Bonzi Scheme” task, a.k.a. How to hack hex with hyx: A totally serious guide.

Get hyx now: https://yx7.cc/code

Step 0: What to do?

At first, there is a website, so let’s check it out:

Screenshot of the website

There is no flag in this challenge? Sad, but based on this awesome and timeless webdesign, I just had to become Bonzi’s friend. So, let’s have a look at the challenge anyway.

On the website I found:

How does it work?
Give us an ACS file and your favorite number and we’ll extract an image of your friend!
If you need an example file, you can download one above.
To be extra nice, we’ll dress up your buddy with a flag in the character description - you won’t see it though!

So, I just have to make my own Bonzi and send it to the website, and he will become friends with the Bonzi there? And I even get a flag? I’m absolutely in.

Additionally there was a huge folder containing the server and client for the Bonzi app. After a quick look at the files we found this at the server files, … and … Bonz says no. Sad, but Bonzi wanted me to prove that I’m worthy. Fine, let’s ditch the given source completely and write our own parser.

Step 1: Use inferior tooling

Like (I guess) everyone who was looking at the challenge I started writing my own parser based on the specification found at http://www.lebeausoftware.org/downloadfile.aspx?ID=25001fc7-18e9-49a4-90dc-21e8ff46aa1d.

After parsing the header and character information (mainly localization info, since the character description is there) I looked how to parse the images and thought: Bonzi wouldn’t want that. And it seemed like a lot of work.

This was more or less my parser for this challenge:

with open("bonz.acs", "rb") as f:
    bonz = f.read()

offset = 0

def read(sz):
    global offset
    ret = int.from_bytes(bonz[offset:offset + sz], "little")
    offset += sz
    return ret

def read_locator():
    loc_offset = read(4)
    read(4) # size
    return loc_offset

head_sig = read(4)
char_info = read_locator()
print(f"Char info: {hex(char_info)}")
offset += 8
image_info = read_locator()
print(f"Image info: {hex(image_info)}")

offset = char_info
read(2) # minor version
read(2) # major version
local_info = read_locator()
print(f"Localized info: {hex(local_info)}")

Memory locations:

Char info: 0x500d06
Image info: 0x4fd13a
Localized info: 0x50198f

Step 2: Use superior tooling

Thanks to my parsing attempts, I knew enough about the layout of the ACS file and also the important memory locations, so I opened bonz.acs in hyx and started hacking hex in hyx.

hyx view of the original character name, description and special info

The pointer towards the localized information is located at 0x500d0a and coincidentally Image 1 is located directly underneath.

Pointer to localized information and Image 1

So I started creating my Localization “struct” where my image was located.

Forged character sheet

Now, the server will write the flag into the image which it will display. One problem remains: the image is compressed in the ACS file, but displayed without compression.

So, sadly my friend Bonzi did suffer a bit from my changes in his code. I hope we can still be friends.

Consequences of Hex-Hacking

Anyway, I found that some bytes remain unchanged.

Leaked bytes

By changing the location of the struct, I’m now able to leak 2 bytes (1 byte apart, don’t ask me). In the screenshot I’m currently leaking the a and o for along in the flag. After leaking around 23 of the flag, I asked my team if someone had a good guess on the rest, since I was kind of tired of manually leaking just 2 bytes at a time and having to solve the captcha every time, as Google did not believe me any longer that I’m a human (I guess I solved around 100 captchas today).

And we finally got the flag:


Maybe continuing with my parser would have been a lot faster, but would that make Bonzi proud?


I will never use different tooling for solving challenges. You really should try hyx.

hyx ad