Cracking Geetest CAPTCHA: A Step-by-Step Analysis
Background
While organizing old files recently, I came across an interesting article about cracking the Geetest CAPTCHA. Looking back, it offers a unique perspective worth sharing.
According to Baidu Baike, Geetest is defined as:
Geetest is a cloud-based verification service used in computing to distinguish between humans and bots. It integrates easily and provides developers with a secure and convenient verification method. Unlike traditional CAPTCHAs, Geetest analyzes user behavior during puzzle completion to determine if the user is human or bot through data analysis. Users no longer need to decipher confusing letters or characters—the verification process becomes as engaging as a game.
Approach
Step 1: Image Retrieval
The core idea behind cracking Geetest involves studying its JavaScript code and analyzing intercepted HTTP rqeuests. Through research, we discovered that accessing a specific URL (shown in the screenshot below) returns two images: a background image (let's call it bg) and a slice image (let's call it slice).
[Image placeholder: Screenshot of the URL]
Figure 2 can be interpreted as the background image, wich contains a shaded area. Figure 3 represents the slice image, but the extracted slice appears as scrambled pieces. These scrambled pieces can only be reconstructed into a complete image using specific CSS slicing code. The CSS code is shown below—replace the $$$$$$$$$$$ placeholders with the parsed image names to assemble the full image.
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title>Untitled Document</title>
<style type="text/css">
<!--
.gt_info,.gt_info .gt_info_tip,.gt_info .info_wait,.gt_info .info_complete,.gt_info .info_error,.gt_info .info_abuse,.gt_info .info_forbidden,.gt_info .info_revalidate,.gt_ads_box_bg,.gt_bottom,.gt_ads_holder_top,.gt_ads_anim,.gt_refresh_button,.gt_refresh_button:hover,.gt_help_button,.gt_help_button:hover,.gt_slider_holder,.gt_slider_knob,.knob_active,.knob_normal,.gt_slider_knob:hover,.gt_refresh_tips,.gt_help_tips,.gt_ads_tips,.gt_ajax_tip,.ajax_lock,.ajax_pass,.ajax_error,.ajax_wait,.ajax_robot,.ajax_revalidate,.gt_popup .gt_form_header,.gt_popup .gt_bottom,.gt_popup .gt_form_header_0,.gt_popup .gt_form_header_1,.gt_popup .gt_form_header_close{background-repeat:no-repeat;background-image:url('http://static.geetest.com/static/golden/sprite.2.9.10.png');_background-image:url('http://static.geetest.com/static/golden/sprite.2.9.10.gif')}@media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 192dpi), (min-resolution: 1.5dppx){.gt_info,.gt_info .gt_info_tip,.gt_info .info_wait,.gt_info .info_complete,.gt_info .info_error,.gt_info .info_abuse,.gt_info .info_forbidden,.gt_info .info_revalidate,.gt_ads_box_bg,.gt_bottom,.gt_ads_holder_top,.gt_ads_anim,.gt_refresh_button,.gt_refresh_button:hover,.gt_help_button,.gt_help_button:hover,.gt_slider_holder,.gt_slider_knob,.knob_active,.knob_normal,.gt_slider_knob:hover,.gt_refresh_tips,.gt_help_tips,.gt_ads_tips,.gt_ajax_tip,.ajax_lock,.ajax_pass,.ajax_error,.ajax_wait,.ajax_robot,.ajax_revalidate,.gt_popup .gt_form_header,.gt_popup .gt_bottom,.gt_popup .gt_form_header_0,.gt_popup .gt_form_header_1,.gt_popup .gt_form_header_close{background-image:url('http://static.geetest.com/static/golden/sprite2x.2.9.10.png');-moz-background-size:290px auto;-o-background-size:290px auto;-webkit-background-size:290px auto;background-size:290px auto}}.gt_info{height:22px;width:260px;background-position:0 -357px;height:0;overflow:hidden;position:absolute;bottom:1px;margin-left:1px;-webkit-transition:height 200ms;-moz-transition:height 200ms;-o-transition:height 200ms;transition:height 200ms}/* ... truncated for brevity ... */
-->
</style>
</head>
<body>
<div class="gt_ads_cut">
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -157px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -145px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -265px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -277px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -181px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -169px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -241px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -253px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -109px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -97px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -289px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -301px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -85px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -73px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -25px -58px;"></div>
<div class="gt_ads_cut_slice" style="background-image: url(http://static.geetest.com/$$$$$$$$$$$); background-position: -37px -58px;"></div>
<!-- ... additional slices ... -->
</div>
</body>
</html>
After obtaining the images, we need to generate track data (a series of mouse movement points).
Step 2: Track Generation
Once the images are acquired, the next task is to generate the track—an ordered list of coordinates that simulates human mouse movement. There are two common approaches:
- Random simulation: Use a program to randomly generate coordinates. This method has low accuracy and is easily detected as bot behavior.
- Manual collection: Perform hundreds of manual drags to collect coordinate points, then slightly perturb these points to create multiple variations. This yields more realistic tracks.
[Image placeholder: Example of collected track data]
Step 3: Encrypted Track Generation
After collecting coordinate points, we use the Microsoft.JScript.Vsa engine to invoke JavaScript functions that generate the encrypted track. The key functions are:
userresponse(a, b): Generates the user's behavioral response.pushPoint(x, y, time): Adds coordinate points to an internal array.f(): Produces the final encrypted track string.
The following JavaScript code demonstrates these functions (slightly refactored for clarity):
var pointStore = [];
function computeDeltas(points) {
var deltas = [];
for (var i = 0; i < points.length - 1; i++) {
var delta = [];
delta[0] = Math.round(points[i + 1][0] - points[i][0]);
delta[1] = Math.round(points[i + 1][1] - points[i][1]);
delta[2] = Math.round(points[i + 1][2] - points[i][2]);
if (delta[0] !== 0 || delta[1] !== 0 || delta[2] !== 0) {
deltas.push(delta);
}
}
return deltas;
}
function encodeNumber(num) {
var charset = '()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqr';
var base = charset.length;
var result = '';
var absoluteVal = Math.abs(num);
var quotient = parseInt(absoluteVal / base);
if (quotient >= base) quotient = base - 1;
if (quotient) result = charset.charAt(quotient);
var remainder = absoluteVal % base;
var prefix = '';
if (num < 0) prefix += '!';
if (result) prefix += '$';
return prefix + result + charset.charAt(remainder);
}
function encodeDelta(delta) {
var patterns = [
[1, 0],
[2, 0],
[1, -1],
[1, 1],
[0, 1],
[0, -1],
[3, 0],
[2, -1],
[2, 1]
];
var chars = 'stuvwxyz~';
for (var i = 0; i < patterns.length; i++) {
if (delta[0] === patterns[i][0] && delta[1] === patterns[i][1]) {
return chars[i];
}
}
return 0;
}
function generateEncodedTrack() {
var deltas = computeDeltas(pointStore);
var encodedX = [];
var encodedY = [];
var encodedTime = [];
for (var i = 0; i < deltas.length; i++) {
var symbol = encodeDelta(deltas[i]);
if (symbol) {
encodedY.push(symbol);
} else {
encodedX.push(encodeNumber(deltas[i][0]));
encodedY.push(encodeNumber(deltas[i][1]));
}
encodedTime.push(encodeNumber(deltas[i][2]));
}
return encodedX.join('') + '!!' + encodedY.join('') + '!!' + encodedTime.join('');
}
function pushPoint(x, y, time) {
pointStore.push([x, y, time]);
}
function getTrack() {
return generateEncodedTrack();
}
function generateTimestamp() {
return parseInt(10000 * Math.random()) + (new Date()).valueOf();
}
function userresponse(distance, challenge) {
var suffix = challenge.slice(32);
var digits = [];
for (var i = 0; i < suffix.length; i++) {
var code = suffix.charCodeAt(i);
digits[i] = code > 57 ? code - 87 : code - 48;
}
var baseVal = 36 * digits[0] + digits[1];
var offset = Math.round(distance) + baseVal;
var prefix = challenge.slice(0, 32);
var groups = [[], [], [], [], []];
var seen = {};
var groupIdx = 0;
for (var i = 0; i < prefix.length; i++) {
var ch = prefix.charAt(i);
if (!seen[ch]) {
seen[ch] = 1;
groups[groupIdx].push(ch);
groupIdx = (groupIdx + 1) % 5;
}
}
var result = '';
var remaining = offset;
var multipliers = [1, 2, 5, 10, 50];
var idx = 4;
while (remaining > 0) {
if (remaining - multipliers[idx] >= 0) {
var randIndex = parseInt(Math.random() * groups[idx].length, 10);
result += groups[idx][randIndex];
remaining -= multipliers[idx];
} else {
groups.splice(idx, 1);
multipliers.splice(idx, 1);
idx--;
}
}
return result;
}
Step 4: Submit the Assembled URL
Finally, we construct and submit the assembled URL containing the generated challenge, track, and other required parameters to bypass the CAPTCHA.