Delphi程序的自我修改


前言: 
    對於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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM