rosieblue
article thumbnail
Published 2023. 10. 4. 16:02
[Kernel] Tasks (1) Linux Exploitation/Kernel
728x90

Tasks

리눅스에서는 프로세스나 스레드를 'Task'라는 하나의 단위로 관리한다.

Task는 결국 하나의 '실행 단위'를 이야기한다. 결국 프로세스나 스레드는 개념적인 구분일 뿐이고, 리눅스 내에서 이들이 관리될 때에는 프로세스나 스레드나 모두 '태스크'로 관리가 된다.

 

테스크 별로 시간을 할당하여 여러 개의 테스크들을 실행하는 멀티 태스킹, 실행 시간을 배분하는 스케줄링 등 모두 많이 들어본 용어들일 것이다.

 

 

각 태스크는 task_struct라는 구조체로 표현이 된다. 커널은 태스크 관리자라고 했었는데 그렇기 때문에 task_struct는 커널 메모리 내에 존재한다. 

생각해보면 당연한 것이, 이렇게 중요한 정보는 일반 사용자가 접근하면 안되므로 커널 메모리 안에 들어가서 쉽게 접근할 수 없게 만드는 것이 적절할 것이다.

 

task_struct 구조체

task_struct 구조체는 include/linux/sched.h에 정의되어 있다.

/* Permal link: https://github.com/torvalds/linux/blob/219d54332a09e8d8741c1e1982f5eae56099de85/
include/linux/sched.h#L624:L1284
*/
struct task_struct {
...
	volatile long			state;
...
	struct list_head		tasks;
...
	struct mm_struct		*mm;
...
	/* Effective (overridable) subjective task credentials (COW): */
	const struct cred __rcu		*cred;
...
	char				comm[TASK_COMM_LEN];
...
	/* Open file information: */
	struct files_struct		*files;
	
...
}

 

각 요소들을 살펴보자.

  • volatile long state : 현재 태스크의 실행 상태
    • 0 : 실행 중이거나 실행(스케줄)가능한 상태
    • 양수 : 대기 중이거나 정지 중인 상태
      • TASK_RUNNING(0), INTERRUPTIBLE(1), UNITERRUPTIBLE(2), STOPPED(4), TRACED(8),  EXIT_ZOMBIE(16), DEAD(32)
  • struct list_head tasks : 커널에 존재하는 테스크들의 연결 리스트 노드
  • struct mm_struct mm : mm_struct는 사용자 메모리 영역(주소공간)에 대한 정보를 가지고 있는 구조체! 같은 프로세스 내의 스레드들은 스택 제외하고 메모리를 공유하므로 모두 mm이 같다!
  • const struct cread __rcu cred : 현재 테스크의 신원 정보를 가리키는 포인터
  • char comm[TASK_COMM_LEN] : 실행 파일 혹은 스레드의 이름
  • struct files_struct files : 열린 파일 디스크립터 정보. 일반적으로 같은 프로세스 내의 스레드들은 모두 files가 같다!

 

cred (신원 정보)

task_struct 내의 cred 구조체에 대해서 조금 더 자세히 살펴보자!

/* Permal link: https://github.com/torvalds/linux/blob/219d54332a09e8d8741c1e1982f5eae56099de85/
include/linux/cred.h#L111:L153 
*/
struct cred {
	atomic_t	usage;
...
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
	unsigned	securebits;	/* SUID-less security management */
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
	kernel_cap_t	cap_permitted;	/* caps we're permitted */
	kernel_cap_t	cap_effective;	/* caps we can actually use */
	kernel_cap_t	cap_bset;	/* capability bounding set */
	kernel_cap_t	cap_ambient;	/* Ambient capability set */
...
}

이렇게 cred구조체는 프로세스의 권한 정보를 저장하고 있다.

 

위에서 usage는 cred 참조 카운터이다. cred는 여러 프로세스에서 동시에 사용할 수 있다!!!

 

uid 프로세스를 소유하고 있는 사용자(즉 프로세스를 실행시킨 사용자) ID(User ID, UID)를 저장. 0으로 덮어쓰면 해당 태스크는 seteuid(0)로 최고관리자 권한을 획득할 수 있음.
euid 실효적인 사용자 ID(Effective User ID, EUID)를 저장. 권한 검사에 실제 사용되는 값을 저장하며, 0으로 덮어쓰면 해당 태스크는 최고관리자 권한을 획득. 일반적으로는 uid와 같은 값을 가짐.
gid, egid 각각 Real GID와 Effective GID를 저장합니다. GID는 group ID의 약자로 사용자 그룹의 식별번호를 의미.

 

나머지 프로세스 권한에 대하여 궁금하다면 아래 포스트를 참고하자.

[🖥️ Computer Science/OS] - [OS/Linux] 프로세스 권한, ID, 프로세스 관리 정보 관련 함수

 

[OS/Linux] 프로세스 권한, ID, 프로세스 관리 정보 관련 함수

프로세스에서의 UID User ID(UID)는 32bit의 정수값! 이걸로 user를 식별! ruid : Real User ID!! 즉 이 프로세스를 '시작'한 유저의 아이디 euid : '권한 검사'할 때 사용되는 id. 보통은 ruid와 같겠지만 SetUID비트

hannahsecurity.tistory.com

 

태스크, 프로세스, 스레드

리눅스 커널에서는 프로세스, 스레드 구분없이 모두 태스크로 처리!

차이점은 공유메모리를 가지는가, 안 가지는가? 공유 file descriptor table을 가지는가? 등등의 차이이다.

 

프로그램이 fork()를 호출해서 프로세스를 생성해도 커널 입장에서는 Task를 생성하는 거고 pthread_create()로 쓰레드를 생성해도 커널입장에서는 Task를 생성한다.

 

  • 프로세스 역할을 하는 task을 생성할 때는 독립된 가상 메모리 영역을 생성한 생성한 후 task_struct에 그 정보를 기록
  • thread 역할을 하는 task을 생성할 때는 가상 메모리 영역을 따로 만들지 않고, thread 생성 함수를 호출한 task(즉, process)가 가진 메모리 정보를 task_struct에 기록함

자세한 내용은 두근두근이야기 :: Task, Process, Thread. (tistory.com) <-여기 참고..

 

 

pid, tgid

리눅스 안에서 태스크들은 유일하게 구분 가능해야한다.

이때 이 태스크들을 구분하는 유일한 값은 task_struct의 'pid' 필드이다. 엄밀히 말해서는 이 pid는 프로세스 아이디인 pid하고는 다르다.

 

예를 들어 운영체제 과목에서 한 프로세스 내의 여러 스레드들은 같은 pid를 공유한다고 배웠다.

그런데 스레드들은 모두 다 다른 태스크일테고 위에서는 pid로 태스크를 구분한다고 했다. 

그래서 tgid라는 개념이 도입되었다. 

 

  • fork(), vfork() - 프로세스 생성
    • 이렇게 생성된 부모 프로세스와 자식 프로세스는 pid와 tgid 모두 다름
    • 참고로 한 프로세스 내의 pid와 tgid는 동일
  • pthread_create() - 스레드 생성
    • 같은 프로세스 내 스레드들의 pid는 서로 다르지만, tgid는 같음
    • 생성된 태스크의 tgid를 부모 쓰레드의 tgid 값과 동일한 값으로 결정. 결국 부모 태스크와 자식 태스크는 동일한 tgid를 갖게 되며 동일한 프로세스에 속해 있는 것으로 해석됨

 

즉, 사실 getpid를 통해 가져오는 값은 pid가 아니라 tgid이다!!

실제 task_struct의 pid를 가져오는 함수는 gettid()이다!

 

 

권한 상승 실습

위에서 cred에는 태스크의 신원 정보가 들어있는 것을 알 수 있었다.

따라서 cred를 조작하여 euid를 0으로 만들 수 있다면 우리는 관리자 권한으로 프로세스를 실행할 수 있을 것이다

 

아래는 gdb에서 사용할 수 있는 유용한 명령어들인데, 원하는 태스크의 정보를 출력해준다!

$lx_current() 선택된 CPU 코어의 현재 프로세스 또는 스레드의 태스크 구조체를 반환
$lx_task_by_pid(<PID>) 프로세스 식별자(PID)가 <PID> 인 프로세스 또는 스레드의 태스크 구조체를 반환

 

간단한 커널 디버깅을 진행해보자

게스트 환경의 쉘 pid 번호

게스트 환경에 gdb를 부착해서 분석해보았다. bash의 pid인 299를 인자로 넣고 위에서 설명한 gdb명령어를 통해 태스크 정보를 출력해 보았다.

 

0x3e8=1000

프로세스의 이름이 bash이고 현재 실행 중이며 uid가 0x3e8(1000)임을 알 수 있었다!

 

 

아래처럼 cred의 euid를 0(root)으로 바꾸어 주었다

다시 게스트 환경을 확인해보자!

 

 

오 게스트 환경의 eid가 0이 되었다 !!

 

 

root만 읽을 수 있는 /etc/shadow 파일을 한번 읽어보자

 

우와 잘 읽혀진다! 

 

이를 통해 권한 상승이 잘 되었다는 것을 확인할 수 있었다~~

 

 

References

https://velog.io/@jinh2352/Linux-5-%EB%A6%AC%EB%88%85%EC%8A%A4%EC%9D%98-%ED%83%9C%EC%8A%A4%ED%81%AC-%EB%AA%A8%EB%8D%B8

https://dreamhack.io/lecture/courses/56

Mint & Latte_. :: Chapter 3 :: 태스크 관리 (2) - 커널의 태스크 관리 (tistory.com)

두근두근이야기 :: Task, Process, Thread. (tistory.com)

profile

rosieblue

@Rosieblue

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!