I once wrote the following extension methods, which basically have the effect of either OrderBy or ThenBy, depending on whether the source is already ordered:
public static class Extensions {
    public static IOrderedEnumerable<TSource> OrderByPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer, bool descending) {
        var orderedSource = source as IOrderedEnumerable<TSource>;
        if (orderedSource != null) {
            return orderedSource.CreateOrderedEnumerable(keySelector, comparer, descending);
        }
        if (descending) {
            return source.OrderByDescending(keySelector, comparer);
        }
        return source.OrderBy(keySelector, comparer);
    }
    public static IOrderedEnumerable<TSource> OrderByPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
        return source.OrderByPreserve(keySelector, null, false);
    }
    public static IOrderedEnumerable<TSource> OrderByPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer) {
        return source.OrderByPreserve(keySelector, comparer, false);
    }
    public static IOrderedEnumerable<TSource> OrderByDescendingPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
        return source.OrderByPreserve(keySelector, null, true);
    }
    public static IOrderedEnumerable<TSource> OrderByDescendingPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer) {
        return source.OrderByPreserve(keySelector, comparer, true);
    }
}
The interface is the same as OrderBy / OrderByDescending (alternatively you can pass descending as a boolean). You can write:
list.OrderByPreserve(x => x.A).OrderByPreserve(x => x.B)
which has the same effect as:
list.OrderBy(x => x.A).ThenBy(x => x.B)
Thus you could easily use keyboardP's solution with an arbitrary list of property names:
public static IEnumerable<TSource> OrderByProperties<TSource>(IEnumerable<TSource> source, IEnumerable<string> propertyNames) {
    IEnumerable<TSource> result = source;
    foreach (var propertyName in propertyNames) {
        var localPropertyName = propertyName;
        result = result.OrderByPreserve(x => x.GetType().GetProperty(localPropertyName).GetValue(x, null));
    }
    return result;
}
(the localPropertyName variable is used here because the iteration variable will have changed by the time the query is executed -- see this question for details)
A possible issue with this is that the reflection operations will be executed for each item. It may be better to build a LINQ expression for each property beforehand so they can be called efficiently (this code requires the System.Linq.Expressions namespace):
public static IEnumerable<TSource> OrderByProperties<TSource>(IEnumerable<TSource> source, IEnumerable<string> propertyNames) {
    IEnumerable<TSource> result = source;
    var sourceType = typeof(TSource);
    foreach (var propertyName in propertyNames) {
        var parameterExpression = Expression.Parameter(sourceType, "x");
        var propertyExpression = Expression.Property(parameterExpression, propertyName);
        var castExpression = Expression.Convert(propertyExpression, typeof(object));
        var lambdaExpression = Expression.Lambda<Func<TSource, object>>(castExpression, new[] { parameterExpression });
        var keySelector = lambdaExpression.Compile();
        result = result.OrderByPreserve(keySelector);
    }
    return result;
}
Essentially what those Expression lines are doing is building the expression x => (object)x.A (where "A" is the current property name), which is then used as the ordering key selector.
Example usage would be:
var propertyNames = new List<string>() { "Title", "Artist" };
var sortedList = OrderByProperties(list, propertyNames).ToList();
You just need to add the ascending / descending logic.