使用usb hid完美实现自动化脚本防风控

实战派 ESP32-S3,双模无线开发板

ESP32-S3 原生支持 ESP-IDF,WiFi + 蓝牙一次搞定

在当前移动应用安全机制日益严密的背景下,传统的自动化脚本若依赖 Android 系统的无障碍服务(Accessibility Service)进行界面操作,极易被各类 App 的风控系统识别并拦截。这是因为无障碍服务具有明显的特征:它需要用户主动授权、会在系统中留下可检测的日志记录,并且其操作行为(如快速遍历控件、无真实触摸轨迹等)与人类自然交互存在显著差异。尤其在金融、社交、游戏等对安全高度敏感的应用场景中,一旦检测到无障碍服务被启用或调用异常,系统往往会直接限制功能、弹出验证甚至封禁账号。

为有效规避此类风控机制,越来越多的自动化方案转向更底层、更仿真的输入模拟技术——其中,USB HID(Human Interface Device)技术成为一种极具潜力的替代路径。USB HID 是一种标准的人机接口协议,常用于键盘、鼠标等外设。通过将自动化设备(如树莓派、单片机开发板等)配置为 USB HID 设备,并以 OTG 方式连接至目标手机,即可在操作系统内核层面模拟真实的物理输入事件。由于这些操作不经过 Android 应用层,也不依赖 Accessibility API,因此不会触发与无障碍相关的权限检查或行为分析,极大提升了隐蔽性。

更重要的是,USB HID 产生的点击、滑动、按键等信号,在系统看来与用户手动操作几乎无异,具备完整的触摸坐标、时间戳和事件序列,难以被上层应用区分。此外,该方案无需 root 权限(部分机型需开启“USB 调试”或“HID 支持”),部署相对稳定,响应延迟低。尽管其硬件成本略高于纯软件方案,且需物理连接,但在对抗高强度风控、保障长期运行稳定性方面优势显著。因此,采用 USB HID 技术实现自动化,不仅有效绕开了基于无障碍服务的检测逻辑,也为高安全环境下的智能辅助和批量操作提供了更可靠的技术路径。 

在冰狐平台中可通过内置的 usb 对象与 USB HID 硬件设备(如 ESP32-S3、CH552 等)通信,实现底层输入模拟,从而完成自动化操作。该方案不依赖 Android 无障碍服务,能有效规避多数 App 的风控检测。

一、准备工作

  1. 硬件要求
    • 支持 USB HID 协议的主控设备(如 ESP32-S3 开发板),并已烧录兼容冰狐通信协议的固件。
  2. 手机设置
    • 手机需支持 OTG 功能(在“设置”中搜索 “OTG” 并开启);
    • 使用 OTG 数据线将 USB HID 设备连接至手机;
    • 首次连接时,系统可能弹出“允许此设备使用 USB”的提示,需点击“确定”。
  3. 冰狐环境
    • 已安装最新版冰狐 App 并登录账号;
    • 脚本运行在具备 USB 权限的环境中(通常首次调用会自动申请)。

二、核心 API(来自官方文档)

冰狐提供以下 usb 对象方法:

方法功能说明
usb.connect()连接 USB HID 设备,返回 1 表示成功
usb.send(data)发送原始字节数据(数组或字符串)
usb.receive([timeout])接收设备返回数据,默认超时 10 秒
usb.click(x, y)模拟屏幕点击(像素坐标)
usb.swipe(x1, y1, x2, y2, [duration])模拟滑动操作
usb.home() / usb.back() / usb.recentApps()模拟系统按键
usb.mouseMove(x, y) / usb.mouseUp()模拟鼠标移动与释放

注:所有坐标基于目标手机屏幕分辨率(如 1080×2400),建议使用全局变量 rsScreenWidth 和 rsScreenHeight 动态适配。


三、使用步骤与完整示例脚本

步骤 1:初始化并连接设备
function main() {
    // 尝试连接 USB HID 设备
    var connected = usb.connect();
    if (connected !== 1) {
        console.error('USB 连接失败,错误码:', connected);
        return;
    }
    console.log('USB HID 设备连接成功');

    // 初始化屏幕分辨率(必须!)
    usbInit(rsScreenWidth, rsScreenHeight);

    // 执行自动化任务
    runAutomation();
}
步骤 2:初始化 HID 设备(关键!)
function usbInit(screenWidth, screenHeight) {
    // 向设备发送屏幕分辨率信息(协议规定格式)
    // 命令头 0x0E + 宽度(2字节) + 高度(2字节)
    usb.send([0x0E].concat(int2bytes(screenWidth, 2), int2bytes(screenHeight, 2)));
    sleep(3000); // 等待设备配置完成
}
步骤 3:执行自动化操作
function runAutomation() {
    // 返回桌面
    usb.home();
    sleep(1000);

    // 点击某个应用图标(假设位于屏幕中央偏下)
    usb.click(540, 2000); // 以 1080×2400 为例
    sleep(2000);

    // 向上滑动页面
    usb.swipe(500, 1800, 500, 600, 800); // 持续 800ms
    sleep(1000);

    // 点击“确认”按钮
    usb.click(600, 1900);
    sleep(500);

    // 返回上一页
    usb.back();
}

四、注意事项

  1. 必须初始化分辨率:HID 设备需知道屏幕尺寸才能正确映射坐标,否则点击位置会偏移。
  2. 坐标单位为像素:务必根据实际设备分辨率调整,避免硬编码。
  3. 适当添加延时:USB 指令执行需要时间,关键操作后应使用 sleep() 防止过快。
  4. 无需无障碍权限:此方案完全绕过 Accessibility,适用于强风控场景。
  5. 硬件兼容性:确保 HID 固件遵循冰狐定义的通信协议(命令码如 0x09=移动、0x0A=释放等)。

五、实战例子

下面给出使用usb hid实现携程自动化脚本的例子,仅供参考

function main() {
    requestScreenShot()
   

    var b = usb.connect();
    console.log('connect:', b);
    if (b) {
        usbInit(rsScreenWidth, rsScreenHeight)

        launchApp('ctrip.android.view')

        var enterApp = false
        var startTime = new Date().getTime();
        while (new Date().getTime() - startTime < 30 * 1000) {
            var ret = ocrFindView('txt:消息|txt:我的')
            if (ret.length > 0) {
                console.log('进入app')
                enterApp = true
                break;
            } else {
                sleep(1000)
            }
        }

        if (!enterApp) {
            console.log('进入app失败')
            return;
        }

        var id = 0
        var name = '万季酒店(扬州东关街瘦西湖店)'
        var year1 = 2025, month1 = 10, day1 = 25
        var year2 = 2025, month2 = 10, day2 = 26

        while (true) {
            var r = callMicroService('xiecheng_get_task', [rsDeviceName])
            console.log('r:', r)
            if (r.length > 0) {
                var success = false;

                id = r.id
                name = r.name
                year1 = r.year1
                month1 = r.month1
                day1 = r.day1
                year2 = r.year2
                month2 = r.month2
                day2 = r.day2


                var tIndex = 0, enter = false;
                while (tIndex < 3 && !enter) {
                    var ret = ocrFindView('txt:旅游地图|txt:搜索')
                    console.log('ret', ret)
                    if (ret.length > 0) {
                        if (ret.tag == '旅游地图') {
                            var x = parseInt(rsScreenWidth / 2)
                            var y = ret.region[1] + tIndex * 10

                            console.log('点击x:', x, '  y:', y)
                            usbClick(x, y)

                            sleep(1000)
                            var ret = ocrFindView('txt:搜索')
                            console.log('ret:', ret)
                            if (ret.length > 0) {
                                console.log('进入了搜索页面')
                                enter = true
                            } else {
                                console.log('没进入搜索页面')
                            }
                        } else {
                            console.log('直接找到了哈')
                            enter = true
                        }
                    } else {
                        console.log('没找到旅游地图')
                    }

                    ++tIndex
                }

                if (enter) {
                    // var name = '北京西站希尔顿惠庭酒店'
                    console.log('进入搜索页面,输入文本')
                    ime.inputText(name)

                    var ret = ocrFindView('txt:搜索')
                    if (ret.length > 0) {
                        console.log('找到搜索')
                        usbClick(parseInt(ret.region[0] + ret.region[2] / 2), parseInt(ret.region[1] + ret.region[3] / 2))

                        sleep(3000)
                        var r = ret.region

                        var nn = name.substring(0, 4)

                        var enter = false, index = 0;
                        while (!enter && index < 3) {
                            var ret = ocrFindView('txt*:' + nn, {debug: true, region:[0, r[1] + r[3], rsScreenWidth, rsScreenHeight / 2]})
                            if (ret.length > 0) {
                                console.log('点击酒店名')
                                usbClick(parseInt(ret.region[0] + ret.region[2] / 2), parseInt(ret.region[1] + 100))

                                var ret = ocrFindView('txt:免费停车|txt:充电柱|txt:洗衣房|txt:景点', {debug: true})
                                if (ret.length > 0) {
                                    if (ret.tag == '景点') {
                                        console.log('没有点击到')
                                    } else {
                                        console.log('进入了酒店页面了:' + ret.tag)
                                        enter = true
                                    }
                                } else {
                                    console.log('进入了其他页面,退出')
                                    break
                                }
                            }
                            ++index;
                        }

                        if (enter) {
                            console.log('进入了酒店页面xxxx')
                            var ret = ocrFindView('txt*:月@txt*:日')
                            if (ret.length > 0) {
                                console.log('找到日期')
                                usbClick(parseInt(ret.region[0] + ret.region[2] / 2), parseInt(ret.region[1] + 20))

                                if (ocrFindView('txt*:日历').length > 0) {
                                    console.log('进入日历')
                                    selectDate(year1, month1, day1, 1)
                                    if (month1 == month2) {
                                        selectDate(year2, month2, day2, 1)
                                    } else {
                                        selectDate(year2, month2, day2, 2)
                                    }
                                } else {
                                    console.log('进入日历失败')
                                }

                                var count = randInt(3, 6)
                                while (count > 0) {
                                    var x1 = randInt(100, 200), x2 = randInt(400, 500)
                                    var y1 = randInt(parseInt(rsScreenHeight / 2 + 100), rsScreenHeight - 200), y2 = randInt(200, parseInt(rsScreenHeight / 2 - 100))
                                    console.log(`x1:${x1}, y1:${y1},x2:${x2}, y2:${y2}`)
                                    usbSwipe(x1, y1, x2, y2)

                                    sleep(2000);
                                    --count;
                                }

                                var r = callMicroService('xiecheng_update_status', [id, 2])
                                console.log('通知微服务已经完成:', r)

                                success = true;
                            } else {
                                console.log('没找到日期')
                            }
                            // console.log('r:', ret)

                            // console.log(ocr())

                        } else {
                            console.log('没进入酒店')
                        }
                    } else {
                        console.log('没找到搜索')
                    }
                } else {
                    console.log('没进入搜索界面')
                }

                if (!success) {
                    var r = callMicroService('xiecheng_update_status', [id, -1])
                    console.log('通知微服务任务失败:', r)
                }
                var r = ocrBack2Page('txt:消息|txt:我的', {backMode: 3})
                console.log('返回首页:', r)

            } else {
                console.log('没有获取到任务')
                sleep(2000)
            }
        }
    }
    console.log('end')
}

function selectDate(year, month, day, SK) {
    var d = new Date()
    d.setYear(year)
    d.setMonth(month - 1)
    d.setDate(day)

    var dd = d.getDay(); // 0位周日
    console.log('dd:', dd)

    var arr = [day]
    var i = dd - 1, k = day - 1;
    while (i >= 0) {
        if (k > 0) {
            arr.push(k)
        } else {
            break;
        }
        --i;
        --k;
    }
    i = dd + 1, k = day + 1
    while (i <= 6) {
        if (k <= 31) {
            arr.push(k)
        } else {
            break;
        }
        ++i;
        ++k;
    }

    console.log('arr:', arr)

    var vv = []
    var sk = 0;
    var ret = ocr();
    for (var item of ret) {
        var text = item.text
        if (text.includes('年') || text.includes('月')) {
            console.log('ss:', text);
            ++sk
        }
        if (sk == SK) {
            // 只计算合适范围内的
            var x = parseInt(text.trim())
            if (null != x) {
                if (arr.includes(x)) {
                    console.log('text:', text, ' top:', item.region[1])
                    vv.push(item.region[1])
                }
            }
        } else if (sk > SK) {
            break;
        }
    }

    console.log('vv1', vv)
    if (vv.length >= 3) {
        // 去掉一个最大值和最小值
        vv.sort()
        vv.shift()
        vv.pop()

        console.log('vv2', vv)
    }

    var tt = parseInt(vv.sum() / vv.length)

    var ll = parseInt((rsScreenWidth / 7) * dd)
    console.log('ll:', ll, 'tt:', tt)

    usbClick(ll + 30, tt + 30)
}

function usbInit(screenWidth, screenHeight) {
    usb.send([0xe].concat(int2bytes(screenWidth, 2), int2bytes(screenHeight, 2)))
    sleep(3000)
}

function usbClick(x, y, duration) {
    if (null == duration) {
        duration = 200
    }
    usbMouseMove(x, y);
    sleep(duration);
    usbMouseUp(x, y);
}

function usbMouseMove(x, y) {
    usb.send([0x9].concat(int2bytes(x, 2), int2bytes(y, 2)))
}

function usbMouseUp(x, y) {
    usb.send([0xa].concat(int2bytes(x, 2), int2bytes(y, 2)))
}

function usbSwipe(x1, y1, x2, y2, duration) {
    if (null == duration) {
        duration = 300
    }

    var moveCount = randInt(6, 20)
    var dTime = parseInt(duration / moveCount);
    var startX = 0, startY = 0, endX = 0, endY = 0, useCurve = false;
    if (x1 != x2 && y1 != y2) {
        if (x1 < x2 && y1 > y2) {
            startX = x1;
            startY = y1;
            endX = x2;
            endY = y2;
            useCurve = true;
        } else if (x1 > x2 && y1 < y2) {
            startX = x2;
            startY = y2;
            endX = x1;
            endY = y1;
            useCurve = true;
        }
    }

    if (useCurve) {
        var x = endX - startX;
        var y = startY - endY;

        var a = (endX - startX) / ((endY - startY) * (endY - startY));
        var yb = y1;
        var ya = (y2 - yb) / (moveCount * moveCount);
        for (var i = 1; i < moveCount; ++i) {
            var fY = (ya * i * i + yb);
            var y = parseInt(fY);
            var x = parseInt(a * (y - startY) * (y - startY) + startX);
            if (i > 1 && i < moveCount - 1) {
                x += randInt(0, 5) - 2;
                y += randInt(0, 5) - 2;
            }
            usbMouseMove(x, y)
            sleep(dTime);
        }
    } else {
        usbMouseMove(x1, y1)
        var a = (x1 - x2) / (y1 - y2);
        var b = x1 - a * y1;

        var yb = y1;
        var ya = (y2 - yb) / (moveCount * moveCount);
        for (var i = 1; i < moveCount; ++i) {
            var fY = (ya * i * i + yb);
            var y = parseInt(fY);
            var x = parseInt((a * y + b));
            if (i > 1 && i < moveCount - 1) {
                x += randInt(0, 5) - 2;
                y += randInt(0, 5) - 2;
            }
            usbMouseMove(x, y)
            sleep(dTime);
        }
    }

    usbMouseMove(x2, y2)
    sleep(10);
    usbMouseUp(x2, y2)
}

function usbHome() {
    usb.send([0x20])
}

function usbBack() {
    usb.send([0xd])
}

function usbRecentApps() {
    usb.send([0x8, 0x4, 0x2b])
}

function usbCopy() {
    usb.send([0x08, 0x01, 0x06])
}

function usbPaste() {
    usb.send([0x08, 0x01, 0x19])
}

function usbCut() {
    usb.send([0x08, 0x01, 0x1B])
}

function usbSelectAll() {
    usb.send([0x08, 0x01, 0x04])
}

实战派 ESP32-S3,双模无线开发板

ESP32-S3 原生支持 ESP-IDF,WiFi + 蓝牙一次搞定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值