Overloading operator== in C# is syntactic sugar for calling a static function. The overload resolution, like all overload resolution, happens based on the static type of the object, not the dynamic type. Let's look at Object.ReferenceEquals again:
public static bool ReferenceEquals (Object objA, Object objB) {
    return objA == objB;
}
Here, the static type of objA and objB is Object.  The dynamic type can be anything; a string, some other user defined type, whatever; that does not matter. The determination of which operator== is called is determined statically when this function is compiled, so you always get the default, non-overloaded, built in language-supplied one. .NET could just have not had a ReferenceEquals and let users do ((object)a) == ((object)b), but having a specific named function to say what's going on improves clarity.
Object.Equals, on the other hand, is just a virtual function. As a result, which Equals is chosen is based on the dynamic type of the object to the left of the .Equals(, like any other virtual function call.