They took a shortcut on the PARAMDESC.lpVarValue because the pointer type is hard to declare in managed code. It is actually a pointer to PARAMDESCEX, declared like this in oaidl.idl:
typedef struct tagPARAMDESCEX {
ULONG cBytes; /* size of this structure */
VARIANTARG varDefaultValue; /* default value of this parameter */
} PARAMDESCEX, * LPPARAMDESCEX;
So, yes, using Marshal.GetObjectForNativeVariant() is a good way to get the varDefaultValue field. You however have to add to skip the cBytes field. That takes adding 4 to skip the ULONG and an extra 4 to skip the padding between the fields. So use something like:
PARAMDESC pd = ...;
object defValue = null;
if ((pd.wParamFlags & 0x20) != 0) {
IntPtr defptr = new IntPtr((long)pd.lpVarValue + 8);
defValue = Marshal.GetObjectForNativeVariant(defptr);
}