15

How I can read WebSocket frames of a web page in a Chrome extension or Firefox add-on, in a way that cannot be detected by the page?

Inspect WebSockets frames from a Chrome Dev Tools extension formulates a similar question, but developing a NPAPI plugin no longer makes sense because it will soon be removed.

Community
  • 1
  • 1

3 Answers3

36

Intercepting the WebSocket data is easy. Simply execute the following script before the page constructs the WebSocket. This snippet monkey-patches the WebSocket constructor: When a new WebSocket constructor is created, the snippet subscribes to the message event, from where you can do whatever you want with the data.

This snippet is designed to be indistinguishable from native code so the modification cannot easily be detected by the page (however, see the remarks at the end of this post).

(function() {
    var OrigWebSocket = window.WebSocket;
    var callWebSocket = OrigWebSocket.apply.bind(OrigWebSocket);
    var wsAddListener = OrigWebSocket.prototype.addEventListener;
    wsAddListener = wsAddListener.call.bind(wsAddListener);
    window.WebSocket = function WebSocket(url, protocols) {
        var ws;
        if (!(this instanceof WebSocket)) {
            // Called without 'new' (browsers will throw an error).
            ws = callWebSocket(this, arguments);
        } else if (arguments.length === 1) {
            ws = new OrigWebSocket(url);
        } else if (arguments.length >= 2) {
            ws = new OrigWebSocket(url, protocols);
        } else { // No arguments (browsers will throw an error)
            ws = new OrigWebSocket();
        }

        wsAddListener(ws, 'message', function(event) {
            // TODO: Do something with event.data (received data) if you wish.
        });
        return ws;
    }.bind();
    window.WebSocket.prototype = OrigWebSocket.prototype;
    window.WebSocket.prototype.constructor = window.WebSocket;

    var wsSend = OrigWebSocket.prototype.send;
    wsSend = wsSend.apply.bind(wsSend);
    OrigWebSocket.prototype.send = function(data) {
        // TODO: Do something with the sent data if you wish.
        return wsSend(this, arguments);
    };
})();

In a Chrome extension, the snippet can be run via a content script with run_at:'document_start', see Insert code into the page context using a content script.

Firefox also supports content scripts, the same logic applies (with contentScriptWhen:'start').

Note: The previous snippet is designed to be indistinguishable from native code when executed before the rest of the page. The only (unusual and fragile) ways to detect these modifications are:

  • Pass invalid parameters to the WebSocket constructor, catch the error and inspecting the implementation-dependent (browser-specific) stack trace. If there is one more stack frame than usual, then the constructor might be tampered (seen from the page's perspective).

  • Serialize the constructor. Unmodified constructors become function WebSocket() { [native code] }, whereas a patched constructor looks like function () { [native code] } (this issue is only present in Chrome; in Firefox, the serialization is identical).

  • Serialize the WebSocket.prototype.send method. Since the function is not bound, serializing it (WebSocket.prototype.send.toString()) reveals the non-native implementation. This could be mitigated by overriding the .toString method of .send, which in turn can be detected by the page by a strict comparison with Function.prototype.toString. If you don't need the sent data, do not override OrigWebSocket.prototype.send.

Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • This method I was using before, but I'm in a situation I should not use that method, since in the game is forbidden to edit a rare changes to JavaScript functions – Walter Chapilliquen - wZVanG Jul 02 '15 at 11:07
  • You should have written that constraint in the question... What makes you think that you're allowed to modify the browser if modifying the JS runtime is forbidden? – Rob W Jul 02 '15 at 11:13
  • I just mean injection to [client page](http://stackoverflow.com/questions/9515704/building-a-chrome-extension-inject-code-in-a-page-using-a-content-script), and not the use of JavaScript – Walter Chapilliquen - wZVanG Jul 02 '15 at 11:20
  • 1
    @wZVanG I've edited the answer and updated the snippet. Use one of the synchronous methods from https://stackoverflow.com/questions/9515704/building-a-chrome-extension-inject-code-in-a-page-using-a-content-script to insert the snippet, and the modification is practically undetectable (however see the notes at the end of my answer). – Rob W Jul 02 '15 at 13:28
  • Is there a firefox version of that synchronus thing? I'm curious :P – Noitidart Jul 02 '15 at 14:43
  • 1
    @Noitidart Method 2 and 3 of [the answer](https://stackoverflow.com/questions/9515704/building-a-chrome-extension-inject-code-in-a-page-using-a-content-script) are synchronous and also usable in Firefox. – Rob W Jul 02 '15 at 14:47
  • Thanks Rob, couldnt using `document-element-inserted` and `loadFrameScript` also work? http://stackoverflow.com/a/31127275/1828637 – Noitidart Jul 02 '15 at 14:51
  • @Noitidart Possibly, but that's not part of the Addon SDK. Internally, the Addon SDK does use these APIs though. – Rob W Jul 02 '15 at 15:03
  • @RobW This is what makes the page when it starts to work, what do you think?: http://pastebin.com/mc7NwrbT – Walter Chapilliquen - wZVanG Jul 02 '15 at 22:31
  • @wZVanG That anticheat snippet won't detect the method from my answer. Make sure that you follow the instructions in my answer to the letter. – Rob W Jul 02 '15 at 22:44
  • I check your notes, but what do you suggest for definitely not detect the injection? If we are in the case of `WebSocket.toString()` or passing invalid arguments – Walter Chapilliquen - wZVanG Jul 02 '15 at 23:34
  • 2
    @wZVanG You could overwrite `Function.prototype.toString` and return a custom value that matches "reality" if the input is the custom constructor. But this in turn can be defeated by creating a new script context (iframe) and using the `Function.protototype.toString` from that context to get a true serialization. It is unlikely that the webpage uses the methods from my notes, because they are sophisticated and the output is non-standard (and could break whenever the browser decides to change the internals). – Rob W Jul 03 '15 at 08:32
  • @RobW, Could you give an example in your code of that? – Walter Chapilliquen - wZVanG Jul 03 '15 at 08:47
  • 1
    @wZVanG No, because it could be bypassed by those who are specifically trying to target the code from my answer as I explained in my previous comment. If you only override the constructor and not `.send`, then you are quite safe, especially if you use Firefox. – Rob W Jul 03 '15 at 08:56
  • the send method I do not want to implement, I just want to use "onmessage" for the results received. In short, there is no method in JavaScript for this browser game to detect this manipulation?, there will be some way you can pass me an example? – Walter Chapilliquen - wZVanG Jul 03 '15 at 09:29
  • 2
    @wZVanG Then the only way to detect the modification is by passing bad parameters to the constructor and inspect the stack trace of the error. This could be countered by catching errors, rewriting the stack trace and then rethrowing the error. But after seeing the pastebin from your website, I don't think that this extra countermeasure is needed. – Rob W Jul 03 '15 at 09:46
  • Hey Rob do you have a nice simple example as of chrome.debugger posted by xan but for Firefox? Im real curious as I dont understand the chrome code, i would learn a lot from the firefox equivalent of it. I would use what I learn from there to keep helping others so I promise it wont be wasted :) – Noitidart Jul 08 '15 at 06:00
  • 1
    @Noitidart I haven't used the debugger protocol in Firefox, but I think that https://github.com/jimblandy/DebuggerDocs does a good job at describing how to use the debugger (and the remote debugging protocol, similar to Chrome's). – Rob W Jul 08 '15 at 06:05
  • Thanks @Rob for super duper fast reply, ok thanks Ill try to figure out from that repo and share learnings. :) – Noitidart Jul 08 '15 at 06:06
  • 1
    This answer will actually remove the constants on the WebSocket. So you can no longer use eg. WebSocket.CLOSED or WebSocket.OPEN. This tripped me up in a library I wrote where I was using these constants but the user of the library was doing something like this. I would recommend adding something like this to your code: for (var key in OrigWebSocket) { if (OrigWebSocket.hasOwnProperty(key)) { window.WebSocket[key] = OrigWebSocket[key]; } }); – Adam Ullman Feb 09 '18 at 05:05
  • @RobW this doesn't work with Firefox any other solution? – aldokkani Mar 28 '20 at 01:09
19

There is an alternative to Rob W's method that completely masks any interaction with the page (for Chrome)

Namely, you can take out some heavy artillery and use chrome.debugger.

Note that using it will stop you from opening Dev Tools for the page in question (or, more precisely, opening the Dev Tools will make it stop working, since only one debugger client can connect). This has been improved since: multiple debuggers can be attached.

This is a pretty low-level API; you'll need to construct your queries using the debugger protocol yourself. Also, the corresponding events are not in the 1.1 documentation, you'll need to look at the development version.

You should be able to receive WebSocket events like those and examine their payloadData:

{"method":"Network.webSocketFrameSent","params":{"requestId":"3080.31","timestamp":18090.353684,"response":{"opcode":1,"mask":true,"payloadData":"Rock it with HTML5 WebSocket"}}}
{"method":"Network.webSocketFrameReceived","params":{"requestId":"3080.31","timestamp":18090.454617,"response":{"opcode":1,"mask":false,"payloadData":"Rock it with HTML5 WebSocket"}}}

This extension sample should provide a starting point.

In fact, here's a starting point, assuming tabId is the tab you're interested in:

chrome.debugger.attach({tabId:tab.id}, "1.1", function() {
  chrome.debugger.sendCommand({tabId:tabId}, "Network.enable");
  chrome.debugger.onEvent.addListener(onEvent);
});

function onEvent(debuggeeId, message, params) {
  if (tabId != debuggeeId.tabId)
    return;

  if (message == "Network.webSocketFrameSent") {
    // do something with params.response.payloadData,
    //   it contains the data SENT
  } else if (message == "Network.webSocketFrameReceived") {
    // do something with params.response.payloadData,
    //   it contains the data RECEIVED
  }
}

I have tested this approach (with the linked sample modified as above) and it works.

Xan
  • 74,770
  • 16
  • 179
  • 206
  • The problem is, those events are not part of the 1.1 protocol. Internally, DevTools use a higher version.. – Xan Jul 04 '15 at 17:50
  • What do you mean with that ?. Would I have a problem? – Walter Chapilliquen - wZVanG Jul 04 '15 at 18:04
  • No, I was just not sure it works. Now I tested it, it actually works. – Xan Jul 04 '15 at 18:05
  • +1 for `chrome.debugger`. This is what I wanted to suggest after the rejection of my initial answer, but the lack of stable documentation plus the demonstrated absence of engineering skills from the OP (no offence intended) deterred me from hinting towards `chrome.debugger`. – Rob W Jul 04 '15 at 18:11
  • @RobW This answer (and yours too!) may help someone else later anyway. – Xan Jul 04 '15 at 18:12
  • If you just read the responses like that, then no, it's undetectable. – Xan Jul 05 '15 at 08:56
  • Well, you've earned it in 7hours. Best regards – Walter Chapilliquen - wZVanG Jul 05 '15 at 09:19
  • 1
    @wZVanG There is a heuristic to detect the debugging session, namely by monitoring window resize events. If the yellow infobar appears at the top of the page, the page's content becomes smaller, which could in theory be detected by the page. In practice, this detection method is farfetched. If you want to, you could enable the `chrome://flags/#silent-debugger-extension-api` flag to prevent the infobar from showing up. – Rob W Jul 05 '15 at 11:13
  • @RobW I thought about it, but infobars do not indicate debugging, they can be legitimate. – Xan Jul 05 '15 at 12:23
  • Yes, I read it in the documentation, it was necessary to have `#silent-debugger-extension-api` and the message does not appear, but that's not all it does, also this option allows you to inspect tabs in our background: `chrome.debugger.attach({extensionId: 'id'}` instead of `chrome.debugger.attach({tabId: 'id'}`. – Walter Chapilliquen - wZVanG Jul 05 '15 at 23:27
  • @Xan Is it possible to delete this question ?, for reasons of concealment, this not has to be seen by the game, That is why I made an effort to ask the question. Thanks – Walter Chapilliquen - wZVanG Jul 08 '15 at 11:54
  • 5
    No. If you do, I will make effort to undelete it. Because the answers are useful for general audience, and Stack Overflow's purpose is not answering _you personally_ but other people as well who might need to know the same thing. – Xan Jul 08 '15 at 12:30
  • A sample extension implementing this: https://github.com/mr-yt12/CDP-Network-Intercepting-Websockets – teg_brightly Dec 22 '21 at 19:34
1

Just to add an exception to @Xan answer (I don't have enough rep to post a comment on his answer so I add it here cause I believe it can save some time to someone else).

That example won't work if the WebSocket connection is established in a context that was loaded via about:, data: and blob: schemes.

See here for the related bugs: Attach debugger to worker from chrome devtools extension

Pablo Meni
  • 87
  • 9
  • Workers are treated as different targets. It's possible to attach debugger to workers like this: https://stackoverflow.com/a/70636184/10364842 – teg_brightly Jan 08 '22 at 20:33