Shell | Debian | Archlinux | Alpine |
---|---|---|---|
bash | |
|
|
zsh | |
|
|
ash | |
|
|
sh | |
|
|
ksh | |
|
|
yash | |
|
|
Execute binary code from shell without touching the filesystem. Hard copied from arget13/ddsc.sh
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 |
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
- 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
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 |
TODO
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
- 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
- [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"
- Do not support
- 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
- Supports unicode which is bad for us: at beginning, cannot expand char large than 0x7f, See input.c::read_input using std::mbrtowc
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
The main routine is crafting and writing in memory
- shellcode
- jumper: small
jmp
instruction to jump to it.
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"
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
TODO
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.
Embed hex strings of binaries, maybe with recently fetched pointers
".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
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
2/ memfd_create
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
3/ ftruncate
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
5/ mprotect
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
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).
TODO Implementing for a nice extension, this should print the addr, as I do not see it in /proc/self/maps
Jumper addr: page, hex, dec 7ffff7d14000 7ffff7d14992 140737351076242
-
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
Copied from arget13/ddsc.sh => see credit there
- syscall and arguments (@google)
- parent project (@arget: ddexec)
- assembly code snippet (@Igor Zhirkov: low level programming)
- local: error number list
- local: memory protection list
- list of posix complaint shell (@archliux)
- awesome shell list (@github)
- posix shell specification
- arm instruction set