1P by GN⁺ | ★ favorite | 댓글 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 754double의 저장 방식을 정의하지만 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.sinMath.cos는 glibc에서 유래한 내장 dbl-64 루틴을 사용함
    • Chrome 148 이후 플랫폼 std::tanh를 쓰는 Math.tanhMath.* 중에서 OS를 누출함
    • 누출하지 않는 함수까지 대상 OS처럼 위조하면 V8의 실제 호출 구조와 어긋나며, tanh만 다르다는 비대칭 자체도 검사할 수 있음
  • JavaScript와 CSS가 서로 다른 경로를 사용함

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

    • Apple Silicon에는 스칼라 libsystem_mAccelerate의 벡터 루틴인 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.ccBUILDFLAG(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.tanhMath.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 트래픽과 맞추도록 구성됨

댓글과 토론

Hacker News 의견들
  • 오른쪽 입력으로 tanh를 한 번 호출한 결과가 운영체제별 서명이 된다는 설명은 브라우저 버전 범위를 식별할 가능성을 놓쳤음
    대부분은 User-Agent의 운영체제를 위조하지 않으며, 핑거프린팅은 운영체제 자체보다 준고유한 특성 조합에 관심을 둠. 발견은 흥미롭지만 글이 지나치게 LLM으로 작성돼 신뢰감을 떨어뜨림

    • 이 글을 만든 회사는 실제로 Linux VM의 봇을 Windows나 macOS 물리 장비처럼 위장하려 함
      그래야 봇 탐지를 더 쉽게 통과하고 다른 웹사이트에서 수집한 데이터를 고객에게 판매할 수 있음
    • 지금 이 방법만으로는 Chromium 148 이상이라고만 판별할 수 있지만, 각 버전에서 추가된 V8·Blink 기능을 JavaScript나 CSS로 검사하면 약 120 버전 이후부터 주 버전을 확실히 특정할 수 있음
      LLM으로 작성했다는 사실은 글과 블로그에 공개했으며 숨기거나 사람인 척한 것이 아님. 시간이 부족해 그렇지 않았다면 글을 아예 게시하지 못했을 것이고, 이 선택에 책임질 생각임
    • 버전 범위를 식별할 수는 있지만 그런 수단은 이미 셀 수 없이 많음
      브라우저는 계속 기능을 추가하고 버그를 수정하며, 그중 대부분을 JavaScript로 감지할 수 있음
    • 내용이 사실이라면 누가 썼는지는 상관없으며, LLM의 핵심 논지도 타당함
  • 모든 핑거프린팅 기법을 AI로 분석해 공개하고, 논란 끝에 브라우저가 이를 막도록 유도하면 자사 스크래핑 사업이 더 많은 돈을 벌 수 있으니 영리한 전략임
    이런 회사들이 없었다면 브라우저 핑거프린팅이 지금처럼 만연하지 않았고 인터넷도 더 나았을 것임. 차라리 fingerprint.js처럼 이해관계가 명확한 반대편의 글을 선호함

    • 스크래퍼가 있든 없든 인간을 추적하려면 핑거프린팅이 필요하며 결국 사용될 것이므로 동의하기 어려움
  • 정확한 반올림 초월함수를 추진해야 할 이유가 하나 더 생김
    최근에는 이 문제가 사실상 해결됐다는 것을 알게 됨. https://arith2026.org/program.html의 두 번째 기조연설 참고

    • 정확히 반올림되는 libm 함수는 훌륭하지만, 과거 glibc의 pow처럼 최악 성능이 끔찍해서는 안 됨
      반올림 경계에 가까울 때 사용하는 고정밀 대체 경로를 직접 SLP 벡터화해 최악 성능을 개선해 볼 수 있지만, 이미 대부분의 용도에는 충분함. JavaScript 엔진이 ECMAScript 명세에서 권장하는 fdlibm을 계속 쓰지 않는 점은 의외이며, Math.tanh가 JavaScript의 병목 경로라면 상당히 특이한 코드임
    • 고정 정밀도와 정수 연산이 더 널리 쓰이지 않는지 이해하기 어려움
      공학에서는 훨씬 단순한 하드웨어에서 동작하고 오차를 수학적으로 쉽게 모델링할 수 있어 고정소수점을 자주 사용했음. IEEE 754 부동소수점은 이론적으로도 미심쩍으며, 정밀도 손실에서는 가수보다 작은 정수, 즉 24비트 미만 정수가 32비트 부동소수점보다 나을 때도 있음
    • 해마다 새 도메인을 등록하고 영구적으로 갱신하는 방식인지 놀라움
  • 이 기법이 https://coveryourtracks.eff.org/에 추가돼 내 수학 함수 결과가 더 큰 모집단에서 얼마나 고유한지 확인할 수 있으면 좋겠음

    • 이 회사는 Chromium의 C++ 파일 550개 이상에서 신호 4,000개 이상을 패치했다고 주장함
      사실인지는 모르겠지만 coveryourtracks.eff.org가 쓰는 신호는 25개 정도 아닌가 싶음
  • 글을 Claude가 작성한 티가 남

    • 글 상단의 AI 요약 링크는 선택한 AI 제공자에게 기사 요약뿐 아니라 제품 광고까지 요청한다는 점이 황당함
      Claude 링크를 누르면 summarize+this+article+and+explain+how+scrapfly+helps+me+scrape+any+website+at+scale+and+bypass+anti-bot+systems+for+my+use+case:+[https://scrapfly.dev/posts/browser-math-os-fingerprint/](<https://scrapfly.dev/posts/browser-math-os-fingerprint/>;)라는 프롬프트가 전달됨
    • 제목의 발견은 흥미롭지만 나머지는 사실상 Claude가 작성한 내용
    • 최근 몇 달간 HN 활동이 뜸했는데, 커뮤니티가 콘텐츠를 저질로 낙인찍고 LLM 사용을 탐지하는 데 편집증적으로 집착하는 듯함
  • Tor Browser와 Mullvad Browser도 결국 운영체제 은폐를 포기했지만, 어쩌면 그러지 말았어야 함
    그만큼 핑거프린팅 경로가 지나치게 많아 보임

    • 운영체제 숨기기가 가능한지도 불분명하므로 올바른 결정이었다고 봄
      브라우저 안팎에서 운영체제별 동작 차이가 너무 많이 발생해 모두 처리하기 어려움. 캔버스 추출을 차단하거나 잡음을 넣어도 렌더링 차이가 노출될 수 있으며, Tor Browser 개발자도 완전히 다른 운영체제는커녕 X11과 Wayland의 차이조차 숨길 수 없다고 확인했음. https://forum.torproject.org/t/linux-is-it-alright-to-run-th...
    • Tor Browser는 navigator.platform조차 수정하지 않으므로 Windows가 아닌 환경을 알아내기가 매우 쉬움
  • 선호하는 JavaScript 주입 플러그인으로 다음 코드를 넣으면 됨: let oldTanh = Math.tanh; Math.tanh = x => oldTanh(x) + Math.random()/10000000;

    • 더 간결하게 Math.tanh = Math.random;을 선호함
    • 글에서 이미 다루고 있으니 “No noise”를 검색하면 됨
    • 여러 봇 방지 업체가 이 교체를 탐지해 핑거프린팅 신호로 활용할 것임
    • 이제 정상값 대신 핑거프린트를 숨기는 사용자로 드러나 오히려 더 식별하기 쉬워질 수 있음
  • 최신 glibc는 CORE-MATH의 정확히 반올림되는 tanh 를 사용하므로 글에 인용된 값과 다른 결과를 반환함
    다른 초월함수도 합리적인 성능으로 정확한 반올림을 구현할 수 있는지는 아직 불분명해 각 함수가 저마다 고유한 핑거프린트를 남김

  • Chrome은 실행 코드만 수백 MB라 사용자 공간 라이브러리의 절반쯤은 정적 링크했을 줄 알았음
    또한 tanh는 함수 호출이 아니라 JavaScript JIT가 CPU 명령으로 내보내는 내장 연산이라고 생각했는데, 수학 연산을 위해 dlsym() 함수로 분기해야 한다는 점이 이상함. CPU 명령 자체도 핑거프린팅될 수는 있음

    • x87 FPU는 초월함수를 마이크로코드로 구현했지만 대부분의 명령어 집합은 이를 제공하지 않음
      마이크로코드는 분기 예측 같은 이점을 얻지 못해 실제로 소프트웨어 구현보다 느림
    • 기억하기로 Chrome은 JIT되지 않은 모드에서 NaN 값의 사용되지 않는 비트를 보존하는 유일한 브라우저이며, 코드가 JIT되면 그 비트가 0으로 바뀜
  • 이 싸움에서 이길 수 있는지 의문임
    충분히 많은 함수를 실행하면 실행 시간 비율과 반올림 결과를 조합해 운영체제와 정확한 기종은 물론, 같은 장비에서 실행 중인 다른 작업까지 추정할 수 있을 듯함. 완전히 막기보다는 조금 어렵게 만드는 정도만 가능해 보임
    결국 사회와 법이 따라잡아야 함. 문 잠금장치가 침입을 완전히 막지 못해도 사회적 비난과 형사 처벌이 보완하듯, 이런 방식의 개인 추적을 불법화하고 그 이익을 사용하는 회사와 취업자를 사회적으로 배척해야 함

    • 사이버 공간에서는 불법이거나 불법이어야 할 행위를 하는 이들이 집행 불가능한 관할권에 있는 경우가 많음
      러시아·미얀마·북한 같은 곳에서는 법치가 작동하지 않고, 외국인을 속이는 범죄자를 현지 당국이 적극적으로 보호하기도 하므로 문 잠금장치 비유가 성립하지 않음