审稿&指导:とある名前のない大佬
前言
野獣と化した先輩
说过:逸一时,误一世;忆伊时,吾衣湿;伊已失,勿抑视
(ps:先輩讲道图)
希望我们要像野獣と化した先輩
那样,即使在被命运无情的雷普之际、即使在被生活残酷的撅力之时,也不对现实屈服,即使在被命运雷普之际,也要高声呐喊那句いいよ、こいよ
(很好!来吧)
指针就是存储地址的一个地址空间,而学习相关的啥数组指针或者指针数组,是用来取值或者进行一些我们想要执行的操作的
相关知识
虚拟地址
ps:不想看这章节可以直接跳过虚拟地址直接看后边儿的,这儿只是一点简单的科普
虚拟内存是一种抽象的概念,它为每个进程提供了一个看起来连续的地址空间,这个地址空间被称为虚拟地址空间。虚拟地址空间通常比实际的物理内存大小大得多,并且由操作系统来管理。操作系统会将虚拟内存分为若干个页面(通常是4KB
或者8KB
),每个页面都有一个唯一的标识符,称为页面号或页码。当一个进程需要访问某个虚拟地址时,操作系统会将该虚拟地址转换为对应的物理地址,这个过程称为地址映射。
底层扇区(也称为磁盘扇区)是硬盘上的物理存储单元。硬盘被分为多个扇区,每个扇区通常是512
字节或4KB
字节。操作系统将硬盘分为多个分区,并将每个分区格式化为一个文件系统。文件系统是一种将文件和目录组织成树状结构的机制,它将文件和目录存储在不同的扇区中,并使用文件指针或索引来跟踪它们的位置。因此,操作系统可以将虚拟内存地址和硬盘上的物理扇区之间建立映射关系。
在操作系统中,虚拟内存地址和底层扇区之间的关系通常是通过页表来维护的。页表是一种数据结构,用于记录虚拟地址和物理地址之间的映射关系。当进程访问虚拟地址时,操作系统会通过查找页表来确定该虚拟地址对应的物理地址,并将数据从物理地址读取到内存中。如果所需的数据不在内存中,操作系统就会将相应的页面从磁盘上读取到内存中,并更新页表中的映射关系。
32bit
的地址范围是从0x00000000
到0xFFFFFFFF
的连续地址空间,我们可以在这段空间上存储信息、数据,既然都是存储的数据,那么我们也可以把指针看成存储了一个地址的地址空间,我们需要使用的时候就将其从地址空间里取出来,我们还可以使用相对的定位方法定位到我们想要取出来的数据,当然,我们下面会讲到
数组
首先我们讲一讲数组在内存中的定义、分配和存储
我们先定义一个这样的代码
我们都知道他返回的结果
现在我们来分析
首先我们看sub esp,4Ch
,从这儿我们知道,他分配的空间是76,当然,这点不重要,我们只需要知道他分配的空间大小
现在我们看代码int a = 0
,从这儿我们知道,a这个变量的值是0,所以在对应的地址空间也就是[ebp-4]
这儿赋值为0
现在我们看数组,数组在地址[ebp-0Ch]到[ebp-8]
上分配地址空间,这是一段连续的地址空间,也就是说数组其实就是分配在内存中的一段连续的地址,地址中存储了数组的值
再举个例子
而这个二维数组在连续的地址空间[ebp-0Ch]到[ebp-7]
上存储
二维数组,他在内存中分配也是一段连续的地址空间
所以无论二维数组、一维数组甚至是多维数组,他们在内存中分配都是一维数组的形式
下面,我将从汇编的角度和手动操作的过程给大家演示指针及其指针的计算方法
指针中的等量代换
我们先生成一组10*10
的十六进制数作为地址空间中的垃圾数或者内存中的数据
这里使用python生成我们需要的实验目标
import random
def printHex():
a = random.randint(1,255)
return hex(a)
if __name__ == "__main__":
x = ""
for i in range(10):
for k in range(10):
a = printHex()
x += a+","
print(x)
x = ""
然后随机生成的十六进制数弄到
char
类型的数组中去
char a[] =
{
0x4c,0x6c,0x96,0x6b,0x79,0x4f,0xe2,0x44,0x0b,0x5e,
0xed,0xec,0xde,0x04,0x6b,0x56,0xfb,0xcd,0xc2,0xc5,
0x40,0xed,0xea,0x95,0x29,0x95,0xa3,0x1a,0xec,0xa8,
0x14,0x8b,0xff,0x2f,0xcf,0xaf,0x6c,0xe0,0x5c,0xf9,
0xed,0x16,0x36,0xea,0xbc,0x0c,0xca,0xf4,0xa8,0xeb,
0xe0,0x64,0xe3,0x6d,0xf2,0x7d,0x5c,0xd3,0xf1,0x44,
0xdb,0xe8,0xdf,0xbb,0xd1,0x4c,0x8c,0xdf,0x7e,0x5b,
0x79,0xee,0x05,0x58,0x92,0xe2,0xb1,0xb7,0xbe,0xbb,
0xfe,0xa8,0xde,0x62,0x70,0x9f,0x43,0xa7,0xd5,0xfa,
0xce,0xa8,0x49,0x1f,0x52,0x59,0xf6,0x89,0xa2,0x02
};
int main(int argc, char* argv[])
{
int* p;
p = (int*)&a;
printf("%d,%d,%dn",*p,*(p+0),p[0]);
return 0;
}
我们为什么要赋值给一组数组呢?因为在内存中,数组就是一个连续的地址空间,可以用来模拟我们在读取数据时候的操作,所以我们这儿使用数组作为target,我们再生成一堆随机的十六进制数用来模拟内存中的数据,不然全是00
也分不出来读的是些啥值
编译,生成,我们来看看他的结果值
当然,打印成十六进制的就是
因为是小端存储,所以得倒过来
0x4c,0x6c,0x96,0x6b
输出就是6b966c4c
,当然,这点不重要,这儿只是补充说明一下
我们回过头来看,我们会惊奇的发现,取出指针指向内存空间存储的值,原来可以有这么多种写法,他们取出来的值都已一模一样的
那么我们就有了公式:*p ~ *(p+0) ~ p[0]
再看看反汇编代码
根据上边我们不难看出,C语言编译器在解释的时候,就把这三者当成同一个来解释的
我们定义的是一个char
类型的数组,但是我们取值居然是用的int*
的指针取出来的,只需要简单的转换类型就可以了,为啥喃,因为不转换会报错(机智),那我们也可以使用这样的写法来取出内存中保存的数据,根据你的需要来取,比如我想取单个字符,我可以使用char*
类型,我想取个int
,那就取一串儿出来
那我们是不是可以试试,给他加上一个数或者减去一个数,他会有些啥子反应喃?
那么就涉及到上一篇讲过的指针的加减法
这就说明,我想遍历一个地址空间的数据,我可以使用指针去循环,将地址空间中的数据取出来
发现没有,int*
自加是加4,这儿正好对应,加了4个偏移量,指向了44e24f79
总结规律
个人理解,指针加减法本质上是偏移量的计算,计算偏移量,就是我们想输出一段连续的地址空间里边存储的内容,我们可以选择首地址或者结束的地址作为参照物,加减一个偏移量去计算并拿出 那个位置存储的数据,举个例子,我们去中药房抓药,就有很多的小柜子,我们就把他当成一段连续的地址空间,我们先假定他每一个柜子里只装了一味药,现在我们把甘草当成这个连续地址空间的首地址,如果我们要抓黄芩这味药,位置是甘草右边第二个,如果我们把每一个柜子的长度设置成4,那么他的偏移相对于甘草的地址就是2×4=8,那么我们就有代码:
int 药柜[] = {"甘草","附子","黄芩","火麻仁","忍冬",....}
int *p;
p = (int *)药柜;
printf("%dn",*(p+2));
这儿我们只是举例,因为我们知道,药柜里边儿每一个柜子宽度为4,所以我们的int指针数两个柜子就到了
你干嘛~
那么我假定数组为1到7
的数表示药柜上的药的序号,那么就有
这样我们就把我们想要的取出来了,484很简单?
那么如果一个柜子里正常装四味药,也就是一个柜子里存储了四个,我们现在可以一次抓四味药,那就是int类型抓药法,也可以单个去抓,那就是char抓药法,也可以是两味的抓,那就用short,就像买鸡蛋一样,你可以一拾一拾的捡,或者一打一打的捡,或者一个一个捡也行
这样我们就有代码
char 药柜[] =
{
{"甘草","茯苓","党参","忍冬"},
{"黄芩","大蓟","甘遂","锁阳"},
{"生地","细辛","芍药","白芨"},
......
}
int* p = (int *)药柜;
printf("%dn",*(p+1));//这是一口气取出来四味药
char* px = (char*)药柜;
printf("%dn",*(p+4));//这是一口气拿一味药出来
因为我们在前边讲过,二维数组在内存中的存储也是一维的,就相当于是
char 药柜[] = {
"甘草","茯苓","党参","忍冬","黄芩","大蓟","甘遂","锁阳","生地","细辛","芍药","白芨",......
}
这样我们就能把他们当成一段连续内存地址
所以我们可以这样子写,也可以像一维数组那样子取值出来用,就是这个原因
这儿我就使用1
到12
表示柜子里的药材
哦豁,为啥子输出的值是个这样的值喃
我们强转char
看看喃
那么就很有可能,编译器把他当成一个十六进制数了,转换成十进制输出后就成了这样子,那么我们换成十六进制输出
看,这样子就把这个柜子里所有的药取出来了
所以,你是写代码的人,想怎么指就怎么指,举个简单的例子,我可以加,那我也可以减啊,我想把甘草定为我的起始地址,那我还可以把结束地址比如石斛当成起始地址,自减去遍历数组啊,比如现在我就拿个数组的结束地址当成首地址去取值
你想怎么定义就怎么定义,你只需要掌握好如何计算偏移地址和取出偏移地址对应的值就行了,指针就是求取地址就是参照物,指针的加减就是求偏移地址怎么取值,向前还是向后,一次移动几个字节,对应的值取出来后是什么类型,比如当成int或者char
寻找规律
那我们来试试二次指针、三次指针寻找偏移量变化规律
那我们定义一个char**
的二次指针
发现没有,和上一章节的二次指针宽度是一样的,都是4
也就是dword
那么short
也一定是44e24f79
484很简单?
那我们有一段代码:
char a[100] =
{
0x4c,0x6c,0x96,0x6b,0x79,0x4f,0xe2,0x44,0x0b,0x5e,
0xed,0xec,0xde,0x04,0x6b,0x56,0xfb,0xcd,0xc2,0xc5,
0x40,0xed,0xea,0x95,0x29,0x95,0xa3,0x1a,0xec,0xa8,
0x14,0x8b,0xff,0x2f,0xcf,0xaf,0x6c,0xe0,0x5c,0xf9,
0xed,0x16,0x36,0xea,0xbc,0x0c,0xca,0xf4,0xa8,0xeb,
0xe0,0x64,0xe3,0x6d,0xf2,0x7d,0x5c,0xd3,0xf1,0x44,
0xdb,0xe8,0xdf,0xbb,0xd1,0x4c,0x8c,0xdf,0x7e,0x5b,
0x79,0xee,0x05,0x58,0x92,0xe2,0xb1,0xb7,0xbe,0xbb,
0xfe,0xa8,0xde,0x62,0x70,0x9f,0x43,0xa7,0xd5,0xfa,
0xce,0xa8,0x49,0x1f,0x52,0x59,0xf6,0x89,0xa2,0x02
};
int main(int argc, char* argv[])
{
char** p;
p = (char**)&a;
printf("%xn",*(*(p+1)+2));
return 0;
}
我们对其编译调试
无法直接查看到结果,需要我们对其进行反汇编分析
反汇编结果是这样的
我们对其分析
mov dword ptr [ebp-4],offset data (00422300)
这儿是把地址00422300
这个地址给变量[ebp-4]
,也就是赋值给我们的局部变量
接下来
mov eax,dword ptr
现在把这个局部变量赋值给eax,eax是一个寄存器,是通用寄存器的一种,用来存储临时数据
接下来,我们可以看到
mov ecx,dword ptr
这儿+4
就是此处的偏移量,四个字节的单位,这儿就是我们内层的*(p+1)
=4
下一条指令
movsx edx,byte ptr
令*(p+1)
= x
,那么最外层偏移量*(x+1)
= 1
也就是说,最外边儿的1加上他自身,最里边儿的1×4,所以他的偏移量是5
我们貌似掌握了一点规律,那就是内层是二次指针,那就是默认宽度为4,外层默认自身宽度
那我们试试short
类型
这儿的偏移就是4+2=6
那么三次指针喃
他的偏移就是4+4+2=10
那么我们可以总结一个公式:
最外层是常数×类型宽度
,内层是常数×4
举个例子
short**** a
的*(*(*(*(p+1)+2)+3)+4)
他的偏移计算结果就是(1+2+3)×4+(4×2)
函数指针
函数指针的一般形式为:
int sub(int x, int y)
{
return x-y;
}
int main(int argc, char* argv[])
{
int(*p)(int,int);
p = (int(*)(int,int))sub;
int a = p(4,1);
printf("%dn",a);
return 0;
}
也就是参数定义的参数个数一致,类型一致
这样就是一个函数指针了,现在我们来实现数据段代码当成代码段代码执行
我们在这儿讲一讲段的概念
我们一般会把数据分为数据段、代码段栈段和附加段,也就是所谓的data sement
、code segment
、stack segment
和extra segment
。一般来说,我们的数据段只会存储数据,代码段只会存储可执行数据,但是函数指针可以将一个数据段内的数据当成代码执行,也就是上边儿的地址,因为地址空间很多,所以我们不可能把代码和数据放一块儿,会专门存放在代码段或者数据段,但是又很想我们把代码隐藏在数据里该咋弄喃?
首先,我们定义一个简单的函数
我们在子程序内打上断点
反汇编一下
把图中圈出来的硬编码弄到一个数组里
注:从push ebp
开始到ret
结束,才是一个完整的子程序哈
char[] func = {
0x55,0x8b,0xec,0x83,0xec,0x40,0x53,0x56,0x57,0x8d,
0x7d,0xc0,0xb9,0x10,0x00,0x00,0x00,0xb8,0xcc,0xcc,
0xcc,0xcc,0xf3,0xab,0x8b,0x45,0x08,0x03,0x45,0x0c,
0x5f,0x5e,0x5b,0x8b,0xe5,0x5d,0xc3
}
然后定义一个函数指针指向它,注意,类型得一样参数个数也得一样昂
现在我们知道了,函数指针是可以把数据段里边儿的数据当成代码执行的
就很简单,他的汇编语句大概就是这样(我随便写的)
lea dword ptr ss:[ebp-4],offset data (func的地址)
call dword ptr ss:[ebp-4];;调用shellcode,也就是调用子程序
......;;往下执行其他比如结束主函数
;;
data
;;执行shellcode,数据段当成代码使用
......
retn;;内平,返回调用子程序的下一行
当然我们可以使用一些比较复杂且恶心人的指针算法或者伤脑袋掉头发的位运算逻辑运算设计的算法去混淆shellcode
从而达到免杀的目的
当然,加载恶意程序的shellcode当然不止这个,你也可以去看一看这篇文章shellcode loader的编写 by 免杀三@LeiyNeKo
数组指针
众所周知,数组指针的定义int *(p)[2]
一维数组指针
那么,我们现在来学一下数组指针定位偏移
根据上边的可以知道,一次指针取值,最里边儿现在看来是类型宽度
现在再变一下
刚刚的偏移值是4,现在的偏移量为8
,也就是说,他是长度为2的数组那就比长度为1的数组大两倍,说明与数组长度也有关,现在变一下,验证我们的猜想
现在的偏移量为12,也就是数组指针大小×类型大小
也就是1×3×4
=12
换成+2
呢
现在偏移了整整24
说明,他加上去的常数的大小也是很有关系的,我们改变了加上去常数的大小,现在正好是两倍关系也正好是常数变成了2
现在能得到公式:加上去的常数×数组宽度×类型大小
我们可以使用公式套一下这段代码他的取出来的值是多少喃
2×3×2
=12,那么取出来的值应该是04de
果然,猜想正确
那我们接着加加加
我们加上一个1喃
我们现在能确定,很有可能像上边儿一样,是类型宽度
我们验证一下
这儿偏移都知道吧,那就懒得去圈了
(12-8)/2=2
正好是类型宽度
如果有
int (*px)[z];
printf("%dn",*(*(px+x)+y))
那么就有内层为px+x
,得出x*z*类型
,令*(px+x)=f
,那么就有*(f+y)
,那就回到指针的加减法了,,所以加上的是类型宽度
那么就有公式:
内层:常数×数组元素个数×类型宽度
,外层:常数×类型宽度
那么二维数组呢?
二维数组指针
控制变量法yyds
我们现在知道,他的偏移应该是只与宽度有关,因为数组内的元素个数为1,
那么我们修改一下呢?
正好是二倍关系,现在的偏移是8,也就是说,内层现在是宽度×第二层宽度 = 4 × 2 = 8
,我们现在修改第一层呢
现在的偏移也是8,也就是说宽度×一维数组子元素个数×二维数组子元素个数 = 4 × 2 × 1 = 8
那我们不妨计算一下
现在的偏移就是4*2*2=16
对了嘛?
那么我们的常数不是1而是其他呢?
偏移16,正好 常数×宽度×一维数组子元素个数×二维数组子元素个数 = 2 * 4 * 2 * 1 = 16
说明,二维数组最内层加减法就是数×宽度×一维数组子元素个数×二维数组子元素个数
那么再外层一点呢
偏移为8,那就是宽度和常数有关,再修改一下一维数组个数
与一维数组无关,修改一维的没有改变,那么修改一维数组中每个元素存储的元素个数喃
现在的偏移为16,也就是与一维数组中子元素包含子元素的个数有关
也就是常数×一维数组子元素中包含子元素的个数×宽度
,也就是一维数组求法
那么我们算一下
那么内层的
px+2
偏移量就是4*2*2*2 = 32
*(px+2)+2*
就是32+2*4*2=32+16=48
*(*(px+2)+2)+2
就是48+4*2=56
那么我们就来做一下
数呗,偏移正好是56
三维数组也是一样的,这儿就不多赘述了哈
now , nobody knows pointer better than you(确信
所以,你学废了嘛
原文始发于微信公众号(深夜笔记本):指针(下)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论