目标:某度旋转验证码v2,fs参数逆向分析
网站:aHR0cHM6Ly96aXl1YW4uYmFpZHUuY29tL2xpbmtzdWJtaXQvdXJs
加密算法:AES、MD5、SHA1、SHA256、SHA512、SHA3
抓包分析init接口有四个请求参数,ver为固定值1,_为13位时间戳,refer为该网站的url,ak是固定值,相当于盾ID,不同网站不一样,很多厂商的验证码也都会有这么一个参数:
init接口响应返回的参数中as、tk在后续请求中会用到:
style接口响应返回的参数中,path即旋转验证码的底图,backstr是生成fs参数值的关键部分:
转动验证码后,会抓包到log接口,请求参数中,ak为盾ID,tk、as均是之前的接口响应返回的,fs参数需要逆向分析,由轨迹、旋转角度、as、backstr参数等加密生成。最新的v2较之前多了一个浏览器指纹参数fuid,不同浏览器的值不一致,相同浏览器的值短时间内不会改变:
Chrome:"FOCoIC3q5fKa8fgJnwzbE67EJ49BGJeplOzf+4l4EOvDuu2RXBRv6R3A1AZMa49IZbsw3/U3NYEqD0LjhKzgMn8fIES5OyXlgwN5I+F8wHowpWWfXkQJw8/9AsO5Q2VOvnc2JlHGIlGS8Vq2z4OA80lVLon08EG3PPxkVZGm39fDi2exK9NDrZB+tNLX6ISxE5PzBgXpCOJ6oP9F1B0OBWaCMD/m01n8FhdDNCvP8EO5cetU79+pgL+ECRdtN6V4VElGJE0mxV4+4Zq4Jf/Xe/q8CkoTNf7Ti1glGYmN32UM9dg0uX+VzET/mmTRe4Dt+MuVHSzsI/bKCjPbpaOqfM8UsxDJUG9hyrGZ8QHa1kC04aTxkkTxI275dv3+ijS1zkWOdjFiy1eD/0R8HcRWYp2smk9EmXBkIAHL4H0gC9lQtdjey37/kyl4JA9Fp4zjuVO0arsD8MrGy1divU++B1KdawGqXpnbOcHZ3CctNGrpgmswaScc6DNWb34jFj0X3tdRE0uuHuqiYa5BClFS2V0TCorKi4CobgR419xWaX8IKLJiaNNLOShWdZdlQO2DXXVxcinzKHqUvWTYx45jsiUVlY78AHQGol6CJLQQ8Q797MShlazvdSwPXgJP5z0uMJp9L+3x/Y2GGhW5sit55sFuMXafALTYf69FCUw5+nVIRs150a4+KK+tA0Eu7Itiu3dM2pflKYWwPE6SDZznyejQ08vd+HpXRB/zhfSUcIYlT5gFEiMIA6SXZCo/XT7vC8D3gHdN+yr46XdVol/WkjFQof0JQH/Vhjj5C1xcAyNxq/VVBT01vdKk6zo6c08e84FEVMLd0m3XWtjFOYu7wRI7lldw2pSxyGnWvA4aiYWcWvvKNJtqB8wHqc5RPr9KRzhbxJnTM5K1vTx4xT/1ZUR3pU7nQKZo/4kP9XycIr/Jg3XMRSnqCBUJlagKAFPt2HF0LdsSk4WWcldb97Ar584nVGbSjPXEUVH0VgbUEm+dADzPoLP+NPMYOyhwgfADiqWaXyKT4UNESYXsPBkdGk6mLCaNSEQsDN1G2677Se3qjzDcyXBnEmHEFptRbmyJzKJ73veHPqfFYtsHO9jH0XnhYk8zKdRuqQ7dnuNIDwxm3UCPo22uFI0ZcgPvQm01s+8jYiMEFJDVra9jWyWTdMpMuhT3p2yYLf70CvUwIkw="
过一段时间,fuid的值是会变化的:
fuid在文件中生成,由user-agent、canvas、plugins等浏览器属性构成,可以逐个跟栈分析,F方法是对URI进行编码,即encodeURIComponent(),U方法为AES加密,加密模式为ECB,填充方式为PKCS7,key为固定值:
ECB:ElectronicCodeBook(电子码本模式),是一种基础的加密方式,密文被分割成分组长度相等的块(不足补齐),然后单独一个个加密,一个个输出组成密文;
PKCS7:在填充时首先获取需要填充的字节长度=块长度-(数据长度%块长度),在填充字节序列中所有字节填充为需要填充的字节长度值。
相关环境:
br{br"userAgent":"Mozilla%2%20(Windows%20NT%2010.0%3B%20WOW64)%20AppleWebKit%2%20(KHTML%2C%20like%20Gecko)%20Chrome%2%20Safari%2",br"canvas":"2a5a8f1d4173bc025dab2d50816ba134",br"language":"zh-CN",br"colorDepth":"24",br"deviceMemory":"8",br"hardwareConcurrency":"8",br"screenResolution":"1920%2C1080",br"availableScreenResolution":"1032%2C1920",br"timezoneOffset":"-480",br"timezone":"",br"sessionStorage":"true",br"localStorage":"true",br"indexedDb":"true",br"addBehavior":"false",br"openDatabase":"true",br"cpuClass":"",br"platform":"Win32",br"plugins":"null",br"webgl":"1af17d7a73799a2516da34f3b37bb363",br"webglVorAndRerer":"Google%20Inc.~ANGLE%20(NVIDIA%20NVS%205400M%20Direct3D11%20vs_5_0%20ps_5_0)",br"adBlock":"false",br"hasLiedLanguages":"false",br"hasLiedResolution":"false",br"hasLiedOs":"false",br"hasLiedBrowser":"false",br"touchSupport":"0%2Cfalse%2Cfalse",br"fonts":"42",br"audio":"null"br}验证码未转正,响应返回的op值为3:
验证码转正,则响应返回的op值为1:
提交成功---{"over":0,"status":0}
验证码信息有误---{"status":101}
未添加cookies---notallowed
逆向分析先分析第一个:
=(0,)((),)
()是将转换成字符串形式,在第12871行打下断点,由backstr及鼠标轨迹等参数构成,backstr参数是style接口响应返回的,ac_c是旋转比例,现在的底图是AI生成的,和之前的风景图不一样了,识别模型最好重新训练,轨迹和旧版不一样,需要模拟构造不能直接固定,且校验相对严格,轨迹中有个参数很重要:
向上跟栈,即可定位到计算ac_c的位置:
brNumber((/(e-52)).toFixed(2))
是滑动的距离,e为定值290,实际上就是滑动条能够滑动的最大长度:
主要包含as参数,该参数是init接口响应返回的:
()、作为参数传递到函数中,进行加密处理之后生成了第一个fs值:
鼠标选中后跟进去:
encrypt(key,word,!0)即关键加密函数:
先来分析下传入的三个参数,!0为true,word参数即(),key参数定义在第13312行,旧版v2的fs参数是AES加密,key为"as+appsapi2",新版的key经过getNewKey()方法做了进一步的处理,是init接口响应返回的,跟进到getNewKey函数中去:
t即as,e为t+"appsapi2",r取了e的最后一位字符,下面是一段switchcase语句,根据r的值选择相应的加密算法,截取加密后的字符串的前16位,即是key值,目前主要碰到过三种加密算法,MD5、SHA3-256以及SHA3-512,以下通过JavaScript对其进行复现:
constCryptoJS=require('crypto-js');brbrfunctiongetNewKey(as){br/**br*encryptedStr(SHA3-256)---f25f1614appsapi2br*encryptedValue---49d3a9685870cc30f63330b8136c7adfdb8859c6b538308992a1c9a456db2e59br*br*encryptedValue(MD5)---5e4ebc8cappsapi2br*encryptedValue---c30b8b5289e46489598de382a658cc7fbr*/brvarencryptedStr=as+"appsapi2";brvarr=(,1);brswitch(true){brcase['A','B','C','D','E','F','G','a','b','c','d','e','f','g'].includes(r):brencryptedValue=(encryptedStr).toString();brbreak;brcase['H','I','J','K','L','M','N','h','i','j','k','l','m','n'].includes(r):brencryptedValue=(encryptedStr).toString();brbreak;brcase['O','P','Q','R','S','T','o','p','q','r','s','t'].includes(r):brencryptedValue=(encryptedStr).toString();brbreak;brcase['U','V','W','X','Y','Z','u','v','w','x','y','z'].includes(r):brencryptedValue=(encryptedStr).toString();brbreak;brcase['0','1','2','3','4'].includes(r):brencryptedValue=(encryptedStr,{outputLength:256}).toString();brbreak;brcase['5','6','7','8','9'].includes(r):brencryptedValue=(encryptedStr,{outputLength:512}).toString();brbreak;brdefault:brencryptedValue=e;br}brvarkey=(0,16);brreturnkey;br}brbrvaras="7d7f8765";brvarkey=getNewKey(as);('key:',key);SHA3-512结果校验一致:
至此key参数分析完了,回到第13315行,跟进到加密函数encrypt中,和旧版v2一样,是AES加密,不同点在于:1.旧版的key=as+"appsapi2",新版key的值为as+"appsapi2"加密后取前16位字符;2.旧版的填充方式padding为Pkcs7,新版的为ZeroPadding:
PKCS7:在填充时首先获取需要填充的字节长度=块长度-(数据长度%块长度),在填充字节序列中所有字节填充为需要填充的字节长度值;
ZeroPadding:在填充时首先获取需要填充的字节长度=块长度-(数据长度%块长度),在填充字节序列中所有字节填充为0。
接下来分析第二个,也就是最终的fs参数的值,同样是经过函数加密生成,不过传入的两个参数发生了改变,common_en的值即第一个的值,backstr以及as是前面接口响应返回的值,key即上文所述的AES算法加密的密钥值:
=(0,)((br{brcommon_en:,brbackstr:}),{brkey:,bras:,brmethod:"aes-ecb"br})在第12873行打下断点,此时第一个参数的值生成了:
在第12882行打下断点,此时生成了第二个参数的值,即最终的fs参数的值,对比验证一下:
结果验证