⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 揭开木马的神秘面纱3.htm

📁 可以对黑客编程有一定的了解
💻 HTM
📖 第 1 页 / 共 2 页
字号:

<p><b>揭开木马的神秘面纱&lt;三&gt; - ICMP的木马篇</b><br>
Shotgun</p>
<p><font size="2"><br>
</font><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;在揭开木马的神秘面纱(二)发表后,有很多朋友来信询问新型木马的详细情况,本文会详细的分析Win2000下一种新型木马的内部构造和防御方法。(本文默认的操作系统为Win2000,开发环境为VC++6.0。)</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;大家知道,一般的“古典”型木马都是通过建立TCP连接来进行命令和数据的传递的,但是这种方法有一个致命的漏洞,就是木马在等待和运行的过程中,始终有一个和外界联系的端口打开着,这是木马的阿喀琉斯之踵(参看希腊神话《特洛伊战纪》),也是高手们查找木马的杀手锏之一(Netstat大法)。所谓道高一尺,魔高一丈,木马也是在斗争中不断进步不断成长的,其中一种ICMP木马就彻底摆脱了端口的束缚,成为黑客入侵后门工具中的佼佼者。<br>
&nbsp;什么是ICMP呢?ICMP全称是Internet&nbsp;Control&nbsp;Message&nbsp;Protocol(互联网控制报文协议)它是IP协议的附属协议,用来传递差错报文以及其他需要注意的消息报文,这个协议常常为TCP或UDP协议服务,但是也可以单独使用,例如著名的工具Ping(向Mike&nbsp;Muuss致敬),就是通过发送接收ICMP_ECHO和ICMP_ECHOREPLY报文来进行网络诊断的。<br>
&nbsp;实际上,ICMP木马的出现正是得到了Ping程序的启发,由于ICMP报文是由系统内核或进程直接处理而不是通过端口,这就给木马一个摆脱端口的绝好机会,木马将自己伪装成一个Ping的进程,系统就会将ICMP_ECHOREPLY(Ping的回包)的监听、处理权交给木马进程,一旦事先约定好的ICMP_ECHOREPLY包出现(可以判断包大小、ICMP_SEQ等特征),木马就会接受、分析并从报文中解码出命令和数据。<br>
&nbsp;ICMP_ECHOREPLY包还有对于防火墙和网关的穿透能力。对于防火墙来说,ICMP报文是被列为危险的一类:从Ping&nbsp;of&nbsp;Death到ICMP风暴到ICMP碎片攻击,构造ICMP报文一向是攻击主机的最好方法之一,因此一般的防火墙都会对ICMP报文进行过滤;但是ICMP_ECHOREPLY报文却往往不会在过滤策略中出现,这是因为一旦不允许ICMP_ECHOREPLY报文通过就意味着主机没有办法对外进行Ping的操作,这样对于用户是极其不友好的。如果设置正确,ICMP_ECHOREPLY报文也能穿过网关,进入局域网。</p>
<p>&nbsp;&nbsp;&nbsp; 为了实现发送/监听ICMP报文,必须建立SOCK_RAW(原始套接口),首先,我们需要定义一个IP首部:<br>
&nbsp;typedef&nbsp;struct&nbsp;iphdr&nbsp;{<br>
&nbsp;unsigned&nbsp;int&nbsp;version:4;  &nbsp;//&nbsp;IP版本号,4表示IPV4<br>
 unsigned&nbsp;int&nbsp;h_len:4;   //&nbsp;4位首部长度<br>
 unsigned&nbsp;char&nbsp;tos;      //&nbsp;8位服务类型TOS<br>
 unsigned&nbsp;short&nbsp;total_len;   &nbsp;//&nbsp;16位总长度(字节) <br>
 unsigned&nbsp;short&nbsp;ident;     //16位标识<br>
 unsigned&nbsp;short&nbsp;frag_and_flags; //&nbsp;3位标志位<br>
 unsigned&nbsp;char ttl;     &nbsp;//8位生存时间&nbsp;TTL<br>
 unsigned&nbsp;char&nbsp;proto;     //&nbsp;8位协议&nbsp;(TCP,&nbsp;UDP&nbsp;或其他)<br>
 unsigned&nbsp;short&nbsp;checksum;   //&nbsp;16位IP首部校验和<br>
 unsigned&nbsp;int&nbsp;sourceIP;    &nbsp;//32位源IP地址<br>
 unsigned&nbsp;int&nbsp;destIP;     &nbsp;//32位目的IP地址<br>
&nbsp;}IpHeader;<br>
&nbsp;<br>
&nbsp;然后定义一个ICMP首部:<br>
&nbsp;typedef&nbsp;struct&nbsp;_ihdr&nbsp;{<br>
&nbsp;BYTE&nbsp;i_type;       &nbsp;//8位类型<br>
&nbsp;BYTE&nbsp;i_code;       //8位代码<br>
&nbsp;USHORT&nbsp;i_cksum;     //16位校验和&nbsp;<br>
&nbsp;USHORT&nbsp;i_id;       //识别号(一般用进程号作为识别号)<br>
&nbsp;USHORT&nbsp;i_seq;  &nbsp;//报文序列号&nbsp;<br>
&nbsp;ULONG&nbsp;timestamp;    &nbsp;//时间戳<br>
&nbsp;}IcmpHeader;<br>
&nbsp;<br>
&nbsp;这时可以同过WSASocket建立一个原始套接口:<br>
&nbsp;SockRaw=WSASocket&nbsp;(<br>
&nbsp;AF_INET,    //协议族 <br>
&nbsp;SOCK_RAW,  &nbsp;//协议类型,SOCK_RAW表示是原始套接口&nbsp;<br>
&nbsp;IPPROTO_ICMP, //协议,IPPROTO_ICMP表示ICMP数据报<br>
&nbsp;NULL,     //WSAPROTOCOL_INFO置空<br>
&nbsp;0,  //保留字,永远置为0<br>
&nbsp;WSA_FLAG_OVERLAPPED&nbsp;//标志位<br>
&nbsp;);<br>
&nbsp;注:为了使用发送接收超时设置(设置SO_RCVTIMEO,&nbsp;SO_SNDTIMEO),必须将标志位置为WSA_FLAG_OVERLAPPED<br>
&nbsp;<br>
&nbsp;随后你可以使用fill_icmp_data子程序填充ICMP报文段:<br>
&nbsp;调用方法fill_icmp_data(icmp_data,datasize);<br>
&nbsp;fill_icmp_data函数:<br>
&nbsp;void&nbsp;fill_icmp_data(char&nbsp;*&nbsp;icmp_data,&nbsp;int&nbsp;datasize)<br>
&nbsp;{<br>
 &nbsp;IcmpHeader&nbsp;*icmp_hdr;<br>
 &nbsp;char&nbsp;*datapart;<br>
 &nbsp;icmp_hdr&nbsp;=&nbsp;(IcmpHeader*)icmp_data;<br>
 &nbsp;icmp_hdr-&gt;i_type&nbsp;=&nbsp;ICMP_ECHOREPLY;    &nbsp;//类型为ICMP_ECHOREPLY<br>
 &nbsp;icmp_hdr-&gt;i_code&nbsp;=&nbsp;0;<br>
 &nbsp;icmp_hdr-&gt;i_id&nbsp;=&nbsp;(USHORT)GetCurrentProcessId();&nbsp;//识别号为进程号&nbsp;<br>
 &nbsp;icmp_hdr-&gt;i_cksum&nbsp;=&nbsp;0;   &nbsp;//校验和初始化<br>
 &nbsp;icmp_hdr-&gt;i_seq&nbsp;=&nbsp;0;    //序列号初始化<br>
 &nbsp;datapart&nbsp;=&nbsp;icmp_data&nbsp;+&nbsp;sizeof(IcmpHeader);&nbsp;//数据端的地址为icmp报文地址加上<br>
&nbsp;ICMP的首部长度<br>
&nbsp;memset(datapart,‘A‘,&nbsp;datasize&nbsp;-&nbsp;sizeof(IcmpHeader)); //这里我填充的数据全部为’A’,你可以填<br>
&nbsp;充任何代码和数据,实际上木马和控制端<br>
&nbsp;之间就是通过数据段传递数据的。<br>
&nbsp;}<br>
&nbsp;<br>
&nbsp;再使用CheckSum子程序计算ICMP校验和:<br>
&nbsp;调用方法:<br>
&nbsp;((IcmpHeader*)icmp_data)-&gt;i_cksum&nbsp;=&nbsp;checksum((USHORT*)icmp_data,&nbsp;datasize);<br>
&nbsp;CheckSum函数:<br>
&nbsp;USHORT&nbsp;CheckSum&nbsp;(USHORT&nbsp;*buffer,&nbsp;int&nbsp;size)&nbsp;<br>
&nbsp;{<br>
&nbsp;unsigned&nbsp;long&nbsp;cksum=0;<br>
&nbsp;while(size&nbsp;&gt;1)&nbsp;<br>
&nbsp;{ &nbsp;<br>
&nbsp;cksum+=*buffer++;<br>
 &nbsp;size&nbsp;-=sizeof(USHORT);<br>
&nbsp;}<br>
&nbsp;if(size&nbsp;)&nbsp;cksum&nbsp;+=&nbsp;*(UCHAR*)buffer;<br>
  &nbsp;cksum&nbsp;=&nbsp;(cksum&nbsp;&gt;&gt;&nbsp;16)&nbsp;+&nbsp;(cksum&nbsp;&amp;&nbsp;0xffff);<br>
&nbsp;cksum&nbsp;+=&nbsp;(cksum&nbsp;&gt;&gt;16);<br>
  &nbsp;return&nbsp;(USHORT)(~cksum);<br>
&nbsp;}//&nbsp;CheckSum函数是标准的校验和函数,你也可以用优化过的任何校验和函数来代替它<br>
&nbsp;<br>
&nbsp;随后,就可以通过sendto函数发送ICMP_ECHOREPLY报文:<br>
&nbsp;sendto(sockRaw,icmp_data,datasize,0,(struct&nbsp;sockaddr*)&amp;dest,sizeof(dest));<br>
&nbsp;<br>
&nbsp;作为服务端的监听程序,基本的操作相同,只是需要使用recvfrm函数接收ICMP_ECHOREPLY报文并用decoder函数将接收来的报文解码为数据和命令:<br>
&nbsp;recv_icmp=recvfrom(sockRaw,recvbuf,MAX_PACKET,0,(struct&nbsp;sockaddr*)&amp;from,&amp;fromlen);<br>
&nbsp;decode_resp(recvbuf,recv_icmp,&amp;from);<br>
&nbsp;decoder函数:<br>
&nbsp;void&nbsp;decoder(char&nbsp;*buf,&nbsp;int&nbsp;bytes,struct&nbsp;sockaddr_in&nbsp;*from)&nbsp;<br>
&nbsp;{<br>
 IpHeader&nbsp;*iphdr;<br>
 IcmpHeader&nbsp;*icmphdr;<br>
 unsigned&nbsp;short&nbsp;iphdrlen;<br>
 iphdr&nbsp;=&nbsp;(IpHeader&nbsp;*)buf;     //IP首部的地址就等于buf的地址<br>
 iphdrlen&nbsp;=&nbsp;iphdr-&gt;h_len&nbsp;*&nbsp;4&nbsp;; //&nbsp;因为h_len是32位word,要转换成bytes必须*4<br>
 icmphdr&nbsp;=&nbsp;(IcmpHeader*)(buf&nbsp;+&nbsp;iphdrlen); //ICMP首部的地址等于IP首部长度加buf<br>
 printf(&quot;%d&nbsp;bytes&nbsp;from&nbsp;%s:&quot;,bytes,&nbsp;inet_ntoa(from-&gt;sin_addr));  //取出源地址<br>
 printf(&quot;&nbsp;icmp_id=%d.&nbsp;&quot;,icmphdr-&gt;i_id);    //取出进程号<br>
 printf(&quot;&nbsp;icmp_seq=%d.&nbsp;&quot;,icmphdr-&gt;i_seq);    //取出序列号<br>
 printf(&quot;&nbsp;icmp_type=%d&quot;,icmphdr-&gt;i_type);   &nbsp;//取出类型<br>
 printf(&quot;&nbsp;icmp_code=%d&quot;,icmphdr-&gt;i_code);   &nbsp;//取出代码<br>
 for(i=0;i&lt;ICMP_DATA_SIZE;i++)&nbsp;printf(&quot;%c&quot;,*(buf+iphdrlen+i+12));&nbsp;//取出数据段<br>
&nbsp;}<br>
&nbsp;注:在WIN2000下使用SOCK_RAW需要管理员的权限。<br>
&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 对于ICMP木马,除非你使用嗅探器或者监视windows的SockAPI调用,否则从网络上是很难发现木马的行踪的(关于进程的隐藏及破解会在下一篇文章中进行讨论),那么,有什么可以补救的方法呢?有的,就是过滤ICMP报文,对于win2000可以使用系统自带的路由功能对ICMP协议进行过滤,win2000的Routing&nbsp;&amp;&nbsp;Remote&nbsp;Access功能十分强大,其中之一就是建立一个TCP/IP协议过滤器:打开Routing&nbsp;&amp;&nbsp;Remote&nbsp;Access,选中机器名,在IP路由-&gt;General-&gt;网卡属性中有两个过滤器-输入过滤和输出过滤,只要在这里将你想过滤的协议制定为策略,ICMP木马就英雄无用武之地了;不过值得注意的是,一旦在输入过滤器中禁止了ICMP_ECHOREPLY报文,你就别想再用Ping这个工具了;如果过滤了所有的ICMP报文,你就收不到任何错误报文,当你使用IE访问一个并不存在的网站时,往往要花数倍的时间才能知道结果(嘿嘿,网络不可达、主机不可达、端口不可达报文你一个都收不到),而且基于ICMP协议的tracert工具也会失效,这也是方便和安全之间的矛盾统一了吧。&nbsp;&nbsp;&nbsp; 
</p>
<p>&nbsp;&nbsp;&nbsp; 本文的撰写是为了深入地研究Win2000的入侵和防御技术,探讨TCP/IP协议和Windows编程技巧,请不要将文中的内容用于任何违法的目的,文中所附为试验性的ICMP通讯程序,仅仅提供通过ICMP_ECHOREPLY进行通讯交换数据的功能以供研究;如果你对本文中的内容或代码有疑问,请Mail&nbsp;to:Shotgun@xici.net,但是出于网络安全的考虑,本人不会提供任何木马软件及代码。<br>
&nbsp;<br>
&nbsp;附录:<br>
&nbsp;1、发送ICMP_ECHOREPLY报文的程序代码<br>
&nbsp;#include&nbsp;&lt;winsock2.h&gt;<br>
&nbsp;#include&nbsp;&lt;stdio.h&gt;<br>
&nbsp;#include&nbsp;&lt;stdlib.h&gt;<br>
&nbsp;<br>
&nbsp;#define&nbsp;ICMP_ECHO&nbsp;8  //ICMP回显请求报文的类型值为8<br>
&nbsp;#define&nbsp;ICMP_ECHOREPLY&nbsp;0 //ICMP回显应答报文的类型值为0<br>
&nbsp;#define&nbsp;ICMP_MIN&nbsp;8  //&nbsp;ICMP报文的最小长度是8字节(仅为首部)<br>
&nbsp;#define&nbsp;ICMP_DEST_IP&nbsp;&quot;127.0.0.1&quot;&nbsp;//目标主机的IP<br>
&nbsp;#define&nbsp;ICMP_PASSWORD&nbsp;1234 //密码设置,用来识别控制端<br>
&nbsp;<br>
&nbsp;//&nbsp;定义IP&nbsp;首部&nbsp;<br>
&nbsp;typedef&nbsp;struct&nbsp;iphdr&nbsp;{<br>
&nbsp;unsigned&nbsp;int&nbsp;version:4;  //IP版本号,4表示IPV4<br>
 unsigned&nbsp;int&nbsp;h_len:4;  //4位首部长度<br>
 unsigned&nbsp;char&nbsp;tos;  //8位服务类型TOS<br>
 unsigned&nbsp;short&nbsp;total_len;    //16位总长度(字节) <br>
 unsigned&nbsp;short&nbsp;ident; &nbsp;//16位标识<br>
 unsigned&nbsp;short&nbsp;frag_and_flags; &nbsp;//3位标志位<br>
 unsigned&nbsp;char ttl;  //8位生存时间&nbsp;TTL<br>
 unsigned&nbsp;char&nbsp;proto; &nbsp;//8位协议&nbsp;(TCP,&nbsp;UDP&nbsp;或其他)<br>
 unsigned&nbsp;short&nbsp;checksum; //16位IP首部校验和<br>
 unsigned&nbsp;int&nbsp;sourceIP; &nbsp;//32位源IP地址<br>
 unsigned&nbsp;int&nbsp;destIP;   &nbsp;//32位目的IP地址<br>
&nbsp;}IpHeader;<br>
&nbsp;<br>
&nbsp;<br>
&nbsp;//&nbsp;定义ICMP首部<br>
&nbsp;typedef&nbsp;struct&nbsp;_ihdr&nbsp;<br>
&nbsp;{<br>
 BYTE&nbsp;i_type;    //8位类型<br>
 BYTE&nbsp;i_code;  &nbsp;//8位代码<br>
 USHORT&nbsp;i_cksum;  //16位校验和&nbsp;<br>
 USHORT&nbsp;i_id;  //识别号(一般用进程号作为识别号)<br>
 USHORT&nbsp;i_seq;  //报文序列号&nbsp;<br>
 ULONG&nbsp;timestamp; &nbsp;//时间戳<br>
&nbsp;}IcmpHeader;<br>
&nbsp;<br>
&nbsp;<br>
&nbsp;#define&nbsp;STATUS_FAILED&nbsp;0xFFFF<br>
&nbsp;#define&nbsp;DEF_PACKET_SIZE&nbsp;64 //定义报文的大小为64字节<br>
&nbsp;#define&nbsp;MAX_PACKET&nbsp;6500 //定义最大报文的大小为6500字节<br>
&nbsp;<br>
&nbsp;#define&nbsp;xmalloc(s)&nbsp;HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,(s))<br>
&nbsp;#define&nbsp;xfree(p) &nbsp;HeapFree&nbsp;(GetProcessHeap(),0,(p))<br>
&nbsp;<br>
&nbsp;void&nbsp;fill_icmp_data(char&nbsp;*,int); //填充ICMP报文的子程序<br>
&nbsp;USHORT&nbsp;checksum(USHORT&nbsp;*,&nbsp;int);&nbsp;//计算校验和的子程序  <br>
&nbsp;<br>
&nbsp;int&nbsp;main(int&nbsp;argc,&nbsp;char&nbsp;**argv)<br>
&nbsp;{<br>
 &nbsp;WSADATA&nbsp;wsaData;<br>
 &nbsp;SOCKET&nbsp;sockRaw&nbsp;=&nbsp;(SOCKET)NULL;<br>
 &nbsp;struct&nbsp;sockaddr_in&nbsp;dest,from;<br>
 &nbsp;struct&nbsp;hostent&nbsp;*&nbsp;hp;<br>
 &nbsp;int&nbsp;bread,datasize,retval,bwrote;<br>
 &nbsp;int&nbsp;fromlen&nbsp;=&nbsp;sizeof(from);<br>
 &nbsp;int&nbsp;timeout&nbsp;=&nbsp;1000;<br>
 &nbsp;char&nbsp;*icmp_data;<br>
 &nbsp;char&nbsp;*recvbuf;<br>
 &nbsp;unsigned&nbsp;int&nbsp;addr=0;<br>
 &nbsp;USHORT&nbsp;seq_no&nbsp;=&nbsp;0;<br>
 &nbsp;static&nbsp;int&nbsp;nCount=0;<br>
&nbsp;<br>
 &nbsp;if((retval=WSAStartup(MAKEWORD(2,1),&amp;wsaData))&nbsp;!=&nbsp;0)<br>
 {fprintf(stderr,&quot;WSAStartup&nbsp;failed:&nbsp;%d\n&quot;,retval);ExitProcess(STATUS_FAILED);}<br>
&nbsp;if((sockRaw=WSASocket(AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL,0,WSA_FLAG_OVERLAPPED))==INVALID_SOCKET)&nbsp;<br>
 {fprintf(stderr,&quot;WSASocket()&nbsp;failed:&nbsp;%d\n&quot;,WSAGetLastError());ExitProcess(STATUS_FAILED);}<br>
 &nbsp;__try<br>
 &nbsp;{<br>
 &nbsp;if((bread=setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char*)&amp;timeout,sizeof(timeout)))==SOCKET_ERROR)<br>
  {fprintf(stderr,&quot;Failed&nbsp;to&nbsp;set&nbsp;recv&nbsp;timeout:&nbsp;%d\n&quot;,WSAGetLastError());__leave;}&nbsp;//设置接收超时<br>
 &nbsp;if((bread=setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char*)&amp;timeout,sizeof(timeout)))==SOCKET_ERROR)<br>
  {fprintf(stderr,&quot;Failed&nbsp;to&nbsp;set&nbsp;send&nbsp;timeout:&nbsp;%d\n&quot;,WSAGetLastError());__leave;}&nbsp;//设置发送超时<br>
 &nbsp;memset(&amp;dest,0,sizeof(dest));<br>
 &nbsp;dest.sin_family&nbsp;=&nbsp;AF_INET;<br>
 &nbsp;dest.sin_addr.s_addr&nbsp;=&nbsp;inet_addr(ICMP_DEST_IP);<br>
 &nbsp;datasize=DEF_PACKET_SIZE;<br>
 &nbsp;datasize+=sizeof(IcmpHeader); <br>
 &nbsp;icmp_data=xmalloc(MAX_PACKET);<br>
 &nbsp;recvbuf=xmalloc(MAX_PACKET);<br>
 &nbsp;if(!icmp_data)&nbsp;{fprintf(stderr,&quot;HeapAlloc&nbsp;failed&nbsp;%d\n&quot;,GetLastError());__leave;}<br>
  &nbsp;memset(icmp_data,0,MAX_PACKET);<br>
 &nbsp;printf(&quot;\nSend&nbsp;Packet&nbsp;to&nbsp;%s&nbsp;Success!\n&quot;,ICMP_DEST_IP);<br>
 &nbsp;fill_icmp_data(icmp_data,datasize);  //填充ICMP报文<br>
 &nbsp;((IcmpHeader*)icmp_data)-&gt;timestamp&nbsp;=&nbsp;GetTickCount();&nbsp;//设置时间戳<br>
 &nbsp;((IcmpHeader*)icmp_data)-&gt;i_seq&nbsp;=&nbsp;ICMP_PASSWORD; //设置序列号,实际使用时可以用这个密码验证<br>
 &nbsp;((IcmpHeader*)icmp_data)-&gt;i_cksum&nbsp;=&nbsp;checksum((USHORT*)icmp_data,&nbsp;datasize); &nbsp;//计算校验和<br>
 &nbsp;bwrote=sendto(sockRaw,icmp_data,datasize,0,(struct&nbsp;sockaddr*)&amp;dest,sizeof(dest));&nbsp;//发送报文<br>
 &nbsp;if&nbsp;(bwrote&nbsp;==&nbsp;SOCKET_ERROR)<br>
 &nbsp;{<br>
  if&nbsp;(WSAGetLastError()&nbsp;==&nbsp;WSAETIMEDOUT)&nbsp;printf(&quot;Timed&nbsp;out\n&quot;);<br>
  fprintf(stderr,&quot;sendto&nbsp;failed:&nbsp;%d\n&quot;,WSAGetLastError());<br>
  __leave;<br>
 &nbsp;}<br>
 &nbsp;if&nbsp;(bwrote&nbsp;&lt;&nbsp;datasize&nbsp;)&nbsp;fprintf(stdout,&quot;Wrote&nbsp;%d&nbsp;bytes\n&quot;,bwrote);<br>
 &nbsp;}<br>
 &nbsp;__finally&nbsp;<br>
 &nbsp;{<br>
 if&nbsp;(sockRaw&nbsp;!=&nbsp;INVALID_SOCKET)&nbsp;closesocket(sockRaw);<br>
 WSACleanup();<br>
 &nbsp;}<br>

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -