破解某游戏修改器时的意外收获插图

本来是想简单破解一下某国产单机游戏的修改器,没想到还有意外收获。


今天没事干,翻出来一款国产单机游戏,重复刷刷刷太无聊了,于是网上下了个修改器,没想到需要购买激活码才能用。并不是很贵,只要十块几毛钱,在发卡的网站上直接购买即可,推测是支付完就可以获得一个激活码,不需要人工介入。虽然不贵,但是作为一个逆向选手,难得遇到一个锻炼自己的机会,直接开搞喽。

简单一看,是个c#写的修改器,拖进dnspy里面,感觉像是被混淆了:

Decrypt是这样的:

private static GCHandle Decrypt(uint[] A_0, uint A_1)
{
    uint[] array = new uint[16];
    uint[] array2 = new uint[16];
    ulong num = (ulong)A_1;
    for (int i = 0; i < 16; i++)
    {
        num = num * num % 339722377UL;
        array2[i] = (uint)num;
        array[i] = (uint)(num * num % 1145919227UL);
    }
    array[0] = (array[0] ^ array2[0]) * 2849015165u;
    array[1] = (array[1] ^ array2[1] ^ 2786556537u);
    array[2] = array[2] * array2[2] * 2849015165u;
    array[3] = array[3] * array2[3] + 1807995571u;
    array[4] = (array[4] ^ array2[4]) + 1807995571u;
    array[5] = (array[5] ^ array2[5]) + 1807995571u;
    array[6] = (array[6] * array2[6] ^ 2786556537u);
    array[7] = (array[7] + array2[7]) * 2849015165u;
    array[8] = array[8] * array2[8] * 2849015165u;
    array[9] = (array[9] + array2[9] ^ 2786556537u);
    array[10] = array[10] + array2[10] + 1807995571u;
    array[11] = array[11] * array2[11] * 2849015165u;
    array[12] = (array[12] ^ array2[12]) * 2849015165u;
    array[13] = (array[13] + array2[13] ^ 2786556537u);
    array[14] = (array[14] ^ array2[14] ^ 2786556537u);
    array[15] = (array[15] * array2[15] ^ 2786556537u);
    Array.Clear(array2, 0, 16);
    byte[] array3 = new byte[A_0.Length << 2];
    uint num2 = 0u;
    for (int j = 0; j < A_0.Length; j++)
    {
        uint num3 = A_0[j] ^ array[j & 15];
        array[j & 15] = (array[j & 15] ^ num3) + 1037772825u;
        array3[(int)((UIntPtr)num2)] = (byte)num3;
        array3[(int)((UIntPtr)(num2 + 1u))] = (byte)(num3 >> 8);
        array3[(int)((UIntPtr)(num2 + 2u))] = (byte)(num3 >> 16);
        array3[(int)((UIntPtr)(num2 + 3u))] = (byte)(num3 >> 24);
        num2 += 4u;
    }
    Array.Clear(array, 0, 16);
    byte[] array4 = <Module>.Decompress(array3);
    Array.Clear(array3, 0, array3.Length);
    GCHandle result = GCHandle.Alloc(array4, GCHandleType.Pinned);
    ulong num4 = num % 9067703UL;
    for (int k = 0; k < array4.Length; k++)
    {
        byte[] array5 = array4;
        int num5 = k;
        array5[num5] ^= (byte)num;
        if ((k & 255) == 0)
        {
            num = num * num % 9067703UL;
        }
    }
    return result;
}

Decompress是这样的:

// <Module>
// Token: 0x06000004 RID: 4 RVA: 0x00016364 File Offset: 0x00014564
internal static byte[] Decompress(byte[] A_0)
{
    MemoryStream memoryStream = new MemoryStream(A_0);
    <Module>.LzmaDecoder lzmaDecoder = new <Module>.LzmaDecoder();
    byte[] array = new byte[5];
    memoryStream.Read(array, 0, 5);
    lzmaDecoder.SetDecoderProperties(array);
    long num = 0L;
    for (int i = 0; i < 8; i++)
    {
        int num2 = memoryStream.ReadByte();
        num |= (long)((long)((ulong)((byte)num2)) << 8 * i);
    }
    byte[] array2 = new byte[(int)num];
    MemoryStream memoryStream2 = new MemoryStream(array2, true);
    long num3 = memoryStream.Length - 13L;
    lzmaDecoder.Code(memoryStream, memoryStream2, num3, num);
    return array2;
}

<Module>.Main是这样的:

private static int Main(string[] A_0)
{
    uint[] array = new uint[]
    {
        2433042623u,
        3134412526u,
        156519932u,
        2628545700u,
        // 此后省略,共20244个uint
    };
    Assembly executingAssembly = Assembly.GetExecutingAssembly();
    Module manifestModule = executingAssembly.ManifestModule;
    GCHandle gchandle = <Module>.Decrypt(array, 4159999126u);
    byte[] array2 = (byte[])gchandle.Target;
    Module module = executingAssembly.LoadModule("koi", array2);
    Array.Clear(array2, 0, array2.Length);
    gchandle.Free();
    Array.Clear(array, 0, array.Length);
    <Module>.key = manifestModule.ResolveSignature(285212673);
    AppDomain.CurrentDomain.AssemblyResolve += <Module>.Resolve;
    module.GetTypes();
    MethodBase methodBase = module.ResolveMethod((int)<Module>.key[0] | (int)<Module>.key[1] << 8 | (int)<Module>.key[2] << 16 | (int)<Module>.key[3] << 24);
    object[] array3 = new object[methodBase.GetParameters().Length];
    if (array3.Length != 0)
    {
        array3[0] = A_0;
    }
    object obj = methodBase.Invoke(null, array3);
    if (obj is int)
    {
        return (int)obj;
    }
    return 0;
}

Main中的array里面有两万多个uint数据,肯定是混淆无疑了。

13行的<Module>.Decrypt(array, 4159999126u)解密得到gchandle:

15行把gchandle.Target加载成module,其ScopeName为koi:

然后再通过后续几行代码的数据变化,最后于28行,通过反射,调用处理后的数据所形成的函数/方法:

不到万不得已,我真的不想手动处理混淆。先换条路试试看。

掏出fiddler抓包,没抓到,所以激活码验证时大概率使用的不是http(s)协议。

进而掏出wireshark抓包,这次抓到了。随即大吃一惊,居然是tds协议:

tds协议是微软sql server通信的协议。这个修改器居然在本地客户端使用这个协议,那数据库的用户名和密码也必然存在于本地客户端中。

这里我简单介绍一下上图的各个数据包。

第一个数据包是客户端发往数据库的服务端,传输一些预登录的信息,比如版本号等:

第二个数据包是服务端针对第一个数据包的响应。

后面的四个数据包是tls包,进行数据库登录操作。注意,这个tls包的架构是ip->tcp->tds->tls,tls的数据是作为tds的负载而存在的、tds的数据是作为tcp的负载而存在的;而非我们常见的ip->tcp->tls:

然后通过一个tls数据包加密传输数据库的账号和密码,注意这个数据包是真正的ip->tcp->tls的数据包,不是tds数据包,所以过滤条件是tds的话看不到这个包,需要追踪tcp流才能看到:

另外,我在检索资料时发现,有的sql server数据库登录其实没有使用tls,就直接是账号+密码,比如:

(sa是sql server的管理员用户名)

我们回到正题。随后的那个Response是一些登录成功后的相关信息,是tds数据包,明文形式的:

最后四个数据包,两两组合,分别是数据库查询和对应的查询结果:

分析到这里,我们有了意外收获:数据库的账号和密码都存在于这个修改器的本地客户端中。

它可能会被混淆了,被加壳了,或是其他怎么样了让我们难以找到,但必然存在某一时刻,它是存在于程序中的,不然是不可能通过tds协议连接上远程的microsoft sql server的。