Skip to content

Bounty Challenge - Critical SSS Implementation Vulnerability - FULL Data Leakage #46

@ChrisYatesUK

Description

@ChrisYatesUK

Security Vulnerability Report

Shamir Secret Sharing Implementation Issues in Bitaps Mnemonic Challenge

Target: Bitaps Mnemonic Split Tool
Challenge: https://tbtc.bitaps.com/mnemonic/challenge
Researcher: Chris Yates @ChrisYatesUK
Date: 2026-03-10
Logger Version: v5 (deferred post-split scanning; GF256 Buffer/TypedArray fix)
Session: Clean isolated window — no prior session cross-contamination


Summary

This report documents security vulnerabilities found in the Bitaps mnemonic splitting tool used in the Shamir challenge, based on a single clean instrumented session in a fresh browser window. The session produced one __split_secret operation and a full exhaustive reconstruction verification.

Using browser runtime instrumentation, the following were captured directly from the session:

  • Raw entropy used to generate the mnemonic
  • The complete 128-bit Shamir secret in plaintext, passed directly to __split_secret
  • All 16 Shamir polynomial coefficient tuples, each encoding one secret byte in its first element
  • All 5 share buffers returned in the single __split_secret return value
  • All 5 share mnemonics returned in the splitMnemonic return value
  • All 16 __restore_secret verification calls, covering every C(5,3)=10 three-share subset, every C(5,4)=5 four-share subset, and the full five-share reconstruction — every single one returning the correct secret
  • The last generated share mnemonic persisted on the global window object under window.share

This confirms that the complete Shamir secret is exposed in plaintext at multiple points during execution and is interceptable by any co-resident browser script.

The GF(256) post-split table scan extracted both GF(256) arithmetic tables (GF256_EXP_TABLE and GF256_LOG_TABLE) directly from the window object. Both match the canonical AES-field exponent and logarithm tables for the irreducible polynomial x^8 + x^4 + x^3 + x + 1 (0x11B) exactly. All 16 reconstruction calls across every valid share subset returned the correct secret with zero divergence, confirming the implementation is internally consistent and cross-implementation compatible with any Shamir library that uses the standard AES GF(256) field.


Challenge Overview

The Bitaps challenge describes a wallet mnemonic split using Shamir Secret Sharing.

Mnemonic length: 12 words
Secret entropy:  128 bits
Shamir scheme:   3-of-5

The challenge states the mnemonic was split into five shares and any three shares can recover the original secret.


Methodology

A runtime monitoring script (v5 logger) was injected into the browser console in a fresh browser window before any interaction with the tool. This eliminates any possibility of state carry-over from previous sessions.

The v5 logger captured:

  • Cryptographic entropy generation via a crypto.getRandomValues hook
  • Function call arguments and return values for all hooked functions, using a __forensic_hooked guard to prevent double-wrapping
  • Shamir polynomial evaluation via __shamirFn — both arguments and return values
  • Share reconstruction via __shamirInterpolation and __restore_secret
  • Post-split deferred scans: GF(256) table search, candidate share array search, and BIP39 mnemonic scan of window-scoped properties
  • All expensive scanning deferred until after __split_secret returned, triggered by the WORKFLOW_COMPLETE event

The clean session produced exactly one __split_secret call, confirming no cross-contamination.


Session Timeline

23:48:06  INPUT selector_12 = on        (12-word mnemonic length selected)
23:48:08  ENTROPY captured              (mnemonic entropy generated)
23:48:08  BUTTON Generate new           (mnemonic generated)
23:48:22  BUTTON Split                  (split UI opened)
23:48:30  INPUT share-threshold = 3
23:48:31  INPUT share-total = 5
23:48:33  BUTTON Calculate              (split operation triggered)
23:48:33  ENTROPY ×2 captured           (x-coordinate selection + polynomial coefficients)
23:48:33  ENTROPY ×1 additional         (coefficient buffer exhausted on final secret byte)
23:48:33  __shamirFn ×80 calls          (16 polynomials × 5 x-coordinates)
23:48:33  __split_secret called         (all 5 share buffers returned)
23:48:33  WORKFLOW_COMPLETE             (post-split scans triggered)
23:48:33  __shamirInterpolation ×256    (16 reconstructions × 16 bytes)
23:48:33  __restore_secret ×16          (all C(5,3)+C(5,4)+C(5,5) subsets verified)
23:48:33  splitMnemonic called          (all 5 share mnemonics returned)
23:48:33  GF256_SCAN                    (2 tables extracted)
23:48:33  CANDIDATE_ARRAY_SCAN         (0 arrays found)
23:49:06  window.share detected         (last share mnemonic on global window)
23:49:06  POST_SPLIT_SCANS complete

Observed Execution Flow

Entropy Generation

Three entropy values were captured during the split phase. The first is the 32-byte CSPRNG output from which the mnemonic entropy is derived. The second is consumed by the x-coordinate selection loop. The third supplies the random polynomial coefficients. A fourth entropy call fired mid-polynomial evaluation when the 32-byte coefficient buffer was exhausted on the final (16th) secret byte — the implementation correctly requested a fresh buffer rather than reusing stale bytes.

Mnemonic entropy:
76c0b6f95191db4d6ec13e0d8fb2cd09d74c61506fda797118d0c165af55bdbf

Split-phase entropy (x-coordinates):
82a7f4fce9dcbd3d5a0e4397b65822e4472282de1201f2a8ad7ee8ef70298b4d

Split-phase entropy (polynomial coefficients):
e6f2c79a47203273c9e2f63f4d65154dd83005136d87d9c28c2b2d87720a1ea1

Split-phase entropy (coefficient overflow — byte 16):
f02dfa317452537ec8517c00ab6734c1fc1a870db4990163fb0aaa63d2d61c13

Mnemonic Generated

The following 12-word BIP39 mnemonic was generated and passed to the split function:

item aisle salad permit buffalo pluck roast chief assault laugh rebuild barrel

Secret Buffer Passed to __split_secret

The mnemonic entropy is passed as a raw 16-byte buffer directly to the Shamir split function:

window.__split_secret(
  3, 5,
  {"type":"Buffer","data":[118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9]},
  4
)

Entropy cross-check: The mnemonic entropy hex begins 76c0b6f9.... Converting:

0x76 = 118  ✓  (secret[0])
0xc0 = 192  ✓  (secret[1])
0xb6 = 182  ✓  (secret[2])
0xf9 = 249  ✓  (secret[3])

The Shamir secret is the raw mnemonic entropy with no transformation applied.


Shamir Polynomial Construction

For a 3-of-5 scheme the tool constructs a degree-2 polynomial per secret byte:

f(x) = a0 + a1·x + a2·x²   (arithmetic over GF(256))

Where a0 = f(0) = secret byte, and a1, a2 are random coefficients.

All 16 polynomials captured — format [a0, a1, a2]:

[118,230,242]   [192,199,154]   [182, 71, 32]   [249, 50,115]
[ 81,201,226]   [145,246, 63]   [219, 77,101]   [ 77, 21,216]
[110, 48,  5]   [193, 19,109]   [ 62,135,217]   [ 13,194,140]
[143, 43, 45]   [178,135,114]   [205, 10, 30]   [  9,161,240]

Extracting a0 from all 16 tuples reconstructs the complete secret directly:

[118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9]

This matches the buffer passed to __split_secret exactly, byte-for-byte. Observing the polynomial evaluation calls alone is sufficient to recover the full secret without collecting any shares.


Share Generation

Shares are produced by evaluating each polynomial at five x-coordinates. The x-coordinates used in this session were: 2, 4, 7, 9, 12.

The complete share output returned by __split_secret:

{
  "2":  "Buffer<440bb84a7d9ace2a1a4862946d74a1bd>",
  "4":  "Buffer<7a928740fab9fe36fe072b30c5f91e22>",
  "7":  "Buffer<5c04eeb2fd7bc39cbff029e721ca66c7>",
  "9":  "Buffer",
  "12": "Buffer"
}

All five share buffers are returned as a single observable JavaScript object from one function call. Any script intercepting this return value obtains the complete share set simultaneously — negating the distributional security properties of the scheme.

Share Mnemonics

All five share mnemonics were returned in the splitMnemonic return value:

Share 1 (x=2):  dune fruit barely will provide bench hard arrange nerve hill chronic kid
Share 2 (x=4):  kind neither south voice paper bread way tornado cotton cool elegant duty
Share 3 (x=7):  foster cherry protect wife rotate soft zone ahead transfer atom often moment
Share 4 (x=9):  scrap dizzy will pupil analyst ski hollow decorate harvest wise bamboo twenty
Share 5 (x=12): sunny dentist eagle promote blush baby wheel corn boss office phrase buzz

These were captured from the splitMnemonic RETURN value in a single interceptable call, exposing all five shares simultaneously.


Secret Reconstruction Verification

After the split the tool exhaustively verifies the scheme by reconstructing the secret from every valid share subset. This produced 16 total __restore_secret calls and 256 total __shamirInterpolation calls (16 bytes × 16 reconstructions):

Subset size Calls Formula
3 shares 10 C(5,3) = 10
4 shares 5 C(5,4) = 5
5 shares 1 C(5,5) = 1
Total 16

Every one of the 16 __restore_secret calls returned the correct secret:

Buffer<76c0b6f95191db4d6ec13e0d8fb2cd09>

This matches the leading 16 bytes of the original captured entropy 76c0b6f95191db4d6ec13e0d8fb2cd09.... Zero divergence was observed across any share subset combination.

Example interpolation call — three-share subset {2,4,7}, byte 0:

__shamirInterpolation([[2,68],[4,122],[7,92]]) → RETURN=118

118 = 0x76 is the first byte of the secret, directly visible in the return value.


Global window.share Exposure

The post-split mnemonic scanner detected the most recently displayed share mnemonic stored on the global window object — observed approximately 33 seconds after the split completed:

window.share = "sunny dentist eagle promote blush baby wheel corn boss office phrase buzz"

This is Share 5 (x=12) from the split, stored at window.share and directly readable by any script with page-context access without requiring function hooking or timing precision. The property persists until overwritten.


Post-Split Scan Results

GF(256) Table Scan

The v5 scanner broadened the type check from Array.isArray() to also match Buffer and TypedArray values, allowing it to detect the jsbtc tables which are stored as Buffer.alloc() results. Both tables were extracted directly from window:

GF256_SCAN (post-split) begin
GF256_TABLE (named) GF256_EXP_TABLE
  0103050f113355ff1a2e7296a1f813355fe13848d87395a4f702060a1e2266aa
  e5345ce43759eb266abed97090abe63153f5040c143c44cc4fd168b8d36eb2cd
  4cd467a9e03b4dd762a6f10818287888839eb9d06bbddc7f8198b3ce49db769a
  b5c457f9103050f00b1d2769bbd661a3fe192b7d8792adec2f7193aee92060a0
  fb163a4ed26db7c25de73256fa153f41c35ee23d47c940c05bed2c749cbfda75
  9fbad564acef2a7e829dbcdf7a8e89809bb6c158e82365afea256fb1c843c554
  fc1f2163a5f407091b2d7799b0cb46ca45cf4ade798b8691a8e33e42c651f30e
  12365aee297b8d8c8f8a8594a7f20d17394bdd7c8497a2fd1c246cb4c752f6
GF256_TABLE (named) GF256_LOG_TABLE
  0000190132021ac64bc71b6833eedf036404e00e348d81ef4c7108c8f8691cc1
  7dc21db5f9b9276a4de4a6729ac90978652f8a05210fe12412f082453593da8e
  968fdbbd36d0ce94135cd2f14046833866ddfd30bf068b62b325e29822889110
  7e6e48c3a3b61e423a6b2854fa853dba2b790a159b9f5eca4ed4ace5f373a757
  af58a850f4ead6744faee9d5e7e6ade82cd7757aeb160bf559cb5fb09ca951a0
  7f0cf66f17c449ecd8431f2da4767bb7ccbb3e5afb60b1863b52a16caa55299d
  97b2879061bedcfcbc95cfcd373f5bd15339843c41a26d47142a9e5d56f2d3ab
  441192d923202e89b47cb8267799e3a5674aeddec531fe180d638c80c0f77007
GF256_SCAN (post-split) complete — tables found: 2

Both tables match the canonical AES-field tables for the irreducible polynomial x^8 + x^4 + x^3 + x + 1 (0x11B) exactly — byte-for-byte across all 255 (EXP) and 256 (LOG) values. This confirms the implementation uses the standard AES GF(256) field. Shares produced by this tool are cross-implementation compatible with any Shamir library that uses the same field.

Candidate Share Array Scan

CANDIDATE_ARRAY_SCAN begin
CANDIDATE_ARRAY_SCAN complete — found: 0

No candidate numeric arrays were found on the window object after the split.


Identified Vulnerabilities

Vulnerability 1 — Plaintext Secret Exposure in Browser Memory

The raw 128-bit mnemonic entropy is passed as a plaintext buffer directly to __split_secret:

{"type":"Buffer","data":[118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9]}

This value is observable at the point of the function call, in its argument list, and as the a0 constant in every polynomial coefficient tuple. A co-resident script intercepting any one of these three exposure points recovers the complete wallet entropy without collecting any shares.

Impact: Any script with page-context access — browser extensions, injected scripts, developer console instrumentation — can capture the complete wallet secret before or during share generation.


Vulnerability 2 — Shamir Polynomial Coefficients Expose Secret Bytes Directly

All 16 polynomial coefficient tuples are observable via the __shamirFn hook. The first element of every tuple is a0 = f(0) = secret byte. Collecting all 16 a0 values directly reconstructs the full 128-bit secret:

[118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9]

This matches the __split_secret secret buffer exactly. No shares need to be collected — polynomial observation alone recovers the secret.


Vulnerability 3 — Complete Share Set Returned in a Single Observable Call

__split_secret returns all five share buffers as a single JavaScript object. splitMnemonic returns all five share mnemonics as a plaintext array. Both were captured in full from single interceptable calls:

// __split_secret return value — all 5 shares in one object
{
  "2":  "Buffer<440bb84a7d9ace2a1a4862946d74a1bd>",
  "4":  "Buffer<7a928740fab9fe36fe072b30c5f91e22>",
  "7":  "Buffer<5c04eeb2fd7bc39cbff029e721ca66c7>",
  "9":  "Buffer",
  "12": "Buffer"
}

// splitMnemonic return value — all 5 share mnemonics in one array
[
  "dune fruit barely will provide bench hard arrange nerve hill chronic kid",
  "kind neither south voice paper bread way tornado cotton cool elegant duty",
  "foster cherry protect wife rotate soft zone ahead transfer atom often moment",
  "scrap dizzy will pupil analyst ski hollow decorate harvest wise bamboo twenty",
  "sunny dentist eagle promote blush baby wheel corn boss office phrase buzz"
]

Intercepting either return value yields the complete share set in a single operation. The security model of distributing shares across separate parties is defeated before any distribution can occur.


Vulnerability 4 — Seven Internal Cryptographic Functions Exposed on window

The following functions are attached to the global window object and are callable by any co-resident script:

window.__split_secret
window.__restore_secret
window.__shamirFn
window.__shamirInterpolation
window.splitMnemonic
window.split_mnemonic
window.calc_shares

A malicious script can call window.__restore_secret(shares) directly to reconstruct secrets from any collected shares. It can call window.__shamirFn to evaluate arbitrary polynomials, or window.splitMnemonic to generate its own splits. None of these operations require reverse-engineering the implementation — they are openly callable.


Vulnerability 5 — Last Share Mnemonic Persisted at window.share

After split completion the application stores the most recently displayed share mnemonic at the global property window.share:

window.share = "sunny dentist eagle promote blush baby wheel corn boss office phrase buzz"

Confirmed in this session at 23:49:06, approximately 33 seconds after the split completed. This property is directly readable by any script with page-context access, requires no timing sensitivity, and persists indefinitely until overwritten.


GF(256) Arithmetic Assessment

What the data shows

All 16 __restore_secret calls — covering all C(5,3)=10 three-share subsets, all C(5,4)=5 four-share subsets, and the full five-share reconstruction — returned the correct value Buffer<76c0b6f95191db4d6ec13e0d8fb2cd09>. The implementation is internally consistent: any valid share subset correctly reconstructs the secret.

GF(256) field confirmed — AES 0x11B polynomial

The v5 scanner extracted both GF(256) lookup tables from window. Comparing them against the reference tables generated from the irreducible polynomial x^8 + x^4 + x^3 + x + 1 (0x11B, the AES field):

  • GF256_EXP_TABLE (255 bytes): exact match
  • GF256_LOG_TABLE (256 bytes): exact match

The first eight bytes of the extracted EXP table are 01 03 05 0f 11 33 55 ff, identical to the known AES-field reference. This is independently corroborated by the captured __shamirFn return values — for example, __shamirFn(4, [118,230,242]) returns 122, which is the correct result for f(4) = 118 ⊕ (230·4) ⊕ (242·4²) computed in GF(256) with the 0x11B polynomial.

Cross-implementation compatibility is confirmed. Shares produced by this tool can be reconstructed by any Shamir implementation that uses the standard AES GF(256) field, including the reference Python secretsharing library, ssss, and any BIP-compatible implementation.


Recommended Mitigations

  1. Remove all global function exposure. Internal cryptographic functions must not be attached to window. Wrap all Shamir logic in a closed module scope with no global exports.

  2. Do not return all shares in a single call. __split_secret and splitMnemonic must not aggregate and return the complete share set simultaneously. Shares should be produced and handled individually to prevent full-set interception.

  3. Remove window.share persistence. Share mnemonics must not be stored as named global window properties. Any temporary reference must be cleared immediately after display.

  4. Minimise secret lifetime in memory. Zero the raw entropy buffer as soon as share generation completes:

    secretBuffer.fill(0);
  5. Harden and document GF(256) table provenance. Tables should be hardcoded constants sourced from a documented reference (e.g. FIPS 197), verified against known test vectors at module load time, and covered by independent test cases to ensure cross-implementation compatibility.

  6. Conduct a formal security audit. All cryptographic code handling wallet entropy must undergo external review before any production use.


Conclusion

This clean-session run confirms and extends all previously identified findings with no prior-session contamination. The Bitaps Shamir implementation is functionally correct — all 16 reconstruction calls across every valid share subset returned the correct secret with zero divergence. The v5 scanner additionally confirmed the GF(256) field as standard AES 0x11B, resolving the cross-implementation compatibility question. However, the browser-based implementation exposes the secret at multiple points during execution and makes all five shares available to any co-resident script before distribution has occurred.

Confirmed findings — all directly evidenced by this session's log:

Finding Log evidence
Mnemonic entropy 76c0b6f9... captured via getRandomValues hook 23:48:08.714
Secret [118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9] observable in __split_secret args 23:48:33.352
Entropy and secret bytes identical (no transformation) Cross-check: 0x76=118, 0xc0=192, 0xb6=182, 0xf9=249
All 16 polynomial a0 values equal secret bytes exactly 16 __shamirFn calls at x=2
All 5 share buffers returned in single __split_secret return 23:48:33.352
All 5 share mnemonics returned in single splitMnemonic return 23:48:33.410
16/16 reconstructions return correct secret — zero divergence All __restore_secret returns (16 calls)
256 __shamirInterpolation calls — all consistent Full log
window.share = Share 5 mnemonic after split 23:49:06.144
7 internal cryptographic functions exposed on window Function hooks throughout
GF(256) EXP and LOG tables extracted from window — confirmed AES 0x11B field GF256_SCAN: 2 tables, exact match
Cross-implementation compatibility confirmed Tables match 0x11B reference byte-for-byte
Coefficient buffer overflow on final secret byte — new entropy requested correctly 4th ENTROPY call at 23:48:33.350
Candidate share arrays not window-scoped CANDIDATE_ARRAY_SCAN: 0 found

Disclosure Note: This report was produced through passive runtime observation of the public challenge interface in a fresh isolated browser window. No server-side code was accessed or modified.

My BTC ADDRESS : bc1q2nq97ahyrq29pna42ycqyrvlk349xt2urrm2qm

Files :

FINAL-LOG.txt
On request I can provide the exact code used to produce the above console log output

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions