1. 程式人生 > >PC逆向之程式碼還原技術,第一講基本資料型別在記憶體中的表現形式.浮點,指標定址公式

PC逆向之程式碼還原技術,第一講基本資料型別在記憶體中的表現形式.浮點,指標定址公式

目錄

程式碼還原技術

一丶簡介程式碼還原

  • 例子一:我們很多人都學習過彙編.但是彙編的核心知識就是我能看的懂.有人拿彙編去做外掛.比如我去追偏移.看著視訊去做.然後換一個遊戲依然這樣.但是終有一天,你可能發現沒意思了.因為這些知識都是死的.比如我們想看遊戲中,這段程式碼做了什麼事情.這個時候就需要將彙編轉為高階程式碼查看了. IDA的F5外掛.一般能做到.但是很多是做不到的. 比如遊戲中.這段程式碼你找到一個物件+多少偏移是什麼什麼功能.但是會逆向的人.這段程式碼摳出來.轉為高階程式碼.一看.原來這個意思.+多少是什麼作用.另外還實現了什麼功能.這個就是核心技術了.為什麼別人的外掛功能比較多.你的比較少.其核心就在這裡.
  • 例子二:演算法逆向,如一個軟體.讓你追出註冊碼.你可能就爆破.但是如果你能把它演算法逆出來.那麼是不是第一提升了自己,第二,自己可以寫註冊機專門為這個程式生成註冊碼了.
  • 例子三: 如果你是為公司工作.可能某一天,公司需要你進行逆向.發現xx軟體的一個功能比較好.此時你需要怎麼辦.完整的根據彙編去逆向出來這個功能.並且讓公司去做出這個功能.這個也是一個很好的例子.

二丶程式碼還原中的資料型別表現形式

上面說了很多了,那麼真正的開始篇幅講解.

1.整數型別

C++中整數的基本資料型別有三種, int long short. 在 VC6.0中,int long所佔記憶體都是4位元組. short兩個位元組. 以16進製為例 int long 分別就是4個位元組. short兩個位元組. 一個位元組是8位.

2.無符號整數

在記憶體中,無符號整數是用來表示數值的.如果32位下.那麼取值範圍是 0x00000000~0xFFFFFFF 10進位制: 0~4294967295,因為無符號數,那麼最高位就是0填充.所以表示數值比較大.

3.有符號整數

有符號整數跟上面無符號整數一樣.只不過高位用來表示符號位,其餘低位表示數值.這樣有符號的整數.表示的數值就只有31位了.範圍則是 0x80000000~0x7FFFFFFF 轉為十進位制: -2147483648~ 2147483647 因為最高位是符號位,可以表示 負數. 例如 -3 在記憶體中負數都是補碼形式表示的

補碼規則: 補碼規則則是用0 - 去這個數的絕對值

例如: 0 - 3 的結果就是 -3在記憶體中的表現形式. 因為補碼高位為1,要轉為真值也是 0 - 補碼的形式. 但是一般計算機計算的話,通常都是用補碼取反+1進行獲得真值. 前邊帶上符號即可.

為什麼負數取值總比整數取值多一個值. 例如如上: -2147483648~2147483647 原因: 對於四個位元組補碼 0x80000000 代表的是-0. 但是對於0來講. 正負區分沒必要.所以0x800000000規定了就是4位元組補碼最小值了.所以這也是負數比正數多一位的原因.

4.浮點數資料型別

關於浮點數儲存.科學上有很多爭議.有很多儲存實數(小數)的方式.不過很少用了.所以我們也不再介紹了 現在是不管如何儲存.都分為 定點實數儲存 跟 浮點數實數儲存 這兩種方式

  • 定點實數儲存 定點實數儲存,就是約定整數位和小數位的長度.比如4個位元組為例,高2個位元組儲存整數.低兩個位元組儲存實數.這樣的好處是計算的效率高,缺點是儲存不靈活.比如儲存65536.5 整數部分已經儲存不了65536了.
  • 浮點實數儲存 浮點實數儲存就是用一部分二進位制位存放小數點的位置資訊,我們可以稱之為指數域其它的資料位用來儲存沒有小數點時的資料和符號,我們可以稱之為資料域丶符號域 如: 67.625 我們可以使用浮點實數儲存, 資料域 可以存放67625 小數位置可以記住位置為 10~-3次方 ,對這個數進行訪問的時候.只需要計算一下即可. 優缺點: 優點缺點跟第一種是相反的. 80286CPU之前,程式設計師常常為實數的計算,傷腦筋.最後出來了浮點協處理器.可以協助CPU計算.程式設計師計算實數的效率就大大的提高了.於是現在 浮點儲存的方式就推廣了出來 注意: 現在都是第二種方法進行儲存的.不是定點儲存方式了
  • C++中的浮點 在C++當中,有浮點數 float 以及 double用來儲存浮點數. float 4個位元組. double 8個位元組. 由於double空間大,所以精度高. 兩種資料型別在記憶體中同樣的是16進位制儲存.但是與10進位制的16進位制不同. float dobule 的16進位制比較大. 原因:浮點型別並不是將一個浮點小數直接轉為二進位制進行儲存的.而是將浮點小數轉換成二進位制,重新編碼.再進行儲存.C/C++中的浮點數是有符號的.

值得注意 浮點數轉為整數,並不是四捨五入.而是向0取整. 也就是說捨棄小數位.轉為整數. 例如: a = 3.78; int b = (int) a; 此時b的值是3. 而不是傳統意義上的 4; 因為不是四捨五入.

5.浮點編碼

  • 浮點編碼轉換. 我們上面說了,浮點數是重新進行編碼進行儲存的.所以我們只要搞明白了編碼.那麼就可以自己算出浮點數在記憶體中怎麼表示.或者反轉回來.16進位制怎麼轉換為浮點數
  • 浮點編碼採用的是 IEEE規定的編碼. float double轉換方式一樣. 都是因為表示範圍不一樣.所以編碼方式有些特別.
  • 1.浮點編碼的編碼方式 浮點編碼,會將一個浮點數轉為二進位制數.以科學計數法進行區分.分為三部分 1.符號域 2.指數域 3.尾數域 如下圖所示:

最高位是符號位,表示正負 去掉符號位往後數8位 是指數域. 最後的23位則表示尾數.

1.正數浮點轉為十六進位制表示

  • 2.浮點數轉為16進位制儲存 現在我們要把浮點數轉為十六進位制儲存在記憶體裡.轉換步驟 1.將一個浮點數轉化為二進位制

    例如:12.25 轉為2進位制 = 1100.01 整數直接轉為二進位制即可. 小數不斷 * 2 取整. 例如: 0.25 0.25 * 2 = 0.5 取整 = 0 0.5 * 2 = 1.0 取整就是1 所有12.25 轉為二進位制表示就是 1100.01

2.計算指數位 計算指數位首先移動小數點位置到符號位置除最高位為1的地方. 也就是符號位也好.不是符號位也好.移動到最高位為1的地方. 7.25 轉換之後是 0111.01 移動到最高位則是 1.1101.

1100.01 移動 1.10001 總共移動了3位.每次移動一位,指數+1 因為指數為移動了三位.所以 3 + 127(8位) = 130 轉為二進位制 10000010 這個就是指數位. 也就是上圖中所說的符號位後面數8位是指數位. 我們上邊計算的就是指數位的值. 為什麼 +127.因為可能會出現負數.十進位制127可以表示二進位制的01111111. IEEE浮點編碼規定 當指數域< 0111111的時候,就是一個負數.如果大於01111111的時候就是一個正數. 所以01111111為0. IEEE浮點編碼規定的.所以只要記住即可. 127即可. 也可以理解為指數域是8位,表示的數值是128.但IEE規定了.所以-1 指數最大值 - 1即可.

3.計算尾數位 經過上面計算我們符號是1,但是符號位基本不變.因為是正數浮點.所以符號位為0: 指數位為: 130 10000010 現在計算尾數位. 尾數位就是我們移動小數點之後的數值

1.10001 尾數位就是 10001,但是他不組23位.所以我們補0填充.

1000 1000 0000 0000 0000 000 補0之後,我們需要從左到右,按照4個位元組分開 100 0100 0000 0000 0000 0000 分開之後. 此時加上我們之前的符號位以及指數位

0100 0001 0100 0100 0000 0000 0000 0000 這是拼接好的.我們轉換為16進位制進行儲存 0x41440000 那麼在記憶體中,我們的浮點數12.25 其實就是16進位制 0x41 44 00 00 進行儲存的.

2.負數浮點轉為十六進位制表示.

負數跟上邊一樣.一樣計算指數位.也是分為以下步驟 1.轉為科學計數法. 2.移動指數位. 3.計算指數位 4.尾數位補零到23位. 5.拼接進行二進位制,並且二進位制轉為16進位制.

1.轉為科學計數法 -0.125 = 0.001 2.移動指數位 此時移動指數位是往小數點右邊移動,移動到最高位為1的地方. 0.001 =>1.0 移動了三位,計算 -3. 往右邊移動就是負數 1.0 則符號位是1代表負數. 指數位是負數.

3.計算指數位. 上面我們計算指數位是往小數點左邊移動.所以指數位去相加.現在是往右邊移動.所以相減 127-3 = 124 轉為二進位制 = 01111100 4.尾數位補零 0000 0000 0000 0000 0000 000 5.符號位 指數位 尾數位 進行拼接

1011 1110 0000 0000 0000 0000 0000 0000 (總共32位) 轉為16進位制 0xBE00 0000 所以-0.125 在記憶體中的16進位制則是 0xBE000000

3.正數浮點16進位制轉為浮點數解析

我們會轉換為16進位制那麼也要回轉換回來 1.16進位制拆分為2進位制. 2.分出符號位 指數位 尾數位 3.求指數位是負數還是整數 4.移動指數位

比如我們的12.25f. 十六進位制是x41440000 1.16進位制轉為2進位制 0100 0001 0100 0100 0000 0000 0000 0000 2.分出符號位 指數位 尾數位 0 10000010 10001000000000000000000 3.求出指數位 指數位位 10000010 > 01111111 所以可以判斷我們的小數是正數 10000010 - 01111111 = 130 - 127 = 3; 得出了我們要移動的位數 4.移動指數位 首先計算出的指數轉為2進位制 3 = 0011;

然後反過來.尾數位右移動三位.如下. 100010 >> 3 = 100.010 尾數的最高位不需要.所以補零

然後加上符號位. 符號位為0.代表是正數.所以+1 1100.010 再舉個例子 7.25 在記憶體的16進製為0x40E8 1.16進位制轉為二進位制 0100 0000 1110 1000 0000 0000 ..... 2.計算指數位,尾數需要往右移動紀委 10000001 - 01111111 = 129 - 127 = 2;得出移動2位 3.移動尾數位 因為高位為0,所以代表我們轉換的浮點數是正數.最後我們的高位要加上1才可以. 11010 ==>2位 = 11.010 符號位為0.所以高位補1 111.01 這個二進位制在轉換為10進製得出7.25

小數轉為10進位制: .01是是兩位. 分別記位 2的-1次方 2-2次方. 第一位計算: 0 * 1/2-1次方. 第二位計算: 1 * 1/2-2次方即可. 最後結果相加. 如果有三位.那麼就是 用第三位數值 * 1/2-3次方即可. 01/2 + 1 1/4 = 0.25.所以我們可以推算出是0.25

4.Double型別解析.

double型別轉換跟float一樣.只不過指數位變成了11位. 剩餘的42位表示尾數位.

2~11次方 - 1;就是用於計算的指數.也就是 1023.

三丶浮點彙編

1.浮點棧

因為有了浮點協處理器.所以浮點指令的操作有點不同.它是通過浮點暫存器來實現的. 浮點暫存器是通過棧結構來實現的.也稱作浮點棧. 由 st(0) - st(7); 其中寫st預設就是st(0) 操作任意浮點棧就需要加上序號 st(7); 值得注意的是浮點棧是迴圈棧. 也就是說st(0)出棧的資料.會放到st(7)中.這樣依次使用.

2.浮點彙編

針對協處理器.也提供的相應的彙編進行操作. 分別是 fld類指令 fst 指令. 以及 fcom fadd等指令 都是大寫

  • 壓棧指令 FLD IN 將浮點數IN 壓入浮點棧 FILD IN 將整數壓入浮點棧 mem32/64 80 FLDZ 預設壓入0,浮點棧是0
  • 出棧指令 FST OUT 將浮點棧頂(st(0))的值給OUT儲存. out可以是 mem32/64,但是不出棧 FSTP OUT 同FST out儲存值,但是會出棧. FISTP OUT 出棧,並且以整數的形式給OUT儲存.

  • 棧比較 也可以進行棧中的值比較.用來更改標誌位. FCOM IN 將IN地址的內容.與浮點棧頂比較(st(0)); FTST 比較棧頂(st(0)); 是否為空.
  • 浮點加法 FADD IN 將St(0)的資料於in做加法. 值儲存在 棧頂st(0);中. FADDP st(N),st 將st(n)棧中的資料於st(0)中的資料進行運算.浮點棧有7個.那麼N的取值就是0~7; 先執行一次出棧冬棗.然後相加結果放在 st(0)中儲存.

3.使用內聯浮點彙編實現加法

  • 浮點做加法
int main(int argc, char* argv[])
{
    float a = 11.25;
    float c = 12.35;
    float d = 13.25f;
    float b = 0.0f;
    __asm{
        fld dword ptr[ebp - 0x4];
        fld dword ptr[ebp - 0x8];
        fld dword ptr[ebp - 0xc];
        faddp st(1),st(0)
        fstp  dword ptr[ebp - 0x10];
    }
    printf("%f \r\n",b);
    
    return 0; 
}

實現結果:

  • 浮點做返回值
float GetFloatValue()
{   
    return 12.25f;

}
int main(int argc, char* argv[])
{
    int value = GetFloatValue();
    
    return 0; 
}

觀看彙編,彙編分為兩層.一層是呼叫內.一層是呼叫外. 呼叫內: 也就是GetFloatValue()函式內部.

   push        ebp
   mov         ebp,esp
   sub         esp,40h
   push        ebx
   push        esi
   push        edi
   lea         edi,[ebp-40h]
   mov         ecx,10h
   mov         eax,0CCCCCCCCh
   rep stos    dword ptr [edi]
   fld         dword ptr [[email protected]@4002c400000000000000 (00423fd0)]

主要看最後一樣. fld 記憶體的值. 其實就是把我們的浮點數轉為IEE編碼.放到記憶體中. 其實就是放到記憶體中.

外層呼叫: 就是呼叫完畢之後.

0040EB1D   call        __ftol (004010ec)
0040EB22   mov         dword ptr [ebp-4],eax

呼叫完畢之後,會使用 _ftol. 浮點數轉為整數進行轉化.下面的返回值放到我們的區域性變數中 所以以後看到這樣操作.我們就要明白. 返回值是float或者double型別.進行了轉換. _ftol內部

004010EC   push        ebp
004010ED   mov         ebp,esp
004010EF   add         esp,0F4h
;浮點異常檢查
004010F2   wait
004010F3   fnstcw      word ptr [ebp-2]
004010F6   wait
004010F7   mov         ax,word ptr [ebp-2]
004010FB   or          ah,0Ch
004010FE   mov         word ptr [ebp-4],ax
00401102   fldcw       word ptr [ebp-4]
;從str(0)中取出八個位元組放到區域性變數 ebp -och中. 所以後面是qword ptr代表8個位元組.
;將st(0);從棧中彈出.
00401105   fistp       qword ptr [ebp-0Ch]
00401108   fldcw       word ptr [ebp-2]
;下方 eax edx同用,eax儲存4位元組的整數部分. edx則儲存小數部分.
0040110B   mov         eax,dword ptr [ebp-0Ch]
0040110E   mov         edx,dword ptr [ebp-8]
平展返回.
00401111   leave
00401112   ret

內部則是進行浮點轉化.比較等等.

四丶布林型別

布林型別就是0 跟 1 表示.在記憶體中就是這樣的表示形式. 0就是 false 1就是true

地址丶指標丶引用表達形式

  • 地址 在C++中,使用地址需要使用&取地址符號. 取一個變數所在的記憶體地址.
  • 指標 指標的本質就是儲存地址的.只不過有型別一說.表示我已什麼方式儲存這個地址. 比如 char szBuff[10] = {1,2,3...}; char sz = szBuffer; 那麼sz儲存的是szBuffer的地址.只不過a按照1個位元組解釋. 比如 sz++,因為是char型別.所以地址就是+1, 如果是int型別解釋+1就是+4個位元組. 如果對其去內容 sz那麼此時的值是2.因為是char 型別解釋的地址 sz++ sz就是3
  • 引用 在C++中,建立引用 TYPE & a = szBuffer; 建立引用的時候必須給變數給初始化. 本質就是一個變數的別名.在記憶體中其實就是對地址 取內容的操作.

    1.指標的定址方式

    關於指標.我們說過有不同的表達形式. 例如 BYTE * short ... 因為指標有不同的表達形式.所以自增自減都會產生偏移計算. 例如: mov eax,byte ptr[ebp - 0xc]; mov ebx,byte ptr[ebp - 0xb]; mov ecx,byte ptr[ebp - 0xa]; .... 所以我們可以總結一條定址公式 目的地址 = 首地址 + sizeof(type) n的值.

目的地址就是我們要進行定址目的. sizeof(type) 就是你的資料型別大小 n的值就是你的偏移量. 例如一個數組: char szBuf[10] = {1,2...7,8,9,10}; 我們想要得到下標為8的位置的的值怎麼獲得. 高階程式碼: int a = szbuf[8];完了. 因為有公式,我們可以不用這樣寫. 寫成如下: 目的地址 = 首地址 + sizeof(type) *n; 套公式

szbuffer = szbuffer + sizeof(char) * 8; 此時szBuffer的地址就是指向陣列下表為8的位置.我們對其取內容即可獲取其值.

int main(int argc, char* argv[])
{
    char szBuf[10] = {1,2,3,4,5,6,7,8,9,10};
    char *dst = szBuf + sizeof(char) * 8;
    printf("Value = %d\r\n",*dst);
    system("pause");
    
    return 0; 
}