-
Notifications
You must be signed in to change notification settings - Fork 6
Concolic Execution Bind User Functions
Typically, the call starts with the user-defined instrumented program, which then passes control to our analysis program. The analysis program generally follows these steps: 1/ it strips tainted operands and performs the original operation; 2/ it checks whether taint propagation to the return value is necessary; 3/ it applies the updated taint to the return value and returns it to the user program.
However, we encountered more complex call stacks that need careful handling. During the first step, when executing the original operation (e.g., a built-in function or a binary/unary/get/set operation) on stripped values, it might implicitly call a user-defined function (e.g., toString, getter, or setter). This can lead to several issues if the bind function returns a tainted value: 1/ it might directly apply to the original operation without stripping, making the value taint-aware, or 2/ it could alter the program logic. The following two cases illustrate these problems.
Besides, when we check the value's taint status recursively, it may trigger value's [[Get]] handler if the value has been proxied, and this will cause recursive call as it will bump to user-defined function again and arrive at similiar stage. When, we try to set taint to a value, it may trigger the user-defined [[DefineOwnProperty]] handler. And when we try to remove a taint from a value, it may trigger the user-defined [[Delete]] handler. Here is a list of internal methods that can be redefined by the user.
In this case, an error arises during the binary addition operation between ".js" and taintedValues. When attempting to execute the first step on these values, the JavaScript engine detects the type difference and applies type coercion. This triggers the object's toString function, defined as customToString. However, since the string is tainted, the return value of customToString remains an object. The JavaScript engine expects the return value of the toString function to be of type String, but receives an Object instead, causing an error.
(function() {
// Check if J$$ exists
if (typeof J$$ !== 'undefined' && J$$.wrapTaint) {
let taintedValues = {oa: J$$.wrapTaint("TAINT")};
taintedValues.toString = customToString;
let taintedSrc = taintedValues + ".js";
// Create a new script element
let scriptEle = document.createElement('script');
scriptEle.src = `https://example.com/${taintedSrc}`;
} else {
console.error("J$$ is not defined or does not have wrapTaint method.");
}
function customToString() {
return this.oa;
}
})();
In this case, the issue arises during the filtering operation on an array. The function foo returns J$$.wrapTaint(false) to the Array.filter function. Instead of using the concrete value for filtering, the tainted value is used, resulting in all elements being retained. The correct behavior would be to use the concrete value, which would filter out all elements and return an empty array.
(function() {
if (typeof J$$ !== 'undefined' && J$$.wrapTaint) {
let arr = [1, 2, 3];
let filteredArr = arr.filter(foo);
function foo(val) {
return J$$.wrapTaint(false);
}
if (filteredArr.length == 0) {
let taintedResult = J$$.wrapTaint('tainted');
let scriptEle = document.createElement('script');
scriptEle.src = `https://example.com/${taintedResult}`;
}
} else {
console.error("J$$ is not defined or does not have wrapTaint method.");
}
})();
(function() {
if (typeof J$$ !== 'undefined' && J$$.wrapTaint) {
let taintedValue = { a: J$$.wrapTaint('tainted') };
const handler = {
get(target, prop, receiver) {
return Reflect.get(...arguments);
},
};
let proxy = new Proxy(taintedValue, handler);
let taintedResult = proxy.a;
let scriptEle = document.createElement('script');
scriptEle.src = `https://example.com/${taintedResult}`;
} else {
console.error("J$$ is not defined or does not have wrapTaint method.");
}
})();
The current solution aims to handle all implicit user function calls properly to avoid unexpected behavior from user-defined functions.
Binary/Unary Operation
Before performing the first step, check if type coercion is needed and perform it before passing the value to the first step. The return value of the user-defined function will overwrite the passed operand.
getField Operation
Ensure that any implicit calls to user-defined functions during field access are properly managed.
invokeFun Operation
All built-in functions should be called through RuleBuilder.runOriginFunc. and we will handle specific functions explicitly, especially when the argument is a function pointer:
- Array.filter(f):
- Wrap the function f with a wrapper to concretize its return value before running the original built-in function.
![]()
- Related Works
- HTML Injection
- DOM Clobbering
- Evaluation
- Discussion
- Others