들어가며
이 글은 상용 DLP(Data Loss Prevention) 프로그램의 핵심 기능을 분석하고, 윈도우 커널 드라이버 기반으로 직접 구현해보는 시리즈입니다.
지난 글에서는 네트워크 필터(WFP)의 한계와 유저모드 클립보드 차단을 다루었습니다. 이번 편에서는 다시 커널로 돌아가서, Minifilter를 확장하여 '민감정보 패턴 검사'와 '파일 첨부 차단' 기능을 구현하는 과정, 그리고 그 과정에서 마주친 Office 파일(.xlsx) 검사의 딜레마를 어떻게 해결했는지 공유합니다.
그리고 파일 첨부를 차단하는 기능을 구현하는 과정에 대해 서술합니다.
이전 글 보러가기
https://imoracle.tistory.com/75
[DLP 개발기] 4. WFP DNS 차단의 한계와 유저모드 클립보드 제어
들어가며이 글은 상용 DLP(Data Loss Prevention) 프로그램의 동작 원리를 분석하고, 윈도우 커널 드라이버 기반의 보안 솔루션을 직접 구현해보는 시리즈입니다.지난 글에서는 WPF 트레이 앱 통신과 매
imoracle.tistory.com
1. 구현 목표 및 정책 확장
이번 단계의 핵심 목표는 두 가지입니다.
- 민감정보 검사 : 파일 저장 시 버퍼를 읽어 주민번호, 카드번호, 전화번호 패턴이 있으면 저장을 차단 (또는 사후 격리).
- 파일 첨부 차단: 웹 브라우저나 메일 클라이언트가 특정 확장자 파일을 열려고 할 때 권한 거부.
이를 위해 드라이버와 에이전트가 공유하는 OG_POLICY 구조체에 관련 필드를 추가합니다.

[주의] 구조체를 수정했다면 드라이버, 필터, 에이전트 전체를 Clean/Rebuild 하고 .sys 파일을 교체해야 합니다. 크기가 1바이트라도 다르면 IOCTL 통신 시 즉시 블루스크린(BSOD)이 발생합니다.
2. 민감정보 패턴 검사 1단계: 커널 실시간 차단 (IRP_MJ_WRITE)
메모장(.txt)이나 CSV 같은 평문 파일은 디스크에 쓰이기 직전(IRP_MJ_WRITE)에 커널 버퍼를 낚아채서 검사할 수 있습니다.
상태머신(State Machine) 기반 패턴 매처 구현
커널에서는 <regex> 같은 C++ 표준 정규식 라이브러리를 쓸 수 없습니다.
따라서 버퍼를 순회하며 직접 문자열 상태를 체크하는 매칭 로직을 구현해야 합니다.
주민등록번호 검사 예시 (앞 6자리 - 뒷자리 1~4 시작)

이제 PreWriteCallback에서 커널 쓰기 버퍼(WriteBuffer 또는 MdlAddress)를 가져와 위 함수로 검사합니다. 패턴이 발견되면 STATUS_ACCESS_DENIED를 반환하여 저장을 원천 차단합니다.
민감정보로 인해 저장이 되지 않는 모습

(※ 오탐 주의: Windows Search DB의 무작위 바이너리가 우연히 주민번호 패턴과 일치하여 차단되는 문제가 발생해, \Windows, \ProgramData 경로는 검사에서 제외하는 로직을 추가해야 했습니다.)

이 부분은 나중에 관리자 웹 대시보드에서 차단할 폴더 경로를 동적으로 받아오도록 변경해 준다면 더욱 좋겠네요.
3. 구조적 한계: 엑셀(.xlsx) 파일은 왜 안 막힐까?
위 로직을 적용하고 기분 좋게 테스트를 진행했는데, 큰 벽에 부딪혔습니다. 메모장(.txt)은 완벽하게 막아내는데, 엑셀(.xlsx)에 민감정보를 적고 저장하면 아무런 차단이 안 되는 것입니다.
이유는 간단하면서도 절망적입니다. 엑셀(.xlsx)이나 워드(.docx) 파일은 사실 평문 파일이 아니라 XML 파일들을 ZIP으로 압축해 놓은 아카이브(Archive)이기 때문입니다.
txt 파일 구조

xlsx 파일 구조
document.xlsx (ZIP 파일)
├── [Content_Types].xml
├── _rels/
├── xl/
│ ├── workbook.xml
│ ├── worksheets/
│ │ └── sheet1.xml ← 실제 셀 데이터
│ └── sharedStrings.xml ← 문자열 데이터
└── docProps/
셀에 260506-3333333 을 입력하면 실제 디스크에 이렇게 저장됩니다
<!-- sharedStrings.xml -->
<sst>
<si><t>260506-3333333</t></si>
</sst>
위 XML 데이터가 zlib으로 압축되어 디스크에 쓰이기 때문에, 커널의 IRP_MJ_WRITE 버퍼에는 주민번호가 아닌 PK\x03\x04... 로 시작하는 알 수 없는 압축 바이너리 덩어리만 들어오게 됩니다.
평문 패턴 매칭이 불가능한 것이죠.
4. 민감정보 패턴 검사 2단계: 유저모드 사후 격리 아키텍처
이 문제를 해결하기 위해 상용 DLP들은 보통 두 가지 방식을 씁니다.
방식 1: 커널에서 ZIP 해제 + XML 파싱

1. IRP_MJ_WRITE 단계에서 커널 엔진으로 압축을 풀고 파싱
문제점 :
- 커널에서 zlib 구현이 매우 복잡
- 압축 해제 중 메모리 할당이 크면 커널 풀 고갈 위험
- 분할 쓰기 시 ZIP 블록 경계 처리 어려움
- 구현 난이도가 매우 높음
방식 2: 에이전트 레벨 검사 (현실적)

에이전트 레벨 사후 검사인 방식입니다.
파일 저장이 완료되면 유저모드 에이전트가 파일을 열어서 검사하고 위반 시 강제 격리하는 방법이며
훨씬 현실적이고 구현 가능합니다.
Step 1. 커널: 파일 저장 완료 감지 및 경로 전달
파일 쓰기가 끝나고 핸들이 닫히는 IRP_MJ_CLEANUP의 PostOp 콜백을 사용합니다. 파일이 .xlsx라면 유저모드 에이전트로 경로를 쏘아줍니다.
FLT_POSTOP_CALLBACK_STATUS PostCleanupCallback(...) {
// ... xlsx, docx 확장자 확인 ...
if (isTarget && g_FilterPolicy.blockSensitiveData) {
WCHAR pathBuf[MAX_PATH] = { 0 };
wcsncpy_s(pathBuf, nameInfo->Name.Buffer, nameInfo->Name.Length / sizeof(WCHAR));
// FltSendMessage로 유저모드 에이전트에 경로 전송
FltSendMessage(g_FilterHandle, &g_ClientPort, pathBuf, nameInfo->Name.Length, NULL, NULL, NULL);
}
return FLT_POSTOP_FINISHED_PROCESSING;
}
Step 2. 에이전트: xlsx 압축 해제 및 파싱 (minizip + tinyxml2)
에이전트는 메시지 스레드를 돌리며 경로를 수신 대기합니다. 경로를 받으면 외부 오픈소스 라이브러리를 사용해 압축을 풀고 XML 텍스트를 파싱하여 검사합니다.
BOOL CheckXlsxSensitiveData(LPCWSTR filePath) {
// 파일 잠금 해제를 위해 잠시 대기
Sleep(500);
unzFile zf = unzOpen(ansiPath); // ZIP 열기
// 검사할 핵심 XML 파일 (엑셀 셀 데이터, 워드 본문)
const char* targets[] = { "xl/sharedStrings.xml", "xl/worksheets/sheet1.xml", "word/document.xml" };
BOOL found = FALSE;
for (int t = 0; t < 3 && !found; t++) {
if (unzLocateFile(zf, targets[t], 0) == UNZ_OK && unzOpenCurrentFile(zf) == UNZ_OK) {
char xmlBuf[1024 * 1024] = { 0 };
unzReadCurrentFile(zf, xmlBuf, sizeof(xmlBuf) - 1);
unzCloseCurrentFile(zf);
// tinyxml2로 파싱 및 텍스트 노드 추출 후 패턴 검사
tinyxml2::XMLDocument doc;
if (doc.Parse(xmlBuf) == tinyxml2::XML_SUCCESS) {
ExtractAndCheckXmlText(doc.RootElement(), &found);
}
}
}
unzClose(zf);
// 패턴 발견 시 Quarantine(보안 격리 폴더)으로 파일 이동
if (found) QuarantineFile(filePath);
return found;
}
이제 엑셀에 민감정보를 적고 저장하면, 파일이 디스크에 써지자마자 에이전트가 이를 낚아채어 쥐도 새도 모르게 격리 폴더로 이동(MoveFileW) 시켜버립니다. 실시간 차단은 아니지만 정보 유출을 막는 완벽한 대안입니다.
구현 시 주의사항
파일 잠금 문제
IRP_MJ_CLEANUP PostOp 시점에 파일이 닫히는 중이라 에이전트에서 바로 열면 잠금 충돌이 날 수 있습니다. 약간의 딜레이 후 열어야 합니다.
Sleep(500); // 파일 완전히 닫힐 때까지 대기
unzFile zf = unzOpen(ansiPath);
대용량 파일 처리
1MB 버퍼로 XML을 읽는데 실제 xlsx는 더 클 수 있습니다. 스트리밍 방식으로 처리하거나 청크 단위로 읽는 것이 안전합니다.
성능이슈
모든 xlsx 저장 시마다 ZIP 해제 + XML 파싱이 발생하면 성능에 영향을 줄 수 있습니다. 파일 크기 제한(예: 10MB 이하만 검사)이나 비동기 처리가 필요합니다.
결론
| 방식 | 난이도 | 정확도 | 비고 |
| 커널 ZIP 해제 | 높음 | 높음 | BSOD 위험, 구현 복잡 |
| 에이전트 레벨 검사 | 중간 | 높음 | 현실적, 딜레이 있음 |
| IRP_MJ_WRITE 평문 검사 | 할만함 | 낮음 | txt 등 평문만 가능 |
OpenGuard에서는 현재 평문 파일(txt)에 대한 패턴 검사만 구현했고, xlsx/docx에 대한 검사는 에이전트 레벨로 추후 구현 할 생각은 가지고 있습니다만 글쎄요 어떻게 될 지는.
상용 DLP 제품들이 왜 복잡하고 무거운지, 단순해 보이는 기능 하나도 실제로는 얼마나 많은 예외 처리가 필요한지 직접 경험할 수 있었던 부분이었습니다.
5. 파일 첨부 차단 (IRP_MJ_CREATE)
마지막으로, 브라우저를 통한 웹 메일 첨부 등을 막는 기능입니다. 브라우저가 파일을 첨부(읽기)하려 할 때 IRP_MJ_CREATE에서 이를 가로챕니다.
동작원리

프로세스 이름 가져오기의 함정
가장 중요한 건 "누가 파일을 열려고 하는가?" 입니다. 초기에는 EPROCESS 구조체의 ImageFileName 오프셋을 하드코딩해서 읽었는데, 윈도우 버전마다 오프셋이 달라 쓰레기 값이 읽히는 치명적인 버그가 있었습니다.
결국 공식 커널 API인 SeLocateProcessImageName을 사용하도록 로직을 수정했습니다.

PreCreateCallback 확장

이 함수를 PreCreateCallback에 연동하여, 대상 프로세스가 읽기 권한(FILE_READ_DATA)을 요구하며 차단 확장자(.txt 등)를 열려고 할 때 접근을 거부하면 파일 첨부가 원천 차단됩니다.
관리자 웹 정책 설정
DB 컬럼 추가
ALTER TABLE og_policies ADD COLUMN block_file_attach TINYINT DEFAULT 0;
ALTER TABLE og_policies ADD COLUMN attach_block_exts VARCHAR(500) DEFAULT '';
ALTER TABLE og_policies ADD COLUMN block_sensitive_data TINYINT DEFAULT 0;
ALTER TABLE og_policies ADD COLUMN block_rrn TINYINT DEFAULT 0;
ALTER TABLE og_policies ADD COLUMN block_card_num TINYINT DEFAULT 0;
ALTER TABLE og_policies ADD COLUMN block_phone TINYINT DEFAULT 0;
관리자 웹 UI

웹 메일 쓰기 첨부파일 차단 된 모습


트러블슈팅 정리 (삽질 노트)
이번 편에서도 많은 아키텍처적 한계와 오류를 경험했습니다.
| 문제 | 원인 | 해결 |
| 시스템 DB 파일 오탐 | 바이너리 데이터가 패턴과 우연히 일치 | 시스템 경로 제외 로직 추가 |
| IsAttachProcess 항상 FALSE | EPROCESS 오프셋 버전마다 다름 | SeLocateProcessImageName API 사용 |
| 구조체 크기 불일치 크래시 | 정책 구조체 수정 후 일부만 리빌드 | 전체 솔루션 리빌드 + 드라이버 교체 |
| xlsx 패턴 검사 불가 | ZIP+XML 구조라 바이너리로 저장 | 추후 에이전트 레벨 구현 생각 중 |
마무리 및 다음 편 예고
"단순히 저장할 때 내용을 읽어서 차단한다"라는 한 줄짜리 요구사항이, 커널의 제약과 Office 파일의 압축 포맷이라는 현실을 만났을 때 얼마나 아키텍처가 복잡해지는지 뼈저리게 느낄 수 있었습니다. 상용 DLP들이 왜 커널과 유저모드 에이전트를 유기적으로 통신시키며 무겁게 동작하는지 직접 경험으로 이해하게 되었습니다.
다음 글에서는 DLP의 꽃이자 최종 관문, 보안 폴더 기반 AES-256 파일 투명 암호화(Transparent Encryption) 구현에 대한 내용을 다루어 볼 예정입니다. 파일이 디스크에 쓰일 때 몰래 암호화하고, 읽을 때 싹 복호화해서 올려주는 마법 같은 필터 드라이버의 세계로 들어가 볼 생각입니다만 이 또한 구현 중 방향이 바뀔 수 있음을 알려드립니다.
긴 글 읽어주셔서 감사합니다.
이전 글 보러가기
https://imoracle.tistory.com/75
[DLP 개발기] 4. WFP DNS 차단의 한계와 유저모드 클립보드 제어
들어가며이 글은 상용 DLP(Data Loss Prevention) 프로그램의 동작 원리를 분석하고, 윈도우 커널 드라이버 기반의 보안 솔루션을 직접 구현해보는 시리즈입니다.지난 글에서는 WPF 트레이 앱 통신과 매
imoracle.tistory.com
'Project > [DLP] OpenGuard' 카테고리의 다른 글
| [DLP 개발기] 4. WFP DNS 차단의 한계와 유저모드 클립보드 제어 (0) | 2026.05.06 |
|---|---|
| [DLP 개발기] 3. WPF 트레이 앱 통신과 커널 레벨 매체/파일 제어 (USB & Minifilter) (0) | 2026.05.06 |
| [DLP 개발기] 2. 커널 드라이버와 IOCTL, 그리고 E2E 파이프라인 구축 (0) | 2026.04.29 |
| [DLP 개발기] 1. 커널 드라이버 기반 보안 시스템, OpenGuard 시작하기 (0) | 2026.04.29 |