帮我看下这个pwn的修复题,sub_1554()有格式化字符串漏洞: .text:000000000...
Creato il: 21 marzo 2026
Risposto usando GPT-5.4 Thinking da Chat01
Creato il: 21 marzo 2026
Risposto usando GPT-5.4 Thinking da Chat01
帮我看下这个pwn的修复题,sub_1554()有格式化字符串漏洞:
.text:0000000000001662 lea rsi, size ; "%lu"
.text:0000000000001669 mov eax, [rbp+var_11C]
.text:000000000000166F cdqe
.text:0000000000001671 lea rdx, ds:0[rax*8]
.text:0000000000001679 lea rax, unk_4060
.text:0000000000001680 mov rax, [rdx+rax]
.text:0000000000001684 lea rdx, [rbp+s] ; format
.text:000000000000168B mov ecx, 8
.text:0000000000001690 mov rdi, rax ; str
.text:0000000000001693 mov eax, 0
.text:0000000000001698 call _snprintf
怎么修?我想用read系统调用替换snprintf可以吗?
.text:0000000000001410 mov rax, [rbp+var_48]
.text:0000000000001414 add rax, 8
.text:0000000000001418 mov ebx, [rbp+var_4C]
.text:000000000000141B mov rdi, rax ; size
.text:000000000000141E call _malloc
还有这个malloc有漏洞吗
可以,但不是直接把 call snprintf 改成 call read。
这段代码的真实问题其实有两个:
按 SysV x64 调用约定,这里实际等价于:
csnprintf(card_ptr, 0x2072, s, 8);
也就是:
card_ptr:目标缓冲区0x2072:第二个参数,被当成 sizes:第三个参数,被当成 format8:第四个参数,可变参数所以漏洞是:
s 可控,且被当成 format → 格式化字符串漏洞size = 0x2072,但实际堆块只有 malloc(user_size + 8),最大也就 0x208 → 堆溢出read 替换 snprintf?因为 read 原型是:
cssize_t read(int fd, void *buf, size_t count);
而当前寄存器里装的是 snprintf 的参数布局。
如果你只把 call snprintf 改成 call read,那就会变成类似:
cread(card_ptr, (void *)0x2072, s);
这显然是错的,参数全乱了。
这块应该拆成两步:
snprintf比如:
csize_t old_size = card->size; size_t new_size = old_size; char tmp[0x20] = {0}; printf("New size of content: "); ssize_t n = read(0, tmp, sizeof(tmp) - 1); if (n <= 0) return; tmp[n] = '\0'; char *end = NULL; unsigned long x = strtoul(tmp, &end, 10); if (end == tmp || x > old_size) { puts("Too large"); new_size = old_size; } else { new_size = x; } card->size = new_size;
card + 8 里写cprintf("What do you want to say: "); read(0, (char *)card + 8, card->size);
把这次 snprintf 整段逻辑废掉,保持原 size 不变。
也就是让 edit 只改 content,不改 size。这样最省事,也最安全:
sizeread(0, card+8, old_size) 仍然是有界写snprintf 改 readmalloc 那里有没有漏洞?你贴的这段:
asm1410 mov rax, [rbp+var_48] 1414 add rax, 8 141b mov rdi, rax 141e call _malloc
对应逻辑大概是:
cptr = malloc(size + 8); ptr[0] = size;
malloc 本身 不算明显漏洞因为前面已经限制了:
cif (size <= 0x200)
所以:
size + 8 不会整数溢出0x208,范围安全没检查 malloc 返回值是否为 NULL
这个算健壮性问题,不是主要利用点。
把 size 存在 chunk 前 8 字节,后续又能被错误写入覆盖
也就是说,malloc 本身没问题,真正危险的是后面的 edit 逻辑把 chunk header 搞坏了。
snprintf 把用户输入当 formatread 替换 snprintfstrtoul/scanf("%lu")read(0, card+8, size) 或 memcpymalloc 有没有漏洞malloc(size+8) 本身没明显漏洞如果你愿意,我可以直接按这个二进制给你写一个“最小补丁方案”(源码修复版 / 二进制 patch 版各一份)。
说错了,我的意思是write系统调用。我知道不能用write直接换snprintf,我是说通过write系统调用修改snprintf,把参数按正确顺序传给它,这样可以吗
可以“改寄存器传参”,但前提是你改去调一个“解析输入”的函数。
如果你最后还是调 snprintf,那不行,因为语义就错了。
这段逻辑真正想做的是:
cnew_size = parse_unsigned_long(s);
而不是:
cformat_into_buffer(...);
snprintf 的作用是把数值格式化成字符串写到目标缓冲区里;
你这里需要的是把字符串解析成整数写进 *(uint64_t *)card。
所以即便你把参数“排正确”了,只要目标还是 snprintf,写进去的仍然是 ASCII 字符串,不是一个真正的 uint64_t 数值。
snprintf 语义不对后面代码是这样用结果的:
asm16b8 mov rax, [rax] ; 直接把 card 前8字节当整数 size 读出来 ... 1744 mov rdx, rax 1747 mov rsi, rcx 174a mov edi, 0 1754 call read@plt ; read(0, card+8, size)
也就是说,card 开头那 8 字节必须是:
c*(uint64_t *)card = new_size;
但 snprintf(card, ..., "%lu", x) 写进去的是:
c'1','2','3','\0',...
不是整数 123。
所以后面 mov rax, [rax] 读出来会变成小端 ASCII 值,不是你想要的数。
strtoul 或 sscanf最正统的是:
cchar s[0x100] = {0}; read(0, s, 0xff); *(unsigned long *)card = strtoul(s, NULL, 10);
或者:
csscanf(s, "%lu", (unsigned long *)card);
这个才和后续逻辑匹配。
scanf("%lu", card)如果你是修复题、只能 patch 二进制,而且当前导入表里只有 __isoc99_scanf,没有 sscanf/strtoul,那最实用的是:
asmmemset(s, 0, 0x100) read(0, s, 0x100) snprintf(card, 0x2072, s, 8)
asmscanf("%lu", card)
也就是直接从 stdin 读一个 unsigned long 到 card[0]。
这时前面的 read(0, s, 0x100) 必须一起去掉/跳过,不然输入已经被 read 吃掉了,后面的 scanf 会等下一次输入。
所以:
snprintf 调用点,不够s 里”这一步也一起改掉分两层说:
那当然可以,任何能把字节写回 ELF 的方式都行,write 只是落盘手段之一。
snprintf,只是把参数排对”那不行,因为函数选错了,不是传参顺序的问题。
snprintfscanf / sscanf / strtoul如果不想加新导入:
read(0, s, 0x100)snprintf(...) 改成 __isoc99_scanf("%lu", card)这是最稳的。
你要是愿意,我可以直接按你这个 ELF 给你写一版汇编级 patch 方案,把 0x1644 到 0x1698 这一段具体怎么改成字节序列给你列出来。
这个snprintf本意是把s变量的前八字节输出到[rbp+var_11C]是吗
不是。[rbp+var_11C] 这里不是输出位置,它只是前面 scanf("%d", &idx) 读进来的 index。
按 x86-64 SysV 调用约定,snprintf 参数是:
cint snprintf(char *str, size_t size, const char *format, ...);
对应寄存器:
rdi = strrsi = sizerdx = formatrcx = 第一个可变参数你这段在 call 前实际是:
asm1662 lea rsi, 0x2072 ; rsi = "%lu" 的地址 1669 mov eax, [rbp+var_11C] ... 1680 mov rax, [rdx+rax] ; rax = cards[idx] 1684 lea rdx, [rbp+s] ; rdx = &s 168b mov ecx, 8 1690 mov rdi, rax ; rdi = cards[idx] 1698 call snprintf
所以它真正等价于:
csnprintf(cards[idx], 0x2072, s, 8);
也就是:
cards[idx]0x2072(因为把 "%lu" 的地址误塞进了 rsi)s8[rbp+var_11C]”更准确地说:
var_11C 只是 idxs 被当成了 format string8 不是“拷贝 8 字节”的意思,而是 snprintf 的第一个格式化参数例如如果 s 是:
c"%lu"
那效果近似是:
csnprintf(cards[idx], 0x2072, "%lu", 8);
输出字符串 "8" 到 cards[idx]。
如果 s 是:
c"%p %p %p"
那就会把后续寄存器/栈里的值拿来格式化,形成你说的格式化字符串漏洞。
如果 s 是:
c"%n"
还会尝试写内存。
因为你看到了:
asmmov ecx, 8
但这里的 8 不是长度参数。
snprintf 的长度参数在 rsi,而 rsi 现在装的是 "%lu" 的地址。
所以这里完全不是:
csnprintf(dst, 8, "%s", s);
也不是:
cwrite(dst, s, 8);
结合后面逻辑:
asm16b8 mov rax, [rax] ; 取 cards[idx] 前8字节当 size 16bb cmp old_size, new_size ... 1740 lea rcx, [card+8] 1744 mov rdx, rax 1754 call read ; read(0, card+8, size)
很明显它想做的是:
*(uint64_t *)cards[idx]card+8 读内容所以作者本意大概率不是输出字符串,而是想:
csscanf(s, "%lu", (unsigned long *)cards[idx]);
或者:
c*(unsigned long *)cards[idx] = strtoul(s, NULL, 10);
这才和后面的逻辑一致。
结论就是:
这段 snprintf 不是“把 s 的前 8 字节输出到 [rbp+var_11C]”,而是把 s 当 format,往 cards[idx] 这个堆块写格式化结果;var_11C 只负责选哪张 card。
这里printf可以改成write吗?怎么改
可以,而且这处用 write 比用 printf("%s", buf) 更稳。
因为这里原代码:
cbuf = malloc(0x400); read(0, buf, 0x400); printf((char *)buf);
问题有两个:
printf((char *)buf) 是格式化字符串漏洞read 不会自动补 \0,所以就算你改成 printf("%s", buf),也还是可能继续读到堆上后面的垃圾数据最合理的是改成:
cssize_t n = read(0, buf, 0x400); if (n > 0) { write(1, buf, n); }
这样:
\0malloc 出来的未初始化尾部一起泄露出去write(1, buf, 0x400)可以这样写,但不推荐:
cwrite(1, buf, 0x400);
因为如果 read 实际只读了 20 字节,后面 0x400-20 字节还是堆上的未初始化内容。
你这样会把这些内容也输出,造成信息泄露。
所以一定要用 read 的返回值当长度。
你图里这段:
asmmov rax, [rbp+buf] mov rdi, rax mov eax, 0 call _printf
如果前面刚执行完 read,那么:
rax 里原本就是 read 的返回值 nmov rax, [rbp+buf] 把它覆盖掉了所以要改成:
cwrite(1, buf, n);
write 参数cssize_t write(int fd, const void *buf, size_t count);
寄存器是:
rdi = 1rsi = bufrdx = nwrite@plt那最直接的 patch 是把 printf 那 17 字节改成:
asmmov rdx, rax ; rdx = read 返回值 n mov rsi, [rbp+buf] ; rsi = buf mov edi, 1 ; stdout call _write
也就是:
asm; read 返回后,rax = n mov rdx, rax mov rsi, [rbp+buf] mov edi, 1 call _write
这个逻辑就是:
cwrite(1, buf, n);
因为如果 read 返回:
0:EOF-1:失败那你直接:
asmmov rdx, rax
就会把 -1 变成超大长度,出事。
所以严谨版应该先判断:
asmtest rax, rax jle short skip_write mov rdx, rax mov rsi, [rbp+buf] mov edi, 1 call _write skip_write:
write@plt那你有两个办法:
Linux x86_64 下:
rax = 1 → sys_writerdi = 1rsi = bufrdx = n即:
asmtest rax, rax jle short skip_write mov rdx, rax mov rsi, [rbp+buf] mov edi, 1 mov eax, 1 syscall skip_write:
write,改成安全字符串输出如果你只是想修漏洞,不执着于 write,也可以改成:
cssize_t n = read(0, buf, 0x3ff); if (n > 0) { ((char *)buf)[n] = 0; printf("%s", (char *)buf); }
但这比 write 麻烦,因为:
read 长度成 0x3ff\0printf 可以改成 write而且这是个好修法。
cn = read(0, buf, 0x400); if (n > 0) write(1, buf, n);
把:
asmmov rax, [rbp+buf] mov rdi, rax mov eax, 0 call _printf
改成:
asmmov rdx, rax mov rsi, [rbp+buf] mov edi, 1 call _write
但这只是最小版,没处理 read <= 0。
正式修复最好加 test rax,rax / jle。
你要的话,我可以继续按你这张图里的地址,直接给你写一份二进制 patch 方案,包括“如果有 write@plt 怎么改”和“如果没有 write@plt 怎么塞 syscall stub”。
write(1, buf, 0x400)能过check吗
不一定。从“真正修复”角度看,write(1, buf, 0x400) 不算合格修复。
原因很直接:
cbuf = malloc(0x400); n = read(0, buf, 0x400); write(1, buf, 0x400);
这里 read 实际可能只读到很少的数据,比如 10 字节。
这时候你却把整整 0x400 字节都打出去,后面剩下的就是:
所以:
更稳的写法是:
cssize_t n = read(0, buf, 0x400); if (n > 0) write(1, buf, n);
这个才是标准修法。
如果你现在做的是 patch 题的自动 check,我的判断是:
write(1, buf, 0x400) 有概率过简单 check,但不应该当成正确答案。
想稳过,最好用 read 返回值当第三个参数。
puts是printf的上位替代吗
不是。puts 不是 printf 的上位替代,只是更受限的一个字符串输出函数。
printf:
cprintf("x=%d\n", x); printf("%s", buf);
puts:
cputs(buf);
puts 的特点是:
只能输出一个字符串
不能像 printf 那样做格式化。
会自动在末尾加一个换行
puts("abc") 实际输出是 abc\n。
要求参数是以 \0 结尾的字符串
它会一直读到空字节为止。
大概关系更像:
puts(s) ≈ printf("%s\n", s)%d、%p、%lx 这些格式化能力printf(buf) 改成 puts(buf)?例如:
cread(0, buf, 0x400); printf(buf);
改成:
cputs(buf);
格式化字符串漏洞确实没了,因为 puts 不解析 %n/%p/%s。
但是这里还有个大问题:
cread(0, buf, 0x400);
read 不会自动补 \0。
所以 puts(buf) 可能继续往后读,直到碰到某个偶然的 \0,导致:
如果数据来自 read,优先级一般是:
cn = read(0, buf, 0x400); if (n > 0) write(1, buf, n);
cn = read(0, buf, 0x3ff); if (n > 0) { buf[n] = '\0'; puts(buf); }
但它会自动多输出一个换行,行为和原程序未必完全一致。
puts 不是 printf 的上位替代。
它更像是一个只会输出 C 字符串、并且自动补换行的简化版函数。
在你这种 read 读入原始字节的修复题里,write 往往比 puts 更合适。