님아. 2026. 2. 24. 13:28

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/