|Portada|Blog|Wiki|
image

In this post we are trying to first try to deduce the encryption algorithm of
the Memo app in the HP 200LX handheld (which encrypts documents when a password
is used at save time) and then if possible find a way to deduce the password
from any encrypted file without any help.

The reason I'm doing this is because this handheld computer is very good as a
note taking machine given its size, and it would be good to know what attacks
can to perform to it.  Being its software closed source and written by
corporation (HP in this case) workers I was expecting quite a poor level of
security, and truth be told, from the beginning I saw the classic signs that
the password protection for this application was done by people that ended up
"Rolling Their Own Crypto".

The problem of reading a couple of books on cryptography and doing some
college courses is that you will feel confident about having knowledge on the
area, and thus tempted at using it in your own software.  Open source standard
protocols on the other hand have multiple years of research and cryptoanalysis
done to them, so even in the best case it's very likely that you are going to
make a few mistakes here and there ending up with an insecure piece of code.
Furthermore, if you only work with people that knows less crypto than you, your
work won't ever get properly reviewed.

This Memo app is in summary a simple WYSIWYG text editor with its own binary
format that supports a few extra attributes over standard plain text: bold and
underlined text, headers and footers, custom tab positions, custom margins,
multi level outlines with a couple of formats and print fields; and in
particular, it supports saving documents with passwords.

The first thing we should do is try to detect whether a stream cipher or a
block cipher is used.  Old apps such as this one, are usually written with
really insecure algorithms, as the developers involved in them tend have little
to no knowledge in cryptography.  We can see that for other apps like the
builtin Pocket Quicken or the Note Taker there are available crack programs
that can very easily obtain the passwords from the files.  Now it's time to
break Memo as well.

The simplest way to use a Stream cipher is to generate a stream of bytes from a
password and then XOR those generated bytes with the clear text.  On the other
hand block ciphers have many possible operation modes like ECB, CBC, etc.
Since them are in general a bit more difficult to properly implement it's less
likely to be used.

There's a simple way to detect if the simplest and insecure mode of operation
for a stream cipher was used: Given two files in clear text t1 and t2, a stream
cipher c, we then encrypt them with the same password, resulting in:

  e1 = c(password) XOR t1
  e2 = c(password) XOR t2

then by using the commutative, associative and existence of a unique zero
properties of XOR, together with x XOR x = 0 for all x, we obtain:

  e1 XOR e2
    = (c(password) XOR t1) XOR (c(password) XOR t2)
    = (t1 XOR c(password)) XOR (c(password) XOR t2)
    = t1 XOR (c(password) XOR c(password)) XOR t2
    = t1 XOR 0 XOR t2,
    = t1 XOR t2.

This actually happens with our Memo documents, which btw could easily have been
prevented by adding a unique "Nonce" initialization vector.  This IV has to be
unique and should never be reused (otherwise it would become meaningless).
An easy way to produce a most-likely-nonce is by hashing the date, the plain
text to cipher and real random numbers (not pseudo-random), eg: the number of
microseconds between succesive keystrokes.  Once we have this IV, the output
would become:

  e = IV || (c(IV XOR password) XOR t)

Then:
  e1 XOR e2 =
    = (IV1 || (c(IV1 XOR password) XOR t1)) XOR
      (IV2 || (c(IV2 XOR password) XOR t2))
    = (IV1 XOR IV2) || (
         c(IV1 XOR password) XOR t1 XOR
         c(IV2 XOR password) XOR t2
      )
    = (IV1 XOR IV2) || (
         c(IV1 XOR password) XOR t1 XOR
         c(IV2 XOR password) XOR t2
      )

which would equal (IV1 XOR IV2) || (t1 || t2) only if there is a collision in c
with different initialization vectors (this is why ideally we would like a
cryptographically secure function f), or if IV1=IV2 which should not happen by
definition.

In any case when we save different documents using the same key we have seen
that the same stream is generated every time and simply XORed with the plain
text.

Note that we can obtain the stream for a given password by saving a document
with and without password, and then:
  c(password)
    = c(password) XOR 0
    = c(password) XOR (t1 XOR t1)
    = (c(password) XOR t1) XOR t1
    = e1 XOR t1

image

Now that we can obtain c(password), The next easy step is to see if there's a
certain period in the stream s, defined as min_p such that exists x where for
all i>=x: s[i]=s[i+p].

It happened that independently of the password length there was actually a
small period: 2159.  Every time that we see a period we should decompose it
into powers of their prime factors.  In this case 2159 = 17 * 127.

When the period is a composite number it might be the case that the stream s is
built by XORing two streams of smaller periods, which here would be s = s1 XOR
s2 with respective periods of 17 and 127.  We can check this hypothesis by
testing:

  s1[1..127] =? s[1..127] XOR s[128..254].
  and checking if s1 has period 17.

This once again happens to be the case in the documents saved by our Memo app.
Furthermore we can see that s1 is just the password padded with null bytes to
length 17 and then repeated an infinite number of times:

  mysecurepassword\0mysecurepassword\0mysecurepassword\0

Now that we know s1 for our document, this also means that s2 can be obtained
from simply s2 = s XOR s1.  Additionally by testing with a different password
we observed that s2 does not depend on the content of the document.  It's the
same password dependent 127 bytes stream repeated an infinite number
of times, thus a very poor Key Derivation Function (KDF).

In summary we have:

      image

image

So, in order to obtain our s2 stream for a given document we could open a
certain clear text document, save it encrypted with the password in to a
different file, then XOR both files between them together with the cycled
password, and so in order to gain more info about the KDF I've simply tried
looking at the streams being generated from different passwords, in some
cases obtaining interesting data:
  * KDF('@@@@@@@A') and KDF('@@@@@@A@') resulted in the same stream!  This
    meant that the function had serious problematic symmetries.
  * This same stream was also obtained by KDF('@@@@@A@@'), KDF('@@@@A@@@'),
    KDF('@@@A@@@@'), KDF('@@A@@@@@') and KDF('@A@@@@@@'), but KDF('A@@@@@@@')
    generated a different ones.
  * The streams for KDF('123') and KDF('132') were different, so a permutation
    of the password not always kept the same stream.
  * The stream for KDF('122') was different than KDF('123') which shown that
    all of the password bytes were being considered.

Then I looked into the patterns with passwords of length 1 to see what
difference was obtained, basically an informal kind of basic differential
cryptoanalysis, showing that the probabilities of (specially the first few)
output bytes of the stream were heavily dependent on the password bits.

In particular for a password of length 1, I could easily observe that for
most values of b:
  * KDF(string.char(b+1))[1] - KDF(string.char(b))[1] = 5
  * KDF(string.char(b+1))[2] - KDF(string.char(b))[2] = 1
  * KDF(string.char(b+1))[3] - KDF(string.char(b))[3] = 40,
  * 10, 94, 91, 73, 44, 16, 29, 198, 224, 239, 35, 199, ...
  * Smelling really bad like a LCG:
    https://en.wikipedia.org/wiki/Linear_congruential_generator

But there was a particularly curious relationship between KDF('7') and
KDF('8'), they generated mostly the same bytes!, not only that they differed
only every 17 bytes!!! đŸ˜®

>  for i=1,127 do
      io.write(('%02x '):format(
         bit.band(secret_at0x38[i] - secret_at0x37[i], 0xff)
      ))
      if i%16==0 then print'' end
   end
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 f1 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 f5 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 f7 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 03 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 03 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 f9 00 00 00 00 00 00 00

Then I came back to the HP 200LX and tried opening that very same file (the
one encrypted with the password '7'), and as suspected, an invalid password
error was thrown; so this has to be a bug somewhere in the application,
although I currently don't know where...  I suspect it might be related to the
fact that when I saving the files with a passwords one by one, I made a few
mistakes here and there like non-matching passwords (the app asks for
confirmation) or opening previous files and/or re-saving them.

Tired of doing guesswork I changed tactics.

image

Before anything we need the executable that we want to decompile, which in the
case of the built-in HP 200LX apps is not so easy, as they are not visible on
the ROM drive, and no, they are not present as hidden files.

So there are two reasonable ways:

1. Dumping the RAM while the program is running.

This is doable in the HP 200LX because it lets you start a DOS shell while
other builtin apps are running, just start the Memo app, leave it open and
open DEBUG in the DOS shell, if you set bx=10 and cx=0, you can dump the
addressable 10 first 64KB segments of RAM (1MB), to do this just run:
  r bx
  10
  r cx
  0
  n RAMDUMP.BIN
  w 0:0
  q

The problem is that this DUMP will not contain the code of the Memo app, not
unless we tell the HP 200LX Memory Controller that it should make the Memo
app available.  For more information of what I mention here, it can be greatly
helpful the LXREF reference manual available at:
  http://mizj.com/

In particular the part of our 3MB ROM that contains the MEMO app is located
between 0x260000 and 0x280000 (128kB), so we need to map those address ranges
to the memory controller available pages: 1x64KB (page 0 at C0000h) or
8x16KB (pages 4..B at D0000h, D4000h, ..., EC000h).
This can be done by using INT 63h (AH=0) with:
  AL=page number (0, 4..B)
  BX=hardware page#
  CX=number of consecutive 16KB blocks to map
  DX=device select code (0=ROM, 1..4=RAM, 5=Plug-in slot 0, 6=Unused)
Return code is left in AH, being 0=ok, 83h=invalid DX, 8Ah=invalid BX,
8Bh=invalid AL or A4h=invalid CX.

Don't run the following DEBUG commands inside the built-in environment,
and also avoid using page 0 as it will hang-up the system, and you'll need
to do a hard reset (Control-Shift-ON).

So going back to DEBUG, we then need to map the RAM:
  a
  mov ax,4
  mov bx,98
  mov cx,1
  mov dx,0
  int 63
  int 20
  (blank line)
  g
  n DUMP98.bin
  r bx
  0
  r cx
  4000
  w d000:0
  q

Rinse and repeat for the 8 pages 98h to 9Fh.  I suggest saving the commands
into a script and then loading them with: debug <dump.txt

2. The easier alternative: downloading CMEMO.EXE directly.

When reading about this handheld computer in multiple backups and forums I've
learned that at some time HP did provide a version of the built-in software
as a software package to be run on any standard PC.  This is called CPACK200
or "HP 200LX Connectivity Pack", available after free registration on
VetusWare.com:

https://vetusware.com/download/200LX%20Connectivity%20Pack%20__/?id=8545

That's a "RAR" file containing 3 disk images, that can be mounted with:
mkdir /tmp/1; sudo mount HP200lx_1of3.IMA /tmp/1;  Inside of the first disk
image there's a file called APPS.ARJ (that can be extracted with 7z x), and
within it there's CMEMO.EXE, our desired executable.

Since I already did all of this, you can get this file from /hp200lx/cmemo

3.  Reverse Engineering CMEMO

To do this I've used ghidra:

  https://ghidra-sre.org/

Where I created a new project, imported CMEMO.EXE, analyzed the file, then at
"Window -> Memory Map" I created a new Memory RW entry at 0000:0000 with length
0x10000; and finally started adding annotations at the detected functions.

One easy one to start with is "fclose", which you can find by searching at
the bytes b3 3e (MOV AH, 0x3e) cd 21 (INT 0x21), being located at 1000:3244.

int fclose(int file_dos_handler)

{
  code *pcVar1;
  int iVar2;
  undefined in_CF;
  
                    /* close file (bx=handle) */
  pcVar1 = (code *)swi(0x21);
  (*pcVar1)();
  iVar2 = 0;
  if ((bool)in_CF) {
    iVar2 = -1;
  }
  return iVar2;
}


Then we search for the write syscall INT 21h with AH=40h, same as before we
can see at 1000:3278, write_to_file(int handler, char *buffer, int nbytes):

int write_to_file(int dos_file_handler,char *buffer,int nbytes)

{
  code *pcVar1;
  int iVar2;
  
                    /* Write To File or Device Using Handle */
  pcVar1 = (code *)swi(0x21);
  iVar2 = (*pcVar1)();
  return iVar2;
}


If we right click on the function address, then select "References -> Show
Call Tree", we will see 3 functions, one of them being "write_block", that
as we can see it's in charge of calling "write_to_file", yet if we look before
that we can see that it runs some extra stuff before saving the block
depending on whether a certain string is empty!  This is more or less where
the bytes to save are XORed with the content of two streams, one of 17 bytes
and another of 127 bytes!

Now we know that not only the PRNG KDF function was generating a weak stream
of bytes, it was also truncated to 127 bytes and then looped forever!, here
I added symbols and a bunch of comments:

int write_block(int dos_file_handler,char *buffer,int nbytes)

{
  byte *pbVar1;
  int *piVar2;
  int iVar3;
  undefined2 unaff_DS;
  char *buffer_00;
  int local_6;
  byte *local_4;
  
  if (*(char *)PASSWORD == '\0') {
    buffer_00 = (char *)((ulong)buffer & 0xffff | (ulong)buffer._2_2_ << 0x10);
  }
  else {

                    /* assume DS=0, here 0x51ee is TMP_WRITE_BUFFER */

    memcpy((char *)CONCAT22(unaff_DS,0x51ee),
           (char *)((ulong)buffer & 0xffff | (ulong)buffer._2_2_ << 0x10),nbytes);
    local_4 = (byte *)TMP_WRITE_BUFFER;
    for (local_6 = 0; local_6 < nbytes; local_6 = local_6 + 1) {
      pbVar1 = local_4;
      *pbVar1 = *pbVar1 ^ **(byte **)&PASSWORD_CURSOR ^ **(byte **)&PRNG_CURSOR;
      local_4 = local_4 + 1;
      piVar2 = (int *)&PASSWORD_CURSOR;
      *piVar2 = *piVar2 + 1;

/* easier to read as if (PASSWORD_CURSOR > (uint)PASSWORD + 0x10U),
   which is equivalent to (PASSWORD_CURSOR >= (uint)PASSWORD + 17) */

      if ((int)PASSWORD + 0x10U < *(uint *)&PASSWORD_CURSOR) {
        *(undefined2 *)&PASSWORD_CURSOR = (int)PASSWORD;
      }
      piVar2 = (int *)&PRNG_CURSOR;
      *piVar2 = *piVar2 + 1;

        /* OMG, this is: if (PRNG_CURSOR >= (int)PRNG_START+0x7f): */

      if ((int)PRNG_START + 0x7eU < *(uint *)&PRNG_CURSOR) {
        *(undefined2 *)&PRNG_CURSOR = (int)PRNG_START;
      }
    }
        /* assume DS=0, passing TMP_WRITE_BUFFER(=0x51ee) a write_to_file */
    buffer_00 = (char *)CONCAT22(unaff_DS,0x51ee);
  }
  iVar3 = write_to_file(dos_file_handler,buffer_00,nbytes);
  if (iVar3 != nbytes) {
    return 1;
  }
  return 0;
}


Once again, if we go to the callers, we will see the following snippet,
letting us know that this is the actual "save" function.  Note as well that
I also included an annotation for "init_prng" on the only function being
called from here that writes on top of the PASSWORD global variable:

[...]
    strcpy((char *)CONCAT22(unaff_DS,0x61f0),(char *)CONCAT22(unaff_DS,0x4b00));
    init_prng();
    pbVar1 = (byte *)0x3e81;
    *pbVar1 = *pbVar1 | 0x80;
    FUN_1000_0323(0x455d);
    local_8 = *(undefined2 *)0x3eb2;
    local_10 = *(undefined2 *)0x3eb4;
    iVar5 = *(int *)0x3eba;
    local_236 = (undefined *)*(int *)0x3eb8;
    iVar6 = FUN_1000_31f6(param_1,unaff_DS,0x8302);
    if (iVar6 == -1) {
      *(undefined *)PASSWORD = 0;
      return false;
    }
    FUN_1000_0323(0x456b);
    piVar4 = *(int **)0x3e72;
    dos_file_handler = *piVar4;
    local_a = piVar4[1];
    fp = (uint *)CONCAT22(local_a,dos_file_handler);
    local_c = dos_file_handler;
    iVar6 = write_to_file(dos_file_handler,(char *)fp,5);
    local_22a = (uint)(iVar6 != 5);
    local_c = local_c + 5;
    local_232 = (*(uint *)0x3ebe >> 0xc) + *(int *)0x3ebc * 4;
    uVar7 = *(uint *)0x3ebe & 0xfff;
    if (uVar7 < 5) {
      local_22e = uVar7 + 0xffb;
      local_232 = local_232 - 1;
    }
    else {
      local_22e = uVar7 - 5;
    }
    for (local_228 = 0; (iVar6 = (int)fp, local_22a == 0 && (local_228 < local_232));
        local_228 = local_228 + 1) {
      local_22a = write_block(local_c,(char *)CONCAT22(local_a,local_c),0x1000);
      fp = (uint *)CONCAT22(unaff_SS,&local_c);
      dos_file_handler = 0x212f;
      add_offset_to_fp(fp,0x1000);
    }
    if ((local_22a == 0) && (local_22e != 0)) {
      buffer = (char *)CONCAT22(local_a,local_c);
      dos_file_handler = local_c;
      local_22a = write_block(local_c,buffer,local_22e);
      iVar6 = (int)buffer;
    }
    FUN_1000_0323(0x456a);
    uVar10 = 0x2171;
    iVar8 = fclose(iVar6);
    if (iVar8 != 0) {
      local_22a = local_22a + 1;
    }
    *(undefined *)PASSWORD = 0;
    unaff_SS = iVar5;
[...]


Finally we have "init_prng", showing us the actual chunk of code that is used
to build the KDF stream.  Here we can also see that the generator is a
Lehmer RNG, an even weaker LCG variant:

  https://en.wikipedia.org/wiki/Lehmer_random_number_generator

With the state in a uint16_t variable PRNG_VAL_3b12, using 0x105 as the
multiplier and 0xfff1 as the prime modulus in order to achieve the maximum
period, m-1 = 0xfff0 (almost 64kB):

void init_prng(void)

{
  int *piVar1;
  uint *puVar2;
  int iVar3;
  undefined2 unaff_DS;
  uint local_4;
  
  if (*(char *)PASSWORD != '\0') {
    *(undefined2 *)&PRNG_VAL_3b12 = 0;
    for (local_4 = 0; ((int)local_4 < 0x11 && (*(char *)((int)PASSWORD + local_4) != '\0'));
        local_4 = local_4 + 1) {
      if ((local_4 & 1) == 0) {
        piVar1 = (int *)&PRNG_VAL_3b12;
        *piVar1 = *piVar1 + *(int *)((int)PASSWORD + local_4);
      }
      else {
        puVar2 = (uint *)&PRNG_VAL_3b12;
        *puVar2 = *puVar2 ^ *(uint *)((int)PASSWORD + local_4);
      }
    }
    for (; (int)local_4 < 0x11; local_4 = local_4 + 1) {
      *(undefined *)((int)PASSWORD + local_4) = 0;
    }
    if (*(int *)&PRNG_VAL_3b12 == 0) {
      *(undefined2 *)&PRNG_VAL_3b12 = 0x4b45;
    }
    local_4 = 0;
    do {
      iVar3 = (int)(((ulong)*(uint *)&PRNG_VAL_3b12 * 0x105) % 0xfff1);
      if (iVar3 == 0) {
        iVar3 = 0x4b45;
      }
      *(int *)&PRNG_VAL_3b12 = iVar3;
      *(undefined2 *)((int)PRNG_START + local_4) = *(undefined2 *)&PRNG_VAL_3b12;
      local_4 = local_4 + 2;
    } while ((int)local_4 < 0x80);
    *(undefined2 *)&PRNG_CURSOR = (int)PRNG_START;
    *(undefined2 *)&PASSWORD_CURSOR = (int)PASSWORD;
  }
  return;
}


Here is an easy to understand re-implementation in lua:

function password_seed(password)
   -- super dumb HP 200LX Memo's PRNG!
   local seed = 0 
   local function get_int16le(pos)
      local lo, hi = password:byte(pos, pos+1)
      return lo + (hi or 0)*256
   end
   for i = 1, #password do
      if i%2 == 1 then -- first, third, fifth... chars
         seed = seed + get_int16le(i)
      else -- second, fourth, sixth.. chars
         seed = bit.bxor(seed, get_int16le(i))
      end
   end
   seed = bit.band(seed, 0xffff)
   if seed == 0 then seed = 0x4b45 end
   return seed
end

function gensecret(seed)
   -- let's build 127 pseudo-random bytes
   local res = {}
   for i = 1, 127 do
      seed = seed * 0x105 % 0xfff1
      if seed == 0 then seed = 0x4b45 end
      res[#res+1] = bit.band(seed, 0xff) -- lo byte first
      res[#res+1] = bit.rshift(seed, 8)
   end
   return res
end

function kdf(password)
   return gensecret(password_seed(password))
end


And to fully decrypt the file we only need to tie the loose ends:

function crypt(text, password, secret)
   assert(#password<17)
   local p = (password..('\0'):rep(16)):sub(1,17)
   return text:sub(1,5)..text:sub(6):gsub('()(.)',
      function(i,c)
         return string.char(bit.bxor(
            c:byte(),
            secret[(i-1)%127+1],
            p:sub((i-1)%17+1):byte()))
      end)
end

>  crypt(plaintext, 'mypassword', kdf'mypassword') == encrypted
true
>  crypt(encrypted, 'mypassword', kdf'mypassword') == plaintext
true

Loading error: [string " return then 'COULD WE CRACK IT?' "]:1: unexpected symbol near 'then'

So finally we can present a few ideas on how we could crack the password with
only the encrypted file:

   Note that positions 0x32..0x35 are shared with 0xba..0xbd (because their
   indexes differ by 17*8), thus:
      secret[0x32-4]^secret[0xba] =
         = (c:byte(0x32+1)^0xff^p) ^ (c:byte(0xba+1)^0x87^p)
         =  c:byte(0x32+1)^0xff    ^  c:byte(0xba+1)^0x87, a known value!

   So we only need to hash the seeds by their xors at these shared positions
   to obtain the possible seeds, and if there's only one seed[*] then we can
   already deduce 4 characters of the password.

   [*] And there will be only one seed because the KDF dumps the full state
   every two bytes (there's no hidden state).

   Additionally the password stream has a null byte every 17 bytes, and some
   of them fall in positions that always have a known value independent of
   the content or parameters of the file, so we could use those to additionally
   filter out the state.

   Then by using the known fixed values of any file and we have obtained the
   kdf seed, we can obtain the password by XORing certain positions of the
   ciphertext with the kdf stream and the known plaintext values.

   This is an hex dump of a bare-bones example document where I enclosed
   in brackets the constant values (there may be more):

00000000: ff 32 00 45 4b 09 00 f9 00 00 00 00 00 02 00 03  .2.EK...........
00000010: 00 00 80 80 80 80 80 80 80 80 80 80 80 80 80 80  ................
00000020: 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80  ................
00000030: 80(31 ff ff 88 01 00 00)00(00)00 00 00 00 00 00  .1..............
00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000b0: 00 00 00 00 00 00 00 00 00 00(87 ff ff 88 02 00  ................
000000c0: 00)00(00)00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000130: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000140: 00 00 00(87 ff)74 68 69 73 20 69 73 20 65 6e 63  .....this is enc
00000150: 72 79 70 74 65 64 20 74 65 78 74 41 41 41 41 41  rypted textAAAAA
00000160: 41 41 41 41 41 41                                AAAAAA

   With this we can obtain the following characters: "yy3y567890yyyyyy"

   Additionally if either the header or the footer have less than 128 bytes
   then we can use the padded null bytes to continue obtaining more and more
   characters from the password.  Furthermore, if the tabs have not been
   changed on columns 200..248 (and I don't think nobody would mess with the
   tabs, especially on these columns) then we could also crack the password
   deterministically.

   And finally if that wasn't enough, then after obtaining the KDF seed and
   XORing its stream into the ciphertext; what would remain is just a simple
   Vigenère of length 17 (with the minor exception of having the addition
   done on GF(2) instead of Z(mod 256))...  We can finally perform basic
   frequency analysis (which is particularly easy to do when we have
   standard English text with spaces in between the words).

image

TLDR: Don't Roll Your Own Crypto.

Even though in this blog post I broke this joke of a cryptosystem (it took me
only a few days, less than a week), there are experts all around the world
that know what they're doing and can even perform attack on real world
protocols and implementations.

Always use popular open source implementations.  It is easier to check the
quality of the code and see if the coding practices are good, than to implement
your own library.  Crypto code can be surprisingly tricky, and even when the
source code is public and reviewed thousands of times the bugs can still be
hidden.  On closed source software this is even worse.

Always use open standards.  Most protocols in modern cryptography go through
serious review processes before being published, and after that it is
very common to see the scientific community finding attacks to them.

Always use initialization vectors with stream ciphers (with most ciphers btw),
and be very careful in how you obtain those IVs, as the security of the system
can be dependent on these values being used only once.

Be also careful on the boring details: protecting a piece of software from
side channel attacks (cache, timing, power, etc) is extremely difficult even
for experts on the subject.

How you use the algorithms is also very important.  Modes of operation,
padding construction, choosing and validation of session parameters, number of
rounds discarded and performed, among many other protocol characteristics
usually play a crucial role in the whole security and failing to properly
implement them can break the whole system.  Example:

  https://medium.com/@c0D3M/bleichenbacher-attack-explained-bc630f88ff25
  https://en.wikipedia.org/wiki/Padding_oracle_attack

Thank you for reading this blog post and feel free to leave any comments,
corrections are specially welcomed!