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