Research · · 15 min read

What a Binance CAPTCHA solver tells us about today’s bot threats

What a Binance CAPTCHA solver tells us about today’s bot threats

In this post, we analyze an open-source CAPTCHA solver designed to bypass a custom challenge deployed on Binance, one of the most popular crypto platforms. While the solver is publicly available, we’ve intentionally chosen not to link to the original repository. The code is minimally documented and was clearly intended for direct exploitation. Publishing the link would only lower the barrier to abuse, especially for actors looking to launch credential stuffing attacks.

Instead, we’ll walk through how the solver works, what it reveals about attacker workflows, and how defenders can respond.

Disclaimer: This post focuses solely on the CAPTCHA-solving component. Successfully bypassing a CAPTCHA does not imply access to user accounts. Based on what we observed, Binance likely applies additional security measures behind the scenes, including device fingerprinting, behavioral detection, and risk-based controls.

Why this matters

It’s increasingly rare to find public solvers for modern custom CAPTCHA challenges. Studying this one provides insight into the current tactics used by attackers targeting high-value platforms, particularly in crypto. It also underscores how easily legacy defenses like CAPTCHA and IP rate limiting can be defeated when implemented in isolation.

This example shows how CAPTCHA protections can be bypassed when used in isolation. It’s a useful reminder for teams that still rely on them as primary controls, since it can introduce a single point of failure.

Overview of the CAPTCHA solver

The solver was released publicly on GitHub on April 17 and shared shortly after on Reddit. Here’s how it can be used programmatically (we’ll explain the security_check_response_validate_id parameter shortly, it appears as securityCheckResponseValidateId in HTTP requests):

from binance.session import BinanceCaptcha

binance = BinanceCaptcha(
    biz_id="login",
    security_check_response_validate_id="7d1bf5349be4481c8b2de29cc24f8451"
)

token = binance.solve()

Unlike most solvers, this one doesn't rely on a browser environment. It operates entirely through a custom HTTP client, made possible by a full reverse engineering of Binance’s CAPTCHA flow. That includes:

All of this was achieved despite the use of common protection techniques like JavaScript obfuscation and encrypted payloads.

Running without a browser is a big advantage for attackers: it allows parallelization at scale with minimal resource usage. It's faster, cheaper, and harder to detect when paired with rotating IPs or spoofed device fingerprints.

Before diving into how the solver works, it’s important to understand how Binance implements its CAPTCHA as part of the login flow. Then, we break down how this solver bypasses each layer of protection, but before that, we analyze the CAPTCHA

Overview of Binance’s CAPTCHA login flow

Triggering the CAPTCHA: Binance doesn't always present a CAPTCHA challenge. However, you can reliably trigger one by:

When a CAPTCHA is triggered, the browser sends a request to https://accounts.binance.com/bapi/accounts/v1/public/account/security/request/precheck with a payload like:

{"email":"mygreatemail@gmail.com","bizType":"login"}

The response includes metadata about the challenge. For example:

{
    "code": "000000",
    "message": null,
    "messageDetail": null,
    "data": {
        "captchaType": "bCAPTCHA2",
        "sessionId": "EF37BA84D96E841383699219D6DF1F22B1DE4AEB",
        "validateId": "1c5164a543e148b9a4426afdedcd78a3",
        "validationTypes": [
            "captcha"
        ]
    },
    "success": true
}

Fetching the CAPTCHA: Next, the browser retrieves the CAPTCHA challenge from https://accounts.binance.com/bapi/composite/v1/public/antibot/getCaptcha, with the following payload:

bizId=login&sv=20220906&lang=en&securityCheckResponseValidateId=2f002e79df044d98a6d1801f2e08404b&clientType=web

We notice the securityCheckResponseValidateId used in the constructor of the CAPTCHA solver class presented earlier.

The response includes the CAPTCHA image path:

{
   "code":"000000",
   "data":{
      "sig":"jZdAbkvlphXJihXgZ5QTnL8BRmkd6KCMjm04CYQOVpTyWBdf",
      "salt":"6103613765",
      "path2":"/image/antibot/SLIDE/img/20250416/08/6355847912ff4055b4ea014f43df94fd.png",
      "ek":"p73l",
      "captchaType":"SLIDE",
      "tag":"default",
      "fb":"false",
      "i18n":"{\"cap_timeout\":\"Timeout\",\"cap_try_again\":\"Please try again\",\"cap_select_all_match_images\":\"Please select all images with\",\"cap_verify_fail\":\"Please try again\",\"cap_success\":\"Success\",\"cap_too_many_attempts\":\"Too many attempts\",\"cap_verify\":\"Verify\",\"cap_verify_success\":\"Success\",\"networkerror\":\"change to a new captcha image\",\"cap_loading\":\"Loading\",\"cap_security_verification\":\"Security Verification\",\"describe\":\"select all image with {{tag}}\",\"cap_complete_puzzle\":\"Slide to complete the puzzle\",\"cap_network_error\":\"Network error\",\"cap_next\":\"Next\",\"cap_system_error\":\"System error\"}"
   },
   "success":true
}

The challenge image can be accessed at the full CDN path, for example https://bin.bnbstatic.com/image/antibot/SLIDE/img/20250416/08/6355847912ff4055b4ea014f43df94fd.png and looks as follows:

Visual layout: Once rendered, the CAPTCHA appears as a slider-based challenge, a common puzzle format where users drag a piece into position to pass the test. It’s embedded as an SVG component in the page (the 2 images look different because they come from different CAPTCHA passing attempts as I kept tinkering with it)

In terms of HTML, the CAPTCHA is rendered using a SVG component.

Submitting the challenge: After solving the CAPTCHA, a request is sent to https://accounts.binance.com/bapi/composite/v1/public/antibot/validateCaptcha with a payload like this:

bizId=login&sv=20220906&lang=en&securityCheckResponseValidateId=1c5164a543e148b9a4426afdedcd78a3&clientType=web&sig=GAImTowZQTdITR79a32BHUq0doTaRkeBUORxIl3IRGPkVNZ8&data=SRUTAkNTA0hFU1ROVlhUSFtaVE5UWlRIVlJUTkNLVEhCRRIRQ1NaWAEbT0VNXklGCgBUWEMLCgZTFUxCVkVaGl ... (truncated for readability) EHU1ATIwgNDAIQDVRGV19aFw%3D%3D&s=339628

The data parameter is encrypted. When decrypted (as shown in the solver section), it contains:

The API response includes a CAPTCHA token:

{
   "code":"000000",
   "data":{
      "result":0,
      "tag":"default",
      "i18n":"{\"cap_timeout\":\"Timeout\",\"cap_try_again\":\"Please try again\",\"cap_select_all_match_images\":\"Please select all images with\",\"cap_verify_fail\":\"Please try again\",\"cap_success\":\"Success\",\"cap_too_many_attempts\":\"Too many attempts\",\"cap_verify\":\"Verify\",\"cap_verify_success\":\"Success\",\"networkerror\":\"change to a new captcha image\",\"cap_loading\":\"Loading\",\"cap_security_verification\":\"Security Verification\",\"describe\":\"select all image with {{tag}}\",\"cap_complete_puzzle\":\"Slide to complete the puzzle\",\"cap_network_error\":\"Network error\",\"cap_next\":\"Next\",\"cap_system_error\":\"System error\"}",
      "token":"captcha#0dc8766a7ab44ff0b020046eda03acf2-GAImTowZQTdITR79a32BHUq0doTaRkeBUORxIl3IRGPkVNZ8"
   },
   "success":true
}

This token is then validated with a final request to /bapi/accounts/v1/public/account/security/check/resultwith the following POST payload (obtained with the CAPTCHA)

{
   "sessionId":"EF37BA84D96E841383699219D6DF1F22B1DE4AEB",
   "validateCodeType":"bCAPTCHA2",
   "bCaptchaToken":"captcha#0dc8766a7ab44ff0b020046eda03acf2-GAImTowZQTdITR79a32BHUq0doTaRkeBUORxIl3IRGPkVNZ8"
}

It seems like the goal of this HTTP request is to actually validate the token, and verify that data related to the CAPTCHA passing attempt is actually valid. It responds with

{
    "code": "000000",
    "message": null,
    "messageDetail": null,
    "data": {
        "sessionId": "EF37BA84D96E841383699219D6DF1F22B1DE4AEB"
    },
    "success": true
}

CAPTCHA client-side code: The CAPTCHA logic is implemented in the script served from https://bin.bnbstatic.com/static/js/se/captcha/v1/captcha.min.js. The JavaScript is obfuscated, likely using a tool like obfuscator.io, as shown in the screenshot below.

Obfuscation transforms readable code into a form that’s intentionally hard to analyze. In this case, it obscures how data like mouse coordinates and fingerprinting signals are collected and packaged before being sent to the server.

This technique complements payload encryption. Since attackers have full access to the client-side code, obfuscation adds an additional barrier to reverse engineering the underlying logic.

The goal of this blog post isn’t to fully reverse the CAPTCHA script’s obfuscation. Instead, the focus is to provide just enough context to understand how the solver works, which we’ll cover in the next section.

To gain insight into the script’s behavior, we placed breakpoints at key points in the execution flow and inspected memory using Chrome DevTools. This allowed us to observe some of the data collected by the script, such as browser attributes and the navigator.webdriver property, which is commonly used to detect headless or automated browsers.

We also observed encrypted/obfuscated values in memory, including challenge-specific data tied to the CAPTCHA challenge.

If the user fails the CAPTCHA multiple times, Binance escalates to a more complex visual challenge, visible in the screenshot below. The solver we analyzed is limited to the slider CAPTCHA and does not support this advanced version.

While not directly related to the solver, we also identified another anti-bot script during analysis. It runs as soon as the page loads and likely collects an initial set of fingerprinting signals to help decide whether to trigger a CAPTCHA challenge. This second obfuscated script is served from https://bin.bnbstatic.com/static/js/se/se_new.min.js.

We only provide a brief look at this script, as it’s not part of the CAPTCHA solver's scope. That said, it appears to be invoked during CAPTCHA validation, especially when the more complex image challenge is triggered, as shown in the call stack below.

Note that the bundle.es5.min.js script is just used to make the fetch request. It is not related to any security feature.

Like the CAPTCHA script, this secondary script is also obfuscated to hinder reverse engineering and make its purpose less immediately clear.

However, not all content is hidden. Some strings and object properties remain in plain text, which makes it possible to infer the types of signals being collected and the script’s overall role in fingerprinting and bot detection. For example, in the code snippet below (taken from the CAPTCHA script), we see that it collects device-related signals such as navigator.language and navigator.platform.

function J() {
      function L(M) {
          return M + '';
      }
      return {
          'language': L(i[b('0x221', 'vniK')]),
          'languages': i[b('0x0', 'W*^[')] ? i[b('0xa2', '!i[a')][b('0x2d', 'KRIJ')](',') : '',
          'browserLanguage': L(i[b('0x52', 'KcFx')]),
          'systemLanguage': v[b('0x88', '!mWg')](L, i[b('0x219', '!%Y$')]),
          'cpuClass': L(i[b('0x69', '0!O2')]),
          'oscpu': L(i[b('0x325', 'HeIb')]),
          'appName': v[b('0x1e4', 'w79K')](L, i[b('0x141', '8@X%')]),
          'appVersion': L(i[b('0x1b9', 'Z[bM')]),
          'appMinorVersion': v[b('0x184', 'X&5L')](L, i[b('0x46', '^Eyp')]),
          'mimeTypesLength': L(i[b('0x6', '$^ka')][b('0x121', '!mWg')]),
          'buildID': L(i[b('0x202', 'y(fn')]),
          'cookieEnabled': v[b('0x10d', '^Eyp')](L, i[b('0x97', 'y(fn')]),
          'deviceMemory': L(i[b('0x1db', 'j]*c')]),
          'maxTouchPoints': L(i[b('0x252', 'OsVO')]),
          'doNotTrack': v[b('0x18f', 'Ubj@')](L, i[b('0x10e', 'HeIb')]),
          'hardwareConcurrency': L(i[b('0xc2', '6Qqh')]),
          'platform': L(i[b('0xe5', 'ksml')]),
          'product': L(i[b('0xe3', 'j]*c')]),
          'productSub': v[b('0x117', 'W*^[')](L, i[b('0x281', 'HeIb')]),
          'vendor': L(i[b('0x291', 'sI$V')]),
          'vendorSub': v[b('0x34', 'O*rz')](L, i[b('0x1e3', 'W*^[')]),
          'plugins': v[b('0x294', 'y(fn')](G)[b('0x8a', '1HC3')](',')
      };
  }

By setting a breakpoint on the return statement, we can trace how the function is used at runtime. Specifically, it’s called at the line 'navi': f[b('0x1da', 'w79K')](J), which extracts fingerprinting information from the window.navigator object.

Other functions in the same context gather additional signals, including the user's timezone and screen resolution.

function K() {
    var L = '';
    try {
        L = Intl[b('0x62', 'P2V*')]()[b('0x47', 'RiKs')]()[b('0xc7', '!mWg')];
    } catch (N) {}
    var M = {
        'paur': g[b('0x2dd', 'vniK')][b('0x158', 'GeXz')],
        'pati': h[b('0xd', '8@X%')],
        'ivw': z[b('0x14e', 'ksml')]() ? '1' : '0',
        'rf': h[b('0x2ee', 'P2V*')],
        'ismo': A() ? '1' : '0',
        'tina': L,
        'hl': f[b('0x3d', 'OsVO')](history[b('0x2ad', ']Csl')], ''),
        'tizoof': new n()[b('0x1ea', '4m#D')](),
        'capa': f[b('0x32', 'OsVO')](E),
        
        // Function above is called here!
        'navi': f[b('0x1da', 'w79K')](J),
        'wiin': f[b('0xdc', 'KRIJ')](C),
        'scin': D(),
        'prde': f[b('0x20f', 'X&5L')](F),
        'cors': f[b('0x255', 'qwyx')](B),
        'hk': I
    };
    return f[b('0x25e', 'Pd3Y')](x, JSON[b('0x2a1', 'O*rz')](M));
}

On my machine, the M variable contains the following signals:

{
    "paur": "https://accounts.binance.com/en/login",
    "pati": "Log In | Binance",
    "ivw": "0",
    "rf": "https://accounts.binance.com/en/login",
    "ismo": "0",
    "tina": "Europe/Paris",
    "hl": "2",
    "tizoof": -120,
    "capa": {
        "sest": true,
        "lost": true,
        "indb": true,
        "ontost": false
    },
    "navi": {
        "language": "en",
        "languages": "en",
        "browserLanguage": "undefined",
        "systemLanguage": "undefined",
        "cpuClass": "undefined",
        "oscpu": "undefined",
        "appName": "Netscape",
        "appVersion": "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
        "appMinorVersion": "undefined",
        "mimeTypesLength": "2",
        "buildID": "undefined",
        "cookieEnabled": "true",
        "deviceMemory": "8",
        "maxTouchPoints": "0",
        "doNotTrack": "null",
        "hardwareConcurrency": "14",
        "platform": "MacIntel",
        "product": "Gecko",
        "productSub": "20030107",
        "vendor": "Google Inc.",
        "vendorSub": "",
        "plugins": "PDF Viewer,Chrome PDF Viewer,Chromium PDF Viewer,Microsoft Edge PDF Viewer,WebKit built-in PDF"
    },
    "wiin": {
        "outerWidth": "2560",
        "outerHeight": "1415",
        "innerWidth": "322",
        "innerHeight": "863",
        "devicePixelRatio": "1.5",
        "orientation": "undefined"
    },
    "scin": {
        "left": "undefined",
        "right": "undefined",
        "availLeft": "1728",
        "availTop": "25",
        "availWidth": "2560",
        "availHeight": "1415",
        "width": "2560",
        "height": "1440",
        "colorDepth": "24",
        "deviceXDPI": "undefined",
        "logicalXDPI": "undefined",
        "systemXDPI": "undefined"
    },
    "prde": "0,0,0,0",
    "cors": "1",
    "hk": {
        "xh_sd": "function(){for(var n=[],r=0;r<arguments.length;r++",
        "xh_op": "nc",
        "xh_sh": "nc",
        "nd_se": "nc",
        "in_se": "nc"
    }
}

The script collects a range of signals commonly used for browser fingerprinting, including:

Analyzing the CAPTCHA solver

With a better understanding of Binance’s login flow and the associated client-side protections, we can now turn our attention to the solver itself. The project is organized into multiple modules:

Based on the example import (from binance.session import BinanceCaptcha), the entry point appears to be the BinanceCaptcha class located in the binance/session.py file. The file starts with the following imports:

import time

from curl_cffi import requests
from binance.fingerprint import Fingerprint
from binance.crypto import BinanceCrypto
from json import dumps

The solver relies on the curl_cffi library, which provides an HTTP client with fine-grained control over TLS fingerprinting. This allows the solver to mimic a browser’s TLS signature, aligning it with the declared User-Agent string, a common anti-bot evasion tactic.

The BinanceCaptcha class is responsible for solving the challenge and accepts three key parameters:

security_check_response_validate_id: str = "",  # from precheck request
biz_id: str = "register",  # from precheck request
user_agent: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",          

The solver then uses curl_cffi to initialize an HTTP session with a forged TLS fingerprint that mimics Chrome 116:

self.session = requests.Session(
            impersonate="chrome116",
            ...
            )

It also dynamically generates a fingerprint:

self.device = Fingerprint(self.user_agent)

The Fingerprint class is defined in fingerprint.py. We’ll take a closer look at its logic later in the analysis.

Within session.py, the BinanceCaptcha class defines three core methods:

  1. _get_captcha , fetches the CAPTCHA challenge from the server
  2. _validate_captcha , submits the solved challenge for verification
  3. solve , a wrapper that orchestrates the full solve process by calling _validate_captcha

Beyond TLS-level evasion, the solver also replicates realistic HTTP headers. For instance, in _get_captcha, it sets headers that closely match a Chrome-on-Windows browser, including headers that the Binance’s API expects:

self.session.headers = {
        'accept': '*/*',
        'accept-language': 'en-US,en;q=0.9',
        'bnc-uuid': 'xxx',
        'cache-control': 'no-cache',
        'captcha-sdk-version': '1.0.0',
        'clienttype': 'web',
        'content-type': 'text/plain; charset=UTF-8',
        'device-info': self.device.generate_device_id(),
        'fvideo-id': 'xxx',
        'origin': 'https://accounts.binance.com',
        'pragma': 'no-cache',
        'priority': 'u=1, i',
        # 'sec-ch-ua': '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'sec-fetch-dest': 'empty',
        'sec-fetch-mode': 'cors',
        'sec-fetch-site': 'same-origin',
        'user-agent': self.user_agent,
        'x-captcha-se': 'true',
    }

response = self.session.post("https://accounts.binance.com/bapi/composite/v1/public/antibot/getCaptcha",
                             data=payload)

The CAPTCHA challenge includes two key protection layers:

  1. An image-based puzzle (slider CAPTCHA)
  2. Device fingerprinting data, used to verify client authenticity

In the next sections, we examine how the solver bypasses both mechanisms, without relying on a real browser.

Analyzing the automatic image recognition solver

The logic for solving the image-based CAPTCHA is implemented in slide.py. The approach is inspired by a known technique used to bypass similar slider challenges. While we won’t go deep into the image recognition details, the script relies on the cv2 package (OpenCV) and uses the matchTemplate function to locate the position of the missing puzzle piece through basic image processing.

def solve(self):
    self._split_piece()

    edge_puzzle_piece = cv2.Canny(self.puzzle_piece, 100, 200)
    edge_background = cv2.Canny(self.background, 100, 200)

    edge_puzzle_piece_rgb = cv2.cvtColor(edge_puzzle_piece, cv2.COLOR_GRAY2RGB)
    edge_background_rgb = cv2.cvtColor(edge_background, cv2.COLOR_GRAY2RGB)

    res = cv2.matchTemplate(edge_background_rgb, edge_puzzle_piece_rgb, cv2.TM_CCOEFF_NORMED)
    _, _, _, max_loc = cv2.minMaxLoc(res)
    top_left = max_loc
    h, w = edge_puzzle_piece.shape[:2]

    center_x = top_left[0] + w // 2

    cv2.line(self.background, (center_x, 0), (center_x, edge_background_rgb.shape[0]), (0, 255, 0), 2)
    #cv2.imwrite('output.png', self.background)

    return center_x  - 31

One key weakness in the CAPTCHA design is that the image URL includes both the full background and the puzzle fragment, always located at the same position on the left of the image. This significantly lowers the complexity of the task, making it easier for an attacker to solve the challenge without needing to render the CAPTCHA in a real browser.

Analyzing the fingerprint generation process

The fingerprinting logic is implemented in fingerprint.py. Its primary purpose is to generate a fake device fingerprint, specifically, a base64-encoded device-info string used in HTTP requests to mimic a legitimate client.

This is handled by the generate_device_id method of the Fingerprint class:

def generate_device_id(self):
    device_id = {
        "screen_resolution": "1920,1080",
        "available_screen_resolution": "1920,1032",
        "system_version": "unknown",
        "brand_model": "unknown",
        "timezone": "",
        "web_timezone": "Europe/Berlin",
        "timezoneOffset": -120,
        "user_agent": self.user_agent,
        "list_plugin": "PDF Viewer,Chrome PDF Viewer,Chromium PDF Viewer,Microsoft Edge PDF Viewer,WebKit built-in PDF",
        "platform": "Win32",
        "webgl_vendor": "unknown",
        "webgl_renderer": "unknown"
    }

    return base64.b64encode(json.dumps(device_id, separators=(",", ":")).encode("utf-8")).decode("utf-8")

This fingerprint includes several attributes commonly used in browser fingerprinting, such as:

These values are then serialized and base64-encoded before being injected into the device-info header.

The Fingerprint class also defines two static methods, which we’ll examine next.

@staticmethod
def generate_ev() -> dict:
    return {
        'wd': Fingerprint._generate_unflagged(),  # selenium check
        'im': Fingerprint._generate_unflagged(),  # mobile check
        'de': "",
        'prde': ",".join([str(Fingerprint._generate_unflagged()) for _ in range(4)]),  # 4 checks
        'brla': Fingerprint._generate_unflagged(),
        'pl': "Win32",  # platform
        'wiinhe': 945,  # innerHeight
        'wiouhe': '1032'  # outerHeight
    }

@staticmethod
def generate_data(url: str) -> dict:
    dist = SlideSolver(url).solve()
    return {
        "ev": Fingerprint.generate_ev(),
        "be": MouseMovement().generate_mouse_movement_slide(dist),
        "dist": dist,
        "imageWidth": "310"
    }

The generate_data method is called from within the Session class, specifically in _validate_captcha, to construct the payload expected by the CAPTCHA validation endpoint:

data = self.device.generate_data("https://bin.bnbstatic.com" + captcha_data["path2"])
data_encrypted = BinanceCrypto.encrypt(dumps(data, separators=(",", ":")), captcha_data["ek"])

This is the point where two critical elements are generated:

Both are produced inside the generate_data method.

The logic for generating fake mouse movements, used to simulate biometric behavior during the slider challenge, is implemented in biometrics.py. While we won’t go into all the details here, the core goal is to produce a sequence of cursor movements that appear human and are consistent with the slider’s final position.

Each point in the movement sequence is represented by a Point object:

class Point:
    def __init__(self, x, y, button_id="", rel_x=-1, rel_y=-1):
        self.x = x
        self.y = y
        self.button_id = button_id
        self.rel_x = rel_x
        self.rel_y = rel_y

    def __str__(self):
        return f"({self.x}, {self.y}, {self.button_id}, {self.rel_x}, {self.rel_y})"

A full movement is stored as a collection of these points, along with additional metadata:

class MouseMovement:
    def __init__(self, ):
        self.points = []
        self.mm = []

        self.mm_count = 0
        self.cl_count = 0
        self.mu_count = 0

        self.first = True

The generate_mouse_movement_slide method handles the generation of realistic movement paths based on the target slider position. It constructs the trajectory using helper functions like connect_points, which interpolates between two cursor positions:

def generate_mouse_movement_slide(self, pos):
      self._generate_points_slide()
      for (i, point) in enumerate(self.points):
          if i == 0:
              continue

          mms = MouseMovement.connect_points(self.points[i - 1], point)
          for mm in mms:
              if self.mm_count > 150:
                  break

              self.mm_count += 1
              self.mm.append(f"|mm|{mm[0]},{mm[1]}|{self.random_delay()}|1")

      th = MouseMovement.connect_points(
          Point(43 + randrange(-2, 2), 16 + randrange(-2, 2)),
          Point(29 + randrange(-2, 2), 18 + randrange(-2, 2)))

      return {
          # ... encoded mouse movements truncated for clarity purposes
      }

By chaining together short segments between points, adding randomness, and limiting movement counts, the function aims to produce a trajectory that mimics human interaction. These mouse movements are then encoded in the format expected by Binance’s CAPTCHA validation logic.

Before submitting the CAPTCHA response, the _validate_captcha method encrypts and formats the data using the same structure expected by Binance’s backend. This ensures the request passes structural validation checks on the server side.

payload = {
      'bizId': self.biz_id,
      'sv': self.sv,
      'lang': 'en',
      'securityCheckResponseValidateId': self.security_check_response_validate_id,
      'clientType': 'web',
      'data': data_encrypted,
      's': BinanceCrypto.calculate_s(
          self.biz_id + captcha_data["sig"] + data_encrypted + captcha_data.get("salt", "")), # they don't even check this
      'sig': captcha_data["sig"],
  }

  response = requests.post(
      'https://accounts.binance.com/bapi/composite/v1/public/antibot/validateCaptcha',
      data=payload,
  )

The solver then returns the token received in the response from the CAPTCHA validation endpoint https://accounts.binance.com/bapi/composite/v1/public/antibot/validateCaptcha.

return response.json()["data"]["token"]

Final thoughts: Solving CAPTCHAs is easier than you think

In this article, we analyzed an open-source solver targeting Binance’s slider CAPTCHA. The tool doesn’t rely on a real or headless browser. Instead, it uses a custom HTTP client, image recognition, and reverse engineering to bypass the challenge, efficiently and at scale.

It’s tempting to assume this kind of attack is rare or only relevant to high-value targets like crypto platforms or banks. But that’s no longer true.

These techniques are now widely used. Bots routinely solve CAPTCHAs to scrape data, bypass rate limits, or launch credential stuffing attacks. And if you’re still relying on standard CAPTCHAs like reCAPTCHA for protection, you're exposed. Commercial solvers like 2Captcha or CapSolver return solutions in seconds, for under $3 per thousand requests. For a bot, solving a CAPTCHA is just another API call.

Bot developers continue to evolve. They reverse engineer defenses, improve their fingerprints, simulate human input, and openly share tools. Projects like Nodriver and Selenium Driverless make it easy to build realistic bots that leverage modified automation frameworks to bypass classical fingerprinting detection techniques.

Many also use residential proxy networks to rotate IPs and avoid detection. Increasingly, they leverage AI-based solvers that solve CAPTCHAs at scale, for just a few cents per challenge.

In 2025, detecting bots is a continuous effort. It’s not enough to rely on fingerprinting or static rules. Effective defense requires layered protection: real-time behavior analysis, IP reputation, proxy detection, encrypted client signals, and resilient JavaScript instrumentation. Staying ahead means adapting faster than attackers do.

Read next