2006年04月05日

    这几天帮助同事研究菜单,发现普通软件的菜单都有一个边框,且无阴影效果(如图1)。

                        
        (图1)                                                                        (图2)

    这可能是因为使用的操作系统不是windows XP或者windows XP没有把菜单阴影效果的属性打开。但是发现在这种情况下的Visual Studio .net 2003却显示阴影效果的菜单(如图2)。

    对普通菜单进行修改。首先考虑到阴影可以使用GDI画出来,但是在哪里画是个问题。菜单是一个窗体,这是肯定的(现在无法证明,看后文)。如果能够获得菜单窗体的矩形,在矩形右下方向画出阴影就可以了。要获得菜单窗体的矩形,必须得到菜单的窗体句柄,窗体句柄怎么获得呢?

    字画菜单需要使用SetMenuItemInfo函数设置菜单的信息,在MENUITEMINFO结构的fType成员设置为MFT_OWNERDRAW(关于自绘菜单,请参考msdn)。这样我们拥有菜单的窗体可以响应WM_MEASUREITEM和WM_DRAWITEM消息了。主要画的功能是在WM_DRAWITEM消息中,WM_DRAWITEM消息会提供一个LPDRAWITEMSTRUCT结构的指针,其中hDC成员,就是我们能够在菜单上画图的HDC。这时调用WindowFromDC就可以获得菜单窗体的句柄了!

    获得了菜单的句柄,那么使用GetWindowRect可以获得菜单的矩形,注意,菜单并不是拥有菜单的那个窗体的子窗体,你可以把窗体调小再让菜单显示出来看,菜单并不会被那个拥有它的窗体遮挡住。所以画菜单阴影就要使用屏幕DC,也就是用CreateDC("DISPLAY", NULL, NULL, NULL);创建的dc上面画阴影,菜单内部你可以使用Rectangle画出漂亮的边框线和菜单的颜色,在矩形中你也可以像vs.net的菜单一样用灰色画出菜单左面的那个条。剩下的就是怎么画阴影了,这个就不是我的工作了,呵呵。不过我想了想,还是很复杂的。这里就不探讨这个了。

2006年03月27日
刚毕业的时候写的东西,时间过得真快2年多了…
一九九九年九月一六日,一个重要的日子,我第一次到学校报道。在新生大会上我正在想着怎么溜出去的时候,同宿舍的一个哥们说:“快看

”,顺着他手指的方向,看到了一个很文静很漂亮的小姑娘,“怎么样?”“看她坐的地方是咱们系的”。其实,虽然大家刚刚到学校一天,

但是早已经知道了,我们可怜的机电系女生是最最贫乏的了。接下来,哥几个还很“仗义”,决定把这个小姑娘发给我了。当时只是觉得好玩

,也没有考虑太多,就开始了行动了。殊不知这个决定竟然影响了我这样一个野小子的生活,就在这一刻,我的大学生活的80%的样子已经确

定下来了。但是这时候我还是不知道呢。
下面主要讲述一下我的战略战术,以及具体的实施,细节记得不太清楚了。

1.第一次亲密接触
3天后,第一次系里面上大课,我坐在了教室的最后,寻找目标。终于在人群中找到了她,呵呵,果然是我们专业的。下课的铃声终于响了。我

尾随着她和她的一个同学走出了教室。在出了楼门30米左右的地方,在同屋兄弟的“鼓励”下,我终于做出了这一辈子第一次但不时最后一次

主动大胆的行动。我大步走上前。“同学(好俗的称呼阿),问你一个问题行吗?”“什么啊?你是谁?”“我们是一个专业的,刚才一起上

课呢,我想问你,你叫什么名字啊?”“干什么?”“没什么阿,以后我们就是同学了,问问你叫什么名字呀。”“那你以后不久知道了吗?

”“可是我想现在就知道呀”… …在100米的路程中,我的大脑从来都没有这么飞快地运转过,而且我们的行进速度居然没有受到影响。结

果当然可想而知了,失败,还是没有弄到她的名字。但是和她说了这么长时间话,呵呵,感觉还不错。

2.制定方针
回到宿舍,把事情的经过和哥们们说了之后,宿舍里响起了了各种声音:“牛*”“是男人”“没被当流氓抓走阿?”… …
不管他们的,我开始制定作战方针,在和同宿舍的兄弟们的研究下,最后决定,一定要围绕“枕边风”这一有利武器作战,也就是说,先要把

她周围的人笼络住,然后,让她们帮助我每天晚上在她旁边吹枕边风,哈哈哈哈。想好了作战方针就开始实施。隔三差五的去买些水果给她们

宿舍送过去,呵呵,也不贵,但是效果显著!不到2个星期,我已经在她们宿舍人的脑海里形成了概念。

3.坚持去上课
上了大学,和上中学最大的区别(对我来说)就是没有人管着了,再也不用为了逃课向各种各样的理由。但是,这样的好日子,我在上大学半

年后才开始享受。因为我要天天上课去找她呀。无论风多大,无论雨多大,无论烈日当头还是冰天雪地,我都要坚持去上每一节全系一起上的

大课。那时候她不答理我,我就是没次都坐在她的旁边。这主要还有我同学的功劳,那时候我们班的同学有的人起的很早,但是无论是谁,只

要看到她来了,肯定在她身边的座位上放一本书为我占座。
评论:
在这里我感谢我的同学。没有你们,我是无法有今天的成就的,谢谢你们…(注意与自己身边的朋友搞好关系,他们有的时候能帮你大忙,所

以听我一句话,泡妞不是一个人的事)


4.要不断地在各种方面“充实自己”
课堂对我来说就是宿舍,我在那里惟一作的事情就是睡觉。不知道为什么下课的铃声响起的时候,就是我苏醒的时候,我充满了活力,充满的

对知识的渴望…当然了这已经是回到宿舍的事情了。下面我要先介绍一下我们宿舍的两位成员,这两个都是冲在情场第一线的杀手,但是两个

人的作风完全不同,一正一邪,一白一黑。第一位是来自大连的哥们。他就是走白道的,他熟读心理学,能够迅速找到小姑娘的心理弱点,从

而花费最短的时间和金钱得到最大的回报,而他所付出的仅仅是一些电话费和一瓶水的钱。电话和花园是他的有利武器和最佳利用地点。第二

位是北京的哥们,他是真正情场老手,下手稳、准、狠。能够用我们男人看来是最卑鄙的方法换取女孩子的芳心。
我每天回到宿舍,先向大连的哥们讲述我当天所做的事情所说的话,还有她的反应等等,然后,通过他经过心理学方面科学的分析,给我制定

下一步实施方案。这样的教学每天熄灯前坚持30-60分钟。而那位北京的哥们,则培训我另外的行为。他的名言:“慌个毛啊?男人,不慌!”

和“泡妞,就要不要脸!”。所以,我每天要在镜子的前面指着自己说:“你*不要脸,丢了人,过2分钟水还认识你呀?”。
评论:
经过上面2位大哥的培训,我日益成长,不断地充实了自己。真的学到了很多书本上没有学到的东西。


5.第一次约会
学校卖篮球比赛的票,我买了两张,想邀请她和我一起去看。可是,真的找不到合适的理由,因为她好像不太喜欢体育运动的说。有人支招说

先给她骗出去,然后不去也要去了。但是,我想起了一句名人名言:“当你不知道说实话还是说谎话好的时候,请说实话”。于是,我直接和

她说:“我买了2张篮球比赛的票,我们一起去看吧?北京首钢的比赛”“可我都不认识他们呀,也没看过”“其实,我知道你不怎么看篮球比

赛,但是,这次我为什么要请你一起去看呢,因为…北京队又一个叫巴特尔的,张的特别像张信哲…”“是吗?真的阿?那好吧,去看看,

呵呵” “… …”
在顶住了她刚刚见到巴特尔的那5分钟的强有力的气愤的谩骂后,终于全身心的投入到了比赛中,呵呵,这时候,我突然又想起了某人说过的一

句话“其实,女孩子很喜欢男孩运动的大汗淋漓的感觉的”。大家记住,这也是关键… …
评论:
体育运动最能体现男孩子阳光的一面…


6.生日晚宴
很快就到了我在大学的第一个生日,一定要好好准备啊,决定宴请八方客,当然也要有她和她们宿舍的人了。为了筹划这次生日晚宴。我们全

宿舍动员,进行了周密部署。我专门为她写了一首歌还谱了曲,准备在生日晚宴上为她演唱。说明一下:我会弹吉他,但是只限于能骗骗纯情

小女孩的水平,专业人士不要见怪,大家学习的目的不同…。
好了,言归正转,生日那天是这样安排的,两个包厢,一个是她们宿舍和几个外系的朋友,另外一个包厢是我们宿舍和几个亲密的朋友.那天我可真

是喝得烂醉,中途就不行了,独自跑回宿舍"解决"去了.大连的哥们到宿舍找到我的时候,我已经包着脸盆睡在了地上."我不行了,头晕得厉害,我

不回去了…""那个准备了半个月的歌也不唱了?你就把她们宿舍的冷在哪儿了?".是啊,我联系了半个月呢!不行,我要去!她抚着我又回到了饭店

,她问我有事没有,一下子我的感觉好多了,那起我心爱的吉他,从袋子中抽出早准备好的玫瑰花对她说:"这是送给你的,你先拿着,别说别的,我下

面给大家唱一首歌.""这是他自己作词作曲的,对吧?"大连的哥们帮我捧场."对,是的,为你写的,你要好好听阿"我对她说.接着,一阵熟练的solo

为前奏(这个最能唬人,:>))后,我唱了一首校园民谣风格的歌曲(我回忆了好久,可怎么也回忆不起来歌词和旋律了,以后有机会再写给大

家)。唱得怎么样我已经记不得了,但她宿舍的女孩后来说很成功,也因为这个她们后来第一次当面的对我进行了称赞(我不知道这算不算称

赞):以前以为你就是一个小痞子,天天游手好闲的,真没想到你还有点人样,居然还有一技之长,真没想到阿…@#$@^$&%#$!
这其中还有一段插曲,老板给我们介绍了2个日本留学生,我从宿舍回去唱歌的时候他们已经在那儿了。后来我唱完歌,哥们给我到了杯雪碧润

润嗓子,还没喝就糊里糊涂的被别人叫走和那两个小日本喝去了,我一下子把雪碧干了,那两个小日本各干了一杯啤酒,他们也喝高了,连连

的向我伸大拇指,“二锅头,干了,厉害!”#$#$%@!!$
吃完了饭,我送她回宿舍,故意放慢脚步,和大部队脱离。就只有我们两个并肩的走着。她突然问我:“你送我的这个花没有什么意思吧?”

。“怎么了?有意思和没意思有区别吗?”“要是有什么意思,我就不要了,还给你”。“我认为它有意思,你也可以认为它没有意思,但是

我送给你你已经收了,所以它现在是你的,你还给我可以,那就代表你送给我一束玫瑰,我可觉得这个有意思了,那你还给我吧!”“… …


评论:
感谢我那位北京的哥们,是他对我的不断教导才能练就了我现在这样厚颜无耻,不要脸制及的好习惯,衷心的谢谢他。也希望广大的朋友们记

住。


7.出奇招
一段时间我们宿舍哥几个闲来无趣,想出了一个奇招,这一招可以号称一击必杀!屡试不爽,很少有女孩子能够禁的住这一招的。下面我将第

一次公开的向大家传授这一招:
必备工具、人和环境:电话机一台、耳机(用听力的那种耳机最好,能够把电话的说话筒整个盖住)、CD播放机一台(普通的磁带机也行,就

是效果不好,mp3应该也可以了)、说普通话很流利但声音不是很特别且应变能力比较强的人一位、要绝对安静的环境,最好不要有噪音。下面

用一个我们的例子来说明整个过程。
先将楼道清场,30分钟内不要有任何噪音,单独派2个人维护楼道秩序。拨通女孩宿舍的电话,
我:喂,您好请问**小姐在吗?
**小姐舍友:哦,您等一下
**小姐:你好
我:您好,您是**小姐吗?
**小姐:是的,您是?
我:您好,我们是千禧有约声讯点歌台,有一位姓*的先生为您点了一首歌,是迪克牛仔的爱如潮水,*先生还进行了留言,我们在歌曲播放

完毕的时候向您播放它的留言,那现在就请您欣赏*先生为您点拨的,迪克牛仔为您带来的 爱如潮水。
(早就准备好的CD开始播放,并且把耳机扣在电话的话筒上…)
(这时候我们做什么??还用问!!早就笑的眼泪都出来了…就是不敢出声…)
(歌曲播放完毕)
我:**小姐,您还在收听吗?
**小姐:是的我在…
我:那好现在我们将播放,*先生给您的留言
(这时候轻轻的把话筒交给“*先生”,他一边哆嗦着拿着兄弟们一起为他写好的讲演稿,一边小心翼翼的念着…)
(当他念完的时候,呵呵,后背都湿透了,虽然已经练习了一下午了,但是到了现场还是紧张阿…)
我:您好,**小姐,按照我们栏目的规定,您可以给*先生回复留言,给您30秒钟的考虑时间,然后您将听到di的一声响,就请您开始留言

,留言的时间是30秒,好现在开始…
**小姐:呀!他让我留言呢!!我说什么好呀??
**小姐舍友:随便说呀!
**小姐:我也不知道该说什么阿?
(这时候按电话上任意一个键,电话都会有di的一声响…)
(由“*先生”手拿听筒,其它人在四面八方围着听…)
**小姐:呀!开始录音了,我说什么好呀(从声音能听出来,小姑娘真的慌了)
**小姐:那那那…就谢谢你吧,我很高兴…
(再次按电话上任意一键,发出di的一声响)
我:**小姐,我们将把您的录音播放给*先生,好,谢谢您使用千禧有约声讯点歌台,下次再见…

注意:
由于我们那时候正是1999年年末,所以把点歌台命名为千禧有约,你们在使用的时候请酌情更改名称,还有特此声明:如果你接到过我们千禧

有约声讯点歌台的点歌电话,请不要用西红柿或者臭鸡蛋扔我,你的男友应该对此负责任… …
补充说明一点,不要找她熟悉的人做主持人,我就是找了我们宿舍一个河北的哥们做主持人,普通话讲得不错,差一点被揭穿,还在还是瞒过

去了,嘿嘿,还愣着干甚么?快去打电话吧!!要不要请我做你的主持人也行,我又丰厚的地下点歌台主持人经验…


8.一击必杀,“斩”情敌
前文已经说过了,这个女孩子是比较漂亮的那种,所以,很快就发现了原来他有一个青梅竹马的,呸呸呸呸,允许我更正一下,不是青梅竹马

,是一个傻小子,和他一个初中一个高中,家离得也很近的小男人。我的同学说居然居然…(等我喘喘气…)居然有一天看见他们两个手拉

着手去参观学校发给我们的专业教室了!!我靠!而且,他们两个人每天晚上一起上自习。我在上了大学2周以后才由高我们一届的师兄口中得

知,上自习是大学生活中最有乐趣,也是参加者最多的活动。在自习室里,有学习成绩十分突出的未来国家的栋梁之材,有闲聊无事聚众娱乐

的混子,还有打情骂俏的情侣们,更有的则是单身一族,女孩们比较腼腆,在这里她们出门的次数应该是比较多的,她们会频繁的出现在男生

聚集的地方,但只是一次次的路过,连正脸也不会看一眼的,可要知道她们的内心,是多么的渴望男生的母光能够停留在她们身上,能够有传

说中的奇遇啊…另一方面,新入道的或者是入道不久的少女杀手,或称爱情杀手、处女终结者等等,也在其中。
了解到了大学自习室的错综复杂的社会背景后,我不禁有些着急了。不行!虽然她一再的拒绝和我上自习,但是,今晚我一定要去找她!不能

让她们2个人在这样在一起上自习了!太太太危险了!我没有给她任何的余地,只说了一句:“今天晚上,无论如何我都要去找你上自习!”,

然后转身就走了。晚上,我怀着紧张的心情来到了自习室。她和他在二楼的大厅等我。我走了过去你好呀,我看了那男的一眼。“我不上自习

了,你们都回去!”说着,她往楼外走去,我当然紧跟着出来了。他也跟在后面。走出了教学楼,她回头对我们两个说,“我要回宿舍了,你

们谁也别跟着我!”,说完她就走了,我看了一眼那个男的,他站在原地望着她…傻*,还在这儿看呢,这次要不追过去就他*没戏了。一边想

着,我一边追了过去,在她宿舍楼下赶上了她。大约8点了,天已经黑了,有风,吹得她的短发。“我再问你一次,你喜欢我吗?”,我有些紧

张。她没有说话,只是低着头踢他的脚。突然想起了我宿舍那个心理学大师曾经在一堂课上对我说过。“记住,在你表白的时候要营造一个气

氛,比如你们还在谈笑,你突然要梳理一下头发,或者最一个特别的动作,引起他的注意,这是算作一个前奏,然后再盯着她的眼睛对她说…

…”“她在你表白后,心里肯定很慌乱,你一只手扶着她的肩膀,轻轻地前后摇摆,注意一定要轻,不要让她察觉的。因为,女孩子在身处一

个不稳定的环境中的时候,她会特别希望有一个靠山,这时候就是你的肩膀,这样会加大你成功的机率…” 说时迟那时快,我立即按照这2种

方法去做… 哈哈,2个字,搞定!!她说了句:“我失去了一个好朋友…”“那你还得到了我这个好好朋友呢!!”我说…
评论:
大家注意上面我那哥们的2句名言!一般人我还不告诉他呢!实践证明,有用,真的有用啊!还不赶快收藏起来!


9.圣诞树的感动
马上就是圣诞节了,真个校园充满了圣诞的气氛。其实有女朋友的同志们都知道,逢年过节的时候最头痛,真不知道该买些什么送给她。还好

,第一次过圣诞节,我心里面正盘算着买棵圣诞树呢。到学校旁边的批发市场看看,还好不是很贵,100块钱全部拿下,一人多高的。说干就干

,买了一颗背回宿舍,好好的布置一下,泥红灯、小礼物全都挂好。又开始想另外一个问题了,怎么把它送过去,我们公寓和她的公寓相离100

米,但是几乎要路过所有的公寓门口,我考,众目睽睽之下我扛着这玩意…想想就丢人。“你*还怕丢什么人啊!!”北京的那位仁兄开始教

导我,“过两分钟谁还知道你是谁…”。也是,等到天黑,我扛着圣诞树就直奔女生公寓下…
这一路上,看到了各种各样的目光。有的女孩子走过我身旁不屑的看我一眼切了一声就走,但是,更多的是羡慕的目光,还有几个女生跟着我

,要看看我把圣诞树送给谁,呵呵,心里好高兴啊。和公寓看门的阿姨说了说,居然很轻易的就让我上去了。当我把圣诞树立在她们宿舍的正

中间,关掉屋里的大灯,打开圣诞树上的泥红灯,真没想到效果这么好,真的好漂亮。她高兴得搂着我合不拢嘴。把好几个宿舍的同学都叫来

,告诉她们这时她男朋友送的。
晚上回到宿舍,我们宿舍的哥们说,她可能是今天全校最幸福的女孩子了。我心里听的真实高兴啊。
评论:
其实,不需要你天天做出能感动对方的事情,但是你要把握住机会,争取每一次都要让她记忆犹新,像现在,过了这么长时间,她还总提起那

棵圣诞树呢…


10.让她知道我想着她呢
由于初中高中“交友不慎”和自身性格的原因,所以养成了很多的坏毛病。尤其是身上有一些痞样。我爱抽烟,更爱喝酒。所以我总是和哥们

们外面逍遥快活,当然特别是节假日了。每次都要喝的烂醉如泥,怎一个爽字所能形容啊。可每次喝高了的时候,我都会给她打电话,不知道

为什么,我特别想听到她的声音。我更想让她知道我无论何时何地都是爱着她的。
那年的八月十五(好像是),我们宿舍组织到外面喝酒,酒过3旬以后,打道回府。不料半路遇上了隔壁宿舍的同学,结果又被他们拉进去继续

喝。因为我已经约好10点整在她们公寓下见面,所以,一上来我就干了一瓶啤酒走人了。而前面提到的那个北京的大哥可能是喝高了,根本不

知道我提前走了,结果他被隔壁宿舍的兄弟们围攻了(因为我们宿舍就我们两个北京的)。结果他认为我不仗义,提前退场,把他一个人丢在

那里,就让另外一个哥们扶着他到女生公寓下来找我了…可是,这个时候我正在和她“谈心”呢。各种海誓山盟,甜言蜜语,大家自己yy去吧

,我也记不清楚了,先是听到有人喊我的名字而且声音越来越接近,我正在茫然中,只听到“扑通”一声,一个黑影摔在了我的脚下,吓了我

一跳,我定睛一看,就是他。嘴里面还骂着我呢“你*不仗义,**你在哪儿呢,给我出来,你*报我一个人留在那儿,不说一声就跑了… …”

。我考,这位大哥怎么了?扶他来的人赶快把他拉起来,这时候他可能有些清醒了,看了我一眼,二话没说就直回宿舍了。我正纳闷,她说“

你快去看看他怎么了,你们喝这么多,你还来找我啊,赶快回去休息吧”。“那怎么行,这么重要的日子我怎么能不看你一眼就回去呀。”“

我已经很高兴了,真的,你去好好休息吧”,说完,她踮起脚尖亲了我一下。我考!!!啥也别说了,我都不知道我是怎么回的宿舍…
其实,女孩子很容易打动的,并不需要你处心积虑的去想如何打动她,做好自己应该做的,并偶尔出一些鬼点子就可以了。
到宿舍后,经过几个同学帮我作证,终于让北京那哥们相信了我没有临阵脱逃。可是他又开始郁闷了,丢了这么大的人,居然在女生公寓下大

喊大叫,还当众给我下跪…我考,今天这么重要的日子,楼下mm可多了去了。“这让我我以后还在女生公寓那边混阿?”他开始郁闷。要知道

,对于一个大学里的情场杀手来说,女生公寓可是他第二个家呀!!“幸亏天黑,大不了以后不穿这件衣服了,*的”。
… …四年过去了,我真的没有再见到他穿那件长衣… …

11.军训生活(上)--兄弟连
关于军训一直都是大学里面最值得回忆的事情之一。军训生活上不是写我和mm的。所以有些偏离题目。我们专业的军训生活是在大二,和大一

新生一起。先开始是2个星期的试训,在校内,由老师组织。我们连就是唯一的大二年纪的老生,带队老师是大一的年级组长。所以我们根本不

把他放在眼里,因为他不是我们年级的老师,呵呵
那天,上面通知晚上不用训练,组织拉歌,让各个连下午联系歌曲。到了下午的时候,连里流传过来了一张纸条,是我们连独具特色的一首歌

,大家把它当作保留曲目。歌名叫《光棍歌》,歌词可能记不全了,大概如下:
面对着大青山,我光棍发了言
看着别人搂搂抱抱我眼红不眼馋
看着别人亲亲热热多么的讨人嫌
光棍要喝酒呀,光棍要抽烟
光棍的零花钱,是花也花不完
光棍多自由呀,光棍没负担
光棍的乐趣是说也说不完… …

大家都在加紧联系,光棍之声此起彼伏,不料,有一个班的歌词竟然被老师发现了,那个老师非常气愤,说这样的歌不让唱,还说我们不正经

!我靠,连里决定(连长和政委都是学生担任),不让唱这首歌?我们什么歌也不唱了!背向场地中央,拒绝拉歌。后来学校老师非要让我们

唱一首歌,我们就坚决不唱,罚我们站军姿,我们全体起立就站军姿,就是不答理他。我们是军人,连长不让唱我们就不唱,一切命令听指挥

嘛。至于连长,我们早就帮他报个病假让他先闪人了。由于其它连都是大一新生,虽然对老师已经有很多不满了,但是他们不敢有什么作为。
这样僵持的局面持续了很长时间,操场上也十分安静。突然我们系女生带头,全体其它连的同学一起高唱国际歌给我们加油。当时的场面,老

师真的有些下不来台了。无奈,他向我们道歉,并请我们唱一首歌,我们唱后它就灰溜溜的走了…
“请英雄连,先走!”师弟们齐声高喊。“请兄弟连先走,我们掩护!”我们也要拿出师哥的风范呀… …
我们的军训就在这样的和老师关系紧张的情况下转移到了部队,可想而知了,到了部队可要有我们好果子吃了。
不过这次事件倒是让我明白了一个道理,周围环境和气氛对一个人的影响。当气氛达到某种境界的时候,我自己已经不受自己支配了,热血沸

腾,大家一下子就冲动起来了。要不曾经有一个哥们,陪他女朋友逛街,看到路边的献血车,本来不打算献血的,但是听到那里播放着孙悦等

人的献血歌曲,而且声音很大,旋律也很特别。那哥们说他刚听了几句就热血沸腾,还没扎针就有要向外喷的趋势,再加上要在刚好不久的女

朋友面前逞英雄,大步流星的走上了献血车… …“我他*就是一傻*,献什么血呀!那孙悦那几个也是,那个唱的还真他妈扇情!!!我受

得了吗我”

12.军训生活(下)–距离美
终于到了军营,刚刚几天的训练下来,已经尝到了苦头。病的病(30%的人装病,1%的人成功),伤的伤(最离奇的受伤是一哥们夜里做梦梦

见教官狂喊“向左转”,此兄睡梦中向左翻身2-3次后从上铺掉了下来,摔骨折了…)。可对像我这样有女朋友的人来说,男女分开训练,平

时不能接触的规定真是让人难以忍受。突然间,想起了电影《鸡毛信》,对,没错。我终于提起笔来洋洋千字,把思念之情写了下来。信写好

了,关键是怎么送过去。于是在自由活动时间,装作闲逛并且与我班女生“偶遇”,神速把纸条递给她并跟她说明原因。好在以前在看革命电

影的时候这种镜头多的是,所以没有被教官发现顺利接头成功。她读过了信迅速的回了信。就这样,我们在地下维持着联系。看来我们的爱国

主义教育,自己经常观看爱国主义题材的影片真的是很有用的阿…
评论:
其实,现在电话手机这么流行,我们通信都利用这种高科技的东西,又是后感觉挺没意思的,如果你要是偶尔的亲手写上这么一封信,真的会

让人觉得很特别的。大家记住哦…


13.找到了我以前的一个用具
希望对大家有用
这是我拜访一位高数强人才计算出来的,
我高数很差所以不要深究啊




14.最后再教你一招(情侣手套)
记起了一个我也没有实施的不成熟的想法。以前在学校里,一般男孩子都是比较邋遢的,而且很不会照顾自己,冬天在学校里经常看到一男一

女手拉着收散步,一般都是女孩子戴着手套,男孩子把收缩到袖口里。我想如果制作这么一种情侣手套应该很好吧?方案如下:
男孩:那种露出半个手指的手套(小时候好像叫霹雳手套)
女孩:平常的毛线手套,样式随意,只不过在手套的手新处缝制4个小兜,每个小兜半个手指深,供男孩的四个手指插入,在手背处缝制1个小

兜,共男孩大拇指插入,可根据自己情况的不同调整位置和深度。
这样,拉着手的时候可以互相取暖,还能够随时分开。我想你自己要是制作这样一个情侣手套送给她,嘿嘿。蛮不错的。不知你意下如何。赶

快行动吧
2005年12月19日

    本软件最主要功能是对于员工机器的ip拦截,ip拦截是指在服务器设置要拦截的ip,每个客户端将被禁止访问此ip(包括禁止任何软件、ie等访问此ip)。注意,并不是所有客户端都需要禁止所有在服务器显示的ip,这里面有个组的概念,也就是在服务器端指定要拦截的ip时需要指定此规则所作用的组。不在这个组内的客户端不受拦截。

    服务器端添加ip。添加ip可以通过添加ip段和单个添加ip来实现,可以把一个ip添加到多个组中,也可以针对某一个用户设置单独的拦截列表。当服务器有新的列表时,自动报告给客户端,让客户端更新。

    客户端开机时从服务器获得自己要拦截的Ip列表,开始拦截。每隔一定时间向服务器查询自己当前的列表是否正确。

    服务器端可以分别控制一个客户的各种状态,比如针对某个ip是否打开使用权等

2005年11月24日

 

关于foxmail的研究2—-找到帐户邮件箱的指定路径篇

 由于foxmail可以指定他收发邮件的保存目录,所以不能认定foxmail的安装目录就是他邮件的所在目录,通过研究发现,在foxmail的安装目录中有一个名为accounts.cfg的文件(通过注册表可以方便的查到foxmail的安装路径),在此文件中记载着foxmail的邮件路径。下面首先说明一下accounts.cfg文件的格式。首先将此文件打开,直接将文件指针定到800H位置。800H-83FH这40个字节记载文件信息,可以称作文件头部,我们只关心807这个字节,他代表当前用户帐户的个数。从844H开始记载第一个帐户的信息。

下面按照每一个帐户信息讨论(即下面所列出的地址都加上844H才是真正的地址):

00H 表示每个帐户的编号

04H-07H 表示帐户名称字符串的长度

08H- 表示帐户名称,长度由04H-07H记载

帐户名称的下4个字节表示 此帐户目录字符串长度

紧接着 记录此帐户的邮件存放目录,字符串长度由上一项表示。

例:如图1

 

1

[800H] = 03H 说明有3个帐户

[844H] = 01H 他是第一个帐户的起始点

[848H向后的4字节] = 00 00 00 04H 说明帐户名称 由4字节表示,我们读取后面的4个字节

[84cH后面的4个字节] 就是帐户的名称

[850H]-[853H] = 00 00 00 0FH 说明此帐户邮件存储路径 有后面的0FH个字节存储

[854H后面的0FH字节] = 此帐户邮件存储路径。

向下搜索到 6cH 69H 6eH 73H 68H 69H,表示此帐户信息结束,向下的第四个字节开始记载下一帐户信息.

如此循环,嘿嘿,你就可以得到所有帐户邮件的存储路径了,此目录下包括收件箱、发件箱、已发送邮件箱、垃圾邮件箱、废件箱。下次再具体分析他们。

现在使用FoxMail的用户越来越多了,但是FoxMail却不像OutLook一样给编程人员提供开发接口。我认为这对一个好的软件的推广来说是很不利的。前几个月小弟由于开发需要对FoxMail进行了以下研究,这几天闲下来将材料整理了一下,供大家参考。我使用FoxMail4.2进行研究,但是在FoxMail5.0推出后,我也进行了测试,仍然使用。

FoxMail的数据保存,都是由两个文件组成 .Ind 和 .Box的文件,Ind 文件主要记录简单信息,以及详细信息在.Box文件中的起始位置。

地址簿:

默认情况下,FoxMail的地址簿由两个文件组成,Address.ind 和 Address.box。

Address.Ind文件

文件头的范围为00H-39H,共64个字节。

05H保存纪录的个数,12H-21H记录地址薄名称。

记录部分40H开始是第一条记录,每条记录长B0H

每条记录的00H是记录号(从01开始顺序编号),04H是删除标记,该字节为1表明此记录被用户删除(由此可见在FoxMail中删除的信息没有被真正的删除)

05H06H纪录在.BOX中的地址如 [05H]=01,[06H]=EA 那么该项纪录的详细信息被保存在Address.Box文件中起始位置为01FA

09H0AH此记录在.Box文件中详细信息的字节数目加上156H(记录整条记录在Box文件中的总长度)

11H是组记录,该字节为1表明此记录为一条组记录;(我没有对此项进行详细研究)

12H是此项纪录名称的长度,最大为20H;

13H-32H是此项纪录名称的内容

33H是邮件地址长度,最大为40H

34H-73H是邮件地址内容

Address.Box文件

每条记录由“_____________S”开始,后加一回车符(0A0D)

紧接着是详细信息,分别由以下几项组成(按顺序排列)

Emails

Mobile

PagerNum

OICQ

ICQ

HomePage

Sex

Birthday

FmCountry

FmProvince

FmCity

FmPostcode

FmStreetAddr

HomeTel

HomeTel2

FmFax

Company

OfCountry

OfProvince

OfCity

PostCode

HomeAddress

OfHomePage

OfPosition

OfDepartment

OfficeTel

OfficeTel2

Fax

构造格式:名称 + 冒号 + 0A0D + 信息内容 + 0A0D

最后一项构造完成 要加入2个 0A0D

实例1:

从FoxMail中遍历地址簿联系人姓名和EMail信息:

bool GoAll(char *szFileNameOfInd)

char headbuf[0x41];        // 头buf

char recbuf[0xb1];           // 内容buf

        FILE* pf=fopen(szFileNameOfInd,"rb");

        fseek(pf,0×40,SEEK_SET);        // 到项目开始

        while(!feof(pf))

        {

                char name[0x21],email[0x41]; // 定义存储姓名和email的字符数组

                for(i=0;i<0×21;i++)

                        name[i]=“\0“;

                for(i=0;i<0×41;i++)

                        email[i]=“\0“;

                fread(recbuf,1,0xb0,pf);                // 读取一条记录

                if(recbuf[0x4]==1)                              // 判断删除标志

                {

                       continue;

               }

                if(recbuf[0x11]==1)                     // 判断组标志

                {

                       continue;

               }

                for(i=0;i<recbuf[0x21];i++)             // 获取此条目的联系人姓名

                {

                        if(recbuf[i+0x13]==0)

                                break;

                        name[i]=recbuf[i+0x13];

               }

                for(i=0;i<recbuf[0x33];i++)             // 获取此条目的联系人EMail

                {

                        if(recbuf[i+0x34]==0)

                                break;

                        email[i]=recbuf[i+0x34];

               }

                //此处添加处理name(收件人名),email(收件人地址)的程序

                        return true;

   }

 

实例2:

向地址簿中添加一条记录信息,没有给出全部代码。

 

1.      得到文件头

BOOL GetIndFileHead(char* szheadbuf)         // 得到文件头部的函数

{

        fseek(FoxWriter_pf_addr,0×0,SEEK_SET);

        long len=0;

        len=fread(szheadbuf,1,0×40,FoxWriter_pf_addr);

        if(len==0)

                return FALSE;

        return TRUE;

}

 

2.      修改项目数;

int itemcount=szheadbuf[0x5];

itemcount++;

szheadbuf[0x5]=itemcount; 

3.      修改项目代号

szbuf[0x0]=itemcount;

4.      创建项目名称

szbuf[0x12]=strlen(strItemName);

for(i=0;i<0×20;i++)

        szbuf[i+0x13]=strItemName[i];

5.      创建项目email

szbuf[0x33]=strlen(strEmails);

        for(i=0;i<0×40;i++)

        szbuf[i+0x34]=strEmails[i]; 

6.      构造Box文件buf

(略)

7.      创建项目对Box文件指针

char* pbuf=szbuf;

        pbuf++;

        short* pshbuf=(short*)pbuf;

    pshbuf[0x2]=point;

8.      创建项目字数值

long len=strlen(szBoxbuf) 

len+=0×156; 

9.      保存Ind和Box文件

        本文不是介绍怎样用纯VB实现HOOK API的,如果您想了解细节那么您必须懂得一些vc和很少的汇编知识,并且参考我的其它文章,如果您是忠实VB使用者那么您完全可以不去了解内部的原理,因为那些是vc干的事情(不要不爱听,我没有种族歧视的倾向,因为我不知道怎样用VB实现它,我完全不会使用VB,为了本文才研究了几天,本文中涉及VB的代码也是非常非常的少,本文中涉及VB的代码都是前几天在CSDN上法帖问到的),您可以直接使用我提供的一个标准dll文件,并且像调用普通API一样调用几个简单的函数,并且在您的VB代码中实现您自己的拦截函数就可以了。好了,让我详细的说一下吧。

        首先,假设您希望了解我是怎样用VB HOOK API的,否则您应该继续滚屏,直到介绍怎样使用的那个段落。其次,如果您没有看我的另外的几篇关于VC下HOOK API的文章,那么您赶快去看,因为我下面要讲的内容假设您已经阅读过这几篇文章了,那个系列文章第一篇的地址是:HOOK API 跳转大法

        在系列文章中请注意一下关于通用函数的问题,因为这是本文实现的关键。我们回顾一下那个通用函数的功能。我们知道我们拦截到的任何API函数,都将进入到我们的通用函数中去,而通用函数是无参数无返回值类型的。我们是用汇编通过对堆栈的操作在不知道参数类型的情况下构造函数调用的堆栈并且调用用户的拦截函数。也就是说,你要想拦截一个API,那么,你告诉我你要拦截的API的名字,还有你处理这个API的替换函数,那么当我拦截到一个API的时候,就会调用你告诉我的处理这个API的替换函数。现在要解决的问题是,这个处理API的替换函数是在VB里面的!经过我不懈努力,终于知道VB里面怎样把函数地址传递出来了!(其实就是一个addressof操作符)。好了,现在HOOK API要工作的所有条件都具备了,开始工作吧。

        修改《防火墙原理及实现》里面的代码,其实只要修改初始化的代码,原来是从一个VC写的dll文件里获得要拦截的API的信息,主要是通过GetHookApiInfo函数获得HOOKAPIINFO结构,然后把HOOKAPIINFO结构转化成APIINFO结构就可以了,其实关键点还是怎样获得APIINFO结果。

        在VB里面,创建一个ActiveX DLL项目。这样您就创建了一个Com组件,默认的项目就为您创建了一个类模块。这个类模块中您必须添加一个方法,叫做GetHookApiInfo,它的返回值是APIINFO_VB类型的数组,这个类型的VB定义如下:
Public Type HOOKAPIINFO_VB
    MyApiFuncAddr As Long   ‘我们自己的函数地址
    OrgModuleName As String ‘要拦截的API所在的模块名称
    OrgApiName As String    ‘要拦截的API名称
    ParamCount As Long      ‘要拦截的API的参数个数
End Type

还要提供一个获得函数地址的方法,很简单,如下:
Private Function GetAddr(ByVal address As Long) As Long
    GetAddr = address
End Function

那么,GetHookApiInfo方法结构大致如下
Public Function GetHookApiInfo()
    Dim Param(1) As HOOKAPIINFO_VB
    Param(0).MyApiFuncAddr = GetAddr(AddressOf message)
    Param(0).OrgApiName = "MessageBoxA"
    Param(0).OrgModuleName = "User32.dll"
    Param(0).ParamCount = 4
    GetHookApiInfo = Param
End Function

这里只是要拦截MessageBoxA函数。当然,我们还要添加一个模块,并定义拦截函数,message,当目标进程执行要拦截的函数,此处是MessageBoxA的时候,将调用VB代码的message方法。在c++的CHooApi类的初始化函数中我们将获得APIINFO_VB结构数组。与vb的APIINFO_VB结构对应的c++结构是_HOOKAPIINFO_VB结构,定义如下:
typedef struct _HOOKAPIINFO_VB
{
 // 我们自己的函数地址
 DWORD dwMyApiFuncAddr;
 // 要拦截的API所在的模块名称
 BSTR bstrOrgModuleName;
 // 要拦截的API名称
 BSTR bstrOrgApiName;
 // 要拦截的API的参数个数
 long ParamCount;
} HOOKAPIINFO_VB;

为了调用VB代码中的GetHookApiInfo函数来获得HOOKAPIINFO_VB结构,我们CHookApi的初始化代码这样写:
CoInitialize(NULL);
 CLSID clsid = {0×2ED7F30B,0×2135,0×4DE1,{0×99,0×22,0xEE,0xB7,0×02,0×1E,0×8F,0xB4}};
HRESULT hr = CoGetClassObject(clsid,CLSCTX_INPROC_SERVER,NULL, IID_IClassFactory, (void**)&m_pCF);
 m_pCF->CreateInstance(NULL,IID_IDispatch,(void**)&m_pDsp);
 OLECHAR *szGetHookApiInfo[]={L"GetHookApiInfo"};
 DISPID dispID;
 hr = m_pDsp->GetIDsOfNames(IID_NULL,szGetHookApiInfo,1,LOCALE_SYSTEM_DEFAULT,&dispID);
 DISPPARAMS params = {NULL,NULL,0,0};
 VARIANT vResult;
 hr = m_pDsp->Invoke(dispID,IID_NULL,LOCALE_SYSTEM_DEFAULT,DISPATCH_METHOD,&params,&vResult,NULL,NULL);
 m_pDsp->Release();
 m_pCF->Release();

当然这里省略了错误处理。上面这段代码是通过VB的IDispatch接口调用GetHookApiInfo方法,这里我们需要你的VB写的Com组件的CLSID的值,不用我告诉你怎么得到这个值了吧?这里其实可以通过配置文件获得这个值,那么会更有通用性。到此HOOKAPIINFO_VB结构数组就在vResult里面了,下面我们把它拿出来,并转化为APIINFO结构,代码如下:
for(ULONG i = 0; i < vResult.parray->rgsabound->cElements; i++)
 {
  HOOKAPIINFO_VB *phai = (HOOKAPIINFO_VB*)vResult.parray->pvData;
  if(phai == NULL)
   continue;
  char szOrgModuleName[100];
  char szOrgApiName[50];
  if(!WideCharToMultiByte(CP_ACP,0,phai->bstrOrgModuleName,-1,szOrgModuleName,99,NULL,NULL))
   continue;
  if(!WideCharToMultiByte(CP_ACP,0,phai->bstrOrgApiName,-1,szOrgApiName,99,NULL,NULL))
   continue;
  APIINFO *pai = new APIINFO;
  // 保存参数个数
  pai->ParamCount = phai->ParamCount;
  // 设置此API是否已经被HOOK
  pai->bIsHooked = FALSE;
  // 内存保护标志
  pai->dwOldProtectFlag = 0;
  // 我们的函数地址
  pai->lfMyApiAddr = (CMAPIFUNC)phai->dwMyApiFuncAddr;
  // 要拦截的函数地址
  HMODULE hmod = GetModuleHandle(szOrgModuleName);
  if(hmod == NULL)
  {
   delete pai;
   continue;
  }
  pai->lfOrgApiAddr = (CMAPIFUNC)GetProcAddress(hmod,szOrgApiName);
  if(pai->lfMyApiAddr == NULL)
  {
   delete pai;
   continue;
  }
  strcpy(pai->szOrgApiName,szOrgApiName);
  m_vpApiInfo.push_back(pai);
 }

到这里我们已经填充了m_vpApiInfo容器,其他的HOOK操作就和二、防火墙原理及实现 (1)里面写的差不多了。

下面我说一下二、防火墙原理及实现 (1)一文当中为了符合我们的VB代码所要修改和注意的地方。

1.关于调用约定

    我在开始实现这个程序的时候遇到的各种弹窗的情况,当时的情景那叫一个壮烈。后来才知道原来VB模块里面的函数的调用约定是__stdcall的,也就是说,堆栈由函数自己平衡,不需要调用者干预,而我在二、防火墙原理及实现 (1)一文当中的CommonFunc的代码是回调调用c类型的函数,所以调用VB的函数会有问题,解决办法是把堆栈平衡的代码删除,修改后的代码如下:

void CHookApi::CommonFunc(void)
{
 DWORD dwlpFunc; // 替换函数的地址 __stdcall
 DWORD* pdwCall;// 保存被调用前压在栈中的返回地址,也就是Call XXXX 的地址
 DWORD* pdwESP;// 保存ESP内容
 DWORD* pdwParam;  // 第一个参数的地址
 DWORD dwParamSize; // 所有参数所占用的大小应该=4* dwParamCount
 DWORD dwRt;  // 保存返回值
 DWORD dwRtAddr;  // 我们的函数真正要返回的地址
 PROCESS_INFORMATION *pPi;// 进程信息

 // 得到栈中第一个参数的位置
 _asm
 {
  mov EAX,[EBP+8]
  mov [dwRtAddr],EAX
  lea EAX,[EBP+4]  // call XXXX所在的地址
  mov [pdwCall],EAX
  lea EAX, [EBP+12] // 第一个参数所在地址
  mov [pdwParam],EAX
 }
 (*pdwCall) -= 5;
 vector<APIINFO*>::iterator it;
 APIINFO* pai = NULL;
 for(it = m_vpApiInfo.begin(); it != m_vpApiInfo.end(); it++)
 {
  APIINFO* papiinfo = *it;
  if((DWORD)papiinfo->lfOrgApiAddr == *pdwCall)
  {
   pai = *it;
   break;
  }
 }
 if(pai == NULL)
  return;
 BYTE* pbtapi = (BYTE*)pai->lfOrgApiAddr;
 dwParamSize = 4*pai->ParamCount;
 EnterCriticalSection(&pai->cs);
 // 恢复被修改的5个字节
 memcpy(pbtapi,pai->OrgApiBytes,5);
 pai->bIsHooked = FALSE;

 dwlpFunc = (DWORD)pai->lfMyApiAddr;
 // 构造参数
 _asm
 {
  sub esp,[dwParamSize]
  mov [pdwESP],esp
 }
 memcpy(pdwESP, pdwParam, dwParamSize);

 //COMMONFUNC myapifunc = (COMMONFUNC)pai->lfMyApiAddr;
 //pai->lfMyApiAddr();// 调用替换API的函数
 _asm
 {
  call dwlpFunc
  // 保存返回值
  mov [dwRt],eax
 }
 // 如果是CreateProcess,那么继续hook它
 pPi = (PROCESS_INFORMATION*)pdwParam[9];
 if(strcmpi(pai->szOrgApiName,"CreateProcessA") == 0 || strcmpi(pai->szOrgApiName,"CreateProcessW") == 0)
 {
  InjectDll(pPi->dwProcessId,m_szDllPathName);
 }
 // 再修改5字节
 pbtapi[0] = CALLCODE;//jmp
 DWORD* pdwapi = (DWORD*)&(pbtapi[1]);
 pdwapi[0] = (DWORD)CommonFunc – (DWORD)pbtapi – 5;// 我的api的地址偏移
 pai->bIsHooked = TRUE;

 LeaveCriticalSection(&pai->cs);

 // 下面准备返回的操作
 _asm
 {
  //add esp,[dwParamSize] // 清理我们为了调用真正的替换函数而分配的堆栈里的参数
  mov EDX,[dwRtAddr] // 保存返回地址
  mov EAX,dwRt // 设置返回值
  mov ECX,[dwParamSize] // 获得参数的大小
  // 下面弹出所有保存的寄存器值(按照入栈的逆顺序)
  pop EDI // 恢复EDI
  pop ESI // 恢复ESI
  pop EBX // 恢复EBX
  // 我们没有改动过EBP的值,所以EBP指向堆栈中OldEBP的位置
  mov ESP,EBP
  pop EBP // 恢复EBP
  // 由于堆栈中还剩下参数和两个返回地址(我们真正要返回的地址和原始API中的第6个字节的地址),所以我们把这些数据也清除出堆栈
  add ESP,8 // 清除两个返回地址
  add ESP,ECX // 清除参数
  // 由于调用ret返回时,程序先从堆栈中取出返回地址,所以我们把要真正返回的地址压入堆栈中
  push EDX
  ret // 返回
 }
}

2.关于VB的字符串

为了解决字符串问题,我也想过了很多种方法,但是都失败了,造成这种麻烦的主要原因是VB的字符串都是BSTR类型的,而****A类型的API都是普通的ANSI类型的字符串。因为在我的通用函数中,不知道那个参数或者返回值是字符串。所以我把这个麻烦扔给了VB的使用者,让你们来处理这个问题。比如拦截MessageBoxA这个函数,那么VB的拦截函数应该这样声名:
Public Function message(ByVal h As Long, ByVal msg As Long, ByVal title As Long, ByVal nType As Long) As Integer
    Dim mymsg As String
    Dim strlen As Long
    mymsg = SysAllocString(msg)
    MsgBox mymsg, vbOKOnly, "liutao hooked"
End Function
也就是第二个和第三个参数,用两个long类型的变量保存LPCTSTR类型的字符串指针,然后通过SysAllocString来构造BSTR进而把它转化成VB的字符串类型。SysAllocString的声名如下:
Private Declare Function SysAllocString Lib "oleaut32" (ByVal OLECHAR As Long) As String

到此,我们解决了在VC代码中回调VB写的DLL的所有问题,那么我的HOOK API也就能够正常地工作了。

2005年10月30日

    关于NAT的穿透,我有一些不成熟的想法。

    对于Full Cone NAT类型的NAT,完全可以通过服务器握手进行通信,例如A和B都位于Full Cone NAT后面,那么,A和B都向具有公网IP的服务器S进行注册,这时候服务器S记住A和B被分配的公网IP和端口,并相互告知对方的IP和端口,那么A和B就可以互相通信了。穿透成功。

    对于Address Restricted Cone NAT和Port Restricted Cone NAT这两种类型的NAT来说,我们可以这样进行穿越。例如A和B分别位于这两种类型的NAT后面,A要发请求给B。那么A先向具有公网IP的服务器S发请求,这样,服务器S获知了A的公网IP和端口,S通过任意途径通知B(例如B注册时候的端口),B分配一个端口先向服务器S发送回应,然后向A的公网IP和端口发送请求包,这时候,从A的公网IP和端口发送的数据B都可以接收了。服务器S把B的公网IP和端口告知A,A再用公网IP和端口向B发送信息,这样,A和B都可以使用自己当前的端口向对方发送信息了。穿透成功。

   对于Symmetric NAT类型的NAT是比较复杂的。也没有好的解决方案。我这里提出一个方案,使用IP地址欺骗的方法。例如A和B都位于这种NAT的后面(且使用STUN协议知道自己和对方位于这种NAT后面)。那么,A要想向B发送请求,则A先向具有公网IP的服务器S发送请求。那么服务器S就能获得A的公网IP和端口,称为A{ip,port},B也向S发送请求,S得知B的公网IP和端口B{ip,port}。然后A在发送数据的时候修改自己数据包的源IP和端口为S的,然后发送到B,这样B是允许此数据包通过的,因为它认为这个数据包是S发送过来的。同理,B也可以用这种方法发送给A。当然这样需要对数据内容做一定的处理,在数据中包含自己真正的公网IP和端口,对方收到信息后,解析出你真正的IP和端口,告知上一层,这样能够对你所做的处理进行透明。这种方式的穿透可能有一些取巧的成分,但是应该是可以的,我正在进行实验。

    综合以上3种方法,提取出本机代理的概念,那么自己写一个代理端口,进行NAT的检测和穿透,那么可以对所有的NAT类型进行穿透,且对这些NAT类型适用不同的方法穿透。

    你有什么意见或者建议,或者对这些方法,特别是最后一种方法有什么看法和不满意的请和我联系,我觉得一些防火墙或者严格进行IP地址回溯的安全产品会阻碍最后一种穿透。我询问过一个防火墙厂商的技术人员,他说他们没有对这种IP欺骗进行处理。

2005年09月21日

    对于考勤系统,第一版本做得很简单,只是记录统计员工一天中最早的上线时间和最晚的下线时间。这个模块应该是成对出现的,也就是说,客户端和服务器端要遵循相同协议。

2005年09月05日

  今天比较有时间,先大概的说一下这个软件的功能,不是最终的功能描述,只是把一些想法提出来。
  前一阵子了解了一下现在公司里面对员工管理方面的问题,包括我在自己公司的切身体会,公司对员工需要监控的方面可能主要集中在考勤和工作中电脑的使用限制上。这样有些公司使用抓屏软件定时对员工的电脑屏幕进行抓取,从而起到督促作用,我认为这样不能保证员工的隐私权。所以本软件主要从这两个方面考虑,提供解决办法。
  对于员工的机器,安装本软件的客户端,客户端启动时自动连接到服务器,关闭时断掉与服务器的连接,这样对于一般性的上下班时间就有了参考依据,也就是一天当中第一次开机和最后一次关机时间就可以当做员工当天的上下班时间。而当软件运行当中,对于员工的计算机的网络和软件运行进行监控,即可以组织员工浏览某些IP段的网络,可以组织员工启动某些特定应用软件。
  也就是说,考勤管理的功能主要监控员工的上下线时间,对员工电脑的管理主要分为网络和软件的拦截这两个大功能模块。服务器确定软件和网络拦截策略,客户端执行。

2005年07月01日

什么是HOOK API

       Windows下暴露的对开发人员的接口叫做应用程序编程接口,就是我们常说的API。我们在写应用层应用程序软件的时候都是通过调用各种API来实现的。有些时候,我们需要监控其他程序调用的API,也就是,当其他应用程序调用我们感兴趣的API的时候,我们在他调用前有一个机会做自己的处理,这就是HOOK API的涵义。

 

 

思路:

       我们知道Windows系统API函数都是被封装到DLL中,在某个应用程序要调用一个API函数的时候,如果这个函数所在的DLL没有被加载到本进程中则加载它,然后保存当前环境(各个寄存器和函数调用完后的返回地址等)。接着程序会跳转到这个API函数的入口地址去执行此处的指令。由此看来,我们想在调用真正的API之前先调用我们的函数,那么可以修改这个API函数的入口处的代码,使他先跳转到我们的函数地址,然后在我们的函数最后再调用原来的API函数。下面以拦截WS2_32.dll中的recv函数为例说明拦截的主要过程。首先把自己编写的DLL挂接到系统当前运行的所有进程中(要排除一些Windows系统自身的进程,否则会出现问题,影响系统正常工作),挂接的意思是,要我们的DLL运行在目标进程的地址空间中。可以使用列举系统进程然后用远程线程注入的方法,但是这种方法只适用于Win2000以上的操作系统。

       当我们的DLL被所有目标进程加载后,我们就可以进行真正的工作了。首先使用Tool Help库的相关函数列举目标进程加载的所有模块,看看是否有ws2_32.dll,如果没有,说明这个进程没有使用Winsock提供的函数,那么我们就不用再给这个进程添乱了。如果找到ws2_32.dll模块,那么OK,我们可以开工了。先是用GetProcAddress函数获得进程中ws2_32.dll模块的recv函数的入口地址,也就是函数的起始地址。刚才说过,我们想把recv函数起始位置加入一条跳转指令,让它先跳转到我们的函数中运行。跳转指令可以用0xE9来表示(0xE9是汇编语言中CALL指令的机器码),后面还有4个字节的我们函数的相对地址。也就是我们要修改recv函数前5个字节。这5个字节由1个字节的跳转指令和4个字节的地址组成。这样当程序运行到这里的时候,将会跳转到这4个字节表示的地址处去运行代码。还要注意的是这4个字节的地址是偏移地址,而偏移地址 = 我们函数的地址API函数的地址 – 5(我们这条指令的长度)。好了,别忘了我们要先读取稍后要被覆盖的recv函数入口处的5个字节的内容,把它保存起来留着以后恢复时使用。因为在我们的函数中要想调用真正的recv的时候,必须把它前5个字节恢复了,他才能正常工作呢。

       通过上面的说明,我们可以整理出这样的一个流程:

1.  保存recv的前5个字节的内容

2.  recv的前5个字节的内容改变成CALL xxxxxxxx是我们的函数的偏移地址)

3.  在我们的函数中恢复recv的前5个字节的内容,并作处理。

4.  我们的函数返回后,再把recv的前5个字节的内容改变成CALL xxxx

慢着,你一定发现问题了吧?当我们为了调用原来的recv函数而刚刚把recv入口处的5个字节恢复,这时系统中的其他线程调用了recv函数,而这个调用将会成为漏网之鱼而不会进入到我们的函数中来。简单的解决办法是使用临界对象CriticalSection来保证同时只能有一个线程对recv函数入口处5个字节进行读写操作。

最后记得在你想要停止拦截的时候恢复所有你修改过的进程和这些进程中被修改的API的前5个字节。其实原理讲着容易,在实现的时候会遇到各种各样的问题,如98下这些系统的DLL被加载到系统内存区供应用程序共享,所以这些内存是受保护的,不能随意修改,还有nt/2000下权限问题,还要考虑到不要拦截某些系统进程,否则会带来灾难性的后果。这些都是在实践当中遇到的实际问题。

 

 

下面结合代码给大家讲解一下吧,首先我们要实现HOOK模块,我们给它起个名字叫做MainHookDll.DLL。在此模块中,主要要实现一个CHookApi的类,这个类完成主要的拦截功能,也是整个项目的技术核心和难点,后面将具体介绍它。而且,MainHookDll模块就是将来要注入到系统其它进程的模块,而远程调用函数是非常困难的事情,所以我们设计此模块的时候应让其被加载后自动执行拦截的初始化等工作。这样,我们只需要让远程的进程加载HOOK,然后MainHookDll.dll就能够自动执行其它操作从而HOOK该进程的相关API

 

 

MainHookDll模块中的CHookApi类拥有2个向外部提供的主要的方法,HookAllAPI,表示拦截指定进程中的指定APIUnhookAllAPI,表示取消拦截指定进程中的指定API。进行具体设计的时候,会遇到一个问题。大家看到,上文所说的开始将原始API的前5个字节写成CALL XXXX,而在我们的替换函数中要恢复保存的API原始的5个字节,在调用完成后又要把API5个字节改为CALL XXXX。如果我们拦截多个API要在每个替换函数中按照如上的方法进行设置,这样虽然我们自己明白,但是可能您只是实现HOOKAPI部分,而别人实现调用,这样会使代码看起来很难维护,在别人写的替换函数中加上这些莫名奇妙的语句看来不是一个好主意,当需要拦截多个感兴趣的API函数,那样的话将会在每一个要拦截的函数里都有这些莫名其妙的代码将会是件很恶心得事情。而且对于CALL XXXX中的地址,要对于不同的API设置不同的替换函数地址。那么能不能把这些所有的函数归纳为一个函数,所有的API函数前5字节都改为CALL到这个函数的地址,这个函数先恢复API的前5字节,然后调用用户真正的替换函数,然后再设置API函数的前5字节,这样可以使真正的替换函数只做自己应该做的事情,而跟HOOK API相关的操作都由我们的通用函数来干。

 

 

这样的想法是好的,但是有一个突出问题,因为替换函数的函数声明与原API一致,所以对于要拦截的不同的API,它们的的参数和返回值是不一样的。那我们怎样通过一个函数获得用户传递给API的参数,然后使用这些参数调用替换函数,最后把替换函数的返回值再返回给调用API的客户?要想实现这个功能,我们需要了解一个知识,也就是C++究竟是怎样调用一个函数的。我们以ws2_32.dll中提供的recv函数为例进行说明,recv函数的声明如下:

        int recv(

            SOCKET s,

            char* buf,

        int len,

            int flags

            );

可以看出它具有4个参数,返回值类型是int。我们作如下调用:

        recv(s,buf,buflen,0);

    那么在调用recv前,这四个参数将按照从右向左的顺序压到栈中,然后用Call指令跳转到recv函数的地址继续执行。recv可以从栈中取出参数并执行其他功能,最后返回时返回值将被保存在寄存器EAX中。最后还要说明一点的是,在汇编语言看来这些参数和返回值都是以DWORD类型表示的,所以如果是大于4字节的值,就用这4个字节表示值所在的地址。

 

 

有了这些知识我们就可以想到,如果用户调用recv函数并被拦截跳转到我们的函数中运行,但是我们并不知道有多少个参数和返回值,那么我们可以从栈中取出参数,但是参数的个数需要提供,当然我们可以在前面为每个API函数指定相应的参数个数,然后运行真正的替换函数,最后在返回前把替换函数的返回值放到寄存器EAX中,这样就解决了不知道参数和返回值个数的问题。那么我们的函数应该是看起来无参数无返回值的。

 

 

        基本原理我们大家都清楚了,但是继续之前我还是想讲一讲几个汇编的知识,如果没有这些知识那么看下面的代码就好像天书一样。

关于参数

        我们讲过,在调用一个子函数前要把参数按顺序压栈,而子函数会从栈中取出参数。对于栈操作,我们一般使用EBPESP寄存器,而ESP是堆栈指针寄存器,所以多数情况下使用EBP寄存器对堆栈进行暂时操作。还是用调用recv函数为例,假设调用前ESP指向0×00000100处(程序运行时ESP是不可能为这个值的,此处只是为了举例说明问题)。先将参数一次压栈

        push 0          // flags入栈

        lea eax, [len]

        push eax        // len入栈

        lea eax, [buf]

        push eax        // buf入栈

        lea eax, [s]

        push eax        // s入栈

        下面使用call调用真正的recv函数,

        call dword ptr [recv] // 调用recv

        call指令先将返回地址压入栈中,返回地址就是CALL指令的下一条指令的地址,然后跳转到recv入口地址处继续执行。进入recv后,recv使用EBP临时访问堆栈之前,要保存EBP的当前内容,以便以后再使用(在 关于调用函数时保存各个寄存器的值 部分将详细讨论)。所以位于recv函数开始可能是这样的

        push ebp    // 保存ebp的当前值

        mov ebp,esp // 使把esp负给ebp

堆栈指针

[ESP]

堆栈的内容

堆栈内容的含义

0×00000100

flags

参数

0×000000fc

len

参数

0x000000f8

buf

参数

0x000000f4

s

参数

0x000000f0

RetAddress

返回的地址

0×00000ec

OldEBP

保存EBP的当前值

 

 

    到此,我们可以知道,如果现在要想通过EBP获得最后一个入栈的参数,那么需要用EBP+8来获得,因为最后一个入栈的参数被保存在返回地址和EBP原始值的上面(一定记住,栈是由高地址到低地址的)。而返回地址被放在EBP+4处,EBP的原始值放在EBP+0处。

 

 

    关于调用函数时保存各个寄存器的值

        当我们要调用其它函数的时候,程序应该先保存各个寄存器的值,然后转去调用其它函数,最后会恢复各个寄存器的值使它们恢复成调用其它函数之前的状态。当然我们使用高级语言写程序的时候,编译器为我们做了这些事情。使用vc调试程序,打开反汇编窗口。运行一个简单的程序,该程序调用一个我们自己写的简单函数,在这个简单函数中设置断点,可以看到,编译器生成的汇编代码使用堆栈保存各个寄存器的值,上面提到当执行一个函数的时候,首先保存的是EBP的值,然后依次压入栈中保存的寄存器为EBXESIEDI,我们在恢复这些寄存器的值的时候将逆向出栈来完成。

    关于函数调用的返回

        调用子函数前ESP指针会因为压栈参数而改变,然后压入返回地址等,子函数中会使用ret指令从栈中取出返回地址并跳转到返回地址,而在子函数返回到CALL的下一条指令时栈中还保存着参数,所以我们需要手工的将栈中的参数所占用的空间释放,如在调用完成一个4个参数的子函数后,我们应该将ESP指针上移4*4个字节,如

        add esp,16

    这个操作在调用API的时候是不需要的,因为,windows API在函数中自己将参数弹出堆栈了。所以这就有一个调用约定的问题,默认情况下调用约定是__cdecl,表示参数从右向左入栈,由调用者清理参数。而windows API使用的是__stdcall调用,表示参数从右向左入栈,由函数自己清理参数。我们的程序API的替换函数使用__stdcall调用约定。所以不用考虑清理栈中的参数的问题,由替换函数自己处理。

       

    有了上面的知识,让我们回顾一下先前讨论的问题,首先,用户调用APIrecv函数,程序运行到recv的入口地址处,此时堆栈中拥有用户调用recv的参数和用户代码中CALL [recv]的下一条指令的地址。堆栈如下图:

堆栈指针

[ESP]

堆栈的内容

堆栈内容的含义

0×00000100

0

参数

0×000000fc

len

参数

0x000000f8

buf

参数

0x000000f4

S

参数

0x000000f0

RetUserAddress

用户调用recv的下一条指令的地址

 

 

然后程序指针EIP被修改为recv入口处的地址,而入口地址处有一条简单的CALL指令,它使程序将recv的第6个字节的地址压入栈中(因为CALL XXXX占用5个字节,第六个字节被认为为返回地址),然后跳转到我们的无参数无返回值的通用替换函数中去了,好了看看现在堆栈中都有些什么?如图:

堆栈指针

[ESP]

堆栈的内容

堆栈内容的含义

0×00000100

0

参数

0×000000fc

len

参数

0x000000f8

buf

参数

0x000000f4

s

参数

0x000000f0

RetUserAddress

用户调用recv的下一条指令的地址

0×00000ec

RetrecvAddress

recv的第六个字节的地址

 

 

首先是参数,其次是用户调用recv后的返回值,然后是recv调用我们的替换函数中的返回值,紧接着就像刚才提到的那样,程序将EBP当前内容压入栈中。如图

堆栈指针[ESP]

堆栈的内容

堆栈内容的含义

0×00000100

0

参数

0×000000fc

len

参数

0x000000f8

buf

参数

0x000000f4

S

参数

0x000000f0

RetUserAddress

用户调用recv的下一条指令的地址

0×00000ec

RetrecvAddress

recv的第六个字节的地址

0×000000e8

OldEBP

保存的旧的EBP的内容,然后[EBP]= 0×000000e8

0×000000e4

OldEBX

保存的旧的EBX的内容

0×000000e0

OldESI

保存的旧的ESI的内容

0×000000dc

OldEDI

保存的旧的EDI的内容,此时[ESP]=0×000000dc

 

 

此时我们可以看到,[EBP]为保存的ebp的值,现在对我们没有用处,函数返回前用于恢复EBP的值,[EBP+4]recv函数的CALL XXXX后面指令的地址(也就是第六个字节的地址),我们可以通过将此值减去5来得到recv的入口地址,这样在我们所有hookapi函数的列表中进行检索,就可以匹配出用户调用的是哪一个API函数,从而为后面恢复和再次改变该API的入口5字节做准备,因为调用任何我们需要HOOKAPI程序都会进入到这个无返回值无参数的函数,所以通过这种方法找到当前HOOK的是哪一个API,从而可以区分不同的API进行特殊的处理。[EBP+8]保存的是用户调用recv后的返回地址,由于我们执行完替换函数后,应该返回到这个地址,而不应该返回到recv的第6个字节处执行,所以我们还是需要保存下这个值,以便在我们用ret返回前把它压栈从而使程序返回到用户调用recv的下一条指令处继续运行。

我们现在实现它,请注意参考上面堆栈表格。

void CHookApi::CommonFunc(void)

{

     DWORD* pdwCall;// 保存被调用前压在栈中的返回地址,也就是Call XXXX 的地址

     DWORD* pdwESP;// 保存ESP内容

     DWORD* pdwParam;       // 第一个参数的地址

     DWORD dwParamSize; // 所有参数所占用的大小应该=4* dwParamCount

     DWORD dwRt;        // 保存返回值

     DWORD dwRtAddr;        // 我们的函数真正要返回的地址

     PROCESS_INFORMATION *pPi;// 进程信息

 

 

     // 得到栈中第一个参数的位置

     _asm

     {

         mov EAX,[EBP+8]

         mov [dwRtAddr],EAX

         lea EAX,[EBP+4]        // call XXXX所在的地址

         mov [pdwCall],EAX

         lea EAX, [EBP+12]  // 第一个参数所在地址

         mov [pdwParam],EAX

     }

     (*pdwCall) -= 5;

     vector<APIINFO*>::iterator it;

     APIINFO* pai = NULL;

     for(it = m_vpApiInfo.begin(); it != m_vpApiInfo.end(); it++)

     {

         APIINFO* papiinfo = *it;

         if((DWORD)papiinfo->lfOrgApiAddr == *pdwCall)

         {

              pai = *it;

              break;

         }

     }

     if(pai == NULL)

         return;

     BYTE* pbtapi = (BYTE*)pai->lfOrgApiAddr;

     dwParamSize = 4*pai->ParamCount;

     EnterCriticalSection(&pai->cs);

     // 恢复被修改的5个字节

     memcpy(pbtapi,pai->OrgApiBytes,5);

     pai->bIsHooked = FALSE;

     // 构造参数

     _asm

     {

         sub esp,[dwParamSize]

         mov [pdwESP],esp

     }

     memcpy(pdwESP, pdwParam, dwParamSize);

 

 

     COMMONFUNC myapifunc = (COMMONFUNC)pai->lfMyApiAddr;

     _asm

     {

        

         call myapifunc // 调用替换API的函数

         mov [dwRt],eax // 保存返回值

     }

     // 如果是CreateProcess,那么继续hook

     pPi = (PROCESS_INFORMATION*)pdwParam[9];

     if(strcmpi(pai->szOrgApiName,"CreateProcessA") != 0 || strcmpi(pai->szOrgApiName,"CreateProcessW") != 0)

     {

         InjectDll(pPi->dwProcessId,m_szDllPathName);

     }

     // 再修改5字节

     pbtapi[0] = CALLCODE;//jmp

     DWORD* pdwapi = (DWORD*)&(pbtapi[1]);

     pdwapi[0] = (DWORD)CommonFunc – (DWORD)pbtapi – 5;// 我的api的地址偏移

     pai->bIsHooked = TRUE;

 

 

     LeaveCriticalSection(&pai->cs);

 

 

     // 下面准备返回的操作

     _asm

     {

         mov EDX,[dwRtAddr] // 保存返回地址

         mov EAX,dwRt  // 设置返回值

         mov ECX,[dwParamSize]  // 获得参数的大小

         // 下面弹出所有保存的寄存器值(按照入栈的逆顺序)

         pop EDI  // 恢复EDI

         pop ESI // 恢复ESI

         pop EBX // 恢复EBX

         // 我们没有改动过EBP的值,所以EBP指向堆栈中OldEBP的位置

         mov ESP,EBP

         pop EBP // 恢复EBP

         // 由于堆栈中还剩下参数和两个返回地址(我们真正要返回的地址和原始API中的第6个字节的地址),所以我们把这些数据也清除出堆栈

         add ESP,8 // 清除两个返回地址

         add ESP,ECX // 清除参数

         // 由于调用ret返回时,程序先从堆栈中取出返回地址,所以我们把要真正返回的地址压入堆栈中

         push EDX

         ret // 返回

     }

}

要说明的一点是,如果要执行得API函数是CreateProcess,那么应该把它新开启得进程也HOOK掉。以上我们了解了通用替换函数的原理,那么让我们深入的讨论CHookApi类,并且实现它。

 

 

前面提到过,我们的CHookApi类主要向外部提供2个方法,HookAllAPI方法和UnhookAllAPI方法。当调用HookAllAPI的时候,将拦截系统所有用户程序中我们感兴趣的API函数。当调用UnhookAllAPI的时候,将撤销拦截。当拦截启动之前,我们应该将所有我们感兴趣的,即需要拦截的API信息(如API名称,对应的替换函数名称、参数个数等)交给CHookApiCHookApi类内部才能完成所有的拦截工作。以下是CHookApi类的声明

 

 

 

 

(HookApi.h)

#include "../include/CommonHeader.h"

#include <Psapi.h>

#include <tlhelp32.h>

#pragma comment(lib,"Psapi.lib")

 

 

// 通用替换函数指针声明

typedef void(*COMMONFUNC)(void);

 

 

/*

     CHookApi类实现了Hook的核心功能

*/

class CHookApi

{

public:

     // 构造函数

     CHookApi(void);

     // 析构函数

     ~CHookApi(void);

private:

     // 自身路径

     static char m_szDllPathName[MAX_PATH];

     // 此容器包含所有要HookAPI信息(在类中使用的有效信息)

     static vector<APIINFO*> m_vpApiInfo;

     // 此容器包含所有要HookAPI信息(用户填写的信息)

     vector<HOOKAPIINFO*>* m_vpHookApiInfo;

     // 防火墙策略模块句柄

     HMODULE m_hFireWall;

     // 加载防火墙策略模块

     BOOL LoadFireWallModule(void);

     // 卸载防火墙策略模块

     BOOL UnloadFireWallModule(void);

public:

     // 使用远程线程注入的方法将我们的DLL注入到指定进程

     static int WINAPI InjectDll(DWORD dwProcessId, LPTSTR lpDllName);

     // 使用远程线程注入的方法将我们的DLL卸载

     static int WINAPI EjectLib(DWORD dwProcessId, LPTSTR lpDllName);

protected:

     // 通用替换函数,这是技术核心部分

     static void CommonFunc(void);

private:

     // 初始化函数

     BOOL Init(void);

     // 挂钩一个指定API的函数

     BOOL HookOneApi(APIINFO* pai);

     // 挂钩所有指定API的函数

     BOOL HookAllApis(void);

     // 取消挂钩一个指定API的函数

     BOOL UnhookOneApi(APIINFO* pai);

     // 取消挂钩所有指定API的函数

     BOOL UnhookAllApis(void);

 

 

     // 设置进程内存区域的存取权限

     BOOL SetMemmoryAccess(APIINFO* pai,BOOL CanWritten);

     // 修改/恢复指定API的前5个字节

     BOOL SetCallMemmory(APIINFO* pai,BOOL bHook);

};

 

 

其中HOOKAPIINFO结构填写基本的拦截信息,在CHookApi内部将会把它转化为APIINFO结构。HOOKAPIINFOAPIINFO结构定义在CommonHeader.h文件中,CommonHeader.h文件如下:

CommonHeader.h

#define FIREWALLDLLNAME "ReplacelDll.Dll"                    // 实现被拦截的API替换函数的名字

#define MAINHOOKDLLNAME "MainHookDll.Dll"                    // 拦截API主模块的名字

#define GETHOOKINFOFUNCNAMEINREPLACEDLL "GetHookApiInfo"     // 替换模块导出函数,它返回要拦截的信息

#define FREEHOOKINFOFUNCNAMEINREPLACEDLL "FreeHookApiInfo"   // 替换模块导出函数,它释放要拦截的信息

 

 

#define CALLCODE   0xE8

 

 

#ifndef COMMONHEADER_H

#define COMMONHEADER_H

 

 

#include <vector>

using namespace std;

 

 

typedef void(*CMAPIFUNC)(void);

 

 

// 关于API函数信息的结构

typedef struct _APIINFO

{

     // 要拦截的API函数名

     char szOrgApiName[100];

     // 要拦截的API的地址

     CMAPIFUNC lfOrgApiAddr;

     // 我们要替换原来API的地址

     CMAPIFUNC lfMyApiAddr;

     // 要保存的原来API入口点的前5个字节

     BYTE OrgApiBytes[5];

     // 对进程内存的保护状态,调用VirtualProtect来改变对内存访问权限时得到的。

     DWORD dwOldProtectFlag;

     // 参数的个数

     int ParamCount;

     // 临界对象,为了互斥用,避免同时修改原始api的前5个字节

     CRITICAL_SECTION cs;

     // 是否已经HOOK

     BOOL bIsHooked;

 

 

// 用户需要使用的结构,通过这个结构来了解用户需要拦截的API的信息

typedef struct _HOOKAPIINFO

{

     union

     {

         struct

         {

              // 我们要替换的API所在的DLL的名字

              char szMyModuleName[100];

              // 我们要替换原来API的函数名字

              char szMyApiName[50];

         } MyApi;

         CMAPIFUNC lfMyApi;

     };

     // 要拦截的API所在DLL的名字

     char szOrgModuleName[100];

     // 要拦截的API的名字

     char szOrgApiName[50];

     // 参数的个数

     int ParamCount;

     // 提供MyAPI的方式

     BOOL bMyApiType;   // 如果是0代表提供地址,1代表使用模块名和函数名指定

} HOOKAPIINFO;

// 获得要拦截的API的信息函数指针

typedef vector<HOOKAPIINFO*>*(*GETHOOKAPIINFO)(void);

 

 

// 释放要拦截的API的信息函数指针

typedef void(*FREEHOOKAPIINFO)(void);

 

 

// 远程注入函数指针

typedef int (*INJECTDLL)(DWORD dwProcessId, LPTSTR lpDllName);

#endif

在这个头文件中,除了定义了HOOKAPIINFOAPIINFO结构还有一些其它定义,FIREWALLDLLNAME宏指定防火墙策略模块的文件名称,MAINHOOKDLLNAME宏指定本模块的文件名称,GETHOOKINFOFUNCNAMEINREPLACEDLL宏指定我们写的API替换函数所在的DLL(我们成为替换模块)中用来返回用户的HOOKAPIINFO结构容器指针的函数名称,程序将会自动加载替换模块并调用这个函数获得用户的HOOKAPIINFO结构容器的指针,根据后面的函数指针的定义不难看出,这个函数必须是一个返回值为vector<HOOKAPIINFO*>*,并且没有参数的导出函数。FREEHOOKINFOFUNCNAMEINREPLACEDLL 是释放返回的vector<HOOKAPIINFO*>*指针的导出函数名字。CALLCODE宏指定要替换的跳转指令,这里只用CALL指令,他的机器码是E8

在构造函数中调用Init函数和HookAllApis函数,这样使该类被构造的时候就能够自动进行初始化和hook工作。Init函数的主要工作是将用户提供的HOOKAPIINFO结构转化成APIINFO结构。HookAllApis函数内部循环调用HookOneApi函数进行真正的Hook操作。详见源代码中的注释。

CHookApi类完成了Hook的核心工作后,我们需要让系统所有的进程加载我们的MainHookDll.Dll,从而对系统所有进程中的指定API进行拦截。我们的想法是,MainHookDll模块提供2个导出函数,HookAllProcessesUnhookAllProcesses函数完成这个功能,当UserUI调用HookAllProcesses函数的时候,系统所有的进程将加载MainHookDll.Dll,在加载的同时让CHookApi类开始工作,对目标进程中的所有指定API的前5个字节进行替换。当调用UnhookAllProcesses函数的时候,系统所有的进程将卸载MainHookDll.Dll,从而取消对指定API的拦截。在HookAllProcessesUnhookAllProcesses函数中实际上是列举当前系统的所有进程,并对所有进程进行相同的操作。所以我们可以再实现两个函数,用CHookApi类的静态成员InjectDllEjectLib函数完成真正的功能,而HookAllProcessesUnhookAllProcesses中实现列举系统进程并对所有进程调用InjectDllEjectLib函数。我们使用远程线程来实现InjectDllEjectLib函数。远程线程是指在当前进程中使目标进程启动一个线程。可以通过以下方法完成远程线程的调用。首先使用OpenProcess打开目标进程得到进程句柄,要启动远程线程最少需要用PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE, and PROCESS_VM_READ权限打开目标进程(我们使用PROCESS_ALL_ACCESS)。然后调用VirtualAllocEx函数在目标进程中分配内存,使用WriteProcessMemory函数将当前进程中的线程函数的参数写入在目标进程分配的内存中,最后调用CreateRemoteThread函数使目标进程执行线程函数。(实现过程请参考源代码中的注释)。下面给出InjectDll函数的代码,该函数将指定的Dll文件注入到指定ID号的进程中。

int WINAPI CHookApi::InjectDll(DWORD dwProcessId, LPTSTR lpDllName)

{

     PTHREAD_START_ROUTINE pfnRemote =(PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"), "LoadLibraryA");

     if(pfnRemote ==NULL)

         return -1;

     HANDLE hProcess =OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

     if(hProcess ==NULL)

     {

         return -1;

     }

     int iMemSize = (int)strlen(lpDllName)+1;

     void *pRemoteMem =VirtualAllocEx(hProcess, NULL, iMemSize, MEM_COMMIT, PAGE_READWRITE);

     if(pRemoteMem ==NULL)

     {

         CloseHandle(hProcess);

         return -1;

     }

     if (!WriteProcessMemory(hProcess, pRemoteMem, lpDllName, iMemSize,NULL))

     {

         VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);

         CloseHandle(hProcess);

         return -1;

     }

     HANDLE hThread =CreateRemoteThread(hProcess, NULL, 0, pfnRemote, pRemoteMem, 0, NULL);

     if(hThread ==NULL)

     {

         VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);

         CloseHandle(hProcess);

         return -1;

     }

     WaitForSingleObject(hThread, INFINITE);

     VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);

     CloseHandle(hProcess);

     CloseHandle(hThread);

     return 0;

}

函数一开始,指定了要被注入的线程函数的位置,在这里,我们的线程函数就是LoadLibraryA函数,远程线程中的线程函数应该是PTHREAD_START_ROUTINE类型的函数指针,该函数指针的声明如下:

typedef DWORD (*PTHREAD_START_ROUTINE)(LPVOID)

由声明可知,该函数是一个只有一个参数的,且返回值是DWORD类型的函数,而LoadLibraryA函数符合要求的,所以它可以直接作为远程线程的线程函数。当远程进程调用LoadLibraryA把我们的MainHookDll.Dll加载后,拦截工作就会自动进行。紧接着,用VirtualAllocEx函数在远程进程中开辟一块内存,开辟的内存的大小是MainHookDll的全路径名(注意最后的’\0’)。接着使用WriteProcessMemory函数将参数(即MainHookDll的全路径名)写入到刚刚开辟的远程进程中的地址空间。最后调用CreateRemoteThread函数启动远程线程。要注意的是,当线程结束后,要用VirtualFreeEx函数释放在远程进程内开辟的内存。

下面的代码列出了EjectLib函数,它是使系统当前进程卸载我们的MainHookDll文件:

int WINAPI CHookApi::EjectLib(DWORD dwProcessId, LPTSTR lpDllName)

{

     // open the process

     HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcessId);

     if(hProcess == NULL)

         return -1;

     // 枚举进程中的模块

     HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);

     if(hModuleSnap == INVALID_HANDLE_VALUE)

     {

         CloseHandle(hProcess);

         return -1;

     }

     MODULEENTRY32 me32;

     me32.dwSize = sizeof(MODULEENTRY32);

     BOOL bFound = FALSE;

     HMODULE hmod = NULL;

     if(Module32First(hModuleSnap, &me32))

     {

         do

         {

              if(strcmpi(me32.szModule,lpDllName) == 0)

              {

                   hmod = me32.hModule;

                   bFound = TRUE;

              }

         }while(!bFound && Module32Next(hModuleSnap, &me32));

     }

     CloseHandle(hModuleSnap);

     if(hmod == NULL)

     {

         // 没有指定的模块

         CloseHandle(hProcess);

         return 0;

     }

     // 创建远程线程

     PTHREAD_START_ROUTINE pfnRemote =(PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"),"FreeLibrary");

     if(pfnRemote ==NULL)

     {

         CloseHandle(hProcess);

         return -1;

     }

     HANDLE hThread =CreateRemoteThread(hProcess,NULL,0,pfnRemote,hmod,0,NULL);

     if(hThread ==NULL)

     {

         CloseHandle(hProcess);

         return -1;

     }

     WaitForSingleObject(hThread, INFINITE);

 

 

     CloseHandle(hProcess);

     CloseHandle(hThread);

     return 0;

}

我们在函数中使用了Psapi中定义的函数列举指定进程中加载的模块,看看是否加载了我们的MainHookDll文件,如果加载了,那么使用FreeLibrary函数卸载它。

 

 

    我们写一个要被测试的程序叫test.exe,在里面调用要被拦截的API,比如是MessageBox。我们的主程序是MainApp.exeHOOK模块是MainHookDll.dll,所有要替换的函数都在Replace.dll里面导出。当MainApp.exe运行起来后,他先加载MainHookDll.dll,并且调用MainHookDll.dllInjectDll方法把MainHookDll.dll注入到目标进程。MainHookDll.dll被加载后,先主动加载Replace.dll并调用Replace.dllGETHOOKINFOFUNCNAMEINREPLACEDLL方法获得要HOOK的信息。然后就按照刚才分析的机制对目标进程进行拦截了。由此可见,我们在写程序的时候,拦截某个API的处理函数都在Replace.dll中,其他部分的代码都是固定的。