逆向笔记之函数与指针(三)
本文笔记均来自于对滴水逆向教程的学习,通过学习教程记录的笔记,个人所见所得。
大纲:
- 堆栈图学习
- 函数
- 函数调用
- 调用约定
- 入口点
- 函数分析
windows堆栈
- 先进后出
- 向低地址扩展
什么是堆栈平衡: 简单的说就是还原现场
问: cmp ebp,esp后,如果ebp!=esp是否可以跳转
答: 可以,因为z标志位改了就行
堆栈图练习
堆栈图练习1
单层函数的堆栈调用图
堆栈图练习2
带子函数的堆栈调用图
堆栈图练习3
粗略看了下,是个多层加法计算
第一层 7,6,5,4,3
第二层: int num1=2,num2=3; 子函数传入参数6,5,4,3,计算结果为num1+num2+ 子函数计算结果+7
第三层:int num1=2, 子函数传入参数5,4,3 计算结果为 num1+ 子函数计算结果+6
第四层: 返回5+4+3
所以整体为: 5+4+3+6+2+7+2+3=32
至于堆栈图,不画了,我有点基础,大概懂这些了
函数分析
空函数分析
无参数、无返回值的函数格式
1 | void function() |
两个数加法函数分析
1 | int func(int x,int y) |
三个整数加法操作
1 | int Plus1(int x,int y) |
5个整数加法并要求用前两个函数
1 | int Plus1(int x,int y) |
递归反汇编分析练习
1 | int fcb(int n) |
大概函数逆向出来了,接着编写代码,运行一下就知道了?
我跟踪了下整个过程,fcb(3)
首先他会执行fcb(3-1)->fcb(2-1)->fcb(1)
然后从fcb(1)那一层返回去执行fcb(2-1)+fcb(2-2)
在返回一层执行fcb(3-1)+fcb(3-2)
所以整体就是 fcb(3-2) + fcb(3-1) = fcb(1) +(fcb(0) + fcb(1))
实际结果就是1+0+1=2
参数和返回值以及局部变量
1 | //8位参数 |
- 本机尺寸:如果本机是32位的,那么对32位的数据支持最好,如果是64位的,那么对64位的支持最好.
- 编译器遵守了这个规则:char类型或者short类型的参数不但没有节省空间,反而浪费了多余的操作.
结论:整数类型的参数,一律使用int类型
参数传递的本质:将上层函数的变量,或者表达式的值“复制一份”,传递给下层函数.
- 小于32位的局部变量,空间在分配时,按32位分配.
- 使用时按实际的宽度使用.
- 不要定义char/short类型的局部变量.
- 参数与局部变量没有本质区别,都是局部变量,都在栈中分配.
- 完全可以把参数当初局部变量使用
赋值的本质: 将某个值存储到变量中的过程就是赋值.
练习题
返回值超过32位时,存在哪里?用long long(__int64)类型做实验
char arr[3] = {1,2,3};与 char arr[4] = {1,2,3,4};哪个更节省空间,从反汇编的角度来说明你的观点
找出下面赋值过程的反汇编代码
1
2
3
4
5
6
7
8
9
10
11
12void Function()
{
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];
}
- 选做题: 桶排序,学校c语言老师教过了,不搞了
第一题解答
高位存在edx, 低位存在eax里
实验证明
c语言代码:
1 | __int64 test() |
汇编代码:
第二题解答
char arr[4] = {1,2,3,4};更节省空间,因为char arr[3]虽然只用了三个空间,而实际还是占用一个单元,4个字节,剩余的一个字节不会被使用,浪费了
第三题解答
1 | # int x =1; |
1 | # int y=2; |
1 | # int arr[10] = {1,2,3,4,5,6,7,8,9,10}; |
1 | # r = arr[1]; |
1 | # r = arr[x]; |
1 | #r = arr[x+y]; |
1 | # r = arr[x*2+y]; |
对比分析结果:
- 越先定义的越靠近栈底
- 数组寻址,如果是一个常量,他直接通过ebp-几完成
- 数组寻址,如果是一个变量,他通过计算这个变量的值后,通过[ebp-34+变量值*4]寻址,解释,ebp-34是数组的第一个元素地址,而 *4是因为这是int型数组,一个int占4个字节
第四题解答
多维数组跟一维数组在内存中是一样的
调用约定
调用约定 | 参数压栈顺序 | 平衡堆栈 |
---|---|---|
__cdecl | 从右至左入栈 | 调用者清理栈 |
__stdcall | 从右至左入栈 | 自身清理堆栈 |
__fastcall | ECX/EDX传送前两个 剩下:从右至左入栈 | 自身清理堆栈 |
__cdecl
外平衡
1 | push 1 |
1 | int __cdecl Plus(int a,int b) |
__stdcall
内平衡
1 | push 1 |
1 | int __stdcall Plus(int a,int b) |
__fastcall
利用寄存器传参
1 | mov edx,2 |
1 | int __fastcall Plus(int a, int b) |
练习
将下列代码裸函数实现
1 | int plus(int x, int y, int z) |
1 | int __declspec(naked) Plus(int x, int y,int z) |
如何查找修改入口点
查找main方法,可以push 3个参数的
入口点实际不是main
link->entrypoint可以修改入口函数
函数分析
全局变量与局部变量
全局变量的特点:
- 全局变量在程序编译完成后地址就已经确定下来了,只要程序启动,全局变量就已经存在了,启动后里面是否有值取决于声明时是否给定了初始值,如果没有,默认为0
- 全局变量的值可以被所有函数所修改,里面存储的是最后一次修改的值.
- 全局变量所占内存会一直存在,知道整个进程结束.
- 全局变量的反汇编识别:MOV 寄存器,byte/word/dword ptr ds:[0x12345678]
全局变量就是所谓的基址
局部变量的特点:
局部变量在程序编译完成后并没有分配固定的地址.
在所属的方法没有被调用时,局部变量并不会分配内存地址,只有当所属的程序被调用了,才会在堆栈中分配内存.
当局部变量所属的方法执行完毕后,局部变量所占用的内存将变成垃圾数据.局部变量消失.
局部变量只能在方法内部使用,函数A无法使用函数B的局部变量.
局部变量的反汇编识别:[ebp-4],[ebp-8],[ebp-0xc]
函数参数确认
步骤1: 观察调用处代码
push 3
push 2
push 1
call 0040100f
步骤2: 找到平衡堆栈的代码继续论证
call 0040100f
add esp,0Ch
或者函数内部
ret 4/8/0xc/0x10
最后,两者一综合,函数的参数个数基本确定
- 参数传递未必都是通过堆栈,还可能通过使用寄存器
- 函数调用处的代码无法查看.
函数分析步骤
练习
自行分析如下代码,并还原成c语言
1 | num = 0; |
以下两题找出原因,并解释
有趣的返回地址
1 | void HelloWorld() |
永不磨灭的Hello World
1 | void Fun() |
这个原因我就不自己测试了
第一题,arr[6]覆盖了返回地址,傻逼了,因为arr[5]就能覆盖ebp,arr[6] 肯定能覆盖返回地址啊
第二题, arr[i]覆盖了i,至于为什么先定义的能被覆盖,想想堆栈,先定义的,就是说我需要一块一个这个的内存空间,所以int i肯定是比较靠近ebp的,接下来才是arr[5],所以覆盖得到i
指针
指针基础学习
大纲:
- 宽度
- 声明
- 赋值
- ++ –
- 加/减 一个整数
- 求差值
- 比较
这些我都学过了,c语言基础相对较好,略过
1 | char**** a; |
结果就是砍掉一个*,在除以数据宽度就行
总结:
- 带有 * 的变量类型的标准写法:变量类型* 变量名
- 任何类型都可以带 * 加上 * 以后是新的类型
- 可以是任意多个
- 带 * 类型的变量赋值时只能使用“完整写法”.
- 带 * 类型的变量宽度永远是4字节、无论类型是什么,无论有几个*.
- 不带*类型的变量,++或者– 都是假1 或者减1
- 带*类型的变量,可是进行++ 或者 –的操作
- 带 * 类型的变量,++ 或者 – 新增(减少)的数量是去掉一个*后变量的宽度
- 带*类型的变量可以加、减一个整数,但不能乘或者除.
- 带*类型变量与其他整数相加或者相减时:
- 带 * 类型变量 + N = 带 * 类型变量 + N(去掉一个后类型的宽度)
- 带 * 类型变量 - N = 带 * 类型变量 - N(去掉一个后类型的宽度)
- 两个类型相同的带 * 类型的变量可以进行减法操作.
- 想减的结果要除以去掉一个 * 的数据的宽度.
- 带 * 的变量,如果类型相同,可以做大小的比较。
练习
练习全都是针对32位程序
- char类型占几字节?char类型占几字节?int****占几字节?
- char** arr[10] 占多少个字节?
练习1解答
1个字节,char* 4个字节,int* 4个字节
练习2解答
40个字节,就是定义了一个数组,存的类型为char**,所以就是10 × 4
练习3解答
带*结构体的加减法
1 | struct Student |
先盲猜下,第一个s=104,第二个s=112.第三个s=100
1 | void test1() |
先盲猜,x=(200-100)/4=25
1 | void test2() |
盲猜,s=108, s=124,s=100
1 | void test3() |
盲猜, x=(200-100)/8=12
大胆猜想,小心论证,结果是正确的,没毛病
指针的基本使用
大纲:
- 类型转换
- &符号的使用
- “带 * 类型” 求值
- 用指针操作数组
- &是地址符,类型是其后面的类型加一个“*”,任何变量都可以使用&来获取地址,但不能用在常量上。
- 带 * 类型的变量,可以通过在其变量前加*来获取其指向内存中存储的值
- 在带类型的变量前面加,类型是其原来的类型减去一个 *.
练习
分析下列c代码的反汇编
1 | void func() |
利用堆栈图,分析下,相对不怎么难,二级**而已
分析下列代码反汇编
1 | void func1() |
结果就是p7->p6->p5->p4->p3->p2->p1->p
p7存p6地址,p6存p5地址,一直存下去
还行,逆向起来好像并不怎么复杂
完成代码,实现数组值的互换
1 | void Function() |
1 | void Function() |
指针与数组
编写一个函数,能够打印任意整形数组的值。
1 | void PrintArray(int arr[],int nLength) |
观察反汇编
通过这个顺便复习了for循环
练习
模拟实现CE的数据搜索功能:这一堆数据中存储了角色的血值信息,假设血值的类型为int类型,值为100(10进制)
请列出所有可能的值以及该值对应的地址.
1 | char data[]= { |
对其进行解耦,接口不改变
1 | char data[]= { |
指针与字符串
1 | char* x = "china"; //china存在常量区 |
常见字符串操作,学过了
练习
模拟实现CE的数据搜索功能:这一堆数据中存储了角色的名字信息(WOW),请列出角色名的内存地址.
1 | char data[]= |
- 编写函数,返回角色名字信息的地址,如果没有返回0
- char* FindRoleNameAddr(char* pData,char* pRoleName)
- 编写函数,遍历上面数据中所有角色名字.
1 |
|
指针数组
练习
- 创建一个int* arr[5] 数组,并为数组赋值(使用&)
- 创建一个字符指针数组,存储所有的C的关键词(查资料找),并全部打印出来.
练习1解答
1 | void test() |
练习2解答
1 | void test1() |
练习3解答
查找这些数据中,有几个id=1 level=8的结构体信息。
1 | typedef struct TagPlayer |
代码编写
1 | typedef struct TagPlayer |
跟前面的差不多,强转成struct就行
多级指针
反汇编并分析下列结果
1 | void test() |
这个挺好玩的,挺有意思
*(p+i) = p[i]
* (*(p+i)+k) = p[i][k]
*(*(*(p+i)+k)+m) = p[i][m]
*(*(*(*(*(p+i)+k)+m)+w)+t) = p[i][k][m][w][t]
*() 与 []可以相互转换
练习
- () 与 []是可以互换的,也就是说: * ( (p+1)+2) 相当与 p[1][2],那*(p+1)[2] 是否一定等于p[1][2]呢? 通过反汇编进行论证。
- 使用数组指针遍历一个一维数组.
练习1
不一定,这里有c语言结合顺序的问题
1 | void test() |
这个就是一样的,而原来的那个,结合顺序是优先数组((p+1)[2]) 首先是p+1,char *, +1 就是加上他当砍掉一个*的数据类型,也就是+4, 而p+1是char **类型,所以(p+1)[2]实际就是char *类型 +2 也就是+8,所以实际合起来就是+0xc,也就是p+1+2 ,猜想归猜想,实践证明
1 | 0040D8B8 mov eax,dword ptr [ebp-4] |
你看它的取法,就是直接+0xc,也就是一样的说法,而结合顺序对的话,结果就是对的
练习2
1 | void test() |
easy的一件事,注意的是,*p 还是个地址,至于为什么,请看滴水p32 指针第7讲
数组指针与函数指针
大纲:
- 数组指针
- 函数指针
练习
- 下列说法是否正确?为什么?
- 指针的指针:就是指向指针的指针
- 结构指针:就是指向结构的指针
- 数组指针:就是指向数组的指针
- 函数指针:就是指向函数的指
- 将一个函数存储到数据区,通过指针进行访问.
练习1解答
不正确,前面的指针练习已经说明了,结构指针不一定也得指向结构,他可以指向随便一个地址,强制转换后按结构来解析
练习2解答
1 | unsigned char array[]= { 0x55, 0x8b,0xec,0x83,0xec,0x40,0x53,0x56,0x57,0x8d,0x7d,0xc0,0xb9,0x10,0x00,0x00,0x00,0xb8,0xcc,0xcc,0xcc,0xcc,0xf3,0xab,0xb8,0x01,0x00,0x00,0x00,0x5f,0x5e,0x5b,0x8b,0xe5,0x5d,0xc3}; |
有点小疑惑,为啥要取地址才能搞定,经过测试后,发觉他们的汇编是一样的,而编译器却不通过,array是char []类型也就是char *类型,而&array是数组指针,也就是char (*p)[len]; 至于编译器为什么不通过,这里我也不得而知
练习3解答
3、char数组内容如下:
1 | char data[] = { |
不运行说出下面的结果: 指针定义如下:
*(*(px+0)+0) int (*px)[2];
*(*(px+1)+0) int (*py)[2][3];
*(*(px+2)+3) char (*pz)[2];
*(*(*(py+1)+2)+3) char (*pk)[2][3];
*(*(pz+2)+3)
*(*(*(pk+2)+3)+4)
这个也挺简单的
px+0
px+(2*4*1)
px+(2*4*2)+(3*4)
py+(2*3*4*1) + (3*4*2) + (4*3)
pz+(2*1*2)+3
pk+(2*3*1*2)+(3*1*3)+(1*4)
论证
1 | 0040D428 8B 45 FC mov eax,dword ptr [ebp-4] |
实际确实如此,反汇编结果发觉,居然跟char **额外的相似
本文作者:NoOne
本文地址: https://noonegroup.xyz/posts/45d100c3/
版权声明:转载请注明出处!