Delphi程序的自我修改

时间:2024-01-11 15:14:02

前言: 
    对于Delphi在编译时对代码所做的工作,大部分使用Object Pascal之类的高级语言的程序员并不是很熟悉。如果你对汇编程序以及EXE文件格式有一点基本认识,那么源代码里包含的注释将把一切解释得非常清楚。另外,我还要说明一下源代码在编译时被做了什么处理。 
    我对汇编程序以及EXE文件格式的认识也是及其有限的,大部分是我在寻找反盗版和程序的自我修改等信息时自学的。为什么我要写这篇文章?因为我发现这方面的信息非常少,因此我把收集到的信息整理到一起,并希望能和大家一起分享。

程序的自我修改: 
    这是什么意思呢?一般情况下,我们只能在设计期修改我们的源代码。代码修改一般是在代码被编译前在Delphi内部完成的,这一点我们都很清楚。 
    但是,有时编译好的程序也被修改了。例如,给一个没有运行的EXE文件打补丁可以升级原来的EXE文件。当应用程序已经广泛发布后,用户想把它升级到新版本一般都使用这种方法。为了节约下载时间和排除用户把整个程序重新安装一遍,只有两个版本的EXE文件的不同之处被分发在补丁文件里,这样这个补丁就可以应用于老版本的EXE文件。另一个补丁的例子就是破解——小小的COM文件或者EXE文件就可以移走一个软件原来的限制性(比如时间限制)。 
    很显然,这两种代码修改的方式是在EXE文件运行之前进行的。当一个EXE文件运行时文件被载入内存,这时要影响程序的行为就只能修改该EXE文件占用的内存了。 
    通过在程序运行时期改变内存来修改自身的方式称为“程序的自我修改”。

程序自我修改的缺点: 
    程序自我修改加大了调试的难度,因为内存的实际信息和调试器所认为的信息其实是有差异的。 
    程序自我修改还有一个不好的名声,尤其因为它的显著表现就是病毒。这意味着如果你使用了程序的自我修改,那么很多杀毒软件会误以为你的程序是病毒。

程序自我修改的优点: 
    程序自我修改加大了调试的难度。在你调试代码时你觉得它是个缺点,为了不让其他用户调试你的代码,或者说增加他的调试难度,从这方面来说它又是个优点。这就是说程序自我修改是反盗版计划的有效组成部分。

需要什么函数: 
    在Windows环境下我们需要调用如下几个API函数: 
◎ReadProcessMemory(hProcess,lpBaseAddress,lpBuffer,nSize,lpNumberOfBytesRead);  
    这个函数用于读取某一个进程的内存,由于本文是一个关于程序自我修改的例子,所以只能在我们的进程内使用这个函数。 
◎WriteProcessMemory(hProcess,lpBaseAddress,lpBuffer,nSize,lpNumberOfBytesWritten);  
    这个函数用于向某一个进程的内存写入信息。 
◎VirtualProtect(lpAddress,dwSize,flNewProtect,lpflOldProtect);  
    这个函数用于修改进程的内存数据存取保护的区域。 
    以上函数的具体参数含义详见Win32的帮助文件,具体使用方法请参见下面给出的例子。

示例代码实现什么功能? 
    将被自我修改的代码在CallModifiedCode过程的内部: 
procedure TForm1.CallModifiedCode(Sender: TObject);  
var  
  b:boolean;  
  c:TColor;  
label 1;  
begin  
  c := clgreen;  
  b := true;  
  if b then goto 1;  
  asm  
    nop  
    nop  
    nop  
    nop  
    nop  
    nop  
  end;  
  c := clred;  
 1:  
  form1.Color := c;  
end;  
    在看完这段代码以后你可能对某些地方疑惑不解。很显然这段代码用于设置Form1的颜色,但是正如你所理解的那样,Form1的颜色总是绿色的,因为布尔变量b的值一直为true,所以程序总会跳到标号1处使得“c:=clred”语句不会被执行。 
    然而,程序这个程序里面将有另一个函数,它将在程序运行时把“if b then goto 1;”语句改为“if NOT(b) then goto 1;”语句,当内存中完成这个修改后,再次调用CallModifiedCode过程时,窗体将会变成红色。注意我们不是改变布尔变量b的值,而是在if语句里面插入一个“NOT”。 
    你一定也注意到过程内部的六个“nop”了,“nop”是一个汇编指令,完成的是没有实际用处的空操作,因此这六行其实也没有做实际工作。连续六个“nop”在编译好的EXE文件里是很不寻常的,因此我们将使用它们作为一个标志,用来在EXE文件里给上面的if语句定位。 
    为了理解我们是如何修改代码的,我们首先需要知道编译器是如何处理这段Pascal源代码的。在窗体上放置一个名为“Executecode”的按钮,它的单击事件设置为“CallModifiedCode”。在Delphi的IDE窗口里运行这个程序,在if语句处设置断点(可以通过单击按钮调用CallModifiedCode过程,再由调试器中断程序的执行),打开CPU视图窗口。你将会看到类似以下的代码: 
807DFB00 cmp byte ptr [ebp-$05],$00  
750B jnz TForm1.CallModifiedCode+$2A  
90 nop  
90 nop  
90 nop  
90 nop  
90 nop  
90 nop  
    我们可以很清楚的从上面的代码里看到这六个“nop”,上面的两行就是if语句的汇编指令。第一行用于把一个值(从Pascal源代码中我们知道它必须是布尔类型b的值)与$00进行比较,$00是表示0的十六进制,表示布尔类型则代表false。第二行以jnz开头,表示“不相等则跳转”,如果第一行的比较不相等的话就跳转到后面的地址去。所以,最上面的两行表示:比较b的值和0(false)的大小,如果不相等则跳转。 
    注意上面汇编语句的左边的十六进制值,每个汇编指令都拥有一个唯一的十六进制标识符。显然,$90表示“nop”。$75表示“jnz”,后面跟着的地址表示要跳转的目标地址(相对于当前地址),本例中跳转地址为$0D。$80表示“cmp”,后面跟着的地址表示它要比较的数据类型和数值。这些十六进制的汇编指令的标识符组成了EXE文件。如果你有一个十六进制编辑器,打开这个编译好的EXE文件,寻找“909090909090”,你会很快找到并发现上面的这些十六进制标识符。 
    下面返回到我们的任务上来,如果要在if语句里加入“NOT”,我们就要把汇编指令“jnz”换成“jz”(“jz”表示“相等则跳转”)。把jnz换成jz将会否定原来的if语句的判断条件,所以一旦修改成功程序就不会跳到标号1处,这样“c:=clred”语句就会被执行,那么窗体颜色就会被设置成红色。上文提到$75表示“jnz”,那么我们还需要知道$74表示“jz”。

自我修改的实现: 
    下面概述以下自我修改的实现:为了把“if b then goto 1;”语句换成“if NOT(b) then goto 1;”语句,定位到内存地址$909090909090处,从这个位置向前两个字节,把$75换成$74。如果还想执行以前未经修改的代码,执行类似的操作,不过是把$74换成$75。 
    本文修改的代码示例如下面的TForm1.ModifyCode过程: 
procedure TForm1.ModifyCode(Sender: TObject); 
const
  BUFFMAX = 65536;
type
  TBytes6 = Array [0 .. 5] of byte;
  TMemblock = array [0 .. BUFFMAX - 1] of byte;

Function ReadBufferFromMemory(ad, size: Integer; var MB: TMemblock): cardinal;
  var
    cnt: cardinal;
  begin
    ReadProcessMemory(Getcurrentprocess, pointer(ad), @MB[0], size, cnt);
    // 返回读取到的字节
    ReadBufferFromMemory := cnt;
  End;

procedure WriteByteToMemory(ad: cardinal; rt: byte);
  var
    cnt: cardinal;
    oldprotect: dword;
  begin
    // 确保拥有向这个地址写入的权限
    VirtualProtect(pointer(ad), sizeof(rt), PAGE_EXECUTE_READWRITE,
      @oldprotect);
    WriteProcessMemory(Getcurrentprocess, pointer(ad), @rt, sizeof(rt), cnt);
    // 恢复以前的权限保护模式
    VirtualProtect(pointer(ad), sizeof(rt), oldprotect, @oldprotect);
  End;

var
  st: TBytes6;
  rt: byte;
  stcount: word;
  BytesRead: cardinal;
  sad, ead, ad: cardinal;
  x, y, z: cardinal;
  found: boolean;
  MemBlock: TMemblock;
begin
  // 定义查询条目,$90表示汇编指令nop
  st[0] := $90;
  st[1] := $90;
  st[2] := $90;
  st[3] := $90;
  st[4] := $90;
  st[5] := $90;
  stcount := 6;
  // 两个按钮的name属性分别为red和green,事件都是ModifyCode
  if (Sender = red) then
    rt := $74 // $74表示汇编指令jz
  else
    rt := $75; // $75表示汇编指令jnz
  // 寻址范围
  sad := ($00400000);
  ead := ($7FFFFFFF);
  // 当前地址
  ad := sad;
  found := false;
  repeat
    // 从当前地址ad开始读取长度BUFFMAX的范围
    BytesRead := ReadBufferFromMemory(ad, BUFFMAX, MemBlock);
    // 如果没有读取到字节则退出
    if BytesRead = 0 then
      break;
    // 确保没有错过查询条件
    If BytesRead = BUFFMAX Then
      BytesRead := BytesRead - stcount;
    // 循环查询这个内存区域
    For x := 0 To BytesRead - 1 do
    begin
      found := true;
      // 检测查询条目
      For y := 0 To stcount - 1 do
        If MemBlock[x + y] <> st[y] then
        begin
          found := false;
          break;
        end;
      If found Then
      begin
        // 查询条目开始地址:ad+x+y-stcount
        z := ad + x + y - stcount;
        // 需要改变的代码在这个地址之前两个字节处
        WriteByteToMemory(z - 2, rt);
        break;
        // 停止查询
      end;
    end;
    ad := ad + BytesRead;
  until (ad >= ead) or found;
end;
    在窗体上放置两个名称分别为“red”和“green”的按钮,单击事件都设置为“ModifyCode”。在CPU窗口观察,分别点击这两个按钮以后再点击“Executecode”按钮,“$75”和“$74”是来回变换的,当然窗体的颜色也是红绿交替变化的。由于源代码的注释比较详细,这里就没有必要再浪费太多语言了。

最后总结: 
    通过点击按钮改变窗体的颜色当然有更简单的办法,但是本文这么做的目的是要演示“Delphi程序的自我修改”。程序的自我修改实际上一门强大的技术,本文的例子可能对反盗版有一定的提示作用。 
    最后给各位一个小小的忠告:在实际应用程序中,你最好小心使用连续的汇编指令“nop”,因为这种无用的代码区域可能就是一些病毒的落脚点,比如CIH病毒就是。

=========上面文章是对下面英文的翻译==========
Self-Modifying Code With Delphi
by Marcus M?nnig - minibbjd@gmx.de
Preface
Lots of people using high-level languages, like Object Pascal, do not know much about what happens with their code when they click compile in Delphi. If you have a basic knowledge about assembler and about the exe file format, the comments in the source code should make everything pretty clear. For everyone else, I will try to explain what's done in the source code.
My own knowledge about assembler and the exe format is limited and I learned most of it while looking for information about piracy protection and how to implement self-modifying code myself. The reason why I did this article is that I found very little information about this issue, so I put everything I found together to share it. Further, english is not my native language, so excuse any spelling and grammatical mistakes.
Self-modifying code with Delphi
What is it? Normally, we modify our code at design time. This usually happens inside Delphi before the code is compiled. Well, we all know this.
Then, sometimes compiled code gets modified, e.g. a patch might be applied to a (non-running) exe file to do changes to the original exe. This is often used when applications are distributed widely and the users want to update to a newer version. To save download time and to prevent that the user has to reinstall the whole application again, only the differences between two versions of an exe file are distributed in a patch file an applied to the old version of the exe. Another example of patch files are "cracks"... little com or exe files that remove built-in limitations (evaluation time limits, etc.) from applications.
These two kinds of code modifications are obviously done before the exe is executed. When an exe file is executed the file gets loaded into memory. The only way to affect the behavior of the program after this point is to modify the memory where the exe now resides.
A program that modifies itself while it is running by doing changes to the memory uses "self-modifying code".
Why is it bad?
Self-modifying code makes debugging harder, since there is a difference in what is in the memory and what the debugger thinks is in the memory.
Self-modifying code also has a bad reputation, especially because the most prominent use for it are viruses, that do all kinds of hide and seek tricks with it. This also means that if you use self-modifying code it's always possible that a virus checker will complain about your application.
Why is it good?
Self-modifying code makes debugging harder. While this is bad if you want to debug your code, it's good to prevent others from debugging your code or at least make it harder for them. This is the reason why self-modifying code can be an effective part of a piracy protection scheme. It won't prevent that an application can be cracked, however a wise use of this technique can make it very hard.
What functions are needed?
In a Windows environment we can make use the following API calls:
ReadProcessMemory(hProcess,lpBaseAddress,lpBuffer,nSize,lpNumberOfBytesRead);
This function is used, well, to read the memory of a process. Since this article is about _self_-modifying code, we will always use this function on our process only.
WriteProcessMemory(hProcess,lpBaseAddress,lpBuffer,nSize,lpNumberOfBytesWritten);
Used for writing data to a process memory.
VirtualProtect(lpAddress,dwSize,flNewProtect,lpflOldProtect);
Used to change the access protection of a region in memory. To learn more about these functions, refer to the Win32 help file that ships with Delphi and take a look how they are used in the sample code.
What does the example code do?
The code that will be modified is inside the CallModifiedCode procedure:
procedure TForm1.CallModifiedCode(Sender: TObject);
var
b:boolean;
c:TColor;
label 1;
begin
c := clgreen;
b := true;
if b then goto 1;
asm
nop
nop
nop
nop
nop
nop
end;
c := clred;
1:
form1.Color := c;
end;
After studying the code you might be puzzled about some things. Obviously, this code sets the color of Form1, but as it is, the color will always be green, since b is always true, so it will always jump to label 1 and c:=clred before never gets called.
However, there is another function in the program that will change the line if b then goto 1; to if NOT(b) then goto 1; while the program is running, so after this modification in memory is done and this function is called again the form will actually be changed to red. Note that we will not change the boolean value of b, but virtually insert a "NOT" into the if statement.
Surely you noticed the six "nop"'s. "nop" is an assembler instruction and means "no operation", so these 6 lines do just nothing. 6 nop's in a row are quite unusual in a compiled exe, so we will use these nops as a marker for the position of the if statement above inside the compiled exe.
To understand how we will modify the code, we need to take a look at what the compiler will make from our pascal code. You can do this by running the project from Delphi, setting a breakpoint on the line with the if statement and (once you called the CallModifiedCode procedure by clicking the button and the debugger stopped the execution) opening the CPU window from Delphi's debug menu. You will see something like this:
807DFB00 cmp byte ptr [ebp-$05],$00
750D jnz TForm1.CallModifiedCode + $2A
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
Well, we can clearly see the 6 nops we placed in our code. The two lines above are the assembler code of the if statement. The first line compares a value (as we know from the pascal code this has to be the boolean value of b) with $00, the hexadecimal notation of 0, that in the case of a boolean variable means false.
The second line starts with jnz, what means "jump if not equal" (technically, "jump if not zero") and the address to jump to if the compared values from line one are not equal. So, the first two lines mean: "Compare the value of variable b with 0 (false) and if they are not equal jump away."
Note the hexadecimal values to the left of the asm code above. Each assembler instruction has a unique hexadecimal identifier. Obviously, $90 means "nop". $75 means "jnz", which is followed by the address (relative to the current address) to jump to ($0D in this case). $80 means "cmp" followed by some hexadecimal data specifying what and how it it compared. This hexadecimal representation of the assembler instructions is what makes the exe. If you have a hex editor, load the compiled exe and try to search for "909090909090". You will quickly find it and you will notice that the values before will be identical with the values above.
So, coming back to our task, if we want to insert "NOT" into our if statement, we will need to replace "jnz" with "jz". "jz" means "jump if zero" or "jump if equal". Replacing "jnz" with "jz" will reverse the condition in the original if statement, so once this modification is done the jump will not be done and the line c:=clRed; will be executed and the form will get red. As I said, "jnz" is represented by the hexadecimal value $75. The hexadecimal value for "jz" is $74.
Let's summarize what we have to do to change "if b then goto 1;" to "if NOT(b) then goto 1;": Locate $909090909090 in memory. From this position, go back two bytes and replace $75 with $74. If we want to go back to the original code, we do the same, but replace $74 with $75.
This is what is done in procedure TForm1.ModifyCode. I'll not go into further details here, but the source has lots of comments. You can download the sample code for this article by clicking here. After calling ModifyCode by clicking one of the two buttons on the right, click the "Execute code" button again and open the CPU view in Delphi to see that $75 was actually replaced with $74 or vice versa.
Epilog
There are easier ways to set the color of a form depending on which button was clicked ;-), but of course the purpose here is to demonstrate the concept of self-modifying code. Self-modifying code is a powerful technique and the example code might be very useful to implement a piracy protection scheme.
Finally, a small warning: You should take care when using a series of assembler nop's as a marker in real world applications, as these kind of unused code sections can be a nest for some viruses, e.g. the
W95/CIH1003 virus.