Inspecting WebSocket frames in an undetectable way
Asked Answered
M

3

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.

Mythology answered 2/7, 2015 at 10:6 Comment(5)
Just hook up the WebSocket constructor and register a message listener. That will always work, regardless of the browser.Rafaello
@RobW, Thanks for your comment. WebSocket constructor is created by the page, if I create it, I do not obtain resultsMythology
You can monkey-patch the constructor.Rafaello
@RobW What do you mean by that? Inject something in the WebSocket function?Mythology
Your bounty does not change the fact that your question is very poorly specified. It's not made clear why the current answer does not satisfy your needs, and that way Rob will probably gain the (deserved) half bounty.Downswing
D
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.

Downswing answered 4/7, 2015 at 17:26 Comment(13)
The problem is, those events are not part of the 1.1 protocol. Internally, DevTools use a higher version..Downswing
What do you mean with that ?. Would I have a problem?Mythology
No, I was just not sure it works. Now I tested it, it actually works.Downswing
+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.Rafaello
@RobW This answer (and yours too!) may help someone else later anyway.Downswing
If you just read the responses like that, then no, it's undetectable.Downswing
Well, you've earned it in 7hours. Best regardsMythology
@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.Rafaello
@RobW I thought about it, but infobars do not indicate debugging, they can be legitimate.Downswing
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'}.Mythology
@Downswing 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. ThanksMythology
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.Downswing
A sample extension implementing this: github.com/mr-yt12/CDP-Network-Intercepting-WebsocketsTeacup
R
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.

Rafaello answered 2/7, 2015 at 10:56 Comment(21)
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 functionsMythology
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?Rafaello
I just mean injection to client page, and not the use of JavaScriptMythology
@wZVanG I've edited the answer and updated the snippet. Use one of the synchronous methods from #9516204 to insert the snippet, and the modification is practically undetectable (however see the notes at the end of my answer).Rafaello
Is there a firefox version of that synchronus thing? I'm curious :PPhilomena
@Philomena Method 2 and 3 of the answer are synchronous and also usable in Firefox.Rafaello
Thanks Rob, couldnt using document-element-inserted and loadFrameScript also work? https://mcmap.net/q/821625/-using-loadframescript-how-can-inject-file-before-the-page-39-s-script-executionPhilomena
@Philomena Possibly, but that's not part of the Addon SDK. Internally, the Addon SDK does use these APIs though.Rafaello
@RobW This is what makes the page when it starts to work, what do you think?: pastebin.com/mc7NwrbTMythology
@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.Rafaello
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 argumentsMythology
@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).Rafaello
@RobW, Could you give an example in your code of that?Mythology
@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.Rafaello
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?Mythology
@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.Rafaello
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 :)Philomena
@Philomena I haven't used the debugger protocol in Firefox, but I think that github.com/jimblandy/DebuggerDocs does a good job at describing how to use the debugger (and the remote debugging protocol, similar to Chrome's).Rafaello
Thanks @Rob for super duper fast reply, ok thanks Ill try to figure out from that repo and share learnings. :)Philomena
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]; } });Allwein
@RobW this doesn't work with Firefox any other solution?Dannadannel
D
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.

Downswing answered 4/7, 2015 at 17:26 Comment(13)
The problem is, those events are not part of the 1.1 protocol. Internally, DevTools use a higher version..Downswing
What do you mean with that ?. Would I have a problem?Mythology
No, I was just not sure it works. Now I tested it, it actually works.Downswing
+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.Rafaello
@RobW This answer (and yours too!) may help someone else later anyway.Downswing
If you just read the responses like that, then no, it's undetectable.Downswing
Well, you've earned it in 7hours. Best regardsMythology
@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.Rafaello
@RobW I thought about it, but infobars do not indicate debugging, they can be legitimate.Downswing
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'}.Mythology
@Downswing 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. ThanksMythology
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.Downswing
A sample extension implementing this: github.com/mr-yt12/CDP-Network-Intercepting-WebsocketsTeacup
P
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

Padauk answered 24/3, 2021 at 8:59 Comment(1)
Workers are treated as different targets. It's possible to attach debugger to workers like this: https://mcmap.net/q/821626/-attach-debugger-to-worker-from-chrome-devtools-extensionTeacup

© 2022 - 2024 — McMap. All rights reserved.