Fetching contributors…
Cannot retrieve contributors at this time
639 lines (540 sloc) 21.9 KB

# Time's Up, For the Last Time!

Reverse Engineering, 500 points

## Description:

You've solved things fast. You've solved things faster! Now do the impossible.

## Solution:

This is the follow-up for Time's Up, Again!.

Let's run the attached file:

```root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# ./times-up-one-last-time
Challenge: (((((-1584302183) + (-971218067)) / ((1583603162) + (-545554333))) * (((1232213110) % (196087411)) t (((-1081550733) - (-182471578)) / ((-661669944) ^ (-1055552917))))) ^ ((((663349295) o (-1576825584)) r ((430910707) + (-1708745368))) * (((-169273934) % ((-1532525736) o (1490257609))) x ((-1897158128) ^ (-1518431513)))))
Setting alarm...
Solution? Alarm clock```

We got a mathematical expression (with new and weird operators), and shortly after, the program got killed by an alarm. How long is the alarm? Let's use `strace` to see:

```root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# strace ./times-up-one-last-time  2>&1 | grep setitimer
setitimer(ITIMER_REAL, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=10}}, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=0, tv_usec=0}}) = 0```

10 uSeconds? There's nothing really we can do in such a short period. There must be a workaround.

Searching for methods to disable an `alarm()`, a repository named preeny surfaces.

Preeny helps you pwn noobs by making it easier to interact with services locally. It disables fork(), rand(), and alarm() and, if you want, can convert a server application to a console one using clever/hackish tricks, and can even patch binaries!

This solution creates dummy libraries which can replace the standard libraries using the `LD_PRELOAD` environment variable. When this variable is set to the path of a shared object, that file will be loaded before any other library (including the C runtime, `libc.so`). Therefore, it's possible to replace the functionality of any C library function.

Let's try it locally. Since our program uses `ualarm` and not `alarm`, I had to implement another function in `preeny/src/dealarm.c` (using `LD_DEBUG=all` was very helpful in identifying this):

```root@kali:~/utils/preeny# cat src/dealarm.c
#include "logging.h"
#include <unistd.h>

unsigned int alarm(unsigned int seconds)
{
preeny_info("alarm blocked\n");
return 0;
}

useconds_t ualarm(useconds_t usecs, useconds_t interval)
{
preeny_info("ualarm blocked\n");
return 0;
}```

Now we can use `LD_PRELOAD` and the alarm won't trigger:

```root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# LD_PRELOAD=/root/utils/preeny/x86_64-linux-gnu/dealarm.so ./times-up-one-last-time
Challenge: ((((((-1300698887) ^ (-949765080)) r (1864106005)) - ((-1324978037) | (573959822))) * (((107131526) o (62906438)) x ((1530078919) % ((1826216697) t (-1912045987))))) o ((((-1157469812) - (1887024561)) t ((-896795094) / (-521129896))) * (((-1434685180) + (-1436379568)) r ((-698190073) * (458457333)))))
Setting alarm...
Solution? 1234
Nope!```

Unfortunately, this won't work on the remote server, since the program has `setgid` attribute set, allowing it to elevate privileges when needed (e.g. when it wants to read the flag).

```dvdalt@pico-2019-shell1:/problems/time-s-up--for-the-last-time-_1_a7830af9d51a361ee5d3b9eece69c22f\$ ls -al
total 92
drwxr-xr-x   2 root       root                             4096 Sep 28 21:53 .
drwxr-x--x 684 root       root                            69632 Oct 10 18:02 ..
-r--r-----   1 hacksports time-s-up--for-the-last-time-_1    46 Sep 28 21:52 flag.txt
-rwxr-sr-x   1 hacksports time-s-up--for-the-last-time-_1 10224 Sep 28 21:52 times-up-one-last-time```

For security purposes, the invoking user is usually prohibited by the system from altering the new process in any way, such as by using ptrace, LD_LIBRARY_PATH or sending signals to it, to exploit the raised privilege, although signals from the terminal will still be accepted. (Source: Wikipedia)

This includes also `LD_PRELOAD`. However, the article didn't mention blocking signals.

A signal may be blocked, which means that it will not be delivered until it is later unblocked. Between the time when it is generated and when it is delivered a signal is said to be pending.

...

A child created via fork(2) inherits a copy of its parent's signal mask; the signal mask is preserved across execve(2). (Source)

So we should be able to reuse our `C` program from the previous challenge. This time, before `fork`-ing, we must block the `SIGALRM` signal as well.

This brings us to our next problem: How do we solve the new expression and what are all these new operators?

It's time to dive into Ghidra's disassembly:

```undefined8 main(void)
{
init_random();
printf("Challenge: ");
create_expression();
putchar(10);
fflush(stdout);
puts("Setting alarm...");
fflush(stdout);
ualarm(10,0);
printf("Solution? ");
__isoc99_scanf(&g_format,&g_user_input);
if (g_user_input == g_expected_result) {
puts("Congrats! Here is the flag!");
system("/bin/cat flag.txt");
}
else {
puts("Nope!");
}
return 0;
}```

The interesting function is `create_expression`:

```void create_expression(void)
{
g_expected_result = get_expression(4);
return;
}

ulong get_expression(uint depth)
{
undefined8 uVar1;
ulong uVar2;
ulong uVar3;
ulong uVar4;

if (depth == 0) {
uVar1 = get_random_number();
uVar2 = SEXT48((int)uVar1);
printf("(%lld)",uVar2);
}
else {
uVar2 = maybe_decrease_depth(depth);
uVar3 = maybe_decrease_depth(depth);
uVar4 = get_random_operator();
putchar('(');
uVar2 = get_expression((uint)uVar2);
printf(" %c ",(ulong)(uint)(int)(char)uVar4);
uVar3 = get_expression((uint)uVar3);
putchar(')');
uVar2 = solve_expression((char)uVar4,uVar2,uVar3);
}
return uVar2;
}```

What we see here is a function that recursively builds an expression using random choices. In parallel, also the expected result is built via calls to `solve_expression`:

```ulong solve_expression(undefined param_1,ulong param_2,ulong param_3)

{
switch(param_1) {
case 0x25: // %
if (param_3 != 0) {
param_2 = (long)param_2 % param_3;
}
break;
case 0x26: // &
param_2 = param_2 & param_3;
break;
default:
/* WARNING: Subroutine does not return */
exit(1);
case 0x2a: // *
param_2 = param_2 * param_3;
break;
case 0x2b: // +
param_2 = param_3 + param_2;
break;
case 0x2d: // -
param_2 = param_2 - param_3;
break;
case 0x2f: // /
if (param_3 != 0) {
param_2 = (long)param_2 / (long)param_3;
}
break;
case 0x5e: // ^
param_2 = param_2 ^ param_3;
break;
case 0x66: // f
break;
case 0x6f: // o
param_2 = param_3;
break;
case 0x72: // r
param_2 = param_3;
break;
case 0x74: // t
break;
case 0x78: // x
param_2 = param_3;
break;
case 0x7c: // |
param_2 = param_2 | param_3;
}
return param_2;
}```

This is the function that explains what the new operators do. We should be able to modify our previous solution to account for the new operators, but we'd like to generate a few examples to make sure our implementation is correct.

Let's do that with `GDB`. What we need is the expression (which is printed to `stdout` by the program) and the expected result (which is visible in `create_expression`). Since the executable has `PIE` enabled, we start by calling `set disable-randomization on` to disable address randomization, then step through the code untl we arrive to `create_expression`:

``````gdb-peda\$ disas 0x0000555555554e96, 0x555555554ead
Dump of assembler code from 0x555555554e96 to 0x555555554ead:
0x0000555555554e96:  push   rbp
0x0000555555554e97:  mov    rbp,rsp
0x0000555555554e9a:  mov    edi,0x4
0x0000555555554e9f:  call   0x555555554dce
=> 0x0000555555554ea4:  mov    QWORD PTR [rip+0x2038cd],rax        # 0x555555758778
0x0000555555554eab:  nop
0x0000555555554eac:  pop    rbp
End of assembler dump.
``````

Our result is the return value of `call 0x555555554dce`, visible in `rax` at address `0x0000555555554ea4`. In order to save the need to repeat the process multiple times, let's rephrase this as a one-liner which will print the expression and the expected result directly from the command line:

``````root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# LD_PRELOAD=/root/utils/preeny/x86_64-linux-gnu/dealarm.so gdb -ex 'set disable-randomization on' -ex 'dprintf *0x555555554ea4, "Result: %lld\n", \$rax' -ex 'r <<< "0\n"' -ex 'q' ./test 2>&1 | grep "Result" -A 1
Result: -1077552436
Challenge: (((((1010419654) f (833747938)) % ((1326039599) & (-1838119057))) r (((-645474463) f (-323135453)) o ((-1741026520) + (1399763289)))) - ((((1010473730) | (-10720867)) * ((-830514850) x (-1313980518))) x (((1863694028) % (1127404824)) | (((284466338) + (966994831)) / (660177013)))))
root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# LD_PRELOAD=/root/utils/preeny/x86_64-linux-gnu/dealarm.so gdb -ex 'set disable-randomization on' -ex 'dprintf *0x555555554ea4, "Result: %lld\n", \$rax' -ex 'r <<< "0\n"' -ex 'q' ./test 2>&1 | grep "Result" -A 1
Result: -1567918110277765
Challenge: (((((1591946486) r (-1331969196)) o (((-1911788283) o (1371174778)) & (1747890195))) - (((-53926145) * (199210871)) - ((2133037019) * (196902112)))) | ((((842431978) & (481528503)) | ((-1399236960) - (-1419357215))) * (((-1890214325) x (-1261630713)) % ((1427727991) ^ (1213549524)))))
root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# LD_PRELOAD=/root/utils/preeny/x86_64-linux-gnu/dealarm.so gdb -ex 'set disable-randomization on' -ex 'dprintf *0x555555554ea4, "Result: %lld\n", \$rax' -ex 'r <<< "0\n"' -ex 'q' ./test 2>&1 | grep "Result" -A 1
Result: -92577501878885090
Challenge: (((((1209766958) * (-1502282569)) & ((29553023) - (-466860704))) * (((1886285652) + (-781523424)) + ((-2092310292) x (-1793126629)))) f ((((-1933428190) * (-1349379277)) r ((-651888835) % (-1712739931))) + ((((-279846789) - (-1521590110)) | ((255665580) t (-1236705863))) - ((1878626940) r (361305662)))))
root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# LD_PRELOAD=/root/utils/preeny/x86_64-linux-gnu/dealarm.so gdb -ex 'set disable-randomization on' -ex 'dprintf *0x555555554ea4, "Result: %lld\n", \$rax' -ex 'r <<< "0\n"' -ex 'q' ./test 2>&1 | grep "Result" -A 1
Result: 373430904388490120
Challenge: (((((-1702607148) ^ (-70680992)) + ((-446151739) & (-629732860))) * (((1851976134) / (712844382)) + ((-1376285629) ^ (-1970105546)))) t ((((25694460) / (960535758)) % ((-2069553659) o (-38889948))) / (((2043401392) f (-1555367099)) o ((-900709696) x (-248816132)))))
root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# LD_PRELOAD=/root/utils/preeny/x86_64-linux-gnu/dealarm.so gdb -ex 'set disable-randomization on' -ex 'dprintf *0x555555554ea4, "Result: %lld\n", \$rax' -ex 'r <<< "0\n"' -ex 'q' ./test 2>&1 | grep "Result" -A 1
Result: -143688065
Challenge: (((((-1044282253) | (1371394754)) + ((1107954027) + (851034211))) + (((-929168316) x (1981424730)) * ((-64515369) t (967367402)))) | ((((-1340669973) + (-579974564)) % ((-49649651) + (-838622239))) f (((75578907) ^ (1899437617)) & ((171087065) f (828726573)))))
``````

What we're doing here is call `GDB` with a set of instructions to execute. The important one is `dprintf *0x555555554ea4, "Result: %lld\n", \$rax` which means: "When executing the command at `0x555555554ea4`, also print the value of `rax` as a formatted string".

This output allows us to implement and test our new expression parser. Putting all the pieces together, we get:

```#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <stdint.h>
#include <stdbool.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/prctl.h>

// ----------------------------------------------------------------------------------------------------------

//Based on https://stackoverflow.com/questions/9329406/evaluating-arithmetic-expressions-from-string-in-c
const char* g_expr_to_parse = NULL;

char peek()
{
while(*g_expr_to_parse == ' ')
{
g_expr_to_parse++;
}
return *g_expr_to_parse;
}

char get()
{
while(*g_expr_to_parse == ' ')
{
g_expr_to_parse++;
}
return *g_expr_to_parse++;
}

int64_t expression();

int64_t number()
{
int64_t zero = '0';
int64_t result = get() - zero;

while (peek() >= '0' && peek() <= '9')
{
result = 10 * result + get() - '0';
}
return result;
}

int64_t factor()
{
if (peek() >= '0' && peek() <= '9')
return number();
else if (peek() == '(')
{
get(); // '('
int64_t result = expression();
get(); // ')'
return result;
}
else if (peek() == '-')
{
get();
return -factor();
}

perror("factoring error");
exit(EXIT_FAILURE);
}

bool is_operator(char c)
{
char operators[] = "+-*/%^|&fortx";
int i;

for (i = 0; i < sizeof(operators) - 1; i++)
{
if (operators[i] == c)
{
return true;
}
}
return false;
}

int64_t solve_expression(char operator, int64_t operand1, int64_t operand2)
{
switch (operator)
{
case '%':
if (operand2 != 0)
{
operand1 = operand1 % operand2;
}
break;
case '&':
operand1 = operand1 & operand2;
break;
default:
perror("Unknown operator");
exit(EXIT_FAILURE);
case '*':
operand1 = operand1 * operand2;
break;
case '+':
operand1 = operand2 + operand1;
break;
case '-':
operand1 = operand1 - operand2;
break;
case '/':
if (operand2 != 0)
{
operand1 = operand1 / operand2;
}
break;
case '^':
operand1 = operand1 ^ operand2;
break;
case 'f':
break;
case 'o':
operand1 = operand2;
break;
case 'r':
operand1 = operand2;
break;
case 't':
break;
case 'x':
operand1 = operand2;
break;
case '|':
operand1 = operand1 | operand2;
}
return operand1;
}

int64_t expression()
{
char operator;
int64_t result = factor();
int64_t op2;
while (is_operator(peek()))
{
operator = get();
op2 = factor();
result = solve_expression(operator, result, op2);
}
return result;
}

int64_t parse_expression(const char* p_expr)
{
g_expr_to_parse = p_expr;
return expression();
}

// ----------------------------------------------------------------------------------------------------------

#define PIPE_WRITE 1

struct subprocess
{
pid_t   pid;
int     stdin;
int     stdout;
int     stderr;
};

void close_fd(int fd)
{
if (close(fd) == -1)
{
perror("Could not close pipe end" );
exit(EXIT_FAILURE);
}
}

void mk_pipe(int fds[2])
{
if (pipe(fds) == -1)
{
perror("Could not create pipe");
exit(EXIT_FAILURE);
}
}

void mv_fd(int fd1, int fd2)
{
if (dup2(fd1,  fd2) == -1)
{
perror("Could not duplicate pipe end");
exit(EXIT_FAILURE);
}
close_fd(fd1);
}

// Start program at argv[0] with arguments argv.
// Set up new stdin, stdout and stderr.
// Puts references to new process and pipes into `p`.
// https://jameshfisher.com/2017/02/17/how-do-i-call-a-program-in-c-with-pipes/
void call(char* argv[], struct subprocess* p) {
int child_in[2] = {-1, -1};     // Parent to child
int child_out[2] = {-1, -1}; ;  // Child to Parent
int child_err[2] = {-1, -1}; ;  // Child to Parent

mk_pipe(child_in);
mk_pipe(child_out);
mk_pipe(child_err);

pid_t child_pid = fork();

if (child_pid == -1)
{
perror("Could not fork");
exit(EXIT_FAILURE);
}
else if (child_pid == 0)
{
//
// Child
//

// Close parent pipes
close_fd(STDIN_FILENO);
close_fd(STDOUT_FILENO);
close_fd(STDERR_FILENO);

// Close unused child pipe ends
close_fd(child_in[PIPE_WRITE]);

mv_fd(child_out[PIPE_WRITE], STDOUT_FILENO);
mv_fd(child_err[PIPE_WRITE], STDERR_FILENO);

// Kill child if parent dies
prctl(PR_SET_PDEATHSIG, SIGTERM);

char* envp[] = { NULL };
execve(argv[0], argv, envp);

// Shouldn't get here
perror("Child Error");
}
else
{
//
// Parent
//

// unused child pipe ends
close_fd(child_out[PIPE_WRITE]);
close_fd(child_err[PIPE_WRITE]);

p->pid = child_pid;
p->stdin  = child_in[PIPE_WRITE];  // Parent wants to write to subprocess child_in
}
}

// ----------------------------------------------------------------------------------------------------------

#define BUFFER_LENGTH 512

int main(int argc, char* argv[])
{

struct subprocess   proc;
ssize_t             nbytes;
char*               newline_location = NULL;
size_t              write_len;
int                 status;
int64_t             result;

// We have three different buffers, allowing us to print the full details at the end
char                expr_buffer[BUFFER_LENGTH]  = {0};
char                res_buffer[BUFFER_LENGTH]   = {0};
char                buffer[BUFFER_LENGTH]       = {0};

size_t              skip_len = sizeof("Challenge: ") - 1;

char*               child_argv[] = {"./times-up-one-last-time", NULL};

// Block alarm signal
{
perror("Failed to initialize signal set");
exit(EXIT_FAILURE);
}

{
perror("Failed to add signal to set");
exit(EXIT_FAILURE);
}

{
perror("Failed to block signal set");
exit(EXIT_FAILURE);
}

printf("Alarm blocked!\n");

// Fork child process
call(child_argv, &proc);

// Read expression from child STDOUT
nbytes = read(proc.stdout, expr_buffer, sizeof(expr_buffer) - 1);
if (nbytes < 0)
{
perror("Could not read from child stdout");
exit(EXIT_FAILURE);
}

expr_buffer[nbytes] = '\0';

// Format expression so that parse_expression() will accept it
newline_location = strchr(expr_buffer, '\n');
if (newline_location == NULL)
{
perror("Could not find newline");
exit(EXIT_FAILURE);
}

*newline_location = '\0';

// Evaluate expression
result = parse_expression(&expr_buffer[skip_len]);
write_len = snprintf(res_buffer, sizeof(res_buffer), "%ld\n", result);
if ( (write_len < 0) || (write_len >= sizeof(res_buffer)) )
{
perror("Could not sprintf result");
exit(EXIT_FAILURE);
}

// Write answer back to the child processes' STDIN
nbytes = write(proc.stdin, res_buffer, write_len);
if (nbytes != write_len)
{
perror("Could not write to stdin");
exit(EXIT_FAILURE);
}

// Read all output from child process STDOUT
{
}

// Print details
printf("Expression: %s\n", &expr_buffer[skip_len]);
printf("Result: %s\n", res_buffer);
printf("%s", buffer);

// Cleanup and wait for child to exit
close_fd(proc.stdin);
close_fd(proc.stdout);
close_fd(proc.stderr);

waitpid(proc.pid, &status, 0);

return EXIT_SUCCESS;
}```

Ouput:

```root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# gcc main.c -o tuftlt
root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# scp tuftlt dvdalt@2019shell1.picoctf.com:/home/dvdalt
tuftlt                                                                                100%   17KB   5.8KB/s   00:03
root@kali:/media/sf_CTFs/pico/Times_Up_For_the_Last_Time# ssh dvdalt@2019shell1.picoctf.com
dvdalt@pico-2019-shell1:~\$ cd /problems/time-s-up--for-the-last-time-_1_a7830af9d51a361ee5d3b9eece69c22f
dvdalt@pico-2019-shell1:/problems/time-s-up--for-the-last-time-_1_a7830af9d51a361ee5d3b9eece69c22f\$ ~/tuftlt
Alarm blocked!
Expression: (((((1278126002) f (1043275798)) x ((-464689218) - (322949368))) - (((1447574425) & (-1224253887)) x ((-1216973929) o (116652416)))) % ((((1762630346) % (1604569453)) * ((-592128242) f (778774872))) r (((783785328) & (1511276257)) ^ ((-2081807565) - (-502728913)))))
Result: -904291002

picoCTF{And now you can hack time! #2e0a37d1}
Solution? Congrats! Here is the flag!```
You can’t perform that action at this time.