0%

Linux 二进制程序保护机制详解

0x00 检测

可以使用 pwntools 中的 checksec 工具进行检测,红色表示未开启该保护。
checksec 检测效果

0x01 保护

一、ASLR

1.介绍:

ASLR 的是操作系统的功能选项,作用于 executable(ELF)装入内存运行时,因而只能随机化 stack、heap、libraries 的基址。

2.表现形式

开启后每次加载程序的 stack、libarys、heap 等地址都会随机化,可用 cat /proc/pidof xxx/maps 查看。

3.作用

未开启:无作用
半开启:随机化 stack 和 libarys
全开启:随机化 stack、libarys 和 heap

阅读全文 »

简述神经网络

任务描述

如下图,我们已知四个数据点(1,1)(-1,1)(-1,-1)(1,-1),这四个点分别对应I~IV象限,如果这时候给我们一个新的坐标点(比如(2,2)),那么它应该属于哪个象限呢?(没错,当然是第I象限,但我们的任务是要让机器知道)

“分类”是神经网络的一大应用,我们使用神经网络完成这个分类任务。

阅读全文 »

PyTorch使用指南

PyTorch 是由 Facebook 开发,基于 Torch 开发,从并不常用的 Lua 语言转为 Python 语言开发的深度学习框架,Torch 是 TensorFlow 开源前非常出名的一个深度学习框架,而 PyTorch 在开源后由于其使用简单,动态计算图的特性得到非常多的关注,并且成为了 TensorFlow 的 最大竞争对手。目前其 Github 也有 2w8+ 关注。
Github 地址: https://github.com/pytorch/pytorch
官网: https://pytorch.org/
论坛:https://discuss.pytorch.org/

本文是翻译自官方版教程–DEEP LEARNING WITH PYTORCH: A 60 MINUTE BLITZ,一份 60 分钟带你快速入门 PyTorch 的官方教程。

(ps. 文末有最新的更新,介绍了深度学习的入门资料推荐、PyTorch 的教程推荐,如果阅读本文后还是有些困难的,可以看看文末推荐的深度学习书籍和教程,先入门深度学习,有一定基础再学习 PyTorch,效果会更好!)

阅读全文 »

PLT&GOT及延迟绑定

linux 下的动态链接是通过 PLT&GOT 来实现的,这里做一个实验,通过这个实验来理解

使用如下源代码 test.c:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(void)
{
print_banner();
return 0;
}

依次使用下列命令进行编译:

1
2
gcc -Wall -g -o test.o -c test.c -m32
gcc -o test test.o -m32

这样除了原有的 test.c 还有个 test.o 以及可执行文件 test

通过 objdump -d test.o 可以查看反汇编

img

printf () 和函数是在 glibc 动态库里面的,只有当程序运行起来的时候才能确定地址,所以此时的 printf () 函数先用 fc ff ff ff 也就是有符号数的 -4 代替

运行时进行重定位是无法修改代码段的,只能将 printf 重定位到数据段,但是已经编译好的程序,调用 printf 的时候怎么才能找到这个地址呐?

链接器会额外生成一小段代码,通过这段代码来获取 printf () 的地址,像下面这样,进行链接的时候只需要对 printf_stub () 进行重定位操作就可以

1
2
3
4
5
6
7
8
9
10
11
.text
...
// 调用printf的call指令
call printf_stub
...
printf_stub:
mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址
jmp rax // 跳过去执行printf函数
.data
...
printf函数的储存地址,这里储存printf函数重定位后的地址

总体来说,动态链接每个函数需要两个东西:

1、用来存放外部函数地址的数据段

2、用来获取数据段记录的外部函数地址的代码

对应有两个表,一个用来存放外部的函数地址的数据表称为全局偏移表(GOT, Global Offset Table),那个存放额外代码的表称为程序链接表(PLT,Procedure Link Table)

img

可执行文件里面保存的是 PLT 表的地址,对应 PLT 地址指向的是 GOT 的地址,GOT 表指向的就是 glibc 中的地址

那我们可以发现,在这里面想要通过 plt 表获取函数的地址,首先要保证 got 表已经获取了正确的地址,但是在一开始就进行所有函数的重定位是比较麻烦的,为此,linux 引入了延迟绑定机制

延迟绑定

只有动态库函数在被调用时,才会地址解析和重定位工作,为此可以使用类似这样的代码来实现:

1
2
3
4
5
6
7
8
9
//一开始没有重定位的时候将 printf@got 填成 lookup_printf 的地址
void printf@plt()
{
address_good:
jmp *printf@got
lookup_printf:
//调用重定位函数查找 printf 地址,并写到 printf@got
goto address_good;//再返回去执行address_good
}

说明一下这段代码工作流程,一开始,printf@got 是 lookup_printf 函数的地址,这个函数用来寻找 printf () 的地址,然后写入 printf@got,lookup_printf 执行完成后会返回到 address_good,这样再 jmp 的话就可以直接跳到 printf 来执行了

也就是说这样的机制的话如果不知道 printf 的地址,就去找一下,知道的话就直接去 jmp 执行 printf 了

接下来,我们就来看一下这个 “找” 的工作是怎么实现的:

通过 objdump -d test > test.asm 可以看到其中 plt 表项有三条指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Disassembly of section .plt:
080482d0 <common@plt>:
80482d0: ff 35 04 a0 04 08 pushl 0x804a004
80482d6: ff 25 08 a0 04 08 jmp *0x804a008
80482dc: 00 00 add %al,(%eax)
...
080482e0 <puts@plt>:
80482e0: ff 25 0c a0 04 08 jmp *0x804a00c
80482e6: 68 00 00 00 00 push $0x0
80482eb: e9 e0 ff ff ff jmp 80482d0 <_init+0x28>
080482f0 <__libc_start_main@plt>:
80482f0: ff 25 10 a0 04 08 jmp *0x804a010
80482f6: 68 08 00 00 00 push $0x8
80482fb: e9 d0 ff ff ff jmp 80482d0 <_init+0x28>

ps. 这里 plt 表的第一项使用 objdump 的时候给没有符号名的一项自动改成了离他最近的一项,为了避免引起误会,改成了 common,而且随着不断深入,会发现,确实可以叫 common

其中除第一个表项以外,plt 表的第一条都是跳转到对应的 got 表项,而 got 表项的内容我们可以通过 gdb 来看一下,如果函数还没有执行的时候,这里的地址是对应 plt 表项的下一条命令,即 push 0x0

(说一下怎么查看,先 gdb test 然后 b main,再 run, 再 x/x jmp的那个地址 就可以)

img

还记得之前我们说的,在还没有执行过函数之前 printf@got 的内容是 lookup_printf 函数的地址吗,这就是要去找 printf 函数的地址了

现在要做的是:

1
2
push  $0x0       //将数据压到栈上,作为将要执行的函数的参数
jmp 0x80482d0 //去到了第一个表项

接下来继续

1
2
3
4
5
080482d0 <common@plt>:pushl 
0x804a004 //将数据压到栈上,作为后面函数的参数
jmp *0x804a008 //跳转到函数
add %al,(%eax)
...

我们同样可以使用 gdb 来看一下这里面到底是什么,可以看到,在没有执行之前是全 0

img

当执行后他有了值

img

这个值对应的函数是 _dl_runtime_resolve

那现在做一个小总结:

在想要调用的函数没有被调用过,想要调用他的时候,是按照这个过程来调用的

xxx@plt -> xxx@got -> xxx@plt -> 公共 @plt -> _dl_runtime_resolve

到这里我们还需要知道

  1. _dl_runtime_resolve 是怎么知道要查找 printf 函数的
  2. _dl_runtime_resolve 找到 printf 函数地址之后,它怎么知道回填到哪个 GOT 表项

第一个问题,在 xxx@plt 中,我们在 jmp 之前 push 了一个参数,每个 xxx@plt 的 push 的操作数都不一样,那个参数就相当于函数的 id,告诉了 _dl_runtime_resolve 要去找哪一个函数的地址

在 elf 文件中 .rel.plt 保存了重定位表的信息,使用 readelf -r test 命令可以查看 test 可执行文件中的重定位信息

img

这里有些问题,对应着大佬博客说 plt 中 push 的操作数,就是对应函数在.rel.plt 段的偏移量,但是没对比出来

第二个问题,看 .rel.plt 的位置就对应着 xxx@plt 里 jmp 的地址

在 i386 架构下,除了每个函数占用一个 GOT 表项外,GOT 表项还保留了3个公共表项,也即 got 的前3项,分别保存:

  • got [0]: 本 ELF 动态段 (.dynamic 段)的装载地址
  • got [1]:本 ELF 的 link_map 数据结构描述符地址
  • got [2]:_dl_runtime_resolve 函数的地址

动态链接器在加载完 ELF 之后,都会将这3地址写到 GOT 表的前3项

跟着大佬的流程图来走一遍:

第一次调用

img

之后再次调用img

ubuntu18安装各种pwn工具


更换系统镜像为清华镜像源

https://blog.csdn.net/ifreewolf_csdn/article/details/83185505
不换的话以后下载东西会很慢

遇到报错

1
2
E: Could not get lock /var/lib/dpkg/lock-open(11: Resource temporarily unavailable)
E: Unable to lock the administration directory (/var/lib/dpkg/), is another process using it?

解决:https://www.cnblogs.com/yun6853992/p/9343816.html
然后重启终端即可,后面还有遇到,直接kill进程,不用再删除文件了

阅读全文 »

pwntools使用指南

pwntools是一个二进制利用框架。官方文档提供了详细的api规范。然而目前并没有一个很好的新手教程。因此我用了我过去的几篇writeup。由于本文只是用来介绍pwntools使用方法,我不会过于详细的讲解各种二进制漏洞攻击技术。

Pwntools的“Hello World”

栈溢出无疑是二进制攻击的“Hello World”。这里,我们用pwnable.kr的bof来进行展示。

阅读全文 »

简述动态链接


0x00 前言

  本文主要分析了在延迟绑定中,调用某函数之后如何找到正确的地址。文章中深入的分析了这个过程,并且分析完之后针对该链接介绍了一些攻击手法和程序所作的一些保护。

0x01 基础知识

一、动态链接

  在动态链接方式实现以前,普遍采用静态链接的方式来生成可执行文件。 如果一个程序使用了外部的库函数,那么整个库都会被直接编译到可执行文件中。ELF 支持动态链接,这在处理共享库的时候就会非常高效。 当一个程序被加载进内存时,动态链接器会把需要的共享库加载并绑定到该进程的地址空间中。随后在调用某个函数时,对该函数地址进行解析,以达到对该函数调用的目的。

二、两个表

1.PLT表(Procedure Linkage Table)

(1)简介: 过程连接表,在程序中以 .plt 节表示,该表处于代码段,每一个表项表示了一个与要重定位的函数相关的若干条指令,每个表项长度为 16 个字节,存储的是用于做延迟绑定的代码。
(2)结构简介:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PLT[0] --> 与每个函数第一次链接相关指令
例:
0x4004c0:
0x4004c0: ff 35 42 0b 20 00 push QWORD PTR [rip+0x200b42] // push [GOT[1]]
0x4004c6: ff 25 44 0b 20 00 jmp QWORD PTR [rip+0x200b44] // jmp [GOT[2]]
0x4004cc: 0f 1f 40 00 op DWORD PTR [rax+0x0]
即:
第一条指令为 push 一个值,该值为 GOT[1] 处存放的地址,
第二条指令为 jmp 到一个地址执行,该值为 GOT[2] 处存放的地址

PLT[1] --> 某个函数链接时所需要的指令,与 got 表一一对应
例:
0x4004d0 <__stack_chk_fail@plt>:
0x4004d0: ff 25 42 0b 20 00 jmp QWORD PTR [rip+0x200b42] // jmp GOT[3]
0x4004d6: 68 00 00 00 00 push 0x0 // push reloc_arg
0x4004db: e9 e0 ff ff ff jmp 0x4004c0 <_init+0x20> // jmp PLT[0]
即:
第一条指令为: jmp 到一个地址执行,该地址为对应 GOT 表项处存放的地址
第二条指令为: push 一个值,该值作用在下文提到
第三个指令为: jmp 一个地址执行,其实该地址就是上边提到的 PLT[0] 的地址,也就是说接下来要执行 PLT[0] 中保存的两条指令
2.GOT表(Global Offset Table)

(1)简介: 全局偏移表,在程序中以 .got.plt 表示,该表处于数据段,每一个表项存储的都是一个地址,每个表项长度是当前程序的对应需要寻址长度(32位程序:4字节,64位程序:8字节)。d_tag = DT_PLTGOT
(2)结构简介:

1
2
3
4
5
6
GOT[0] --> 此处存放的是 .dynamic 的地址;该节(段)的作用会在下文讨论
GOT[1] --> 此处存放的是 link_map 的地址;该结构也会在下文讨论
GOT[2] --> 此处存放的是 dl_runtime_resolve 函数的地址
GOT[3] --> 与 PLT[1] 对应,存放的是与该表项 (PLT[1]) 要解析的函数相关地址,
由于延迟绑定的原因,开始未调用对应函数时该项存的是 PLT[1] 中第二条指令的地址, 当进行完一次延迟绑定之后存放的才是所要解析的函数的真实地址
GOT[4] --> 与 PLT[2] 对应,所以存放的是与 PLT[2] 所解析的函数相关的地址
3.两个表之间的关系
1
2
3
4
5
6
7
GOT[0]: .dynamic 地址          PLT[0]: 与每个函数第一次链接相关指令
GOT[1]: link_map 地址G
OT[2]: dl_runtime_resolve 函数地址
GOT[3] --> PLT[1] // 一一对应
GOT[4] --> PLT[2] // 相互协同,作用于一个函数
GOT[5] --> PLT[3] // 一个保存的是该函数所需要的延迟绑定的指令
GOT[6] --> PLT[4] // 一个是保存个该函数链接所需要的地址

三、一个段(节)三个节

  在下面只对一些接下来要用到的结构体成员做一些中文解释。

1. .dynmic

(1)介绍:
  因为在加载过程中,.dynmic 节整个以一个段的形式加载进内存,所以说在程序中的 .dynmic 节也就是运行后的 .dynmic 段。该段主要与动态链接的整个过程有关,所以保存的是与动态链接相关信息,此处主要用于寻找与动态链接相关的其他节( .dynsym .dynstr .rela.plt 等节)。该段保存了许多 Elf64_Dyn 结构,该数据结构保存了一些其他节的信息。下面展示该段所保存的数据结构。p_type = PT_DYNAMIC(值为 0x2)的段。

(2)结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 该结构都有 64 位程序和 32 位程序的区别,不过大致结构相似,此处只讨论 64 位程序中的
// /usr/include/elf.h

typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
// d_tag 识别该结构体表示的哪一个节,通过以此字段不同来寻找不同的节
union
{
Elf64_Xword d_val; /* Integer value */
// 对应节的地址,用于存储该结构体表示下的节所在的地址
Elf64_Addr d_ptr; /* Address value */
// 一般于上一个字段表示的值相同,所以笔者现在并不了解他们的区别
} d_un;
} Elf64_Dyn;
2. .dynsym

(1)介绍:
  动态符号表,存储着在动态链接中所需要的每个函数所对应的符号信息,每个结构体分别对应一个符号 (函数) 。结构体数组。 d_tag = DT_SYMTAB(值为 0x6) 的节。
(2)结构:

1
2
3
4
5
6
7
8
9
10
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
// 保存着该函数函数名在 .dynstr 中的偏移,可以结合 .dynstr 找到准确函数名。
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
// 如果这个符号被导出,则存有这个导出函数的虚拟地址,否则为NULL. Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
3. .dynstr

(1)介绍:
  动态字符串表,表中存放了一系列字符串,这些字符串代表了符号的名称,在此处可以看成函数名,以空字符作为终止符。 该结构是一个字符串数组。d_tag = DT_STRTAB(值为 0x5) 的节。
(2)结构:
  一个字符串数组

4. .rel.plt (.rela.plt)

(1)介绍:
  重定位节,保存了重定位相关的信息,这些信息描述了如何在链接或者运行时,对 ELF 目标文件的某部分内容或者进程镜像进行补充或修改。每个结构体也与某一个重定位的函数相关。结构体数组。d_tag = DT_REL(值为 0x11) / d_tag = DT_RELA(值为 0x7) 的节。
(2)结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct
{
Elf64_Addr r_offset; /* Address */
// 此处表示的是解析完的函数真实地址存放的位置,
// 即对应解析函数的 GOT 表项地址
Elf64_Xword r_info; /* Relocation type and symbol index */ // 该结构主要用到高某位,表示索引,低位表示类型
//每一个表项的第二条指令, PUSH 了一个索引,所 PUSH 的索引与此相关,
//也就是通过 PLT 中 PUSH 的索引找到当时解析的函数对应的此结构体的
} Elf64_Rel;
//与上一结构体类似,只是不同编译环境下产生的不同结构,作用相同,就不再次讨论
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
Elf32_Sword r_addend; /* Addend */
} Elf32_Rela;

四、扩充结构体(在 Full RELRO 用到)

  本节讨论的是在 Full RELRO 攻击中用到的结构,所以如果不打算研究该攻击手法可以跳过该节。d_tag = DT_DEBUG。

1
2
3
4
5
6
7
struct r_debug{ //由于并没有找到该结构体的定义,所以没有声明类型  
r_version
r_map //指向 link_map
r_brk
r_state
r_ldbase
}
1.简介:

  保存着 Binary 里面所有信息的一个结构体,该结构体很大,内容丰富。

2.主要字段:
1
2
3
4
5
6
7
8
9
l_next:链接着该程序所有用到的 libary    
上边提到的 GOT[1] 中保存的地址是第一层 link_map 中所表示的 libary,此时是指向的程序本身,
不过可以用 l_next 结构寻找下一层表示的 libary,以此来遍历程序中所用到的 libary,
并利用下边所提到的字段找到该层 libary 的名字、基地址、以及所有的 section 等信息。
l_name:表示 libary 的名字
l_addr:表示 libary 的基地址
l_info[x]:指向该 libary 下的 .dynamic。
l_info[1] 指向 d_tag = 1 时所表示的 section ,所以可以改变 x 的值找到每个相关 section 的地址。
在链接过程中 binary 中的 section 地址,以及 libary 中的地址都是通过此方法确定的。

0x02 链接过程

一、概括描述

  完成延迟绑定的函数主要是 dl_runtime__resolve(link_map_obj, reloc_arg) ,该函数的第一个参数是一个 link_map 结构,第二个参数是一个重定位参数,即运行 PLT 中的代码时 PUSH 进栈中的参数。该函数主要是调用一个 dl_fixup(link_map_obj, reloc_arg) 完成了主要功能。参数一的主要作用是:获得重定位函数所在了 libary 的基地址,以及获取在 libary 中寻找需要定位函数时所需要的 Section (.dynstr .dynsym 等)。第二个函数主要是确定需要解析的函数名,以及解析完之后写回的地址。
  该过程可以先大概理解为,dl_fixup 函数通过 reloc_arg 参数确定当前正在解析的函数名。之后,拿着这个函数名,再利用 link_map 结构找到 libary 中的 .dynsym .dynstr 。利用 .dynsym .dynstr 进行匹配。若匹配成功,则从 .dynsym 中获取该函数的函数地址。

1
2
3
4
5
6
7
8
9
// 上边的详细过程
reloc_arg --> 函数名 A

利用 link_map --> l_info[x] 通过改变 x 的值,确定 .dynsym .dynstr
再用 .dynsym 与 .dynstr 对整个动态符号表 .dynstym 进行遍历,去匹配函数名 A
若 某一个 Elf64_Sym(符号) 的 st_name + .dynstr == A
则 该 Elf64_Sym 表示的符号即为函数 A

// 整个过程可以这样理解,不过真实情况使用的 Hash 方法去寻找的这个 Elf64_Sym(符号)

二、具体过程

1.调用某个函数后进入该函数的 PLT[x] ,在 PLT[x] 中 push 一个参数 reloc_arg 。

通过参数确定作用函数
【问题 1】通过这个 reloc_arg 可以干什么?
【答案 1】拿到这个 reloc_arg 后,链接器会通过该值找到对应函数的 Elf_Rel 结构,通过该结构的 r_info 变量中的偏移量找到对应函数的 Elf_Sym 结构,然后再通过 Elf_Sym 结构的 st_name 结合之前已经确定的 .dynstr 地址,通过 st_name + .dynstr 获得对应函数的函数名。这就是拿到 reloc_arg 参数后链接器获得的信息,即知道了本次链接中的函数的函数名。(注:此处用到的 binary 中的 Elf_Rel Elf_Sym .dynstr 等地址都是通过 link_map->l_info[x] 的方式寻找的。)

【问题 2】通过 link_map 我们能获得什么?
【答案 2】拿到这个变量后链接器会获得所要解析的函数的函数库(通过 link_map 的 l_next 字段),然后拿到这个外部库之后 link_map 的 l_addr 字段会记录该库的基地址,然后链接器通过 new_hash 函数求出要链接函数的 hash(new_hash(st_name + .dynstr)),然后通过该 hash 和之前的保存值进行匹配,如果匹配上就获得了该函数在外部库的 Elf64_Sym 结构,然后通过该结构的 st_value 获取该函数在外部库里面的偏移,最后通过 st_value + l_addr 获取该函数的真实地址,最后通过 Elf64_Rel 的 r_offset 定位该函数在 GOT 中对应的地址,然后将最后结果写入该地址中。(其中有通过这两个参数共同获得的东西,不过为了便于理解就不再分开讨论。)

0x03 攻击

一、保护手段 (RELRO)

  RELRO:重定位只读手段

1.无保护

  在这种模式下关于重定位并不进行任何保护。

2.部分保护

  在这种模式下,一些段 (包括.dynamic) 在初始化后将会被标识为只读。

3.完全保护

  在这种模式下,除了会开启部分保护外。惰性解析会被禁用(所有的导入符号将在开始时被解析,.got.plt 段会被完全初始化为目标函数的终地址,并被标记为只读)。此外,既然惰性解析被禁用,GOT[1] 与 GOT[2] 条目将不会被初始化,存值为0。

二、对应攻击方法

  动态装载器认为它接收到的参数都是值得信任的,因为它假设这些都是直接由 ELF 文件提供的或者是它自己在开始时初始化的。然而,当一个攻击者能够修改这些数据时,这个假设就不成立 了。一些动态装载器(FreeBSD)会验证自己接收到的输入。然而,他们还是完全地信任控制结构 ,但这些也会可以轻易地破坏。

1.无保护

原理:
  动态装载器从 .rel.plt 中的 Elf_Rel 结构开始工作,顺着其中的下标找到 .dynsym 段中对应 Elf_Sym 结构的位置,并终使用它确定待解析符号的名称(在 .dynstr 段中的一段字符串)。简单的调用任意函数的办法就是使用希望的函数的名称覆盖字符串表中的条目 ,然后再调用动态装载器,但这是不可能的,因为保存着动态符号字符串表的段,即.dynstr,是不可写的。
过程:
  动态装载器是从 .dynamic 段的 DT_STRTAB 条目中获得 .dynstr 段的地址的,而且 DT_STRTAB 条目的位置是已知的,默认情况下也可写。我们可以将这个条目的 d_val 域覆盖为 .bss 段。 这块内存区域上将会包含一段字符串,比如 system。到了这一步,攻击者需要选择一个已经存在的符号,它的偏移在伪造的字符串表中正好指向 system 的位置,接着调用其对应的符号解析重定位过程。可以通过将其重定位项的偏移压栈并跳转到 PLT0 实现。
限制:
   这种方式非常简单,但仅当二进制程序的 .dynamic 段可写时有效。对于使用部分或完全 RELRO 编译的二进制程序,需要使用更复杂的攻击。

2.部分保护

原理:
  _dl_runtime_resolve 函数的第二个参数是 Elf_Rel 条目在 .rel.plt 段中对应当前请求函数的偏移。动态装载器将这个值加上 .rel.plt 的基地址来得到目标 Elf_Rel 结构的绝对地址。然而多数动态装载器实现不去检查重定位表的边界。这就表明如果一个大于 .rel.plt 的值传到 _dl_runtime_resolve 中,装载器将会认为特定的地址上的数据是一个 Elf_Rel 结构并使用它,即使那里已经超出了.rel .plt段的范围。
过程:
  计算一个新的 reloc_arg 参数,将 _dl_runtime_resolve 解析的位置劫持到一个可控内存。然后在那里构造一个 Elf_Rel 结构,并填写 r_offset 的值为一个可写的内存地址,将最后解析出的函数地址写在那里。同时,r_info 也要修改成一个可控区域处。并在该区域伪造一个 Elf_Sym 结构,其中的 st_name 域,指向另一个可控区域,并在该处填写要伪造成的函数名(例:system)。
  简而言之,该过程伪造了函数链接中所需要的所有结构(Elf_Sym Elf_Rel .dynstr),通过控制 reloc_arg 指向到伪造的 Elf_Rel ,再通过Elf_Rel 中的 r_info 找到伪造的 Elf_Sym 最后通过 Elf_Sym 的 st_name 找到最终伪造后需要解析的函数(例:system),解析完后通过 Elf_Rel 的 r_offset 写回到正确位置,达到劫持函数解析的目的,最终执行自己想要执行的函数。
限制:
  首先,Elf_Rel 的下标需要是正数,因为 r_info 域在 ELF 标准中规定是一个无符号整数。这就意味着在实际中这块可写的内存空间(例如.bss段)必须是位于 .dynsym 段之后。这种情况总是满足的。另一个限制是 ELF 会使用的符号版本系统。在这种情况下,Elf_Rel 的 r_info 域不仅用作动态符号表中的下标,也用作符号版本表(.gnu.version段)中的下标。
扩充方法:
  可以通过修改指向程序那一层的 link_map,具体做法是把该层 link_map->l_info[DT_STRTAB]->st_value 的值劫持到一个我们可控的区域,然后在该区域填充伪造函数,其实该方法也是通过修改 .dynstr 的方式实现攻击的手法。不过该方法必须有能够改写 st_value 值所需要的 gadget。

3.完全保护

原理:
  DT_DEBUG 条目的值是动态装载器在加载时设置好的,它指向一个 r_debug 类型的数据结构。这个数据结构保存着调试 器用来标识动态装载器的基地址并拦截相应事件需要的信息。此外,这个结构的 r_map 域保存着一个指向 link_map 链表头部的指针。
过程:
  攻击者使用 DT_DEBUG 这个动态条目来获取 r_debug 结构。接着,解引用 r_map 域从而得到主程序的 link_map 结构。然后像上边扩充方法那样破坏 l_info[DT_STRTAB]。接着攻击者同样需要恢复 _dl_runtime_resolve 函数的指针,通过 link_map->l_next 获取其他链接库中用到的该函数。具体获取手法是攻击者通过 l_info[DT_PLTGOT] 来获取对应的符号,然后通过 st_value 获取 .plt.got 节所在的地址,前面讨论过,该节的第三个偏移所存放的内容即为 _dl_runtime_resolve 函数地址。不过在这一切都做好之后,还有一个问题值得关注,_dl_runtime_resolve 不仅仅会调用目标函数,还会尝试将它的地址写到 GOT 项中。因为完全 RELRO 保护下 GOT 是不可写的,所以程序就会崩溃。不过我们可以通过伪造 link_map 中的 DT_JMPREL 动态条目来绕过这个问题。(具体操作方式和扩充方法中修改 .dynstr 类似)原本 DT_JMPREL 指向 .rel.dyn 段,我们将其改为一块可控区域,并那里写有一个 Elf_Rel 结构,并将其 r_offset 域指向一块可写的内存区域,其 r_info 指向我们的目标符号。至此,我们就完成了整个攻击过程。

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Hello

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

csapp_BombLab(待完成

3 程序的机器级表示

编译器基于编程语言的规则、操作系统的惯例、目标机器的指令集生成机器代码。

汇编代码是机器代码的一种形式,它是机器代码的文本表示。

高级代码可移植性好,而汇编代码与特定机器密切相关。

能够阅读汇编代码:

  • 好处:可以理解编译器的优化能力,并分析代码中隐含的低效率

  • 条件:了解编译器将高级语言转换为机器代码的转换方式。

精通细节很重要,是理解更深和更基本概念的先决条件。要认真研究示例、完成练习。

32位机器可以使用约 4GB 的随机访问存储器,64位机器可以使用 256TB(2^48) 的内存空间(这里说的是主存)。

3.2 程序编码

汇编器产生的目标代码是机器代码的一种形式,它包含二进制形式表示的所有指令,但还没有填入全局值的地址。

3.2.1 机器级代码

影响机器级程序的两种抽象:

  • 指令集架构:定义了处理器状态、指令的格式、指令对状态的影响。
  • 虚拟地址:机器代码将内存看成一个按字节寻址的数组。

对机器代码可见的处理器状态:

  1. 程序计数器
  2. 整数寄存器文件:保存临时数据或重要的程序状态
  3. 条件码寄存器:保存最近执行的算术或逻辑指令的状态信息。
  4. 一组向量寄存器:保存一个或多个整数或浮点数值

C 语言中的数组和结构,在机器代码中用一组连续的字节来表示。

汇编代码不区分有符号数和无符号数,不区分指针的不同类型,不区分指针和整数

一条机器指令只执行一个非常基本的操作。

3.2.2 代码示例

反汇编

使用反汇编器可以根据机器代码产生汇编代码。如:48 89 d3 → mov %rdx,%rbx

机器代码与反汇编表示的特性:

  1. x86-64 的指令长度范围为 1~15 字节。常用指令和操作数少的指令所需字节少。
  2. 从十六进制字节值到汇编指令,格式为:某个数字唯一地对应某个汇编指令,比如 mov 指令以 48 开头。
  3. 指令结尾的 ‘q’ 是大小指示符,大多数情况下可以省略。

从源程序转换来的可执行目标文件中,除了程序过程的代码,还包含启动和终止程序的代码,与操作系统交互的代码。

链接器的任务之一就是为函数调用找到匹配的函数的可执行代码的位置。

3.2.3 关于格式的注解

在汇编代码中,以 ‘.’ (点) 开头的行是指导汇编器和链接器工作的伪指令。

3.3 数据格式

字节:byte,8位;字:word,16位;双字:double words,32位;四字:quad words,64位。

对应的指令后缀:movb, movw, movl, movq。

这里说的都是整数,浮点数使用一组完全不同的指令和寄存器。

3.4 访问信息

一个 64 位 CPU 中包含一组 16 个存储 64 位值的通用目的寄存器,用来存储整数和指针。

16 个寄存器标号为 raxrbp,r8r15

16 个寄存器的低位部分都可以作为字节、字、双字、四字来单独访问。分别表示为 al, ax, eax, rax。

低位操作的规则:

  1. 将寄存器作为目标位置时,生成字节和字的指令会保持剩下的字节不变
  2. 生成双字的指令会把高位四字节置为 0.

16个寄存器的作用

  1. rax:返回值
  2. rsp:栈指针
  3. rdi, rsi, rdx, rcx, r8, r9:第 1 到第 6 个参数
  4. rbx, rbp, r12~r15:被调用者保存
  5. r10, r11:调用者保存

3.4.1 操作数指示符

指令的操作数有三种类型:立即数寄存器内存引用

最常用的寻址方式:Imm(rb, ri, s):Imm + rb + ri*s

s 为比例因子,只能是 1,2,4,8 中的某一个

3.4.2 数据传送指令

mov类

mov 只会更新目的操作数指定的寄存器字节或内存位置。

mov 类是最简单的数据传送指令,mov 类有 5 种:

  1. movb, movw, movl:传送字节、字、双字
  2. movq:传送四字。如果源操作数是立即数,只能是双字,然后符号扩展到四字(假的四字)
  3. movabsq:传送绝对的四字。只能以立即数作为源操作数,以寄存器为目的。可以传送任意 64 位立即数。

movq 用来传送寄存器和内存引用中的四字,movabsq 用来传送四字的立即数

mov 类的源操作数和目的操作数不能同时为内存,即不能将值从内存复制到内存。

mov 指令中寄存器的大小必须与 mov 的后缀字符大小匹配。

​ movb $-17, %al

movz类

movz 系列和 movs 系列可以把较小的源值复制到较大的目的,目的都是寄存器

movz 将目的寄存器剩余字节做零扩展,movs 做符号扩展

movz类:movzbw, movzbl, movzbq, movzwl, movzwq(movzbw 即从字节复制到字,其他类似)

movs类:movsbw, movsbl, movsbq, movswl, movswq, movslq, cltq

  • cltq:没有操作数,将 eax 符号扩展到 rax,等价于 movslq %eax,%rax

3.4.3 数据传送示例

局部变量通常保存在寄存器中。

函数返回指令 ret 返回的值为寄存器 rax 中的值

强制类型转换是通过 mov 指令实现的。

当指针存在寄存器中时,a = *p 的汇编指令为: mov (rdi), rax

3.4.4 压入和弹出栈数据

栈向下增长,栈顶的地址是栈中元素地址中最低的。栈指针 rsp 保存栈顶元素的地址。

出入栈指令:

  • pushq rax:压栈,栈指针减 8 并将 rax 中的值写入新的栈顶地址,等价于:subq $8, (rsp) ; movq rax,(rsp)。
  • popq rax:出栈,栈指针加 8 并将出栈的值写入 rax 中,等价于:movq (rsp),rax ; add $8,(rasp)

使用 mov 指令和标准的内存寻址方法可以访问栈内的任意位置,而非仅限于栈顶。

3.5 算术和逻辑操作

x86-64 的每个指令类都有对应四种不同大小数据的指令

算术和逻辑操作共有四组:

  1. 加载有效地址

    1. leaq S, D:将 S 的地址保存到 D 中,D 必须是寄存器
  2. 一元操作

    1. inc D: D+1
    2. dec D: D-1
    3. neg D:取负
    4. not D:取补
  3. 二元操作(加减乘,与或异或,没有除法)

    1. add s, d: d=d+s
    2. sub s, d: d=d-s
    3. imul s, d: d=d*s 乘
    4. xor s, d: d=d^s 异或
    5. or s, d: d=d|s 或
    6. and s,d: d=d&s 与
  4. 移位

    1. sal k,d: d=d<
    2. shl k,d: d=d<
    3. sar k,d: d=d<
    4. shr k,d: d=d<

3.5.1 加载有效地址

leaq 实际上是 movq 指令的变形。操作是从内存读数据地址到寄存器。

leaq 在实际应用中常常不用来取地址,而用来计算加法和有限形式的乘法

​ leaq 9(rdi, rsi, 4), rax;//x in rdi,y in rsi。此操作实际上等于将 x+4*y+9 的结果存入 rax

3.5.2 一元和二元操作

一元操作中的操作数既是源又是目的。

二元操作中的第二个操作数既是源又是目的。

因为不能从内存到内存,因此当第二个操作数是内存地址时,要先从内存读出值,执行操作后再把结果写回去。

注意 sub s,d 是 d-s 而不是 s-d

3.5.3 移位操作

移位操作的移位量可以是一个立即数或放在单字节寄存器 cl 中。

当移位量大于目的数的长度时,只取移位量低字节中的值(小于目的数长度)来作为真实的移位量。

3.5.4 特殊的算术操作

两个 64 位数的乘积需要 128 位来表示,x86-64指令集可以有限的支持对 128 位数的操作,包括乘法和除法。

128 位数需要两个寄存器来存储,移动时也需要两个 movq 指令来移动。

这种情况对于有符号数和无符号数采用了不同的指令。

3.6 控制

条件语句、循环语句、分支语句都要求有条件的执行。

机器代码提供两种低级机制来实现有条件的行为:

  1. 测试数据值,然后根据测试的结果来改变控制流或数据流
  2. 使用 jump 指令进行跳转

3.6.1 条件码

条件码寄存器都是单个位的,是不同于整数寄存器的另一组寄存器。

条件码描述了最近的算术或逻辑操作的属性,可以通过检测这些寄存器来执行条件分支指令。

常用条件码:

  1. CF:进位标志。最近的操作使最高位产生了进位。可以用来检查无符号数的溢出
  2. ZF:零标志。最近的操作的结果为 0
  3. SF:符号标志。最近的操作的结果为负数。
  4. OF:溢出标志。最近的操作导致了补码溢出

除了 leaq 指令外,其余的所有算术和逻辑指令都会根据运算结果设置条件码

此外还有两类特殊的指令,他们只设置条件码不更新目的寄存器:

  • cmp s1, s2:除了不更新目的寄存器外与 sub 指令的行为相同
  • test s1, s2:除了不更新目的寄存器外与 and 指令的行为相同

3.6.2 访问条件码

条件码一般不直接读取,常用的使用方法有 3 种:

  1. 根据条件码的某种组合,使用 set 指令类将一个字节设置为 0 或 1。
  2. 条件跳转到程序的某个其他部分
  3. 有条件地传送数据

set 指令类

set 指令的目的操作数是低位单字节寄存器元素或一个字节的内存位置。set 会将该字节设置为 0 或 1

set 指令类的后缀指明了所考虑的条件码的组合,如 setl (set less) 表示“小于时设置”

0

注意到上图中,set 指令对于大于、小于的比较分为了有符号和无符号两类。

大多数时候,机器代码对无符号和有符号两种情况使用一样的指令。

使用不同指令来处理无符号和有符号操作的情况:

  1. 不同的条件码组合:
  2. 不同版本的右移:sar 和 shr
  3. 不同的乘法和除法指令

汇编语言中数据本身不区分有符号和无符号,通过不同的指令来区分有符号操作和无符号操作。

注意在汇编代码中,8字节的操作数可能是 long,long long 或 指针

3.6.3 跳转指令

跳转指令的目的地由一个标号指明

​ jmp .L1 ;//跳转到 .L1 。在实际的跳转指令中,.L1 会直接编码为跳转目标的地址。 movq (rax),rdx .L1: popq rdx

jmp 可以是直接跳转,即操作数为标号。也可以间接跳转,即操作数是寄存器或内存引用,这种情况下跳转到寄存器中存储的地址处。

跳转指令分为有条件跳转无条件跳转,只有 jmp 是无条件跳转。有条件跳转都只能是直接跳转。

有条件跳转类似 set 指令系列,根据条件码寄存器的值来判断是否进行跳转。

0

3.6.4 跳转指令的编码

跳转指令的机器编码(就是纯粹数字表示的机器语言)有几种方式,其中两种如下:

  • PC 相对跳转:使用目标地址与跳转指令之后下一条指令的地址之间的差来编码。可以用 1、2 或 4 个字节来编码。
  • 绝对地址编码:使用目标的绝对地址。用 4 个字节直接指出。

汇编器和链接器会自己选择适当的编码方式

3.6.5 用条件控制来实现条件分支

汇编代码层面的条件控制类似于 c 语言的 goto 语句。

汇编语言使用条件码和条件跳转来起到和 c 语言中 if 相似的作用

​ ‘C 语言’ if( x<y ) { i++ } else { i– } ‘汇编’ cmpq rsi,rdi jge .L2 incl rax; .L2: decl rax;

csapp_DataLab

tip:全文引用部分皆如此段标识所示 摘处后附

**1、综述:留张图 其他的这里就不提了嗷**(看v5比赛去咯

img

image-20220711210344347

可以细看大佬笔记orz[读书笔记]CSAPP:2[B]计算机系统漫游 - 知乎 (zhihu.com)

2、信息的表示和处理

先列出个人觉得重要的几个知识点:

  • 计算机使用二值信号存储和表示信息
  • 当计算结果太大以至于不能表示时,就会产生溢出
  • 浮点数表示的精度有限,因而浮点运算是不可结合的。
  • 整数的表示范围小但是精确,浮点数表示的范围大但是是近似的。
  • 许多安全漏洞是由算术运算的微妙细节导致的。
  • 模运算相当于忽略某些位的值,比如计算x mod 8,则只保留x低三位的值。
  • 比较有符号数和无符号数之间的大小,先将其转化为二进制编码,再根据特定编码计算比较。
  • 所有性质都可以直接通过二进制以及对应的编码去解释,首先考虑二进制的值是什么。
  • 补码和无符号数对应的位向量,在计算结果上都是相同的,大部分计算机中使用相同的机器指令进行计算,所以可以先将补码转换为无符号数进行推导,再转为补码会更容易。
  • 加法逆元的求法就是通过溢出实现的,比如求x的加法逆元,直接计算~x+1,就能保证有效位里都是0。
  • 尽量不使用无符号数。
阅读全文 »