Introduction
When I wrote the demonstration application to accompany sprintf_s: Speed Bumps Ahead, I noticed that the stack frames that lay below the frame that belonged to the active function remained intact until the the next time that a function was called, and that portions could survive for much longer, even outlinving the active function. The research that went into that article also paved the way for being able to read registeers by way of inline asssembly instructions, and to stash their values anywhere that I wanted, including out parameters of the active function.
Background
My renewed interest in that discovery was prompted by recent reading about how the Spectre and Melteown CPU flaws work. It occurred to me that if another process can peek at a look-aside table, then abandoned stack frames aren't safe, either. Though I am not sufficiently familiar with interprocess communication to demonstrate such an exploit, I intend to show how a routine that becomes part of the same process can do so, with potentially devastating consquences for data that was supposed to be secure.
Using the code
The archive contains the following directories.
PilferMe
is the source code of the program, along with the master copies of the shell script and response files required to put it through its paces, which include all permutations of operating parameters. Debug
is the debug build of the program, along with a shell script and the ten response files required to run it. The script and response files are identical to those in the Release
directory, and are copied into it by a post-build script. Release
is the release build of the program, along with a shell script and the ten response files required to run it. The script and response files are identical to those in the Debug
directory, and are copied into it by a post-build script. Util
contains the utility program used by the shell script to suspend its termination so that you can view or copy the output, along with the support DLLs required by the main program and that utility. As well, there is a link from which you can obtain Microsoft Visual C++ 2015 Redistributable Update 3 RC, which you will need if you don't already have it.
The simplest way to run the script is by displaying the Release directory in the Windows File Explorer, then double-clicking or otherwise selecting PilferMe_UnitTests.CMD
to run it.
- When launched, the script executes
PilferMe.exe
, and puts it through one of the 10 scenarios that I identified. Since PilferMe_UnitTests.CMD
feeds the inputs from a set of response files, all ten scenarios run "hands free" until the final prompt, which comes courtesy of WWPause.exe
, and keeps the window open for your inspection and copying. - Due to a shortcoming of the design and implementation of
WWPause.exe
, you cannot use the spiffy new Ctrl-A
and Ctrl-C
keyboard accelerators that arrived with recent improvements in Windos 10 to copy the contents of the window into the clipboard. However, the old way of copying text from a console window that uses Alt-Spacebar
followed by Ctrl-A
, then the Enter
key works just fine, and is how I captured the text that appears in the listing below.
Listing 1, shown below, is the output displayed on my machine from a recent run.
---------------------------------------------------------------------
Info: C:\Users\DAG\Documents\Articles_2018\Pilfering_Old_Stack_Frames\PilferMe\Release\PilferMe_UnitTests.CMD, version 2018/07/23 03:00 Begin
---------------------------------------------------------------------
-------------------------------------------------------------------
Add the utilities directory to the system PATH list.
-------------------------------------------------------------------
Added to PATH = C:\Users\DAG\Documents\Articles_2018\Pilfering_Old_Stack_Frames\PilferMe\Release\..\utl
-------------------------------------------------------------------
Case 01: Play by the rules.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:03:57 Central Daylight Time
The caller insists on playing by the rules by decrypting the message.
The caller insists on leaving the plaintext buffer exposed.
A command line argument covered the cheat/honest flag.
Since the Cheat flag is OFF, the Scrub flag is moot.
Values within function CreateEncryptedMessage:
lpSubEBP = 0x0075f9b0
achPlainText = 0x0075b9ac
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
52 68 2d 8c e7 14 42 d1 d7 ce c4 8f ed 72 8f 2b df f0 2c fe 40 5f 13 d9 28 86 1f ae a9 f6 e0 89 8c de 73 bd e4 11 c7 88 e2 6c 83 a0 1a 75 30 8b e2 95 8b 7e 63 8e 0c 19 54 d2 e9 56 b3 73 f0 ef f6 89 11 77 30 66 01 47 17 56 51 5a 34 c1 46 d5 c7 cf f8 df 36 08 8a f6 38 5a 41 47 03 ed d6 20 32 0d 15 b6 56 72 b9 c7 6a bd 3b fa 34 76 ee 60 ff 27 0e 79 98 cf d9 92 b2 b4 3b 1e 5e d4 8b 0d 13 7a 7d 08 dc 13 b2 18 cb 7c c8 0a dd 6b 15 fb c8 c2 cb 14 d4 d0 0b 6e 49 0b ca da 84 0d dd 57 8d b0
Character array achPlainText is intact in the stack frame.
Message digest of original, per CreateEncryptedMessage, and decrypted copy, per DecryptString_P6C, are identical.
Message text is as follows:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Done!
<*>
Status code = 0x0001 (1 decimal)
Please press the Return (ENTER) key to exit the program.
-------------------------------------------------------------------
Case 02: Play by the rules AND ovwrwrite the plaintext.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:03:58 Central Daylight Time
The caller insists on playing by the rules by decrypting the message.
The caller instructed that the plaintext should be overwritten.
A command line argument covered the cheat/honest flag.
Since the Cheat flag is OFF, the Scrub flag is moot.
Values within function CreateEncryptedMessage:
lpSubEBP = 0x0057fd3c
achPlainText = 0x0057bd38
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
a2 ae db 63 e7 ed 19 a1 26 b2 68 88 37 51 b9 26 1e 9c a7 3e ae 13 09 52 41 e5 92 ab b7 6f b0 ac dc 91 55 9e f2 02 8d 5e 96 6c 6b 72 05 6b fb 96 72 d9 9a 5b ab 16 4a 2f 00 ab 0d f1 f6 75 40 58 56 c8 62 64 84 26 39 5c fe 54 97 32 50 c9 92 4b 07 bb 18 8e 7f 84 6f 45 f7 eb c2 0d 03 c8 1d 24 4d 28 e8 ff a7 a2 85 a1 42 b5 c7 b2 a5 02 18 ba 8a 6d f3 da 52 e9 8f 0a cc 37 84 f6 32 6f 02 9e 61 42 e1 90 2b 63 83 06 2c e4 59 6c 32 a5 1e ed fb 97 b6 2a f6 7c e8 16 7e 2b 8e 73 16 12 07 2a fc 4e
The leak has been plugged by overwriting character array achPlainText
with ASCII NULL characters.
Message digest of original, per CreateEncryptedMessage, and decrypted copy, per DecryptString_P6C, are identical.
Message text is as follows:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Done!
<*>
Status code = 0x0001 (1 decimal)
Please press the Return (ENTER) key to exit the program.
-------------------------------------------------------------------
Case 03: Attempt to cheat BUT ovwrwrite the plaintext.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:03:59 Central Daylight Time
The caller granted permission to cheat by harvesting the plaintext from the working storage
that function CreateEncryptedMessage abandoned.
The caller instructed that the plaintext should be overwritten.
A command line argument covered the cheat/honest flag.
A command line argument covered the scrub/leave flag.
Values within function CreateEncryptedMessage:
lpSubEBP = 0x00f6fc24
achPlainText = 0x00f6bc20
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
a2 ae a7 46 e7 ed 7d 33 26 b2 f3 c2 37 51 e7 1b 1e 9c 8b cf ae 13 72 f9 41 e5 5e b7 b7 6f cc a1 30 ba aa 9f 10 d0 51 71 05 9c 85 2f a1 ee 68 8e 1b 93 68 5a 4b 0d e8 a0 9e b4 51 8b ac 01 d4 42 cd 1a 71 ce 01 2f 09 fd 11 c4 3a f9 64 2b 0f 89 e3 df a2 0c 62 dd eb d9 2c 13 ed 4b 1c f8 a0 77 96 b8 7d 55 a1 29 19 d9 d5 61 96 2d 2f 54 23 69 aa 90 d5 16 af ba 57 13 0f d9 6b d2 46 12 d1 38 59 9a ea 87 27 5f 42 1b 4c 92 4e 1b f9 69 38 b2 71 33 ca 55 34 7b 5a 47 e2 e2 49 97 9d a0 97 97 ab f2
The leak has been plugged by overwriting character array achPlainText
with ASCII NULL characters.
Cheating...
Values within function main:
MAIN_OFFSET_TO_PLAINTEXT_LENGTH = 0x0000404c (16460 decimal)
SUB_OFFSET_TO_PLAINTEXT_LENGTH = 0x00004028 (16424 decimal)
MAIN_OFFSET_TO_PLAINTEXT = 0x00004028 (16424 decimal)
SUB_OFFSET_TO_PLAINTEXT = 0x00004004 (16388 decimal)
OFFSET_TO_FUNCTION_EBP = 0x00000024 (36 decimal)
lpMainEBP = 0x00f6fc48
intPlainTextLength = 0x00f6fc24 (16186404 decimal)
lpszPlainText = 0x00f6bc20
lpszPlainText:
CreateEncryptedMessage ran in secure mode and scrubbed the plaintext.
Cheating... Done!
Done!
<*>
Please press the Return (ENTER) key to exit the program.
-------------------------------------------------------------------
Case 04: Attempt to cheat AND leave the plaintext exposed.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:04:00 Central Daylight Time
The caller granted permission to cheat by harvesting the plaintext from the working storage
that function CreateEncryptedMessage abandoned.
The caller insists on leaving the plaintext buffer exposed.
A command line argument covered the cheat/honest flag.
A command line argument covered the scrub/leave flag.
Values within function CreateEncryptedMessage:
lpSubEBP = 0x00f8f724
achPlainText = 0x00f8b720
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
f2 f4 61 d3 e7 c6 9e b3 76 95 58 39 81 2f 7c 75 5d 47 1b d8 1c c8 dd f7 59 44 65 89 c6 e8 10 d3 7e 2f 11 20 f2 f4 4d a1 ec e9 f2 60 41 70 f5 56 c1 1d 45 99 8a 19 7a 35 3e 28 c8 12 55 3d 11 13 20 05 ce 7b e0 61 b6 f8 48 c1 df 35 ca 3d 3c 0b 9a 61 27 db 11 c3 d5 d5 10 bb 82 ad 49 d3 eb 75 4c 71 ba ef b3 6d b3 fe 52 9f ed b0 07 3e 80 81 17 55 e2 cd 1f c7 47 0b 2d bb 0c b0 b1 12 5b 9d 00 33 63 96 d2 32 67 07 00 92 b6 29 af 69 b8 b6 9b 9e 17 d1 b4 0d 74 b9 fb 31 47 8d c8 dc c1 64 8b 5b
Character array achPlainText is intact in the stack frame.
Cheating...
Values within function main:
MAIN_OFFSET_TO_PLAINTEXT_LENGTH = 0x0000404c (16460 decimal)
SUB_OFFSET_TO_PLAINTEXT_LENGTH = 0x00004028 (16424 decimal)
MAIN_OFFSET_TO_PLAINTEXT = 0x00004028 (16424 decimal)
SUB_OFFSET_TO_PLAINTEXT = 0x00004004 (16388 decimal)
OFFSET_TO_FUNCTION_EBP = 0x00000024 (36 decimal)
lpMainEBP = 0x00f8f748
intPlainTextLength = 0x00f8f724 (16316196 decimal)
lpszPlainText = 0x00f8b720
lpszPlainText:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Cheating... Done!
Done!
<*>
Please press the Return (ENTER) key to exit the program.
-------------------------------------------------------------------
Case 05: Attempt to cheat AND leave the plaintext exposed,
but prompt for the second option, responding with NO.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:04:01 Central Daylight Time
The caller granted permission to cheat by harvesting the plaintext from the working storage
that function CreateEncryptedMessage abandoned.
A command line argument covered the cheat/honest flag.
Scrub the plaintext output buffer or leave it intact?
Enter Y to scrub it or N to leave it exposed. >>
Response from user = n
Values within function CreateEncryptedMessage:
lpSubEBP = 0x010ffd58
achPlainText = 0x010fbd54
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
42 3b e0 21 e8 9f 4f 42 c6 78 19 cc cb 0d 43 d0 9c f2 93 1e 89 7c 41 42 72 a3 87 79 d4 61 21 3e 8e e2 1c 7e 6a b6 c9 f4 4a 0b e4 a4 52 c1 4a 17 c1 65 b0 8d be 4a 4a a5 94 01 47 5d 2f 66 0c b8 7c 9b 18 2e 6c 34 5b c2 39 62 b1 c9 73 28 b3 8e ed b1 78 1f f0 34 db e8 49 25 58 1c 64 05 90 ae 2a 56 38 4d 3d 25 47 74 c3 45 b6 fd 2a 2b bf 2a 9f a9 ad b5 d8 a8 2b c9 1f 48 c4 57 9d a7 8c ac 2a 29 9d a9 3b 09 14 21 67 04 24 47 2b 99 fc fa 0d b1 64 56 72 f6 f4 d8 41 d6 70 0c 42 a2 0a d6 74 b6
Character array achPlainText is intact in the stack frame.
Cheating...
Values within function main:
MAIN_OFFSET_TO_PLAINTEXT_LENGTH = 0x0000404c (16460 decimal)
SUB_OFFSET_TO_PLAINTEXT_LENGTH = 0x00004028 (16424 decimal)
MAIN_OFFSET_TO_PLAINTEXT = 0x00004028 (16424 decimal)
SUB_OFFSET_TO_PLAINTEXT = 0x00004004 (16388 decimal)
OFFSET_TO_FUNCTION_EBP = 0x00000024 (36 decimal)
lpMainEBP = 0x010ffd7c
intPlainTextLength = 0x010ffd58 (17825112 decimal)
lpszPlainText = 0x010fbd54
lpszPlainText:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Cheating... Done!
Done!
<*>
Please press the Return (ENTER) key to exit the program.
-------------------------------------------------------------------
Case 06: Attempt to cheat AND overwrite the plaintext,
but prompt for the second option, responding with YES.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:04:02 Central Daylight Time
The caller granted permission to cheat by harvesting the plaintext from the working storage
that function CreateEncryptedMessage abandoned.
A command line argument covered the cheat/honest flag.
Scrub the plaintext output buffer or leave it intact?
Enter Y to scrub it or N to leave it exposed. >>
Response from user = y
Values within function CreateEncryptedMessage:
lpSubEBP = 0x0057f9d4
achPlainText = 0x0057b9d0
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
92 81 bb 0e e8 78 b2 f5 16 5c ca 9e 15 ec ed e1 da 9d 89 82 f7 30 8b ae 8b 02 32 01 e3 da 09 b4 d7 b5 86 1e 28 27 f5 be 59 05 61 65 de f6 f3 67 91 3e 01 51 94 24 71 5d 0d 9c 0a 3b 0e 95 9a 1e 6f 19 a2 3f 97 ce 73 02 19 89 63 7b ee bf eb e5 64 0a 42 4a 99 f8 b7 c7 fe 66 e9 b0 a5 bf 76 9b cc b6 fc 2e 8a a9 1f dc b2 89 25 55 d0 91 fd eb 5a 3f b6 43 15 1d 91 5f 2c a1 93 c5 e9 f0 36 14 5a 2e be 72 3e b4 5a c2 8d b6 76 11 1c a8 a7 d6 3c a4 0f 0d 84 bf 65 4d 40 7c 50 2b 21 be ab cf 82 d5
The leak has been plugged by overwriting character array achPlainText
with ASCII NULL characters.
Cheating...
Values within function main:
MAIN_OFFSET_TO_PLAINTEXT_LENGTH = 0x0000404c (16460 decimal)
SUB_OFFSET_TO_PLAINTEXT_LENGTH = 0x00004028 (16424 decimal)
MAIN_OFFSET_TO_PLAINTEXT = 0x00004028 (16424 decimal)
SUB_OFFSET_TO_PLAINTEXT = 0x00004004 (16388 decimal)
OFFSET_TO_FUNCTION_EBP = 0x00000024 (36 decimal)
lpMainEBP = 0x0057f9f8
intPlainTextLength = 0x0057f9d4 (5765588 decimal)
lpszPlainText = 0x0057b9d0
lpszPlainText:
CreateEncryptedMessage ran in secure mode and scrubbed the plaintext.
Cheating... Done!
Done!
<*>
Please press the Return (ENTER) key to exit the program.
-------------------------------------------------------------------
Case 07: Play by the rules in responde to a single command line
argument. There should be no prompt.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:04:03 Central Daylight Time
The caller insists on playing by the rules by decrypting the message.
A command line argument covered the cheat/honest flag.
Since the Cheat flag is OFF, the Scrub flag is moot.
Values within function CreateEncryptedMessage:
lpSubEBP = 0x00effcd8
achPlainText = 0x00efbcd4
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
e2 c7 a9 bc e8 51 d3 61 66 3f 82 63 5f ca db 6f 19 49 c8 3f 65 e5 45 56 a3 61 23 8a f1 53 1a 31 c3 90 ba 70 bc a8 ca 7e 61 11 e8 e7 91 9a 2e 95 ab d8 f5 94 e2 e1 39 d9 bd 2d 3b ea 64 56 f0 46 7a 3a 66 84 be 08 09 cf ec 08 42 ff 1e a6 46 01 84 28 b1 fe ac 44 0c 30 6d 8c ae 62 42 85 38 d2 d5 13 ef 1f b4 ae af 80 e6 9d 0b d7 db fc 75 80 a4 2b 6e ad c1 cc 52 7c fd 42 12 e1 f0 12 22 85 7e f0 0c 07 af b0 a7 cf d2 dc 10 3f 1d 2b e3 fd 9e 31 b7 bb 0a e4 02 3e 9d 2b e3 a3 3b d5 48 9c ff 32
Character array achPlainText is intact in the stack frame.
Message digest of original, per CreateEncryptedMessage, and decrypted copy, per DecryptString_P6C, are identical.
Message text is as follows:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Done!
<*>
Status code = 0x0001 (1 decimal)
Please press the Return (ENTER) key to exit the program.
-------------------------------------------------------------------
Case 08: Play by the rules in response to a prompt. The command line
argument list is empty.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:04:04 Central Daylight Time
The command line was input bare (without arguments).
Prompting for selection:
OK to cheat? Enter Y to cheat, or N to play by the rules. >>
Response from user = n
Values within function CreateEncryptedMessage:
lpSubEBP = 0x0053fe68
achPlainText = 0x0053be64
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
e2 c7 b2 1f e8 51 00 f7 66 3f 6b 8a 5f ca 92 c9 19 49 ca 11 65 e5 b2 cb a3 61 4f 7a f1 53 64 b5 11 87 48 14 7c 13 27 b2 84 58 00 c4 1b 1f 39 22 7e f4 7c 70 40 fe 1a e9 d3 17 6b e1 58 0c 89 f7 4a 30 35 05 31 5f 81 db b3 73 3e 16 06 3d a6 16 66 04 2f 71 6c ed 0a 09 a3 57 c1 8d c2 7f 4e de da a9 9e 97 9b 15 3b 9d e5 27 3f 62 3c 83 13 71 84 0d a3 14 6d b4 8e 7e 77 25 eb 4f f8 53 71 95 99 05 42 37 9e 27 58 f7 fa d2 d6 3d f5 26 5c c6 6a ac 39 8a f5 33 91 12 98 53 78 57 bc 3d ee 66 17 e3
Character array achPlainText is intact in the stack frame.
Message digest of original, per CreateEncryptedMessage, and decrypted copy, per DecryptString_P6C, are identical.
Message text is as follows:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Done!
<*>
Status code = 0x0001 (1 decimal)
Please press the Return (ENTER) key to exit the program.
-------------------------------------------------------------------
Case 09: Permit cheating, but prevent it by overwriting the
plaintext. All options are responses to prompts.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:04:05 Central Daylight Time
The command line was input bare (without arguments).
Prompting for selection:
OK to cheat? Enter Y to cheat, or N to play by the rules. >>
Response from user = y
Scrub the plaintext output buffer or leave it intact?
Enter Y to scrub it or N to leave it exposed. >>
Response from user = y
Values within function CreateEncryptedMessage:
lpSubEBP = 0x013ff888
achPlainText = 0x013fb884
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
32 0e d5 d3 e9 2a e9 a2 b6 22 e3 62 a9 a8 e5 bc 58 f4 99 40 d2 99 69 17 bc c0 23 98 ff cc 35 ff 79 64 9a 75 4c 9e bd c9 a5 c2 01 ce fa 2e b5 c5 00 0b bc 7e 18 68 25 f2 fe 8e 6d c5 7e a3 11 01 f7 cb 4c 0b 06 0d b3 de 9f 69 a3 17 95 2a 13 38 17 09 c7 24 a8 bb 92 8f cb 3c 4e 4b d1 b1 85 25 63 db 2b 37 b6 f9 19 fb b0 74 b8 d4 45 38 51 f2 d5 4f ff 20 f1 04 c5 f2 5b a6 8b 71 25 f2 82 86 ae de 2b f6 78 55 17 63 04 8f 90 bb 8a 37 34 d1 a9 cb ee f2 a4 8d 33 d4 0f 15 aa 76 46 13 f2 00 ef 32
The leak has been plugged by overwriting character array achPlainText
with ASCII NULL characters.
Cheating...
Values within function main:
MAIN_OFFSET_TO_PLAINTEXT_LENGTH = 0x0000404c (16460 decimal)
SUB_OFFSET_TO_PLAINTEXT_LENGTH = 0x00004028 (16424 decimal)
MAIN_OFFSET_TO_PLAINTEXT = 0x00004028 (16424 decimal)
SUB_OFFSET_TO_PLAINTEXT = 0x00004004 (16388 decimal)
OFFSET_TO_FUNCTION_EBP = 0x00000024 (36 decimal)
lpMainEBP = 0x013ff8ac
intPlainTextLength = 0x013ff888 (20969608 decimal)
lpszPlainText = 0x013fb884
lpszPlainText:
CreateEncryptedMessage ran in secure mode and scrubbed the plaintext.
Cheating... Done!
Done!
<*>
Please press the Return (ENTER) key to exit the program.
-------------------------------------------------------------------
Case 10: Permit cheating, but prevent it by overwriting the
plaintext. All options are responses to prompts.
-------------------------------------------------------------------
PilferM begin at 2018/08/04 18:04:05 Central Daylight Time
The command line was input bare (without arguments).
Prompting for selection:
OK to cheat? Enter Y to cheat, or N to play by the rules. >>
Response from user = y
Scrub the plaintext output buffer or leave it intact?
Enter Y to scrub it or N to leave it exposed. >>
Response from user = n
Values within function CreateEncryptedMessage:
lpSubEBP = 0x008ff77c
achPlainText = 0x008fb778
Message 1 = This is message 1 of 2.
Message 2 = This is message 2 of 2.
Message Text:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Encrypting the message.... Done
Plaintext length = 130
Ciphertext length = 162
Encrypted message:
82 54 d5 26 e9 03 12 c1 06 06 97 79 f3 86 9c 0f 97 9f 89 f2 40 4e 55 8a d5 1f e6 4b 0e 46 b5 0e 1e dd 03 50 10 8a 9d 46 f7 ad 38 5e 75 d8 d8 11 ac 2e 5d 96 f9 65 99 50 32 9d ba 2b 37 79 2a d1 2e 9d 51 d5 94 b9 10 74 17 e0 cd 76 a2 28 8e da e4 9f 3b 90 cf 73 70 34 2b 5c a7 c4 8e 21 13 6c b8 e9 54 19 50 9c a8 4a f0 ed 14 36 6b 53 02 a1 15 9d b7 c3 69 0a 0f 17 89 a4 9f 47 c7 00 30 7c 05 20 bc 45 b9 b1 af 89 02 3f 4a 4c 09 72 e4 1b ad ee bd 09 50 19 d9 00 50 46 9d 34 73 ca 74 91 e6 18
Character array achPlainText is intact in the stack frame.
Cheating...
Values within function main:
MAIN_OFFSET_TO_PLAINTEXT_LENGTH = 0x0000404c (16460 decimal)
SUB_OFFSET_TO_PLAINTEXT_LENGTH = 0x00004028 (16424 decimal)
MAIN_OFFSET_TO_PLAINTEXT = 0x00004028 (16424 decimal)
SUB_OFFSET_TO_PLAINTEXT = 0x00004004 (16388 decimal)
OFFSET_TO_FUNCTION_EBP = 0x00000024 (36 decimal)
lpMainEBP = 0x008ff7a0
intPlainTextLength = 0x008ff77c (9435004 decimal)
lpszPlainText = 0x008fb778
lpszPlainText:
The two messages follow.
Message 1: This is message 1 of 2.
Message 2: This is message 2 of 2.
End of Transmission
<*>
Cheating... Done!
Done!
<*>
Please press the Return (ENTER) key to exit the program.
WWPause version 3, 0, 0, 4
Sat 04 Aug 2018 18:04:06 Local
Please press RETURN when ready...
Listing 1 is the entire output of a recent run of PilferMe_UnitTests.CMD
on my development machine.
There are a few things to call to your attention about the listing.
- Since the hexadecmial dumps of the ciphertext were written as long lines of text, they didn't wrap around.
- Although the the message text and encryption key are identical, the ciphertext generated on each run is different, thanks to the salt in the initialization vector of the AES context block.
- Since the program was linked with Adress Space Location Randomization (ASLR) enabled, no two runs will yield the same register values, even on the same machine.
Although I could easily have included an image of the command prompt window, it wouldn't show enough of the output to contribute to the article, and the editors are thereby relieved of having to ensure that the image file makes it into the publication.
Points of Interest
Earlier, I mentioned that I had learned how to use inline assembly language to grab arbitrary blocks of memory, and even CPU registers, and stuff them into locations that are visible to the C++ source code, I didn't go to that much trouble for this demonstration. As it is, the code makes it clear that the base pointer of function CreateEncryptedMessage
could easily be harvested and made visible to the calling routine, which could then work out the offset to the beginning of the buffer that holds the message. That is partially demonstrated by the following excerpt from that routine.
LPVOID lpSubEBP = NULL ;
LPVOID lpPlainText = NULL;
__asm {
lea eax , [ ebp ];
mov lpSubEBP , eax;
lea eax , [ achPlainText ];
mov lpPlainText , eax;
}
_tprintf ( TEXT( "\nValues within function %s:\n\n" ) ,
__FUNCTION__ );
_tprintf ( TEXT ( " lpSubEBP = 0x%08x\n\n" ) ,
( DWORD_PTR ) lpSubEBP );
_tprintf ( TEXT ( " achPlainText = 0x%08x\n\n" ) ,
( DWORD_PTR ) lpPlainText );
_tprintf ( TEXT ( "Message 1 = %s\n" ) ,
plpMessage1 );
_tprintf ( TEXT ( "Message 2 = %s\n" ) ,
plpMessage2 );
Following the above demonstration of register pilfering, the real work begins; the two input messages are substituted into a template, which is subsequently encrypted and returned to the calling routine. Note that, officially, the caller has access only to the two input messages and the encrypted message; it knows nothing about the template.
The remainder of CreateEncryptedMessage
is implemted as a nested IF
block, of which the most important parts are shown next.
if ( ( rlpCipherInfo->intPlainTextLen = _stprintf_s ( achPlainText ,
MESSAGE_BUFFER_SIZE ,
TEXT ( "The two messages follow.\n\n Message 1: %s\n\n Message 2: %s\n\nEnd of Transmission\n<*>\n" ) ,
plpMessage1 ,
plpMessage2 ) )
> SPRINTF_ERROR_RESULT )
{
_tprintf ( TEXT ( "\nMessage Text:\n\n%s\n\nEncrypting the message.... " ) ,
achPlainText );
if ( unsigned char * lpKeyBuffer = ( unsigned char * ) AllocBytes_WW ( SHA256_DIGEST_SIZE ) )
{
if ( rlpCipherInfo->dwStatusCode = KeyGen ( lpKeyBuffer , SHA256_DIGEST_SIZE ) == ERROR_SUCCESS )
{ if ( rlpCipherInfo->lpCipherText = EncryptString_P6C ( achPlainText ,
rlpCipherInfo->intPlainTextLen ,
lpKeyBuffer ,
SHA256_DIGEST_SIZE ) )
{
Listing 2 is the essential part of CreateEncryptedMessage
, whichi creates and encrypts the message that is made available to the calling routine. The remainder of the routine just shows my work before a pointer to the rlpCipherInfo
is returned to the main routine.
When control returns to the main routine, the epilogue of CreateEncryptedMessage
restores its stack and base pointers, leaving its stack frame abandoned, but completely intact.
_tprintf ( TEXT ( "\nCheating... \n\n" ) );
int intPlainTextLength = STRLEN_EMPTY_P6C;
TCHAR * lpszPlainText = NULL;
LPVOID lpMainEBP = NULL ;
_tprintf ( TEXT( "\nValues within function %s:\n\n" ) ,
__FUNCTION__ );
_tprintf ( TEXT ( " MAIN_OFFSET_TO_PLAINTEXT_LENGTH = 0x%08x (%d decimal)\n" ) ,
MAIN_OFFSET_TO_PLAINTEXT_LENGTH ,
MAIN_OFFSET_TO_PLAINTEXT_LENGTH );
_tprintf ( TEXT ( " SUB_OFFSET_TO_PLAINTEXT_LENGTH = 0x%08x (%d decimal)\n\n" ) ,
SUB_OFFSET_TO_PLAINTEXT_LENGTH ,
SUB_OFFSET_TO_PLAINTEXT_LENGTH );
_tprintf ( TEXT ( " MAIN_OFFSET_TO_PLAINTEXT = 0x%08x (%d decimal)\n" ) ,
MAIN_OFFSET_TO_PLAINTEXT ,
MAIN_OFFSET_TO_PLAINTEXT );
_tprintf ( TEXT ( " SUB_OFFSET_TO_PLAINTEXT = 0x%08x (%d decimal)\n\n" ) ,
SUB_OFFSET_TO_PLAINTEXT ,
SUB_OFFSET_TO_PLAINTEXT );
_tprintf ( TEXT ( " OFFSET_TO_FUNCTION_EBP = 0x%08x (%d decimal)\n\n" ) ,
OFFSET_TO_FUNCTION_EBP ,
OFFSET_TO_FUNCTION_EBP );
__asm {
lea eax , [ ebp ]
mov lpMainEBP , eax
mov eax , dword ptr [ ebp - MAIN_OFFSET_TO_PLAINTEXT_LENGTH ]
mov dword ptr [ intPlainTextLength ] , eax
lea eax , [ ebp - MAIN_OFFSET_TO_PLAINTEXT ]
mov lpszPlainText , eax
}
_tprintf ( TEXT ( " lpMainEBP = 0x%08x\n\n" ) ,
( DWORD_PTR ) lpMainEBP );
_tprintf ( TEXT ( " intPlainTextLength = 0x%08x (%d decimal)\n" ) ,
intPlainTextLength ,
intPlainTextLength );
_tprintf ( TEXT ( " lpszPlainText = 0x%08x\n\n" ) ,
( DWORD_PTR ) lpszPlainText );
_tprintf ( TEXT ( " lpszPlainText:\n\n%s\n\n" ) ,
StringIsNullOrEmptyWW ( lpszPlainText )
? "CreateEncryptedMessage ran in secure mode and scrubbed the plaintext.\n" :
lpszPlainText );
_tprintf ( TEXT ( "\nCheating... Done!\n\n" ) );
Listing 3 is the combination of ordinary C++ and inline assembly that harvests data from the abandoned CreateEncryptedMessage
stack frame.
Since the main routine immediately sets about harvesting data from it, it can grab whatever it wants from the stack frame that belonged to CreateEncryptedMessage
.
However, all is not lost!
If CreateEncryptedMessage
takes the trouble to overwrite the message buffer, which it does when the second question is answered affirmatively, there is nothing for the main routine to show, and it must use the key to unlick the message.
Of course, none of this is news to my security-conscious colleagues, who developed the habit of ovewriting sensitive data before any block of memory is abandoned. Nevertheless, it serves as a reminder that this rule must be applied to any sensitive data, even if it lives in one of those ephemeral "automatic" variables that make their home on the stack frame of the function that created them.
I am unaware of any compiler option that you can set that causes the compiler to generate the code that would be necessary to overwrite abandoned stack frames. That being the case, you must keep in mind that abandoned stack frames remain in memory until other stack frames overwrite them. Hence, they can stay in memory for a long time, especially when they arise to meet the needs of a deeply nested method or function call.
References
- sprintf_s: Speed Bumps Ahead not only covers the work that led to this discovery, but it provides background that is essential to understanding how this proof of concept program works.
- Microsoft Visual C++ 2015 Redistributable Update 3 RC is a location from which you can get the CRT libriary that ships with the most recent versions of Microsoft Visual Studio, which you will need in order to run the demonstration program.
History
Saturday, 04 August 2018 - This article was submitted for publicathon.