question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:10 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
Syforcecommented, Jan 29, 2021

I have nothing to add, just want to congratulate @NikolaiT for all his findings. Nice job, dude !

1reaction
prescience-datacommented, Jan 15, 2021

A few thoughts:

  • navigator: @berstend is aware of the navigator issues, it’s a work in progress.
  • permissions: Good catch on that one.
  • resOverflow: This one is interesting, I’m going to look into that - let me know if you test same browser versions as per your edit though.
  • webgl: You are supposed to set your own string here, the default one is just a default.
Read more comments on GitHub >

github_iconTop Results From Across the Web

puppeteer-extra-plugin-stealth - npm
Stealth mode: Applies various techniques to make detection of headless puppeteer harder.. Latest version: 2.11.1, last published: 5 months ...
Read more >
Detecting Headless Chrome: Puppeteer-Extra-Plugin-Stealth
The main techniques that are useful to detect the stealth plugin are: A powerful behavioral detection engine. Advanced IP/session reputation.
Read more >
Chase event stops when approaching stealth area
I thought oh look Yanfly already has a stealth plugin. ... If there is a way to fix this that already exists I...
Read more >
80,443 - Pentesting Web Methodology - HackTricks
Bug bounty tip: sign up for Intigriti, a premium bug bounty platform ... If a CMS is used don't forget to run a...
Read more >
Download not working on Stealth Plugin with used with ...
Basically, removing puppeteer.use(StealthPlugin()) makes the entire code work. However I need Stealth Plugin to avoid being detection as an ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found