Skip to content

Concolic Execution Bind User Functions

jackfromeast edited this page Aug 5, 2024 · 2 revisions

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.

Example Cases

Case One

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;
  }
})();

Case Two

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.");
  }
})();

Case Three: Proxy

(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.");
  }
})();

Solution

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.

Clone this wiki locally