Skip to main content
22 min read Beginner Desktop

Thick Client Pentesting: A Beginner's Guide to Memory Analysis with WinDbg

A step-by-step guide that starts from the basics of computer memory and ends with using WinDbg to find secrets inside a running application. No prior memory-analysis knowledge is needed.

Level: Beginner to Intermediate. Focus: Windows thick clients.

If you have only tested web apps before, thick clients feel different. There are no browser dev tools and no obvious request and response. There is just an .exe file running on your machine. But that is also the good part. Because the app runs on hardware you control, you can open it up and inspect it closely. One of the best ways to inspect it is memory analysis.

This guide takes you from the basics of what memory is, all the way to attaching WinDbg to a running app, loading extensions, and searching memory for secrets.


Part 1: Setting the Scene

What Is a Thick Client?

A thick client (also called a fat client or rich client) is a desktop app that does most of its work on the local machine instead of on a server. Examples include trading terminals, banking desktop apps, VPN clients, hospital software, and internal business tools.

  • A thin client is a web browser. The server does the work.
  • A thick client is an installed program that runs logic, stores data, and does encryption on your own machine.

Because a large part of the app runs on hardware an attacker controls, the attack surface is large. You can debug it, change its behavior, read its network traffic, and dump its memory.

Where Memory Analysis Fits

Memory analysis is one part of thick client testing. Here is the full list so you can see where it fits.

CategoryWhat you look forCommon tools
Information gatheringTech stack, architecture, local filesDetect It Easy, CFF Explorer, Process Explorer
Network analysisCleartext traffic, weak TLS, API abuseWireshark, Burp, mitmproxy
Local storageSecrets in files, registry, databasesProcMon, Regshot, DB Browser
Static and binary analysisHardcoded secrets, weak cryptoGhidra, IDA, dnSpy, JD-GUI
Dynamic instrumentationRuntime tampering, hookingFrida, x64dbg
Memory analysisPasswords, keys, tokens, PII in RAMWinDbg, Process Hacker, Frida
DLL and injectionDLL hijacking, side-loadingProcMon
Privilege and IPCInsecure pipes, weak service permissionsaccesschk, RpcView

This guide covers the memory analysis row, using the most capable tool on Windows: WinDbg.

Why Memory Analysis Matters

An app has to decrypt data before it can use it. A password you typed, a TLS session key, a token, or a decrypted record all exist as plaintext in memory at some point, even if they are encrypted on disk and on the network.

Memory analysis catches those secrets while they are exposed. Common findings include passwords not cleared after login, encryption keys, session keys, API tokens, personal data, and values you can change at runtime such as an isAdmin flag.

To find any of this, you first need to understand how memory is organized.


Part 2: Memory Basics

Think of RAM as Numbered Boxes

RAM is a long row of numbered boxes. Each box holds one byte. The number of each box is its address. Addresses are written in hexadecimal with a 0x prefix.

Address: 0x0000 0x0001 0x0002 0x0003
Contents: 0x48 0x65 0x6C 0x6C -> "Hell..."

On a 64-bit system, addresses are 64 bits wide, for example 0x00007ff63a2b1000. The operating system gives each process its own virtual address space. The app thinks it owns a large private block of memory, and the OS maps that to real physical RAM in the background.

That address space is split into regions, and each region has a job. These regions are the core of memory analysis.

The Memory Map

HIGH addresses
+----------------------------+
| STACK | grows DOWN
| v | locals, return addresses, call frames
| (free space) |
| ^ |
| HEAP | grows UP
| | dynamically allocated objects and buffers
+----------------------------+
| BSS (zeroed) | uninitialized globals
| Data segment | initialized globals and statics
| Text / Code | the machine instructions (read-only)
+----------------------------+
LOW addresses

Separate from all of this, inside the CPU itself, are the registers.


Part 3: The Regions, One by One

Registers

Registers are small, very fast storage slots inside the CPU. They are not in RAM. The CPU can only do math and logic on data that is in registers, so programs keep moving data from RAM into a register, do the work, then store it back.

Key x64 registers you will see in WinDbg:

RegisterRole
RAXReturn values and math results
RCX, RDX, R8, R9First four function arguments on Windows x64
RSPStack pointer, points to the top of the stack
RBPBase pointer, points to the base of the current frame
RIPInstruction pointer, the next instruction to run
RFLAGSStatus flags such as zero, carry, and sign

In WinDbg you view all registers with r. These matter because RIP tells you where execution is, RCX often holds the first argument (often a pointer to a buffer you care about), and RAX often holds what a function just returned.

The Stack

The stack handles function calls and local variables. It works as last in, first out. Each function call pushes a stack frame that holds local variables, the return address, and saved registers. When the function returns, its frame is popped.

int add(int a, int b) {
int result = a + b; // result, a, b are on the STACK
return result;
}
int main() {
int total = add(3, 4); // total is on main's STACK frame
}

Inside add():

+-------------------------+ <- RSP (top)
| result = 7 |
| b = 4 , a = 3 |
| return address (to main)|
| saved RBP | <- RBP (frame base)
+-------------------------+
| total (main's frame) |
+-------------------------+

Why this matters: typed passwords and decrypted values often sit in stack buffers, the return address is the target of buffer overflow attacks, and reading the stack shows you how execution reached the current point.

The Heap

The heap is for dynamic memory. This is memory whose size or lifetime is not known when the program is compiled. It is used by malloc, new, and every object in .NET or Java.

char* buffer = malloc(64); // pointer is on the STACK, 64 bytes are on the HEAP
strcpy(buffer, "SecretPassword123");
free(buffer); // freed, but the bytes may still remain

The important detail: free() marks memory as reusable but does not erase it. That leftover "SecretPassword123" in freed heap memory is exactly what we search for. Long-lived secrets such as session state, cached credentials, and decrypted documents also live on the heap.

Code, Data, and BSS

  • Text or Code: the actual instructions. Read-only and executable. This is what a disassembler shows.
  • Data: initialized global and static variables, for example static int x = 5;.
  • BSS: uninitialized globals, filled with zeros at startup.

Quick Comparison

FeatureStackHeapRegisters
LocationRAMRAMInside CPU
SpeedFastSlowerFastest
Managed byCompilerProgrammer or GCCPU
LifetimeUntil function returnsUntil freedInstant
HoldsLocals, return addressObjects, buffersWorking values
GrowsDownUpNot applicable

One Program, Every Region

int g_init = 42; // DATA
int g_uninit; // BSS

void login() {
char localPassword[32]; // STACK
char* token = malloc(64); // 'token' pointer on STACK, 64 bytes on HEAP
strcpy(localPassword, "Winter2024!"); // secret on the STACK
strcpy(token, "eyJhbGciOi..."); // secret on the HEAP
free(token); // freed, not wiped, still recoverable
}

Both "Winter2024!" on the stack and the token on the heap are the kinds of secrets we will look for with WinDbg.


Part 4: Meet WinDbg

WinDbg is Microsoft's debugger. The same engine (dbgeng) powers both the classic build and the modern WinDbg app. Every command below works in both.

Installing It

  • Microsoft Store: search for WinDbg (easiest, modern UI), or
  • winget install Microsoft.WinDbg, or
  • Windows SDK: select Debugging Tools for Windows.

Attach vs. Dump: The Two Ways In

Attach (live)Dump (static)
What it isConnect to a running appOpen a saved memory snapshot (.dmp)
BreakpointsYes, you can step throughNo, it is frozen and read-only
Modify memoryYesNo
Use whenYou want to trigger behavior and catch secrets liveYou have a crash dump or want a repeatable offline snapshot

Both use the same inspection commands. Only the setup is different.


Part 5: Symbols and Extensions (Read This First)

Many beginners skip this part and then wonder why every address is just a number. Symbols and extensions are what make WinDbg readable.

Step 1: Set the Symbol Path

Symbols (.pdb files) map raw addresses to function names. Point WinDbg at Microsoft's public symbol server:

.sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols

Or set it once for the whole system using an environment variable:

_NT_SYMBOL_PATH = srv*C:\symbols*https://msdl.microsoft.com/download/symbols

Step 2: Reload Symbols, and When to Force It

.reload reload symbols for the loaded modules
.reload /f force a full reload, download everything now
.reload /f myapp.exe force reload for one module only

Use plain .reload first. If function names still look wrong or missing, which is common, run .reload /f to force a fresh download. The /f flag means fetch it now instead of loading it later.

Quick checks on your symbol state:

.sympath show the current symbol path
lm list loaded modules (a "(deferred)" tag means symbols are not loaded yet)
!sym noisy turn on verbose symbol logging to see why a symbol will not load

Step 3: Understand Extensions (the ! Commands)

Any command that starts with ! is an extension command. It comes from a loadable extension DLL. !address, !heap, !analyze, and !dumpheap all come from extensions. Some load automatically and some you load yourself.

List what is already loaded:

.chain show all loaded extension DLLs in search order

Load an extension manually:

.load <name> load an extension DLL by name or path
.loadby <name> <module> load an extension that sits next to a given module
.unload <name> unload one

The two you will actually use:

  • Built-in extensions such as !address, !heap, !teb, and !peb ship with WinDbg and are usually loaded already. Just call them.
  • SOS, for .NET managed apps, you load yourself:
.loadby sos clr load SOS from beside the CLR (.NET Framework)
.loadby sos coreclr for .NET Core and .NET 5 or newer
.load sos fallback if the runtime path is already known

Step 4: Your First Extension Command, !address

!address shows a map of the whole process memory: every region, its size, and its protection. It is the fastest way to answer whether a pointer is on the stack, the heap, or in code.

!address dump the full memory map
!address <addr> details for the region that contains <addr>
!address -summary totals by usage (how much heap vs stack vs image)
!address -f:Heap show only heap regions

Example:

0:000> !address 000001f23a4b02c0
Usage: Heap
Base Address: 000001f23a4b0000
Protect: PAGE_READWRITE

Those two lines, Usage: Heap and PAGE_READWRITE, are often the whole finding: a plaintext secret sitting in writable heap memory.

Two more map helpers:

!teb Thread Environment Block (stack base and limit for the current thread)
!peb Process Environment Block (image name, command line, environment variables)

Part 6: Getting In, Attach or Dump

Attaching to a Running Process

GUI: File > Attach to Process, pick the .exe, then click Attach. The app freezes and you get a prompt.

Command line:

windbg -p <PID> attach by process ID
windbg -pn myapp.exe attach by name
windbg -pv -p <PID> read-only attach, cannot crash the app

Find the PID with:

Get-Process myapp | Select-Object Id, ProcessName

Once attached, the app is paused. Resume it with g (go). Break back in with Ctrl+Break.

Loading a Dump File

First create a dump, which is a memory snapshot:

  • Task Manager: Details tab, right-click the process, choose Create dump file. No extra tool needed.
  • ProcDump: procdump -ma myapp.exe C:\dumps\myapp.dmp. The -ma flag means full memory.

Open it:

windbg -z C:\dumps\myapp.dmp

Then get oriented:

.reload /f
!analyze -v auto-analysis, explains why it crashed if it did

A dump is frozen. There is no g and no breakpoints. But every read command works the same as in a live session.


Part 7: The Commands You Will Actually Use

Displaying Memory (the "d" family)

db <addr> bytes and ASCII (best for finding strings)
dq <addr> qwords (8-byte pointers on x64)
da <addr> ASCII string
du <addr> Unicode (UTF-16) string, which is most Windows text
dps <addr> pointers with symbol names (good for stacks)

Length suffix: db <addr> L80 shows 0x80 (128) bytes.

Searching Memory for Secrets

The main command is s (search):

s -a 0 L?80000000 "password" ASCII search across memory
s -u 0 L?80000000 "Winter2024" Unicode search, usually the one that hits
s -b 0 L?80000000 41 42 43 44 raw byte pattern (here "ABCD")

Once you have a hit address, look closer:

du <hitaddr> read it as Unicode
db <hitaddr>-20 L60 look at the bytes around it
!address <hitaddr> confirm stack vs heap and the protection

Inspecting the Stack

k call stack (how execution got here)
kb call stack with the first arguments
dps rsp L40 stack contents with symbols resolved
db rsp L200 raw stack bytes (look for ASCII secrets)

Inspecting the Heap

!heap -s summary of all heaps
!heap -stat -h 0 block-size statistics
!heap -x <addr> find the heap block that contains <addr>

For .NET apps, after running .loadby sos clr:

!dumpheap -stat all managed types and counts
!dumpheap -type System.String every string on the managed heap
!do <addr> dump one object, a String prints its text

Running !dumpheap -type System.String and then !do on each address is a common way to read every string a .NET app holds, including passwords.

Breakpoints (Live Only)

bp myapp!login break when login() is called
g resume until it is hit
r rcx RCX holds the first argument
du rcx read that argument as a string
ba r4 <addr> break when 4 bytes at <addr> are read
ba w4 <addr> break when 4 bytes at <addr> are written

Useful API breakpoints to catch data in transit:

bp kernelbase!lstrcmpW catch string comparisons, such as password checks
bp bcrypt!BCryptDecrypt catch data right after it is decrypted

Part 8: Full Walkthrough, Find a Password in a Live App

Goal: show that an app leaves a password in memory after login.

  1. Attach.
windbg -pn vulnerableapp.exe
  1. Set symbols and extensions, then run.
.sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols
.reload /f
.chain
g
  1. Interact. In the app, log in with Winter2024!. This forces the secret into memory.

  2. Break in. Press Ctrl+Break.

  3. Search.

0:000> s -u 0 L?7fffffff "Winter2024!"
000001f23a4b02c0 0057 0069 006e 0074 0065 0072 ... W.i.n.t.e.r...
000001f23a5c1180 0057 0069 006e 0074 0065 0072 ... W.i.n.t.e.r...

Two hits.

  1. Confirm the location.
0:000> du 000001f23a4b02c0
000001f23a4b02c0 "Winter2024!"

0:000> !address 000001f23a4b02c0
Usage: Heap
Protect: PAGE_READWRITE

A plaintext password in writable heap memory.

  1. Show that it stays. Log out, break in, and search again. If it is still there, the app failed to clear the secret. That is a reportable finding.

  2. Save evidence. Create a dump (Task Manager > Create dump file) and run the same s -u search on the .dmp. Now you have a repeatable file you can attach to a report.

That is the full loop: set symbols, load extensions, attach or dump, search, confirm with !address, and report.


Part 9: Memory Fundamentals Every Tester Should Know

The parts above tell you where data lives and how to read it. This part fills in the concepts behind those commands.

Bits, Bytes, and How Numbers Are Stored

Everything in memory is numbers. It helps to read three number systems.

SystemBaseExampleWhere you see it
Binary20100 1000bit flags and masks
Decimal1072everyday counting
Hex160x48addresses and memory dumps

One hex digit is 4 bits (a nibble). Two hex digits are one byte. So 0x48 is one byte, which is the letter H in ASCII. Common sizes:

byte = 1 byte = 8 bits
word = 2 bytes (db shows bytes, dw shows words)
dword = 4 bytes (a 32-bit int)
qword = 8 bytes (a 64-bit pointer on x64)

Endianness, Why Bytes Look Reversed

x86 and x64 use little-endian order. Multi-byte numbers are stored with the least significant byte first. This confuses most beginners. The 4-byte value 0x11223344 is stored like this:

Address: +0 +1 +2 +3
Bytes: 44 33 22 11 (reversed)

So when db shows 44 33 22 11, the real value is 0x11223344. When you use dd or dq, WinDbg puts the bytes back in the right order for you. Only the raw byte view (db) shows the reversed order. Knowing this saves confusion when reading pointers and integers by hand.

Pointers, the Most Important Concept

A pointer is a variable whose value is an address. It points to another location in memory. This is the key idea in memory analysis. A local variable on the stack often just holds an address into the heap, where the real data lives.

char* token = malloc(64); // token holds an address; the 64 bytes are elsewhere

In WinDbg you often follow a pointer, which means reading the value at the address it holds:

dq <addr> read 8 bytes at addr, likely another address
poi(<addr>) the value stored at addr (one dereference)
dps <addr> show a run of pointers and resolve any that are code or symbols

Example of following a pointer from an object to a field to a string:

0:000> dq 000001f23a4b0000 L1
000001f23a4b0000 000001f23a4b0250 (this is a pointer)

0:000> du poi(000001f23a4b0000)
000001f23a4b0250 "Winter2024!" (followed it to the actual string)

Learning to follow these chains, from an object to a field to a buffer, is the core skill of manual memory analysis.

How a Function Call Passes Arguments

When you set bp myapp!login and then read RCX, the reason is the calling convention. This is the set of rules for how arguments are passed. On Windows x64, the rules are:

Argument numberRegister
1stRCX
2ndRDX
3rdR8
4thR9
5th and beyondpushed onto the stack

The return value comes back in RAX. So the moment you break at the start of a function, the arguments are already sitting in those registers. Read them before the function changes them. That is why the walkthrough reads RCX right after the breakpoint hits. It is argument one, often a pointer to the buffer you want.

On 32-bit (x86) it is different. Arguments are usually pushed on the stack, so you read them with dd esp+4, dd esp+8, and so on. Check your architecture first with lm, or note whether addresses are 8 bytes or 4 bytes.

Data Structures in Memory (dt)

Real programs store structs, which are groups of fields laid out one after another in memory. The dt (display type) command overlays a struct definition onto raw memory so you see named fields instead of raw hex.

dt myapp!_USER_SESSION <addr> overlay the struct at addr
dt ntdll!_PEB @$peb the Process Environment Block, labeled
dt -r ... recursive, also expands nested structs

The output turns raw bytes into readable fields:

+0x000 Username : 0x000001f23a4b0250 "alice"
+0x008 IsAdmin : 0x1
+0x010 SessionKey : 0x000001f23a4b0300

Now you can see IsAdmin : 0x1 directly, and on a live target you could change it. The +0x008 is the field's offset from the start of the struct. Offsets are how you move through a structure by hand.

Reading secrets is only one family. Here is the wider list so you can recognize each one.

Bug classRoot causeWhat happens
Sensitive data exposureSecrets not clearedPasswords, keys, or PII readable in RAM or dumps
Stack buffer overflowWriting past a stack bufferOverwrites the saved return address and hijacks execution
Heap overflowWriting past a heap blockCorrupts nearby heap data or objects
Use-after-freeUsing memory after free()A stale pointer touches reused memory, causing crashes or code execution
Double freeFreeing the same block twiceCorrupts the heap allocator's bookkeeping
Uninitialized memoryReading before writingLeaks old data left in that region
Integer overflowBad size mathUnder-allocates a buffer, which enables an overflow
Format stringUser-controlled format specifier%x and %n leak or write memory

They share one theme: memory being read or written outside its intended bounds or lifetime.

How a Stack Buffer Overflow Works

This is the classic memory bug, so here is the mechanic. A stack frame stores locals and the return address.

Before overflow: After overflowing 'buf' with many 'A's:
+------------------+ +------------------+
| buf[16] | | AAAAAAAAAAAAAAAA | buf overflowed
| saved RBP | | AAAAAAAAAAAAAAAA | spilled past buf
| return address --+-> caller | 4141414141414141 | return address overwritten
+------------------+ +------------------+

If the code runs strcpy(buf, input) without checking the length, input longer than 16 bytes spills past buf and overwrites the return address. When the function returns, the CPU jumps to whatever address is now there, which is 0x4141414141414141 ("AAAA..."). Control the overflow precisely and you decide where RIP goes.

In WinDbg you can see it happen. The app crashes, r shows RIP full of your input bytes, and k shows a corrupt stack. A register full of your input is the sign of a controllable overflow.

Memory Protections You Will Run Into

Modern Windows has defenses. Know them because they change what is possible.

  • DEP or NX (Data Execution Prevention): stack and heap pages are marked non-executable, so you cannot simply run shellcode you placed there. Check a region's Protect: field with !address. No EXECUTE means DEP applies.
  • ASLR (Address Space Layout Randomization): module base addresses are randomized on each run, so fixed addresses do not work. This is why we use a symbol like bp myapp!login instead of a fixed address. Symbols resolve wherever the module loads.
  • Stack canaries (/GS): a secret cookie value placed before the return address. An overflow corrupts it, and it is checked before return. A mismatch stops the program.
  • CFG (Control Flow Guard): checks indirect call targets, which limits hijacked-pointer attacks.

For this guide's goal, reading secrets from memory, none of these stop you. DEP, ASLR, and CFG protect against code execution, not against reading data that sits in readable memory. That is one reason a secret left in memory is such a reliable finding. The defenses do not get in the way.


Part 10: Reporting, Fixes, and Safe Practice

Writing It Up

When you find secrets in memory, report:

  • What: password, key, token, or personal data.
  • Where: stack or heap, and the region's protection from !address.
  • Repro: the exact steps, either attach or dump, plus the s -u command.
  • Impact: any local user, malware, or memory scraper can read it. This is worse on shared RDP or Citrix hosts where many users share one machine.

Fix Guidance for Developers

  • Clear secret buffers right after use with SecureZeroMemory, which is not optimized away like a plain memset.
  • Use protected memory, such as DPAPI, CryptProtectMemory, or a platform keystore.
  • Keep plaintext for the shortest time possible. Decrypt as late as you can and clear it as early as you can.
  • Avoid immutable String for secrets in .NET. It cannot be reliably cleared and the garbage collector copies it around. Use SecureString or spans carefully.
  • Use VirtualLock to keep secrets out of the page file so they do not reach disk.

Practice

  • Try DVTA (Damn Vulnerable Thick Client Application), which is built for these techniques.
  • Dump your own apps with Task Manager and search for strings you typed.
  • Build a small WinForms app, attach, run .loadby sos clr, then !dumpheap -type System.String, and read your own input back.
  • For overflow practice, use a beginner CTF binary or a simple vulnerable strcpy program and watch RIP fill with 0x41414141.

Summary

  • Thick clients run on hardware you control, so the attack surface is large. Memory analysis catches secrets while they are decrypted.
  • Memory is registers (in the CPU), the stack (locals and calls, grows down), the heap (objects, grows up), and the static code, data, and BSS regions. It is all just bytes. Read them in hex, remember little-endian, and follow pointers.
  • The calling convention tells you where arguments are. On x64, arguments one to four are RCX, RDX, R8, and R9, and the return value is in RAX.
  • Memory bugs come down to reading or writing outside the intended bounds or lifetime.
  • Defenses like DEP, ASLR, canaries, and CFG block code execution, not reading secrets. That is why a secret left in memory is such a reliable finding.
  • In WinDbg: set symbols, run .reload /f if names look wrong, load extensions with .load or .loadby, and check them with .chain. Then !address maps memory, s -u searches, dt and poi decode structures and pointers, and !heap and SOS inspect the heap.
  • Attach to control a live app. Load a dump for a frozen, repeatable snapshot.

Practical Appendix: Finding Common Vulnerabilities During Analysis

This is the hands-on cheat sheet. The guide above covered how memory works. This appendix covers what to run to find bugs. It is split into two collapsible sections. Each finding has a Command, a Description (what it means and why it is a bug), and a Vulnerable Output (what a real hit looks like).

Note: the expand and collapse blocks below use <details> HTML. They work as clickable toggles on GitHub, GitLab, VS Code, and most static site tools. Medium does not support them, so on Medium treat each summary line as a subheading.

Section A: Sensitive Data Exposure (secrets sitting in memory)

These are the most common and highest value thick client findings. The pattern is always the same: search memory, get a hit, confirm the region is readable, and show that the data should not still be there.

A1. Plaintext password left in memory

Command

s -u 0 L?7fffffff "Winter2024!" Unicode search for the known password
s -a 0 L?7fffffff "Winter2024!" ASCII fallback
!address <hit> confirm region and protection

Description

After login, the password should be cleared from memory almost right away. If a search still finds it, especially minutes later or after logout, the app is holding plaintext credentials in RAM. Any local user, malware, or memory scraper can read it.

Vulnerable Output

0:000> s -u 0 L?7fffffff "Winter2024!"
000001f23a4b02c0 0057 0069 006e 0074 0065 0072 ... W.i.n.t.e.r...
000001f23a5c1180 0057 0069 006e 0074 0065 0072 ... W.i.n.t.e.r...

0:000> !address 000001f23a4b02c0
Usage: Heap
Protect: PAGE_READWRITE plaintext password in writable heap = finding
A2. Hardcoded or in-memory encryption keys

Command

s -a 0 L?7fffffff "AES" strings near a key
s -a 0 L?7fffffff "-----BEGIN" PEM private keys
s -b 0 L?7fffffff 30 82 ASN.1 or DER key header bytes
bp bcrypt!BCryptGenerateSymmetricKey break when a key is created
bp bcrypt!BCryptDecrypt break right before decryption

Description

Encryption is only as strong as the secrecy of the key. If the key is a fixed value baked into the binary, it can be recovered from every copy of the app. If it floats around in readable memory, the encryption is defeated. Breaking on the crypto API lets you read the key or IV from the argument registers.

Vulnerable Output

0:000> bp bcrypt!BCryptDecrypt
0:000> g
Breakpoint 0 hit
0:000> db rdx L20 RDX often points at key or data material
000001f23a4b0700 73 65 63 72 65 74 41 45-53 4b 65 79 31 32 33 34 secretAESKey1234
16-byte AES key in the clear
A3. Session tokens, JWTs, and API keys

Command

s -a 0 L?7fffffff "eyJ" JWTs start with eyJ (base64 of '{"')
s -a 0 L?7fffffff "Bearer " Authorization headers
s -a 0 L?7fffffff "Authorization"
da <hit> read the full token

Description

Session tokens are bearer credentials. Whoever holds one is treated as the user. Finding a valid JWT or API key in memory, or in a dump you can copy out, is a direct path to account takeover. JWTs are easy to spot because they start with eyJ.

Vulnerable Output

0:000> s -a 0 L?7fffffff "eyJ"
000001f23a4c1800 65 79 4a 68 62 47 63 69-4f 69 4a 49 55 7a 49 31 eyJhbGciOiJIUzI1

0:000> da 000001f23a4c1800
000001f23a4c1800 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9..."
full JWT, decodes to {"alg":"HS256"} . {"sub":"admin"}
A4. Database connection strings and backend credentials

Command

s -a 0 L?7fffffff "Password=" common connection string field
s -a 0 L?7fffffff "Data Source="
s -a 0 L?7fffffff "Server="
s -a 0 L?7fffffff "Uid="
s -a 0 L?7fffffff "Trusted_Connection"

Description

Two-tier thick clients often connect straight to a database, with the connection string (server, username, password) stored in the client. Recovering it from memory gives direct database access, usually with more privilege than the app's UI allows.

Vulnerable Output

0:000> s -a 0 L?7fffffff "Password="
000001f23a4d0100 50 61 73 73 77 6f 72 64-3d ... Password=...

0:000> da 000001f23a4d00e0
000001f23a4d00e0 "Server=10.0.0.5;Database=Prod;Uid=sa;Password=Sup3rS3cret!"
full sa-level database connection string
A5. Secret not cleared after logout

Command

step 1: log in, break in, confirm the secret is present
s -u 0 L?7fffffff "Winter2024!"

step 2: log out in the app, break in again, run the same search
s -u 0 L?7fffffff "Winter2024!"

Description

This is a stronger version of A1. If the secret is gone after logout, the app clears memory correctly. If the same address still returns a hit after you log out or lock the app, you have shown that the app does not clear its secret buffers. That is a clear, repeatable finding.

Vulnerable Output

after logout:
0:000> s -u 0 L?7fffffff "Winter2024!"
000001f23a4b02c0 0057 0069 006e 0074 ... W.i.n.t.e.r...
still present after logout, secret was never cleared
A6. Personal data in memory (cards, SSNs, emails)

Command

s-sa 0 L?7fffffff dump all readable ASCII strings, then review them
s -a 0 L?7fffffff "@" emails
s -a 0 L?7fffffff "4111" test Visa card prefix

Description

Regulated data such as card numbers, SSNs, and health records should not stay unencrypted in RAM. This matters for PCI-DSS and HIPAA. Dumping strings and looking for known patterns surfaces personal data that the app decrypted for display and never cleared.

Vulnerable Output

0:000> s -a 0 L?7fffffff "4111"
000001f23a4e2200 34 31 31 31 31 31 31 31-31 31 31 31 31 31 31 31 4111111111111111
full 16-digit card number in cleartext
A7. Cleartext data before encryption or after decryption

Command

bp ws2_32!send catch outbound data before TLS, if TLS sits above this
bp ws2_32!recv catch inbound data after decryption
bp schannel!EncryptMessage
db rbx L100 dump the buffer argument

Description

Even when traffic is protected by TLS on the network, the plaintext exists in memory right before encryption and right after decryption. Setting breakpoints on the send and receive path lets you read request and response bodies. This is useful when the app pins its certificate and you cannot intercept the network.

Vulnerable Output

0:000> bp ws2_32!send
0:000> g
Breakpoint hit
0:000> da poi(rsp+10) buffer pointer, adjust for architecture
... "POST /login user=admin&pass=Winter2024!&remember=1"
credentials visible before encryption

Section B: Memory Corruption and Logic Findings

These are about memory being written or used outside its bounds or lifetime, plus runtime logic you can change. You usually find these with a live attach, so you can set breakpoints and watch registers, or by causing a crash and reading the dump.

B1. Stack buffer overflow (return address overwrite)

Command

feed an over-long input to the app, then when it crashes:
r look at RIP and RSP
k call stack, which will be corrupt
db rsp L40 see your input bytes on the stack
!exchain exception handler chain, checks for SEH overwrite

Description

If long input reaches a fixed-size stack buffer through an unchecked copy such as strcpy, sprintf, or memcpy, it overruns the buffer and overwrites the saved return address. On return, execution jumps to attacker-controlled bytes. The sign is RIP (or EIP on x86) filled with your input pattern.

Vulnerable Output

(1c4.9a0): Access violation - code c0000005
0:000> r
rip=0000000041414141 rsp=000000e59f3ff820
RIP is 'AAAA', you control the instruction pointer

0:000> db rsp L20
000000e59f3ff820 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
B2. Heap corruption or heap overflow

Command

!heap -s heap summary, look for corruption notes
!heap -x <addr> locate the block that contains an address
!heap -p -a <addr> block header and alloc stack (needs Page Heap or gflags)
!analyze -v on the crash dump, classifies heap corruption

Description

Writing past a heap allocation corrupts the allocator's bookkeeping (block headers) or nearby objects. It shows up as crashes inside ntdll heap routines such as RtlpAllocateHeap or RtlFreeHeap. Turning on Page Heap with gflags /p /enable app.exe /full makes overruns crash right at the offending write.

Vulnerable Output

0:000> !analyze -v
...
HEAP_CORRUPTION_DETECTED
Corrupt block at 000001f23a5c1000
Stack: myapp!copy_record+0x54 -> ntdll!RtlpAllocateHeap
a write in copy_record damaged heap metadata
B3. Use-after-free (dangling pointer)

Command

with Page Heap enabled so freed memory is unmapped:
g run until the use-after-free access faults
r RIP and faulting instruction
dc <faulting-addr> shows the freed-fill pattern
!heap -p -a <addr> shows the block was already freed, plus the free stack

Description

The app frees an object but keeps using a pointer to it. Later the memory is reused or unmapped, so the stale pointer reads or writes the wrong data. This causes crashes, information leaks, or code execution. With Page Heap, freed pages are guarded, so the access faults exactly where the stale pointer is used.

Vulnerable Output

(a1c.b30): Access violation reading 0x000001f23a5c1010
0:000> !heap -p -a 000001f23a5c1010
Address State
000001f23a5c1000 freed used after it was freed
free stack: myapp!session_close+0x22
B4. Logic flags you can change in memory (for example isAdmin)

Command

x myapp!*isAdmin* find the symbol or global
dt myapp!_USER_SESSION <addr> view the struct fields and offsets
db <flagaddr> L1 read the flag
eb <flagaddr> 01 write it to 1 (live only), flips privilege
ba w1 <flagaddr> break when the app changes it

Description

Thick clients often check authorization on the client side, using a boolean in memory. If you can find and change that flag at runtime, you gain privilege without touching the server. This shows that the authorization decision was made in a place the user controls.

Vulnerable Output

0:000> dt myapp!_USER_SESSION 000001f23a4b0000
+0x000 Username : "alice"
+0x008 IsAdmin : 0y0 currently false

0:000> eb 000001f23a4b0008 01
0:000> dt myapp!_USER_SESSION 000001f23a4b0000
+0x008 IsAdmin : 0y1 now admin, client-side privilege bypass
B5. Format string vulnerability

Command

bp myapp!printf or wsprintf or _vsnprintf
da poi(rsp+8) inspect the format argument
then supply input with %x %x %n and watch the behavior

Description

When user input is passed as the format argument, for example printf(userInput) instead of printf("%s", userInput), an attacker can use %x to leak stack memory and %n to write to memory. If the format argument at the breakpoint contains user input where a fixed string should be, it is vulnerable.

Vulnerable Output

0:000> da poi(rsp+8)
000000e59f3ff900 "%x.%x.%x.%x" the format string is user input
program then prints: "3ff9.0.7ffd.41414141" leaked stack memory
B6. Uninitialized or leftover memory disclosure

Command

!address -f:Heap enumerate heap regions
db <freshly-allocated-addr> L40 read a buffer before it is written
s-sa <regionbase> L<size> strings in a region that should be blank

Description

A buffer that is allocated but sent or displayed before it is fully written leaks whatever was there before. This is often another user's data or a previous secret. If a new buffer already contains recognizable old strings, that is a disclosure bug.

Vulnerable Output

0:000> db 000001f23a4f0000 L20
000001f23a4f0000 57 69 6e 74 65 72 32 30-32 34 21 00 cc cc cc cc Winter2024!.....
a new buffer still holds a previous password