Our team at Include Security is often asked to examine applications coded in languages that are usually considered “unsafe”, such as C and C++, due to their lack of memory safety functionality. Critical aspects of reviewing such code include identifying where bounds-checking, input validation, and pointer handling/dereferencing are happening and verifying they’re not exploitable. These types of vulnerabilities are often disregarded by developers using memory safe languages.
In 2023 the NSA published a paper on Software Memory Safety that included Delphi/Object Pascal in a list of “memory safe” languages. The paper caveats the statement by saying:
Most memory safe languages recognize that software sometimes needs to perform an unsafe memory management function to accomplish certain tasks. As a result, classes or functions are available that are recognized as non-memory safe and allow the programmer to perform a potentially unsafe memory management task.
With that in mind, our team wanted to demonstrate how memory management could go wrong in Delphi despite being included on the aforementioned list and provide readers with a few tips on how to avoid introducing memory-related vulnerabilities in their Delphi code.
In this blog post, we take the first steps of investigating memory corruption in Delphi by constructing several simple proof-of-concept code examples that demonstrate memory corruption vulnerability patterns.
Delphi is the name of a set of software development tools, as well as a dialect of the Object Pascal programming language. Delphi was originally developed by Borland, and is now developed by Embarcadero Technologies https://en.wikipedia.org/wiki/Delphi_(software). We chose to target Delphi as it is still used by some important companies, yes even top 10 Internet companies! These days Delphi and Object Pascal may be less popular than other languages, but they are still used in popular software as seen in the very extensive awesome-pascal repository list and by many companies as shown by Embarcadero in a variety of case-studies.
There’s also a free and open source IDE named Lazarus, which uses the Free Pascal compiler, and aims to be Delphi compatible.
We’d like to take a look at how memory corruption vulnerabilities could be introduced in languages other than C/C++, where such vulnerabilities are often discussed. This blog post takes the first steps in investigating what memory corruption vulnerabilities might look like in Delphi code by writing several proof-of-concept demonstrations of the types of memory corruption that often lead to vulnerabilities.
Delphi has been claimed to be a memory-safe language in some contexts but we consider it similar to C++ in regards to memory safety. Object Pascal and Delphi support arbitrary untyped pointers and unsafe pointer arithmetic, which can intentionally or not lead to memory corruption. But rather than simply show that dereferencing an arbitrary pointer value could cause a crash, we wanted to demonstrate a couple of memory corruption patterns that commonly lead to vulnerabilities in software. The following examples were compiled in the RAD Studio Delphi IDE with all of the default compiler options.
Let’s start by trying to write a simple stack-based buffer overflow in Delphi. Note that in these following examples, the code was compiled in Win32 mode, which was the default, though the general concepts apply to other platforms as well. Here’s our first attempt at a stack-based buffer overflow:
procedure Overflow1; var ar: Array[0..9] of Byte; // Fixed-length array on the stack i: Integer; begin for i := 0 to 999 do begin ar[i] := $41; // Try to overflow the array end; end; // If overflow happens, returns to $41414141
We define an array of 10 bytes, and then try to write 1000 values to the array. When we compile and run this code, it raises an exception but doesn’t crash:
Why didn’t it crash? Since the array ar is defined with a static length, the compiler can insert code that does bounds-checking at runtime whenever the array is indexed. Let’s take a look at the compiled procedure (disassembled in Ghidra):
The code tests that the index is less than or equal to 9 and if it’s not, it calls a function that raises the exception (CMP EAX, 0x9, JBE, CALL).
But wait, this was the application compiled in debug mode. What happens if we compile the application in release mode?
Ah! In release mode, the compiler didn’t include the array bounds check, and the code overwrote the return address on the stack. Shown above is the Delphi debugger after returning to $41414141. Here’s the release build code, again disassembled in Ghidra:
No bounds check in sight. Why not? The “Range checking” (which is what caught the overflow in debug mode) and “Overflow checking” (which checks for integer overflows) compiler settings are disabled by default in release mode:
So here is a lesson: consider turning on all the “Runtime errors” flags in release mode when hardening a Delphi build. Of course, the added checks are likely disabled by default to avoid performance impacts.
But how easy is it to overflow a buffer with Range checking enabled? Well, the official Delphi documentation warns about memory corruption in a few of its system library routines:
System.FillChar
System.BlockRead
System.BlockWrite
System.Move
Note that these are just the system library routines that clearly warn about memory corruption in their documentation; it’s not a comprehensive list of dangerous routines in Delphi.
Here’s an example that uses Move to cause a stack buffer overflow:
procedure Overflow2; var ar1: Array[0..9] of Byte; // Smaller array on the stack ar2: Array[0..999] of Byte; // Larger array on the stack i: Integer; begin for i := 0 to 999 do begin ar2[i] := $41; // Fill ar2 with $41 end; Move(ar2, ar1, SizeOf(ar2)); // Oops should have been SizeOf(ar1) end; // Returns to $41414141
This time, we create two stack buffers, fill the bigger one with $41, then use Move to copy the bigger array into the smaller array. When we run this code, even the debug build with Range checking enabled overflows the stack buffer and returns to $41414141:
Let’s take a look at a couple examples of how heap-based vulnerabilities might be introduced. In these examples it was easy to cause the default heap implementation to allocate the same memory after a previous allocation had been freed by specifying allocations of the same size.
In this first example, we allocate a string on the heap, assign a value to it, free the string, then allocate another string which shares the same memory as the previous string. This demonstrates how reading uninitialized memory might lead to an information disclosure vulnerability. In this case, SetLength was used to set the length of a string without initializing memory.
The default string type in Delphi is UnicodeString; UnicodeStrings are automatically allocated on the heap.
In this example, Heap1c first calls Heap1a, which dynamically constructs a string, causing memory to for it to be allocated on the heap. The memory is freed as the string goes out of scope. Next, Heap1c calls Heap1b. Heap1b calls SetLength, which allocates memory for a string without initializing the heap memory, then reads the contents of the string revealing the string constructed in Heap1a.
procedure Heap1a; var s: String; // Unicode string variable on the stack, contents on the heap begin s := 'Super Secret'; // Assign a value to the string s := s + ' String!'; // Appending to the string re-allocates heap memory end; // Memory for s is freed as it goes out of scope procedure Heap1b; var s: String; // Unicode string variable on the stack, contents on the heap begin SetLength(s, 20); // Trigger re-allocation, does not initialize memory ShowMessage(s); // Shows 'Super Secret String!' end; procedure Heap1c; begin Heap1a; Heap1b; end;
When the above code is run, the ShowMessage call produces the “Super Secret String!” in a dialog:
In this second heap example, memory is allocated for an object on the heap, then freed, then another object is allocated using the same heap memory. This is similar to how the same memory region was re-used by the strings in the previous example. In this case, the freed object is written to, modifying the second object. This represents a use-after-free vulnerability, where an attacker might be able to modify an object, either to obtain code execution or otherwise modify control flow.
TmyFirstClass and TmySecondClass are two classes that contain a similar amount of data. In the procedure Heap2, obj1, an instance of TMyFirstClass is created then immediately freed. Next, obj2, an instance of TmySecondClass is created and read. Then, the freed obj1 is written to. Finally, obj2 is read again showing that it was modified by the access to obj1.
type […] TMyFirstClass = class(TObject) public ar: Array[0..7] of Byte; end; TMySecondClass = class(TObject) public n1: Integer; n2: Integer; end; […] implementation [...] procedure Heap2; var obj1: TMyFirstClass; obj2: TMySecondClass; begin obj1 := TMyFirstClass.Create; // Create obj1 obj1.Free; // Free obj1 obj2 := TMySecondClass.Create; // Create obj2 (occupies the same memory obj1 did) ShowMessage('obj2.n2: ' + IntToStr(obj2.n2)); // Shows 0 - uninitialized memory obj1.ar[4] := $41; // Write to obj1 after it has been freed, actually modifying obj2 ShowMessage('obj2.n2: ' + IntToStr(obj2.n2)); // Shows 65 - the value has been overwritten obj2.Free; // Free obj2 end;
In the first screenshot, the dialog shows that obj2.n2 was equal to 0:
Then, after obj1 was written to, the second dialog shows that the value of obj2.n2 was set to 65 (the decimal representation of $41):
These examples only scratch the surface of how memory corruption vulnerabilities might happen in Delphi code; future research could investigate more potentially dangerous library routines in official or common third-party libraries, how FreePascal behaves compared to Delphi, especially on different platforms (Win64, Linux, etc.), or how different heap implementations work to explore exploitability of heap memory corruption.
Based on what we covered in this blog post, here are some suggestions for Delphi developers:
Hopefully these examples begin to demystify Delphi and Object Pascal, and also demonstrate that though memory corruption concepts are most commonly discussed in the context of C/C++, familiar vulnerabilities can be found in other languages as well.
unit Unit1; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type TForm1 = class(TForm) Button1: TButton; Button2: TButton; Button3: TButton; Button4: TButton; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); procedure Button3Click(Sender: TObject); procedure Button4Click(Sender: TObject); private { Private declarations } public { Public declarations } end; TMyFirstClass = class(TObject) public ar: Array[0..7] of Byte; end; TMySecondClass = class(TObject) public n1: Integer; n2: Integer; end; var Form1: TForm1; implementation {$R *.dfm} procedure Overflow1; var ar: Array[0..9] of Byte; // Fixed-length array on the stack i: Integer; begin for i := 0 to 999 do begin ar[i] := $41; // Raises an exception if dynamic bounds-checking is enabled end; end; // If overflow happens, returns to $41414141 procedure Overflow2; var ar1: Array[0..9] of Byte; // Smaller array on the stack ar2: Array[0..999] of Byte; // Larger array on the stack i: Integer; begin for i := 0 to 999 do begin ar2[i] := $41; // Fill ar2 with $41 end; Move(ar2, ar1, SizeOf(ar2)); // Oops should have been SizeOf(ar1) end; // Returns to $41414141 procedure Heap1a; var s: String; // Unicode string variable on the stack, contents on the heap begin s := 'Super Secret'; // Assign a value to the string s := s + ' String!'; // Appending to the string re-allocates heap memory end; // Memory for s is freed as it goes out of scope procedure Heap1b; var s: String; // Unicode string variable on the stack, contents on the heap begin SetLength(s, 20); // Trigger re-allocation, does not initialize memory ShowMessage(s); // Shows 'Super Secret String!' end; procedure Heap1c; begin Heap1a; Heap1b; end; procedure Heap2; var obj1: TMyFirstClass; obj2: TMySecondClass; begin obj1 := TMyFirstClass.Create; // Create obj1 obj1.Free; // Free obj1 obj2 := TMySecondClass.Create; // Create obj2 (occupies the same memory obj1 did) ShowMessage('obj2.n2: ' + IntToStr(obj2.n2)); // Shows 0 - uninitialized memory obj1.ar[4] := $41; // Write to obj1 after it has been freed, actually modifying obj2 ShowMessage('obj2.n2: ' + IntToStr(obj2.n2)); // Shows 65 - the value has been overwritten obj2.Free; // Free obj2 end; procedure TForm1.Button1Click(Sender: TObject); begin Overflow1; end; procedure TForm1.Button2Click(Sender: TObject); begin Overflow2; end; procedure TForm1.Button3Click(Sender: TObject); begin Heap1c; end; procedure TForm1.Button4Click(Sender: TObject); begin Heap2; end; end.