Javascript逆向之动态分析

Published: 2023年06月06日

In Reverse.

虽然现在是移动互联网时代,浏览器还是扮演着重要角色,甚至还有混合应用直接嵌它,所以对它做安全分析是有必要的,这里是关注JS,主要是为了分析它的加密参数是干嘛的,是否是用于用户追踪或其它恶意行为,本文就记录点动态分析的笔记~

手动分析

关键点定位

控制台面板

搜索Ctrl + FCtrl + Shift + F

源代码/来源标签下,可以设置多种类型的断点:

  1. xhr/fetch:可设置所有或根据url过滤的断点
  2. 事件:可设置所有或根据事件名过滤的断点,如鼠标/触控/网络/键盘等各种事件
  3. 日志断点/条件断点:找到对应源代码,又键设置。这里的日志断点是会作为console.log参数拼起来,条件断点会拼接&& debugger,也就是说他们都可以是复杂的表达式!

它还有个snippets/代码段面板,能持久存储代码段并在所有页面可访问,非常适合将一些常用代码保存下来,需要用时直接运行。

元素标签下,可找到元素右键设置DOM事件的断点。当然它也可以去找各种元素上绑定的事件和对应的JS代码。

网络标签里面,选择请求后initiator可看到该请求的调用栈。保留日志和停用缓存也很常用

hook点

有一些访问操作是很敏感的,去hook它们可快速定位关键点:

// 敏感属性
window attr

// 存储
localStorage.(setItem|getItem), document.cookie

// 日期时间
Date.now, new Date().getTime, performance.now

// 编解码
JSON.(stringify|parse), atob, btoa

// 随机数
Math.random, crypto.getRandomValues

// 请求
XMLHttpRequest, fetch, WebSocket

// 动态代码
eval, Function

// WASM
WebAssembly.compile, WebAssembly.instantiate, WebAssembly.instantiateStreaming, WebAssembly.compileStreaming

还有一种思路,就是指纹收集的代码通常会很集中,指纹收集使用的一些api不咋常用,所以可以先去hook一些指纹收集会用到的api,当它被调用时再判断这是业务目的还是指纹目的,如果是指纹目的那么调用栈上几层可能就是关键点咯!

反调试

devtool检测

有可能会禁用F12/Ctrl+Shift+I,换个方式,比如鼠标右键打开就行。更多是检测有没有打开,检测到可能会执行虚假代码,devtools-detector记录了多种检测方式,下面简单说明原理:

  1. toStringx2:老版本chrome dev tool实现方式中,console.log输出对象时调用两次toString,因此可以通过hook它来检测。
  2. elementId getter:log输出到控制台后,会调用id的getter,现在也不顶用了。
  3. 基于时间差异:console.logconsole.table显示数据是将数据绘制到dev tool这个DOM里,前者只绘制简单形式,后者绘制复杂形式需要创建大量DOM元素,因此比较两者时间差异可检测。

  4. 特性:比如eruda控制台会有全局的erudaeruda._devTools._isShow

debugger暂停

部分js引擎支持debugger语句,它的作用是在调试模式下中断(非调试模式不起任何作用相当于nop),因此可能会使用它做反调(循环执行它影响调试),绕过的方法是在该语句上设置为永不中断(Never pause here)或条件断点为永假。不过还有很多动态生成的(通常利用evalFunctionSetInterval ),可通过hook定时器(或通过调用栈找到关键点后hook为undefined)、修改浏览器源码来实现(如改字节码生成器),也可以试试SkipJSDebugger,它的思路是自己实现CDP Client自动去恢复断点🤦。

不过debugger本身还是很有用的,比如hook时通过debugger指令加断点便于调试!

格式化检测

JS要检测格式化/修改就是通过Function.prototype.toString,然后去做匹配(如哈希/正则等)

HOOK检测

hook都是做替换,根据怎么实现替换和替换为什么可有如下形式:

// 直接替换方法 重写全局函数或对象方法
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {  // 这里也可以直接替换XMLHttpRequest.send
    console.log("发送请求:", body);
    return originalSend.apply(this, arguments);
};
// 替换属性访问
let _value = 100;
Object.defineProperty(obj, 'prop', {
    get() {
        console.log(`读取prop: ${_value}`);
        return _value;
    },
    set(newVal) {
        console.log(`设置prop: ${newVal}`);
        _value = newVal;
    }
});
// 替换为代理
const proxy = new Proxy(target, {
    get(target, prop) {
        console.log(`读取属性: ${prop}`);
        return Reflect.get(target, prop);
    },
    set(target, prop, value) {
        console.log(`设置属性: ${prop}=${value}`);
        return Reflect.set(target, prop, value);
    }
});

这里面可以通过改原型去hook所有子类,不多说。这里主要提下proxy,它能代理函数和对象,能拦截13种操作,详见Proxy and Reflect

而对hook的检测,基本分为三类:

  1. toString:如果直接替换了函数,那么toString会露馅,proxy时也存在细微差异
  2. prototype:如果用proxy,原型可能露馅
  3. 异常:用proxy时在异常情况下,如原型出现循环也会表现出行为差异[8]。

对此,可以去hook关键函数,如toString去看谁调用了它,针对性对抗。在对抗时,可使用油猴脚本或自定义chrome扩展在网页加载前注入我们的脚本,注入后记得删掉防止被检测。

调用栈检测

这更多出现在指纹收集中,就是用下面的方法去看调用栈是否存在异常

const callstack = new Error().stack.split("\n");

补环境

其实不像分析更像是莽!就是把关键代码抠出来放到node等里执行,于是要做:

  1. 补充浏览器的API(DOM/BOM等),当然了,是按需补充,缺啥补啥
  2. 隐藏node里的API

改代码

控制台无法直接改代码,一般是本地修改,例如burpsuite去做替换,不过更方便的是用chrome插件,如Requestly/ Netify/ v_jstools 等...这也能对抗js动态变化的场景...

随机值固定

随机值会导致现象不稳定,比如每次运行加密结果不同,增加调试难度,可以将他们固定下来,如:

Math.random = function() {
  console.log("Hook Math.random: 返回固定值");
  // debugger; // 触发断点,便于跟踪调用栈
  return 0.5;
};

window.crypto.getRandomValues = function(array) {
  console.log("Hook crypto.getRandomValues: 填充固定值到数组");
  // debugger; // 触发断点

  for (let i = 0; i < array.length; i++) {
    array[i] = 0;
  }

  return array;
};

时间修改

这里主要是修改它的执行时间等,便于调试:

const _setTimeout = window.setTimeout;
window.setTimeout = function(cb, delay) {
  console.log(`[Hook] setTimeout: delay=${delay}ms`);
  if (delay < 100) delay = 1000; // 强制最小延迟
  return _setTimeout(() => {
    console.log("[Hook] 执行回调");
    cb(); // 可插入debugger
  }, delay);
};

window.setInterval = function(cb, interval) {
  console.log(`[Hook] setInterval: interval=${interval}ms`);
  const proxyCB = () => {
    console.log(`[Hook] 周期执行计数`);
    cb();
  };
  return _setInterval(proxyCB, interval);
};

自动化监视-插桩

运行时插桩(runtime instrumentation)

就是解释器,改IR到机器码的翻译过程,这里以VV8为例[4],它通过修改V8,只用少量的代码就实现对浏览器API的追踪。

V8 JS代码处理流程

它分两个版本,老版本是没有字节码的,咱只关注于当前的版本,如下所示[5]:

Image

简单来说就是由Parser将源码转换为AST,再由Ignition将AST转换为ByteCode,Ignition是个解释器它能直接执行字节码,并且执行字节码时会记录额外信息,这些信息和原始字节码可以再输入到Turbofan里去生成Machine Code,Turbofan就是JIT了,若它假设的失效则回退回到Ignition,而VV8的作者就是Ignition里搞的事情。

VV8 插桩系统

如下是VV8的架构图:

Image

它通过修改修改Ignition来实现如下功能:

  1. 属性访问插桩
  2. 内建函数调用插桩
  3. 反射API调用插桩

下面详细说明

属性访问插桩

BytecodeGenerator的作用是将AST转为字节码,作者就在这里通过注入字节码来trace读写的,具体来讲它在会通过VisitCountOperation(++/--)/VisitCompoundAssignment(+=/-=)/VisitAssignment(=)去为属性赋值,VisitPropertyLoad去读取属性,它就在原始字节码执行前,插入自己的log函数:

diff --git a/src/interpreter/bytecode-generator.cc b/src/interpreter/bytecode-generator.cc
index 0464ba548d..d1b65fb6b4 100644
--- a/src/interpreter/bytecode-generator.cc
+++ b/src/interpreter/bytecode-generator.cc
@@ -4489,6 +4537,18 @@ void BytecodeGenerator::VisitPropertyLoad(Register obj, Property* property) {
     case NON_PROPERTY:
       UNREACHABLE();
     case NAMED_PROPERTY: {     // 对于obj.prop的读访问。 case KEYED_PROPERTY是obj['prop']
+      // VisibleV8: generate code to trace named property loads
+      {
+        RegisterList trace_args = register_allocator()->NewRegisterList(3);        // 分配3个寄存器存储参数
+        builder()->    // builder是字节码生成器的构建器,它能正确选择最合适的字节码
+          LoadLiteral(Smi::FromInt(property->position())).  // 将位置放到累加器里
+          StoreAccumulatorInRegister(trace_args[0]).  // 将累加器值存到参数1
+          MoveRegister(obj, trace_args[1]).    // 将对象存到参数2
+          LoadLiteral(property->key()->AsLiteral()->AsRawPropertyName()).      // 获取属性名到累加器
+          StoreAccumulatorInRegister(trace_args[2]).   // 将属性名存到参数3
+          CallRuntime(Runtime::kTracePropertyLoad, trace_args); //args: (call-site, this, key) 调用插入的log函数
+      }
+

简单说下V8的字节码,它是基于虚拟寄存器的,这些寄存器会保存在栈上,有一个特殊的累加器作为隐含寄存器用于减少字节码长度,全体字节码可见bytecode ,详细信息可见[6].

函数调用

它通过修改如下四个api,去追踪函数调用:

  • HandleApiCallHelper - 函数调用
  • CreateDynamicFunction - 追踪new Function()
  • GlobalEval/ResolvePossiblyDirectEval - 追踪 eval()
反射调用

通过修改ReflectGetReflectSet去追踪反射调用:

diff --git a/src/builtins/reflect.tq b/src/builtins/reflect.tq
index 95d83b5ead..0292b35287 100644
--- a/src/builtins/reflect.tq
+++ b/src/builtins/reflect.tq
@@ -58,6 +58,9 @@ namespace reflect {
   extern transitioning builtin GetPropertyWithReceiver(
       implicit context: Context)(JSAny, Name, JSAny, Smi): JSAny;

+  // VisibleV8: defining external trace-property-load runtime function
+  extern transitioning runtime TracePropertyLoad(implicit context: Context)(Smi, JSAny, JSAny);
+
   // ES6 section 26.1.6 Reflect.get
   transitioning javascript builtin
   ReflectGet(js-implicit context: NativeContext)(...arguments): JSAny {
@@ -68,6 +71,10 @@ namespace reflect {
     const propertyKey: JSAny = length > 1 ? arguments[1] : Undefined;
     const name: AnyName = ToName(propertyKey);
     const receiver: JSAny = length > 2 ? arguments[2] : objectJSReceiver;
+
+    // VisibleV8: call-out to property-load tracer runtime function
+    TracePropertyLoad(-1, object, propertyKey);
+
     return GetPropertyWithReceiver(
         objectJSReceiver, name, receiver, SmiConstant(kReturnUndefined));
   }

VV8日志格式说明

它的原始日志是纯文本形式,每行一条,有专门的格式可解析,如:

~0x2e54001cc000     
@?
!?
$5:"https\://g.alicdn.com/??mtb/lib-promise/3.1.3/polyfillB.js,mtb/lib-windvane/3.0.7/windvane.js":!function a(b,c,d){function e(g,h){i....
c-1:%Save:{158894,Window}:"Object":%safe
g212098:{249011,Window}:"navigator"
s75:{158894,Window}:"enableWwSsoPluginLogin":#T

下面是完整的格式说明:

前缀 完整格式 记录类型 含义
~ ~<isolate_pointer> Isolate发生变化 isolate改变后,这里显示新的isolate指针
@ @<origin_info> Origin发生变化 Origin改变后,显示新的
$ $<script_id>:<parent_script_id>:<source_code> 新脚本定义 新脚本出现,会分配一个脚本id,若是eval等会写上父脚本id,再接上完整源码
! !<script_id> 脚本执行 脚本切换时,指明新脚本id
c c<call_site>:<function_name>:<receiver>:<arguments> API调用 api调用时,显示调用点,它是最近脚本切换时的新脚本的位置
g g<call_site>:<receiver>:<property_key> 属性读 属性读
s s<call_site>:<receiver>:<property_key>:<value> 属性写 属性写,能看到新值

还有个点,上面会记录很多其它日志,作者在日志记录器里写了过滤,只关心浏览器相关的操作:

+// Predicate to see if an object (for property load/store) is worth logging
+static bool visv8_should_log_object(Object obj) {
+  if (obj.IsPrimitive()) {
+    // Never log accesses on primitive values
+    return false;
+  }
+
+  HeapObject hobj = HeapObject::cast(obj);
+  auto itype = hobj.map().instance_type();
+ // JS_GLOBAL_OBJECT_TYPE 是全局对象,即window下的,如navigator
+ // JS_GLOBAL_PROXY_TYPE 是全局对象的代理
+ // JS_API_OBJECT_TYPE 是V8扩展API创建的
+ // JS_SPECIAL_API_OBJECT_TYPE 是特需的
+  return ((itype == JS_GLOBAL_OBJECT_TYPE) || (itype == JS_GLOBAL_PROXY_TYPE) ||
+          (itype == JS_SPECIAL_API_OBJECT_TYPE) ||
+          (itype == JS_API_OBJECT_TYPE));
+}
+

VV8总结

它的项目结构很清晰,对chrome的修改也是单独的patch文件便于分析,该项目通过对chrome的少量修改就实现了很好的插桩效果,可移植性高,且能一定程度避免被感知。当前它通过字节码插桩的方式无法捕获wasm的操作,但是可能影响不大。该项目的目的只是为了识别JS使用了哪些浏览器API,缺乏一些必要数据的记录,比如属性读操作的结果,函数调用的结果(issue#3: Return Value wanted),若有必要可优化。

其实它蛮适合用于自动化补环境的~

源代码插桩(source code instrumentation)

直接使用esprima/acorn/babel等去改代码,这里得祭出Jalangi2,它就是一个JS的源码级插桩框架,它本身就带很多常用的追踪插件,并提供较全面的API便于我们编写自己的插桩插件。

初体验

这个项目有一个优秀的demo网页,可以直接在它上面查看demo,包括原始代码(Target Code)、插桩后代码(Transformed Code)、分析代码(Analysis Code)、运行时库(Runtime Lib)和插桩执行结果:

从上图能了解它的原理,它通过acorn解析原始代码,将关注的操作转换为对相应运行时库的调用,再用esotope写回插桩后代码。如上图的var a = 1;转换为var a = J$.W(8, 'a', J$.T(0, 1, 22, false), a, false, true);,而在运行时库里,它再调用由我们编写的相关的回调,即可完成相应的插桩,还是以这里的J$.W为例,它在运行时库里的定义如下:

// variable write
function W(iid, name, val, lhs, flags) {
    var bFlags = decodeBitPattern(flags, 3); // 解码标志位 [isGlobal, isScriptLocal, isDeclaration]
    var aret;
    if (sandbox.analysis && sandbox.analysis.write) {   // 如分析插件定义了write就调用它
        aret = sandbox.analysis.write(iid, name, val, lhs, bFlags[0], bFlags[1]);
        if (aret) { // 如果write有返回值,就用新的返回值覆盖原始值
            val = aret.result;
        }
    }
    if (!bFlags[2]) {   // 执行真正的写操作
        return (lastComputedValue = val);
    } else {
        lastComputedValue = undefined;
        return val;
    }
}

注意:该项目有jalangi和jalangi2两个版本,本文只关注后者,似乎文档是有些混乱的,可能里面有些代码是针对旧版的吧,一切以源码为准~

运行时介绍

上面对原理有个大概的了解了,现在来看看它的封装代码和对应的分析函数,下面是从代码和文档分析获取的,可能并不完整(代码有一点点乱...):

运行时封装代码 分析函数 说明
J$.F invokeFunPre(iid, f, base, args, isConstructor, isMethod, functionIid, functionSid):{Object|undefined} 在函数、方法或构造函数调用之前触发,用于监控和修改函数调用参数
invokeFun(iid, f, base, args, result, isConstructor, isMethod, functionIid, functionSid):{Object|undefined} 在函数、方法或构造函数调用之后触发,用于监控和修改函数返回值
J$.T literal(iid, val, hasGetterSetter):{Object|undefined} 在字面量创建后触发,包括函数字面量、对象字面量、数组字面量、数字、字符串、布尔值等
J$.H forinObject(iid, val):{Object|undefined} 在for-in循环中迭代对象属性时触发,用于监控被迭代的对象
J$.N declare(iid, name, val, isArgument, argumentIndex, isCatchParam):{Object|undefined} 在作用域开始时为每个局部变量、形参、函数声明、arguments变量触发
J$.G getFieldPre(iid, base, offset, isComputed, isOpAssign, isMethodCall):{Object|undefined} 在访问对象属性之前触发,用于监控和修改属性访问操作
getField(iid, base, offset, val, isComputed, isOpAssign, isMethodCall):{Object|undefined} 在访问对象属性之后触发,用于监控和修改属性访问的结果值
J$.P putFieldPre(iid, base, offset, val, isComputed, isOpAssign):{Object|undefined} 在设置对象属性之前触发,用于监控和修改属性设置操作
putField(iid, base, offset, val, isComputed, isOpAssign):{Object|undefined} 在设置对象属性之后触发,用于监控和修改属性设置的结果
J$.R read(iid, name, val, isGlobal, isScriptLocal):{Object|undefined} 在变量读取后触发,用于监控和修改变量读取的结果值
J$.W write(iid, name, val, lhs, isGlobal, isScriptLocal):{Object|undefined} 在变量写入前触发,用于监控和修改要写入变量的值
J$.Rt _return(iid, val):{Object|undefined} 在使用return关键字返回值之前触发,用于监控和修改返回值
J$.Th _throw(iid, val):{Object|undefined} 在使用throw关键字抛出值之前触发,用于监控和修改抛出的值
J$.Wi _with(iid, val):{Object|undefined} 在执行with语句时触发,用于监控和修改with语句的参数
J$.Fe functionEnter(iid, f, dis, args):{undefined} 在函数体开始执行前触发,用于监控函数执行的开始
J$.Fr functionExit(iid, returnVal, wrappedExceptionVal):{Object|undefined} 在函数体执行完成时触发,用于监控函数执行的结束和处理返回值/异常
J$.Se scriptEnter(iid, instrumentedFileName, originalFileName):{undefined} 在JavaScript文件开始执行前触发,用于监控脚本执行的开始
J$.Sr scriptExit(iid, wrappedExceptionVal):{Object|undefined} 在JavaScript文件执行完成时触发,用于监控脚本执行的结束
J$.B binaryPre(iid, op, left, right, isOpAssign, isSwitchCaseComparison, isComputed):{Object|undefined} 在二元操作之前触发,包括+、-、*、/、%、&、|、^等操作
binary(iid, op, left, right, result, isOpAssign, isSwitchCaseComparison, isComputed):{Object|undefined} 在二元操作之后触发,用于监控和修改二元操作的结果
J$.U unaryPre(iid, op, left):{Object|undefined} 在一元操作之前触发,包括+、-、~、!、typeof、void等操作
unary(iid, op, left, result):{Object|undefined} 在一元操作之后触发,用于监控和修改一元操作的结果
J$.C conditional(iid, result):{Object|undefined} 在条件检查后分支之前触发,用于监控和修改条件表达式的结果
J$.S instrumentCodePre(iid, code, isDirect):{Object|undefined} 在传递给eval或Function的字符串被插桩之前触发
instrumentCode(iid, newCode, newAst, isDirect):{Object|undefined} 在传递给eval或Function的字符串被插桩之后触发
runInstrumentedFunctionBody(iid, f, functionIid, functionSid):{boolean} 仅在J$.Config.ENABLE_SAMPLING = true时触发,决定是否执行插桩的函数体
J$.X1 endExpression(iid):{undefined} 在表达式求值完成且其值被丢弃时触发,如表达式语句完成执行时
J$.endExecution endExecution():{undefined} 在node.js中执行终止时触发,或在浏览器中按Alt-Shift-T时触发
- onReady(cb):{undefined} 用于在node.js中进行异步初始化,初始化完成后调用回调开始执行

想对哪类操作插桩就编写对应的分析函数即可,这里对一些符号再做解释:

  1. J$:它是插桩框架的容器,含所有所需工具与数据,在分析插件里它也叫sandbox(下面可能混用,它们是一个东西),我们的分析函数是直接定义在它上面的,它也提供额外的信息公分析函数使用。
  2. iid:当前脚本的插桩ID,在插桩时就静态的为每个插桩点分配了一个独一无二的id,iid分三类,condIid/memIid/opIid是单独编号的(但互不重复),除了iid外还有个sid,它是脚本id,在分析函数中隐含着,可用sandbox.sid获取当前调用处的sid,并通过J$.iidToLocation(sid, iid)去获取它在原始文件中的位置(依赖于J$.smap,需要启用--inlineIID参数哦,另一个--inlineSource是再把原始代码也塞进去的,建议都加上)
  3. SMemory:尽管没有直接出现,但还是挺重要(估计在第一版中更重要,从论文中看它占了很大比重介绍,要做混合执行也严重依赖它),它用于存储分析数据,相关API可见SMemory.html

项目结构

下面是项目的tree,它很大所以忽略了部分文件,并为剩下的关键文件添加了注释:

├── docs  # 各种文档 有可能并不是最新的
├── scripts
│   ├── proxy.py    # mitmproxy实时插桩分析
│   ├── sj.py       # 跨平台node操作封装
├── src
│   ├── java
│   ├── js
│   │   ├── commands
│   │   │   ├── curl.js
│   │   │   ├── direct.js   # 对于已经插桩的代码,直接运行分析插件
│   │   │   ├── esnstrument_cli.js  # 对单个文件插桩
│   │   │   ├── instrument.js   # 对目录/多个文件插桩
│   │   │   ├── jalangi.js  # 运行插桩+分析
│   │   ├── Config.js               # 配置对哪些部分插桩,这是JS,可以做精细化控制
│   │   ├── instrument
│   │   │   ├── astUtil.js
│   │   │   ├── esnstrument.js  # 插桩的核心逻辑
│   │   │   └── instUtil.js
│   │   ├── runtime
│   │   │   ├── analysis.js         # 分析代码,它会调用我们编写的分析插件
│   │   │   ├── analysisCallbackTemplate.js # 模板代码,可以基于它写分析插件
│   │   │   ├── iidToLocation.js    # id转源码位置
│   │   │   └── SMemory.js  # 影子内存
│   │   ├── sample_analyses
│   │   │   ├── ChainedAnalyses.js              # 链式分析,来支持同时使用多个分析插件 [PS:感觉怪怪的,像是临时打了个不定来支持,似乎设计时就该有这种能力?]
│   │   │   ├── coverage                        # 记录代码覆盖率的,可用于追踪分支/脚本执行情况
│   │   │   │   ├── AddCoverage.js
│   │   │   │   ├── mocha_prefix.js
│   │   │   │   ├── ProcessCoverage.js
│   │   │   │   ├── qunit_prefix.js
│   │   │   │   ├── SpecialCoverage.js
│   │   │   │   └── tests
│   │   │   │       ├── FeaturesMain.js
│   │   │   │       ├── FeaturesTest.js
│   │   │   │       └── SetTest.js
│   │   │   ├── datatraces                      # 这里的样例是用于做trace的,比如属性读些访问的,很有用!
│   │   │   │   ├── LogData.js
│   │   │   │   └── TraceWriter.js
│   │   │   ├── pldi16
│   │   │   │   ├── BackTrackLoop.js
│   │   │   │   ├── BranchCoverage.js
│   │   │   │   ├── ChangeSematicsOfMult.js
│   │   │   │   ├── CheckUndefinedConcatenatedToString.js
│   │   │   │   ├── CountObjectsPerAllocationSite.js
│   │   │   │   ├── LogLoadStore.js
│   │   │   │   ├── LogLoadStoreAlloc.js
│   │   │   │   ├── SkipFunction.js
│   │   │   │   └── TraceAll.js     # 追踪所有插桩点,打log
│   │   ├── template.js
│   │   └── utils
│   │       ├── api.js  # 便node等编程使用,去插桩和分析
│   └── python
│       └── datatraces.py
└── tests

再简单说下:

  1. commands目录下有很多命令行工具,分别用于各种场合的插桩与分析,注意插桩和分析是分开的,本身插完桩后不会有任何实际功能,需要我们自己写分析代码。
  2. scripts下存放了一些作者写的脚本,重点关注proxy.py,分析web时用它很方便,而且该项目也对web做了些额外的功能,能直接控制台输出分析结果。
  3. sample_analyses目录下有很多分析脚本,比如数据追踪、控制流追踪的,一方面学习用,另一方面可以直接拿去做分析。

附:ast-hook-for-js-RE 也是类似的,但它的目的更明确,就是去记录所有的赋值操作,于是后续想分析一个值的来源,从数据库里查就能定位到js代码的位置,它的通过拦截HTML/JS响应,利用Babel对其做插桩后返回给浏览器,插桩时它关注如下4类节点:

Hook Type 描述 代码模式 AST 节点类型
变量声明 在声明时捕获初始值 var/let x = value; VariableDeclaration
赋值表达式 赋值时捕获 x = value; AssignmentExpression
对象属性 在定义对象属性时捕获值 {key: value} ObjectExpression
函数参数 调用函数时捕获参数值 function(param) FunctionDeclaration

通过自己实现的cc11001100_hook函数去替换原始节点,该函数额外做的事就是记录所需的所有信息,如变量/对象域名,值,调用位置和时间等。

元循环解释器(Meta-Circular Interpreter)

所谓元,是个很玄的东西,就是代码本身,其实这里就是要单独把photon-js拎出来[2],它是用javascript实现一个javascript解释器,利用底层解释器的对象表示来模拟javascript程序状态,将javascript程序中的原生调用委托给底层解释器,通过修改元循环解释器的行为来实现动态分析,如下图:

这个项目有点老了,它也只能处理方法操作和函数调用,提出来是它的想法很有趣,它通过动态优化插桩的JS代码来实现高效的插桩,比如我们可能会使用apply/call去做通用的调用,而它会直接重写为丑丑的原始调用,来提升性能!

参考

[0] FV8: A Forced Execution JavaScript Engine for Detecting Evasive Techniques -- Nikolaos Pantelaios, Alexandros Kapravelos

[1] A Survey of Dynamic Analysis and Test Generation for JavaScript -- Esben Andreasen, Liang Gong et al

[2] Portable and Efficient Run-time Monitoring of JavaScript Applications using Virtual Machine Layering -- Erick Lavoie, Bruno Dufour, and Marc Feeley

[3] Jalangi: A Selective Record-Replay and Dynamic Analysis Framework for JavaScript -- Koushik Sen, Swaroop Kalasapur, Tasneem G. Brutch, and Simon Gibbs pdf

[4] VisibleV8: In-browser Monitoring of JavaScript in the Wild (VisibleV8 code) -- Nikolaos Pantelaios, Alexandros Kapravelos

[5] JavaScript 引擎 V8 执行流程概述 -- 赖勇高

[6] Ignition: V8 Interpreter

[7] 一种基于动态插桩的 JavaScript 反事实执行方法 -- 龚伟刚,游伟,李赞,石文昌,梁彬

[8] my-fingerprint:issue 42 关于 hook 的反检测 -- Hosinoharu