728x90

사무실에서 PC로 문서 보면서 폰으로 음악 듣는 사람, 생각보다 많다. 멀티포인트 지원 헤드셋이면 PC에 우선권을 주고 PC가 조용할 때 폰 소리가 나오는 식으로 알아서 왔다 갔다 한다. 그런데 어느 순간부터 폰 음악이 뚝 끊긴다. 브라우저 탭을 하나씩 닫다 보니 범인이 알리익스프레스 탭이었다 — 이 이야기가 최근 HN에 올라온 laserphile 블로그 글의 출발점이다.

재밌는 건 원인이 광고 자동재생이 아니라는 점이다. 페이지에 <audio><video>도 없다. 탭 음소거를 눌러도 소용없다. 실제 범인은 게인 0으로 물려 있는 WebAudio 그래프, 즉 소리는 안 나지만 오디오 파이프라인은 계속 살아 있는 핑거프린팅 코드였다.

왜 지금 이 얘기가 화제인가

핑거프린팅 자체는 새로운 게 아니다. canvas, WebGL, 폰트, 하드웨어 스펙 긁어가는 스크립트는 10년 넘게 있었다. 이번 건이 눈에 띈 이유는 두 가지다.

  • 추적 코드가 물리 하드웨어의 동작을 바꿔버렸다. 브라우저 안에서 끝나는 게 아니라, OS 오디오 스택 → 블루투스 A2DP/HFP 링크 → 헤드셋 멀티포인트 중재 로직까지 영향을 줬다. 사용자 입장에서는 "헤드셋이 고장났나?" 싶은 증상이다.
  • 브라우저가 제공하는 통제 수단이 안 먹혔다. 탭 음소거는 미디어 엘리먼트를 대상으로 하지, "이 페이지의 AudioContext를 정지"시키는 버튼이 아니다. Firefox도 Chrome도 마찬가지였다고 원문은 적고 있다.

원인 스크립트는 두 개로 특정됐다. 둘 다 Alibaba의 AWSC(안티어뷰즈/보안) 계열로 보인다.

https://assets.aliexpress-media.com/g/AWSC/uab/1.140.0/collina.js
https://assets.aliexpress-media.com/g/AWSC/fireyejs/1.231.67/fireyejs.js

동작 원리: 소리 안 나는데 오디오가 도는 이유

원문에서 리버스한 그래프 구조는 이렇다.

Sawtooth Oscillator
    → AnalyserNode          (통과한 신호를 측정)
    → ScriptProcessorNode
    → GainNode (gain = 0)   (사람 귀에는 무음)
    → AudioContext.destination

핵심 트릭은 마지막 줄이다. 측정만 할 거면 OfflineAudioContext로 렌더링해도 되는데, 굳이 실시간 AudioContext를 만들고 destination까지 연결한다. destination에 물리는 순간 브라우저는 "이 페이지는 지금 라이브 오디오를 처리 중"이라고 판단하고 오디오 콜백을 실제 하드웨어 클럭에 맞춰 계속 돌린다. 게인이 0이라 파형 값은 전부 0이지만, 스트림 자체는 살아 있다.

비유하자면 이렇다. 스피커 볼륨만 0으로 내려놓고 앰프 전원은 켜둔 상태다. 소리는 안 나지만 앰프는 "지금 재생 중"이라고 상위 시스템에 계속 알린다. Windows 오디오 세션이 활성 상태로 유지되고, 블루투스 링크가 유휴로 떨어지지 않으니, 헤드셋 입장에서는 "PC가 아직 재생 중"이므로 폰으로 우선권을 넘길 이유가 없다. 원문 저자의 환경에서 관측된 결과가 딱 이거다.

왜 오디오로 지문을 뜨냐면, 같은 톱니파를 넣어도 브라우저 버전·OS 오디오 라이브러리·리샘플링 경로·부동소수점 처리 차이로 미세하게 다른 스펙트럼 값이 나오기 때문이다. 이거 하나로 개인 식별은 안 되지만 canvas 해시, WebGL 렌더러 문자열, hardwareConcurrency, deviceMemory, 지원 코덱, WebRTC 동작, 마우스/스크롤 이벤트 패턴과 조합하면 엔트로피가 꽤 올라간다. 쿠키처럼 지운다고 리셋되지도 않는다.

직접 확인해보기

남의 분석을 믿기 전에 자기 브라우저에서 재현해보는 게 빠르다. DevTools 콘솔에 페이지 로드 전에 이 스니펫을 넣어두면 된다(Chrome이면 Sources → Snippets, 또는 DevTools 설정에서 "Preserve log" 켜고 about:blank에서 실행 후 이동).

// AudioContext 생성 + destination 연결을 전부 로깅
const OrigAC = window.AudioContext;
window.AudioContext = class extends OrigAC {
  constructor(...a) {
    super(...a);
    console.warn('[AC] created', this.state, new Error().stack);
  }
};
const origConnect = AudioNode.prototype.connect;
AudioNode.prototype.connect = function (dest, ...rest) {
  if (dest instanceof AudioDestinationNode) {
    console.error('[AC] -> destination', this.constructor.name,
                  new Error().stack);
  }
  return origConnect.call(this, dest, ...rest);
};

알리 홈페이지를 열고 수 초간 가만히 두면 로그가 뜬다. 즉시 뜨지 않는 게 포인트라 성급하게 "안 나오네" 하고 닫으면 놓친다. 대략 이런 출력이 나온다.

[AC] created running
    at new AudioContext (<anonymous>)
    at .../AWSC/uab/1.140.0/collina.js:1:38412
[AC] -> destination GainNode
    at .../AWSC/uab/1.140.0/collina.js:1:38790
[AC] created running
    at .../AWSC/fireyejs/1.231.67/fireyejs.js:1:...

실시간으로 몇 개나 살아 있는지 보고 싶으면 이렇게 카운터를 하나 더 붙여두는 것도 편하다.

window.__acs = [];
const OrigAC2 = window.AudioContext;
window.AudioContext = class extends OrigAC2 {
  constructor(...a) { super(...a); window.__acs.push(this); }
};
// 나중에 콘솔에서
// __acs.map(c => [c.state, c.sampleRate, c.currentTime])
// → [["running", 48000, 12.37], ["running", 48000, 11.90]]

currentTime이 계속 증가하고 staterunning이면, 그 탭은 지금 이 순간에도 오디오 클럭을 붙잡고 있는 것이다.

실무 관점: 막을 것인가, 감수할 것인가

차단 룰

원문에서 제시한 uBlock Origin 커스텀 필터는 딱 두 줄이다. 대시보드 → 내 필터에 넣고 "변경 사항 적용".

! AliExpress AWSC fingerprinting scripts
||assets.aliexpress-media.com/g/AWSC/uab/*/collina.js$script,domain=aliexpress.com
||assets.aliexpress-media.com/g/AWSC/fireyejs/*/fireyejs.js$script,domain=aliexpress.com

버전 부분을 와일드카드로 빼놓은 게 실무적으로 중요하다. 1.140.0을 하드코딩하면 다음 배포에 바로 뚫린다. 반대로 ||assets.aliexpress-media.com^$script 같은 광범위 차단은 상품 이미지 CDN이나 결제 위젯까지 같이 죽일 위험이 있어서 권하지 않는다.

흔한 함정 1 — 룰 적용했는데 그대로다

가장 많이 걸리는 부분. 이미 만들어진 AudioContext는 스크립트를 차단해도 죽지 않는다. 네트워크 요청 차단은 앞으로의 요청에만 적용되고, 이미 메모리에 올라간 오디오 그래프와는 무관하다. 룰 적용 후에는 반드시 열려 있는 알리 탭을 전부 닫고 다시 열어야 한다. 하드 리로드(Ctrl+Shift+R)로도 대개 해결되지만, 확실하게 하려면 탭 종료가 낫다.

흔한 함정 2 — 안티프로드가 튕겨낸다

이 스크립트들은 봇 탐지/부정거래 방지 파이프라인의 일부로 보인다. 즉, 막으면 사이트가 나를 "측정 불가능한 클라이언트"로 취급할 수 있다. 원문 저자도 홈 브라우징과 상품 페이지는 멀쩡했지만 로그인·결제에서 문제가 생기면 룰을 일시 해제하겠다고 적었다. 실제로 이런 계열 스크립트가 차단됐을 때 콘솔에서 자주 보는 형태는 이렇다.

GET https://assets.aliexpress-media.com/g/AWSC/uab/1.140.0/collina.js
    net::ERR_BLOCKED_BY_CLIENT

Uncaught (in promise) TypeError: Cannot read properties of undefined
    (reading 'getUA')
    at HTMLFormElement.<anonymous> (login.js:1:20714)

net::ERR_BLOCKED_BY_CLIENT가 보이면 확장 프로그램이 막은 것이다(Firefox는 NS_ERROR_FAILURE나 "요청 중단됨"으로 표시되기도 한다). 그 뒤에 이어지는 getUA, getToken 같은 전역 객체 undefined 에러가 로그인 폼 제출을 막고 있다면, 결제할 때만 uBlock 아이콘에서 해당 사이트를 잠시 끄는 게 현실적인 타협이다. 로그인 실패를 계정 문제로 오해해서 비밀번호를 세 번 틀리고 계정 잠기는 게 최악의 시나리오다.

흔한 함정 3 — 원인을 하드웨어로 오진한다

헬프데스크 운영해본 사람은 공감할 텐데, "폰 음악이 안 나와요"는 100이면 99가 헤드셋 펌웨어나 블루투스 드라이버 이슈로 분류된다. 펌웨어 업데이트하고, 페어링 지우고 다시 잡고, 드라이버 재설치까지 다 해도 안 고쳐진다. 증상이 특정 사이트 탭을 열었을 때만 재현되는지를 먼저 확인하는 게 훨씬 빠르다. Windows라면 소리 설정의 볼륨 믹서에서 브라우저 프로세스가 활성 오디오 세션으로 잡혀 있는지 보면 힌트가 된다(무음이라 미터는 안 움직여도 세션은 떠 있는 경우가 있다).

대안들과 트레이드오프

  • uBlock Origin 좁은 룰 — 부작용 최소, 대신 스크립트 경로가 바뀌면 유지보수 필요. 지금으로선 가장 현실적.
  • Firefox의 강화 추적 방지 "엄격" — 알려진 핑거프린팅 도메인 차단 목록 기반이라 이 두 스크립트가 목록에 있는지는 별도 확인이 필요하다. 만능은 아니다.
  • AudioContext 자체를 패치하는 사용자 스크립트destination 연결을 무력화하는 방식. 이론적으로 가능하지만 정상 사이트(유튜브, 화상회의, 웹 게임)까지 깨질 수 있고 탐지도 쉽다. 도메인 한정으로만 쓰는 걸 권한다.
  • 그냥 감수하기 — 결제 흐름을 절대 건드리기 싫다면 알리 탭을 필요할 때만 열고 바로 닫는 운영이 제일 안전하다. 실제로 탭 닫으면 즉시 복구된다.

프론트엔드/보안 담당자에게 주는 교훈

이건 알리 욕하고 끝낼 얘기가 아니다. 우리가 봇 탐지 SDK나 상용 안티프로드 스크립트를 서비스에 붙일 때도 똑같은 일이 벌어질 수 있다. 벤더 스크립트 하나 붙였는데 사용자 헤드셋 동작이 바뀌고, 그 CS는 우리 팀으로 들어온다. 도입 전 체크리스트는 단순하다.

  • 벤더 SDK가 AudioContext를 만드는지, 만든다면 OfflineAudioContext가 아닌 실시간 컨텍스트인지
  • 측정 끝난 뒤 ctx.close()를 호출하는지 (안 하면 그냥 leak이다)
  • 측정 타이밍이 홈 화면 idle 시점인지, 로그인·결제 같은 민감 액션 직전인지
  • 차단당했을 때 우아하게 degrade 되는지, 아니면 TypeError 뿜고 폼 제출이 막히는지

특히 두 번째. destination에 붙일 이유가 없는 측정이라면 OfflineAudioContext로 렌더링하고 끝내면 된다. 그러면 하드웨어 오디오 경로를 건드리지 않는다. 굳이 실시간 컨텍스트를 쓰는 건 측정 정확도 때문일 수도 있고, 오프라인 렌더링을 후킹하는 프라이버시 확장을 피하려는 의도일 수도 있다 — 후자는 추측이고 원문도 서버 측 의도까지는 확인하지 못했다고 명시하고 있다.

정리

한 줄 요약: 알리익스프레스 홈에서 도는 난독화된 안티어뷰즈 스크립트(collina.js, fireyejs.js)가 게인 0짜리 WebAudio 그래프를 실제 오디오 destination에 연결한 채로 유지하는데, 이게 PC 쪽 블루투스 오디오 경로를 계속 붙잡아 멀티포인트 헤드셋이 폰으로 넘어가지 못하게 만든다. 탭 음소거로는 안 잡히고, 탭을 닫거나 스크립트를 차단해야 한다.

이 글이 필요한 사람:

  • 멀티포인트 헤드셋 쓰는데 이유 없이 폰 음악이 끊기는 사람 → 브라우저 탭부터 의심하고, 위 후킹 스니펫으로 확인
  • 사내 헬프데스크/IT 운영자 → "블루투스 이상" 티켓의 오진 후보 하나 추가
  • 서비스에 서드파티 안티프로드/봇탐지 SDK 붙이는 프론트엔드·보안 담당자 → 벤더 스크립트의 AudioContext 사용 여부를 릴리스 전에 한 번 확인

차단 룰은 언제든 깨질 수 있다. 원문 저자도 "그때 가서 다시 보겠다"고 했고, 그게 맞는 태도다. 다만 최소한 내 브라우저가 지금 뭘 돌리고 있는지 확인할 수 있는 도구는 손에 쥐고 있는 게 좋다. AudioContext 후킹 스니펫은 딱 그런 용도다.

참고 자료

    728x90

    + Recent posts