您的位置:首页 > 其它

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

2017-08-28 22:19 267 查看

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: 小于或等于就跳转到00401059

mov  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  《标题》

(一)小节

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: