分类

课内:
不限
类型:
不限 游戏 项目 竞赛 个人研究 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
年份:
不限 2018 2019 2020

资源列表

  • 基于Android实现的锁屏软件APP

    一、概述1.1 选题背景首先现在各种安卓的游戏和APP充斥应用市场,但是创意还是有的。
    首先,锁屏软件虽然不是一个创新点,我也承认好多人都做过了,也有做的很绚丽的,这个事实我也承认,但是当前背景是,很多锁屏软件,仅仅是PIN或者是九宫格密码形式,但是造成的问题是,破解起来也很容易,网上就有好多破解教程,简单粗暴。
    所以锁屏软件的安全性还是一个值得考究的部分。同时网上有很多需求,百度知道等等,有好多人都需求一个真正属于他们自己的软件,能够真正的自定义。
    所以基于上述两点,我选择了这个方向的开发,我的APP“ALL IN ONE”就是能为了实现这种的需求实现的,既有防盗功能,也有自定义锁屏界面的功能。
    1.2 项目意义这个项目的意义十分的明显,首先是解决了当前的需求,所以说是一个使用的app,不是说为了创意而创意的产物。
    其次,他的功能很多,应用层面很广阔。首先可以个人使用这款软件,用作娱乐,放松身心以及自由的用户体验。同时他也可以给企业用,企业可以用这种功能来宣传,来投放广告,可以进行营利。所以项目意义重大,one here,use in many place。所以总体上来讲,有足够多的理由证明其意义还是存在的。
    二、可行性分析和目标群体2.1 可行性分析首先可行性是毋庸置疑的,首先,市场方面是存在的,我在做这个项目之前进行了简单的调研,返现有很多人还是希望能够自定义锁屏界面的,因为许多市面上的APP没有这种功能,同时对于防盗功能更是必须的,举例来说在我们的图书馆,很多同学看书看累了或者是粗心大意,就会直接把手机丢到桌子上,同时每个月都有好多同学不是丢电脑就是丢手机的。所以做一个这种的防盗功能的app更是情势所迫,这样可以在某种程度上减少手机的丢失概率。正是因为市场的需求造成这种app的诞生。
    2.2 目标群体目标群体分为两种,主要是分为个人和组织,个人主要面向群体是在校学生这种手机长用户,因为他们容易丢手机,同时也容易成为盗窃的受害者,同时比如像图书管这种防盗不好进行地方,更有这种软件市场,同时对于学生来说,更喜欢个性化,喜欢追求自由。
    而对于组织来说,主要面向各种手机公司,尤其是卖手机的地方,我们的APP的市场更为广阔,企业可以通过自定义锁屏的方式不断循环播放广告,低成本,但是高效率。同时也可以起到防盗的功能,比如某些不可以取下的手机。
    三、作品功能和原型设计3.1 总体功能结构
    3.2 具体功能模块设计3.2.1 锁屏/解锁模块监听android系统级广播,通过service和broadcast receiver共同实现锁屏界面实现。
    3.2.2 九宫格密码加密模块自定义View控件,实现OnCompleteListener,同时使用sharedpreferences操作存贮密码,同时定义相关函数。
    3.2.3 防盗功能模块监听Sensor,和密码加密模块通信。
    3.2.4 自定义锁屏背景模块使用viewpager轮播,app自带锁屏背景,只有当你在相关文件夹中添加了相关图片才会自动载入,实现自定义。
    3.3 界面设计3.3.1 主界面设计
    3.3.2 密码功能界面设计


    3.3.3 防盗功能界面设计

    四、作品实现、特色和难点4.1 作品实现
    作品实现:使用工具
    IDE:adt-bundle-windows-x86_64-20140702,
    SDK:API-19
    Tools: sdk tool:hierarchyviewer.bat, draw9patch.bat

    4.2 特色分析特色有三:

    首先是正常的密码加密以及锁屏功能,有基本的保护功能
    防盗功能,正如同前面分析的,有广泛的应用空间和很强的实用性
    自定义锁屏背景,可以让你的生活不再单调,手机也能透露你的个性,一个好的锁屏背景不如一个真正属于你自己的锁屏背景更加适合你

    4.3 难点和解决方案难点有监听系统消息,同时最重要的是对于九宫格密码的存储识别以及最后校验的过程,同时还有在进行防盗功能时候设置移动度量以及最后senor的register和unregister的事件相应问题。还有就是关于一些逻辑的设计问题,很是个难题,要考虑很多,要不就会留下bug,或者是做出交互性不好的软件。
    解决方案即使查找资料,并且多次试验,找人进行用户体验试验以及重复测试,同时通过画状态图的方式进行相关状态的制约问题。
    1 评论 12 下载 2019-02-27 13:54:04 下载需要16点积分
  • 基于80x86汇编实现的双任务管理系统-贪吃蛇小游戏

    一、题目设计双任务系统设计
    二、设计内容采用8086汇编语言,设计并实现双任务系统设计,对两个任务(两个窗口)进行管理和调度,能够实现任务之间的切换,保存上下文信息。任务调度程序使用循环程序来完成。在左边显示窗口,能够运行简单的贪吃蛇游戏,在右边显示窗口,能够画出等边三角形。
    三、需求分析
    贪吃蛇游戏采用键盘按键控制贪吃蛇前进方向,如“W、S、A、D”键分别为上下左右方向控制按键
    游戏终止条件为贪吃蛇碰触窗口边框、蛇头碰触身体、身体长度达到上限,以“R”键为游戏重新开始。若游戏进行当中无键按下,则贪吃蛇保持当前方向不变直至撞墙
    等边三角形位置在该显示区域的中部,参数边长由键盘输入确定
    三角形每次根据输入的参数,在该窗口将三角形重新绘制出来
    初始工作窗口为右边显示窗口,以后每按一次Tab键切换至旁边显示窗口
    当某个显示窗口被选中时,则焦点停留在该窗口,键盘输入对当前窗口有效
    整个系统按ESC键退出,返回DOS操作系统界面

    四、概要设计4.1 方案设计
    4.2 模块功能说明4.2.1 I/O模块说明
    getInt() 读入整数函数:键盘输入一串字符,检测该字符是否为数字字符。若不是数字字符,则做无效处理。直至输入完整的数值字符保存在AX中并将ZF置1(调用者可用JZ判断是否发生特殊情况)。并做如下处理:①若键盘输入Esc键,返回AX=0,并将ZF置0。②若键盘输入Tab键,返回AX=1,并将ZF置0
    getchar() 函数:输入一个字符,回显
    puts() 函数:输出字符串
    getch() 函数:输入一个字符,不回显
    putInt() 函数:将AX寄存器中的数字以十进制的形式输出

    4.2.2 控制模块说明
    movCursor() 移动光标模块:将光标移动至y行x列
    sDelay() 延时函数模块:控制贪吃蛇移动速度
    kbhit() 模块:检测键盘有无输入
    rand() 随机数发生模块:生成一定范围内的随机数

    五、详细设计及运行结果5.1 流程图三角形模块流程图

    贪吃蛇模块流程图

    5.2 函数之间相互调用的图示函数内部调用图

    顶层调用图示

    5.3 程序设计主要代码任务切换
    yield proc pushf ;先保存一下flags cmp ax,0 ;判断是否是esc jne yEls1 mov AH, 4CH ;esc exit int 21h yEls1: ;是tab,先在它自己的栈里保存寄存器, push ax push bx push cx push dx push di push si push bp ;再切换栈,恢复另一组寄存器 cmp yFunc,0 ;当前是tri? je yTri mov stack_snake_sp,sp mov sp,stack_tri_sp ;当前是1:snake,要切换成tri mov ax,STACK_RTI mov ss,ax mov yFunc,0 jmp yTriEd yTri: mov stack_tri_sp,sp mov sp,stack_snake_sp ;当前是0:tri,要切换成snake mov ax,STACK_SNAKE mov ss,ax mov yFunc,1 ;恢复寄存器 yTriEd: pop bp pop si pop di pop dx pop cx pop bx pop ax popf ret ;切换yield endp
    贪吃蛇移动
    sMove proc pushf push ax push bx push di mov ah,0 mov al,sbody_move mov di,ax ;擦除身体最后一节 shl di,1 mov bl,sbody[di] ;x di*2+0 inc di mov al,sbody[di] ;y call movCursor cmp bl,-1 jz SMEls0 mov al,' ' call putchar SMEls0: mov ah,0 mov al,sbody_move mov bx,ax mov al,x mov di,bx shl di,1 mov sbody[di],al mov al,y inc di mov sbody[di],al dec sbody_move cmp sbody_move,0 ja sMElse1 ;手动取余 mov al,blength mov sbody_move,al sMElse1: ;不需要擦除头部 ;switch(m) cmp m,'s' je sMCs1 cmp m,'a' je sMCs2 cmp m,'d' je sMCs3 cmp m,'w' je sMCs4 ;case sMCs1: inc y jmp SCsEd1 sMCs2: dec x jmp SCsEd1 sMCs3: inc x jmp SCsEd1 sMCs4: dec y jmp SCsEd1 SCsEd1: ;end of switch-case mov al,y ;重新绘制头部 mov bl,x call movCursor mov al,HEAD_CHAR call putchar cmp HEAD_CHAR,'#' jne sMElse2 mov HEAD_CHAR,'*' jmp sMIfEd2 sMElse2: mov HEAD_CHAR,'#' sMIfEd2: ;移动光标 mov bl,0 mov al,SCY call movCursor pop di pop bx pop ax popf retsMove endp
    画三角形主要流程
    triangle proc near pushf push AX push BX push CX push DX mov triLenth,AX ;记录AX输入的边长 call triHypotenuse ;第一次call画左边 call triHypotenuse ;第二次call画右边 call triBase ;画横线 pop DX pop CX pop BX pop AX popf ret
    5.4 运行结果开始界面

    输入等边三角形边长
    若输入超出范围,清除输入数据并等待重新输入(此处为500)

    输入符合规范(此处为200)

    按Enter画等边三角形并重新打印提示信息

    可循环读入等边三角形边长(此处为80)

    按Enter清除右半部分屏幕并重新打印

    按Tab键画等边三角形程序停止,贪吃蛇程序开始运行

    食物出现在随机位置,用W A S D键控制贪吃蛇运动

    若撞墙或碰到自己游戏结束并暂停

    游戏过关

    按R键重新开始游戏

    按Tab键贪吃蛇程序暂停,画等边三角形程序等待输入

    六、总结6.1 不足和改进
    不足

    贪吃蛇游戏制作的精细度和画面质量有待提高贪吃蛇的移动速度无法改变三角形使用整数近似计算
    改进

    可以通过改变蛇身的样子和食物的颜色来提高画面质量可以在界面中设置游戏的速度:慢速 中速 高速三角形可使用浮点数寄存器进行划线

    6.2 体会在本次竞赛学习过程中,项目经过功能划分、肢解分为一层一层的模块调用。将计算机基本指令和中断调用整合起来形成底层模块,再由底层模块整合起来形成较大的功能模块,再由功能模块进行逻辑组合形成snake和tri模块,使用yield任务切换模块作为桥梁将整个程序整合在一起。
    在实际操作中我们认识到熟练掌握汇编语言中的指令的基本用法和组织结构的重要性。只有熟练掌握指令的基本用法,我们才能在实际的编程中像运用高级语言一样灵活的变通,认清计算机组织结构才能灵活设计程序整体架构。汇编语言作为一种低级设计语言,它是最底层的、与计算机内部的结构联系密切,我们在这次竞赛过程中深刻地了解到了这一点。
    在贪吃蛇程序和画等边三角形的程序设计中,加深了对计算机体系结构的理解,深刻理解汇编语言和其他语言的不同。在代码设计中也遇到很多的困难,比如一些寄存器使用冲突的问题,还有一些宏的使用问题和两个程序切换的问题等,以及如何对程序调用时对参数和返回值做一系列约定。在这个方面,我们深刻理解了团队协作能力的重要性。
    1 评论 2 下载 2019-02-13 17:41:41 下载需要16点积分
  • Windows驱动级云安全主动防御系统

    1 使用说明本系统分成四个模块,包含一个应用层可执行文件(.exe),四个内核层驱动(.sys),一个规则库数据库文件(.mdb),请保证六个文件在同一目录。
    1.1 安装服务与启动监控程序不需要安装,双击即可打开可执行文件。打开后,如果需要启动服务,请点击“安装服务”,请切换至“主要”标签页,接着“启动进程监控”、“启动注册表监控”、“启动文件监控”、“启动内存加载监控”,程序开始拦截系统调用。

    1.2 停止监控与停止服务如果需要停止监控,请先在各标签页“停止功能”,例如停止注册表监控,请先点击注册表监控标签页“停止功能”按钮。接着打开“主要”标签页,停止相当服务即可, 如图:

    2 系统设计与架构2.1 开发环境本系统应用层采用Visual Studio 2008(C++)开发,驱动层开发环境为WDK 6001.18002、Notepad++,测试环境为VMware 6.0、Windows XP Sp3。
    2.2 系统结构病毒在计算机运行之后将根据自身的目的呈现出一系列的动作,包括写注册表项,生成文件,远程线程注入等等。本系统通过拦截系统调用对程序行为进行监控,将监控的行为信息交给监控中心和网络服务器分析处理,根据程序行为分析判断病毒,云安全概念的加入,本地特征库极小,占用系统资源很少。本系统设想根据这一系列的动作所组成的行为进行智能的逻辑判断该程序是不是病毒。
    系统结构示意图

    2.3 系统流程本系统分为六大模块:进程监控模块、注册表监控模块、内存加载监控模块、文件创建加载模块、云安全模块、安全监控中心模块。各个模块相互独立,系统流程图如下:

    3 系统实现原理3.1 Hook SSDT在 Windows NT 下,用户模式(User mode)的所有调用,如Kernel32.dll,User32.dll, Advapi32.dll等提供的API,最终都封装在Ntdll.dll中,然后通过Int 2E或SYSENTER进入到内核模式,通过服务ID,在System Service Dispatcher Table中分派系统函数。这些本地系统服务的地址在内核结构中称为系统服务调度表(System Service Dispatch Table,SSDT)中列出,该表可以基于系统调用编号进行索引,以便定位函数的内存地址。下图是Windows NT系列操作系统的体系结构,系统服务调用只是一个接口,它提供了将用户模式下的请求转发到Windows 2000内核的功能,并引发处理器模式的切换。在用户看来,系统服务调用接口就是Windows内核组件功能实现对外的一个界面。系统服务调用接口定义了Windows内核提供的大量服务。

    KeServiceDescriptorTable是由内核导出的表,这个表是访问SSDT的关键,具体结构是:
    typedef struct ServiceDescriptorTable { PVOID ServiceTableBase; PVOID ServiceCounterTable(0); unsigned int NumberOfServices; PVOID ParamTableBase;}
    其中:

    ServiceTableBase是 System Service Dispatch Table 的基地址NumberOfServices 由 ServiceTableBase 描述的服务的数目ServiceCounterTable 包含着 SSDT 中每个服务被调用次数的计数器ParamTableBase 包含每个系统服务参数字节数表的基地址
    System Service Dispath Table(SSDT):系统服务分发表,给出了服务函数的地址,每个地址4子节长。
    System Service Parameter Table(SSPT):系统服务参数表,定义了对应函数的参数字节,每个函数对应一个字节。

    为了调用特定函数,系统服务调度程序KiSystemService将该函数的ID编号乘以4以获取它在SSDT中的偏移量。注意KeServiceDescriptorTable包含了服务数目,该值用于确定在SSDT或SSPT中的最大偏移量。下图也描述了SSPT。该表中的每个元素为单字节长度,以十六进制指定了它在SSDT中的相应函数采取多少字节作为参数。在这个示例中,地址0x804AB3BF处的函数采用0x18个字节的参数。

    当调用INT 2E或SYSENTER指令时会激活系统服务调度程序。这导致进程通过调用该程序转换到内核模式。若将SSDT改为指向rootkit所提供的函数,而不是指向Ntoskrnl.exe或Win32k.sys,当非核心的应用程序调用到内核中时,该请求由系统服务调度程序处理,并且调用了rootkit的函数。这时,rootkit可以将它想要的任何假信息传回到应用程序,从而有效地隐藏自身以及所用的资源。
    由于Windows系统版本对某些内存区域启用了写保护功能,SSDT就包括在其中,若向只读内存区域中执行写入操作,则会发生蓝屏死机(Blue Screen of Death,BSoD)。Windows中可以通过Memory Descriptor List(MDL)修改内存属性, 把原来SSDT的区域映射到我们自己的MDL区域中,并把这个区域设置成可写。MDL的结构:
    typedef struct _MDL { struct _MDL *Next; CSHORT Size; //设置成MDL_MAPPED_TO_SYSTEM_VA ,这块区域就可写 CSHORT MdlFlags; struct _EPROCESS *Process; PVOID MappedSystemVa; PVOID StartVa; ULONG ByteCount; ULONG ByteOffset;} MDL, *PMDL;
    知道KeServiceDscriptorTable的基址和入口数后,用MmCreateMdl创建一个与KeServiceDscriptorTable对应地址和大小的内存区域,然后把这个MDL结构的flag改成MDL_MAPPED_TO_SYSTEM_VA ,那么这个区域就可以写了。最后把这个内存区域调用MmMapLockedPages锁定在内存中。
    通过微软已经导出的几个宏可以很方便地Hook SSDT。
    SYSTEMSERVICE 宏采用由ntoskrnl.exe导出的Zw*函数的地址,并返回相应的Nt*函数在SSDT中的地址。Nt*函数是私有函数,其地址列于SSDT中。Zw*函数是由内核为使用设备驱动程序和其他内核组件而导出的函数。注意,SSDT中的每一项和每个Zw*函数之间不存在一对一的对应关系。
    SYSCALL_INDEX宏采用Zw*函数地址并返回它在SSDT中相应的索引号。该宏和SYSTEMSERVICE宏发挥作用的原因在于Zw*函数起始位置的操作码。通过将该函数的第二个字节看作ULONG类型,这些宏能得到该函数的索引号。
    HOOK_SYSCALL和UNHOOK_SYSCALL宏采用被钩住的Zw*函数的地址,获取其索引号,并自动将SSDT中该索引的相应地址与_Hook函数的地址进行交换。
    #define SYSTEMSERVICE(_func) \ KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_func+1)] #define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1) #define HOOK_SYSCALL(_Function, _Hook, _Orig ) \ _Orig = (PVOID) InterlockedExchange( (PLONG) \ &MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook) #define UNHOOK_SYSCALL(_Func, _Hook, _Orig ) \ InterlockedExchange((PLONG) \ &MappedSystemCallTable[SYSCALL_INDEX(_Func)], (LONG) _Hook)
    3.2 监视进程创建和销毁原理监视系统进程开始和结束的方法是通过DDK中的PsSetCreateProcessNotifyRoutine函数设置一个CALLBACK函数。该函数的原形如下:
    NTSTATUS PsSetCreateProcessNotifyRoutine( IN PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine, IN BOOLEAN Remove);

    NotifyRoutine指定了当进程被创建和结束的时候所需要调用的回调函数
    Remove是用来告诉该函数是设置该回调还是移除

    NotifyRoutine的类型为PCREATE_PROCESS_NOTIFY_ROUTINE,其定义为:
    VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE) ( IN HANDLE ParentId, IN HANDLE ProcessId, IN BOOLEAN Create);
    NotifyRoutine的传入参数中,ParentId和ProcessId用来标识进程,Create则是用来表示该进程是正在被创建还是正在被结束。这样,每当进程被创建或者结束的时候,操作系统就会立刻调用NotifyRoutine这个回调函数并正确提供参数。
    由于回调函数NotifyRoutine传入参数的类型为HANDLE,而PID为DWORD类型,可以通过强制转换为DWORD得到父进程、正在创建的进程的PID。
    但此时仍不能得到父进程和正在创建的路径,通过调用一个未公开函数 PsLookupProcessByProcessId得到该进程的PEPROCESS,每个进程都有一个 EPROCESS 结构,里面保存着进程的各种信息,和相关结构的指针。这个函数的原型如下:
    NTSTATUS PsLookupProcessByProcessId( IN ULONG ulProcId, OUT PEPROCESS * pEProcess);
    得到该进程的PEPROCESS对象后,由于PEPROCESS是微软未公开的结构,而且在不同的操作系统下结构还不一样。EPROCESS 偏移0X1fc中保存着一个给人看的进程名,更准确的叫法是映像名称。
    EPROCESS +1fc byte ImageFileName[16]
    ImageFileName[16] 是一个16个字节长的字节数组,保存着进程名。当进程名的长度大于等于16个字节时,在 ImageFileName[16] 只保存进程名的前15个字节,ImageFileName[16] 最后一个字节为0,字符串的结束符。不同进程的进程名可以相同,比如打开多个记事本,那么每个记事本的 ImageFileName[16] 都是 “NOTEPAD.EXE”,进程名只是给人看的,每个进程的 进程ID 都是不同的。对于父进程,在PEPROCESS保存进程名,通过自定义的一个函数当前进程可以得到完整的路径,该函数定义如下:
    PCWSTR GetCurrentProcessFileName() { DWORD dwAddress = (DWORD)PsGetCurrentProcess(); DWORD dwAddress1; if(dwAddress == 0 || dwAddress == 0xFFFFFFFF) return NULL; dwAddress += 0x1B0; if((dwAddress = *(DWORD*)dwAddress) == 0) return 0; dwAddress += 0x10; if((dwAddress = *(DWORD*)dwAddress) == 0) return 0; dwAddress1 = dwAddress;//2000 dwAddress += 0x3C; if((dwAddress = *(DWORD*)dwAddress) == 0) return 0; if (dwAddress < dwAddress1) dwAddress = dwAddress+dwAddress1; return (PCWSTR)dwAddress; }
    但是对于正在创建的进程,并不能如此。必须使用ObOpenObjectByPointer得到进程名柄,ObOpenObjectByPointer的原型如下:
    NTSTATUS ObOpenObjectByPointer( IN PVOID Object, // 进程的pEProcess IN ULONG HandleAttributes, IN PACCESS_STATE PassedAccessState OPTIONAL, IN ACCESS_MASK DesiredAccess, IN POBJECT_TYPE ObjectType, IN KPROCESSOR_MODE AccessMode, OUT PHANDLE Handle //此处将得到名柄);
    得到名柄后可进一步通过未微软未公开的函数ZwQueryInformationProcess 可得到进程的镜像路径。ZwQueryInformationProcess原型如下:
    NTSTATUS ZwQueryInformationProcess( IN HANDLE ProcessHandle, //传入进程句柄 IN PROCESSINFOCLASS ProcessInformationClass,// 指定此处为ProcessImageFileName OUT PVOID ProcessInformation,//此处得到进程镜像信息 IN ULONG ProcessInformationLength, OUT PULONG ReturnLength OPTIONAL);
    得到镜像路径、进程PID等信息后可通知应用层用进程创建,应用层便能迅速取得进程信息,从而可以对进程在未完全加载完成时处理,在进程监控中,应该有内核事件对象、驱动层与用户层、用户层与驱动层通信的知识,首先是内核事件对象。
    在内核中创建的事件对象,用户层只能读,不能给事件置信号,这多少给应用程序与驱动程序之间同步造成麻烦。
    在使用KEVENT 事件对象前,需要首先调用内核函数KeInitialize Event 对其初始化,这个函数的原型如下所示:
    VOID KeInitializeEvent( IN PRKEVENT Event, IN EVENT_TYPE Type, IN BOOLEAN State);

    第一个参数Event 是初始化事件对象的指针
    第二个参数Type 表明事件的类型,事件分两种类型:

    一类是“通知事件”,对应参数为NotificationEvent另一类是“同步事件”,对应参数为SynchronizationEvent
    第三个参数State 如果为TRUE,则事件对象的初始化状态为激发状态,否则为未激发状态。如果创建的事件对象是“通知事件”,当事件对象变为激发态时,需要我们手动将其改回未激发态。如果创建的事件对象是“同步事件”,当事件对象为激发态时,如果遇到相应的KeWaitForXXXX 等内核函数,事件对象会自动变回到未激发态

    设置事件的函数是KeSetEvent,可通过该函数修改事件对象的状态,这个函数的原型如下:
    VOID KeClearEvent( IN PRKEVENT Event);
    在本系统进程监控模块中,我采用的方案是在内核监视进程的创建和销毁,并且应用程序开启新线程一直等待驱动中创建的命名事件。
    在驱动层中,一旦检测到有进程进程的创建立即将事件对象置上信号, 这时应用层立即取得进程PID等信息,利用ZwSuspendProcess将进程挂起,从而可以对进程的信息进行进一步的检验,但是,ZwSuspendProcess是NTDLL.DLL中一个未导出的函数,在《Windows NT 2000 Native API Reference》中找不到其原型。我尝试过在驱动中申明NTSYSAPI的方法尝试利用其在内核层中挂起进程,可惜的是编译不能通过。
    当然在用户层中挂起线程与在内核层中挂起进程是没有差别的,于是采用了在用户态下挂起进程的方法。
    在用户态中使用未导出的内核函数,必须先将ZwSuspendProcess导出。导出.DLL内的函数方法之一是利用GetProcAddress ,这个函数的原型是:
    FARPROC WINAPI GetProcAddress( __in HMODULE hModule, __in LPCSTR lpProcName);
    传入的HMODULE的原型是:
    HMODULE WINAPI LoadLibrary( __in LPCTSTR lpFileName);
    于是通过下面两行代码可以将ZwSuspendProcess内核函数导出:
    HANLDE hdll=LoadLibrary(L"NTDLL.DLL");SuspendProcess=(pfsuspend)GetProcAddress(hdll, "ZwSuspendProcess");
    同时,还得利用相同方法将ZwResumeProcess函数导出,当然,必须得先申明函数原型。经过查阅《Windows System Call Table》,这两函数的原型如下(Nt与Zw系统函数完全相同):
    NTSYSAPI NTSTATUS NTAPI NtSuspendProcess( IN HANDLE Process);NTSYSAPI NTSTATUS NTAPI NtResumeProcess( IN HANDLE Process);
    使用OpenProcess传入PID即可得到进程句柄,将获得的进程句柄传入 ZwSuspendProcess/ZwResumeProcess 中即可实现进程的挂起与恢复。
    应用层传入信息的时候,可以使用WriteFile,也可以使用DeviceIoControl。
    DeviceIoControl是双向的,在读取设备的信息也可以使用。DeviceIoControl 函数会使操作系统产生一 IRP_MJ_DEVICE_CONTROL 类型的IRP,然后这个IRP 会被分发到相应的派遣例程中。因此在驱动入口函数中需要先设置 IRP_MJ_DEVICE_CONTROL 的派遣例程,如
    DriverObject->MajorFunctions[IRP_MJ_DEVICE_CONTROL] = MyDeviceIoControl;
    DeviceIoControl 函数的原型如下:
    BOOL DeviceIoControl( HANDLE hDevice, // handle to device DWORD dwIoControlCode, // operation LPVOID lpInBuffer, // input data buffer DWORD nInBufferSize, // size of input data buffer LPVOID lpOutBuffer, // output data buffer DWORD nOutBufferSize, // size of output data buffer LPDWORD lpBytesReturned, // byte count LPOVERLAPPED lpOverlapped// overlapped information);
    第二个参数dwIoControlCode,它是I/O 控制码,即IOCTL 值,是一个32 位的无符号整型数值。实际上DeviceIoControl 与ReadFile 和WriteFile 相差不大,不过它可以同时提供输入/输出缓冲区,而且还可以通过控制码传递一些特殊信息。IOCTL 值的定义必须遵循DDK 的规定,使用宏CTL_CODE 来声明,如下:
    #define MY_DVC_IN_CODE \(ULONG)CTL_CODE( FILE_DEVICE_UNKNOWN, \0x900, \ // 自定义IOCTL 码METHOD_BUFFERED, \ // 缓冲区I/OFILE_ALL_ACCESS)
    当然在应用DeviceIoControl时,应用程序应该与驱动程序包含相同的IOCTL定义,最好的方法是共享一个头文件,并且应用程序需要包含winioctl.h头文件。
    下为本系统危险进程主动防御模块的流程图

    3.3 监视注册表修改/创建原理注册表监视原理是通过HOOK_SYSCALL宏勾住ZwSetValueKey, 该宏定义如下:
    #define HOOK_SYSCALL(_Function, _Hook, _Orig ) \ _Orig = (PVOID) InterlockedExchange( (PLONG) \
    具体挂接语句为:
    HOOK_SYSCALL(ZwSetValueKey,HookZwSetValueKey,RealZwSetValueKey);
    其中ZwSetValueKey用于更新或者新建一个键值,它的原型如下:
    NTSTATUS ZwSetValueKey( IN HANDLE KeyHandle, IN PUNICODE_STRING ValueName, IN ULONG TitleIndex, IN ULONG Type, IN PVOID Data, IN ULONG DataSize);
    由于DDK文档中没有该函数,在驱动中使用它时必须申明其原型,方法如下:
    NTSYSAPI NTSTATUS NTAPI ZwSetValueKey( IN HANDLE KeyHandle, IN PUNICODE_STRING ValueName,// The name of the value to be set. IN ULONG TitleIndex, IN ULONG Type, IN PVOID Data, // Points to a caller-allocated buffer or variable that contains the data of the value. IN ULONG DataSize);
    HOOK_SYSCALL时,为了保存ZwSetValueKey 函数的真实地址在RealZwSetValueKey中,须定义一个与之类似的结构:
    typedef NTSTATUS (*REALZWSETVALUEKEY)( IN HANDLE KeyHandle, IN PUNICODE_STRING ValueName, IN ULONG TitleIndex OPTIONAL, IN ULONG Type, IN PVOID Data, IN ULONG DataSize);REALZWSETVALUEKEY RealZwSetValueKey=NULL;//初始化
    Hook ZwSetValueKey后,当应用层修改注册时,总会先进入我们挂接的函数HookZwSetValueKey,假若在HookZwSetValueKey函数中不再调用真实的ZwSetValueKey,那么应用层无法修改、创建注册表键键值。HookZwSetValueKey定义与ZwSetValueKey一致,如下:
    NTSTATUS HookZwSetValueKey( IN HANDLE KeyHandle, IN PUNICODE_STRING ValueName, IN ULONG TitleIndex OPTIONAL, IN ULONG Type, IN PVOID Data, IN ULONG DataSize);
    HookZwSetValueKey传入参数包含了将要修改的信息:ValueName指向了要修改的键名。
    在Win32 API中,管理内核对象是通过HANDLE,而在内核内部是通过Object来管理内核对象的。下面是Object属性结构:
    typedef struct _OBJECT_ATTRIBUTES { ULONG Length; //结构长度 HANDLE RootDirectory; //根目录 UNICODE_STRING *ObjectName; //Object的名称 ULONG Attributes; //属性 PSECURITY_DESCRIPTOR SecurityDescriptor;//安全描述符 PSECURITY_QUALITY_OF_SERVICE SecurityQualityOfService;} OBJECT_ATTRIBUTES,*POBJECT_ATTRIBUTES;
    我们感兴趣的是ObjectName成员,因为它包含了注册表名。
    但要想进一步得到要修改的键值则必须进一步通过ObReferenceObjectByHandle得到对象,这是一个在驱动开发中经常用到的函数,该函数原型如下:
    NTSTATUS ObReferenceObjectByHandle( IN HANDLE Handle, //此处传入HookZwSetValueKey的参数KeyHandle IN ACCESS_MASK DesiredAccess, IN POBJECT_TYPE ObjectType OPTIONAL, IN KPROCESSOR_MODE AccessMode, OUT PVOID *Object, //这里返回Object指针 OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL);
    得到PObject后,理论上可以其通过获得键名信息,由于OBJECT_ATTRIBUTES是不公开的结构,不同操作系统可能不同。为了跨越不同的版本,这里采用未公开的内核函数ObQueryNameString,才能得到键名信息,至此,只得到了操作注册表方面的信息,因为还需得到是哪个进程引发的操作,进程名可通过当前进程偏移量方法获取,而进程的完整路径同样是通过一个自定义函数取得:
    PCWSTR GetCurrentProcessFileName() { DWORD dwAddress = (DWORD)PsGetCurrentProcess(); DWORD dwAddress1; if(dwAddress == 0 || dwAddress == 0xFFFFFFFF) return NULL; dwAddress += 0x1B0; if((dwAddress = *(DWORD*)dwAddress) == 0) return 0; dwAddress += 0x10; if((dwAddress = *(DWORD*)dwAddress) == 0) return 0; dwAddress1 = dwAddress;//2000 dwAddress += 0x3C; if((dwAddress = *(DWORD*)dwAddress) == 0) return 0; if (dwAddress < dwAddress1) dwAddress = dwAddress+dwAddress1; return (PCWSTR)dwAddress; }
    相关方面的信息获取成功后,下一步是检查这些信息,或者让用户决定是否执行这个操作,如果执行,在HookZwSetValueKey只需将传入的各参数原样交给真实的RealZwSetValueKey的处理即可,否则不交给真实RealZwSetValueKey处理,从而导致应用程度修改操作失败。
    由于应用层不能简单地通知驱动层的事件,所以本系统采用了双事件:驱动层通知应用层事件、应用层通知驱动层事件,其中应用层通知驱动层事件通过传递IOCTL ,驱动在IOCTL的派遣函数中设置事件。IOCTL定义如下:
    #define IOCTL_NTPROCDRV_SET_APPEVENT_OK CTL_CODE(FILE_DEVICE_UNKNOWN,0x0911, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS)
    在驱动的派遣例程中KeSetEvent置信号方法如下:
    case IOCTL_NTPROCDRV_SET_APPEVENT_OK: { KdPrint(("放行!应用层通知驱动层的事件")); RegPass=1; //全居变量,通过,事件设置必须在后面 KeSetEvent(RegAppEvent,IO_NO_INCREMENT,FALSE); status=STATUS_SUCCESS;}break;
    下为本系统注册表主动防御模块的流程图

    3.4 监视文件修改/创建原理文件监视原理是通过HOOK_SYSCALL宏勾住ZwWriteFile, 该宏定义如下:
    #define HOOK_SYSCALL(_Function, _Hook, _Orig ) \ _Orig = (PVOID) InterlockedExchange( (PLONG) \
    具体挂接语句为:
    HOOK_SYSCALL(ZwWriteFile,HookZwWriteFile,RealZwWriteFile);
    ZwWriteFile用于向文件写入数据. 为了能成功挂接ZwWriteFile,与注册表监控相似,必须先申明其原型、Hook函数的原型、用于保存真实地址的RealZwWriteFile。
    NTSYSAPI NTSTATUS NTAPI ZwWriteFile( IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID Buffer, IN ULONG Length, IN PLARGE_INTEGER ByteOffset OPTIONAL, IN PULONG Key OPTIONAL);
    申明保存真实地址的RealZwWriteFile:
    typedef NTSTATUS (*ZWWRITEFILE)( IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID Buffer, IN ULONG Length, IN PLARGE_INTEGER ByteOffset OPTIONAL, IN PULONG Key OPTIONAL);ZWWRITEFILE RealZwWriteFile=NULL;
    申明Hook函数原型:
    NTSTATUS HookZwWriteFile( IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID Buffer, IN ULONG Length, IN PLARGE_INTEGER ByteOffset OPTIONAL, IN PULONG Key OPTIONAL );
    由于本软件不是做文件加密,因些目标是从传入HookZwWriteFile的参数中解析出进程名、进程完整路径、写入(新建)文件的完整路径。其中,进程名、进程完整路径与注册表监视中的原理相似,通过进程上下文和偏移量可获得。
    将要写入的文件的完整路径(以下称目标路径)获得需几步,首先通过传入的FileHandle句柄,使用ObReferenceObjectByHandle得到文件对象,ObReferenceObjectByHandle原型如下:
    NTSTATUS ObReferenceObjectByHandle( IN HANDLE Handle, //传入FileHandle IN ACCESS_MASK DesiredAccess, IN POBJECT_TYPE ObjectType OPTIONAL,//指定为*IoFileObjectType IN KPROCESSOR_MODE AccessMode, OUT PVOID *Object, //得到文件对象 OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL);
    由于FileObject是一个没有被文档化的对象(This information class is not implementedby any of the supported file systems.),仅知道在文件对象中FileName域保存着部分目标路径,DeviceObject域保存着卷标信息,但是对于本系统这已经够用了。
    以”C:\windows\1.txt”为例,FileName这个UNICODE_STRING的Buffer中保存着“ \windows\1.txt”,经过转换后的卷标将保存着“C:”,使用的函数是IoVolumeDeviceToDosName,以下是函数原型:
    NTSTATUS IoVolumeDeviceToDosName( IN PVOID VolumeDeviceObject, //传入((PFILE_OBJECT)fileobj)->DeviceObject OUT PUNICODE_STRING DosName //得到卷标);
    文件监控做为本系统的一个辅助模块,主要监视的操作系统内进程的文件操作行为,是必需的一个模块,病毒的破坏操作系统的时,往往伴随着文件的释放与生成,例如木马下载者,本身可能无毒,但是后期可以从网上下载其它木马,再比如本机中了蠕虫后,exe大面积感染必然是对各文件进行了写入操作,通过文件监控即可检测到这种行为。同时,能过文件监控,在查打出木马之后,可以对其“同伙”一伙打尽,下图是本系统文件监控的流程图:

    3.5 内存映射监控原理内存监视原理是通过HOOK_SYSCALL宏勾住ZwCreateSection, ZwCreateSection用于routine creates a section object.Hook方法与上类似。该函数原型为:
    NTSYSAPI NTSTATUS NTAPI ZwCreateSection( OUT PHANDLE SectionHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN PLARGE_INTEGER MaximumSize OPTIONAL, IN ULONG SectionPageProtection, IN ULONG AllocationAttributes, IN HANDLE FileHandle OPTIONAL//此处包含文件路径信息 );
    其中最后一个参数FileHandle句柄中包含有目标文件的路径信息,与文件监视原理相同,使用ObReferenceObjectByHandle得到文件对象,在FileObject中可进一步取得文件路径与卷标。进程名可通过当前进程偏移量方法获取,进程的完整路径同样是通过一个自定义函数取得,以上原理中已多次提到。
    一个程序得以运行必然要经过内存映射,勾住ZwCreateSection可以拦截记录风险程序的行为,与注册表监控模块、文件监控模块、进程监控模块共同组成完整的程序行为监控系统。
    以下是本系统内存加载监控的流程图

    3.6 云安全模块原理“云安全(Cloud Security)”计划是网络时代信息安全的最新体现,它融合了并行处理、网格计算、未知病毒行为判断等新兴技术和概念,通过网状的大量客户端对网络中软件行为的异常监测,获取互联网中木马、恶意程序的最新信息,传送到Server端进行自动分析和处理,再把病毒和木马的解决方案分发到每一个客户端。
    未来杀毒软件将无法有效地处理日益增多的恶意程序。来自互联网的主要威胁正在由电脑病毒转向恶意程序及木马,在这样的情况下,采用的特征库判别法显然已经过时。云安全技术应用后,识别和查杀病毒不再仅仅依靠本地硬盘中的病毒库,而是依靠庞大的网络服务,实时进行采集、分析以及处理。整个互联网就是一个巨大的“杀毒软件”,参与者越多,每个参与者就越安全,整个互联网就会更安全。
    本系统云安全模块分成服务器,客户端两块。
    服务器采用windows+apache+php+mysql架构,复责返回客户端查询文件信息,处理客户端上传的可疑文件。
    客户端集成在本系统中,可获取文件名、文件详细路径、文件MD5、文件签名等信息,在必要的情况下以这些信息向服务器查询验证信息,并且在客户机允许的情况下自动上传可疑文件,让服务器处理并且反馈结果。
    同时客户端也会将众多用户处理程序行为的信息传至服务器,服务器经过统计分析,可更新各客户机上的规则,实现云规则。
    下图为云模块原理:

    3.7 规则动态加载原理本系统规则存储于一access数据库文件中,程序运行时将从数据库文件初始化规则,规则在程序中是以几条STL中的双向链表(LIST)实现的。
    在规则未经修改时,避免了多次查询数据库而耗时浪费资源。当本系统模块修改、删除数据库时,保存着规则的双向链表将会更新,实现了规则的动态更新。
    下图为规则动态更新原理图:

    4 性能与测试4.1 测试环境本系统的测试环境为:

    VMware 6.0
    Windows XP SP3
    驱动级云安全主动防御
    迅雷
    木马
    等等

    4.2 进程监控测试
    测试项目:测试本系统是否能够有效监控进程创建
    测试工具:驱动级云安全主动防御、记事本程序、SRVINSTW.EXE(病毒)
    测试过程:

    开启本系统进程监控运行”记事本” 程序将”记事本” 程序行为加入白名单,再次运行运行SRVINSTW.EXE(病毒)将SRVINSTW.EXE(病毒)行为加入黑名单,再次运行
    结果及分析如下所示:

    下图为在部署了本系统的主机上打开桌面” 新建 文本文档.txt”时, ” 新建 文本文档.txt”并没有被打开,而是弹出了本系统的用户确认对话框,在云安全模块已经开启的情况下,系统自动联网查询该文件信息,在行为描述一栏有提示“windows自带记事本,该程序是安全的”,用户点击”放行一次”, ” 新建 文本文档.txt”将被打开;用户点击”我认识它,永久放行”, 以后打开记事本程序将都不再提醒。

    下图为把本次程序行为加入白名单,即始终允许本程序启动。

    加入白名单后,再次打开桌面”新建 文本文档.txt”,本系统不再弹出提示信息框,”新建 文本文档.txt”被直接打开,如图:

    此时桌面有一病毒文件SRVINSTW.EXE,在云安全模块已经开启情况下,双击打开,系统将自动联网查询该文件信息,进程打开被阻塞,本系统弹出用户确认对话框,在行为描述一栏有提示“该程序为病毒,请谨慎运行!”,用户选择“禁止一次”,该程序被阻止运行;用户选择“这个很危险,永久禁止”,将自动添加黑名单规则,该程序被阻止运行。如下图:

    用户选择“禁止一次”,进程结束。接着将此进程信息加入黑名单:

    再次双击桌面图标,双击后没有反应,从进程监控主页面拦截信息可知,该程序还没完全开启时已经被主动防御进程结束了。


    从实验结果可以看出,本系统能成功拦截进程创建,在云安全模块和用户维护的规则下能成功阻击病毒的运行。
    4.3 注册表监控测试
    测试项目:测试本系统是否能够有效监控注册表创建、修改
    测试工具:卓然驱动级云安全主动防御、迅雷、DAEMON Tools(虚拟光驱软件)
    测试过程:

    开启本系统注册表监控功能,运行迅雷,运行DAEMON Tools修改迅雷配置,设置“开机启动迅雷”,并放行一次将修改“开机启动迅雷”程序行为加入白名单,再次修改“开机启动迅雷”修改DAEMON Tools开机启动,并拒绝一次将修改DAEMON Tools开机启动行为加入黑名单,再次修改DAEMON Tools开机启动,查看结果
    结果及分析如下所示:

    打开迅雷,配置迅雷,勾选“开机启动迅雷”, 如图:

    本系统立即暂停该操作,并且弹出对话框,请求用户确认是不是要进程该操作,在云安全模块开启的情况下,行为描述一栏会从网络获取该行为的信息,此处为“迅雷开机启动”,如图:

    若此时选择“放行一次”,修改行为将生效;选择“相同一直放行”,修改行为将生效,且以后相同行为不再提醒;选择“禁止一次”,修改请求将被拒绝;选择“相同一直禁止”,修改请求将被拒绝,且以后相同行为不再提醒,直接拒绝。
    测试时选择“放行一次”,再次打开迅雷的配置,可见修改已经成功。

    接着,把迅雷添加启动项行为加入白名单规则,如图:

    再次打开迅雷,修改配置,去掉“开机启动迅雷”前的勾,点击确认,本系统没有弹出确认对话框,修改生效。
    打开DAEMON Tools,修改DAEMON Tools开机启动,如图:

    并且弹出对话框,请求用户确认是不是要进程该操作,在云安全模块开启的情况下,行为描述一栏会从网络获取该行为的信息,此处为“daemon.exe添加启动项可能影响您电脑的开机速度”,如图:

    因为该行为可能影响开机速度,测试时选择禁止一次,拒绝该操作,daemon.exe弹出修改注册表出错,即修改启动不成功,如图:

    将DAEMON Tools添加开机启动行为加入黑名单,如图:

    再次修改DAEMON Tools开机启动,本系统没有弹出确认对话框,DAEMON Tools直接报错,修改不成功,如图:

    4.4 文件监控测试
    测试项目:测试本系统是否能够有效监控文件修改/创建
    测试工具:卓然驱动级云安全主动防御、记事本程序
    测试过程:

    开启本系统文件监控在桌面新建一文本文档,任意写入文本信息,保存。查看结果修改本系统注册表规则,保存。查看结果
    结果及分析如下所示:

    在桌面新建一文本文档,任意写入文本信息,回到本系统文件监控模块,查看信息如下:

    由文件监控信息可知,explorer.exe先创建了“最近打开文件”的链接,接着notepad.exe修改了桌面“新建 文本文档.txt”,完整地记录了修改文件信息。
    在注册表监控测试时,由于本程序需要修改数据库记录,查看文件监控信息,此行为也记录了下来,如图:

    文件监控在新建或修改.exe/.dll/.sys等可执行文件时,在云安全模块开启的情况下,能向服务器验证MD5、签名、文件名等信息,保证不被下载者等木马从网络下载有害程序破坏系统。
    此外,在有蠕虫感染系统时,同一进程将对系统众多可执行文件感染,必然要有打开的修改操作,在文件监控功能与监控中心的配合下,能在早期就察觉出这种危险行为,进而阻止。
    4.5 内存加载监控测试
    测试项目:测试本系统是否能够有效监控内存加载
    测试工具:卓然驱动级云安全主动防御、记事本程序,
    测试过程:

    开启本系统内存加载监控在桌面新建一文本文档,任意写入文本信息,保存。查看结果停止本系统进程监控服务,再次启动,查看结果
    结果及分析如下所示:

    停止本系统进程监控服务,并再次启动,查看内存加载信息,由加载信息可看出,本系统的进程监控服务服务加载时需要一个名为ProcMon.sys加载进入内存,如图:

    在桌面新建一文本文档,任意写入文本信息,回到本系统内存加载模块,查看信息如下:

    从内存加载信息中,可以查看notepad.exe进程加载时哪些文件映射进入了内存,得到结果后可进一步处理。
    在云安全模块开启的情况下,本系统会自己向服务器验证加载进入内存的文件信息,与其它模块相互配合,可将病毒及期衍生物一网打尽。
    参考文献[1] Mark E.Russinovich, David A.Solomon .深入解析Windows操作系统. 电子工业出版社, 2007年6月.
    [2] 张帆.Windows驱动开发详解.电子工业出版社.2008年2月.
    [3] 张静盛.windows编程循序渐进. 机械工业出版社. 2008年6月.
    [4] Jeffrey Richter.WINDOWS核心编程. 清华大学出版社. 2008年9月
    [5] 谭文,杨潇,邵坚磊.寒江独钓—Windows内核安全编程.电子工业出版社.2009.6
    [6] 谭文,邵坚磊.天书夜读—从汇编语言到windows内核编程.电子工业出版社.2009.3
    [7] Greg Hoglund/James Butler .RootKits――Windows内核的安全防护. 清华大学出版社. 2007.4
    [8] liuke_blue.黑客防线.再论进程防火墙.200809
    [9] SVEN B. SCHREIBER .Undocumented Windows 2000 Secrets
    [10] Windows NT 2000 Native API Reference
    3 评论 15 下载 2018-11-06 18:48:31 下载需要12点积分
  • 基于Python和PyQt5库实现的面向英文文献的编辑与检索

    1 分析1.1 需要完成的功能1.1.1 基本要求
    设计图形界面,可以实现英文文章的编辑与检索功能
    编辑过程包括:

    创建新文件;打开文件;保存文件查找:输入单词在当前打开的文档中进行查找,并将结果显示在界面中替换:将文章中给定的单词替换为另外一个单词,再存盘等
    对于给定的文章片段(30<单词数量<100),统计该片段中每个字符出现的次数,然后以它们作为权值,对每一个字符进行编码,编码完成后再对其编码进行译码。在 图形界面中演示该过程
    对于给定的多篇文章构成的文档集中,统计不同词汇的出现频率,并进行排序,在界面中显示 TOP 20 的排序结果
    对于给定的多篇文章构成的文档集中,建立倒排索引,实现按照关键词的检索,并在界面中显示检索的结果(如:关键词出现的文档编号以及所在的句子片段,可以将关键词高亮显示)

    1.1.2 扩展要求
    界面设计的优化
    对于编码与译码过程,可以自行设计其他算法
    扩展检索:例如,可以实现多于 1 个关键词的联合检索,要求检索结果中同时出现所有的关键词
    优化检索,对于检索结果的相关性排序,例如:包含关键词的数量等信息为依据
    可以自行根据本题目程序的实际应用情况,扩展功能

    1.2 需要处理的数据1.2.1 英文文本多个 txt 文档的导入,主要是文件路径,然后对文档中的内容进行处理。 首先读取文本。然后统计单词、字符的频率以及位置,生成索引;或者直接进行查找等功能。
    1.2.2 用户输入的字符串有多个功能需要用户输入字符串,比如创建新的文档需要命名、搜索单词、修改文档、 在文档中替换内容。
    1.2.3 网络爬取的信息从网站上爬取单词翻译信息,包括 HTML 页面。
    1.2.4 选择的文档信息用户通过“导入文件”,通过窗口选择并导入文档。
    1.3 程序开发运行选用的环境
    操作系统

    版次:Windows 10 家庭中文版版本:1709 OS 内部版本:16299.125
    语言

    Python 3.6.3


    PyQt5==5.9.2 bs4==0.0.1 six==1.11.0 requests==2.18.4

    1.4 用户界面的设计使用 PyQt5 库设计用户界面;主要窗口及功能如下: main:进入程序的 MainWindow,用来导入文件、搜索关键字等。

    item:从 main 进入。主体为显示文章的文本框,同时有多个功能按钮。

    1.5 主要流程图
    2 数据结构设计2.1 所定义主要的数据结构2.1.1 Python 内置结构
    list进行列表建立,以及一些需要排序的操作,其他大部分数据结构无法排序,需要先转化成 list,做排序再考虑转化回去(较麻烦),或者直接进行操作。用[]表示 。
    dict进行词典建立,使用键-值(key-value)存储,具有极快的查找速度,用于在不同目录下查找文本,不同文本下查找单词位置的功能,用牺牲空间的方式加快查找速度。用{}表 示。
    tuple与列表一样,也是一种序列,唯一不同的是元组不能被修改。用()表示。

    2.1.2 哈夫曼树给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
    本次构建的哈夫曼树的权值是文章中出现的所有字符的频率。并由此生成每个字符的编 码,频率越高,其叶子结点距根越近,对应的编码也就越短。
    2.1.3 索引结构构建的索引,用来更快更方便的搜索单词或统计频率。利用 Python 本身的 dict, list 以及 tuple 建立索引,结构如下:
    [(文件地址 1, [{'word': 单词 1, 'pos': [位置下标]}, {'word': 单词 2, 'pos': [位置下 标…]},…]), (文件地址 2, [{'word': 单词 1, 'pos': [位置下标]} , {'word': 单词 2, 'pos': [位置下 标…]},…]),…]
    2.2 程序整体结构以及各模块的功能描述
    main.py:总的 main 启动文件
    img.py:资源文件。图片转为文本格式,以便于程序的封装
    KMP.py

    def get_next(p):寻找前缀后缀最长公共元素长度,计算字符串 p 的 next 表def kmp(s, p):核心计算函数,返回字符串 p 的位置下标def positions(string, pattern):查找 pattern 在 string 所有的出现位置的起始下标def count(string, pattern):计算 pattern 在 string 的出现次数
    Huffman.py

    class Node:
    def __init__(self, freq):初始化属性 left, right, father, freqdef isLeft(self):返回该节点是否是父节点的左节点
    def create_Nodes(freqs):根据频率表创建叶子结点def create_Huffman_Tree(nodes):根据频率大小,使用队列创建哈夫曼树def Huffman_Encoding(nodes, root):用哈夫曼树生成哈夫曼对照表def cal_count_freq(content):计算字符频率def cal_Huffman_codes(char_freqs):整合上述 functions
    File.py

    def search(files, keyword):用 KMP 算法搜索计算所有传入的文件中 keyword 的数量,返回有序的文件及数量列表def cal_words_freq(files, reverse=True):用 KMP 算法搜索计算所有传入的文件中所有单词各自的数量,返回有序的文件及单词列表及数量def cal_words_freq(files, reverse=True):用 KMP 算法搜索计算所有传入的文件中所有单词各自的数量,返回有序的文件及单词列表及位置class File:
    def __init__(self, file_pos):初始化函数,传入文件路径作为属性def get_content(self):获得文件内容def set_content(self, content):将文件内容修改为 contentdef get_huffman_codes(self):统计文件内容中 word 出现的次数def get_huffman_codes(self):获得哈夫曼编码表def get_encodeStr(self):获得文件内容通过哈夫曼编码表编码后的字符串def get_decodeStr(self, huffmanStr):通过哈夫曼编码表将编码转换成原字 符串

    SplashScreen.py

    class SplashScreen:
    def __init__(self):继承 PyQt5.QtWidgets.QSplashScreendef effect(self):启动页的渐隐渐出特效

    main_UI.py

    class UI:
    def __init__(self):构造函数,将属性 search_status 和 freqs 初始化def init(self):设置事件触发def add_files(self):调用 PyQt5 的 FileDialog 选择要添加的文件def about(self):“关于”窗口def create_file(self):创建文本文档,并添加文件def cal_words_freqs(self):使用索引,统计词频并生成显示降序列表def load_package(self):读取索引def packaging(self):生成索引文件。使用 six 库中 pickle 打包成二进制文件def clear_list(self):清空文件列表def get_research_content(self):获得用户输入的需要检索的内容def get_files_from_table(self):获得当前文件列表def creat_tableWidget(self, files, nums=[], poss=[]):在 GUI 中生成列表及相关信息def closeEvent(self, event):窗口关闭时出发的关闭事件def search(self):根据索引查找多个单词或词组,并显示频率及位置信息def buttonClicked(self):debug 所用,在底部状态栏显示相关信息def itemClicked(self, row, col):文件表单项目点击事件,打开新的 item 窗口

    item_UI.py

    class item_UI
    def __init__(self, file_pos, keyword=None):初始化,将属性 file 初始化为file_pos,同时高亮 keyworddef init(self, keyword, filename):连接按钮点击事件def highlight(self, pattern, color=”yellow”):将所有 pattern 做黄色高亮处理def hightlight_specific(self, pos=(0, 0), color=”gray”):按位置高亮def encode(self):调用 self.file.get_encodeStr()显示编码def decode(self):调用函数进行译码def huffman_codes(self):显示哈夫曼编码表def edit(self):将文本框控件变为可编辑def save(self):将文本框中的文本保存到文件中def search_substitute(self):打开新窗口,查找或替换def translate(self):翻译所选文本

    about_UI.py:“关于”窗口
    freq_UI.py:显示表单的窗口。用来显示词频统计
    huffman_UI.py:显示表单的窗口。用来显示哈夫曼表
    file_UI.py:用来选择文件的窗口
    create_file_UI.py:新建文档的窗口
    progressbar_UI.py:进度条窗口
    search_UI.py

    class search_UI:
    def __init__(self, item_ui):构造函数def init(self):连接按钮事件def prepare(self):计算所有用户查询的单词的位置信息def next_word(self):高亮下一个搜索结果def count(self):计数def substitute(self):替换当前高亮的一个结果def substitute_all(self):替换所有符合条件的结果


    3 详细设计3.1 构造哈夫曼树
    3.2 KMP 算法3.2.1 获取 next 表
    3.2.2 文本匹配
    3.3 UI 设计3.3.1 主界面搜索多个单词和词组

    创建索引

    3.3.2 item 窗口
    查找下一个

    翻译功能

    显示哈夫曼编码表

    4 总结与提高当我刚拿到题目要求的时候,我的内心是抗拒的——需要自己完成哈夫曼编码(包括统计词频,建立哈夫曼树,根据哈夫曼树计算对应的编码),KMP 算法(之前只是听说过,完全不清楚原理及算法),而且这一切都需要用图形用户界面来展现出来,于是只能硬着头皮上了。
    最开始时,我决定从数据结构和算法入手,先不管图形界面。于是开始在网上学习 KMP 算法的原理及步骤。在和同学讨论的过程中,我们慢慢地结合着资料把代码写了出来,并测试了与传统算法的速度对比,很有成就感。
    然后是哈夫曼树。这部分相对简单,因为其原理我们已经在上学期的课程中接触过,所以主要的时间都用来写代码。过程很顺利,达到了理想结果。
    到了最麻烦的图形界面部分,开始时我本想使用 wxpython 库来实现,但在简单的了解 过后,我发现这个库的更新情况不是很理想,尤其是它的 designer 和库的版本不同步,designer 生成的代码甚至直接运行会报错,于是我放弃了,转向更为成熟的、跨平台的 PyQt5。早就 听说过 Qt 的大名,终于有机会使用下了。最开始遇到的问题,每个程序进程只能运行一个 MainWindow 类,在不知道的情况下打开多窗口一直报错,后来得以解决。随之而来的是文本的高亮问题,后来查到的方法是在文本框中控制虚拟光标选中目标然后更改其格式,已达到高亮的效果。
    在完成大部分功能要求后,我开始想其他扩展功能。翻译功能是个很棒的想法。起初, 我想建立本地的词典资源文件,通过索引或二分法进行查找,但后来发现很难找到好用的文本词典资源。于是转而使用爬虫进行翻译,还好效果不错,只是需要联网。
    另一个扩展功能本来是想做通过自然语言识别来对文本进行一些情感分析或者全文概括,但在查阅部分资料后发现很难实现。首先,文本本身多为记叙文,很长且情感并不明显, 其次需要的类似的数据集几乎找不到,所以最终放弃了这个想法。
    最后是将整个程序封装成 Windows 下运行的 exe 应用程序,使用了 pyinstaller 库。过程很简单,只需要在命令行进行一些操作就好了。
    在整个编写课设的过程中,主要学习了 PyQt5 的使用,对于以后编写简单的图形用户界 面非常有帮助。复习巩固了哈夫曼树的创建。学习了 KMP 这种字符串匹配的算法。同时我对于 Python 的使用也更加熟练。
    对于自己完成课设情况的评价呢,我觉得我完成的还是不错的(哈哈哈)。虽然从美观方面来讲很一般,但是功能的实现以及代码的结构还是可以的,基本功能和扩展功能基本都做到了,后期的 Bug 调试也基本保证了正常使用过程中程序不会报错崩溃。如果要改进的话,可能更多会把注意力放在如何把各种功能的表现形式变得更美观。
    非常感谢这种独自完成整个项目的机会,各方面都有所提升,也很有成就感。能看着整个程序跑起来还是非常爽的!
    0 评论 15 下载 2018-12-13 09:32:33 下载需要9点积分
  • 基于VC++实现的Windows平台逆向调试器

    前 言程序不是写出来的,而是调出来的!回忆学生时代写程序的时候,遇到 bug 往往很无奈,常常是把程序从头到尾看一遍,或者是在一些关键的地方,觉得会出错的地方,printf一下,来判断是不是这里出错了。曾经还觉得这个方法不错,沾沾自喜呢。
    后来工作后,或多或少要调一些程序,但是总觉得不得要领。 所以我觉得无论是软件逆向工程的研究者还是开发人员,都有必要自己实现一个调试器,当你的调试器完成的时候,或许你对软件调试会有一种顿悟的感觉。
    而且可以根据自己的需要,写一个适合自己的调试器,对于逆向或者调试程序来说,都会得心应手,而且还可以过针对某些调试器的反调试哦!
    因鄙人没有天马行空的创意,亦没有巧夺天工的技术!所以调试器设计得也有很多不足,但也略有心得,故拿出来,与大家分享之,希望能帮助到有需要的人。
    由于本人能力有限,文中必然有错漏之处,恳请读者不吝赐教。
    第一章 调试器框架1.1 框架的搭建有写过Windows程序的人都知道Windows是基于消息的,当程序创建,窗口移动,键盘按下等,窗口过程函数都会收到消息,然后根据消息做相应的处理。其实调试器也是这样的,在 MSDN 中已经给出了一个现成的框架了,如下,我已经去掉部分注释了,如果想获得祥细信息的话可以查下 MSDN。
    DEBUG_EVENT DebugEv; // debugging event information DWORD dwContinueStatus = DBG_CONTINUE; // exception continuation for(;;) { // Wait for a debugging event to occur. The second parameter indicates // that the function does not return until a debugging event occurs. WaitForDebugEvent(&DebugEv, INFINITE); // Process the debugging event code. switch (DebugEv.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT: // Process the exception code. When handling // exceptions, remember to set the continuation // status parameter (dwContinueStatus). This value // is used by the ContinueDebugEvent function. switch (DebugEv.u.Exception.ExceptionRecord.ExceptionCode) { case EXCEPTION_ACCESS_VIOLATION: case EXCEPTION_BREAKPOINT: case EXCEPTION_DATATYPE_MISALIGNMENT: case EXCEPTION_SINGLE_STEP: case DBG_CONTROL_C: } case CREATE_THREAD_DEBUG_EVENT: case CREATE_PROCESS_DEBUG_EVENT: case EXIT_THREAD_DEBUG_EVENT: // Display the thread's exit code. case EXIT_PROCESS_DEBUG_EVENT: // Display the process's exit code. case LOAD_DLL_DEBUG_EVENT: // Read the debugging information included in the newly // loaded DLL. case UNLOAD_DLL_DEBUG_EVENT: // Display a message that the DLL has been unloaded. case OUTPUT_DEBUG_STRING_EVENT: // Display the output debugging string. } // Resume executing the thread that reported the debugging event. ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus); }
    Windows 提供了一些 Debug API,他们已经实现了一般调试器所需的大部分功能,通过现有的框架和Windows提供的API我们可以很容易的搭建起一个调试器的框架出来。
    1.2 调试事件种类
    1.3 调试事件处理通过对上面几个事件的处理,基本可以实现我们的调试器了。当我们创建一个调试进程后,调试器会收到 CREATE_PROCESS_DEBUG_EVENT 事件,在这里我们做一些PE解析工作,比如取得OEP等等,然后在OEP处下一个Int3 断点,处理完之后,将调用ContinueDebugEvent函数来继续线程的运行,这里要注意,这个函数的 dwContinueStatus 有二个取值,DBG_CONTINUE 和DBG_EXCEPTION_NOT_HANDLED,如果这个异常我们可以处理则取DBG_CONTINUE , 否 则 取 DBG_EXCEPTION_NOT_HANDLED 。DBG_EXCEPTION_NOT_HANDLED 则告诉操作系统,我的程序不能处理这个异常,给你处理,然后操作系统再问下调试进程能不能处理,如果不能的话,直接给干掉。
    同时还有一点要注意,当创建一个调试进程的时候,系统会先调用一次DebugBreak 函数,我们只要判断一下是不是系统调用的,是的话无视掉就行了。
    接着调试器再调用WaitForDebugEvent函数等待调试事件的发生,直到接收到EXIT_PROCESS_DEBUG_EVENT事件时结束。
    第二章 内存断点2.1 问题的提出与分析2.1.1 问题提出因为内存断点是调试器的一个难点,所以我们这里放在最前面讲,这样也可以避免在后面的设计中一些组合问题的产生。
    一个内存分页可能会存在多个断点,一个断点可能会跨多个内存分页。某个内存分页可能不具有用户要设的断点属性。某内存分页具有读与写的权限,但是用户只下了写的断点,如果将读的权限也一起拿掉,程序在读那块内存分页也要断下来,这样将大大的降低程序的效率,如何优化,以及断点信息与内存信息的存储。
    2.2 问题的解决2.2.1 内断断点与内存分页的关系一个内存分页可能会存在多个断点,一个断点可能会跨多个内存分页。这个不由的让我们想起第一阶段的学生管理系统(一个学生可以选任意门课程,一门课程可能有任意个学生选)。其实二者的关系是一样的,因为是多对多的关系,所以我们要构造出第三张表来管理他们的关系。
    为了检测用户下断点有效性等问题,在下断点的时候取得那个内页分页信息,判断地址是否有效,然后再判断是否具有要断下来的属性。对一个没有读属性的内存分页下读断点是无效的。
    为了提高效率,中间表用了二个,一个是以内存页序号为主键,一个是以断点序号为主键,使用动态邻接表(长度自动增长)存储,这样可以提高程序的处理速度。当调试器收到异常信息,首先取得产生异常的地址,以及错误编码,再通过异常地址取得内存页的序号,再通过内存页的序号去邻接表里查他有多少个断点,如果当前有内存断点的话,恢复内存页原属性,然后再判断当前位置是否处于用户设的断点地址内,是的话,处理用户的请求,最后设单步,在单步中再将内存页的属性设成新的属性。
    添加内存断点流程图如下所示:

    处理异常流程图如下所示:

    内存断点本来就是一件很耗资源的,为了最大的提升效率,当处理某内存分页具有读写的权限,而用户只下读或写的权限的这类情况,我们可以通过对原属性的分析,然后再减去用户新的属性,再根据结果生成新的属性,因为移除某个属性,并不能单单的通过去除某个位而完成。
    相关记录项的结构:

    内存页与断点关系表(邻接表):

    第三章 int3 断点与硬件断点3.1 问题的提出与解决3.1.1 问题提出如何实现软硬件断点?如何判断用户断点的正确性?如何提高效率?
    3.2 问题的解决3.2.1 Int3断点的实现8086/8088 提供断点指令Int3 和单步标志TF,调试工具利用它们可以设置断点和实现单步。从 80386 开始,在片上集成了调试寄存器。利用这些调试寄存器不仅可以设置代码执行断点,而且还可以设置数据访问断点,不仅可以把断点设置在RAM 中,也可以把断点设置在ROM 中。
    Int3 是Intel系列 CPU专门引入的用于表示中断的指令,对于Int3 断点,常见的做法是在用户要下断点的地方,将指令替换成CC,当调试程序运行到 CC 处,调试器将会收到EXCEPTION_BREAKPOINT消息,然后在这里将原CC处的代码还原及 EIP 减一再处理用户的请求。用 Int3 的好处很明显,可以设任意多个断点,缺点是改变了程序的原指令,容易被软件检测到,而且这里可能会遇到一个问题,当用户在某个内存分页设了一个不可读、不可写的断点,这时调试器是无法将CC 写进去的,也无法将原来的指令读出来。所以在设之前,我们先将目标内存页的属性设为可读可写的,设完之后再将内存页置为新的属性(移除了用户断点权限的新属性),这个开销是非常大的。
    本程序中,对于用户断点的正确性检测只做了如下判断,首先是判断用户的断点是否是处于有效的内存分页内,然后再判断是否重复,同一个内存地址,只能设一个断点,而对于Int3 断点和硬件执行断点并没有判断是否处于指令起始处。
    当初的设想是被调试程序跑起来的时候,将被调试程序的指令全部解析一下,然后取得每条指令的正确起始位置,添加到树中,然后用户在下 Int3 断点的时候,再判断一下下的断点是否处于指令的首地址处。后来请教了一下老师以及查看windbg 等同类软件,发现也没有做这类的检测,同时分析觉得此弊大于利,所以放弃了这种做法,程序中并没有检测。
    3.2.2 硬件断点的实现 80386和80486都支持6个调试寄存器,如图:

    他们分别是 DR0、DR1、DR2、DR3,调试状态寄存器 DR6 和调试控制寄存器DR7。这些断点寄存器都是32位寄存器。
    Dr0~3 用于设置硬件断点的线性地址,也就是断点位置,产生的异常是STATUS_SINGLE_STEP 。这些寄存器长 32 位,与 32 位线性地址长度相符,处理器硬件把执行指令所涉及的线性地址和断点地址寄存内的线性地址进行比较,判断执行指令是否触及断点。与 Int3 不同的是,硬件断点不仅可以设置在RAM 中,也可以设置在ROM 中,且速度比Int3 更快。
    Dr7 是一些控制位,用于控制断点的方式。其中有四个GiLi,分别对应四个断点,Gi、Li控制DRi所指示的断点i在断点条件满足是,是否引起异常。
    Gi 和 Li 分别称为全局断点允许位和局部断点允许位。在任务切换时,处理器清各 Li 位,所以 Li 位只支持一个任务范围内的断点,任务切换并不影响 Gi位,所以Gi支持系统内各任务的断点。
    R/Wi分别对应四个DRi,R/Wi字段指示DRi断点信息的类型。每一个R/W占两位,所表示的类型为:

    LENi字段指示DRi断点信息的长度,每一个LENi占两位。所表示的长度如下:

    注意: 指令执行断点的断点长度必须为 1 字节,数据访问的断点长度可以是 1、2、4 字节。
    Dr6 用于显示是哪些引起断点的原因,如果是 Dr0~3 或单步(EFLAGS 的TF)的话,则相应设置对应的位。
    Bi表示由DRi所指示的断点引起的调试陷阱。
    对于 DR7 的设置,原本程序中是使用位移来完成的,但是调试的时候非常之不方便,所以后来改用结构体了。结构体的定义如下:

    3.2.3 对于int3断点的优化当设int3 断点的时候,判断一下当前调试寄存器有没有空闲的,有的话则优先使用调试寄存器,因为 int3 断点涉及内存分页属性,读目标进程内存,写目标进程,而硬件断点只需设调试寄存器,这样可以大大的提高效率。
    第四章 函数名的解析4.1 问题的提出与分析4.1.1 问题的分析最初对于这个问题处理的想法就是分析被调试程序的导入表,后来和同学讨论,发现这样不能解决问题,因为 API 里可能还调用了别的 API,再跟进 API的话,程序就不能正确解析函数名了!
    据说Windows有上万个API,如何组织这些数据,如果提高查询的效率、以及最大化的合理使用空间。
    4.2 问题的解决4.2.1 函数名与函数地址的组织用一个链表用来存储已经加载的 DLL的名字已及当前状态。为了提高查询的效率,同时记录DLL名字的CRC值,当查找时,算出要查找的DLL名的CRC值,再直接查找通过CRC值查找,当 CRC值相同时,再用字符串比较是否完全相等,当加载DLL比较多的时候,或者经常要查询的话,这样可以大大的提升的效率。
    同时用红黑树记录函数名地址(函数名在被调试进程里面的地址)以及函数的绝对地址(基址加偏移后的真实地址)。
    红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。
    4.2.2 函数名的显示在我的设计中,使用的是 OD的反汇编引擎,结构体中有记录命令的类型,首先判断他是不是call、jmp 这类的指令,然后再根据相应的值,找出API的地址,再判断目标地址是否是 API 函数的地址,是的话则从本地加载 DLL 链表中取得DLL名字,然后再通过函数名地址去调试进程中取得函数名(在程序中,并没有申请空间保存函数名,因为导出函数可能非常之多,不停的申请空间,然后字符串拷备,这个开销是非常之大的,所以在程序中我们只记录函数名在调试进程中的地址,同时 dll 名链表中有个状态值,当状态值为真时,表明那个 dll 当前已经加载,这样可以直接去内存拿函数名)。而对于call ebp 之类的指令,而需要在运行时才解析,因为在没有执行到那条语句的时候,寄存器的值是不确定的。
    效果如下:

    第五章 指令记录5.1 问题的提出与分析5.1.1 问题的分析当初听到这个的时候,觉得无从下手,而且也想到了很多极端的问题,比如运行的时候不停的修改原来的指令来达到往下运行的效果。然后进到 API 里面去了,那个记录量是非常之大的,而且当时 A1Pass 的测试结果是每秒只能跑 7 条指令。于是花了很多时间在想着效率提升这块,后来和同学讨论一下,他叫我学习一下OD的这个功能。
    5.2 问题的解决5.2.1 数据的组织因为可能记录的数据量非常之大,所以要采用一种合理的数据结构,比如用户可以按指令跑的顺序记下来,也可以按地址的高低来记录,程序在运行中,可能某个地址的指令要跑很多次,但是我们不能重复记录很多次,所以这需要一种特定的数据结构,在程序中,我使用的是带头尾指针的链表与红黑树的结合,首先是将指令加到链表尾部,然后再插到红黑树里面去,这样查找起来的话,速度非常快的(因为我们每跑一条指令,都要先判断一下树中有没有,没有的话才插到树中去),而且体积只是比单单的红黑树多一个指针域。

    结合后的数据结构:这样就可以把链表与树的优点结合起来了,而补了链表的查找效率的不足。

    5.2.2 指令记录实现在第一个解决方案中,采用的是置单步记录,但是经过测试这个效率不高,而且处理起来也很复杂,比如对于每个 call 之类的指令,判断是不是调用 API 之类的,而且经常会出现一些小问题,兼容性也不太好。
    在第二个解决方案中,采用的是内存断点的方法,这个实现起来方便快捷,也不用判断是不是调用API,而且可以指定要记录的部分,不过内存断点的开销还是挺大的。
    5.2.3 效率的优化假定计算机访问寄存器需要花费一个单位时间片,访问 cache可能要花费几个单位时间片,或许现代计算机已经可以像访问寄存器那样高速了,但是访问内存却要花费一百多的单位时间,则访问磁盘则更慢了。
    为了充分的利用计算机时间,让写入磁盘与处理同时进行,在树节点中,我们直接记录指令的十六进制的值,而不直接记录反汇编结果,这样最大的节省空间,如果对于调用 API 的话,在节点 JmpConst 中记录,如果没有的话,这个值为零。然后在写入磁盘的时间,调用反汇编引擎将 Dump 的内容解析出来,如果JmpConst有值的话,再转成相应的DLL名和函数名。这样就可以并发处理了,从而充分的利用了CPU的时间以及节省了内存空间。
    结点内存图:

    第六章 反反调试6.1 问题的提出与分析一些恶意软件常常使用一些反调技手段用以阻碍对其进行逆向工程。反调试技术是一种常见的反检测技术,因为恶意软件总是企图监视自己的代码以检测是否自己正在被调试。为做到这一点,恶意软件可以检查自己代码是否被设置了断点,或者直接通过系统调用来检测调试器。
    虽然在本设计中还没有来得及添加这些功能,但是想通过本章的内容,达到一个抛砖引玉的目的。
    下面介绍二种常见的反调试方法:

    检测CC 断点
    检测调试器

    6.2 问题的解决对于检测断点的反调试方法,我们可以动态的生成断点,比如 CC 断点,CD断点等等,然后在处理函数做相应的处理就可以了。
    检测调试器的存在,一般是使用 IsDebuggerPresent 函数,跟进 API 我们发现他的实现很简单。
    7C8130A3 > 64:A1 18000000 MOV EAX,DWORD PTR FS:[18] 7C8130A9 8B40 30 MOV EAX,DWORD PTR DS:[EAX+30] 7C8130AC 0FB640 02 MOVZX EAX,BYTE PTR DS:[EAX+2] 7C8130B0 C3 RETN
    FS 指向的当前活动线程的 TEB 结构,TEB 的第一个结构是 TIB,TIB 偏移0x18 处是一个回指指针,指向自身,这里虽然是指向自身,但是我觉得这可能是微软以后为了扩展功能用的,而且从相关的代码中也可以看出来,他们都是先在TEB 偏0x18 处取值,然后再做相关的操作。 之后再取得PEB 的地址。
    通过Windbg 我们可以到PEB 的结构信息:
    0:000> dt _PEB 0x7ffde000 ntdll!_PEB +0x000 InheritedAddressSpace : 0 '' +0x001 ReadImageFileExecOptions : 0 '' +0x002 BeingDebugged : 0x1 '' +0x003 SpareBool : 0 '' +0x004 Mutant : 0xffffffff +0x008 ImageBaseAddress : 0x01000000 +0x00c Ldr : 0x00181ea0 _PEB_LDR_DATA +0x010 ProcessParameters : 0x00020000 _RTL_USER_PROCESS_PARAMETERS ………………
    这样我们就可以看到,IsDebuggerPresent 只是简单的通过判断 PEB 结构的BeingDebugged 值来判断是否被调试器加载。
    这样很简单的反反调试就出来了,我们只需将 PEB 结构的BeingDebugged的值改成0 就可以了。
    __asm { mov eax, fs:[0x18] mov eax, [eax + 0x30] mov byte ptr [eax+2], 0 }
    只要将这段代码写到目标进程里跑一下就可以了。当然,更好的话当然是自己实现CreateProcess这类函数。
    结束语本系统的功能基本实现,因为个人努力不够的关系,很多地方不尽人意,系统原定的很多功能也被减去了,对于部分反调试功能等等,这个将作为后期的开发计划。
    1 评论 7 下载 2018-12-04 09:28:27 下载需要11点积分
  • 基于QT实现的宠物小精灵人机对战游戏

    1 依赖环境
    C++ 11 Support

    MSVC >= 14 (2015)GCC >= 5
    SQLite 3 (include in ./include)
    QT5

    2 特性
    使用C++11编写
    实现了简单反射,可以通过GetClassByName通过字符串获得相应类的对象
    使用反射工厂来创建Pokemon对象,配合多态大大简化代码并提供扩展性
    通过宏注入获得类元数据,实现ORM(对象关系模型)封装SQLite数据库操作,运用访问者模式来自动生成SQL语句
    使用JSON提高协议的可扩展性
    良好的人机交互界面,支持多用户同时在线
    健全的错误恢复处理

    3 界面设计登陆及注册
    游戏主界面
    用户列表
    抓取
    背包
    对战
    实时对战(动画)
    战斗结束
    4 框架设计
    5 协议设计游戏通讯协议基于json,以达到良好的扩展性登录
    {"define" : LOG_IN, "username" : username, "password" : password}{"define" : LOG_IN_SUCCESS/LOG_IN_FAIL_WP/LOG_IN_FAIL_AO/SERVER_ERROR}
    注册
    {"define" : SIGN_UP, "username" : username, "password" : password}{"define" : SIGN_UP_SUCCESS/SIGN_UP_FAIL/SERVER_ERROR}
    获取在线用户列表
    {"define" : GET_ONLINE_LIST}{"define" : QUERY_SUCCESS/SERVER_ERROR, "info" : [list]}
    获取离线用户列表
    {"define" : GET_OFFLINE_LIST}{"define" : QUERY_SUCCESS/SERVER_ERROR, "info" : [list]}
    获取用户背包内容
    {"define" : GET_USER_BAG, "username" : username}{"define" : QUERY_SUCCESS, "info" : [{"id" : id, "name" : name, "level" : level, "exp" : exp, "type" : type, "atttackPoint" : ap, "defensePoint" : dp, "healthPoint" : hp, "attackFrequence" : af, "property" : property }]}{"define" : SERVER_ERROR}
    获取用户成就
    {"define" : GET_USER_ACH}{"define" : QUERY_SUCCESS, "rate" : rate, "sum_ach" : sum , "advance_ach" : advance_sum}{"define" : SERVER_ERROR}
    获取对战精灵列表
    {"define" : GET_POKEMON_LIST}{"define" : QUERY_SUCCESS, "info" : [list]}{"define" : SERVER_ERROR}
    获取对战精灵信息
    {"define" : GET_POKEMON_INFO}{"define" : QUERY_SUCCESS, "info" : {"name" : name, "type" : type, "attackPoint" : ap, "defensePoint" : dp, "healthPoint" : hp, "attackFrequence" : af, "property" : property, "exp" : exp}}{"define" : SERVER_ERROR}
    反馈对战胜利
    {"define" : GAME_WIN, "get" : acquire_pokemon, #对战胜利小精灵的名字 "name" : user_pokemon, #用户对战小精灵的名字 "type": type, "attackPoint" : ap, "defensePoint" : dp, "healthPoint" : hp, "attackFrequence" : af, "property" : property, "level" : level, "exp" : exp, "id" : id #小精灵信息在数据库中的唯一id}{"define" : ACCEPT/SERVER_ERROR}
    反馈对战失败
    {"define" : GAME_LOSE, "isLose" : true/false}if isLose == true: {"define" : ACCEPT, "info" : [{"id" : id, "name" : name, "level" : level, "exp" : exp, "type" : type, "atttackPoint" : ap, "defensePoint" : dp, "healthPoint" : hp, "attackFrequence" : af, "property" : property }] }else {"define" : ACCEPT/SERVER_ERROR}
    反馈损失小精灵
    {"define" : LOSE_POKEMON, "id" : id}{"define" : ACCEPT/SERVER_ERROR}
    随机获得一个新小精灵
    {"define" : GET_ONE_POKEMON}{"define" : ACCEPT, "name" : name}{"define" : SERVERE_ERROR}
    6 结构说明6.1 通用6.1.1 Connor_Socket::Socket(socket.h)完成WSA最基本的初始化,加载库文件,以及释放相关资源#define DEFAULT_BUFLEN 10000 // Socket传输缓冲区大小#define CLIENT_PORT 5150 // 客户端连接端口#define SERVER_PORT 2222 // 服务器监听端口 // 单机模式下都指向localhost#define CLIENT_ADDR "127.0.0.1" // 客户端IP地址#define SERVER_ADDR "127.0.0.1" // 服务器IP地址
    6.1.2 Reflector(reflector.h)反射类,维护类名与之对应构造函数的map定义:
    // 定义生成Pokemon对象的函数指针typedef Pokemon* (*PTRCreateObject)(std::string, unsigned int, unsigned int, Attribute, int);// 新建全局函数以new类对象,模拟反射#define REGISTER(_CLASS_NAME_) \_CLASS_NAME_* Create##_CLASS_NAME_(std::string name, unsigned int level,\ unsigned int exp, Attribute attribute, int id) \{ \ return new _CLASS_NAME_(name, level, exp, attribute, id); \} \ \RegisterAction createRegister##_CLASS_NAME_( \ #_CLASS_NAME_, (PTRCreateObject)Create##_CLASS_NAME_)
    成员函数:
    // 使用单例模式,获得Reflector实例// @return:// Reflector唯一对象的引用static Reflector& GetInstance(){ static Reflector instance; return instance;}// 通过类名获得类的构造函数// @param:// className 需要获得的类名// @return:// 生成相应类的函数指针PTRCreateObject GetClassByName(std::string className);// 将类名和构造函数注册到map中// @param:// className 需要注册的类名// method 生成相应类的函数指针void RegistClass(std::string className, PTRCreateObject method);// 隐藏Reflector的构造函数,单例模式只运行使用GetInstance获得Reflector唯一实例Reflector() {}
    成员变量:
    // 类名与之对应构造函数的mapstd::map<std::string, PTRCreateObject> _classMap;
    6.1.3 RegisterAction(reflector.h)注册行为类,通过构造函数以实现自动向Reflector注册成员函数:
    // 将类与回调函数注册到Reflector// @param:// className 类名// PtrCreatFn 创建相应类的回调函数RegisterAction(std::string className,PTRCreateObject PtrCreateFn);
    6.1.4 PokemonFactory(pokemonfactory.h)产生小精灵对象的工厂,通过多态获得小精灵对象成员函数:
    // 产生对战电脑的小精灵实体// @param:// name 对战小精灵的名字// client 与服务器连接的socket指针// @return:// 小精灵对象指针static Pokemon* CreateComputer(std::string name, Connor_Socket::Client *client);// 产生用户的小精灵实体// @param:// str 用户小精灵的序列化数据,包含各种属性// @return:// 小精灵对象指针static Pokemon* CreateUser(std::string str);
    6.1.5 Pokemon(pokemon.h)产生小精灵对象的工厂,通过多态获得小精灵对象定义:
    // 获得对象运行时多态类型// 多编译器支持#ifdef __GNUC__#include <cxxabi.h>#define GET_CLASS_TYPE(_OBJECT_) \ std::string(abi::__cxa_demangle(typeid(_OBJECT_).name(), nullptr, nullptr, nullptr))#elif _MSC_VER && !__INTEL_COMPILER#include <typeinfo>#define GET_CLASS_NAME(_OBJECT_) \ std::string(typeid(_OBJECT_).name())#define GET_CLASS_TYPE(_OBJECT_) \ GET_CLASS_NAME(_OBJECT_).substr(GET_CLASS_NAME(_OBJECT_).find("class ") + 6, \ GET_CLASS_NAME(_OBJECT_).length() - 6)#else#define GET_CLASS_TYPE(_OBJECT_) \ std::string(typeid(_OBJECT_).name())#endif#define CRTICAL_RATE 0.1 // 暴击概率#define MISS_RATE 0.2 // 闪避概率// 小精灵主属性类别enum class Type{ Strength, //高攻击 Tanker, //高生命值 Defensive, //高防御 Swift, //低攻击间隔};// 小精灵属性typedef struct struct_Attribute{ Type type; // 小精灵主属性类型 int attackPoint; // 攻击力 int defensePoint; // 防御力 int healthPoint; // 生命值 int attackFrequence; // 攻击间隔}Attribute;// 经验值列表const int LEVEL_EXP_LIST[15] = {0, 100, 250, 500, 800, 1200, 1800, 2500, 3300, 4500, 6000, 7000, 8000, 9000, 10000};
    成员函数:
    // 各种获取属性函数Type GetType() const { return _attribute.type; }std::string GetName() const { return _name; }int GetLevel() const { return _level; }unsigned long GetExp() const { return _exp; }int GetAttackPoint() const { return _attribute.attackPoint; }int GetHealthPoint() const { return _attribute.healthPoint; }int GetDefensePoint() const { return _attribute.defensePoint; }int GetAttackFrequence() const { return _attribute.attackFrequence; }int GetHp() const { return _hp; }int GetId() const { return _id; }// 判断是否最近一次攻击为暴击bool IsCritical() const { return _critical; }// 小精灵受伤函数// 小精灵真正受到的伤害 = (damage-小精灵的防御值) * 随机系数// 同时有几率闪避// @param:// damage 受到的伤害// @return:// 小精灵是否死亡virtual bool Hurt(int damage);// 小精灵攻击函数// @param:// opPokemon 攻击小精灵的指针// @return:// 攻击造成的伤害virtual int Attack(Pokemon * opPokemon) = 0;// 根据获得的经验增加经验值并自动升级,返回是否升级// @param:// exp 小精灵获得的经验值// @return:// 小精灵是否升级virtual bool Upgrade(int exp);// 小精灵升级时各属性的成长// @param:// master 小精灵主属性virtual void Grow(int *master);// 在攻击、防御、升级时有一定的随机性// @return:// 产生随机的比例系数(0~1)virtual double Bonus();
    成员变量:
    // 精灵名字std::string _name;// 等级int _level;// 经验值unsigned long _exp;// 各种属性Attribute _attribute;// 对战时的实时血量int _hp;// 在数据库中对应的唯一idint _id;// 暂存最近一次攻击是否暴击bool _critical;
    6.2 客户端6.2.1 Connor_Socket::Client(client.h)继承于Socket类,封装winsock,管理socket的发送与接受信息成员函数:
    // 默认构造函数// 委托到带名字的构造函数,传参为UNKOWNClient() : Client("UNKNOWN") { }// 带名字的构造函数,表明与该socket绑定的用户名// @param:// name 用户名Client(string name);// 注册、登陆时使用的函数// 建立与服务器的连接// @param:// requestInfo json序列化后请求信息// @return:// json序列化后的返回信息// @exception:// Server关闭连接/发送失败,抛出 runtime_error;std::string Connect(std::string requestInfo);// 建立连接后使用此函数向服务器发送数据// @param:// requestInfo json序列化后请求信息// @return:// json序列化后的返回信息// @exception:// Server关闭连接/发送失败,抛出 runtime_error;std::string Send(std::string requestInfo);// 关闭与服务器的连接void Close();// 获取该client绑定的用户名// @return:// 用户名std::string GetUserName();
    成员变量:
    // 与服务器通信的socketSOCKET _connectSocket;// 服务器的地址信息SOCKADDR_IN _serverAddr;// 用户名std::string _username;
    6.2.2 Widget(widget.h)登录及注册界面成员函数:
    // 登陆void Login();// 注册void Signup();// 初始化UI界面void InitUi();// 初始化信号槽void InitConnect();
    成员变量:
    // 该widget的ui界面指针Ui::Widget *ui;// 与服务器连接的socket指针Connor_Socket::Client *_client;
    6.2.3 StackWidget(stackwidget.h)管理多个视图的切换成员函数:
    // 刷新用户列表信号void refreshUserList();// 刷新用户背包信号void refreshBag();// 刷新对战界面信号void refreshFight();// 设置当前显示的视图// index = 0 游戏主界面// index = 1 用户列表界面// index = 2 抓取界面// index = 3 用户背包界面// index = 4 对战选择界面// @param:// index 视图的编号void SetCurrentIndex(int index);// 返回到主界面void BackToLobby();// 初始化UI界面void InitUi();// 初始化信号槽void InitConnect();
    成员变量:
    // 该widget的ui界面指针Ui::StackWidget *ui;// 与服务器连接的socket指针Connor_Socket::Client *_client;// 管理多个视图切换的stacklayoutQStackedLayout *_stackLayout;// 各个分视图指针GameLobby *_gameLobby;UserListWidget *_userListWidget;BagWidget *_bagWidget;FightWidget *_fightWidget;ScratchWidget *_scratchWidget;
    6.2.4 GameLobby(gamelobby.h)游戏主界面,包括进入四个功能界面的入口成员函数:
    // 鼠标点击在相应的区域信号void clicked(int type);// 关闭程序信号void closeAll();// 登出void LogOut();// 重载鼠标点击事件,对四个不规则区域监测鼠标点击事件void mousePressEvent(QMouseEvent *event);// 初始化UI界面void InitUi();// 初始化信号槽void InitConnect();
    6.2.5 UserListWidget(userlistwidget.h)用户列表界面,包括查看在线用户和所有用户的背包及胜率成员函数:
    // 返回到主界面信号void back();// 设置用户列表界面的内容void SetUserList();// 显示背包内容// @param:// username 要查看背包的所属用户void ShowBag(QString username);// 初始化UI界面void InitUi();// 初始化信号槽void InitConnect();
    成员变量:
    // 该widget的ui界面指针Ui::UserListWidget *ui;// 与服务器连接的socket指针Connor_Socket::Client *_client;// 在选择用户背包按钮的mapperQSignalMapper *_signalMapper;
    6.2.6 ScratchWidget(scratchwidget.h)抓取界面,点击精灵球有机率随机获得小精灵成员函数:
    // 返回到主界面信号void back();// 重载事件过滤器// 当鼠标移动到精灵球上对其实现震动动画bool eventFilter(QObject *watched, QEvent *event);// 初始化UI界面void InitUi();// 初始化信号槽void InitConnect();// 该widget的ui界面指针Ui::ScratchWidget *ui;// 与服务器连接的socket指针Connor_Socket::Client *_client;
    成员变量:
    // 该widget的ui界面指针Ui::ScratchWidget *ui;// 与服务器连接的socket指针Connor_Socket::Client *_client;
    6.2.7 BagWidget(bagwidget.h)用户背包界面,包括查看背包内小精灵属性和用户胜率及成就的显示成员函数:
    // 返回到主界面信号void back();// 设置背包界面的内容void SetBag();// 初始化UI界面void InitUi();// 初始化信号槽void InitConnect();
    成员变量:
    // 该widget的ui界面指针Ui::BagWidget *ui;// 与服务器连接的socket指针Connor_Socket::Client *_client;
    6.2.8 FightWidget(fightwidget.h)对战选择界面,包括选择用户精灵和对战的电脑精灵成员函数:
    // 返回到主界面信号void back();// 设置背包界面的内容void SetBag();// 对战开始,构建fightroom// @param:// isLose 对战失败用户是否会损失小精灵void FightBegin(bool isLose);// 重载事件过滤器,监测鼠标对背包小精灵label的点击bool eventFilter(QObject *watched, QEvent *event);
    成员变量:
    // 该widget的ui界面指针Ui::FightWidget *ui;// 与服务器连接的socket指针Connor_Socket::Client *_client;// 父对象指针QWidget *_parent;// 选中出战的小精灵QObject *_select;
    6.2.9 FightRoom(fightroom.h)实时对战界面,包括小精灵打斗动画和血量显示以及实时计算成员函数:
    // 开始对战void Fight();// 重载关闭事件void FightRoom::closeEvent(QCloseEvent *event);// 小精灵普通攻击信号void attack(QLabel *, QLabel *);// 小精灵特殊攻击信号void attack_SP(std::pair<Pokemon *, QLabel *> *, std::pair<Pokemon *, QLabel *> *);// 游戏结束信号void over(Pokemon *);// 小精灵受伤害信号void hurt(QLabel *attacker, QLabel *suffer);// 窗口关闭信号void isClosed();// 清除掉血文字信号void clearText();// 隐藏招式标签信号void hideLabel();// 设置普通攻击动画// @param:// attacker GUI中攻击方的Label指针// suffer GUI中被攻击方的Label指针void setAnimation(QLabel *attacker, QLabel *suffer);// 设置特殊攻击动画// @param:// attacker 攻击方的小精灵对象指针和GUI中的显示Label// suffer 被攻击方的小精灵对象指针和GUI中的显示Labelvoid setAnimation_SP(std::pair<Pokemon *, QLabel *> *attacker, std::pair<Pokemon *, QLabel *> *suffer);// 对战完成// @param:// winner 胜利小精灵的对象指针void GameComplete(Pokemon *winner);// 更新双方血量// @param:// attacker GUI中攻击方的Label指针// suffer GUI中被攻击方的Label指针void UpdateHp(QLabel *attacker, QLabel *suffer);// 设置掉血数值void setText();// 设置招式Labelvoid setLabel();// 选择损失的小精灵void Choose(int);// 初始化UI界面void InitUi();// 初始化信号槽void InitConnect();
    成员变量:
    // 该widget的ui界面指针Ui::FightRoom *ui;// 与服务器连接的socket指针Connor_Socket::Client *_client;// 对战用户方的小精灵对象指针和GUI中的显示Labelstd::pair<Pokemon *, QLabel *> _fighter;// 对战电脑方的小精灵对象指针和GUI中的显示Labelstd::pair<Pokemon *, QLabel *> _againster;// 该场对战用户失败是否会损失小精灵bool _isLose;// 在选择损失小精灵所用的信号mapperQSignalMapper *_signalMapper;// 标识用户在对战中是否中途退出bool _quit;
    6.3 服务器6.3.1 Connor_Socket::Server(server.h)继承于Socket类,封装winsock,管理socket的发送与接受信息成员函数:
    // 构造函数,打开监听接口等待请求Server();// 查询用户是否在线// @param:// username 需要查询的用户名// connection 与该用户名绑定的socket// @return:// 是否在线bool Online(std::string username, SOCKET connection);// 将某用户从在线列表移除// @param:// username 需要移除的用户名void Offline(std::string username);// 获得在线用户列表// @return:// 装载有所有在线用户名的liststd::list<std::string> GetOnlineList();
    成员变量:
    // 监听客户端访问的socketSOCKET _listeningSocket;// 地址信息SOCKADDR_IN _serverAddr;// 持有与各个客户端保持通信的线程std::vector<std::thread> _socketThreads;// 持有用户名相对应的socket链接std::unordered_map<std::string, SOCKET> _sockets;// 连接到服务器的客户端数size_t _count;
    6.3.2 Dispatch(dispatch.h)继承于Socket类,封装winsock,管理socket的发送与接受信息成员函数:
    // 传入SOCKET和Server的构造函数// @param:// connection 与相应客户端建立的socket连接// parent server对象指针Dispatcher(SOCKET &connection, Connor_Socket::Server *parent);// 根据请求信息,分发到相应的函数处理请求// @param:// requestInfo json序列化后请求信息// @return:// json序列化后的返回信息std::string Dispatch(json requestInfo);// 登陆处理逻辑json LoginHandle(json&);// 注册处理逻辑json SignupHandle(json&);// 获取在线用户列表处理逻辑json OnlineListHandle(json &);// 查看用户背包处理逻辑json UserBagHandle(json &);// 获取离线用户列表处理逻辑json OfflineListHandle(json &);// 获取用户成就逻辑json UserAchievementHandle(json &);// 获取对战精灵列表处理逻辑json PokemonListHandle(json &);// 获取对战精灵信息处理逻辑json PokemonInfoHandle(json &);// 对战胜利处理逻辑json GameWinHandle(json &);// 对战失败处理逻辑json GameLoseHandle(json &);// 损失小精灵处理逻辑json LosePokemonHandle(json &);// 为用户随机分发一个宠物小精灵// @param:// username 获得小精灵的用户名// @return:// 获得小精灵的名字std::string DispatchPokemon(std::string username);// 与该Dispatch绑定用户登出void Logout();// 获取Dispatcher的内部状态int getState();
    成员变量:
    // 代表用户处于什么状态,如在线空闲或对战int _state;// 与dispatcher绑定的socket连接SOCKET _connection;// 与socket连接绑定的用户名std::string _username;// server指针,以访问server维护的在线列表Connor_Socket::Server *_parent;
    2 评论 45 下载 2018-11-06 18:59:59 下载需要8点积分
  • 基于开源Alice的聊天机器人

    一、聊天机器人基本原理
    语料库中的pattern是模式的意思,可理解为问题,而相应的template可理解为回答(而这一对问答被包裹在了category标签里面)。假如你的语料库像上面的xml文件这样简单,那么当你输入“你是谁”,机器人就会在内存中去一个一个的匹配pattern,最后匹配到了,就会回答“我是小龙”,而你输入其他任何语句,机器人就无从匹配了,程序会出现匹配不到的错误。那么怎样避免程序出错呢?我们修改语料库如下:

    上图中的*,是AIML中的通配符,它匹配任何你输入的语句。当你输入的语句成功匹配,那么返回相应的template后,就不会再去匹配其他的category了。假如程序没有任何相匹配的,那么*总是可以匹配你的输入,机器人会输出“对不起,主人还没有教我怎么回答这个问题呢
    当然AIML解析器所支持的xml标签种类远不止这些,上述是最基本的。AIML所支持的标签种类目测有20种。
    二、为什么AIML解析器不支持中文国外的一款做的很好的聊天机器人(通过了图灵测试),她叫“Alice”(你可以用英文和Alice聊天),它内部有很庞大的语料库,几乎所有用户可能说的话,语料库中都有,而且它用的就是AIML解析器。然而AIML程序中有一些地方会用正则表达式将除了英文字母和数字外的其他字符全部用空格替代,这就是Aiml解析器不支持中文的重要原因,这种正则表达式出现在bitoflife.chatterbean.text.Transformations 类中,比如下面这个函数:


    其中fitting为:

    上面的normalization函数是用来对用户输入做规范化处理的,它做了一下工作:

    在原始输入的内容两头加空格
    把句子中间的任何2个以上连续的空白字符都替换成一个空格
    并对input进行字符过滤

    修改完正则表达式之后,算是成功了一半,那还需要做怎么处理呢?
    我们知道AIML当初仅仅是针对English语言开发的,而English单词之间都是有空格的,所以在前期载入语料库阶段,解析器默认xml语料库中的词之间都有空格,然后通过空格将句子分成一个个单词,最后在内存构建一个匹配树,而且在处理用户输入的句子时也是将其进行了规范化处理,如下代码:

    而为了让它支持中文,一个比较直观的方法是在对用户输入做规范化处理的时候,我们将输入的中文句子中加入空格,比如上述代码的chineseTranslate函数:

    同理,在载入语料库的时候,同样需要写一个类似的函数,功能就是将语料库中的字之间加上空格。
    三、关于标签的支持与使用Aiml标签使用的官方文档
    http://www.alicebot.org/TR/2005/WD-aiml
    http://www.pandorabots.com/botmaster/en/tutorial?ch=4
    根据上面的连接我们可以初步了解各种标签的作用和用法。k
    然而,就我现在的中文聊天机器人,有些标签还不能正常使用,这是需要改进的地方。
    下面是一些我尝试过的一些标签用法:
    最基本:
    <category><pattern>你好</pattern> <template>呵呵</template></category>
    每一个问题和回答都被包裹在<category>标签中
    随机返回功能:
    <category> <pattern>你好</pattern> (或者在后面加一个*) <template> <random> <li>你好呀!</li> <li>嘿嘿</li> <li>我很好,你呢?</li> </random> </template></category><li>是library的意思,不是list
    当你输入“你好”的时候,机器人会从random里面随机取出一句回答你。不过默认的都是先取第一句回答。
    输入重定向功能(<srai>):
    <category> <pattern>你好</pattern> <template> <random> <li>你好呀!</li> <li>嘿嘿</li> <li>我很好,你呢?</li> </random> </template></category><category> <pattern>HELLO</pattern> <template><srai>你好</srai></template> </category>
    输入“hello”的时候,会匹配到第二个category,而srai标签的功能是,将“你好”当成用户的输入,并重新到语料库里去匹配,最后就匹配到了第一个category。换句话说,用户输入“hello”和输入“你好”的效果是一样的。但是在使用srai标签的时候有可能会形成死循环,所以请慎重。
    另外需要注意的是,如果你想要在语料库里面写英文的语料库,那么英文单词都要是大写的,而用户输入的英文可以不用大写。如果你觉得用大写很不爽,那么你可以去修改源代码。
    *,<think>,<set>,<get>,<star>的使用:
    <category> <pattern>我叫*</pattern> <template> <think> <set name="myname"><star/></set> </think>hello, <get name="myname"/>.</template></category>
    测试结果为:

    you say>
    我叫小龙
    Alice>hello, 小龙.

    标签解释:
    set和get里面的myname相当与参数名,首先在set标签中给myname赋值,然后用get标签得到相应参数的值,如果myname之前没有被赋值,那么就是空字符串。<star/>指的是pattern标签中*所匹配的内容。它还能指定index,举个例子:
    <pattern>我叫*呵呵*</pattern> <template> <think> <set name="myname"><star index=”2”/></set> </think>hello, <get name="myname"/>.</template>
    那么这时star标签就会被pattern中第2个*号所匹配的内容替代。而<star/>其实相当与<star index=”1”/> <think>标签可以理解为机器人在思考,它只会在“脑子”里默默的记住一些事情,或者完成一些不会被用户看到的工作。
    Condition标签使用:
    <category> <pattern>我叫*</pattern> <template> <think> <set name="myname"><star/></set> </think>hello, <get name="myname"/>.</template></category><category> <pattern>你好*</pattern> <template> 你好啊! <condition name="myname" value="jack">怎么又是你?</condition> </template></category>
    测试结果:

    you say>
    我叫jack
    Alice>hello, jack.
    you say>
    你好啊
    Alice>你好啊! 怎么又是你?
    you say>
    我叫job
    Alice>hello, job.
    you say>
    你好啊
    Alice>你好啊!

    标签解释:
    <condition>标签中的myname是在set中被赋值的。然后在匹配到“你好*”后,就要判断是不是“jack”
    input标签的用法:
    <category> <pattern>我叫*</pattern> <template> <think><set name="name"><star/></set></think> 你好啊,<get name="name"/> </template></category><category> <pattern>嘿嘿</pattern> <template> 你刚才说:“<input index="2"/>”? </template></category>
    测试结果:

    you say>
    我叫jack
    Alice>你好啊,jack
    you say>
    嘿嘿
    Alice>你刚才说:“我 叫 jack”?

    标签解释:
    <input>标签指的是用户之前的输入,加上一个index,那就是说,用户倒数第几句输入,注意是“倒数”!index=”1”,就是用户倒数第一句输入的内容,以此类推,当然index是会出现越界错误的。
    date标签的使用:
    <category> <pattern>现在什么时间*</pattern> <template><date format="h:mm a"/>.</template></category>
    测试结果:

    you say>
    现在什么时间啊
    Alice>It is 9:49 下午.

    Date标签将获得当前的系统时间
    <that>元素表示先前机器人说的话,例如:
    <category> <pattern>聊什么好呢*</pattern> <template>一起聊聊电影好吗?</template></category><category> <pattern>好</pattern> <that>一起聊聊电影好吗?</that> <template>那你喜欢什么电影呢?</template></category><category> <pattern>不好</pattern> <that>一起聊聊电影好吗?</that> <template>那我也不知道聊什么了~</template></category>
    测试结果:

    you say>
    聊什么好呢?
    Alice>一起聊聊电影好吗?
    you say>

    Alice>那你喜欢什么电影呢?
    you say>
    聊什么好呢
    Alice>一起聊聊电影好吗?
    you say>
    不好
    Alice>那我也不知道聊什么了~

    这个标签还能取前面任意机器人说的话,不过不太熟…没有试验过
    如果要取前面的前面机器人的话,可以用:<that index=”nx,ny”>,例如:<that index=”2,1”表示取机器人倒数第2句的话,<that index=”2,1”>也等于<justbeforethat/>
    <thatstar>标签:
    <category> <pattern>你好</pattern> <template>计算机的型号是什么</template> </category> <category> <pattern>*</pattern> <that>*的型号是什么</that> <template><star/> --》》这里的star标签匹配的是pattern中的*,但是奇怪的,如果把index改成2以后,却也不会出错。 这个型号是 <thatstar/> 里面 <random> <li>很好的商品</li> <li>很流行的商品</li> <li>很华丽的商品</li> </random>。 </template> </category>
    测试结果:

    you say>
    你好
    Alice>计算机的型号是什么
    you say>
    d
    Alice>d 这个型号是 里面 很好的商品。

    thatstar是匹配pattern-side that标签里面的*号的,但是这里没匹配到。
    我想这里也还需要修改源代码。
    set标签也有问题。
    <category> <pattern>他做到了</pattern> <template>谁 ?</template></category><category> <pattern>*</pattern> <that>谁 *</that> <template> Oh, why do you think <set name="他"><star/></set> did that? I wouldn't expect that kind of behavior from <get name="他"/>. </template></category><category> <pattern>*</pattern> <template>啊哦~</template></category>
    测试结果:

    you say>
    他做到了
    Alice>谁?
    you say>
    小龙
    Alice>Oh, why do you think did that? I wouldn’t expect that kind of behavior from .

    假如这样写:<set name="he"><star/>
    那么测试结果为:

    you say>
    他做到了
    Alice>谁 ?
    you say>
    jack
    Alice>Oh, why do you think jack did that? I wouldn’t expect that kind of behavior from jack.

    也就是说这个标签不支持中文。还是需要修改源代码。
    template-side input有问题:
    <aiml:input index = (single-integer-index | comma-separated-integer-pair) /><category><pattern>HELLO</pattern><template>吃饭了吗?</template></category><category><pattern>吃了</pattern><template>我也吃了</template></category><category> <pattern>你好</pattern> <template>计算机 的 型 号 是 什 么</template> </category> <category><pattern>*</pattern><that>* 的 型 号 是 什 么</that><template><input index="4,1"></input> 《《----</template> </category>
    测试结果:

    you say>
    hello
    Alice>吃饭了吗?
    you say>
    吃了
    Alice>我也吃了
    you say>
    你好
    Alice>计算机的型号是什么
    you say>
    345
    Alice>hello

    Input标签中的index貌似当第一个参数是几,就返回倒数第几个用户的说的话,而第二个参数好像只能是1,其他的就会出现数组越界的错误。不知道为什么?
    上面描述的标签部分还有问题,需要改进。
    另外我想说的是,在写xml语料库的时候,一定要写一点,马上重启程序测试一次,看新加的预料是否工作正常,否则你写了一堆的预料后在去测试如果出错的话,就很难跟踪到错误的地方了。
    四、项目结构总览
    五、将数据库集成到聊天机器人中5.1 为什么需要使用数据库Xml文件是AIML所支持的预料载体,而且凭借AIML提供的各种丰富的标签,作者可以设计并编写出很人性化的语料库。显然,通过这种方式写语料库的特点是灵活性很好,能很容易写出“唠嗑”类型的聊天内容。然而,当时对这个项目的定位是客服机器人,也就是说,语料库还应该包含具有业务针对的预料,这部分预料将随着业务的不同而不同。于是我想把这部分预料存储在数据库中形成动态的语料库(我把xml文件中的预料称为静态预料,也就是说这部分预料完善之后就不去频繁的修改),这样做的好处有一下几点:

    客服不用去学习怎样写xml预料,降低门槛
    可以避免xml中预料越来越凌乱,到最后难以管理
    以后可以针对数据库在开发一个语料库管理系统,方便客服管理有业务针对的这部分预料

    5.2 数据库的表是怎样设计的
    字段解释:

    Id:预料的编号,自动生成(identify)
    Createtime:该条数据创建时间,该条数据产生时自动生成(触发器)
    Lastmodifytime:最近更新时间,该条数据的可填写字段被更新时自动修改(触发器)
    Question:具体问题,自己填写
    Replay:具体回答,自己填写
    Label:标签字段(词语之间用空格隔开),里面填写的词语是要能体现question字段主题的,可以理解为一种补充说明,例:question:书是什么?那么书应该是被讨论的主题,lebel就可以填写和书意义相近的词,比如课本,教科书,教材,book,有了这个字段可以从某种程度上增强匹配效果
    Copyfield:拷贝字段,这个字段会在你填写完(或者更新)question,replay,label这3个字段后自动生成(触发器),其内容为上述3个字段的合体,期间用空格隔开。这个字段是要被索引的重点字段

    在以上的描述中,也许你会对某些字段存在的必要性产生疑惑,没有关系,在下面的叙述中也许能解决你的问题。
    5.3 数据库里面的预料怎样使用到机器人当中这就是基于lucene的处理。当程序启动的时候,程序会在载入xml语料库后,lucene就开始对数据库进行全量索引(这其实也是一种载入语料库行为),并在项目的根目录下产生相应的索引文件index以及时间戳文件(timesTamp.txt:该文件记录了当前索引行为发生的时间。将在增量索引时用到)。索引文件会在后面响应用户输入的时候用到。
    下面是索引操作的代码(在com.job包):



    其中sql语句是这样的:

    5.4 在机器人处于运行状态时修改了数据库的预料,怎样做到与客户端的同步这里将用到时间戳的概念。首先当机器人程序运行的时候,里面的一个timmer任务也会同时运行,这个任务做的工作是定期(比如每隔10秒),进行一次增量索引—-lucene中的概念。增量索引所针对的数据等同与这样一条sql语句所返回的数据,该sql语句满足的逻辑是:查找出数据库中所有Lastmodifytime字段值大于timesTamp.txt中记录的时间。那么这样,每次客服对数据库做的预料修改,都会在隔一段时间后同步到客户端。

    六、机器人怎样响应用户的输入在没有引入数据库前,只要调用Aiml提供的聊天接口就能得到一个字符串返回了,但是现在加入了数据库,那么我的处理的思路是这样的:我在xml语料库里面的*通配符所对应的templete做了标记,如下图:

    注意到上面的红点了吗?我在这个回答最前面加上了“#”的标记。逻辑如下:

    代码如下:


    七、聊天机器人学习功能实现其实之前在介绍Aiml标签的时候,有2个很重要的标签还没有介绍到,那就是\<system>和\<gossip>标签。
    在我的xml语料库中有一个文件叫Study.xml,它的内容如下:

    (ps:如果不懂这里面一些标签的功能,可以回顾之前的标签功能解释)
    我们看看这样的测试结果是什么:

    等重启聊天机器人程序的时候,问同样的问题:

    这其中都做了哪些工作呢?下面解释:
    system标签的工作原理我还不是很清楚,但是我们可以看一下AIML解析器对应的System.java里面的process方法干了什么:

    这个函数我暂时解释不清楚,但我知道上面的语料库中system标签只是会被learn函数的返回值替代。
    我重点介绍一下gossip标签的工作过程:
    首先看AIML解析器中对应的gossip.java文件的process方法干了什么:

    match参数其实已经封装了上述语料库中learn函数的返回值“你的主人帅吗:帅到爆棚”。而super.process(match)就是取出这个字符串。
    然后我们在看print函数:

    看起来像是在什么文件里面写入了什么内容,我们在看outputStream()函数:

    上述代码中的path其实就是指的这个文件:

    很显然,客户每一次对机器人“教学”的内容,都会被写入gossip.txt文件当中,而且在写操作时是append的(即不会把原来的覆盖掉)。而当每次重启机器人程序的时侯,GossipLoad.java类都会去读gossip.txt的内容,并构造一个gossip.xml文件将其写到预料库中.该类主要代码如下:

    Load函数在加载xml语料库到内存之前调用。这是必须的,因为必须先通过load函数生成gossip.xml文件后,然后统一加载到内存中去。代码如下:

    Gossip.xml中的内容如下:

    八、聊天机器人存在的不足
    数据库匹配做的不好,或者说匹配率低,而且还不是很准确
    两种语料库的结合显的有点牵强
    xml语料库的设计还是比较欠缺的,首先预料不够丰富,而且靠人工编写预料不是一个好办法
    0 评论 16 下载 2018-11-20 23:31:30 下载需要10点积分
  • 基于C#实现的支持AI人机博弈的国际象棋游戏程序

    1 背景和意义1.1 项目意义
    该项目的成功推进和完成将达到 AI 比赛过程自动化的目的,有助于比赛的顺畅、成功开展以及比赛时间的有效节约
    该项目的成果将有助于《人工智能原理》课程的学生对于自己编写的 AI 程序的测试
    该项目的成果将有助于国际象棋 AI 的后续研究和教学展示
    该项目的成果由于支持人机、机机博弈,也具有一定的游戏性和观赏价值

    1.2 项目目标完成一个图形界面国际象棋棋盘软件。它主要具备以下功能:

    图形界面显示(显示与用户交互的窗体控件、显示棋盘和棋子)
    游戏参与者加载 AI 程序
    游戏组织者选择游戏模式(自动、手动)
    游戏组织者开始游戏、进行游戏

    软件与 AI 程序通信,完成自动博弈
    游戏参与者/游戏测试者手动走子
    软件判断走法符合规则
    软件判断游戏结束(局面是否出现将军、欠行等,计时是否结束)
    软件对走子计时


    一些性能约束:

    能在时下主流的笔记本电脑(x86和x64 架构的多核 CPU)上运行
    在 Windows 7 及以上操作系统运行

    1.3 开发用语言和环境
    项目的编码用C#语言写成,图形界面用 WinForm(Windows 窗体 API)实现。
    开发环境为 Visual Studio 2017,框架为.NET Framework 4.6。

    2 详细需求描述2.1 对外接口需求2.1.1 用户界面
    UI1:唯一的一个象棋棋盘和控制窗体主界面。该界面的风格为 WinForm (Windows 风格窗体),方便熟悉 Windows 界面语言的人快速上手和操作。该界面的图示为:


    UI1.1:在载入 AI 时,应弹出填入载入 AI 信息的对话框,如下图所示:


    UI1.2:在设置选项时,应弹出含有选项的对话框,如下图所示:

    2.1.2 通信接口本软件与参赛 AI 交互使用的是操作系统的“标准输入输出”;内容协议采用 SAN 格式。
    2.2 功能需求2.2.1 图形化显示棋盘和棋子2.2.1.1特性描述 主界面显示一个 8x8 的正方形国际象棋棋盘。棋子以图形的方式显示在国际象棋 棋盘中。参赛者或组织者开始游戏或重置游戏时,应达到初始化棋盘显示的功能;游戏开始后,棋盘显示游戏的棋局,并随着双方走子不断更新显示。如 UI1 所示。
    2.2.2 加载 AI2.2.2.1 特性描述参与比赛的两方参赛者,均可以在比赛开始前在本软件中载入自己的 AI 作为进程 运行,准备参与博弈。载入内容包括“是白/黑方”、“可执行文件路径”、“执行参数”。
    2.2.2.2 刺激/响应序列
    刺激:用户点击“载入白方 AI”或载入“黑方 AI”
    响应:系统弹出对话框,用户界面如 UI1.1
    刺激:用户在可执行文件文本框中输入可执行文件路径
    响应:文本框成功接受输入
    刺激:用户在对话框中点击“浏览”
    响应:系统弹出二级对话框,允许用户浏览文件选择可执行文件
    刺激:用户选择完毕,点击“确认”


    2.2.3 以不同的模式进行游戏2.2.3.1 特性描述进行游戏可以有以下几种模式:

    其中,”人类手动”指人类利用鼠标点击棋盘上的棋子、选择其移动位置来完成走 子,”AI 自动”指本软件不经用户确认直接从 AI 读入信息完成走子,”AI 手动”指本软件需要用户手动点击“从 AI 读入”按钮才从 AI 读入信息。
    游戏组织者在开始游戏前,需要可以从本软件中选择其中一种模式。
    2.2.3.2 刺激/响应序列
    刺激:用户点击界面 UI 上的“模式选择”单选按钮
    响应:本软件接受用户的点选输入,并相应地更新 UI
    刺激:用户点击“开始游戏”
    响应:本软件进入游戏进行状态,根据选好的模式决定是否向 AI 发送其所在方(黑/白)信息,并开始从 AI 读入走子信息走子

    2.2.3.3 相关功能需求
    2.2.4 与 AI 进程通信、处理用户交互,实现自动、手动博弈;判断走子是否符合规则2.2.4.1 特性描述作为棋盘平台,本软件在象棋游戏进行过程中,要根据模式选择的不同,与 AI 进程通信以获得它们的走子信息,以及处理用户交互以获得人工走子信息,实现无组织 者人工干预的自动、手动博弈。同时,本软件还应充当裁判的作用,预防不合法的走子产生。
    2.2.4.2 刺激/响应序列
    刺激:用户点击棋盘
    响应:UI 以颜色的方式提醒用户点击是否有效合法;若有效合法,推进走子的流程
    刺激:用户成功走子
    响应:更新 UI 为走子后的局面;判断游戏是否结束并给出提醒;若对方 为 AI,向其发送走子信息
    刺激:AI 发来走子信息
    响应:判断走子信息是否有效合法;若有效合法,更新 UI 为走子后的局面;判断游戏是否结束并给出提醒;且若对方为 AI 则向其发送走子信 息。若不合法,则回送特殊信息说明走子错误
    刺激:用户点击“停止”
    响应:等待所有附加线程运行完毕,然后停止游戏,更新 UI,给出提示 “游戏已停止”
    刺激:用户点击“重置”
    响应:等待所有附加线程运行完毕,然后重置游戏,更新 UI

    2.2.4.3 相关功能需求
    2.2.5 给游戏计时2.2.5.1 特性描述由于需要控制 AI 走子的时间,故组织者需要对 AI 的走子进行计时。计时的方法是,轮到该 AI 走子时,AI 在走子前等待的时间累计起来,即是该 AI 所用的时间。
    2.2.5.2 刺激/响应序列
    刺激:由于上一步的用户手动走子或 AI 的自动走子,将主动权让给了我方 AI
    响应:本软件开始对我方 AI 的走子计时
    刺激:我方 AI 思考后走子
    响应:本软件停止对我方 AI 的走子计时,将计时的这段时间累加到我方 AI 所用时间上
    刺激:用户点击“重置”
    响应:两方 AI 的计时均归零

    2.2.5.3 相关功能需求
    2.2.6 保存比赛棋谱2.2.6.1 特性描述一场游戏结束后,组织者可能需要保存其棋谱。本软件允许组织者将比赛棋谱复制到剪贴板,以便粘贴到别处保存。
    2.2.6.2 刺激/响应序列
    刺激:组织者选择历史记录里所有走子
    响应:本软件的 UI 做出相应变化,表示组织者成功选定了这些走子
    刺激:组织者按下“Ctrl-C”键。(“复制”操作快捷键)
    相应:本软件将已经选定的走子送至操作系统的剪贴板上

    2.2.6.3 相关功能需求
    2.2.7 处理和保存用户的设置2.2.7.1 特性描述本软件应处理和保存用户的一些参数偏好,包括默认可执行文件路径、是否在运行时隐藏 AI 窗口、观棋时间、是否自动保存 AI 配置。
    2.3 性能需求
    Performance1:速度:每一步走棋所产生的相应变化需在 1 秒内完成,点击按钮后所产生的变化应在 3 秒内完成
    Performance2:负载:能够接受两个 AI 同时运行
    Performance3:适应性:在不同 Windows 版本上能够运行

    2.4 数据需求2.4.1 数据定义和格式要求本软件需要在计算机上存取的数据只有用户设置。用户设置包含如下定义和格式的数据:

    2.5 安全性需求
    Safety1:本软件的运行应对操作系统的完整性无害
    Safety2:本软件的运行应不违反操作系统规则,不导致操作系统陷入崩溃
    Safety3:本软件应不对用户的其他文件造成危害,包括修改和删除
    Safety4:本软件应对每个 Windows 用户的用户设置数据设置屏蔽,防止互通

    2.6 可靠性需求
    Reliability1:本软件不应在用户对其正常使用时突然退出、崩溃
    Reliability2:本软件不应在 AI 程序出现错误、故障、突然退出时发生故障
    Reliability2.1:本软件应该检测到 AI 出现异常,并弹窗报告用户,并停止 正在进行的游戏
    Reliability3:本软件应当合理管理分配的内存,防止出现内存泄漏
    Reliability4:本软件应当线程安全,防止多个线程同时操作一个对象产生的不 协调和错误

    2.7 用例图
    2.8 用例描述2.8.1 选择模式
    2.8.2 进行国际象棋游戏
    2.8.3 AI接受系统标准输入
    2.8.4 AI给出标准输出
    2.9 概念类图由于本软件从需求来看功能要求不多,故对于所有用例,统一绘制一个概念类图于此。

    解释:ChessGameLogic 是关于游戏运行时逻辑的类,为软件的核心; ChessGameForm 是游戏运行的窗体,充当 UI;ChessGameRule 是判断走子是否合法、游戏是否结束时用到的象棋规则类;AIProcess 和 StopWatch 分别为 AI 进程类和计时类。
    2.10 系统顺序图同概念类图,我们将所有用例集合在一起,以一个用户开启软件到进行完一局游戏的全过程绘制了系统顺序图。

    2.11 状态图
    3 整体架构描述本节将描述本软件的整体架构,从逻辑视角和组合视角来描述,采用 UML 包图、 构件图。
    3.1 逻辑视角3.1.1 体系结构设计风格本软件体系结构设计的风格采用模型-视图-控制器(Model-View-Control, MVC)风格。采用该风格的方案明显较好,因为:

    实际开发时使用 C# 结合.NET Framework,非常适合于实现 MVC 风格的体系结构
    能够促进并行开发,缩短开发时间
    该风格的部件区分明朗,便于进行体系结构设计和详细设计

    MVC 风格将整个系统分为三个部件(子系统):

    模型(Model):封装系统的数据和状态信息,也包含业务逻辑。在本软件 中,模型包含国际象棋的规则部分、国际象棋游戏进程的逻辑
    视图(View):提供业务展现,接受用户行为。在本软件中,视图包含程序 的显示窗体、控件。控件既可向用户展示信息,也可以被用户以点击的形式交互
    控制(Controller):封装系统的控制逻辑。在本软件中,控制包含用户点击 控件后触发的事件函数,以及刷新 UI 所需调用的事件函数

    构件图如下:

    3.1.2 概要功能设计和逻辑包图由于本软件的界面和控制不复杂,实现较为简单,视图和控制部件包均可只用一 个逻辑包实现;模型涉及到功能较多,用多个逻辑包实现。用包图表达的最终软件体系结构逻辑设计方案如下:

    3.2 组合视角3.2.1 开发包设计在逻辑视角的基础上,可以用组合视角对于体系结构进行开发包的设计。由于我们的项目较为简单,故采用以下的开发包设计:

    每一个组合包最多转化为一个开发包
    模型部件中依赖关系较多的包组合为一个开发包
    逻辑包中没有循环依赖需要解决,故无需再增加开发包
    为简洁,不再另设不同部件之间的接口包

    在引入.NET Framework 框架提供的类库之后,整个软件的开发包图如下:

    各包的名称、功能和依赖关系均已在图中呈现,故不另外列开发包表。
    3.2.2 运行时进程由于软件简单,故运行时排除 AI 进程外,只有一个主进程。
    3.2.3 物理部署由于软件简单,只需要一个可执行文件部署在本地计算机。
    4 模块分解对于诸模块的分解设计采用结构视角和接口视角来说明。
    4.1 模块的职责按照 MVC 的部件划分,可以直接将每个部件转换为一个大的模块:Model 模块、 View 模块、Control 模块。其职责如下:

    不同模块之间通过简单的函数调用完成连接。
    4.2 MODEL的分解Model 模块包含与象棋的状态和信息有关的对象类,如 ChessGame(以象棋规则为中心的象棋棋局类)、ChessGameLogic(以象棋游戏进程逻辑为中心的象棋棋局 类)、ChessPieces(象棋的棋子类)等。
    4.2.1 Model分解后的职责 Model 模块包含三个开发包,其职责如下表所示:

    4.2.2 Model 分解后的接口规范注:只列出对于本软件有关键作用的接口,重要性较小的接口如计时、用户设置有关的在此不列出。
    4.2.2.1 ChessGameWithRule 的接口规范
    4.2.2.2 AlProcess 的接口规范
    4.2.2.3 ChessGameLogic 的接口规范
    5 详细设计本软件详细设计的基本方法为面向对象设计方法(Object-oriented Design Method),意在将各个构件实现时,用抽象为一系列对象的方式看待。
    5.1 MODEL的分解5.1.1 模块概述和整体结构Model 模块的职责为记录软件运行的状态、象棋的局面信息,处理象棋的走子、 规则。在软件的体系结构设计中,其下分为 ChessGameWithRule、ChessGameLogic、 AIProcess 三个包,分别包含象棋规则、象棋游戏进程逻辑、AI 进程逻辑三个方面的逻 辑。后两个包可各用一个类实现,而前一个包由于构成与所实现的功能更复杂一些, 故可以用多个类来实现。
    这三个开发包的内部构造和职责、相互协作描述如下:

    ChessGameWithRule 包中的核心实现类是同名类 ChessGameWithRule(在实 际代码编写中,更名为 ChessGame)。它除了对.NET Framework 框架,以及 同一包内的一些数据结构类有依赖之外,是一个自成一体的象棋规则实现 类。一个 ChessGameWithRule 对象可以完备地从规则角度上实现一局象棋游 戏的过程。其内有包含棋盘(棋子对象构成的数组 Piece [][])、历史行棋 (Move 对象构成的列表 List<Move>)等,也有 ApplyMove(实现走子)、 GetValidMoves(获得当前所有合法走子)等具体功能方法。除此之外, ChessGameWithRule 包还包含与象棋规则有关的数据结构,如 Piece(棋 子)、Move->MoreDetailedMove(走子,MoreDetailedMove 继承自 Move,包含更多信息)、Position(位置)等等,被 ChessGameWithRule 核心类所聚合。ChessGameWithRule 还包含了一个用于处理 SAN 字符串为 Move 对象的分析函数 PgnMoveParser,便于游戏进程逻辑层面的使用。
    AIProcess 由一个同名类实现,实现一个 AI 进程的逻辑,如启动、停止、标准 输入输出的读写。它调用.NET Framework 系统类,可以控制启动、停止系统 进程,并操作标准输入输出。可以说它是系统进程与本软件的接洽。
    ChessGameLogic 由一个同名类实现,注重于象棋游戏的进程(开始、循环走 棋、何时结束游戏)来实现一局象棋游戏。由于象棋游戏的进程取决于规 则,故它依赖并使用 ChessGameWithRule 作为规则的实现。同时,它也使用 AIProcess 类,向其发送有关于黑白双方 AI 的命令,以实现机器博弈。至于人 工博弈,人类的行棋是通过用户界面,从 View 模块传导到 Control 模块,再调用 ChessGameLogic 里的 ApplyMove 实现的,不全部由ChessGameLogic 实现。

    5.1.1.1 ChessGameWithRule 内的类图
    5.1.1.2 AIProcess 内的类图
    5.1.1.3 ChessGameLogic 内的类图
    以下将不再分每一个小包进行接口规范的描述,而是直接对 Model 内的类进行接 口规范的描述。
    5.1.2 内部类的接口规范与“体系结构设计”中的 Model 接口有所重合的类接口,这里有些就省略不列出。 现将“体系结构设计”中细化的 Model 内部类接口规范描述如下。
    ChessGameWithRule 类的接口规范

    MoreDetailedMove 类的接口规范

    Piece 类(抽象类,具体棋子类的基类)的接口规范

    AIProcess 类的接口规范

    ChessGameLogic 类的接口规范

    5.1.3 Model 的动态模型由于本软件详细设计中的动态模型中,状态图的设计与软件结构是否分解为类关系不大,故动态模型中的状态图省略不画。 现将进行一盘游戏的系统顺序图扩展为详细顺序图,描绘如下:

    6 核心算法6.1 GETVALIDMOVES算法描述ChessGameWithRule.GetValidMoves 是 Model 里象棋规则实现库 ChessGameWithRule 中的一重要函数,可以获得某一方的所有允许的走子。它由两个同名重载函数实现。
    public virtual ReadOnlyCollection<MoreDetailedMove> GetValidMoves(Position from, bool returnIfAny, bool careAboutWhoseTurnItIs)
    这个函数可以将该行动方从 Position from 出发的所有可行走子返回。returnIfAny 和 careAboutWhoseTurnItIs 是内部使用和调试用参数,可以忽略。
    其具体实现如下:
    public virtual ReadOnlyCollection<MoreDetailedMove> GetValidMoves(Position from, bool returnIfAny, bool careAboutWhoseTurnItIs) { ChessUtilities.ThrowIfNull(from, "from"); Piece piece = GetPieceAt(from); if (piece == null || (careAboutWhoseTurnItIs && piece.Owner != WhoseTurn)) return new ReadOnlyCollection<MoreDetailedMove>(new List<MoreDetailedMove>()); return piece.GetValidMoves(from, returnIfAny, this, IsValidMove); }
    描述为自然语言:

    获取当前棋盘 from 位置的棋子为 piece
    如果那个位置没有棋子,直接返回空集合
    否则 ,调用这个具体棋子的多态函数 GetValidMoves,给出从 from 出发的所有 可行走子。同时,还传入当前 ChessGameWithRule 对象下的,对于本局 游戏特化的走子验证函数,方便 Piece.GetValidMoves 调用用于二次验证 走子的合法性

    public virtual ReadOnlyCollection<MoreDetailedMove> GetValidMoves(Player player, bool returnIfAny, bool careAboutWhoseTurnItIs)
    这个函数可以将该行动方(不受 Position 制约)的所有合法走子返回。
    public virtual ReadOnlyCollection<MoreDetailedMove> GetValidMoves(Player player, bool returnIfAny, bool careAboutWhoseTurnItIs) { if (careAboutWhoseTurnItIs && player != WhoseTurn) return new ReadOnlyCollection<MoreDetailedMove>(new List<MoreDetailedMove>()); List<MoreDetailedMove> validMoves = new List<MoreDetailedMove>(); for (int r = 1; r <= Board.Length; r++) { for (int f = 0; f < Board[8 - r].Length; f++) { Piece p = GetPieceAt((File)f, r); if (p != null && p.Owner == player) { validMoves.AddRange(GetValidMoves(new Position((File)f, r), returnIfAny)); if (returnIfAny && validMoves.Count > 0) { return new ReadOnlyCollection<MoreDetailedMove>(validMoves); } } } } return new ReadOnlyCollection<MoreDetailedMove>(validMoves); }
    描述为自然语言:

    初始化 validMoves 为空集
    对于棋盘上的所有格子(位置 Position)作一遍历:

    获得该格子上的棋子为 p
    若格子上有棋子(p 非空)且 p 的拥有者是 player:

    调用 同名函数 GetValidMoves,获取从该位置出发的所有合法走子
    将 validMoves 并上刚刚返回的走子集合


    返回 validMoves

    6.2 ISMOVEVALID算法描述这个函数同样属于象棋规则库。它判断某一个走子在当前游戏的进行情况中是不是合法的,这对于合法走子的生成和判定非常重要。
    public virtual bool IsValidMove(Move move, bool validateCheck, bool careAboutWhoseTurnItIs) { ChessUtilities.ThrowIfNull(move, "move"); if (move.OriginalPosition.Equals(move.NewPosition)) return false; Piece piece = GetPieceAt(move.OriginalPosition.File, move.OriginalPosition.Rank); if (careAboutWhoseTurnItIs && move.Player != WhoseTurn) return false; if (piece == null || piece.Owner != move.Player) return false; Piece pieceAtDestination = GetPieceAt(move.NewPosition); bool isCastle = pieceAtDestination is Rook && piece is King && pieceAtDestination.Owner == piece.Owner; if (pieceAtDestination != null && pieceAtDestination.Owner == move.Player && !isCastle) { return false; } else if(move is MoreDetailedMove m) if (pieceAtDestination != null && pieceAtDestination.Owner == ChessUtilities.GetOpponentOf(move.Player)) { m.IsCapture = true; m.CapturedPiece = pieceAtDestination; } if (!piece.IsValidMove(move, this)) { return false; } if (validateCheck) { if (!isCastle && WouldBeInCheckAfter(move, move.Player)) { return false; } } return true; }
    自然语言描述如下:

    若 move 为空

    返回 false
    获取 move 的源位置的行、列,并以此获取该位置的棋子到 piece
    若 piece 为空或不是本方棋子

    返回 false
    获取 move 的目标位置的,并以此获取目标位置的棋子到 pieceAtDestination
    对于 move 是王车易位的情况作特殊判断,若王车易位不合法:

    返回 false
    调用该 piece 的 IsValidMove 函数,获知该棋子能否如此移动。若不能:

    返回 false
    若该走子非王车易位,且走子后会陷入将军:

    返回 false
    返回 true
    1 评论 20 下载 2018-11-20 09:06:49 下载需要12点积分
  • 基于C++实现的坦克大战游戏

    游戏介绍


    上面分别是下一次要改变的方向,是上、左的时候,原始状态的不同情况;右、下类似
    已移动后,不擦除原来的障碍物为第一出发点
    走到这种状态,是有前提的,这点很重要,也是能处理好的根本原因;不会凭空,走到某种状态
    符合行为规范

    注意:关于同类坦克情况,禁止互相跨越的时候,和这个障碍物的判断,还有所不同,这个 判断,要完全按照9格处理,否则,会导致无法移动;
    1 子弹和坦克数据结构
    2 子弹组织成链表结构敌方坦克的移动和发射子弹,主要考虑在单线程下,如何判断——时间差。
    另外,在控制坦克移动时,可以使用的逻辑,但是,也要注意下,按键去抖。

    3 游戏设计子弹遇到边界或这敌方子弹,消失的时候,对应要free掉产生时的空间,同时,维持链表结 构不被破坏。
    子弹遇到敌方坦克,敌方坦克消失,这个处理,可以在相遇的位置,设置一个不同的值;坦 克移动的时候,检查,它的值是不是包含这个,包含,证明自己被击中,就要销毁掉它的数 据结构,尤其注意子弹的销毁。
    关于,存档,如果杀死了一个坦克,这时候保存,要注意消失的是哪个,因此,需要记录下 标;不过,后来,设计的时候,每死掉一个坦克,就会自动产生一个,因此,这个也算是一 个预留的接口吧。
    单线程处理起来,有时候,会感觉到控制坦克时,有时候会很快,有时候,会比较慢,这也 体现出了一定的局限性。
    另外,最为重要的一点,思想要灵活,各种语言的规范,规矩,互相之间,可以在遵循某些 情况的前提下,进行变通和引进。
    记录至此;至于画图,等其他功能,和贪食蛇基本类似,可以拿来主义。
    4 游戏演示


    1 评论 65 下载 2018-11-06 18:55:18 下载需要8点积分
  • Linux环境下基于GTK的CS聊天系统

    1 项目综述本项目是以Linux C 为主体,使用网络套接字编写,并且具有图形界面(GTK)的可视化聊天室软件。完整实现私人聊天与群组聊天,传文件,日志处理,离线消息,管理员管理功能,信息修改等功能。
    2 开发平台与工具
    编辑器:vim gedit
    编译器:gcc
    调试工具:gdb
    开发平台:fredoa 21 linux4.0.6-200.fc21.x86_64

    3 核心模块思路综述3.1 私聊模块客户端发送信息时,需要发送IP/名称,然后发送信息到服务器端,通过服务器端根据IP/名称,确定其链接套接字,然后发送信息到对应的客户端。就是使用服务器作为中转站。将消息转发到对方的套接字中。
    3.2 群聊模块服务器每接收一条信息就将这条信息,发送给已经为人聊天状态的所有用户。
    3.3 文件传送类似于传送消息,FTP功能只有在处理文件传输功能的时候才会被开启,首先,当需要发送文件的时候,打开文件传输窗口,选择相关文件以及目标对象的名字,发送一个询问语句,等待对方接受。测试经过服务器的转发对方已经接受到了一个文件传送的消息或是显示于聊天窗口,或是显示在FTP 专用窗口,但是只有在FTP主窗口中点击RECV按钮,方可接受,这时服务器首先转发RECV 消息,当消息转发完成后,客户端接受到RECV 信息,确认文件名,开始传输文件,当文件传输完毕后发送一个传送完成的信号,其中如果文件大于800K,那就增加分片标识机制。进一步设计:首先发送端,点击发送按钮,此时立即发送一个基本的数据包,其中包括需要传送的文件,然后有一个读取语句,将后边的发送语句挂起等待一个条件变量,此时对方收发到了先遣数据包,当对方点击接受按钮时,首先会发送一个带有接受标识的数据包用来改变对方的条件变量随后开启接受线程等待最终文件接受完毕后,对方会发送一个文件已经传送完成信号,此时最后一个数据包被接受,跳出循环,线程终止。
    3.4 好友处理模块添加一种新的消息包类型,用新的类型来标记好友相关信息:
    其中好友处理函数又包含三个方面。刷新,添加,删除。这三个字函数,他们根据消息传第的不同内容作出三种响应,最终完成好友相关操作。其中,服务器首先接受一个添加好友的请求,然后将请求转发给目标用户,前提:目标在线,暂不考率目标离线情况(离线默认也可以添加,对方会收到一个被添加的离线消息)。然后由目标给与一个反馈信息,此时需求客户端并不是阻塞模式,可以正常收发信息。才用“发出后不管”的策略。其时每个客户端都在等待一个加它为好友的信息。待客户端成功得到结果后,就将反馈信息反馈给服务器,最终由服务器来完成加好友操作工作。
    3.5 离线消息模块对方发送一个消息,服务器判断目标是否在线,如果是离线就暂时存储在服务器端的目标文件中,对方上线后会首先读取这个文件以达到得到离线消息的目的。
    4 线程池技术应用针对不同的客户端分配给它一个线程,这个线程调用work( )函数,work( ): 首先从服务器的存储用户信息的文件中读取所有用户信息进链表然后便利链表查找相应信息,如果没有发送错误信息并且关闭链接套接字,断开本次链接。如果查找成功,修改标志值,保持链接存在,创建发送,获取数据的两个线程,与客户端进行通信。上线后首先读取离线文件,如果文件里有东西就将文件内的离线消息打印出来接下来读取发送过来的信息确定是要私人聊天还是多人聊天,根据标识与IP等信息确定聊天状态,用户成功登录或者注册后,使用gtk弹出一个窗口,让用户选择当前需要的聊天模式,私人聊天,多人聊天,以及文件发送总共三个模块,用户选择后则进入各自不同的界面,聊天模块都是相类似的两个界面,文件传输为一个单独的模块。
    关于服务器上文件的相应处理与使用,首先服务器存在一个服务器-客户文件(server)这个文件用来存储所有客户的信息其中包括用户名,密码,登录状态,套接字等,每次客户登录时首先刷新读取这个文件一遍,将其中信息读取到一个公共的链表上,其次改变自己的状态,与描述符,将这些信息改变后即刻写入文件中(应当使用读写锁),以便于下一个用户在登录后刷新读取链表时得到的是一个正确的链表信息。所以服务器在使用链表前都应当刷新读取文件进链表一边(或者在下线时在改变相关文件的时候,同时更改链表的信息,这种方法更有可行性)。
    5 运行展示5.1 开始菜单
    5.2 登陆示例
    5.3 功能菜单
    5.4 私人聊天功能
    5.5 群组聊天
    5.6 文件传输


    5.7 修改信息和管理员
    1 评论 38 下载 2018-11-06 17:03:09 下载需要5点积分
  • 基于HTML5实现的一笔画小游戏

    1 游戏介绍一笔画是图论中一个著名的问题,它起源于柯尼斯堡七桥问题。数学家欧拉在他1736年发表的论文《柯尼斯堡的七桥》中不仅解决了七桥问题,也提出了一笔画定理,顺带解决了一笔画问题。用图论的术语来说,对于一个给定的连通图存在一条恰好包含所有线段并且没有重复的路径,这条路径就是「一笔画」。
    寻找连通图这条路径的过程就是「一笔画」的游戏过程,如下:

    2 游戏的实现「一笔画」的实现不复杂,笔者把实现过程分成两步:

    底图绘制
    交互绘制

    「底图绘制」把连通图以「点线」的形式显示在画布上,是游戏最容易实现的部分;「交互绘制」是用户绘制解题路径的过程,这个过程会主要是处理点与点动态成线的逻辑。
    2.1 底图绘制「一笔画」是多关卡的游戏模式,笔者决定把关卡(连通图)的定制以一个配置接口的形式对外暴露。对外暴露关卡接口需要有一套描述连通图形状的规范,而在笔者面前有两个选项:

    点记法
    线记法

    举个连通图 ——— 五角星为例来说一下这两个选项。

    点记法如下:
    levels: [ // 当前关卡 { name: "五角星", coords: [ {x: Ax, y: Ay}, {x: Bx, y: By}, {x: Cx, y: Cy}, {x: Dx, y: Dy}, {x: Ex, y: Ey}, {x: Ax, y: Ay} ] } ...]
    线记法如下:
    levels: [ // 当前关卡 { name: "五角星", lines: [ {x1: Ax, y1: Ay, x2: Bx, y2: By}, {x1: Bx, y1: By, x2: Cx, y2: Cy}, {x1: Cx, y1: Cy, x2: Dx, y2: Dy}, {x1: Dx, y1: Dy, x2: Ex, y2: Ey}, {x1: Ex, y1: Ey, x2: Ax, y2: Ay} ] }]
    「点记法」记录关卡通关的一个答案,即端点要按一定的顺序存放到数组 coords中,它是有序性的记录。「线记法」通过两点描述连通图的线段,它是无序的记录。「点记法」最大的优势是表现更简洁,但它必须记录一个通关答案,笔者只是关卡的搬运工不是关卡创造者,所以笔者最终选择了「线记法」。
    2.2 交互绘制在画布上绘制路径,从视觉上说是「选择或连接连通图端点」的过程,这个过程需要解决2个问题:

    手指下是否有端点
    选中点到待选中点之间能否成线

    收集连通图端点的坐标,再监听手指滑过的坐标可以知道「手指下是否有点」。以下伪代码是收集端点坐标:
    // 端点坐标信息let coords = []; lines.forEach(({x1, y1, x2, y2}) => { // (x1, y1) 在 coords 数组不存在 if(!isExist(x1, y1)) coords.push([x1, y1]); // (x2, y2) 在 coords 数组不存在 if(!isExist(x2, y2)) coords.push([x2, y2]); });
    以下伪代码是监听手指滑动:
    easel.addEventListener("touchmove", e => { let x0 = e.targetTouches[0].pageX, y0 = e.targetTouches[0].pageY; // 端点半径 ------ 取连通图端点半径的2倍,提升移动端体验 let r = radius * 2; for(let [x, y] of coords){ if(Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0), 2) <= r){ // 手指下有端点,判断能否连线 if(canConnect(x, y)) { // todo } break; } } })
    在未绘制任何线段或端点之前,手指滑过的任意端点都会被视作「一笔画」的起始点;在绘制了线段(或有选中点)后,手指滑过的端点能否与选中点串连成线段需要依据现有条件进行判断。

    上图,点A与点B可连接成线段,而点A与点C不能连接。笔者把「可以与指定端点连接成线段的端点称作有效连接点」。连通图端点的有效连接点从连通图的线段中提取:
    coords.forEach(coord => { // 有效连接点(坐标)挂载在端点坐标下 coord.validCoords = []; lines.forEach(({x1, y1, x2, y2}) => { // 坐标是当前线段的起点 if(coord.x === x1 && coord.y === y1) { coord.validCoords.push([x2, y2]); } // 坐标是当前线段的终点 else if(coord.x === x2 && coord.y === y2) { coord.validCoords.push([x1, y1]); } })})
    但是,有效连接点只能判断两个点是否为底图的线段,这只是一个静态的参考,在实际的「交互绘制」中,会遇到以下情况:

    如上图,AB已串连成线段,当前选中点B的有效连接点是 A 与 C。AB 已经连接成线,如果 BA 也串连成线段,那么线段就重复了,所以此时 BA 不能成线,只有 AC 才能成线。
    对选中点而言,它的有效连接点有两种:

    与选中点「成线的有效连接点」
    与选中点「未成线的有效连接点」

    其中「未成线的有效连接点」才能参与「交互绘制」,并且它是动态的。

    回头本节内容开头提的两个问题「手指下是否有端点」 与 「选中点到待选中点之间能否成线」,其实可合并为一个问题:手指下是否存在「未成线的有效连接点」。只须把监听手指滑动遍历的数组由连通图所有的端点坐标 coords 替换为当前选中点的「未成线的有效连接点」即可。
    3 自动识图笔者在录入关卡配置时,发现一个7条边以上的连通图很容易录错或录重线段。笔者在思考能否开发一个自动识别图形的插件,毕竟「一笔画」的图形是有规则的几何图形。

    上面的关卡「底图」,一眼就可以识出三个颜色:

    白底
    端点颜色
    线段颜色

    并且这三种颜色在「底图」的面积大小顺序是:白底 > 线段颜色 > 端点颜色。底图的「采集色值表算法」很简单,如下伪代码:
    let imageData = ctx.getImageData(); let data = imageData.data; // 色值表let clrs = new Map(); for(let i = 0, len = data.length; i < len; i += 4) { let [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]]; let key = `rgba(${r}, ${g}, ${b}, ${a})`; let value = clrs.get(key) || {r, g, b, a, count: 0}; clrs.has(key) ? ++value.count : clrs.set(rgba, {r, g, b, a, count});}
    对于连通图来说,只要把端点识别出来,连通图的轮廓也就出来了。
    3.1 端点识别理论上,通过采集的「色值表」可以直接把端点的坐标识别出来。笔者设计的「端点识别算法」分以下2步:

    按像素扫描底图直到遇到「端点颜色」的像素,进入第二步
    从底图上清除端点并记录它的坐标,返回继续第一步

    伪代码如下:
    for(let i = 0, len = data.length; i < len; i += 4) { let [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]]; // 当前像素颜色属于端点 if(isBelongVertex(r, g, b, a)) { // 在 data 中清空端点 vertex = clearVertex(i); // 记录端点信息 vertexes.push(vertext); }}
    但是,上面的算法只能跑无损图。笔者在使用了一张手机截屏做测试的时候发现,收集到的「色值表」长度为 5000+ !这直接导致端点和线段的色值无法直接获得。
    经过分析,可以发现「色值表」里绝大多数色值都是相近的,也就是在原来的「采集色值表算法」的基础上添加一个近似颜色过滤即可以找出端点和线段的主色。伪代码实现如下:
    let lineColor = vertexColor = {count: 0}; for(let clr of clrs) { // 与底色相近,跳过 if(isBelongBackground(clr)) continue; // 线段是数量第二多的颜色,端点是第三多的颜色 if(clr.count > lineColor.count) { [vertexColor, lineColor] = [lineColor, clr] }}
    取到端点的主色后,再跑一次「端点识别算法」后居识别出 203 个端点!这是为什么呢?

    上图是放大5倍后的底图局部,蓝色端点的周围和内部充斥着大量噪点(杂色块)。事实上在「端点识别」过程中,由于噪点的存在,把原本的端点被分解成十几个或数十个小端点了,以下是跑过「端点识别算法」后的底图:

    通过上图,可以直观地得出一个结论:识别出来的小端点只在目标(大)端点上集中分布,并且大端点范围内的小端点叠加交错。
    如果把叠加交错的小端点归并成一个大端点,那么这个大端点将十分接近目标端点。小端点的归并伪代码如下:
    for(let i = 0, len = vertexes.length; i < len - 1; ++i) { let vertexA = vertexes[i]; if(vertextA === undefined) continue; // 注意这里 j = 0 而不是 j = i +1 for(let j = 0; j < len; ++j) { let vertexB = vertexes[j]; if(vertextB === undefined) continue; // 点A与点B有叠加,点B合并到点A并删除点B if(isCross(vertexA, vertexB)) { vertexA = merge(vertexA, vertexB); delete vertexA; } }}
    加了小端点归并算法后,「端点识别」的准确度就上去了。经笔者本地测试已经可以 100% 识别有损的连通图了。
    3.2 线段识别笔者分两个步骤完成「线段识别」:

    给定的两个端点连接成线,并采集连线上N个「样本点」;
    遍历样本点像素,如果像素色值不等于线段色值则表示这两个端点之间不存在线段

    如何采集「样式点」是个问题,太密集会影响性能;太疏松精准度不能保证。
    在笔者面前有两个选择:N 是常量;N 是变量。
    假设 N === 5。局部提取「样式点」如下:

    上图,会识别出三条线段:AB, BC 和 AC。而事实上,AC不能成线,它只是因为 AB 和 BC 视觉上共一线的结果。当然把 N 值向上提高可以解决这个问题,不过 N 作为常量的话,这个常量的取量需要靠经验来判断,果然放弃。
    为了避免 AB 与 BC 同处一直线时 AC 被识别成线段,其实很简单 ——— 两个「样本点」的间隔小于或等于端点直径。
    假设 N = S / (2 * R),S 表示两点的距离,R 表示端点半径。局部提取「样式点」如下:

    如上图,成功地绕过了 AC。「线段识别算法」的伪代码实现如下:
    for(let i = 0, len = vertexes.length; i < len - 1; ++i) { let {x: x1, y: y1} = vertexes[i]; for(let j = i + 1; j < len; ++j) { let {x: x2, y: y2} = vertexes[j]; let S = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); let N = S / (R * 2); let stepX = (x1 - x2) / N, stepY = (y1 - y2) / n; while(--N) { // 样本点不是线段色 if(!isBelongLine(x1 + N * stepX, y1 + N * stepY)) break; } // 样本点都合格 ---- 表示两点成线,保存 if(0 === N) lines.push({x1, y1, x2, y2}) }}
    4 性能优化由于「自动识图」需要对图像的的像素点进行扫描,那么性能确实是个需要关注的问题。笔者设计的「自动识图算法」,在识别图像的过程中需要对图像的像素做两次扫描:「采集色值表」 与 「采集端点」。在扫描次数上其实很难降低了,但是对于一张 750 * 1334 的底图来说,「自动识图算法」需要遍历两次长度为 750 * 1334 * 4 = 4,002,000 的数组,压力还是会有的。笔者是从压缩被扫描数组的尺寸来提升性能的。
    被扫描数组的尺寸怎么压缩?
    笔者直接通过缩小画布的尺寸来达到缩小被扫描数组尺寸的。伪代码如下:
    // 要压缩的倍数let resolution = 4; let [width, height] = [img.width / resolution >> 0, img.height / resolution >> 0];ctx.drawImage(img, 0, 0, width, height); let imageData = ctx.getImageData(), data = imageData;
    把源图片缩小 4 倍后,得到的图片像素数组只有原来的 4^2 = 16倍,这在性能上是很大的提升。
    1 评论 34 下载 2018-10-31 20:09:18 下载需要4点积分
  • 基于HTML5实现的贪吃蛇小游戏

    1 游戏介绍贪吃蛇的经典玩法有两种:

    积分闯关
    一吃到底

    第一种是笔者小时候在掌上游戏机最先体验到的(不小心暴露了年龄),具体玩法是蛇吃完一定数量的食物后就通关,通关后速度会加快;第二种是诺基亚在1997年在其自家手机上安装的游戏,它的玩法是吃到没食物为止。笔者要实现的就是第二种玩法。
    2 MVC设计模式基于贪吃蛇的经典,笔者在实现它时也使用一种经典的设计模型:MVC(即:Model - View - Control)。游戏的各种状态与数据结构由 Model 来管理;View 用于显示 Model 的变化;用户与游戏的交互由 Control 完成(Control 提供各种游戏API接口)。
    Model 是游戏的核心也是本文的主要内容;View 会涉及到部分性能问题;Control 负责业务逻辑。 这样设计的好处是: Model完全独立,View 是 Model 的状态机,Model 与 View 都由 Control 来驱动。
    2.1 Model看一张贪吃蛇的经典图片。

    贪吃蛇有四个关键的参与对象:

    蛇(snake)
    食物(food)
    墙(bounds)
    舞台(zone)

    舞台是一个 m * n 的矩阵(二维数组),矩阵的索引边界是舞台的墙,矩阵上的成员用于标记食物和蛇的位置。
    空舞台如下:
    [ [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0],]
    食物(F)和蛇(S)出现在舞台上:
    [ [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,F,0,0,0,0,0,0,0], [0,0,0,S,S,S,S,0,0,0], [0,0,0,0,0,0,S,0,0,0], [0,0,0,0,S,S,S,0,0,0], [0,0,0,0,S,0,0,0,0,0], [0,0,0,0,S,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0],]
    由于操作二维数组不如一维数组方便,所以笔者使用的是一维数组, 如下:
    [ 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 0,0,F,0,0,0,0,0,0,0, 0,0,0,S,S,S,S,0,0,0, 0,0,0,0,0,0,S,0,0,0, 0,0,0,0,S,S,S,0,0,0, 0,0,0,0,S,0,0,0,0,0, 0,0,0,0,S,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,]
    舞台矩阵上蛇与食物只是舞台对二者的映射,它们彼此都有独立的数据结构:

    蛇是一串坐标索引链表
    食物是一个指向舞台坐标的索引值

    2.1.1 蛇的活动蛇的活动有三种,如下:

    移动(move)
    吃食(eat)
    碰撞(collision)

    2.1.1.1 移动蛇在移动时,内部发生了什么变化?

    蛇链表在一次移动过程中做了两件事:向表头插入一个新节点,同时剔除表尾一个旧节点。用一个数组来代表蛇链表,那么蛇的移动就是以下的伪代码:
    function move(next) { snake.pop() & snake.unshift(next); }
    数组作为蛇链表合适吗?
    这是笔者最开始思考的问题,毕竟数组的 unshift & pop 可以无缝表示蛇的移动。不过,方便不代表性能好,unshift 向数组插入元素的时间复杂度是 O(n),pop 剔除数组尾元素的时间复杂度是 O(1)。
    蛇的移动是一个高频率的动作,如果一次动作的算法复杂度为 O(n) 并且蛇的长度比较大,那么游戏的性能会有问题。笔者想实现的贪吃蛇理论上讲是一条长蛇,所以笔者在本文章的回复是 ——— 数组不适合作为蛇链表。
    蛇链表必须是真正的链表结构。
    链表删除或插入一个节点的时间复杂度为O(1),用链表作为蛇链表的数据结构能提高游戏的性能。javascript 没有现成的链表结构,笔者写了一个叫 Chain 的链表类,Chain 提供了 unshfit & pop。以下伪代码是创建一条蛇链表:
    let snake = new Chain();
    2.1.1.2 吃食 & 碰撞「吃食」与「碰撞」区别在于吃食撞上了「食物」,碰撞撞上了「墙」。笔者认为「吃食」与「碰撞」属于蛇一次「移动」的三个可能结果的两个分支。蛇移动的三个可能结果是:「前进」、「吃食」和「碰撞」。
    回头看一下蛇移动的伪代码:
    function move(next) { snake.pop() & snake.unshift(next); }
    代码中的 next 表示蛇头即将进入的格子的索引值,只有当这个格子是 0 时蛇才能「前进」,当这个格子是 S 表示「碰撞」自己,当这个格子是 F 表示吃食。
    好像少了撞墙?
    笔者在设计过程中,并没有把墙设计在舞台的矩阵中,而是通过索引出界的方式来表示撞墙。简单地说就是 next === -1 时表示出界和撞墙。
    以下伪代码表示蛇的整上活动过程:
    // B 表示撞墙let cell = -1 === next ? B : zone[next]; switch(cell) { // 吃食 case F: eat(); break; // 撞到自己 case S: collision(S); break; // 撞墙 case B: collision(B): break; // 前进 default: move; }
    2.1.2 随机投食随机投食是指随机挑选舞台的一个索引值用于映射食物的位置。这似乎很简单,可以直接这样写:
    // 伪代码food = Math.random(zone.length) >> 0;
    如果考虑到投食的前提 ——— 不与蛇身重叠,你会发现上面的随机代码并不能保证投食位置不与蛇身重叠。由于这个算法的安全性带有赌博性质,且把它称作「赌博算法」。为了保证投食的安全性,笔者把算法扩展了一下:
    // 伪代码function feed() { let index = Math.random(zone.length) >> 0; // 当前位置是否被占用 return zone[index] === S ? feed() : index; }food = feed();
    上面的代码虽然在理论上可以保证投食的绝对安全,不过笔者把这个算法称作「不要命的赌徒算法」,因为上面的算法有致命的BUG ——— 超长递归 or 死循环。
    为了解决上面的致命问题,笔者设计了下面的算法来做随机投食:
    // 伪代码function feed() { // 未被占用的空格数 let len = zone.length - snake.length; // 无法投食 if(len === 0) return ; // zone的索引 let index = 0, // 空格计数器 count = 0, // 第 rnd 个空格子是最终要投食的位置 rnd = Math.random() * count >> 0 + 1; // 累计空格数 while(count !== rnd) { // 当前格子为空,count总数增一 zone[index++] === 0 && ++count; } return index - 1; }food = feed();
    这个算法的平均复杂度为 O(n/2)。由于投食是一个低频操作,所以 O(n/2)的复杂度并不会带来任何性能问题。不过,笔者觉得这个算法的复杂度还是有点高了。回头看一下最开始的「赌博算法」,虽然「赌博算法」很不靠谱,但是它有一个优势 ——— 时间复杂度为 O(1)。
    「赌博算法」的靠谱概率 = (zone.length - snake.length) / zone.length。snake.length 是一个动态值,它的变化范围是:0 ~ zone.length。推导出「赌博算法」的平均靠谱概率是:

    「赌博算法」平均靠谱概率 = 50%

    看来「赌博算法」还是可以利用一下的。于是笔者重新设计了一个算法:
    // 伪代码function bet() { let rnd = Math.random() * zone.length >> 0; return zone[rnd] === 0 ? rnd : -1; }function feed() { ...}food = bet(); if(food === -1) food = feed();
    新算法的平均复杂度可以有效地降低到 O(n/4),人生有时候需要点运气。
    2.2 View在 View 可以根据喜好选择一款游戏渲染引擎,笔者在 View 层选择了 PIXI 作为游戏游戏渲染引擎。
    View 的任务主要有两个:

    绘制游戏的界面;
    渲染 Model 里的各种数据结构

    也就是说 View 是使用渲染引擎还原设计稿的过程。本文的目的是介绍「贪吃蛇」的实现思路,如何使用一个渲染引擎不是本文讨论的范畴,笔者想介绍的是:「如何提高渲染的效率」。
    在 View 中显示 Model 的蛇可以简单地如以下伪代码:
    // 清空 View 上的蛇view.snake.clean(); model.snake.forEach( (node) => { // 创建 View 上的蛇节点 let viewNode = createViewNode(node); // 并合一条新蛇 view.snake.push(viewNode); });
    上面代码的时间复杂度是 O(n)。上面介绍过蛇的移动是一个高频的活动,我们要尽量避免高频率地运行 O(n) 的代码。来分析蛇的三种活动:「移动」,「吃食」,「碰撞」。
    首先,Model 发生了「碰撞」,View 应该是直接暂停渲染 Model 里的状态,游戏处在死亡状态,接下来的事由 Control 处理。
    Model 中的蛇(链表)在一次「移动」过程中做了两件事:向表头插入一个新节点,同时剔除表尾一个旧节点;蛇(链表)在一次「吃食」过程中只做一件事:向表头插入一个新节点。

    如果在 View 中对 Model 的蛇链表做差异化检查,View 只增量更新差异部分的话,算法的时间复杂度即可降低至 O(1) ~ O(2) 。以下是优化后的伪代码:
    let snakeA = model.snake, snakeB = view.snake; // 增量更新尾部while(snakeB.length <= snakeA.length) { headA = snakeA.next(); // 头节点匹配 if(headA.data === headB.data) break; // 不匹配 else { // 向snakeB插入头节点 if(snakeA.HEAD === headA.index) { snakeB.unshift(headA.data); } // 向snakeB插入第二个节点 else snakeB.insertAfter(0, headA.data); }}// 增量更新头部 let tailA = snakeA.last(), tailB; while(snakeB.length !== 0) { tailB = snakeB.last(); // 尾节点匹配 if(tailA.data === tailB.data) break; // 不匹配 else snakeB.pop(); }
    2.3 ControlControl 主要做 3 件事:

    游戏与用户的互动
    驱动 Model
    同步 View 与 Model

    「游戏与用户的互动」是指向外提供游戏过程需要使用到的 APIs 与 各类事件。笔者规划的 APIs 如下:



    name
    type
    detail




    init
    method
    初始化游戏


    start
    method
    开始游戏


    restart
    method
    重新开始游戏


    pause
    method
    暂停


    resume
    method
    恢复


    turn
    method
    控制蛇的转向。如:turn(“left”)


    destroy
    method
    销毁游戏


    speed
    property
    蛇的移动速度



    事件如下:



    name
    detail




    countdown
    倒时计


    eat
    吃到食物


    before-eat
    吃到食物前触发


    gameover
    游戏结束



    事件统一挂载在游戏实例下的 event 对象下。
    snake.event.on("countdown", (time) => console.log("剩余时间:", time));
    「驱动 Model 」只做一件事 ——— 将 Model 的蛇的方向更新为用户指定的方向。
    「同步 View 与 Model 」也比较简单,检查 Model 是否有更新,如果有更新通知 View 更新游戏界面。
    1 评论 34 下载 2018-10-31 18:11:26 下载需要7点积分
  • Anti-Rootkit(ARK)内核级系统防护软件KsBinSword的设计与实现

    KsBinSword是一斩断黑手的利刃,它适用于Windows 2000/XP/2003操作系统,用于查探系统中的幕后黑手(木马后门)并作出处理,当然使用它需要用户有一些操作系统的知识。
    KsBinSword内部功能是十分强大的。它有着自己的独创核心态进程管理方案、简洁而不失效率的网络防火墙、强大而稳定的文件过滤驱动、深入磁盘底层甚至穿透还原软件的磁盘微端口驱动。可能您也用过很多类似功能的软件,比如一些进程工具、端口工具,但是现在的系统级后门功能越来越强,一般都可轻而易举地隐藏进程、端口、注册表、文件信息,一般的工具根本无法发现这些“幕后黑手”。KsBinSword使用大量新颖的内核技术,使得这些后门躲无所躲。
    本程序分为应用态与核心态两部分。
    1 应用层程序采用VS2005编写,负责与核心态交互通信,将核心态的信息处理后反馈至用户。应用层使用标准的MFC框架,分为:

    CAboutDlg:About对话框所在类
    CEditEx:重载了的编辑框类
    CHexEdit:重载了的十六进制编辑框类
    CKsBinSwordApp:程序主应用框架类
    CKsBinSwordDlg:程序主界面类
    CMyList:重载了的列表框类
    CMySystem:静态系统函数类
    CPage1:进程管理页的类
    CPage2:监控管理页的类
    CPage3:驱动模块枚举页的类
    CPage4:列举LSP页的类
    CPage5:SSDT页的类
    CPage6:文件管理页的类
    CPage7:磁盘编辑器页的类
    CPage8:防火墙页的类
    CPage9:PE文件查看页的类
    CRuleDlg:防火墙规则对话框类
    CTrayIcon:系统托盘类

    其中CMysystem类比较重要,它封装了对驱动操作、各个系统函数调用等操作,各个类都要用到。程序主界面如下图所示:

    主界面上,又划分为九个子界面。分别为:进程管理、系统监控、LSP管理、SSDT检测部分、文件管理、磁盘编辑、防火墙部分、PE信息查看和系统模块列举。
    1.1 进程管理
    内核级进程、线程检测,顺利查找各种病毒隐藏的进程、线程
    细致的内核级模块检测,深刻扫描系统潜在危险模块
    强大的内核级进程、线程结束,尚无病毒能够抵挡

    1.2 系统监控
    使用当前杀毒软件最新HIPS技术(主动防御),防范病毒于未然
    U盘扫描、脚本木马查杀,确保用户中毒后第一时间清理系统

    1.3 LSP管理
    列举系统socket所依赖的动态链接库,揭发病毒隐藏之处
    1.4 SSDT检测部分
    完全彻底扫描系统所有SSDT(系统服务派发表),病毒无遁形之处
    从系统原生文件还原SSDT,确保系统未修改,阻断病毒自我防护

    1.5 文件管理
    强大的文件过滤驱动技术,彻底检测所有隐藏文件,安全可靠。独创的强制删除文件功能,绕过FSD(文件系统驱动),底层删除文件。
    1.6 磁盘编辑
    方便的十六进制编辑器,完美模拟WinHex功能
    强大的底层磁盘编辑,甚至穿透还原卡,读取写入任何被保护扇区

    1.7 防火墙部分
    自定义安全规则,拦截一切未知数据包,更灵活的保护系统
    实时反馈系统网络数据流,提供网络详细信息

    1.8 PE信息查看
    详细列举PE文件信息,如导入表,区块等,方便了解未知文件结构。
    1.9 系统模块列举
    列举系统所有模块,查杀潜在内核级病毒威胁
    2 内核层驱动层采用DDK 2003编写,共四个NT驱动程序,分别为:

    KsBinSword.sys:负责处理进线程相关检测、查杀
    SIoctl.sys:负责处理硬盘编程
    DrvFltIp.sys:负责处理防火墙相关部分
    Explore.sys:负责处理文件编辑相关部分

    3 应用层各个功能实现原理下面结合代码详细介绍应用层各个功能及其实现。
    3.1 进程管理3.1.1 普通列举进程本功能封装在消息响应函数CPage1::OnBnClickedListProcess()中。采用系统PSAPI.LIB库导出的函数CreateToolhelp32Snapshot()、Process32First()、Process32Next()等函数工作。属于应用态列举进程。取得进程PID后,将使用我们独创的My_OpenProcess()打开进程句柄。
    My_OpenProcess()的功能类似于普通的OpenProcess()作用,但功能更为强大。我们知道一些病毒等软件为了防止自己被杀毒软件结束,会采用一定的防御手段。为了关闭进程,进程句柄是必不可少的。所以很多病毒会在OpenProcess()上采用API HOOK技术保护自己不被结束。而我们的CMySystem::My_OpenProcess()将完美绕过,并且采用了一定的新颖的微软未公开技术。
    My_OpenProcess()实现原理简介
    Windows在子系统进程CSRSS.EXE里维护了一张整个应用层句柄表。通过系统未文档函数ZwQuerySystemInformation()将得到这张表。然后遍历所有句柄,如果是进程句柄则通过ZwDuplicateObject()将句柄复制到本进程,并调用ZwQueryInformationProcess()查询是否为我们需要打开的进程句柄。如果是则返回,否则继续遍历。
    通过以上手段,应用层很少病毒能躲过KsBinSword的扫描。但不排除一些内核级的ROOTKIT(既一些底层核心态病毒)使用篡改系统内核技术逃过杀毒软件的检测。这时我们可以采用驱动列举进程功能。
    3.1.2 驱动列举进程本功能封装在函数CMySystem::ListProcessByDrive()中。
    实现原理简介
    在Windows下,所有资源都是以对象方式进行管理。我们要访问一个对象时,系统就会创建一个对象句柄。句柄和对象之间是通过句柄表来完成的。准确来说,一个句柄是它所对应的对象在句柄表中的索引。PspCidTable是Windows系统上一个特殊的句柄表。它不链接在系统句柄表上,也不属于任何进程。通过它可以访问系统任何对象。
    在Windows XP中,为了节省系统空间,采用了动态扩展的表结构。当句柄表数目少的时候仅采用下层表。数目增大后才采用更多的层。最多有三层句柄表。当我们获得三层句柄表后,我们就可以通过句柄来访问对象了。
    利用PspCidTable来检测隐藏进程的基本原理正是如此,系统内所有进程对象的对象类型是一样的,先取得任一进程对象的对象类型,然后访问所有句柄值,是进程对象则记录下来。下面是实现代码:
    VOID IsValidProcess(){ //判断是否是进程对象,是则记录,不是则放弃 ULONG PspCidTable; ULONG TableCode; ULONG table1,table2; ULONG object,objectheader; ULONG NextFreeTableEntry; ULONG processtype,type; ULONG flags; ULONG i; PspCidTable=GetCidAddr(); processtype=GetProcessType(); if(PspCidTable==0) { return ; } else { //TableCode的最后两位在XP中决定了句柄表的层数 TableCode=*(PULONG)(*(PULONG)PspCidTable); if((TableCode&0x3)==0x0) { table1=TableCode; table2=0x0; } if((TableCode&0x3)==0x1) { TableCode=TableCode&0xfffffffc; table1=*(PULONG)TableCode; table2=*(PULONG)(TableCode+0x4); } //对cid从0x0到0x4e1c进行遍历 for(i=0x0;i<0x4e1c;i++) { if(i<=0x800) { if(MmIsAddressValid((PULONG)(table1+i*2))) { object=*(PULONG)(table1+i*2); if(MmIsAddressValid((PULONG)(table1+i*2+NEXTFREETABLEENTRY))) { NextFreeTableEntry=*(PULONG)(table1+i*2+NEXTFREETABLEENTRY); if(NextFreeTableEntry==0x0)//正常的_HANDLE_TABLE_ENTRY中NextFreeTableEntry为0x0 { object=((object | 0x80000000)& 0xfffffff8);//转换为对象(体)指针 objectheader=(ULONG)OBJECT_TO_OBJECT_HEADER(object);//获取对象(头)指针 if(MmIsAddressValid((PULONG)(objectheader+TYPE))) { type=*(PULONG)(objectheader+TYPE); if(type==processtype) { flags=*(PULONG)((ULONG)object+FLAGS); if((flags&0xc)!=0xc) RecordInfo(object);//flags显示进程没有退出 } } } } } } else { if(table2!=0) { if(MmIsAddressValid((PULONG)(table2+(i-0x800)*2))) { object=*(PULONG)(table2+(i-0x800)*2); if(MmIsAddressValid((PULONG)((table2+(i-0x800)*2)+NEXTFREETABLEENTRY))) { NextFreeTableEntry=*(PULONG)((table2+(i-0x800)*2)+NEXTFREETABLEENTRY); if(NextFreeTableEntry==0x0) { object=((object | 0x80000000)& 0xfffffff8); objectheader=(ULONG)OBJECT_TO_OBJECT_HEADER(object); if(MmIsAddressValid((PULONG)(objectheader+TYPE))) { type=*(PULONG)(objectheader+TYPE); if(type==processtype) { flags=*(PULONG)((ULONG)object+FLAGS); if((flags&0xc)!=0xc) RecordInfo(object); } } } } } } } } }}
    上面解决了检测进程功能。但PspCidTable是未被Windows导出的,属于未文档结构。下面的代码负责查找PspCidTable:
    //通过搜索PsLookupProcessByProcessId函数,获取PspCidTable的地址ULONG GetCidAddr(){ PUCHAR addr; PUCHAR p; UNICODE_STRING pslookup; ULONG cid; RtlInitUnicodeString (&pslookup, L"PsLookupProcessByProcessId"); addr = (PUCHAR) MmGetSystemRoutineAddress(&pslookup);//MmGetSystemRoutineAddress可以通过函数名获得函数地址 for (p=addr;p<addr+PAGE_SIZE;p++) { if((*(PUSHORT)p==0x35ff)&&(*(p+6)==0xe8)) { cid=*(PULONG)(p+2); return cid; break; } } return 0;}
    具体细节的补充说明:

    本程序所使用的结构都是在Windows xp sp2下实现的,所以移植性比较差
    这种检测方式是针对系统句柄 ,所以可以从结果看出不存在系统句柄的System IDIE Process 进程无法列举
    因为进程的退出也是基于句柄的,所以存在进程已经退出而进程对象仍然存在的情况。这种情况可以通过EPROCESS结构中的ProcessExiting等标志位来判断是否退出

    3.1.3 结束进程结束进程我们提供了三种方式:

    普通TerminateProcess()法结束进程,封装在CPage1::OnMenuKillProcessByTer()
    强制清零法结束进程封装在CMySystem::KillProcess()
    驱动调用PspTerminateProcess()结束进程,封装在CMySystem::ForceKillProcess()中

    清零法
    程序调用ZwProtectVirtualMemory()和ZwWriteVirtualMemory() 等函数,强制将目标进程的ring3层的地址空间清除为零。由于连异常处理等Windows特定结构都被清除,故目标进程甚至连异常对话框都不会出现便自动被Windows内存进程管理器消除进程执行体等进程标志,此为目前ring3层最强的结束进程法。
    驱动层结束进程将在下面的驱动部分再行介绍。
    3.1.4 模块列举本功能封装在CPage1::ListProDllByQueryVirMem()中。
    实现方法
    通过ZwQueryVirtualMemory()函数暴力搜索目标进程应用层任何一处位置,并得到响应的地址信息,如果是模块的话列举出来。目前绝大多数工具查找模块也是通过Toolhlp32、psapi,前者会调用RtlDebug***函数向目标注入远线程,后者会用调试api读取目标进程内存,本质上都是对PEB的枚举,通过修改PEB就轻易让这些工具找不到北了。而KsBinSword的核心态方案原原本本地将模块展示,病毒无所逃匿。
    3.1.5 线程列举本功能封装在CMySystem::ListThread(void) 中。
    线程列举完全使用了内核态方案,在驱动中遍历线程结构体ETHREAD,通过ETHREAD中的双向链表完成线程列举。完全杜绝了病毒的一些常规拦截操作。
    3.1.6 线程结束线程结束我们提供了两种方案:

    基于应用层的TerminateThread()的结束进程。封装在CMySystem::KillProcess()中。原理是创建远程线程,注入目标进程中,再调用TerminateThread()的结束进
    基于核心态的PspTerminateThread()结束进程。原理是内核态搜索未导出函数PspTerminateThread()结束进程。

    3.2 监控配置3.2.1 进程监控本功能封装在CPage2::OnBnClickedOk()中。应用层传递消息控制字给内核层,内核层SSDT挂钩了内核函数ZwCreateProcess(),对每个新创建进程进行用户询问。
    3.2.2 注册表监控本功能封装在CPage2::OnBnClickedCancel()中。应用层传递消息控制字给内核层,内核层SSDT挂钩了内核函数ZwSetValueKey(),对每个注册表访问进行用户询问。
    3.2.3 模块监控本功能封装在CPage2:: OnBnClickedButton1 ()中。应用层传递消息控制字给内核层,内核层SSDT挂钩了内核函数ZwLoadDriver(),对每个内核模块加载进行用户询问。
    3.2.4 U盘辅助插件本功能对U盘可疑文件(如AUTORUNS.INF)进行彻底查杀,在源头上封堵了U盘病毒的来源
    3.2.5 脚本木马查杀本功能采用特征码杀毒方式,能全盘扫描脚本木马,速度快,稳定性高,可靠性好。
    3.3 驱动模块检测本功能采用两种不同的方式列举系统驱动:ZwQuerySystemInformation()和ZwQueryDirectoryObject()方式。前者属于常规法,容易遭到病毒拦截,而后者列举了系统的对象目录,极少数病毒会注意到这个地方,所以采用这种方式查找病毒安全又可靠。本来我们打算移进内核态。但由于这两个函数能在应用态调用,为了增强稳定性,就在应用层实现了。
    3.4 LSP枚举LSP枚举我们采用了API: WSCEnumProtocols()、WSCGetProviderPath()
    遍历每个socket协议链得到相应模块路径。病毒有可能更改这个协议链表,所以这里列举出来给用户自行判断。
    3.5 SSDT操作3.5.1 SSDT枚举本部分操作比较多。分别封装两个函数:CPage5::ShowSSDT(void), OnReShowSSDT()中。CPage5::ShowSSDT(void) 调用BOOL CMySystem::EnumSSDT(IN HANDLE hDriver ) 枚举SSDT。
    实现原理
    从系统内核读取出SSDT表,然后使用PE文件操作,从系统内核文件ntoskrnl.exe中分别读取PE头部->数据目录->导出表->导出目录表->函数名数组指针。再通过内核中得到ntoskrnl.exe在内存中的基址,根据上述各数据得到相应的SSDT函数在内存中的正确地址,通过与前述得到的数据相对比,判断是否SSDT被更改。关键函数代码:
    //枚举SSDTBOOL CMySystem::EnumSSDT( IN HANDLE hDriver ){ HINSTANCE hNtDllInst = NULL; ULONG ulNtDllOffset; ULONG ulFuncNameCount = 0; PIMAGE_EXPORT_DIRECTORY pImgExpDir = NULL; PULONG pFuncNameArray = NULL; ULONG i; BOOL bOK = TRUE; do { RealCount = 0; //个数清 if( pList ) //还有存没有释放 { DestroyModList( pList ); //释放它 pList = NULL; } pList = CreateModList( &NTBase ); //创建模块信息链表,顺便得到NT基址 if( pList == NULL ) //创建失败 { bOK = FALSE; break; } if( !( hNtDllInst = LoadLibrary( L"ntdll" ) ) ) { bOK = FALSE; break; } ///////////////////////////////////////////////////////// //分配SSDT保存缓冲表 //得到SSDT个数 SSDT ssdt; if( !GetSSDT( hDriver, &ssdt ) ) { bOK = FALSE; break; } if( TotalSSDTCount == -1 ) //得到SSDT个数失败 { bOK = FALSE; break; } if( pSSDTST ) //pSSDTST已有值,先释放它 { free( pSSDTST ); pSSDTST = NULL; } pSSDTST = (pSSDTSaveTable)malloc( TotalSSDTCount * sizeof( SSDTSaveTable ) ); if( pSSDTST == NULL ) { bOK = FALSE; break; } for( i = 0; i < TotalSSDTCount; i ++ ) //初始化它 { ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulServiceNumber = -1; ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulCurrentFunctionAddress = 0L; ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulOriginalFunctionAddress = 0L; memset( ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ServiceFunctionName, \ 0, \ sizeof(((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ServiceFunctionName)); memset( ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ModuleName, \ 0, \ sizeof(((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ModuleName)); } ///////////////////////////////////////////////////////// //枚举 ulNtDllOffset = (ULONG)hNtDllInst; //PE头部 ulNtDllOffset += ((PIMAGE_DOS_HEADER)hNtDllInst)->e_lfanew + sizeof( DWORD ); //数据目录 ulNtDllOffset += sizeof( IMAGE_FILE_HEADER ) + sizeof( IMAGE_OPTIONAL_HEADER ) - IMAGE_NUMBEROF_DIRECTORY_ENTRIES * sizeof( IMAGE_DATA_DIRECTORY ); //导出表 ulNtDllOffset = (DWORD)hNtDllInst + ((PIMAGE_DATA_DIRECTORY)ulNtDllOffset)->VirtualAddress; //导出目录表 pImgExpDir = (PIMAGE_EXPORT_DIRECTORY)ulNtDllOffset; //得到函数个数 ulFuncNameCount = pImgExpDir->NumberOfNames; //函数名数组指针 pFuncNameArray = (PULONG)( (ULONG)hNtDllInst + pImgExpDir->AddressOfNames ); ///////////////////// //循环找函数名 for( i = 0; i < ulFuncNameCount; i ++ ) { //函数名 PCSTR pszName = (PCSTR)( pFuncNameArray[i] + (ULONG)hNtDllInst ); if( pszName[0] == 'N' && pszName[1] == 't' ) //Nt 开头的函数 { //查找表 LPWORD pOrdNameArray = (LPWORD)( (ULONG)hNtDllInst + pImgExpDir->AddressOfNameOrdinals ); //函数地址 LPDWORD pFuncArray = (LPDWORD)( (ULONG)hNtDllInst + pImgExpDir->AddressOfFunctions ); //函数代码 LPCVOID pFuncCode = (LPCVOID)( (ULONG)hNtDllInst + pFuncArray[pOrdNameArray[i]] ); //获取服务号 SSDTEntry EntryCode; memcpy( &EntryCode, pFuncCode, sizeof( SSDTEntry ) ); if( EntryCode.byMov == 0xB8 ) // MOV EAX, XXXX { ULONG ulAddr = 0; if( !GetHook( hDriver, EntryCode.ulIndex, &ulAddr ) ) { bOK = FALSE; break; } //////////////////////// //通过地址得到模块名 char ModNameBuf[MAX_PATH+1]; memset( ModNameBuf, 0, sizeof( ModNameBuf ) ); if( GetModuleNameByAddr( ulAddr, pList, ModNameBuf, sizeof( ModNameBuf )-1 ) ) { memcpy( \ ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ModuleName, \ ModNameBuf, \ sizeof( ModNameBuf ) \ ); } //////////////////////////////////////////////////// //保存SSDT信息到缓冲表中 ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ulServiceNumber = \ EntryCode.ulIndex; //服务号 ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ulCurrentFunctionAddress = \ ulAddr; //当前函数地址 memcpy( \ ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ServiceFunctionName, \ pszName, \ sizeof( ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ServiceFunctionName ) ); RealCount ++; } } } } while( FALSE ); ::FreeLibrary( hNtDllInst ); if( bOK ) //成功 { //获取剩下的服务号 for( i = RealCount; i < TotalSSDTCount; i ++ ) { if( !GetHook( hDriver, i, &((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulCurrentFunctionAddress ) ) { bOK = FALSE; break; } //////////////////////// //通过地址得到模块名 char ModNameBuf[MAX_PATH+1]; memset( ModNameBuf, 0, sizeof( ModNameBuf ) ); if( GetModuleNameByAddr( \ ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulCurrentFunctionAddress, \ pList, ModNameBuf, sizeof( ModNameBuf )-1 ) ) { memcpy( \ ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ModuleName, \ ModNameBuf, \ sizeof( ModNameBuf ) \ ); } } //按服务号进行排序 SSDTSTOrderByServiceNum( pSSDTST ); //获取原始函数地址 GetOldSSDTAddress(); } if( pList ) { DestroyModList( pList ); //释放模块链表 pList = NULL; } return bOK;}
    3.5.2 SSDT恢复该功能封装在CMySystem::SetSSDT()中。这个函数只是一个用户层通信函数,真正恢复函数在内核态中。内核态将用户层传来的SSDT函数正确地址,写入到内存中。其中为了防止被中断打乱,关闭了一次中断,操作完成后恢复中断。
    3.6 文件管理器本文件管理采用了独创的文件过滤驱动技术,未使用windows API,能查探、删除目前已知的绝大多数病毒、Rootkit等。
    文件管理应用层负责与核心态通信。下面分析应用层框架:
    3.6.1 列目录、文件CPage6::OnItemexpanding(NMHDR *pNMHDR, LRESULT *pResult) 函数负责响应文件管理器中树型控件的节点展开消息。然后将点击的路径传送至内核,由文件过滤驱动构造IRP包发送至相应卷驱动。根据卷驱动返回的数据包传递给应用层。应用层通过CPage6::IsMediaValid()、CPage6::IsPathValid()、CPage6::AddDirectoryNodes()判断路径下是否有文件,是何种文件,并将信息添加至树型控件节点。相应的消息控制字为:IOCTL_MT_GETDIRINF、IOCTL_MT_GETDIRNUMINF。其中最重要的函数代码如下:
    UINT CPage6::AddDirectoryNodes(HTREEITEM hItem, CString &strPathName){ WCHAR wBuf[60]; DWORD bytesReturned=0; ULONG num=0; PDIRECTORY_INFO temp={0}; DIRECTORY_INFO_EX DIRECTORY_INFO_b; CString str,str1,strFileSpec = strPathName; if (strFileSpec.Right (1) != "\\") strFileSpec += "\\"; char a[MAX_PATH]; str1=strFileSpec; CMySystem::WCHARToChar(a,strFileSpec.GetBuffer(strFileSpec.GetLength())); strFileSpec += "*.*"; DeviceIoControl(hDevice,(DWORD)IOCTL_MT_GETDIRNUMINF,a,sizeof(a),&num,sizeof(ULONG),&bytesReturned,NULL); if(num==0) { AfxMessageBox(L"驱动未加载,列举出错!"); return 0; } temp=(PDIRECTORY_INFO)calloc(num,sizeof(DIRECTORY_INFO)); if(temp==NULL) { return 0; } DeviceIoControl(hDevice,(DWORD)IOCTL_MT_GETDIRINF,a,sizeof(a),temp,num*sizeof(DIRECTORY_INFO),&bytesReturned,NULL); CWaitCursor wait; WCHAR wTemp[MAX_PATH]={'\0'}; m_FileList.DeleteAllItems(); index=0; SetPath(str1,hDevice); for(ULONG i=0;i<num;i++) { TRACE("AddDirectoryNode:%d\n",i); CMySystem::CharToWCHAR(wTemp,temp[i].FileName); str.Format(L"%s",wTemp); str=str1+str; CString strFileName = (LPCTSTR) &temp[i].FileName; if(PathIsDirectory(str)) { if(strcmp(temp[i].FileName,".")) { if(strcmp(temp[i].FileName,"..")) { CMySystem::CharToWCHAR(wTemp,temp[i].FileName); HTREEITEM hChild = m_FileTree.InsertItem ((LPCTSTR) wTemp,//&fd.cFileName, ILI_CLSDFLD , ILI_OPENFLD , hItem , TVI_SORT); CString strNewPathName = strPathName; if (strNewPathName.Right (1) != "\\") {strNewPathName += "\\";} CMySystem::CharToWCHAR(wBuf,temp[i].FileName); strNewPathName += wBuf;//fd.cFileName; SetButtonState (hChild, strNewPathName); } } } else { DIRECTORY_INFO_b.DirectoryInfo=temp[i]; DIRECTORY_INFO_b.path=str1; AddToListView(&DIRECTORY_INFO_b); } } delete temp; return num;}
    3.6.2 文件删除文件删除分为普通删除与驱动删除。

    普通删除:调用Win32 API DeleteFile()删除文件。对付普通病毒这种方式有效。但某些病毒会采用文件占用式保护本体不被删除,甚至采用驱动形式保护,此时普通删除无效
    驱动删除:传递控制字IOCTL_MT_KILLFILE至驱动Explorer.sys,驱动删除病毒,此方式有一定危险性,但对病毒有奇效。极少数病毒能逃离此法删除

    3.7 磁盘编辑器3.7.1 十六进制编辑器界面处理我们的磁盘编辑器界面采用了重载后的CEdit类。新类CHexEdit响应下列消息:
    WM_CHARWM_KILLFOCUSWM_PAINTWM_SETFOCUSWM_SIZEWM_VSCROLLWM_HSCROLLWM_GETDLGCODEWM_ERASEBKGNDWM_LBUTTONDOWNWM_LBUTTONDBLCLKWM_MOUSEMOVEWM_LBUTTONUPWM_KEYDOWN
    并在WM_CHAR的响应函数CHexEdit::OnPaint()中绘制了三大部分:地址栏、十六进制栏、字符显示栏。其中的细节比较多,这里就不全部描述了。
    3.7.2 硬盘编辑功能硬盘编辑有两种选择, 一种是普通的通过应用层API CreateFile()打开物理对象\\.\PhysicalDrive0,实现函数为CMySystem::ReadSector(__int64Sect, BYTE *OutBuf).另一种在核心态自己构造IRP发送至磁盘驱动ATAPI.SYS(这是Windows处理磁盘请求的最后一站,再往下就是硬盘IO指令了),直接绕过文件系统FSD.在我们的测试中,意外的发现这种极为底层的技术甚至连知名的影子系统, RVS,冰点……等还原软件被穿透。
    3.8 网络防火墙网络防火墙有两种用途,一种是建立规则,阻止或通过指定网络包。防火墙的驱动实现将在后面讲解。
    3.8.1 建立防火墙规则通过CPage8:: OnButtonAdd()打开规则对话框,对规则进行相关配置后,调用CPage8::OnButtoninStall再调用CPage8::AddFilterToFw发送规则至驱动。
    3.8.2 监视网络数据包在驱动中我们自己实现了一个类似DbgPrint的函数。应用层中申请了一个定时器,反复读取内核传来的网络数据包。并分析数据包中的源IP、目标IP和数据包协议类型。相关函数为CPage8::OnBnClickedMonitor()
    3.9 PE文件分析文件分析部分没有什么内核技术,纯粹是个辅助性功能。相应部分看函数便可知。
    CPage9::LoadFile(); CPage9::IsPEFile() CPage9::PrintFileHeader(); CPage9::PrintOptionAlHeader(); CPage9::PrintSectionInfo(); CPage9::printET(); CPage9::printIAT(); CPage9::UnLoadFile()
    至此,应用层分析完毕。
    4 内核层各个功能实现原理下面结合代码详细介绍内核层各个功能及其实现。
    4.1 进程管理见应用层分析部分
    4.2 自动防御见应用层分析部分
    4.3 驱动模块列举见应用层分析部分
    4.4 列举LSP见应用层分析部分
    4.5 SSDT见应用层分析部分
    4.6 文件管理文件管理器通过自己构造IRP数据包下发至卷驱动。相应的函数位于Explorer.sys中的GetDirectory(char *lpDirName, PULONG dwRetSize)里。函数调用ZwOpenFile打开设备链接\\DosDevices\\C:\\卷驱动,调用IoAllocateIrp分配一个IRP,然后填充IRP:
    KeInitializeEvent(&event,SynchronizationEvent,FALSE); lpInformation = ExAllocatePool(PagedPool,655350); lpSystemBuffer = ExAllocatePool(PagedPool,655350); RtlZeroMemory(lpSystemBuffer,655350); RtlZeroMemory(lpInformation,655350); lpirp->UserEvent = &event; lpirp->UserBuffer = lpInformation; lpirp->AssociatedIrp.SystemBuffer = lpInformation; lpirp->MdlAddress = NULL; lpirp->Flags = 0; lpirp->UserIosb = &ios; lpirp->Tail.Overlay.OriginalFileObject = lpFileObject; lpirp->Tail.Overlay.Thread = PsGetCurrentThread(); lpirp->RequestorMode = KernelMode; lpsp = IoGetNextIrpStackLocation(lpirp); lpsp->MajorFunction = IRP_MJ_DIRECTORY_CONTROL; lpsp->MinorFunction = IRP_MN_QUERY_DIRECTORY; lpsp->FileObject = lpFileObject; lpsp->DeviceObject = lpDeviceObject; lpsp->Flags = SL_RESTART_SCAN; lpsp->Control = 0; lpsp->Parameters.QueryDirectory.FileIndex = 0; lpsp->Parameters.QueryDirectory.FileInformationClass = FileDirectoryInformation; lpsp->Parameters.QueryDirectory.FileName = NULL; lpsp->Parameters.QueryDirectory.Length = 655350;
    填这样当IRP返回时便携带了我们所需要的文件信息。
    强制删除文件同样采用构造IRP下发方式。不同的是下发的IRP参数不同:
    FileInformation.DeleteFile = TRUE; Irp->AssociatedIrp.SystemBuffer = &FileInformation; Irp->UserEvent = &event; Irp->UserIosb = &ioStatus; Irp->Tail.Overlay.OriginalFileObject = fileObject; Irp->Tail.Overlay.Thread = (PETHREAD)KeGetCurrentThread(); Irp->RequestorMode = KernelMode; irpSp = IoGetNextIrpStackLocation(Irp); irpSp->MajorFunction = IRP_MJ_SET_INFORMATION; irpSp->DeviceObject = DeviceObject; irpSp->FileObject = fileObject; irpSp->Parameters.SetFile.Length = sizeof(FILE_DISPOSITION_INFORMATION); irpSp->Parameters.SetFile.FileInformationClass = FileDispositionInformation; irpSp->Parameters.SetFile.FileObject = fileObject;
    4.7 硬盘编辑硬盘编辑功能的实现位于驱动SIoctl.sys中。为了实现底层的硬盘编辑,我们选择PhysicalDrive0设备对象。根据《深入解析windows》(第5版),这个对象其实是硬盘驱动atapi.sys的一个驱动。在这层驱动中,文件系统所需要的文件路径等已经不存在了,我们面对是直接是硬盘。下面是填充IRP和下发过程:
    irpSp = IoGetNextIrpStackLocation(irp); irp->UserEvent = &event; irp->IoStatus.Status = 0; irp->IoStatus.Information = 0; irp->UserBuffer = NULL; irp->Flags = (irp->Type << 16) | 5; irp->Tail.Overlay.Thread = PsGetCurrentThread(); irp->Cancel = FALSE; IoSetCancelRoutine(irp,NULL); irp->RequestorMode =KernelMode; irp->AssociatedIrp.SystemBuffer = NULL; irpSp->DeviceObject = DeviceObject; irpSp->MajorFunction = (UCHAR)ReadOrWrite; irpSp->Parameters.DeviceIoControl.InputBufferLength = 0;
    4.8 网络防火墙在WINDOWS 2000 DDK中,微软包含了称为Filter-HookDriver的新型网络驱动。可以使用它来过滤所有进出接口的数据。实际上,Filter-Hook Driver并不是网络驱动,它是一种内核模式驱动(Kernel Mode Driver)。大致上是这样的:在Filter-Hook Driver中我们提供回调函数(callback),然后使用IP Filter Driver注册回调函数。这样当数据包发送和接收时,IP Filter Driver会调用回调函数。那么我们到底该如何实现这些步骤呢?总结如下:

    建立Filter-HookDriver.我们必须建立内核模式驱动,你可以选择名称,DOS名称和其它驱动特性,这些不是必须的,但我建议使用描述名称
    如果我们要安装过滤函数,首先我们必须得到指向IP Filter Driver的指针,这是第二步
    我们已经取得了指针,现在我们可以通过发送特殊的IRP来安装过滤函数,该”消息”传递的数据包含了过滤函数的指针
    过滤数据包
    当我们想结束过滤,我们必须撤销过滤函数。这通过传递NULL指针作为过滤函数指针来实现

    下面是我们的防火墙构架:

    一个创建设备的驱动程序入口,为通讯创建符号连接和处理IRPs(分派,加载,卸载,创建…)的标准例程
    在标准例程里管理IRPs。在我们的代码中,我们实现了四个IOCTL代码:START_IP_HOOK(注册过滤函数),STOP_IP_HOOK(注销过滤函数),ADD_FILTER(安装新的过滤规则),CLEAR_FILTER(清除所有规则)
    对于我们的驱动,我们实现多个用于过滤的函数

    我们在IP Filter Driver中执行一个函数来注册过滤函数,步骤如下:

    首先,得到IP Filter Driver的指针,这要求驱动已经安装并执行。为了保证IP Filter Driver已经安装并执行,在前述用户程序中,在加载本驱动前加载并启动IP Filter Driver
    第二步,建立用IOCTL_PF_SET_EXTENSION_POINTER作为控制代码的IRP。传递PF_SET_EXTENSION_HOOK_INFO 参数,该参数结构中包含了指向过滤函数的指针。如果要卸载该函数,必须在同样的步骤里传递NULL作为过滤函数指针
    向设备驱动发送:创建IRP, 这里有一个大的问题,只有一个过滤函数可以安装,因此如果另外的应用程序已经安装了一个过滤函数,你就不能再安装了

    设置过滤函数的代码如下:
    NTSTATUS SetFilterFunction(PacketFilterExtensionPtr filterFunction){ NTSTATUS status = STATUS_SUCCESS, waitStatus=STATUS_SUCCESS; UNICODE_STRING filterName; PDEVICE_OBJECT ipDeviceObject=NULL; PFILE_OBJECT ipFileObject=NULL; PF_SET_EXTENSION_HOOK_INFO filterData; KEVENT event; IO_STATUS_BLOCK ioStatus; PIRP irp; dprintf("Getting pointer to IpFilterDriver\n"); RtlInitUnicodeString(&filterName, DD_IPFLTRDRVR_DEVICE_NAME); status = IoGetDeviceObjectPointer(&filterName,STANDARD_RIGHTS_ALL, &ipFileObject, &ipDeviceObject); dprintf("OK:%x",status); if(NT_SUCCESS(status)) { filterData.ExtensionPointer = filterFunction; KeInitializeEvent(&event, NotificationEvent, FALSE); irp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER, ipDeviceObject, (PVOID) &filterData, sizeof(PF_SET_EXTENSION_HOOK_INFO), NULL, 0, FALSE, &event, &ioStatus); if(irp != NULL) { status = IoCallDriver(ipDeviceObject, irp); if (status == STATUS_PENDING) { waitStatus = KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL); if (waitStatus != STATUS_SUCCESS ) dprintf("Error waiting for IpFilterDriver response."); } status = ioStatus.Status; if(!NT_SUCCESS(status)) dprintf("Error, IO error with ipFilterDriver\n"); } else { status = STATUS_INSUFFICIENT_RESOURCES; dprintf("Error building IpFilterDriver IRP\n"); } if(ipFileObject != NULL) { dprintf("ObDereferenceObject"); ObDereferenceObject(ipFileObject); } ipFileObject = NULL; ipDeviceObject = NULL; } else dprintf("Error while getting the pointer\n"); return status;}
    这时已经完成了建立过滤函数的工作,当取得设备驱动的指针后必须释放文件对象。我们使用事件来通知IpFilterDriver 已经完成了IRP处理。
    下面是过滤函数代码:
    PF_FORWARD_ACTION cbFilterFunction(IN unsigned char *PacketHeader, IN unsigned char *Packet, IN unsigned int PacketLength, IN unsigned int RecvInterfaceIndex, IN unsigned int SendInterfaceIndex, IN unsigned long RecvLinkNextHop, IN unsigned long SendLinkNextHop){ IPPacket *ipp; TCPHeader *tcph; UDPHeader *udph; int countRule = 0; struct filterList *aux = first; WCHAR wcMessage[MAXSTR]; ipp = (IPPacket*)PacketHeader; MyPrint(SEPARATOR); dprintf("PacketInfo %x, %d\r\n", PacketLength, RecvInterfaceIndex); dprintf("Source: %x Destination: %x Protocol: %d\r\n", ipp->ipSource, ipp ->ipDestination, ipp->ipProtocol); swprintf(wcMessage, L "PacketLength: %x, RecvInterfaceIndex:%d\r\n", PacketLength, RecvInterfaceIndex); MyPrint(wcMessage); swprintf(wcMessage, L "NetInfomation:@%x@@%x@@@%d\r\n", ipp->ipSource, ipp ->ipDestination, ipp->ipProtocol); MyPrint(wcMessage); if (ipp->ipProtocol == 6) { tcph = (TCPHeader*)Packet; dprintf("FLAGS: %x\r\n", tcph->flags); swprintf(wcMessage, L "FLAGS: %x\r\n", tcph->flags); if (!(tcph->flags &0x02)) return PF_FORWARD; } while (aux != NULL) { dprintf("Comparing with Rule %d", countRule); if (aux->ipf.protocol == 0 || ipp->ipProtocol == aux->ipf.protocol) { if (aux->ipf.sourceIp != 0 && (ipp->ipSource &aux->ipf.sourceMask) != aux->ipf.sourceIp) { aux = aux->next; countRule++; continue; }if (aux->ipf.destinationIp != 0 && (ipp->ipDestination &aux->ipf.destinationMask) != aux->ipf.destinationIp) { aux = aux->next; countRule++; continue; } //tcp, protocol = 6 if (ipp->ipProtocol == 6) { if (aux->ipf.sourcePort == 0 || tcph->sourcePort == aux->ipf.sourcePort) { if (aux->ipf.destinationPort == 0 || tcph->destinationPort == aux->ipf.destinationPort) //puerto tcp destino { if (aux->ipf.drop) return PF_DROP; else return PF_FORWARD; } } } //udp, protocol = 17 else if (ipp->ipProtocol == 17) { udph = (UDPHeader*)Packet; if (aux->ipf.sourcePort == 0 || udph->sourcePort == aux->ipf.sourcePort) { if (aux->ipf.destinationPort == 0 || udph->destinationPort == aux->ipf.destinationPort) { if (aux->ipf.drop) return PF_DROP; else return PF_FORWARD; } } } else { if (aux->ipf.drop) return PF_DROP; else return PF_FORWARD; } } countRule++; aux = aux->next; } return PF_FORWARD;}
    4.9 PE文件分析见应用层分析部分
    至此,整个KsBinSword分析完毕。
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++资源转载自:https://download.csdn.net/download/anzyky/3329962+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    1 评论 10 下载 2018-10-05 22:02:18 下载需要13点积分
显示 45 到 58 ,共 13 条
eject