49 min read
TOC
Condensed C
Notes and materials adapted from Code Academy & learn-c, reorganized here in text form since it’s easier for me to recall this way.
Introduction
C is a low level language that allows you to directly interact with the CPU and memory, that’s why it’s mainly used for developing kernels, malware, OS, and etc.
Fundamentals
Below is a basic hello world:
| |
In order to run this, we have to compile it using a GCC compiler (it translates C code into an executable program that your computer can run, turning human readable code into machine code that the processor can understand).
| |
Variables and its data types
A variable is a container that can be used to store data, which can be used or modified later in the program.
| |
float vs double: double is the default floating point type in C and is more precise. Use float only when you specifically need to save memory, for example in large arrays or graphics code. When in doubt, use double.
We can do many things with variables like storing input, calculations, decisions, etc. Below is an example:
| |
Implicit Type Conversion
Implicit conversion is when the compiler automatically converts a value to match the type of the variable it’s being assigned to.
| |
Explicit Conversion
Explicit conversion, or type casting, is when you manually specify the type a value should be converted to. This is the standard and reliable way.
| |
Also, a char doesn’t store a letter directly, it stores a numeric value representing that letter.
| |
Operators
Basic arithmetic operations:
| |
Increment and Decrement
| |
Shorthand Assignment Operators
Instead of manually typing full expressions, you can use shorthand expressions in C:
| |
Bitwise Operators
Bitwise operators work directly on the individual bits of a value, not the value as a whole. They come up constantly when working with flags, masks, and low-level data.
Before the table: each of these operators works on the individual bits of a number, not the number as a whole. So instead of asking “is 5 greater than 3,” you are asking “which specific binary digits are set.”
| Operator | Name | Description |
|---|---|---|
& | AND | Bit is 1 only if both corresponding bits are 1 |
| | | OR | Bit is 1 if either corresponding bit is 1 |
^ | XOR | Bit is 1 only if exactly one of the two bits is 1 |
~ | NOT | Flips all bits (0 becomes 1, 1 becomes 0) |
<< | Left shift | Shifts bits left, zero-fills from the right |
>> | Right shift | Shifts bits right, zero-fills from the left |
Binary operation diagrams
| |
AND: checking and clearing bits
| |
OR: setting bits
| |
XOR: toggling bits
| |
One caveat: the XOR swap breaks silently if p and q point to the same memory location. Both become zero. Use a temp variable when you are not certain they are distinct.
NOT: flipping all bits
| |
Left and Right Shift
Shifting left by N is equivalent to multiplying by 2^N. Shifting right by N is equivalent to dividing by 2^N.
| |
Shifts are commonly used to construct bitmasks or pack and unpack data:
| |
Operator Precedence
C doesn’t evaluate strictly left to right, it follows a precedence order: some operators run before others, and operators of equal priority run left to right.
| Priority | Operators | Description |
|---|---|---|
| 1 | ++, --, () | Increment, decrement, grouping |
| 2 | !, (typecast) | Logical NOT, type conversion |
| 3 | *, /, % | Multiplication, division, modulo |
| 4 | +, - | Addition, subtraction |
| 5 | <, <=, >, >= | Comparison |
| 6 | ==, != | Equality |
| 7 | && | Logical AND |
| 8 | || | Logical OR |
| 9 | =, +=, -=, *=, etc. | Assignment |
| |
You may use () to override the order:
| |
Control Flow
Programs often need to make decisions based on different conditions. This is where conditional logic comes in.
An if statement lets you check a condition, if it’s true, the code block runs, if it’s false, the block is skipped.
| |
You can also use relational and logical operators inside an if statement, for example if (grade >= 60).
Relational and Logical Operators in if Statements
We’ve already used relational operators like ==, !=, <, >, <=, and >= in if statements. These operators compare values and return true or false, making them perfect for conditionals.
| |
If grade is 60 or higher, the message is printed. Otherwise, nothing happens.
Logical Operators in Conditionals
Sometimes we need to check multiple conditions at once. This is where logical operators come in:
- && (AND) - true only if both conditions are true
- || (OR) - true if at least one condition is true
- ! (NOT) - reverses the condition
Checking two conditions with && (AND):
| |
Here, both a and b must be positive for the message to print.
Checking either condition with || (OR):
| |
This prints if either a or b is positive.
Using ! (NOT):
| |
This prints if a is NOT positive (i.e. a <= 0).
Sample code putting it all together:
| |
Here’s a full example using if / else with a coin flip:
| |
If there are more than two possible outcomes, you can use else if:
| |
switch Statement
When checking one variable against multiple values, a switch statement provides a cleaner and more readable alternative to a long if / else if chain.
| |
Ternary Operator
A shortcut for writing simple if / else statements in one line.
| |
Finding the minimum of two numbers
With if / else:
| |
With the ternary operator:
| |
Deciding what to print
| |
Loops & Errors
A loop is a way to repeat a block of code until a certain condition is met.
while loop
A loop that keeps running over and over until the condition becomes false.
| |
for loop
This is used when we know exactly how many times we want to iterate.
| |
break
This statement allows us to exit a loop immediately, regardless of the loop’s condition.
| |
Loop 1 uses while (1), which has no natural stopping condition on its own, so break is the only way out.
Loop 2 has no break at all, it relies entirely on the condition while (number > 0). Each time through, scanf() updates number, and once the user enters 0 or a negative value, that condition becomes false on its own, ending the loop naturally without any manual exit.
continue
This statement skips only the remaining code inside the loop body for the current iteration, unlike break, which exits the loop entirely. Instead, it jumps back to the start of the loop and moves on to the next iteration.
| |
Arrays
Let’s talk about arrays, a collection of variables of the same type, stored in contiguous memory. It helps manage multiple related variables efficiently.
| |
Looping Through Arrays
Arrays let you store multiple values in a single variable. Instead of handling each value individually, you can use a loop to go through all of them at once.
Using a while loop
| |
Using a for loop
| |
Same result, but the initialization, condition, and increment are all on one line, this is why for is the more common choice for looping through arrays.
The problem: hardcoded size
Both examples above hardcode 5 as the array length. That’s bad practice, if the array’s size ever changes, the loop breaks unless you remember to update the number too.
Determining Array Size with sizeof()
sizeof() returns the total memory (in bytes) occupied by a variable or data type.
| |
The array holds 5 integers, and each int takes 4 bytes, so the total size is 5 x 4 = 20 bytes. To get the actual number of elements, divide the total size by the size of one element:
| |
Now we can rewrite the loop so it adapts to any array length automatically:
| |
You can even skip the len variable and calculate the size directly inside the loop condition:
| |
This only works because the array’s declaration is still visible nearby. Once arrays get passed into other functions, sizeof() stops working this way, that’s covered later once we get to pointers.
Multidimensional Arrays
A multidimensional array is an array of arrays, most commonly a 2D grid of rows and columns, used for things like matrices, grids, and lookup tables.
Declaring
| |
Visually:
| |
Initializing
| |
| |
The row count can be left out when the array is initialized, since it can be inferred from how many { } groups you wrote, but the column count must always be specified. Arrays with more than 2 dimensions exist, but are rarely used in practice.
Accessing elements
Elements are accessed with array[row][column], and just like regular arrays, indexing starts at 0.
| |
Looping through it
A single loop isn’t enough since there are two dimensions to walk through, one loop for the rows, and a nested loop for the columns within each row:
| |
Avoiding hardcoded dimensions
Same idea as before, use sizeof() instead of hardcoding the row/column counts:
| |
Where this actually shows up: anywhere there is a natural row/column or lookup relationship. The pattern transition[state][input] replacing a long if/else chain shows up in parsers, lexers, and cipher substitution tables. A grid of pixel values or a game board are the obvious visual examples.
Strings
A string is simply a sequence of characters. In C, strings are represented as arrays of char, so they follow the same rules as arrays.
Creating a String
There are two ways to create one:
| |
Both produce the same result. The null terminator (\0) marks the end of the string, so “Hello” takes up 6 bytes in memory, not 5. Note also that C is case-sensitive, ‘A’ and ‘a’ are different characters.
Printing strings
| |
Accessing and Modifying Characters
Since a string is just an array, you access and modify characters the same way, by index, starting at 0:
| |
Important: you can only overwrite existing characters, you can’t add or remove characters from a string this way, the array’s size is fixed once declared.
Looping Through a String
| |
Use strlen() instead of hardcoding the length, it calculates the string’s length dynamically, so the loop works for any string. Note that strlen() doesn’t count the null terminator.
strcat(), Joining Strings
strcat(destination, source) appends source onto the end of destination, modifying destination directly.
| |
destination must have enough space for both strings combined, plus the null terminator. If it doesn’t, you get a buffer overflow, where the extra data spills into memory it shouldn’t, corrupting adjacent data and causing crashes or security vulnerabilities:
| |
To avoid this, either size the destination buffer generously, or use strncat(), which limits how many characters get copied:
| |
strcpy(), Copying Strings
strcpy(destination, source) copies source into destination, replacing whatever was there (unlike strcat(), which appends).
| |
Same overflow risk applies here: if destination is too small, strcpy() overwrites memory it shouldn’t. The safer alternative is strncpy(), which caps how many characters get copied, but you have to manually null-terminate the result yourself:
| |
Pointers
A quick mental model of memory
When a program runs, the OS gives it access to RAM to store data while it’s running. Think of RAM like a giant row of numbered mailboxes, each box holds one byte, and each byte has its own unique address (written in hexadecimal, like 0x1000).
Memory is roughly divided into a few regions:
- Code segment - the compiled instructions themselves (read-only)
- Data segment - global/static variables
- Heap - memory you manually allocate and free (malloc(), free()), grows as needed
- Stack - local variables and function call info, automatically allocated and freed as functions are called and return
You don’t need to memorize this yet, just know that every variable you declare lives somewhere in this memory, at a specific address.
What is a Pointer?
A pointer is a variable that stores a memory address instead of storing a value directly. Instead of holding data like 4 or ‘A’, it holds where that data lives in memory.
| |
Declaring a pointer
| |
The address-of operator (&)
& retrieves a variable’s memory address:
| |
Note that %p is the format specifier for printing addresses, and the actual address will differ every time you run the program.
Assigning an address to a pointer
| |
Dereferencing
Once a pointer holds an address, ***** lets you reach into that address and read or modify the value stored there:
| |
You can also modify the original variable through the pointer:
| |
Don’t confuse ***** here with multiplication, context (whether it’s next to a pointer declaration/variable vs. two numbers) tells them apart.
Reassigning a pointer
A pointer isn’t locked to one variable, it can be pointed at a different variable of the same type later:
| |
Uninitialized pointers
A pointer that hasn’t been assigned an address holds garbage, or in some cases prints as (nil). Always initialize pointers, even if just to NULL:
| |
Pointer Arithmetic
Because a pointer holds an address, you can shift it forward or backward, but only addition and subtraction are allowed (multiplying or dividing an address makes no sense):
| |
The key detail: ptr + 1 doesn’t move 1 byte, it moves 1 element’s worth of bytes, so for int (4 bytes), ptr + 2 actually shifts the address by 8 bytes. ptr++ and ptr– work the same way, one element at a time.
Going out of bounds (moving a pointer past the memory it’s actually allowed to access) can crash your program or corrupt other data, so pointer arithmetic needs to stay within the array you’re working with.
Pointers and Arrays
An array’s name is itself a pointer to its first element, arr is equivalent to &arr[0]. This means you can walk through an array with a pointer instead of indices:
| |
The same works for modifying values:
| |
Pointers and Strings
Since a string is just a char array, the same pattern applies. A common approach is to loop until you hit the null terminator instead of using a fixed count:
| |
Quick recap
- A pointer stores an address, not a value
- & gets a variable’s address
- ***** dereferences a pointer, getting (or setting) the value at that address
- %p prints an address
- Pointer arithmetic moves in steps of the data type’s size, not raw bytes
- Always initialize pointers, uninitialized ones hold garbage
Memory Management
Unlike languages like Java or Python, C doesn’t manage memory for you automatically, there’s no garbage collector cleaning up after you. This gives you direct control over memory, which is powerful, but it also means mistakes (forgetting to free memory, using memory after it’s freed) can cause crashes or unpredictable behavior.
Stack vs Heap
- Stack - stores local variables. Memory is automatically allocated when a variable comes into scope, and automatically freed when it goes out of scope (e.g. when a function returns). You don’t manage this yourself.
- Heap - memory you manually request and release. It stays reserved for as long as you want, even after the function that created it returns, until you explicitly free it.
| |
The heap is what we’re managing manually in this section, using four functions from <stdlib.h>:
| Function | Purpose |
|---|---|
malloc() | Allocates a block of memory |
calloc() | Allocates memory and zeroes it out |
realloc() | Resizes a previously allocated block |
free() | Releases allocated memory |
malloc() and free()
| |
A couple of rules that matter here:
- Always free() memory you no longer need, forgetting to is a memory leak, memory that stays reserved for nothing.
- Never use a pointer after you’ve freed it, that’s undefined behavior (this is called a use-after-free).
Shorthand for sizeof, useful when the type might change later:
| |
Checking for allocation failure
malloc() returns NULL if it fails to get memory (e.g. system is out of memory). Always check before using the pointer:
| |
Or combined into one line:
| |
Allocating an array on the heap
| |
Once allocated, you can index into arr exactly like a regular array.
Why malloc() memory is dangerous by default
malloc() reserves memory but doesn’t clean it, whatever was left over from the previous program that used that memory is still sitting there. This is called garbage data:
| |
If your code assumes those values start at 0 (like a counter or accumulator), this will silently produce wrong results instead of crashing, which makes it a nasty bug to track down.
calloc(), Allocating Pre-Zeroed Memory
calloc() does the same job as malloc(), but guarantees every byte starts at 0:
| |
Note the arguments are different: calloc(count, size) instead of malloc(count * size).
You could zero out malloc()’d memory manually with memset(arr, 0, 5 * sizeof(int)), calloc() just does that for you in one step.
When to use which:
- Use calloc() when you need memory to start at zero, e.g. a counter array, or you want to be safe against reading uninitialized values.
- Use malloc() when you’re about to overwrite the memory immediately anyway (e.g. reading data straight into it), since zeroing it first would just be wasted work.
realloc(), Resizing Memory
If you allocated memory and later need more (or less) space, realloc() resizes the existing block instead of you manually creating a new one and copying everything over:
| |
realloc() might move the block to a completely different address if it cannot expand in place. Always treat the return value as a potentially new address, never assume it stayed the same.
Always assign the result to a separate pointer first and check for NULL before overwriting your original. If you assign straight back and realloc() returns NULL, you lose your only reference to the original block and it leaks.
memset() and memcpy()
Two functions from <string.h> that appear in almost every loader, shellcode stub, and Windows API call sequence.
memset(), Filling Memory
memset(ptr, value, size) fills a block of memory with a single byte value.
| |
The most common use is zeroing a buffer or struct before use:
| |
memcpy(), Copying Memory
memcpy(destination, source, size) copies size bytes from source to destination. No null terminator logic. It copies raw bytes.
| |
In a loader, memcpy is how you copy sections from a file buffer into allocated memory:
| |
Two things to remember. memcpy does not handle overlapping regions. If source and destination overlap, use memmove instead. And there is no bounds checking. Writing more bytes than the destination can hold is a buffer overflow.
Functions
A function is a reusable block of code that performs a specific task. Instead of writing the same logic over and over, you write it once and call it whenever you need it.
| |
Why use functions:
- Avoid repeating code
- Make code easier to read and manage
- Break big problems into smaller, manageable pieces
Calling functions and arguments
To use a function, you “call” it by writing its name followed by (). If it needs input, you pass that input inside the parentheses, these are called arguments. Different functions expect different numbers of arguments:
| |
Library functions
C comes with built-in functions grouped into libraries (headers), so you don’t have to write everything from scratch. To use them, include the relevant header at the top of your file:
| Header | Provides |
|---|---|
<stdio.h> | Input/output, e.g. printf() |
<stdlib.h> | Utilities, e.g. abs(), malloc(), rand() |
<math.h> | Math functions, e.g. ceil(), log() |
<ctype.h> | Character functions, e.g. toupper() |
| |
Defining Your Own Functions
A function’s signature tells you three things: its name, what inputs (parameters) it needs, and what type of value it returns (or void if it returns nothing).
| |
No parameters, no return value:
| |
With parameters and a return value:
| |
Use void inside the parentheses when a function takes no parameters (makeCookie(void)), it’s more explicit than leaving them empty, even though both compile.
Return Values
A function’s return type must match the type it actually returns, int, double, char, and pointers are all valid return types.
| |
Once return executes, the function exits immediately, any code written after it inside that function never runs:
| |
Type mismatches
Arguments you pass in must match the parameter types the function expects:
| |
This compiles with a warning at best, but causes unpredictable behavior, always match your argument types to the parameters.
Passing by Value vs. Passing by Pointer
By default, C passes a copy of a variable into a function, so changes made inside the function don’t affect the original:
| |
To actually modify the original variable, pass a pointer to it instead, then dereference it inside the function:
| |
This pattern is also how you can get a function to “return” more than one value, since a function can only return a single value directly:
| |
You’ll reach for pointer parameters when you want to: modify a variable without returning it, avoid copying large data (arrays, structs), return multiple values, or work with dynamically allocated memory.
Function Prototypes
A function must be declared before it’s used, if you call a function before the compiler has seen its definition, you’ll get an error. A function prototype solves this: it declares the function’s name, return type, and parameter types up front, so the full definition can come later in the file.
| |
Scope
Scope determines where in your code a variable can be seen and used.
| |
Even with the same name, these two myVariables don’t conflict, they live in different scopes.
Local scope - a variable declared inside a function or block only exists within that function or block:
| |
Global scope - a variable declared outside all functions is accessible everywhere in the file:
| |
Global variables are generally best avoided where possible, they’re harder to track (any function can change them), can cause naming conflicts, and make debugging harder.
Nested scope - a block inside another block (child scope) can access variables from the scope it’s nested in (parent scope), but not the reverse:
| |
When C looks up a variable, it checks the current scope first, then works outward through parent scopes, all the way up to global, if it’s not found anywhere, that’s a compile error.
Function Pointers
In offensive development you will constantly need to call functions without importing them, resolve APIs at runtime, and execute code from a buffer. All three of those rely on function pointers.
A function pointer stores the address of a function rather than data. This lets you call a function indirectly, pass functions as arguments, or jump to code you wrote at runtime.
How to declare one
The declaration wraps the pointer name in parentheses to separate it from the return type:
| |
typedef makes the declaration reusable and readable. Without it, every use of the function pointer type has to repeat the full syntax:
| |
Dynamic API resolution
Resolving and calling a Windows API without importing it:
| |
The typedef here defines the function signature once. GetProcAddress returns a raw address. The cast tells the compiler how to interpret that address as a callable function.
Executing shellcode from a buffer
| |
void(*)() means: a pointer to a function that takes no arguments and returns nothing. Wrapping it in another set of parentheses and adding () calls it immediately.
Structures
So far we’ve worked with basic types (int, char) and derived types built from them (arrays, pointers). A structure is another derived type, it lets you group different types of variables into a single unit.
Unlike an array, which holds multiple values of the same type, a structure can hold multiple different types together, useful for representing one real-world “thing” made of several pieces of data.
Defining a structure
| |
- struct defines a structure.
- Bottle is the structure’s name.
- The variables inside (name, maxCapacity, currentCapacity) are called member variables.
- Members are only declared here, not initialized, giving them a value at this stage is an error.
Why bother? Compare storing two bottles without a structure:
| |
Six separate variables for two bottles, and it only gets worse as you add more. With a structure, each bottle is a single variable:
| |
Initializing a structure
Values are assigned in the same order the members were declared:
| |
If you don’t want to worry about order, use named (designated) initializers instead:
| |
You can also declare first and assign values later:
| |
Dot notation
Use . to access or modify a structure’s members:
| |
Structure pointers
Structures can take up a fair amount of memory, especially with several fields or large strings, so it’s common to work with a pointer to a structure instead of copying the whole thing around.
| |
There are two ways to access members through a pointer:
| |
*(bottlePointer).maxCapacity and bottlePointer->maxCapacity do exactly the same thing, arrow notation is just shorthand that’s easier to read, and what you’ll see in most real C code.
Why use structure pointers:
| |
- Avoids copying large structures unnecessarily, saving memory
- Cheaper to pass into functions, since only an address is passed instead of the entire structure
- Matters more as your structures grow bigger or more complex
Structures and functions
Passing a structure by value gives the function a full copy, changes made inside don’t affect the original:
| |
Passing a pointer to the structure instead lets the function modify the original:
| |
Both in the same function, to see the contrast directly:
| |
A function can also return a structure, using struct as the return type:
| |
Always use the struct keyword when referring to a structure type in a parameter list or return type.
typedef Structs
Typing struct Bottle every time is verbose. typedef gives the structure an alias.
| |
Three names are now defined:
| Name | Meaning |
|---|---|
struct _BOTTLE | The original struct name |
BOTTLE | Alias for struct _BOTTLE |
PBOTTLE | Pointer to a BOTTLE (BOTTLE*) |
| |
This exact pattern is used throughout the Windows API. Every Windows type that starts with P is a pointer to the base type. So PHANDLE is HANDLE*, PDWORD is DWORD*, PSYSTEM_INFO is SYSTEM_INFO*. The P just means pointer-to.
{ 0 } zero-initializes the entire struct. You will see this constantly in Windows API code. STARTUPINFOW si = { 0 }; before passing it to CreateProcessW is a standard pattern.
Struct Alignment and sizeof()
Structs are not always the size you expect. Here is why.
The CPU reads memory most efficiently when values sit at addresses that are multiples of their size. A 4-byte int prefers to start at an address divisible by 4. To enforce this, the compiler silently inserts padding, unused filler bytes between members, so each one lands at the right boundary.
| |
You might expect 6 bytes. It is actually 12.
| |
Always use sizeof() on a struct rather than adding up the members:
| |
Why this matters when parsing binary formats
If you cast a raw pointer to a struct to parse a PE header, network packet, or binary file format, padding will silently break it. The struct layout will not match the on-disk layout.
The fix is #pragma pack:
| |
Use #pragma pack(push, 1) before any struct that maps directly to a binary format, and #pragma pack(pop) after it. Forgetting this is one of the most common causes of silent data corruption when writing PE parsers.
Enumerations
An enum defines a set of named integer constants. It is used to represent a fixed set of states, options, or return values, anywhere you would otherwise use magic numbers.
| |
The compiler assigns values starting from 0. You can override the starting value:
| |
| |
Enum values are accessed directly by name. They are just named integers. In Windows API and offensive dev contexts, enums represent memory protection states, process access rights, and NTSTATUS codes:
| |
Unions
A union stores different data types in the same memory location. Only one member is active at a time. Writing to one overwrites all the others because they all share the same space.
| |
The memory allocated equals the size of the largest member.
| |
Setting IntegerVar to 65 also changes CharVar to ‘A’ because they occupy the same memory.
You will encounter unions inside Windows-defined structures. A common pattern is a union inside a struct:
| |
This lets you access the same 64-bit value either as two 32-bit halves or as a single 64-bit integer. You will see LARGE_INTEGER used in file size and timestamp APIs.
Introduction to the Windows API
The Windows API provides a way for applications to interact with the Windows operating system. Displaying something on screen, modifying a file, querying the registry, allocating memory, all of these go through the Windows API.
Windows Data Types
The Windows API defines its own data types on top of standard C. Most are just typedefs for types you already know.
| Type | Description | Equivalent |
|---|---|---|
DWORD | 32-bit unsigned integer, always 32-bit | unsigned long |
SIZE_T | Unsigned integer the size of a pointer | size_t |
VOID | Absence of a specific type | void |
PVOID | Pointer to any data type | void* |
HANDLE | Identifies an OS-managed object (file, process, thread) | void* |
HMODULE | Handle to a loaded module, base address of a DLL or EXE | void* |
BOOL | Boolean. TRUE (nonzero) or FALSE (0) | int (4 bytes) |
BOOLEAN | Smaller boolean | unsigned char |
ULONG_PTR | Unsigned integer the same size as a pointer | pointer-sized |
ULONG_PTR is needed for pointer arithmetic on PVOID, since direct arithmetic on void* does not compile:
| |
String types
| Type | Description | Equivalent |
|---|---|---|
LPCSTR | Pointer to a constant ANSI string (read-only) | const char* |
LPSTR | Pointer to a writable ANSI string | char* |
LPCWSTR | Pointer to a constant wide string (read-only) | const wchar_t* |
LPWSTR | Pointer to a writable wide string | wchar_t* |
The L prefix is a leftover from 16-bit Windows. The C means const. The W means wide (UTF-16). LPCWSTR = Long Pointer to Const Wide String = const wchar_t*.
Data Type Pointers
For most Windows types, a P-prefixed pointer version exists:
| Pointer type | Same as |
|---|---|
PHANDLE | HANDLE* |
PSIZE_T | SIZE_T* |
PDWORD | DWORD* |
PBYTE | BYTE* |
ANSI and Unicode Functions
Most Windows API functions come in two versions. CreateFileA is ANSI, CreateFileW is Unicode. Always use the W variants. The A variants convert your string and call W anyway.
| |
IN and OUT Parameters
Windows API parameters are annotated with IN and OUT as documentation hints, not keywords.
- IN parameter: you provide the value.
- OUT parameter: the function writes a result back. These are always pointers.
| |
Using a Windows API: CreateFileW
| |
| |
CloseHandle is not optional. Handles are kernel objects. Leaking them in a long-running process or implant means the OS keeps the object alive for the process lifetime. Call CloseHandle on every handle you open, including on error paths.
Note that CreateFileW returns INVALID_HANDLE_VALUE on failure, not NULL. INVALID_HANDLE_VALUE is -1 cast to a HANDLE, which is 0xFFFFFFFFFFFFFFFF on x64. Not all handles use the same failure value. Read the docs per function.
HeapAlloc and the Windows Heap APIs
In Windows malware development you will sometimes want to allocate memory without depending on the C runtime. malloc internally calls HeapAlloc via the CRT. Going directly removes that dependency.
| |
HEAP_ZERO_MEMORY zeroes the allocation. Without it the memory contains whatever was there before.
At the lower level, HeapAlloc calls RtlAllocateHeap from ntdll.dll (the core Windows runtime library that sits below the Win32 API layer). Some shellcode loaders avoid importing malloc or linking the CRT entirely, using HeapAlloc or VirtualAlloc directly. Knowing both lets you read and write either style.
Error Handling: Win32 vs Native API
Win32 API errors
When a Win32 function fails, it sets a thread-local error code. Retrieve it with GetLastError().
| |
Common codes:
| Code | Meaning |
|---|---|
| 2 | ERROR_FILE_NOT_FOUND |
| 5 | ERROR_ACCESS_DENIED |
| 87 | ERROR_INVALID_PARAMETER |
Native API errors (NTSTATUS)
Functions from ntdll.dll (Nt/Zw prefixed) return the error code directly as an NTSTATUS value. Zero means success (STATUS_SUCCESS).
| |
NT_SUCCESS is a macro: #define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0).
Win32 functions return BOOL or a handle, and you call GetLastError() to find the error. Native API functions return NTSTATUS directly, so you check the return value with NT_SUCCESS.