首页 > 其他分享 >C语言逆向——指针

C语言逆向——指针

时间:2023-04-03 23:25:25浏览次数:34  
标签:逆向 arr int px C语言 char 数组 指针

指针类型

在C语言里面指针是一种数据类型,是给编译看的,也就是说指针与int、char、数组、结构体是平级的,都是一个类型。

"*"号的变量我们称之为指针类型,例如:

char* x; short* y; int* a; float* b; ...

任何类型都可以带这个符号,格式就是:类型* 名称;星号可以是多个

指针变量的赋值格式如下:

images/download/attachments/12714021/image2021-3-11_17-46-21.png

指针类型的变量宽度永远是4字节,无论类型是什么,无论有几个星号。

指针类型和其他数据类型是一样的,也可以做自增、自减,但与其他数据类型不一样的是,指针类型自增、自减是加、减去掉一个星号后的宽度

如下代码:

#include <stdio.h>   int main() { int** a; char** b;   a = (int**)1; b = (char**)2;   a++; b++;   printf("%d %d \n", a, b);   return 0; }

第一次自增,a+4 = 5,b+4 = 6,因为减去一个星号还是指针,指针的宽度永远是4:

images/download/attachments/12714021/image2021-3-11_18-6-47.png

自减同理可得,那么我们知道了自增、自减就可以知道指针类型的加减运算规律:

因为自增、自减,本质上就是+1,所以我们可以得出算式:

指针类型变量 + N = 指针类型变量 + N * (去掉一个*后类型的宽度)

指针类型变量 - N = 指针类型变量 - N * (去掉一个*后类型的宽度)

但需要注意,指针类型无法做乘除运算。

最后:指针类型是可以做比较的。

images/download/attachments/12714021/image2021-3-11_18-19-53.png

&的使用

&符号是取地址符,任何变量都可以使用&来获取地址,但不能用在常量上。

如下代码:

#include <stdio.h>   struct Point { int x; int y; };   char a; short b; int c; Point p;   int main() { printf("%x %x %x %x \n", &a, &b, &c, &p); return 0; }

images/download/attachments/12714021/image2021-3-11_20-20-12.png

在这里使用取地址符可以直接获取每个变量、结构体的地址,但是这种格式可能跟我们之前看到的8位不一样,前面少了2个0,这时候可以将%x占位符替换为%p来打印显示。

images/download/attachments/12714021/image2021-3-11_20-28-26.png

那么取地址符(&变量)的类型是什么呢?我们可以来探测下:

char a; short* b; int** c;   int x = &a; int x = &b; int x = &c;

以上代码我们在编译的时候是没法成功的,它的报错内容是:

images/download/attachments/12714021/image2021-3-11_20-32-32.png

通过报错内容我们可以看出类型不同无法转换,但是我仔细观察报错内容:char*无法转换成int,short**无法转换成int...那么就说明了一点,在我们使用取地址符时,变量会在其原本的数据类型后加一个星号

可以这样进行指针赋值:

char x; char* p1; char** p2; char*** p3; char**** p4;   p1 = &x; // &x -> char* p2 = &p1; p3 = &p2; p4 = &p3;   p1 = (char*)p4;

取值运算符

取值运算符就是我们之前所了解的“*”星号,“*”星号有这几个用途:

  1. 乘法运算符(1*2)

  2. 定义新的类型(char*)

  3. 取值运算符(星号+指针类型的变量);也就是取地址的存储的值。

如下代码就是使用取值运算符:

int* a = (int*)1; printf("%x \n", *a);

这段代码可以编译,但是是无法运行的,我们可以运行一下然后来看看反汇编代码:

images/download/attachments/12714021/image2021-3-11_20-54-42.png

images/download/attachments/12714021/image2021-3-11_20-55-56.png

如上反汇编代码,我们可以清楚的看见首先0x1给了局部变量(ebp-4),之后这个局部变量(ebp-4)给了eax,而后eax又作为了内存地址去寻找对应存储的值,但是这里eax为0x1,所以在内存中根本就不存在这个地址,也就没办法找到对应的值,自然就无法运行。

那么取值运算符(星号+指针类型)是什么类型呢?我们来探测下:

int*** a; int**** b; int***** c; int* d;   int x = *(a); int x = *(b); int x = *(c); int x = *(d);

以上代码我们在编译的时候是没法成功的,它的报错内容是:

images/download/attachments/12714021/image2021-3-11_21-11-11.png

通过报错内容我们可以看出类型不同无法转换,但是我仔细观察报错内容:int**无法转换成int,int***无法转换成int...那么就说明了一点,在我们使用取值运算符时,变量会在其原本的数据类型后减去一个星号

取值运算符举例

int x = 1; int* p = &x; int** p2 = &p; *(p) = 2; int*** p3 = &p2; int r = *(*(*(p3)));

数组参数传递

之前我们学过几本类型的参数传递,如下代码所示:

#include <stdio.h>   void plus(int p) { p = p + 1; }   int main() { int x = 1; plus(x); printf("%d \n", x); return 0; }

如上代码变量x最终值是多少?相信很多人都知道答案了,是1,原封不动。

images/download/attachments/12714021/image2021-3-11_21-26-28.png

为什么是1?这是因为在变量x作为参数传入plus函数,是传入值而不是这个变量本身,所以并不会改变变量本身。

数组也是可以作为参数传递,我们想要传入一个数组,然后打印数组的值(定义数组参数时方括号中不要写入常量):

#include <stdio.h>   void printArray(int arr[], int aLength) { for (int i=0; i<aLength; i++) { printf("%d \n", arr[i]); } }   int main() { int arr[] = {0,1,2,3,4}; printArray(arr, 5); return 0; }

我们想打印数组不仅要知道数组是什么,也要获取数组的长度,所以需要两个参数(实际上我们也有其他方法获取长度,这里先不多说)。

images/download/attachments/12714021/image2021-3-11_21-34-18.png

我们来看下反汇编代码,看看数组是否和基本类型一样传入的是值:

images/download/attachments/12714021/image2021-3-11_21-36-47.png

通过如上代码所示,我们可以很清晰的看见,ebp-14也就是数组的第一个值的地址给了eax,最后eax压入堆栈,也就是传递给了函数。

所以我们得出结论:数组参数传递时,传入的是数组第一个值的地址,而不是值;换而言之,我们在printArray函数中修改传入的数组,也就修改了数组本身

我们再换个思路,在数组作为参数传递的时候,可以换一种形式,直接传入地址也是可以打印的,也就是使用指针来操作数组:

images/download/attachments/12714021/image2021-3-11_21-42-23.png

事实证明这是可行的,我们在传入参数的时候使用数组第一个值的地址即可。

指针与字符串

在学习完指针类型后,我们可以来了解一下这些函数:

int strlen(char* s); // 返回类型是字符串s的长度,不包含结束符号\0 char* strcpy(char* dest, char* src); // 复制字符串src到dest中,返回指针为dest的值 char* strcat(char* dest, char* src); // 将字符串src添加到dest尾部,返回指针为dest的值 int strcmp(char* s1, char* s2); // 比较s1和s2,一样则返回0,不一样返回非0

字符串的几种表现形式:

char str[6] = {'A','B','C','D','E','F'}; char str[] = "ABCDE"; char* str = "ABCDE";

指针函数(本质就是函数,只不过函数的返回类型是某一类型的指针):

char* strcpy(char* dest, char* src); char* strcat(char* dest, char* src);

指针取值的两种方式

如下图所示的则是一级指针(一个星号)和多级指针(多个星号):

images/download/attachments/12714021/image2021-3-11_21-53-1.png

这段代码看着很复杂,但我们有基础后再看它轻而易举,脑子里浮现的就是汇编代码。

指针取值有两种方式,如下代码:

#include <stdio.h>     int main() { int* p = (int*)1; printf("%d %d", *(p), p[0]); return 0; }

我们可以使用取值运算符,也可以使用数组的方式,因为其本质都是一样的,我们来看下反汇编代码:

images/download/attachments/12714021/image2021-3-11_21-58-55.png

也就说明:*()与[]的互换,如下是互换的一些例子:

int* p = (int*)1; printf("%d %d \n",p[0],*p); //p[0] = *(p+0) = *p   int** p = (int**)1; printf("%d %d \n",p[0][0],**p); printf("%d %d \n",p[1][2],*(*(p+1)+2));   int*** p = (int***)1; printf("%d %d \n",p[1][2][3],*(*(*(p+1)+2)+3));   /* *(*(*(*(*(*(*(p7)))))))) = *(*(*(*(*(*(p7+0)+0)+0)+0)+0)+0) = p7[0][0][0][0][0][0][0] */

总结:

*(p+i) = p[i] *(*(p+i)+k) = p[i][k] *(*(*(p+i)+k)+m) = p[i][k][m] *(*(*(*(*(p+i)+k)+m)+w)+t) = p[i][k][m][w][t]

结构体指针

我们来了解一下结构体指针,如下代码:

#include <stdio.h>   struct Point { int a; int b; };   int main() { Point p;   Point* px = &p;   printf("%d \n", sizeof(px));   return 0; }

我们打印结构体指针的宽度,最终结果是4,这时候我们需要知道不论你是什么类型的指针,其特性就是我们之前说的指针的特性,并不会改变。

如下代码就是使用结构体指针:

// 创建结构体 Point p; p.x=10; p.y=20;   // 声明结构体指针 Point* ps;   // 为结构体指针赋值 ps = &p;   // 通过指针读取数据 printf("%d \n",ps->x);   // 通过指针修改数据 ps->y=100;   printf("%d\n",ps->y);

提问:结构体指针一定要指向结构体吗?如下代码就是最好的解释:

#include <stdio.h>   struct Point { int x; int y; };   int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10};   Point* p = (Point*)arr;   int x = p->x; int y = p->y;   printf("%d %d \n", x, y); return 0; }

images/download/attachments/12714021/image2021-3-11_22-17-45.png

指针数组与数组指针

指针数组和数组指针,这两个是完全不一样的东西,指针数组的定义:

char* arr[10];   Point* arr[10];   int********** arr[10];

指针数组的赋值方式:

char* a = "Hello"; char* b = "World";   // one char* arr[2] = {a, b};   // two char* arr1[2]; arr1[0] = a; arr1[1] = b;   // three char* arr2[2] = {"Hello", "World"};

一共有三种赋值方式,在实际应用中我们更偏向于第三种方式。

结构体指针也有数组,我们可以看下其定义和对应宽度:

images/download/attachments/12714021/image2021-3-11_22-26-54.png

接下来我们要学习的是数组指针,数组指针在实际应用很少用到,数组指针是最难学的。

首先分析一下如下代码:

int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; int* p = &arr[0]; int* p = arr; // &arr[0] int* p = (int *)&arr; // &arr -> int *[10]

&arr就是我们要学的数组指针,也就是int *[10],数组指针的定义如下:

int(*px)[5]; // 一维数组指针 char(*px)[3]; int(*px)[2][2]; // 二维数组指针 char(*px)[3][3][3]; // 三维数组指针

px就是我们随便定义的名字,本质上就是指针,那么也就有着指针的特性,无论是长度还是加减法...

思考:int *p[5] 与 int(*p)[5]有什么区别?我们可以来看看宽度:

images/download/attachments/12714021/image2021-3-11_22-42-13.png

可以看见一个是20,一个是4;一个是指针变量的数组,一个是数组指针,本质是不一样的。

数组指针的宽度和赋值:

int(*px1)p[5]; char(*px2)[3]; int(*px3)[2][2]; char(*px4)[3][3][3];   printf("%d %d %d %d\n",sizeof(px1),sizeof(px2),sizeof(px3),sizeof(px4)); // 4 4 4 4 px1 = (int (*)[5])1; px2 = (char (*)[3])2; px3 = (int (*)[2][2])3; px4 = (char (*)[3][3][3])4;

数组指针的运算:

int(*px1)p[5]; char(*px2)[3]; int(*px3)[2][2]; char(*px4)[3][3][3];   px1 = (int (*)[5])1; px2 = (char (*)[3])1; px3 = (int (*)[2][2])1; px4 = (char (*)[3][3][3])1;   px1++; //int(4) *5 +20 =21 px2++; //char(1) *3 +3 =4 px3++; //int(4) *2 *2 +16 =17 px4++; //char(1) *3 *3 *3 +9 =10   printf("%d %d %d %d \n",px1,px2,px3,px4);

数组指针的使用:

// 第一种: int arr[] = {1,2,3,4,5,6,7,8,9,0}; int(*px)[10] = &arr; // *px 是啥类型? int[10] 数组类型 // px[0] 等价于 *px 所以 *px 也等于 int[10]数组 printf("%d %d \n",(*px)[0],px[0][0]);   px++; // 后 (*px)[0]就访问整个数组地址后的地址内的数据     // 第二种: int arr[3][3] = { {1,2,3}, {4,5,6}, {7,8,9} };   // 此时的 px指针 指向的 {1,2,3}这个数组的首地址 int(*px)[3] = &arr[0];   // *px -> 此时就是数组{1,2,3}本身   // 越过第一个数组 此时px指针指向 {4,5,6}的首地址 px++;   printf("%d %d \n",(*px)[0],px[0][0]); // 这里打印的就是 4 4

思考:二维数组指针可以访问一维数组吗?

int arr[] = {1,2,3,4,5,6,7,8,9,0}; int(*px)[2][2] = (int(*)[2][2])arr;

是可以的,因为*px实际上就是int[2][2],我们之前学过多维数组,int[2][2]也就等于int[4],所以{1,2,3,4}就给了int[2][2],也就是{{1,2}, {3,4}},所以(*px)[1][1]为4。

images/download/attachments/12714021/image2021-3-11_22-56-38.png

调用约定

函数调用约定就是告诉编译器怎么传递参数,怎么传递返回值,怎么平衡堆栈。

常见的几种调用约定:

调用约定

参数压栈顺序

平衡堆栈

__cdecl

从右至左入栈

调用者清理栈

__stdcall

从右至左入栈

自身清理堆栈

__fastcall

ECX/EDX 传送前两个,剩下:从右至左入栈

自身清理堆栈

一般情况下自带库默认使用 __stdcall,我们写的代码默认使用 __cdecl,更换调用约定就是在函数名称前面加上关键词:

int __stdcall method(int x,int y) { return x+y; }   method(1,2);

函数指针

函数指针变量定义的格式:

// 返回类型 (调用约定 *变量名)(参数列表);   int (__cdecl *pFun)(int,int);

函数指针类型变量的赋值与使用:

// 定义函数指针变量 int (__cdecl *pFun)(int,int); // 为函数指针变量赋值 pFun = (int (__cdecl *)(int,int))10;   // 使用函数指针变量 int r = pFun(1,2);

我们来看下函数指针的反汇编代码:

images/download/attachments/12714021/image2021-3-11_23-5-37.png

可以很清晰的看见函数指针生成来一堆汇编代码,传参、调用、以及如何平衡堆栈;而如上这段代码最终会调用地址0xA,但它本身不存在,所以无法运行。

所以我们想要调用某个函数时可以将地址赋值给pFun即可,并且在定义时写好对应的参数列表即可;也就是说函数指针通常用来使用别人写好的函数。

我们也通过函数指针绕过调试器断点,假设有攻击者要破解你的程序,它在MessageBox下断点,你正常的代码就会被成功断点,但是如果你使用函数指针的方式就可以绕过。

首先,我们先写一段正常的MessageBox程序,然后使用DTDebug来下断点:

images/download/attachments/12714021/image2021-3-11_23-29-48.png

可以看见,我们下断点成功断下来了,断点本质上就是在MOV EDI, EDI所在那行的地址下断点,那么我们可以直接跳过这行调用调用下一行,实际上这段汇编的核心在于我标记的部分,准确一点的说就是CALL指令哪一行,我们可以右键Follo跟进:

images/download/attachments/12714021/image2021-3-11_23-49-11.png

images/download/attachments/12714021/image2021-3-11_23-50-10.png

我们可以从0x77D5057D开始执行,通过汇编代码可以知道这个函数需要5个参数,并且最后的RETN则表示内平栈则使用__stdcall,所以我们函数指针(操作系统API返回通常是4字节)可以这样写:

#include <windows.h>   int main() { int (__stdcall *pFun)(int,int,int,int,int); pFun = (int (__stdcall *)(int,int,int,int,int))0x77D5057D;   MessageBox(0,0,0,0);   pFun(0,0,0,0,0);   return 0; }

这样就可以绕过断点了:

images/download/attachments/12714021/image2021-3-11_23-52-19.png

 

标签:逆向,arr,int,px,C语言,char,数组,指针
From: https://www.cnblogs.com/bonelee/p/17284876.html

相关文章

  • C语言逆向——预处理之宏定义、条件编译与文件包含
    预处理之宏定义、条件编译与文件包含预处理一般是指在程序源代码被转换为二进制代码之前,由预处理器对程序源代码文本进行处理,处理后的结果再由编译器进一步编译。预处理功能主要包括宏定义、文件包含、条件编译三部分。宏定义简单的宏:#define标识符字符序列#defineFALS......
  • 【数据结构】二叉树先序、中序、后序及层次遍历(C语言版)
    一、图示展示1.先序遍历先序遍历可以想象为,一个小人从一棵二叉树根节点为起点,沿着二叉树外沿,逆时针走一圈回到根节点,路上遇到的元素顺序,就是先序遍历的结果先序遍历结果为:ABDHIEJCFKG动画演示:记住小人沿着外围跑一圈(直到跑回根节点),多看几次动图便能理解2.......
  • 【UNCTF逆向】ezmaze详解
    题目ezmaze解法题目下载下来是一个ezmaze.exe文件,用exeinfo打开看一下好像还可以,用ida打开看看刚开始我甚至找不到这个界面,问了一名比较厉害的同学,他告诉我就一个个函数找找看,可能会找到可疑的内容,我就一个个找,最后锁定了这个140001490。打开是这样的反编译一下有点......
  • C语言再学习 -- 详解C++/C 面试题 2
    (经典)C语言测试:想成为嵌入式程序员应知道的0x10个基本问题。参看:嵌入式程序员面试问题集锦1、用预处理指令#define声明一个常数,用以表明1年中有多少秒(忽略闰年问题) #defineSENCONDS_PER_YEAR(60*60*24*365)UL解答:#define声明一个常量,使用计算常量表达式的值来表明一年中有多少......
  • C语言再学习 -- 输入/输出
    一、缓冲区输入字符的立即回显是非缓冲或直接输入的一个实例,它表示你说键入的字符被收集并存储在一个被成为缓冲区的临时存储区域中。按下回车可使你所键入的字符块对程序变成可用。为什么需要缓冲区?首先,将若干个字符作为一个块传输比逐个发送这些字符耗费的时间少。其次,如果你输入......
  • C语言再学习 -- 运算符与表达式
    分三部分来讲一、左值与右值参看:左值与右值首先我们需要理解左值和右值的定义:左值指的是如果一个表达式可以引用到某一个对象,并且这个对象是一块内存空间且可以被检查和存储,那么这个表达式就可以做为一个左值。      右值指的是引用了一个存储在某个内存地址里的数据。从上面......
  • IDEA Spring-boot 使用@Component注解的工具类,用@Autowired注入 @Service或者@Reposit
    IDEASpring-boot使用@Component注解的工具类,用@Autowired注入@Service或者@Repository会空指针(使用@PostContruct)原文链接:https://blog.csdn.net/ld_secret/article/details/104627597/使用idea编译器时,对于spring-boot的项目,大都使用注解,那么:一、现象:@Component标注的U......
  • C++this指针
    C++只有非静态成员变量才属于类的对象上在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上首先看一下空对象占多少空间classFoo{};intmain(){Foofoo;cout<<"sizeofis:"<<sizeof(foo)<<endl;}结果为sizeofp=1;......
  • C语言酒水批发管理系统[2023-04-03]
    C语言酒水批发管理系统[2023-04-03]编写一个C语言程序,实现一个酒水批发管理系统,至少能够管理30条进货/批发销售记录。其中:管理系统所管理物品仅包括各种不同品牌的酒水类货物货物信息主要包括:货物名称、货物编号、货物库存数、货物属性(不同包装、是否促销)等;进货记录主......
  • C语言文件操作
    一、为什么要使用文件我们在正常编写程序时,程序里的数据是存放在内存里的。当程序结束后,这些数据自然就不存在了。当下次运行程序的时候,数据又重新录入。而使用文件可以把数据存放到电脑里的硬盘里,这样数据就会一直存在,我们能够自己控制数据的保存与删除,做到了数据的持久化。二、什......