Perl로 하드 드라이브를 안전하게 지우기

[譯, 윤봉환 el@linuxlab.co.kr]
원문 : Mark Nielsen, translate into korean: electuz(el@kldp.org)

 

     

    Contents

             들어가며
             문제들
             펄 스크립트
             맺으며
             참고문헌

 

들어가며

    오하이오에서 캘리포니아로 옮길 때, GNUJobs.com에는 COLUG(http://www.colug.net/, Central Ohio Linux Users Group)에 기증하려는 하드 드라이브 몇 개를(다른 하드웨어, 소프트웨어와 함께) 가지고 있었습니다. 그리고, 하드 드라이브들을 기증하기 전에 그 내용을 지워야 했습니다. 하드 드라이브 셋 중에 둘은 배드 섹터를 가지고 있었습니다. 그래서, 이 기사를 만드는 것처럼 테스트할 목적으로, 마지막으로 남은 세 번째 하드 드라이브를 사용하게 되었고, 결국 누구에게 줄게 남지 않았습니다. 아무튼, 이 것뿐만 아니라, 앞으로도 하드 드라이브를 깨끗이 지워야 할 일들이 생길 테고, 그래서 나는 이 펄(Perl) 스크립트를 만들게 되었습니다 (다음에 나는 파이썬-Python으로 바꾸고, 더 많은 옵션들을 넣으려 합니다)

    이 펄 스크립트의 목적은 그저 /dev/hdb에(주 IDE 컨트롤러에 연결된 슬레이브 드라이브) 있는 하드 드라이브를(내용을) 지우려는 것입니다. 나는 이 스크립트로 모든 파티션들을 지우고, 하드 드라이브 전체를 차지하는 커다란 파티션을 만든 다음 그 파티션을 쓰레기 데이터로(무슨 데이터인지 찾아내려고 애쓰는 동안, 해커의 하루가 몰락하도록, 마구잡이로 암호화된 의미 없는 데이터들을 넣어서) 가득 채우려고 합니다.

 

문제들

    내가 만난 문제들과 그 것을 해결한 방법입니다:

    1. 어떻게 하면 스크립트가 모든 파티션을 지울 수 있을까?

    나는 하드 드라이브에서 파티션을 바꾸기 위한 많은 다른 옵션들을 찾아보았으며 직접 그런 것들을 사용했을 때 최적의 결과들을 가져왔던 것을 떠올렸습니다. 전에 fdisk 프로그램을 (fdisk는 리눅스에서 하드 드라이브 파티션을 편집하는 도구입니다) 자동화하는 데 Perl Expect 스크립트를 사용했었고, 이번에도 그 방법을 사용하기로 마음 먹었습니다. 나는 sfdisk와 다른 것들처럼 간단하게 모든 파티션을 지우는 도구로 바꾸는 것이 더 나을 것이라 생각했지만, 한 솔루션이 100% 파워와 유연성으로 모든 가능성을 떠맡는다면, 하려는 일에 대해 한 가지 방법에만 매달릴 것이고, 그러면 너무 많은 것들에 대해서 기억할 필요가 없을 테고, 더 복잡한 것들을 만나게 되어도, 새로 배워야 할 것은 아무 것도 없을 것입니다.

    그러므로 나는 사용자가 fdisk에서 명령하는 것을 흉내 내는 Expect 코드를 사용했습니다. Expect 코드는 fdisk를 이용해서 자동으로 모든 파티션을 지웠고, 새로 큰 파티션을 하나 만들었습니다.

    2. 하드 드라이브를 쓰레기 데이터로 가득 채우는 방법은?

    파티션을 삭제할 때에 단지 데이터만 지워서는 모자랍니다. 나는 지난 데이터 위에 쓰레기 데이터를 덮어써서, 이전 데이터가 완전하게 지워지게 만들려고 합니다. 나는 파티션을 만들고 크기를 정하기 위해 fdisk를 사용했습니다. 그 다음, 쓰레기 데이터가 새로 생성된 파티션 크기보다 크거나 같아질 때까지 계속해서 출력하는 루프를 만들었습니다.

    3. 하드 드라이브에 해커를 놀리기 위한 바이너리 데이터는 어떻게 넣을까?

    나는 Perl에 있는 random 함수와 “chr” 함수를 이용해서 랜덤 바이너리 데이터를 만들었습니다. 그 다음, Perl Blowfish 모듈을 이용해서 랜덤 데이터를 암호화했습니다. 누군가 그 데이터를 용케 해독해냈다면, 엄청난 노력에도 불구하고 여전히 쓰레기처럼 보이는 데이터들 때문에 어리둥절할 것입니다. 나는 수학적인 면에서 완전히, 마구잡이로(random) 만들어진 것처럼 보이지 않게 (약간의 규칙성을 넣어서) 암호화하고 싶었습니다.

    4. 큰 파티션 재포맷은 어떻게 할까?

    쉽습니다. 나는 그저 간단히 “mkfs” 명령만 사용했습니다.

 

Perl 스크립트

    내가 Perl 스크립트에 사용한 Perl 버전은 구식이었습니다. Perl 5.005_03 버전을 사용하고 있었는데, 내가 알기로 Perl은 2001년 1월에 5.6으로 버전이 올랐습니다.

    사용자에게 더 손쉬운 스크립트를 만들기 위해서는 기능을 강화해야 할 것들이 많이 있습니다. 더 많은 에러 검사와 스크립트가 가진 위험성에 대한 고찰이 있어야 하며, 그들이 정말 그렇게 채워 넣으려 한다면 사용자들에게 그런 질문을 보여줘야 할 것입니다. 나는 이 스크립트를 더 향상시키에 앞서 내 MILAS 프로젝트(譯註:MILAS Installs Linux And Stuff 프로젝트는 Mark Neilsen이 지난해부터 공언해온 일련의 프로젝트입니다. Nielsen은 루트 파일시스템이 포함된 인스톨 CD 등 많은 글을 그동안 써 왔습니다, Perl로 계획했던 이 프로젝트를 Python으로 다시 쓰거나 새로 만든다고 합니다) 재시작을 준비하고 있습니다.
    나는 코드에 주석을 많이 붙였습니다. 그러므로 잘만하면 풋내기 프로그래머들이라도 내 의도를 거의 이해할 수 있을 것입니다.
     

    -------------------------------------------------------start of script

    #!/usr/bin/perl

    ##### 할 일들
    # 1. 누군가 로그인 될 경우 보안 위험을 피하기 위해
    #     임시로 마운트 할 새로운 디렉토리를 만들고 퍼미션을 바꿉니다.
    # 2. 많은 시스템 콜들을 다루기 위해 perl 함수를 사용합니다.
    # 3. 하드 드라이브들과 플로피 드라이브들을 자동으로 찾게 하고,
    #     마운트되지 않은 하드 드라이브들과 플로피 드라이브에서만 작동하도록 만듭니다.
    #####

    use strict;
    use Expect;
    use Crypt::Blowfish;

    #-----------------------------------------------
    my $Junk;

      ### Primary IDE 컨트롤러에 연결된 슬레이브 드라이브를 변수 $Drive에 넣습니다.
    my $Drive = “hdb”;

      ### 마구잡이로 많은 것들을 채워 넣게 하고,
      ### 정말 마구잡이로 만들기 위해
      ### /etc/passwd 파일에서 마지막 줄을 가져와서,
      ### 컴퓨터에 한 사람이 추가된 것으로 가정합니다.
    my $time = time();
    my $Ran = rand($time);
    my $Ran = rand(10000000000000);
    my $LastLine = ‘tail -n 1 /etc/passwd’; chomp $LastLine;
    $LastLine = substr ($LastLine,0,30);
    my $Blowfish_Key = $LastLine . $Ran . $time;
    $Blowfish_Key = substr ($Blowfish_Key,0,20);
    while (length ($Blowfish_Key) < 56)
      {
      $Blowfish_Key .= $Ran = rand($time);
      }
    $Blowfish_Key = substr ($Blowfish_Key,0,56);

      ### 랜덤 키 만들기를 마치고, 이제 Blowfish 암호 개체를 만듭니다.
    my $Blowfish_Cipher = new Crypt::Blowfish $Blowfish_Key;

    #------------------------------------
    system “clear”;
    #print “This will wipe out the hard drive on Drive /dev/$Drive\n”;
    print “드라이브 /dev/$Drive\n에서 하드 드라이브를 깨끗이 청소합니다”;
    #print “Press enter to continue\n”;
    print “아무 키나 누르세요.\n”;
    my $R = <STDIN>;

      ### 청소하려는 드라이브에서 마운트된 파티션 목록을 읽어들입니다.
    my @Mounted = ‘df’;
    @Mounted = grep($_ =~ /\/dev\/hdb/, @Mounted);

      ### umount 명형으로 마운트된 파티션을 모두 해제합니다.
    foreach my $Mount (@Mounted)
      {
      my ($Partition,$Junk) = split(/\s+/, $Mount,2);
      #print “Unmounting $Partition\n”;
      print “마운트를 해제합니다: $Partition\n”;
      my $Result = system (“umount $Partition”);
      if ($Result > 0)
        {
        #print “ERROR, unable to umount $Partition, aborting Script, Error = $Result\n”;
        print “ERROR, $Partition 파티션을 umount할 수 없습니다, 스크립트 실행을 취소합니다, Error = $Result\n”;
        exit;
        }
      }

      ### 누군가 fdisk 명령을 사용하고 있는 것처럼 흉내내는 expect 스크립트를 시작합니다.
    my $Fdisk = Expect->spawn(“/sbin/fdisk /dev/$Drive”);

      ### 파티션 테이블을 출력해서 마운트된 파티션 목록을 얻습니다.
    print $Fdisk “p\n”;
    my $match=$Fdisk->expect(30,“Device Boot    Start”);

    my $Temp = $Fdisk->exp_after();
    my @Temp = split(/\n/, $Temp);
      ## 파티션에 대해 우리에게 말해주는 라인들을 추출합니다.
    my @Partitions = grep($_ =~ /^\/dev\//, @Temp);
      ## 각 라인을 이용해서 파티션을 지웁니다.
    foreach my $Line (reverse @Partitions)
      {
        ## /dev/hdb 부분과 숫자들을 가져옵니다.
      my ($Part,$Junk) = split(/[\t ]/, $Line,2);
      my $No = $Part;
      $No =~ s/^\/dev\/$Drive//;

      #print “Deleting no $Drive $No\n”;     
      print “번호 $Drive $No을 지웁니다\n”;     

        ## 지우는 명령
      print $Fdisk “d\n”;    
      $match=$Fdisk->expect(30,“Partition number”);
       
        ## 지우려는 파티션 번호
      print $Fdisk “$No\n”;
      $match=$Fdisk->expect(30,“Command (m for help):”);
      }

    $Fdisk->clear_accum();

      ### 파티션이 있으면 바꿔 쓰고 그렇지 않다면, 그냥 끝냅니다.

    if (@Partitions < 1) {print $Fdisk “q\n”; $Fdisk->expect(2,“:”);}
    else
      {
      print $Fdisk “w\n”;
      $Fdisk->expect(30,“Command (m for help):”);
      }

    #-------------------------------
      ## 하드 드라이브 지오메트리를 구합니다.
    my $Geometry = ‘/sbin/sfdisk -g /dev/$Drive’;
    my ($Junk, $Cyl, $Junk2, $Head, $Junk3, $Sector,@Junk) = split(/\s+/,$Geometry);
    if ($Cyl < 1)
       #{print “ERROR: Unable to figure out cylinders for drive. aborting\n”; exit;}
       {print “ERROR: 드라이브에서 실린더 수를 계산할 수 없습니다. 실행을 취소합니다.\n”; exit;}

      ### 사람이 fdisk를 사용하는 것처럼 흉내내기 위해 새로운 expect 스크립트를 만듭니다.
    my $Fdisk = Expect->spawn(“/sbin/fdisk /dev/$Drive”);

       #### fdisk에 새 파티션을 만드는 명령을 전달합니다.
    print $Fdisk “n\n”;
    $Fdisk->expect(5,“primary”);

      ### 새로운 파티션이 primary 파티션이라고 말해 줍니다.
    print $Fdisk “p\n”;
    $Fdisk->expect(5,“:”);

      ### 새로운 파티션에 번호 1을 할당합니다.
    print $Fdisk “1\n”;
    $Fdisk->expect(5,“:”);

      ### 실린더 번호 1에서 시작합니다.
    print $Fdisk “1\n”;
    $Fdisk->expect(5,“:”);

      ### 실린더 끝까지 파티션에 할당합니다.
    print $Fdisk “$Cyl\n”;
    $Fdisk->expect(5,“:”);

      ### 기록하고 저장합니다.
    print $Fdisk “w\n”;
    $Fdisk->expect(30,“Command (m for help):”);

    #------------------------------------------
    ### 파티션을 포맷하고 마운트합니다.

    my $Partition = “/dev/$Drive” . “1”;
    my $Result = system (“mkfs -t ext2 $Partition”);
    if ($Result > 0) {print “Error making partition, aborting.\n”; exit;}

       ### 에러가 체크되면 더 좋습니다.
    system “umount /tmp/WIPE_IT”;
    system “rm -rf /tmp/WIPE_IT”;
    system “mkdir -p /tmp/WIPE_IT”;
    system “chmod 700 /tmp/WIPE_IT”;

      ## 우리가 새 파티션을 마운트 할 수 있는지 확인합니다.
    my $Result = system (“mount $Partition /tmp/WIPE_IT”);
    #if ($Result > 0) {print “Error mounting drive, aborting.\n”; exit;}
    if ($Result > 0) {print “드라이브를 마운트할 수 없습니다, 실행을 취소합니다.\n”; exit;}
    system “chmod 700 /tmp/WIPE_IT”;

    #--------------------------------
    ### 이제 파일을 만들고 크기를 맞추었을 때 멈춥니다.

    my $Count = 0;
    my $Written_Size = 0;  

      ### 새 파일을 엽니다.
    open(FILE,“>>/tmp/WIPE_IT/Message.txt”);
       ### 누군가 실제로 여러분의 하드 드라이브 주변을 어슬렁거리려 한다면,
       ### 풀기 어려운 장난감을 하나 보태서
       ### 하드 드라이브에서 놀게 해주는 대신 자신들의 시간을 허비하게 만듭니다.
    my $Ran = rand 259200000;   # 지금부터 10년쯤 전까지(대략)
    ($Ran, $Junk) = split(/\./, $Ran, 2);
       ## 새로운 날짜는 음수인 초로 생성합니다
    my $Date = ‘date --date ‘-$Ran seconds’’;

    print FILE “DATE CREATED $Date\n”;
    my $Ran = rand 50;
    ($Ran, $Junk) = split(/\./, $Ran, 2);
    $Ran = $Ran + 10;
    #print FILE “This document is extremely secure. It is a violation to let
    #any unauthorized persons read it. Known password holders need to
    #apply Method $Ran in order to decrypt binary data.\n”;
    print FILE “이 문서는 철저한 비밀이며 허가받지 않은 이가 이 문서를 읽어서는 안됩니다.
    패스워드를 알고 있다면 바이너리 데이터를 해독하기 위해 $Ran 메쏘드를 적용해야 합니다.\n”;

      ### 양수 25000으로 랜덤 수를 만듭니다.
    my $Ran = rand 25000;
    ($Ran, $Junk) = split(/\./, $Ran, 2);
    $Ran = $Ran + 25000;

      ### 우리가 자주 사용하게 될 수열을 만듭니다.
    my @Blank =  (1..$Ran);
      ### 배열을 가져와서 문자열로 만듭니다.
    my $Blank = “@Blank”;
      ### 메모리를 비우기 위해 수의 배열을 비웁니다.
    @Blank = ();
    my $B_Length = length $Blank;

      ### 우리가 나눈 공간의 총계를 구합니다.
    my @Temp = ‘df’;
    @Temp = grep($_ =~ /^$Partition/, @Temp);
    my $Line = $Temp[0];
    my ($Junk,$Blocks,@Junk) = split(/\s+/, $Line,4);
      ### 블록을 1k라 가정합니다.
    my $Size = $Blocks*1000;

      ## 우리가 쓴 파일이 파티션 크기보다 작으면 계속해서 무언가 데이터를 출력합니다.
    while ($Written_Size < $Size)
      {
      $Count++;

            ### 열 번 가운데 아홉 번 정도는, 출력 속도를 높이기 위해 빈 공간을 출력합니다.
            ### 열 번 가운데 한 번 정도는, 쓰레기 바이너리를 출력합니다.
         my $Ran = rand (10);
         if ($Ran > 1)
           {
           print FILE $Blank;
           $Written_Size = $Written_Size + $B_Length;
           }  
         else
           {
             ## 긴 랜덤 데이터 문자열을 만드는 부분입니다(10000 바이츠 가까이)
           my $Garbage = "";
           my $Length = rand(10000);
           ($Length, $Junk) = split(/\./, $Length, 2);
           for (my $i = 0; $i < $Length; $i++)
             {
             my $Ran = rand 256;
             ($Ran, $Junk) = split(/\./, $Ran, 2);
             $Garbage .= chr $Ran;
             }
             ## 이 부분에서는 한번에 8 바이츠씩 랜덤 데이터를 암호화합니다.
           my $Temp = $Garbage;
           my $Encrypted = "";   

        while (length $Temp > 0)  
             {
             while (length $Temp < 8) {$Temp .= “\t”;}
             my $Temp2 = $Blowfish_Cipher->encrypt(substr($Temp,0,8));
             $Encrypted .= $Temp2;
             if (length $Temp > 8) {$Temp = substr($Temp,8);} else {$Temp = "";}
             }

             ### 암호화된 데이터를 파일에 출력합니다.
           print FILE $Encrypted;
           $Length = length $Encrypted;

           $Written_Size = $Written_Size + $Length;
           my $Rest = $Size - $Written_Size;
           print “$Size - $Written_Size = $Rest to go\n”;
           }

       ### 500 출력마다 새 파일을 저장하기 시작합니다.
      if ($Count =~ /500$/)
        {
        close FILE;
        open(FILE,“>>/tmp/WIPE_IT/$Count”);
        }
      }

    close FILE;
    #----------------------------------------------------

    my $Result = system (“umount $Partition”);
    #if ($Result > 0) {print “Error unmounting partition $Partition, aborting.\n”; exit; }
    if ($Result > 0) {print “$Partition 파티션을 해제할 수 없습니다, 실행을 취소합니다.\n”; exit; }

      ### 파티션을 다시 포맷합니다. 데이터는 삭제하지 않고, 디렉터리로부터 지웁니다.
    my $Result = system (“mkfs -t ext2 $Partition”);
    if ($Result > 0) {print “Error making partition, aborting.\n”; exit;}

    --------------------------------------------------------end of script

     

    이 스크립트 원문은 http://linuxgazette.com/issue63/misc/nielsen2/Wipe_It.pl.txt에 있습니다.

 

맺음

    꼭 Expect를 사용해야 하는 것은 아닙니다. (다른 프로그램들은 내가 어렵게 처리한 문제들을 간단하게 해결할 수 있었습니다). Blowfish 역시 반드시 사용해야 하는 것은 아닙니다. 사실, 이렇게 짜집기한 스크립트 전체는, 여러분이 단지 하드 드라이브를 지우고 그 속을 비워내려고만 한다면 너무 긴 방법입니다. 그렇기는 해도, 나는 늘 fdisk를 사용하므로, 이 스크립트에서도 역시 fdisk를 사용했습니다. 또, Expect는 매우 강력한 도구여서, 어떻게 동작하는지 알아보는 것도 유익한 일입니다. 그리고, 마구잡이로 암호화된 쓰레기 바이너리 데이터를 집어 넣는 일은 해커들을 놀리기 위해 그냥 한번 해본 것입니다.(역시 꼭 필요한 것은 아닙니다)

    나는 하드 드라이브의 복잡한 원리에 대해 완전하게 이해하지는 못하므로, 하드 드라이브에 데이터 찌꺼기가 남는다면 안심할 수가 없습니다. 내가 하려는 일에, 그리고 내게 필요한 보안 수준에, 이 스크립트는 딱 맞는 일을 합니다. 내가 MILAS를 더 개발함에 따라, 더 꼼꼼하게 점검해서 하드 드라이브 안에 들어 있는 모든 데이터를 지우는 방법들을 강화할 것이라고 확신합니다.

    나는 미래에 필요하게 될지도 모르는 일들을 미리 예상하여, 항상 프로그래머들이 가까운 프로젝트에 필요한 것들보다 더 많은 일을 하게 만드는 경향이 있습니다. 아무튼 이미 그렇게 되어버렸고, 나는 스크립트 진행 방향이 마음에 듭니다. 그리고, 그렇게 이 기사를 쓰는 것은 비행기 여행을 지겹지 않게 해줍니다. 누군가를 위해 일해야 하는 것과는 다르게 무언가 신나는 것을 만들 때에는 지치지 않습니다. 그런게 진짜 일입니다.

 

참고

    Perl.com 웹사이트 - http://www.perl.com/

    Expect Perl Module -
            http://www.perl.com/CPAN-local/modules/by-category/ \
            21_File_Handle_Input_Output/Expect /Expect-1.10.readme

    Blowfish Perl Module -
            http://www.perl.com/CPAN-local/modules/by-category/ \
           14_Security_and_Encryption/Crypt /Crypt-Blowfish-2.06.readme

    이 기사의 원래 사이트 -
            http://www.gnujobs.com/Articles/14/Wipe_It.html
            (기사가 업데이트되면 이 사이트에 둡니다)

    디스크에 내용을 덮어쓸 때 /dev/random이나 /dev/urandom을 사용할 수도 있습니다. Answer Gang을 보세요: 60회에 실린 “Classified Disk - Low-level Format”, 그러나 이 기사는 암호화하지는 않습니다. - http://linuxgazette.com/issue60/lg_answer60.html#tag/9

 

Copyright

    ---------------------------------------------------------
    Copyright ⓒ 2001, Mark Nielsen.
    Copying license http://www.linuxgazette.com/copying.html
    Published in Issue 63 of Linux Gazette, Mid-February (EXTRA) 2001




▲ top

home으로...