如何动态的添加这段代码 !而且要对NAME里面的进行动态淘宝css初始化代码

前言  & 软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类。不管是数组还是集合类,它们都有各自的优缺点。如何使用好集合是我们在开发过程中必须掌握的技巧。不要小看这些技巧,一旦在开发中使用了错误的集合或针对集合的方法,应用程序将会背离你的预想而运行。  本文已更新至/aehyok/p/3624579.html&。本文主要学习记录以下内容:  建议16、元素数量可变的情况下不应使用数组  建议17、在多数情况下使用foreach进行循环遍历  建议18、foreach不能代替for  建议19、使用更有效的对象和集合初始化建议16、元素数量可变的情况下不应使用数组  在C#中,数组一旦被创建,长度就不能改变。如果我们需要一个动态且可变长度的集合,就应该使用ArrayList或List&T&来创建。而数组本身,尤其是一维数组,在遇到要求高效率的算法时,则会专门被优化以提升其效率。一维数组也成为向量,其性能是最佳的,在IL中使用了专门的指令来处理它们。  从内存使用的角度来讲,数组具有以下特点:  1、数组在创建时被分配了一段固定长度的内存。  2、如果数组元素是值类型,则每个元素的长度等于相应的值类型的长度  3、如果数组的元素是引用类型,则每个元素的长度为该引用类型的IntPtr.Size。  4、数组的存储结构一旦被分配,就不能再变化。  而ArryaList是这样的:  1、ArrayList是链表结构,可以动态增减内存空间。  2、如果ArrayList存储的是值类型,则会为每个元素增加12字节的空间,其中4字节用于对象引用,8字节是元素装箱时引入的对象头。  而List&T&是ArrayList的泛型实现,它省去了拆箱和装箱带来的开销。如果一定要动态改变数组的长度,一种方法是将数组转换为ArrayList或List&T&,如下面的代码所示:
///定义一个一维数组
int[] iArr = { 0,1,3,4,6,7,9};
///将数组转换为ArrayList
ArrayList arrayListInt = ArrayList.Adapter(iArr);
arrayListInt.Add(11);
///将数组转换为List&T&
List&int& listInt = iArr.ToList&int&();
listInt.Add(11);  还有一种方法是用数组的复制功能。数组继承自System.Array,抽象类System.Array提供了一些有用的实现方法,其中就包含了Copy方法,它负责将一个数组的内容复制到另外一个数组中。无论是哪种方法,改变数组长度就相当于重新创建了一个数组对象。  为了让数组看上去本身就具有动态改变长度的功能,还可以创建一个名为ReSize的扩展方法。
public static class ClassForExtensions
public static Array ReSize(this Array array,int newSize)
Type t = array.GetType().GetElementType();
Array newArray = Array.CreateInstance(t, newSize);
Array.Copy(array, 0, newArray, 0, Math.Min(array.Length, newSize));
return newA
}调用方式如下:
static void Main(string[] args)
int[] iArr = { 0,1,3,4,6,7,9};
iArr = (int[])ClassForExtensions.ReSize(iArr, 20);
Console.ReadLine();
}下面我们来对比一下性能,先来看代码:
class Program
static void Main(string[] args)
ResizeArray();
ResizeList();
Console.ReadLine();
public static void ResizeArray()
int[] iArr = {0,1,3,4,6,8 };
Stopwatch watch = new Stopwatch();
watch.Start();///用于测量时间间隔
iArr = (int[])iArr.ReSize(10);
watch.Stop();///
Console.WriteLine("ResizeArray:{0}", watch.Elapsed);
public static void ResizeList()
List&int& iArr = new List&int&(new int[] { 0, 1, 3, 4, 6, 8, 9 });
Stopwatch watch = new Stopwatch();
watch.Start();
iArr.Add(0);
iArr.Add(0);
iArr.Add(0);
watch.Stop();
Console.WriteLine("ResizeList:{0}", watch.Elapsed);
}Main函数中主要是调用,自己定义的两个方法,第一个是重新设置数组的长度,第二个是设置List&T&的长度,通过运行时间进行测量:严格意义上讲,List&T&不存在改变长度的说法,此处主要是来进行对比一下,对List&T&设置长度,并且进行赋值,即便是这样,在时间效率上ResizeList比ResizeArray要高很多很多。建议17、在多数情况下使用foreach进行循环遍历&这里关于如何针对集合才能使用foreach进行遍历我刚刚写了一篇有关IEnumerable和IEnumerator两个接口的文章,有兴趣的话可以看一下。/aehyok/p/3641193.html感觉使用foreach进行循环遍历,总共有三个好处吧:1、提供了比较简单、简洁的语法。2、自动将代码置入try-finally块3、若类型实现IDispose接口,foreach会在循环结束后自动调用Dispose方法建议18、foreach不能代替forforeach存在一个问题是:它不支持循环时对集合进行增删操作。我们来看一下简单的例子:
List&int& list = new List&int&() { 1, 2, 3, 4, 5 };
foreach (int item in list)
list.Remove(item);
Console.WriteLine(item.ToString());
Console.ReadLine();一起看一下执行结果:那么下面我们来使用for进行尝试:
List&int& list = new List&int&() { 1, 2, 3, 4, 5 };
for (int i = 0; i & list.Count(); i++)
list.Remove(list[i]);
Console.ReadLine();  进行删除肯定是没问题的。但是要仔细看一下,比如它第一次删除索引0的时候,也就是删除了1,那么它会立即重新调整索引,然后第二次删除的时候,删除的不是2,而是3这个项。那么最终运行完发现还剩余两项 foreach循环使用了迭代器进行集合的遍历,它在FCL提供的迭代器内部维护了一个对集合版本的控制。那么什么是集合版本呢?简单的说,其实它就是一个整型的变量,任何对集合的增删操作都会使版本号加1。foreach循环会调用MoveNext方法来遍历元素,在MoveNext方法内部会进行版本号的检测,一旦检测到版本号有变动,就会抛出InvalidOperationException异常。& 如果使用for循环就不会带来这样的问题。for直接使用所引器,它不对集合版本号进行判断,所以不存在因为集合的变动而带来的异常(当然,超出索引长度这种情况除外)。  索引,因为版本检测的缘故,foreach循环并不能带起for循环。 建议19、使用更有效的对象和集合初始化&  对象初始化设定项支持可以直接在大括号中对自动实现的属性进行赋值。
class Person
public string Name { }
public int Age { }
class Program
static void Main(string[] args)
Person person = new Person() { Name = "aehyok", Age = 25 };
Console.ReadLine();
}以往只能依靠构造方法传值进去,或者在对象构造完毕后对属性进行赋值。现在这些步骤简化了,初始化设定项实际相当于编译器在对象生成后对属性进行了赋值。
class Person
public string Name { }
public int Age { }
class Program
static void Main(string[] args)
Person person = new Person() { Name = "Kris", Age = 22 };
List&Person& personList = new List&Person&()
new Person() { Name = "aehyok", Age = 25 },
Console.ReadLine();
}使用集合的初始化设定项,编译器会在集合对象创建完毕后对集合调用Add方法。上面这段代码展示了如何在初始化语句中创建一个新对象或一个现有对象,以及一个null值。&不过,初始化设定项绝不仅仅是为了对象和集合初始化的方便,它更重要的作用是为LINQ查询中的匿名类型进行属性的初始化。由于LINQ查询返回的集合中匿名类型的属性都是只读的,如果需要为匿名类型属性赋值,或者增加属性,只能通过初始化设定项来进行。初始化设定项还能为属性使用表达式。来看一段代码:List&Person& lst = new List&Person&() {
new Person(){ Age = 10,Name="Tommy"},
new Person(){ Age = 20,Name="Sammy"} };var entity = from p in lst
select new { p.Name, AgeScope = p.Age & 10 ? "Old" : "Young" };foreach (var item in entity){
Response.Write(string.Format("name is {0},{1}", item.Name, item.AgeScope));}AgeScope 属性是经过计算得出的,有了如此方便的初始化方式,使得代码更加优雅灵活。&
编写高质量代码改善C#程序的157个建议[动态数组、循环..._第七城市编写高质量代码改善C#程序的157个建议[动态数组、循环遍历、对象集合初始化]... 软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类。不管...C# - 第87页 - 站分类 - 博客园 编写高质量代码改善C#程序的157个建议[动态数组、循环遍历、对象集合初始化] 前言 软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类。不管是...编写高质量代码改善C#程序的157个建议[泛型集合、选择集合、集合...编写高质量代码改善C#程序的157个建议[泛型集合...会用到集合,C#中的集合表现为数组和若干集合类...主要包括从终结器队列中出来的对象 ...编写高质量代码改善C#程序的157个建议(含源代码) - 下载频道 - ...编写高质量代码改善C#程序的157个建议(含源代码)共计十二章节,络整理,仅供学习交流。编写高质量代码改善C#程序的157个建议/陆敏技【图片 价格 ..._京东 《编写高质量代码:改善C#程序的157个建议》是C#...建议17:多数情况下使用foreach进行循环遍历建议18:...建议19:使用更有效的对象和集合初始化建议20:使用...编写高质量代码改善C#程序的157个建议[泛型集合、选择集合、集合...编写高质量代码改善C#程序的157个建议[泛型集合、选择集合、集合的安全]...泛型集合没有这样的属性来进行加锁,必须要自己创建一个锁定对象来完成同步...抛弃NVelocity,来玩玩Razor__程序员俱乐部编写高质量代码改善C#程序的157个建议pdf _IT168文库编写高质量代码改善C#程序的157个建议pdf 文档资料推荐阅读编程精粹:编写高质量C语言代码.pdf标签: C语言 C 下载量:179次 阅读量:1334次 分享到: ...编写高质量代码改善C#程序的157个建议[协变和逆变]编写高质量代码改善C#程序的157个建议[协变和逆变] 分类: IC
05:36 3人阅读 评论(0) 收藏 举报 今天有幸被召回母校给即将毕业的学弟学妹们讲我...编写高质量代码改善C#程序的157个建议【图片 价格 品牌 报价】-京东图书 & 计算机与互联 & 编程语言与程序设计 & 编写高质量代码改善C#程序的157个建议 编写高质量代码改善C#程序的157个建议京东价: ¥46.02 (降价通知) ...编写高质量代码改善C#程序的157个建议[10-12]编写高质量代码改善C#程序的157个建议[10-12]
| 阅: 转: | ...如果集合中有成千上万个复杂的实体对象,那么进行排序时耗费的时间是巨大的。所以...编写高质量代码_上书店买书_购编写高质量代码相关图书_孔夫子... 编写高质量代码改善C#程序的157个建议 作者:陆敏技 著 出版社:机械工业出版社 出版时间: 装订:平装 开本:16开 版次:1 淘和乐书店(3) 北京市...C#事件由浅至深简析 - 百科教程_经验分享平台[上学吧经验教程... 3.C#事件机制 所有的教材里面都会先讲解C#委托,然后才会讲解C#事件,因为事件...:编写高质量代码改善C#程序的157个建议[动态数组、循环遍历、对象集合初始化]...编写高质量代码改善C#程序的157个建议[匿名类型、Lambd..._第七城市编写高质量代码改善C#程序的157个建议一:基本语言要素 - ..._博客园编写高质量代码改善C#程序的157个建议一:基本语言要素 1. 正确操作字符串 确保...如果类型的值相等,则应该返回T对于应用类型,如果类型指向同一个对象,则返回...编写高质量代码改善C#程序的157个建议-陆敏技(新博)价格(怎..._易购编写高质量代码改善C#程序的157个建议-陆敏技(新博) 商品介绍编写高质量代码改善C#程序的157个建议-陆敏技(新博)用户评论 我也来说两句 发表点评...《编写高质量代码:改善C#程序的157个建议》扫描版[PDF] [编写高质量代码改善C#程序的157个建议].陆敏技.扫描版.pdf 26.42 MB ...建议19:使用更有效的对象和集合初始化建议20:使用泛型集合代替非泛型集合建议21:...编写高质量代码改善C#程序的157个建议[避免finaly内的..._第七城市编写高质量代码改善C#程序的157个建议读书笔记【11-20】 ..._博客园 建议16:元素数量可变的情况下不应使用数组 建议17:多数情况下使用foreach进行循环遍历 建议18:foreach不能代替for 建议19:使用更有效的对象和集合初始化 建议20:...编写高质量代码改善C#程序的157个建议读书笔记【11-20】 - ..._推酷编写高质量代码改善C#程序的157个建议读书笔记【11-...其原因是foreach循环使用了迭代器进行集合的遍历,在... 初始化设定项除了为对象、集合初始化方便外,还为...编写高质量代码改善C#程序的157个建议. - 下载频道 - CSDN编写高质量代码改善C#程序的157个建议.gegushalin上传 [编写高质量代码改善C#程序的157个建议].陆敏技.扫描版资源积分:2分 下载次数:2次 资源类型: ...编写高质量代码改善C#程序的157个建议——导航开篇 - aeh..._博客园 建议16、元素数量可变的情况下不应使用数组 建议17、在多数情况下使用foreach进行循环遍历 建议18、foreach不能代替for 建议19、使用更有效的对象和集合初始化 http...编写高质量代码改善C#程序的157个建议 - 下载频道 - CSDN编写高质量代码改善C#程序的157个建议 u上传 编写高质量代码改善C#程序的157个建议.陆敏技.扫描版.pdf嵌到我的页面 ...编写高质量代码改善C#程序的157个建议(含源代码)_51CTO下载中心_...编写高质量代码改善C#程序的157个建议(含源代码)共计十二章节,络整理,仅供学习交流。编写高质量代码改善C#程序的157个建议 陆敏技【图片 价格 ..._京东 商品名称: 编写高质量代码改善C#程序的157个建议 ... 建议17:多数情况下使用foreach进行循环遍历 建议18:... 建议19:使用更有效的对象和集合初始化 建议20:...编写高质量代码改善c#程序的157个建议 B1_上买书..._孔夫子旧书编写高质量代码改善C#程序的157个建议 - 下载频道 - CSDN编写高质量代码改善C#程序的157个建议 xu上传 编写高质量代码改善C#程序的157个建议资源积分:2分 下载次数:10 资源类型:文档 资源大小:27.69...编写高质量代码改善C#程序的157个建议 - 下载频道 - CSDN编写高质量代码改善C#程序的157个建议 陆敏技著 机械工业出版社 2011.10 原书扫描版嵌到我的页面 资源积分:10分 下载次数:9 资源类型:文档 资源大小:26.59MB...SimpleCD | 《编写高质量代码:改善C#程序的157个建议》扫描版[PDF]建议16:元素数量可变的情况下不应使用数组建议17:多数情况下使用foreach进行循环遍历建议18:foreach不能代替for建议19:使用更有效的对象和集合初始化建议20:使用泛型...编写高质量代码改善C#程序的157个建议 - 千寻 - 博客频道 - CSDN... 建议16、元素数量可变的情况下不应使用数组 建议17、在多数情况下使用foreach进行循环遍历 建议18、foreach不能代替for 建议19、使用更有效的对象和集合初始化 3、...编写高质量代码改善C#程序的157个建议 - 下载频道 - CSDN[编写高质量代码改善C_#程序的157个建议].陆敏技.扫描版(ED2000)... [编写高质量代码改善C_#程序的157个建议].陆敏技.扫描版(ED2000)...编写高质量代码改善C#程序的157个建议 - 天天三国杀 建议16、元素数量可变的情况下不应使用数组 建议17、在多数情况下使用foreach进行循环遍历 建议18、foreach不能代替for 建议19、使用更有效的对象和集合初始化 http...C# - 第86页 - 站分类 - 博客园编写高质量代码改善C#程序的157个建议[4-9]前言 本文首先亦同步到http://..字段通常保存和对象状态有关的数据,而创建本地变量经常用于保存... 幕三少 ...【论文存档】Windows可执行程序内存动态代码化的研究与实现 | LXF's X Factory
Categories
Select Month
February 2015 &(3)
January 2015 &(4)
December 2014 &(2)
November 2014 &(4)
October 2014 &(1)
August 2014 &(2)
July 2014 &(4)
June 2014 &(4)
May 2014 &(4)
April 2014 &(2)
March 2014 &(2)
February 2014 &(3)
January 2014 &(1)
December 2013 &(5)
November 2013 &(2)
October 2013 &(4)
September 2013 &(8)
June 2013 &(2)
May 2013 &(2)
April 2013 &(3)
March 2013 &(7)
January 2013 &(6)
December 2012 &(6)
November 2012 &(5)
October 2012 &(1)
September 2012 &(1)
August 2012 &(4)
July 2012 &(2)
June 2012 &(2)
May 2012 &(1)
April 2012 &(5)
March 2012 &(4)
February 2012 &(13)
November 2011 &(1)
October 2011 &(1)
September 2011 &(2)
August 2011 &(1)
July 2011 &(1)
June 2011 &(5)
May 2011 &(2)
April 2011 &(8)
March 2011 &(6)
February 2011 &(6)
January 2011 &(8)
December 2010 &(4)
November 2010 &(4)
October 2010 &(5)
September 2010 &(8)
August 2010 &(12)
July 2010 &(6)
October 2009 &(1)
August 2009 &(1)
Recent Posts
Contact Me暂无贡献等级
最近登录: 16:16:49
实现了s2sh的整合,前端使用EasyUI,实现了管理员的增删改查功能,DAO实现了对持久化数据的基本操作;管理员的登录、退出、密码修改,数据库存储中文。拦截器拦截未登录用户,下载直接使用。。。如图,有些素材来自最代码站。
&&&由最代码官方编辑于 1...
实现了s2sh的整合,前端使用EasyUI,实现了管理员的增删改查功能,DAO实现了对持久化数据的基本操作;管理员的登录、退出、密码修改,数据库存储中文。拦截器拦截未登录用户,下载直接使用。。。如图,有些素材来自最代码站。
由编辑于 14:32:37
暂无贡献等级
最近登录: 16:16:49
实现了s2sh的整合,前端使用EasyUI,实现了管理员的增删改查功能,DAO实现了对持久化数据的基本操作;管理员的登录、退出、密码修改,数据库存储中文。拦截器拦截未登录用户,下载直接使用。。。如图,有些素材来自最代码站。
&&&由最代码官方编辑于 1...
实现了s2sh的整合,前端使用EasyUI,实现了管理员的增删改查功能,DAO实现了对持久化数据的基本操作;管理员的登录、退出、密码修改,数据库存储中文。拦截器拦截未登录用户,下载直接使用。。。如图,有些素材来自最代码站。
由编辑于 14:32:37
暂无贡献等级
最近登录: 16:16:49
下载分享代码
1、myEclipse2013 2、tomcat7.0 3、SQLserver2008 MyEclipse2013 &UTF-8& SQLserver2008 数据库为db_user,表为user 可以导入SQL文件,执行以下。也可以自己创建一个数据库和数据表,在uti...
1、myEclipse2013 2、tomcat7.0 3、SQLserver2008 MyEclipse2013 &UTF-8& SQLserver2008 数据库为db_user,表为user 可以导入SQL文件,执行以下。也可以自己创建一个数据库和数据表,在util包里修改数据库名字,和dao包里修改数据库表名。
由编辑于 12:11:55
暂无贡献等级
最近登录: 16:16:49
1、myEclipse2013 2、tomcat7.0 3、SQLserver2008 MyEclipse2013 &UTF-8& SQLserver2008 数据库为db_user,表为user 可以导入SQL文件,执行以下。也可以自己创建一个数据库和数据表,在uti...
1、myEclipse2013 2、tomcat7.0 3、SQLserver2008 MyEclipse2013 &UTF-8& SQLserver2008 数据库为db_user,表为user 可以导入SQL文件,执行以下。也可以自己创建一个数据库和数据表,在util包里修改数据库名字,和dao包里修改数据库表名。
由编辑于 12:11:55
暂无贡献等级
最近登录: 16:16:49
taobaotutu
暂无贡献等级
最近登录: 16:16:49
扫描二维码关注最代码为好友"/>扫描二维码关注最代码为好友11205人阅读
摘要:所谓SMC(Self Modifying Code)技术,就是一种将可执行文件中的代码或数据进行加密,防止别人使用逆向工程工具(比如一些常见的反汇编工具)对程序进行静态分析的方法,只有程序运行时才对代码和数据进行解密,从而正常运行程序和访问数据。计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。由于该技术需要直接读写对内存中的机器码,所以多采用汇编语言实现,这使得很多想在自己的程序中使用SMC技术进行软件加密的C/C++程序员望而却步。针对这种现状,本文提出了几种基于C/C++语言的机器指令定位方法,从而用C/C++语言实现了动态代码修改技术。关键词:SMC 动态代码修改 软件加密
输入您的搜索字词
提交搜索表单
一、什么是SMC技术
&&& 所谓SMC(Self Modifying Code)技术,就是一种将可执行文件中的代码或数据进行加密,防止别人使用逆向工程工具(比如一些常见的反汇编工具)对程序进行静态分析的方法,只有程序运行时才对代码和数据进行解密,从而正常运行程序和访问数据。计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。现在,很多加密软件(或者称为&壳&程序)为了防止Cracker(破解者)跟踪自己的代码,也采用了动态代码修改技术对自身代码进行保护。以下的伪代码演示了一种SMC技术的典型应用:proc main:............IF .运行条件满足& CALL DecryptProc (Address of MyProc);对某个函数代码解密& ........& CALL MyProc&&&&&&&&&&&&&&&&&&&&&&&&&& ;调用这个函数& ........& CALL EncryptProc (Address of MyProc);再对代码进行加密,防止程序被Dump......end main&&& 在自己的软件中使用SMC(代码自修改)技术可以极大地提高软件的安全性,保护私有数据和关键功能代码,对防止软件破解也可以起到很好的作用。但是,SMC技术需要直接读写对内存中的机器码,需要对汇编语言和机器码有相当的了解,具体的实现一般都是采用汇编语言。由于汇编语言晦涩难懂,不容易掌握,这使得很多想在自己的程序中使用SMC技术进行软件加密的C/C++程序员望而却步。难道只能用汇编语言实现SMC技术?其实不然,从理论上讲,只要支持指针变量和内存直接访问,象C/C++这样的高级语言一样可以使用SMC技术。本文就是利用C/C++语言的一些特性,比如函数地址和变量地址直接访问等特性,实现了几种对运行中的代码和数据进行动态加密和解密的方法。首先是利用Windows可执行文件的结构特性,实现了一种对整个代码段进行动态加密解密的方法;接着又利用C/C++语言中函数名称就是函数地址的特性,实现了一种对函数整体进行加密解密的方法;最后采用在代码中插入特征代码序列,通过查找匹配特征代码序列定位代码的方式,实现了一种对任意代码片断进行解密解密的方法。下面就分别介绍这几种方法。
二、对整个代码段使用SMC方式加密解密
&&& 在程序中使用SMC最简单的方法就是修改(或加密)整个数据段或代码段,这里首先要讲一下&段&的概念。这个&段&有两层含义,第一层含义是程序在内存中的分布,老的16位操作系统对内存使用分段映射的方式,使用不同的段分别存放代码、数据和堆栈,使用专用的基址寄存器访问这些段,于是就有了代码段、数据段和堆栈段等等区分。随着32位Windows的兴起,一种新的32位平坦(Flat)内存模式被引入Windows内存管理机制,在平坦模式下对段的区分已经没有意义了,但是段的概念依然被保留下来,这些同名的基址寄存器现在被成为&段选择器&,只是它们的作用和普通的寄存器已经没有区别了。段的另一层含义是指保存在磁盘上的Windows可执行文件中的数据结构(就是PE文件中的Section),是Windows在装载这个可执行文件时对代码和数据定位的参考。不过要真正理解段的概念,还需要了解Windows 可执行文件的结构和Windows将可执行文件加载到内存中的方式。&&& Microsoft为它的32位Windows系统设计了一种全新的可执行文件格式,被成为&Portable Executable&,也就是PE格式,PE格式的可执行文件适用于包括Windows 9X、Windows NT、Windows 2000、Windows XP以及Windows 2003在内的所有32位操作系统,估计以后的Windows新版本也将继续支持PE格式。PE文件格式将文件数据组织成一个线性的数据结构,图2-1展示了一个标准PE文件的映象结构:
图2-1 Windows PE文件映像结构
位于文件最开始部位的是一个MS-DOS头部和一段DOS stub代码,在PE文件中保留这一部分是为了DOS和Windows系统共存那一段时期设计的,当程序运行在DOS系统时,DOS系统按照DOS可执行文件的格式调用DOS stub代码,一个典型的DOS stub代码就是在控制台上输出一行提示:&This program cannot be run in MS-DOS mode&,当然不同的编译器产生的DOS stub代码也各不相同。曾经有一段时间很流行一种既可以在DOS系统上运行,又可以在Windows上运行的程序,其原理就是人为地替换这段DOS stub代码。紧跟在DOS stub代码之后的就是PE文件的内容了,首先是一个PE文件标志,这个标志有4个字节,也就是&PE/0/0&。这之后紧接着PE文件头(PE Header)和可选头部(Optional Header,也可以理解为这个PE文件的一些选项和参数),这两个头结构存放PE文件的很多重要信息,比如文件包含的段(Sections)数、时间戳、装入基址和程序入口点等信息。这些之后是所有的段头部,段头部之后跟随着所有的段实体。PE文件的尾部还可能包含其它一些混杂的信息,包括重分配信息、调试符号表信息、行号信息等等,这些信息并不是一个PE文件必须的部分,比如正常发布的Release版本的程序就没有调试符号表信息和行号信息,所以图2-1 表示的结构图中省略了这些信息。&&& 在整个头结构中,我们关心的仅仅是各个段的段头部,因为段头部包含这个段在文件中的起始位置、长度以及该段被映射到内存中的相对位置,在对内存中的代码修改时,需要这些信息定位内存读写地址和读写区域长度。下面来看看winnt.h中对段首部的定义,typedef struct _IMAGE_SECTION_HEADER {&&& BYTE&&& Name[IMAGE_SIZEOF_SHORT_NAME];&&& union {&&&&&&&&&&& DWORD&& PhysicalA&&&&&&&&&&& DWORD&& VirtualS&&& } M&&& DWORD&& VirtualA&&& DWORD&& SizeOfRawD&&& DWORD&& PointerToRawD&&& DWORD&& PointerToR&&& DWORD&& PointerToL&&& WORD&&& NumberOfR&&& WORD&&& NumberOfL&&& DWORD&& C} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;在这个头结构中我们关心的是Name、VirtualSize、VirtualAddress和Characteristics四个属性。Name是这个段的名称,长度是8个字节,段名称一般以&.&开始,如&.text&,&.data&等等,但是这并不意味着段名称必须以&.&开始,这只是Microsoft的编译器的一个约定,很多编译器并不遵循这个约定。段名称对直接修改内存代码和数据是一个很重要的属性。因为在内存中定位段头部是通过搜索这个Name字符串来实现的。VirtualSize是一个段的真实长度,它有别于SizeOfRawData,SizeOfRawData是文件对齐后的长度,通常PE文件是以200H字节对齐的,所以SizeOfRawData是200H的整数倍。但是被Windows装入内存中就不一定是按照200H字节对齐了,所以要用VirtualSize来确定段的长度。VirtualAddress是这个段在内存中的相对虚地址(RVA),这个相对虚地址加上程序加载的基地址就是这个段在内存中的真正地址。最后是段属性Characteristics,操作这个段属性的目的是为这个段增加可写入的属性,因为Windows不允许向一个只读的段写数据。段属性由一些标志位组成,各个常用标志位的含义以及它们的值如下表所示:Flag&&&&&&&&&&&&&&&&& & & & && 意义0x&&&&&&&&&&&& 这是一个代码段0x&&&&&&&&&&&& 这个段包含已初始化数据0x&&&&&&&&&&&& 这个段包含未初始化数据0x&&&&&&&&&&&& 这个段的数据可被丢弃(EXE文件装载完成后,进程就不需要这些数据了)0x&&&&&&&&&&&& 该段可以执行0x&&&&&&&&&&&& 该段为共享段0x&&&&&&&&&&&& 该段可读0x&&&&&&&&&&&& 该段可写
表 2-1常用段属性标志位
通常编译器生成的程序的代码段具有0xxx属性,如果我们要修改代码段的代码,就需要为其添加0x标志,否则会引起Windows报告非法访问的异常。&&& PE格式文件的使用,使得Windows加载可执行文件不用再象以前一样将可执行文件拆开,在内存中东一块西一块地放置,取而代之的是一种简单的加载方式,就是按照顺序将PE文件读取到内存中,这也使得加载到内存中的PE文件和存放在磁盘上的PE文件具有相似的结构,只是各个段因为对齐方式的不同而导致偏移位置略有不同,下图演示了这种差别:
图2-2 PE文件磁盘逻辑结构和那粗映象逻辑结构
&&& 上面只是简单介绍了PE文件的格式以及加载方式,如果想更加深入了解PE文件,可以查阅本文的参考文献[2],下面本文就通过一个简单的例子介绍一下如何通过直接访问内存实现对代码的动态加密和解密。首先要说明的是不能对编译器生成的默认代码段进行全代码段加密,这是很显然的,因为整个程序的入口代码也在默认代码段,如果对整个默认代码段加密,你将没有机会对其解密,从而造成程序加载运行失败。不同的编译器生成的默认代码名称是不一样的,一般Microsoft的编译器会将所有的代码放置在一个名为&.text&的默认代码段中,而Borland的编译器的默认代码段名为&CODE&,其它的编译器可能有其它的代码生成策略,不过有一点是相通的,就是不能对程序入口点所在的代码段实行整段加密。针对这种情况,本文介绍的策略就是将需要加密的重要代码或数据放置在一个单独的代码段中,然后通过内存查找定位到这个段并对其进行加密解密操作。首先是通知编译器在生成代码时生成一个新的代码段,并将我们指定的代码放置在这个代码段中,对于做到这一点,不同的编译器有不同的实现方法,本文的例子使用的编译器是Visual C++,可以使用预编译指令#pragma为程序添加一个代码段。首先用VC的向导生成一个Win32应用程序框架,然后添加如下代码:#pragma code_seg(&.scode&)int CalcRegCode(const char *pszUserName, char *pCodeBuf, int nbufSize){&&& if(!pszUserName || !pCodeBuf)&&& &&& return 0;&&& int nLength = strlen(pszUserName);&&& if(nLength &= 0 || nLength &= nbufSize)&&& &&& return 0;&&& if(::IsBadReadPtr(pszUserName,nLength) || ::IsBadWritePtr(pCodeBuf,nbufSize))&&& &&& return 0;&&& for(int i = 0; i & nL i++)&&& &&& pCodeBuf[i] = pszUserName[i] + 1;//为了演示,仅仅是作个移位变换&&& pCodeBuf[nLength] = 0;&&& return nL}#pragma code_seg()#pragma comment(linker, &/SECTION:.scode,ERW&)CalcRegCode()函数根据用户名生成一个合法的注册码,这是一个应该受到重点保护的函数,所以要对其进行加密,此处的CalcRegCode()函数代码非常简单,只是为了演示之用,其功能就是把用户名向后移一位形成注册码。#pragma code_seg(&.scode&)指令是告诉编译器为程序生成一个名为&.scode&的代码段,另一个不带参数的预编译指令#pragma code_seg()告诉编译器此处是新代码段的结束位置,这两个预编译指令之间的代码将被编译器放置在这个名为&.scode&的新代码段中。段的名称&.scode&可以根据自己的意愿随意命名,但是长度(不包括结尾的/0结束符)不能超过8个字节,这是由Windows PE文件的结构所决定的。最后一行#pragma comment(linker, &/SECTION:.scode,ERW&)是告诉链接程序最终在生成代码时添加这个名为&.scode&的代码段,段属性为&ERW&,分别表示可执行、可读和可写。也可以不使用预编译指令#pragma comment,直接在编译选项中添加&/SECTION:.scode,ERW&选项也可以达到相同的目的。现在编译这个程序,使用PE文件查看工具可以看到程序中已经有了一个名为&.scode&的代码段,段属性为0xE0000020,也就是0x(代码段)、0x(可执行)、0x(可读)和0x(可写)四个属性的组合。
图2-3 演示程序的Section Table
&&&& 有了新的可读写代码段之后的问题就是如何在程序运行期间定位到这个段的位置,并对其进行修改,这就需要知道PE文件加载以后在内存中的位置。当一个可执行程序被Windows加载以后,Windows的虚拟内存管理机制就为其映射了一个单独的4GB内存空间(当然应用程序只能使用其中的一部分,另一部分被操作系统占用),应用程序中的地址都被映射到这个虚拟的内存空间中,整个PE文件被映射到这个虚拟空间的某一段中,开始的位置就被称为映象基地址(Image Base),这个地址当然也是一个&虚地址&(区别于在内存硬件中的真实地址)。Windows提供了一个API用于获得应用程序的基地址,这个API就是GetModuleHandle(),它的函数原型是:HMODULE GetModuleHandle(LPCTSTR lpModuleName);参数lpModuleName用于指定模块的名字,如果是获得当前可执行文件加载的基地址,只需传递一个NULL就可以了,返回值类型HMODULE看起来有些神秘,其实可以将其强制转换成一个void类型的指针使用,它指向的位置就是我们需要的基地址。找到映象基地址以后,就可以根据PE文件的结构依次遍历所有的Section(段)表,找到名为&.scode&的段,然后通过段表中的VirtualAddress属性得到&.scode&段在内存中的起始地址,实际上这个VirtualAddress只是相对于映象基地址的一个偏移量,,&.scode&段的真正位置要通过VirtualAddress加上映象基地址获得。&.scode&段的大小通过VirtualSize属性得到,这个大小是对齐前的大小,也就是全部代码的真正大小,不包括为对齐而填充的0字节。在前面对PE文件介绍的基础上,不难写出这个查找程序,下面就给出一个查找某个段的虚地址和大小的通用函数:bool GetSectionPointer(void *pModuleBase,const char *lpszSection,void** ppPos,LPDWORD lpSize){&&& IMAGE_DOS_HEADER *pDosH&&& IMAGE_FILE_HEADER *pPEH&&& IMAGE_SECTION_HEADER *pS&&& *ppPos = NULL;&&& *lpSize = 0;&&& if(::IsBadReadPtr(pModuleBase,sizeof(IMAGE_DOS_HEADER)) || ::IsBadReadPtr(lpszSection,8))&&& &&&&&& if(strlen(lpszSection) &= 16)&&& &&&&&& char szSecName[16];&&& memset(szSecName,0,16);&&& strncpy(szSecName,lpszSection,IMAGE_SIZEOF_SHORT_NAME);&&& unsigned char *pszModuleBase = (unsigned char *)pModuleB&&& pDosHead = (IMAGE_DOS_HEADER *)pszModuleB&&& //跳过DOS头不和DOS stub代码,定位到PE标志位置&&& DWORD Signature = *(DWORD *)(pszModuleBase + pDosHead-&e_lfanew);&&& if(Signature != IMAGE_NT_SIGNATURE) //&PE/0/0&&&& &&&&&& //定位到PE header&&& pPEHead = (IMAGE_FILE_HEADER *)(pszModuleBase + pDosHead-&e_lfanew + sizeof(DWORD));&&& int nSizeofOptionH&&& if(pPEHead-&SizeOfOptionalHeader == 0)&&& &&& nSizeofOptionHeader = sizeof(IMAGE_OPTIONAL_HEADER);&&& else&&& &&& nSizeofOptionHeader = pPEHead-&SizeOfOptionalH&&& bool bFind =&&& //跳过PE header和Option Header,定位到Section表位置&&& pSection = (IMAGE_SECTION_HEADER *)((unsigned char *)pPEHead + sizeof(IMAGE_FILE_HEADER) + nSizeofOptionHeader);&&& for(int i = 0; i & pPEHead-&NumberOfS i++)&&& {&&& &&& if(!strncmp(szSecName, (const char*)pSection[i].Name,IMAGE_SIZEOF_SHORT_NAME)) //比较段名称&&& &&& {&&& &&& &&& *ppPos = (void *)(pszModuleBase + pSection[i].VirtualAddress);//计算实际虚地址&&& &&& &&& *lpSize = pSection[i].Misc.VirtualS//实际大小&&& &&& &&& bFind =&&& &&& &&&&&& &&& }&&& }&&& return bF}&&& 虽然对CalcRegCode()函数做了很多手脚,但是在程序中对CalcRegCode()函数的使用方式和调用其它的函数没有区别,只是需要在调用之前对&.scode&段解密。由于本文介绍的方法需要较多的内存直接操作,特别是对程序要运行的代码进行读写操作,很可能会引起代码的异常,比如对代码解密失败将导致程序运行不可预料的指令,如果你不想让你的程序死的很难看,最好使用异常处理。以下就是对CalcRegCode()函数的使用方法:&&& try&&& {&&& &&& bool bFind = GetSectionPointer((void *)hImageBase,&.scode&,&pSecAddr,&dwSecSize);&&& &&& if(!bFind || !pSecAddr)&&& &&& &&& throw &Not find special section!&;&&& &&& //注意,解密和加密函数也是重要的函数,这两个函数的调用最好放在距离CalcRegCode()函数调用&&& &&& //远一点的位置,避免被发现&&& &&& DecryptBlock(pSecAddr,dwSecSize,0x5A);//首先解密代码段&&& &&& CalcRegCode(&system&,szBuff,128);//调用注册码计算函数&&& &&& &&& &&& EncryptBlock(pSecAddr,dwSecSize,0x5A);//调用后加密代码段&&& }&&& ....//异常处理&&& 到现在为止所有的动态准备工作已经做完,只差最后一道工序,那就是在程序生成之后对&.scode&代码段预先加密。由于编译器生成的代码是不加密的代码,为了使本文介绍的方法能够正常使用,必须手工对PE文件中的&.scode&段进行加密处理。本文的例子代码中有一个小程序CryptExe.exe,这是个命令行工具,可以加密指定PE文件的某个位置。剩下的工作就是在磁盘文件中定位&.scode&段的偏移位置。在磁盘文件中定位&.scode&段和在内存映象中定位&.scode&段的方法一样,也是查找Section表中的&.scode&段,然后通过段相应的属性定位这个段在文件中的偏移位置和大小(此时需要访问的属性是PointerToRawData和SizeOfRawData)。不过还有更简单的方式,那就是使用PE文件查看工具直接查看偏移位置和大小,以前面的Section Table为例(图3),演示程序的&.socde&段在文件中的偏移位置是6000H,大小是1000H,换成成十进制分别是2,使用以下命令行就可以对演示程序进行初始加密:CryptExe.exe CrkTest.exe 现在运行CrkTest.exe,会弹出一个OK消息框,显示的内容就是根据字符串&system&计算出来的注册码&tztufn&,如果在CrkTest.exe生成之后忘记了对其进行预先加密,就会出现一个Error消息框,显示错误信息。至此,就完整地实现了对真个代码段进行SMC加密解密的功能。
三、对整个函数体使用SMC方式加密
&&& 上一节本文介绍了一种动态加密代码的方法,就是在程序运行期间对整个代码段进行加密和解密操作,可以保护一些对软件防破解至关重要的代码,但是这样的方法也有一些弊端,那就是需要一个额外的代码段,这有点儿&此地无银三百两&的感觉,这个额外的代码段无疑会成为破解者重点&照顾&的对象。这一节本文将介绍一种对某个函数的代码进行加密解密的方法,这种方法不需要创建额外的代码段,使用上比较隐蔽,不易觉察。&&& 对单个函数进行加密和对整个代码段加密的原理一样,也需要在内存映象和PE文件中定位代码的起始位置和代码块的大小,只是代码定位方式不同。首先介绍一下如何在程序的内存映象中定位函数的起始位置和函数代码块的大小。C/C++语言有一个特性,那就是函数名就代表函数的开始地址,所以根据函数名可以得到代码块在内存中的位置,剩下的问题就是如何确定函数代码块的大小,也就是如何找到函数的最后一条指令的位置。很不幸,对于这个问题除了直接查看汇编代码之外确实没有很完美的解决方法,不过,如果我仅仅说:去查看汇编代码吧,找到最后的ret指令就行了,那就太&不负责任&了,也违背了本文的初衷。&行走江湖&,第一招不行肯定要有&Plan B&,备用方案当然是一些不太&完美&的方法,比如本文使用的方法就是计算与这个函数相邻的下一个函数的起始位置与这个函数的起始位置的差,这个差值就可大致认为是函数代码块的大小。尽管很多资料也都介绍了这种方法,但是这种方法的不完美性还是表现在以下两个方面:一方面是编译器不能保证两个C/C++代码相邻的函数在最终生成的机器代码中也是相邻的,没有任何编译器做了这个承诺,所以使用这种方法是有风险的。另一方面的不完美性是因为这种方法对函数有很多的约束,这种约束体现在编译器生成代码的策略上,很多资料对此都有特别的说明,比如函数中最好不要使用longjmp()之类的函数,也不要使用switch...case语句,当然更不能使用异常处理机制了,这是因为当代码中出现上述情况时,编译器不能保证生成的代码会在一个连续的代码块中,特别是异常处理这种情况。尽管这种方法有这样那样的不完美性,但它还是得到了广泛应用,因为对于第一个不完美性,除非出现意外情况,很多编译器都会尽力做到代码的连续性,至于第二个不完美性,只要巧妙地构造代码,避免上述语句的使用,同时合理设置if判断语句,缩减函数代码长度,就可以避免长跳转代码块的出现。看来,使用这种方法虽然不是十分安全,不过只要方法得当,也还是值得信赖的,作者在参与的几个软件加密项目中都使用了这种方法,目前都能够可靠地工作,所以,此处推荐使用这种简单的方法。&&& 现在还是用一个例子来看看具体的效果吧。首先使用VC创建一个基于对话框的项目,然后将上一个例子中的CalcRegCode()函数复制到这个项目中,并紧跟其后添加一个空函数,函数类型和名称随便,比如:void CalcRegCodeEnd();然后在程序中就可以通过下面一行程序得到CalcRegCode()函数的长度:int nFuncSize = ((char *)CalcRegCodeEnd - (char *)CalcRegCode);不要急着编译运行这个程序,因为有个特殊情况需要了解,那就是这行代码只在Release版本的程序中才能得到正确的结果,因为Visual C++编译器生成的Debug版本通常将一些调试信息放在函数代码开始之前,所以函数开始位置被转向到了一条跳转指令jmp(0xE9),这样VC的调试器就可以根据函数名定位到函数的调试信息,而这条跳转指令又能保证函数体代码被正确地执行,真是一举两得,但是也给我们的方法带来了小小的麻烦。不过既然知道原因,就不难想出对策,下面的代码就是针对这种情况做一下调整,通过简单的计算得到函数代码的真正开始位置:&&& char *pFuncAddr = (char *)CalcRegC&&& if(*((unsigned char*)pFuncAddr) == 0xE9)//判断是否是跳转指令&&& {&&& &&& pFuncAddr++; //跳过0xE9指令&&& &&& i =* (int *)pFuncA//这个jmp指令的操作数,也就是跳转的距离&&& &&& pFuncAddr += (i + 4); //修正到正确的位置,多加4是因为这个操作数也是4个字节&&& }上面代码的判断依据就是函数的第一条指令通常不是跳转指令(除非程序已经被破解了),调整的方法上面注释已经有详细说明,此处就不再赘述。至此,函数在内存映象中的定位问题已经解决,剩下的事情就是如何定位函数开始位置在PE文件中的偏移量,以便我们的外部工具CryptExe.exe能够对其进行初始加密。不过,这次我们同样没有完美的解决方法,更糟糕的是,对于这个问题我们甚至连&不完美&的方法都没有,那为什么还要浪费时间写这些没用的东西?因为我们还有最后一条&救命稻草&,这根所谓的&救命稻草&就是:代码的内存映象地址和代码在PE文件中的偏移位置存在线性关系。在本文前面&对代码段加密&一节曾经提到Windows加载PE可执行文件的方式是一种简单的按照PE文件字节顺序的方式,加载到内存中的PE文件和存放在磁盘上的PE文件具有相似的结构,只是各个段因为对齐方式的不同而导致段的偏移位置略有不同,这也就是说,这种不同是指的段偏移位置不同,而代码在段内相对于段首的偏移量是不变的,当链接器生成可执行文件时,代码的段内偏移量就已经固定下来了,并且不会因为Windows加载可执行文件到内存中的不同位置而改变。也就是说,代码在内存映象中的地址与PE文件中的偏移量存在以下线性关系:代码内存虚拟地址 - 代码段内存虚拟地址 = 代码文件偏移量 - 代码段的文件偏移转换这个等式就可以得到文件偏移的计算公式:代码文件偏移量 = 代码内存虚拟地址 - (代码段内存虚拟地址 - 代码段的文件偏移)公式中的&代码段的文件偏移&就存在与段头部表中的PointerToRawData属性,在介绍PE文件的段头部结构时没有提到这个属性,这个属性给出了段的原始数据在文件中的开始位置,这也是段头部信息中一个很重要的属性。&代码段内存虚拟地址&可以通过段头部的VirtualAddress属性获得,前面已经介绍过,这个属性是一个相对虚地址,需要加上PE内存映象的基地址才是代码段的内存虚拟地址。有了这些信息,就可以很容易地根据虚拟地址计算出PE文件偏移位置,具体的算法和上一节介绍的GetSectionPointer()函数相似,就是遍历段头部信息表,找到代码段&.text&(这是VC编译器生成的默认代码段名称,如果是Borland的编译器,可能是&CODE&),然后根据PointerToRawData属性、VirtualAddress属性和程序的基地址计算出文件偏移位置,下面给出这个计算函数的代码:int VAtoFileOffset(void *pModuleBase,void *pVA){&&& IMAGE_DOS_HEADER *pDosH&&& IMAGE_FILE_HEADER *pPEH&&& IMAGE_SECTION_HEADER *pS&&& if(::IsBadReadPtr(pModuleBase,sizeof(IMAGE_DOS_HEADER)) || ::IsBadReadPtr(pVA,4))&&& &&& return -1;&&& unsigned char *pszModuleBase = (unsigned char *)pModuleB&&& pDosHead = (IMAGE_DOS_HEADER *)pszModuleB&&& //跳过DOS头不和DOS stub代码,定位到PE标志位置&&& DWORD Signature = *(DWORD *)(pszModuleBase + pDosHead-&e_lfanew);&&& if(Signature != IMAGE_NT_SIGNATURE) //&PE/0/0&&&& &&& return -1;&&& unsigned char *pszVA = (unsigned char *)pVA;&&& int nFileOffset = -1;&&& //定位到PE header&&& pPEHead = (IMAGE_FILE_HEADER *)(pszModuleBase + pDosHead-&e_lfanew + sizeof(DWORD));&&& int nSizeofOptionH&&& if(pPEHead-&SizeOfOptionalHeader == 0)&&& &&& nSizeofOptionHeader = sizeof(IMAGE_OPTIONAL_HEADER);&&& else&&& &&& nSizeofOptionHeader = pPEHead-&SizeOfOptionalH&&& //跳过PE header和Option Header,定位到Section表位置&&& pSection = (IMAGE_SECTION_HEADER *)((unsigned char *)pPEHead + sizeof(IMAGE_FILE_HEADER) + nSizeofOptionHeader);&&& for(int i = 0; i & pPEHead-&NumberOfS i++)&&& {&&& &&& if(!strncmp(&.text&, (const char*)pSection[i].Name,5)) //比较段名称&&& &&& {&&& &&& &&& //代码文件偏移量 = 代码内存虚拟地址 - (代码段内存虚拟地址 - 代码段的文件偏移)&&& &&& &&& nFileOffset = pszVA - (pszModuleBase + pSection[i].VirtualAddress - pSection[i].PointerToRawData);&&& &&& &&&&&& &&& }&&& }&&& return nFileO}&&& 现在,已经得到了函数代码在文件中的偏移位置和大小,似乎可以收工庆祝了,不过如果你仔细研究可这种方法就会发现事情没有那么简单,这种方法除了前面提到的几个不够&完美&的手段之外,还存在一个重大&缺陷&。好吧,现在就坦白这个&缺陷&,那就是到,代码在文件中的偏移位置和大小是在自己的程序中计算得到的,外部加密工具CryptExe.exe如何得到这两个值?这确实是个棘手的问题,不过既然这种对函数代码的加密方式已经有这么多的&不完美&,也不在乎再多一个,本文采用的方法是给程序加一个隐蔽功能,这个隐蔽功能不向用户公开,开发者利用这个隐蔽功能获取这两个重要的参数。这看起来很&ungainly&,不过现在在软件中添加隐蔽功能的的软件也不少啊,想到这里应该可以心理平衡一点了吧。本文的例子程序将这个隐蔽功能添加到&关于...&对话框中,只要在打开&关于...&对话框的时候按住键盘右侧的&Ctrl&键,此时显示出来的关于对话框就会多两个控件,分别显示函数代码块在PE文件中的偏移位置和代码块长度,所用的方法就是通过计算函数地址得到函数代码块的长度,通过VAtoFileOffset()函数计算出函数代码块在PE文件中的偏移位置。本例程序运行结果如图3-1所示:
图3-1 通过&关于...&对话框获得文件偏移位置
然后就可以使用如下命令对例子程序CrkTest3.exe中的CalcRegCode()函数进行加密:CryptExe.exe CrkTest3.exe 下面就是加密后的CalcRegCode()函数在某反汇编工具中显示的代码,因为代码已经经过异或加密,而反汇编工具不知道,还按照加密后的机器码生成汇编代码,所以结果看起来很奇怪,当然也不能正常运行。
图3-2 加密后的CalcRegCode()函数代码
&&& 在程序中使用CalcRegCode()函数的方法和上一节介绍的方法一样,就是先调用DecryptBlock()对函数代码块解密,然后正常调用CalcRegCode()函数,最后再调用EncryptBlock()加密,当然,异常处理也是必不可少的。除此之外,还有一些细节需要注意,首先是代码段的读写问题,上一节的例子创建了一个单独的数据段,编译器创建这个段的时候就为其指定了&可写&属性,所以没有特别强调对段属性的修改,但是这一节的例子是直接修改&.text&段的代码,而这个段默认属性是只读的,所以需要考虑段属性的修改问题。有两种方法可以解决这个问题,一种是采用上一节介绍的方法,使用以下预处理指令为&.text&段添加&可写&属性:#pragma comment(linker, &/SECTION:.text,ERW&)另一种方法是在程序生成以后使用peeditor之类的工具修改&.text&的段属性,涉及到具体的软件操作,这里就不再赘述。下面来介绍一下使用这种方法的注意事项,首先要注意的就是CalcRegCodeEnd()函数的设计,这个函数看似无关紧要,但是它却有一个检查加密是否越界的功能,最好不要将其设计成空函数,而是为其填充一些有意义的代码,这样就可以在隐蔽功能中调用这个函数,如果这个函数出现代码异常就说明通过函数地址方法计算出来的函数代码块大小不正确,导致这个函数受到破坏,这时就要重新设计CalcRegCode()函数的结构。另一个需要注意的地方是编译器的优化方式,很多C/C++编译器都提供了优化代码的功能,比如VC的编译器就有对生成代码大小和运行速度进行优化的选项,这里需要提醒大家的是尽量不要使用对可执行文件代码大小进行优化的选项,理由很简单,编译器对代码大小进行优化的时候可能影响到生成代码的连续性,本节提到的这种对函数代码块加密的方法很依赖于代码块的连续性,所以,尽量不要使用减少可执行文件大小的优化选项。最后一个需要注意的事项就是每次编译程序后都要重新通过隐蔽功能重新获得偏移位置,因为编译代码就涉及到重新生成机器代码,当然会影响到代码的位置。只要编译完成以后,这个偏移位置就固定下了,即使程序加载过程中被Windows重定位了,也不会影响到计算出来的偏移位置,因为本方法计算采用的内存虚拟地址就是根据程序的内存映象基地址计算出来的,基地址变了,这个虚拟地址也会跟着改变。况且很多EXE文件都没有地址重定位段,Windows默许这种情况出现就意味着Windows会尽力保证将程序加载到默认的基地址位置,不过对于DLL(动态链接库)来说就没有这么幸运了,它在加载的时候被重定位几乎是&家常便饭&,虽然从理论上讲DLL的加载方式和EXE的加载方式没有区别,但是本人依然不推荐在DLL中使用这种方法。
四、对代码块使用SMC方式加密
&&& 本节将在前面介绍的方法的基础上,再介绍一种直接对函数内的代码块(片断)进行加密的方法,这种方法将更加隐蔽,使破解者更难以定位。通过图3-2可以看出,加密后的函数代码与没有加密的函数代码有明显的不同,主要使函数入口部分的代码,通常正常的函数入口部分代码是栈操作,比如基地址寄存器(ebp)的值入栈,为局部变量预置空间等等,破解者看到如图3-2那样的函数代码,肯定可以判断出这个函数已经被做过手脚了。如果能够避开函数入口处和结尾部分的敏感代码,直接修改函数内部某个位置的代码片断,就可以极大地提高隐蔽性。直接对代码块加密的难点在于代码块的定位,本文前面介绍的两种方法可以通过段属性或函数地址定位到代码块的开始位置,但是直接对任意函数内的代码块进行加密,无论是在内存映象中还是在PE文件中都没有很好的方法可以用来定位代码块的开始位置。当然,没有好的方法并不等于没有方法,人们在实践中还是探索出了一些比较实用的方法。目前最广泛采用的方法是使用对某个特征代码序列的查找来定位代码块的开始位置,就是在程序设计的过程中,人为地构造一个特殊的代码序列,编译器会根据这个代码序列生成相应的机器码,然后就可以在内存映象中或文件中搜索一段机器代码序列,从而实现代码片断的定位。可见,这种方法的重点就是构造一个代码序列,所谓的代码序列就是一条或几条连续的代码,这些代码应该不具备一般性,也就是越特殊,程序的其它地方越不容易出现重复越好,目的是为了防止查找使出现多重匹配,不能唯一定位到人为构造的代码序列的位置。&&& 以往的资料在介绍这种方法的时候多采用汇编语言为例子,构造特征代码序列也是使用汇编代码,这是因为从汇编语言到机器语言比较直观,而且基本上是一一对应,不会产生变形,同时也便于设计人员通过汇编代码手工翻译出机器代码。象C/C++这样的高级语言,编译器根据C/C++代码生成机器代码的过程中需要经过多个步骤,这中间有很多不确定因素都可能导致编译器不能产生预想的机器代码序列,这也就是大家都&不约而同&地采用汇编语言的原因。但是,这并不说明就不能使用C/C++构造特征代码序列,使用嵌入式汇编就是最简单的方法,如果对汇编语言不了解,或者担心嵌入的汇编代码影响了寄存器的正常使用,也可以简单地使用_emit指令在当前代码位置嵌入一些特殊的数据构成特征数据组,特征数据组和特征代码序列一样可以通过查找的方法定位代码块的位置。下面的例子就是使用嵌入式汇编在当前代码中添加了一个字符串&HelloWorld/0&:__asm{_emit 'H';_emit 'e';_emit 'l';&&&& _emit 'l';&&&& _emit 'o';&&&& _emit 'W';&&&& _emit 'o';&&&& _emit 'r';&&&& _emit 'l';&&&& _emit 'd';&&&& _emit 0x00;&&&& }在文件中定位这个位置时就可以使用一些16进制编辑器在文件中查找这个特征字符串,找到开始位置后向后偏移11个字节就是代码块的开始位置。不过本节介绍的方法不使用嵌入式构造特征代码序列,而是利用C/C++语言中与汇编语言对应性最好的赋值语句实现了一种特征代码构造方法。&&& C/C++语言中将常数赋值给某个变量的简单赋值语句,通常可以被翻译成一条简单的汇编代码,以下面的C/C++代码为例:DWORD dwSignVar = 0;//定义一个全局变量dwSignVar = 0x5A5A5A5A;这条赋值语句汇编成机器代码后就是:mov DWORD PTR [AAAAAAAAH], 5A5A5A5AH最终生成的机器码就是:C7 05 AA AA AA AA 5A 5A 5A 5A,C7 05是mov指令的机器码,紧跟其后的四个字节是mov指令的第一个操作数,就是变量的dwSignVar的地址AAAAAAAA,再后面的四个字节是mov指令的第二个操作数,也就是常数0x5A5A5A5A。如果在需要加密的关键代码块的开始位置和结束位置使用几条这样精心构造的赋值语句,就可以在关键代码块前后各形成一个比较长的特征代码序列,从而实现代码块的查找定位。这种使用C/C++语言构在特征代码序列的方法同样有很多需要注意的地方,首先是变量要使用全局变量,因为通常将全局变量安排在数据段,这样可以保证程序被加载到内存中执行的时候它的虚拟地址是固定的,这一点很重要,因为这个地址(就是上面例子中的AA AA AA AA)是特征代码序列的重要组成部分,它必须是固定的,不随程序每次加载运行而改变。其次是赋值语句的使用数量问题,一般连续的一至两条赋值语句就可以了,如果太多反而会起副作用,这是因为编译器进行代码优化的时候为了对寄存器访问进行优化,通常会调整代码的顺序,这样就很可能在我们的赋值语句中间插入其它代码,从而影响特征代码序列。最后一点需要注意的是必须是直接常数赋值,这个常数不能使用变量替代,也就是说不能用一个值是0x5A5A5A5A的变量代替这个常数,因为那样会导致mov指令的第二个操作数发生变化,无法得到预期的mov指令。&&& 剩下的问题就是如何定位代码块的位置以及如何将代码块的文件偏移位置显示出来。定位代码块的位置很简单,就是从一个指定的内存位置开始,搜索特征字符串,因为特征代码块一般是位于某个函数内部,所以通常从一个函数的开始位置搜索特征代码序列。例子中的函数FindCodeTag()就是负责在一块内存区域中定位特征代码序列的位置:int FindCodeTag(void *pStartAddr, unsigned long *pTagLoc, unsigned long lTagValue, int nSerachLength){&&& int nPos = -1;&&& int i = 0;&&& unsigned char *pAddr = (unsigned char *)pStartA&&& while(i & nSerachLength)&&& {&&& &&& if((*pAddr == 0xC7) && (*(pAddr + 1) == 0x05))//查找mov指令&&& &&& {&&& &&& &&& unsigned long *Loc = (unsigned long *)((unsigned char*)pAddr + 2);&&& &&& &&& if(*Loc == (unsigned long)pTagLoc)//此处的数据*Loc就是全局静态变量的地址&&& &&& &&& {&&& &&& &&& &&& unsigned long *Val = (unsigned long *)((unsigned char*)pAddr + 6);&&& &&& &&& &&& if(*Val == lTagValue)//此处的数据*Val就是常数lTagValue值&&& &&& &&& &&& {&&& &&& &&& &&& &&& nPos =&&& &&& &&& &&& &&&//find tag&&& &&& &&& &&& }&&& &&& &&& }&&& &&& }&&& &&& pAddr++;&&& &&& i++;&&& }&&& return nP}第一个参数pStartAddr是开始位置,第二个参数就是赋值语句中使用的全局变量的地址,第三个常数是赋值语句中常数的值,最后一个参数是搜索区间的长度,如果从pStartAddr开始超过nSerachLength长度的区域中没有找到特征代码序列,就返回-1表示没有找到,否则就返回特征代码序列现对于pStartAddr的偏移量。如果在某个函数内部使用了如下赋值语句作为特征代码序列:void SomeFunction(){&&& ......&&& dwSignVar = 0x5A5A5A5A;&&& ......//关键代码块&&& dwSignVar2 = 0x;}那么就可以这样找到它的开始位置:int nStartPos = FindCodeTag((void *)SomeFunction,&dwSignVar,0x5A5A5A5A,1000);//1000是个大致估计的值nStartPos += 10;//10 是特征代码序列(也就是mov指令)的长度返回值只是特征代码序列的开始位置,还要向后偏移10各字节(这条mov指令的长度)才是代码块的真正开始位置,这里的搜索长度1000只是一个估计值,也可以使用上一节介绍的方法通过函数地址差值计算出搜索长度的大小。同样的方法可以得到另一个特征代码序列的开始位置(也就是关键代码块的结束位置):int nEndPos = FindCodeTag((void *)SomeFunction,&dwSignVar2,0x0);计算nEndPos和nStartPos的差值就是关键代码块的大小。从内存地址计算出文件偏移位置的方法和上一节介绍的方法一样,使用VAtoFileOffset()函数计算出这个偏移量,然后使用&关于...&对话框的隐蔽功能显示给软件开发人员。其它问题,比如异常处理、Debug版函数地址修正以及修改代码段的读写属性等等问题都已经在上一节介绍了,此处不再赘述,具体内容可参考CrkTest2的例子代码,演示程序CrkTest2的使用方法和上一节的例子程序CrkTest3类似,程序编译完成以后要使用CryptExe.exe对关键代码加密,否则会出现指令异常。
&&& 本文介绍了三种使用SMC动态修改代码技术实现的代码加密方法,这些方法采用动静结合的方式,通过对可执行程序文件的静态加密,提高了程序反静态分析的能力,在运行过程中对装载到内存中的可执行程序代码进行动态修改,对动态反跟踪,反调试也很有帮助,如果能够在程序中合理地应用这些方法,可以提高软件的安全性,增加破解难度。本文还使用具体的例子程序演示了每种方法的具体使用,这里例子都是用C/C++语言实现,大大降低了程序员在自己的软件中使用这些技术的门槛。
关于演示程序代码
&&& 演示程序的代码有四部分组成,CryptExe是外部加密工具CryptExe.exe的源代码,CrkTest是第二节介绍的对整个代码段加密的演示程序,CrkTest3是对函数进行加密的演示程序,CrkTest2是对内部代码片断进行修改加密的演示程序,所有的代码都在VC6和Visual Studio 2003下编译测试通过,演示程序的使用也很简单,首先编译生成演示程序(如果是CrkTest2和CrkTest3,请使用Release方式生成程序,如果要在Debug版本中使用,请参考本文第三节介绍的方法修正函数地址),然后按照本文介绍的方法使用CryptExe.exe工具加密生成的应用程序,最后就可以运行演示程序看结果了。对这些演示程序感兴趣的朋友还可以访问以下链接获取代码的最新修改和勘误:
[1]&&& 段刚.软件加密技术内幕.北京:电子工业出版社,2004.[2]&&& Matt Pietrek.Peering Inside the PE: A Tour of the Win32 Portable Executable File Format.MSDN Magazine,1994
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:824157次
积分:9488
积分:9488
排名:第636名
原创:95篇
译文:12篇
评论:992条
阅读:50519
文章:31篇
阅读:403537
(1)(1)(1)(1)(1)(2)(1)(1)(2)(1)(1)(5)(2)(4)(1)(1)(5)(1)(1)(2)(3)(1)(1)(1)(1)(1)(3)(1)(1)(5)(1)(1)(1)(1)(3)(1)(1)(2)(1)(1)(2)(3)(1)(3)(2)(3)(8)(8)(1)(2)(2)(1)(5)}

我要回帖

更多关于 淘宝css初始化代码 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信