Recover - CS50x 2023
实现一个程序,从取证镜像中恢复 JPEG 文件,如下。
背景
为了准备这个问题,我们花了几天时间在校园里拍照,所有照片都以 JPEG 格式保存在数码相机的存储卡上。不幸的是,我们不知何故删除了所有照片!幸运的是,在计算机世界中,“删除”通常不是真的删除了,而是更像是被“遗忘”了。即使相机显示卡是空白的,但我们非常确定这并非完全正确。 事实上,我们希望你能编写一个程序来帮我们恢复照片!我们对此充满期待!
即使 JPEG 比 BMP 更复杂,JPEG 也有“签名”,即可以将其与其他文件格式区分开来的字节模式。具体来说,JPEG 的从左到右,前三个字节是
同时,第四个字节可能是以下值之一:0xe0
、0xe1
、0xe2
、0xe3
、0xe4
、0xe5
、0xe6
、0xe7
、0xe8
、0xe9
、0xea
、0xeb
、0xec
、0xed
、0xee
或 0xef
。换句话说,第四个字节的前四位是 1110
。
很有可能,如果在已知存储照片的介质(例如,我的存储卡)上找到这四个字节的模式,它们就标志着 JPEG 的开始。 您可能会纯粹偶然地在某些磁盘上遇到这些模式,因此数据恢复并非一门绝对精确的科学。
幸运的是,数码相机倾向于将照片连续存储在存储卡上,每张照片都紧接在之前拍摄的照片之后存储。因此,一个JPEG的开始通常意味着前一个JPEG的结束。但是,数码相机通常使用 FAT 文件系统初始化卡,该文件系统的“块大小”为 512 字节 (B)。也就是说,相机每次写入都是 512 B 的倍数。因此,一张 1 MB(即 1,048,576 B)的照片在存储卡上占用 1048576 ÷ 512 = 2048 个“块”。但是,一张小一个字节的照片(即 1,048,575 B)也是如此!磁盘上浪费的空间称为“slack space”(剩余空间/空闲空间)。取证调查员经常查看剩余空间,以寻找可疑数据的痕迹。 所有这些细节意味着,作为调查员的你,应该能够编写一个程序来遍历我的存储卡副本,寻找 JPEG 的签名。每次找到签名时,就打开一个新文件写入,并将存储卡中的字节写入该文件,直到遇到下一个签名为止,再关闭该文件。此外,为了提高效率,与其一次读取一个字节,不如一次性读取 512 个字节到缓冲区。由于 FAT 文件系统的特性,你可以确信 JPEG 的签名是“块对齐”的。也就是说,你只需要在每个块的起始四个字节中寻找这些签名。
当然,需要意识到 JPEG 文件可能会跨越多个连续的块。否则,JPEG 文件的大小不可能超过 512 字节。但是,JPEG 文件的最后一个字节不一定落在块的末尾。记住,可能存在空闲空间。因为我开始拍照时,这张存储卡是全新的,所以很可能已经被制造商“清零”(即全部填充为 0),在这种情况下,所有空闲空间都会被填充为 0。即使恢复的 JPEG 文件末尾包含这些尾随的 0 也没关系,它们通常仍然可以正常查看。
现在,我只有一张存储卡,但你们有很多!因此,我已经创建了这张存储卡的“取证镜像”,并将其内容以字节为单位存储在一个名为 card.raw
的文件中。为了避免你浪费时间不必要地遍历数百万个 0 字节,我只镜像了存储卡的前几个兆字节。但你最终应该能从该镜像中恢复出 50 个 JPEG 文件。
开始
登录 cs50.dev,点击终端窗口,然后输入 cd
并回车。你应该会看到类似下面的终端提示符:
接下来,执行
wget https://cdn.cs50.net/2022/fall/psets/4/recover.zip
这样你就可以将名为 recover.zip
的 ZIP 文件下载到你的 codespace 中。
然后执行
来创建一个名为 recover
的文件夹。创建完成后,你就不再需要该 ZIP 文件了,所以可以执行
并在提示符后输入 "y" 并按回车键,删除你下载的 ZIP 文件。
现在输入
然后按回车键进入该目录。你的提示符现在应该类似于以下内容。
输入 ls
并回车,你应该会看到两个文件:recover.c
和 card.raw
。
规范
编写一个名为 recover
的程序,用于从取证镜像中恢复 JPEG 文件。
- 你应该在
recover
目录下编写recover.c
文件来实现你的程序。 - 你的程序应该接受且仅接受一个命令行参数,即用于恢复 JPEG 图像的取证镜像文件名。
- 如果程序未能接收到恰好一个命令行参数,则应提醒用户正确用法,并且
main
函数应返回1
。 - 如果取证镜像文件无法打开读取,程序应告知用户,并且
main
函数应返回1
。 - 程序生成的文件应命名为
###.jpg
,其中###
是一个三位十进制数,从000
开始递增。 - 如果程序中使用了
malloc
函数,则必须保证没有内存泄漏。
详细步骤
用法
你的程序应该按照以下示例运行。
$ ./recover
Usage: ./recover IMAGE
其中 IMAGE
代表取证镜像文件名。例如:
提示
请注意,如果 argv[1]
存在,你可以像下面这样使用 fopen
以编程方式打开 card.raw
文件。
FILE *file = fopen(argv[1], "r");
程序执行后,应从 card.raw
文件中恢复所有 JPEG 图像,并将它们保存到当前工作目录下。程序应将输出文件命名为 ###.jpg
,###
为从 000
开始的三位十进制数字。建议熟悉 sprintf
函数,它能将格式化后的字符串存储到指定的内存地址。考虑到 JPEG 文件名必须符合 ###.jpg
格式,你应该为存储该字符串分配多少字节的内存?(别忘了结尾的空字符!)
你无需尝试恢复 JPEG 的原始名称。要验证程序恢复的 JPEG 图像是否正确,只需双击打开查看即可。如果图像显示完整,则说明程序很可能执行成功。
然而,你的程序初次运行时很可能无法正确恢复 JPEG 图像。(如果打开后一片空白,则说明图像未被正确恢复。) 可以执行以下命令来删除当前工作目录下的所有 JPEG 文件。
如果你不想在每次删除时都收到确认提示,请改为执行以下命令。
但请小心使用 -f
开关,因为它会在不提示你的情况下“强制”删除。
如果需要定义一个新的字节类型,可以使用以下方式:typedef uint8_t BYTE;
其中 uint8_t
是 stdint.h
中定义的 8 位无符号整数类型。
此外,请注意,您可以使用 fread
从文件中读取数据,它会将文件中的数据读取到内存中。查阅其手册页可知,如果 card.raw
包含若干 512 字节的块,fread
应该返回 512
或 0
。要读取 card.raw
中的所有块,在用 fopen
打开文件后,可以使用以下循环:
while (fread(buffer, 1, BLOCK_SIZE, raw_file) == BLOCK_SIZE)
{
}
这样,一旦 fread
返回 0
(即假 (false)),您的循环就会结束。
测试
运行以下命令,使用 check50
评估代码的正确性。但请务必自己编译并测试它!
check50 cs50/problems/2023/x/recover
运行以下命令,使用 style50
评估代码的风格。
如何提交
在终端中运行以下命令来提交你的作业。
submit50 cs50/problems/2023/x/recover