tinmarino / inmembin

Execute binary code in momory from shell

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

In Memory Binary

Shell Debian Archlinux Alpine
bash x86 Bash on Debian arm Bash on Debian
x86 Bash on Debian x86 Bash on Debian
x86 Bash on Arch arm Bash on Arch
x86 Bash on Arch x86 Bash on Arch
x86 Bash on Alpine arm Bash on Alpine
x86 Bash on Alpine x86 Bash on Alpine
zsh x86 Zsh on Debian arm Zsh on Debian
x86 Zsh on Debian x86 Zsh on Debian
x86 Zsh on Arch arm Zsh on Arch
x86 Zsh on Arch x86 Zsh on Arch
x86 Zsh on Alpine arm Zsh on Alpine
x86 Zsh on Alpine x86 Zsh on Alpine
ash x86 Ash on Debian arm Ash on Debian
x86 Ash on Debian x86 Ash on Debian
x86 Ash on Arch arm Ash on Arch
x86 Ash on Arch x86 Ash on Arch
x86 Ash on Alpine arm Ash on Alpine
x86 Ash on Alpine x86 Ash on Alpine
sh x86 Sh on Debian arm Sh on Debian
x86 Sh on Arch x86 Sh on Debian
x86 Sh on Arch arm Sh on Arch
x86 Sh on Arch x86 Sh on Debian
x86 Sh on Arch arm Sh on Arch
x86 Sh on Arch x86 Sh on Arch
ksh x86 Ksh on Debian arm Ksh on Debian
x86 Ksh on Debian x86 Ksh on Debian
x86 Ksh on Arch arm Ksh on Arch
x86 Ksh on Arch x86 Ksh on Arch
x86 Ksh on Alpine arm Ksh on Alpine
x86 Ksh on Alpine x86 Ksh on Alpine
yash x86 Yash on Debian arm Yash on Debian
x86 Yash on Debian x86 Yash on Debian
x86 Yash on Arch arm Yash on Arch
x86 Yash on Arch x86 Yash on Arch
x86 Yash on Alpine arm Yash on Alpine
x86 Yash on Alpine x86 Yash on Alpine

license 100% shell Typos Shellcheck Yamllint Codecov

Execute binary code from shell without touching the filesystem. Hard copied from arget13/ddsc.sh

Content

Chapter Content
Quickstart get in
Requirement what you can expect
Memory memory access primitive
read write seek
Shell lessons learned on posix shell
command compat latin1 main hex2dec unhexify hexify endian craft
Asm assembly snippet used
0/debug 1/dup2 2/memfd 3/ftruncate 4/pause 5/mprotect 6/restore 7/mmap
Todo what is next
Credit @arget13 idea
Link references

Quickstart

git clone --depth=1 --branch=main https://github.com/tinmarino/inmembin && cd inmembin

/proc/self/exe ./inmembin.sh & pid=$!           # Create memfd
sleep 0.3 
cp "$(command which echo)" /proc/"$pid"/fd/4    # Fill it with a binary
/proc/"$pid"/fd/4 -e "\e[34mmy message\e[0m"    # Execute the in-mem binary

Requirement

  • Command: dd <= used to seek, read and write memory. Posix shell limitation is that is is character wise (i.e null terminated strings) and not bytewise (i.e. supporting null bytes)
  • Shell: bash zsh ash (dash) ksh (mksh) sh <= posix compliant tested shells
  • File: /proc/self/syscalls /proc/self/maps /proc/self/mem <= used to hook instruction pointer, search memory and edit memory

Memory access

read

xxd -s "$shellcode_addr_dec" -l 100  -c 100000 -p <&3
dd bs=1 count=100 skip=$shellcode_addr_dec <&3

setarch x86_64 -R bash -c "read -r syscall_info < /proc/self/syscall; echo \"$syscall_info\""0
0x0  0x55a7df932260 0x1000 0x2ca893f4 0x0 0xffffffff 0x7fff7835dbe8 0x7fb390914992
N def x86 arm comment
1 sycall number rax x8 0 => read
2 arg1 rdi x0 address to read
3 arg2 rsi x1 size to read
4 arg3 rdx x2
5 arg4 r10 x3
6 arg5 r8 x4
7 arg6 r9 x5
8 stack pointer rsp sp maybe we can play here
9 program counter rip pc target: first instruction executed at return to userland

write

TODO

seek

Using the dd method

dd bs=1 skip="$1" > /dev/null 2>&1  # From coreutils
tail -c +$(( $1 + 1 )) >/dev/null 2>&1  # From coreutils + Bad for ksh
od -j $(( $1 + 1 )) -N 0 >/dev/null 2>&1  # From coreutils
cmp -i "$1" /dev/null > /dev/null 2>&1  # From diffutils
hexdump -s "$1" > /dev/null 2>&1  # From util-linux
xxd -s "$1" > /dev/null 2>&1  # From vim

Ksh is not supporting the $(( $1 + 1 )) arithmetic. This is unfortunate, I would have preferred to use tail like @arget13, just for personal affinity.

Silence error to avoid: error reading standard input: Bad file descriptor, which I do not care

Posix shell

Command cheatsheet

  • od
    • -t x1z: output in hex, 1 byte, with zero as tail
    • -v: do not cheat with * instead of newline
    • -N 10: read 10 next bytes
    • -j 4096: seek offset 4096
  • dd
    • count
    • skip
    • bs

Shell compatibility

  • [Sh]: The bourne shell, the first one (1979)
  • Bash: default on Linux
    • BASH_VERSION is set
    • Limited to signed 64 bits integer arithmetic
  • Zsh: default on Mac
    • ZSH_VERSION is set
  • Ash: The smallest BSD, no more feature than Posix
    • Do not support printf "\x41"
  • Ksh: Powerful for 1990 (pre 64 bits era)
    • mksh or ksh93
    • KSH_VERSION is set
    • Limited to 32 bits integer arithmetic
    • Uses FD 3 to pass stdout and stdout on command redirection so echo $(read_mem) is not the same as read_mem is function read_mem uses FD 3 (as originally did)
    • For example printf %08x $(( 0xffffffff )) returns ffffffffffffffff, because 0xffffffff expands to -1
  • Yash: Japanese => made for unicode

latin1

locale  # Show current setting
man latin1

# list available locales
locale -a
cat /usr/share/i18n/SUPPORTED
cat /usr/share/i18n/charmaps/ISO-8859-15.gz
ls /usr/share/i18n/locales/musl/  # Musl instead of Glibc

# Install locale
wget https://github.com/tinmarino/inmembin/raw/ci/test/ISO-8859-15.gz
mkdir -p /usr/share/i18n/charmaps
cp ISO-8859-15.gz /usr/share/i18n/charmaps/ISO-8859-15.gz
echo "en_US ISO-8859-15" >> /etc/locale.gen
locale-gen

# Update locale
# -- Archlinux
echo "LANG=en_US.ISO-8859-15" >> /etc/locale.conf
echo "LC_CTYPE=en_US.ISO-8859-15" >> /etc/locale.conf
echo "export LC_CTYPE=en_US.ISO-8859-15" >> /etc/profile.d/locale.sh  # For alpine
unset LANG LC_CTYPE
. /etc/profile.d/locale.sh
# -- Debian
update-locale  # sudo

# Worklog
export LC_CTYPE=en_US.ISO-8859-15
LC_CTYPE=en_US.ISO-8859-15 yash -c ". inmembin.sh; unhexify a4 | hexify 1"   # Here it return ac, even on my Ubuntu, it should be EURO SIGN but it is NOT SIGN, this is because america latin15 has no euro. better latin1

# Keep eye
cat /etc/environment
cat /etc/profile

main

The main routine is crafting and writing in memory

  1. shellcode
  2. jumper: small jmp instruction to jump to it.

hex2dec

Using the printf method

printf "%d" 0x10  # Posix compatible
$(( 0x10 ))  # More readable

Ksh (mksh) do not support 64 bit arithmetic and hardly support unsigned integer (I did not succeed). So the hex2dec: $((0x10)) used by @arget13 was replaced by printf "%d"

unhexify

In shell, variable cannot hold null byte as strings are zero terminated. So byte streams must be send to pipes

printf "\\$(printf "%o" 0x41)"  # Posix compatible
printf "\x41"  # Bash but not posix compatible

hexify

TODO

endian

String indexing is not supported in sh and ash. As shellcode are supposed to be small, just invert pair of chars by preprending each next pair (like vim :g/.*/m0).

Initially I was appending, but anyway the += operator is not supported in ash.

craft shellcode and jumper

Embed hex strings of binaries, maybe with recently fetched pointers

Asm

0/ debug

".ELF"  ; 7f454c46
int    0x3  ; 0: cd 03 => just a sheetcheat

pop eax       ; 58
xor eax, eax  ; 31c0
nasm syscall_memfd_create.asm -o syscall_memfd_create.bin
objdump -b binary -m i386:x86-64 -D syscall_memfd_create.bin

sudo xxd -s $((0x7f5a53314992)) -c 10000 -l 10000 -p /proc/$$/mem | xxd -r -p > tail.bin

objdump -b binary -m i386:x86-64 -D -M intel tail.bin > tail.asm

setarch x86_64 -R bash inmembin.sh


# Get hex on ARM
file=arm; aarch64-linux-gnu-as -o $file.o $file.S  && aarch64-linux-gnu-objcopy -j.text -O binary $file.o $file  && xxd -c 10000 -p $file

Craft ARM move

printf %x $((  (0x0123 << 5) + (1 << 31) + (1 << 30) + (1 << 28) + (1 << 25) + (1 << 23) + (0 << 22) + (0 << 4) ))
# Reduces to
printf %x $(( 0xf2800000 + (0x0123 << 5) + (0 << 21) ))

Outputs: d2802460

where

mov     x0, #0x0123           // 602480d2

1/ dup2

Arget13 is duplicating 0<-2 because his stdin was redirected from a pipe.

xor    rax,rax  ; 0:   48 31 c0                
mov    rsi,rax  ; 3:   48 89 c6                
mov    al,0x2   ; 6:   b0 02                   
mov    rdi,rax  ; 8:   48 89 c7                
mov    al,0x21  ; b:   b0 21                   
syscall         ; d:   0f 05                   
mov     x8, #0x18  // 080380d2
mov     x0, #0x2   // 400080d2
mov     x1, #0x0   // 010080d2
svc     #0x0       // 010000d4

080380d2400080d2010080d2010000d4

int memfd_create(char* filename, unsigned int flags)
// Return 4 <= FD // sig 0x164 for x86

If filename is NULL => Segmentation fault (I think this is subobtimal impl from Linux kernel as the name is useless)

Original: (20 bytes, including 8 to set filename string)

push   0x44414544   ; 0:   68 44 45 41 44 => "DEAD"
mov    rdi,rsp      ; 5:   48 89 e7 => arg1: Point to "DEAD"
xor    rsi,rsi      ; 8:   48 31 f6 => arg2: Flag 0
mov    rax,rsi      ; b:   48 89 f0
mov    ah,0x1       ; e:   b4 01
mov    al,0x3f      ;10:   b0 3f
syscall             ;12:   0f 05

68444541444889e74831f64889f0b401b03f0f054889c7b04d0f05b0220f05

Naive: For comparison, here is my naive intent (17 bytes but crash)

mov    eax,0x13f  ;0:   b8 3f 01 00 00          
mov    edi,0x0    ;5:   bf 00 00 00 00          
mov    esi,0x0    ;a:   be 00 00 00 00          
syscall           ;f:   0f 05                   
mov     x0, #0x4144           // 802888d2
movk    x0, #0x4445, lsl #16  // a088a8f2
str     x0, [sp, #-16]        // e00f1ff8
mov     x0, sp                // e0030091
eor     x1, x1, x1            // 210001ca
mov     x8, #0x117            // e82280d2
svc     #0x0                  // 010000d4

802888d2a088a8f2e00f1ff8e0030091210001cae82280d2010000d4

Optional, seems to be for early fail

int truncate(const char *path, off_t length);  // sig 0x4d
// Return 0 => success
mov    rdi,rax      ;14:   48 89 c7
mov    al,0x4d      ;17:   b0 4d
syscall             ;19:   0f 05

4/ pause

Wait for a signal

int pause(void);
// Do not return or returns -1 in case a signal stopped it
// -- but, for me, return 0xfffffffffffffdfe = -514
// -- with debugger and ctrl-c
mov    al,0x22      ;1b:   b0 22
syscall             ;1d:   0f 05

Page size is 4096

int mprotect(void *addr, size_t len, int prot);
// sig x86: 0xa, sig arm: 0xe2
; Can write to mem
mov rax, 0xa  ; mprotect
mov rdi, 0x7ffff7d14000  ; start addr
mov rdx, 0x7  ; R=1 W=2 X=4
mov rsi, 0x10000  ; jumper len
syscall
mov     x2, #0x3              // 620081d2: RW
mov     x1, #0x10000          // 2100a0d2: 16 pages
// Place holder to mov jumper immediate to x0
mov     x8, #0xe2             // 481c80d2: mprotect
svc     #0x0                  // 010000d4: syscall

481c80d2405999d24059b9f24059d9f24059f9f2e10080d2820180d2010000d4

6/ copy back content at jumper

For x86, it is straight forward:

mov r15, 0x7ffff7d14992        ; jmp addr
mov [r15], dword 0xf0003d48    ; 4
mov [r15+4], dword 0x5677ffff  ; 8
mov [r15+8], dword 0x441f0fc3  ; 12
jmp r15

But for arm, on my Android, things get much more complicated, as I cannot mprotect. I tried to by pass this problem by hooking the return value in the stack instead of the syscall, but after hours looking around, I found no obvious way to ensure bash will come back at this stack (keep increasing).

7/ mmap

TODO Implementing for a nice extension, this should print the addr, as I do not see it in /proc/self/maps

Temporary Dump

Jumper addr: page, hex, dec 7ffff7d14000 7ffff7d14992 140737351076242

TODO

  • Investigate the stack hooking, remembers that the stack goes down when calling, so look values up to hook return

  • Add Yash shell

  • Remove the hardcoded setarch

  • Compatible with Alpine (the linker has other instruction <= DONE with read_mem using xxd for now)

  • Find a way to cleanly jump after "infection"

  • Create a memory maps to put arguments

  • Get the return value of syscall in shell

  • Get the ARM compliant version of core

  • Doc: Improve doc with other files

  • Implement some syscalls

  • Refactor with core and syscall list

Credit

Copied from arget13/ddsc.sh => see credit there

Link

About

Execute binary code in momory from shell

License:GNU General Public License v3.0


Languages

Language:Shell 100.0%