吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 7|回复: 0
收起左侧

[Web逆向] funcaptcha类ob逆向解混淆处理

[复制链接]
buluo533 发表于 2026-2-14 12:50
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责

demo 站点:aHR0cHM6Ly9zcGlkZXJhcGkuY24vY2FwdGNoYS9mdW5jYXB0Y2hhLw==
推荐验证码训练文章:aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JvZW5kZWlkL2FydGljbGUvZGV0YWlscy8xNTY2MDY4Njk/c3BtPTEwMDEuMjAxNC4zMDAxLjU1MDE=

本文主要做 ast 解混淆讲解,为开发通用 ob 解混淆器做准备,只涉及一些基础的逆向,方便定位分析

一、基础逆向
     

func1

func1

        

func2

func2

          这是一个验证码的盾,所以第一步我们得先看看他验证码图片的来源。最简单的方式就是加载出来后刷新图片,主动让他加载
            

func3

func3

           可以看到加载的就是这一个接口https://client-api.arkoselabs.com/fc/gfct/ ,这个就是验证码的来源
         

func4

func4

          他有一个参数,刷新几次发现是动态更新的,这里可以复制 curl 本地去测试,直接搜搜先看看这个是怎么来的,是服务器返回或者是其他情况
        

func5

func5

         

func6

func6

         能看出来这是另一个接口的返回值,https://client-api.arkoselabs.com/fc/gt2/public_key/3EE79F8D-13A6-474B-9278-448EA19F79B3
       接下来就是分析这个接口的加密参数 bda,解混淆也是用于这个文件,跟栈很简单,加密逻辑在enforcement.cdeb82f474225dff1677448c6bc82e87.js 的文件中
      

func7

func7

二、代码预处理
       将代码取下来,观察整体的结构,整体就是一个类 ob,主要是多组特征
         

ob1

ob1

           

ob2

ob2

          会发现有很多这样先赋值,再去用变量名调用函数,这个会影响到我们后面解密函数的调用,所以我们需要在这里先去处理掉这个,这里的核心就是 scope 和 binding。首先理解这两个的一个概念。             Scope (作用域): 代表代码的一个区域(如函数、块级作用域),它负责收集该区域内定义的所有变量。
            Binding (绑定): 代表一个特定变量的“声明”与该变量在作用域内所有“引用”之间的连接。
        简单来说:              Scope 是容器,装着所有的 BindingBinding 记录了变量在哪里被定义,以及在哪里被使用。        
               假设有代码:let a = 1; a = 2; console.log(a);,对于变量 a 的 Binding 对象:
                binding.path: 变量声明的 AST 路径(指向 let a = 1)。
                binding.referenced: truefalse。变量是否被使用过?
                binding.referencePaths: 一个数组,包含所有使用该变量的 AST 路径(指向 console.log(a) 中的 a)。
                binding.constant: truefalse。变量是否从未被重新赋值?
                binding.constantViolations: 一个数组,包含所有修改该变量的 AST 路径(指向 a = 2)。
         首先,ast 最重要的思路,也就是怎么去定位,怎么去处理。这里的话,我们可以看到,都是 var 的变量声明语句,同时右边,对应的语句的 init 节点都是数字,也就是NumericLiteral类型,同时可以看到他都被包裹在一个函数里面。
           

ob3

ob3

          这样可以去定位到,然后再用绑定查找他被引用,也就是binding.referencePaths的方法,再进行一层替换。在替换时还需要去匹配调用的特征,也就是CallExpression 类型
           

ob4

ob4

         
         
// 还原的基本框架在之前的一篇文章中
traverse(ast, {
  VariableDeclarator: function (path) {
    let {node, parentPath, scope} = path;
    // 判断是不是辅助语句
    if (!types.isVariableDeclaration(parentPath)) return;
    // 同时满足一边是Identifier类型,一般是NumericLiteral类型
    if (!types.isIdentifier(node.id) || !types.isNumericLiteral(node.init)) return;
    let name = node.id.name;
    let value = node.init.value;
    // 获取绑定
    let bindings = scope.getBinding(name);
    if (!bindings) return;
    bindings.referencePaths.forEach(function (re_path) {
      // 可以在这里去打印看看,这里的其实还是name的值,没有拿到函数函数调用
      let {parentPath} = re_path;
      // 我们知道这是在一个函数里面
      if (!types.isCallExpression(parentPath)) return
      // 他的值本身是一个Identifier,或者是等于name
      if (!types.isIdentifier(parentPath.node.callee)) return;
      // 直接替换
      parentPath.node.arguments[0] = types.numericLiteral(value);
      re_path.skip();
    })
    path.remove()
  }
})

         这样就可以实现基础的还原,看看效果
      

ob5

ob5

         

ob6

ob6


三、ob 混淆特征处理
         
function l() {
  var t = ["orted", "hesSel", "apply", "undefi", "matche", "9gmWyLK", "(((.+)", "8472CchwhG", "640232giirHG", "uctor", "toStri", "unknow", "search", "msMatc", "53350DLKijk", "1043Rkhvwx", "+)+)+$", "length", "765nORZzx", "ned", "509240FbRROQ", "ector", "constr", "86794nJKbSs", "unsupp", "29644JQRcdH", "33889BOoZEr"];
  return (l = function () {
    return t;
  })();
}
function p(t, e) {
  var r = l();
  return p = function (t, e) {
    return r[t -= 420];
  }, p(t, e);
}
!function (t, e) {
  for (var r = 441, n = 438, o = 420, i = 440, a = 435, c = 422, u = 430, s = 423, f = 433, l = 429, h = p, v = t();;) try {
    if (161193 === parseInt(h(r)) / 1 + parseInt(h(n)) / 2 * (-parseInt(h(o)) / 3) + -parseInt(h(i)) / 4 + parseInt(h(a)) / 5 + -parseInt(h(c)) / 6 * (parseInt(h(u)) / 7) + -parseInt(h(s)) / 8 + -parseInt(h(f)) / 9 * (-parseInt(h(l)) / 10)) break;
    v.push(v.shift());
  } catch (t) {
    v.push(v.shift());
  }
}(l)

          这是提取的一组funcaptcha 的混淆特征,包含了大数组,解密函数以及自执行的数组移位,还是先观察特征。我们发现,大数组的函数在数组移位和解密函数都有引用,是不是可以从大数组定位开始,去找他的绑定来定位其他两个函数的特征,从而存进内存,用于执行解密操作。
         

ob7

ob7

      还是定位,直接上代码梭哈。
   
let array_string = ""
traverse(ast, {
    VariableDeclarator: function (path) {
        let {node, scope} = path;
        if (!types.isIdentifier(node.id) || !types.isArrayExpression(node.init)) return;
        // 大数组对象 判断是不是在函数内部
        let black_func = path.find(p => types.isFunctionDeclaration(p))
        if (!black_func) return;
        // 数组长度超过20
        if (node.init.elements.length < 20) return;
        array_string += black_func.toString() + "\r\n"
        let obj_name = black_func.node.id.name;
        // 获取绑定
        let bindings = scope.getBinding(obj_name);
        if (!bindings) return;
        bindings.referencePaths.forEach(function (re_path) {
            let expath = re_path.parentPath;
            // 判断是不是移位自执行函数
            if (types.isUnaryExpression(expath.parentPath)) {
                let shift_func = expath.parentPath;
                array_string += shift_func.toString() + "\r\n"
                // 解密函数定位
            } else if (types.isVariableDeclarator(expath.parentPath)) {
                let de_func = expath.find(p => types.isFunctionDeclaration(p));
                array_string += de_func.toString() + "\r\n"
            }
        })
    }
})
// 写入看看情况
fs.writeFileSync("./funcaptcha混淆特征.js", array_string)

      

ob8

ob8

    没毛病,应该把所有的 ob 混淆特征都成功的拿到了。接下来就是收藏所有的解密函数名称,用于去查找还原。还是在这个基础上去优化代码。
    这样就实现了最基本的字符串还原脚本。   
let defunc_name
// ob特征写入内存
traverse(ast, {
  VariableDeclarator: function (path) {
    let array_string = ""
    let {node, scope} = path;
    if (!types.isIdentifier(node.id) || !types.isArrayExpression(node.init)) return;
    // 大数组对象
    let black_func = path.find(p => types.isFunctionDeclaration(p))
    if (!black_func) return;
    if (node.init.elements.length < 20) return;
    array_string += black_func.toString() + "\r\n"
    let obj_name = black_func.node.id.name;
    let bindings = scope.getBinding(obj_name);
    if (!bindings) return;
    bindings.referencePaths.forEach(function (re_path) {
      let expath = re_path.parentPath;
      // 判断是不是移位自执行函数
      if (types.isUnaryExpression(expath.parentPath)) {
        let shift_func = expath.parentPath;
        array_string += shift_func.toString() + "\r\n"
        // 解密函数定位
      } else if (types.isVariableDeclarator(expath.parentPath)) {
        let de_func = expath.find(p => types.isFunctionDeclaration(p));
        array_string += de_func.toString() + "\r\n"
        defunc_name = de_func.node.id.name;
      }
    })
    // 将ob特征存进内存
    eval(array_string)
    // 解密函数的绑定
    let binding = bindings.scope.getBinding(defunc_name);
    if (!binding) return;
    binding.referencePaths.forEach(function (re_path) {
      let expath = re_path.parentPath;
      if (!types.isVariableDeclarator(expath)) return;
      let other_name = expath.node.id.name;
      let func_binding = re_path.scope.getBinding(other_name)
      if (!func_binding) return;
      func_binding.referencePaths.forEach(function (ref_path) {
        let pa_path = ref_path.parentPath;
        // 这里过滤掉的可能就有另一层赋值
        if (!types.isCallExpression(pa_path)) return
        let args = pa_path.node.arguments[0].value;
        // 计算结果
        let value = eval(defunc_name).apply(null, [args])
        console.log(pa_path.toString(), "----->", value)
        pa_path.replaceWith(types.valueToNode(value))
      })
    })
  }
})

         

ob9

ob9

       酷酷输出,但是结果一看还是有一点小问题。这里的代码只有一层的赋值,代码中解密函数存在不停的去嵌套赋值,我们还需要一个函数,去深层获取所有的赋值操作
      
function traceAndReplace(currentName, scope, decryptFn) {
  let binding = scope.getBinding(currentName);
  if (!binding) return;
  binding.referencePaths.forEach(refPath => {
    let parentPath = refPath.parentPath;
    // 递归追踪
    if (types.isVariableDeclarator(parentPath) && types.isIdentifier(parentPath.node.id)) {
      let nextName = parentPath.node.id.name;
      traceAndReplace(nextName, parentPath.scope, decryptFn);
      parentPath.remove();
    } else if (types.isCallExpression(parentPath) && parentPath.node.callee === refPath.node) {
      try {
        // 仅处理参数为常量的调用,避免执行出错
        if (parentPath.node.arguments.length == 1) {
          let args = parentPath.get('arguments').map(arg => arg.evaluate().value);
          let result = decryptFn.apply(null, args);
          console.log(`${parentPath.toString()} ----> ${result}`);
          parentPath.replaceWith(types.valueToNode(result));
        }
      } catch (e) {
      }
    }
  });
}

      这样我们就搞定了funcaptcha 的字符串类 ob 混淆,先看看目前的成品吧
        

ob10

ob10

         

ob11

ob11

四、总结
funcaptcha的混淆难度整体不大,算法也不算太难,大佬们可以尝试继续还原剩下的两个点,一个是还原后字符串的拼接和ob特征的垃圾代码删除。这里也祝所有论坛的大佬,小伙伴们新年快乐,马到成功{:1_932:}

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-2-14 13:38

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表