Step 169. 암호화와 탐지 회피 개념 — 숨기는 자와 찾는 자의 군비 경쟁
Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★☆☆☆ | 예상 소요 시간 2시간
전제: Step 90(XOR 파일 암호화 도구)의 XOR 개념, Step 168의 악성코드 분류와 생애주기를 안다.
- 준비물: 파이썬 3, 리눅스 랩(WSL 또는 Kali, gcc 포함). 인터넷 접속 없이도 따라 할 수 있습니다.
- ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
- 성격 안내: 오늘은 [개념] 챕터입니다. 탐지를 빠져나가는 "기술"을 만드는 것이 아니라, 백신이 무엇을 보고 잡는가의 원리를 배우는 날입니다.
백신과 공격자는 오래된 숨바꼭질을 하고 있습니다. 백신이 "알려진 악성코드의 지문"을 저장해 두면, 공격자는 같은 코드를 다른 모양으로 포장해 지문을 바꿉니다. 백신이 다시 "행동"을 보기 시작하면, 공격자는 행동마저 정상처럼 위장하려 합니다. 오늘 우리는 이 숨바꼭질의 규칙을 배웁니다 — 난독화(obfuscation, 코드 꼬기), 패킹(packing, 실행 파일 포장), 그리고 탐지의 두 축인 시그니처 기반과 행위 기반 탐지입니다. 악성코드를 만들지 않습니다. 무해한 문자열과 직접 짠 10줄짜리 C 프로그램으로 "숨기면 무엇이 바뀌고, 무엇은 못 바꾸는가"를 실측으로 확인합니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 난독화와 암호화의 차이를 "되돌리는 열쇠" 관점에서 설명한다
- 시그니처 기반 탐지와 행위 기반 탐지의 장단을 비교한다
- base64, hex, XOR로 문자열을 숨기고 복원해 본다
strings와sha256sum으로 "겉모습이 바뀐 같은 프로그램"을 실증한다- "행위는 숨기기 어렵다"는 원리를 사례로 설명한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 (표준 라이브러리), 리눅스 쉘 + gcc |
| 오늘의 명령 | strings, sha256sum, gcc -o, 파이썬 base64 / bytes.hex() / ^ |
| 필요한 개념 | 난독화 vs 암호화, 시그니처 탐지, 행위 탐지, 패킹, EICAR 테스트 파일 |
| 오늘의 산출물 | 난독화 실험 출력 3종 + "탐지 2축 비교표"와 군비 경쟁 그림 |
2-1. 백신은 두 가지로 잡는다
백신(안티바이러스)의 탐지는 크게 두 가지입니다.
첫째, 시그니처 기반 탐지(signature-based detection). 알려진 악성코드의 지문 — 해시, 특정 바이트 패턴, 특정 문자열 — 을 데이터베이스에 저장해 두고, 파일에서 그 지문이 발견되면 잡아냅니다. 지문 데이터베이스는 하루에도 여러 번 갱신됩니다.
둘째, 행위 기반 탐지(behavior-based detection). 파일의 생김새가 아니라 실행 중의 행동을 봅니다. "문서 프로그램이 갑자기 PowerShell을 띄웠다", "실행 직후 수백 개 파일을 열어 내용을 바꿨다"처럼 정상 프로그램은 잘 하지 않는 행동이 의심 대상입니다.
2-2. 난독화 ≠ 암호화 — 열쇠의 유무
난독화(obfuscation)는 "읽기 어렵게 꼬는 것"입니다. base64로 바꾸거나, 문자열을 잘게 쪼개거나, 변수명을 의미 없는 글자로 바꾸는 식입니다. 핵심은 이것입니다 — 열쇠가 없습니다. 절차를 알면 누구나 되돌릴 수 있습니다.
반면 암호화(encryption)는 열쇠 없이는 되돌릴 수 없어야 합니다. Step 90에서 확인했듯 암호의 강함은 키 공간에서 나옵니다. 그래서 "base64로 인코딩했다"는 절대 "암호화했다"가 아닙니다 — 비밀번호를 base64로 저장하는 사고가 지금도 일어나는 이유이기도 합니다.
2-3. 패킹 — 실행 파일을 포장하기
패킹(packing)은 실행 파일을 압축·변형해 원래 바이트 패턴을 감추는 기법입니다. 대표 도구가 UPX인데, UPX 자체는 정상적인 압축 도구입니다. 문제는 포장된 파일은 시그니처가 완전히 달라진다는 것 — 그래서 악성코드 제작자도 사랑하고, 백신은 "포장돼 있다"는 사실 자체를 의심 신호로 씁니다.
2-4. 군비 경쟁의 구조 — 왜 끝나지 않는가
이 관계를 한 줄로 정리하면 이렇습니다.
백신: 지문 저장 → 공격자: 지문 바꿈(난독화·패킹) → 백신: 행위 감시
→ 공격자: 행위 위장(정상 프로그램 흉내) → 백신: 행위 조합·평판·AI ...
어느 한쪽이 이기고 끝나는 게임이 아닙니다. 그래서 방어자는 시그니처와 행위를 겹쳐서 씁니다 — 시그니처는 빠르고 정확하지만 우회가 쉽고, 행위는 우회가 어렵지만 오탐(false positive, 멀쩡한 것을 악성으로 오인)이 있습니다. 약점이 다른 두 방식을 겹치는 것이 실무의 표준입니다.
3. 따라 하기
3-1. 문자열 하나를 세 가지로 숨기기
무해한 문자열 http://files.local/readme.txt를 세 방식으로 숨겨 봅니다. 악성코드가 아니라 "겉모습 바꾸기"의 원리를 보는 실험입니다.
입력: obfuscation.py:
import base64
original = "http://files.local/readme.txt"
print("원본:", original)
b64 = base64.b64encode(original.encode()).decode()
print("base64:", b64)
print("base64 복원:", base64.b64decode(b64).decode())
hexed = original.encode().hex()
print("hex:", hexed)
print("hex 복원:", bytes.fromhex(hexed).decode())
key = 42
xored = bytes([b ^ key for b in original.encode()])
print("xor 바이트:", xored)
print("xor 복원:", bytes([b ^ key for b in xored]).decode())
출력 (2026-09-09 실측):
원본: http://files.local/readme.txt
base64: aHR0cDovL2ZpbGVzLmxvY2FsL3JlYWRtZS50eHQ=
base64 복원: http://files.local/readme.txt
hex: 687474703a2f2f66696c65732e6c6f63616c2f726561646d652e747874
hex 복원: http://files.local/readme.txt
xor 바이트: b'B^^Z\x10\x05\x05LCFOY\x04FEIKF\x05XOKNGO\x04^R^'
xor 복원: http://files.local/readme.txt
읽는 법: 세 겉모습은 전부 다릅니다. 사람 눈에는 — 그리고 문자열을 그대로 찾는 시그니처 엔진에는 — 원본이 보이지 않습니다. 그런데 복원 결과는 셋 다 정확히 원본입니다. 실행 결과가 같다는 말은, 이 프로그램의 "행위"는 하나도 안 바뀌었다는 뜻입니다. 이것이 오늘의 첫 번째 결론입니다 — 난독화는 지문을 바꾸지만 행위는 바꾸지 못합니다.
3-2. 같은 프로그램, 두 얼굴 — strings 실험
이번에는 진짜 실행 파일에서 확인합니다. 문자열을 그대로 박은 C 프로그램과, 같은 문자열을 XOR로 숨겨 실행 시점에 복원하는 프로그램을 둘 다 만듭니다. 둘의 동작은 완전히 같습니다.
입력 (리눅스 랩에서):
// plain.c — 문자열이 그대로 보이는 버전
#include <stdio.h>
int main(void){ printf("http://files.local/readme.txt\n"); return 0; }
// hidden.c — 문자열을 XOR로 숨긴 버전
#include <stdio.h>
#include <string.h>
int main(void){
unsigned char enc[] = {0x42,0x5e,0x5e,0x5a,0x10,0x05,0x05,0x4c,0x43,
0x46,0x4f,0x59,0x04,0x46,0x45,0x49,0x4b,0x46,0x05,0x58,0x4f,0x4b,
0x4e,0x47,0x4f,0x04,0x5e,0x52,0x5e};
char buf[sizeof(enc)+1]; size_t i;
for(i=0;i<sizeof(enc);i++) buf[i]=enc[i]^42;
buf[i]=0;
printf("%s\n", buf);
return 0;
}
입력 (컴파일과 실행):
gcc -o plain plain.c
gcc -o hidden hidden.c
./plain
./hidden
strings plain | grep "files.local"
strings hidden | grep "files.local" || echo "(hidden: 문자열 안 보임)"
출력 (2026-09-09 실측):
http://files.local/readme.txt
http://files.local/readme.txt
http://files.local/readme.txt
(hidden: 문자열 안 보임)
읽는 법: 실행 결과는 두 번 다 같은 한 줄입니다. 그런데 strings(실행 파일에서 읽을 수 있는 문자열을 뽑는 도구)로 보면, plain에서는 문자열이 그대로 나오고 hidden에서는 없습니다. 같은 행동을 하는 프로그램인데 겉모습만 다른 것입니다 — 시그니처 탐지가 보는 것이 바로 이 겉모습입니다.
3-3. 지문이 달라졌다 — 해시 비교
파일의 지문으로 가장 많이 쓰이는 것이 해시입니다. 두 파일의 지문을 비교해 봅시다.
입력:
sha256sum plain hidden
ls -l plain hidden
출력 (2026-09-09 실측):
72f24c30479f9a800d3071b4d8f26c2dd4c1a598a72eebc5dfc8adb4e2f6c34d plain
f0061c5b7129bdb6ed6889d4acbc736a07ec650e5cc1593704733c54720792e1 hidden
-rwxr-xr-x 1 root root 16016 ... hidden
-rwxr-xr-x 1 root root 15960 ... plain
읽는 법: 해시가 완전히 다릅니다. "이 해시의 파일은 악성"이라는 데이터베이스는 이런 변형에 무력합니다 — 해시가 바뀌면 다른 파일이니까요. 실제 악성코드가 조금씩 변형돼 퍼지는 이유가 여기 있습니다. 그래서 현대 백신은 해시 하나에만 의존하지 않고 바이트 패턴, 구조 특징, 그리고 행위를 함께 봅니다.
왜 하는가: "겉을 바꾸면 지문 탐지는 빠져나간다"를 내 손으로 만든 두 파일로 증명했습니다. 이제 반대쪽 질문이 남습니다 — 그럼 백신은 뭘 더 보는가?
3-4. EICAR — 백신이 반응하는 ‘가짜 바이러스’
백신이 시그니처로 잡는 모습을 안전하게 보는 표준 방법이 있습니다. EICAR 테스트 파일 — 유럽 컴퓨터 바이러스 연구소가 만든, 아무 기능도 없는 68글자짜리 문자열입니다. 세계의 거의 모든 백신이 이 문자열을 "테스트용 악성코드"로 잡도록 약속돼 있습니다.
이 실습은 외부 조회가 필요해 여기서는 화면 예시로 대체합니다. 백신이 켜진 컴퓨터에서 EICAR 문자열을 파일로 저장하거나 VirusTotal 같은 검사 서비스에 올리면, 대부분의 엔진이 "EICAR-Test-File"로 탐지하는 화면이 나옵니다.
화면 예시 (VirusTotal에 EICAR 파일을 올렸을 때의 모양):
엔진 A: EICAR-Test-File (not a virus)
엔진 B: Test.EICAR
엔진 C: Eicar-Test-Signature
... (수십 개 엔진 전부 탐지)
읽는 법: 이 문자열은 해를 끼치지 못하는데도 전부 잡아냅니다 — 순수하게 "지문이 DB에 있어서"입니다. 시그니처 탐지의 힘과 한계가 동시에 드러나는 장면입니다. 지문은 확실하지만, 지문이 바뀐 것(3-2의 hidden 같은)에는 닿지 않습니다.
주의: 백신이 EICAR 파일을 격리·삭제하는 것은 정상 동작입니다. 테스트가 끝나면 격리소에서 정리하면 됩니다.
3-5. 행위는 숨기기 어렵다 — 사례 읽기
행위 기반 탐지가 잡는 대표적인 조합은 "문서가 셸을 띄우는 것"입니다. 정상적인 워드 문서는 PowerShell을 실행할 이유가 없습니다. 그래서 winword.exe → powershell.exe라는 프로세스 부모-자식 관계가 나타나면, 파일이 아무리 교묘하게 난독화돼 있어도 EDR(Endpoint Detection and Response, 단말 행위 감시)이 경보를 울립니다.
화면 예시 (EDR 경보의 전형적 문구):
Suspicious process tree detected:
winword.exe (PID 5120)
└─ powershell.exe -enc aHR0cDov... (PID 6012)
Rule: Office application spawned script interpreter
읽는 법: 여기서 -enc 뒤의 base64 문자열은 난독화된 명령입니다 — 3-1에서 우리가 한 바로 그 기법입니다. 난독화는 내용을 숨겼지만, "워드가 PowerShell을 띄웠다"는 행위는 숨기지 못했습니다. 공격자의 포장 기술과 방어자의 탐지 원리가 정확히 마주 보는 지점입니다.
왜 하는가: 오늘의 두 번째 결론입니다 — 탐지 회피 기법을 "알아야" 방어 설계가 됩니다. 어느 쪽으로 가든 이 구조를 모르면 말이 안 됩니다.
4. 미션과 연습문제
미션 — "탐지 2축 비교표 + 군비 경쟁 그림" 완성
- 시그니처 기반과 행위 기반 탐지를 비교하는 표를 직접 만듭니다 — 비교 항목은 최소 4개(보는 대상, 속도, 우회 난이도, 오탐)로 합니다
- 2-4의 군비 경쟁 순환을 자기 말로 다시 그립니다 — 각 단계에 오늘 실습의 증거(3-1~3-5 중 하나)를 괄호로 답니다
- 난독화와 암호화의 차이를 "열쇠의 유무"로 두 문장 안에 설명합니다
- Step 89의 위키에
탐지원리.md로 저장합니다 — 한 줄 요약 / 비교표 / 그림 / 오늘 실측 명령 3개
연습문제
문제 1. base64로 변환한 비밀번호를 "암호화해 두었다"고 말하는 동료에게, 무엇이 틀렸는지 설명해 보세요.
문제 2. 3-2에서 hidden의 문자열은 strings에 잡히지 않았는데, 이 프로그램이 "안전한지" 판단할 수 있나요? 시그니처 관점과 행위 관점에서 각각 답해 보세요.
문제 3. 해시 기반 탐지(3-3)가 난독화에 무력한 이유를 "해시 함수의 성질"에서 설명해 보세요.
문제 4. 시그니처와 행위 탐지를 겹쳐 쓰는 이유를, 각 방식의 약점으로 설명해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
비교표의 예시입니다 — 문구는 여러분의 말로 바꾸세요.
| 항목 | 시그니처 기반 | 행위 기반 |
|---|---|---|
| 보는 대상 | 파일의 지문(해시·패턴·문자열) | 실행 중의 행동(프로세스·파일·네트워크) |
| 속도 | 빠름 (미리 켜기 가능) | 느림 (실행을 지켜봐야 함) |
| 우회 | 쉬움 — 겉만 바꾸면 됨 (3-2, 3-3 실측) | 어려움 — 행위까지 바꿔야 함 (3-5) |
| 오탐 | 적음 (지문이 정확) | 있음 — 정상 프로그램도 수상한 행동을 할 수 있음 |
군비 경쟁 그림의 예시: "지문 DB(EICAR로 확인, 3-4) → 난독화로 지문 바꿈(3-1~3-3 실측) → 행위 감시 등장 → 정상 흉내 위장(3-5의 프로세스 트리 속이기 시도) → 행위 조합 분석…". 화살표마다 오늘 본 증거가 하나씩 붙어 있으면 완성입니다.
검증하는 법: ① 표에 네 항목이 있는가. ② 그림의 각 단계에 오늘 실습 증거가 괄호로 달렸는가. ③ "난독화 ≠ 암호화" 설명에 "열쇠"가 등장하는가. 셋 다 ‘예’이면 완성입니다.
연습문제 해답
문제 1 해답. base64는 열쇠가 없는 변환 — 즉 난독화이지 암호화가 아닙니다. 변환 절차는 전 세계가 알고 있으므로 누구나 즉시 복원할 수 있습니다 (3-1 실측에서 복원 한 줄로 돌아옴). 암호화라고 부르려면 키 없이 되돌릴 수 없어야 하고, 강함은 키 공간에서 나와야 합니다.
문제 2 해답. 시그니처 관점에서는 문자열이 안 보이므로 "이 파일"만으로는 알려진 지문과 대조할 수 없습니다 — 겉으로는 깨끗해 보입니다. 하지만 행위 관점에서는 실행했을 때 무엇을 하는지가 전부 드러납니다. 오늘의 예는 화면에 한 줄 찍는 것이 전부라 무해하지만, 파일을 지우거나 네트워크로 나가는 행동이었다면 행위 탐지가 잡습니다. "지문에 안 걸린다"와 "안전하다"는 다른 문장입니다.
문제 3 해답. 해시 함수는 입력이 한 바이트만 바뀌어도 출력이 완전히 달라집니다 (3-3 실측: 두 해시가 전혀 다름). 난독화는 바이트를 바꾸는 작업이므로 해시는 필연적으로 바뀌고, DB에 저장된 지문과 더는 일치하지 않습니다. 해시는 "정확히 같은 파일"만 찾는 도구입니다.
문제 4 해답. 시그니처는 빠르고 오탐이 적지만 겉모습 변형에 무력하고(3-2), 행위는 변형을 뚫고 보지만 오탐이 있고 실행을 기다려야 합니다. 한쪽만 쓰면 그 약점이 그대로 구멍이 됩니다. 약점이 서로 다른 방식을 겹치면 공격자는 두 장벽을 동시에 넘어야 하므로 비용이 크게 올라갑니다 — 그래서 실무는 항상 다층입니다.
완료 기준 체크리스트
- [ ] 난독화와 암호화의 차이를 "열쇠 유무"로 설명할 수 있다
- [ ] base64/hex/XOR로 문자열을 숨기고 복원했다
- [ ]
strings로 평문 버전과 숨김 버전의 차이를 실측했다 - [ ]
sha256sum으로 지문이 달라짐을 확인했다 - [ ] 시그니처/행위 탐지의 장단을 비교표로 만들었다
- [ ] "행위는 숨기기 어렵다"를 사례로 설명할 수 있다
- [ ] EICAR가 무엇인지, 왜 안전한 테스트인지 말할 수 있다
6. 흔한 실수와 해결
벽 1. binascii.Error: Incorrect padding이 난다
증상 (2026-09-09 실측, base64 문자열의 =을 떼고 복원한 경우):
binascii.Error: Incorrect padding
원인: base64는 길이를 4의 배수로 맞추기 위해 끝에 =을 붙입니다. 이걸 떼거나 중간이 잘리면 복원이 거부됩니다.
해결: 문자열을 복사할 때 끝의 =까지 통째로 가져오세요. "눈에 안 보이는 한 글자도 데이터"라는 것이 인코딩 다루기의 첫 규칙입니다.
벽 2. ValueError: non-hexadecimal number found in fromhex()가 난다
증상 (2026-09-09 실측, hex 문자열을 한 글자 잘라먹은 경우):
ValueError: non-hexadecimal number found in fromhex() arg at position 7
원인: hex는 두 글자가 한 바이트입니다. 홀수 길이가 되면 짝이 안 맞아 실패합니다.
해결: 복사 누락이 없는지 확인하고, 직접 만들 때는 문자열.encode().hex()로 만든 출력을 그대로 쓰세요.
벽 3. TypeError: unsupported operand type(s) for ^: 'str' and 'int'가 난다
증상 (2026-09-09 실측, 문자열에 바로 XOR한 경우):
TypeError: unsupported operand type(s) for ^: 'str' and 'int'
원인: XOR은 숫자끼리 하는 연산입니다. 문자열(str)의 글자는 그대로는 숫자가 아닙니다.
해결: 3-1처럼 .encode()로 바이트로 바꾼 뒤 각 바이트에 ^ key를 하세요. Step 90의 "파일은 숫자 나열"이 여기서도 같은 규칙입니다.
벽 4. strings에서 숨긴 문자열이 보인다
증상: hidden을 만들었는데도 strings에 원문이 나옵니다.
원인: 소스 코드(c 파일)를 스캔했거나, 컴파일 전에 문자열이 그대로 박힌 plain을 스캔한 경우가 대부분입니다. 아니면 복원 코드 없이 문자열을 주석에 남겨 두었거나.
해결: 스캔 대상이 컴파일된 바이너리인지 확인하세요. 그리고 XOR로 숨긴 버전에는 원문 문자열이 소스의 어디에도 없어야 합니다 — 바이트 배열(0x42,0x5e,...)만 있어야 합니다.
벽 5. "난독화했으니 안전/뚫렸다"로 성급히 결론 내린다
증상: 문자열이 안 보이는 것을 보고 "탐지를 완전히 우회했다"고 씁니다.
원인: 3-5를 건너뛴 것입니다 — 지문은 숨었지만 행위는 그대로입니다.
해결: 오늘의 결론 문장을 그대로 쓰세요 — "난독화는 지문을 바꾼다. 행위를 바꾸지는 못한다." 방어자는 두 개를 다 봅니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 시그니처 탐지 | 알려진 지문(해시·패턴·문자열)과 대조 — 빠르지만 변형에 약함 |
| 행위 탐지 | 실행 중의 행동 감시 — 우회가 어렵지만 오탐 존재 |
| 난독화(obfuscation) | 열쇠 없이 꼬기 — 절차를 알면 누구나 복원 가능 |
| 암호화(encryption) | 열쇠 없이는 못 여는 변환 — 강함은 키 공간에서 |
| 패킹(packing) | 실행 파일을 포장해 지문을 바꾸는 기법 (예: UPX) |
| EICAR | 기능 없는 68글자 테스트 문자열 — 모든 백신이 잡는 안전한 시험대 |
| EDR | 단말의 행위를 감시하는 체제 — 프로세스 트리가 주요 단서 |
오늘의 명령어·코드
| 도구 | 하는 일 |
|---|---|
base64.b64encode() / b64decode() |
대표적 난독화 변환과 복원 |
문자열.encode().hex() / bytes.fromhex() |
16진 변환과 복원 |
b ^ key |
XOR로 바이트 뒤집기 (지문 바꾸기의 최소 단위) |
strings 바이너리 |
실행 파일에서 보이는 문자열 뽑기 — 시그니처의 눈 |
sha256sum 파일 |
파일의 해시 지문 — 바이트가 바뀌면 전부 바뀜 |
gcc -o 출력 소스.c |
실험용 바이너리 만들기 |
명령어보다 중요한 감각
오늘 여러분은 같은 행동을 하는 두 개의 파일을 만들고, 겉모습만 바꿨을 때 무엇이 무너지는지(해시, 문자열 지문)와 무엇이 무너지지 않는지(행위)를 손으로 확인했습니다. 이 한 쌍의 실험이 공격-방어 군비 경쟁의 축소판입니다. 회피 기법의 이름은 앞으로도 계속 바뀌겠지만, "지문을 바꾸는 자와 행위를 보는 자"라는 구도는 바뀌지 않습니다. 기법을 외우지 말고 구도를 기억하세요 — 구도를 아는 사람은 새 기법을 봐도 그 자리가 어디인지 압니다.
전부 체크되면 Step 169 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.