UIUCTF 2022 - Easy Math 1-3

Categories: other
by sera

Easy Math 1-3 (pwn?)

easy math 1

Take a break from exploiting binaries, and solve a few* simple math problems!

$ ssh ctf@easy-math.chal.uiuc.tf password is ctf

author: kuilin

easy math 2

The flag file for easy math 1 has information about how to solve this second part.

author: kuilin

easy math 3

Easy Math 2 had an unintended solve, which has been patched here. If your solution did not use that unintended solve, it should work here as-is:

ssh ctf-part-3@easy-math.chal.uiuc.tf

The password is 6d49a6fb (not ctf)

author: kuilin

This series of 3 challenges requires faking terminal input.


Upon sshing into the server for the first challenge, we are greeted with a restricted shell. The current directory contains a README, an easy-math binary and C source file, and unreadable flag. The easy-math binary is SUID (meaning it runs as root) so we can assume we somehow need to get the binary to read the flag for us.

The README file has the following contents:

Take a break from exploiting binaries, and solve a few* simple math problems!

Note: You can use /tmp to store files. We installed a bunch of tools for your hacking convenience:

    sudo apt-get install vim nano gdb git python3-dev gcc build-essential

Have fun!

Some tools have been installed so we can build/run exploits on the server. If we check out the source of easy-math, we see that the binary will read the flag for us if we solve 10000 math problems of the form a * b = ? within 3 hours. Normally, we could just use python to communicate with the easy-math program and easily parse/solve the problems.

The catch lies in this function:

int check_id() {
  printf("Checking your student ID...\n\n");
  struct stat real, given;
  if (stat("/proc/1/fd/0", &real)) return 1;
  if (fstat(0, &given)) return 1;
  if (real.st_dev != given.st_dev) return 1;
  if (real.st_ino != given.st_ino) return 1;
  return 0;

This function compares the output of stat (file information, including unique inode number) from two sources - 0, the standard input file descriptor, and /proc/1/fd/0. the standard input of process with id 1. If we check the output of ps, the challenge has been set up so our bash shell always has this process ID.

In essence, this comparison ensures that /proc/1/fd/0 and the program’s standard input are the same file. If we try to simply spawn easy-math as a subprocess, this check will return 1, and we won’t be able to start the test and get the flag.

Solution for first part

The intended solution for this first part is to solve the math problems outside the ssh session on our own computers. We can use pwntools to set up a ssh connection to the server with a tty and send/receive data.

Unfortunately, my internet is too slow to solve the challenge this way since the ssh connection times out after 10 minutes, so we will have to find another way.

Solving without output on remote

To make easy-math’s standard input the same as bash, we have to set its standard input as the python/C program’s standard input also, but then we don’t have the normal write end of this file descriptor. We can pass our standard input through in python easily enough, and verify that passes the check_id function:

import subprocess
proc = subprocess.Popen(
# Reading from this shows a pass

Trying to write to standard input

My first idea was to just try to write to standard input. Although this doesn’t seem like it would make sense, file descriptors can be bidirectional, and if we try to write to standard input anyway we observe an interesting result:

import os
os.write(0, b"hello world")

After sending the write command, the string is actually printed to our terminal, but unfortunately not passed back as input again. This turns out to be a dead end, although it’s kind of neat and/or upsetting that it works at all.


Running a stat command on stdin ourselves shows that the standard input is a symlink to a file in /dev/pts:

ctf@test-center:~$ stat /proc/1/fd/0                                                          
  File: /proc/1/fd/0 -> /dev/pts/0
  Size: 64        	Blocks: 0          IO Block: 1024   symbolic link
Device: 6000a7h/6291623d	Inode: 56178257    Links: 1
Access: (0700/lrwx------)  Uid: ( 1000/     ctf)   Gid: ( 1000/     ctf)
Access: 2022-08-07 22:04:28.866734424 +0000
Modify: 2022-08-07 22:04:28.866734424 +0000
Change: 2022-08-07 22:04:28.866734424 +0000
 Birth: -

Running man pts tells us that the files in /dev/pts are pseudoterminal slave devices, and programs like ssh use a pseudoterminal to receive input. However, we do not have control over the master for this connection, but we can learn more about pseudoterminals with man pty as recommended in the SEE ALSO section.

This man page tells us the slave end is exactly like a real terminal, and we can use most tty ioctls to control this device. man ioctl_tty gives us a listing of commands we can use, and one is particularly suited for our purpose:

   Faking input
       TIOCSTI   const char *argp
              Insert the given byte in the input queue.

The TIOCSTI ioctl takes a single char of input and adds it to the input of the terminal. Perfect!

Python provides access to the ioctl syscall in the fnctl package, and the TIOCSTI constant is defined in the termios package. Knowing this, we can make a python script that solves the problems and prints the flag.

Solving the first challenge with the method above gives instructions for the second challenge - which has the same program but just hides shell output until we exit, so we can use the exact same solution. There is a cheese for this part - if you write to standard input with python like shown earlier, you can smuggle the output of the easy-math binary over the SSH connection and then proceed with a similar solution to the first part.

The third challenge is similarly identical but doesn’t show any output until done, including input echo. This doesn’t affect our solution and we can use it to obtain our third flag.


The solution can be ran in the shell using something similar to the following:

cat >/tmp/a <<EOF
(paste below script)

python3 /tmp/a
import subprocess
import os
import time
import fcntl 
import termios

sor, sow = os.pipe()
process = subprocess.Popen(
log = b""
cnt = 0
while cnt < 10000:
    log += os.read(sor, 1)
    if log[-1] == ord("="):
        os.read(sor, 1)
        res = eval(log.split(b": ")[-1].split(b"=")[0].decode())
        for c in str(res) + "\n":
            fcntl.ioctl(0, termios.TIOCSTI, c)
        log = b""
        cnt += 1
        if cnt == 10000:
            while True:
                    log += os.read(sor, 1)
                    print(bytes([log[-1]]).decode(), end="")


Part 1

Nice job! Now, the question is, did you do it the fun way, or by hiding behind your ssh client?

Part 1 flag: uiuctf{now do it the fun way :D}

To solve part 2, use `ssh ctf-part-2@easy-math.chal.uiuc.tf` (password is still ctf)
This time, your input is sent in live, but you don't get any output until after your shell exits.

Part 2:

uiuctf{excellent execution}

Part 3:

uiuctf{file descriptors are literally magic}