Skip to content

[Bug] I maybe found some ways to detect stealth plugin #403

Closed
@NikolaiT

Description

@NikolaiT

I conducted the tests with the following chrome/chromium browsers on Linux Ubuntu 18.04:

Puppeteer Vanilla Chrome/88.0.4298.0 / 20.10.2020
npm version: puppeteer-5.5.0

Puppeteer Stealth Chromium version: Chrome/88.0.4298.0 / 20.10.2020
npm versions: puppeteer-5.5.0 puppeteer-extra-3.1.16 puppeteer-extra-plugin-stealth-2.6.6

Google Chrome version: Chrome/87.0.4280.141 / 07.01.2021

Chromium version: Chrome/87.0.4280.66 / 17.11.2020

navigatorPrototype

Test

['hardwareConcurrency', 'languages'].forEach((prop) => {
  let objDesc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), prop);

  if (objDesc !== undefined) {
    if (objDesc.value !== undefined) {
      res = objDesc.value.toString();
    } else if (objDesc.get !== undefined) {
      res = objDesc.get.toString();
    }
  } else {
    res = "";
  }
  
  console.log(prop + "~~~" + res)
})

Results

Puppeteer Vanilla hardwareConcurrency~~~function get hardwareConcurrency() { [native code] }
Puppeteer Stealth hardwareConcurrency~~~4
Google Chrome hardwareConcurrency~~~function get hardwareConcurrency() { [native code] }
Chromium "hardwareConcurrency~~~function get hardwareConcurrency() { [native code] }"

Puppeteer Vanilla languages~~~function get languages() { [native code] }
Puppeteer Stealth languages~~~() => opts.languages || ['en-US', 'en']
Google Chrome languages~~~function get languages() { [native code] }
Chromium languages~~~function get languages() { [native code] }

Conclusion

Plugin Stealth seems to change the protoype of navigator.plugins and navigator.hardwareConcurrency
in way such that it is unique to plugin stealth!

permissions

Test

var permissions =  () => {
  return new Promise((resolve) => {
    navigator.permissions.query({name: 'notifications'}).then((val) => {
      resolve({
        state: val.state,
        permission: Notification.permission
      })
    });
  })
}
permissions().then((res) => console.log(res))

Results

Puppeteer Vanilla { "state": "prompt", "permission": "default" }
Puppeteer Stealth { "state": "default", "permission": "default" }
Google Chrome { "state": "prompt", "permission": "default" }
Chromium { "state": "prompt", "permission": "default" }

Conclusion

Here is the evasion: https://github.com/berstend/puppeteer-extra/blob/master/packages/puppeteer-extra-plugin-stealth/evasions/navigator.permissions/index.js

state should be prompt and not default. plugin stealth spoofs wrong value and
creates behavior that seems as far as I know unique to plugin stealth.

platform

Test

navigator.platform

Results

Puppeteer Vanilla "Linux x86_64"
Puppeteer Stealth "Win32"
Google Chrome "Linux x86_64"
Chromium "Linux x86_64"

Conclusion

I get why you set Win32 as default. But it is very easy to detect that navigator.platform is lying,
when the other properties in navigator are not compatible. Why is it not possible to detect the correct
platform on startup of puppeteer stealth?

plugins

Test

Iterate over navigator.plugins and build str repr.

Results

Puppeteer Vanilla "Chromium PDF Plugin" and "Chromium PDF Viewer"
Puppeteer Stealth "Chromium PDF Plugin" and "Chromium PDF Viewer"
Google Chrome "Chrome PDF Plugin" and "Chrome PDF Viewer"
Chromium "Chromium PDF Plugin" and "Chromium PDF Viewer"

Conclusion

If I am not mistaken, at some other evasion you try to hide the fact that plugin stealth
is in fact chromium (instead of Google Chrome). You do not do it here. Could be inconsistent.

resOverflow

Test

let depth = 0;
let errorMessage = '';
let errorName = '';
let errorStacklength = 0;

function iWillBetrayYouWithMyLongName() {
  try {
    depth++;
    iWillBetrayYouWithMyLongName();
  } catch (e) {
    errorMessage = e.message;
    errorName = e.name;
    errorStacklength = e.stack.toString().length;
  }
}

iWillBetrayYouWithMyLongName();
console.log({
  depth: depth,
  errorMessage: errorMessage,
  errorName: errorName,
  errorStacklength: errorStacklength
})

Results

Puppeteer Vanilla { "depth": 10465, "errorMessage": "Maximum call stack size exceeded", "errorName": "RangeError", "errorStacklength": 864 }
Puppeteer Stealth { "depth": 10465, "errorMessage": "Maximum call stack size exceeded", "errorName": "RangeError", "errorStacklength": 864 }
Google Chrome { "depth": 10476, "errorMessage": "Maximum call stack size exceeded", "errorName": "RangeError", "errorStacklength": 864 }
Chromium { "depth": 10474, "errorMessage": "Maximum call stack size exceeded", "errorName": "RangeError", "errorStacklength": 914 }

Conclusion

Values seem to be stable and equivalent for Puppeteer Vanilla and Puppeteer Stealth, but different when puppeteer is not used...Could this be a way to detect pptr usage regardless whether you use stealth or not?

Edit: This particular deviation in call stack size might be due to different browser versions used.

videoCard

Test

Get video card name code.

Results

Puppeteer Vanilla [ "Google Inc.", "ANGLE (Intel Open Source Technology Center, Mesa DRI Intel(R) Ivybridge Mobile , OpenGL 4.2 core)" ]
Puppeteer Stealth [ "Intel Inc.", "Intel Iris OpenGL Engine" ]
Google Chrome [ "Google Inc.", "ANGLE (Intel Open Source Technology Center, Mesa DRI Intel(R) Ivybridge Mobile , OpenGL 4.2 core)" ]
Chromium [ "Google Inc.", "ANGLE (Intel Open Source Technology Center, Mesa DRI Intel(R) Ivybridge Mobile , OpenGL 4.2 core)" ]

Conclusion

Stealth plugin sets static values that never change. This could be an indicator that puppeteer stealth plugin might be used...
Why is this being overwritten? I think vanilla puppeteer is using the correct values?

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions