CTF集训(5)

PWN(1)

pwn,yyds

一些基本概念

exploid:用于攻击的脚本或方案

payload:恶意数据,构造用于破坏,一般翻译为攻击载荷

shellcode:用户和操作系统的命令行借口

程序的编译和链接

考虑一个C语言程序

1
2
3
4
5
6
#include <iostream>

int main() {
printf("hello world\n");
return 0;
}

Linux是通过文件头来判断文件类型的,而Windows使用后缀名标识文件。可以用file a来查看a的文件类型。

利用gcc a.c就可以输出a.out。但是编译过程本身有很多步,我们这里主要涉及两步:

(1)编译为汇编文件

gcc -S a.c ,那么出现了a.s,可以打开为汇编文件

(2)汇编文件进行链接

例如printf函数,需要链接起来取得库中的代码。

可执行文件的概念比较复杂。广义的可执行文件是所有可执行的数据,比如.out、.py等;狭义的可执行程序是机器码文件,比如.out、.exe、.dll、.so

pwn主要研究的分成Windows和Linux。

  • Windows PE:.可执行程序exe,动态链接库.dll,静态链接库.lib
  • Linux ELF: 可执行程序.out,动态链接库.so,静态链接库.a

多数pwn题目都是ELF。一个ELF文件结构如下:

  • Header 1/2

    • ELF header,文件头表,记录文件组织结构
    • Program Header table,程序头表/段表,标识进程不同部分的程序,链接库文件不一定需要段表
  • Sections

    • Code
    • Data
    • Sections’ names
  • Header 2/2

    • Section Header table,节头表,储存ELF文件各个节的信息

段和节的映射

程序运行需要加载到内存中。编译好的可执行文件储存在磁盘中,运行则变成了进程映像。许多节合成了一个段,并只占内存中的一小部分。

利用objdump -s elf可以查看磁盘中的可执行文件的结构,利用gdb可以管理内存中的C语言程序。

VMA是虚拟内存的结构。传统内存使用物理内存,则物理内存很容易被篡改,导致计算机很容易受攻击。为此,人们发明了一种保护模式,让内存地址映射到虚拟的内存,之后操作系统再将虚拟内存转换为物理内存。这样,用户不能直接拿到物理内存地址,保证了硬件和用户直接控制的分离。操作系统提供了硬件的调用接口,用户通过确定规则来访问硬件。

对于32位系统,每个进程都有一个4G的虚拟内存空间。所有的内存分配问题都由操作系统完成。对于Linux系统,将3GB分配给用户成为用户空间,1GB分配给操作系统成为内核空间。多个进程的用户空间相互独立,但是内核空间可以共享。

虚拟内存中地址以字节编码,1Byte=8bits,以两个16进制表示,即0x**

64位系统则分别取128T.由于空间过大,还要一部分未定义区域。

节决定存储结构,段决定执行的结构。比较重要的节有:

  • .text节:用户代码
  • .rodata节:只读数据
  • .plt节:解析动态链接函数的地址
  • .got.plt节:保存plt解析的实际地址
  • .bss节:只占用内存空间

我们来以下面这个程序为例分析段的载入结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int glb;
char* str = “Hello world!”;

int sum(int x, int y) {
int t = x + y;
return t;
}

int main() {
sum(1, 2);
void* ptr = malloc(0x100);
read(0, ptr, 0x100); // input “deadbeef”
return 0;
}

  • text段:main,sum,”Hello World!”
  • data段:已初始化的变量,strr
  • bss段:未初始化的遍历,glb
  • Heap:动态存储区,”deadbeef”
  • Stack:局部变量,t、ptr

x和y是形参。也就是说,只有在父函数调用起到传递作用,在32位架构下也存储在栈中,而64位则不出现在虚拟内存空间、而是直接存储在寄存器中,提高执行效率。

函数的调用

这里还有一点需要注意,就是大端机和小端机。例如0xABCD,那么小端序中,AB放在最大的地址;大端序中,AB放在最小的地址。多数题目都是小端序。

内存和CPU通过地址总线、数据总线和控制总线交互。内存储存了CPU的指令和所需数据,而主要通过寄存器完成指令。

常见的是i386寄存器:

  • EAX 一般做累加器
  • EBX 一般做基址寄存器
  • ECX 一般用于计数
  • EDX 一般用于存放数据
  • ESP 一般用作堆栈指针
  • EBP 一般用作基址指针
  • ESI 一般用作源变址
  • EDI 一般用于目标变址

程序装载过程也比较复杂。

对于静态链接程序,会经历一系列过程。首先会进行fork,将自己的用户空间进行复制一份。接下来调用execve("/binary", argv[], *envp[]),开始访问操作系统层。操作系统会执行一系列操作,将fork后的用户空间变得可用。此时回到用户接管,通过_start入口执行main函数。

动态链接是在执行的时候寻找动态文件库,再进行执行。这要求其库文件都存在,并能随时找到。具体过程上,在回到用户之后,需要借助ld.so来链接,之后进行_start,再执行__libc_start_main()_init才能开始执行功能。

汇编基础

[]相当于取值,例如[00404011H]表示取这个地址上的值

MOV将操作符传送给目标,MOV EAX,1234H表示EAX赋值成1234X

LEA是取地址,LEA EBX,ASC表示把ASC的地址储存在EBX寄存器中,LEA EAX, 6[ESI]表示将ESI+6传给EAX

PUSH是压栈,POP是弹出栈。栈总是从高地址往低地址增长。POP EAX可以将栈顶的值保存到EAX中。

LEAVE在函数返回的时候,表示离开当前函数,销毁子函数的栈帧。等效于

1
2
MOV ESP,EBP
POP EBP

RET则将控制流返回父函数。

函数调用栈

栈帧

一个栈帧保存了函数调用的状态,按照高地址到低地址增长。其栈顶是ESP,栈底是EBP。

stack frame pointer保存了父函数栈顶和栈底指针的位置。callee saved registers保存了子函数,local variables保存了局部变量。

子函数所用的参数并不是保存在自己的栈帧中,而是保存在父函数栈帧的arguments中。父函数末尾的stack frame pointer指向自己的栈顶。

工作过程

在调用子函数的时候,首先将子函数需要参数倒序压入栈中。EIP保存即将执行的程序指令的地址。

例如

1
2
3
4
5
6
7
8
int sum(int x, int y) {
return x + y;
}

int main() {
sum(1, 2);
return 0;
}

对于一个main函数栈帧而言,其结构为:

  • sum函数
  • 参数2
  • 参数1
  • sum下一条指令的地址

接下来我们要把main函数的EBP压入到sum函数中,保存其地址。将局部变量压入被调函数的栈内,完成子函数的功能。此时EBP、ESP指向子函数的栈底和栈顶,栈帧为子函数的栈帧。

接下来将ESP和EBP移动位置,就相当于进行了数据删除。先将ESP和EBP移动到相同位置,再弹出寄存器,EBP变成了寄存器的保存值,也就是main函数栈顶的位置。ESP则自动减去一个字长。

我们用下面这个例子来演示过程。

1
2
3
4
5
6
7
8
9
int callee(int a, int b, int c) {
return a + b + c;
}
int caller(void) {
int ret;
ret = callee(1,2,3);
ret += 4;
return ret;
}

我们分析其汇编代码,然后给出每一步的栈变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;<caller>:
push %ebp ; 保存父函数的栈顶状态
mov %esp, %ebp ; esp移动到ebp位置
sub $0x10, %esp ; 给esp移动16的空间,向下扩展所以减
push $0x3 ; 保存栈里,反向压入
push $0x2
push $0x1
call 1f <caller+0xd> ; 将eip移动到目标代码位置,自动保存return address到子函数
add $0xc,%esp ; 清空数据
mov %eax, -0x4(%ebp)
addk $0x4, -0x4(%ebp)
mov -0x4(%ebp), %eax ;将结果保存到eax
leave ; esp指向ebp位置并移动ebp到父函数
ret ; 返回父函数下条指令
;<callee>:
push %ebp ; 保存父函数栈帧
mov %esp,%ebp ; esp移动到ebp
mov 0x8(%ebp),%edx
mov 0xc(%ebp),%eax
add %eax,%edx
mov 0x10(%ebp),%eax
add %edx,%eax ; 计算并保存到eax寄存器
pop %ebp ; 释放esp指向的数据。由于esp指向父函数栈底,所以ebp移向上边。
ret ; esp抬高一位

具体的动图建议去看视频(

这样我们就做好了准备,去开始我们的第一个pwn漏洞——stack overflow.

Author

LittleRewriter

Posted on

2020-08-02

Updated on

2021-07-28

Licensed under

Comments