1-1. AES란 무엇인가
AES(Advanced Encryption Standard)는 미국 NIST가 2001년 표준으로 지정한 대칭키 블록 암호다.
- 대칭키: 암호화할 때 쓴 키 = 복호화할 때 쓰는 키 (같은 키)
- 블록 암호: 데이터를 고정된 크기(16바이트 = 128bit)의 블록 단위로 나눠서 처리
- 키 길이: 128bit, 192bit, 256bit 세 가지 → 숫자가 클수록 강력
쉬운 비유:
AES = 자물쇠
Key = 열쇠 (AES-128은 16바이트짜리 열쇠, AES-256은 32바이트짜리)
평문(Plaintext) = 잠그기 전 내용
암호문(Ciphertext) = 자물쇠로 잠근 결과
복호화(Decrypt) = 열쇠로 다시 여는 것
1-2. 블록 암호의 동작 모드 (Mode of Operation)
AES는 16바이트씩 나눠서 처리하는데, 각 블록을 어떻게 연결할지에 따라 여러 모드가 있다.
모의해킹에서 가장 많이 만나는 두 가지:
ECB 모드 (Electronic Codebook)
평문 블록1 → [AES 암호화] → 암호문 블록1
평문 블록2 → [AES 암호화] → 암호문 블록2
평문 블록3 → [AES 암호화] → 암호문 블록3
- 각 블록을 독립적으로 암호화
- 같은 평문 블록 = 항상 같은 암호문 블록 → 패턴이 그대로 드러남
- IV 불필요 (연결이 없으므로)
- 대칭키 암호화에서 ECB 모드 사용은 보안상 취약하므로 비권장
CBC 모드 (Cipher Block Chaining)
[암호화 방향]
평문 블록1 ─→ XOR ─→ [AES 암호화] ─→ 암호문 블록1
↑ │
IV │ (이전 암호문을 다음 XOR에 전달)
↓
평문 블록2 ─→ XOR ─→ [AES 암호화] ─→ 암호문 블록2
↑ │
암호문 블록1 │
↓
평문 블록3 ─→ XOR ─→ [AES 암호화] ─→ 암호문 블록3
↑
암호문 블록2
- 이전 암호문 블록을 다음 블록 암호화에 XOR로 섞어서 연결
- 같은 평문이라도 다른 암호문이 나옴 (이전 블록의 영향을 받으므로)
- 첫 번째 블록은 XOR할 "이전 블록"이 없으므로 IV(초기화 벡터)가 필요
- 현재 가장 널리 쓰이는 모드
1-3. IV(Initialization Vector)란 무엇인가
IV = 초기화 벡터. CBC 모드에서 첫 번째 블록을 암호화할 때 XOR에 사용하는 16바이트 값이다.
왜 필요한가?
IV 없이 CBC를 시작하면, 같은 키로 같은 평문을 암호화했을 때
항상 같은 암호문이 나와버린다 → 패턴 노출
IV를 매번 랜덤으로 생성하면?
같은 키 + 같은 평문이라도 → IV가 다르면 → 암호문이 달라짐 ✅
쉬운 비유:
IV = 같은 자물쇠(Key)를 쓰더라도,
문마다 고유한 "시리얼 번호"를 추가해서
찍어낸 열쇠가 다른 자물쇠에는 안 맞게 만드는 장치
핵심 특성:
- AES에서 IV는 항상 16바이트(128bit) 고정
- 비밀로 유지할 필요 없음 (암호문과 함께 전송해도 됨)
- 하지만 매번 랜덤하게 생성해야 안전
- 고정 IV 사용은 취약점 (같은 키 + 고정 IV = 패턴 노출)
1-4. PKCS7 패딩이란
AES는 정확히 16바이트 배수의 데이터만 처리할 수 있다.
평문이 딱 떨어지지 않으면 패딩(padding)으로 채워야 한다.
PKCS7 패딩 규칙:
부족한 바이트 수 N을 값 N으로 채운다 (N은 1~16 사이)
예시: 평문 = "Hello" (5바이트)
→ 16바이트 블록을 채우려면 11바이트 부족 → N = 11 = 0x0B
→ 0x0B를 11개 추가
48 65 6C 6C 6F 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B
H e l l o [───────── 패딩 0x0B × 11개 ─────]
특수 케이스: 평문이 정확히 16바이트 배수인 경우
→ 빈 블록이 없어도 패딩 블록을 반드시 하나 추가해야 함
→ 0x10(=16)을 16개로 구성된 패딩 블록 추가
→ 이유: 복호화 시 "마지막 16바이트가 모두 0x10이면 패딩 블록"으로 구분하기 위함
복호화 후 패딩 검증:
① 마지막 바이트 값 = N 확인
② 마지막 N바이트가 모두 동일하게 값 N인지 확인
③ ①②가 모두 참 → 마지막 N바이트 제거 → 원본 평문 복원 ✅
④ 조건 불만족 → InvalidPaddingException 발생 → 복호화 실패 ❌
1-5. XOR 연산이란 (CBC 이해의 핵심)
XOR(Exclusive OR)은 비트 연산이다.
CBC 모드의 핵심 메커니즘이므로 이해해두면 분석에 큰 도움이 된다.
XOR 규칙:
0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0 ← 같으면 0, 다르면 1
중요한 특성:
A XOR B = C 이면
C XOR B = A (역연산 가능!)
C XOR A = B
이것이 CBC에서 왜 중요한가?
평문 = AES_복호화(암호문) XOR IV
↓
만약 IV를 틀리게 쓰면 → AES_복호화(암호문)은 맞지만 → XOR이 달라져 → 평문이 깨짐
IV를 바꿔도 → AES_복호화(암호문) 값 자체는 변하지 않음
1-6. CryptoJS란 무엇인가
CryptoJS는 자바스크립트에서 암호화 기능을 제공하는 오픈소스 라이브러리다.
(저장소: https://github.com/brix/crypto-js)
<!-- CDN으로 불러오는 가장 흔한 패턴 (버전은 사이트마다 다를 수 있음) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
주요 특징:
- AES, DES, SHA, HMAC, PBKDF2 등 다양한 알고리즘 지원
- 모든 바이트 데이터를
WordArray객체로 표현 - 웹 어플리케이션에서 클라이언트 사이드 암호화에 널리 사용
- v3.x → v4.x가 시장에 혼재 (v4.0.0에서 Math.random() → Web Crypto API 난수 생성으로 변경, v4.2.0에서 PBKDF2 기본 파라미터 강화)
현재 버전 확인 (콘솔):
CryptoJS.version
// "4.2.0", "4.1.1", "3.1.9" 등 버전별로 다름
// 현재 최신 공식 릴리스: 4.2.0 (npm/github 기준, 공식 유지보수 중단 상태)
2. 모의해킹 관점에서의 클라이언트 암호화
핵심 원칙
클라이언트 사이드 암호화는 구조적으로 키를 완전히 숨길 수 없다.
브라우저에서 복호화가 일어나려면 키가 반드시 클라이언트에 존재해야 한다.
이것은 구현 실수가 아니라 클라이언트 암호화의 근본적 한계이며,
모의해킹에서 반드시 검토해야 하는 공격 표면이다.
쉬운 비유:
"잠겨있는 금고를 열어야 하는데,
열쇠를 금고 앞에 붙여놓고 '잠겨 있으니 안전하다'고 하는 것"
클라이언트 암호화가 방어할 수 있는 것 vs 없는 것
| 구분 | 내용 | 비고 |
|---|---|---|
| ✅ 방어 가능 | 네트워크 패킷 스니핑 | HTTPS와 병행 시 |
| ✅ 방어 가능 | 서버 로그에서의 평문 노출 | 서버가 암호문만 저장할 때 |
| ✅ 방어 가능 | 서버 DB 탈취 시 데이터 보호 | 서버가 복호화 안 할 때 |
| ❌ 방어 불가 | JS 소스코드 정적 분석 | 키가 소스에 반드시 존재 |
| ❌ 방어 불가 | 브레이크포인트 런타임 분석 | 실행 중 메모리에서 키 추출 가능 |
| ❌ 방어 불가 | 함수 후킹 | 실행 시점 파라미터 캡처 가능 |
공격 흐름 전체 구조
[1단계] 정보수집 (Reconnaissance)
├─ CryptoJS 로드 여부 확인
├─ JS 파일 목록 파악 (Network 탭)
└─ 난독화 여부 확인
[2단계] 정적 분석 (Static Analysis)
├─ JS 소스에서 key / iv 변수 검색
├─ 암호화 함수 호출 패턴 분석
└─ 키 생성 방식 파악 (하드코딩 / 파생 / 동적)
[3단계] 동적 분석 (Dynamic Analysis)
├─ 브레이크포인트로 런타임 키 추출
├─ 함수 후킹으로 모든 파라미터 자동 캡처
└─ Network 탭 / Burp Suite로 암호문 수집
[4단계] 복호화 (Decryption)
├─ Key + IV + 암호문으로 복호화
├─ 브라우저 콘솔 / CyberChef / Python 활용
└─ 결과 검증 (UTF-8 / Hex / Base64)
[5단계] 보고서 작성 (Reporting)
├─ 재현 절차 문서화
└─ 개선 방안 제시
3. 취약점 분류
클라이언트 사이드 암호화에서 발견되는 주요 취약점 유형이다.
보고서 작성 시 참조 기준: OWASP WSTG v4.2 WSTG-CRYP-04 (Testing for Weak Encryption)
| 취약점 유형 | 위험도 | 설명 |
|---|---|---|
| JS 소스에 키 하드코딩 | 🔴 High ~ Critical | 소스 분석만으로 키 추출 가능 |
| 고정 IV 사용 (CBC) | 🟡 Medium | 동일 평문 → 동일 암호문, 패턴 노출 |
| ECB 모드 사용 | 🟡 Medium ~ High | 블록 단위 패턴이 암호문에 그대로 드러남 |
| MD5 기반 키 파생 | 🟡 Medium ~ High | MD5는 깨진 알고리즘, 키 강도 저하 |
| 약한 키 길이 (64bit 이하) | 🔴 High | 브루트포스 현실적으로 가능 |
| 암호화 토큰 구조 노출 | 🟡 Medium | 토큰 위변조로 인증 우회 가능성 |
4. 정보 수집 단계
4-1. 콘솔에서 CryptoJS 로드 여부 확인
전제: 반드시 분석 대상 페이지의 콘솔에서 실행해야 한다.
다른 페이지 콘솔에서는 CryptoJS 객체가 없어 오류 발생.
F12 → Console 탭 클릭
// ① CryptoJS 존재 여부
typeof CryptoJS
// 결과: "object" → 로드됨 / "undefined" → 로드 안 됨
// ② AES 모듈 존재 여부
typeof CryptoJS.AES
// 결과: "object" → AES 모듈 있음
// ③ WordArray 사용 가능 여부
typeof CryptoJS.lib.WordArray
// 결과: "function" → 사용 가능
// ④ 버전 확인
CryptoJS.version
// 결과: "3.1.9" 또는 "4.1.1" 또는 "4.2.0" 등
// v4.2.0 미만이면 PBKDF2 취약 기본값(CVE-2023-46233) 확인 필요
v3 vs v4 차이점:
- v4.0.0에서 Math.random() 대신 Web Crypto API로 난수 생성으로 변경
- v4.2.0에서 PBKDF2 기본 알고리즘 SHA1+iterations=1 → SHA256+iterations=250,000으로 변경
- 암호화/복호화 API는 버전 무관하게 동일하므로 아래 내용 모두 적용 가능
4-2. Network 탭에서 JS 파일 목록 확인
개발자도구 → Network 탭
→ F5로 페이지 새로고침
→ 필터: JS 클릭
→ 파일명에서 키워드 확인:
crypto-js, crypto.min.js, aes.js, cryptojs
→ 파일 클릭 → Response 탭에서 소스 확인
4-3. 소스 전체 검색으로 암호화 위치 파악
Sources 탭 → Ctrl + Shift + F (전체 파일 검색)
| 검색 키워드 | 찾는 것 |
|---|---|
CryptoJS.AES |
AES 암호화 사용 위치 |
.encrypt( |
암호화 함수 호출 위치 |
.decrypt( |
복호화 함수 호출 위치 |
WordArray |
키/IV 생성 위치 |
enc.Hex |
Hex 형식 키/IV 파싱 위치 |
enc.Utf8 |
UTF8 형식 키/IV 파싱 위치 |
{ iv |
IV 옵션 전달 위치 |
4-4. 난독화 여부 확인 및 해제
난독화 코드 식별 패턴:
// 패턴 1: 변수명 난독화 (_0x 접두사)
var _0x1a2b = ['CryptoJS', 'AES', 'encrypt'];
// 패턴 2: eval + packed 압축
eval(function(p,a,c,k,e,d){ ... }('...', 62, 62, '...'.split('|')))
// 패턴 3: 한 줄 압축 (minified)
function a(b,c){var d=CryptoJS.AES.encrypt(b,c);return d.toString()}
해제 방법:
| 방법 | 도구 | 용도 |
|---|---|---|
| 브라우저 내장 | Sources 탭 하단 { } 버튼 |
일반 minified JS |
| 온라인 | https://beautifier.io/ | 일반 minified JS |
| 온라인 | https://de4js.kshift.me/ | 고급 난독화 |
| 온라인 | https://obf-io.deobfuscate.io/ | _0x 패턴 난독화 |
| 로컬 | npx js-beautify -f obfuscated.js -o readable.js |
Node.js |
5. JS 소스 분석 및 키 추출
5-1. 키 유형 분류
유형 A: 하드코딩 평문 키 (가장 흔함)
// Hex 형식
var key = CryptoJS.enc.Hex.parse('0123456789abcdef0123456789abcdef');
// UTF8 문자열 형식
var key = CryptoJS.enc.Utf8.parse('MySecretKey12345');
// 전역 상수로 선언
const SECRET_KEY = 'hardcodedKeyHere';
추출 방법: 소스 검색으로 직접 값 확인 가능
유형 B: 서버에서 수신하는 동적 키
fetch('/api/session').then(r => r.json()).then(data => {
var key = CryptoJS.enc.Hex.parse(data.encKey);
encryptData(payload, key);
});
// 또는 전역 변수에 저장
window.__ENC_KEY = serverResponse.key;
추출 방법: Network 탭에서 API 응답 확인, 또는 콘솔에서 전역변수 직접 출력
유형 C: 파생 키 (PBKDF2 / MD5 기반)
// PBKDF2로 키 파생 (v4.2.0 이상에서는 기본값이 SHA256+250,000 iterations로 강화됨)
var key = CryptoJS.PBKDF2('userPassword', salt, {
keySize: 4, // 4 words = 16bytes = AES-128
iterations: 250000 // v4.2.0 이상의 기본값
// v4.1.x 이하에서는 iterations 기본값이 1 (취약! CVE-2023-46233)
});
// MD5 해시로 키 생성 (보안 취약 — MD5는 이미 깨진 알고리즘)
var key = CryptoJS.MD5('someStaticPassword');
// MD5 결과 = 16바이트 → AES-128 키로 사용
추출 방법: 브레이크포인트에서 파생 후 key 변수 값 직접 추출
유형 D: CryptoJS 자체 패스워드 파생 (OpenSSL EVP_BytesToKey KDF)
// key 자리에 문자열을 직접 넣으면 CryptoJS 내부에서 MD5 기반 KDF 자동 실행
CryptoJS.AES.encrypt('plaintext', 'myPassword');
특징: 암호화 결과를 Base64 디코딩하면 다음 구조의 헤더가 붙는다 (OpenSSL EVP_BytesToKey 호환 포맷):
[ "Salted__" (8바이트 ASCII) ][ salt (8바이트 랜덤) ][ 실제 암호문 ] ← 총 16바이트 헤더 →생성 키 사이즈: 패스프레이즈를 넣으면 AES-256 (256bit 키) 이 생성된다. (출처: CryptoJS 공식 문서)
EVP_BytesToKey가 MD5를 반복해 key 32바이트 + IV 16바이트 = 총 48바이트를 파생한다.내부적으로 MD5 해시 기반 EvpKDF를 사용해 key와 IV를 함께 파생.
복호화 시: 동일한 패스워드 문자열을 key 자리에 그대로 넣으면 CryptoJS가 salt를 자동으로 읽어 복호화 가능.
단, Python/OpenSSL에서 복호화하려면 암호문에서 salt(9~16번째 바이트)를 추출해 별도로 EVP_BytesToKey 과정을 수행해야 한다.
5-2. 브레이크포인트 설정 및 런타임 키 추출
Step 1: 브레이크포인트 설정 방법 선택
| 방법 | 설명 | 언제 사용 |
|---|---|---|
| 라인 브레이크포인트 | Sources 탭 → 라인 번호 클릭 → 파란 점 생성 | 암호화 코드 위치를 아는 경우 |
| 조건부 브레이크포인트 | 라인 번호 우클릭 → "Add conditional breakpoint" | 특정 조건일 때만 멈추고 싶을 때 |
| XHR/Fetch 브레이크포인트 | Sources 우측 → XHR/fetch Breakpoints → + → URL 패턴 입력 | API 요청 시점에서 멈추고 싶을 때 |
| 이벤트 리스너 | Sources 우측 → Event Listener Breakpoints → Mouse → click | 버튼 클릭 시점에서 멈추고 싶을 때 |
Step 2: 정지 후 콘솔에서 키 추출
브레이크포인트에서 실행이 정지되면 Console 탭 또는 ESC 키로 하단 콘솔 열기:
// ① 현재 스코프 확인 (우측 Scope 패널 또는 직접 콘솔 입력)
typeof key
// "object" → WordArray 가능성 높음
// ② WordArray 여부 확인
key instanceof CryptoJS.lib.WordArray
// true → WordArray 확정
// ③ Hex로 키 추출 ★★★ 핵심
key.toString()
// 또는 동일한 결과:
CryptoJS.enc.Hex.stringify(key)
// 출력 예: "0123456789abcdef0123456789abcdef"
// → 이 32자(16바이트) 값이 AES-128 키
// ④ 키 길이로 AES 종류 확인
key.sigBytes
// 16 → AES-128 / 24 → AES-192 / 32 → AES-256
// ⑤ 추가 형식 확인
CryptoJS.enc.Base64.stringify(key) // Base64 형식으로 출력
// ※ Utf8.stringify는 key가 유효한 UTF-8 바이트 시퀀스일 때만 성공
// 임의 바이트이면 에러 발생 → 사용 시 주의
try {
console.log('Utf8:', CryptoJS.enc.Utf8.stringify(key));
} catch(e) {
console.log('Utf8 변환 불가 (임의 바이트 키)');
}
// ⑥ words 배열 확인 (디버깅용)
key.words // [정수, 정수, ...] 형태 (자세한 내용은 7섹션 참조)
key.sigBytes // 유효 바이트 수
Step 3: 계속 실행
F8 → 실행 계속 (Resume)
F10 → 다음 줄로 이동 (Step Over — 함수 내부로 안 들어감)
F11 → 함수 내부로 진입 (Step Into)
Shift+F11 → 현재 함수에서 나가기 (Step Out)
6. IV 존재 여부 검증 완전 절차
6-1. 정적 분석: 소스에서 IV 패턴 검색
Ctrl + Shift + F로 전체 검색:
{ iv: / ,iv: / iv = / iv=CryptoJS / initialization
발견 가능한 소스 패턴:
// 패턴 1: 고정 Hex IV (취약 — 항상 같은 IV)
var iv = CryptoJS.enc.Hex.parse('00000000000000000000000000000000');
// 패턴 2: 고정 문자열 IV (취약 — 반드시 16바이트여야 함)
var iv = CryptoJS.enc.Utf8.parse('1234567890123456');
// 패턴 3: 키와 동일한 IV (매우 취약)
var iv = key;
// 패턴 4: 키 앞 16바이트를 IV로 사용 (취약)
var iv = CryptoJS.lib.WordArray.create(key.words.slice(0, 4));
// key.words.slice(0, 4) → 앞 4개 word = 16바이트
// 패턴 5: 서버에서 동적으로 받는 IV (그나마 나음)
var iv = CryptoJS.enc.Hex.parse(serverResponse.iv);
// 패턴 6: IV가 암호문 앞에 붙어서 전송됨 (IV prepended)
// → 소스에 iv 변수 따로 없음 → 6-5 섹션 참조
6-2. 동적 분석: 콘솔에서 IV 추출
// ① options 객체 전체 확인 (JSON.stringify는 WordArray 직렬화 불가 → console.dir 사용)
console.dir(options)
// 출력: { iv: WordArray, mode: CBC, padding: Pkcs7 }
// ② iv 변수 확인
typeof iv
iv instanceof CryptoJS.lib.WordArray // true
// ③ IV Hex 추출
iv.toString()
// 출력: "0123456789abcdef0123456789abcdef" (32자 = 16바이트)
// ④ IV 크기 확인 (CBC에서 반드시 16바이트)
iv.sigBytes // 16 → 정상
// ⑤ iv가 일반 JS 배열인 경우
CryptoJS.lib.WordArray.create(iv).toString()
// 배열을 WordArray로 감싼 뒤 hex 추출
6-3. ECB 모드 여부 확인
// 소스에서 ECB 명시 패턴 찾기
{ mode: CryptoJS.mode.ECB }
// ECB는 IV가 없어도 됨 → 아래처럼 복호화 시도
CryptoJS.AES.decrypt('암호문_Base64', key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8)
6-4. IV 없이 복호화 시도로 모드 판별
키를 확보한 후 여러 설정을 순서대로 시도:
const key = CryptoJS.enc.Hex.parse('추출한_키_hex');
const ct = '캡처한_Base64_암호문';
// ① ECB 모드 (IV 없음)
let r1 = CryptoJS.AES.decrypt(ct, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
console.log('[ECB]', r1.toString(CryptoJS.enc.Utf8));
// ② CBC + Zero IV
let r2 = CryptoJS.AES.decrypt(ct, key, {
iv: CryptoJS.enc.Hex.parse('00000000000000000000000000000000'),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
console.log('[CBC+ZeroIV]', r2.toString(CryptoJS.enc.Utf8));
// ③ CBC + Key 앞 16바이트를 IV로
let r3 = CryptoJS.AES.decrypt(ct, key, {
iv: CryptoJS.lib.WordArray.create(key.words.slice(0, 4)),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
console.log('[CBC+KeyIV]', r3.toString(CryptoJS.enc.Utf8));
결과 해석 표:
| 결과 | 원인 | 조치 |
|---|---|---|
| ✅ 정상 UTF-8 텍스트 (전체) | 해당 설정이 정답 | 완료 |
| ✅ 앞 16바이트만 깨진 문자열 | IV가 틀림, 키는 맞음 | 6-4-1 참조 |
⚠️ 빈 문자열 "" 반환 (throw 없음) |
키/IV 오류 — 가장 흔한 실패 패턴 | NoPadding raw hex로 키 오류 여부 판단 |
| ❌ Error: Malformed UTF-8 | 패딩 오류 → 키 또는 IV 불일치 | NoPadding으로 raw hex 확인 |
| ❌ NoPadding hex 전체 랜덤 | 키 자체가 틀림 | 5단계부터 재확인 |
6-4-1. ★ IV 오류 시 앞 블록만 깨지는 원리와 복구
모의해킹에서 매우 자주 만나는 상황이다. 원리를 이해하면 진단이 빨라진다.
왜 앞 16바이트만 깨지는가? — CBC 복호화 수식
CBC 복호화 수식:
평문 블록 1 = AES_복호화(암호문 블록 1) XOR IV
평문 블록 2 = AES_복호화(암호문 블록 2) XOR 암호문 블록 1
평문 블록 3 = AES_복호화(암호문 블록 3) XOR 암호문 블록 2
...
평문 블록 N = AES_복호화(암호문 블록 N) XOR 암호문 블록 (N-1)
핵심: IV는 오직 첫 번째 블록의 XOR 연산에만 사용된다.
2번째 블록부터는 "앞 블록의 암호문"을 XOR에 쓰므로 IV와 완전히 무관하다.
CBC 복호화 흐름 시각화
암호문: [C1] [C2] [C3] [C4]
│ │ │ │
AES↓ AES↓ AES↓ AES↓
[D1] [D2] [D3] [D4] ← AES_복호화 결과
│ │ │ │
XOR: IV [C1] [C2] [C3] ← XOR에 사용되는 값
│ │ │ │
평문: [P1] [P2] [P3] [P4]
↑ IV가 틀리면 P1(앞 16바이트)만 깨짐
P2부터는 C1~C3을 XOR에 쓰므로 IV 영향 전혀 없음
진단 요약표
| 상황 | 결과 |
|---|---|
| IV + 키 모두 정확 | 전체 평문 정상 |
| IV만 틀림 (키는 정확) | 앞 16바이트만 깨짐, 나머지 정상 |
| 키 오류 | 전체 블록 모두 깨짐 |
| ECB인데 CBC로 시도 | 패딩 오류 또는 전체 깨짐 |
실전 진단 코드
// ZeroIV로 먼저 시도 → 증상 확인
// ※ CryptoJS toString(enc.Utf8) 오류 동작:
// - 대부분(~97%): 빈 문자열("") 반환 — 오류 없이 조용히 실패
// - 일부(~2.5%): Error("Malformed UTF-8 data") throw
// → 빈 문자열 체크 + try-catch 모두 필수
let plain;
try {
const r = CryptoJS.AES.decrypt(ct, key, {
iv: CryptoJS.enc.Hex.parse('00000000000000000000000000000000'),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
plain = r.toString(CryptoJS.enc.Utf8);
if (!plain) {
// 빈 문자열 → 키/IV 오류일 가능성 높음 (throw 없이 조용히 실패)
console.warn('[빈 문자열] 키/IV 오류 가능성 높음 → NoPadding으로 raw hex 확인');
const r2 = CryptoJS.AES.decrypt(ct, key, {
iv: CryptoJS.enc.Hex.parse('00000000000000000000000000000000'),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.NoPadding
});
console.log('[NoPadding raw hex]', r2.toString());
} else {
console.log('[결과]', plain);
}
} catch(e) {
// Error: Malformed UTF-8 data → NoPadding으로 hex 확인
const r2 = CryptoJS.AES.decrypt(ct, key, {
iv: CryptoJS.enc.Hex.parse('00000000000000000000000000000000'),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.NoPadding
});
console.log('[Malformed UTF-8] raw hex:', r2.toString());
}
/*
결과 패턴 해석:
정상 문자열 출력
├── 전체 정상 → 성공 ✅
└── 앞 16자만 깨짐 → IV가 틀림, 키는 맞음 (전략 1~4로 IV 복구)
빈 문자열("") 반환 (대부분의 오류 케이스)
└── NoPadding raw hex 확인
├── 의미있는 값 보임 → 키/모드는 맞음, IV 불일치
└── 전체 랜덤 hex → 키 자체가 틀림 ❌
catch(e) 진입 (약 2.5%)
└── 동일하게 NoPadding raw hex 확인
*/
IV 복구 전략
앞 16바이트만 깨진 상황이 확인되면, 순서대로 시도한다:
전략 1: 소스 재추적 / 후킹 코드
// 후킹 코드 실행 후 (8-2 섹션 참조) 페이지 정상 사용
// → 콘솔에 IV hex 자동 출력
전략 2: 서버 응답에서 IV 찾기
Network 탭 → XHR/Fetch 요청 응답 확인
{"data": "Base64암호문", "iv": "hex_or_base64_IV"}
전략 3: Prepended IV 시도 (암호문 앞 16바이트 = IV)
// 암호문 앞 16바이트를 IV로 추출
const raw = CryptoJS.enc.Base64.parse('전체_Base64_암호문');
const extractedIV = CryptoJS.lib.WordArray.create(
raw.words.slice(0, 4), 16 // 앞 4 words = 16바이트
);
const realCT = CryptoJS.lib.WordArray.create(
raw.words.slice(4), raw.sigBytes - 16
);
console.log('추출 IV:', extractedIV.toString());
const result = CryptoJS.AES.decrypt(
{ ciphertext: realCT },
CryptoJS.enc.Hex.parse('추출한_키_hex'),
{ iv: extractedIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
);
console.log('복호화:', result.toString(CryptoJS.enc.Utf8));
전략 4: IV 역산 (첫 블록 평문을 이미 알 때)
원리: P1 = AES_복호화(C1) XOR IV
→ IV = AES_복호화(C1) XOR P1
즉, 첫 블록의 원래 평문을 알면 IV를 역산할 수 있다.
(예: 로그인 요청의 첫 16바이트가 고정된 헤더인 경우)
빠른 진단 플로우
복호화 시도 (try-catch + 빈 문자열 체크)
│
├── catch(e): Malformed UTF-8 (~2.5%)
│ └─ NoPadding으로 raw hex 확인
│ ├─ hex에 의미있는 값 보임 → 키/모드 맞음, IV 불일치 → 전략 1~4
│ └─ 전체 랜덤 hex → 키 오류 ❌ 5단계부터 재확인
│
├── 빈 문자열("") 반환 (~97%, 가장 흔한 실패 패턴)
│ └─ NoPadding으로 raw hex 확인 (위와 동일 절차)
│
└── 정상 문자열 출력
├─ 전체 정상 → 성공 ✅
└─ 앞 16바이트만 깨짐 → IV 오류 (키는 맞음) → 전략 1~4로 IV 복구
6-5. Prepended IV 탐지 및 추출
암호문 앞 16바이트가 IV인 패턴 (보안상 올바른 구현 중 하나):
// ─── 전체 암호문 구조: [IV 16바이트] + [실제 암호문] ───
const rawData = CryptoJS.enc.Base64.parse('전체_암호문_Base64');
// 앞 16바이트(4 words) 분리 → IV
const iv = CryptoJS.lib.WordArray.create(
rawData.words.slice(0, 4),
16 // sigBytes = 16바이트
);
// 나머지 분리 → 실제 암호문
const ciphertextOnly = CryptoJS.lib.WordArray.create(
rawData.words.slice(4),
rawData.sigBytes - 16
);
console.log('추출된 IV hex:', iv.toString());
console.log('실제 암호문 길이:', ciphertextOnly.sigBytes, 'bytes');
const key = CryptoJS.enc.Hex.parse('추출한_키_hex');
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: ciphertextOnly },
key,
{ iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
);
console.log('복호화 결과:', decrypted.toString(CryptoJS.enc.Utf8));
7. WordArray 구조 및 Hex 변환 완전 정리
7-1. WordArray란 무엇인가
CryptoJS는 모든 바이트 데이터(키, IV, 암호문 등)를 WordArray 객체로 표현한다.
WordArray = {
words: [w0, w1, w2, w3, ...], // 32bit 정수(Int32) 배열, Big Endian
sigBytes: N // 유효한 실제 바이트 수
}
쉬운 이해:
bytes를 4개씩 묶어서 하나의 32bit 정수(word)로 저장
예: hex "54a000a1 596e7de1" → words: [0x54a000a1, 0x596e7de1]
→ sigBytes: 8
크기 참고표:
| words 배열 길이 | sigBytes | 의미 |
|---|---|---|
| 4개 | 16 | AES-128 Key 또는 IV |
| 6개 | 24 | AES-192 Key |
| 8개 | 32 | AES-256 Key |
공식 소스: CryptoJS core.js 기준.
sigBytes != undefined 이면 명시된 값 사용, 없으면 words.length * 4.
7-2. 음수 Word가 나오는 이유 및 Hex 변환
JavaScript는 32bit 정수를 부호 있는 정수(Signed Int32)로 저장한다.
최상위 비트(MSB)가 1이면 음수로 해석되어 콘솔에서 음수처럼 보인다.
예시:
hex: 8D60D867
이진: 1000 1101 0110 0000 1101 1000 0110 0111
↑ MSB = 1 → 부호 있는 정수에서는 음수
JavaScript: -1916280729 ← 음수로 보임
실제 비트: 0x8D60D867 ← hex 표현은 동일
변환 방법: 부호 없는 우측 시프트 (>>> 0)
(-1916280729 >>> 0) = 2378686567 (부호 없는 값)
.toString(16) = "8d60d867"
padStart(8, '0')가 필요한 이유:
작은 값은 hex 자릿수가 8자 미만일 수 있음
예: (256).toString(16) = "100" (3자) → "00000100" (8자)로 패딩 필요
7-3. 일반 배열 → WordArray → Hex 변환
디버거에서 IV를 일반 JS 배열로 발견했을 때:
// 발견한 배열 (예시)
const ivWords = [1411816533, 1497320417, -1497320418, -1916280729];
// 방법 1: CryptoJS WordArray.create 사용 (권장)
const iv = CryptoJS.lib.WordArray.create(ivWords);
console.log(iv.toString());
// CryptoJS가 내부적으로 sigBytes=words.length*4=16으로 설정
// 방법 2: 수동 변환 (CryptoJS 없어도 가능)
const ivHex = ivWords
.map(w => (w >>> 0).toString(16).padStart(8, '0'))
.join('');
console.log(ivHex);
// 결과: 방법 1과 동일
// 방법 3: Node.js 환경
const buf = Buffer.alloc(16);
ivWords.forEach((w, i) => buf.writeInt32BE(w, i * 4));
console.log(buf.toString('hex'));
검증: 세 방법 모두 동일한 hex가 나와야 한다.
7-4. sigBytes가 words 배열보다 작은 경우
// 예: 17바이트 데이터 → words 5개(20바이트 공간), sigBytes=17
const wa = CryptoJS.lib.WordArray.create([w0, w1, w2, w3, w4], 17);
// CryptoJS의 toString()이 sigBytes까지만 자동 처리
wa.toString() // → 34자 hex (17바이트 × 2)
// 수동 변환 (sigBytes 정확히 반영)
const hex = wa.words
.map((w, i) => {
const remaining = wa.sigBytes - i * 4;
if (remaining <= 0) return '';
const bytes = Math.min(remaining, 4);
return (w >>> 0).toString(16).padStart(8, '0').slice(0, bytes * 2);
})
.join('');
8. 암호문 캡처 기법
8-1. Network 탭에서 암호문 찾기
개발자도구 → Network 탭
→ 필터: XHR 또는 Fetch 선택
→ 암호화가 일어나는 동작 수행 (로그인, 데이터 전송 등)
→ 관련 요청 클릭
→ Headers 탭: 요청 헤더 확인
→ Payload 탭: 전송 데이터 확인 (Base64 여부 판별)
→ Response 탭: 서버 응답 확인
Base64 판별 기준:
문자 구성: A-Z, a-z, 0-9, +, /, = 만 사용
길이: 4의 배수
끝 패딩: = 또는 == 있을 수 있음
검증 코드:
function isBase64(str) {
try { return btoa(atob(str)) === str; }
catch(e) { return false; }
}
8-2. 함수 후킹 (Function Hooking) — 핵심 기법
핵심 아이디어: 원본 함수를 래핑해서, 함수가 호출될 때마다 파라미터를 자동으로 콘솔에 출력하게 만든다.
소스를 분석하지 않아도 키, IV, 평문, 암호문이 모두 자동으로 수집된다.
반드시 대상 페이지 콘솔에서 실행:
// ============================================================
// CryptoJS AES 완전 후킹 코드
// 실행 후 → "후킹 완료" 메시지 확인
// 이후 페이지 정상 사용 → 암호화/복호화 발생 시 콘솔 자동 출력
// ============================================================
(function() {
if (typeof CryptoJS === 'undefined') {
console.error('[!] CryptoJS가 이 페이지에 없습니다.');
return;
}
// 원본 함수 백업
const _orig_encrypt = CryptoJS.AES.encrypt.bind(CryptoJS.AES);
const _orig_decrypt = CryptoJS.AES.decrypt.bind(CryptoJS.AES);
// encrypt 후킹
CryptoJS.AES.encrypt = function(message, key, cfg) {
console.group('%c=== AES ENCRYPT ===', 'color: #2ecc71; font-weight: bold;');
// 평문 출력
if (typeof message === 'string') {
console.log('평문 (문자열):', message);
} else if (message && message.words) {
console.log('평문 (WordArray hex):', message.toString());
}
// 키 출력
if (typeof key === 'string') {
console.log('Key (문자열 → 내부 KDF 사용):', key);
} else if (key && key.words) {
console.log('Key (hex):', key.toString());
console.log('Key 길이:', key.sigBytes, 'bytes → AES-' + (key.sigBytes * 8));
}
// IV 출력
if (cfg && cfg.iv) {
console.log('IV (hex):', cfg.iv.toString());
console.log('IV 길이:', cfg.iv.sigBytes, 'bytes');
} else {
console.log('IV: 없음 (ECB 또는 기본값)');
}
if (cfg && cfg.mode) console.log('Mode:', cfg.mode);
// 원본 함수 실행 후 결과 출력
const result = _orig_encrypt(message, key, cfg);
console.log('암호문 (Base64):', result.toString());
console.groupEnd();
return result;
};
// decrypt 후킹
CryptoJS.AES.decrypt = function(ciphertext, key, cfg) {
console.group('%c=== AES DECRYPT ===', 'color: #e74c3c; font-weight: bold;');
// 암호문 출력
if (typeof ciphertext === 'string') {
console.log('암호문 (Base64):', ciphertext);
} else if (ciphertext && ciphertext.ciphertext) {
console.log('암호문 (CipherParams hex):', ciphertext.ciphertext.toString());
}
// 키/IV 출력
if (typeof key === 'string') {
console.log('Key (문자열 → 내부 KDF 사용):', key);
} else if (key && key.words) {
console.log('Key (hex):', key.toString());
console.log('Key 길이:', key.sigBytes, 'bytes → AES-' + (key.sigBytes * 8));
}
if (cfg && cfg.iv) {
console.log('IV (hex):', cfg.iv.toString());
} else {
console.log('IV: 없음 (ECB 또는 기본값)');
}
// 원본 함수 실행 후 결과 출력
const result = _orig_decrypt(ciphertext, key, cfg);
try {
const plaintext = result.toString(CryptoJS.enc.Utf8);
console.log('복호화 결과 (UTF-8):', plaintext || '(빈 문자열 또는 UTF-8 아님)');
} catch(e) {
console.log('복호화 결과 (Hex):', result.toString());
}
console.groupEnd();
return result;
};
console.log('%c[+] CryptoJS AES 후킹 완료. 이제 페이지를 정상 사용하세요.',
'color: #f39c12; font-weight: bold;');
})();
8-3. Burp Suite를 이용한 트래픽 캡처
1. Burp Suite 실행
2. Proxy → Proxy settings → Proxy Listeners → Add → Bind to port: 8080 / Bind to address: 127.0.0.1
3. 브라우저 프록시 설정: 127.0.0.1:8080
4. Proxy → Intercept → "Intercept is on"
5. 페이지에서 암호화 동작 수행 (로그인 등)
6. Burp에서 요청 캡처 → 암호화된 파라미터 확인
7. Repeater로 복사 → 값 변조 후 재전송 테스트
Burp에서 Base64 디코드:
요청 본문 선택 → 우클릭 → "Send to Decoder" → Decode as Base64
9. 복호화 실행 및 검증
9-1. 브라우저 콘솔에서 복호화
// ── Step 1: 키 설정 ──
const key = CryptoJS.enc.Hex.parse('추출한_키_hex_값_여기에');
console.log('Key hex:', key.toString(), '| 길이:', key.sigBytes, 'bytes');
// ── Step 2: IV 설정 ──
// IV가 hex 형식으로 추출된 경우
const iv = CryptoJS.enc.Hex.parse('추출한_IV_hex_값_여기에');
// IV가 일반 배열로 추출된 경우
// const iv = CryptoJS.lib.WordArray.create([정수1, 정수2, 정수3, 정수4]);
console.log('IV hex:', iv.toString(), '| 길이:', iv.sigBytes, 'bytes');
// ── Step 3: 복호화 실행 ──
const ciphertext = '여기에_Base64_암호문_붙여넣기';
const decrypted = CryptoJS.AES.decrypt(ciphertext, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
// ── Step 4: 결과 확인 ──
// ※ toString(enc.Utf8) 동작:
// - 대부분(~97%): 키/IV 오류 시 빈 문자열("") 반환 — throw 없이 조용히 실패
// - 일부(~2.5%): Error("Malformed UTF-8 data") throw
// → 빈 문자열 체크 + try-catch 모두 필수
console.log('Hex 결과 (raw):', decrypted.toString()); // 항상 출력 가능
try {
const r_utf8 = decrypted.toString(CryptoJS.enc.Utf8);
if (!r_utf8) {
// 빈 문자열 → 키/IV 오류 가능성 높음 (throw 없이 조용히 실패한 경우)
console.warn('⚠️ 빈 문자열 반환 — 키/IV 오류 가능성 높음');
console.log('진단용 NoPadding hex:', decrypted.toString());
// hex가 의미있어 보이면 → 키/모드 맞음, IV 재확인
// hex 전체 랜덤이면 → 키 자체 오류
} else {
console.log('✅ UTF-8 결과:', r_utf8);
}
} catch(e) {
console.log('⚠️ Malformed UTF-8 — NoPadding으로 raw hex 확인');
const r_nopad = CryptoJS.AES.decrypt(ciphertext, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.NoPadding
});
console.log('NoPadding hex:', r_nopad.toString());
// hex 결과가 의미있는 텍스트처럼 보이면 → 키/IV는 맞음, 패딩 처리만 문제
// 전체 랜덤한 hex이면 → 키 또는 모드 자체가 틀림
}
// Latin1은 모든 바이트를 처리 가능 (ASCII 범위 깨짐 확인용)
console.log('Latin1 결과:', decrypted.toString(CryptoJS.enc.Latin1));
9-2. CyberChef에서 복호화
URL: https://gchq.github.io/CyberChef/
① Input 박스에 Base64 암호문 붙여넣기
② 좌측 검색창에 "AES Decrypt" 검색 → Recipe 영역에 드래그
③ AES Decrypt 설정:
Key: [추출한 키 hex 값] 타입: Hex
IV: [추출한 IV hex 값] 타입: Hex
Mode: CBC
Input: Base64
Output: Raw
④ 하단 Output에서 결과 확인
결과가 깨지면:
→ Output 타입을 Hex 로 변경하거나
→ Output 뒤에 "Decode text" Operation 추가 → UTF-8 선택
9-3. Python 스크립트로 자동화
#!/usr/bin/env python3
"""
CryptoJS AES-CBC/ECB 복호화 + IV 오류 자동 진단 스크립트
pip install pycryptodome
"""
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64
import struct
def wordarray_to_bytes(words: list) -> bytes:
"""
CryptoJS words 배열(Int32 list)을 bytes로 변환
Big Endian signed int32 → bytes
"""
result = b''
for w in words:
result += struct.pack('>i', w)
return result
def try_decrypt_cbc(key_hex: str, iv_hex: str, ct_b64: str, label: str) -> None:
"""
AES-CBC 복호화 시도 + IV 오류 자동 진단
- 앞 16바이트만 깨지면 → IV 오류로 진단
- 전체 깨지면 → 키 오류로 진단
"""
try:
key = bytes.fromhex(key_hex)
iv = bytes.fromhex(iv_hex)
data = base64.b64decode(ct_b64)
cipher = AES.new(key, AES.MODE_CBC, iv)
raw = cipher.decrypt(data)
try:
plain = unpad(raw, 16).decode('utf-8', errors='replace')
print(f"[{label}] ✅ 성공: {plain[:100]}")
except Exception:
# 패딩 오류 → raw 결과로 IV/키 오류 진단
raw_text = raw.decode('utf-8', errors='replace')
first_block = raw[:16].decode('utf-8', errors='replace')
rest_blocks = raw[16:48].decode('utf-8', errors='replace')
is_first_bad = '\ufffd' in first_block or not first_block.isprintable()
is_rest_ok = '\ufffd' not in rest_blocks and len(rest_blocks) > 0
print(f"[{label}] ⚠️ 패딩오류")
if is_first_bad and is_rest_ok:
print(f" → 🔑 진단: IV가 틀림 (키는 맞음) - 앞 16바이트만 깨짐")
print(f" → 2번째 블록부터 내용: {rest_blocks[:60]}")
else:
print(f" → ❌ 진단: 키 오류 또는 모드 불일치")
print(f" → raw (앞 64B): {raw_text[:64]}")
except Exception as e:
print(f"[{label}] ❌ 오류: {e}")
def try_prepended_iv(key_hex: str, ct_b64: str) -> None:
"""
암호문 앞 16바이트를 IV로 추출해서 복호화 시도
"""
try:
key = bytes.fromhex(key_hex)
data = base64.b64decode(ct_b64)
if len(data) < 32:
print("[Prepended IV] ❌ 데이터가 너무 짧음 (최소 32바이트 필요)")
return
iv = data[:16]
ct_only = data[16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
plain = unpad(cipher.decrypt(ct_only), 16).decode('utf-8')
print(f"[Prepended IV] ✅ 성공!")
print(f" 추출된 IV (hex): {iv.hex()}")
print(f" 복호화 결과: {plain[:100]}")
except Exception as e:
print(f"[Prepended IV] ❌ 실패: {e}")
def try_decrypt_ecb(key_hex: str, ct_b64: str) -> None:
"""AES-ECB 복호화 (IV 없음)"""
try:
key = bytes.fromhex(key_hex)
data = base64.b64decode(ct_b64)
cipher = AES.new(key, AES.MODE_ECB)
plain = unpad(cipher.decrypt(data), AES.block_size).decode('utf-8')
print(f"[ECB] ✅ 성공: {plain[:100]}")
except Exception as e:
print(f"[ECB] ❌ 실패: {e}")
# ================================================================
# 사용 예시
# ================================================================
if __name__ == '__main__':
KEY_HEX = '추출한_키_hex_여기에'
CT_B64 = '캡처한_Base64_암호문_여기에'
# IV가 WordArray 배열 형태인 경우 hex로 변환:
# IV_WORDS = [정수1, 정수2, 정수3, 정수4]
# IV_HEX = wordarray_to_bytes(IV_WORDS).hex()
# print(f"변환된 IV hex: {IV_HEX}")
print("=" * 60)
print("IV 오류 자동 진단 스크립트")
print("=" * 60)
# 후보 IV 목록으로 순차 시도
iv_candidates = {
'Zero IV': '00000000000000000000000000000000',
'Key 앞16B': KEY_HEX[:32],
}
for label, iv_hex in iv_candidates.items():
try_decrypt_cbc(KEY_HEX, iv_hex, CT_B64, label)
print("\n--- Prepended IV 시도 ---")
try_prepended_iv(KEY_HEX, CT_B64)
print("\n--- ECB 모드 시도 ---")
try_decrypt_ecb(KEY_HEX, CT_B64)
pip install pycryptodome
python3 decrypt_tool.py
10. 취약점 심화: CBC Padding Oracle Attack
개념 (쉬운 설명)
일반 시나리오:
공격자 → 암호화된 값 전송 → 서버 복호화 → 결과 반환
Padding Oracle 취약점이란?
서버가 복호화 실패 이유를 구체적으로 알려줄 때 발생
정상 응답: {"status": "ok"}
패딩 오류 응답: {"error": "padding error"} ← 이게 문제!
평문 오류 응답: {"error": "invalid data"}
왜 위험한가?
→ "패딩 오류"와 "평문 오류"의 응답이 다르면,
공격자가 암호문 바이트를 조작하면서 서버 응답만으로
키 없이도 모든 바이트를 1/256 확률로 맞출 수 있음
→ 블록 16바이트 × 최대 256번 = 4,096번 요청으로 블록 복호화 가능
공격 원리
CBC 복호화 수식 (1바이트 단위 표현):
평문 마지막 바이트 = AES_복호화(C_N)[last] XOR C_{N-1}[last]
─────────────────────────────────────────────────────────────
이 중 AES_복호화(C_N)[last] 를 "중간값(intermediate)"이라 하자
공격자가 C_{N-1}[last] 를 변조값 X (0x00~0xFF)로 바꿔 전송하면:
서버가 복호화: 중간값 XOR X
중간값 XOR X = 0x01 이 되는 X를 찾으면 → PKCS7 패딩 유효(1바이트 패딩)
→ 서버 응답: 패딩 오류 없음
X를 찾은 순간 역산:
중간값 = X XOR 0x01
원래 평문 마지막 바이트 = 중간값 XOR 원래_C_{N-1}[last]
이를 뒤에서 앞으로 16바이트 전체 반복:
→ 블록 하나 완전 복호화 (키 없이!)
→ 여러 블록에 반복 적용 → 전체 평문 복호화 가능
취약점 탐지 방법
1. 암호화된 파라미터 포함 정상 요청 캡처 (Burp Suite)
2. 암호문 마지막 바이트를 0x00~0xFF로 변조하며 반복 요청
3. 서버 응답 관찰:
- 응답이 2종류로 구분 → ⚠️ 취약
- 응답이 모두 같음 → ✅ 안전 (구분 불가)
자동화 도구
# PadBuster (Perl)
git clone https://github.com/AonCyberLabs/PadBuster
perl padBuster.pl "https://target.com/api" "암호화된값" 16 -encoding 0
# Burp Suite Intruder
# Payload: 0x00~0xFF 브루트포스 / Match/Grep으로 응답 차이 탐지
11. 취약점 심화: 고정 IV 및 ECB 모드
11-1. 고정 IV의 위험성
정상 (매번 랜덤 IV):
평문 "password123" + 랜덤 IV1 → 암호문 A
평문 "password123" + 랜덤 IV2 → 암호문 B (A ≠ B)
→ 공격자는 두 암호문이 같은 의미인지 알 수 없음 ✅
취약 (고정 IV):
평문 "password123" + 고정 IV → 암호문 A
평문 "password123" + 고정 IV → 암호문 A (항상 동일!)
→ 공격자: "이 두 요청은 같은 비밀번호구나" 추론 가능 ❌
공격자가 할 수 있는 것:
- Replay Attack: 이전에 캡처한 암호문을 재전송해서 인증 우회
- 사전 공격: 자주 쓰는 패스워드의 암호문 테이블 생성
- 패턴 분석: 같은 암호문 = 같은 의미 확신
고정 IV 탐지 코드:
const key = CryptoJS.enc.Hex.parse('추출한_키_hex');
const iv = CryptoJS.enc.Hex.parse('추출한_IV_hex');
const enc1 = CryptoJS.AES.encrypt('테스트', key, { iv: iv }).toString();
const enc2 = CryptoJS.AES.encrypt('테스트', key, { iv: iv }).toString();
console.log('enc1:', enc1);
console.log('enc2:', enc2);
console.log(enc1 === enc2 ? '⚠️ 고정 IV 확인 (취약)' : '✅ IV가 매번 달라짐 (안전)');
11-2. ECB 모드의 위험성
쉬운 비유 (팽귄 이미지 문제):
ECB는 각 블록을 독립적으로 암호화 → 같은 블록 = 같은 암호문
원본 이미지: [A영역][B영역][A영역][C영역]
ECB 암호화: [암A] [암B] [암A] [암C] ← 같은 패턴 그대로!
CBC 암호화: [X1] [X2] [X3] [X4] ← 모두 다름
ECB 모드 탐지 (실전):
아이디어: 동일한 16바이트 블록 2개로 구성된 평문을 암호화해서 결과 블록이 동일하면 ECB.
const key = CryptoJS.enc.Hex.parse('추출한_키_hex');
// 동일한 16바이트 블록 2개로 구성된 테스트 평문 (총 32바이트)
// → ECB이면 두 블록 암호문이 동일, CBC이면 다름
const testPlain = 'AAAAAAAAAAAAAAAA' + 'AAAAAAAAAAAAAAAA';
// ※ 아래에서 mode를 '앱이 실제로 사용하는 옵션' 그대로 사용해야 함
// 앱이 CBC를 쓰면 CBC로 시도, ECB를 쓰면 ECB로 시도
// 또는 mode 지정 없이 CryptoJS 기본값(CBC)으로 시도
const encrypted = CryptoJS.AES.encrypt(testPlain, key); // 기본: CBC+랜덤IV
const cipherHex = encrypted.ciphertext.toString(); // CipherParams의 암호문만 hex
const block1 = cipherHex.slice(0, 32); // 앞 16바이트 = 32자 hex
const block2 = cipherHex.slice(32, 64); // 다음 16바이트
console.log('블록1:', block1);
console.log('블록2:', block2);
if (block1 === block2) {
console.log('⚠️ ECB 모드 의심 — 동일 평문 블록이 동일 암호문 블록을 생성');
} else {
console.log('✅ ECB 아님 — 블록들이 서로 다름 (CBC 또는 다른 체이닝 모드)');
}
주의: 실제 탐지는 앱이 실제로 어떻게 암호화하는지를 확인하는 것이므로,
함수 후킹(8-2 섹션)으로 앱의 암호화 결과를 캡처해서 블록을 비교하는 것이 더 정확하다.
12. 도구 정리
| 도구 | 용도 | 접속 / 설치 |
|---|---|---|
| Chrome DevTools | 동적 분석, 브레이크포인트, 함수 후킹 | F12 (브라우저 내장) |
| CyberChef | 복호화, 인코딩 변환, Recipe 자동화 | https://gchq.github.io/CyberChef/ |
| Burp Suite | 트래픽 인터셉트, 변조, Repeater | https://portswigger.net/burp |
| JS Beautifier | minified JS 가독성 복원 | https://beautifier.io/ |
| de4js | 고급 JS 난독화 해제 | https://de4js.kshift.me/ |
| deobfuscate.io | _0x 패턴 난독화 해제 | https://obf-io.deobfuscate.io/ |
| PyCryptodome | Python 기반 복호화 자동화 | pip install pycryptodome |
| PadBuster | Padding Oracle 자동화 | https://github.com/AonCyberLabs/PadBuster |
| OWASP ZAP | 웹 취약점 자동 스캔 | https://owasp.org/www-project-zap/ |
'🥕 저장소 (Dev & Tools) > 잡다구리 (ETC)' 카테고리의 다른 글
| Proxmark3 × ChameleonUltra (0) | 2026.02.21 |
|---|---|
| 해킹 상황별 도구 및 활용 전략 (0) | 2026.02.21 |
| 기업 네트워크 계층 구조 정리 (0) | 2026.02.21 |
| 공개 와이파이, 진짜 위험한가? HTTPS면 안전하지 않나? (0) | 2026.02.18 |
| 웹사이트 접속가능여부 확인 (0) | 2026.02.09 |
