데이터를 다루는 조사·인텔리전스 업무에서는 표준 포맷으로 깔끔하게 정리된 데이터만 들어오지 않는다. 보안 조사 기업 Glazer는 최근 CronosPro의 원본 파일(CroBank.dat, CroIndex.dat, CroStru.dat)을 받았는데, 기존 도구로는 제대로 파싱되지 않아 '손상된 파일'로 여겨지던 것이었다. 이들은 파일을 폐기하는 대신 포맷 자체를 역공학해 데이터를 복원했고, 그 과정을 공개했다. 단순한 무용담이 아니라, 문서화되지 않은 이진 포맷을 안전하게 다뤄야 하는 실무자에게 참고할 만한 방법론이 담겨 있다.
CronosPro라는 낯선 유산
CronosPro(Cronos)는 러시아와 구소련권 조직에서 등기부, 검색 가능한 아카이브, 내부 정보 시스템을 구축하는 데 오래 쓰인 상용 데스크톱 데이터베이스다. 일반적인 관계형 DB와 용어부터 다르고, 하나의 데이터베이스가 여러 이진 파일로 나뉜다. .dat 파일에는 실제 데이터가 들어 있고, 대응하는 .tad 파일은 각 레코드가 .dat 안 어디에 있는지 알려주는 디렉터리 역할을 한다. 이 구조에서 핵심은 레코드 데이터 자체는 읽을 수 있었지만, 그것을 해석할 스키마(CroStru.dat)가 보호되어 있었다는 점이다. 스키마 없이는 CroBank.dat의 거대한 값 스트림이 그저 이진 구분자로 나뉜 정체불명의 바이트열에 불과했다.
오픈소스 변환기 Cronodump가 포맷의 상당 부분을 읽어내지만, 버전별 세부 차이 때문에 항상 성공하지는 못한다. 이번 덤프도 그런 경우였다. Glazer는 코드 분석에 Codex를 활용해 Cronodump의 파싱 로직을 개선하는 방식으로 접근했다. 여기서 말하는 정규화(normalization)는 관계형 정규형 이론이 아니라, 독점 이진 포맷을 CSV처럼 이식 가능하고 검토 가능한 형태로 바꾸는 것을 뜻한다. 목표는 단순히 문자열을 뽑아내는 게 아니라 값과 컬럼의 대응 관계를 정확히 보존하는 것이었다. 읽을 수 있는 값이 엉뚱한 헤더 아래 놓인 CSV는, 겉보기엔 멀쩡하면서 의미상 오염되어 있어 명백한 오류보다 오히려 위험하기 때문이다.
압축이 아니라 KOD 치환
문제를 좁히기 위해 먼저 모든 파일의 헤더를 살폈다. 덤프는 Cronos v4에 해당하는 01.11 포맷이었고, 구성 요소 플래그를 보니 보호된 것은 작은 스키마 파일뿐이었다. 즉 수십 기가바이트의 레코드 데이터를 해독할 필요 없이, 상대적으로 작은 CroStru만 읽어내면 나머지는 일반 파서로 처리할 수 있었다. 여기서 압축과 KOD 보호를 구분하는 것이 중요하다. 압축은 데이터 크기를 줄이는 것이고, KOD는 256개 항목짜리 바이트 치환표(S-box)로 값을 바꾸는 보호 기법이다. 게다가 Cronos의 KOD 디코딩은 바이트의 위치와 레코드 번호에 따라 달라지기 때문에, 단 한 바이트만 어긋나도 그 이후 전체 해독이 무너진다.
Cronodump에는 통계적 성질을 이용하는 두 가지 복구 방식이 있다. strucrack은 스키마에 흔한 0 바이트를 근거로, 위치별로 가장 빈번한 결과가 평문 0일 것이라 가정해 KOD 표를 추정한다. 또 하나는 압축 레코드 헤더의 예측 가능한 바이트로 매핑을 유추한다. 그러나 이번 파일들은 애초에 KOD로 인코딩되어 있지 않아 후자의 가정이 성립하지 않았다. 두 방식 모두 자동으로는 유효한 표를 복구하지 못했다. 관련 미병합 브랜치(PR #22)가 KOD 모호성을 다루긴 했지만, 이번 사례의 다른 문제까지 해결하지는 못했다.
할당 문제로 푼 KOD 표
Glazer는 참조용으로 확보한 정상 Cronos 컴포넌트와 테스트 데이터베이스에서 스키마 바이트의 일반적 빈도 분포를 추정했다. 스키마를 그대로 베낀 것이 아니라, 유효한 구조가 어떤 바이트를 자주 포함하는지를 기준으로 삼은 것이다. 암호문 바이트마다 가능한 모든 KOD 매핑이 만들어낼 평문을 계산하고, 참조 분포에 비춰 얼마나 그럴듯한지 점수를 매겼다. 문제는 각 바이트마다 독립적으로 최고 점수를 고르면 여러 바이트가 같은 출력을 택해 유효하지 않은 표가 나온다는 점이다. 이는 전형적인 할당 문제였고, SciPy의 헝가리안 알고리즘(linear_sum_assignment)으로 유일성을 강제하면서 전체 우도를 최대화해 완전한 256바이트 순열을 얻었다.
읽을 수 있는 출력만으로는 증거가 약하다. 잘못된 치환도 우연히 글자나 숫자를 만들어낼 수 있기 때문이다. 후보 표를 예상 구조와 대조하니 한 레코드만 빼고 모두 통과했다. 그 예외는 v4의 0x08 플래그를 가진 첫 CroStru 레코드였는데, 데이터가 12바이트 익스텐트 헤더로 시작했다. 파서가 헤더까지 페이로드와 함께 KOD 디코더에 넘긴 것이 원인이었다. 디코딩이 위치에 의존하므로 12바이트 헤더는 단순히 불필요한 12글자가 아니라 이후 모든 페이로드의 위치 i를 12만큼 어긋나게 만들었다. 파서가 헤더를 건너뛰고 선언된 페이로드만 디코딩하도록 고치자 레코드가 올바르게 풀렸다. 디코딩 실패가 항상 잘못된 키를 뜻하지는 않으며, 옳은 디코더가 잘못된 경계에 적용된 경우도 있다는 교훈이다.
인코딩과 컬럼 정렬의 마지막 함정
스키마가 풀리자 데이터 테이블과 파일 테이블 구조가 재구성됐지만, 첫 CSV를 표본 검사하니 텍스트 값이 16진수 문자열로 보였다. 손상이 아니라 Windows-1251로 저장된 키릴 문자가 16진수로 표현된 것이었다. 이전에 안전을 위해 미지의 필드 타입을 그대로 16진수로 보존하도록 한 변경이 알려진 텍스트 타입까지 건드린 탓이다. 문서화된 타입 1, 2, 3을 명시적으로 디코딩하고 미지의 이진 타입만 무손실 16진수로 내보내도록 고쳤다. 그러자 이름 등 텍스트는 읽을 수 있게 됐지만, 일부 값이 여전히 엉뚱한 헤더 아래 놓였다.
원인은 숨겨진 필드였다. Cronos 레코드에는 통상적인 가시 정의가 없는 내부 필드가 있을 수 있고, 필드 정의의 idx2 값이 직렬화된 레코드 안 실제 위치를 지정한다. 파서가 보이는 세 정의를 저장된 첫 세 값에 그대로 대응시키자, 소비되어야 할 숨은 값들이 무시되며 데이터가 통째로 밀렸다. 겉보기엔 컬럼 수가 맞아 유효해 보였지만 값은 무관한 컬럼 아래로 이동한 상태였다. 파서가 물리적 필드 위치를 추적하고, 각 가시 정의를 읽기 전에 idx2 위치까지 숨은 필드를 소비하도록 바꿨다. 0x1b로 시작하는 복합 값 처리도 함께 보존했다. 이 사례 전체가 남기는 실무적 함의는 명확하다. 출력이 그럴듯해 보인다는 것만으로 파싱을 신뢰해서는 안 되며, 경계·인코딩·필드 정렬을 표본으로 직접 검증하지 않으면 조용히 오염된 데이터가 그대로 흘러간다. 다만 이 접근은 참조 분포와 알려진 구조가 있을 때 성립하는 휴리스틱이라는 한계도 함께 기억할 필요가 있다.
TTJ 코딩클래스 정규반
월급 외 수입,
코딩으로 만들 수 있습니다
17가지 수익 모델을 직접 실습하고, 1,300만원 상당의 자동화 도구와 소스코드를 받아가세요.
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공