
개요
많은 서비스가 k8s등 container 환경에서 서비스 운영을 하는데, 보안상의 이슈로 privileged 권한은 주지 않는 경우가 많다. 혹은 아예 컨테이너를 VM수준의 격리를 주기 위해 gVisor등으로 운영하는 경우도 존재한다. 이런 경우 syscall 사용에 제약이 걸린 경우가 많다. 예를 들어, 해당 환경에서 cgroup으로 특정 프로세스의 cpu사용률을 제어하는 등의 세밀한 조정을 할 수 없는 경우가 있는데, 이때 기반연산들을 LD_PRELOAD trick으로 적절하게 throttling걸어서 cpu사용률을 강제로 낮출 수 있다. 혹은 dns resolution시 특정 ip에 해당되는 url만 resolution을 허용한다든지 하는 작업도 가능하다.
정의
LD_PRELOAD는 시스템의 동적 링커(ld.so)가 인식하는 Linux나 Unix-like OS의 환경변수이다.
역할은 해당 환경변수가 세팅된 프로그램 실행시 프로그램에서 특정 공유 라이브러리 로드할 때 지정된 라이브러리를 먼저 로드하게 동작한다. (*여러 라이브러리에 같은 이름의 함수가 있으면, 먼저 로드된 함수가 사용되는 것이 expected behavior이다.)
사용법
어떻게 사용하는지는 Claude-Opus-4.1이 잘 알려준다. use-case에 맞게 물어보자.
특정 ip에 해당되는 Dns resolution만 허용: https://poe.com/s/kPidxHKHpbzO6f0njTN7 (주의: 간단하게 리뷰했지만 정확히 동작하는지는 확인하지 않았습니다.)
dns_filter.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <errno.h>
// 허용된 IP 목록
static const char *allowed_ips[] = {
"aaa.bbb.ccc.ddd",
"XXX.YYY.ZZZ.PPP",
NULL
};
// 원본 함수 포인터들
static int (*original_getaddrinfo)(const char *, const char *,
const struct addrinfo *,
struct addrinfo **) = NULL;
static void (*original_freeaddrinfo)(struct addrinfo *) = NULL;
static struct hostent *(*original_gethostbyname)(const char *) = NULL;
// IP 주소가 허용 목록에 있는지 확인
static int is_ip_allowed(const char *ip) {
for (int i = 0; allowed_ips[i] != NULL; i++) {
if (strcmp(ip, allowed_ips[i]) == 0) {
return 1;
}
}
return 0;
}
// getaddrinfo 후킹 - 새로운 접근 방식
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res) {
// 원본 함수 로드
if (!original_getaddrinfo) {
original_getaddrinfo = dlsym(RTLD_NEXT, "getaddrinfo");
}
// 원본 함수 호출
int result = original_getaddrinfo(node, service, hints, res);
if (result == 0 && *res) {
struct addrinfo *curr = *res;
struct addrinfo *new_head = NULL;
struct addrinfo *new_tail = NULL;
// 허용된 IP만 포함하는 새로운 리스트 생성
while (curr) {
char ip_str[INET6_ADDRSTRLEN];
void *addr_ptr = NULL;
int should_include = 0;
// IPv4와 IPv6 모두 처리
if (curr->ai_family == AF_INET) {
struct sockaddr_in *addr_in = (struct sockaddr_in *)curr->ai_addr;
addr_ptr = &(addr_in->sin_addr);
} else if (curr->ai_family == AF_INET6) {
struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)curr->ai_addr;
addr_ptr = &(addr_in6->sin6_addr);
}
if (addr_ptr) {
inet_ntop(curr->ai_family, addr_ptr, ip_str, sizeof(ip_str));
should_include = is_ip_allowed(ip_str);
} else {
// IP 주소를 파싱할 수 없는 경우 포함
should_include = 1;
}
if (should_include) {
// 새로운 addrinfo 노드 생성 (깊은 복사)
struct addrinfo *new_node = malloc(sizeof(struct addrinfo));
if (new_node) {
memcpy(new_node, curr, sizeof(struct addrinfo));
new_node->ai_next = NULL;
// ai_addr 복사
if (curr->ai_addr && curr->ai_addrlen > 0) {
new_node->ai_addr = malloc(curr->ai_addrlen);
if (new_node->ai_addr) {
memcpy(new_node->ai_addr, curr->ai_addr, curr->ai_addrlen);
}
}
// ai_canonname 복사
if (curr->ai_canonname) {
new_node->ai_canonname = strdup(curr->ai_canonname);
}
// 새 리스트에 추가
if (!new_head) {
new_head = new_node;
new_tail = new_node;
} else {
new_tail->ai_next = new_node;
new_tail = new_node;
}
}
}
curr = curr->ai_next;
}
// 원본 리스트 해제 (원본 freeaddrinfo 사용)
if (!original_freeaddrinfo) {
original_freeaddrinfo = dlsym(RTLD_NEXT, "freeaddrinfo");
}
original_freeaddrinfo(*res);
*res = new_head;
// 모든 결과가 필터링된 경우 에러 반환
if (!new_head) {
return EAI_NONAME;
}
}
return result;
}
// freeaddrinfo 후킹 (우리가 생성한 구조체를 올바르게 해제)
void freeaddrinfo(struct addrinfo *res) {
if (!original_freeaddrinfo) {
original_freeaddrinfo = dlsym(RTLD_NEXT, "freeaddrinfo");
}
// 우리가 할당한 메모리인지 확인하는 방법이 필요
// 간단한 방법: 모든 노드를 수동으로 해제
struct addrinfo *curr = res;
while (curr) {
struct addrinfo *next = curr->ai_next;
if (curr->ai_addr) {
free(curr->ai_addr);
}
if (curr->ai_canonname) {
free(curr->ai_canonname);
}
free(curr);
curr = next;
}
}
// gethostbyname 후킹
struct hostent *gethostbyname(const char *name) {
if (!original_gethostbyname) {
original_gethostbyname = dlsym(RTLD_NEXT, "gethostbyname");
}
struct hostent *result = original_gethostbyname(name);
if (result && result->h_addr_list[0]) {
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, result->h_addr_list[0], ip_str, sizeof(ip_str));
if (!is_ip_allowed(ip_str)) {
// 차단된 IP인 경우 NULL 반환
h_errno = HOST_NOT_FOUND;
return NULL;
}
}
return result;
}
// 초기화 함수
__attribute__((constructor))
void init(void) {
char *env_ips = getenv("ALLOWED_IPS");
if (env_ips) {
fprintf(stderr, "[DNS Filter] Initialized with allowed IPs\n");
}
}
컴파일:
# 공유 라이브러리 컴파일
gcc -shared -fPIC -o dns_filter.so dns_filter.c -ldl
# 디버그 심볼 포함 (선택사항)
gcc -shared -fPIC -g -o dns_filter.so dns_filter.c -ldl
사용법:
# 단일 명령어 실행
LD_PRELOAD=./dns_filter.so curl http://example.com
# 환경변수로 설정
export LD_PRELOAD=/path/to/dns_filter.so
./your_application
# 허용 IP를 환경변수로 동적 설정 (코드 수정 필요)
ALLOWED_IPS="1.2.3.4,5.6.7.8" LD_PRELOAD=./dns_filter.so ./your_app
소켓 통신: https://poe.com/s/ZsFbygdHrQlZ9Kl7WvrW
위 내용은 길어서 본문에 첨부하진 않겠다.
보안 고려사항
- setuid/setgid 바이너리에서는 보안상 LD_PRELOAD가 무시된다.
- 신뢰할 수 없는 LD_PRELOAD 라이브러리는 보안 위험이 될 수 있다.
- 프로덕션 환경에서는 라이브러리 파일 권한 관리가 중요하다.
장단점
장점:
- 소스 코드 수정 없이 프로그램 동작 변경 가능하다
- 바이너리만 있는 프로그램도 수정 가능하다
- 런타임에 동적으로 적용/해제 가능하다
- 시스템 전체가 아닌 특정 프로세스에만 선택적 적용 가능하다
단점:
- Application layer에서 동작하기 때문에 유저 코드가 실행되는 환경에서는 유저가 다시 해당 환경변수를 재지정해서 의도하지 않은대로 동작하게 수정할 수 있다.
- 유지 보수를 위한 복잡성이 매우 증가한다.
- 최후의 수단으로만 사용하자.
'개발 > 개발 상식' 카테고리의 다른 글
| Blocking vs Non-Blocking (3) | 2023.12.03 |
|---|