C语言逆向分析语法超详细分析

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

C语言逆向分析语法超详细分析

ch132   2022-11-30 我要评论

基本数据类型

在c中基本数据类型分为:char,short,int,long,float,double

以上数据类型除float和double外均可以分为有符号(singed)和无符号(unsigned)两类

有符号时最高位为符号位,用来表示数据的正负

无符号情况下最高位为正常的数据位不做特殊含义

类型占位
Char1
short2
int4
long8
float4
Double8

浮点数类型的存储

浮点数类型是比较特殊的,首先他是交给专门的cpu来处理的,比如在80386中就引入了8087协处理器来专门处理浮点数的计算

C中的浮点数存储方式采用了浮点实数存储方式,也就是在全部二进制位上选取一段用来表示实数另一段表示小数点的位置,如952.7可以分为9527和0.1

C的浮点数的编码采用的是ieee标准编码格式,

如float类型下将浮点数分为三部分:符号位(1bit)、小数位(8bit)、实数位(23bit)

double:符号位(1bit)、小数位(11bit)、实数位(52bit)

举例:12.25f拆分:符号位:0

​ 小数位:1000 0010

​ 实数位:10001 后续均为0

字符类型的存储

字符类型是根据字符的编码格式将对应字符的数字表示存储为二进制。

具体的字符编码解析可以划到底部

指针和引用类型

在C中用指针类型(TYPE*)来表示一个用来存储一个地址的DWORD类型,用&符号来表示取一个变量的地址

如:int* a;此时a则会被认为是一个指针类型,在对a进行操作时则会被编译器编译为汇编中的间接操作

举例:

int tmp = 10;
int* a = &tmp;
(*a)+=1;
对应的汇编简单来写如下:
mov dword ptr [esp],0Ah;           将10存在栈中
lea eax,[esp]						取得tmp所在的地址
mov dword ptr [esp-4],eax					将tmp所在的地址存储到栈中
mov ecx,dword ptr [esp-4]					取出tmp所在的地址
mov ecx,dword ptr [eax]
add dword ptr ecx,1								将tmp所在地址所指向的内容加一
mov dword ptr[eax],ecx

在c中以引用类型(type&)来表示一个操作的集合,每次对这个引用类型的操作都是取变量的内容将内容作为地址修改此地址中的数据并写回的一个操作的集合

举例:

int tmp = 10;
int& a = &tmp;
a+=1;
对应的汇编简单来写如下:
mov dword ptr [esp],0Ah;           将10存在栈中
lea eax,[esp]						取得tmp所在的地址
mov dword ptr [esp-4],eax					将tmp所在的地址存储到栈中
mov ecx,dword ptr [esp-4]					取出tmp所在的地址
mov ecx,dword ptr [eax]
add dword ptr ecx,1								将tmp所在地址所指向的内容加一
mov dword ptr[eax],ecx

可以看到引用类型和指针类型操作编译为汇编其实是基本一样的,区别就在于指针类型变量所存储的地址也可以进行算术运算

举例:

int tmp = 10;
int* a = &tmp;
a++;
对应的汇编简单来写如下:
mov dword ptr [esp],0Ah;           将10存在栈中
lea eax,[esp]						取得tmp所在的地址
mov dword ptr [esp-4],eax					将tmp所在的地址存储到栈中
mov eax,dword ptr [esp-4]		
add eax,4												此时加的不在是1而是当前指针所表示类型的大小
mov dowrd ptr [esp-4],eax

常量数据类型

常量类型表示在程序运行前便已久可以确认的数据,一般存储在只读数据区,这块内存在页的属性上便是不可写只可读,所以对这段内存的写操作都会抛出内存访问异常。

常量举例:如define所定义的常量,或者char* str = “ABC”;这种方式所定义的字符串。

注意const修饰符所修饰的变量并不意味着是在内存层面上的常量,他仅仅是编译器会在编译过程中进行检测,在程序运行中完全可以通过取地址并修改的间接修改方式对其内存数据进行修改。

函数

在内存的识图中并没有函数这一个说法只存在段的层级每个段都有自己的内存属性可读可写可执行等待,函数的目的便是能够将某一段内存明确的用一种概念来分开,而不至于将全部的代码片段都混杂在一段内存中而没有明确的一个分界和定义。

函数简单的来看便是将一块代码封装到一起。下面直接反汇编一个函数的调用看一下

首先要说明的是ebp代表了栈底指针,esp代表了栈顶指针

c代码

int test(int a,int b){
	return a+b;
}
int _tmain(int argc, _TCHAR* argv[])
{
	int a=10,b=1;
	int res = test(a,b);
	printf("%d",res);
	return 0;
}
简单汇编代码:
int test(int a,int b){
009D1A50  push        ebp  								//同样是保存和初始化堆栈
009D1A51  mov         ebp,esp 
009D1A53  sub         esp,0C0h 
009D1A59  push        ebx  
009D1A5A  push        esi  
009D1A5B  push        edi  
009D1A5C  lea         edi,[ebp-0C0h] 			
009D1A62  mov         ecx,30h 
009D1A67  mov         eax,0CCCCCCCCh 
009D1A6C  rep stos    dword ptr es:[edi] 
	return a+b;
009D1A6E  mov         eax,dword ptr [a] 		//取出将a,b做合
009D1A71  add         eax,dword ptr [b] 		//此处的a是ebp+4h,b是ebp+8h
}
009D1A74  pop         edi  
009D1A75  pop         esi  
009D1A76  pop         ebx  
009D1A77  mov         esp,ebp 
009D1A79  pop         ebp  									//回退堆栈
009D1A7A  ret     													//返回
int _tmain(int argc, _TCHAR* argv[])
{
009D1AF0  push        ebp  								//保存ebp
009D1AF1  mov         ebp,esp 						//将栈底指向当前栈顶
009D1AF3  sub         esp,0E4h 						//提升堆栈
009D1AF9  push        ebx  								//保存寄存器
009D1AFA  push        esi  
009D1AFB  push        edi  
009D1AFC  lea         edi,[ebp-0E4h] 			//初始化堆栈内容
009D1B02  mov         ecx,39h 
009D1B07  mov         eax,0CCCCCCCCh 
009D1B0C  rep stos    dword ptr es:[edi] 
	int a=10,b=1;														//这里开始进入我们在main中写的代码
009D1B0E  mov         dword ptr [a],0Ah 	//a其实是ebp-4h,这里将10存入到ebp-4,也就是栈底的第																							一个4字节内存
009D1B15  mov         dword ptr [b],1 		//这里同上b是ebp-8h,将1放入栈底开始的第二个4字节中
	int res = test(a,b);										//下面要注意,下面压栈是从esp开始压栈,前面的通过ebp																					所操作的赋值语句是将内容存放到开始提升堆栈所占有的内存
009D1B1C  mov         eax,dword ptr [b] 	//这里是取出1到eax
009D1B1F  push        eax  								//将eax压栈
009D1B20  mov         ecx,dword ptr [a] 	//取出10到ecx
009D1B23  push        ecx  								//ecx压栈
009D1B24  call        func (9D126Ch) 			//调用我们的test方法此时可以看做一个																														jmp详细的后续再讲
009D1B29  add         esp,8 							//平衡传入参数时提升的堆栈
009D1B2C  mov         dword ptr [res],eax //eax便是返回值
	printf("%d",res);
009D1B2F  mov         esi,esp 
009D1B31  mov         eax,dword ptr [res] 
009D1B34  push        eax  
009D1B35  push        offset string "%d" (9D774Ch) 
009D1B3A  call        dword ptr [__imp__printf (9DA40Ch)] 
009D1B40  add         esp,8 
009D1B43  cmp         esi,esp 
009D1B45  call        @ILT+435(__RTC_CheckEsp) (9D11B8h) 
	return 0;
009D1B4A  xor         eax,eax 
}

从上面的例子可见函数的调用便是从代码段中的一块跳转到另一块去执行,在执行结束后再返回,

函数的参数是通过栈来传递的,在函数结束后要重新保证栈回退到调用函数之前的状态。

其次call命令可以分为两个部分

  • 压入当前地址作为函数调用结束后回退时用
  • jmp到对应的位置(如果是跨段调用则是jmp far)

函数调用的约定分为三类

  • stdcall:标准的winapi调用约定平栈操作交给函数自行处理,通过ret arg来实现
  • cdecl:c语言调用约定,平栈操作交给调用方实现,也就是上面例子中的调用
  • fastcall:参数通过寄存器传递,如eax,ebx

结构体和类

结构体就是将一系列数据整合到一起的一块内存,下面通过例子来看一下

struct test_struct{
	int a;
	char b;
	int c;
};
int _tmain(int argc, _TCHAR* argv[])
{
	struct test_struct s;
	s.a = 10;
	s.b = 11;
	s.c = 12;
	test(&s);
	return 0;
}

首先建立了一个结构体有三个参数

先来看一下结构体在内存中的存储方式

int _tmain(int argc, _TCHAR* argv[])
{
。。。。。。。
	struct test_struct s;
	s.a = 10;
00E524DE  mov         dword ptr [s],0Ah 			//这里的s可以简单看为ebp-4
	s.b = 11;
00E524E5  mov         byte ptr [ebp-0Ch],0Bh 
	s.c = 12;
00E524E9  mov         dword ptr [ebp-8],0Ch 
	test(&s);
00E524F0  lea         eax,[s] 						//lea为取地址的指令,前面我们也遇到过
00E524F3  push        eax  								//将这个地址作为参数传递
00E524F4  call        test (0E511B8h) 
00E524F9  add         esp,4 
	return 0;
00E524FC  xor         eax,eax 
。。。。。
}

可以看出来结构体在内存中的存储方式便是将数据按顺序排放在内存中并根据字段类型的大小计算偏移量来取得对应的字段内容

如果我们直接将struct关键字改为class看看会不会出错

class test_struct{
public:
	int a;
	char b;
	int c;
};
void test(test_struct* s){
	printf("%d",s->a);
}
int _tmain(int argc, _TCHAR* argv[])
{
	test_struct s;
	s.a = 10;
	s.b = 11;
	s.c = 12;
	test(&s);
	return 0;
}

改后的代码,完全可以运行

并且如果看返汇编的话会发现汇编代码也没有变化

下面我们将函数放到class中看一下汇编是否会有变化

class test_struct{
public:
	int a;
	char b;
	int c;
	
	void test(test_struct* s){
	printf("%d",s->a);
}
};
int _tmain(int argc, _TCHAR* argv[])
{
	test_struct s;
	s.a = 10;
	s.b = 11;
	s.c = 12;
	s.test(&s);
	return 0;
}
汇编只看main这部分的代码
  int _tmain(int argc, _TCHAR* argv[])
{
。。。。。
	test_struct s;
	s.a = 10;
00D3339E  mov         dword ptr [s],0Ah 
	s.b = 11;
00D333A5  mov         byte ptr [ebp-0Ch],0Bh 
	s.c = 12;
00D333A9  mov         dword ptr [ebp-8],0Ch 
	s.test(&s);
00D333B0  lea         eax,[s] 
00D333B3  push        eax  
00D333B4  lea         ecx,[s] 
00D333B7  call        test_struct::test (0D311D6h) 
	return 0;
00D333BC  xor         eax,eax 
  。。。。。。。。
}

注意 lea ecx,[s] 这段代码,这个ecx便是所谓的this指针,通过编译器将结构体自己的地址作为参数传入函数这样就可以通过this符号访问结构体自己了。其余的部分完全没有变化,调用class的函数时也是通过地址调用的。

注意:数据在内存中的存储还取决于数据对齐,这部分的知识在我前面的笔记中有详细解析

面向对象的特性

面向对象的特性有

  • 封装
  • 继承
  • 多态

封装在上一块我们已经看过了,便是将操作数据的算法和存放数据的结构体封装到一起来调用,真正的实现通过编译器来实现。

下面说一下

继承

结构体

异常处理

字符编码

计算机中的存储是以字节为单位的,能反映的也仅仅是数字而已,为了能够用数字将文字信息反映出来人们设计出了各种字符编码表,将数字与文字对应。

1、ASCI编码

1.原始Ascii编码

原始ASCI使用1到127(0X00~0X7F)来对应常用的一些字母等文本,127到255则是扩展到一些不常用的类似于=号这种内容。

但原始ASCI所支持的字符仅仅能够反映英文国家的使用场景。

2.ASCI扩展编码

对于某些地区是无法用原始ASCI编码来反映当地的语言的,所以就有了ASCI扩展编码的这种形式。

扩展ASIC编码将不常用的127到255的(0x80~0xFF)位置采用两个数字对应一个文字的方式

如:可能128和129代表一个中,129和130代表一个国

呢么中国对应的编码就是:0x 8081 8182

例如国内常用的GBK、GB2312和台湾的big5等编码方式都是采取的此类

但是这种编码方式有一个问题,就是他占用了ASCI表的127到255的位置并且不同的地区这部分的编码均不一样,呢么国内的中文文件发到国外采用了不同的编码去读取则会出现乱码

2、Unicode编码

Unicode编码就是为了解决ASCii扩展码在不同地区的实现下解码后对应不同的文字这个问题

Unicode编码将全世界常用的符合都构建到一个表中,这个表的范围是:0x10 FF FF到0
Unicode仅仅提供了一个表,他并没有对存储做过多的要求,而下面所说的utf-8和utf-16以及现在的utf-32则是对Unicode编码存储方式的不同实现

1.unicode编码实现(utf-8)

utf-8较之utf-16在存储上更为复杂,但是所占空间是更小的,这也是网络传输大多为utf-8的格式的原因

存储规则:

如果目标符合在Unicode中为:0到00007F则会被编码为:0xxx xxxx

如果目标符合在Unicode中为:80到00007FF则会被编码为:110xx xxx 10xx xxxx

如果目标符合在Unicode中为:800到00FFFF则会被编码为:1110 xxxx 10xx xxxx 10xx xxxx

如果目标符合在Unicode中为:10000到10FFFF则会被编码为:1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx

2.unicode编码实现(utf-16)

utf-16名如其意,就是以两个byte为单位进行存储。

举例:如果说在Unicode编码中 中对应0x10 61 62

那么采用utf-16存储在文件中的byte就是 0x 0010 6162

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们