
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$)에 어떻게 의존하는지 문서화한다.
sub_1801C6B30)server.dll (Win64 Retail Build)0x1C6B30void __fastcall sub_1801C6B30(int *pMovementServices, __int64 pPlayerController, __int64 pUserCmdPB)이 루틴은 클라이언트 CBaseUserCmdPB 페이로드를 통해 제출된 이동 버튼 전환 이벤트(+moveleft / +moveright)를 평가한다. 실행은 조건부 파이프라인을 따른다:
sv_auto_cstrafe_attempt_window, 기본값 100)에 저장된다.sub_1801C6B30)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);
}
}
}
$\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}$$
기본 구성 값은 다음과 같이 정의된다:
sv_auto_cstrafe_sequence_length)sv_auto_cstrafe_success_threshold)sv_auto_cstrafe_lower_overlap_pct_threshold)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)$$