软件安全实验_1


实验1 磁盘结构与文件系统

实验目的

  • 加深对 FAT32 分区及文件系统格式的理解

  • 掌握借助 WinHex 等工具手工定位磁盘文件数据的技能

  • 通过开发一个磁盘文件数据提取工具,强化编程实践能力。

实验内容

内容一:手工定位和提取 FAT32 分区中的文件数据

在 FAT32 分区下创建一个不小于 10K 的文件,文件名称用本组组长姓名的拼

音命名。根据课上介绍的 FAT32 分区及文件系统知识,必要时自行上网查阅

相关资料,借助 WinHex 或其它十六进制工具,以手工方式从磁盘中逐一找

到该文件的各个存储扇区,复制其中的有效内容,并拼接组合成一个与原文

档内容相同的完整文档。

要求:操作过程描述中,要展示带有组长姓名字样的截图。

内容二:编程实现“内容一”的全过程

输入:某个文件(A)的路径

输出:

(1)该文件的短文件名目录项信息

(2)该文件的簇链

(3)根据上述的文件簇链,从磁盘上提取数据并拼接而得的新文件(B)

(4)文件 A 与文件 B 内容的比较结果(要求二者完全一致)

实验工具和编程语言

实验工具:Winhex、VSCode

编程语言:Python 3.9.12

实验步骤

内容一:

提前在新分区的 F 盘上创建 zqb.txt,并填入内容,文件大小 10K。

打开 Winhex,并且打开 F 磁盘。如下图,可得 BPB 中的重要字段:

·每扇区字节数:0x0200

·每簇扇区数:0x08

·保留扇区数:0x100E

·FAT 的个数:2

·每个 FAT 占用的扇区数:0x000007F9

·BPB_RootClus(根目录首簇号):0x02

故 FirstSectorofRoot=保留扇区数 + (FAT 个数 * 每个 FAT 所占扇区数)

=0x100E+(2*0x07F9)=0x2000

定位到 0x2000*0x200=0x400000 根目录处,找到 ZQB.TXT 在 0x4000E0 处。

参考此表可得

·起始簇号的高 16 位=0x0001 ·起始簇号的低 16 位=0x022A

计算得起始簇号=0x0001022A

定位到 FAT1,即保留扇区数每扇区字节数=0x100E0x0200=0x201c00

起始簇号=0x0001022A,簇大小 4B,故位于 4*0x0001022A+0x201c00=0x2424A8

故起始簇的下一簇为 0x0001022B,下一簇=0x0001022C,再下一个为

0x0FFFFFFF,即最后一簇。

最后来定位文件,计算文件初始簇的偏移。根据下图的公式可得,

文件某簇的偏移 = 根目录首簇偏移 + (文件某簇号 - 根目录首簇号)*每簇

扇区数

zqb.txt 文件初始簇的偏移

=0x400000+(0x0001022A-0x2)80x200=0x10628000

可以发现,已经定位到 zqb.txt 文件。

复制其中的有效内容拼接组合成一个与原文档内容相同的完整文档。

最后可成功打开

内容二:

程序主要设计思路

路径分割,打开磁盘

将输入的绝对路径进行分割,分割成一个一个的子文件(不区分文件和目录,

因为在磁盘中,目录其实就是特殊的文件)。

例如,对于输入的路径 F:/lab/123/test.txt,会将其分割成[“F:”, “lab”, “123”,“test” ]

读取磁盘

BPB 中的数据,主要数据如下

读取目录项

读取目录项时,要注意长文件名的单独处理,对于文件名大于 8 字节或者

文件后缀大于 3 字节的为长文件名。

首先由公式:FAT32 分区根目录项=保留扇区大小+文件分配表大小*文件分配

表个数 定位到根目录项。

然后通过根目录可以读取到一级目录的目录项,然后就可以通过一个循环去

读取二级、三级等目录项。

最后找到最后一级目录项,然后定位文件的首簇号,再定位其簇链(实验一

已经说明),最后就可以通过簇链定位文件内容。

读取文件内容并写入新文件

在第 3 步中,通过起始簇号可以在 FAT 中将文件的整个簇链找出来,然后就可以直接定位文件内容了。

读取文件内容时,要注意磁盘给文件分配存储空间时,是以簇为单位进行分配的。这也就是说文件的实际大小不一定就为操作系统给其分配的大小,所以这里在最后一簇时,要进行判断。

程序使用说明

考虑到文件路径可能过长,所以只需要将文件路径替换成自己的路径即可,

注意文件路径应当是绝对路径,并且使用 “\” 进行分割,比如:F:/lab/123/test.txt

编程语句使用 python,所以无需编译,直接运行即可,如果出现语法错误,

可以是由于 python 版本导致,笔者使用的是 Python 3.9.12

程序测试结果

注意,如果文件目录过深,分析过程可能比较长,请耐心等待

测试 1:

输入文件内容及其路径:路径包含 6 级目录,并且包含长文件名及短文件名

输出结果其新文件内容:

新旧文件对比:

测试 2:

输入文件内容及其路径:文件在根目录下,为短文件

输出结果:

新旧文件对比:

程序代码

import os

# 打印二进制数据, 方便 debug
def binary_dump(s, length=1000000000000000):
    print(" "*(len(hex(len(s)))+2), end="")
    for i in range(32):
        if i % 8 == 0 and i != 0: print(" ", end="")
        print(str.upper(hex(i)[2:].zfill(2)), end=" ")
    print("\n      "+"-"*98)
    print(hex(0).rjust(len(hex(len(s))), " "), end=": ")
    for i in range(len(s)):
        if i >= length: break
        if i % 8 == 0 and i != 0: print(" ", end="")
        if i % 32 == 0 and i != 0:
            print("")
            print(hex(i).rjust(len(hex(len(s))), " "), end=": ")
        print(str.upper(hex(ord(s[i]))[2:].zfill(2)), end=" ")
    print("")

# 字符串转换为 16 进制数据
def str_to_hex(s):
    return [hex(ord(i)) for i in s]

# 将 16 进制按照指定大小端进行转换
def hex_to_num(s, endian="little"):
    hex_num, num = [int(i, 16) for i in s], 0
    if endian == "big":
        hex_num = [i for i in reversed(hex_num)]
#    print(hex_num)
    for i in range(len(hex_num)):
        num |= hex_num[i] << (i*8)
    return num

# 读取基本数据
def read_BPB(disk):
    DBR = disk.read(512).decode(encoding='latin-1')
    disk.seek(0)
#    print(DBR)
#    binary_dump(DBR)
    DBR_hex = str_to_hex(DBR)
    byte_per_sector = hex_to_num(DBR_hex[0xb:0xb+2])
    sector_per_clust = hex_to_num(DBR_hex[0xd:0xd+1])
    saved_sector = hex_to_num(DBR_hex[0xe:0xe+2])
    fat_nums = hex_to_num(DBR_hex[0x10:0x10+1])
    sector_per_fat = hex_to_num(DBR_hex[0x24:0x24+4])
    root_clust_idx = hex_to_num(DBR_hex[0x2c:0x2c+4])
    """
    print("byte_per_sector: ", hex(byte_per_sector))
    print("sector_per_clust: ", hex(sector_per_clust))
    print("saved_sector: ", hex(saved_sector))
    print("fat_nums: ", hex(fat_nums))
    print("sector_per_fat: ", hex(sector_per_fat))
    print("root_clust_idx: ", hex(root_clust_idx))
    """
    return byte_per_sector, sector_per_clust, saved_sector, fat_nums, sector_per_fat, root_clust_idx

# 寻找目录项
def find_file_dir_entry(filename, root_dir_entry_offset, disk, suffix="", max_len = 0x10000000):
    k = 0
    # 短文件名
    if len(filename) <= 8 and len(suffix) <= 3:
        while True:
            dentry = disk.read(root_dir_entry_offset+0x20*k)
            disk.seek(0)
        #    print(dentry[-0x20:])
            dentry = dentry.decode(encoding='latin-1')[-0x20:]
            k += 1
        #    binary_dump(dentry)
        #    print("======================================")
        #    print(str(dentry[:len(filename)]))
        #    print(str(dentry[8:8+len(suffix)]))
        #    print(dentry[0])
            if str(dentry[:len(filename)]) == str.upper(filename): 
                #print("good")
                if len(suffix) == 0 or str(dentry[8:8+len(suffix)]) == str.upper(suffix):
                #    print("good")
                    break
            if k > max_len:
                return [], False
        return dentry, True
    # 长文件名
    else:
        continue_flag = False
        idx = [1, 3, 5, 7, 9, 14, 16, 18, 20, 22, 24, 28, 30]
        length = len(filename)+len(suffix) 
        dentry_num = length // 13 + 1
        first_num = length % 13
        if len(suffix) != 0: file_name = filename+"."+suffix
        else: file_name = filename
        file_name_list = []
        for i in range(dentry_num-1):
            file_name_list.append(file_name[i*13:i*13+13])
        if first_num: file_name_list.append(file_name[(dentry_num-1)*13:])
        file_name_list = [i for i in reversed(file_name_list)]
        #print(file_name_list)
        #print(first_num)
        while True:
            if k > max_len:
                return [], False
            continue_flag = False
            dentrys_origin = disk.read(root_dir_entry_offset+0x20*(k+dentry_num+1))
            disk.seek(0)
            dentrys_origin = dentrys_origin.decode(encoding='latin-1')[-0x20*(dentry_num+1):]
            #binary_dump(dentrys_origin)
            dentrys = [dentrys_origin[i*0x20:i*0x20+0x20] for i in range(dentry_num+1)]
            for i in range(len(dentrys)): dentrys[i] = str_to_hex(dentrys[i])
            #print(dentry_num)
            if int(dentrys[0][0], 16) == 0xe5:
                k += 1
                continue
            if first_num:
                for i in range(first_num):
                    #print(str.upper(file_name_list[0][i]))
                    #print(str.upper(chr(int(dentrys[0][idx[i]], 16))))
                    if str.upper(chr(int(dentrys[0][idx[i]], 16))) != str.upper(file_name_list[0][i]):
                        continue_flag = True
                        break
                   #else:
                        #print(str.upper(chr(int(dentrys[0][idx[i]], 16))), end="")
                        #print("good")
            if first_num: m = 1 
            else: m = 0
            for i in range(m, dentry_num):
                for j in range(13):
                    if str.upper(chr(int(dentrys[i][idx[j]], 16))) != str.upper(file_name_list[i][j]):
                        continue_flag = True
                        break
                    #else:
                        #print(str.upper(chr(int(dentrys[i][idx[j]], 16))), end="")
            
            if continue_flag == True: 
                k += 1
                continue
            else:
            #    binary_dump(dentrys_origin)
            #    print("good")
                break

    #    print(dentrys)
    #    print(file_name)
    #    print(dentrys_origin)
    #    print(dentrys_origin[dentry_num*0x20:])
        return dentrys_origin[dentry_num*0x20:], True

# 通过目录项获取起始簇号
def get_clust_idx_and_file_size_by_dentry(dentry):
    dentry_hex =  str_to_hex(dentry)
#    print(dentry_hex)
    high = dentry_hex[0x14:0x14+2]
    low = dentry_hex[0x1a:0x1a+2]
    clust_idx_hex = low + high
#    print(high)
#    print(low)
#    print(clust_idx_hex)
    clust_idx = hex_to_num(clust_idx_hex)
    file_size = hex_to_num(dentry_hex[0x1c:0x1c+4])
#    print(hex(clust_idx))
    return clust_idx, file_size

# 通过起始簇号在 FAT 表中找出簇链
def get_clust_list_by_fat(clust_idx, fat_offset, fat_size, disk):
    clust_idx_list, flag = [], True
    fat = disk.read(fat_offset+fat_size)[fat_offset:].decode(encoding='latin-1')
    disk.seek(0)
#    binary_dump(fat, 0x100)
    fat_hex = str_to_hex(fat)
    while True:
    #    print(hex(clust_idx))
        clust_idx_list.append(clust_idx)
        clust_idx = hex_to_num(fat_hex[clust_idx*4:clust_idx*4+4])
        if clust_idx == 0xffffff7:
            print("出现坏簇{0}".format(hex(clust_idx)))
            break
        if clust_idx == 0xfffffff or clust_idx == 0xffffff8: break
        if clust_idx >= 2 and clust_idx <= 0xfffffef:
            continue
        else:
            break

    return clust_idx_list

# 找目录项, 其实就是对 find_file_dir_entry 的一个封装, 这里我只是为了方便而已
def find_sub_dir_clust_idx_list(dir_name, parent_clust_idx_list, root_dir_entry_offset, root_clust_idx, sector_per_clust, byte_per_sector, disk, suffix=""):
    flag, dentry = False, ""
    for clust_idx in parent_clust_idx_list:
        if flag: break
        """
        file_clust_offset = root_dir_entry_offset + (clust_idx - root_clust_idx)*sector_per_clust*byte_per_sector
#        print("file_clust_offset:", hex(file_clust_offset))
        content = disk.read(file_clust_offset+sector_per_clust*byte_per_sector)[file_clust_offset:].decode(encoding='latin-1')
        disk.seek(0)
        for i in range(0, len(content), 0x20):
            dentry = content[i:i+0x20]
            if str(dentry[:len(dir_name)]) == str.upper(dir_name):
                flag = True
                break
        """
        dir_offset = root_dir_entry_offset + (clust_idx - root_clust_idx)*sector_per_clust*byte_per_sector
        max_len = sector_per_clust*byte_per_sector // 0x20
        dentry, flag = find_file_dir_entry(dir_name, dir_offset, disk, suffix, max_len)
    return dentry

# 读取文件内容并写入新文件
def read_file_by_clust_list(filename, suffix, clust_idx_list, file_size, root_clust_idx, root_dir_entry_offset, sector_per_clust, byte_per_sector, disk):
    with open(filename+"2."+suffix, "wb") as f:
        for i in range(len(clust_idx_list)):
            clust_idx = clust_idx_list[i]
            file_clust_offset = root_dir_entry_offset + (clust_idx - root_clust_idx)*sector_per_clust*byte_per_sector
            content = disk.read(file_clust_offset+sector_per_clust*byte_per_sector)[file_clust_offset:]
            disk.seek(0)
            if i == len(clust_idx_list) - 1 and (file_size % (sector_per_clust*byte_per_sector)) != 0:
                content = content[:file_size-sector_per_clust*byte_per_sector*(len(clust_idx_list)-1)]
            f.write(content)
    print("\n\"{0}\" 的文件内容已经存入 \"{1}\"\n".format(filename+"."+suffix, filename+"2."+suffix))

def run(filename):
    file_list_str = str(filename).split("/")
    file_list_str[-1], suffix = file_list_str[-1].rsplit(".", maxsplit=1)[0], file_list_str[-1].rsplit(".", maxsplit=1)[1]
#    print(file_list_str)
#    print(suffix)

    disk = open(r'\\.\\' + file_list_str[0], 'rb')
    byte_per_sector, sector_per_clust, saved_sector, fat_nums, sector_per_fat, root_clust_idx = read_BPB(disk)
    root_dir_entry_offset = (saved_sector + fat_nums*sector_per_fat) * byte_per_sector
#    print(file_list_str)
#    print("root_dir_entry_offset: ", hex(root_dir_entry_offset))
    if len(file_list_str) == 2:
        dentry, _ =  find_file_dir_entry(file_list_str[1], root_dir_entry_offset, disk, suffix)
    else:
        dentry, _ =  find_file_dir_entry(file_list_str[1], root_dir_entry_offset, disk)
#    binary_dump(dentry)

    clust_idx, file_size = get_clust_idx_and_file_size_by_dentry(dentry)
#    print("clust_idx: ", hex(clust_idx))
#    print("file_size: ", hex(file_size))

    fat_offset = saved_sector * byte_per_sector
    fat_size = fat_nums * sector_per_fat * byte_per_sector
#    print("fat_offset: ", hex(fat_offset))
    clust_idx_list = get_clust_list_by_fat(clust_idx, fat_offset, fat_size, disk)

    for i in range(2, len(file_list_str)):
        if i == len(file_list_str)-1:
            dentry = find_sub_dir_clust_idx_list(file_list_str[i], clust_idx_list, root_dir_entry_offset, root_clust_idx, sector_per_clust, byte_per_sector, disk, suffix)
        else:
            dentry = find_sub_dir_clust_idx_list(file_list_str[i], clust_idx_list, root_dir_entry_offset, root_clust_idx, sector_per_clust, byte_per_sector, disk)
        #binary_dump(dentry)
        clust_idx, file_size = get_clust_idx_and_file_size_by_dentry(dentry)
        #print("{0} 2".format(hex(clust_idx)))
        clust_idx_list = get_clust_list_by_fat(clust_idx, fat_offset, fat_size, disk)
        #print("{0} 3".format(i))
    
    print("\"{0}\" 的短文件名目录项为: \n".format(filename), end="")
    binary_dump(dentry)
    print("     起始簇号为: ", hex(clust_idx))
    print("     实际文件长度为: ", hex(file_size))

    # 输出文件簇链
    print("\n\"{0}\" 的簇链为: \n     ".format(filename), end="")
    for i in clust_idx_list:
        print(hex(i), end=" ==> ")
    print("0xfffffff(结束标志)")

    read_file_by_clust_list(str(filename).rsplit(".", maxsplit=1)[0], suffix, clust_idx_list, file_size, root_clust_idx, root_dir_entry_offset, sector_per_clust, byte_per_sector, disk)
    

if __name__ == "__main__":
    
#    filename = input("请输入正确格式的\033[31m绝对文件路径, 其中路径用 \"/\" 进行分割\033[0m, 如 \033[32m\"F:/lab/123/test.txt\"\033[0m : ")
#    filename = "F:/321/123/456/789/zbp_c.txt"
#    filename = "F:/123456789ABCDEFJLMOPQRESUVWXYZ.txt"
#    filename = "F:/PPPP.docx"
#    filename = "F:/GFEDCBA9876543210/132/ABCDEFHJHQDJHHJJHHHHHD/132dsa1d321s32a1dqw5/123456789ABCDEFJLMOPQRESUVWXYZ.txt"
#    filename = "F:/GFEDCBA9876543210/132/xx.txt"    
    
    # 将 filename 替换成自己的文件即可
    # filename = "F:/GFEDCBA9876543210/132/ABCDEFHJHQDJHHJJHHHHHD/132dsa1d321s32a1dqw5/xxx/123456789ABCDEFJLMOPQRESUVWXYZ.docx"
    filename = "F:/zbp_c.txt"
    print("请注意, \033[31m文件路径必须是绝对路径, 且用 \"/\" 进行分割\033[0m, 如 \033[32m\"F:/lab/123/test.txt\"\033[0m")
    if os.path.isfile(filename):
        print("正在尝试解析该文件, 请耐心等待......")
        run(filename)
    else:
        print("\"{0}\" 文件不存在".format(filename))

声明

感谢组长大佬zqb的带领,没有他我肯定是不能独立完成实验全部内容。


Author: 寒风渐微凉
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source 寒风渐微凉 !
 Previous
Next 
  TOC