Air-Gapped Sites — Defence and Government
Quarantining Incoming Media Before You Unpack It
한국어 원문으로 표시합니다.
목표
반입 매체의 압축 파일을 풀기 전에 목록만 읽어 규모·위험 항목·압축비·형식 불일치를 판정하는 검역기를 만들고, 추출 필터로 항목마다 걸러 격리 디렉터리에 푼 뒤 근거 해시를 붙인 반입 판정서를 씁니다.
왜 중요한가
폐쇄망에 들어오는 것은 대부분 사람이 들고 온 매체이고, 그 안에는 압축 파일 하나가 들어 있습니다. 안을 보려고 푸는 그 한 줄이 이미 파일 시스템을 건드리는 행위입니다. 절대 경로, 상위 경로 탈출, 밖을 가리키는 링크, 특수 파일, 압축 폭탄은 전부 푸는 순간에 효과가 납니다. tar 헤더에는 이름·크기·권한·항목 종류·링크 대상이 적혀 있어서, 풀지 않고도 이 다섯 가지를 전부 볼 수 있습니다. 그래서 순서를 바꿉니다. 먼저 읽고, 판정하고, 조건을 걸어 풉니다. 판정서에는 근거가 붙어야 합니다. 어떤 아카이브의 어떤 해시를 어떤 보고서로 읽었고 그 보고서에서 어떤 사유가 나왔는지가 이어져야, 협력사가 무엇을 고쳐야 하는지 알고 심사자는 같은 판정을 다시 낼 수 있습니다. 채점기는 여러분의 문구를 믿지 않습니다. 아카이브를 직접 열어 나와야 하는 값을 스스로 계산하고, 실행 계약대로 여러분의 검역기를 다시 돌려 그 출력까지 대조합니다.
단계
- 생성 스크립트로 매체 두 개를 만들고 접수 대장 /root/media/intake.tsv 에 일련번호·수령 시각·제출자 역할·바깥 해시·크기를 적습니다.
- /root/media/inspect.py 를 만들어 MEDIA-2609-017 을 풀지 않고 목록만 읽어 항목 수·총 해제 크기·최대 항목을 /root/media/report/MEDIA-2609-017.json 에 적습니다.
- inspect.py 에 danger 분류를 넣습니다. absolute·traversal·escaping_link·special·exec_bit 다섯 칸을 두고 해당 없는 칸도 빈 목록으로 남깁니다.
- inspect.py 에 ratio 와 ratio_verdict 를 넣습니다. 총 해제 크기를 압축 파일 크기로 나눈 값이 100 을 넘으면 bomb 입니다.
- inspect.py 에 mismatch 를 넣습니다. 확장자가 말하는 형식과 앞머리 바이트가 말하는 형식이 다른 항목을 name·declared·actual 로 적습니다.
- inspect.py 에
--extract-to를 넣어 data 필터로 항목마다 판정하고, /root/media/quarantine/MEDIA-2609-017 에 풀고 extracted 와 rejected 를 보고서에 적습니다. - /root/media/decide.py 로 보고서를 반입 판정서 /root/media/report/verdict-MEDIA-2609-017.json 으로 옮깁니다. media·archive_sha256·report_sha256·decision·reasons·accepted_entries·rejected_entries 를 적습니다.
- 같은 검역기와 같은 판정 규칙을 MEDIA-2609-018 에 돌려 /root/media/report/MEDIA-2609-018.json 과 /root/media/report/verdict-MEDIA-2609-018.json 을 만들고, 두 매체를 나란히 놓은 /root/media/report/compare.tsv 를 씁니다.
참고
- 실행 계약:
python3 /root/media/inspect.py --archive <매체> --out <보고서> [--extract-to <디렉터리>]. 판정 규칙은python3 /root/media/decide.py --report <보고서> --out <판정서>입니다. 채점기가 이 계약대로 직접 다시 돌립니다. - 매체는
/root/media/in/MEDIA-2609-017.tar.gz와/root/media/in/MEDIA-2609-018.tar.gz, 격리 디렉터리는/root/media/quarantine/<일련번호>입니다. - 판정 규칙(이 실습의 가정): absolute·traversal·escaping_link·special·bomb 중 하나라도 있으면 reject, 사유가 exec_bit 이나 mismatch 뿐이면 quarantine, 아무것도 없으면 accept 입니다. reasons 는 사유 이름을 정렬한 목록입니다.
- 목록은
tar -tvf, 파이썬은tarfile.open(...).getmembers()로 읽습니다. 항목별 판정은tarfile.data_filter(member, dest)가 냅니다. - 앞머리 바이트는
od -An -tx1 -N8이나 파이썬으로 봅니다. NUL 이 섞인 항목이 있어grep·head로는 제대로 못 봅니다. - 흔한 실수:
extractall()에 필터만 걸어 첫 거절에서 멈추기, 보고서를 다시 만들고 판정서의 해시는 그대로 두기, danger 의 빈 칸을 아예 빼 버리기. - 이 실습은 폐쇄망이라
zip·unzip·xz·gpg가 없습니다.tar와gzip만 씁니다.
매체를 열기 전에 접수 대장부터
생성 스크립트로 매체 두 개를 만들고 /root/media/intake.tsv 에 머리글 1줄과 매체 2줄을 적으세요. 칸은 serial·received_at·submitter_role·sha256·bytes 이고 탭으로 나눕니다.
밖에서 받아 올 표본이 없으니 python3 로 직접 만듭니다. 시드와 mtime 이 고정돼 있어 몇 번을 돌려도 같은 매체가 나옵니다. 해시는 아카이브를 풀지 않고 파일 그대로 계산합니다 - 이 값이 나중에 '그때 받은 그 파일인가' 를 되묻는 유일한 근거입니다. 제출자는 사람 이름이 아니라 역할로 적습니다.
풀지 않고 목록만 읽어 규모 재기
/root/media/inspect.py 가 MEDIA-2609-017 을 풀지 않고 헤더만 읽어 archive_sha256·compressed_bytes·entries·total_uncompressed_bytes·largest_entry_bytes·largest_entry_name 을 /root/media/report/MEDIA-2609-017.json 에 쓰게 하세요.
tarfile.open(경로, 'r:gz').getmembers() 가 헤더 목록을 그대로 돌려줍니다. 각 항목의 size 는 헤더에 적힌 값이라 내용을 읽지 않아도 됩니다. archive_sha256 은 아카이브 파일 자체의 해시이고 compressed_bytes 는 그 파일의 크기입니다.
위험 항목을 다섯 갈래로 나누기
inspect.py 가 보고서에 danger 를 넣게 하세요. absolute·traversal·escaping_link·special·exec_bit 다섯 칸에 항목 이름 목록을 적고, 해당 항목이 없는 칸도 빈 목록으로 둡니다.
이름이 슬래시로 시작하면 절대 경로입니다. 상위 경로 탈출은 문자열에 '..' 가 들어 있는지가 아니라 칸 단위로 깊이를 세어 판정합니다. 링크는 issym 과 islnk 를 나눠 보세요 - 심볼릭 링크는 자기가 놓인 자리 기준이고 하드 링크는 아카이브 뿌리 기준입니다. 특수 파일은 ischr·isblk·isfifo 입니다.
압축비로 폭탄 가리기
inspect.py 가 보고서에 ratio 와 ratio_verdict 를 넣게 하세요. ratio 는 총 해제 크기를 압축 파일 크기로 나눠 소수 둘째 자리에서 반올림한 값이고, 100 을 넘으면 ratio_verdict 는 bomb, 아니면 ok 입니다.
두 값 모두 이미 보고서에 있습니다. 풀어서 재지 마세요 - 폭탄을 판정하려고 폭탄을 푸는 것이 됩니다. 임계값은 상수로 빼 두면 다음 사람이 조직 기준에 맞춰 바꿀 수 있습니다.
확장자와 실제 형식 맞춰 보기
inspect.py 가 보고서에 mismatch 를 넣게 하세요. 정규 파일 가운데 확장자가 말하는 형식과 앞머리 바이트가 말하는 형식이 다른 항목을 name·declared·actual 로 적습니다.
extractfile 은 디스크에 풀지 않고 스트림만 엽니다. 앞머리 512바이트면 충분합니다. gzip 은 1f 8b, ELF 는 7f 45 4c 46 으로 시작합니다. 앞머리에 NUL 이 있으면 텍스트가 아닙니다. 확장자를 모르는 항목은 판정하지 말고 건너뛰세요 - 모르는 것을 틀렸다고 적으면 목록이 못 쓰게 됩니다.
추출 필터로 항목마다 걸러 풀기
inspect.py 에 --extract-to 를 넣어 data 필터로 항목마다 판정하고 /root/media/quarantine/MEDIA-2609-017 에 풀게 하세요. 보고서에 extracted 수와 rejected 목록(name·reason)을 적습니다.
extractall 에 filter='data' 만 걸면 첫 거절에서 예외가 올라와 멈춥니다. 항목마다 tarfile.data_filter(member, dest) 를 직접 불러 FilterError 를 받으면 거절 항목과 사유가 한 번에 모입니다. reason 은 예외 클래스 이름을 그대로 쓰세요. 거절되지 않은 항목만 모아 마지막에 한 번에 풉니다.
근거 해시를 붙인 반입 판정서 쓰기
/root/media/decide.py 로 보고서를 읽어 /root/media/report/verdict-MEDIA-2609-017.json 을 만드세요. media·archive_sha256·report_sha256·decision·reasons·accepted_entries·rejected_entries 를 적습니다.
판정 규칙은 안내의 '## 참고' 에 있습니다. reasons 는 danger 에서 비어 있지 않은 칸 이름에 bomb 과 mismatch 를 더해 정렬한 목록입니다. report_sha256 은 sha256sum /root/media/report/MEDIA-2609-017.json 의 값입니다 - 보고서를 다시 만들면 이 값도 다시 적어야 합니다. 막는 항목을 고쳐서 통과시키는 단계가 아닙니다.
두 번째 매체로 판정이 갈리는지 보기
같은 검역기와 같은 판정 규칙을 MEDIA-2609-018 에 돌려 보고서·판정서를 만들고, 두 매체를 나란히 놓은 /root/media/report/compare.tsv 를 쓰세요. 머리글은 media·entries·ratio·ratio_verdict·danger·mismatch·rejected·decision 입니다.
새로 짜지 말고 앞에서 만든 두 스크립트를 그대로 씁니다. danger 칸은 다섯 갈래의 항목 수를 전부 더한 값입니다. 절차가 값에 반응하지 않고 늘 같은 답을 내면 그건 검역이 아니라 도장입니다 - 두 줄의 decision 이 갈리는지 보세요.