Chromium 148부터 Math.tanh로 기반 OS를 식별할 수 있음

3 hours ago 1
  • Chrome 148부터 V8의 Math.tanh가 내장 fdlibm 대신 호스트의 std::tanh를 호출하면서, 같은 입력도 Linux의 glibc·macOS의 libsystem_m·Windows의 UCRT에서 서로 다른 마지막 비트를 반환함
  • Math.tanh(0.8)은 Linux에서 0.6640367702678491, macOS에서 0.664036770267849, Windows에서 0.6640367702678489가 되어 한 번의 호출로 세 OS를 구분할 수 있으며, User-Agent가 주장하는 OS와 결과가 다르면 위장이 드러남
  • 엔진마다 누출 경로가 달라 V8의 Math.*에서는 tanh만 호스트 수학 라이브러리를 사용하지만, Blink의 CSS 삼각함수 전체와 Web Audio 일부 연산도 OS별 라이브러리를 거침
  • 값을 임의로 흔들면 실제 OS 어느 쪽과도 일치하지 않고 결정성까지 깨지므로, 대상 라이브러리의 계수·테이블·범위 축소·FMA 동작을 비트 단위로 재현하거나 원본 UCRT 코드를 직접 매핑해야 함
  • Scrapfly는 릴리스마다 871,000개 입력을 실제 Mac과 Chrome에 대조해 Math.tanh와 CSS 삼각함수 7개의 비트 일치를 검증하며, 정확도뿐 아니라 아키텍처 차이와 실행 시간도 실제 브라우저 수준으로 맞춤

Math.tanh가 드러내는 OS

  • Math.tanh(0.8)의 결과는 호스트 수학 라이브러리에 따라 달라짐
    • Linux Chrome의 glibc: 0.6640367702678491
    • macOS Chrome의 libsystem_m: 0.664036770267849
    • Windows Chrome의 UCRT: 0.6640367702678489
  • Apple과 glibc는 전체 입력의 약 4분의 1에서 대체로 1 ULP 차이를 보이며, Windows UCRT는 두 라이브러리와 몇 퍼센트의 입력에서 달라짐
    • ULP(unit in the last place)는 특정 크기에서 표현 가능한 연속 부동소수점 수 사이의 간격이며, 1 ULP는 double이 나타낼 수 있는 최소 차이임
  • 실제 Chrome 150을 Linux, Apple Silicon 기반 macOS 26, Windows 11에서 DevTools Protocol로 측정하면 입력에 따라 분류력이 달라짐
    • tanh(0.5)는 세 OS 모두 0.46211715726000974로 같아 탐지에 쓸 수 없음
    • tanh(0.7)은 Linux만 1 ULP 차이남
    • tanh(0.8)은 세 OS가 모두 다르며 전체 범위는 2 ULP임
    • tanh(0.9)는 Windows만 1 ULP 차이남
  • 약 4분의 3의 입력에서는 세 OS가 같은 결과를 내지만, 적절한 입력 하나면 OS별 서명을 얻을 수 있음
  • macOS를 주장하면서 Linux의 수학 비트를 반환하면 Math.tanh 결과가 User-Agent와 모순됨

Chrome 148에서 생긴 변화

  • Chrome 147까지 V8은 이식 가능한 수학 구현인 fdlibm 포트를 내장해 Math.tanh를 계산했으므로 모든 OS에서 같은 비트를 반환함
  • V8 커밋 c1486295ae5은 내장 구현을 플랫폼의 std::tanh로 교체함
    • 이 변경은 V8 14.8.57과 Chrome 148에 처음 포함됨
    • Chrome 148·149·150은 호스트 libm의 차이를 노출하지만, Chrome 147 이하는 이 경로로 OS를 누출하지 않음
  • IEEE 754는 double의 저장 방식을 정의하지만 sin, cos, tanh, exp 같은 초월함수가 반드시 올바르게 반올림되도록 요구하지 않음
  • 각 OS의 수학 라이브러리(libm) 는 성능과 ULP 오차를 절충하며 서로 다른 최소최대 근사 다항식 계수, 조회 테이블, 범위 축소 상수를 사용함
    • Linux는 glibc
    • macOS는 Apple libsystem_m
    • Windows는 UCRT의 ucrtbase.dll
  • 탐지기는 수학 연산 자체를 분석할 필요 없이 실제 Chrome의 입력별 결과표와 값을 비교할 수 있음

재현을 어렵게 만드는 네 가지 함정

  • V8의 일부 함수만 누출됨

    • V8은 대부분의 수학 구현을 정적으로 링크하므로 OS와 무관하게 같은 결과를 냄
    • Math.exp, Math.pow, Math.atan 등은 내장 llvm-libc 구현을 사용함
    • Math.sin과 Math.cos는 glibc에서 유래한 내장 dbl-64 루틴을 사용함
    • Chrome 148 이후 플랫폼 std::tanh를 쓰는 Math.tanh만 Math.* 중에서 OS를 누출함
    • 누출하지 않는 함수까지 대상 OS처럼 위조하면 V8의 실제 호출 구조와 어긋나며, tanh만 다르다는 비대칭 자체도 검사할 수 있음
  • JavaScript와 CSS가 서로 다른 경로를 사용함

    • CSS의 sin(), cos(), atan2()는 JavaScript의 Math.sin과 코드를 공유하지 않음
    • Blink 레이아웃 엔진은 각도를 도 단위로 축소한 뒤, 축소된 값에 플랫폼 std::sin 등을 호출함
    • 직접 라디안 입력을 계산한 결과와 달라지며, CSS 삼각함수 7개 전체가 호스트 libm을 통해 OS를 누출함
    • 비트 단위 재현에는 최종 수학 함수뿐 아니라 도 단위 범위 축소와 라디안·도 변환 과정도 포함해야 함
  • macOS 안에도 서로 다른 두 라이브러리가 있음

    • Apple Silicon에는 스칼라 libsystem_m과 Accelerate의 벡터 루틴인 vvsin, vvtanh가 함께 있으며 두 구현은 같지 않음
    • 100만 개 입력에서 함수에 따라 10~89% 의 결과가 달랐음
    • cos(0)은 스칼라 구현에서 정확히 1.0임
    • Accelerate에서는 0.9999999999999999를 반환함
    • 실제 Mac의 Chrome을 디버깅 프로토콜로 측정해 호출 지점별 라이브러리를 구분함
    • Math.tanh, CSS 삼각함수, 오디오 압축기의 샘플별 초월함수는 스칼라 libsystem_m을 사용함
    • Mac의 Web Audio DSP, FFT, 벡터 수학, 바이쿼드 필터는 Accelerate를 사용함
    • 관련 Chromium 경로에는 fft_frame_mac.cc, vector_math_mac.h, biquad.cc와 BUILDFLAG(IS_MAC)가 있음
    • 호출 지점에 맞지 않는 Apple 라이브러리를 선택하면 대부분의 입력에서 1 ULP가 어긋날 수 있음
  • CPU 아키텍처도 결과에 개입함

    • ARM과 x86은 융합 곱셈-덧셈(FMA) 및 NaN 부호 전파에서 차이를 보임
    • 수학적 절차가 맞더라도 컴파일러가 한 아키텍처에서만 곱셈과 덧셈을 융합하면 결과 비트가 달라짐

엔진과 기능별 누출 경로

  • JavaScript의 V8 Math.*는 거의 모두 내장 구현을 사용하며, 호스트 libm으로 연결되는 지점은 Math.tanh 하나임
    • sin, cos, tan, asin, acos, atan, atan2, exp, log, log2, log10, pow는 V8 내장 구현을 사용함
    • sqrt, abs, 사칙연산은 하드웨어 연산임
  • CSS calc()의 수학 함수는 Blink가 플랫폼 라이브러리를 직접 호출함
    • sin, cos, tan, asin, acos, atan, atan2, exp, log, log2, log10, pow가 호스트 libm 을 사용함
    • CSS에는 대응하는 tanh 경로가 없음
  • Web Audio는 호출 지점에 따라 여러 구현을 섞어 사용함
    • Mac의 오실레이터 FFT, 벡터 덧셈·곱셈·스케일, FFT는 Accelerate의 vDSP를 사용함
    • DynamicsCompressor의 sin, exp, log10f, powf 등 샘플별 초월함수는 스칼라 libsystem_m을 사용함
    • 하나의 오디오 그래프가 V8 내장 수학, 스칼라 라이브러리, Accelerate 등 세 가지 라이브러리에 걸칠 수 있음
  • WebAssembly에는 초월함수 명령어가 없음
    • sin 등의 결과는 모듈에 포함된 libm에 따라 결정됨
    • f64.sqrt, f64.mul 같은 산술은 하드웨어에서 실행되므로 OS별로 같음
    • 남는 지문 축은 ARM과 x86 사이의 NaN 정규화 및 일부 SIMD 반올림 차이임
  • 탐지 신호는 Math.tanh, 모든 CSS 삼각함수, Web Audio에 집중됨
    • Web Audio의 Accelerate FFT는 CPU 아키텍처를 드러냄
    • 압축기의 스칼라 libsystem_m은 OS를 드러냄

값을 흔드는 대신 정확히 재현하기

  • 노이즈가 실패하는 이유

    • 결과에 노이즈를 추가하면 기준표에 있는 어느 실제 OS 값과도 일치하지 않을 수 있음
    • 호출마다 무작위 값이 달라지면 결정성이 깨지며, 이 현상 자체가 별도의 탐지 신호가 됨
    • 목표는 비슷한 값이 아니라 주장한 OS가 반환하는 값과 비트 단위로 같은 결과임
  • 대상 알고리듬의 모든 요소 복원

    • 대상 libm에서 최소최대 근사 계수, 지수 테이블, 범위 축소 상수를 복구해 이식 가능한 C 코드로 옮김
    • 대상 라이브러리가 잘못된 방향으로 반올림하는 입력까지 그대로 일치시켜야 함
    • Apple sin 재현은 libsystem_m에서 추출한 계수의 정확한 비트 패턴과 명시적인 fma() 호출을 사용함
    • 계수를 10진수로 옮기면 전사 과정에서 다시 반올림될 수 있으므로 16진 부동소수점 값으로 보존함
    • Apple이 융합하는 각 곱셈-덧셈을 코드에서도 명시적으로 융합함
  • FMA를 결정적으로 고정

    • -ffp-contract=off로 컴파일해 컴파일러가 임의로 FMA를 추가하거나 제거하지 못하게 함
    • 코드에 명시한 fma()만 Apple과 같은 위치에서 실행되므로 ARM을 모방하면서 x86 서버에서 실행해도 같은 비트를 얻을 수 있음
    • 하드웨어 FMA와 올바르게 반올림되는 소프트웨어 FMA는 같은 비트를 반환함

Windows UCRT 원본 코드 사용

  • Windows UCRT는 Linux 서버와 같은 x86-64 ISA를 사용하고 위치 독립적이므로, 실제 ucrtbase.dll을 런타임 메모리에 매핑하고 수학 함수 내보내기(export)를 직접 호출할 수 있음
  • 원본 코드를 실행하므로 별도의 수학 알고리듬 역공학 없이 실제 UCRT 비트를 얻을 수 있음
  • Linux의 System V ABI와 Windows x64 ABI 차이를 처리해야 함
    • Windows x64에서는 피호출자가 반환 주소 위의 32바이트 shadow space를 사용함
    • 피호출자 보존 레지스터 집합도 System V와 다름
    • 함수 포인터를 ms_abi로 선언하지 않으면 shadow-space 쓰기가 clang 스택 프레임을 손상시키고 간접 호출이 잘못된 주소로 이동할 수 있음
  • 매핑한 DLL 코드는 CFI에 등록된 간접 호출 대상이 아님
    • 프로덕션의 -fsanitize=cfi-icall이 호출마다 #UD 트랩과 SIGILL을 일으킬 수 있음
    • 함수 포인터를 호출하는 래퍼에는 clang::no_sanitize("cfi-icall")가 필요함
  • UCRT 수학 함수는 시작 부분의 mov eax, [rip+disp32]로 CPU 디스패치 플래그를 읽어 스칼라 또는 FMA/AVX2 경로를 선택함
    • 새로 매핑한 DLL에서는 플래그가 0이라 느린 스칼라 경로를 선택함
    • 이 경로의 결과는 최신 Windows 시스템의 결과 비트와 다름
    • tanh 프롤로그에서 플래그 주소를 찾아 첫 호출 전에 FMA 경로로 강제해야 실제 Windows와 비트 단위로 일치함

패치 위치와 성능 제약

  • 엔진이 libm을 호출하는 단일 병목 지점을 훅하고, 브라우저가 주장하는 OS에 따라 경로를 선택함
    • Linux를 주장하면 glibc를 유지함
    • macOS를 주장하면 Apple 재현 구현을 사용함
  • 결과가 정확해도 실행 시간이 실제 브라우저와 다르면 탐지할 수 있음
  • 첫 빌드는 기본 x86 기준선이 하드웨어 FMA보다 오래돼 모든 fma()를 소프트웨어 호출로 낮췄으며, 네이티브보다 2.5~6배 느렸음
  • Math.tanh와 Math.sin 반복문의 시간 비율을 비교하면 실제 브라우저에는 없는 성능 패턴이 드러날 수 있음
  • 하드웨어 FMA를 활성화하자 각 융합 연산이 단일 명령어가 되어 약 6배 빨라졌으며, glibc보다 빠르면서도 결과 비트는 동일했음

871,000개 입력으로 검증

  • 검증 하네스는 릴리스마다 871,000개 입력을 모든 분기와 정의역에 걸쳐 실행함
    • 조밀한 입력 격자
    • 구간 경계
    • 비정규수
    • 부호 있는 0
    • 무한대
    • NaN
  • 두 종류의 실제 환경을 기준값으로 사용함
    • 실제 Mac이 모든 입력에서 스칼라와 Accelerate 결과를 각각 계산해 두 구현이 갈리는 지점을 확인함
    • 실제 Mac의 Chrome을 디버깅 프로토콜로 구동해 Math.tanh와 모든 CSS 삼각함수의 전체 정밀도 결과를 수집함
  • Math.tanh와 CSS의 sin, cos, tan, asin, acos, atan, atan2가 실제 Mac Chrome과 비트 단위로 일치
  • 재현 구현이 배포 바이너리 안의 실제 기계어와 동일하게 동작하는지도 검증함
  • 정의역 경계의 브라우저 후처리까지 맞춰야 함
    • 실제 Mac에서 CSS asin(2)는 정의역 밖이라 NaN이 되고 CSS가 NaN을 0으로 제한하므로 최종값은 0임
    • 단순한 재현 구현은 이를 90도로 잘못 반환할 수 있음

브라우저 위장에서 수학이 중요한 이유

  • 수학 결과는 결정적이고 저렴하게 검사할 수 있지만, 벤더 libm 내부와 엔진별 호출 경로를 알아야 정확히 위장할 수 있음
  • 실제 브라우저와 일치하려면 V8·Blink·Web Audio가 호출 지점마다 어떤 수학 라이브러리를 선택하는지 파악하고, 마지막 비트와 아키텍처별 동작 및 실행 시간까지 맞춰야 함
  • Scrapfly의 Scrapium은 macOS로 표시하도록 요청받으면 코사인의 반올림 비트까지 실제 macOS 트래픽과 맞추도록 구성됨
Read Entire Article