기 타 안전한 유닉스 프로그래밍을 위한 지침서 V.0.7
2011.12.19 10:35
안전한 유닉스 프로그래밍을 위한 지침서 V.0.7
2001. 1.
박현미/CERTCC-KR
hmpark@{certcc,kisa}.or.kr
시작하면서
이 지침서는 안전한 프로그램을 위한 프로그래머가 지켜야할 설계와 구현 방법에대한 지침서로 어플리케이션 프로그램과 웹 어플리케이션(CGI), 네트워크 서버, setuid/setgid 프로그램 등의 보안 영역(Security Boundary)에 대하여 설명한다.
또한 이 지침서는 프로그래머가 실제 프로그램을 개발하면서 참조할 수 있는 실용적인 지침서로 리눅스나 유닉스 시스템을 기본 템플릿으로 한다. 이 지침서를 읽는 독자는 기본적인 유닉스 시스템의 보안과 C 언어에 대한 이해가 요구되며 이 지침서의 목표가 안전한 프로그래밍(Secure Programming)임을 기억해야 한다.
지침서는 계속 업데이트되며 수정하거나 추가할 사항이 있으면 언제든지 cert@certcc.or.kr이나 hmpark@certcc.or.kr로 연락주기 바란다.
Ⅰ. 프로세스 보안
유닉스 시스템은 윈도우 시스템과는 달리 파일이나 프로세스의 권한을 설정하는 특별한 속성들을 가지고 있다. 이러한 속성들은 안전한 프로그램을 작성하는데 직접적으로 영향을 미치므로 유닉스의 속성에 대하여 이해하고 이것을 안전하게 프로그램에 작성하는 것이 필요한다.
1. SUID/EUID 보안
1.1 SUID와 EUID, SUID
(1) Real UID, effective UID와 saved UID
유닉스 시스템에서 파일과 프로세스의 권한을 나타낸 것으로 다음과 같은 속성이 있다.
? Real UID와 GID(RUID와 RGID)
프로세스가 실행될 때 사용자의 실제 UID와 GID를 나타내는 용어로 특히 RUID가 0인 것은 파일시스템에 대한 모든 권한을 가진 사용자로 root라고 한다. root는 대부분의 중요한 보안 사항을 체크하고 수정하고 시스템을 관리할 수 있으므로 다른 RUID보다 더 큰 권한을 가지고 있기 때문에 보안상으로 중요하다.
? Effective UID와 GID(EUID와 EGID)
누구의 권한으로 프로세스가 실행하는가를 나타내는 용어로 이것은 프로세스가 실행될 때 누구의 권한으로 실행되는지를 나타내므로 보안에서 특히 문제가 되는 부분이다. /sbin/passwd 프로그램처럼 EUID가 root로 실행될 경우에 주의해야 한다.
? Saved UID와 GID(SUID와 SGID)
프로그램에 의해 변하기 전의 UID를 나타내는 것으로 권한 교환을 허용하고 허용하지 않음을 지원하기 위하여 사용한다.
(2) SUID와 SGID 프로그램
프로그램들이 파일과 프로세스들에 접근하는 것을 허락하는 것으로 setuid는 사용자의 권한을 임시적으로 바꿔준다. 즉, 권한이 없는 사용자가 특별한 권한을 요구하는 작업을 해야할 경우에 사용한다. 이러한 특별한 권한은 이 프로그램이 실행하는 동안에만 영향을 받고 프로그램이 끝나면 원래의 사용자 권한으로 돌아오게 된다. SUID/SGID 프로그램은 setuid/setgid 비트 s로 표시한다.
(예제 1) SUID/SGID 프로그램 - 패스워드 프로그램
패스워드 프로그램에서 패스워드 파일은 root만이 수정할 수 있다. 그런데 일반 사용자가 자신의 패스워드를 수정하기 위해서는 root의 권한이 필요하다. 이 때 패스워드 프로그램을 SUID 프로그램으로 하여 일반사용자도 잠시동안 root의 권한을 가져 패스워드 파일을 수정할 수 있게 한다.
[cert:root]:/user/staff> ls -la /etc/passwd
-rw-r--r-- 1 root sys 2657 10월 5일 14:08 /etc/passwd
[cert:root]:/user/staff> ls -la /bin/passwd
-r-sr-sr-x 3 root sys 96796 1997년 7월 16일 /bin/passwd*
1.2 SUID/SGID 프로그램의 위험성
SUID 프로그램은 실행될 때 프로그램 소유자의 권한으로 수행되므로 잘 못 사용될 경우에 위험할 수도 있다. /bin/sh은 쉘을 실행하는 프로그램으로 일반적으로 사용자가 로그인하였을 때 실행되어 사용자는 쉘 상에서 유닉스 명령어를 사용할 수 있다.
다음은 setuid 된 /bin/sh 프로그램의 실행을 보여주는 예이다. 처음 사용자의 권한은 일반사용자 hmpark을 가지지만 setuid 된 /bin/sh을 실행하고 난 후에는 /bin/sh의 소유자의 권한인 root로 실행되는 것을 확인할 수 있다.
[hmpark@linux80 ~]$ whoami
hmpark
[hmpark@linux80 ~]$ ls -la /bin/sh
-rwsr-xr-x 1 root root 378024 10월 8 1999 /bin/sh*
[hmpark@linux80 ~]$ /bin/sh
[hmpark@linux80 hmpark]# id
uid=504(hmpark) gid=504(hmpark) euid=0(root) groups=504(hmpark)
[hmpark@linux80 hmpark]# whoami
root
위에서 보는 바와 같이 setuid 비트가 설정되어 있는 프로그램이 실행하는 경우 그 프로세스의 UID는 실제 파일을 실행하는 사용자의 권한을 가지지만 프로세스의 EUID는 setuid 된 프로그램을 실행한 사용자의 권한을 가지는 것을 알 수 있다.(uid=504(hmpark), euid=0(root)) 즉, 위의 /bin/sh이라는 프로세스는 hmpark라는 UID를 가지지만 EUID는 /bin/sh의 실제 소유자의 권한을 가지므로 root가 되는 것이다. 그래서 이 쉘 프로세스는 root의 권한으로 실행된다는 것을 알 수 있다.
위와 같은 상황에서 SUID 프로그램에서 발생할 수 있는 위협에 대하여 생각해 보아야 한다. root 권한을 가진 쉘이 실행될 때 쉘이 끝내기 전까지는 root로 setuid된 상태가 계속된다. 이때 공격자는 root 권한으로 할 수 있는 모든 작업을 할 수 있기 때문에 시스템에 치명적인 피해를 입힐 수도 있다.
1.3 안전한 SUID/SGID 프로그램 원리
? UID와 GID를 가능한 제한한다.
setuid 프로그램을 실행할 때 가능한 UID와 GID가 낮은 권한을 가지도록 해야 한다. root로 setuid된 프로그램이 침범 당하면 모든 시스템을 파괴할 가능성이 있지만 일반 사용자로 setuid된 프로그램은 일반 사용자의 권한만을 침범당하므로 피해가 적기 때문이다.
? exec를 호출하기 전에 effective UID와 GID를 재 설정한다.
일반적으로 popen이나 system과 같은 라이브러리 서브루틴이 실행될 때 내부적으로 exec 함수가 호출된다. 그런데 대부분의 프로그래머는 이러한 사실을 인식하지 못한채 그냥 서브루틴 함수를 사용하는 경우가 있다. 이 프로그램이 setuid root인 프로그램이라면 여기에서 실행되는 쉘도 권한이 root인 쉘이 실행될 것이다. 그러므로 exec 함수가 호출되기 전에 effective UID와 GID를 재 설정하는 것이 중요하다.
? exec를 호출하기 전에 모든 파일 기술자를 닫는다.
setuid 프로그램이 중요한 파일을 읽을 경우에 exec된 프로그램도 그 중요한 파일을 읽을 수가 있다. 그러므로 이것을 방지하기 위해서는 exec가 발생할 때마다 중요한 파일을 닫도록 하는 flag를 설정해야 한다. 이 flag는 파일이 열리자마자 즉시 설정되어야 한다.
즉 sfd가 중요한 파일의 기술자인 경우
fcntl(sfd, F_SETFD, 1)
ioctl(sfd, FIOCLEX, NULL)
의 명령들은 exec가 실행될 때 파일을 닫도록 한다.
? root가 확실하게 제한되어있는지 다시 확인하라.
chroot()는 새로운 루트 디렉토리를 설정하여 chroot된 프로세스가 디렉토리의 상위 디렉토리에 접근할 수 없게 해주는 역할을 하는 함수이다.
chroot("/usr/riacs")
이 함수를 사용하여 프로세스가 접근할 수 있는 영역을 미리 제한하여 파일 시스템의 임의의 파일을 읽거나 쓸 수 있는 문제점에 대한 보안 환경을 제공해 준다.
ln -s /usr/demo /usr/riacs/demo
그런데 /usr/demo를 /usr/riacs/demo에 링크시키면 제한되어 있는 디렉토리까지 침범할 수 있는 위험이 있으므로 주의해야 한다. 즉, 위의 링크에서 /usr/riacs는 "/"로 해석되기 때문에
cd /demo
cd ..
명령 후에 /usr 디렉토리에 접근할 수 있게 된다. 그러므로 chroot하여 생긴 새로운 루트 디렉토리의 서브디렉토리에서는 링크된 디렉토리를 사용하지 않아야 한다.
? 실행될 프로세스의 환경을 검사하라.
많은 환경변수은 부모 프로세스로부터 상속된 PATH나 IFS, umask와 같은 변수들에 의해 좌우된다.
? 최소한의 권한 원리
권한을 일시적으로 낮추거나 권한을 완전히 제거하는 것은 잘못된 권한 설정으로 인하여 발생할 수 있는 결점을 최소화 할 수 있는 방법이다.
? 외부의 입력값을 믿지 말아라.
외부에서 입력된 값은 충분하게 검사하고 필요없는 값들을 지운 후에 유효한 값으로 평가되었을 경우에 사용하도록 한다.
2. 새로운 프로세스의 생성 보안
2.1 프로세스 실행시 위험성
생성한 프로세스는 exec 계열 시스템 함수를 이용하여 새로운 프로그램을 실행시키는데 특히 system(), popen() 함수는 새로운 프로그램을 실행시킬 때 setuid나 네트워크 서비스처럼 특별한 권한을 요구하는 프로그램인 경우에는 특별히 유의해야 한다.
2.2 안전한 프로그램 원리
? system(), popen() 함수를 사용하지 않는다.
SUID 프로그램이나 네트워크 서비스 프로그램은 특별한 권한을 가지고 실행된다. 그런데 system(), popen() 함수는 쉘 인터프리터인 /bin/sh를 실행하여 다른 프로그램을 실행하기 때문에 위험하다. 그러므로 대신 execl()이나 execv() 시스템 함수를 사용해야 한다.
? 모든 파일 기술자를 닫았는지 꼭 확인한다.
새로 생성된 자식 프로세스는 부모 프로세스로부터 파일 기술자의 복사본을 가지므로 부모 프로세스가 중요한 파일(ex, /etc/passwd)을 열었을 경우 이 파일의 파일 기술자 상속받기 때문에 파일이 노출될 수 있다. 그러므로 파일 기술자를 열었을 경우에는 자식 프로세스가 생성되기 전에 모든 파일 기술자를 닫아야 한다.
? 프로그램을 실행할 때 전체 경로 이름을 사용하는지 확인한다.
상대 경로를 사용하여 프로그램을 실행하였을 경우에 임의의 프로그램이나 트로이잔 프로그램을 실행될 수 있기 때문에 꼭 절대 경로를 사용하도록 한다.
? 자식 프로세스에 전달된 환경변수를 확인한다.
자식 프로세스는 부모 프로세스로부터 환경변수를 상속받는다. 그런데 환경변수가 수시로 정의될 수 있으므로 위험하다. 그러므로 환경변수를 상속받을 경우에는 꼭 필요한 환경변수를 상속받도록 하고 다른 환경변수는 깨끗한지 확인하도록 한다.
Ⅱ. 파일시스템 보안
1. 디폴트 권한 설정 보안 - umask
1.1 umask의 위험성
생성된 파일이나 실행되는 프로그램은 디폴트 파일 권한을 가지고 설정된다. 이러한 디폴트 권한은 프로세스 umask에 의해서 설정되는데 이 권한은 부모 프로세스나 로그인 쉘에의해 상속받는다. 그런데 umask가 안전하지 않은 권한으로 설정된 경우 허가되지 않은 사용자에게까지 파일이나 프로세스에 접근을 허락하여 보안 문제를 야기할 수 있다.
1.2 안전한 umask 사용 원리
디폴트로 umask는 022로 설정되어 있는데 umask를 수정하기 위해서는umask()라는 라이브러리 호출 함수를 사용한다.
umask 사용자 접근 그룹 접근 다른 사용자
0000 all all all
0002 all all read, execute
0007 all all none
0022 all read, execute read, execute
0027 all read, execute none
0077 all none none
[표-1] 일반적인 umask 설정값
2. 입력 시간 제한
특히 네트워크에서 들어오는 자료에는 타임 아웃과 로드 한도를 제한해야 한다. 만약 그렇지 않으면, 서비스를 끊임없이 요청하는 서비스 거부 공격을 쉽게 초래할 수 있을지도 모른다.
3. 안전한 임시 파일 사용 보안
3.1 임시(tmp) 파일의 위험성
임시 파일은 /tmp 디렉토리에 저장되는 파일로 이 파일에 접근하기 위해 특별한 권한이 필요하지 않고 또한 이름을 예측하기 쉽고 잘 알려져 있다. 그래서 프로그램들은 이 파일에 접근하여 파일을 연 후 파일안에 어떤 데이터들을 삽입하기 쉽다.
(예제 2) 임시 파일을 이용한 공격
① root로 실행되는 시스템 프로그램을 공격자가 실행시킨다. 이 시스템 프로그램은 tmp 디렉토리 안의 임시 파일(/tmp/program.temp)을 연다.
② 공격자는 이 임시 파일을 중요한 파일(권한있는 사용자가 소유한 파일, /etc/passwd)에 링크를 건다.
> ln -s /etc/passwd /tmp/program.temp
이렇게 링크를 걸면 공격자가 임시파일에 임의의 데이터를 쓸 경우에 링크에의해 실제 써지는 파일은 /etc/passwd 파일이 되어 passwd 파일에 악의적인 정보가 삽입되게 된다.
(예제 3) 임시 파일을 이용한 공격
① 임시 파일을 처리하는 권한이 없는 프로그램을 실행시킨다.
② 중요한 파일에 링크를 건다.
> ln -s /etc/passwd /tmp/program.temp
③ 신뢰할 수 있는 사용자가 이 프로그램을 실행한다.
이렇게 하면 공격 (예제 2)에서처럼 임시파일에 쓰나 실제로는 중요한 파일(/etc/passwd) 파일에 써지게 된다. 공격 (예제 1)과 (예제 2)의 차이점은 프로그램이 (예제 1)에서는 root의 권한으로 실행되고 (예제 2)에서는 일반 사용자의 권한으로 실행된다는 것이다.
3.2 안전한 임시파일 사용 프로그램 원리
? /tmp 디렉토리안에 임시 파일을 생성하지 말아라.
? 임시파일을 생성하는 인터페이스를 제공하는 시스템을 사용한다.
임시파일을 생성하기 위한 많은 함수들이 제공되는데 발생할 수 있는 보안 문제에 대비하기 위하여 주의깊게 사용해야 한다.
- tmpfile()의 사용
FILE *tmpfile(void);
tmpfile() 함수는 임시 파일을 생성하여 파일 스트림에 파일 기술자를 리턴한다. 특히 tmpfile() 함수는 mkstemp() 함수를 이용하여 임시 파일을 생성하고 바로 파일을 unlink() 하기 때문에 레이스컨디션의 발생을 피할 수 있다.
? 임시 파일의 이름을 예측할 수 있는 이름으로 생성하지 말고 랜덤하게 생성하라.
- mkstemp() 함수의 사용
int mkstemp(char *template);
mkstemp() 함수는 매개변수로 임시 파일의 형식을 입력받아 랜덤한 값을 이용하여 랜덤한 임시 파일 이름을 생성한다.
fd = mkstemp("/tmp/tempfileXXXXXX");
또한 이 함수는 임시 파일이름의 생성과 파일의 열기 사이에서 발생할 수 있는 레이스컨디션 오류를 방지할 수 있다.
- mktemp() 함수의 사용
char *mktemp(char *template);
mktemp() 함수도 mkstemp() 함수와 같이 임시 파일의 형식을 매개변수로 입력받안 랜덤한 파일 이름을 생성한다. 그러나 대부분의 시스템이 랜덤값으로 PID를 사용하기 때문에 파일의 이름을 쉽게 예측할 수 있어 레이스컨디션 공격을 받기 쉽다.
filename = mktemp("/tmp/tempfileXXXXXX");
또한 이 함수는 임시 파일을 생성한 후 바로 이 파일을 열어야 한다. 만약 다음과 같은 호출로 파일을 열었을 경우
open(filename, O_WRONLY|O_CREAT, 0644);
파일이 이미 존재할지라고 파일을 생성하기 때문에 위험하다. 그렇기 때문에
open(filename, O_WRONLY|O_CREAT|O_EXCL, 0644);
은 파일이 이미 존재할 때 호출이 실패하기 때문에 더 안전한 프로그램을 작성할 수 있다.
? 임시 파일을 생성하기 위한 /tmp 디렉토리 안에 추가 디렉토리를 생성한다. mktemp() 함수를 이용하여 임시 파일을 생성할 때 파일 이름을 디렉토리 이름으로 사용할 수 있다.
3.3 /tmp 디렉토리 보안
가. /tmp 디렉토리에서 발생할 수 있는 위험성
유닉스 시스템 /tmp 디렉토리에 임의의 크기를 가진 파일을 생성하도록 하는데 /tmp 디렉토리의 할당량을 검사하지 않아 한 사용자가 /tmp 디렉토리의 모든 용량을 사용하여 다른 사용자가 파일을 생성할 수 없도록 하는 문제점을 가지고 있다.
나. /tmp 디렉토리의 보안 원리
? /tmp 디렉토리의 용량을 검사하여 한 사용자가 /tmp 디렉토리 용량의 40% 이상을 차지할 수 없게 한다.
? /tmp 디렉토리를 모니터링하는 프로세스를 생성하여 시스템 관리자에게 통보할 수 있게 한다.
Ⅲ. 자원 보안
1. 시스템 자원의 할당과 자원 제한의 중요성
유닉스 시스템에서 자원에 대한 파일시스템 할당량(filesystem Quota)과 프로세스 자원 제한(process resource Limit)을 두는 것은 각 사용자가 사용할 수 있는 자원에 제한을 두는 것으로 저장(storage) 블록 수나 사용할 수 있는 유일한 파일(inode) 수를 제한하여 사용자나 그룹에 대한 한계를 설정할 수 있다. 이러한 제한은 의미상으로 약간의 차이가 있는 'hard'와 'soft' 제한으로 나눌 수 있는데 'hard'는 제한에 대하여 한계를 넘을 수 없는 제한이고 'soft' 제한은 한계을 임시적으로 넘을 수 있는 것이다. quota(), quotactl(), quotaon()과 같은 함수를 이용할 수 있다.
시스템 자원에 대한 할당량을 제한하는 것은 서비스 거부 공격(Denial of Service Attack)을 막을 수 있게 하는 이점이 있다. 또한 파일의 크기(file size)나 자식 프로세스(child process)의 수, open file의 수 등의 프로세스에 대한 할당을 지원하는 rlimit 메커니즘이 있는데 getrlimit(), setrlimit(), getrusage() 함수를 이용하여 사용할 수 있다.
2. core 파일 보안
보통 core 파일은 유닉스 시스템에서 예외 상황이 발생했을 때 생성되는 파일로 core나 program.core라는 이름을 가진다. 예외가 발생하는 상황은 다음과 같다.
- 프로그램 메모리에 침범당한 경우
- 프로그램 스택에 침범당한 경우
- 유효하지 않은 메모리에 접근하는 경우
- 잘못 정렬된 구조체에 접근하는 경우
core 파일은 운영체제에 의해 실행 프로그램의 메모리가 디스크 파일에 쓰여진다. core 파일은 보통 파괴된 프로그램의 상태를 점검하는데 이용한다.
2.1 core 파일의 위험성
? 프로그램의 모든 메모리 내용이 이 파일에 쓰여지므로 중요하고 결정적인 정보등을 저장하고 있다.
? 과거의 어떤 운영체제에서는 core 파일을 점검할 수 없었다. 그래서 core 파일을 중요한 파일에 링크를 걸어 높은 권한을 가진 파일을 SUID/SGID 프로그램을 실행시킴으로써 중요한 파일을 가져올 수 있었다.
2.2 안전한 core 파일 원리
? 예외가 발생했을 경우 core 파일을 생성하지 못하게 제한하는 setrlimit() 함수를 사용한다.
int setrlimit(int recource, const struct rlimit *rlp);
이 함수는 RLIMIT_CORE라는 자원의 타입을 사용하여 생성되는 core 파일의 크기를 설정할 수 있다. 그래서 파일의 크기를 0으로 설정하면 core 파일은 생성되지 않는다.
<프로그램-1> core 파일이 생성되지 않는 프로그램 int nocore()
{
struct rlimit rlp;
rlp->rlim_cur = 0;
rlp->rlim_max = 0;
return(setrlimit(RLIMIT_CORE, &rlp));
}
Ⅳ. 입력값 평가
입력된 값으로 인하여 공격자로부터 공격을 받을 수 있다. 그래서 입력된 값은 그 값이 사용되기 전에 제거되어야 하는데 이 장에서는 신뢰할 수 없는 값이 입력되는 방법을 알아보고 각 입력을 처리하여 안전한 프로그램을 작성할 수 있도록 하는 방법을 제시하고자 한다.
입력값을 처리하기 위해서는 먼저 어떤 값이 적합한지 규칙을 정해야 한다. 그래서 입력값이 정의된 규칙에 맞지 않으면 제거하고 규칙에 합당한 값만을 입력받아야 한다. 그런데 역으로 적합하지 않은 값을 정하고(what is illegal?) 규칙에 맞지 않는, 적합한 값을 제거하는 방법은 생각하지 못했던 치명적인 오류들을 그냥 지나칠 수 있으므로 이런 방법으로 규칙을 정하지 않아야 한다. 또 생각해야 할 것은 입력되는 값의 최대 길이에 제한을 두는 것이다. 제한을 두지 않은 입력값은 대표적인 공격 방법인 버퍼오버플로우 취약점의 원인이 될 수 있기 때문이다.
1. 명령어 라인 보안
많은 프로그램은 인수(argument)로 전달된 입력값을 받아들이는 명령어 라인(command line)을 제공한다. 그런데 SUID/SGID 프로그램은 신뢰할 수 없는 사용자가 명령어 라인 인터페이스를 사용할 수 있으므로 안전하지 않을 수 있다. 그러므로 SUID/SGID 프로그램은 명령어 라인 입력값에 대하여 확인해야 하고 명령어 라인 매개변수에 의해 전달된 프로그램의 이름을 믿지 말아야 한다.
2. 리턴 값 보안
에러 상태를 리턴할 수 있는 모든 시스템 호출 함수는 제한된 자원을 요구하거나 사용자가 자원에 영향을 미칠 수 있으므로 항상 에러 상태를 검사해야 한다.
이러한 보안 기능으로 Setuid/Setgid 프로그램은 자원의 사용을 제한하는 함수인 setrlimit()나 스케줄링 우선권(priority)을 조절하여 명령어를 실행하는 nice() 함수를 프로그램상에서 사용할 수 없도록 제한한다. 또한 서버프로그램의 외부 사용자와 CGI 스크립트는 많은 request 요구하여 자원이 고갈되도록 할 수 있으므로 이에 대한 처리도 해주어야 한다.
3. 유효값(Valid Value) 제한
popen()이나 system()과 같은 시스템 호출 함수는 명령어 쉘(command shell)을 호출하여 실행되는데 이 함수들은 메타문제(metacharacter)에 의해 영향을 받는다. 또한 execlp()와 execvp() 함수도 쉘이 호출된다. 쉘이 호출되면 메타문자는 특별한 의미를 가지고 해석된다. 그래서 이런 메타문자가 입력되어 쉘에 보내지면 프로그램을 파괴할 수 있으므로 메타문제를 제거해야 한다. 쉘에서 특별한 의미를 갖는 메타문자는 다음과 같다.
& ; ` ' \ " | * ? ~ < > ^ ( ) [ ] { } $ \n \r
!
!은 "not"의 의미를 가지고 또 명령어 history에 접근할 수 있게 해준다. bash에서는 상호모드에서 실행하지만 tcsh에서는 스크립트로 인식되므로 문제가 될 수 있다.
#
주석문으로 모든 텍스트가 무시된다.
-
옵션으로 잘못 인식될 수 있도 있고 만약 파일의 이름에서 사용된다면 쉘이 공백문자로 인식할 수 있어 문제가 될 수 있다.
' '
공백문자는 파일이름을 여러개의 인수(argument)로 인식할 수 있다.
'.'과 '='
현재 쉘에서 실행된다는 의미의 '.'과 변수 설정시 사용되는 '='의 사용이 문제가 될 수 있다.
Ⅴ. 환경변수 보안
프로그램에서 환경변수를 사용할 때에는 환경변수에 의존해서는 안된다. 즉, 환경변수의 값을 가정하지 말아야 하고 아니면 모든 환경변수를 설정하는 것이 안전하다. 또한 프로그램에 정보(환경변수)를 전달해야 한다면 필요한 환경변수를 테스트하고 사용후에는 완전히 삭제하도록 한다.
1. 환경변수 설정의 위험성
? 버퍼오버플로우 보안 문제가 발생할 수 있다.
환경변수에 의한 버퍼오버플로운 보안 문제가 가장 흔하게 발생하는 보안 문제로
<프로그램-2> 환경변수 사용의 버퍼오버플로우 ...
char *s, buf[128];
if(!(s = getenv("HOME")))
return -1;
strcpy(buf, s);
...
이 프로그램은 환경변수 HOME의 크기를 고려하지 않고 그냥 128 byte 버퍼에 복사함으로 버퍼오버플로우 보안 문제가 발생할 경우를 보여주고 있다.
? 상속에 의한 문제가 발생할 수 있다.
일반적으로 자식 프로세스에게 환경변수가 상속되므로 환경변수를 자식 프로세스에게 전달할 때 주의해야 한다.
? 잘못 사용한 경우 위험할 수 있다.
IFS는 command line에서 인수(argument)들을 분리하는 문자를 나타내는 환경변수로 흔히 " "(공백문자)를 사용하는데 사용자들에게 친근하지 않은 문자로 설정된 경우 쉘을 호출하는 명령(C에서의 system, popen이나 Perl에서 back-tick 명령어)을 실행하여 쉘을 파괴할 수 있다.
? 문서화가 제대로 되지 않아서 시스템에 익숙하지 않은 사용자는 이런 환경변수를 잘 모르는 경우가 있다.
? 문서화가 잘 되어 있더라도 환경변수가 수정될 수 있어 위험하다.
2. 환경변수 저장 형식
프로그램이 환경변수에 접근하기 위해서는 표준 접근 방법을 사용한다. getenv(), putenv(), setenv(), unsetenv()과 같은 함수들을 이용하여 환경변수를 수정하거나 설정할 수 있는데 execve() 함수를 이용하여 프로그램에 전달되는 환경변수의 데이터 영역을 제어할 수 있어서 위험하다. Linux 시스템의 environ 변수는 환경변수가 어떻게 작동하는지 보여주는 변수로
extern char **environ;
위와 같은 형식을 가지고 있다. 이 environ 변수에 저장되는 값은 NAME=value라는 형태의 스트링인데 환경변수의 이름들이 = 사인을 포함하지 않을 수도 있고 이름(NAME)이나 값(value)이 NIL 문자를 내포하고 있지 않을 수도 있어 위험하다. 또한 같은 이름을 가지고 다른 값을 갖는 변수들이 존재하여 execve()를 사용하여 위험한 상황이 실행되게 할 수도 있다.
3. 환경변수 문제의 보안 원리
안전한 SUID/SGID 프로그램을 작성하기 위해서는 입력값으로 입력되는 환경변수들을 제거하고 모든 환경변수를 삭제한 후 필요한 환경변수는 안전하게 다시 설정해야 한다. 모든 안전하지 않은 환경변수를 알 수 있는 방법이 없기 때문에 프로그램의 소스 코드를 다 확인한다 하더라도 다시 수정될 수 있으므로 안전하지 않다.
3.1 환경변수를 지우는 방법
? environ 변수를 NULL로 설정한다.
environ 변수는 unistd.h에 정의되어 있고 이 헤더파일의 environ 변수를 수정하여 실행하도록 한다.
? clearenv() 함수를 사용한다.
clearenv()는 stdlib.h 헤더파일에 정의되어 있고 사용전에 _USE_MISC이 #define 되어야 한다.
Ⅵ. 버퍼오버플로우 보안
대부분 발생하는 보안 결점은 버퍼오버플로우 문제이다. 버퍼오버플로우는 기술적으로 프로그램 내부의 실행 문제에서 발생하는데 이 문제는 가장 일반적이면서도 심각한 문제이기도 한다. CERT에서는 1998과 1999년부터 계속 논의되어 왔고 Bugtraq에 올라오는 응답의 2/3가 버퍼오버플로우 문제일만큼 오래되고 잘 알려졌지만 계속해서 이슈가 되고 있는 문제이다.
버퍼오버플로우는 고정된 길이의 버퍼에 값을 쓸 때 버퍼의 경계값을 넘어서면서 발생하는데 사용자 입력 값을 읽을 때나 프로그램내에서 처리하는 중간에 발생하기도 한다. 안전한 프로그램이 이런 버퍼오버플로우를 허용하면 C와 같은 언어에서는 공격자가 작성한 악의적인 코드를 강제로 실행하게 치명적인 피해를 입을 수 있다. 버퍼오버플로우 취약점을 "stack smashing"이라고 부르고 힙버퍼에서 발생하는 오버플로우도 간간이 발생하고 있다.
대부분의 프로그래밍 언어는 버퍼오버플로우 문제에 대한 면역기능을 가지고 있다. Perl은 자동으로 배열의 크기를 다시 계산하고 Ada95는 버퍼오버플로우를 탐지하여 막는다. 그러나 C 언어와 C++는 버퍼오버플로우 문제에 대하여 어떠한 보호기능도 제공하지 않아서 문제가 되고 있다.
1. 안전한 함수 사용으로인한 해결책
C언어나 C++의 구조상 경계값을 넘는 것을 차단하지 못하므로 프로그래머는 경계값을 체크하는 않는 함수를 사용하지 않아야 한다.
strcpy(), strcat(), sprintf()(vsprintf()), gets()와 같은 함수는 경계값 체크를 하지 않으므로 strncpy(), strncat(), snprintf(), fget()과 같은 함수로 대체해야 한다. 또한 scanf 계열의 함수들오 위험하므로 최대 입력받을 수 있는 스트링의 길이 제한 없이 사용하지 말아야 한다. realpath()나 getopt()과 같은 함수도 최소한의 PATH_MAX 바이트 길이를 정해주는 getwd() 함수를 사용하는 것이 안전하다.[표 ]
취약한 함수
대체 함수
strcpy()
strcat()
sprintf()(또는 vsprintf())
gets()
strncpy()
strncat()
snprintf()
fget()
scanf() fscanf() sscanf() vscanf() vsscanf() vfscanf()
realpath() getopt() getpass() streadd() strecpy() strtrns()
getwd()
[표-2] 취약한 함수와 대체 함수
2. 각 함수의 안전한 사용
? strcpy()
strcpy() 함수는 버퍼의 크기를 평가하지 않아 문제가 발생하므로 복사할 데이터의 크기를 미리 검사하는 strncpy() 함수를 대신 사용할 수 있다. strncpy() 함수는 NULL 문자로 끝내야 하는데 소스의 버퍼 크기가 복사할 버퍼보다 크거나 같으면 NULL로 끝나지 않을 수 있기 때문이다.
Incorrect
Correct
void func(char *str)
{
char buffer[256];
strcpy(buffer, str);
return;
}
void func(char *str)
{
char buffer[256];
strncpy(buffer, str, sizeof(buffer)-1);
buffer[sizeof(buffer)-1] = 0;
return;
}
<프로그램-3> strcpy() vs. strncpy()
? strcat()
strcat() 함수도 strcpy() 함수와 비슷하게 첨가할 스트링의 길이를 검사하지 않아 문제가 발생하고 대신 strncat() 함수를 사용하여 명시한 길이만큼 원래의 스트링에 덧붙인다. 그리고 NULL 문자로 끝난다.
Incorrect
Correct
void func(char *str)
{
char buffer[256];
strcat(buffer, str);
return;
}
void func(char *str)
{
char buffer[256];
strncat(buffer, str, sizeof(buffer)-1);
return;
}
<프로그램-4> strcat() vs. strncat()
? sprintf()
sprintf() 함수는 포맷 스트링 변수가 사용될 때 버퍼오버플로우 문제가 발생할 수 있고 버퍼에 출력되는 데이터의 길이를 제한하기 위해 snprintf() 함수를 사용한다. 이 함수는 데이터의 길이가 버퍼보다 더 크면 버퍼에 어떤 것도 기록하지 않는다. snprintf() 함수의 리턴값을 확인하여 버퍼에 쓰여진 값을 확인할 수 있다.
Incorrect
Correct
void func(char *str)
{
char buffer[256];
sprintf(buffer, "%s", str);
return;
}
void func(char *str)
{
char buffer[256];
if(snprintf(target, sizeof(target)-1, "%s", string) > sizeof(target)-1)
/*....*/
return;
}
<프로그램-5> strcpy() vs. strncpy()
? gets()
gets() 함수는 길이를 명시하는 부분이 나와 있지 않으므로 오버플로우 문제가 항상 발생할 수 있다. 이 함수는 new-line이나 EOF를 만나거나 버퍼가 다 찰 때까지 표준 입력값을 읽는다. fgets() 함수는 n-1 개의 문자를 읽는다.
Incorrect
Correct
void func(char *str)
{
char buffer[256];
gets(buffer);
return;
}
void func(char *str)
{
char buffer[256];
fgets(buffer, sizeof(buffer)-1, stdin);
return;
}
<프로그램-6> gets() vs. fgets()
? scanf(), sscanf(), fscanf()
지정된 크기의 버퍼를 읽어들이는데 읽어들일 수 있는 최고의 버퍼 길이를 명시해야 한다.
Vulnerable
Safe
char buffer[256];
int num;
num = fscanf(stdio, "%s", buffer);
char buffer[256];
int num;
num = fscanf(stdio, "%255s", buffer);
<프로그램-7> scanf(), sscanf(), fscanf()
? memcpy()
외부의 자료에의해서 memcpy() 함수에서 명시된 길이가 바뀔 때 버퍼오버플로우 문제가 발생할 수 있다.
Incorrect
unsigned long copyaddress(struct hosten *hp) {
unsigned long address;
memcpy(&address, hp->h_addr_list[0], hp->h_length);
}
Correct
unsigned long copyaddress(struct hosten *hp) {
unsigned long address;
if(hp->h_length > sizeof(address))
return 0;
memcpy(&address, hp->h_addr_list[0], hp->h_length);
return address;
}
<프로그램-8> memcpy() 함수
이것은 실제로 BIND에서 나타난 취약점으로 hp->h_length의 길이만큼 address 변수에 복사하는데 hp->h_length 변수는 인터넷 주소의 크기이므로 4 byte이다. 그런데 공격자가 위조된 DNS reply를 스푸핑할 수 있다면 h_length의 값은 더 커져서 address 변수에 더 많은 데이터가 복사되어 버퍼오버플로우가 발생할 수 있다. 이것을 위의 표에서처럼 길이를 검사한 후 복사한다면 해결할 수 있다.
3. 더 생각해야 할 문제
위의 [표-2]에서처럼 취약한 함수를 안전한 대체 함수로 바꿨다고 버퍼오버플로우의 모든 문제가 해결되는 것은 아니라는 사실을 알아야 한다.
? snprintf()와 같은 함수는 ISO 1990(ANSI 1989)의 표준 C 함수가 아니다. 그래서 대부분의 시스템이 sprintf()함수는 지원하더라도 snprintf() 함수는 지원하지 않을 수 있고 지원한다고 하더라도 snprintf() 함수가 바로 sprintf() 함수를 호출하도록 되어 있어 버퍼오버플로우 문제에 대한 보호기능을 제대로 해주지 못하는 경우도 있다. 어떤 버전의 snprintf() 함수에서는 NULL 문자로 끝나도록 보장하지 않아 긴 스트링이 입력되면 NULL 문자로 끝나지 않을 수도 있는데 glib 라이브러리에서는 항상 NULL 문자로 끝나는 g_snprintf() 함수를 지원한다.
? strlen() 함수는 NULL 문자를 만날 때까지 문자열의 수를 계산하는데 NULL 문자로 끝나지 않는 문자열을 입력받을 때는 처리하지 못할 수 있으므로 주의해야 한다.
4. C++의 버퍼오버플로우 문제
C++ 언어도 부가적인 보안 문제점을 가지고 있는데 C 코드의 gets() 함수와 같은 버퍼오버플로우 공격이 발생할 수 있다.
char buf[128];
cin >> buf;
위의 함수는 문자를 읽을 때 어떤 길이 검사도 하지 않아서 버퍼오버플로우 보안 문제가 발생할 수 있다. 버퍼의 최대 입력 길이를 알기 위해서는 cin.width() 멤버함수를 사용할 수 있다.
Ⅶ. 레이스컨디션 보안
접근 권한을 파괴하거나 파일 생성을 파괴할 수 있는 문제를 가지는 경쟁 상태가 보안 문제를 발생시킬 수 있다. 이러한 경쟁 상태는 다음과 같은 상황에서 발생한다.
- 접근 권한 체크나 상태 체크는 파일명을 이용할 때 발생한다.
- 파일 명령은 같은 파일이름의 명령어를 통해 실행된다.
여기서 일어나는 문제는 첫 번째와 두번째 명령어 사이에 접근 권한이나 상태 체크를 체크하거나 다른 파일이 파일 명령어를 참조할 때 공격자가 파일을 위조할 수 있다는 것이다.
일반적으로 이 타입의 공격은 프로그램의 불안정을 이용하기 위해 심볼릭 링크를 이용한다. 불안정한 setuid 있는 루트 프로그램 소스코드의 일부분을 예로 보자.
<프로그램-9> 레이스컨디션이 발생할 수 있는 프로그램 int unsafeopen(char *filename)
{
struct stat st;
int fd;
if (stat(filename, &st) != 0)
return -1;
if (st.st_uid != 0)
return -1;
fd = open(filename, O_RDWR, 0);
if (fd < 0)
return -1;
return fd;
}
위의 프로그램은 다음과 같은 특징을 가지고 있다.
① 파일명이 존재하는지 체크하고, 그 파일이 루트 소유가 확실하면 파일을 만든다.
② 파일을 연다.
두 개의 분리된 시스템 호출이 일어나는데 두 개의 명령 사이에 시간 지연있다. 이 시간 지연내에서 파일과 시스템 특징을 바꾸는 것이 가능하다. 공격자는 아래의 방법으로 가능하게 된다.
① 루트 권한의 파일(/etc/passwd)을 심볼릭 링크(/tmp/filename)로 생성할 수 있다.
② stat()는 심볼릭 링크를 통해 호출한다. 그리고 /etc/passwd의 정보를 되돌린다.
③ 공격자는 심볼릭 링크를 해제한다. 그리고 파일을 자신의 권한으로 한다.
④ 프로그램에서 현재 우연히도 /tmp/filename이 열려 있으면, 루트 권한의 또 다른 프로세스의 파일의 데이터 대신에 자신의 데이터를 읽는다.
<프로그램-10> 레이스컨디션이 발생하지 않는 프로그램 int safeopen(char *filename)
{
struct stat st, st2;
int fd;
if (lstat(filename, &st) != 0)
return -1;
if (!S_ISREG(st.st_mode))
return -1;
if (st.st_uid != 0)
return -1;
fd = open(filename, O_RDWR, 0);
if (fd < 0)
return -1;
if (fstat(fd, &st2) != 0) {
close(fd);
return -1;
}
if (st.st_ino != st2.st_ino || st.st_dev != st2.st_dev) {
close(fd);
return -1;
}
return fd;
}
위의 프로그램은 안전하게 작성된 프로그램으로 stat() 대신에 lstat()를 사용한다. 만일 지정된 파일이 심볼릭 링크면 이것은 링크의 상태를 돌려준다. 다음 파일을 열고, 열린 파일의 상태를 얻는다. 상태 정보의 inode와 device number는 비교되어 그들이 동일하니 않으면 중지된다.
Ⅷ. chroot()의 보안
유닉스 시스템은 운영체제의 관점에서 프로그램을 제한함으로써 외부에 노출되는 것을 막을 수 있는 능력이 있는데 이러한 기능을 chroot() 함수가 수행한다.
int chroot(const char *path);
chroot() 함수에서의 path는 새로운 파일시스템의 root 디렉토리로 인식되어 파일시스템의 다른 부분에 대한 접근은 제한된다
if (chroot("/jail") < 0 || chdir("/") < 0)
perror("Failure setting new root directory");
chroot() 함수를 사용하는 것도 수퍼유저의 권한을 제한하는 좋은 방법이기도 한다.
Ⅸ. 최소한의 권한 원리
1. 최소한의 권한의 필요성
대부분의 프로그램은 root나 특별한 권한을 가진 사용자가 소유하고 있는 시스템 자원에 접근하기 위해서는 특별한 권한을 가져야 한다. 네트워크 서비스와 같은 경우 일반사용자는 사용할 수 없는 권한있는 TCP나 UDP 포트를 할당하기 위해서 특별한 권한이 필요하다. 로컬 권한이 있는 프로그램은 메모리나 디스크, 시스템 구성 정보 등의 제한된 시스템 자원을 사용할 수 있다. 특히 모든 자원들에 대해 초기화하고 권한을 낮추는 것은 특별한 권한을 가지고 실행되는 프로그램에서는 중요하다.
2. 최소한의 권한 설정 방법
권한을 낮추기 위해서는 프로세스의 EUID와 EGID를 더 낮은 권한으로 설정해야 한다. 임의로 권한을 낮추거나 제거하기 위해서는 seteuid()와 setegid() 함수가 필요하고 영구적으로 권한을 제거하기 위해서는 setuid()와 setgid() 함수를 사용해야 한다.
2.1 네트워크 서비스에서 권한 낮추기
네트워크 서비스의 권한을 낮추기 위해서는 프로그램이 실행되자마자 권한을 낮추어야 한다. 보통 최소한의 접근 권한을 가진 사용자를 "nobody"로 사용한다.
int drop()
{
struct passwd *pep = getpwnam("nobody");
if(!pep)
return -1;
if(setgid(pep->pw_gid) < 0)
return -1;
if(setuid(pep->pw_uid) < 0)
return -1;
return 0;
}
<프로그램-11> 최소한의 권한
특히 위의 프로그램에 setuid를 설정하기 전에 setgid를 먼저 설정할 것을 볼 수 있다. 이것은 setuid를 먼저 설정하였을 경우 권한이 gid가 가지고 있는 권한보다 낮아져 권한을 수정할 없게 될 수 있기 때문에 setgid 먼저 설정한다.
2.2 로컬 setuid 프로그램에서 권한 낮추기
권한있는 SUID/SGID 프로그램을 실행하는 경우 프로세스의 RUID와 RGID는 프로그램을 실행한 사용자의 uid와 gid이지만 EUID와 EGID, saved UID, saved GID은 파일의 소유자나 그룹의 권한으로 설정된다.
이런 SUID 프로그램는 권한을 임시로나 영구적으로 권한을 수정할 수 있다.
? 권한을 영구적으로 낮추기
권한을 영구적으로 낮추기 위해서는 프로그램의 EUID, EGID SUID와 SGID를 RUID와 RGID로 설정해야 한다. setuid()와 setgid() 함수를 이용하여 RUID/RGID로 설정하면 EUID/SUID/RUID는 모두 변한다.
<프로그램-12> 영구적으로 권한 낮추는 프로그램 if(setgid(getgid()) < 0)
return -1;
if(setuid(getuid()) < 0)
return -1;
? 권한을 임시로 낮추기
임시로 권한을 낮추기 위하여 EUID의 값을 원하는 값으로 설정한다. EUID은 시스템 함수나 권한 검사를 수행할 때 사용되므로 다음에 사용하기 위하여 이 값을 저장해 놓는다.
<프로그램-13> 임시적으로 권한을 낮추는 프로그램 struct passwd *pep = getpwnam("nobody");
uid_t saved_uid;
gid_t saved_gid;
if (!pep)
return -1;
saved_uid = geteuid();
saved_gid = getegid();
if (setegid(pep->pw_gid) < 0)
return -1;
if (seteuid(pep->pw_uid) < 0)
return -1;
/* perform desired unprivileged operations then revert back */
if (setegid(saved_gid) < 0)
return -1;
if (seteuid(saved_uid) < 0)
return -1;
마치며
지금까지 전반적으로 유닉스 프로그램 작성시 유의해야 할 사항과 어떻게 작성해야 안전한 프로그램을 작성할 수 있는가에 대하여 알아보았다. 위에서 설명한 것과 같이 프로그램을 작성했다고 완전하게 안전한 프로그램은 아니다. 다만 안전성을 고려하지 않은 프로그램보다는 약점이나 헛점이 없는 프로그램을 작성하자는 의도에서 이 지침서 작성한 것이다. 대부분 해킹 공격의 원인이 되는 취약점들이 운영체제나 시스템 프로그램등의 안전성을 고려하지 않은 코드에서 나온다고 할 수 있기 때문에 이 지침서는 실제 프로그램을 개발하는 프로그래머에게 유용하게 사용될 수 있다.
또한 해킹 공격 방법이 설명한 방법들에만 머무르지 않고 계속 변화하고 개발되어지므로 안전한 소스코드 지침서도 그에 따라 수정되고 버전업되어야 할 필요성이 있다.
<참고문헌>
[1] Simson Garfinke and Gene Spafford, "Practical UNIX & Internet Security", O'Reilly & Associates, Inc., 2nd Edition, April 1996.
[2] Nemeth·Snyder·Seebass·Hein, "Unix System Administration Handbook", A Division of Simon & Schusyer, Inc., Third Edition, October 1998.
[3] Grahan Glass 저. 조경산 역, "프로그래머와 사용자를 위한 UNIX 완성", 이한출판사, March 1998.
[4] David A. Wheeler, "Secure Programming for Linux and Unix HOWTO", 1999.
[5] whitefang.com, "Secure UNIX Programming FAQ", 1999.
[6] M. Bishop, "Writing Safe Privileged Programs," Network Security 1997.
[7] M. Bishop, "How to Write a Setuid Program," 1986.
[8] ``Secure Programming Guidelines''. FreeBSD, Inc. 1999.
[9] AUSCERT and O'Reilly, "Lab Engineers Check List for Writing Secure Unix Code.", 1996
[10] NCSA "Secure Programming Guidelines".
[11] SETUID manual page.
댓글 [0]
번호 | 제목 | 글쓴이 | 조회 | 추천 | 등록일 |
---|---|---|---|---|---|
[공지] | 강좌 작성간 참고해주세요 | gooddew | - | - | - |
1341 | 소프트웨어| 크롬용 확장 프로그램 1개 추천 [14] | 꼬마야 | 8689 | 3 | 01-20 |
1340 | 소프트웨어| 크롬 몇가지 팁 [3] | 꼬마야 | 8855 | 1 | 01-20 |
1339 | 소프트웨어| USB키보드인채로 PLOP 사용하기 (USB 2.0 MODE + PE) [18] | hazuki | 48420 | 0 | 01-19 |
1338 | 소프트웨어| 구글 크롬 유용한 확장기능 10개 [8] | APPCRASH | 11671 | 4 | 01-19 |
1337 | 윈 도 우| winpe.wim, install.wim에 컴퓨터 드라이브 통합하기. [4] |
|
27206 | 2 | 01-16 |
1336 | 윈 도 우| 권한 거부된 폴더나 레지스트리 삭제.(알면 싱겁고 모르면 ... [4] | 오펜하이머 | 15771 | 2 | 01-14 |
1335 | 서버 / IT| 언제 어디서 든지 즐겨찾기 관리하기 [15] | 컴돌이 | 11090 | 5 | 01-13 |
1334 | 소프트웨어| USB를 NTFS USB HDD+ 로 만들기 [14] |
|
19320 | 0 | 01-12 |
1333 | 윈 도 우| 쓸만한 윈도우7가젯입니다. [11] | kailcarson | 32356 | 2 | 01-11 |
1332 | 소프트웨어| [TIP] yy.to 무료 포워딩 절대 쓰지마세요 ! [6] |
|
7787 | 0 | 01-10 |
1331 | 윈 도 우| SSD 기반에서 자동트림 관련 [11] | 오징어튀김 | 15777 | 0 | 01-04 |
1330 | 윈 도 우| SP1의 터미널 서비스 멀티세션 패치 [4] | 오펜하이머 | 10929 | 0 | 01-04 |
1329 | 소프트웨어| 가상으로 5.1 ch 을 들어보자 [8] | Visored | 11936 | 0 | 01-04 |
1328 | 소프트웨어| 얄약 광고 제거하기 [4] | gooddew | 8524 | 0 | 01-02 |
1327 | 하드웨어| WLAN AP모드유틸 Connectify 3.2 패치. [6] | 오펜하이머 | 12900 | 0 | 01-01 |
1326 | 소프트웨어| 2부 VMware에서 Mac OS X 10.7(Lion) 시동 디스크로 설치하기 [1] | 다른 의견 | 10036 | 0 | 12-24 |
1325 | 소프트웨어| 1부 VMware에서 Mac OS X 10.7(Lion) 시동 디스크로 설치하기 [6] | 다른 의견 | 14205 | 0 | 12-24 |
1324 | 윈 도 우| 탐색기의 눈에까시 라이브러리 제거 [25] | 오펜하이머 | 13289 | 1 | 12-24 |
1323 | 윈 도 우| 윈도우 7 에서 USB 제거를 XP 처럼 하기 [16] | 해밀 | 16045 | 0 | 12-23 |
1322 | 윈 도 우| 시작 사용자 이미지 제거(내용 살짝 정리) [16] | 양철나무꾼 | 7924 | 0 | 12-21 |