学习笔记(逆向汇编)Day11-Day15

来源:互联网 发布:弟子知罪,愿受师父责罚 编辑:程序博客网 时间:2024/06/06 00:09

Day 11  《存储与判断语句》

(一)字符存储

1.字符存储:

2.ASCII码:
ASCII码使用指定的7位或8位二进制数组合来表示128或256种可能;标准的只有7位;
扩展ASCII码:
由于标准ASCII码字符集字符数目有限,在实际应用中往往无法满足要求。为此,国际标准化组织又将ASCII码字符集扩充为8位扩展ASCII码的扩充。这样,ASCII码的字符集可以扩充128个字符,也就是实用8位扩展ASCII码能为256个字符提供编码,这些扩充字符的编码均为高位1的8位代码(即十进制数128~255),称为扩展ASCII码。扩展ASCII码所增加的字符包括加框文字、圆圈和其他图形符号;

3.GB2312:
GB2312是用两个ASCII码表示,即16位;

(二)内存

1.内存图:
内存图

所谓的可读可写限制是对正向开发人员而言的。我们的源代码都在代码区中,数值保存在对应的区,比如int x = 2;这个代码放在代码区中,但是x对应的数值存储在全局变量区;

2.全局变量的特点:
 1. 全局变量在程序编译完成后地址就已经确定下来了,只要程序启动,全局变量就已经存在;是否有值取决于声明时是否给定了初始值,如果没有,默认为0;
 2. 全局变量的值可以被所有函数所修改,里面存储的是最后一次修改的值;
 3. 全局变量所占内存会一直存在,直到整个进程结束;
 4. 全局变量的反汇编识别:
    mov 寄存器,byte/word/dword ptr ds:[0x12345678]
    通过寄存器的宽度,或者byte/word/dword来判断全局变量的宽度
   全局变量就是所谓的基址;
  
注意:函数名也是全局变量;

3.局部变量的特点:
 1. 局部变量在程序编译完成后并没有分配固定的地址;
 2. 在所属的方法没有被调用时,局部变量并不会分配内存地址,只有当所属的程序被调用时,才会在堆栈中分配内存;
 3. 当局部变量所属的方法执行完毕后,局部变量所占用的内存将变成垃圾数据,局部变量消除;
 4. 局部变量只能再方法内部使用,函数A无法使用函数B的局部变量;
 5. 局部变量的反汇编识别:
    [ebp-4]这个是局部变量,即ebp-多少,
    但是这个有时候也不是绝对的,有的编译器使用ESP+多少之类的;

4.判断函数的参数个数:
步骤一:

步骤二:参数的传递未必都是通过堆栈,还能使用寄存器

当函数调用处的代码无法查看:
步骤一:查看函数内用于赋值的寄存器:eax、ecx、edx、ebx、esi、edi

不考虑ebp、esp,找到用于赋值的寄存器后,查看其来源
如果该寄存器的值不是再函数内存赋值的,那一定是传进来的参数;

步骤二:
观察ebx-x之类的代码

查看ret返回时的操作数

寄存器 + ret x = 参数个数
寄存器 + [ebp + x] + … = 参数个数;

5.函数内部功能分析:
1. 分析参数:
2. 分析局部变量
3. 分析全局变量
4. 功能分析

练习:

(三)if语句的反汇编判断

1.执行各类影响标志位的指令:jxx xxxx

mov  eax,dword ptr [ebp+8]cmp  eax,dword ptr [ebp+0Ch]  jle  00401059分析:cmp指令 影响标志位jle: 小于或等于就跳转到00401059mov  eax,dword ptr [ebp+8]cmp  eax,dword ptr [ebp+0Ch]jl   00401059分析:cmp指令 影响标志位jl:  小于则跳转  



2017/5/20 23:39:08

Day 12  《反汇编C语法结构》

(一)练习

1.从堆栈的角度理解这个程序为什么可以执行:

答案:

2.分析此循环为什么永远无法停止:

答案:

(二)MOVSX 和 MOVZX

MOVSX (move with sign-extend) 符号位扩展
适用于有符号整数,将源操作数内容复到目标操作数,用较小操作数的最高位填充所有扩展位(根据符号位使用0或1扩展)
MOVZX (move with zero-extend) 零扩展传送
适用于无符号整数,将源操作数内容复制到目标操作数,用0扩展到16位/32 位,适用于无符号整数

(三)反汇编C基本语法

1.++/–

2.数组
数组是按顺序存于栈中的;

3.循环语句
执行效率如图,由上至下递减;

(四)练习

#include<stdio.h>int Found(int arr[], int count){   int i = 0;   int _max = arr[0];   do    {      if (arr[i] < arr[i + 1])      {         _max = arr[i + 1];      }      ++i;   } while (i<count-1);   return _max;}int Sum(int arr[], int count){   int i = 0;   int _sum=0;   do    {      _sum += arr[i];      ++i;   } while (i<count);   return _sum;}int Prime(int x){   int i, k;   k = (int)sqrt(x);   for (i = 2; i <= k; ++i)   {      if(x%i==0)           break;   }   return i == k;}int main(){   //1.将两个变量的值交换     int i = 5;   int j = 4;   int temp = i;   i = j;   j = temp;   //2.将一个数组中的数倒序输出     i = 4;   int arr[5] = { 1,2,3,4,5 };   do    {      printf("%d ", arr[i]);      --i;   } while (i>=0);   //3.找出数组里面最大的值,并返回     int _max = Found(arr, 5);   //4.将数组所有的元素相加,将结果返回   int _sum = Sum(arr, 5);   //5.将两个等长数组相同位置的值相加,存储到另外一个等长的数组中   i = 4;   int arr2[5] = { 9,8,7,6,5 };   int arr3[5];   do    {      arr3[i] = arr[i] + arr2[i];      ++i;   } while (i<=4);   //6.写一个函数int prime(int x),如果x是素数返回值为1,否则为0.     int j = Prime(9);   //7.俩俩比较数组的值,获取最大的一个值。   int _max = 0;   for (i = 0; i < 5; i++)   {      if (arr[i] > arr2[i])      {         j = arr[i];      }      else      {         j = arr2[i];      }      if (_max < j)         _max = j;   }}  


2017/5/23 17:50:04

Day 13  《函数缓冲与变量内存》

(一)练习

1.long long类型在C中如何存储

2.char arr[3]={1,2,3} 与 char arr[4]={1,2,3,4}哪个更省空间?

3.找出下面赋值过程的反汇编代码

void Funcition(){    int x = 1;    int y = 2;    int r;    int arr[10] = {1,2,3,4,5,6,7,8,9,10};    r = arr[1];    r = arr[x];    r = arr[x+y];    r = arr[x*2+y];}

(二)函数内缓冲

1.函数调用传入实参换成char或short,会节省空间吗?

2.参数和局部变量的区别

参数在函数调用的时候分配空间,位于函数栈的ebp下;局部变量是在函数执行的时候分配空间,位于函数栈的ebp上面(即函数缓冲区内);
小于32位的局部变量,在空间分配时,按照32位的数据规格来分配,根据编译器的不同,可能每增加一个4字节及以下的变量会增加8个字节,有的会增加12(Ch)个字节;使用时按实际的宽度使用。

3.数组

4.数组的赋值

#include<stdio.h>int Add(int x, int y){   return x + y;}int main(){   int x = 1;   int y = 2;   int r;   int arr[5] = { 1,2,3,4,5 };   r = arr[1];   r = arr[x];   r = arr[x + y];   r = arr[x * 2 + y];   r = arr[arr[1] + arr[2]];   r = arr[Add(1, 2)];   r = arr[100];   return 0;}

反汇编结果:

每条赋值语句都能够执行;
数组下标不但可以越界,而且可以做到一些不可思议的事情。堆栈图必须过硬。

5.二维数组
其内存的存储上,完全一样,并无区别。

但其取值上有区别:

6.缺省一维数组个数的二维数组定义


2017/5/24 17:49:57

Day 14  《结构体》

(一)结构体的存储与传递

1.反汇编结构体

查看反汇编,发现结构体和数组的存储几乎一样,难以区分;
数组和游戏中的地图有关,结构体和游戏中的角色有关;
所以我们逆向的时候,只需要实现相同的功能就行;

2.结构体作为参数时的传递方式

这里没有使用push的堆栈方式,而是直接提升堆栈,在堆栈中使用变址寄存器完成结构体数据的复制,或者直接movs数据以传递参数;

3.结构体作为返回值

注意:
开发中,最好不要使用结构体当参数,或结构体当返回值,这样将大量的浪费空间;

(二)结构体的内存对齐

1.#pragma pack的基本用法

#pragma pack(n)struct MyStruct{};#pragma pack()

n为字节对齐数,其取值为1,2,4,默认是8;

如果这个n值比结构体成员的sizeof值小,那么该成员的偏移量应该以此n值为准,即是说,结构体成员的偏移量应该取二者的最小值;

2.深入结构体内存对齐

对齐原则:
1. 结构的数据成员,第一个成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的证书倍开始(比如int再32位机为4字节,则要从4的整数倍地址开始存储);
2. 结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐;
3. 如果一个结构里有某些结构体成员,则结构成员要从其内部最大元素大小的整数倍地址开始存储(比如struct a里有struct b,b里有char int double等元素,那b应该从8的整数倍开始存储);
4. 对齐参数如果比结构体成员的sizeof值小,该成员的偏移量应该以此值为准,也就是说,结构体成员的偏移量应该取二者的最小值;

建议:按照数据类型由小到大的顺序进行书写;

3.练习
1.分析结构体大小

struct S1{   char c;   double i;};struct S2{   char c1;   struct S1 s;   char c2;   char c3;};struct S3 {   char c1;   struct S1 s;   char c2;   double i;};struct S4{   int c1;   char c2[10];};

答案:

int main(){   //8+8=16字节   int i = sizeof(struct S1);   struct S1 s = { 1,1 };   //8+16+8=32字节(根据S1里成员最大的数据类型为double,倍数为8)   int j = sizeof(struct S2);   //8+16+8+8=40字节   int k = sizeof(struct S3);   //8+8=16字节(此处是先填满前8个字节再填后面的)   int l = sizeof(struct S4);   return 0;}

2.递归函数反汇编分析

#include<stdio.h>int test(int i){      if (i < 1)      return 1;   int k = test(--i);   return k;}int main(){   int i = test(3);   return 0;}

main中调用test(3)后,传入3,在第一次test中,i 不小于 1,则调用函数test(2),同样,再调用test(1),第3次test中,i=1,i不小于1,同样,调用test(0),则此第4次,0<1,则返回1;第3次中,k=1,返回k;在第2次中,k=1,返回k;在第1次中,k=1,返回k;在main中,i = 1;


2017/5/25 15:33:21

Day 15  《逆向switch》

(一)switch

1.switch的反汇编

switch 在游戏中和技能配合使用最多, 比如 F 加血等等
上方为什么要减去1再比较? (图中已说明)
Switch 会建立一个大的表,把我们要执行的语句对应的地址都保存起来。

2.switch的地址表
情况一:3个分支

情况二:4个分支

情况三:分支常量值顺序打乱

当case分支少于4个时,并不会生成case分支地址表,因为编译器会生成类似if…else之类的反汇编;
case后面的常量可以是无序的,并不会影响表的生成;且其表内的元素排列会按照case分支的值从小到大排列,表是有序的;

3.switch的case分支语句地址的寻址方式
1. 情况一:

2. 情况二:

从上面两种情况来看,在编译器的处理中,如果switch的case常量之间差值太大的话,会采取情况二的方法(大表+小表)。如果差值不大的话,会采取情况一(大表)的方式去处理;
情况一:差值之间的数据统一都使用default语句的EIP地址填充;
情况二:该方法有利于节省空间;

(二)switch练习

1.写一个switch语句,不产生大表与小表
2.写一个switch语句,只产生大表
3.写一个switch语句,产生大表与小表

#include<stdio.h>char Fun1(char c){    char j = 0;    switch (c)    {    case 'a':        j = 'a';        break;    case 'b':        j = 'b';        break;    case 'c':        j = 'c';        break;    default:        break;    }    return j;}char Fun2(char c) {    char j = 0;    switch (c)    {    case 'a':        j = 'a';        break;    case 'b':        j = 'b';        break;    case 'c':        j = 'c';        break;    case 'd':        j = 'd';        break;    case 'e':        j = 'e';        break;    case 'f':        j = 'f';        break;    case 'g':        j = 'g';        break;    case 'h':        j = 'h';        break;    case 'n':        j = 'n';        break;    default:        break;    }}char Fun3(char c){    char j = 0;    switch (c)    {    case 'a':        j = 'a';        break;    case 'b':        j = 'b';        break;    case 'c':        j = 'c';        break;    case 'n':        j = 'n';        break;    default:        break;    }    return j;}int main(){    int i = Fun1('n');    int j = Fun2('n');    int k = Fun3('n');    return 0;}

(三)分析循环语句


2017/5/26 12:44:11

Day 16  《标题》

(一)小节


原创粉丝点击