Disclaimer: This study guide attempts to touch upon the most important topics that may be covered on the exam but does not claim to necessarily cover everything that one needs to know for the exam. Finally, don’t take the one hour time window in the title literally.
Last update: Fri Oct 30 13:49:02 2026
Week 5: Hash Pointers, Blockchains, Merkle Trees, and Bitcoin
Bitcoin combines hash functions, digital signatures, and proof of work to maintain a shared transaction history without a central authority. Its cryptography consists of hash pointers that link blocks into a blockchain, Merkle trees that tie each block’s transactions to its header, digital signatures that authorize payments, and proof of work that makes adding a block expensive. Every transaction is public.
Hash Pointers and Blockchains
A hash pointer stores a reference to data together with a cryptographic hash of that data, so a reader can detect whether the data changed. A blockchain is a linked list in which each block holds a hash pointer to the previous block. The newest block is identified by a head pointer, and the first block is the genesis block.
Changing any block changes its hash, which invalidates the hash pointer in the next block, and so on up to the head. An attacker who alters a block must therefore recompute every hash pointer between that block and the head. The chain is a tamper-evident log. Protecting only the head pointer, for example by storing it securely or signing it, is enough to detect tampering anywhere in the chain.
A distributed ledger such as Bitcoin has no single head pointer that one authority can protect, since every participant holds its own copy of the chain and there is no central authority. Recomputing the hashes is easy, so an attacker who can distribute a rewritten chain could pass it off as the real one. Bitcoin instead makes adding each block computationally expensive through proof of work, so that rewriting history requires redoing that work for every block after the change.
Merkle Trees
A Merkle tree is a binary tree of hashes. Each leaf is the hash of one data item, each parent is the hash of its two children joined in order, and the single hash at the top is the Merkle root. Changing any item changes the root.
A Merkle proof, or proof of inclusion, is the set of sibling hashes along the path from one item to the root, together with their left-or-right positions. The verifier hashes the item, combines it with each sibling in turn, and compares the result with a trusted root. A proof for a balanced tree of \(n\) items contains about \(\log_2 n\) hashes. The item and the proof can come from an untrusted source, but the root must be obtained securely. A matching root establishes that the item belongs to the collection.
Bitcoin and Its Goals
Bitcoin is a decentralized payment system. A conventional payment system relies on a bank as a trusted third party that keeps the account balances, checks that a payer has the funds, and records each transfer. Bitcoin replaces the bank with a public ledger, a history of every transaction, replicated across many independent computers. Anyone can run one of these computers, verify the history, and submit payments without registering with any authority.
Removing the central authority creates two problems that the rest of the design solves. The first is identifying who may spend funds when there are no names or accounts, which Bitcoin does with cryptographic keys. The second is double spending. Digital data can be copied, so a payer can sign two transactions that spend the same funds. Each signature is valid on its own, so the participants need a way to agree on a single transaction history that decides which payment counts.
Users are pseudonymous rather than anonymous. Transactions do not carry names, but every transaction is public, and outside information can link an address to a person.
Keys, Signatures, and Addresses
Bitcoin identifies participants by keys rather than names or accounts. A user creates an elliptic curve public/private key pair on their own computer, and that key pair is the only information the user needs to keep in order to control funds. The private key signs transactions, and anyone can verify the signature with the public key. A valid signature shows that the signer holds the private key. It does not reveal who that person is, and it does not show whether the funds have already been spent.
A private key must be generated unpredictably and kept secret. A stolen key lets the thief spend the funds, and a lost key makes them permanently inaccessible, since there is no central authority that can reset it. A wallet is software that generates and stores a user’s keys and uses them to create transactions. It does not hold balances or transactions. Those are determined by the public ledger.
An address tells a sender how to construct an output that the recipient can spend. It contains a hash of the recipient’s public key plus a checksum. Spending the output requires presenting a public key that hashes to that value and a valid signature under that key. Hashing the key makes the address shorter and keeps the public key off the blockchain until the output is spent, in which case it appears as the owner of the transaction that is doing the spending. The checksum detects typing errors.
Transactions and Unspent Outputs
Bitcoin has no account balances. The ledger is the history of transactions, and the system tracks unspent transaction outputs (UTXOs). Each output records an amount and the condition for spending it. A user’s balance is the sum of the UTXOs that the user’s keys can spend.
A transaction has the following parts:
-
Each input refers to an earlier output by that transaction’s identifier (its txid, a hash of the transaction) and an output index. An input consumes the referenced output completely.
-
Each output specifies an amount and a spending condition.
-
The sender’s public key, which identifies the user issuing the transaction. It must hash to the value stored in the outputs being spent, and it is the only identification the transaction carries.
-
A signature created with the sender’s private key. It covers every input and every output, including amounts and spending conditions, so it shows that the holder of the private key authorized this exact transaction. Changing the inputs, the amounts, or the recipients invalidates it.
-
Change is an ordinary output that returns the unused value to the sender.
-
The transaction fee is the total of the inputs minus the total of the outputs. It is not a separate output. The miner who includes the transaction collects it.
Verification and Replication
A full node is a computer that holds a complete copy of the blockchain, the ledger of every transaction since the genesis block. It verifies every block and transaction in that copy for itself rather than trusting any other node, and from that history it maintains the current UTXO set. For each new transaction, it checks that every referenced output exists and is unspent, that no output is used twice, that each public key hashes to the required value, that each signature is valid, and that the outputs do not exceed the inputs. The node takes the input amounts from the earlier outputs, not from the transaction.
A verified transaction is not yet part of the blockchain. The node holds it in memory as a pending transaction, and it does not change the confirmed UTXO set until a block containing it joins the node’s chain. Different nodes can hold conflicting pending transactions, so local verification cannot settle which of two competing spends will count.
There is no master copy of the blockchain. Full nodes around the world each hold their own copy, and no node has authority over another. A lightweight client can use a Merkle proof and an accepted block header to check that a transaction is in a block, but it relies on full nodes and miners for the other checks.
Blocks and Proof of Work
Miners collect pending transactions into blocks. Each block has a header and an ordered list of transactions. The header contains the following fields:
-
The hash of the previous block’s header, which links the blocks into a chain.
-
The Merkle root of the block’s transaction identifiers, which ties the transactions to the header.
-
A timestamp.
-
The mining target, in encoded form.
-
A nonce, a number the miner changes while searching for an acceptable hash.
The block hash is the double SHA-256 hash of the header. Changing any transaction changes the Merkle root, the block hash, and the link from the next block.
Proof of work requires the block hash, interpreted as a 256-bit integer, to be at most the value of the target. Hash outputs cannot be predicted, so a miner repeatedly changes the nonce and hashes the header again until the result qualifies. A smaller target leaves fewer acceptable hashes and makes the search harder. Finding a qualifying header is expensive, but checking one takes a single hash computation. The nonce has a limited range, and trying every value usually does not produce a hash below the target. Miners therefore also vary other permitted header data, including the set of transactions, which changes the Merkle root and gives them a new header to search.
A miner’s hash rate is the number of hashes it can try per second. As the network’s total hash rate changes, the difficulty adjustment periodically recomputes the target so that blocks continue to arrive about every ten minutes on average. Every node derives the required target from the chain, so a miner cannot choose an easier one.
Agreement, Chain Selection, and Reorganization
Chain selection is how Bitcoin resolves double spending. Two transactions can carry valid signatures while spending the same output, and signatures alone cannot settle which one counts. Only an agreed history can.
Two miners can find valid blocks at about the same time. Both blocks extend the same previous block, so the chain forks into two competing branches, and nodes that received different blocks first temporarily follow different branches. To choose between branches, each node assigns every branch it knows about a score, which is its accumulated work, the total proof of work that its blocks represent. The node follows the valid branch with the highest score. Miners extend the branch they follow, so one branch soon gains more work than the other, and every node converges on it.
The work credited to a block is determined by the target it was mined under, with a smaller target counting for more work. It is not the number of hash attempts the miner actually made. When all blocks have the same target, the branch with the most work is also the longest one, which is why the rule is often described as following the longest chain. Branch selection depends only on work and validity. There is no vote among nodes, and running more nodes does not add mining power. Valid proof of work does not make invalid transactions acceptable, because every node validates each block’s contents independently.
When a competing branch overtakes the one a node had followed, the node performs a chain reorganization. It undoes the abandoned branch’s changes to its UTXO set and applies the winning branch. Transactions from the abandoned branch become pending again if they are still valid. A transaction that conflicts with a spend in the winning branch is rejected.
The 51% Attack and Confirmations
A 51% attack refers to control of a majority of the network’s hash rate, not of nodes or coins. An attacker with that much hash rate can mine a private branch faster than the honest miners on average, replace recent blocks, double-spend its own funds, and keep chosen transactions out of blocks. It cannot forge other users’ signatures or create coins beyond what the rules allow, because every full node checks those rules. An attacker with less than half the hash rate can still succeed by luck, but the chance falls as more blocks are added.
A transaction has one confirmation when it is in a block in the active chain and gains another with each later block. More confirmations make a transaction more expensive to reverse, since an attacker would have to redo the work of every block since then. The protocol does not require any number of confirmations. The recipient decides how many to wait for, weighing the value of the payment against the delay, and six is the traditional guideline for valuable payments. No number of confirmations is a guarantee.
Mining Rewards
The first transaction in every block is the coinbase transaction (no relationship to the Coinbase company). It has no inputs and lets the miner claim the block reward, which consists of newly created bitcoin, called the block subsidy, plus the fees of the block’s transactions. Nodes reject a block whose coinbase claims more than the rules allow.
The subsidy halves on a fixed schedule, which limits the total supply to about 21 million BTC and makes fees a growing share of the reward over time. The reward is the incentive for miners to participate. Mining consumes electricity and hardware, and without payment for finding blocks there would be no reason to do it, and no proof of work to protect the chain. A reward in an abandoned branch cannot be spent, so miners are paid only for blocks the network accepts.
What You Don’t Need to Study
The following are not required for exams:
-
The sections marked Optional in the notes: Other Output Types, Compact Target Encoding, Mining Hardware and Pools, and Benefits and Limitations.
-
The example private key, public key, and address, the sample block and its target, the network statistics, the mining pool shares, and the dates and block heights in the reward table.
-
The name pay-to-public-key-hash (P2PKH), the idea that Bitcoin supports other output types, the address derivation steps (SHA-256 followed by RIPEMD-160, the version byte, and Base58Check encoding), and the name of the elliptic curve. The reason for hashing the public key is still required.
-
The name of the signature algorithm and the
SIGHASH_ALLsignature mode. What the signature covers is still required. -
The names Hashcash and Difficulty Adjustment Algorithm, the 2,016-block adjustment interval, the factor-of-four limit, the 80-byte header size, and the order of the header fields. The purpose of the previous block hash, Merkle root, target, and nonce is still required.
-
The 210,000-block halving interval, the current subsidy, the halving dates, and the 100-block coinbase maturity rule. The existence of the subsidy, its halving, and the 21 million BTC limit are still required.
-
The term mempool for a node’s set of pending transactions, the distinction between archival and pruned full nodes, and the term simplified payment verification. What a lightweight client can and cannot verify is still required.
-
Ethereum’s account model. However, you should still know the difference between account balances and unspent outputs.
-
Bitcoin’s scripting language, its peer-to-peer protocol, wallet implementation, and payment channels, none of which the notes or the lecture covered.
Week 6: Access Control
Access control decides which subjects may perform which operations on which objects. The hardware and kernel mechanisms below are what make those decisions enforceable, and the models that follow are different ways of stating the policy.
Protection and the Operating System
Access control decides which subjects may perform which operations on which objects and enforces those decisions. A security policy defines what access should be allowed, and protection is the set of mechanisms that enforce it.
An operating system provides processes with controlled access to shared resources: the processor, memory, files and devices, and the network. Programs execute instructions and access mapped memory directly, within limits enforced by the hardware. A system call transfers control to the kernel to request an operating system service, and the kernel checks whether the request is allowed before performing it.
Three things have to be true for protection to work at the operating system level. The operating system must:
-
Protect itself from the programs it runs.
-
Protect programs from each other.
-
Stay in control, so that a process cannot hold the processor forever.
The kernel and the hardware that enforce these properties form the core of the trusted computing base, the set of components whose failure means the security of the whole system fails.
The hardware provides three mechanisms:
-
Preemption: a hardware timer that applications cannot disable interrupts the processor periodically and gives the kernel’s scheduler a chance to run. This is preemptive multitasking. Under cooperative multitasking (used in older operating systems), one program that failed to yield could freeze the machine.
-
Memory isolation: the memory management unit (MMU) hardware translates every address a program uses, and the kernel sets up each process so that it can reach only the memory it has been assigned.
-
Privilege levels: in user mode, applications cannot execute privileged instructions or touch kernel memory. The kernel runs in kernel mode, where every instruction is available.
Execution transfers from user mode to kernel mode through controlled entry points defined by the kernel at boot time. The transfer occurs in three ways:
-
A system call, which the program makes deliberately to request a service.
-
A fault, when an instruction cannot complete without kernel intervention. A fault can indicate an invalid operation or a valid memory access that requires the kernel to supply a page.
-
A hardware interrupt, when a device or the timer needs attention.
The Reference Monitor
The reference monitor is the part of a system that checks every attempt by a subject to reach an object against the policy. It has three requirements:
-
It is invoked on every access.
-
It is tamperproof: the programs it controls cannot change it.
-
It is small enough to be analyzed and tested.
In an operating system, the kernel’s access checks and the hardware mechanisms it configures implement the reference monitor. The MMU enforces memory permissions, while system calls mediate requests for kernel services. User-mode code cannot modify the kernel, but these protections do not prevent bugs in the kernel itself. The size of a modern kernel makes the third requirement difficult to satisfy. A database server or a web application is its own reference monitor for the data it manages, and its enforcement depends entirely on the correctness of that code.
Subjects, Objects, and the Access Matrix
A subject is an active entity that requests access, almost always a process. A principal is the identity on whose behalf a subject acts, usually a user account. An object is a passive resource: a file, a device, a socket, a region of memory, or another process. A right is an operation a subject may perform on an object.
Authorization is the access decision in the following sequence:
-
Identification is a claim of identity.
-
Authentication verifies the claim.
-
Authorization decides what the authenticated principal may do.
The access matrix describes every permission in a system: one row per subject, one column per object, and in each cell the set of rights the subject has over the object, such as read, write, execute, append, and delete.
A row defines a protection domain, the complete set of objects and rights available to a process. Several processes can share the same protection domain. A process operates within a protection domain and exercises that domain’s rights. Deleting an object removes its column from the matrix, while revoking a right removes only that right from a cell.
Other rights govern how permissions are managed or which domain a process uses:
-
Owner: the subject that owns an object may grant or revoke rights in that object’s column.
-
Copy: a subject may pass a right it holds to another subject if that right permits copying.
-
Domain transfer: a subject may switch to another row and acquire that row’s rights. Logging in is a domain transfer, and so is running a setuid program.
The matrix is a model rather than a data structure. A real one would be enormous and almost entirely empty, so systems store it in one of two compressed forms:
-
An access control list (ACL) stores a column with its object: a list of principals or groups and their rights. Inspecting an object’s entries and changing or removing an access right are easy. Finding everything a principal can access requires searching every object’s list.
- “Who has permission to read this file?” is easy to answer by inspecting the file’s ACL. “Which files can Alice access?” requires searching the ACLs of many files.
-
A capability list stores a domain’s row as a list of capabilities held by a process. Each capability in the list is an unforgeable token that names an object and a set of rights the process has over it. Checking and delegation are easy. Revocation requires a way to invalidate capabilities that have already been distributed.
- “Which files can Alice’s process access?” is easy to answer by inspecting its capabilities. “Which processes have read access for this file?” requires finding the capabilities held by other processes.
An open file descriptor is a capability: the permission check happens once, when the file is opened, and the descriptor is then presented on every read and write without rechecking the file’s permissions. A descriptor passed to another process carries the access with it.
A confused deputy is a program that holds authority of its own and can be tricked into using that authority for a caller who lacks it. The deputy’s rights are checked and the caller’s are not, so any program that runs with more privilege than its caller must check what the caller is entitled to. Capabilities can avoid the problem by having the caller supply a capability for each object. The program uses that capability rather than its own authority, and the system enforces the permissions the capability grants.
Discretionary and Mandatory Control
Access control is divided into two families, based on who controls access permissions:
-
In discretionary access control (DAC), the owner of an object decides who may access it. Unix and Windows file permissions are examples.
-
In mandatory access control (MAC), access rules are defined by the organization rather than by the object’s owner, and the system does not allow users, including owners, to override them.
Nearly every system implements some of both.
Unix File Permissions
Unix file permissions are a foundation for understanding access control because the same basic model is used by Linux, Android, BSD, QNX, and macOS. These systems span servers, phones, personal computers, and embedded devices. In this section, “Unix” refers broadly to systems that use this permission model.
Every file has an owner, a group, and nine permission bits, stored in the file’s inode with the rest of its metadata. One set of three bits applies to the owner, one to members of the file’s group, and one to everyone else (which Unix calls “other”), and within each set one bit grants read, one write, and one execute permission. This is a compact ACL with three fixed entries that fit in the inode, so reading the file’s metadata also retrieves its permissions without an additional disk read.
The permission check examines only one set of bits: the owner’s if the process’s effective user ID matches the owner, otherwise the group’s if the process belongs to the file’s group, otherwise the other bits.
A binary file can be executable without being readable, because the kernel loads it; a script cannot, because the interpreter has to read it.
For a directory, read access allows listing the names in the directory, write access allows creating, deleting, or renaming entries, and execute access allows the directory to be searched to reach something inside it. Deleting a file requires write and execute permission on the directory, not write permission on the file, because deletion removes a name from the directory.
Default Permissions and the umask
Most programs (particularly scripts and cross-platform environments like Java and Python) request read and write permission for everyone when creating files and leave the restriction to the environment. The umask is an inherited per-process value listing the permission bits to remove at file creation. The usual default removes write permission for the group and everyone else, which leaves new files readable by every user on the machine.
The Access Check Happens at Open
The permission check runs once, when a file is opened, and not on each read or write. A process that holds an open descriptor keeps its access even if the file’s permissions change afterward. A program that creates a file with permissive bits and then restricts them leaves a window of time in which another process can open the file and keep it open. To avoid this race condition, the file should be created with the right permissions in the first place.
Users, Groups, and Root
A principal on a Unix system is represented by a numeric user ID. A process also contains an effective group ID and a list of supplementary groups. Membership in any of them counts as membership in a file’s group.
User ID 0 is root, the superuser. Root can bypass most discretionary permission checks. However, mandatory controls such as SELinux (Security-Enhanced Linux, described below) apply to root too.
A process inherits its user ID from its parent. The login program runs as root and takes on the user’s identity before starting the user’s shell, which is a domain transfer. In this case, the change is permanent: the process gives up its ability to restore root privileges.
Running with Someone Else’s Authority
A process has more than one user ID. Its real user ID identifies the account that started it, and its effective user ID is the one the kernel uses for permission checks. These are usually the same.
Some legitimate actions, such as changing a password, require a privilege the user should not hold in general. If a program’s setuid (“set user ID”) bit is set, a process that runs it gets the file owner’s user ID as its effective user ID rather than the ID of whoever ran it. The real user ID is unchanged, so the kernel still knows who started the process. A setgid bit does the same for the group ID.
Running a setuid program is a domain transfer, and its code must enforce which privileged operations the caller is entitled to request. The program is a deputy, and exploiting a bug in a setuid-root program can give an attacker root privileges.
Several defensive rules follow from that:
-
Install as few setuid programs as possible.
-
Give up the privilege when it is not needed, and verify that the drop succeeded. A process can lower its effective user ID while keeping a saved user ID so it can raise the privilege again for the moments that need it, or it can drop the privilege permanently so that it cannot be restored.
-
Concentrate privilege in one heavily reviewed program, which is what the
sudocommand does. -
Split root’s power by granting only the privileges a program needs. Linux supports this through capabilities, which divide root’s authority into separate privileges andwe dicuss these later under containment. These Linux privileges are distinct from the capability lists described earlier.
A setuid program that checks whether its caller may access a file by name and then opens the file by name has a time-of-check to time-of-use race condition: the filename can be made to refer to a different file between the check and the use, allowing an attacker to access a different file.
Least Privilege and Privilege Separation
The Principle of Least Privilege states that every program and every user should operate with the smallest set of privileges needed to complete the job. It limits damage rather than preventing mistakes. On Unix the principle is applied with identities: each service runs as its own user, a server that must start as root gives up root before it reads anything from the network, and administrators use sudo for individual privileged commands.
Privilege separation is the structure for programs that cannot give up root entirely. The program is divided into a small privileged part that does only the operations needing privilege and a larger unprivileged part that does everything else, connected by a narrow channel. The unprivileged part handles everything an attacker can send, so a bug there yields a process with little authority, and the attack surface of the privileged code shrinks to the requests the channel accepts.
Full Access Control Lists
The nine Unix permission bits express only three entries: owner, group, and others. Linux and macOS also support access control lists (ACLs) with entries for additional users and groups, while Windows NT and its successors have used ACLs from the start. A full ACL takes a variable amount of space, and the filesystem determines how it is stored.
Some ACL systems support explicit deny entries as well as allow entries. Inheritance lets newly created files and directories receive permissions from their parent directory.
Full ACLs can express any column of the access matrix. The drawback is that nobody can see the matrix: with inherited entries, named users, and deny entries, determining a user’s effective permissions requires evaluating all applicable rules.
Why Discretionary Control Is Not Enough
Discretionary control has three limits that no setting of the permission bits can fix:
-
A program runs with the authority of the user who runs it, so a malicious program can use the user’s permissions to read or disclose files without the user’s intent.
-
Rights propagate. A user who can read a file can copy its contents into a file they own and give others access to the copy.
-
Owners control permissions. Discretionary control alone cannot enforce an organization-wide rule that owners are forbidden to override.
Mandatory access control removes these limits by enforcing rules that users and programs cannot override.
Multilevel Security and Bell-LaPadula
The Bell-LaPadula model describes multilevel security: a system that holds information at several levels of sensitivity and prevents it from flowing downward. Objects are given a classification from an ordered set of levels (Unclassified, Confidential, Secret, Top Secret), and a user’s clearance sets the highest level at which their processes may operate. A process can run at that level or a lower one. Two rules protect confidentiality:
-
The simple security property, or no read up: a subject may read an object only if its operating level is at least as high as the object’s classification. For example, a subject operating at Secret may read a Confidential file but cannot read a Top Secret file.
-
The star property, or no write down: a subject may write an object only if the object’s classification is at least as high as the subject’s operating level. For example, a subject operating at Secret may write to a Top Secret file but cannot write to a Confidential file.
Here, writing means adding information without reading the object. An operation that both reads and writes must satisfy both rules, so the subject and object must be at the same level.
A third rule, the discretionary security property, says that ordinary permissions still apply within those limits: an owner can restrict access further but cannot extend it past what the labels allow.
The star property prevents programs from disclosing information to lower classification levels. A program operating at Secret could read a Secret file and attempt to write its contents to an Unclassified one. The star property forbids that write, whether the disclosure is deliberate or accidental. Under this rule, information may flow to the same or a higher classification level, but never to a lower one.
The model has three limits:
-
Information cannot flow down, so declassifying and sanitizing require trusted subjects that are exempt from the star property, and the security of the system then rests on those programs.
-
It does not protect integrity: a low-clearance subject may write (or overwrite) a Top Secret file.
-
Covert channels can bypass the modeled access controls. For example, a Secret process could signal bits to an Unclassified process by varying its CPU use, causing delays the other process can measure.
Compartments and Multilateral Security
Clearance levels order information by sensitivity. Need to know divides it within a level: a clearance establishes that a person may be trusted at a level, and a separate decision establishes which information at that level the person needs. A compartment is a category of information, such as information about a specific project. Clearance at Secret does not grant access to every Secret compartment. The full Bell-LaPadula label combines a level with a set of compartments. Separating access by compartment as well as by level is called multilateral security.
A subject may read an object only if two tests both pass: its operating level is at least as high as the object’s classification, and its compartment set includes every compartment in the object’s label. When both pass, the subject’s label dominates the object’s. Dominance replaces the level comparison in both rules: a subject may read only what its label dominates and write only to what dominates its label. A subject missing even one of the object’s compartments cannot read it.
Two labels can be incomparable, meaning neither dominates the other. The set of labels ordered by dominance forms a lattice, which can be represented as a directed graph. Each node is a label, and arrows point toward labels that dominate it. Information may flow along these arrows or remain at the same label.
The overhead is administrative: every distinct combination of compartments is in effect a new label, and combining information from two compartments produces a label that readers of either compartment alone cannot see.
Outside government, Bell-LaPadula has seen little use, because organizations’ access policies rarely reduce to an ordering of levels.
The Biba Integrity Model
Bell-LaPadula does not protect trustworthy data from being corrupted by sources that should not write it. The Biba model has the same structure and the opposite goal. Subjects and objects carry integrity levels, where a high level means the data is trusted and the subject is trusted to produce it. The two rules are Bell-LaPadula’s reversed:
-
No read down: a subject may read an object only if the object’s integrity level is at least as high as the subject’s. A trusted process may not take input from an untrusted source.
-
No write up: a subject may write an object only if the subject’s integrity level is at least as high as the object’s. An untrusted process may not modify trusted data.
Biba protects trusted data from contamination. The strict rules are a poor fit for real systems because programs that process untrusted input are the normal case.
Windows Mandatory Integrity Control implements a restricted form of Biba: processes and securable objects have integrity levels, and the default rule prevents a process from modifying an object at a higher level regardless of the object’s discretionary permissions. It does not enforce Biba’s no-read-down rule. Its first main use was running a browser at low integrity, so that an exploited browser could not modify the user’s or the system’s files.
The Chinese Wall Model
The Chinese Wall model is for firms that serve clients who compete with each other. Its access decision depends on what a subject has already done. Objects are organized in a two-level hierarchy: each object belongs to one company dataset, which is all the data the firm holds about one client, and each dataset belongs to one conflict of interest class, which groups the datasets of companies that compete.
The read rule is: a subject may read an object in a company dataset only if everything the subject has previously read is either in the same dataset or in a different conflict of interest class.
Before the first read, every dataset is available. After an employee reads one airline’s data, the other airlines in that class are denied, and companies in other classes are still available. The system must record every access, and the record persists across login sessions.
For example, an advertising agency places Rutgers and NYU in the same conflict of interest class. Once an employee reads data from the Rutgers dataset, they cannot read data from the NYU dataset.
The write rule prevents information from passing between competing clients through another dataset. A subject may write to a company dataset only if it may read that dataset and has no permission to read data from any other company dataset, except sanitized data. Sanitized data has been altered so that it cannot be traced to its source company. Reading sanitized data does not create a conflict, and it may flow between datasets. Creating sanitized data requires a trusted procedure outside the ordinary write rule.
For example, the advertising agency also serves Delta Air Lines, whose dataset belongs to a different conflict of interest class from the universities. Alice may read Rutgers and Delta data, while Bob may read NYU and Delta data. Without the write rule, Alice could copy Rutgers data into Delta’s dataset, where Bob could read it. The write rule prevents this by denying Alice permission to write to Delta’s dataset while she has permission to read Rutgers data.
The model is a dynamic form of separation of duties, the principle that a task capable of causing serious harm is divided so that one person cannot complete it alone.
Type Enforcement
Bell-LaPadula and Biba compare ordered labels. The threat on a server is a compromised program, and the policy a server needs is a list of which programs may perform which operations on which files, and only those operations. With ordered labels, permissions follow from positions in a hierarchy. Allowing a program to read one file may also allow it to read files at lower levels, even if the policy should forbid those reads. Type enforcement allows each permission to be specified independently.
Type enforcement is an implementation of the access matrix in which the rows and columns are classes rather than individuals.
Every subject is assigned a domain, which is a class of subjects, and every object a type, a class of objects. A policy is a list of rules, each allowing a domain to perform certain operations on a type, and anything the policy does not allow is denied. There is neither a hierarchy nor a notion of up or down. Grouping processes and files into classes reduces the number of rules needed to describe their permissions, which keeps the size of the matrix manageable.
SELinux is the implementation of type enforcement in the Linux kernel. Its check is a second reference monitor that runs after the ordinary permission bits, and either check can deny access. The benefit appears when a program is compromised: the process is still running in its domain, so even if running as root, it can touch only what the policy allows that domain. The drawback is a potentially large policy set, and a missing rule can make a program fail with a permission error.
Role-Based Access Control
In an organization, rights are associated with jobs. Role-based access control (RBAC) models that with four kinds of entities:
-
Users are the people or programs that need access.
-
Roles are named sets of permissions corresponding to job functions.
-
Permissions are rights to perform operations on objects.
-
Sessions are the periods during which a user has some of their roles active.
Permissions are assigned to roles and users are assigned to roles. A user never holds a permission directly, and the roles active in a user’s session define what the user can do.
The benefit is administrative: permissions associated with a job can remain stable while users change as people are hired, leave, or change positions.
Roles resemble Unix groups: both let an administrator manage permissions for many users together. A group is a collection of users, while a role is a collection of permissions associated with a responsibility, such as processing payroll. In RBAC, users receive permissions through their assigned roles. Group-based systems may also grant permissions directly to individual users.
A session records which of a user’s assigned roles are currently active, allowing the user to activate only the permissions needed for a specific task. Switching active groups can serve a similar purpose. RBAC makes the distinction between assigned roles and active roles an explicit part of the access control model.
Roles usually correspond to job functions, with permissions such as approving a payment or issuing a refund, but they can also group file access or narrower technical permissions. RBAC is used within applications, databases, directory services, and cloud platforms. Its defining feature is how permissions are assigned and activated, rather than the kinds of operations they control.
RBAC fails when roles multiply: a role for every combination of permissions anyone has ever needed produces role explosion, with the structure harder to audit than the permissions were. Attribute-based access control (ABAC) generalizes roles to rules that test attributes of the subject, the object, and the environment, such as the user’s department, the document’s classification, and the time of day.
The Models Compared
Each model decides the same question, whether a subject may perform an operation on an object, from a different kind of label and a different rule:
| Model | What the label is | The rule | What it protects |
|---|---|---|---|
| Discretionary | Owner and permission entries | The owner decides | Whatever the owner chooses |
| Bell-LaPadula | Classification and clearance | No read up, no write down | Confidentiality |
| Bell-LaPadula with compartments | Level plus compartments | Read only what the label dominates | Need to know |
| Biba | Integrity level | No read down, no write up | Integrity |
| Chinese Wall | Company dataset and conflict class | One dataset per conflict class, fixed by the first access | Conflict of interest |
| Type enforcement | Domain and type | Only what a rule allows | Confinement of programs |
| Role-based | Role | Permissions follow the role | Administrative scale |
A real system runs more than one model, with one layered above another. Bell-LaPadula assumes discretionary permissions beneath it, and a Linux server with SELinux runs the Unix bits with type enforcement above them. An Android phone runs three layers:
-
Unix permissions, with each app as its own user, keep apps out of each other’s files.
-
SELinux confines each app’s process to what the policy grants.
-
The Android framework’s permission model decides whether an app may use the camera or read contacts.
Each layer expresses rules that the one below it cannot.
What You Don’t Need to Study
The following are not required for the exam:
-
Dates, and the names of the people and organizations behind each model or mechanism. Know the names of the mechanisms themselves, such as Bell-LaPadula, Biba, the Chinese Wall model, type enforcement, SELinux, and RBAC.
-
Incidents and designs such as the Capital One breach, the CrowdStrike outage, PwnKit, and the OpenSSH and Chrome designs. Know the concepts they illustrate: a confused deputy and a role with far more rights than it needed, kernel code failing with kernel consequences, a setuid program as a path to root, and privilege separation.
-
How the memory management unit, page tables, and address translation work, protection rings, and hypervisors. Know that the hardware confines each process to the memory the kernel assigned it, that privileged instructions are unavailable in user mode, and that system calls, faults, and hardware interrupts transfer execution to controlled kernel entry points.
-
KeyKOS, seL4, and Google’s Fuchsia. Know that an open file descriptor is a capability.
-
Octal and binary permission values such as 0644 and 0666, and the syntax of
chmod,chown, andchgrp. Know what the nine bits mean, how the check picks one set, and what read, write, and execute mean for a directory. -
The usage of the
setuid,seteuid,setgid,initgroups,chown,chmod, andaccesssystem calls, the exact sequence of calls in the login process, and the difference between temporary and permanent privilege drops. Know the concepts of user IDs, real and effective user IDs, group IDs, and the setuid bit. -
The sticky bit.
-
The syntax of Linux, macOS, and Windows ACLs, the POSIX ACL mask, and how the three systems order and evaluate their entries. Know that full ACLs hold named entries beyond the three Unix permission sets and why full ACLs are hard to audit.
-
The implementation and usage of Windows access control: security identifiers, access tokens, DACLs and SACLs, canonical ordering, and NTFS storage. Know that Windows NT and its successors have used full ACLs with deny entries and inheritance from the start.
-
The implementation and usage of Windows Mandatory Integrity Control: the names of its levels, where the label is stored, and AppContainer. Know that Windows enforces a Biba-style no-write-up rule by default, allows reading down, and first used this mechanism prominently to confine a browser.
-
Sensitive Compartmented Information, detailed lattice mathematics, and the specific label examples. Know what dominance is, that two labels can be incomparable, and why compartments multiply labels.
-
Role hierarchies, RBAC constraints, and static versus dynamic separation of duties. Know the general principle of separation of duties.
-
The SELinux policy language and its syntax, the size of the policy, the audit log tools, Android’s SELinux history, and AppArmor. Know what type enforcement is, what a domain and a type are, that anything a rule does not allow is denied, and why that confines a compromised program.
Week 7: Memory Vulnerabilities and Defenses
Memory-safety bugs violate an object’s bounds or lifetime. They can expose secrets, corrupt data, or let an attacker redirect a program’s execution.
Hijacking and Injection
Control-flow hijacking redirects a program’s execution, usually by corrupting a return address or a function pointer. Code injection places the attacker’s own instructions in the program and arranges to run them. A stack buffer overflow often does both at once. Hijacking and injection are related actions rather than separate categories.
Running code in another process gives the attacker that process’s access. This is worth doing when the program runs with more privilege than the attacker has, when it runs on a machine the attacker cannot log in to, or when it holds secrets the attacker cannot otherwise read. A setuid program runs with its owner’s privileges, so a setuid-root program can give an attacker root. Making a network service run attacker code is remote code execution.
Behind each of these is a program that made an assumption about its input without checking it. A vulnerability is a weakness that can be exploited; an exploit is the input or program that takes advantage of it. The same bug can crash one build and allow code execution on another, because exploitability depends on the surrounding code, the memory layout, and the defenses in place. Encryption and signatures do not make a vulnerable parser memory-safe: the receiving program still has to parse the input, sometimes before it can verify it.
Memory Layout and the Stack
A process runs in its own virtual address space, divided into regions: the read-only text segment of instructions, the data and BSS segments of global variables, the heap for dynamically allocated memory, and a stack for each thread. The program writes to the stack, the heap, and its globals, so all three can be overflow targets. The text segment is read-only.
The stack supports function calls. In the conventional x86 layout used here, each call gets a stack frame holding its stack-passed arguments, saved registers, local variables, and a return address that the processor jumps to when the function returns. Arguments and locals can instead live in registers, and return-address handling varies by architecture. In this layout a local array sits at a lower address than the return address, and arrays fill toward higher addresses, so a write that runs off the end of a local array moves toward the saved return address.
Stack Buffer Overflows
A buffer overflow writes more data into an array than it can hold, overwriting whatever is next in memory. Writing past an array is undefined behavior in C: the processor checks page permissions, not array bounds, so the write usually succeeds silently and the problem surfaces later. When the overflow reaches the return address, the function’s return can send the processor to an invalid address (a crash, which is a denial of service), to existing code, or to attacker-supplied instructions in the buffer.
An overflow that changes a nearby decision variable rather than the return address is a data-only attack: the program runs its own instructions in order and still does the wrong thing. Whether a given overflow reaches a particular variable or the return address depends on how the compiler laid out the frame.
gets() cannot be used safely because it is never told the size of its buffer. The counted functions take a length, but each counts something different: strncpy writes n bytes, padding short strings with null bytes but possibly leaving longer strings unterminated, strncat limits the bytes appended rather than the destination size, and snprintf counts the terminating null within its limit. They prevent an overflow only when the programmer passes the right size, leaves room for the terminating null, and handles truncation. That size has to come from the destination’s capacity, not from sizeof applied to a pointer, which yields only the pointer’s width. The flaw is a missing or wrong size check, not any one function name.
Shellcode is machine code an attacker places in memory to run after a hijack, written to avoid bytes the input path rejects, such as null bytes, and to fit whatever space the bug makes available. A NOP sled is a run of do-nothing instructions before the shellcode that widens the range of addresses a jump can land on. Neither helps once the target memory is non-executable.
Reading Past a Buffer
An out-of-bounds read accesses memory beyond an object’s bounds. It can disclose secrets or pointers when the program exposes the result, or it can crash the program. A leaked pointer reveals the base of a randomized region and undermines address randomization for that region. Reading past a buffer is a vulnerability on its own, separate from the write overflows above.
Heap Corruption and Lifetime Errors
Heap memory is handed out by an allocator that keeps bookkeeping for each block. Traditional allocators often store that metadata next to the user data; others keep it separately. An overflow out of one block can corrupt a neighboring object or the allocator’s metadata, and in older allocators a forged pointer could produce an arbitrary write to an address of the attacker’s choosing. Overwriting a function pointer or a C++ vtable pointer redirects the next call.
Two errors violate an object’s lifetime rather than its bounds. A use-after-free uses a pointer after the memory is freed; if the attacker arranges for the freed block to be reused for their own data, the stale pointer leads into it. A double-free frees the same block twice, which can corrupt the allocator’s bookkeeping or make a hardened allocator abort. These are temporal errors, as opposed to the spatial errors of overflows, and they are hard to find because the symptom appears far from the cause. Type confusion uses an object as a type it is not, so the code reads its fields with the wrong layout and may treat ordinary data as a pointer.
Integer Errors
Fixed-width integer types have limited ranges. Unsigned overflow wraps in a way C defines; signed overflow is undefined behavior, so a compiler may assume it never happens and drop a check that depends on it. An integer error becomes a memory error when a wrapped size produces an allocation smaller than the code believes, followed by writes that overrun it. Checking that a size calculation fits before performing it, and rejecting a negative length before it is converted to an unsigned size, closes that path. A signed-to-unsigned mismatch lets a negative length pass a signed upper-bound check and then become a huge value when passed as an unsigned size to a copy function. Narrow protocol fields and the 32-bit default int keep these bugs reachable even on 64-bit systems.
An off-by-one error reads or writes one position past a boundary. A write one position past can corrupt a saved value or the low byte of a pointer, and it can be the start of a larger overflow.
Defenses
No single defense closes the hole. Each answers a specific attack and leaves something open, so they are deployed in layers.
The first layer prevents and finds bugs before release:
-
Auditing, static analysis, and compiler warnings catch missing checks and unsafe calls.
-
Sanitizers add runtime checks that report memory and arithmetic errors in the code exercised during testing.
-
Fuzzing feeds malformed input and watches for failures; coverage-guided fuzzers steer input toward new code.
-
Memory-safe languages such as Rust and Go check bounds and manage lifetimes, so an invalid access is caught rather than allowed to corrupt memory; the guarantee stops at unsafe code and native libraries.
When a bug survives, the next layer makes it harder to exploit. Non-executable memory stops the processor from running injected code and forces attackers to reuse existing code. NX is the hardware page permission, DEP is the operating-system protection built on it, and W^X is the policy that no page is writable and executable at the same time. They are related, not three names for one thing.
Non-executable memory does not stop code reuse. A return-to-libc attack redirects execution to an existing library function, and return-oriented programming (ROP) generalizes it by chaining short instruction sequences called gadgets, each ending in a return, with the chain supplying successive gadget addresses and any operands they need. A sufficient set of gadgets can perform arbitrary computation without injecting any code.
Three more defenses raise the cost of a surviving bug:
-
Address space layout randomization (ASLR) places code and data at unpredictable addresses; its strength is its entropy, the amount of randomness in that placement, and a leaked pointer can expose a randomized region’s base and undo it for that region.
-
Stack canaries place a secret value between local buffers and the return address and check it before returning, catching a contiguous overflow but not a heap bug, a leaked canary, or a data-only attack.
-
Hardened allocators check and protect their metadata and delay reuse of freed blocks.
Hardware and compiler checks detect invalid control transfers, pointer tampering, and invalid memory accesses:
-
Control-flow integrity (CFI) checks that an indirect transfer goes to an allowed target, though an attacker-chosen destination that stays within the allowed set still passes, and forward-edge checks do not protect returns.
-
Intel CET combines a hardware shadow stack, which compares a protected copy of each return address with the one on the normal stack, with indirect branch tracking, which checks that indirect calls and jumps land at marked targets.
-
ARM Pointer Authentication (PAC) authenticates the pointers that software signs and checks, so tampering with them is detected; pointers it is not applied to stay unprotected.
-
ARM Memory Tagging Extension (MTE) tags memory and pointers so an enabled check faults on a mismatch, catching many overflows and use-after-free; an access that stays within a granule, a small fixed-size block of memory sharing one tag, or that happens to match the tag can escape it, and a fault may be reported immediately or asynchronously.
Defense in depth combines these independent measures so an exploit must defeat every one on its path, which pushes an attacker toward rarer and less reliable conditions. A data-only attack bypasses most of them, so a memory-safe language is the deeper fix. Rowhammer is a reminder that these defenses assume memory holds what was written to it, which repeated access to dense DRAM can violate.
What You Don’t Need to Study
-
Incident names, CVE numbers, dates, versions, and which incident demonstrated which bug, including the OpenSSL, HP Poly, SMBGhost, FORCEDENTRY, and sudo cases. Know the vulnerability mechanisms described above and recognize them from a description.
-
How to construct shellcode, NOP-sled payloads, ROP chains, or forged allocator metadata. Know what each technique accomplishes and which defenses obstruct it.
-
The names or construction details of heap grooming and heap spraying. Know that reuse of freed memory can place attacker-controlled data behind a stale pointer.
-
Format-string vulnerabilities or the
%nwrite primitive, which are covered in recitation. -
Specific instructions, register names, exact offsets, or a memorized stack diagram. Know how a write past a local buffer can reach nearby data or a saved return address in the layout used here.
-
The exact contract of each counted string function. Know that a size check has to use the destination’s capacity and leave room for the terminating null.
-
The internal field names or algorithms of a heap allocator. Know that it keeps metadata an overflow can corrupt, not its exact layout.
-
The names of particular fuzzers, sanitizers, static analyzers, or compiler options.
-
Entropy bit counts, tag widths, granule sizes, collision probabilities, authentication-code calculations, operating-system versions, overhead percentages, deployment counts, or the specific processors and phones that ship each hardware defense. Know what each defense does and what it leaves open.
-
How Intel CET’s indirect branch tracking works, how ARM Pointer Authentication signs and verifies pointers, and how ARM Memory Tagging Extension assigns and checks tags.
-
How Rowhammer works internally or which memory modules were tested. Know that hardware can be made to flip bits and that the software defenses above it do not fully prevent it.
Week 8: Command Injection and Input Validation Attacks
Injection turns untrusted data into instructions. Unsafe name resolution directs a program to a file, executable, or library the attacker chose. Containment assumes that some program will be compromised anyway and limits what it can see, do, and consume.
Command Injection
An interpreter is any component that parses text into actions: a database engine, a shell, an expression evaluator, a template engine, or a logging library. Injection occurs when input from outside the program reaches an interpreter as part of a command and the interpreter reads some of it as instructions.
Three conditions produce injection:
-
An interpreter.
-
Input the program does not control.
-
A command built by pasting that input into a string.
Removing any one removes the attack, and most defenses remove the third.
The root cause is that the program does not separate data from instructions. Only the interpreter knows which characters are syntax, and it cannot tell which characters the programmer wrote.
Command injection does not need memory bugs. The program’s own interpreter runs the attacker’s command with the program’s privileges, so the defenses against memory corruption do not apply.
SQL Injection
A query built by string concatenation lets a quote in the input close the programmer’s string. The rest of the input is then treated as SQL. A comment marker will discard whatever the programmer wrote after it. This can allow an attacker to bypass a login, read or modify other tables, and, in some databases, reach files or programs on the server.
Two variations extend the basic attack:
-
Blind SQL injection extracts data without displayed results by observing whether a page changes or how long a response takes.
-
Second-order injection stores data safely and uses it unsafely later. Safety is a property of each use of the data, not of the moment it arrived.
The defenses fall into an order of preference:
-
Parameterized queries (prepared statements) separate commands from parameters (which may be user-supplied). They send the query structure to the database before the data, so the data is bound to placeholders after parsing and can never be read as syntax. This is the defense to use whenever input appears in a query.
-
Stored procedures give the same protection unless the procedure builds a query string from its arguments. The procedure is in the database engine and the program calls a function instead of building a query.
-
Allowlists handle the parts of a query that cannot be parameters, such as table and column names: the application checks the user’s choice against a fixed set and uses its own copy.
-
Escaping is a last resort, because the rules differ between databases and character encodings can hide a quote from the escaping function.
-
The Principle of Least Privilege, applied to the database account, limits what an injected query can do.
Syntax, special characters, and available operations differ among databases, so a filter or escaping function written for one might be wrong for another.
Document databases that take queries as structured objects are subject to operator injection: an object containing a query operator is supplied where a string was expected. The defense is to check or convert types before the value reaches the query.
Shell Command Injection
The standard C library functions system(), popen(), and their equivalents in other languages pass a string to sh -c, which gives the shell’s whole language to the attacker. Shell metacharacters separate commands, pipe output, run commands conditionally, substitute a command’s output into the line, redirect input and output, and expand wildcards. Filtering can be error-prone and double quotes still allow command substitution inside the string.
The defenses, in order:
-
Do not invoke a shell. Use a library call, or start the program directly with an argument vector (
execve(), orsubprocess.run()with a list), so that each argument arrives as one string that no shell parses. -
Guard against argument injection, since the program being run still parses its own command line and a value beginning with
-can be read as an option. For some programs. the fixes are--to end options, rejecting option-like values, or prefixing a file name with./. -
If a shell is unavoidable, fix the command structure, validate each value, and quote with a function written for that shell. Quoting is secondary because the rules differ between shells and the shell is not always the only program that parses the arguments.
-
Apply the Principle of Least Privilege, so that an injected command inherits as little authority as possible. This is a design principle rather than a fix for one bug, and it limits the damage from every bug that the other defenses miss.
Interpreters Hidden in Libraries
eval() takes a string and runs it as code in the program’s own language, so input meant to be a number or an expression can contain function calls, imports, or a command that starts a shell.
The difference from shell command injection is the interpreter. The shell parses a command line, while eval() sends the input to the language interpreter itself, with every library the program has loaded. The fix is the same: parse the input as a number or with a data parser, which cannot run anything, rather than evaluating it.
Libraries also contain interpreters that programmers do not know about:
-
A logging library that expands lookup expressions inside logged strings turns every logged field into an attack vector.
-
Data formats that can describe objects to construct run code when loaded.
Validating and Sanitizing Input
Three terms describe how input is handled before it reaches an interpreter:
-
Validation accepts or rejects a value according to a rule.
-
Sanitization transforms a value, by escaping or removing characters, so an interpreter reads it as data.
-
Structural separation supplies data through a channel the interpreter does not parse, such as a query parameter or an argument vector (see parameterized queries, above).
Separation is preferred because it does not depend on predicting the interpreter. Validation is still needed after it, since a bound value can be the wrong value.
An allowlist defines what is permitted and rejects everything else. A denylist names what is forbidden and permits everything else, which makes it a prediction about what the attacker will write, and an interpreter can construct the forbidden string after the filter has run.
Canonicalization converts every equivalent representation of an input into one chosen form. The rule is to decode and canonicalize first, then validate, and never decode again, since a check on one representation does not cover the one the interpreter will see.
Four more rules follow from the same idea:
-
Sanitization has to match the context of use, because SQL, a shell, HTML, and a file path have different special characters.
-
Validating at a program’s entry points and trusting everything inside fails for the same reason second-order injection does.
-
Checks in the browser are bypassed by sending the request directly.
-
Length limits do not stop injection.
Path Traversal and Path Equivalence
Path traversal uses .. in a supplied path to reach files outside the intended directory. The component can appear anywhere in the path and can be encoded so a check made before decoding does not see it. Searching for the characters is not foolproof, since legitimate names contain dots and a .. that stays inside the tree is harmless. The property to enforce is where resolution ends.
Path equivalence is the fact that many strings may refer to the same file: through . and redundant separators, case-insensitive filesystems, Unicode normalization, symbolic links, and hard links. A symbolic link makes a path that passes every textual check resolve somewhere else.
The defenses, from weakest to strongest:
-
Canonicalize with
realpath()and check the prefix, including its trailing separator. -
Have the kernel enforce the restriction during resolution, so
..and links cannot leave a directory. -
Map an identifier to a file instead of accepting a path.
-
Run with permissions that limit what a traversal reaches.
Time-of-Check to Time-of-Use
A TOCTOU race condition occurs when a program checks a name and then uses it, and the binding between the name and the object changes in between, usually through a symbolic link or directory entry the attacker controls.
Two cases are the classic ones:
-
The
access()thenopen()sequence in a setuid program. -
Temporary files: generating a name and then creating the file lets an attacker create the file, or a link under that name, first.
The defenses share one rule, which is to operate on the object rather than on the name:
-
Check and create in one step, with
mkstemp()oropen()withO_CREAT | O_EXCL. -
Open once and perform every later operation through the descriptor, which refers to the file regardless of its name.
-
Attempt the operation and handle failure rather than checking first. A setuid program switches its effective user ID to the caller’s so the kernel makes the check during the open.
-
Refuse to follow symbolic links where none is expected.
Privilege separation removes the race condition by having a process with the user’s own permissions open the file and pass the descriptor to the privileged part.
Inherited Environment, Search Paths, and Library Injection
A program inherits environment variables, open descriptors, a working directory, and a file-creation mask from whatever started it, which may be the attacker. A search path such as PATH decides which program runs when a script names one, so a counterfeit placed earlier in the path runs first. The fixes are absolute paths and an environment the privileged program constructs itself.
Library injection makes a program load library code the attacker controls, and that code runs with the program’s authority. It takes two forms:
-
On Linux,
LD_LIBRARY_PATHchanges where the loader searches for libraries andLD_PRELOADnames libraries to load before all others. The loader ignores these variables for setuid programs, but ordinary programs remain exposed. -
Windows DLL loading is the same class of problem: a program that loads a library by name searches several directories, and a library placed where the search looks first is the one that runs.
In both cases, a library or program loaded by name from a location the attacker can influence is code the attacker chose.
Function interposition replaces or wraps one function that a program expected to get from a shared library. The loader resolves each function name to the first definition it finds, so a preloaded library that defines rand() or the clock function replaces it without modifying the program, and a wrapper can record a function’s arguments and then call the original. Interposition has legitimate uses in debugging and testing. In an attacker’s hands it captures data, changes arguments, or makes an authentication check return success.
Code That Arrives by Package Name
A package name resolves to code that runs with the installer’s privileges, and installing a package can run its install scripts before the package is ever imported.
Three attacks exploit the choice of which package a name resolves to:
-
Dependency confusion exploits a package manager that treats public and private repositories as one pool and picks the highest version, so a public package with an internal name wins.
-
Typosquatting registers a name that is a plausible misspelling of a popular package.
-
Nonexistent names suggested by AI assistants can be registered by an attacker before a developer installs them.
The defenses bind a name to a repository, run builds with few credentials and limited network access, and pin and review dependencies.
Containment
Operating system access control identifies users, not programs, so a compromised process holds every right of its user and the permission model cannot express a rule about one program.
The Principle of Least Privilege is a design principle: a program should run with the fewest permissions its job needs. Three ways to approach it fall short:
-
A dedicated account for the service still reaches every world-readable file.
-
Dropping privileges depends on the program’s own code.
-
Privilege separation depends on the program’s own code.
Containment enforces limits from outside the process. The limits hold after the process is taken over, apply to programs never written to restrict themselves, and allow code that was never trusted, such as apps, web scripts, and dependencies, to run at all. The environment that confines a program is a sandbox.
A confined process has to be kept from more than files:
-
Other processes.
-
The kernel’s system call interface.
-
The machine’s network identity.
-
Shared CPU, memory, disk, and process slots.
The kernel or hypervisor that enforces a boundary is part of the trusted computing base, since the boundary holds only if that layer is correct.
Every mechanism is judged by five questions:
-
What does it restrict?
-
Who enforces it?
-
What interface can the confined code still reach?
-
What overhead does it add?
-
How does it fail?
The second question places each mechanism at one of four levels: a user-level monitor process, a language runtime, the kernel, or the hypervisor. Each level trusts everything beneath it, and going down the list the enforcement gets stronger and the interface narrower.
chroot and Jails
chroot changes where path resolution starts for a process and its children. It restricts only names:
-
System calls, the network, and resource use are untouched.
-
A root process escapes.
-
A descriptor opened before the call stays open.
Jails added a private process view, a network address, and a restricted root.
The lessons are that hiding names is not the same as restricting operations, and that root inside a boundary has to be a weaker root than root outside.
System Call Filtering
The system call interface is the natural point for complete mediation, since a process acts on the world only through it.
Early sandboxes placed a monitor in a separate process, which had three problems:
-
The monitor had to replicate kernel state.
-
A second thread could change a file name between the monitor’s check and the kernel’s use.
-
Every call added two context switches.
seccomp-BPF moves the filter into the kernel. A process installs a filter that the kernel runs on every system call. The filter sees the call number and its integer arguments and allows the call, fails it, or kills the process.
The filter never reads process memory, so it cannot see a file name and cannot decide which files a process may open. Its purpose is to remove the kernel code behind unused calls from a process’s reach.
The overhead per call is small. The real work is a correct policy, which breaks the program if too strict and adds little if too loose.
AppArmor
AppArmor restricts a program by pathname: a profile attached to the program lists the files it may read, write, or execute, along with its network access.
The check runs inside the kernel after the path has been resolved, on the object actually being opened, so the race condition that defeats an external monitor does not apply.
The two kernel mechanisms divide the work: seccomp-BPF limits which system calls a process can make and cannot see names, while AppArmor allows the call and limits which objects it reaches.
Path-based rules are easier to write than label-based ones such as SELinux, but a file with two names can fall under two rules.
Language Runtime Sandboxes
A language runtime such as the Java virtual machine, a JavaScript engine, or WebAssembly confines a program from inside. It verifies code before running it, checks every index and type, and manages memory itself, so the program cannot form a pointer, read outside its memory, or make a system call. The program reaches the outside only through the library the runtime exposes.
The sandbox is only as correct as the runtime, which is a large program in C or C++. A memory bug in the runtime gives the program native code in the host process. A language sandbox needs an operating system boundary beneath it, which is how browsers are built.
Application Sandboxes and Permission Models
Phones make the application, rather than the user, the principal:
-
Android gives each app its own user ID and SELinux domain.
-
iOS gives each app its own container directory under a common sandbox profile, with extra abilities granted by signed entitlements.
Shared resources such as the camera and contacts are reached only through system services that check permissions before acting.
Permission models moved from install-time, all-or-nothing grants to requests at first use that the user can refuse or revoke, with finer grants such as approximate location and one-time access.
The model has three limits:
-
The user’s consent is the policy.
-
A permission covers a resource and not a purpose.
-
An app holding a permission can expose a component that lets another app use it, making the first app a confused deputy.
An escape needs a bug in the kernel or in a system service the app can reach.
Namespaces, Control Groups, and Capabilities
Three Linux kernel features confine groups of processes, and each controls a different thing:
-
A namespace gives a group of processes their own view of one part of the kernel’s state, such as process IDs, mounts, the network stack, or user IDs. Namespaces hide and partition; they change what a privileged call affects and do not forbid it.
-
A control group limits how much CPU, memory, and I/O a group of processes may consume and how many processes it may create, which ends the fork bomb as an attack.
-
Capabilities divide root’s authority into about forty separately grantable privileges, so a process can hold one without being root and root can run with most of them removed. One administrative capability covers so much that holding it is close to being root. These are unrelated to the capability lists of access control.
A user namespace maps root inside to an unprivileged user outside. That lets ordinary users create other namespaces, and it also lets them reach kernel code that once required root, a steady source of privilege-escalation bugs.
Containers
A container is a process started in new namespaces, placed in control groups, given reduced capabilities, run under a seccomp filter and an AppArmor or SELinux profile, with a root filesystem built from an image. The container runtime does the setup; the kernel itself does not know what a container is. Containers were built for packaging and deployment, and isolation was a side effect.
The benefits, at close to zero overhead:
-
Minimal images with fewer programs to attack.
-
Restrictive defaults.
-
Separation between services.
-
Fixes that ship by rebuilding an image.
A container escape gives code inside access to resources outside. The routes to one follow from the shared kernel:
-
Any kernel bug reachable from inside.
-
A flaw in the runtime, which runs on the host as root.
-
Misconfiguration, which is the most common failure: privileged containers, host mounts, the control socket, or root without a user namespace.
-
Public images, which are a supply chain.
Containers separate cooperating services well and are not, on their own, a boundary for hostile code. Rootless runtimes and running containers inside virtual machines reduce what an escape reaches.
Virtual Machines
A hypervisor, or virtual machine monitor, presents a complete computer to each guest operating system. The confined code is an entire operating system, and the interface it reaches is virtual hardware.
Three parts of the machine are handled differently:
-
Processor. With hardware support for virtualization, the processor runs guest instructions directly and hands control to the hypervisor only for operations that would affect the real machine. The processor itself is not emulated.
-
Memory. A second level of address translation, controlled by the hypervisor, limits a guest to its assigned memory.
-
Devices. A device is emulated by software in the hypervisor, supplied through a simplified interface written for virtual machines, or passed through to one guest.
A Type 1 hypervisor runs on the hardware. A Type 2 hypervisor runs under a host operating system, which then joins the trusted computing base.
Each guest has its own kernel, so a kernel bug in one guest stays in that guest, and the interface exposed to a guest is far narrower than a system call interface. A guest application’s system calls enter the guest kernel, not the hypervisor, which is the central difference from a container.
The overheads are transitions into the hypervisor, I/O through emulation, memory for a kernel per guest, boot time in seconds, and another operating system to patch.
The risks:
-
Escape through device emulation code.
-
Compromise of the hypervisor or its management services.
-
Leakage through shared hardware.
-
Sprawl.
Containers inside a virtual machine share that guest’s kernel, and an escape from the container still leaves the hypervisor boundary around the guest. Cloud providers separate customers this way.
Virtualization-Based Security
Virtualization-based security uses a hypervisor that starts before the operating system to protect parts of that operating system from its own kernel. A secure environment whose memory the normal kernel cannot touch decides which kernel code pages may execute and holds the credentials used to authenticate to other machines.
The limits:
-
The hypervisor and secure kernel join the trusted computing base.
-
A compromised kernel still reads every user file.
-
The configuration that turns it on is itself a target.
-
Signed older versions of the protected components can be reinstalled.
-
Code running before the hypervisor defeats it.
-
Compatibility problems and performance overhead lead people to turn it off.
Comparing the Mechanisms
The strength of a boundary depends on three things:
-
The size of the interface the confined code can reach.
-
The correctness of the layer that enforces it.
-
The correctness of its configuration.
A virtual machine has the smallest interface and strongest enforcement and adds the most overhead. A system call filter adds almost none and narrows the interface without closing it. Real systems layer them.
Every mechanism assumes the layer beneath it is honest: a filter trusts the kernel, a container trusts the kernel it shares, a virtual machine trusts the hypervisor, and the hypervisor trusts the firmware and the processor.
What You Don’t Need to Study
These exclusions apply to the command injection and containment material. Injection into web pages, prompt injection and AI security, and covert channels are not part of this material. We will cover these topics later.
The following are not required for the exam:
-
Incident names, dates, CVE numbers, versions, and victim counts, including the Zimbra, MOVEit, FortiSIEM, PHPMailer, BatBadBut, Log4Shell, PAN-OS, IIS, Nimda, Tomcat, NLWeb, Looney Tunables, Symbiote, Dirty Pipe,
runc, VENOM, ESXiArgs, BlackLotus, and Windows Downdate cases. Know the mechanisms described above and recognize them from a description. -
Which function in which language invokes a shell. Know the difference between passing a string to a shell and starting a program with an argument vector, and that
system(),popen(), andshell=Trueare the shell-invoking forms. -
The exact quoting, comment, and multi-statement rules of individual databases, or document-database operator syntax. Know that the rules differ and that a filter written for one interpreter is wrong for another.
-
The bit layout of overlong UTF-8 encodings, Windows path normalization, the Windows DLL search order, the parameters of
CreateProcess(), Windows command-line parsing, or the code in the interposition and descriptor-repair examples. Know the decode-before-validating rule, that a library or program loaded by name from a location the attacker controls is the attacker’s code, and what preloading a library can do. -
Package manager options and script names. Know what dependency confusion is and that installation runs code.
-
The history of Janus and Systrace, seccomp’s original mode, how filters are inherited, the BPF language, seccomp return codes, the supervisor notification feature, AppArmor’s profile syntax and modes, or the Linux Security Module framework.
-
How the browser is sandboxed. Know that it is privilege separation plus the mechanisms above, applied to a program that runs hostile code.
-
Android and iOS version numbers and the years particular permission changes shipped, or how entitlements are signed. Know the install-time to runtime progression and the limits of the model.
-
The complete list of namespace types, cgroup controller names, individual capability names, or the kernel versions that introduced each feature. Know what each of the three mechanisms controls and that user namespaces widen the kernel attack surface.
-
Container image layers, registries, port mapping, orchestration systems, or Docker commands.
-
Trap and emulate, the conditions a processor has to meet for it, binary translation, paravirtualization of the processor, VM exits, shadow page tables, IOMMUs, or the history of IBM, VMware, and Xen. Know what a hypervisor does, that guest code runs directly on the processor with hardware support, and that devices are emulated or passed through.
-
Windows’ names for VBS features or the two-environment architecture in detail. Know what VBS protects and its limits.