selenium을 이용할 때면, webdriver로 chromedriver를 많이 사용하곤 했다. GUI가 필요없을 때에도 headless로 사용했으니 대부분이라 말할수도 있을 것 같다.

하지만 어느 순간부터 selenium을 이용하여 Google login을 자동화하는 부분을 작성하다 보면, 아래와 같은 page를 확인할 수 있다.


문제 파악

email 입력 부분을 채우고 다음 버튼을 누르게 되면, 위와 같은 브라우저 또는 앱이 안전하지 않을 수 있습니다. 라면서 더 이상의 login을 할 수 없도록 한다.

Google 입장에서는 비정상적인 login 시도 행위를 막는 것은 당연한 일이지만, 자동화가 필요한 입장에서는 여간 불편한 것은 사실이다.

조금만 더 생각해 보면 Google에서 만든 chromedriver을 가지고 비정상 행위를 한다고 했을 때, 이를 탐지하는 방법 역시 쉬울테니 언젠가는 막히리라 생각하곤 했으나 실제로 막히니 난감하기는 했다.

그렇다면 Google은 어떻게 chromedriver을 이용한 login automation을 탐지하는 것이며, undetected_chromedriver은 어떻게 이를 우회하는 것일까? 쉬운 방법으로는 firefox와 같은 다른 브라우저의 드라이버를 이용하는 것이나 궁금증을 해결하기 위하여 undetected_chromedriver에 대하여 분석해 봤다.


우회법 분석

Google이 chromedriver을 이용하는 것인지에 대하여 알 방법은 명확하지 않기에 후자인 undetected_chromedriver가 어떻게 login automation을 가능하게 해주는지에 대하여 분석해 본다.

제일 먼저 간단한 binary diffing부터 진행한다.

(.venv) onsoim@MacBook-Pro undetected_chromedriver % ./chromedriver --version
ChromeDriver 98.0.4758.80 (7f0488e8ba0d8e019187c6325a16c29d9b7f4989-refs/branch-heads/4758@{#972})
(.venv) onsoim@MacBook-Pro undetected_chromedriver % md5 chromedriver
MD5 (chromedriver) = 3e5b46c2463efef54a6a8ecfd61b455a
#################################################################################
(.venv) onsoim@MacBook-Pro Safari % ./chromedriver --version
ChromeDriver 98.0.4758.80 (7f0488e8ba0d8e019187c6325a16c29d9b7f4989-refs/branch-heads/4758@{#972})
(.venv) onsoim@MacBook-Pro Safari % md5 chromedriver
MD5 (chromedriver) = 3f71959e5d8644780ee3dc353ffbdc70

기본적으로 undetected_chromedriver에서 설치되는 chromedriver와 https://chromedriver.storage.googleapis.com에서 받은 chromedriver를 비교해 보았다.

Google APIs에서 받은 chromdriver의 cdc_adoQpoasnfa76pfcZLmcfl가 undetected_chromedriver에서 설치되는 chromedriver에서는 xyx_bakbtmdhmelauoizUBpwcp 로 바뀐 것을 확인할 수 있다.

이 부분이 무엇인지 알기 위해서는 binary의 해당 부분을 직접 확인해 보면 명확해 진다.

아래는 위의 문자열이 사용된 함수의 일부를 발췌한 것이다.

function getPageCache(opt_doc, opt_w3c) {
  var doc = opt_doc || document;
  var w3c = opt_w3c || false;
  // |key| is a long random string, unlikely to conflict with anything else.
  var key = '$cdc_adoQpoasnfa76pfcZLmcfl_';
  if (w3c) {
    if (!(key in doc))
      doc[key] = new CacheWithUUID();
    return doc[key];
  } else {
    if (!(key in doc))
      doc[key] = new Cache();
    return doc[key];
  }
}

cdc_adoQpoasnfa76pfcZLmcfl는 window 객체의 여러 값을 가지고 있는 변수명들의 prefix로 사용되는데, 이 prefix는 각 build version마다 고유한 값을 가지고 있기 때문에 Google에서는 이를 이용하여 chromedriver 임을 쉽게 탐지할 수 있는 것 같다.


undetected_chromedriver

실제로 project에서 binary를 patch하는 부분의 코드는 아래와 같다.

@staticmethod
def gen_random_cdc():
    cdc = random.choices(string.ascii_lowercase, k=26)
    cdc[-6:-4] = map(str.upper, cdc[-6:-4])
    cdc[2] = cdc[0]
    cdc[3] = "_"
    return "".join(cdc).encode()

def patch_exe(self):
    """
    Patches the ChromeDriver binary
    :return: False on failure, binary name on success
    """
    logger.info("patching driver executable %s" % self.executable_path)

    linect = 0
    replacement = self.gen_random_cdc()
    with io.open(self.executable_path, "r+b") as fh:
        for line in iter(lambda: fh.readline(), b""):
        if b"cdc_" in line:
            fh.seek(-len(line), 1)
            newline = re.sub(b"cdc_.{22}", replacement, line)
            fh.write(newline)
            linect += 1
            return linect

Google에서는 prefix를 이용하여 chromedriver 임을 탐지하기 때문에 이를 다른 random한 값으로 바꿔주는 방식으로 탐지를 우회한다.

이를 이용하여 Google login을 automation하는 것은 간단하기에 따로 code를 올려두지는 않겠지만 필요하시면 요청해주세요!

+ Recent posts