5
\$\begingroup\$

Here is a little tool for Windows that replaces the content of a file with zero-bytes, keeping the same filesize. It works. The goal is to be sure that the old bytes-content of the file is no more recoverable on disk.

Question: how to be 100% sure that it replaces the content in-place and that it doesn't allocate new sectors on disk to write the zero bytes? (thus leaving the old content somewhere else, still possibly accessible, which would be a security hole!)

Note: I know that freewares exist for this, but I would like to try a simple implementation first

Note2: I know that some more advanced wiping techniques exist (random content and/or multi-pass, etc.), but for me zeroing the content is enough if at least I know that the zeroing is done in-place, i.e. if the new zero bytes replace the old content at the same location on disk (and not moved to another sectors on disk!)

Could you review the following code?

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE *f;

    if (argc != 2) { fprintf (stderr, "Usage: %s <file>\n", argv[0]); exit(1); }

    if ((f = fopen(argv[1], "rb+")) == NULL) { perror("Error opening the file"); exit(1); }

    fseek(f, 0L, SEEK_END);
    unsigned long size = ftell(f);
    fseek(f, 0L, SEEK_SET);

    char buf[1024] = {0};
    unsigned long num = size / 1024;
    int remaining = size % 1024;

    for (unsigned long i = 0; i < num; i++)
        fwrite(buf, 1, 1024, f);

    fwrite(buf, 1, remaining, f);

    fclose(f);
    exit(0);
}
\$\endgroup\$
5
  • 2
    \$\begingroup\$ I feel this may be off-topic here: you're asking a concrete question (how to replace file content in place) rather than for a review of working code that does so. I'd urge you to ask your question on StackOverflow and leave just the code, so that members of this site can offer advice on it (if you want it). \$\endgroup\$
    – fstanis
    Commented Oct 18, 2017 at 21:23
  • 1
    \$\begingroup\$ Not convinced you can do this at the language level directly. This will depend on the implementation details of the the underlying OS and filesystem. I would look into specific OS calls that control the file system directly. \$\endgroup\$ Commented Oct 18, 2017 at 21:26
  • \$\begingroup\$ I would also mention that zeroing the content is usually done on a drive-level, rather than file level, so the OS may not even expose this functionality to you. \$\endgroup\$
    – fstanis
    Commented Oct 18, 2017 at 21:28
  • \$\begingroup\$ "for Windows" - really? That looks like Standard C that you could compile and run anywhere. Obviously, you're at the mercy of a filesystem layer between you and any stored data, and that will be different on different filesystems and OSes, but the code itself is portable. \$\endgroup\$ Commented Oct 19, 2017 at 10:50
  • \$\begingroup\$ Take a look at docs.microsoft.com/en-us/sysinternals/downloads/sdelete Securely deleting a file that has no special attributes is relatively straight-forward: the secure delete program simply overwrites the file with the secure delete pattern. What is more tricky is securely deleting Windows NT/2K compressed, encrypted and sparse files, and securely cleansing disk free spaces. So I guess if the file has no special attributes and you overwrite it, it goes on the same disk location. Although using standard libraries invalidates any such assumptions and you should go with Win32 APIs. \$\endgroup\$
    – Douglas
    Commented Oct 19, 2017 at 17:45

2 Answers 2

2
\$\begingroup\$

Here are some suggestions that may help you improve your code.

Use return rather than exit()

The effect of using exit(0) or exit(1) is going to be identical to return 0 or return 1 but with one difference: exit() is a library call and would therefore require #include <stdlib.h> (which this program does not have) while return does not require any particular #include. Also note that exit(0) implicitly flushes and closes files, as does return 0. Further, the equivalent to return 0; is provided by the compiler if omitted (since C99), so technically, the last two lines of the program:

fclose(f);
exit(0);

could both be omitted with no change to the function of the program. If, however, you decided to move this part of the function to a separate function, I'd suggest keeping fclose(f); and using return 0; rather than exit(0);. Tastes vary on this point.

Don't cram multiple statements per line

Lines such as this:

if ((f = fopen(argv[1], "rb+")) == NULL) { perror("Error opening the file"); exit(1); }

tend to be more readable if formatted as separate lines:

if ((f = fopen(argv[1], "rb+")) == NULL) { 
    perror("Error opening the file"); 
    return 1; 
}

Check return values

The code does check for the return value of fopen, which is good, but does not check the return values for fseek, ftell and fwrite which can also fail.

Use named constants

The number 1024 appears multiple times in the code. If one wanted, for instance, a 2k buffer instead, one would have to change it all three places. Using a named constant instead simplifies maintenance and makes the code easier to read as well.

Understand the bounds of standard C

The C language is very expressive and has a great number of handy library functions for doing things like reading and writing files. However, what exactly the underlying operating system does to manage files on disk is outside the domain of C and squarely in the domain of the OS and hardware designers. There is, in short, no guarantee that this code will physically overwrite the same sectors of physical media, and more importantly, no provision in standard C for doing so. To take just one example, if I run a virtual Windows machine on a system that actually runs on a cluster of Linux machines, there might not actually even be any physical media.

\$\endgroup\$
0
\$\begingroup\$

In terms of wiping out data, files are very commonly saved in blocks (of some unit like 256, 512, 1024, etc.). Stray data from previously files versions often lie in the remnant of the last block. Consider zeroing out past the original file length to a multiple of blocks. Code can later truncate back to the original length if desired.


Check I/O function return values.
Use matching types

// fseek(f, 0L, SEEK_END);
if (fseek(f, 0, SEEK_END)) {  // L not needed
  perror("Error fseek"); 
  exit(1); // or return 
}

// unsigned long size = ftell(f);
long size = ftell(f);
if (size == -1) {
  perror("Error ftell"); 
  exit(1); // or return 
}

// pedantic: any other negative value is questionable, so code could use
if (size < 0) {
  perror("Error ftell"); 
  exit(1); // or return 
}

// fwrite(buf, 1, 1024, f);  // avoid replicating magic numbers
if (fwrite(buf, 1, sizeof buf, f) != sizeof buf) {
  perror("Error fwrite"); 
  exit(1); // or return 
}

ftell() is only good for files up to LONG_MAX in length. Files systems do not have that limitation. Robust code would use other methods to handle such huge files.


\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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