지운 파일은 어디로 가는가: 램디스크를 직접 뜯어보기
오늘의 질문
앞선 글들에서 파일 시스템이 데이터를 어떻게 배치하고, 어떤 순서로 쓰고, 위치를 어떻게 기록하는지를 다뤘다. 이번에는 방향을 뒤집는다. 그 구조를 이미 알고 있다면, 디스크 위의 바이트만 보고 무엇을 알아낼 수 있는가?
출발점은 누구나 한 번쯤 궁금해했을 질문이다.
rm으로 파일을 지웠다. 그 내용은 정말 사라졌는가? 사라지지 않았다면 어디에 남아 있고, 어떻게 확인할 수 있는가?
이 질문은 두 방향에서 실무적이다. 복구가 필요한 쪽에서도, 확실히 지웠는지 확인해야 하는 쪽에서도 답을 알아야 한다. 저장장치를 다루는 입장에서는 후자가 특히 중요하다.
먼저 답하기
두 문장으로 줄이면 이렇다.
파일을 지운다는 것은 내용을 지우는 것이 아니라 찾아가는 길을 끊는 것이다. 디렉터리 엔트리의 연결이 끊기고 비트맵이 “사용 가능”으로 바뀔 뿐, 데이터 블록의 내용은 다른 파일이 그 자리를 덮어쓰기 전까지 그대로 남는다.
그래서 파일 시스템의 구조를 알면 디스크를 직접 읽어 그 흔적을 찾을 수 있다. 이 글은 그 작업의 출발점인 슈퍼블록을 실제로 뜯어보는 데까지 간다. 슈퍼블록은 볼륨의 지도에 해당하는 구조체이고, 여기를 읽어야 나머지 위치를 계산할 수 있기 때문이다.
주제에 맞는 핵심 설명
▎포렌식은 무엇을 하는 일인가
**디지털 포렌식(digital forensics)**이란 저장장치나 메모리에 남은 데이터를 분석해 무슨 일이 있었는지를 재구성하는 작업을 말한다. 사고 조사, 침해 대응, 데이터 복구가 대표적인 쓰임이다.
핵심 전제가 하나 있다. 컴퓨터는 무언가를 “완전히 없애는” 일을 거의 하지 않는다. 없애는 데 시간이 들기 때문이다. 대신 “없는 것으로 취급”하고 넘어간다. 이 게으름이 남기는 틈이 분석의 재료가 된다.
그리고 이 분야에는 지켜야 할 원칙이 있다. 원본을 건드리지 않는 것이다. 분석 대상을 그대로 두고 사본이나 읽기 전용 상태에서 작업한다. 마운트된 상태로 뒤지면 접근 시각이 갱신되거나, 저널이 재생되거나, 최악의 경우 남아 있던 데이터가 새 할당에 덮어써질 수 있다. 관찰하는 행위 자체가 대상을 바꾸면 안 된다.
▎삭제는 지움이 아니라 해제다
이 그림이 없으면 “삭제해도 남는다”는 말이 무엇이 남고 무엇이 사라지는지 구분되지 않는다. 파일 하나를 지울 때 실제로 바뀌는 것은 네 가지다.
첫째, 디렉터리 엔트리의 연결이 끊긴다. 파일 이름과 inode 번호를 잇던 항목에서 inode 번호가 0이 되고, 앞 항목이 그 자리를 흡수하도록 길이 값이 늘어난다. 이름 문자열 자체는 한동안 남아 있을 수 있지만 어느 inode를 가리켰는지는 지워진다. 이름은 찾았는데 그게 어느 파일이었는지 알 수 없는 상황이 여기서 생긴다.
둘째, inode 비트맵의 해당 비트가 0이 된다. 그 inode 자리가 재사용 가능해진다.
셋째, 블록 비트맵의 해당 비트들이 0이 된다. 그 파일이 쓰던 데이터 블록들이 빈 공간 풀로 돌아간다.
넷째, inode 안의 위치 정보가 지워진다. ext4에서는 익스텐트 정보가 0으로 초기화된다. 이 네 번째가 복구 난이도를 결정한다.
그리고 데이터 블록의 내용은 그대로 남는다. 지우는 데 시간이 걸리기 때문에 아무도 지우지 않는다. 그저 “이 자리는 이제 써도 된다”고 표시할 뿐이다.
ext3와 ext4의 차이가 여기서 갈린다. ext3는 블록 포인터 방식이었고, 삭제 후에도 inode에 포인터가 남는 경우가 있어 그것을 따라가 데이터를 되찾는 접근이 가능했다. ext4는 익스텐트 정보를 지우므로 데이터는 남아 있는데 그 위치를 적어둔 지도가 사라진다. 여러 복구 관련 자료들이 ext4 복구가 ext3보다 까다롭다고 말하는 이유가 이것이다.
다만 완전히 닫힌 것은 아니다. ext4 포렌식을 다룬 연구들은 inode를 0으로 지우는 정도가 파일 크기와 파편화 상태에 따라 달라진다고 보고하며, 익스텐트 트리가 깊어진 경우 인덱스 노드가 남아 리프까지 따라갈 수 있는 사례를 언급한다. 저널에 남은 기록도 단서가 된다. 구조가 복잡할수록 흔적이 여러 곳에 흩어진다는 뜻이기도 하다.
여기서 저장장치 관점의 층이 하나 더 붙는다. 앞선 글에서 다룬 SSD를 떠올려보자. 파일 시스템이 “이 블록 안 쓴다”고 표시해도 그건 LBA 수준의 이야기이고, NAND의 물리 페이지에는 여전히 데이터가 남아 있다. TRIM이 전달되면 펌웨어가 무효로 표시하지만, 그것도 즉시 물리적으로 소거한다는 뜻은 아니다. 즉 논리 계층에서 지운 것과 물리 계층에서 사라진 것은 다른 이야기다.
▎실습 환경: 램디스크를 쓰는 이유
구조를 직접 뜯어보려면 실험 대상이 필요하다. 실제 디스크를 쓰면 위험하고 느리다. 그래서 램디스크를 쓴다.
램디스크는 메모리 일부를 블록 장치처럼 보이게 만든 것이다. 리눅스는 /dev/ram0 같은 장치로 제공한다. 파일 시스템 이미지를 파일로 만들어 쓰는 방법도 있다. 어느 쪽이든 장점은 같다.
- 실제 디스크를 건드리지 않는다. 실수해도 재부팅하면 사라진다.
- 크기를 작게 잡을 수 있다. 수십 MB면 구조를 보기에 충분하고, 전체를 훑어도 금방 끝난다.
- 처음부터 내가 만든 것이라 내용을 안다. 어떤 파일을 몇 개 만들었는지 알고 시작하므로, 찾아낸 것이 맞는지 검증할 수 있다.
작업 순서는 이렇다. 램디스크나 이미지 파일을 준비하고, ext4로 포맷하고, 마운트해서 파일 몇 개를 만든다. 그다음 언마운트한 뒤 장치 자체를 바이트 단위로 읽는다. 언마운트가 중요한데, 마운트된 상태에서는 페이지 캐시에만 있고 디스크에 안 내려간 내용이 있을 수 있기 때문이다. 앞선 글에서 다룬 지연 쓰기가 여기서 걸린다.
읽는 도구로는 **xxd**를 쓴다. 파일이나 장치의 내용을 16진수로 찍어주는 도구다. 자주 쓰는 형태는 이렇다.
xxd -s 1024 -l 1024 디스크이미지
-s는 건너뛸 바이트 수(시작 오프셋), -l은 읽을 길이다. 위 명령은 1024번째 바이트부터 1024바이트를 읽는다. 왜 하필 이 위치인지가 다음 절의 내용이다.
▎슈퍼블록은 항상 같은 자리에 있다
분석에는 출발점이 필요하다. 블록 크기가 얼마인지, inode가 몇 개인지, 그룹이 어떻게 나뉘는지를 모르면 나머지 위치를 계산할 수 없기 때문이다. 그 정보를 담고 있는 것이 **슈퍼블록(superblock)**이고, 다행히 위치가 고정되어 있다.
리눅스 커널의 ext4 디스크 레이아웃 문서는 이렇게 명시한다. 블록 그룹 0의 첫 1024바이트는 x86 부트 섹터 설치 등을 위해 쓰지 않고 비워두며, 슈퍼블록은 오프셋 1024바이트에서 시작한다. 그리고 슈퍼블록의 전체 크기는 1024바이트다.
| 항목 | 값 |
|---|---|
| 부트 영역 | 0x0000 ~ 0x03FF (1024 B, 사용 안 함) |
| 주 슈퍼블록 | 0x0400 ~ 0x07FF (1024 B) |
| 4 KiB 블록의 나머지 | 0x0800 ~ 0x0FFF (사용 안 함) |
| 그다음 | 그룹 서술자 테이블 |
그래서 앞의 xxd -s 1024 -l 1024가 정확히 슈퍼블록 하나를 잘라내는 명령이 된다.
같은 문서는 예외도 짚는다. 블록 크기가 1024바이트인 경우에는 0번 블록이 사용 중으로 표시되고 슈퍼블록이 1번 블록으로 간다. 바이트 오프셋 1024는 그대로이고 블록 번호만 달라지는 셈이다.
백업 슈퍼블록도 있다. 볼륨 앞부분이 손상되어도 복구할 수 있게 사본을 흩뿌려 둔다. 오라클의 ext4 레이아웃 해설에 따르면, ext2에서는 모든 블록 그룹에 사본을 두었지만 ext4의 sparse_super 기능이 켜지면 블록 그룹 0과 1, 그리고 3·5·7의 거듭제곱에 해당하는 그룹에만 사본이 놓인다. 3의 거듭제곱이면 9, 27, 81 하는 식이다.
백업의 바이트 위치는 계산으로 나온다.
오프셋 = 그룹 번호 × 그룹당 블록 수 × 블록 크기
가정 — 블록 4 KiB, 그룹당 32,768블록.
| 그룹 | 시작 블록 | 바이트 오프셋 | 위치 |
|---|---|---|---|
| 0 (주) | 0 | 1,024 | 볼륨 맨 앞 |
| 1 | 32,768 | 134,217,728 | 약 128 MiB 지점 |
| 3 | 98,304 | 402,653,184 | 약 384 MiB 지점 |
| 5 | 163,840 | 671,088,640 | 약 640 MiB 지점 |
적용 한계 — 그룹당 블록 수는 파일 시스템마다 다르며 슈퍼블록의 필드를 읽어야 알 수 있다. 32,768은 흔한 값일 뿐 고정값이 아니다. 또 flex_bg 같은 기능이 켜져 있으면 그룹 안의 배치 순서가 달라지므로, 위 계산은 슈퍼블록 위치에만 적용된다.
한 가지 더 짚어둘 점이 있다. 같은 자료들은 백업 슈퍼블록과 백업 그룹 서술자를 커널이 평소에 갱신하지 않는다고 밝힌다. 파일 시스템 크기 변경 같은 근본적인 변경이 있을 때만 갱신된다. 그래서 주 슈퍼블록과 백업의 값이 다를 수 있고, 그 차이 자체가 무슨 일이 있었는지에 대한 단서가 된다.
여기서 앞선 글의 이야기가 다시 만난다. FFS가 그룹마다 슈퍼블록 사본을 둔 것은 성능 설계의 부산물이었는데, 그 설계가 지금은 복구와 분석의 근거로도 쓰이고 있다.
▎슈퍼블록 안을 읽어보기
이 그림이 없으면 헥스덤프에 찍힌 숫자들이 어느 필드인지 알 수 없다. 1024바이트 안에서 특히 자주 보게 되는 필드들을 짚어보자.
먼저 확인할 것은 매직 넘버다. 오프셋 0x38에 2바이트로 들어 있고 값은 **0xEF53**이다. 이 값이 보이면 “여기가 ext 계열 파일 시스템의 슈퍼블록이 맞다”는 확인이 된다. 손상된 디스크에서 백업 슈퍼블록을 찾을 때도 이 패턴을 단서로 삼는다.
그런데 헥스덤프에는 뒤집혀 보인다. ext는 리틀엔디언이라 0xEF53이 디스크에는 53 ef 순서로 저장된다. 오라클 해설도 이 점을 명시적으로 설명하는데, 처음 헥스덤프를 볼 때 가장 자주 걸리는 함정이라 짚어둘 만하다. 4바이트 값도 마찬가지로 뒤에서부터 읽어야 한다.
그다음이 크기 관련 필드들이다. 오프셋 0x00의 전체 inode 개수, 0x04의 전체 블록 개수, 0x18의 블록 크기가 기본 골격을 알려준다. 특히 블록 크기는 다른 모든 오프셋 계산의 기준이 되므로 가장 먼저 확정해야 한다.
구조 계산에 필요한 필드도 있다. 0x20의 그룹당 블록 수는 앞에서 백업 슈퍼블록 위치를 계산할 때 쓴 값이고, 0x28의 그룹당 inode 수는 앞선 글에서 다룬 그룹 = (inode번호 − 1) ÷ 그룹당 inode수 계산에 들어가는 값이다. 이 둘을 알아야 “이 inode가 어느 그룹에 있는지”를 계산할 수 있다.
시각과 상태 필드는 타임라인 작성에 쓰인다. 마지막 마운트 시각, 마지막 기록 시각, 마지막 검사 시각이 각각 별도 필드로 있고, 오프셋 0x3A의 상태 필드는 정상적으로 언마운트되었는지 아니면 크래시 상태로 남았는지를 나타낸다.
기능 플래그 세 벌(0x5C, 0x60, 0x64)은 이 파일 시스템이 어떤 기능을 쓰는지 알려준다. 익스텐트를 쓰는지, 64비트인지, sparse_super가 켜져 있는지가 여기서 결정된다. 앞의 백업 슈퍼블록 위치 계산이 유효한지도 이 플래그를 봐야 확정할 수 있다.
값을 해석했으면 검산하는 습관이 중요하다. dumpe2fs 같은 도구가 같은 슈퍼블록을 읽어 사람이 읽을 수 있는 형태로 출력해준다. 손으로 읽은 값과 도구의 출력이 일치하는지 대조하면, 오프셋을 잘못 잡았거나 엔디언을 뒤집어 읽은 실수를 바로 잡을 수 있다. 손으로 읽는 이유는 도구를 대체하려는 게 아니라 도구가 무엇을 하고 있는지 알기 위해서다.
▎전체 그림으로 다시 보면
지금까지의 순서를 한 줄로 이으면 분석 작업의 골격이 나온다.
- 원본을 보호한다. 사본을 뜨거나 읽기 전용으로 붙인다. 마운트한 채로 뒤지지 않는다.
- 슈퍼블록을 읽는다. 오프셋 1024에서 1024바이트. 매직 넘버로 확인하고 블록 크기를 확정한다.
- 구조를 계산한다. 그룹당 블록 수와 inode 수로 각 그룹의 위치를 계산한다.
- 그룹 서술자를 읽는다. 각 그룹의 비트맵과 inode 테이블이 정확히 어디인지 확인한다.
- inode와 데이터를 따라간다. 여기서부터가 실제 파일의 흔적을 쫓는 단계다.
앞선 글들에서 배운 구조가 그대로 도구가 된다는 점이 이 작업의 성격을 잘 보여준다. 블록 그룹을 알아야 inode 위치를 계산하고, 익스텐트를 알아야 데이터 블록을 찾고, 저널을 알아야 최근 변경 흔적을 읽는다. 파일 시스템 설계를 이해하는 것과 그 위의 흔적을 읽는 것은 같은 지식의 앞뒷면이다.
자주 묻는 질문
파일을 확실히 지우려면 어떻게 해야 하나요? 매체에 따라 방법이 다르다는 점이 핵심이다. 회전 디스크에서는 해당 위치에 다른 값을 덮어쓰면 원래 내용이 실제로 사라진다. 그런데 SSD에서는 이 방식이 그대로 통하지 않는다. 앞선 글에서 봤듯 덮어쓰기 요청은 같은 물리 페이지가 아니라 다른 자리에 기록되고, 원래 페이지는 무효로 표시된 채 남는다. 그래서 SSD에서는 파일 단위로 덮어쓰는 방식보다 장치가 제공하는 소거 기능을 쓰거나, 처음부터 암호화된 상태로 저장하고 키를 폐기하는 방식이 확실하다. 어느 쪽이든 장치 문서를 확인하고 실제로 검증하는 절차가 필요하다.
마운트한 채로 분석하면 정확히 무엇이 문제인가요? 세 가지가 겹친다. 첫째, 읽기만 해도 접근 시각 같은 메타데이터가 갱신되어 타임라인이 오염될 수 있다. 둘째, 마운트 과정에서 저널이 재생되면서 디스크 내용이 실제로 바뀔 수 있다. 셋째, 파일 시스템이 정상 동작하면서 새 블록을 할당하면 남아 있던 데이터가 덮어써질 수 있다. 세 번째가 가장 치명적이다. 그래서 읽기 전용 마운트나 별도 사본 위에서 작업하는 것이 기본 절차다.
dumpe2fs 같은 도구가 있는데 굳이 xxd로 손수 읽어야 하나요? 실무에서는 도구를 쓰는 것이 빠르고 정확하다. 손으로 읽는 것은 도구가 무엇을 하는지 이해하기 위한 과정이다. 그리고 손으로 읽을 줄 알아야 하는 상황도 실제로 생긴다. 슈퍼블록이 손상되어 도구가 파일 시스템을 인식하지 못할 때, 백업 슈퍼블록을 직접 찾아야 할 때, 도구가 지원하지 않는 구조를 다뤄야 할 때가 그렇다. 정상 상태에서는 도구를 쓰고, 비정상 상태를 다룰 때 손으로 읽는 능력이 필요해진다.
램디스크로 연습한 것이 실제 장치에서도 그대로 통하나요? 파일 시스템 구조 자체는 동일하다. 슈퍼블록 위치, 필드 오프셋, 계산식은 매체와 무관하다. 다만 매체 계층에서 달라지는 부분이 있다. 램디스크와 파일 이미지는 논리 주소와 저장 위치가 일대일로 대응하지만, 실제 SSD에서는 그 사이에 FTL이 끼어 있다. 그래서 장치를 통해 읽은 LBA 내용과 NAND에 실제로 남아 있는 것이 같지 않을 수 있다. 파일 시스템 계층의 분석은 그대로 유효하고, 그 아래 물리 계층은 별도의 접근이 필요하다고 나눠 생각하는 편이 정확하다.
저장장치를 폐기할 때 실무적으로 무엇을 확인해야 하나요? 이 글의 내용을 뒤집으면 점검 항목이 나온다. 첫째, 파일을 지우거나 포맷하는 것만으로는 내용이 사라지지 않는다는 전제에서 출발해야 한다. 둘째, 장치가 제공하는 소거 기능을 썼다면 그 기능이 실제로 무엇을 하는지 문서로 확인해야 한다. 논리적 매핑만 초기화하는 것과 물리적으로 소거하는 것은 다르다. 셋째, 가능하면 소거 후 실제로 읽어서 확인하는 절차를 두는 것이 좋다. 조직 차원에서 다룬다면 매체 폐기 관련 공식 지침을 참고해 절차를 문서화하는 편이 안전하다.