Introduction
This post describes a simple trick to copy arbitrary assembly/shellcode (Linux x86_64) into a mapped memory address and then execute it on-the-fly as if it were an external subroutine, no compiling or extra tools needed (based on Perl hacking I: PEEK & POKE & XSUB).
Copying payload into memory
Two things are needed for copying an assembly/shellcode payload into memory: a temporary file with the given payload and two syscalls to map a new memory area and read the temporary file's content into such area. Let's consider the following assembly code which executes /usr/bin/id with execve:
BITS 64
global main
section .text
main:
call run
db "/usr/bin/id", 0x0
run:
;;;;;;;;;;;;;;;;;;;;;;;;;
; call id
;;;;;;;;;;;;;;;;;;;;;;;;;
pop rsi
pop rsi ; twice to remove garbage when called from Perl
xor rax, rax
lea rdi, [rsi]
; argv
; ["/usr/bin/id"]
push 0
push rdi ; "/usr/bin/id"
mov rsi, rsp
; execve & exit
xor rax, rax
mov rax, 59
mov rdx, 0
syscall
pop rsi
xor rdx, rdx
mov rax, 60
syscall
Compile it and extract its hexadecimal representation:
$ nasm -f elf64 id.s -o id.o
$ objcopy -O binary -j .text id.o id
$ od -An -v -t x1 id > idhex
$ cat idhex
e8 0c 00 00 00 2f 75 73 72 2f 62 69 6e 2f 69 64
00 5e 48 31 c0 48 8d 3e 6a 00 57 48 89 e6 48 31
c0 b8 3b 00 00 00 ba 00 00 00 00 0f 05 5e 48 31
d2 b8 3c 00 00 00 0f 05
Modify the second byte by adding 1 (from 0c to 0d). This is to prevent an rsi full of garbage when executing the payload from Perl. Include the hexadecimal payload in the script and write it to a file:
# payload to execute /usr/bin/id with execve (x86_64)
my $p = "";
$p .= "\xe8\x0d\x00\x00\x00\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x69";
$p .= "\x64\x00\x5e\x5e\x48\x31\xc0\x48\x8d\x3e\x6a\x00\x57\x48\x89";
$p .= "\xe6\x48\x31\xc0\xb8\x3b\x00\x00\x00\xba\x00\x00\x00\x00\x0f";
$p .= "\x05\x5e\x48\x31\xd2\xb8\x3c\x00\x00\x00\x0f\x05";
my $f = "p";
open my $fh, '>', $f;
syswrite($fh, $p);
Then use the mmap, open and read syscalls to map a new memory area, open the temporary file and read its content into the buffer starting at the mapped memory area:
my $sz = (stat $f)[7];
# mmap syscall number 9
# 0 to let the system choose the start of the mapped area
# 3 for PROT_READ | PROT_WRITE
# 33 for MAP_SHARED | MAP_ANONYMOUS
# -1 and 0 to avoid the use of files
my $ptr = syscall(9, 0, $sz, 3, 33, -1, 0);
if($ptr == -1) {
die "failed to map memory\n";
}
# open syscall number 2
# 0 for read only flag
my $fd = syscall(2, $f, 0);
# read syscall number 0
# use $ptr as buffer
my $bytes = syscall(0, $fd, $ptr, $sz);
if($bytes == -1) {
die "failed to read payload file\n"
}
Executing assembly/shellcode payload
To executed the payload first the protection of the mapped memory area needs to be updated to allow execution (using mprotect):
# mprotect syscall number 10
# 5 for PROT_READ | PROT_EXEC
my $ret = syscall(10, $ptr, $sz, 5);
if($ret == -1) {
die "failed to update memory protection\n";
}
Then dl_install_xsub() from DynaLoader (standard module) is used to create a new Perl external subroutine based on the parameters $perl_name and $symref and obtain a reference to the “installed function”. The parameter $symref is expected to be a pointer to the function which implements the routine
to be installed, however, a pointer to the payload copied into memory can be used instead. Then a simple call to the dereferenced function executes the payload:
my $x = DynaLoader::dl_install_xsub("", $ptr);
&{$x};
$ perl exec_asm64-read.pl
...
[+] Trying to write payload to a temporary file...OK
[+] Payload size: 57
[+] Trying to map new memory area...OK
[+] Start of mapped area: 0x7fefdda14000
[+] Trying to write payload at 0x7fefdda14000...OK
[+] Trying to update memory protection...OK
[+] Trying to install xsub...OK
[+] Going to execute:
uid=1000(isra) gid=1000(isra)...
A redacted version of the code looks like this (no checks, etc):
use DynaLoader;
$p = "\xe8\x0d\x00\x00\x00\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x69";
$p .= "\x64\x00\x5e\x5e\x48\x31\xc0\x48\x8d\x3e\x6a\x00\x57\x48\x89";
$p .= "\xe6\x48\x31\xc0\xb8\x3b\x00\x00\x00\xba\x00\x00\x00\x00\x0f";
$p .= "\x05\x5e\x48\x31\xd2\xb8\x3c\x00\x00\x00\x0f\x05";
$f = "p";
open $fh, '>', $f;
syswrite($fh, $p);
$sz = (stat $f)[7];
$ptr = syscall(9, 0, $sz, 3, 33, -1, 0); # mmap
$fd = syscall(2, $f, 0); # open
syscall(0, $fd, $ptr, $sz); # read
syscall(10, $ptr, $sz, 5); # mprotect
$x = DynaLoader::dl_install_xsub("", $ptr);
&{$x};
Final words
The execution of assembly/shellcode with Perl opens up the door for various interesting things, considering that no compilation is needed and Perl is available by default on almost all (if not all) Linux distributions. For example, you can take a look at looney.pl, an exploit for CVE-2023-4911 "Looney Tunables". The script uses Perl for obtaining the necessary data from the system and then executes an assembly/shellcode payload to call "usr/bin/su --help" using execve with a crafted envp. In this case looney.pl uses a more complicated mechanism for copying data into memory but it's the same idea at the end.
Stay tuned, more stuff is coming soon!