Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-1801C — Source 2 엔진의 이동 입력 휴리스틱에 대한 정적 리버스 엔지니어링 분석으로, 입력 자동화 및 점프 속도 제한 루틴의 구조적 엣지 케이스를 식별합니다. | Kitploit
도구/GitHubGitHub/misterdengi/cve-2026-1801c
Vulnerability AnalysisExploitationReverse EngineeringBinary AnalysisPapers & Research
GitHubmisterdengi/cve-2026-1801c

CVE-2026-1801C

Source 2 엔진의 이동 입력 휴리스틱에 대한 정적 리버스 엔지니어링 분석으로, 입력 자동화 및 점프 속도 제한 루틴의 구조적 엣지 케이스를 식별합니다.

저장소 보기
25시간 47분 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2026-1801C (QUANTUM-SHIFT) / CVE-2026-180A7 (BAL-JUMP): Source 2 (server.dll)의 이동 입력 휴리스틱에 대한 정적 분석

초록

본 논문은 Counter-Strike 2 엔진(server.dll)에 구현된 두 개의 클라이언트 측 이동 검증 루틴에 대한 정적 리버스 엔지니어링 분석을 제시한다: 입력 자동화 / SOCD 평가기(sub_1801C6B30, CVE-2026-1801C / QUANTUM-SHIFT로 지정) 및 점프 요청 속도 제한기(sub_180A7EDB0, CVE-2026-180A7 / BAL-JUMP로 지정). 디스어셈블리 분석과 기저 상태 머신의 공식 수학적 모델링을 통해, 두 휴리스틱 모두에서 구조적 엣지 케이스를 입증한다. 구체적으로, sub_1801C6B30의 이산 시간 양자화가 고정 단일 틱 위상 오프셋 하에서 불변 분류 경계를 어떻게 생성하는지, 그리고 sub_180A7EDB0이 위상 고정 단일 임펄스 트리거 하에서 불변으로 유지되는 단변량 구간 메트릭($\Delta t$)에 어떻게 의존하는지 문서화한다.


1. 입력 자동화 검증 ()

sub_1801C6B30

1.1 루틴 사양

  • 대상 바이너리: server.dll (Win64 Retail Build)
  • 심볼 오프셋: 0x1C6B30
  • 인터페이스: void __fastcall sub_1801C6B30(int *pMovementServices, __int64 pPlayerController, __int64 pUserCmdPB)

1.2 실행 파이프라인 및 상태 표현

이 루틴은 클라이언트 CBaseUserCmdPB 페이로드를 통해 제출된 이동 버튼 전환 이벤트(+moveleft / +moveright)를 평가한다. 실행은 조건부 파이프라인을 따른다:

  1. 속도 게이팅: 플레이어 폰의 스칼라 2D 속도 제곱은 정적 임계값을 초과해야 한다: $$|v_{xy}|^2 > (0.52 \cdot 260.0)^2 = 18279.04 \text{ unit}^2/\text{s}^2$$
  2. 시간 양자화: 전환 위상 델타($\Delta t_{\text{trans}}$)는 이산 프레임 틱 단위로 양자화된다: $$k = \text{round}\left( \frac{\Delta t_{\text{trans}}}{\tau_{\text{tick}}} \right), \quad \tau_{\text{tick}} \approx 15.625\text{ ms}$$
  3. 기호 분류: 정수 값 $k \in [0, 15]$는 이산 이벤트 코드 $c \in {1, 2, 3}$에 매핑된다: $$c = \begin{cases} 1 & \text{if } k = 0 \text{ (이상적인 0-틱 전환)} \ 2 & \text{if } k \ge 1 \text{ 및 상태가 Overlap} \ 3 & \text{if } k \ge 1 \text{ 및 상태가 Underlap} \end{cases}$$
  4. 링 버퍼 저장: 분류 코드 $c$는 용량 $N$의 순환 히스토리 배열(sv_auto_cstrafe_attempt_window, 기본값 100)에 저장된다.

1.3 디컴파일된 로직 (sub_1801C6B30)

root@kitploit:~
void __fastcall sub_1801C6B30(int *pMovementServices, __int64 pPlayerController, __int64 pUserCmdPB)
{
    if (!pPlayerController || (*(_BYTE *)(pUserCmdPB + 16) & 1) == 0)
        return;

    __int64 pPawn = sub_180B137A0(pPlayerController);
    if (!pPawn || !(*(unsigned __int8 (__fastcall **)(__int64))(*(_QWORD *)pPawn + 3216i64))(pPawn))
        return;

    float *pVel = (float *)sub_1803CBD90(pPawn, &szVelocityBuf);
    float vSq = (pVel[0] * pVel[0]) + (pVel[1] * pVel[1]) + (pVel[2] * pVel[2]);
    
    // 게이팅: 최소 속도 임계값
    if (vSq <= 18279.04f)
        return;

    int rawTicks = *(_DWORD *)(pUserCmdPB + 24);
    bool bIsOverlap = (rawTicks >= 0);
    int absTicks = bIsOverlap ? rawTicks : ~rawTicks;

    double curTime = sub_1801F46D0(off_181C9F350);
    if (curTime * 1e-9 * (double)absTicks >= 15.5)
        return;

    int roundedTicks = (int)V_roundd();
    if ((unsigned int)roundedTicks > 15)
        return;

    // 기호 할당
    char symbolCode;
    if (roundedTicks == 0)
        symbolCode = 1; // 성공 / 완벽한 0-틱
    else
        symbolCode = bIsOverlap ? 2 : 3; // Overlap 또는 Underlap

    // ConVar 구성 포인터 가져오기
    unsigned int windowCap = *GetConVarUInt(&unk_181DDBD30, &qword_181DDBD38);
    if (windowCap - 1 > 999)
        return;

    sub_1801ECEA0(pMovementServices, windowCap);

    // 순환 버퍼에 기호 추가
    int bufCap = pMovementServices[0];
    if (bufCap > 0)
    {
        int writeIdx = pMovementServices[6];
        if (writeIdx >= 0 && writeIdx < bufCap)
        {
            *(_BYTE *)(writeIdx + *((_QWORD *)pMovementServices + 1)) = symbolCode;
            if (++pMovementServices[6] == bufCap)
                pMovementServices[6] = 0;
        }
    }

    // 이산 틱 주파수 히스토그램 업데이트
    if (bIsOverlap)
        pMovementServices[roundedTicks + 7]++;
    else
        pMovementServices[roundedTicks + 23]++;

    int seqLen = *GetConVarInt(&unk_181DDBD40, &qword_181DDBD48);
    if ((unsigned int)(seqLen - 1) > 999)
        return;

    // 슬라이딩 윈도우 시퀀스 메트릭 평가
    int totalValid = 0;
    int totalOverlaps = 0;
    int curSequenceSuccesses = 0;
    int maxSequenceSuccesses = 0;

    for (int i = 0; i < (int)windowCap; ++i)
    {
        if (i < bufCap)
        {
            int readIdx = i + pMovementServices[6] - bufCap;
            if (i + pMovementServices[6] < bufCap)
                readIdx = i + pMovementServices[6];

            char sym = *(_BYTE *)(readIdx + *((_QWORD *)pMovementServices + 1));
            if (sym != 0)
            {
                totalValid++;
                if (sym == 2) totalOverlaps++;
                else if (sym == 1) curSequenceSuccesses++;
            }
        }

        // 슬라이딩 윈도우 업데이트
        if (i >= seqLen && (i - seqLen) >= 0)
        {
            if ((i - seqLen) < bufCap)
            {
                int popIdx = i + pMovementServices[6] - seqLen - bufCap;
                if (i + pMovementServices[6] - seqLen < bufCap)
                    popIdx = i + pMovementServices[6] - seqLen;

                if (*(_BYTE *)(popIdx + *((_QWORD *)pMovementServices + 1)) == 1)
                    curSequenceSuccesses--;
            }
        }

        if (maxSequenceSuccesses < curSequenceSuccesses)
            maxSequenceSuccesses = curSequenceSuccesses;
    }

    int minAttempts = *GetConVarInt(&unk_181DDBD20, &qword_181DDBD28);
    float dynamicLimit = 0.0f;

    if (totalValid >= minAttempts)
    {
        int minSuccessThreshold = *GetConVarInt(&unk_181DDBD50, &qword_181DDBD58);
        if (maxSequenceSuccesses >= minSuccessThreshold)
        {
            float successRatio = 0.0f;
            if (seqLen > minSuccessThreshold)
            {
                successRatio = (float)(maxSequenceSuccesses - minSuccessThreshold) / 
                               (float)(seqLen - minSuccessThreshold);
            }

            float lowerPct = *GetConVarFloat(&unk_181DDBD70, &qword_181DDBD78); // 기본값 20.0%
            float upperPct = *GetConVarFloat(&unk_181DDBD60, &qword_181DDBD68); // 기본값 5.0%
            
            dynamicLimit = (lowerPct - upperPct) * successRatio + upperPct;
        }
    }

    float observedOverlapPct = ((float)totalOverlaps / (float)totalValid) * 100.0f;

    // 위반 조건: 동적 한계가 관찰된 오버랩 비율을 초과
    if (dynamicLimit > observedOverlapPct)
    {
        sub_1801ECEA0(pMovementServices, 0); // 버퍼 상태 플러시
        
        bool bEnableKick = *GetConVarByte(&unk_181DDBD90, &qword_181DDBD98);
        if (bEnableKick)
        {
            __int64 pEngine = qword_182012050;
            void (__fastcall *pfnKickClient)(__int64, unsigned int, _QWORD, __int64) = 
                *(void (__fastcall **)(__int64, unsigned int, _QWORD, __int64))(*(_QWORD *)pEngine + 768i64);

            int slotIdx = -1;
            sub_181265470(pPlayerController, &slotIdx);
            
            // 연결 해제 코드 162: NETWORK_DISCONNECT_KICKED_INPUTAUTOMATION
            pfnKickClient(pEngine, (unsigned int)(slotIdx - 1), 0, 162);
        }
    }
}

1.4 수학적 모델 및 분류 분석

$\mathbf{S} = {s_1, s_2, \dots, s_N}$를 크기 $N$의 히스토리 버퍼에 있는 이벤트 기호의 시퀀스라고 하자. 매개변수 $S_{\text{max}}$는 길이 $L$의 연속 하위 시퀀스 내에서 코드 $1$ 기호($s_i = 1$)의 최대 개수를 나타낸다:

$$S_{\text{max}} = \max_{0 \le j \le N - L} \sum_{i=j}^{j+L-1} \mathbb{I}(s_i = 1)$$

적응형 임계값 함수 $T(S_{\text{max}})$는 $S_{\text{max}} \ge S_{\text{thresh}}$에 따라 조건부로 결정된다:

$$T(S_{\text{max}}) = \begin{cases} \theta_{\text{upper}} + (\theta_{\text{lower}} - \theta_{\text{upper}}) \cdot \frac{S_{\text{max}} - S_{\text{thresh}}}{L - S_{\text{thresh}}} & \text{if } S_{\text{max}} \ge S_{\text{thresh}} \ 0 & \text{if } S_{\text{max}} < S_{\text{thresh}} \end{cases}$$

기본 구성 값은 다음과 같이 정의된다:

  • $L = 15$ (sv_auto_cstrafe_sequence_length)
  • $S_{\text{thresh}} = 10$ (sv_auto_cstrafe_success_threshold)
  • $\theta_{\text{lower}} = 20.0$ (sv_auto_cstrafe_lower_overlap_pct_threshold)
  • $\theta_{\text{upper}} = 5.0$ (sv_auto_cstrafe_upper_overlap_pct_threshold)

위반은 $T(S_{\text{max}}) > P_{\text{overlap}}$인 경우에만 선언되며, 여기서 $P_{\text{overlap}}$는 코드 $2$ 이벤트의 표본 비율이다:

$$P_{\text{overlap}} = \frac{100}{N} \sum_{i=1}^{N} \mathbb{I}(s_i = 2)$$

구조적 불변성

위상 오프셋 $\Delta t_{\text{trans}}$가 $k = \text{round}(\Delta t_{\text{trans}} / \tau_{\text{tick}}) \ge 1$로 제한되는 모든 입력 스트림에 대해:

  1. 모든 $i$에 대해 $s_i \in {2, 3}$이므로 $\mathbb{I}(s_i = 1) = 0$이 된다.
  2. 결과적으로 $S_{\text{max}} = 0$이다.
  3. $0 < S_{\text{thresh}}$이므로 $T(S_{\text{max}}) = 0$이 동일하게 성립한다.
  4. 부등식 $0 > P_{\text{overlap}}$은 모든 음이 아닌 $P_{\text{overlap}}$에 대해 거짓으로 평가된다. pfnKickClient에서 종료되는 평가 경로는 실행되지 않은 채로 남는다.

2. 점프 속도 제한 (sub_180A7EDB0)

2.1 루틴 사양

  • 대상 바이너리: server.dll (Win64 Retail Build)
  • 심볼 오프셋: 0x1A7EDB0
  • 인터페이스: void* __fastcall sub_180A7EDB0(__int64 pMovementServices, __int64 pMoveData)

2.2 디컴파일된 로직 (sub_180A7EDB0)

root@kitploit:~
void *__fastcall sub_180A7EDB0(__int64 pMovementServices, __int64 pMoveData)
{
    __int64 pPawn = *(_QWORD *)(pMovementServices + 8);
    sub_180C28550(pPawn, 2i64);
    
    if (!*(_QWORD *)(pPawn + 56))
        nullsub_908(pPawn);

    // 충돌 힐 및 지면 접촉 평가
    (*(void (__fastcall **)(_QWORD))(**(_QWORD **)(pPawn + 56) + 1624i64))(*(_QWORD *)(pPawn + 56));
    sub_1803CBD30(*(_QWORD *)(pPawn + 56));
    sub_1803CD4C0(*(_QWORD *)(pPawn + 56), v14);

    if (dword_181FDB358)
        --dword_181FDB358;

    // 명령 버튼 마스크에서 IN_JUMP 비트(0x2) 확인
    if ((*(_BYTE *)(pPawn + 88) & 2) != 0 || sub_180C28550(pPawn, 2i64))
    {
        _BYTE *pPenaltyActive = (_BYTE *)(pMovementServices + 16);
        if (*pPenaltyActive)
        {
            float curTime = *((float *)off_181C9F350 + 12);
            float lastJumpTime = *(float *)(pMovementServices + 20);
            float penaltyInterval = sub_18029CE70(&unk_181FDB4D8, 0xFFFFFFFFi64); // sv_jump_spam_penalty_time

            // 구간 평가: curTime > lastJumpTime + penaltyInterval
            if (curTime > (penaltyInterval + lastJumpTime))
            {
                if (*pPenaltyActive)
                {
                    sub_180A99DE0(pMovementServices + 16, 0xFFFFFFFFi64, 0xFFFFFFFFi64);
                    *pPenaltyActive = 0; // 패널티 상태 해제
                }

                // 점프 임펄스 적용
                sub_180A7F550(pMovementServices, pMoveData);

                if (!*pPenaltyActive)
                {
                    sub_180A99DE0(pPenaltyActive, 0xFFFFFFFFi64, 0xFFFFFFFFi64);
                    *pPenaltyActive = 1; // 패널티 상태 활성화
                }

                if (sub_180C28550(*(_QWORD *)(pMovementServices + 8), 2i64))
                {
                    // 점프 타임스탬프 저장: lastJumpTime = curTime - frameTime
                    *(float *)(pMovementServices + 20) = *((float *)off_181C9F350 + 12) - 
                                                         *((float *)off_181C9F350 + 13);
                }
                return pMovementServices;
            }
        }

        bool bDebugSpam = *GetConVarByte(&unk_181FDB640, &qword_181FDB648);
        if (!bDebugSpam)
        {
            // 패널티 디버그가 비활성화된 경우 점프 임펄스 적용
            sub_180A7F550(pMovementServices, pMoveData);
            if (!*pPenaltyActive)
            {
                sub_180A99DE0(pPenaltyActive, 0xFFFFFFFFi64, 0xFFFFFFFFi64);
                *pPenaltyActive = 1;
            }
            *(float *)(pMovementServices + 20) = *((float *)off_181C9F350 + 12) - 
                                                 *((float *)off_181C9F350 + 13);
        }
    }
    return pMovementServices;
}

2.3 타이밍 모델 및 구간 평가

$t_n$을 $n$번째 점프 요청의 실행 타임스탬프라고 하고, $\tau_{\text{penalty}}$를 sv_jump_spam_penalty_time의 값(일반적으로 $1$ 틱, 즉 $\approx 15.625\text{ ms}$)이라고 하자. 패널티 플래그 $P \in {0, 1}$ 및 임펄스 활성화 $I \in {0, 1}$에 대한 상태 전이는 다음과 같이 정의된다:

$$I_n = \begin{cases} 1 & \text{if } P_{n-1} = 0 \text{ 또는 } t_n - t_{n-1} > \tau_{\text{penalty}} \ 0 & \text{if } P_{n-1} = 1 \text{ 및 } t_n - t_{n-1} \le \tau_{\text{penalty}} \end{cases}$$

고주파 vs. 지면 상태 고정 입력 분석

  1. 고주파 입력 스트림: 연속 프레임에 걸쳐 생성된 이산 입력 스트림은 $t_n - t_{n-1} \approx \tau_{\text{tick}} \le \tau_{\text{penalty}}$ 구간을 산출한다. 이 조건에서 $I_n = 0$이 되어 수직 임펄스 생성을 억제한다.
  2. 지면 상태 위상 고정: 점프 명령이 FL_ONGROUND($P_{\text{ground}} = 1$) 상태 전환 시에만 발행되는 경우, 요청 간 지속 시간은 탄도 비행 지속 시간 $T_{\text{flight}}$에 해당한다:

$$t_n - t_{n-1} = T_{\text{flight}} \ge \frac{2 \cdot v_z}{g} \approx 300\text{--}500\text{ ms}$$

$T_{\text{flight}} \gg \tau_{\text{penalty}}$이므로, 술어 $t_n - t_{n-1} > \tau_{\text{penalty}}$는 모든 $n$에 대해 무조건 성립하여 억제 경로의 활성화를 방지한다.


3. 구조적 수정 요약

구성 요소대상 심볼현재 프리미티브분석적 취약점제안된 수정
입력 자동화sub_1801C6B30이진 분류($k=0$ vs $k \ge 1$) + 임계값 설정고정 위상 오프셋($k=1$)은 $S_{\text{max}} = 0$을 동일하게 산출모든 $k$에 걸쳐 표본 분산 $\sigma^2(\Delta t)$ 평가; 빈 인덱스와 무관하게 저분산 분포 플래그 지정
점프 속도 제한sub_180A7EDB0스칼라 시간 델타 비교($t_n - t_{n-1} > \tau$)공중 위상 지연($T_{\text{flight}} \gg \tau$) 하에서 불변속도 벡터 연속성 및 지면 접촉 지속 시간($\Delta t_{\text{contact}}$) 평가
도구 다운로드