rippex/ripple wallet lost passphrase
#4
Quote:now to the problem ,
if you noticed , in this github https://github.com/edward852/xrp-wallet-recovery the password is unknow and the length of the password is unknow .

I'm not sure what you mean by this, but the code uses the password length + "|"+ password tripple for the key generation: https://github.com/edward852/xrp-wallet-...ery.js#L65

so yeah, the password is unknown of course but comes with the "dictionary" and the length can be determined by the password (byte length of the password itself).

Of course you can simple modify my perl script to make it a small cracker that accepts a dictionary like this (ripple_rippex_cracker.pl):
Code:
#!/usr/bin/env perl

# Author:  philsmd
# Date:    April 2021
# License: public domain, credits go to philsmd and hashcat

# This script is a POC for the AES-CCM Ripple Rippex wallet data decryption

# Instead of our own implementation, we could use:
# Crypt::AuthEnc::CCM (ccm_encrypt_authenticate)

# ATTENTION: the length of the password is prepended (and "|") for the key generation

use strict;
use warnings;

use Crypt::PBKDF2;
use Crypt::Mode::ECB;
use JSON         qw (decode_json);
use MIME::Base64 qw (encode_base64 decode_base64);


#
# Example:
#

# Example 1:

my $data = "eyJpdiI6IkZxMmFVQzRlei9nbWowU2MxRzVuZ3c9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IklNbEhiVmk5eVRNPSIsImN0IjoiOENxbkJ1WmFyWms4SzZ3eXpxc0JYNmJ6VS9ENk5MNUhsbEp1TVJyTmRNZEhFcSs2NVJ4aHFVdWxZbmw2b05DWTBSWFBrcmpEMW1aSXdCS253MkxzLzl1ejBFeHV1a1Qwb0haekR0M09BeE8zMDhtbEZHaFB2aEpSa2dYYS8wM21EODJIaHk5L2RjVG5nbS9MdnhwV1VFRTFnVnVTQVFOdm1zVTVTSGJDQVJqYy9qNUZnU28vYjhHYjk4cVUyNGxqU2dOaHBUTUtTODR4a0E9PSJ9";
# my $pass = "qwer123"; # => 7|qwer123 (length + | + pass)

# note: the PBKDF2 key (NOT known without the correct pass) is eC60JsTVBir/DcYcLKXUA71a8kwFFRsA/+JbX2PKBdQ=
# (search for DEBUG below)


# Example 2:

#my $data = "eyJpdiI6InlteHBZMFpWSzJiRXNIYldreWFrSlE9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IjVCbnp4VXZvbEYwPSIsImN0IjoiaHBNNWI1THNUQks3Wjk3WXdyc3h5NkxadG9tMGdtL0pjakxMNnVXL1FBNmh1d1ZZZ3Fab0pYVmwxTkZ6RlJhMy9WUzRoSlJsZ0RFdzcrRlQxeGEyWUxtNkFyMXJFTHA2TlhtUGk4Q2lQanVyRjBtNnFiMU4yU0ROMmhuTU54bWVOWnpqYzZDZVRUeXFGZWt4ZEFTSHVsZ0JKWU5jeGczRlV6VDg4d0FTb0NGRzNlbXZSbkpDRnFsQzFXN3dvSUdtck4yS25xV1dOSVhobEE9PSJ9";
#my $pass = "qwe949461747"; # => 12|qwe949461747 (length + | + pass)

# note: the PBKDF2 key (NOT known without the correct pass) is Q6T/AlK6i+K+xFzIakMkXvU0fi0qabz+OJe2HzyVyrI=


#
# Helper function:
#

sub aes_ccm_decrypt_tag
{
  my $key  = shift;
  my $iv   = shift;
  my $data = shift;

  my $tag = "\x00" x 8; # output

  my $len = length ($data);

  my $aes = Crypt::Mode::ECB->new ('AES', 0);

  my $iv_mod = substr ($iv, 0, 15 - 2); # WTF ? always max 13 IV ? just truncate it ?

  my $q = 2; # 16 - 1 - length ($iv_mod) = 15 - $iv_len = 15 - 13


  # init CTR:

  my $flags = $q - 1; # => 2 - 1 = 1

  my $ctr = "\x00" x 16;

  substr ($ctr,  0,  1) = pack ("C", $flags);
  substr ($ctr,  1, 13) = $iv_mod;
  substr ($ctr, 14,  2) = pack ("S>", 1);

  # 0x01[IV_MOD:13]0001 (16 bytes)


  # set first block:

  $flags = 0;

  $flags |=             0 << 6; # no adata
  $flags |= ((8 - 2) / 2) << 3; # tag_len = 8
  $flags |= $q - 1;

  # => 25 = 0x19

  my $b = "\x00" x 16;

  substr ($b,  0,  1) = pack ("C", $flags);
  substr ($b,  1, 13) = $iv_mod;
  substr ($b, 14,  2) = pack ("S>", $len);

  # 0x19[IV_MOD:13][DATA_WITHOUT_TAG_LEN:2] (16 bytes)


  # init y:

  my $y = $aes->encrypt ($b, $key); # => 16 bytes


  # main data loop:

  for (my $i = 0; $i < $len; $i += 16)
  {
    my $cur_len = 16;

    my $left = $len - $i;

    if ($left < 16)
    {
      $cur_len = $left;
    }


    # this is the main decryption code (tmp already contains the output):

    $b = $aes->encrypt ($ctr, $key);

    my $tmp = "\x00" x 16;

    for (my $j = 0; $j < $cur_len; $j++)
    {
      substr ($tmp, $j, 1) = substr ($data, $i + $j, 1) ^ substr ($b, $j, 1);
    }


    # set y:

    $b = "\x00" x 16;

    substr ($b, 0, $cur_len) = substr ($tmp, 0, $cur_len);

    for (my $j = 0; $j < 16; $j++)
    {
      substr ($y, $j, 1) ^= substr ($b, $j, 1);
    }

    $y = $aes->encrypt ($y, $key);


    # update CTR (increment the counter, the end/right of $ctr):
    # (could also use pack ("S>", $ctr_value + 1))
    #
    # my $ctr_value = unpack ("S>", substr ($ctr, 16 - $q)); # get last 2 bytes
    #
    # $ctr_value++;
    #
    # substr ($ctr, 16 - $q) = pack ("S>", $ctr_value); # set last 2 bytes

    for (my $j = 0; $j < $q; $j++) # q is always 2
    {
      my $val = ord (substr ($ctr, 15 - $j, 1)) + 1;

      if ($val >= 256)
      {
        $val = 0;
      }

      substr ($ctr, 15 - $j, 1) = chr ($val);

      if ($val != 0)
      {
        last;
      }
    }
  }

  # clear some $ctr bytes if needed

  for (my $i = 0; $i < $q; $i++) # q is always 2
  {
    substr ($ctr, 15 - $i, 1) = "\x00";
  }

  # Set the tag:

  $b = $aes->encrypt ($ctr, $key);

  for (my $i = 0; $i < 8; $i++) # or 16 for tag_len = 16
  {
    substr ($tag, $i, 1) = substr ($y, $i, 1) ^ substr ($b, $i, 1);
  }

  return $tag;
}


#
# Start:
#

my $data_raw = decode_base64 ($data);

my $json = decode_json ($data_raw);

if (! defined ($json->{'ct'})    ||
    ! defined ($json->{'iv'})    ||
    ! defined ($json->{'salt'})  ||
    ! defined ($json->{'iter'})  ||
    ! defined ($json->{'adata'}) ||
    ! defined ($json->{'ks'})    ||
    ! defined ($json->{'ts'})    ||
    ! defined ($json->{'mode'})  ||
    ! defined ($json->{'cipher'}))
{
  print "ERROR: invalid data provided\n";

  exit (1);
}

my $ct     = $json->{'ct'};
my $iv     = $json->{'iv'};
my $salt   = $json->{'salt'};
my $iter   = $json->{'iter'};
my $adata  = $json->{'adata'};
my $ks     = $json->{'ks'};
my $ts     = $json->{'ts'};
my $mode   = $json->{'mode'};
my $cipher = $json->{'cipher'};

my $ct_raw   = decode_base64 ($ct);
my $iv_raw   = decode_base64 ($iv);
my $salt_raw = decode_base64 ($salt);


#
# Sanity checks:
#

# mode:ccm, cipher:aes, ks:256, ts:64
# (ks = key size, ts = tag size)

if ($mode ne "ccm")
{
  print "ERROR: the mode is not 'ccm'\n";

  exit (1);
}

if ($cipher ne "aes")
{
  print "ERROR: the cipher is not 'aes'\n";

  exit (1);
}

if ($ks != 256)
{
  print "ERROR: the key size is not 256\n";

  exit (1);
}

if ($ts != 64)
{
  print "ERROR: the tag size is not 64\n";

  exit (1);
}

if ($adata ne '')
{
  print "ERROR: the adata should be empty in our case\n";

  exit (1);
}


my $tag_len = $ts / 8; # => 8
my $ct_len  = length ($ct_raw); # => 154 ?

my $ccm_data = substr ($ct_raw, 0, $ct_len - $tag_len); # length: 154 - 8 = 146
my $ccm_tag  = substr ($ct_raw, $ct_len - $tag_len); # last 8 bytes

# init PBKDF2-HMAC-SHA256

my $pbkdf2 = Crypt::PBKDF2->new
(
  hasher     => Crypt::PBKDF2->hasher_from_algorithm ('HMACSHA2', 256),
  iterations => $iter,
  output_len => $ks / 8
);

while (my $pass = <>)
{
  chomp ($pass);

  # IMPORTANT: prepend length to password:

  my $len_and_pass = length ($pass) . '|' . $pass;


  #
  # KDF (key derivation functions, generate key from password+salt):
  #

  # most heavy part of the algorithm:

  my $key = $pbkdf2->PBKDF2 ($salt_raw, $len_and_pass);

  # DEBUG:
  # print "Key (-m 10900) is: " . encode_base64 ($key, '') . "\n";


  #
  # Decrypt with AES-256 CCM:
  #

  my $result_tag = aes_ccm_decrypt_tag ($key, $iv_raw, $ccm_data);


  #
  # Verify:
  #

  if ($result_tag eq $ccm_tag)
  {
    print "Password found '$pass'\n";

    exit (0);
  }
}

exit (1);

(note: you would only need to change the $data with your own wallet data, of course, if you are testing a different wallet than the "qwer123" test wallet)

it just loops over all password from STDIN/pipe or a dictionary:
Code:
perl ripple_rippex_cracker.pl dict.txt


yeah, I agree many algorithms make somehow sense to be implemented. It's for sure an interesting algorithm and actually NOT too bad to crack (with "only" 1000 iterations)... but again it's very important that the user has quite some idea what the password could be... otherwise it still could become infeasible to crack.

You can run the above script also within several shells and split the dictionary in chunks and therefore use all your CPUs power ( 1 script execution for each CPU thread ).

Maybe you can test it a little bit also with a smaller dict and a new test wallet (generated with the same software) with a known password)... e.g. a dictionary with only a few thousands of words in the dict etc.

That's already an important step to see if the algorithm is correct and that the POC is working, maybe later on we could think about creating a new github issue or even comment on the old SJCL issue (but the algorithm and especially the input is not exactly the same, the other algo doesn't need the password length to be prepended).

Maybe this cracker script already helps, let me know. thx
Reply


Messages In This Thread
rippex/ripple wallet lost passphrase - by kiara - 04-18-2021, 08:13 PM
RE: rippex/ripple wallet lost passphrase - by philsmd - 04-21-2021, 06:05 PM