리눅스 권한을 이해할때 크게
0. 권한은 파일(리눅스에서는 디렉터리도 파일이다)에 부여된다
1. 어떤 권한이 있는가 (rwx)
2. 누구에게 그 권한이 있는가 (owner, group, other)
개념을 이해하면 된다.
1번 rwx는 표현법만 익히면 되는거니 그냥 넘어가고
owner, group, other 개념을 확실히 익혀보자.

바로 예제
아래와 같이 파일들이 있다고 했을때 (ls -l로 확인가능하다)
-r-------- 1 pwned pwned 69 flag-4a366de5f9250ee00973d579cd8a9e87
-r-sr-x--- 1 pwned pwn 17552 flag_reader-2b6cfa9d53f87254b7c90bbd12d17ab6
-rwxr-xr-x 1 root root 30 run.sh
-rw-r--r-- 1 root root 534 runner.py
drwxr-xr-x 4 pwn pwn 4096 x64.release
-rw-r--r-- 1 root root 59528158 x64.release.zip
flag 파일의 경우 owner 에게만 r 권한이 있다. (400)
-r--------
- r-- --- ---
│ │ │ │
│ │ │ └ other
│ │ └ group
│ └ owner
└ file type
이거다.
그다음 숫자
1 pwned pwned 69 flag-4a366de5f9250ee00973d579cd8a9e87
여기 맨 앞에 1은 hard link 갯수 이다 (ln file file2) 했을때 쌓인다.
그다음에 pwned pwned가 순서대로
owner / group이 누군지를 나타내는거다
owner? group? other?
owner -> 파일을 만들거나 소유권을 가진 user
group -> 유저들을 묶은 그룹 : 그룹 권한이 적용됨. 리눅스에서 유저는 owner가 될 자격을 가진 개인이면서 group에 속할 수 있는 개인이다.
other -> owner도 아니고 group도 아닌 모든 user (외부 프로세스에서 접속한 사람 등등)
Local Privilege Escalation
사실 이 글을 쓰게 된 것이 LPE를 이해하기 위함인데,
예를 들어 아래와 같이 RCE를 성공한 상황을 가정해보자.

지금 ctf 문제에서는 문제 바이너리를 프로세스로 실행하는 유저는 pwn이고 flag 파일 만들어둔 주체(owner)는 pwned고 ./flag reader로 pwned 권한을 얻어서 읽는것이다.
위 디렉터리에서 flag_reader는 아래와 같은 코드로 표현할 수 있는데
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv, char **envp)
{
uid_t euid1;
uid_t euid2;
euid1 = geteuid();
euid2 = geteuid();
setreuid(euid2, euid1);
system("/bin/bash");
return 0;
}
이제 이 코드를 하나하나 이해하면 LPE를 이해할 수 있을거다.
geteuid() 를 이용해 현재 프로세스의 effective UID를 가져온다.
질문 1. effective UID가 뭔가???
setreuid()는 프로세스가 가지는 UID 값 read uid와 effective uid를 동시에 변경한다.
질문 2. 프로세스가 UID를 가진다?
질문 3. read uid와 effective uid가 뭔가?
setreuid()는 현재 프로세스가 사용하는 UID credential(ruid,euid)을 바꿔서 프로세스의 권한 컨텍스트를 변경하는 함수다.
질문 4. UID credentail이 뭔가??
UID(UserID) credentail - 프로세스가 가지는 권한
리눅스에서 커널이 권한을 체크할때 "유저의 권한"을 보는 것이 아니라 "프로세스에 붙어있는 UID credentail"을 본다.
그 말인 즉슨
예를 들어 cat flag 명령을 RCE 이후에 특정 프로세스 권한을 이용해 연 쉘에서 수행한다고해보자.
그럼 아래와 같이 커널 syscall을 이용해서 파일을 읽는다(파일을 읽는다는게 syscall 필요하니까)
cat → open("flag")
→ syscall open()
→ kernel
이때 커널이 파일 접근을 검사하는데, 아래와 같은 흐름으로 검사한다.
process = current
euid = process->cred->euid # 현재 프로세스 uid
inode_owner = file->inode->uid # 열려는 파일의 uid
if (euid == inode_owner) # 둘이 같음??
owner permission 적용
즉, 커널이 확인하는건 '현재 프로세스를 실행한 유저' 의 권한이 아니라 '현재 실행중인 프로세스'의 권한을 본다는 것이다.
왜???????? 유저 권한 매번 확인하면 되는거 아니냐? - UNIX의 설계철학이다. "process delegation" 권한 모델.
권한은 “프로세스 실행 시점에 결정”된다. 유저의 권한은 프로세스 생성 시점에 위임 (delegation) 되는 설계철학.
ruid/euid/suid 이야기는 이미 프로그램이 실행된 이후의 프로세스 credential 상태를 말하는 것이고, 거기까지 오려면 그 이전 단계에서 반드시 유저가 파일을 +x 할 수 있는 권한 체크가 통과되어야 한다.
process
└ credentials
├ ruid (real uid) # 이 프로세스를 실행한 실제 사용자
├ euid (effective uid) # 커널이 권한 체크할 때 사용하는 uid
└ suid (saved uid) # 이전 privilege를 복구하기 위해 저장하는 uid
suid???? - 이전 권한을 복구하기 위해 저장??????
: 잠시 프로세스가 권한을 올렸다가 낮출 필요가 있는 경우가 많은데, 그때 높은 권한을 저장해두기 위함
아래와 같은 동작을 하는 프로그램이 있을때
1. config file 읽기
2. user input 처리
3. privileged 작업
보통 이런 식의 동작을 하면 높은 권한으로 시작을 하고
-rwsr-xr-x root root program
# 권한을 내려놓음 (drop privilege)
seteuid(ruid);
ruid = user
euid = user
suid = root
# 다시 올림
seteuid(suid);
ruid = user
euid = root
suid = root
# suid가 없으면
seteuid(user)
# root 권한 영구적으로 잃음
(suid 얘기 끝)
쨋든 그래서 LPE를 하고 싶으면 실행중인 프로그램의 uid를 바꿔주면 된다.
처음 RCE를 했을때 Permission Deny가 되는 원리는
# RCE로 shell 따기 성공
/bin/sh
uid=1000(pwn)
# 현재 프로세스(/bin/sh의 uid - RCE성공한 프로세스의 uid 상속)
ruid = 1000
euid = 1000
# 커널 명령 수행
cat flag
# 커널 딴 체크 -> 파일 권한이랑 안맞음
process.euid = 1000
flag owner = 1001
Permission denied
setuid를 실행하면 해당 프로세스의 euid를 바꿔버려서 아래와 같이 되어버리는 것
./flag_reader
ruid = 1000 (pwn)
euid = 1001 (pwned) # 이게 바뀜
# 커널 체크 성공!
process.euid = 1001
flag owner = 1001
다시 돌아와서,
flag_reader의 권한은
-r-sr-x--- 1 pwned pwn 17552 May 4 2023 flag_reader-2b6cfa9d53f87254b7c90bbd12d17ab6
위와 같았다.
여기서 owner의 s 권한이 바로 setuid buit 인데 -> 이 비트가 있으면 프로세스의 euid를 파일 owner의 UID로 바꾼다.
프로세스가 실행될때 프로세스의 권한은 아래와 같은 순서로 세팅된다.
1 execute permission 체크 -> +x 권한 있음?
2 setuid bit 처리 =? +s 권한 있음? 있으면 실행 후 euid 권한 owner로 변경
3 프로세스 시작
따라서 flag_reader를 실행하면 프로세스의 uid가 아래와 같이 세팅된다.
ruid = pwn
euid = pwned
suid = pwned
아래가 이제 euid를 가져와서 ruid랑 euid 둘 다 지금 euid로 바꿔버리겠다 (setreuid)라는 명령어인데, 그 이유는 bash 보안 로직 내부에 setuid 환경을 감지 (ruid != euid) 인 경우 privilege drop을 시도하는 경우가 있기 때문이라고 한다. (충분히 상식적인 보안로직)
uid_t euid1;
uid_t euid2;
euid1 = geteuid();
euid2 = geteuid();
setreuid(euid2, euid1);
그래서 결론적으로 위의 코드를 실행하면 아래와 같이 세팅된 프로세스의 권한으로 쉘을 사용할 수 있다.
ruid = pwn
euid = pwned
suid = pwned
참고.. setreuid 의 존재 이유에 대해
프로세스 권한(privilege)은 euid로 확인한다면서 프로세스 내부에서 ruid까지 바꿔버리는 setreuid 이런 함수를 만든 이유가 뭘까?
ruid = identity
euid = privilege
ruid가 identity 역할로 프로세스가 identity를 바꿀 필요가 있다는 뜻인데.. 어쩌피 프로세스의 privilege는 euid로 검사하는데, 프로세스가 직접 함수 호출을 이용해서 identity를 변환해야되는 이유가 뭐냐는 말이다.
찾아보니 user에게 아예 ownership을 넘기는 경우가 있기 때문이다.
identity 까지 넘겨버리는.. 예를들면 로그인과 웹서버, 그리고 UNIX에 일부 시스템 콜 kill ptrace 같은 경우에 ruid도 검증을 한다고 한다. 이럴때는 ruid와 euid까지 넘겨버리는 것.
이 정도면 납득.
'공부 > 이모저모' 카테고리의 다른 글
| VMware 에서 Ubuntu 디스크 용량 늘린 후에 적용하는법 (0) | 2026.02.24 |
|---|---|
| 코끼리를 냉장고에 넣는방법 (3) | 2025.05.12 |
| nc 로 특정 포트 UDP 패킷 계속 받기 (한번만 받고말기 X 계속 받기) (0) | 2025.01.07 |
| C언어 가변인자함수 구현 (...) (0) | 2024.04.12 |
| git submodule 기능 (git submodule init / git submodule update) (0) | 2024.03.25 |