Use the tree to jump between collections without leaving the reader.

archive Select writeup Open tree
HackTheBox/Challenges/Challenge Noradar.en.md READ_ONLY

Challenge NoRadar

This challenge consists of statically analyzing a game binary to reverse engineer the logic of a hidden entity and the solution consists of extracting memory coordinates using a script to generate an image that reveals the flag.

An initial examination of the game reveals a map set in space with interacting cubes. Since regular gameplay does not seem to reveal any clear solution, the underlying executable requires further inspection. The file command shows that the executable is an unstripped 64-bit ELF binary, meaning the original function names are intact, which significantly eases the analysis process.

$ file noradar
noradar: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6feab769773bca287ae8231e8fb0d9920d6529b5, for GNU/Linux 3.2.0, not stripped

Dumping the symbols with nm uncovers the main game loops and other subsystems. It highlights the existence of three distinct types of cubes. Given that only two types are visibly active during gameplay, it is highly likely that the third type contains a special or hidden behavior. Interesting variables such as waypoint_index are also identified in the binary.

$ nm noradar
00000000000024a0 T animated_load
0000000000002480 T animated_reset
00000000000023c0 T animated_update
...
0000000000001e70 T green_cube1_new
00000000000019d0 T green_cube1_update
0000000000001f50 T green_cube2_new
0000000000002010 T green_cube3_new
0000000000001ab0 T green_cube3_update
...
0000000000008684 b waypoint_index.100
0000000000004050 T window_close
0000000000004080 T window_init

A deeper static analysis using Cutter allows for examining the decompiled source code. The load_assets function handles the parsing of the assets.dmp resource file.

bool load_assets(int64_t arg1)
{
    int32_t iVar1;
    FILE *stream;
    int64_t iVar2;
    uint32_t uVar3;
    uint64_t uVar4;
    bool bVar5;
    int64_t var_2ch;

    bVar5 = false;
    stream = (FILE *)fopen(arg1, data.00005004);
    if (stream == (FILE *)0x0) {
        bVar5 = true;
        dprintf(2, "can\'t open %s\n", arg1);
    } else {
code_r0x000017f0:
        while ((iVar2 = fread(&var_2ch, 4, 1, stream), iVar2 != 0 && (bVar5 == false))) {
            if ((uint32_t)var_2ch == 3) {
                iVar1 = load_assets_texture(0x81f0, stream);
                bVar5 = iVar1 != 0;
            } else {
                if (3 < (uint32_t)var_2ch) goto code_r0x000018b0;
                if ((uint32_t)var_2ch == 1) {
                    fread(data.00008190, 0x10, 1, stream);
                } else if ((uint32_t)var_2ch == 2) {
                    fread(game, 4, 1, stream);
                    fread(data.00008184, 4, 1, stream);
                    iVar1 = _game * _data.00008184 * 3;
                    _data.00008188 = malloc(iVar1);
                    if (_data.00008188 == 0) goto code_r0x00001937;
                    fread(_data.00008188, iVar1, 1, stream);
                }
            }
        }
        fclose(stream);
    }
    return bVar5;
code_r0x000018b0:
    if ((uint32_t)var_2ch == 4) {
        fread(data.00008258, 4, 1, stream);
        uVar3 = _data.00008258;
        _data.00008250 = malloc((uint64_t)_data.00008258 * 0x50);
        if (_data.00008250 == 0) {
code_r0x00001937:
            bVar5 = true;
        } else {
            uVar4 = 0;
            if (uVar3 != 0) {
                do {
                    iVar1 = load_assets_texture(_data.00008250 + uVar4 * 0x50, stream);
                    if (iVar1 != 0) goto code_r0x00001937;
                    uVar3 = (int32_t)uVar4 + 1;
                    uVar4 = (uint64_t)uVar3;
                } while (uVar3 < _data.00008258);
            }
        }
    }
    goto code_r0x000017f0;
}

Reviewing the green_cube3_update function exposes the logic governing this third type of cube. The routine iterates through a static list of 187 coordinates located at the memory address 0x5060.

void green_cube3_update(int64_t arg1)
{
    uint32_t uVar1;
    int64_t iVar2;
    undefined8 *puVar3;
    int64_t *piVar4;
    double dVar5;
    double dVar6;
    double dVar7;
    double dVar8;
    int64_t var_bb8h;
    int64_t var_bb0h;

    uVar1 = _waypoint_index.1;
    puVar3 = (undefined8 *)data.00005060;
    piVar4 = &var_bb8h;
    for (iVar2 = 0x176; iVar2 != 0; iVar2 = iVar2 + -1) {
        *piVar4 = *puVar3;
        puVar3 = puVar3 + 1;
        piVar4 = piVar4 + 1;
    }
    dVar6 = (double)(&var_bb0h)[(int64_t)(int32_t)_waypoint_index.1 * 2];
    if ((dVar6 == 0.0) && ((double)(&var_bb8h)[(int64_t)(int32_t)_waypoint_index.1 * 2] == 0.0)) {
        _waypoint_index.1 = _waypoint_index.1 + 1;
        _offset_x.0 = _offset_x.0 + 0xe;
        *(undefined8 *)(arg1 + 0x28) = 0;
        *(undefined8 *)(arg1 + 0x30) = 0;
        return;
    }
    dVar8 = *(double *)(arg1 + 0x28);
    dVar7 = *(double *)(arg1 + 0x30);
    dVar5 = (double)_offset_x.0 + (double)(&var_bb8h)[(int64_t)(int32_t)_waypoint_index.1 * 2];
    if ((dVar8 == 0.0) && (dVar7 == 0.0)) {
        *(double *)(arg1 + 0x28) = dVar5;
        *(double *)(arg1 + 0x30) = dVar6;
    } else {
        dVar5 = dVar5 + 0.5;
        dVar8 = (dVar5 - dVar8) * 192.0 * (double)_deltatime + dVar8;
        dVar7 = ((dVar6 + 0.5) - dVar7) * 192.0 * (double)_deltatime + dVar7;
        dVar5 = dVar5 - dVar8;
        *(double *)(arg1 + 0x28) = dVar8;
        dVar6 = (dVar6 + 0.5) - dVar7;
        *(double *)(arg1 + 0x30) = dVar7;
        dVar6 = dVar5 * dVar5 + dVar6 * dVar6;
        if (dVar6 < 0.0) {
            dVar6 = (double)sqrt();
        } else {
            dVar6 = SQRT(dVar6);
        }
        if (dVar6 < 0.1) {
            if (0xbc < uVar1) {
                _waypoint_index.1 = 0;
                _offset_x.0 = 0;
                return;
            }
            _waypoint_index.1 = uVar1 + 1;
            return;
        }
    }
    return;
}

The key to visualizing the flag lies in plotting the path of this hidden cube. However, taking a closer look at the conditions in the routine reveals a specific handling for points where both coordinates are 0.0. In such cases, the variable offset_x is incremented by 14 (0xe).

if ((dVar6 == 0.0) && ((double)(&var_bb8h)[(int64_t)(int32_t)_waypoint_index.1 * 2] == 0.0)) {
        _waypoint_index.1 = _waypoint_index.1 + 1;
        _offset_x.0 = _offset_x.0 + 0xe;

This indicates that the (0.0, 0.0) coordinates act as a separation mechanism for each character of the flag, shifting the drawing point so that the letters do not overlap. To take advantage of this spacing logic, a Python script is built to read the binary, extract the double-precision coordinate pairs starting from the 0x5060 offset, and process these points into a structured SVG image.

$ cat exploit/exploit.py
import struct

with open('gamepwn_noradar/noradar', 'rb') as f:
    f.seek(0x5060)
    data = f.read(187 * 16)

pts = [struct.unpack('dd', data[i:i+16]) for i in range(0, len(data), 16)]

with open('flag.svg', 'w') as f:
    f.write('<svg width="15000" height="300" '
        'xmlns="http://www.w3.org/2000/svg">\n')

    offset_x = 0
    path = []

    for x, y in pts:
        if x == 0.0 and y == 0.0:
            offset_x += 14
            if path:
                f.write(
                    f'  <polyline points="{" ".join(path)}" '
                    f'fill="none" stroke="black" stroke-width="4" />\n'
            )
                path = []
        else:
            path.append(f"{(x + offset_x) * 20},{y * 20}")

    if path:
        f.write(
            f'  <polyline points="{" ".join(path)}" '
            f'fill="none" stroke="black" stroke-width="4" />\n'
    )

    f.write('</svg>\n')

Running the script successfully processes the embedded coordinates and generates the vector drawing.

$ python3 exploit/exploit.py
$ ls
452?auth_user_id=453332&expires=1782314119&signature=fbe664b936b23287e5b190c4ddc5c66146b0b69ade96a83c5c7773f37967dbdb  LINK.lst  OTHERS.lst  gamepwn_noradar  exploit  exploit.feature  flag.svg  exploit.yml

Opening the resulting image clearly reveals the hidden text with the correctly applied spacing, yielding the final flag.

HTB{FLAG}