注意,本文只提供学习的思路,严禁违反法律以及破坏信息系统等行为,本文只提供思路
本文的验证码网址如下,使用base64解码获得
aHR0cHM6Ly93d3cuZGluZ3hpYW5nLWluYy5jb20vYnVzaW5lc3MvY2FwdGNoYQ==
本文以滑块验证码举例,刷新验证码,打开请求
发现是a接口传回验证码图片相应参数,滑动任意参数,查找校验请求
发现是v1接口,其他参数在发送验证码接口,也就是a接口中可以找到,最终确定逆向的是ac参数
我们跟栈进去看看,在哪生成
经过一番查找后,是由hn.getUA()获取,那么getUA这个方法就是我们要针对的目标
跟进去之后,发现是混淆之后的,并且这里也只是单纯的return,说明,在前面就已经计算完成了,我们继续查找最初生成的位置
最终在这个位置发现刚开始生成的位置,传进去的n是之前发送验证码的sid值
但是这里的418值很短,所以还存在其他计算校验的地方,基本都是在同一个js文件里面,这里我们先进行反混淆
大致看了一下,混淆的形式不算多,有switch流程平坦化、变量标识符混淆、join字符串拼接混淆等,我们来一一针对
1、变量名混淆,变量名混淆其实就是自执行函数最后的大数组传入对应的形参,然后解码即可
这里,虽然网站的js会变化,但我为了方便,我暂时先写死
let vmpvar = ['n', 'r', 'e']
let vmpli = [["72,", "str", ",", "]] // 此为大数组,因为篇幅省略
let devmpmember = {
MemberExpression(path) {
// 当成员变量中不为数字时,则跳过
if (!types.isNumericLiteral(path.node.property)) {
return
}
if (types.isIdentifier(path.node.object)) {
vmpindex = vmpvar.indexOf(path.node.object.name)
if (vmpindex != -1) {
values = vmpli[vmpindex]
k = path.node.property.value
// 替换为对应元素
path.replaceWith(types.valueToNode(values[k]))
de_vmpmembernum += 1
}
}
}
}
traverse(ast, devmpmember)
还原之后,对应的变量都有相应的字符串赋值了
还原前
还原后
2、还原switch流程平坦化
还原switch混淆,需要先进行大数组字符串解混淆,因为这里需要获取switch中循环数值
let deWhile2 = {
SwitchStatement(path) {
// 如果父节点不是for循环,则跳过
if (!types.isForStatement(path.parentPath.parentPath)) {
return
}
// 获取switch的条件
let switchvar = path.node.discriminant
// 判断是否为成员变量
if (!types.isMemberExpression(switchvar)) {
return
}
// 获取调用的成员变量
let membervar = switchvar.object.name
// 获取调用的自增变量
let updatevar = switchvar.property.argument.name
// 获取for循环的条件
// 获取调用的成员
member = null
let binding = path.scope.getBinding(membervar)
binding && binding.scope.traverse(binding.scope.block, {
VariableDeclarator(path2){
if(path2.node.id.name == membervar){
try{
member = eval(generator(path2.node.init).code)
path2.stop()
}catch(e){
debugger
}
}
}
})
// 将case的条件转换成对象字面量
inc = 0 // 暂时写死,因为目前都是0
statecases = path.node.cases
cases = {}
for(let i = 0; i < statecases.length; i++) {
cases[statecases[i].test.value] = statecases[i].consequent
}
// 循环member变量
for(let i = 0; i < member.length; i++) {
// 获取对应的case语句
case_num = member[inc++]
// 根据case_num以及case对应的条件获取对应的语句
let casebody = cases[case_num]
// 插入到当前while语句前面
for(let j = 0; j < casebody.length; j++) {
// 如果是break或者continue语句不插入
if (types.isBreakStatement(casebody[j]) || types.isContinueStatement(casebody[j])) {
continue
}
path.insertBefore(casebody[j])
}
}
// 删除while语句
path.remove()
de_whilenum2 += 1
}
}
traverse(ast, deWhile2)
使用代码还原前
还原后
3、变量标识符混淆
因为之前定义了很多的变量,并在代码结构体中引用这些变量,导致变量非常的多,即使我们还原了字符串大数组后,阅读依然困难,所以这一步我们需要将引用的变量直接替换成之前已经定义好的值
let deiden = {
Identifier(path){
// 如果父类是VariableDeclarator,则跳过
if(types.isVariableDeclarator(path.parentPath)){
return
}
// 如果名称在vmpvar中,则跳过
// if (vmpvar.indexOf(path.node.name) != -1) {
// return
// }
// 如果名称的父节点不是是ArrayExpression, 则跳过
if (!types.isArrayExpression(path.parentPath)) {
return
}
// 查找变量定义的位置
identname = path.node.name
binding = path.scope.getBinding(identname)
binding && binding.scope.traverse(binding.scope.block, {
VariableDeclarator(path2) {
// 如果变量定义的父节点是for循环,则跳过
if (types.isForStatement(path2.parentPath.parentPath)) {
return
}
// 如果定义的变量名和当前标识符名相同
if(path2.node.id.name == identname){
// 当节点值不是字符串也不是数字时,则跳过
if (!types.isStringLiteral(path2.node.init) && !types.isNumericLiteral(path2.node.init)) {
return
}
try {
path.replaceWith(types.valueToNode(path2.node.init.value))
unieconsole.log(identname, path2 + "")
idennum += 1
path2.stop()
path.skip()
}catch(e){
//unieconsole.log(identname, path2 + "", "还原失败")
}
}
}
})
}
}
traverse(ast, deiden)
还原前
还原后
除此之外,还涉及到其他混淆,例如return表达式混淆,十六进制等,最基本的混淆我不再一一赘述,大致原理都差不多,下面展示一下,最终还原的结果
可以看到,还原出来,阅读非常的舒适、这样非常利于解决AC的值的时候,具体步骤,将在下一篇介绍