风控这个圈子里,“异常登录检测”一直是账号安全里最靠前的防线。它的核心就两块:地理位置和设备指纹。地理位置决定“这个请求是不是从用户常住地发出来的”,设备指纹决定“这个请求是不是用户平时用的那台机器”。只要这两者中任意一项对不上,系统就会判定风险、弹出验证,甚至直接拒绝登录。
我真正开始研究这个题目,是因为一次线上告警:某账号在极短时间内,先后出现了两个相距上千公里的登录地,但设备指纹几乎没变。第一反应是误判,查完才发现攻击者把“地理位置”和“设备指纹”这两项信任锚点全都伪造了。
这篇内容不是教你怎么做真正的入侵,而是从红蓝对抗视角讲清楚绕过手法背后的原理,以及防守方怎么反制。我所有的实验都在自有或已授权环境中完成,没有对生产系统做过任何未授权操作。如果你所在团队正在做登录风控建设,这篇文章可以直接当加固清单用。
1. 项目背景:为什么风和控会败给“假地理、假设备”
先交代清楚一个核心观点:异常登录检测的目标不是证明“这个人是本人”,而是证明“这次请求和本人的历史行为足够相似”。地理位置和设备指纹之所以被大量平台采用,是因为它们成本低、采集容易、特征稳定。但也正因为这两个特征是“可采集”的,它们天然就是“可伪造”的。
1.1 异常登录检测到底在检测什么
登录场景下的风控,本质是在回答三个问题:
- 这个账号平时在哪里登录?
- 这个账号平时用什么设备登录?
- 这次登录和上面的历史画像是否一致?
地理位置解决了第一个问题,设备指纹解决了第二个问题,两者组合起来就是第三个问题的核心输入。除此之外,风控系统还会叠加登录时间、操作行为、IP信誉、多因素验证等手段,但地理位置和设备指纹始终是最先被计算的两个特征。
这里有一个容易忽略的细节:异常登录检测并不追求“百分百识别攻击者”,它追求的是“在可接受误杀率下,尽量拉高攻击成本”。所以研究绕过不是为了炫技,而是为了搞清楚哪部分成本可以被轻易抹掉,哪部分抹不掉。抹不掉的才是防守方该重点投入的地方。
1.2 两类绕过思路的分类
绕过地理位置和设备指纹检测,本质上只有两条路:直接改特征,或者让特征看起来“合理”。
直接改特征,指的是改地理位置字段、改设备指纹参数、伪造浏览环境。这类手法技术门槛低,很多是前端调试就能完成,但容易被后端一致性校验识破。
让特征“合理”,指的是模拟一个真实用户的完整画像:用全新的设备指纹、搭配地理位置规律、在合理时间段登录。这类手法更像是“造人”,它对风控的威胁要大得多,因为它对抗的不只是一个字段,而是整个行为模型。
我这次的项目,两条路都做了实验验证。下面从地理位置开始讲。
2. 地理位置检测的底层逻辑与绕过窗口
2.1 IP归属地、GPS、Wi-Fi定位:三种信号各有什么盲区
常见的地理位置采集源有三个,它们各自的精度和盲区完全不同:
| 采集源 | 获取方式 | 精度范围 | 典型盲区 |
|---|---|---|---|
| IP归属地 | 服务器从TCP连接解析出口IP | 城市级到省级 | 大型网络出口、动态IP、IDC机房 |
| GPS定位 | 移动端系统接口 | 米级 | 室内、权限被拒、模拟器伪造 |
| Wi-Fi定位 | 扫描附近AP信息后查库 | 街道级 | AP库更新慢、虚拟机无Wi-Fi模块 |
单纯依赖IP归属地的风控,最容易遇到一个问题:用户在公司、学校、公共Wi-Fi环境下,出口IP往往属于某个城市的IDC或大型网关,而不是住宅宽带。这时单纯按“IP城市与注册地不同”判异地,会产生大量误伤。
反过来,单纯依赖GPS定位也有问题。用户在室内打开网页端登录时,浏览器无法直接拿到GPS权限,通常只能靠IP和Wi-Fi信息兜底。移动端虽然能拿GPS,但模拟器框架可以随意伪造坐标。
我的结论是:地理位置信号不能作为单一判断条件,必须把三种信号交叉起来。比如GPS显示在北京、Wi-Fi显示在北京某商圈、IP归属地也是北京,三者才互相印证。
2.2 地理位置“绕过”的三种常见手段与识别点
从防御视角看,攻击者改地理位置通常有三种手段:
第一,直接改前端请求参数。很多业务把定位信息放在请求体里,比如经纬度、城市ID。这种情况下,用抓包工具改一下参数就能伪造。识别点在后端:前端上报的GPS坐标会直接存下来,但后端还可以用TCP出口IP反查归属地。两边一旦出现“城市级”的不一致,就有问题。所以“前端定位参数”永远不能直接信任。
第二,通过修改系统定位服务来伪造GPS。在Android上,开发者选项里自带“模拟位置信息”,某些调试框架还能提供更细粒度的坐标伪造。识别点在于:真实GPS信号伴随gps_status、卫星数量、水平精度因子这些辅助字段,纯模拟器伪造往往缺失这些字段。后端只要校验“定位来源标志位+定位精度半径”,就能筛掉大部分模拟定位。
第三,借助网络链路改变出口IP归属地。这也就是常说的“换区登录”场景。攻击者让请求从目标城市的网络出口发出,从而让IP归属地看起来合理。识别点比较复杂,主要看两点:一是链路延迟是否与地理距离矛盾,比如“人在北京、出口在乌鲁木齐”,但一次往返只有10毫秒,这明显说不过去;二是出口IP是否属于高匿名网络,这类IP通常长期不具备稳定归属地特征。
这里必须强调:网络链路变更属于网络层对抗,不在我的项目里做深度还原。我关注的是它在风控层面产生的信号特征:链路延迟、IP历史、ASN归属、出口类型。这些特征后端是能拿到的,只是很多团队没接。
2.3 为什么“异地登录”不能只看单次位置
误杀率高的风控规则,往往都有一个通病:只判断“本次登录位置是否等于历史常住位置”。
正确的做法是看“位置演化序列”。一个用户可能上午在公司城市A登录,下午出差到城市B登录,晚上回到城市C登录。如果把“常住地”压成一个值,那么城市B和C都会被判定为异地。但如果把所有登录位置按时间轴展开,观察速度、频率、转移路径,就会发现城市B和C是合理移动,因为它们伴随差旅轨迹,而且两次登录之间有时间间隔。
反例呢?一个账号在10分钟内,从北京登录一次、从深圳登录一次。这两个城市之间无论如何也不可能在10分钟内完成物理移动。这就是位置突变检测。规则上要做“最小跨城市耗时”约束,超过阈值才判风险。
我还做了一个“位置熵”的实验:把用户过去30天的登录城市序列做熵值计算。差旅频繁的用户熵值高,规则阈值自动放宽;常年两点一线的用户熵值低,一旦出现新城市,风险系数大幅上调。这个思路比固定阈值靠谱得多。
3. 设备指纹的采集维度与可伪造性分析
设备指纹是另一根支柱。它由浏览器或客户端运行时暴露的硬件和软件信息组合而成。严格来说,没有任何一项指纹信息是设备与生俱来不可改的,它们只是一堆特征的加权组合。
3.1 常见采集维度一览
我整理了一份内部分类清单,把指纹采集项分成五类:
- 硬件类:显卡型号、CPU核数、内存大小、屏幕分辨率、触控支持
- 软件类:操作系统、浏览器版本、内核版本、语言列表、时区
- 渲染类:Canvas指纹、WebGL指纹、音频上下文指纹
- 存储类:Cookie、LocalStorage、IndexedDB、Flash存储
- 行为类:鼠标移动轨迹、键盘输入节奏、滚动行为
用生活化类比来解释:设备指纹就像一个人的“穿搭组合”。一个人的穿搭会有风格倾向,但如果只盯着某一件衣服判断,就很容易被换装骗过。把整套穿搭看成整体,匹配度就会高很多。
3.2 Canvas、WebGL、AudioContext三大指纹为什么不容易伪造
Canvas指纹的原理,是让浏览器绘制一段文字或图形,然后提取像素数据生成哈希值。不同显卡驱动、字体渲染方式、反锯齿算法都会产生细微差异,而这个差异可以被精确测量。
WebGL指纹类似,但它调用的是GPU能力,差异点更明显。不过有一个重要前提:同一个显卡驱动在不同版本的浏览器上表现也可能不同,所以指纹库要做版本管理。
AudioContext指纹,是通过测量音频处理延迟和环境噪声特征生成哈希值。它的伪造难度在于,Web Audio API的音频渲染链路非常底层,常规的浏览器插件无法完全模拟出完全一致的音频缓存和延迟曲线。
我见过一个比较好的“反绕过”做法:不只用哈希值本身,还把Canvas指纹、WebGL指纹、AudioContext指纹分别存储,比对“多指纹引用一致性”。攻击者改了Canvas特征,但WebGL特征没改,后端比对就会发现“子指纹组合在历史记录里从未出现过”。
3.3 不要迷信UA和Screen分辨率
几乎每个前端采集SDK开头都会拿navigator.userAgent和window.screen。这两项最容易伪造,也最不应该作为独立风险因子。
UA字符串本质上是一段可自定义的文本,所有主流浏览器都允许修改。screen分辨率也可以直接模拟,甚至在无头浏览器里可以通过启动参数任意指定。实际风控里,大量误杀都源于“分辨率换了”就被判新设备。
正确用法是:UA和screen只是辅助维度,真正决定设备唯一性的是“硬件特征组合+持久化标识组合”。如果一套指纹只包含UA、语言、时区、屏幕四项,那它的区分度和对抗能力都很弱。建议至少把Canvas、WebGL、音频指纹、字体列表纳入组合。
4. 风控决策引擎设计与评分规则实战
有了地理位置和设备指纹,下一步就是怎么把它们变成可执行的拦截策略。阈值、权重、决策树,这三样缺一不可。
4.1 从特征到风险分的映射
我在项目里把风险评分分成三个维度:位置风险、设备风险、行为风险。每个维度下再细分指标,最后加权得到总分。
位置风险的分值设计:
- 登录城市在历史城市集合内:0分
- 登录城市在历史城市集合外,但距离小于300公里:20分
- 距离超过300公里,且出行速度正常:30分
- 距离超过300公里,且出行速度异常:50分
- 登录城市历史从未出现,且IP类型为IDC机房:60分
设备风险的分值设计:
- 设备指纹完全命中历史记录:0分
- 指纹未命中,但含稳定持久化标识:20分
- 指纹未命中,且无任何持久化标识:40分
- 多子指纹组合在历史中从未出现:50分
- 指纹为空或包含非法字段:55分
行为风险的分值设计:
- 登录时间在用户活跃时段内:0分
- 登录时间在活跃时段外,但操作链路正常:20分
- 连续多次登录尝试,且每次指纹都不同:45分
- 登录后短时间内尝试修改敏感信息:60分
4.2 规则引擎的判定逻辑
光有分数不够,还要有规则决策矩阵。我给出的伪代码如下:
def risk_decision(geo_risk, device_risk, behavior_risk): total = geo_risk + device_risk + behavior_risk # 阈值分段处理 if total >= 120: return "BLOCK" # 直接拦截 elif total >= 80: return "CHALLENGE" # 强制二次验证 elif total >= 50: return "ALERT" # 告警并观察 else: return "PASS" # 放行 # 特殊规则:位置与设备同时高危时,直接升级 if geo_risk >= 60 and device_risk >= 40: return "BLOCK"这个决策逻辑看起来简单,但你要注意几个细节:阈值不能是拍脑袋写的,必须用历史日志回放来定。比如拉一个月真实登录日志,把“风险分大于80”的样本全部人工标注一遍,看多少是真人、多少是攻击,然后调整阈值。这就是常说的分数回灌。
4.3 阈值调优时最容易踩的坑
第一个坑是“阈值定高了就万无一失”。实际上,阈值越高,误杀越少,但漏过攻击越多。你要做的不是追求单次拦截率,而是和运营同学对齐“可接受误杀率”。业内比较常见的做法是:误杀率压在万分之五以内,拦截率尽量做到九成以上。
第二个坑是“新设备判风险”一刀切。用户的手机系统更新、浏览器换版本、清理Cookie都会导致设备指纹变化。如果每次指纹变化都判风险,正常人会被烦死。我做了一点优化:把“完全指纹未命中”和“持久化标识能对上”分开处理。前者是真正的未知设备,后者只是指纹部分更新,风险权重低一档。
第三个坑是“规则没有版本管理”。风控规则几乎每周都在变,如果没做规则版本化,出问题后根本没法快速回滚。我建议所有规则集都配上版本号,每次变更都走灰度:先切5%流量,观察一小时误杀率,再逐步扩大到全量。
5. 受控环境下的绕过实验记录
下面进入实战环节。这部分所有操作都在我自建的实验环境里完成,不涉及任何第三方线上系统。我完整还原了“异地登录+新设备”的绕过链路,并用日志分析了每个环节的蛛丝马迹。
5.1 实验环境配置
我准备了三台主机和一个模拟移动端:
- 一台宿主机,充当“攻击者控制机”,部署抓包工具和签名脚本
- 一个Linux容器,模拟目标业务的前端环境,里面装了Chromium内核
- 一台Android虚拟机,模拟移动端登录场景
- 一个模拟GPS位置的应用,可以给虚拟机制造任意经纬度坐标
在实验开始前,我在宿主机上启动了抓包工具,记录所有HTTP请求和响应。目标业务是自建的测试站,它模拟了真实风控SDK上报地理和设备字段的过程。
5.2 第一次实验:直接改前端定位参数
我先做了一个最基础的尝试:用抓包工具拦截移动端的登录请求,把请求体里的经纬度字段从北京改成纽约。
结果很有意思:测试站只校验了请求体里的定位参数,完全没做服务端IP复核。因此,这次伪造成功。登录页面收到的定位信息是纽约,而出口IP仍然是北京。
从防守方视角看,这就是典型的前后端不一致问题。修复方式很简单:后端用TCP连接信息里的出口IP另算一份归属地,然后和前端上报定位比对。两者城市不一致,直接标记高风险。
5.3 第二次实验:构造全新设备指纹
我接着测试设备指纹。用AutoJS脚本在Android虚拟机里改了一批字段:UA字符串、屏幕分辨率、语言列表、时区,并清空了WebView缓存。然后重新发起登录。
这一次,风控系统判断为“新设备”,但没有直接拦截,只是要求做短信验证。原因是后端还有一个持久化标识——Android ID——被我保留了下来。从这次实验能看出:持久化标识和设备指纹是两套并列体系,攻击者只改了指纹而不改持久化标识,依然会被识别出“部分匹配”。
真正麻烦的情况是攻击者把设备指纹、持久化标识全部重建。这时候风控能用的牌就只剩行为特征和账号历史了。所以我的加固方向是:把设备指纹识别升级为“身份图谱关联”,不只是比对新老指纹,还要看“这套指纹组合曾在多少个账号上用过”。
5.4 第三次实验:镜像真实设备的完整画像
第三次实验我模拟了一个更高级的攻击者:先抓取一台真实设备的完整指纹组合,然后把它移植到实验环境的浏览器容器里。具体做法是修改浏览器指纹生成器的配置,让Canvas、WebGL、音频指纹的哈希值全部替换为目标设备的数值。
这次实验验证了一个防守短板:当攻击者拥有一套完全真实的指纹副本时,指纹比对几乎失效。但我在日志里找到了另一个突破口:时间节律。
真实用户在登录前,通常会先打开登录页、移动鼠标、点击输入框、敲击键盘,然后才提交。而我导入指纹后直接调用了登录接口,整个页面停留在登录页的时间只有几百毫秒,也没有键盘输入事件。虽然指纹完全一致,行为序列却“干净”得不正常。
后面我把这类时序特征加进了风控模型,专门识别“机械式登录”。指纹相似又怎么样?你的操作节律不像真人,照样给你标记。
6. 常见问题与排查实录
做了这么多实验,整理了十几条高价值经验。这里挑最常见的三类问题展开。
6.1 误杀:同一家庭网络下多设备登录,被判新设备风险
我遇到过一个典型误杀:一家三口的手机在同一个家庭Wi-Fi下登录同一个平台,但每台手机的指纹完全不同。风控系统只看到“同一出口IP、三套从未见过的新指纹”,于是给三个账号全部弹了二次验证。
修复方案是把“IP聚簇”和“设备聚簇”分开。同一出口IP下,如果多个设备指纹在短时间内陆续出现,并且账号之间没有明显的关联异常,不应该直接判风险,而是把整簇设备标记为“弱信任聚簇”。等后续设备在别的网络环境下活动时再逐步积累信任。
6.2 漏报:真实设备被镜像后绕过了全部静态校验
这就是上面第三次实验暴露出的问题。静态指纹一旦被完整复制,规则引擎的“指纹匹配”能力就直接归零。破解思路只有一个:引入动态挑战。比如登录时生成随机的一次性绘制指令,要求前端在Canvas上绘制特定图形,后端比对绘制结果。镜像指纹不具备动态执行能力,因为真实设备在拿到随机指令后会产生新的绘制结果,而镜像只能返回预设值,两者不一致就会暴露。
6.3 风控命中后,处置方式怎么选
很多人觉得风险评分高就该直接拦截。实操中不一定。我通常会把处置分成四档:
| 风险等级 | 处置方式 | 用户体感 | 适用场景 |
|---|---|---|---|
| 低 | 放行并记录 | 无感知 | 正常用户 |
| 中低 | 轻量验证 | 滑块 | 新IP、新设备但行为正常 |
| 中高 | 强制验证 | 短信或扫码 | 异常位置+新设备 |
| 高 | 拦截并告警 | 禁止登录 | 多账号批量登录、指纹空值 |
这里有一个重要原则:处置方式必须可回退。如果用户通过短信验证,系统要把该设备的信任等级暂时提高,并同步更新历史画像。不能每次登录都让同一用户做验证,否则用户会流失。
7. 从单点对抗到全链路风控闭环
做完这个项目,我最大的体会是:地理位置和设备指纹只是异常登录检测的两个零件,真正抗打的风控系统需要把“采集—评分—决策—复盘”串成闭环。
7.1 采集层要真实、冗余、带过期时间
采集不是越全越好。关键信号要做冗余交叉:GPS、Wi-Fi、出口IP三路信息至少要能对上两路;指纹采集要区分“核心指纹”和“辅助指纹”,核心指纹过期后要重新采集,不能无限期信任。
我给团队定的采集规范是:核心指纹保留90天,超过90天自动重新采集一次;辅助指纹实时更新,不参与唯一性判断。
7.2 评分层要引入“身份置信度”衰减机制
把用户行为建模拆成两块:静态画像和动态置信度。静态画像是历史记录的汇总,动态置信度则是“最近30天与静态画像的匹配程度”。每次成功登录都会提高置信度,每次异常行为都会降低置信度。置信度低于某个阈值时,即使地理位置和设备指纹都看起来正常,也要做一次轻量验证。
这个机制可以防住“长期潜伏后换完全匹配的指纹”,因为动态置信度知道你过去的操作节律,新指纹再完美,也无法在一夜之间建立高置信度。
7.3 永远保留一条人工申诉通道
再完美的规则也有误杀的时候。我在项目里专门保留了“申诉后立即提审”的接口,人工审核通过后,把该设备加入白名单,并把误杀样本回灌到模型训练集里。这条通道有三重价值:降低用户投诉、积累真实样本、持续校准规则阈值。
最后分享一个实在的操作习惯:每次风控规则上线后,我会在第二天早上看三个指标——风险命中率、用户申诉率、误杀率。这三个数字没有异常,再决定是否扩大流量。规则不是写完就能放心的东西,它和生活里的很多东西一样,需要持续养护,才会真正可靠。