금융 데이터 포털의 Accept-Enckey 파라미터 암호화 분석 및 JS 역공학

특정 금융 데이터 플랫폼(Cninfo)에서 제공하는 API를 분석하다 보면, 요청 헤더에 포함된 Accept-Enckey라는 암호화된 파라미터를 발견할 수 있습니다. 이 글에서는 해당 파라미터의 생성 로직을 추적하고, 이를 파이썬으로 구현하는 과정을 설명합니다.

1. 네트워크 패킷 분석

먼저 브라우저 개발자 도구(F12)의 네트워크 탭을 통해 데이터 요청 과정을 살핍니다. 마켓 데이터 조회 시 sysapi/p_sysapi1007와 같은 엔드포인트로 POST 요청이 전달되는데, 응답 데이터를 받기 위해서는 헤더에 포함된 Accept-Enckey 값이 필수적입니다.

해당 값이 유효하지 않으면 서버는 데이터를 반환하지 않으므로, 이 값이 생성되는 자바스크립트 로직을 찾아야 합니다.

2. 소스 코드 추적 및 디버깅

전체 소스 코드에서 sysapi/p_sysapi1007 키워드를 검색하면 해당 API를 호출하는 로직을 찾을 수 있습니다. 코드 분석 결과, Accept-Enckeyindexcode.getResCode()라는 함수를 통해 생성됨을 확인할 수 있습니다.

getResCode 함수 정의 위치를 찾기 위해 다시 검색을 진행하면, 난독화된 자바스크립트 파일 내에서 다음과 같은 구조의 코드를 발견하게 됩니다.

// 난독화된 코드의 전형적인 구조 예시
function getResCode() {
    var _0x4c7804 = _0x1dd253; // 문자열 치환 함수
    var encryptedData = CryptoJS['AES']['encrypt'](
        CryptoJS['enc']['Utf8']['parse'](Math['floor'](new Date()['getTime']() / 1000)), 
        CryptoJS['enc']['Utf8']['parse']('1234567887654321'), 
        {
            'iv': CryptoJS['enc']['Utf8']['parse']('1234567887654321'),
            'mode': CryptoJS['mode']['CBC'],
            'padding': CryptoJS['pad']['Pkcs7']
        }
    );
    return CryptoJS['enc']['Base64']['stringify'](encryptedData['ciphertext']);
}

3. 로직 복원 및 분석

난독화된 코드를 분석해 본 결과, 암호화 로직은 다음과 같이 요약됩니다.

  • 알고리즘: AES-128-CBC
  • 데이터(Plaintext): 현재 시간의 타임스탬프(초 단위)
  • 암호화 키(Key): 1234567887654321
  • 초기화 벡터(IV): 1234567887654321
  • 패딩 방식: Pkcs7
  • 출력 형태: Base64 인코딩

복원된 깨끗한 자바스크립트 코드는 다음과 같습니다.

const CryptoJS = require("crypto-js");

function generateEnckey() {
    // 타임스탬프를 초 단위로 계산 (원본 코드의 수식을 단순화)
    const timeInSeconds = Math.floor(new Date().getTime() / 1000).toString();
    const commonKey = CryptoJS.enc.Utf8.parse('1234567887654321');
    
    const options = {
        iv: commonKey,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    };

    const encrypted = CryptoJS.AES.encrypt(
        CryptoJS.enc.Utf8.parse(timeInSeconds), 
        commonKey, 
        options
    );

    return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}

console.log(generateEnckey());

4. 파이썬을 이용한 데이터 수집 구현

이제 복원된 로직을 파이썬의 execjs 라이브러리를 사용하여 실제 데이터 요청에 적용해 봅니다.

import requests
import execjs
import json
import time

# 자바스크립트 코드 컴파일
js_logic = """
const CryptoJS = require("crypto-js");
function getResCode() {
    const ts = Math.floor(new Date().getTime() / 1000).toString();
    const key = CryptoJS.enc.Utf8.parse('1234567887654321');
    const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(ts), key, {
        iv: key,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
    return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}
"""
ctx = execjs.compile(js_logic)
enc_key = ctx.call("getResCode")

# API 요청 설정
target_url = "https://webapi.cninfo.com.cn/api/sysapi/p_sysapi1007"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Accept-Enckey": enc_key,
    "Referer": "https://webapi.cninfo.com.cn/"
}
payload = {
    "tdate": "2024-05-01",
    "market": "SZE"
}

response = requests.post(target_url, headers=headers, data=payload)
print(response.json())

5. 추가 분석: 로그인 및 인증 과제

단순 파라미터 구현만으로 데이터 접근이 제한될 경우, 세션 인증 정보가 필요할 수 있습니다. 로그인 과정을 분석해 보면 계정 정보와 비밀번호 역시 AES 암호화 과정을 거칩니다.

로그인 시 사용되는 actionId는 다음과 같은 로직으로 생성됩니다.

// actionId 생성 예시
const actionId = new Date().getTime().toString() + Math.floor((Math.random() + 1) * Math.pow(10, 4));

또한 비밀번호 암호화는 bfd_api_12345678 키를 사용한 AES-ECB 모드를 사용하며, 결과물을 Hex 문자열로 변환하여 대문자로 전송하는 특징이 있습니다. 다만, 로그인 과정에서 이미지 캡차(Verification Code) 처리가 자동화를 어렵게 만드는 요소로 작용할 수 있습니다.

태그: reverse-engineering JavaScript crypto-js python aes-encryption

7월 25일 03:21에 게시됨