3
\$\begingroup\$

This is a file shredder utility that runs on Linux. It writes random bytes over the file contents, repeating this 48 times and calling sys_fsync after each pass to make sure data goes to disk.

; Description     : A file shredder, it reads a dword from urandom then replaces
;                   the file contents with it, this process is repeated 48 times
;                   using a different random dword on each pass. It calls fsync
;                   after each pass is complete to ensure that the changes are
;                   reflected in the disk.
;
; Build using these commands:
;   nasm -f elf shredder.asm
;   ld -o shredit shredder.o -m elf_i386 -s


section .data
UsagePrefix: db 'Usage: '
USAGEPREFIXLEN: equ $-UsagePrefix
UsageSuffix: db ' file_name',0ah
USAGESUFFIXLEN: equ $-UsageSuffix
UrandomPath: db '/dev/urandom',0
ErrorMsg: db 'There was an error, file NOT shredded',0ah
ERRORMSGLEN: equ $-ErrorMsg
PASSES: equ 48 ; how many passes

section .bss
UrandomFd: resd 1
Size: resd 1
RandomBuffer: resd 128
RANDOMBUFFERSIZE: equ $-RandomBuffer  ; changing this breaks the code that
                                      ; calculates the number of passes

section .text

global _start

; in          : eax
; what in     : null-terminated string
; out         : eax
; what out    : length
; trashes     : nothing
; description : very simple function to find the null terminator ('\0'), it uses
;               the naive algorithm and avoids repne scasb because this is ~15%
;               faster than repne scasb on my machine...
GetLength:
  push ecx            ; save caller's registers
  push esi            ; 
  mov esi,eax         ; iterate using esi then sub later
  xor ecx,ecx         ; we are looking for the null byte
  jmp .skip           ; it might be the first byte

  .loop:
  inc esi             ; count this iteration

  .skip:
  cmp byte [esi],cl   ; check if this is the byte
  jne .loop           ; repeat until found

  sub eax,esi         ; calculate the length
  pop esi             ; restore caller's registers
  pop ecx             ; 
  ret                 ; done

Usage:
  ; write 'Usage: '
  mov eax,4                   ; sys_write
  mov ebx,2                   ; stderr
  mov ecx,UsagePrefix         ; string
  mov edx,USAGEPREFIXLEN      ; length
  int 80h                     ; call

  ; write executable name
  pop ecx                     ; program name is next on the stack
  mov eax,ecx                 ; GetLength gets its input in eax
  call GetLength
  mov edx,eax                 ; set up length for the write
  mov eax,4                   ; sys_write
  mov ebx,2                   ; stderr
  int 80h                     ; call

  ; write ' file_name\n'
  mov eax,4                   ; sys_write
  mov ebx,2                   ; stderr
  mov ecx,UsageSuffix         ; string
  mov edx,USAGESUFFIXLEN      ; length
  int 80h                     ; call

  ; fall through

ExitFailure:
  mov eax,1                   ; sys_exit
  mov ebx,-1                  ; failure code
  int 80h                     ; call

Error:
  mov eax,4                   ; sys_write
  mov ebx,2                   ; stderr
  mov ecx,ErrorMsg            ; string
  mov edx,ERRORMSGLEN         ; length
  int 80h                     ; call
  jmp ExitFailure             ; let's stop execution

_start:
  pop eax                     ; get arg count
  cmp eax,2                   ; there should be 2 args: ./shredit file_name
  jne Usage                   ; show user how to run the program

  ; get file name
  lea esp,[esp+4]             ; jump over program name
  pop ebx                     ; file name

  ; open the file to be shredded
  mov eax,5                   ; sys_open
  ; ebx has the name
  mov ecx,1                   ; O_WRONLY
  int 80h                     ; call sys_open
  cmp eax,0                   ; check fd
  jl Error                    ; < 0 means error
  mov esi,eax                 ; save fd

  ; get file size
  mov ebx,eax
  mov eax,19                  ; sys_lseek
  xor ecx,ecx                 ; offset
  mov edx,2                   ; SEEK_END
  int 80h                     ; call
  cmp eax,0                   ; check return value
  jl Error                    ; negative values indicate errors

  mov [Size],eax              ; save file size

  ; now open urandom
  mov eax,5                   ; sys_open
  mov ebx,UrandomPath         ; path
  xor ecx,ecx                 ; O_RDONLY
  int 80h                     ; call open
  cmp eax,0                   ; check returned fd
  jl Error                    ; < 0 means error
  mov [UrandomFd],eax         ; save fd for later

  ; rewind file
  mov eax,19                  ; sys_lseek
  mov ebx,esi                 ; fd
  xor ecx,ecx                 ; offset 0
  xor edx,edx                 ; SEEK_SET
  int 80h                     ; call
  cmp eax,0                   ; error?
  jl Error                    ; exit

  ; We have a random source and the file to shred
  mov ebp,PASSES              ; how many times we are going to overwrite

  .wholeLoop:
  mov edi,[Size]              ; get size
  shr edi,9                   ; how many full 512 byte runs?

  .fillRandom:
  mov eax,3                   ; sys_read
  mov ebx,[UrandomFd]         ; fd
  mov ecx,RandomBuffer        ; destination
  mov edx,4                   ; size
  int 80h                     ; call
  cmp eax,4                   ; did it read all?
  jl Error                    ; we can't risk writing non-random bytes

  mov eax,[RandomBuffer]      ; copy the dword to fill the buffer
  mov ebx,RandomBuffer + 4    ; start with the next dword
  mov ecx,127                 ; repeat until it's full

  .expand:
  mov [ebx],eax               ; do the actual buffer writing
  add ebx,4                   ; move to the next dword
  dec ecx                     ; count this iteration
  jnz .expand                 ; repeat until done

  ; This is where we exit the 512-byte loop
  cmp edi,0                   ; is there any other 512 byte block to write?
  je .handleRemaining         ; if not, handle remaining bytes

  ; Overwrite with random
  dec edi                     ; count this iteration
  mov eax,4                   ; sys_write
  mov ebx,esi                 ; fd
  mov ecx,RandomBuffer        ; string
  mov edx,RANDOMBUFFERSIZE    ; size
  int 80h                     ; call
  cmp eax,RANDOMBUFFERSIZE    ; did it write everything?
  jl Error                    ; something is wrong

  jmp .fillRandom             ; close the loop

  .handleRemaining:
  mov eax,4                   ; sys_write
  mov ebx,esi                 ; fd
  mov ecx,RandomBuffer        ; string
  mov edx,[Size]              ; remaining size is the first byte only
  and edx,0ffh                ; ok, now we counted the 512-byte passes
  int 80h                     ; call
  cmp eax,edx                 ; did it write everything?
  jl Error                    ; something is wrong

  dec ebp                     ; count this iteration
  jz Exit                     ; if done, exit

  mov eax,19                  ; sys_lseek
  mov ebx,esi                 ; file
  xor ecx,ecx                 ; offset is 0, we are overwriting again
  xor edx,edx                 ; SEEK_SET
  int 80h                     ; call
  cmp eax,0                   ; success?
  jl Error                    ; < 0 means error

  mov eax,118                 ; sys_fsync
  int 80h                     ; call, sys_fsync can't fail if fd is valid

  jmp .wholeLoop              ; overwrite again

Exit:
  mov eax,6                   ; sys_close
  mov ebx,esi                 ; shredded file
  int 80h

  mov eax,1 ; sys_exit
  xor ebx,ebx ; EXIT_SUCCESS
  int 80h     ; call
\$\endgroup\$
  • \$\begingroup\$ Can the 'rewind file' code be moved down after .wholeloop? Seems like that might allow you to remove the (now redundant) sys_lseek after checking ebp. Have you experimented with fsync? On large files, do you get better perf with 1 call at the end, or one after each write? From a more utilitarian point of view, is writing random data the best way to shred a file? Seems like writing all 1s followed by all 0s might be more effective. Maybe even 01010101 followed by 10101010. \$\endgroup\$ – David Wohlferd Jul 28 '16 at 3:56
  • \$\begingroup\$ It looks like handleRemaining can also be combined with 'Overwrite with random'. If you keep the real size in edi, then mov edx, edi; and edx, 0xffh (like you do in handleRemaining) always gets you the right size to write. And sub edi, edx after the jl Error keeps the correct remaining size in edi. You are done when edi hits zero. \$\endgroup\$ – David Wohlferd Jul 28 '16 at 23:18
  • \$\begingroup\$ While I am not competent to review the quality of the assembly code, it would be nice to know why you wrote it in assembly rather than in a high-level language. Was this an academic exercise? You probably know this but shredding files like this is not very useful on modern hardware and file-systems any more. \$\endgroup\$ – 5gon12eder Jul 31 '16 at 14:55
  • \$\begingroup\$ @5gon12eder I'm learning assembly. \$\endgroup\$ – Douglas Jul 31 '16 at 21:54
2
\$\begingroup\$

Well, since no one else has responded, I'll turn my comments into an answer. I'm reluctant to do that as I haven't actually tried the changes I am proposing. But they do represent a review, so they allow the question to be resolved.

  1. Can the 'rewind file' code be moved down after .wholeloop? Seems like that might allow you to remove the (now redundant) sys_lseek after checking ebp.
  2. Have you experimented with fsync? On large files, do you get better perf with 1 call at the end, or one after each write?
  3. From a more utilitarian point of view, is writing random data the best way to shred a file? Seems like writing all 1s followed by all 0s might be more effective. Maybe even 01010101 followed by 10101010. I'm sure I've read articles on this that google could turn up.
  4. It looks like handleRemaining can also be combined with 'Overwrite with random'. If you keep the real size in edi, then mov edx, edi; and edx, 0xffh (like you do in handleRemaining) always gets you the right size to write. And sub edi, edx after the jl Error keeps the correct remaining size in edi. You are done when edi hits zero.

Hope this is helpful.

\$\endgroup\$
  • \$\begingroup\$ I have rewritten the code and implemented your suggestions. 1. yes, much better. 2. I'm calling fsync to make sure the bytes at the actual storage device get overwritten, it would be pointless to repeat this process many times if Linux kept the changes in memory. 3. I don't know, but I trust that "A good scrubbing with random data will do about as well as can be expected" (en.wikipedia.org/wiki/Gutmann_method#Criticism). 4. I did that, but writing 511 bytes per pass to fully utilize the buffer with random data. \$\endgroup\$ – Douglas Jul 31 '16 at 21:40
  • \$\begingroup\$ 3. no, that would clearly mark shredded files as shredded, while random data look the same as encrypted data, so you don't leak the information about shredding. Oh, now I see the shredding does overwrite the file with the same random dword, that's from security point of view as bad as using some fixed pattern. Overwriting it 48 times seems to me a bit redundant, isn't like 5-10 times enough to destroy any shadow echo of original value (on HDD plates)? And I would expect that as configurable parameter. \$\endgroup\$ – Ped7g Aug 4 '16 at 10:41
  • 1
    \$\begingroup\$ Recovering 'shadow echo' appears to be more scifi than reality. Near as I know, there are no commercial companies that offer this service. Perhaps the spy agencies of nations could do this, but if you are trying to protect against them, you should physically destroy the media. It's faster, cheaper, and more certain. And don't even get me started on how you scrub files on ssds with wear leveling and/or sector hot swapping. \$\endgroup\$ – David Wohlferd Aug 4 '16 at 12:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.