unipus iTEST考试助手---写脚本与反脚本的拉锯战

none

咕鸽框架 更新2020年07月10日 81 11055字 CID:P15

0x0 前言

脚本已经失效, 此博客只为记录我的开发历程, 方便大家学习油猴脚本开发
学校的英语考试选择在ITEST平台上进行, 并让我们进行了一次模拟考试, 进入考试后发现无法选中切屏等操作, 不如就写个脚本解除这些限制吧, 马上我同学魔改其他人的脚本加入了翻译等功能, 我就想着不如我也写着试一试吧, 随后就开始了我与ITEST的拉锯战

在这里插入图片描述

0x1 1.0版本 -- 解除限制

我方进攻

解除限制非常简单, 首先注意到所有的.itest-ques均被挂上了false, document document.body也被限制了, 直接解除这些限制即可

在这里插入图片描述

function hackClass(className) {
    for (const i of document.getElementsByClassName(className)) {
        hackItem(i);
    }
}

/**
* 现在会被检测
*/
function hackItem(item) {
    item.onpaste = () => true;
    item.oncontextmenu = () => true;
    item.onselectstart = () => true;
    item.ondragstart = () => true;
    item.oncopy = () => true;
    item.onbeforecopy = () => true;
    item.style = '';
}
hackClass('itest-ques');
hackClass('itest-direction');
hackItem(document.body);

0x2 2.0版本 - 自动翻译与解析听力

我方进攻

注意到所有的听力材料都是以json的形式写在了dom里, 只需要遍历这些dom, 就可以将听力的地址提取出来 ITEST开发人员太懒了

在这里插入图片描述

翻译题目很简单, 只需要遍历每个题目的节点, 在每个题目上加上按钮, 传入百度翻译即可返回结果, 这里贴上部分代码

/**
 * 调用翻译api
 * @param context_list
 * @param from
 * @param to
 * @returns {Promise<unknown>}
 */
async function translateAPI(context_list, from, to) {
    const trans_salt = (new Date).getTime();
    const {appid, key} = getBaiduAPIKey()
    const trans_str = appid + context_list + trans_salt + key;
    const trans_sign = md5(trans_str);

    for (let i=0; i<5; i++) {
        const data = await translateAjaxApi({
            q: context_list,
            from,
            to,
            appid: appid,
            salt: trans_salt,
            sign: trans_sign
        })
        if (data.error_code) {
            const errmsg = {
                "52001": "请求超时",
                "52002": "系统错误",
                "52003": "未授权用户",
                "54003": "访问频率受限",
                "54004": "账户余额不足",
                "58002": "服务当前已关闭",
                "90107": "认证未通过或未生效"
            }
            if (parseInt(data.error_code) !== 54003) {
                alert(`错误代码${data.error_code} 信息:${errmsg[data.error_code.toString()]}`)
                throw new Error(`错误代码${data.error_code} 信息:${errmsg[data.error_code.toString()]}`)
            } else {
                await delay(1000);
            }
        }
        return data.trans_result;
    }
    alert("请求过于频繁")
    throw new Error("请求过于频繁");
}
/**
 * 单选题注入
 */
function singleSelect() {
    $('.row').each(function () {
        if ($(this).find('.option').length !== 0) {
            $(this).prepend(`<button class="sk-tr-s sk-translate-btn sk-btn-primary sk-btn-p">翻译</button>`)
        }
    })
    $('.sk-tr-s').on('click', function () {
        if ($(this).is('.sk-btn-p')) {
            $(this).parent().find('span').each(function () {
                $(this).remove();
            })
            const $this = $(this).parent();
            const selectOption = [];
            $this.children('.option').each(function () {
                selectOption.push($(this).find('label'))
            })
            const p = selectOption.map(value => value.text().replace('\n', '')).join('\n');
            translateAPI(p, 'en', 'zh').then(value => {
                for (let i = 0; i < value.length; i++) {
                    selectOption[i].append(`<span style="color: #5093df"><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;${value[i].dst}</span>`)
                }
                $(this).removeClass('sk-btn-p').addClass('sk-btn-g').html('清空');
            })
        } else {
            $(this).removeClass('sk-btn-g').addClass('sk-btn-p').html('翻译');
            $(this).parent().find('span').each(function () {
                $(this).remove();
            })
        }
    })
}

ITEST方防御

因为不少学校的ITEST平台是定制的, 域名未知, 我不得不匹配所有域名, 为了判断一个平台是否为ITEST, 我才用了判断标题的方法

/**
 * 判断是否为Itest平台, 为了防止部分学校定制
 * @returns {boolean}
 */
function isItest() {
    return document.title.indexOf("iTEST") !== -1 && document.getElementById('all-content') !== null;
}

收到失效的报告后, 我进入平台检查, 发现了下面令我瞠目结舌的场面:

在这里插入图片描述

在这里插入图片描述

他们把ITEST的TE替换成了希腊字母ΕεΤτ, 肉眼看不出任何问题, 但是程序不认了, 要不是有拼写检查, 我还真看不出来ITESTITΕSΤ不一样, 你看得出来吗?

0x3 3.0版本 -- 解除切屏限制与添加翻译助手

反制防御

因为ITEST被改, 不能判断ITEST, 我将条件删为仅判断节点

function isItest() {
    return document.getElementById('all-content') !== null;
}

我方进攻

在逻辑里发现切屏代码, 是通过window.onblurwindow.onfocus检测切屏, 很简单, 我只要把这两个函数置空即可

在这里插入图片描述

setInterval(function () {
    window.onblur = () => {};
    window.onfocus = () => {};
}, 5 * 1000)

为了方便查单词, 我添加了翻译助手, 大概就是添加了个节点, 点击会直接调用百度翻译进行翻译, 大致源码, 红框内为铺垫

在这里插入图片描述

ITEST防御

还记得前面那个翻译助手吗? 就那里出现了大问题, ITEST直接检测了翻译助手的悬浮窗, 如何出现问题直接判定为作弊, 并通过ajax传输出去! 至此, 我不得不修改源码

在这里插入图片描述

0x4 4.0版本 - 全随机与ajax拦截

反制防御

为了防止脚本被固定节点检测出, 我决定对脚本进行重构, 对所有的class进行随机化处理, 核心代码如下

/**
 * 生成随机字符串
 * @param len 长度
 * @returns {string}
 */
function getRandomString(len = 10) {
    const str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    const maxPos = str.length;
    let pwd = '';
    for (let i = 0; i < len; i++) {
        pwd += str.charAt(Math.floor(Math.random() * maxPos));
    }
    return pwd;
}

/**
 * 生成范围随机数
 * @param minNum 最小值
 * @param maxNum 最大值
 * @returns {number}
 */
function getRandomInt(minNum, maxNum) {
    return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
}

const RANDOM_CLASS = {  // 对所有的随机化处理
    BTN: getRandomString(getRandomInt(3, 10)),
    BTN_P: getRandomString(getRandomInt(3, 10)),
    BTN_G: getRandomString(getRandomInt(3, 10)),
    TRANSLATE_TEXT: getRandomString(getRandomInt(3, 10)),
}

/**
 * 文章翻译
 */
function article() {
    const ART = getRandomString(getRandomInt(5, 10));
    $('.con-left>.article').each(function () {
        $(this).prepend(`<a class="${RANDOM_CLASS.BTN} ${RANDOM_CLASS.BTN_P} ${ART}" >${getRandomString(4)}</a>`)
    })
    $(`.${ART}`).on('click', function () {
        // 翻译被点击内容
    })
}

如此一来, 所有动态插入的内容全随机, 如图

在这里插入图片描述
在这里插入图片描述

我方进攻

为了防止信息被上报, 我拦截了ajax信息, 对url进行判断, 并选择是否放行, 代码如下

function fuckITEST() {
    // 一袋米要抗几楼
    window.onblur = function () {};
    window.onfocus = function () {};
    setInterval(function () {
        window.onblur = function () {};
        window.onfocus = function () {};
    }, 5 * 1000)

    // 一袋米要抗二楼
    const ajax = $.ajax
    $.ajax = function (obj) {
        const url = obj.url
        if (url.indexOf("log") === -1) {
            return ajax(obj)
        }
        return "欧拉欧拉欧拉欧拉"
    }
    // 一袋米哟给多嘞
    ITEST.util.logNew = function () {
        // 木大木大木大木大木大木大
    };
    // 一袋米哟我洗嘞 !!!!!!
}

ITEST防御

这次防御有点头疼, 首先ITEST去检测了解除限制这一片, 发现如果被置为true则判定开了脚本

在这里插入图片描述

为了防止ajax被拦截, 他们选择了fetch, 并提前保存了fetch防止被篡改
在这里插入图片描述

最后拦截了ajax, 并采用定时器不断修改ajax, 检测是否访问api.fanyi.baidu.com
在这里插入图片描述

0x5 5.0版本 - 只读属性的胜利

反制防御

iTEST再次更改了节点, 从all-content改成了main-content, 必须得找一个无法被修改的办法, 于是选择检测JS

function isITEST() {
    try {
        ITEST;
        return true;
    } catch(e) {
        return false;
    }
}

因为检测oncontextmenu, 只要不返回false便可解除屏蔽, 为了万无一失, 我采用了骚操作, 采用定义属性的方式使其返回false, 但是网页却认为不是false, 右键菜单照常, 但是js读一定是false

function hackItem(item) {
    item.onpaste = () => true;
    item.oncontextmenu = () => true;
    item.onselectstart = () => true;
    item.ondragstart = () => true;
    item.oncopy = () => true;
    item.onbeforecopy = () => true;
    Object.defineProperty(item, 'onpaste', {get: () => (event) => false})
    Object.defineProperty(item, 'oncontextmenu', {get: () => (event) => false})
    Object.defineProperty(item, 'onselectstart', {get: () => (event) => false})
    Object.defineProperty(item, 'ondragstart', {get: () => (event) => false})
    Object.defineProperty(item, 'oncopy', {get: () => (event) => false})
    Object.defineProperty(item, 'onbeforecopy', {get: () => (event) => false})
}

在这里插入图片描述

因为对方拦截了ajax, 我要不想被拦截就可以使脚本比他更先运行, 加入注释头
// @run-at document-body
因为此时网页还未载入jQuery, 我得自己加载jQuery
// @require https://cdn.bootcdn.net/ajax/libs/jquery/1.11.1/jquery.min.js
随后保存ajax, 使其无法篡改

在这里插入图片描述

这样以后调用ajax即可, 对方无法侦测到我使用翻译接口

我方进攻

虽然我承认ajax是我先改的, 但是我现在脚本比你先运行, 那么就可以玩点骚的了, 首先把ajax设为只读, 并采用白名单得形势去检查上报事件是否是我允许的

Object.defineProperty($, 'ajax', {
    get: () => (obj) => {
        const data = JSON.parse(obj.data)
        const allow = data.filter(value => {
            const action = value.action
            return ['pairwork_exam', 'res_download_start', 'next_ques_click', 'pre_ques_click',
                'ans_submit', 'exam_end', 'ans_card_click', 'ans_auto_submit', 'exam_begin', 'res_download_end',
                'down_audio_error'].includes(action);
        })
        if (allow.length === 1 && data.length === 1) {
            console.log("上报信息成功")
            SETTING.AJAX(obj)
        } else {
            console.log("上报信息不在白名单内, 已被阻断!")
            obj.success()
        }
    },
    set: () => {console.log("无事发生")}
})

随后拦截fetch事件, 使其返回永远是成功

window.fetch = () => new Promise((resolve) => {resolve()})
fetch = () => new Promise((resolve) => {resolve()})

0x6 后记

拉锯战到这里就结束了, 因为5.0版本我并没有公开, ITEST无法对我进行反制, 等这篇博客发出来的时候, 我的英语应该已经考完了(滑稽), 不过5.0我也不打算公开了, 通过这次拉锯战巩固了我jQuery框架的知识, 还整会了各种骚操作, 这种东西玩玩就好了, 适时收手, 我可不想收到我的第二封律师函

版权声明:
本文基于《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权
文章链接:http://blog.z31.xyz/index.php/archives/15/ (转载时请注明本文出处及文章链接)

评论

2条评论

    舒傲寒

    舒傲寒 访客

    2020-09-18 08:06 回复

    忘记问博主了,你的JS是什么途径学习的?有好的教程资料或者是渠道吗,我之前受你影响,想学脚本,看了一段时间廖雪峰的教程,有点难,看不懂,放弃了。可以分享的话,我的邮箱你应该可以看到,麻烦指点一下。

    舒傲寒

    舒傲寒 访客

    2020-09-18 08:02 回复

    使用过博主的脚本,确实一波操作下来让iTest人员很头疼,博主是真正有实力的,
    个人而言虽然最后没能使用到博主的5.0脚本,但博主关键时候的选择是很正确的,希望博主越来越强大。