首页
社区
课程
招聘
[原创]010Editor逆向分析笔记
发表于: 2020-2-15 20:54 8929

[原创]010Editor逆向分析笔记

2020-2-15 20:54
8929

(弹窗,有4类相关API:CreateWindow、CreateDialog、DialogBox、MessageBox

(QT同MFC,也是一种界面库,不管库里有什么函数,其根本都是调用了Windows系统中USER32.dll中的API

(QT编写的界面程序,不了解其库函数不碍事,反正底层都是调用Windows的API

(经验:QT中弹窗一般都是调用了CreateWindow,若实在不知道,就4类API依次下断点,反复测试

OD附加010,ctrl+g搜索CreateWindowA/W、下断、运行,断在75B6E9CC W版处

(CreateWindow是一个很常用的底层API,不仅仅是注册弹窗时会用到,程序运行时好多地方都可能会调用,因此,若在测试时发现断下了,而并没有点击check-license,F9放行这些无关的即可

(010程序太大,OD打开太卡,最好附加)

此时,K-查看调用堆栈,看究竟是哪调用了这个API

(调用来自那一栏,只看是主模块010Edito的调用,其他的不管

依次点进去,简单看一下哪个里面有相关字符串,发现主模块中第三个调用,即地址=00169B54,调用来自=010Edito.00C65B29的那一个

双击进入,到00C65B29处,下断, 再次check-license测试,果然断在此处

找到弹出失败的地方(根据字符串),不断向上找,得到两个关键函数、直接跳向失败/成功的跳转(按照CrackMe的思路来找;前面地址不确定,每次运行会变化

image-20200215175153013

将JNZ关键跳转直接NOP,另保存为文件010Editor1.exe,运行时仍旧弹出注册框,点击check后可显示已经成功

image-20200215175216879

JNZ改NOP可实现,粗暴简单,但仍会弹注册框,美中不足,若要改进就得深究那两个关键函数

如此,这两个关键函数就建立了关系

更完美的暴力破解,

F7进入0029A826函数的内部,直接mov eax,0x2D ; retn 8即可(正常就是retn 8 为了栈平衡

有时修改时,原OPCODE中有下划线,说明可能发生地址重定位,可以利用010Editor,修改PE中相关字段,关闭重定位功能(扩展头-DllCharactertistics-DYNAMIC_BASE,将其置为0

image-20200215175426008

最终修改为,

image-20200215175450859

选中这四行-右键-复制到可执行文件-所有修改-保存文件,命名为:010Editor_cracked

要想实现比较完美的暴力破解,需要修改两个地方:

暴力破解后,原程序打开提示已过期,打不开了

image-20200215175515930

除了API下断点-栈回溯外,还可搜索字符串,找到目标位置,两种方法各有千秋,不拘泥方法,能找到地就行

分析一个函数,要关注其传入及传出

此call大概率会传入name和pass,以验证

传入

push了两个立即数:4389、9,暂未知何意义

call之前mov ecx,数据窗口中跟随ecx的值

image-20200215175617955

ecx中是啥?(内存窗口中-右键-长型-地址,显示1列数据,便于观察

F7进入,逐行分析(不求弄懂每一句具体代码,但求其实现了什么

函数内call函数,不必每个都进,根据出入判断其功能;

push进函数一个地址,call之后看函数对他干了啥

最好记下call之前的样子,便于对比,如下是013BDC30 处的call,这是eax之前的样子/之后的样子,eax返回0,并没看出干了啥,先放着,不深究

image-20200215175704016

OD中重要注释

image-20200215175722775

MOVZX A,B

CDQ指令

OD注释:算法部分(先不管其内部原理,能跟着走下来,看最终的公式

image-20200215175828488

第一个call内部

image-20200215175844999

第二个call内部

image-20200215175859025

有三个条件需要满足

第一个JE前,ecx 不可为0

第二个JE前,eax 不可为0

JA,eax 不可大于 0x3e8

生成序列号的代码-注册机(初步序列号,只满足JE、JE、JA三个条件,后面的还没看

经过验证,满足三个条件,均没有发生跳转(跳了就失败了

image-20200215180236757

lea eax[local.x]; push eax; call xxx:这种形式,将局部变量push进函数,一般是作为传出变量的,即call之后,修改eax(联想传指针间接修改值

如图,eax跟随窗口地址,call之后,发生改变,进一步跟随,发现name字符串转为ascii

image-20200215180303309

SETNE指令

遇到push xxx; call yyy :不一定二者是对应的,可能push对应的是后面的call,要想知道call中究竟push了几个,看相邻的call内部是怎么平衡堆栈的,看这几个push是怎么被这几个call瓜分的

OD注释:版本1成功后跳过来的,关键的函数是第三个,即对name字符串加密的那个

image-20200215180342991

需要将name字符串传入402e50的加密函数,从而得到加密值,那个函数怎么获得?

怎么拷贝出来

IDA看这个数组有点乱,获取其地址2E64148,在OD中将其拷出来

image-20200215180432904

数据窗口右键-数据转换(插件实现的)-C++ - Dword(选啥都行,但是IDA中看到那个数组是Dword开头,就选dword吧)

image-20200215180452098

将其粘贴到VS中,作为加密要用到的数组,如图

image-20200215180509359

可以直接在IDA中将C代码拷出来

image-20200215180530614

· 可以直接在IDA中将C代码拷出来

最终代码

运行,输入name,输出相应的序列号,测试,成功

image-20200215180624176

正确序列号使用一段时间后就会报错,因为其远程连接服务器进行了网络验证

标志位判断,直接JMP跳过

image-20200215180644023

网络验证函数内部,使其一直返回1(也就是验证正确才返回的值

image-20200215180700592

PS:流水账的写法,图片说明较少,简单记录为日后翻看方便

文件: 010Editor.exe
大小: 46891744 bytes
文件版本:8.0.1
修改时间: 2019年11月12日, 21:05:55
MD5: 67399A3650BE615DF420393758C799CD
SHA1: 95CC3DBFEF9781C73D147C44E179EDBAFAC37F42
CRC32: 8A47BA40
  #include <stdio.h>
  #include <windows.h>
  #include <time.h>

  int main()
  {
    // 设置随机数种子
    srand(time(NULL));
    // 密码字符串转为16进制字节数组、K[3]只可为9C/FC/AC
    byte K[10] = { 0x11,0x22,0x33,0x9C,0x55,0x66,0x77,0x88,0x99,0xAA };
    // 第一个call中:处理k[0]、k[6]
    // AL= (k[0]^k[6]^0x18 + 0x3D) ^ 0xA7
    while (true)
    {
      // 随机生成k0、k6(小于0xFF
      byte k0 = rand() % 0xFF;
      byte k6 = rand() % 0xFF;
      // 使用k0、k6构造 al
      byte al = (k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7;
      // 若满足第一个JE前的条件,则保存
      if (al != 0)
      {
        K[0] = k0;
        K[6] = k6;
        break;
      }
    }
    // 第二个call中:处理 K[1]、K[7]、k[2]、k[5] 
    // esi = (((K[1] ^ K[7]) ^ 0xFF) * 0x100 + k[2] ^ k[5] & 0xFF) & 0xFFFF
    // eax =(((eax^0x7892)+0x4d30)^0x3421) &0xFFFF
    // edx = 余数;判断是否为0: 为0返回eax=商、不为0返回eax=0
    while (true)
    {
      // 随机生成k1、7、2、5(小于0xFF
      byte k1 = rand() % 0xFF;
      byte k7 = rand() % 0xFF;
      byte k2 = rand() % 0xFF;
      byte k5 = rand() % 0xFF;
      // 使用k1、7、2、5构造esi,即第二个call传入的参数,在call内部先提取到eax中
      DWORD esi = (((k1 ^ k7) & 0xFF) * 0x100 + k2 ^ k5 & 0xFF) & 0xFFFF;
      // 通过传入的参数构造eax,即后面除法中的被除数
      DWORD eax = (((esi ^ 0x7892) + 0x4d30) ^ 0x3421) & 0xFFFF;
      // 余数为0返回商,也就满足返回值不等于0,同时指定 商即返回值<= 0x3EB(同时满足后面两个条件,则保存
      if (eax % 0xB == 0 && eax/0xB <= 0x3EB)
      {
        K[1] = k1;
        K[7] = k7;
        K[2] = k2;
        K[5] = k5;
        break;
      }
    }
    // 从若干随机值中,取出满足三个条件的值(两个JE的一个JA的)
    printf("%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X",K[0], K[1], K[2], K[3], K[4],K[5], K[6], K[7], K[8], K[9]);
    // 不止一种情况,如:049B-CC9C-5508-8609-99AA
    return 0;
  }
  #include <stdio.h>
  #include <windows.h>
  #include <time.h>

  // 从OD拷贝出来的用于加密的数组
  DWORD g_EncodeArray[]={
      0x39cb44b8, 0x23754f67, 0x5f017211, 0x3ebb24da, 0x351707c6, 0x63f9774b, 0x17827288, 0x0fe74821, 0x5b5f670f, 0x48315ae8, 0x785b7769, 0x2b7a1547, 0x38d11292, 0x42a11b32, 0x35332244, 0x77437b60,
      0x1eab3b10, 0x53810000, 0x1d0212ae, 0x6f0377a8, 0x43c03092, 0x2d3c0a8e, 0x62950cbf, 0x30f06ffa, 0x34f710e0, 0x28f417fb, 0x350d2f95, 0x5a361d5a, 0x15cc060b, 0x0afd13cc, 0x28603bcf, 0x3371066b,
      0x30cd14e4, 0x175d3a67, 0x6dd66a13, 0x2d3409f9, 0x581e7b82, 0x76526b99, 0x5c8d5188, 0x2c857971, 0x15f51fc0, 0x68cc0d11, 0x49f55e5c, 0x275e4364, 0x2d1e0dbc, 0x4cee7ce3, 0x32555840, 0x112e2e08,
      0x6978065a, 0x72921406, 0x314578e7, 0x175621b7, 0x40771dbf, 0x3fc238d6, 0x4a31128a, 0x2dad036e, 0x41a069d6, 0x25400192, 0x00dd4667, 0x6afc1f4f, 0x571040ce, 0x62fe66df, 0x41db4b3e, 0x3582231f,
      0x55f6079a, 0x1ca70644, 0x1b1643d2, 0x3f7228c9, 0x5f141070, 0x3e1474ab, 0x444b256e, 0x537050d9, 0x0f42094b, 0x2fd820e6, 0x778b2e5e, 0x71176d02, 0x7fea7a69, 0x5bb54628, 0x19ba6c71, 0x39763a99,
      0x178d54cd, 0x01246e88, 0x3313537e, 0x2b8e2d17, 0x2a3d10be, 0x59d10582, 0x37a163db, 0x30d6489a, 0x6a215c46, 0x0e1c7a76, 0x1fc760e7, 0x79b80c65, 0x27f459b4, 0x799a7326, 0x50ba1782, 0x2a116d5c,
      0x63866e1b, 0x3f920e3c, 0x55023490, 0x55b56089, 0x2c391fd1, 0x2f8035c2, 0x64fd2b7a, 0x4ce8759a, 0x518504f0, 0x799501a8, 0x3f5b2cad, 0x38e60160, 0x637641d8, 0x33352a42, 0x51a22c19, 0x085c5851,
      0x032917ab, 0x2b770ac7, 0x30ac77b3, 0x2bec1907, 0x035202d0, 0x0fa933d3, 0x61255df3, 0x22ad06bf, 0x58b86971, 0x5fca0de5, 0x700d6456, 0x56a973db, 0x5ab759fd, 0x330e0be2, 0x5b3c0ddd, 0x495d3c60,
      0x53bd59a6, 0x4c5e6d91, 0x49d9318d, 0x103d5079, 0x61ce42e3, 0x7ed5121d, 0x14e160ed, 0x212d4ef2, 0x270133f0, 0x62435a96, 0x1fa75e8b, 0x6f092fbe, 0x4a000d49, 0x57ae1c70, 0x004e2477, 0x561e7e72,
      0x468c0033, 0x5dcc2402, 0x78507ac6, 0x58af24c7, 0x0df62d34, 0x358a4708, 0x3cfb1e11, 0x2b71451c, 0x77a75295, 0x56890721, 0x0fef75f3, 0x120f24f1, 0x01990ae7, 0x339c4452, 0x27a15b8e, 0x0ba7276d,
      0x60dc1b7b, 0x4f4b7f82, 0x67db7007, 0x4f4a57d9, 0x621252e8, 0x20532cfc, 0x6a390306, 0x18800423, 0x19f3778a, 0x462316f0, 0x56ae0937, 0x43c2675c, 0x65ca45fd, 0x0d604ff2, 0x0bfd22cb, 0x3afe643b,
      0x3bf67fa6, 0x44623579, 0x184031f8, 0x32174f97, 0x4c6a092a, 0x5fb50261, 0x01650174, 0x33634af1, 0x712d18f4, 0x6e997169, 0x5dab7afe, 0x7c2b2ee8, 0x6edb75b4, 0x5f836fb6, 0x3c2a6dd6, 0x292d05c2,
      0x052244db, 0x149a5f4f, 0x5d486540, 0x331d15ea, 0x4f456920, 0x483a699f, 0x3b450f05, 0x3b207c6c, 0x749d70fe, 0x417461f6, 0x62b031f1, 0x2750577b, 0x29131533, 0x588c3808, 0x1aef3456, 0x0f3c00ec,
      0x7da74742, 0x4b797a6c, 0x5ebb3287, 0x786558b8, 0x00ed4ff2, 0x6269691e, 0x24a2255f, 0x62c11f7e, 0x2f8a7dcd, 0x643b17fe, 0x778318b8, 0x253b60fe, 0x34bb63a3, 0x5b03214f, 0x5f1571f4, 0x1a316e9f,
      0x7acf2704, 0x28896838, 0x18614677, 0x1bf569eb, 0x0ba85ec9, 0x6aca6b46, 0x1e43422a, 0x514d5f0e, 0x413e018c, 0x307626e9, 0x01ed1dfa, 0x49f46f5a, 0x461b642b, 0x7d7007f2, 0x13652657, 0x6b160bc5,
      0x65e04849, 0x1f526e1c, 0x5a0251b6, 0x2bd73f69, 0x2dbf7acd, 0x51e63e80, 0x5cf2670f, 0x21cd0a03, 0x5cff0261, 0x33ae061e, 0x3bb6345f, 0x5d814a75, 0x257b5df4, 0x0a5c2c5b, 0x16a45527, 0x16f23945
  };
  // 从IDA拷贝出来的用于加密name字符串的函数
  int __cdecl EncodeUserName(const char *a1, int a2, char a3, char a4)
  {
      const char *v4; // edx@1
      signed int v5; // esi@1
      signed int v6; // edi@1
      unsigned __int8 v7; // bl@2
      int v8; // eax@3
      int v9; // ecx@3
      int v10; // ecx@4
      int result; // eax@4
      int v12; // ecx@5
      unsigned __int8 v13; // [sp+8h] [bp-10h]@2
      unsigned __int8 v14; // [sp+Ch] [bp-Ch]@2
      unsigned __int8 v15; // [sp+10h] [bp-8h]@2
      int v16; // [sp+14h] [bp-4h]@1

      v4 = a1;
      v16 = 0;
      v5 = strlen(a1);
      v6 = 0;
      if (v5 <= 0)
      {
          result = 0;
      }
      else
      {
          v13 = 0;
          v14 = 0;
          v7 = 15 * a4;
          v15 = 17 * a3;
          do
          {
              v8 = toupper(v4[v6]);
              v9 = v16 + g_EncodeArray[v8];
              if (a2)
              {
                  v10 = g_EncodeArray[v7]
                      + g_EncodeArray[v15]
                      + g_EncodeArray[(unsigned __int8)(v8 + 47)] * (g_EncodeArray[(unsigned __int8)(v8 + 13)] ^ v9);
                  result = g_EncodeArray[v14] + v10;
                  v16 = g_EncodeArray[v14] + v10;
              }
              else
              {
                  v12 = g_EncodeArray[v7]
                      + g_EncodeArray[v15]
                      + g_EncodeArray[(unsigned __int8)(v8 + 23)] * (g_EncodeArray[(unsigned __int8)(v8 + 63)] ^ v9);
                  result = g_EncodeArray[v13] + v12;
                  v16 = g_EncodeArray[v13] + v12;
              }
              v14 += 19;
              ++v6;
              v15 += 9;
              v7 += 13;
              v13 += 7;
              v4 = a1;
          } while (v6 < v5);
      }
      return result;
  }

  int main()
  {
      // 1 初始化
      srand(time(NULL));// 设置随机数种子
      int dwRet = rand() % 0x3E8;        //(v1中JA的条件为不可大于0x3e8
      byte K[10] = { 0x11,0x22,0x33,0x9C,0x55,0x66,0x77,0x88,0x99,0xAA };// 密码字符串转为16进制字节数组、K[3]只可为9C/FC/AC,以9c为例
      // 2 通过用户名 生成对应的key数组
      char szName[100] = { 0 };
      printf("please input name:>> ");
      scanf_s("%s", szName, 50);
      DWORD dwKey = EncodeUserName(szName, 1, 0, dwRet);// 生成与name相对应的key
      // 3 需满足的条件
      /* 
          K[4] == 返回值eax 最低位
          K[5] == 返回值eax 第二位(通过右移取最低位来实现
          K[6] == 返回值eax 第三位
          K[7] == 返回值eax 第四位
      */
      K[4] = dwKey & 0xFF;
      K[5] = dwKey >> 8 & 0xFF;
      K[6] = dwKey >> 16 & 0xFF;
      K[7] = dwKey >> 24 & 0xFF;
      // 4 穷举剩余的字节-0、6
      /*
          第一个call中:处理k[0]、k[6]
          AL= (k[0]^k[6]^0x18 + 0x3D) ^ 0xA7
      */
      while (true)
      {
          // 随机生成k0、k6(小于0xFF
          byte k0 = rand() % 0xFF;
          byte k6 = K[6];        // v1中随机生成,v2中通过加密函数确定
          // 使用k0、k6构造 al
          byte al = (k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7;
          // 若满足第一个JE前的条件,则保存
          if (al >= 9)    //v1中 != 0, v2中JBE的条件为 >= 9
          {
              K[0] = k0;
              K[6] = k6;
              break;
          }
      }
      // 5 穷举剩余的字节 - 1、7、2、5
      /*
          第二个call中:处理 K[1]、K[7]、k[2]、k[5]
          esi = (((K[1] ^ K[7]) ^ 0xFF) * 0x100 + k[2] ^ k[5] & 0xFF) & 0xFFFF
          eax =(((eax^0x7892)+0x4d30)^0x3421) &0xFFFF
          edx = 余数;判断是否为0: 为0返回eax=商、不为0返回eax=0
      */
      while (true)
      {
          // 随机生成k1、7、2、5(小于0xFF
          byte k1 = rand() % 0xFF;
          byte k7 = K[7];// v1中随机生成,v2中通过加密函数确定
          byte k2 = rand() % 0xFF;
          byte k5 = K[5];// v1中随机生成,v2中通过加密函数确定
          // 使用k1、7、2、5构造esi,即第二个call传入的参数,在call内部先提取到eax中
          DWORD esi = (((k1 ^ k7) & 0xFF) * 0x100 + k2 ^ k5 & 0xFF) & 0xFFFF;
          // 通过传入的参数构造eax,即后面除法中的被除数
          DWORD eax = (((esi ^ 0x7892) + 0x4d30) ^ 0x3421) & 0xFFFF;
          // 余数为0返回商,也就满足返回值不等于0,同时指定 商即返回值<= 0x3EB(同时满足后面两个条件,则保存
          if (eax % 0xB == 0 && eax/0xB == dwRet)//v1中 <= 0x3EB,v2中 == dwRet = rand() % 0x3E8,同义
          {
              K[1] = k1;
              K[7] = k7;
              K[2] = k2;
              K[5] = k5;
              break;
          }
      }
      // 6 输出符合条件的序列
      printf("%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X\n",K[0], K[1], K[2], K[3], K[4],K[5], K[6], K[7], K[8], K[9]);    
      system("pause");
      return 0;
  }
  • 系统环境: Windows10-64位、Windows7-32位
  • 工具: 010Editor、OllyDebug、IDA、VS 2017<!--more-->
  • 暴力破解,防止注册弹框
  • 分析算法,实现注册机
  • 暴力破解,去除网络验证
  • ebx != e7那个函数下断,F7进入,看返回值eax会有几种情况:
  • mov eax,93、mov eax,e7、mov eax,esi,继续看 esi 来自哪:有4e、2D、E7几种情况
  • edi = db 那个函数下断,F7进入,看eax=db怎么才成立
    • 有一个mov eax,db
    • 往上找是一个JE跳转,cmp eax,2D,即比较上面的call的返回值
    • 观察得,此处的call同ebx != e7是同一个call,都是0029A826,
  • 如此,这两个关键函数就建立了关系

    • 只要函数1的eax == 2D,自然满足本身 != e7的条件
    • 只要其能一直==2D,那么在函数2的内部,就能通过JE跳转,使得函数2的eax = db
    • 一下子都满足了条件,最终就会成功,因此让其一直==2D是关键
    • (注意,不仅仅是让函数1本身eax==2D,而且要让函数2的内部中,函数1的eax也==2D,就是让函数1的返回值恒== 2D,啰哩啰嗦的。。。
    • 其实上句话完全多余,调用的是0029A826这个函数,如果修改其,就相当于修改了函数定义,不管在哪调用,结果都是一样的
  • 更完美的暴力破解,

    • F7进入0029A826函数的内部,直接mov eax,0x2D ; retn 8即可(正常就是retn 8 为了栈平衡

    • 有时修改时,原OPCODE中有下划线,说明可能发生地址重定位,可以利用010Editor,修改PE中相关字段,关闭重定位功能(扩展头-DllCharactertistics-DYNAMIC_BASE,将其置为0

      image-20200215175426008

    • 最终修改为,

      image-20200215175450859

  • 选中这四行-右键-复制到可执行文件-所有修改-保存文件,命名为:010Editor_cracked

  • 有一个mov eax,db
  • 往上找是一个JE跳转,cmp eax,2D,即比较上面的call的返回值
  • 观察得,此处的call同ebx != e7是同一个call,都是0029A826,
  • 只要函数1的eax == 2D,自然满足本身 != e7的条件
  • 只要其能一直==2D,那么在函数2的内部,就能通过JE跳转,使得函数2的eax = db
  • 一下子都满足了条件,最终就会成功,因此让其一直==2D是关键
  • (注意,不仅仅是让函数1本身eax==2D,而且要让函数2的内部中,函数1的eax也==2D,就是让函数1的返回值恒== 2D,啰哩啰嗦的。。。
  • 其实上句话完全多余,调用的是0029A826这个函数,如果修改其,就相当于修改了函数定义,不管在哪调用,结果都是一样的
  • F7进入0029A826函数的内部,直接mov eax,0x2D ; retn 8即可(正常就是retn 8 为了栈平衡

  • 有时修改时,原OPCODE中有下划线,说明可能发生地址重定位,可以利用010Editor,修改PE中相关字段,关闭重定位功能(扩展头-DllCharactertistics-DYNAMIC_BASE,将其置为0

    image-20200215175426008

  • 最终修改为,

    image-20200215175450859

  • 要想实现比较完美的暴力破解,需要修改两个地方:

    • 关键函数1内部
    • JNZ
  • 暴力破解后,原程序打开提示已过期,打不开了

    image-20200215175515930

  • 除了API下断点-栈回溯外,还可搜索字符串,找到目标位置,两种方法各有千秋,不拘泥方法,能找到地就行

  • 关键函数1内部
  • JNZ
  • 打开原来程序,OD附加(而非破解后的版本),重点分析eax=2d 那个函数
  • 分析一个函数,要关注其传入及传出

    • 传入:push了什么、call之前是否赋值了寄存器环境,如thiscall中的ecx
    • 传出:是否修改了某内存、返回值eax
  • 此call大概率会传入name和pass,以验证

  • 传入

    • push了两个立即数:4389、9,暂未知何意义

    • call之前mov ecx,数据窗口中跟随ecx的值

      image-20200215175617955

  • 传入:push了什么、call之前是否赋值了寄存器环境,如thiscall中的ecx
  • 传出:是否修改了某内存、返回值eax
  • push了两个立即数:4389、9,暂未知何意义

  • call之前mov ecx,数据窗口中跟随ecx的值

    image-20200215175617955

  • ecx中是啥?(内存窗口中-右键-长型-地址,显示1列数据,便于观察

    • 数据窗口跟随1、4,并未发现有用数据
    • 跟随2、3,发现有00xx形式,unicode字符,右键-hex-16字节/unicode,观察数据,发现是键入的name和pass
    • 传入了name和pass,再调用这个call,符合逻辑
  • F7进入,逐行分析(不求弄懂每一句具体代码,但求其实现了什么

    • 函数内call函数,不必每个都进,根据出入判断其功能;

    • push进函数一个地址,call之后看函数对他干了啥

    • 最好记下call之前的样子,便于对比,如下是013BDC30 处的call,这是eax之前的样子/之后的样子,eax返回0,并没看出干了啥,先放着,不深究

      image-20200215175704016

  • OD中重要注释

    image-20200215175722775

  • 数据窗口跟随1、4,并未发现有用数据
  • 跟随2、3,发现有00xx形式,unicode字符,右键-hex-16字节/unicode,观察数据,发现是键入的name和pass
  • 传入了name和pass,再调用这个call,符合逻辑
  • 函数内call函数,不必每个都进,根据出入判断其功能;

  • push进函数一个地址,call之后看函数对他干了啥

  • 最好记下call之前的样子,便于对比,如下是013BDC30 处的call,这是eax之前的样子/之后的样子,eax返回0,并没看出干了啥,先放着,不深究

    image-20200215175704016

  • 小技巧:若不想跳转实现,可手动设置新的EIP
  • MOVZX A,B

    • B 空间必须小于 A
    • 用0来扩展填充A的余下空间
    • 相当于 & 0xFF
    • 如movzx ecx, al 即 ecx = al & 0xFF,若后面是ax,则0xFFFF(几个F根据其几个字节而定
    • 就是普通mov + 高位填充0
  • CDQ指令

    • 把 EAX 的第 31 bit 复制到 EDX 的每一个 bit 上。
    • 大多出现在除法运算之前
    • 它实际的作用只是把EDX的所有位都设成EAX最高位的值
    • 也就是说,当EAX <80000000, EDX 为00000000;
    • 当EAX >= 80000000, EDX 则为FFFFFFFF
  • OD注释:算法部分(先不管其内部原理,能跟着走下来,看最终的公式

    image-20200215175828488

  • 第一个call内部

    image-20200215175844999

  • 第二个call内部

    image-20200215175859025

  • B 空间必须小于 A
  • 用0来扩展填充A的余下空间
  • 相当于 & 0xFF
  • 如movzx ecx, al 即 ecx = al & 0xFF,若后面是ax,则0xFFFF(几个F根据其几个字节而定
  • 就是普通mov + 高位填充0
  • 把 EAX 的第 31 bit 复制到 EDX 的每一个 bit 上。
  • 大多出现在除法运算之前
  • 它实际的作用只是把EDX的所有位都设成EAX最高位的值
  • 也就是说,当EAX <80000000, EDX 为00000000;
  • 当EAX >= 80000000, EDX 则为FFFFFFFF
  • 有三个条件需要满足

    • 第一个JE前,ecx 不可为0
    • 第二个JE前,eax 不可为0
    • JA前,eax 不可大雨0x3e8
  • 第一个JE前,ecx 不可为0

    • 来源:ecx来自edi+0x1c、来自eax、来自AL,即第一个call的返回值
    • 第一个call内部,al = (k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7;(如图函数内部
    • 最终:al =(k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7 !=0
  • 第二个JE前,eax 不可为0

    • 来源:eax来自ax,即第二个call的返回值
    • 第二个call内部:eax返回值有两种情况,0 or 商(对应余数为0/非0)
    • 最终:余数 == 0 即可(=0,返回商,必然eax不为0
  • JA,eax 不可大于 0x3e8

    • 由上,余数==0,返回商,也就保证了返回值eax 不为0
    • 最终:若要满足此,只要保证 商<=0x3e8
  • 生成序列号的代码-注册机(初步序列号,只满足JE、JE、JA三个条件,后面的还没看

      #include <stdio.h>
      #include <windows.h>
      #include <time.h>
    
      int main()
      {
        // 设置随机数种子
        srand(time(NULL));
        // 密码字符串转为16进制字节数组、K[3]只可为9C/FC/AC
        byte K[10] = { 0x11,0x22,0x33,0x9C,0x55,0x66,0x77,0x88,0x99,0xAA };
        // 第一个call中:处理k[0]、k[6]
        // AL= (k[0]^k[6]^0x18 + 0x3D) ^ 0xA7
        while (true)
        {
          // 随机生成k0、k6(小于0xFF
          byte k0 = rand() % 0xFF;
          byte k6 = rand() % 0xFF;
          // 使用k0、k6构造 al
          byte al = (k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7;
          // 若满足第一个JE前的条件,则保存
          if (al != 0)
          {
            K[0] = k0;
            K[6] = k6;
            break;
          }
        }
        // 第二个call中:处理 K[1]、K[7]、k[2]、k[5] 
        // esi = (((K[1] ^ K[7]) ^ 0xFF) * 0x100 + k[2] ^ k[5] & 0xFF) & 0xFFFF
        // eax =(((eax^0x7892)+0x4d30)^0x3421) &0xFFFF
        // edx = 余数;判断是否为0: 为0返回eax=商、不为0返回eax=0
        while (true)
        {
          // 随机生成k1、7、2、5(小于0xFF
          byte k1 = rand() % 0xFF;
          byte k7 = rand() % 0xFF;
          byte k2 = rand() % 0xFF;
          byte k5 = rand() % 0xFF;
          // 使用k1、7、2、5构造esi,即第二个call传入的参数,在call内部先提取到eax中
          DWORD esi = (((k1 ^ k7) & 0xFF) * 0x100 + k2 ^ k5 & 0xFF) & 0xFFFF;
          // 通过传入的参数构造eax,即后面除法中的被除数
          DWORD eax = (((esi ^ 0x7892) + 0x4d30) ^ 0x3421) & 0xFFFF;
          // 余数为0返回商,也就满足返回值不等于0,同时指定 商即返回值<= 0x3EB(同时满足后面两个条件,则保存
          if (eax % 0xB == 0 && eax/0xB <= 0x3EB)
          {
            K[1] = k1;
            K[7] = k7;
            K[2] = k2;
            K[5] = k5;
            break;
          }
        }
        // 从若干随机值中,取出满足三个条件的值(两个JE的一个JA的)
        printf("%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X",K[0], K[1], K[2], K[3], K[4],K[5], K[6], K[7], K[8], K[9]);
        // 不止一种情况,如:049B-CC9C-5508-8609-99AA
        return 0;
      }
    
  • 经过验证,满足三个条件,均没有发生跳转(跳了就失败了

    image-20200215180236757

  • 第一个JE前,ecx 不可为0
  • 第二个JE前,eax 不可为0
  • JA前,eax 不可大雨0x3e8
  • 来源:ecx来自edi+0x1c、来自eax、来自AL,即第一个call的返回值
  • 第一个call内部,al = (k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7;(如图函数内部
  • 最终:al =(k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7 !=0
  • 来源:eax来自ax,即第二个call的返回值
  • 第二个call内部:eax返回值有两种情况,0 or 商(对应余数为0/非0)
  • 最终:余数 == 0 即可(=0,返回商,必然eax不为0
  • 由上,余数==0,返回商,也就保证了返回值eax 不为0
  • 最终:若要满足此,只要保证 商<=0x3e8
  • lea eax[local.x]; push eax; call xxx:这种形式,将局部变量push进函数,一般是作为传出变量的,即call之后,修改eax(联想传指针间接修改值

  • 如图,eax跟随窗口地址,call之后,发生改变,进一步跟随,发现name字符串转为ascii

    image-20200215180303309

  • SETNE指令

    • setne x:if ZF=0 set x =0
    • sete x:if ZF=1 set x =0
  • 遇到push xxx; call yyy :不一定二者是对应的,可能push对应的是后面的call,要想知道call中究竟push了几个,看相邻的call内部是怎么平衡堆栈的,看这几个push是怎么被这几个call瓜分的

  • OD注释:版本1成功后跳过来的,关键的函数是第三个,即对name字符串加密的那个

    image-20200215180342991

  • setne x:if ZF=0 set x =0
  • sete x:if ZF=1 set x =0
  • 在v1基础上,通过对上述04的分析,改进
  • 需要将name字符串传入402e50的加密函数,从而得到加密值,那个函数怎么获得?

    • 可以F7进去,看他是怎么实现的,自己用代码复现(但是太麻烦了
    • 简便方法:直接考出来即可
  • 怎么拷贝出来

    • F7进入,在函数开始前(即push ebp),shift + x复制地址,得013BD120
    • IDA中打开,g到达那个地址,F5看C代码(要等待其分析完才可,左下角不动了就行了
    • 可见:其用到了一个数组,只要将此数组以及此代码拷贝出来即可
  • IDA看这个数组有点乱,获取其地址2E64148,在OD中将其拷出来

    image-20200215180432904

  • 数据窗口右键-数据转换(插件实现的)-C++ - Dword(选啥都行,但是IDA中看到那个数组是Dword开头,就选dword吧)

    image-20200215180452098

  • 将其粘贴到VS中,作为加密要用到的数组,如图

    image-20200215180509359

  • 可以直接在IDA中将C代码拷出来

    image-20200215180530614

  • 可以F7进去,看他是怎么实现的,自己用代码复现(但是太麻烦了
  • 简便方法:直接考出来即可
  • F7进入,在函数开始前(即push ebp),shift + x复制地址,得013BD120
  • IDA中打开,g到达那个地址,F5看C代码(要等待其分析完才可,左下角不动了就行了
  • 可见:其用到了一个数组,只要将此数组以及此代码拷贝出来即可
  • 最终代码

      #include <stdio.h>
      #include <windows.h>
      #include <time.h>
    
      // 从OD拷贝出来的用于加密的数组
      DWORD g_EncodeArray[]={
          0x39cb44b8, 0x23754f67, 0x5f017211, 0x3ebb24da, 0x351707c6, 0x63f9774b, 0x17827288, 0x0fe74821, 0x5b5f670f, 0x48315ae8, 0x785b7769, 0x2b7a1547, 0x38d11292, 0x42a11b32, 0x35332244, 0x77437b60,
          0x1eab3b10, 0x53810000, 0x1d0212ae, 0x6f0377a8, 0x43c03092, 0x2d3c0a8e, 0x62950cbf, 0x30f06ffa, 0x34f710e0, 0x28f417fb, 0x350d2f95, 0x5a361d5a, 0x15cc060b, 0x0afd13cc, 0x28603bcf, 0x3371066b,
          0x30cd14e4, 0x175d3a67, 0x6dd66a13, 0x2d3409f9, 0x581e7b82, 0x76526b99, 0x5c8d5188, 0x2c857971, 0x15f51fc0, 0x68cc0d11, 0x49f55e5c, 0x275e4364, 0x2d1e0dbc, 0x4cee7ce3, 0x32555840, 0x112e2e08,
          0x6978065a, 0x72921406, 0x314578e7, 0x175621b7, 0x40771dbf, 0x3fc238d6, 0x4a31128a, 0x2dad036e, 0x41a069d6, 0x25400192, 0x00dd4667, 0x6afc1f4f, 0x571040ce, 0x62fe66df, 0x41db4b3e, 0x3582231f,
          0x55f6079a, 0x1ca70644, 0x1b1643d2, 0x3f7228c9, 0x5f141070, 0x3e1474ab, 0x444b256e, 0x537050d9, 0x0f42094b, 0x2fd820e6, 0x778b2e5e, 0x71176d02, 0x7fea7a69, 0x5bb54628, 0x19ba6c71, 0x39763a99,
          0x178d54cd, 0x01246e88, 0x3313537e, 0x2b8e2d17, 0x2a3d10be, 0x59d10582, 0x37a163db, 0x30d6489a, 0x6a215c46, 0x0e1c7a76, 0x1fc760e7, 0x79b80c65, 0x27f459b4, 0x799a7326, 0x50ba1782, 0x2a116d5c,
          0x63866e1b, 0x3f920e3c, 0x55023490, 0x55b56089, 0x2c391fd1, 0x2f8035c2, 0x64fd2b7a, 0x4ce8759a, 0x518504f0, 0x799501a8, 0x3f5b2cad, 0x38e60160, 0x637641d8, 0x33352a42, 0x51a22c19, 0x085c5851,
          0x032917ab, 0x2b770ac7, 0x30ac77b3, 0x2bec1907, 0x035202d0, 0x0fa933d3, 0x61255df3, 0x22ad06bf, 0x58b86971, 0x5fca0de5, 0x700d6456, 0x56a973db, 0x5ab759fd, 0x330e0be2, 0x5b3c0ddd, 0x495d3c60,
          0x53bd59a6, 0x4c5e6d91, 0x49d9318d, 0x103d5079, 0x61ce42e3, 0x7ed5121d, 0x14e160ed, 0x212d4ef2, 0x270133f0, 0x62435a96, 0x1fa75e8b, 0x6f092fbe, 0x4a000d49, 0x57ae1c70, 0x004e2477, 0x561e7e72,
          0x468c0033, 0x5dcc2402, 0x78507ac6, 0x58af24c7, 0x0df62d34, 0x358a4708, 0x3cfb1e11, 0x2b71451c, 0x77a75295, 0x56890721, 0x0fef75f3, 0x120f24f1, 0x01990ae7, 0x339c4452, 0x27a15b8e, 0x0ba7276d,
          0x60dc1b7b, 0x4f4b7f82, 0x67db7007, 0x4f4a57d9, 0x621252e8, 0x20532cfc, 0x6a390306, 0x18800423, 0x19f3778a, 0x462316f0, 0x56ae0937, 0x43c2675c, 0x65ca45fd, 0x0d604ff2, 0x0bfd22cb, 0x3afe643b,
          0x3bf67fa6, 0x44623579, 0x184031f8, 0x32174f97, 0x4c6a092a, 0x5fb50261, 0x01650174, 0x33634af1, 0x712d18f4, 0x6e997169, 0x5dab7afe, 0x7c2b2ee8, 0x6edb75b4, 0x5f836fb6, 0x3c2a6dd6, 0x292d05c2,
          0x052244db, 0x149a5f4f, 0x5d486540, 0x331d15ea, 0x4f456920, 0x483a699f, 0x3b450f05, 0x3b207c6c, 0x749d70fe, 0x417461f6, 0x62b031f1, 0x2750577b, 0x29131533, 0x588c3808, 0x1aef3456, 0x0f3c00ec,
          0x7da74742, 0x4b797a6c, 0x5ebb3287, 0x786558b8, 0x00ed4ff2, 0x6269691e, 0x24a2255f, 0x62c11f7e, 0x2f8a7dcd, 0x643b17fe, 0x778318b8, 0x253b60fe, 0x34bb63a3, 0x5b03214f, 0x5f1571f4, 0x1a316e9f,
          0x7acf2704, 0x28896838, 0x18614677, 0x1bf569eb, 0x0ba85ec9, 0x6aca6b46, 0x1e43422a, 0x514d5f0e, 0x413e018c, 0x307626e9, 0x01ed1dfa, 0x49f46f5a, 0x461b642b, 0x7d7007f2, 0x13652657, 0x6b160bc5,
          0x65e04849, 0x1f526e1c, 0x5a0251b6, 0x2bd73f69, 0x2dbf7acd, 0x51e63e80, 0x5cf2670f, 0x21cd0a03, 0x5cff0261, 0x33ae061e, 0x3bb6345f, 0x5d814a75, 0x257b5df4, 0x0a5c2c5b, 0x16a45527, 0x16f23945
      };
      // 从IDA拷贝出来的用于加密name字符串的函数
      int __cdecl EncodeUserName(const char *a1, int a2, char a3, char a4)
      {
          const char *v4; // edx@1
          signed int v5; // esi@1
          signed int v6; // edi@1
          unsigned __int8 v7; // bl@2
          int v8; // eax@3
          int v9; // ecx@3
          int v10; // ecx@4
          int result; // eax@4
          int v12; // ecx@5
          unsigned __int8 v13; // [sp+8h] [bp-10h]@2
          unsigned __int8 v14; // [sp+Ch] [bp-Ch]@2
          unsigned __int8 v15; // [sp+10h] [bp-8h]@2
          int v16; // [sp+14h] [bp-4h]@1
    
          v4 = a1;
          v16 = 0;
          v5 = strlen(a1);
          v6 = 0;
          if (v5 <= 0)
          {
              result = 0;
          }
          else
          {
              v13 = 0;
              v14 = 0;
              v7 = 15 * a4;
              v15 = 17 * a3;
              do
              {
                  v8 = toupper(v4[v6]);
                  v9 = v16 + g_EncodeArray[v8];
                  if (a2)
                  {
                      v10 = g_EncodeArray[v7]
                          + g_EncodeArray[v15]
                          + g_EncodeArray[(unsigned __int8)(v8 + 47)] * (g_EncodeArray[(unsigned __int8)(v8 + 13)] ^ v9);
                      result = g_EncodeArray[v14] + v10;
                      v16 = g_EncodeArray[v14] + v10;
                  }
                  else
                  {
                      v12 = g_EncodeArray[v7]
                          + g_EncodeArray[v15]
                          + g_EncodeArray[(unsigned __int8)(v8 + 23)] * (g_EncodeArray[(unsigned __int8)(v8 + 63)] ^ v9);
                      result = g_EncodeArray[v13] + v12;
                      v16 = g_EncodeArray[v13] + v12;
                  }
                  v14 += 19;
                  ++v6;
                  v15 += 9;
                  v7 += 13;
                  v13 += 7;
                  v4 = a1;
              } while (v6 < v5);
          }
          return result;
      }
    
      int main()
      {
          // 1 初始化
          srand(time(NULL));// 设置随机数种子
          int dwRet = rand() % 0x3E8;        //(v1中JA的条件为不可大于0x3e8
          byte K[10] = { 0x11,0x22,0x33,0x9C,0x55,0x66,0x77,0x88,0x99,0xAA };// 密码字符串转为16进制字节数组、K[3]只可为9C/FC/AC,以9c为例
          // 2 通过用户名 生成对应的key数组
          char szName[100] = { 0 };
          printf("please input name:>> ");
          scanf_s("%s", szName, 50);
          DWORD dwKey = EncodeUserName(szName, 1, 0, dwRet);// 生成与name相对应的key
          // 3 需满足的条件
          /* 
              K[4] == 返回值eax 最低位
              K[5] == 返回值eax 第二位(通过右移取最低位来实现
              K[6] == 返回值eax 第三位
              K[7] == 返回值eax 第四位
          */
          K[4] = dwKey & 0xFF;
          K[5] = dwKey >> 8 & 0xFF;
          K[6] = dwKey >> 16 & 0xFF;
          K[7] = dwKey >> 24 & 0xFF;
          // 4 穷举剩余的字节-0、6
          /*
              第一个call中:处理k[0]、k[6]
              AL= (k[0]^k[6]^0x18 + 0x3D) ^ 0xA7
          */
          while (true)
          {
              // 随机生成k0、k6(小于0xFF
              byte k0 = rand() % 0xFF;
              byte k6 = K[6];        // v1中随机生成,v2中通过加密函数确定
              // 使用k0、k6构造 al
              byte al = (k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7;
              // 若满足第一个JE前的条件,则保存
              if (al >= 9)    //v1中 != 0, v2中JBE的条件为 >= 9
              {
                  K[0] = k0;
                  K[6] = k6;
                  break;
              }
          }
          // 5 穷举剩余的字节 - 1、7、2、5
          /*
              第二个call中:处理 K[1]、K[7]、k[2]、k[5]
              esi = (((K[1] ^ K[7]) ^ 0xFF) * 0x100 + k[2] ^ k[5] & 0xFF) & 0xFFFF
              eax =(((eax^0x7892)+0x4d30)^0x3421) &0xFFFF
              edx = 余数;判断是否为0: 为0返回eax=商、不为0返回eax=0
          */
          while (true)
          {
              // 随机生成k1、7、2、5(小于0xFF
              byte k1 = rand() % 0xFF;
              byte k7 = K[7];// v1中随机生成,v2中通过加密函数确定
              byte k2 = rand() % 0xFF;
              byte k5 = K[5];// v1中随机生成,v2中通过加密函数确定
              // 使用k1、7、2、5构造esi,即第二个call传入的参数,在call内部先提取到eax中
              DWORD esi = (((k1 ^ k7) & 0xFF) * 0x100 + k2 ^ k5 & 0xFF) & 0xFFFF;
              // 通过传入的参数构造eax,即后面除法中的被除数
              DWORD eax = (((esi ^ 0x7892) + 0x4d30) ^ 0x3421) & 0xFFFF;
              // 余数为0返回商,也就满足返回值不等于0,同时指定 商即返回值<= 0x3EB(同时满足后面两个条件,则保存
              if (eax % 0xB == 0 && eax/0xB == dwRet)//v1中 <= 0x3EB,v2中 == dwRet = rand() % 0x3E8,同义
              {
                  K[1] = k1;
                  K[7] = k7;
                  K[2] = k2;
                  K[5] = k5;
                  break;
              }
          }
          // 6 输出符合条件的序列
          printf("%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X\n",K[0], K[1], K[2], K[3], K[4],K[5], K[6], K[7], K[8], K[9]);    
          system("pause");
          return 0;
      }
    
  • 运行,输入name,输出相应的序列号,测试,成功

    image-20200215180624176

  1. (弹窗,有4类相关API:CreateWindow、CreateDialog、DialogBox、MessageBox

  2. (QT同MFC,也是一种界面库,不管库里有什么函数,其根本都是调用了Windows系统中USER32.dll中的API

  3. (QT编写的界面程序,不了解其库函数不碍事,反正底层都是调用Windows的API

  4. (经验:QT中弹窗一般都是调用了CreateWindow,若实在不知道,就4类API依次下断点,反复测试

  5. OD附加010,ctrl+g搜索CreateWindowA/W、下断、运行,断在75B6E9CC W版处

  6. (CreateWindow是一个很常用的底层API,不仅仅是注册弹窗时会用到,程序运行时好多地方都可能会调用,因此,若在测试时发现断下了,而并没有点击check-license,F9放行这些无关的即可

  7. (010程序太大,OD打开太卡,最好附加)

  8. 此时,K-查看调用堆栈,看究竟是哪调用了这个API

  9. (调用来自那一栏,只看是主模块010Edito的调用,其他的不管

  10. 依次点进去,简单看一下哪个里面有相关字符串,发现主模块中第三个调用,即地址=00169B54,调用来自=010Edito.00C65B29的那一个

  11. 双击进入,到00C65B29处,下断, 再次check-license测试,果然断在此处

  12. 找到弹出失败的地方(根据字符串),不断向上找,得到两个关键函数、直接跳向失败/成功的跳转(按照CrackMe的思路来找;前面地址不确定,每次运行会变化

    image-20200215175153013

  13. 将JNZ关键跳转直接NOP,另保存为文件010Editor1.exe,运行时仍旧弹出注册框,点击check后可显示已经成功

    image-20200215175216879

  14. JNZ改NOP可实现,粗暴简单,但仍会弹注册框,美中不足,若要改进就得深究那两个关键函数

  1. 标志位判断,直接JMP跳过

    image-20200215180644023

  2. 网络验证函数内部,使其一直返回1(也就是验证正确才返回的值

    image-20200215180700592

  1. 边分析边测试的方法
  2. 逆向真的是体力活,说难也没太难,就得有耐心
  3. 要细心,失之毫厘,差之千里
  1. 15PB视频课程-逆向工程实战之软件算法分析
  • 01-样本概况
  •          1.1-应用程序信息
  •          1.2-分析环境及工具
  •          1.3-分析目标
  • 02-具体分析过程
  •          2.1-暴力破解

  • [招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

    最后于 2020-2-16 14:36 被21Gun5编辑 ,原因:
    收藏
    免费 1
    支持
    分享
    最新回复 (8)
    雪    币: 729
    活跃值: (383)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    2
    2020-2-16 13:25
    0
    雪    币: 4540
    活跃值: (1036)
    能力值: ( LV10,RANK:160 )
    在线值:
    发帖
    回帖
    粉丝
    3
    gaoliangk [em_63]
    太惭愧了,本来不想发的,觉得写的一点都不正规,完全想到哪写哪
    2020-2-16 13:36
    0
    雪    币: 729
    活跃值: (383)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    4
    21Gun5 太惭愧了,本来不想发的,觉得写的一点都不正规,完全想到哪写哪
    挺好的,之前看过另外一个兄弟写的,你们都研究还是很透彻
    2020-2-16 13:45
    0
    雪    币: 2510
    能力值: ( LV1,RANK:0 )
    在线值:
    发帖
    回帖
    粉丝
    5
    感谢分享
    2020-2-16 14:02
    0
    雪    币: 29228
    活跃值: (3880)
    能力值: ( LV2,RANK:15 )
    在线值:
    发帖
    回帖
    粉丝
    6
    通俗易懂
    2020-2-17 08:58
    0
    雪    币:
    能力值: ( LV1,RANK:0 )
    在线值:
    发帖
    回帖
    粉丝
    7
    保留学习
    2020-2-22 00:37
    0
    雪    币: 49
    活跃值: (118)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    8
    支持一下龙哥!
    2020-2-23 16:20
    0
    雪    币: 228
    活跃值: (582)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    9
    请问"数据窗口右键-数据转换(插件实现的)"是哪个插件?
    2021-8-3 02:51
    0
    游客
    登录 | 注册 方可回帖
    返回
    //