第 15 章
数据清理程序编程
介绍
我们已经讨论过,当您从磁盘删除任何文件时,信息不会从磁盘中完全删除,但会被标记为可用于向其中写入新数据。
当我们格式化磁盘时,有关磁盘的文件和目录的所有信息(例如 FAT 和根目录记录)都将被删除,但数据区保持不变,并且磁盘数据区中的任何内容都不会被删除。被操作系统删除或格式化的数据仍保留在数据区域,可以通过一些数据恢复工作和数据恢复软件进行恢复。
因此,需要从磁盘彻底删除数据就导致了需要一个从磁盘彻底擦除数据的程序。要做到这一点,仅仅删除文件或格式化磁盘是不够的,必须用其他数据覆盖磁盘上的数据。
用于从磁盘彻底删除数据的程序称为数据删除程序。这些程序将随机字符写入数据区以覆盖数据并擦除先前存储在磁盘上的所有信息。
 
当数据完全无法恢复时
要擦除数据,必须用其他数据覆盖磁盘上的数据区域,但问题还没有结束。进一步复杂化的是,磁盘会记住已被覆盖的数据,这要求用随机数据序列多次覆盖数据,因此即使使用复杂的数据恢复工具也无法恢复。
这是因为如今已经有技术能够实现即使使用一些简单的数据删除工具也能恢复数据。
一些数据擦除产品用二进制零和二进制一覆盖数据。写入一系列二进制零和二进制一可提供最深的重写效果,因为这些值分别是最小和最大磁值。
虽然这是理想的数据擦除程序的理论,但通常情况下,用随机 ASCII 字符覆盖数据就足够了。之所以这么说,是因为使用复杂的恢复工具和技术进行恢复不能用于任何组织进行常规数据恢复,因为这些技术非常昂贵,即使一次恢复也要花费数百万美元。不仅如此,这些技术在全世界只有少数国家拥有。
我们将仅讨论简单的数据覆盖以从磁盘中删除数据。但是,您只需稍加努力就可以进一步修改相同的程序,使其只写入随机字符。用这种方法抹去的数据也无法通过任何数据恢复软件恢复。
 
为什么数据删除如此重要?
当我们讨论数据恢复方法时,我们向用户保证可以通过一些常规或特殊的数据恢复方法恢复数据。但数据恢复并不总是每个人都希望和期待的功能。
可能有许多人或组织随时准备以某种方式擦除其磁盘上的数据,使得数据无法以任何方式恢复。在这种情况下,磁盘上可能之前包含非常敏感的数据,如果落入坏人之手,可能会因不当使用这些信息而对组织或用户造成损害。
众所周知,对硬盘空间的需求每天都在增长。因此,几乎每个组织每年都会大规模地用新的高容量驱动器替换旧的低容量驱动器。如果这些旧磁盘落入坏人之手,可能会给该组织带来非常严重的问题。
根据 CNET News.com 于 2003 年 1 月 16 日发布的一则新闻报道,麻省理工学院的学生 Simon Garfinkel 和 Abby Shelat 从互联网和其他二手硬盘销售渠道购买旧硬盘用于研究目的,以获取大量人们懒得删除的个人信息。
他们花费约 1000 美元购买了 158 个驱动器后,成功收集了超过 5000 个信用卡号、医疗记录、详细的个人和公司财务信息以及数 GB 的电子邮件、源代码和其他信息。
两位学生将他们的研究结果汇编成一份报告,题为“传递数据的记忆:磁盘清理研究”,发表在《IEEE 安全与隐私》二月份期刊上。
研究得出的要点是,二手硬盘市场充斥着个人信息,恶意买家很容易冒充他人身份。
 
编写非破坏性数据擦除程序
非破坏性数据擦除器是一种数据擦除程序,通过使用它我们可以擦除磁盘卷的整个“未分配空间”,而不会以任何方式损害存储在磁盘中的数据。
此类数据擦除程序适用于以下情况:您想要擦除磁盘卷的所有未分配空间,但卷中存储的分配数据应保持不变。此类数据擦除程序还会擦除已删除文件的数据区域。
一种非破坏性数据擦除程序的程序编码如下所示:
///// 非破坏性数据擦除程序 \\\\\
    #包括 <stdio.h>
  unsigned int file_num=0; /* 提供文件编号
  在自动创建过程中
  临时数据文件 */
  float status=0;/* 磁盘空间有多少
  仍写着 */
  static char dbuf[40000]; /* 要写入的数据缓冲区
  带有 */ 的临时文件
  char file_extension[5]=".ptt";/* 唯一扩展名
  临时文件 */
  char temp[5]; /* 文件编号转换为
  细绳 */
  char filename[40]; /* 临时文件名 */
  空主()
  {
  无符号整数i=0;
  clrscr();
  while(i<40000)
  {
  dbuf[i] = ' ';
  我++;
  }
  gotoxy(10,14);cprintf(" MB 仍在写入...");
  while(1)
  {
/* 自动创建具有唯一名称的临时文件的逻辑 */
    strcpy(文件名,“TTPT”);
  itoa(文件编号,温度,10);
  strcat(文件名,温度);
  strcat(文件名,文件扩展名);
  文件编号++;
  写入_到_temp(文件名);
  }
  } //// 主程序结束 \\\\
///// 将数据写入临时文件的函数\\\\\
    write_to_temp(char *文件名)
  {
  无符号整数i,计数=1;
  浮动buf_status=0;
  文件*tt;
  如果((tt = fopen(文件名,“wb”))==NULL)
  {
  fclose(tt);
  printf("\n 创建临时文件时出错
  文件, ”);
  printf("\n 删除 KEY BOARD 之后的临时文件
  打”);
  获取();
  remove_temp_file();/*删除所有临时文件*/
  }
  while(1)
  {
  对于(i = 0;i < 50;i ++)
  {
  fprintf(tt,“%s”,dbuf);
  }
  buf_status = (浮点数)((40000*50*count)/512);
  状态=状态+(40000*50);
  计数++;
  乙氧基(10,14);
  cprintf("%.0f",(float)(status/1000000));
  如果(kbhit())
  {
  fclose(tt);
  printf("\n请删除临时文件
  等待...”);
  删除临时文件();
  }
  如果 (buf_status>=10000)
  {
  fclose(tt);
  返回;
  }
  }
  }
/* 自动删除临时文件的功能 */
    删除临时文件()
  {
  int i=0;
  对于(i = 0;i <=文件编号;i ++)
  {
  strcpy(文件名,“TTPT”);
  这个(我,温度,10);
  strcat(文件名,温度);
  strcat(文件名,文件扩展名);
  删除(文件名);
  }
  退出(1);
  返回0;
  }
 
对程序逻辑和编码的评论:
在这个程序中我们基本上按照以下两个步骤来清除磁盘的未分配空间:
  - 自动创建临时数据文件:首先,我们创建具有唯一名称且其中包含一些数据的临时文件,直到磁盘卷被这些临时数据文件填满。通过这样做,逻辑驱动器的所有未分配数据区域都被临时文件的数据占用,并且所有未分配数据都将被覆盖。
为此,我选择了 TTPTxxxx.PTT 格式的临时文件名称,这意味着临时文件的前四个字符是 TTPT,文件扩展名是 .PTT。这样做是为了给临时文件提供唯一的文件名。
我设置了单个临时文件的最大大小,相当于大约 11,718 个扇区数据,但您可以根据自己的需要定义它。我选择空格字符“ ”(ASCII 字符 32)来填充临时文件中的数据。但是也可以使用随机字符代替空格。
  - 删除所有临时文件:当逻辑驱动器中存满临时文件时,表明所有未分配的数据区域现在都已被覆盖。现在程序创建的所有临时文件都会被自动删除。从而实现未分配空间的清除。
在程序编码中,字符数组filename存储了自动生成临时文件的文件名,名字不同。
函数 write_to_temp(filename); 用 40,000 字节的数据缓冲区 dbuf 向临时文件填充最多 11,718 个扇区(因为指定的缓冲区组写入中没有出现 10,000 个扇区)等效数据。每次写入 50 次数据缓冲区以加快写入速度。
临时文件会一直创建,直到磁盘卷已满并发生文件创建错误。函数 remove_temp_file() 删除程序创建的所有临时文件。
这样,所有未分配的空间都将被清除,而不会损害磁盘卷的数据。
 
破坏性数据擦除器的编写程序:
破坏性数据擦除程序是直接在磁盘表面进行写入的程序。此类数据擦除程序在文件系统和操作系统的较低级别上工作,这意味着所有数据和其他逻辑信息(包括操作系统、文件系统、目录条目和写入磁盘的所有内容)都会被擦除。
这些数据擦除程序直接擦除磁盘表面的扇区,并擦除写入的所有内容。由于磁盘的所有数据(包括操作系统)都会丢失,因此这些程序被称为破坏性数据擦除程序。
在用户愿意覆盖磁盘上的所有内容(包括操作系统和磁盘上的所有数据)的情况下,最好使用这些类型的擦除程序。
但是这种类型的数据擦除程序还有一些其他的好处。由于这些破坏性数据擦除程序完全不受操作系统和文件系统的影响,直接写入磁盘表面,因此它们比非破坏性数据擦除程序要快得多。
另外,如果由于非法存储了一些随机数据而导致磁盘上产生了任何逻辑坏扇区,这些逻辑坏扇区也会随磁盘上的数据一起被彻底清除。
接下来给出了破坏性数据擦除程序的编码。该程序还支持大容量磁盘。该程序会擦除连接到计算机的第二个物理硬盘上的数据。
///// 编写破坏性数据擦除程序 \\\\\
    #include<stdio.h>
  #include<dos.h>
/* 使用 INT 13H 扩展、功能编号 0x48 的 getdrivegeometry 函数使用的结构。 */
    结构几何
  {
  unsigned int size ; /*(调用)缓冲区的大小 */
  unsigned int flags ; /* 信息标志 */
  unsigned long cyl ; /* 物理柱面数
  驾驶 */
  无符号长头;/* 物理磁头数量
  驾驶 */
  unsigned long spt ; /* 每个物理扇区数
  追踪 */
  无符号长整型扇区[2];/*总数
  驱动器上的扇区 */
  unsigned int bps ; /* 每扇区字节数 */
  };
/* 磁盘地址包格式的结构,供 writeabsolutesectors 函数使用 */
    磁盘加载包结构
  {
  char packetsize;/*数据包的大小,
  一般为10H */
  char reserved; /* 保留 (0) */
  int blockcount ; /* 要传输的块数
  转移 */
  char far *bufferaddress ; /* 要传输的地址
  缓冲 */
  无符号长整型块号[2];/* 起始绝对
  区块编号 */
  };
///// 获取驱动器参数的函数 \\\\\
    无符号长整型 getdrivegeometry (int drive)
  {
  联合 REGS i,o;
  结构 SREGS s;
  结构几何 g = { 26, 0, 0, 0, 0, 0, 0, 0 };
  ihah = 0x48 ; /* INT 13H 的功能编号 0x48
  扩展 */
  ihdl = drive; /* 驱动器号 */
  ixsi = FP_OFF((void far*)&g);
  s.ds = FP_SEG((void far*)&g);
/* 使用段寄存器值调用 INT 13H 扩展的指定功能号 */
    int86x ( 0x13,&i,&o,&s ) ;
  printf("\n 磁头 = %lu, 每磁道扇区数 = %lu, 柱面数 =
  %lu\n", 
  g.头部,g.spt,g.圆柱体);
/* 如果获取驱动器几何函数失败,则显示错误消息并退出 */
    如果(g.spt==0)
  {
  printf("\n获取驱动器几何功能失败....");
  printf("\n 扩展不支持,按任意键
  出口...”);
  获取();
  退出(1);
  }
  返回 *g.sectors; /* 返回
  驱动器上的扇区 */
  }
  空主()
  {
  无符号长循环=0,Sectors_in_HDD2=0;
  unsigned char buffer[61440]; /* 61440 的数据缓冲区
  字节相当于
  120 个扇区 */
  无符号长整型i=0;
  字符选择;
  clrscr();
/* 如果连接的硬盘总数少于两个,则显示错误消息并退出。 */
    如果(((char)peekb(0x0040,0x0075))<2)
  {
  printf("\n\n 您必须至少有两个硬盘
  连接到您的计算机来运行此程序”);
  printf("\n 程序。该程序已开发
  擦除第二块硬盘的数据。”);
  printf("\n按任意键退出... ");
  获取();
  退出(1);
  }
  Sectors_in_HDD2 = 获取驱动器几何形状 (0x81);
  printf(" 第二块硬盘的总扇区数 =
  %lu\n\n",
  HDD2 中的扇区);
///// 先确认,然后继续 \\\\\
    printf("\n 这是一个数据擦除程序,并写入
  磁盘表面”);
  printf("\n 运行该程序后,数据无法
  可以通过任何软件恢复”);
  printf("\n 第二块硬盘上的所有数据将被
  丢失的 !!!”);
  printf("\n 按 \'Y\' 继续,否则按任意键
  出口... ”);
  选择 = getche();
  开关(选择)
  {
  案例 ‘y’:
  案例‘Y’:
  休息;
  默认:
  退出(0);
  }
  gotoxy(10,15);cprintf(" 正在初始化,请等待...");
  对于(i = 0;i < 61440;i ++)
  {
  缓冲区[i] ='\0';
  }
  gotoxy(10,15);cprintf(“ “)”;
  乙氧基(10,15);
  printf("当前正在擦除绝对扇区:");
  对于(循环=0;循环<= Sectors_in_HDD2;循环=循环+120)
  {
  写入绝对扇区(0x81,循环,120,缓冲区);
  gotoxy(44,15);printf(“%ld”,循环);
  如果(kbhit())
  {
  退出(0);
  }
///// 完成后显示消息 \\\\\
    printf("\n\n 数据擦除现已完成,
  第二块硬盘现在是”);
  printf("\n已彻底清除,按任意键退出...");
  获取();
  }
//// 写入绝对扇区的函数 \\\\
    int writeabsolutesectors(int drive,unsigned long sectornumber,int numofsectors,void *buffer)
  {
  联合 REGS i,o;
  结构 SREGS s;
  结构磁盘地址包 pp;
  pp.packetsize = 16 ; /* 数据包大小 = 10H */
  pp.reserved = 0 ; /* 保留 = 0 */
  pp.blockcount = numofsectors ;/* 要
  被写入 */
/* 用于数据缓冲区 */
    pp.bufferaddress = (char far*) MK_FP ( FP_SEG((void far*)buffer), FP_OFF((void far*)buffer));
  pp.blocknumber[0] = sectornumber ; /* 扇区号
  待写*/
  pp.blocknumber[1] = 0 ; /* 块号 = 0 */
  ihah = 0x43 ; /* 功能编号 */
  ihal = 0x00 ; /* 写标志 */
  ihdl = 驱动器;/* 物理驱动器
  数字 */
  ixsi = FP_OFF((void far*)&pp); /* ds:si 用于
  缓冲区参数 */
  s.ds = FP_SEG((void far*)&pp); /* ds: 说
  缓冲区参数 */
/* 使用段寄存器值调用 INT 13H 的指定函数 */
    int86x ( 0x13,&i,&o,&s ) ;
  如果(oxcflag==1)
  返回 0 ; //失败
  别的
  返回 1 ; // 成功
  }
 
编码注释:
结构 geometry 由 getdrivegeometry 函数使用 INT 13H 扩展、功能编号 0x48 来获取磁盘的各种参数。
结构 diskaddrpacket 用于磁盘地址包格式,供 writeabsolutesectors 函数使用。
函数 getdrivegeometry (int drive) 用于获取指定物理驱动器号 drive 的磁盘的驱动器参数。buffer[61440] 是 61440 字节的数据缓冲区,相当于 120 个扇区。
(char) peekb(0x0040, 0x0075) 用于查找连接到计算机的硬盘数量,存储在由段 0040H:偏移量 0075H 表示的内存位置。如果连接的硬盘总数少于两个,则显示错误消息并退出。
writeabsolutesectors ( 0x81, loop, 120, buffer ) 函数用于从 loop 指定的绝对扇区号开始,每次将数据缓冲区的数据写入 120 个扇区。
我选择在扇区上写入 '\0'(NULL 字符,ASCII 代码 0)来覆盖数据。但是,您可以使用随机字符来覆盖数据。
有关函数 writeabsolutesectors 和 getdrivegeometry 的详细描述,请参阅本书前面的章节。
 
擦除特定文件的数据区域
我们讨论了数据擦除程序,它们会擦除磁盘未分配空间的数据或擦除整个磁盘。但如果用户愿意在每次删除数据时都擦除数据,则擦除整个磁盘未分配空间可能是一个耗时的过程。
我们需要这种类型的数据擦除程序来擦除仅由该特定文件占用的数据区域。为此,我们借助 FAT 和根目录条目来查找该特定文件占用的数据区域
即使是软盘,如果数据没有碎片,我们也只能借助根目录信息来做到这一点。下表显示了 32 字节的根目录条目存储的任何文件的信息:

正如我们在根目录条目的目录中看到的,我们能够找到文件的起始和结束簇。文件名的第一个字节也可能包含有关文件的一些重要信息。该字节提供的信息可能是以下信息之一:

让我们尝试使用此信息,借助根目录信息擦除存储在 1.44Mb、3.5 英寸软盘上的任何文件的数据。假设软盘中的数据没有碎片,下面给出的程序将从其数据区域中擦除指定文件的数据:
 
/* 擦除软盘中指定文件数据区的程序 */
    #include<stdio.h>
  #include<dos.h>
///// 用于读取根目录中文件条目的 32 个字节的结构 \\\\\
    结构根
  {
  unsigned char filename[8]; /* 文件名条目
  8字节*/
  unsigned char extension[3]; /* 文件扩展名
  3 个字节 */
  unsigned char attribute; /* 文件属性字节 */
  unsigned char reserved[10]; /* 保留字节 10 */
  unsigned int time; /* 时间,2 个字节 */
  无符号整数日期;/*日期,2 个字节*/
  unsigned int Starting_cluster; /* 文件的起始簇,
  2 个字节 */
  unsigned long file_size; /* 文件大小(以字节为单位),
  4个字节*/
  };
  /* 应采取此措施来读取所有根目录条目 */
  
//结构根条目[224];
  /* 结构用于读取根目录一个扇区中的所有 16 个文件条目 */
        结构 one_root_sector
    {
    结构根条目[16];
    };
    结构one_root_sector一;
    空主()
    {
    int 结果,i,num_sectors,j;
    char wipe_buf[512]; /* 用于擦除的数据缓冲区
    输出文件的数据区域 */
    clrscr();
    result= absread(0x00, 1, 19, &one); /* 读取绝对扇区
    19(根目录的第一个扇区)*/
    如果(结果 != 0)
    {
    perror(“读取扇区时出错,按任意键
    出口...”);
    获取();
    退出(1);
    }
  
  /* 从根目录读取后显示文件信息 */
        printf(" 文件编号 文件名 扩展名 起始簇
    文件大小\n\n”);
    对于(i = 1;i < 16;i ++)
    {
    printf("\n %5d %8.8s %3.3s %5u %10lu",
    我,一个.entry[i].文件名,一个.entry[i].扩展名,
    一个.条目[i].起始簇,一个.条目[i].文件大小);
    }
  
  //// 获取用户输入以删除文件 \\\\
        printf("\n\n输入要删除的文件编号,然后
    彻底消灭”);
    scanf("%d",&i);
    如果(i<1 || i>15)
    {
    printf(" \"%d\" 是无效选择...,按任意
    退出键...",i);
    获取();
    退出(1);
    }
  
  ///// 先确认,再继续 \\\\\\
        printf("\n你即将被消灭,
    文件 \"%.8s.%s\"",
    one.entry[i].文件名,
    一.条目[i].扩展名);
    printf("\n您是否要继续...(Y/N) ");
    开关(getche())
    {
    案例 ‘y’:
    案例‘Y’:
    休息;
    默认:
    退出(0);
    }
  
  ///// 计算文件的扇区大小 \\\\\
        num_sectors = one.entry[i].file_size/512;
    如果((one.entry[i].file_size%512)>0)
    {
    扇区数 = 扇区数+1;
    }
  
  /* 512 字节的数据缓冲区,包含 512 个 NULL 字符 */
        对于(j = 0; j < 512; j ++)
    {
    wipe_buf[j] = '\0';
    }
  
  ///// 文件的起始扇区 \\\\\
        j = 一.条目[i].起始簇+31;
  
  /* 擦除数据区域直到文件结束扇区 */
        пока(j!=(one.entry[i].starting_cluster +
    扇区数+31) )
    {
    如果((abswrite(0x00,1,j,&wipe_buf))!=0)
    {
    printf("\n写入磁盘扇区时出错");
    得到();
    输出(0);
    }
    j++;
    }
    printf("\n\n 文件 \"%.8s.%.3s\" 已删除!!!",
    one.entry[i].file_name,
    一.条目[i].扩展名);
    一个.条目[i].属性 = 0; /* 设置文件属性
    为 0 */
    一.条目[i].时间 = 0; /* 清除时间信息
    文件 */
    一.条目[i].日期 = 0; /* 清除日期信息
    文件 */
    一.条目[i].起始簇 = 0; /* 将初始簇设置为 0
    */
    一个.条目[i].文件大小 = 0; /* 将文件大小设置为 0 */
    一个.条目[i].文件名[0]=0xE5; /* 给予遥控器
    文件中的文件状态 */
  
  ///// 将以上信息写入根目录\\\\\\
        结果 = abswrite(0x00, 1, 19, &one);
    如果(结果 != 0)
    {
    perror("读取扇区错误,按任意键
    出口...”);
    得到();
    输出(1);
    }
    }
  
 
  对程序逻辑和编码的注释:
  root结构用于读取根目录中一个文件条目的32个字节,one_root_sector结构读取根目录一个扇区中全部16个文件条目。
  如果要读取根目录所有扇区的信息,则应该将其作为struct root[224]的条目;但是,我编写了一个程序来分析根目录的一个扇区的16条记录。
  文件的起始扇区计算如下:
  j = 一.记录[i].初始聚类+31;
  这样做是因为 1.44 MB、3 ½ 英寸软盘的数据区从磁盘的前 32 个扇区之后开始。而在规定容量的软盘中,一个簇对应一个扇区。
  下表显示了 1.44 MB、3.5 英寸软盘的逻辑图:
  
  程序运行结果显示如下:
  
    这里我们删除并擦除了PARTBOOT.C文件的数据。当我们使用 DIR 命令查看软盘的内容时,PARTBOOT.C 文件并没有显示在那里。当程序进一步执行时,已删除文件的条目显示如下:
    
  这里的符号“”(0xE5)表示该文件已被删除。 (文件名的第一个字符见表格)。
  如果要为硬盘编写相同的程序,您还需要使用带根目录的 FAT 来获取有关任何文件的数据区域的信息。
  这是因为随着旧文件的删除和新文件的创建,硬盘上数据碎片化的速度会随着时间的推移而增加。那么磁盘上任何文件的所有数据簇就没有必要在数据区域中连续地保持一个接一个。通过访问 FAT,您可以访问所有这些集群。