우리가 컴퓨터에서 무언가를 ‘삭제’했다는 것은 사실 ‘삭제됐다’기 보다는 ‘잊어버렸다’는 의미에 가깝다. 우리가 디지털 카메라로 사진을 잔뜩 찍어놓고 실수로 모든 파일을 삭제해 버렸다고 하자. 메모리 카드가 비어있다고 나오겠지만 그것은 사실이 아니다. 사진을 복구하는 프로그램을 만들어 보자.
JPEG는 BMP에 비해 복잡하지만, 바이트 패턴에 다른 포맷들과 구분되는 시그니처를 가지고 있다. 우선 처음 세 바이트는 아래와 같다.
0xff 0xd8 0xff
그리고 네 번째 바이트는0xe0
, 0xe1
, 0xe2
, 0xe3
, 0xe4
, 0xe5
, 0xe6
, 0xe7
, 0xe8
, 0xe9
, 0xea
, 0xeb
, 0xec
, 0xed
, 0xee
, 0xef
중에 하나가 된다. 즉 네 번째 바이트의 첫 4비트는 1110
이다.
따라서 만약 사진들을 저장했던 것으로 보이는 곳에서 이러한 패턴을 발견했다면 JPEG의 시작 부분일 테지만, 반드시 그렇지 않을 수도 있다.
디지털 카메라는 보통 메모리 카드에 사진 파일들을 연속적으로 저장한다. 각 사진 파일은 직전에 촬영된 사진의 바로 뒤에 저장되므로, 한 JPEG 파일의 시작은 이전 파일의 끝과 경계를 만든다. 디지털 카메라는 보통 블록 크기가 512 바이트인 FAT 파일시스템에 따라 카드를 초기화한다. 즉 카메라는 오직 512 바이트 단위로만 쓰기가 가능한데, 한 사진의 크기가 1MB(1,048,576 bytes)라면 1048576 ÷ 512 = 2048개의 블록을 사용한다. 1 바이트 적은 크기(1,048,575 bytes)의 사진 또한 같은 수의 블록을 사용한다. 이러한 낭비된 공간을 ‘여유 공간’이라고 하는데, 포렌식 수사관들은 의심스러운 데이터의 잔여물을 이러한 여유 공간에서 찾곤 한다.
JPEG 시그니처를 찾아 메모리 카드를 반복해서 읽어들여 사본 파일을 만드는 프로그램을 작성해보자. 시그니처를 발견할 때마다 새 파일을 열고 메모리 카드에서 읽어온 바이트로 채워나가고, 새로운 시그니처를 발견하면 파일을 닫는다. 메모리 카드는 FAT 시스템에 따라 ‘블록 정렬’되어있기 때문에, 한 번에 1 바이트가 아니라 512 바이트씩 버퍼로 읽어올 수 있다. 다시 말해서 한 블록마다 첫 네 바이트만 체크하면 된다.
하나의 JPEG 파일은 연속된 블록에 걸쳐 있을 수 있다. 그렇지 않다면 JPEG 파일은 512 바이트보다 클 수 없을 것이다. 또한 JPEG의 마지막 바이트가 반드시 블록의 끝에 위치하는 것은 아니다. 하지만 이 메모리 카드는 새 것이었고 제조업체에서 0으로 초기화했다고 가정한다. 즉 여유 공간은 0으로 채워져 있을 것이고, 복구한 파일에 이러한 0이 남아있어도 괜찮다.
아래 예시와 같이 포렌식 이미지 파일에서 JPEG 이미지를 복원하는 프로그램을 작성한다.
$ ./recover
Usage: ./recover image
./recover card.raw
1
을 반환한다.1
을 반환한다.malloc
을 사용하는 경우 메모리 누수가 없어야 한다.#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <getopt.h>
#include <stdbool.h>
typedef uint8_t BYTE;
#define BLOCK_SIZE 512
bool isJPEG(FILE *file);
void copy_block(FILE *inptr, FILE *outptr);
int main(int argc, char *argv[])
{
// Check if the correct command-line arguments were provided and print an error message if not.
if (argc != 2)
{
fprintf(stderr, "Usage: recover infile\n");
return 1;
}
char *infile = argv[1];
FILE *inptr = fopen(infile, "rb");
if (inptr == NULL)
{
fprintf(stderr, "Could not open %s.\n", infile);
return 1;
}
// Get file size and calculate the number of blocks.
fseek(inptr, 0, SEEK_END);
const int N = ftell(inptr) / BLOCK_SIZE;
// Read each 512-byte block of the input file
int fileCnt = 0;
char outfile[8];
FILE *outptr = NULL;
for (int i = 0; i < N; i++)
{
fseek(inptr, BLOCK_SIZE * i, SEEK_SET);
// If the current block contains the start of a JPEG file, create a new file to write the recovered JPEG data to.
if (isJPEG(inptr))
{
sprintf(outfile, "%03i.jpg", fileCnt++);
outptr = fopen(outfile, "a");
}
if (fileCnt > 0)
{
copy_block(inptr, outptr);
}
}
fclose(outptr);
}
bool isJPEG(FILE *file)
{
BYTE start_bytes[4];
fread(&start_bytes, 1, 4, file);
fseek(file, -4, SEEK_CUR);
if (start_bytes[0] == 0xff && start_bytes[1] == 0xd8 && start_bytes[2] == 0xff && (start_bytes[3] & 0xf0) == 0xe0)
{
return true;
}
return false;
}
void copy_block(FILE *inptr, FILE *outptr)
{
BYTE buffer[BLOCK_SIZE];
fread(buffer, 1, BLOCK_SIZE, inptr);
fwrite(buffer, 1, BLOCK_SIZE, outptr);
}
char outfile[8];
BYTE buffer[512];
파일명을 저장하는 outfile 변수와 블록의 내용을 저장하는 buffer 변수에는 계속해서 다른 값이 할당되어서 malloc을 써야 하는지 헷갈렸다.
찾아보니 이 두 변수는 1) 컴파일 시에 변수 크기가 확정되며 2) 메모리의 크기가 크지 않기 때문에 메모리 관리를 해주어야 하고 비교적 느린 malloc을 굳이 쓸 필요가 없는 것 같아 정적 메모리를 사용했다.
이번 과제는 어려운 부분은 없었는데 파일 처리 함수에 대해 잘 몰라서 시간을 많이 썼다. 파일 포인터를 이동시키면서 포렌식 파일의 각 블록에 접근하려고 했지만 원하는 값이 나오지 않았다. 두 가지 원인이 있었는데, 모두 내가 함수에 익숙하지 않은 탓이었다.
첫 번째는 fread()가 값을 읽은 뒤에 그 마지막으로 파일 포인터가 이동된다는 것을 몰랐기 때문이었다. isJPEG() 함수에서 start_bytes에 블록의 첫 네 바이트 값을 저장한 뒤 블록의 첫 위치로 포인터를 돌려놓지 않은 것이 문제였다.
두 번째는 fseek()의 모드를 SEEK_SET으로 지정해야 했는데 SEEK_CUR로 쓴 문제였다. 반복문에서 파일 포인터의 첫 부분으로부터 512 * i 만큼 떨어진 위치로 이동시켜야 하기 때문에 SEEK_SET을 써야하는데, SEEK_CUR을 쓰니까 현재 위치에서 512 * i 떨어진 위치로 이동시키는 것을 반복하니 segmentation fault 오류가 났다.