Shellcodes
Shellcode is the hex representation of a binary's machine code.
If we look at a example program like below
global _start
section .data
message db "Hello HTB Academy!" ; 18 bytes of text
section .text
_start:
mov rsi, message ; rsi = address of the message (text pointer)
mov rdi, 1 ; rdi = file descriptor 1 (stdout)
mov rdx, 18 ; rdx = number of bytes to write
mov rax, 1 ; rax = syscall number 1 (write)
syscall ; do the write
mov rax, 60 ; syscall number 60 (exit)
mov rdi, 0 ; exit code 0 (success)
syscall ; exit
Converted to to hex we get the shellcode:
48be0020400000000000 ; mov rsi, 0x402000 (address of message)
bf01000000 ; mov edi, 1
ba12000000 ; mov edx, 18
b801000000 ; mov eax, 1
0f05 ; syscall
b83c000000 ; mov eax, 60
bf00000000 ; mov edi, 0
0f05 ; syscall
Assembly to Machine Code
Each x86 instruction and each register has its own binary machine represented in hex. When assembling code with nasm it converts assembl instructions to machine code which the processor can understand.
We can assemble and disassemble:
# Assemble
$ pwn asm 'push rax' -c 'amd64'
0: 50 push eax
# Disassemble
$ pwn disasm '50' -c 'amd64'
0: 50 push eax
Extract Shellcode
A binary's shellcode is only the .text section where the instructions and code is found. We can use the ELF library and read the elf binary.
$ python3
>>> from pwn import *
>>> file = ELF('helloworld')
>>> file.section(".text").hex()
'48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05'
Or use this script shellcoder.py
#!/usr/bin/python3
import sys
from pwn import *
context(os="linux", arch="amd64", log_level="error")
file = ELF(sys.argv[1])
shellcode = file.section(".text")
print(shellcode.hex())
Loading Shellcode
We can run shellcode using pwntools.
$ python3
>>> from pwn import *
>>> context(os="linux", arch="amd64", log_level="error")
>>> run_shellcode(unhex('4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05')).interactive()
Hello HTB Academy!
Or use this script loader.py
#!/usr/bin/python3
import sys
from pwn import *
context(os="linux", arch="amd64", log_level="error")
run_shellcode(unhex(sys.argv[1])).interactive()
Then run it
python3 loader.py '4831db536a0a48b86d336d307279217d5048b833645f316e37305f5048b84854427b6c303464504889e64831c0b0014831ff40b7014831d2b2190f054831c0043c4030ff0f05'
Debuggin Shellcode
We can use pwntools to build an elf binary in a script assembler.py
#!/usr/bin/python3
import sys, os, stat
from pwn import *
context(os="linux", arch="amd64", log_level="error")
ELF.from_bytes(unhex(sys.argv[1])).save(sys.argv[2])
os.chmod(sys.argv[2], stat.S_IEXEC)
Use it like
$ python assembler.py '4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05' 'helloworld'
GCC
We can build shellcode into an elf executable using gcc. Using this C template where we must escape hex byte with \x
:
#include <stdio.h>
int main()
{
int (*ret)() = (int (*)()) "\x48\x31\xdb\x66\xbb\...SNIP...\x3c\x40\x30\xff\x0f\x05";
ret();
}
The compile it
$ gcc helloworld.c -o helloworld -fno-stack-protector -z execstack -Wl,--omagic -g --static
Working Shellcode
Not all shellcode can loaded and ran. There can be requirements:
Does not contain variables
Does not refer to direct memory addresses. we can call labels.
Does not contain any NULL bytes
00
Shellcode is expected to be executable once loaded into memory, onlye from the text segment. So I can't load data from .data or .bss.
section .data
message db "Hello HTB Academy!"
We can convert to but a 64-bit register can only hold 8 bytes.
mov rsi, 'Academy!'
We have mov
the string to rbx and the push rbx
.
global _start
section .text
_start:
xor rbx, rbx
mov bx, 'y!'
push rbx
mov rbx, 'B Academ'
push rbx
mov rbx, 'Hello HT'
push rbx
mov rsi, rsp
xor rax, rax
mov al, 1
xor rdi, rdi
mov dil, 1
xor rdx, rdx
mov dl, 18
syscall
xor rax, rax
add al, 60
xor dil, dil
syscall
We must ensure that our shellcode does not contain any NULL bytes 00
. They are used as string terminators and can cause issues. We can check with this python script.
#!/usr/bin/python3
import sys
from pwn import *
context(os="linux", arch="amd64", log_level="error")
file = ELF(sys.argv[1])
shellcode = file.section(".text")
print(shellcode.hex())
print("%d bytes - Found NULL byte" % len(shellcode)) if [i for i in shellcode if i == 0] else print("%d bytes - No NULL bytes" % len(shellcode))
Then load it
$ python3 loader.py '4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05'
Hello HTB Academy!
Tools of the trade
Shellcraft.
From pwntools we can use shellcraft.
$ pwn shellcraft amd64.linux.sh
6a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05
$ pwn shellcraft amd64.linux.sh -r
$ whoami
root
Msfvenome
We use msfvenom for shellcode.
$ msfvenom -l payloads | grep 'linux/x64'
linux/x64/exec Execute an arbitrary command
...SNIP...
# Create a /bin/sh
msfvenom -p 'linux/x64/exec' CMD='sh' -a 'x64' --platform 'linux' -f 'hex'
Shellcode Encoding
Encoding shellcode is used to bypass defender and antivirus. We can use msfvenom for encoding.
msfvenom -l encoders
Framework Encoders [--encoder <value>]
======================================
Name Rank Description
---- ---- -----------
cmd/brace low Bash Brace Expansion Command Encoder
cmd/echo good Echo Command Encoder
We can use one like x64/xor. Using the -i COUNT flag we can specify amount of iterations.
msfvenom -p 'linux/x64/exec' CMD='sh' -a 'x64' --platform 'linux' -f 'hex' -e 'x64/xor'
Last updated
Was this helpful?