猿人学第三届js逆向比赛第一题

网址:https://match2025.yuanrenxue.cn/match2025/topic/1

1、获取图片接口逆向分析


抓包分析,发现只有请求表单参数进行了加密

直接找到发起请求前的第一个栈打上断点

这里的a值已经生成了,本次我们采用补环境的方式来解决这道题,我们将js代码拉到本地进行调试分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
function myProxy(obj, name) {
return new Proxy(obj, {
//拦截对象属性的读取,比如proxy.foo和proxy['foo']。
get(target, propKey, receiver) {
let temp = Reflect.get(target, propKey, receiver)
console.log(`${name} -> get ${propKey} return -> ${temp}`)
if (typeof temp === 'object') {
temp = myProxy(temp, name + ' => ' + propKey)
}
return temp
},
//拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
set(target, propKey, value, receiver) {
const temp = Reflect.set(target, propKey, value, receiver)
console.log(`${name} -> set ${propKey} value -> ${value}`)
return temp
},
//拦截propKey in proxy的操作,返回一个布尔值。
has(target, propKey) {
const temp = Reflect.has(target, propKey)
console.log(`${name} -> has ${propKey}`)
return temp
},
//拦截delete proxy[propKey]的操作,返回一个布尔值。
deleteProperty(target, propKey) {
const temp = Reflect.defineProperty(target, propKey)
return temp
},
//拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
ownKeys(target) {
const temp = Reflect.ownKeys(target)
return temp
},
//拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
getOwnPropertyDescriptor(target, propKey) {
const temp = Reflect.getOwnPropertyDescriptor(target, propKey)
return temp
},
//拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
defineProperty(target, propKey, propDesc) {
const temp = Reflect.defineProperty(target, propKey, propDesc)
return temp
},
//拦截Object.preventExtensions(proxy),返回一个布尔值。
preventExtensions(target) {
const temp = Reflect.preventExtensions(target)
return temp
},
//拦截Object.getPrototypeOf(proxy),返回一个对象。
getPrototypeOf(target) {
const temp = Reflect.getPrototypeOf(target)
return temp
},
//拦截Object.isExtensible(proxy),返回一个布尔值。
isExtensible(target) {
const temp = Reflect.isExtensible(target)
return temp
},
//拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
setPrototypeOf(target, proto) {
const temp = Reflect.setPrototypeOf(target, proto)
return temp
},
//拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
apply(target, object, args) {
const temp = Reflect.apply(target, object, args)
return temp
},
//拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
construct(target, args) {
const temp = Reflect.construct(target, args)
return temp
}
})
}

window = globalThis
myProxy(window, 'window')

location = {}
location = myProxy(location, 'location')

document = {}
document = myProxy(document, 'document')

navigator = {}
navigator = myProxy(navigator, navigator)

screen = {}
screen = myProxy(screen, 'screen')

我们先给浏览器常见的检测对象套上代理,然后执行代码开始补环境

出现报错,这里可以把window的原型链补一下,也可以直接把这里报错的代码直接改为true

这里提示$为定义,$在浏览器中是一个jquery库,我们通过在浏览器打断点的方式可以看到$下还有一个ajax的方法,我们直接在代码中给它定义一个空函数

1
2
3
4
5
6
7
$ = function() {
debugger
}

$.ajax = function() {
console.log(arguments)
}

定义完成后再次运行代码,出现打印信息

1
2
3
4
5
6
7
8
9
10
11
12
[Arguments] {
'0': {
url: '/match2025/topic/1_captcha_jpg',
dataType: 'json',
async: true,
type: 'POST',
data: {
a: 'CmCnJajVa4Wc9zEWAWbvNXvOXdyZdGadFacxDoSAeG1k2UBsWjPzgFLvhSPbY2exvsT8D2t2sC3/bzK5Z4k9hA=='
},
success: [Function (anonymous)]
}
}

我们直接导出这个a进行请求的话肯定是请求不通过的,因为在控制台中没有出现任何的浏览器对象的调用信息, 那么在代码中可能对浏览器对象进行了重新赋值,因为在浏览器中对浏览器对象进行重新赋值的话是无法生效的,但在我们的node环境中是可以生效的,那么我们就直接在代码中搜索浏览器对象的关键字

发现确实有重新赋值的地方,我们直接将这里的返回值修改成返回一个空对象即可,把所有对浏览器对象赋值的地方都修改一下
发现请求获取图片的接口还是不对,那么我们再次分析,发现每次请求接口图片前都会请求一个logo的接口,这个logo的接口响应值是一个时间戳参数,那么这个时间戳参数肯定和加密是有关系的,我们直接搜索Date相关的结果

直接页面上对应Date的位置打上断点

发现这里调用了Date.now,而且这个Date.now是被魔改过的,我们搜索一下Yr在哪里使用到了

发现在so方法中使用到了Yr,我们在页面对应的位置打上断点

a值就是在这里生成的,那么我们就需要在代码中修改一下Date.now方法,让Date.now方法返回我们请求logo时返回的时间戳,将代码封装成一个函数,将timestamp作为参数传递进来

1
2
3
4
5
6
function get_a(timestamp) {
Date.now = function() {
return timestamp
}
...
}

我们拿到a值请求获取图片的接口后发现可以请求成功,但是我们将接口返回的result值转为gif图片有时会出错,我们直接在页面上拿result的值和实际的图片base64的值进行比对发现实际渲染在页面的图片base64值和接口返回的result值不一致,说明在拿到result值之后还是会进行一次加密才的得到最后的图片值,那么我们如何快速定位到在哪进行的图片base64值加密的呢?

我们定位到图片的元素,在元素面板对图片元素进行右击,选择中断于子树修改和属性修改然后刷新页面

这里可以看到图片的base64值就已经生成了,我们直接分析堆栈

这里是在一个jquery中调用了一个方法然后进行了一系列的加密最终得到了加密值,我们直接跟进这个方法看看在哪

点击定位到了这里,看堆栈这个function是赋值给了Kf,那么我们将Kf存为一个全局函数然后进行调用就可以拿到我们的加密值了
在本地的代码中搜索到Kf的位置,然后保存到全局

我们先看一下调用Kf的函数需要传递哪些参数,把它赋值到我们的代码中来进行调用

这里传递了两个参数,其中t[1]是一个数组,我们将t[0]和t[1]都复制到本地然后都挂上代理来查看Kf函数都调用了参数中的哪些属性

1
window.Kf.apply(t0, t1)

调用后发现出现了一些新的环境检测,把需要的环境补上就好,通过代理打印的日志后发现只有t1参数中下标为0的参数被取值了,t1参数下标为0的元素就是请求图片接口返回的值

我们可以将请求图片接口的返回值作为参数传递进来进行加密

继续运行出现报错,这时我们回到浏览器

这里是往$对象中传递了一个#captchaImg然后返回了一个对象,对象中有一个attr函数,我们直接在本地进行模拟

1
2
3
4
5
6
7
8
9
10
$ = function (name) {
if (name == "#captchaImg") {
return {
attr: function() {
console.log(arguments)
}
}
}
debugger
}

这时就已经把图片加密的值打印出来了

2、验证码图片识别

我们识别gif图片的话需要将gif图片的帧进行分析,将停留时间最长帧的图片保存为png图片,然后通过ddddocr进行识别即可
至于如何将停留时间最长帧的图片保存可以直接使用ai帮助编写

3、验证接口逆向分析


对于验证接口的话也是只对请求表单参数进行了加密,那么我们在全扣代码的情况下要触发text加密就得观察页面上是怎样触发验证接口的

我们定位到输入框元素,发现这里有一个text_oninput(this)函数,那么这里肯定就是发起请求的入口了,但是我们并不知道text_oninput(this)函数在哪定义的我们该怎么做
我们直接通过油猴脚本来hook一下,看看它赋值的位置在哪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ==UserScript==
// @name text_oninput
// @namespace http://tampermonkey.net/
// @version 2025-06-11
// @description try to take over the world!
// @author theone
// @match https://match2025.yuanrenxue.cn/match2025/topic/1
// @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @grant none
// @run-at document-start
// ==/UserScript==

(function() {
'use strict';
Object.defineProperty(window, 'text_oninput', {
set: function () {
debugger
}
})
})();


hook完成后发现我们在执行Kf函数的时候就已经将text_oninput函数赋值给了window对象,那么我们在执行完Kf函数的时候text_oninput函数就已经保存在全局了,我们需要关注的是text_oninput函数传参是怎么样的
我们直接对验证接口打上xhr断点看看

触发验证接口后直接来到最下面的堆栈查看,发现text_oninput函数传入的是一个input标签对象,其实input标签对象也可以看成是一个js对象,我们直接调用查看

1
text_oninput(myProxy({}, 'text_oninput'))

我们给text_oninput函数的参数套上代理,发现这里取了对象中的value值,那么我们来到页面上看看这个value值是什么

这里我们可以发现value值就是我们输入的验证码,我们将value值作为参数传递给text_oninput函数,然后就可以得到我们的加密值了
这个value的值就是我们输入框输入的内容,那么这个value值也可以通过传递参数的方式来进行接收,最后我们可以在$方法中把最后的值进行导出即可

最后还需要注意的就是在请求验证接口的时候对请求头的顺序进行了检测