rippex/ripple wallet lost passphrase
#6
very good, thanks for testing.

Well, for better speed you could also use the modified version (variant) that uses the perl module (Crypt::AuthEnc::CCM), which itself is optimized (but the speed isn't that much better, because even the version above has only some additional "slow perl code" around the AES-ECB - a module again - decryption calls, which again is optimized in ANSI C/assembly).

So the variant would be (ripple_rippex_cracker_with_module.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

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

use strict;
use warnings;

use JSON         qw (decode_json);
use MIME::Base64 qw (decode_base64);

use Crypt::PBKDF2;
use Crypt::AuthEnc::CCM;


#
# 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=


#
# 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 $pt_len  = length ($ct_raw) - $tag_len;  # => 146

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);


  #
  # Decrypt with AES-256 CCM:
  #

  my $aes = Crypt::AuthEnc::CCM->new ("AES", $key, $iv_raw, $adata, $tag_len, $pt_len);

  $aes->decrypt_add ($ccm_data); # we don't need the result (plaintext) for the verification

  my $result_tag = $aes->decrypt_done ();

  $result_tag = substr ($result_tag, 0, $tag_len);


  #
  # Verify:
  #

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

    exit (0);
  }
}

exit (1);


It would also be interesting if we can make any assumptions about the "data length". Is it always the same ?
We can see that the decrypted (plain) text has some arrays in it like: "contacts":[]
This probably isn't always empty (the contacts list)... so I'm not sure if the data length is always the same.

In theory you could make some further optimizations if you know that the data length is always the same/fixed (and therefore how often we need to iterate/loop within the main decryption loop)... I show the little bit more optimized version with fixed data length below:
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 (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 $LEN = 146; # length ($data);
  my $DIV = 9; # int ($LEN / 16);
  my $MOD = 2; # int ($LEN % 16);

  my $ctr = "\x01" . $iv . "\x00\x01"; # CTR
  my $b   = "\x19" . $iv . "\x00\x92"; # block , pack ("S>", $LEN);

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


  # init y:

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


  # main data loop:

  for (my ($i, $j) = (2, 0); $i < $DIV + 2; $i += 1, $j += 16)
  {
    $b = $aes->encrypt ($ctr, $key);

    my $d = substr ($data, $j, 16);

    $b ^= $d; # already contains the output

    # set y:

    $y ^= $b;

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

    # set CTR (increment the counter, the end/right of $ctr):

    substr ($ctr, 14) = pack ("S>", $i);
  }

  # remainder:

  #if ($MOD != 0) # for (my $i = 0; $i <= $LEN % 16; $i += 16)
  #if (1)
  {
    $b = $aes->encrypt ($ctr, $key);

    my $p = $DIV * 16;

    my $d = substr ($data, $p, $MOD);

    $b ^= $d; # already contains the output

    $b = substr ($b, 0, $MOD);

    # set y:

    $y ^= $b;

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

  # clear counter:

  substr ($ctr, 14) = pack ("S>", 0); # "\x00\x00"

  # set tag:

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

  return substr ($y ^ $b, 0, 8);
}

#
# 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:
#

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 =   8; # $ts / 8
my $ct_len  = 154; # length ($ct_raw)

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

my $iv_mod = substr ($iv_raw, 0, 13);

# 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);


  #
  # Decrypt with AES-256 CCM:
  #

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


  #
  # Verify:
  #

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

    exit (0);
  }
}

exit (1);

This of course assumes that the data length is always the same (154 total, 154 - 8 = 146 without the tag). Not sure if this is correct for real world wallets that have been used a lot (with contacts etc). So yeah, there are still several open questions, but at least the algorithm is quite clear.

Again, you are testing only with one thread.... the script itself is not multi-threaded, you would need to split rockyou.txt and run the script on each and every core of your CPU (CPU threads) and therefore get almost the full speed (again ripple_rippex_cracker_with_module.pl should be even faster, because the module is written in C/assembly, not slow perl code.... but again, this normally isn't the right place where optimizations occur, the most important thing is the slow PBKDF2 key derivation and this of course would be much faster with GPUs, the remaining part of the algorithm is almost negligible).

Please do some more tests and investigate on the data length etc. 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-24-2021, 10:34 AM