Memory Corruption in Delphi
文章探讨了Delphi/Object Pascal语言的内存安全问题,尽管被归类为“内存安全”语言,但通过构造栈溢出和堆使用后释放等示例代码,展示了潜在的内存腐败漏洞,并提供了开发建议以避免相关风险。 2025-3-13 18:55:16 Author: blog.includesecurity.com(查看原文) 阅读量:13 收藏

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.

What Is Delphi?

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.

Memory Corruption and Memory Safety

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.

Stack-Based Buffer Overflow

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:

The Heap and Use After Free

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):

Conclusion

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:

  • Avoid dangerous routines such as FillChar, BlockRead, BlockWrite, and Move; whenever they must be used, make sure to carefully check sizes using routines such as SizeOf.
  • Consider enabling the “Runtime errors” flags in the compiler options.
  • Be cautious when dynamically creating and freeing objects, paying attention to potentially unexpected code paths that could result in using objects after they have been freed.
  • Make sure to initialize newly allocated memory before it is read.
  • In general, don’t assume that Delphi as a language is inherently safer than other languages such as C/C++.

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.

Appendix: Listing of Unit1.pas


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.

文章来源: https://blog.includesecurity.com/2025/03/memory-corruption-in-delphi/
如有侵权请联系:admin#unsafe.sh