python&js逆向 破解滑动验证码
现在的滑动验证码防盗等级都比较高,之前的是一张完整的图片带缺口,现在返回的图片是打乱顺序拼接而成的,所以现在破解不仅要识别滑块的缺口,同时还需要复原完整的图片

一.伪造请求获取验证码图片


可以看到请求中主要的两个参数ctxid和request,所以我们只需要找到这两个参数的生成逻辑并进行请求就可以获取到验证码图片

根据谷歌浏览器关键字搜索便可以找到相关字段的生成逻辑,我们只需要仿照着他的逻辑进行生成即可,值得注意的是request参数进行了双重加密,base64Encode中嵌套一个encrypt。这块我们不需要关注他具体是怎么加密的,我们只需要找到他对应的js方法,通过python直接调用即可
function a() { for (var n, e = "6_11_7_10_4_12_3_1_0_5_2_9_8".split("_"), t = [], r = 0; r < 52; r++) n = 2 * parseInt(e[parseInt(r % 26 / 2)]) + r % 2, parseInt(r / 2) % 2 || (n += r % 2 ? -1 : 1), n += r < 26 ? 26 : 0, t.push(n); return t } function base64Encode(n) { var e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split(""); return function(n) { var t, r, i, o, a, s, c; for (r = i = 0, o = n.length, s = (o -= a = o % 3) / 3 << 2, a > 0 && (s += 4), t = new Array(s); r < o; ) c = n.charCodeAt(r++) << 16 | n.charCodeAt(r++) << 8 | n.charCodeAt(r++), t[i++] = e[c >> 18] + e[c >> 12 & 63] + e[c >> 6 & 63] + e[63 & c]; return 1 == a ? (c = n.charCodeAt(r++), t[i++] = e[c >> 2] + e[(3 & c) << 4] + "==") : 2 == a && (c = n.charCodeAt(r++) << 8 | n.charCodeAt(r++), t[i++] = e[c >> 10] + e[c >> 4 & 63] + e[(15 & c) << 2] + "="), t.join("") }(n) } function encrypt(n) { var e, t = "e98ae8878c264a7e"; function r(n) { if (/^[\x00-\x7f]*$/.test(n)) return n; for (var e = [], t = n.length, r = 0, i = 0; r < t; ++r, ++i) { var o = n.charCodeAt(r); if (o < 128) e[i] = n.charAt(r); else if (o < 2048) e[i] = String.fromCharCode(192 | o >> 6, 128 | 63 & o); else { if (!(o < 55296 || o > 57343)) { if (r + 1 < t) { var a = n.charCodeAt(r + 1); if (o < 56320 && 56320 <= a && a <= 57343) { var s = 65536 + ((1023 & o) << 10 | 1023 & a); e[i] = String.fromCharCode(240 | s >> 18 & 63, 128 | s >> 12 & 63, 128 | s >> 6 & 63, 128 | 63 & s), ++r; continue } } throw new Error("Malformed string") } e[i] = String.fromCharCode(224 | o >> 12, 128 | o >> 6 & 63, 128 | 63 & o) } } return e.join("") } function i(n) { return 4294967295 & n } function o(n, e, t, r, i, o) { return (t >>> 5 ^ e << 2) + (e >>> 3 ^ t << 4) ^ (n ^ e) + (o[3 & r ^ i] ^ t) } function a(n, e) { var t, r = n.length, i = r >> 2; 0 != (3 & r) && ++i, e ? (t = new Array(i + 1))[i] = r : t = new Array(i); for (var o = 0; o < r; ++o) t[o >> 2] |= n.charCodeAt(o) << ((3 & o) << 3); return t } return null == n || 0 === n.length ? n : (n = r(n), t = r(t), function(n, e) { var t = n.length , r = t << 2; if (e) { var i = n[t - 1]; if (i < (r -= 4) - 3 || i > r) return null; r = i } for (var o = 0; o < t; o++) n[o] = String.fromCharCode(255 & n[o], n[o] >>> 8 & 255, n[o] >>> 16 & 255, n[o] >>> 24 & 255); var a = n.join(""); return e ? a.substring(0, r) : a }(function(n, e) { var t, r, a, s, c, l, d = n.length, u = d - 1; for (r = n[u], a = 0, l = 0 | Math.floor(6 + 52 / d); l > 0; --l) { for (s = (a = i(a + 2654435769)) >>> 2 & 3, c = 0; c < u; ++c) t = n[c + 1], r = n[c] = i(n[c] + o(a, t, r, c, s, e)); t = n[0], r = n[u] = i(n[u] + o(a, t, r, u, s, e)) } return n }(a(n, !0), ((e = a(t, !1)).length < 4 && (e.length = 4), e)), !1)) }
mainJs = execjs.compile(open(r"./Utils/main.js",encoding="utf-8").read()) def getCaptcha(contextid): o = "appid=202503141611|ctxid=" + contextid+ "|a=quoteapi|p=|r=" + str(random.random()) oEncrypt=mainJs.call('encrypt',o) # 对字符串进行URL编码 reqStr=quote(mainJs.call('base64Encode',oEncrypt)) print(reqStr) url='api/captcha/get?callback=&ctxid='+contextid+'&request='+reqStr+'&_='+str(int(time.time()*1000)) print(url) initResponse = http.request('GET', url) print('initResponse:') print(initResponse.data) initResult = initResponse.data.decode('UTF-8') initData = json.loads(initResult) if initData["ReturnCode"]=="0" and initData["Data"]["CaptchaType"]=="init": slideResponse = http.request('GET', url) print('slideResponse:') print(slideResponse.data) slideResult = slideResponse.data.decode('UTF-8') slideData = json.loads(slideResult) print(slideData) if slideData["ReturnCode"]=="0" and slideData["Data"]["CaptchaType"]=="slide": print("getCaptcha slide success") captchaInfo={} captchaInfoObj=json.loads(slideData["Data"]['CaptchaInfo']) captchaInfo["bg"]="https://"+captchaInfoObj['static_servers'][0]+captchaInfoObj['bg'] captchaInfo["fullbg"]="https://"+captchaInfoObj['static_servers'][0]+captchaInfoObj['fullbg'] captchaInfo["slice"]="https://"+captchaInfoObj['static_servers'][0]+captchaInfoObj['slice'] return captchaInfo else: print("getCaptcha slide error") return None else: print("getCaptcha init error") return None
二.重新拼接验证码图片

通过上边的操作,我们就能获得一张顺序错乱的验证码,接下来我们就需要重新复原这张图片。接口返回一张错乱的验证码图片,但页面上为什么显示正常呢?那肯定是前端js逻辑把这块给处理了一下,页面就得到了一张正常的验证码图片,那么我们就需要找一下他前端js的处理逻辑了,只要按照他的逻辑处理,我们同样也能复原这张错乱验证码

def get_image(image_url): e = "6_11_7_10_4_12_3_1_0_5_2_9_8".split("_") location_list = [] for r in range(52): location = {} # 计算索引 index = int(r % 26 / 2) n = 2 * int(e[index]) + (r % 2) # 条件判断 if (int(r / 2) % 2) == 0: n += -1 if (r % 2) else 1 # 添加偏移量 if r < 26: n += 26 location['x'] = n % 26 * 12 + 1 location['y'] = (0 - 160)/ 2 if n <= 25 else 0 location_list.append(location) print('==================================') image_result = requests.get(image_url).content image_file = BytesIO(image_result) image = merge_image(image_file,location_list) return image
三.获取缺口距离
def get_distance(url1,url2): ''' 拿到滑动验证码需要移动的距离 :param image1:没有缺口的图片对象 :param image2:带缺口的图片对象 :return:需要移动的距离 ''' # print('size', image1.size) #print(type(image1)) len=0 threshold = 50 image1=get_image(url1) image2=get_image(url2) # 创建绘图对象 draw = ImageDraw.Draw(image2) for i in range(0,image1.size[0]): # 260 for j in range(0,image1.size[1]): # 160 pixel1 = image1.getpixel((i,j)) pixel2 = image2.getpixel((i,j)) res_R = abs(pixel1[0]-pixel2[0]) # 计算RGB差 res_G = abs(pixel1[1] - pixel2[1]) # 计算RGB差 res_B = abs(pixel1[2] - pixel2[2]) # 计算RGB差 if res_R > threshold and res_G > threshold and res_B > threshold: len=i # print(f'{len}-({res_R},{res_G},{res_B})') # 绘制一条线 draw.line((130, 0, len, 20), fill="red", width=3) break image2.show() #image2.save('./Image/code.jpg') return len
三.根据缺口距离生成滑动轨迹
def generate_sliding_track(x): # 初始化轨迹列表 slide_track = [ [random.randint(-50, -20), random.randint(-200, -100), 0], [0, 0, 0], ] if x < 100: move_section = 1 #如果移动距离小于100 那么move次数为x加上 7到20之间的数 else: move_section = 2 #如果移动距离小于100 那么move次数为x加上 2乘 7到20之间的数 up_down = random.randint(0, 1) #确定一个方向 x大于0或x小于0 y = 0 #数组的y值 time = random.randint(100,180) #初始时间 即为第二个数组的时间 后续时间累加操作就可以了 count = 0 flag = 0 repetition = int(x/4) #重复x出现的个数 frist_count = random.randint(6,10) #前面y为0的数组个数 for i in range(x*random.randint(move_section*7,move_section*21)): #move_section 在这里起作用 if i+1 > x: #如果i+1要等于x 或者小于x 但这里基本上都是等于x break if up_down == 0: #up_down如果大于0 那么这个轨迹就是y增轨迹 if i >frist_count: if count==0: y += random.randint(0, 1) count +=1 if flag>random.randint(8,10): count = 0 flag = 0 if i + 1 > int(x / 5)*4: time += random.randint(20, 70) elif i+1 > x-3: time += random.randint(80, 180) else: time += random.randint(0,5) slide_track.append([i+1,y,time]) flag+=1 if random.randint(0,1): if repetition: slide_track.append([i + 1, y, time+random.randint(0,3)]) flag += 1 repetition -= 1 else: #前面几个数组y都为0 time += random.randint(0, 5) slide_track.append([i + 1, y, time]) if random.randint(0,1): if repetition: slide_track.append([i + 1, y, time+random.randint(0,3)]) repetition -= 1 if up_down == 1: #up_down如果小于0 那么这个轨迹就是y减轨迹 if i > frist_count: if count==0: y -= random.randint(0, 1) count +=1 if flag>random.randint(8,10): count = 0 flag = 0 if i + 1 > int(x / 5)*4: time += random.randint(7, 40) elif i+1 > x-3: time += random.randint(80, 180) else: time += random.randint(0, 5) slide_track.append([i+1,y,time]) flag +=1 if random.randint(0,1): if repetition: slide_track.append([i + 1, y, time+random.randint(0,3)]) flag += 1 repetition -= 1 else: time += random.randint(0, 5) slide_track.append([i + 1, y, time]) if random.randint(0,1): if repetition: slide_track.append([i + 1, y, time+random.randint(0,3)]) repetition -= 1 return slide_track
四.提交验证

通过逆向网站js,我们可以查看到他提交时的逻辑,其中userresponse为缺口距离,data便是轨迹数据,我们只需要根据他的逻辑来实现我们的python逻辑即可
def validate(ctxid,l): try: tracks=generate_sliding_track(l) # # t n={"appid":"202503141611","ctxid":"","type":"slide","userresponse":0,"data":"","account":"quoteapi","password":"","t":0,"random":random.random()} n['ctxid']=ctxid n['userresponse']=l n['data']=format_tracks(tracks) n['t']=tracks[-1][2] o = "appid=" + n['appid'] + "|ctxid=" + n['ctxid'] + "|type=" + n['type'] + "|u=" + str(n['userresponse']) + "|d=" + n['data'] + "|a=" + n['account'] + "|p=" + n['password'] + "|t=" + str(n['t']) + "|r=" + str(n['random']) oEncrypt=mainJs.call('encrypt',o) # 对字符串进行URL编码 reqStr=quote(mainJs.call('base64Encode',oEncrypt)) print(reqStr) url='/api/captcha/Validate?callback=&ctxid='+ctxid+'&request='+reqStr+'&_=1756106181125' print(url) header['Referer']='' response = http.request('GET', url, headers=header) print(response.data) result = response.data.decode('UTF-8') jsonData = json.loads(result) print(jsonData) if jsonData["ReturnCode"]=="0": print("validate success") return json.loads(jsonData["Data"]['Result'])['validate'] else: print("validate error") return None except Exception as e: print(e) return None

浙公网安备 33010602011771号