`
kh477kh
  • 浏览: 36313 次
社区版块
存档分类
最新评论

【源于网络知识点收集】插件破解方法专辑(二)

 
阅读更多

【源于网络知识点收集】插件破解方法专辑(二)
2010年03月11日
  如何用VC实现软件注册 - Visual C++ - Windows应用与开发者社区(转载)
  如何用VC实现软件注册
  要实现软件注册功能,首先需要知道实现注册机制要涉及到的几个问题:1、如何加入注册检测,判断软件是否注册;2、如何生成注册码,如何保证一个用户名只生成与之唯一对应的注册码;3、在软件不注册情况下,如何限制软件功能的局限性;4、对已经负费使用用户而言,不应造成使用不便。
  首先,应该有一个生成注册码的算法,以下是我简单的一个生成15位注册码的算法:
  //该函数返回一个CSTRING类型的15位注册码,入口参数为用户名
  CString GetRegPasswd(CString &DirName)
  {
  //将用户名换算成15位注册码
  long Num1,Num2,Num3;
  char sn[16]={0};
  CString p;
  int i,len;
  Num1=0;
  Num2=0;
  Num3=0;
  len=int(strlen(DirName));
  if(len!=0)
  {
  for( i=1;i'9') && (sn'Z') &&(sn'z') )
  {
  sn=(sn+31+7*i)%128;
  }
  }
  //赋值给一个CSTRING变量,用做函数返回值
  p.Format("%s",sn);
  }
  return p;
  }
  //检查软件是否注册的函数
  BOOL GetRegFlag(void)
  {
  HKEY hKey = NULL;
  BYTE i;
  CString str;
  str.LoadString(IDS_REG_KEY);// IDS_REG_KEY为在注册表中的子目录字符串
  if (RegCreateKey(HKEY_CURRENT_USER, str, &hKey) != ERROR_SUCCESS) return false;
  DWORD cbA;
  cbA=sizeof(int);
  if( RegQueryValueEx(hKey, "SzMima",NULL,NULL, &i,&cbA) != ERROR_SUCCESS)
  return false;
  BYTE j=i;
  if(j==0)//0代表软件已经注册,可以正常使用
  {
  RegCloseKey(hKey);
  return true;
  }
  else
  {
  RegCloseKey(hKey);
  return false;
  }
  return false;
  };
  //设置软件已经注册标志的函数
  BOOL SetRegFlag(void)
  {
  HKEY hKey = NULL;
  BYTE i;
  CString str;
  str.LoadString(IDS_REG_KEY);// IDS_REG_KEY为在注册表中的子目录字符串
  if (RegCreateKey(HKEY_CURRENT_USER, str, &hKey) != ERROR_SUCCESS) return false;
  BYTE j=0;//0代表已经注册
  if(RegSetValueEx(hKey, "SzMima", 0, REG_BINARY, &j,4) != ERROR_SUCCESS)
  {
  AfxMessageBox("设置注册表数据失败!");
  return FALSE;
  }
  return false;
  };
  以上三个函数即可实现软件注册机制,只需要在程序初始化的时候加入以下几句代码即可
  BOOL bReg= GetRegFlag ();
  if(!bReg)
  {
  //在此加入限制功能或者拒绝是使用的代码
  }
  如果用户注册只需要加入以下代码即可
  //这是我的程序中的一个注册界面,输入用户名和注册码提交后检验注册码是否正确的代码
  void CRegEdit::OnBnClickedOk()
  {
  if(!UpdateData()) return;//取得编辑框的内容,并赋值到类变量中
  m_RegUser.TrimLeft();//m_RegUser是用户名
  m_RegUser.TrimRight();
  if(m_RegUser.IsEmpty())
  {
  AfxMessageBox("用户名不能为空,请重新输入。");
  GetDlgItem(IDC_REGUSER)->SetFocus();
  return;
  }
  m_RegPasswd.TrimLeft();m_RegPasswd是注册码
  m_RegPasswd.TrimRight();
  if(m_RegPasswd.IsEmpty())
  {
  AfxMessageBox("注册码不能为空,请重新输入。");
  GetDlgItem(IDC_REGPASSWD)->SetFocus();
  return;
  }
  CString Passwd;
  Passwd=GetRegPasswd(m_RegUser);//调用算法取得该用户名的注册码
  if(Passwd==m_RegPasswd)//与用户输入的注册进行比较
  {
  SetRegFlag();//设置注册标志
  OnOK();
  }
  else
  AfxMessageBox("注册码错误,请重新输入。");
  UpdateData(false);
  }
  于破解过招,保护你的共享软件――此文曾作为连载刊登于《电脑报》2003年41、42期,如要转载,请注明出自《电脑报》
  ――本人仅是一名初学者,如有疏漏之处,还请列位前辈们指教,谢谢![U
  共享软件是软件业目前世界上比较热门的话题,国内更是如此。成千上万的中国程序员以极大的热情投入到这个领域来,都憧憬着用辛勤的劳动来获得丰厚的回报;但,实际并非如此,绝大多数的人都弑羽而归。值得注意的是:除了选题和技术上的原因外,最大的原因就是共享软件被破解(Crack)了。
  破解见得多了,不免有些麻木。大部分作者都是新软件发布一个星期左右甚至一天之内就会在网上发现注册机或者被修改过的软件(行话称之为“爆破”)。破解者制作了英文、中文、俄文、德文等语种的注册机大肆发散不说,还常常给作者寄一份,外加一封挖苦辱骂的信。唉!我们得罪了谁?没日没夜地熬夜编码,难道得到的就是这连绵的挖苦和不尽的羞辱吗?
  不!决不!我们有理由也有能力保护自己的劳动成果!但问题是:如何保护?关注国内,网上关于破解资料和教程俯拾皆是,而关于软件保护方面的资料则是凤毛麟角(大多都关系到什么技术垄断),这种畸形现状就导致了相当一部分朋友的加密非常脆弱甚至可以称得上是“弱智” !要知道,你要面对的是已经形成团伙的众多破解高手呀,国内的什么CCG、BCG,国外的eGis、King、Core、TNT、DAMN和TMG,皆为水平一流的破解组织。全球盗版软件不少于80%都是由他们的破解的,技术实力连大软件公司都不可小视。
  看到这里,你是否已经已经灰心了?别怕,虽然我们理论上无法完全避免被破解,但如果能够有效地拖延被破解的时间,并充分打击破解者的自信心,是可以让破解者无法忍受这种折磨从而最终放弃的。
  破解,通常的做法有两种――暴力破解(爆破)和写注册机。下面我就来依次讲解每种破解方法的原理和应对方法,这些都是鄙人积累的一些共享软件保护经验,某些关键地方还有例程讲解(Delphi代码,使用C++和VB的朋友可以自己稍微修改一下),希望能对新手们有些帮助,能够更有效地保护自己的劳动成果。
  §暴力破解(爆破)
  这是最常见,也是最简单的破解的方法。该法最适合于对付没有CRC效验的软件,破解新手乐于采用。
  大凡共享软件,验证是否注册大多数要采用if条件语句来进行判断,即使你采用了什么RSA或ECC等强力加密算法,也免不了使用if条件语句。呵呵,这里就是共享软件最为危险的地方哦,当然也是爆破手孜孜不倦所寻求的目标呀!
  例如,你的注册函数类似如下:
  { 利用RSA进行注册码的数字签名验证 }    if RSAVerify(MD5(Key), MD5(Code), e, n) then      ShowMessage('注册成功!')    else      ShowMessage('注册失败!');   { 这里Key是用户输入的注册码,是由你发送给注册用户的 }   { Code是根据用户输入的用户名自动计算出来的注册码    }   { e是RSA算法的公匙,而n是RSA算法的模数。            }
  这个注册函数即使使用了强劲的RSA算法进行注册码验证,可是依然很容易被破解,我们只要把这里修改为:
  { 将逻辑判断改为否 }     if not RSAVerify(MD5(Key), MD5(Code), e, n) then      ShowMessage('注册成功!')    else      ShowMessage('注册失败!');
  就可以了。这时戏剧性的结果会产生:随便输入任何注册码都可以注册通过,相反输入正确的注册码却无法通过注册。:) 其具体操作是先反汇编或者跟踪你的程序,找到判断注册码的cmp、test等汇编指令后的关键跳转指令处,通常是je、jz之类的汇编指令,把它们修改为jne或jnz即可,这样常常只需要修改一个字节就可以完美破解之。:)
  令人遗憾的是,目前大部分共享软件都是这样进行判断的,这也是为什么网上被破解的软件铺天盖地的主要原因。因为这样破解实在是太简单了...
  难道没有什么可以防止的方法吗?当然有啊!只要把软件的关键代码嵌入到注册码或者注册文件中就可以充分防止破解。但现在问题是,怎么嵌入呢?
  最简单的方法就是把关键代码(你的软件功能限制部分最关键而且最简单的一个函数)做成一个小Dll(动态链接库),用强力对称算法加密(密匙可以是主程序某一固定不变的部分或壳的特征Hash值)后生成一个注册文件(License文件,呵呵,格式只有你知道哦!),或者Base64编码后生成一个注册表文件,用户可以双击导入注册表内。
  效验流程如下:已注册用户验证注册码时,先验证有没有文件,没有文件者自然受限制的功能无法使用。如果有注册文件,解密之即生成一个小临时文件。如果主程序被脱壳或者被修改(爆破),自然Hash值密码不符,解密出来的肯定都是垃圾码,没有一点用处。只有没有被修改的主程序才能正确的解码,而且当然只有解密正确的文件才是一个真正的Dll文件,才能被GetProcAddress函数找到欲调用的关键函数地址。这样只有已注册用户才可以享受到你软件的全部功能了。
  如此一来,Cracker破解你的软件就变得很困难了:
  首先,他如果没有注册文件,即使他把主程序脱壳了,由于受限制的部分和注册文件是关联的,他也根本无法修补完整。
  第二,即使他得到了你的注册文件,由于是加密文件,他也无法直接利用之,这样就逼迫他去拆解你的算法,这可是他们最不愿意的碰到的事情哦!如果到了这一步,我想99%的Cracker都是会放弃的,呵呵,只有真正对加密算法有研究的Cracker高手才会继续破解下去。
  第三,你是可以用些小技巧来使他的生活更加痛苦一些的,呵呵。这里我推荐大家使用DSA公开密匙加密算法,它和RSA一样,可以进行数字签名(RSA还可以加密,DSA则只能进行数字签名)。我这里选用它的原因就是它有一项非常实用的特性:随机数填充机制。即DSA每次签名都要使用一个随机数K,正因为有这个K的存在,即使是相同的用户名和机器识别码,由DSA加密过的每份注册文件都不会相同。这对Cracker拆解你的注册文件来说是一个极大的障碍。
  第四,即使他得到了解密后的Dll文件,他也需要大幅度地修改主程序或者把你的Dll部分的关键代码拆出来填到主可执行文件中。呵呵,这就看他对PE文件格式理解得如何了。即使这样,如果你的程序中有大量的Hash效验和死机代码,呵呵,你就耐心等着我们可爱的Cracker同志吐血吧……:)
  所以记住啊:用完这个Dll临时文件后立即从内存中卸载此Dll并删掉,而且注意在解密之前探测一下,系统中有没有FileMon这个威胁极大的探测器呀!
  { 探测FileMon }    function DetectFileMon: Boolean;    begin      if CreateFile(PChar('\\.\FILEVXD'),                     GENERIC_READ or GENERIC_WRITE,                    FILE_SHARE_READ or FILE_SHARE_WRITE,                     nil,                    OPEN_EXISTING,                     FILE_ATTRIBUTE_NORMAL,                     0)  INVALID_HANDLE_VALUE then        Result := True //如果有,就Down机!      else         Result := False;    end; 
  当然,你可以保护得更好一些:可以不采用临时Dll,而把解密后的关键代码用WriteProcessMemory这个API函数写入到主可执行文件自己进程被提交(Committed)的内存页面的指定位置去。这样由于磁盘上没有解密后的临时文件,破解更加困难。事实上,目前世界上最强劲的专业保护软件Armadillo就是用的这种方法。而且这种方法可以充分防止被调试器Dump。但实现起来比较困难,尤其是在WinNT 5以后的操作系统中。
  由于这种方法将注册文件和受限制代码唯一关联,爆破手拿到你的软件也只有干瞪眼。建议大家都给共享软件加上功能限制,这样比时间和次数限制更加安全。
  §写注册机
  顾名思义,这种方法就是模仿你的注册码生成算法或者逆向注册码验证算法而写出来的和你一模一样的注册机。这玩意威胁极大,被爆破了还可以升级。如果被写出注册机,呵呵,你的软件只好免费了。或者你必须更换算法,但以前注册过的合法用户都得被迫更换注册码了,累死你!呵呵...
  上面的方法虽然可以避免爆破,但注册机的威胁还是存在的。Cracker要写注册机必须详细研究你软件的验证模块,这必须先将你的软件脱壳,再反汇编或者用调试器跟踪。市面上许多加壳和保护软件都吹嘘不可能被脱壳,令人可惜的是到目前为止没有一个软件兑现了它们的诺言。由于CPU最终执行的都是有效指令,所以等你的程序自解压完成后再从内存中Dump出来就可以实现脱壳。因此不要在壳上面花很多功夫,因为没有这个必要。
  反汇编是和调试器跟踪也都是不可能防止的,因为所有的Win32程序都是必须通过API来调用Windows系统中的关键Dll的(如Kernel32.dll、GDI32.dll等),然而API是可以Hook的。我们只能从自己的代码着手来保护我们的劳动果实了。
  为了自己调试和以后维护的方便,我们一般采用有意义的名字给我们的函数命名,可这给了Cracker可乘之机。例如这样的函数是什么意思大家应该是一目了然吧?IsRegistered(), IsLicensed(), LicenseVerify(), CheckReg()...这样Cracker就可以轻松地从数千个函数中找到他的目标---你的注册码效验函数!而且破解Delphi编写的软件还有一件TMG小组的破解利器---DeDe,它可以轻松看到你软件里的Form、Unit和函数名,还可以反汇编一部分代码,更是可以和Win32DASM合作反汇编更多的代码,对Delphi软件威胁极大。
  为了不给Cracker创造温馨舒适的破解环境,我们要混乱(Obfuscate)我们的代码,将软件中所有的函数名全部替换成随机生成的函数名。例如Func_3dfsa_fs32zlfv()这个函数是什么意思?恐怕只有天知道了。网上有现成的代码混乱器,你按你使用的编程语言的种类可以找到一些。但注意,只有当你要发布软件时才使用之,而且一定注意备份源代码。否则当你看不懂你自己的代码时可别怪我呀!:)
  另外一定要使用公开密匙算法保护你的软件,RSA、DSA和El Gamal之类的算法都可以从网上找到。但注意:将你算法单元中的所有涉及到算法名称的字符串全部改名。避免被Cracker发现你用的算法而模仿写出注册机来!你还可以张冠李戴,明明用的DSA,将名字全部替换成RSA,呵呵,让他模仿去吧!:)
  其它算法如对称算法和Hash算法都也要注意改名,否则这样:
  EncryptedCode = Blowfish(MD5(UserName), MD5(Key));     //你的加密算法,使用了Blowfish(对称算法)和MD5(Hash算法)
  虽然我不了解Blowfish和MD5算法的原理,也不会逆向它们,但我了解你的效验算法的流程和算法名,我马上就可以从网上找到类似的Blowfish和MD5算法包,从而模拟你的软件仿造出注册机,啊?!真是……$&*&($#%@!
  如果你用的什么其它不常见的算法(如Skipjack (NASA美国航天局标准算法), LOKI, 3-WAY, Safer之类不出名但强度很高的算法),并且全部改名,就让他们去研究软件中成堆的如下代码是什么加密算法吧!:)
  0167:005B9F70  MOV     EAX,[EBP-10]    0167:005B9F73  CALL    00404000    0167:005B9F78  PUSH    EAX    0167:005B9F79  MOV     EAX,[EBP-10]    0167:005B9F7C  CALL    004041C4    0167:005B9F81  LEA     ECX,[EBP-14]    0167:005B9F84  POP     EDX     0167:005B9F85  CALL    004B860C
  当然,最好把Hash算法也全部改名,给会给他们制造更多的困难。但注意,MD5和SHA之类的Hash的初始值会被Cracker从内存中找到,这样他就知道了你用的Hash了。所有建议同时使用MD5的变形算法Ripe-MD(RMD)128或160和其它的Hash,如Tiger, Haval等算法。
  另外,请注意要经常效验你的程序是否被修改(Hash效验),如果被修改则退出。但请注意,有些病毒会修改进程的句柄表和它指向的内核对象,这样病毒就可以直接修改运行中的PE文件而感染之了,另外还有网络传输错误的问题也会导致软件CRC出错。因此请不要认为可执行文件的CRC不符而此时程序已被脱壳了。
  其实,程序被脱壳最明显的标志是其大小明显大于脱壳前。1M的PE文件被UPX、ASPack之类的软件压缩后通常只有400左右。如果你的软件在运行中发现自己的大小大于800K,我想你应该知道如何做了吧?呵呵...
  还有一点,调试器对我们的威胁很大,我们不会肯定让Cracker们舒舒服服地使用SoftICE、TRW和OllyDbg来调试我们的程序。除了常用的MeItICE方法外,这里我给一个我写的方法:
  { 检查自己的进程的父进程是否为Explorer.exe,否则是被调试器加载了 }  { 不过注意,控制台程序的父进程在WinNT下是Cmd.exe哦!}  { 注意加载TlHelp32.pas单元 }    procedure CheckParentProc;  var //检查自己的进程的父进程    Pn: TProcesseNtry32;    sHandle: THandle;    H, ExplProc, ParentProc: Hwnd;    Found: Boolean;    Buffer: array[0..1023of Char;    Path: string;    begin      H := 0;      ExplProc := 0;      ParentProc := 0;      //得到Windows的目录      SetString(Path,                Buffer,                GetWindowsDirectory(Buffer, Sizeof(Buffer) - 1));      Path := UpperCase(Path) + '\EXPLORER.EXE'; //得到Explorer的路径      //得到所有进程的列表快照      sHandle := CreateToolHelp32SnapShot(TH32CS_SNAPALL, 0);      Found := Process32First(sHandle, Pn); //查找进程      while Found do //遍历所有进程      begin        if Pn.szExeFile = ParamStr(0) then //自己的进程        begin          ParentProc := Pn.th32ParentProcessID; //得到父进程的进程ID          //父进程的句柄          H := OpenProcess(PROCESS_ALL_ACCESS, True, Pn.th32ParentProcessID);        end        else if UpperCase(Pn.szExeFile) = Path then          ExplProc := Pn.th32ProcessID;      //Explorer的PID        Found := Process32Next(sHandle, Pn); //查找下一个      end;      //嗯,父进程不是Explorer,是调试器……      if ParentProc  ExplProc then      begin        TerminateProcess(H, 0); //杀之!除之而后快耶!        //你还可以加上其它什么死机代码来消遣消遣这位可爱的Cracker      end;    end;
  你可以在Delphi或者VC中试试,呵呵,是不是把Delphi和VC杀掉了,因为你现在用的是Delphi和VC的内置调试器来运行你的程序的,当然它会六亲不认了,呵呵!调试的时候你还是把它注释掉吧,发布时别忘记激活哟!
  最后一个问题,这也是一个非常重要的问题:保护你的字符串!!!字符串在注册模块中非常重要!当一个富有经验的Cracker破解你的软件时,首先做的就是摄取你的字符串。比如他会输入错误的注册码,得到你关于错误注册码的提示,通常是“无效的注册码,请重新输入!”或者“Invalid key, please input again!”等等,然后用OllyDbg下断点调试或者用WinDASM、IDA Pro等静态分析工具在被他脱壳后的程序中查找那个字符串,找到后进行分析。因此,请一定加密你的字符串!!!一定!!! 使用时再临时解密出来,而且要尽量少使用消息提示框 ,避免被Cracker找到漏洞。加密字符串不需要太复杂的算法,随便找一个快速的对称算法就可以了。
  最后提醒你一句,不要在加密上花太多的功夫!你应该把更多的时间和精力都用来完善你的软件,这样会更合算。借用一位前辈的话来忠告大家吧:花点时间考虑你自己的软件,看看它是否值得保护?如果没人用你的软件,保护也就没有意义了,不要过高估计你的软件“对世界的重要性”!
分享到:
评论

相关推荐

    nagios安装文档

    ### Nagios安装与配置知识点详解 #### 一、Nagios概述 - **定义与背景**:Nagios是一款开源的计算机系统与网络监控工具,主要用于监控Windows、Linux及Unix等操作系统下的主机状态以及网络设备(如路由器、交换机...

    Fifteen puzzle-crx插件

    这款Fifteen puzzle-crx插件主要提供了以下知识点: 1. **十五拼图游戏**:游戏由一个4x4的网格组成,其中有15个标有数字的小方块和一个空格。目标是通过滑动方块,使得所有数字按照1到15的顺序排列。玩家只能移动...

    Sharknado but without the sharks Mod-crx插件

    这个插件的工作原理可能包括以下几个关键知识点: 1. **文本替换算法**:插件使用JavaScript的字符串操作函数,如`replace()`,来查找并替换网页上的特定文本。 2. **DOM操作**:通过操作Document Object Model...

    第二次在线答疑问题收集整理1

    【在线答疑问题收集整理1】 1. **自定义旋转参数块与Ceres自带四元数更新参数块**:在优化过程中,使用...以上问题涵盖了SLAM、传感器融合、定位算法、数据处理等多个方面,是深入理解自动驾驶系统的关键知识点。

    运筹学课程设计

    ### 运筹学课程设计知识点概述 #### 一、运筹学基础理论 **1.1 定义与历史背景** 运筹学是一门利用科学方法解决管理决策问题的学科,它通过建立数学模型来分析复杂的系统,并为决策提供依据。运筹学的发展源于...

    java十分好的基础题,建议看十遍

    以下是对这些知识点的详细阐述: 1. **面向对象的三大特性**: - **封装**:封装是将数据和操作数据的方法绑定在一起,隐藏内部实现细节,只通过公共接口与外部交互。对象作为封装的基本单元,提供了数据的安全性...

    原始源码的Nachos java版本

    下面,我们将详细探讨Nachos操作系统以及其Java版本中的相关知识点。 1. **Nachos操作系统**: Nachos是加利福尼亚大学伯克利分校开发的一个小型操作系统,其名称源于墨西哥小吃“ nachos”。它的主要目标是为...

    毕业设计 福鑫家电管理系统,微服务,由开源项目若依魔改而成,主要用于学习若依系统

    在这个系统中,我们可以深入探讨多个IT领域的核心知识点,包括微服务架构、软件开发流程、前端与后端技术、数据库管理以及项目部署等。 首先,微服务架构是本系统的关键特征。微服务架构是一种将大型复杂应用拆分为...

    高级软件人才培训专家-2022护网面试题总结

    ### 高级软件人才培训专家-2022护网面试题总结 #### 一、描述外网打点的流程及重要性 ...以上内容覆盖了高级软件人才培训专家在2022年护网面试题总结中涉及的关键知识点,希望对学习者有所帮助。

    VC.tar.gz,上海交通大学VC论坛精华

    综合以上信息,这个压缩包可能包含以下知识点: 1. **Visual C++基础知识**:包括C++语法、MFC(Microsoft Foundation Classes)、STL(Standard Template Library)等基础知识的讲解。 2. **编程技巧**:如内存...

    2011年数学建模A题 城市表层土壤重金属污染分析(附所有图件的Matlab源代码).docx

    【知识点】 1. **数学建模**:数学建模是一种运用数学工具来描述和解决实际问题的方法。在这个案例中,它被用来分析城市表层土壤中的重金属污染。 2. **三维地形图与等值线图**:通过Matlab软件创建三维地形图可以...

    行业资料-电子功用-动态地控制电子文档中表单控件的冲突的介绍分析.rar

    以下是对这一主题的深入分析和相关知识点的详细说明。 首先,我们需要理解表单控件在电子文档中的作用。表单控件是电子文档(如PDF或HTML文档)中用于收集、展示和交互数据的元素,包括文本框、复选框、单选按钮、...

    Introduction_to_Java

    以上是关于“Introduction_to_Java”的核心知识点概述,深入学习Java将涉及更多的高级特性,如并发编程、NIO(非阻塞I/O)、Lambda表达式、函数式编程、流API等。持续学习和实践是掌握Java的关键。

    《吃货攻略》项目_风险规划1

    以下是对这些风险的深入解析和相关知识点的阐述: 1. **功能不够完善(R1)**:这涉及到需求分析的不充分,可能导致App无法满足用户的基本需求。项目小组需要通过全面的需求收集和分析,确保列出所有必要的功能,以...

    Namanganliklar-24

    【Namanganliklar-24】是一个项目或资源包,其名称可能源于乌兹别克斯坦的一个城市——纳曼干,暗示着这个项目或许与该地区的一些文化、活动或者特定的事件有关。不过,由于提供的信息有限,我们将主要关注与标签...

Global site tag (gtag.js) - Google Analytics