Do you want to use dark theme?

Times Up, Again

Reversing, 450 points.

Challenge

Previously you solved things fast. Now you've got to go faster. Much faster. Can you solve *this one* before time runs out?

Hints

Sometimes, scripts are just too slow. You've got to have much more control.

Target Binary

Download from picoCTF or mirror on my website.

Walkthrough

Let's run the binary and see what the challenge is

[email protected]:/problems/time-s-up--again-_0_ba1fe87bd4905d3f34eb83d66f907fe0$ ./times-up-again
Challenge: (((((1940262799) + (-373423226)) - ((-1892550604) * (2032483161))) + (((1222594655) * (-2070231363)) + ((1446494107) * (1738310615)))) + ((((792904243) - (-1646639892)) + ((665504257) - (1002007274))) + (((-235076176) - (-1517938778)) + ((-869453590) * (-1433446331)))))
Setting alarm...
Solution? Alarm clock

Ok. It seems that it's pretty much the same as the last one. The hint says a script is too slow, which I very much don't believe. So, let's try the solution from the last challenge and see what happens.

[email protected]:/problems/time-s-up--again-_0_ba1fe87bd4905d3f34eb83d66f907fe0$ python ~/times_up_solve.py
[+] Starting local process '/problems/time-s-up--again-_0_ba1fe87bd4905d3f34eb83d66f907fe0/times-up-again': pid 2279733
[*] Process '/problems/time-s-up--again-_0_ba1fe87bd4905d3f34eb83d66f907fe0/times-up-again' stopped with exit code -14 (SIGALRM) (pid 2279733)
Traceback (most recent call last):
  File "/home/pkqxdd/times_up_solve.py", line 11, in <module>
    p.sendline(str(eval(eq)))
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/tube.py", line 726, in sendline
    self.send(line + self.newline)
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/tube.py", line 707, in send
    self.send_raw(data)
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/process.py", line 710, in send_raw
    raise EOFError
EOFError

Interesting...is a script really too slow? Time to see what the binary is doing. Decompiling the binary gives

int __cdecl main(int argc, const char **argv, const char **envp)
{
  init_randomness(*(_QWORD *)&argc, argv, envp);
  printf("Challenge: ");
  generate_challenge();
  putchar(10);
  fflush(stdout);
  puts("Setting alarm...");
  fflush(stdout);
  ualarm(0xC8u, 0);
  printf("Solution? ", 0LL);
  __isoc99_scanf("%lld", &guess);
  if ( guess == result )
  {
    puts("Congrats! Here is the flag.txt!");
    system("/bin/cat flag.txt");
  }
  else
  {
    puts("Nope!");
  }
  return 0;
}

According to ualarm(3), we have at least 200 (0xc8) microseconds to solve the challenge...should be enough? But indeed the script from the last challenge doesn't work. Maybe the problem is in pwnlib? The complicated outputs in pwnlib seems to be taking too much time. Let's try Python's built-in library, which is written in C anyway. 

import subprocess
import os
import time
import sys

os.chdir('/problems/time-s-up--again-_0_ba1fe87bd4905d3f34eb83d66f907fe0')
proc=subprocess.Popen('./times-up-again',stdin=subprocess.PIPE)
proc.stdin.write('0\n')

Running it gives

[email protected]:~$ python test.py
Challenge: (((((-1604883022) - ((-843553522) - (1904421845))) * ((-1455195974) + (-2061175916))) + (((-616619968) + (1059890713)) * ((1904888709) * (-1451154448)))) * ((((570079140) + (402419234)) * ((1341922625) * (1138803193))) - (((-209997659) * (767054531)) - ((2074963076) + (1898443795)))))
Setting alarm...
Solution? Nope!

Oh cool. So we can definitely send something to the target. The problem is how to get the output from the binary and parse it. After some trials and errors, it seems that nothing will be printed if I change the line to 

proc=subprocess.Popen('./times-up-again',stdin=subprocess.PIPE,stdout=subprocess.PIPE)

which means that it's not that a script is too slow; rather, a pipe is too slow. We can get the challenge printed to the terminal and send something back...if only we have more time. As I was reading the documentation for ualarm again, something suddenly pops up:

The ualarm() function causes the signal SIGALRM to be sent to the
       invoking process after (not less than) usecs microseconds.  The delay
       may be lengthened slightly by any system activity or by the time
       spent processing the call or by the granularity of system timers.

Umm...signals. Maybe I can create a race condition? According to Signal(7)

 The signals SIGKILL and SIGSTOP cannot be caught, blocked, or
       ignored.

What if I send a SIGSTOP to the process after the challenge was printed but before it could set the alarm? The target flushes its stdout before setting the alarm anyway. So there should be some room? 

import signal
import subprocess
import os
import time
import sys

os.chdir('/problems/time-s-up--again-_0_ba1fe87bd4905d3f34eb83d66f907fe0')
proc=subprocess.Popen('./times-up-again',stdin=subprocess.PIPE)
time.sleep(0.000175)
proc.send_signal(signal.SIGSTOP)
#print('PID: '+str(proc.pid))
res=raw_input('')
proc.stdin.write(res)
proc.stdin.write('\n\n')
proc.stdin.flush()
proc.send_signal(signal.SIGCONT)

After doing a binary search between 100 to 200 microseconds, I found that 175 microsecond was the best delay. The script does exactly what I wanted: printing the challenge, taking an input, and send back to the target binary before it realizes anything is happening (SIGSTOP cannot be caught, which means the process doesn't even know it ever received the signal, just like SIGKILL). Let's try it:

[email protected]:/problems/time-s-up--again-_0_ba1fe87bd4905d3f34eb83d66f907fe0$ python ~/times_up_again_solve.py
^CTraceback (most recent call last):
  File "/home/pkqxdd/times_up_again_solve.py", line 13, in <module>
    res=raw_input('')
KeyboardInterrupt
[email protected]:/problems/time-s-up--again-_0_ba1fe87bd4905d3f34eb83d66f907fe0$ python ~/times_up_again_solve.py
Challenge: (((((-1025316331) + (1528432253)) + ((502406442) - (1000249101))) - (((-1577946209) - (-1533058465)) * ((1692084378) - (2061498550)))) + ((((-1463855472) * (-1415636225)) - ((-1514247117) * (-708224016))) * (((1491735576) * (-1209809497)) + ((1416116362) + (881622800)))))

By the way, due to the nature of a race condition, I had to run the script several times to get a proper timing. For this problem, the best timing is when the challenge is printed, but "Setting alarms..." is never printed. Anyway, let's try the challenge. We have as much time as we need, so using a "calculator" should suffice:

Python 3.8.0 (v3.8.0:fa919fdf25, Oct 14 2019, 10:23:27)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> s='(((((-1025316331) + (1528432253)) + ((502406442) - (1000249101))) - (((-1577946209) - (-1533058465)) * ((1692084378) - (2061498550)))) + ((((-1463855472) * (-1415636225)) - ((-1514247117) * (-708224016))) * (((1491735576) * (-1209809497)) + ((1416116362) + (881622800)))))'
>>> eval(s)
-1804464395287952001925064239532608785

Umm...it does seem to be quite a large number isn't it? It's probably gonna overflow the long long used to store the answer. So let's calculate the overflow too:

>>> import ctypes
>>> ctypes.c_longlong(eval(s))
c_long(8448811195176710895)

Pasting this number back to the challenge

[email protected]:/problems/time-s-up--again-_0_ba1fe87bd4905d3f34eb83d66f907fe0$ python ~/times_up_again_solve.py
Challenge: (((((-1025316331) + (1528432253)) + ((502406442) - (1000249101))) - (((-1577946209) - (-1533058465)) * ((1692084378) - (2061498550)))) + ((((-1463855472) * (-1415636225)) - ((-1514247117) * (-708224016))) * (((1491735576) * (-1209809497)) + ((1416116362) + (881622800)))))
8448811195176710895
Setting alarm...
Solution? Congrats! Here is the flag.txt!
picoCTF{Hasten. Hurry. Ferrociously Speedy. #16462951}

and we got the flag. Yay!