Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
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_READ 0
#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]); 
        close_fd(child_out[PIPE_READ]);
        close_fd(child_err[PIPE_READ]); 

        mv_fd(child_in[PIPE_READ], STDIN_FILENO); 
        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_in[PIPE_READ]); 
        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
        p->stdout = child_out[PIPE_READ];  // Parent wants to read from subprocess child_out
        p->stderr = child_err[PIPE_READ];  // Parent wants to read from subprocess child_err
    }
}

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

#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;
    int                 total_bytes_read = 0;
    sigset_t            intmask;

    // 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
    if (sigemptyset(&intmask) == -1)
    {
        perror("Failed to initialize signal set");
        exit(EXIT_FAILURE);
    }

    if (sigaddset(&intmask, SIGALRM) == -1)
    {
        perror("Failed to add signal to set");
        exit(EXIT_FAILURE);
    }

    if (sigprocmask(SIG_BLOCK, &intmask, NULL) == -1)
    {
        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
    while ((nbytes = read(proc.stdout, buffer + total_bytes_read, sizeof(buffer) - total_bytes_read - 1)) > 0)
    {
        total_bytes_read += nbytes;
    }
    buffer[total_bytes_read] = '\0';

    // 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.