Skip to content

Level 4: Really Unfair Battleships Game (rev)

The challenge description states that it is a 'pwn/misc' challenge, but it seemed more like yet another rev challenge.

We are given a Linux .appimage and a Windows .exe. The .appimage suggests that the application is packaged somehow.

Despite this obvious clue, I decided to open the .exe file in IDA. I then painstakingly stepped through the binary and observed that it wrote an app.asar file in some temporary directory. This indicates that the app is a packaged Electron application and the app.asar contains the JavaScript and HTML code for the app.

After solving the challenge, I looked a little deeper into the .appimage format and realized that it included a --appimage-extract flag that will automatically unpack the application, revealing the app.asar file.

Anyway, after extracting the app.asar file, we can view the minified JavaScript code of the app in rubg/dist/assets/index-c08c228b.js.

After beautifying the minified code, we observe that there are some interesting functions that appear to interact with a remote HTTP server:

js
const Du = ee,
    ju = "http://rubg.chals.tisc23.ctf.sg:34567",
    Sr = Du.create({
        baseURL: ju
    });
async function Hu() {
    return (await Sr.get("/generate")).data
}
async function $u(e) {
    return (await Sr.post("/solve", e)).data
}
async function ku() {
    return (await Sr.get("/")).data
}

Opening http://rubg.chals.tisc23.ctf.sg:34567/generate in a browser, we observe a string similar to this:

json
{
    "a":[0,0,0,0,0,0,192,0,0,0,0,64,120,64,16,0,16,0,16,0,0,0,0,0,0,0,1,240,0,0,0,0],
    "b":"6016998088646410950",
    "c":"13099033471658947590",
    "d":2954592777
}

We can make a guess that a represents the location of the enemy ships, and the other properties are identifiers for this particular game.

We can also identify an initialization method, E, that calls Hu to fetch data from the /generate endpoint. E then calls the f function to generate the board, which is then stored into t.value.

js
function f(x) {
    let _ = [];
    for (let y = 0; y < x.a.length; y += 2) _.push((x.a[y] << 8) + x.a[y + 1]);
    return _
}
async function E() {
    i.value = 101;
    let x = await Hu();
    t.value = f(x), n.value = BigInt(x.b), r.value = BigInt(x.c), s.value = x.d, i.value = 1, l.value.fill(0), c.value = [], o.value = ""
}

Running the f function on the board configuration produces t.value of [0, 0, 0, 49152, 0, 64, 30784, 4096, 4096, 4096, 0, 0, 0, 496, 0, 0].

We also notice the m function is bound to the onClick event further down in the code:

js
{
    ref_for: !0,
    ref: "shipCell",
    class: on(l.value[y - 1] === 1 ? "cell hit" : "cell"),
    onClick: H => m(y - 1),
    disabled: l.value[y - 1] === 1
}

We can guess that m is the event handler for click events on the board cells.

Let's take a closer look:

js
function d(x) {
    return (t.value[Math.floor(x / 16)] >> x % 16 & 1) === 1
}
async function m(x) {
    if (d(x)) {
        if (t.value[Math.floor(x / 16)] ^= 1 << x % 16, l.value[x] = 1, new Audio(Ku).play(), c.value.push(`${n.value.toString(16).padStart(16, "0")[15 - x % 16]}${r.value.toString(16).padStart(16, "0")[Math.floor(x / 16)]}`), t.value.every(_ => _ === 0))
            if (JSON.stringify(c.value) === JSON.stringify([...c.value].sort())) {
                const _ = {
                    a: [...c.value].sort().join(""),
                    b: s.value
                };
                i.value = 101, o.value = (await $u(_)).flag, new Audio(_s).play(), i.value = 4
            } else i.value = 3, new Audio(_s).play()
    } else i.value = 2, new Audio(qu).play()
}

The function m takes an integer x, which is probably the index of the cell the player clicked. Then, the function d is called with x. This probably checks if the cell clicked contains an enemy ship. Knowing that the board is a 16x16 grid, x probably ranges from 0 to 255.

Next, we notice that the function $u make a request to the /solve endpoint, which probably will return the flag. Sent in the request body are the parameters a, which is derived from the c array and b, which is s.value. Looking back at the E function we can observe that s.value is just x.d, which is 2954592777.

The c array is generated here:

js
c.value.push(`${n.value.toString(16).padStart(16, "0")[15 - x % 16]}${r.value.toString(16).padStart(16, "0")[Math.floor(x / 16)]}`)

x is our cell index, while n and r are the constants obtained from the initial configuration:

n = 6016998088646410950 = 0x5380abe1d7f942c6
r = 13099033471658947590 = 0xb5c91d8a732ef406

Now we just need to find all x such that d(x) is true, then find the corresponding characters in n and r to find the correct c array.

python
n = "5380abe1d7f942c6"
r = "b5c91d8a732ef406"
c = []
tval = [0, 0, 0, 49152, 0, 64, 30784, 4096, 4096, 4096, 0, 0, 0, 496, 0, 0]

def d(x):
    return tval[x//16] >> (x%16) & 1 == 1

for i in range(256):
    has_ship = d(i)
    if has_ship:
        c.append(n[15-i%16]+r[i//16])
        
print("".join(sorted(c)))

Result: 0307080a1438395974787d8894a8d4f4

All that's left is to send this value to the /solve endpoint to get our flag:

python
import requests
a = "0307080a1438395974787d8894a8d4f4"
b = 2954592777


print(requests.post("http://rubg.chals.tisc23.ctf.sg:34567/solve", json={"a":a, "b":b}).text)

Flag: TISC{t4rg3t5_4cqu1r3d_fl4wl355ly_64b35477ac}