基础知识补充

一些基础知识的补充

0x00-typedef定义函数指针

函数指针

在此之前我们要先了解函数指针,即存放函数首地址的变量,而函数名就是函数的首地址,为了存放函数的首地址就需要定义函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void hello()
{
printf("this is hello!");
}
//函数指针定义
void (*fp)()=hello;//函数指针定义时应保证函数返回值与参数个数、类型相同,在这里void是返回类型,(*fp)后面的()是函数参数
/*也可以理解为
void (*fp)();
fp=hello;
*/

//通过函数指针调用函数
fp();

int add(int a,int b)
{
return a+b;
}

//函数指针定义,形参可以不写变量名
int (*fp1)(int,int)=add;
//函数调用
fp1(1,2);

typedef与函数指针混合使用

1
typedef int (*FP)(int,int);

那么我们在赋值的时候就可以改成

1
2
FP fp1=add;//fp1就是返回值为int型,参数为两个int型的函数指针,FP表示函数指针的类型,通过类型名+变量名就可以定义函数指针
fp1(1,2);

0x01-回调函数

定义

如果函数的参数具有函数指针,那么这样的函数就被称为回调函数

函数指针就是当作接口使用的

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//这里的call就是回调函数,这里传递函数指针和我们传入数组时使用函数指针是类似的,都是使用指针将首地址进行传递,而不是传整个函数
void call(void(*fp)())
{
fp();//这样调用的时候就不需要考虑函数指针的变量名,只要传递的函数是这样类型的即可在call函数中进行调用
return ;
}
//当然也可以使用typedef,让回调函数更简洁
typedef void (*fp)();
void call(fp)
{
fp();
return ;
}
void hello()
{
printf("hello");
}
void show()
{
printf("show");
}
//如果不使用回调函数,调用时需要hello();show();
int main()
{
call(show);
call(hello);//使用回调函数调用,这样只需要传入不同参数即可完成调用
return 0;
}

0x02-复杂函数

定义复杂的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void *handle(void*arg)
{
printf("arg is %X\n",arg);
return arg;//返回值是void*
}

//我们要定义一个参数为函数指针,返回值为函数指针的回调函数
//先来看参数,传入函数指针void* (*fp)(void *),另一个还要传入的是arg,这里定义为void*p
//然后再定义一个以上面两个参数为函数参数,返回值是函数指针(这里是handle函数指针)的函数
void *(*call(void *(fp)(void*),void*p))(void *)//在这里call是回调函数的变量名,void*是返回值,最后的(void*)是返回的函数指针的参数,call()里的两个是回调函数的参数
{
fp(p);
return fp;
}
int main()
{
int num=10;
void*(*fp)(void*)=call(handle,&num);//使用对应类型的函数指针接收
return 0;
}

//为了方便理解,使用typedef
typedef void*(*FP)(void*);
FP call(FP fp1,void*p)
{
fp1(p);
return fp;
}

0x03-一维数组内存

1
int arr[6]={10,20,30,40};//当有初始值时,剩余没定义的默认为0,如果没有定义初始值,则为随机值

因为数组长度定义为6,类型为int,所以在内存中分配24个字节

数组在栈中的存储

0x04-函数声明

返回值+函数名+参数

1
2
3
4
5
6
7
//函数声明
int sum(int,int);

int sum(int a,int b)
{
return a+b;
}

原因

0x05-调用约定

为什么要有不同的调用约定——是因为调用函数之后需要清理栈,而不同的调用约定对应不同的清理方式

__cdecl:调用者自己清理栈

1
2
3
//函数外部
call hello;函数
add esp,立即数

__stdcall:函数自己清理栈

1
2
//函数内部
retn 立即数

如果使用__cdcel调用方式,因为不同编译器产生的栈不同,所以不能很好地清理栈,而stdcall则可以在函数内部完成清理栈。

所以,在跨(开发)平台的调用中,我们都使用stdcall(有时是以WINAPI的样子出现)。那么为什么还需要_cdecl呢?当我们遇到这样的函数如fprintf()它的参数是可变的,不定长的,被调用者事先无法知道参数的长度,事后的清除工作也无法正常的进行,因此,这种情况我们只能使用cdecl

0x06-extern “C”

原因

也就是说如果不声明为extern “C”,我们导出的函数名会被修饰

DLL代码

导出的函数名称

可以看到我们的函数名被修饰了,在调用的时候我们无法通过GetProcAddress通过函数名调用

代码

导出的函数名称

可以看到我们的函数名没有被修饰

C++函数重载即函数名可以相等,只要该函数的参数类型或者个数不同即可

补充

标准文件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef __INCvxWorksh  /*防止该头文件被重复引用*/
#define __INCvxWorksh

#ifdef __cplusplus //__cplusplus是cpp中自定义的一个宏
extern "C" { //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
#endif

/**** some declaration or so *****/

#ifdef __cplusplus
}
#endif

#endif /* __INCvxWorksh */

extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用

0x07-HMOUDLE、HANDLE、HWND、HINSTANCE

https://www.cnblogs.com/wingsummer/p/15823780.html

这里就不看定义了,越看越晕(

HWND

HWND是线程相关的,可以通过HWND找到该窗口所属进程的句柄

HANDLE

Handle是代表系统的内核对象,如文件句柄,线程句柄,进程句柄

系统对内核对象以链表的形式进行管理,载入到内存中的内核对象都有一个线性地址,同时相对系统来说,在串列中有一个索引位置,这个索引位置就是内核对象的HANDLE

HINSTANCE

HINSTANCE的本质是模块基地址,它仅在同一进程中才有意义,跨进程的HINSTANCE是没有意义的

HMODULE

代表应用程序载入的模块,WIN32系统下通常是被载入模块的线性地址,比如exe, dll等模块等

HINSTANCE 在win32下与HMODULE是相同的东西(只有在16位windows上,二者有所不同)

0x08-命令行参数

我们知道main函数实际上是有两个参数的,但一般不会进行使用

其原型如下

1
main(int argc,char*argv[])

这两个参数实际上与命令行相关

1
2
3
//argc是一个整数,其代表了命令行参数个数
//argv是一个指针数组,可以接收多个参数,第一个也就是argv[0]指向输入的程序路径及名称
//如果在命令行中运行,argc为1,也就是只有一个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<windows.h>
#include<stdio.h>
#include<string.h>

int main(int argc,char *argv[])
{
printf("命令行参数个数为:%d\n", argc);
for (int i = 0; i < argc; ++i)
{
printf("第%d个命令行参数为:%s\n", i, argv[i]);
}
return 0;
}

0x09-有符号数和无符号数

unsigend和signed

unsigned顾名思义就是无符号数,signed是有符号数

我们以char为例,char类型存储时为8位,当声明为signed的时候(不加unsigned的时候),最高位也就是第八位被当作符号位,当最高位为0的时候为正数,1时为负数,那么后七位就是数据位

所以signed char也就是char可以表示-127-128的值(2**7)

当声明为unsigned char时,最高位也是数据位,此时可以表示0-256中的数据(2**8)

有符号数与无符号数的移位问题

逻辑移位用于无符号数
算术移位用于有符号数

逻辑移位

对于逻辑移位,就是不考虑符号位,移位的结果只是数据所有的位数进行移位,左移时低位补0,右移时高位补0

算术移位

首先我们需要知道

C在存储数字的时候都采用补码的形式,而正数的原码、反码、补码都是一样的,左右移位时直接使用原码即可,负数的需要重新计算,补码计算:除符号位外,其他取反加一

算术移位,右移时(未溢出),保持符号位不变,同时用符号位补数值最高位

-65=11000001,转为补码为10111111,>>2变成11101111,再转为原码10010001,为-17

算术移位,左移时(未溢出),直接将数据最高有效位移入符号位,最低位补0

当符号位为1时,为负数,若数据最高位为0(补码的数据最高位),那么此时左移必定溢出,正数也同理

0x0A-整型溢出与左移溢出

整型溢出

无符号数整型溢出

有符号数溢出

0x7F就是01111111,也就是127,+1之后过渡到0x80,此时为1000000,也就是负数,该值为负数的最小值也就是-128

左移溢出

左移和右移运算过程中也会发生溢出,移位位数并不是可以任意。当移位位数超过该数值类型的最大位数时,编译器会用移位位数去模该类型位数,然后按照余数进行移位。

0x0B-循环左移与循环右移

循环移位区别于一般的移位时没有数位的丢失

循环左移时,用从左边移出的位填充字的右端;循环右移时,用从右边移出的位填充字的左端

以(unsigned char)0x51(无符号)为例,二进制为01010001,循环左移三位最终应该得到10001010,所以我们要先把前三位取出放置到最后三位,把最后五位前移,最后按位或,实现代码

1
((0x51<<3)|(0x51>>5))

01010001000+

00000000010

01010001010,因为最后要截断,需要&0xFF

要注意的是有符号数的循环右移时,使用符号位填充

通用,总长度N(8,16,32),循环左移n:(a>>(N-n))|(a<<n),循环右移n:(a<<(N-n))|(a>>n)

逆运算

只需要将<<改为>>,>>改为<<即可

0x0C-ida生成数组

通过分析直到数组,然后点进去跳转到栈,右键->Array->填写最大大小,然后确定即可

生成数组前

点击数组首元素v17

填写好数组大小

生成数组后

0x0D-IDA设置中文编码

有时候逆向题会出现中文提示字符串,这时候就需要设置中文了

OPtions->General->Strings,然后选择第一个UTF-8,双击后Insert一个GBK

0x0E-IDA创建结构体

ida创建结构体

一种是添加IDA已经存在的结构体,适合于那种规范的

在结构体这一栏中右键创建即可