<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>PAISL</title><description>피지컬 인공지능 보안 연구반</description><link>https://blog.paisl.cloud/</link><language>ko</language><item><title>C언어 구조체 설계와 Opaque Pointer(불투명 포인터)</title><link>https://blog.paisl.cloud/posts/002/</link><guid isPermaLink="true">https://blog.paisl.cloud/posts/002/</guid><description>많은 사람들이 구조체를 단순히 데이터 묶음 정도로만 사용한다. 실제 개발시에는 구조체를 통해 책임분리, 인터페이스 설계, 캡슐화 까지 하게 된다. 필자는 이번 글에서 SRP를 기준으로 구조체를 어떻게 설계해야 하는지, 그리고 C언어 특유의 Opaque Pointer 패...</description><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;많은 사람들이 구조체를 단순히 데이터 묶음 정도로만 사용한다. 실제 개발시에는 구조체를 통해 책임분리, 인터페이스 설계, 캡슐화 까지 하게 된다. 필자는 이번 글에서 SRP를 기준으로 구조체를 어떻게 설계해야 하는지, 그리고 C언어 특유의 Opaque Pointer 패턴을 이용해 객체지향적인 모듈화를 어떻게 달성하는지 설명하도록 하겠다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;C 구조체 설계에 SRP 적용하기&lt;/h1&gt;
&lt;p&gt;SRP는 Single Responsibility Principle 으로 단일 책임 원칙이다. 쉽게 설명하면 하나의 구조체는 하나의 목적만 가지도록 하는것이다. 예시를 통해 알아보도록 하겠다.&lt;/p&gt;
&lt;h2&gt;책임이 섞인 구조체&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
	char name[32];
    int score;
    int level;
    int socket_fd;
    time_t last_login;
} Player;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 예시는 데이터(이름, 점수)와 네트워크 상태(socket_fd)가 섞여있다. 이러한 구조체는 수정할 때 의존성 문제가 생기므로 유지보수성이 악화된다.
그럼 좋은 예시를 설명하도록 하겠다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
	char name[32];
    int score;
    int level;
} PlayerProfile;

typedef struct {
	int socket_fd;
    time_t last_login;
} PlayerSession;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런식으로 분리해서 코드를 작성하면 각각의 변경이 서로 영항을 주지 않고, 컴파일 의존성도 분리되므로 유지보수성이 좋다.&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;h1&gt;구조체와 모듈화를 제대로 하기&lt;/h1&gt;
&lt;p&gt;C에서는 클래스가 존재하지 않는다 그러나 구조체 + 함수 집합 을 하나의 모듈로 묶어서 비슷한 효과를 얻을 수 있다.
모듈화의 기본 패턴은 구조체 선언은 헤더파일(.h) 파일에서 작성하고 구조체 조작 함수는 C언어 파일(.c) 파일에서 작성한다. 또한 캡슐화를 통해 외부에서 내부를 모르게 한다.&lt;/p&gt;
&lt;h1&gt;Opaque Pointer 패턴으로 캡슐화 하기&lt;/h1&gt;
&lt;p&gt;Opaque Pointer는 불투명 포인터 이다.
불투명 포인터란 구조체 내부를 헤더에 숨기고, 포인터만 노출하는 설계 방식이다. 기존 전통적인 공개 구조체 방식은 라이르러리 내부 구조 변경 시 모든 의존 코드도 재컴파일 해야하는 문제가 발생한다.&lt;/p&gt;
&lt;h2&gt;Opaque Pointer 방식&lt;/h2&gt;
&lt;p&gt;예제코드를 통해 불투명 포인터 방식을 살펴보도록 하곘다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// player.h
typedef struct Player Player;  //선언만 하여 구현은 노출되지 않음

Player* player_create(const char* name);
void player_set_score(Player* p, int score);
void player_destroy(Player* p);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// player.c
struct Player {
    char name[32];
    int score;
};

Player *player_create(const char *name) { ... }
void player_set_score(Player *p, int score) { ... }
void player_destroy(Player *p) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런식으로 구현된 불투명 포인터 방식은 헤더파일에는 구조체의 선언만 있고, 구조체의 정의는 .player.c 파일 안에만 있기 때문에 외부 코드(외부모듈, 다른 소스 파일)는 구조체의 내부 구성을 절대 알 수 없다.&lt;/p&gt;
&lt;h3&gt;왜 그렇게 되는것일까?&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;typedef struct Player Player
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이건 Player 라는 구조체 타입이 존재한다. 하지만 구체적으로 어떻게 생겼는지는 모른다 라는 의미이다.
이를 Incomplete Type(불완전 타입)이라고 부른다.
불완전 타입은 포인터 연산(포인터 선언, 포인터 전달)은 가능하지만 구조체 멤버 접근이나 크기는 불가는 하다. 그렇기 때문에 외부 소스 파일에서는 이 구조체 Playe의 구성을 알 수 없다. 예제 코드로 설명하겠다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include &quot;player.h&quot;

int main(){
	player* p = player_create(&quot;name&quot;);
    p-&amp;gt;score = 100;  // 컴파일 에러
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다음 코드에서는 컴파일 에러가 발생한다. 왜냐하면 Player 구조체 내부에 score 라는 맴버가 있다는걸 모르기 때문에 그렇다. 그러므로 외부에서는 포인터를 생성, 피괴, 포인터를 함수의 인자로 넘기기만 할 수 있다. 내부 구현은 전혀 모른 채 오직 함수 인터페이스만 사용할 수 있는 것이다.
컴파일러의 관점에서 구조체가 완전히 정의된 곳(palyer.c)만 sizeof(Player)를 알 수 있다. 외부에서는 sizeof(Player)계신이 안되므로 메모리를 직접 할당하거나 구조체를 직접 접근할 수 없다. 링커의 관점에서는 함수의 심볼만 연결한다. 구조체와 레이아웃에는 전혀 관여하지 않는것이다. 따라서 불투명 포인터를 사용하면 링커  수준에서도 완벽하게 숨겨지는 것이다. 따라서 불투명 포인터 방식은 C언어에서 가능한 가장 강력한 캡슐화 기법이다.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;이번 글은 여기까지이다. 다음 글에서는 SRP + Opaque Pointer로 모듈 설계하기에 대해서 다루도록 하겠다.&lt;/p&gt;
</content:encoded></item><item><title>Buffer Overflow &amp; Stack Canary 공격부터 방어 기법까지</title><link>https://blog.paisl.cloud/posts/001/</link><guid isPermaLink="true">https://blog.paisl.cloud/posts/001/</guid><description>C언어는 성능이 좋지만 메모리 경계를 직접 관리해야 한다. 그 결과 스택 기반 버버 오버플로우든 30년이 넘은 지금도 여전히 실효성이 높은 취약점이다. 이를 방지하기 위해 등장한 기술 중 하나가 Stack Canary 이다. 하지만 Stack Canary는 만능이 아니...</description><pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;C언어는 성능이 좋지만 메모리 경계를 직접 관리해야 한다. 그 결과 스택 기반 버버 오버플로우든 30년이 넘은 지금도 여전히 실효성이 높은 취약점이다. 이를 방지하기 위해 등장한 기술 중 하나가 Stack Canary 이다. 하지만 Stack Canary는 만능이 아니다. 해커들은 이를 Canart Leak을 하거나 우회할 수 있다. 따라서 필자는 이번 글에서 버퍼 오버플로우의 메모리 구조적 원리와 Stack Canary의 내부 동작 원리와 우회 기법, 방어 전략까지 설명하도록 하겠다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;1. 스택 카나리의 내부 동작 원리&lt;/h1&gt;
&lt;p&gt;x86_64 기준 스택 프레임 구조는 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Return Address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Saved RBP (Base Pointer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;canary(4~8 bytes) &amp;lt;- 보호장치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local buffer  &amp;lt;- 오버플로우 발생 위치&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;버퍼 오버플로우가 발생하면 스택이 위로 확장되며 Canary와 RBP, RET를 차례로 덮는다. 해커는 이 구조를 통해 흐름 탈취를 시도한다.&lt;/p&gt;
&lt;h2&gt;1.1 스택 카나리의 삽입 검사&lt;/h2&gt;
&lt;p&gt;스택 카나리는 다음과 같은 과정으로 동작한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;함수 시작 시 __stack_chk_guard 값이 TLS에서 로드되어 스택에 저장된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;함수 종료 시 스택에 저장된 값과 TLS의 현재 Canary 값이 비교된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;불일치시 __stack_chk_fail() 함수가 호출되어 프로그램이 종료된다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.2 카나리의 특성&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;일반적으로 카나리는 0x00XXXXXXXXXX 형식으로 NULL로 시작하여 문자열 기반 함수의 우회를 방지한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;x86_64 아키텍처에서는 %fs:0x28 오프셋을 통해 TLS에서 Canary 값을 로드한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;2. 스택 카나리 우회 기법&lt;/h1&gt;
&lt;h2&gt;2.1 카나리 릭 (Canary Leak)&lt;/h2&gt;
&lt;p&gt;해커는 포맷 문자열 취약점이나 메모리 덤프를 통해 스택에 저장된 값을 유출할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cahr buf[128];
gets(buf);
printf(buf);   // &quot;%19$lx&quot; -&amp;gt; Canary 값 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드에서 printf(buf);는 사용자가 입력한 문자열을 포맷 문자열 포인터 자체로 사용한다. 따라서 해커가 %19$lx 같은 포맷 문자열 지정자를 입력하면 printf()는 현재 스택의 특정 위치에 있는 데이터를 16진수로 출력하게 된다.&lt;/p&gt;
&lt;h3&gt;2.1.1 포맷 문자열 접근 원리&lt;/h3&gt;
&lt;p&gt;C언어 가변 인자 함수(printf, sprintf 등)는 스택에 인자들이 순서대로 푸시된다. 포맷 문자열 내 %N$lx는 스택에 있는 N번째 인자 위치의 값을 출력하라는 의미이다.&lt;/p&gt;
&lt;h3&gt;2.1.2 스택 레이아웃&lt;/h3&gt;
&lt;p&gt;x86_64의 스택 레이아웃은 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;x86_64&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;buf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;padding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;canary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;saved rbp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;return address&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;gets(buf)&lt;/code&gt;를 통해 입력값으로 &lt;code&gt;%19$lx&lt;/code&gt; 같은 포맷 스트링이 들어가면 이 값은 buf에 저장되고 이후 &lt;code&gt;printf(buf)&lt;/code&gt;가 호출되면 이 문자열을 포맷 문자열로 해석한다. &lt;code&gt;printf&lt;/code&gt;는 내부적으로 &lt;code&gt;va_arg()&lt;/code&gt; 매크로를 통해 스택 상의 다음 인자들을 접근한다. 따라서 &lt;code&gt;%19$lx&lt;/code&gt;는 &lt;code&gt;printf&lt;/code&gt;가 스택에서 19번째 인자 위치를 읽어 그 값을 16진후 long 값으로 출력하라는 의미이다. 그러므로 스택의 19번째 위치에 Canary가 있을 경우 &lt;code&gt;%19$lx&lt;/code&gt;는 Canary값을 화면에 출력하게 된다.&lt;/p&gt;
&lt;h2&gt;2.2 부분 덮어쓰기 (Partial Overwrite)&lt;/h2&gt;
&lt;p&gt;카나리 값의 일부만 덮어쓰는 기법으로 특히 카나리 값의 첫 바이트가 NULL(0x00)인 경우 문자열 복사 함수들이 이를 문자열의 끝으로 인식하여 전체 카나리 값을 덮어쓰지 못하게 된다. 그러나 memcpy와 같은 함수는 NULL 바이트를 포함한 데이터를 복사할 수 있으므로 이를 이용하여 카나리 값을 부분적으로 덮어쓸 수 있다.&lt;/p&gt;
&lt;h2&gt;2.3 브루트 포싱&lt;/h2&gt;
&lt;p&gt;32비트 시스템에서는 카나리 값의 엔트로피가 낮아(24비트) 브루트 포싱이 현실적으로 가능할 가능성이 있다. 프로세스가 자주 재시작되거나 포크되는 경우 동일한 카나리 값이 재사용 될 수 있으므로 이를 이용하여 카나리 값을 추측할 수 있다.&lt;/p&gt;
&lt;h1&gt;3. 스택 카나리 방어&lt;/h1&gt;
&lt;h2&gt;3.1 포맷 문자열 취약점 방지&lt;/h2&gt;
&lt;p&gt;printf(user_input)과 같은 코드를 찾아 %s와 같은 포맷 지정자를 명시적으로 사용하는지 확인하고 -Wformat -Werror=format-security 플래그를 사용하여 포맷 문자열 취약점을 컴파일 시점에 탐지한다.&lt;/p&gt;
&lt;h2&gt;3.2 카나리 값의 엔트로피 증가 및 보호&lt;/h2&gt;
&lt;p&gt;프로그램 시작 시 고유한 랜덤 카나리 값을 생성하여 사용하고 카나리 값을 TLS에 저장하여 각 스레드마다 독립적인 값을 유지하며 접근은 제한한다.&lt;/p&gt;
&lt;h2&gt;3.3 메모리 보호 기법 활용&lt;/h2&gt;
&lt;p&gt;ASLR(Address Space Layout Randomization), NX(No-eXecute) 비트 설정, RELRO(Read-Only Relocations) 등의 메모리 보호 기법을 사용한다.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;오늘은 버퍼 오버플로우와 스택 카나리에 대해서 알아보았다. 다음 글에서는 새로운 공격 기법과 방어 기법들을 가지고 오도록 하겠다.&lt;/p&gt;
</content:encoded></item></channel></rss>