帮我看下这个pwn的修复题,sub_1554()有格式化字符串漏洞: .text:000000000...

創建於:2026年3月21日

使用 GPT-5.4 Thinking 回答 Chat01

問題

pwn

帮我看下这个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有漏洞吗

思考

我先看一下可用的相关技能和你上传的二进制,确认能不能直接基于样本把这两个点(snprintfmalloc)说清楚。

Thought for 6s

思考

{"path":"","only_tools":false,"refetch_tools":false}

{}

bash -lc file /mnt/data/pwn && ls -l /mnt/data/pwn && checksec --file=/mnt/data/pwn || true

bash -lc which r2 || which radare2 || which rizin || which objdump || which gdb || which ghidra || true

bash -lc objdump -d -M intel /mnt/data/pwn | sed -n '300,420p'

bash -lc objdump -d -M intel /mnt/data/pwn | sed -n '420,520p'

bash -lc objdump -d -M intel /mnt/data/pwn | sed -n '220,320p' && echo '---strings---' && strings -tx /mnt/data/pwn | sed -n '1,120p'

bash -lc objdump -T /mnt/data/pwn | grep -E 'snprintf|read|scanf|malloc|free|memset|printf'

bash -lc objdump -d -M intel /mnt/data/pwn | sed -n '60,120p'

bash -lc objdump -s -j .rodata /mnt/data/pwn | sed -n '1,80p'

Analyzing potential vulnerabilities

I need to test the functionality and understand the vulnerabilities to get an accurate answer. The disassembly is a bit strange. I'll try decompiling through tools like Ghidra or ObjDump. GDB could help with dynamic behavior from the sample. I'm also checking the user's snippet for potential errors in the IDA comments.

Identifying and explaining the format string vulnerability

This seems to be a format string vulnerability with user input controlling rdx, and the size argument being set to an unusually large value (0x2072, or 8306). The function is called with snprintf(dest, 8306, input_buf, 8). The size is misinterpreted due to a mismatch in the function prototype. I need to explain how to fix this issue.### Evaluating buffer operations and potential malloc vulnerability

It seems the source is already in memory since read(0, s, 0x100) was called, so replacing snprintf with read wouldn't work. I need to consider using memcpy, strncpy, or snprintf appropriately. I'll also check if there's a potential vulnerability related to malloc at line 1410, possibly with size checks.

Identifying potential vulnerabilities in code

There are a few concerns here: no check for integer overflow (though size is capped at 0x200), no check for malloc returning NULL, and allowing size 0. An issue may lie in how snprintf is used later, which can cause buffer overflow. Also, the read at 1754 assumes consistent allocation. Focusing on repairing these issues by addressing the malloc handling and edit path is key.### Assessing potential overflow and format string vulnerability

The call to read with arbitrary length based on possibly corrupted size introduces a risk of heap overflow, especially if snprintf overflows the heap. The format string could leak stack data and even trigger a segmentation fault if %n is present. The fix should address both the format string vulnerability and incorrect size checks.

Providing a fix for the content size and reading process

It seems a better approach would be to use scanf("%lu", &new_size) to capture a numeric size instead of calling read to buffer arbitrary bytes. The function prompt suggests editing size, but it's reading into the wrong area — perhaps to store the new size. A simple fix would be to change the handling of the size input and buffer the content separately.

Thought for 2m 9s

回答

可以,但不是直接把 call snprintf 改成 call read
这段代码的真实问题其实有两个:

1)这里不只是格式化字符串,还是堆溢出

按 SysV x64 调用约定,这里实际等价于:

c
snprintf(card_ptr, 0x2072, s, 8);

也就是:

  • card_ptr:目标缓冲区
  • 0x2072:第二个参数,被当成 size
  • s:第三个参数,被当成 format
  • 8:第四个参数,可变参数

所以漏洞是:

  • s 可控,且被当成 format → 格式化字符串漏洞
  • size = 0x2072,但实际堆块只有 malloc(user_size + 8),最大也就 0x208堆溢出

2)能不能用 read 替换 snprintf

结论:不能直接替换

因为 read 原型是:

c
ssize_t read(int fd, void *buf, size_t count);

而当前寄存器里装的是 snprintf 的参数布局。
如果你只把 call snprintf 改成 call read,那就会变成类似:

c
read(card_ptr, (void *)0x2072, s);

这显然是错的,参数全乱了。


正确修法

如果你有源码

这块应该拆成两步:

A. 读“新长度”时,解析数字,不要用 snprintf

比如:

c
size_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;

B. 读内容时,直接往 card + 8 里写

c
printf("What do you want to say: "); read(0, (char *)card + 8, card->size);

如果你只能打二进制补丁

最稳的修法:

把这次 snprintf 整段逻辑废掉,保持原 size 不变。

也就是让 edit 只改 content,不改 size。这样最省事,也最安全:

  • 不让用户输入参与 format
  • 不改 chunk 头部的 size
  • 后面的 read(0, card+8, old_size) 仍然是有界写

不建议:

  • 直接把 snprintfread
  • 继续把 chunk 首 8 字节当字符串处理

3)malloc 那里有没有漏洞?

你贴的这段:

asm
1410 mov rax, [rbp+var_48] 1414 add rax, 8 141b mov rdi, rax 141e call _malloc

对应逻辑大概是:

c
ptr = malloc(size + 8); ptr[0] = size;

这个 malloc 本身 不算明显漏洞

因为前面已经限制了:

c
if (size <= 0x200)

所以:

  • size + 8 不会整数溢出
  • 最大申请 0x208,范围安全

但有两个小问题:

  1. 没检查 malloc 返回值是否为 NULL 这个算健壮性问题,不是主要利用点。

  2. 把 size 存在 chunk 前 8 字节,后续又能被错误写入覆盖 也就是说,malloc 本身没问题,真正危险的是后面的 edit 逻辑把 chunk header 搞坏了


最后总结

这题该怎么修

最合理的答案:

  • 不要用 snprintf 把用户输入当 format
  • 不要直接用 read 替换 snprintf
  • 读“新大小”就用 strtoul/scanf("%lu")
  • 写内容就用 read(0, card+8, size)memcpy

malloc 有没有漏洞

  • malloc(size+8) 本身没明显漏洞
  • 真正的问题是:后续 edit 把堆块头部的 size 覆盖了

如果你愿意,我可以直接按这个二进制给你写一个“最小补丁方案”(源码修复版 / 二进制 patch 版各一份)。

問題

说错了,我的意思是write系统调用。我知道不能用write直接换snprintf,我是说通过write系统调用修改snprintf,把参数按正确顺序传给它,这样可以吗

思考

Patching registers for scanf and handling read dependencies

To properly patch for scanf, I’ll need to load the format string "%lu" into rdi and the address of card into rsi. Since the current code reads via read(0, s, 0x100) before calling scanf, I’ll need to either remove this read or use a parsing function like sscanf or strtoul to avoid reading extra lines.

Thought for 1m 43s

回答

可以“改寄存器传参”,但前提是你改去调一个“解析输入”的函数
如果你最后还是调 snprintf,那不行,因为语义就错了。

关键点

这段逻辑真正想做的是:

c
new_size = parse_unsigned_long(s);

而不是:

c
format_into_buffer(...);

snprintf 的作用是把数值格式化成字符串写到目标缓冲区里;
你这里需要的是把字符串解析成整数写进 *(uint64_t *)card

所以即便你把参数“排正确”了,只要目标还是 snprintf,写进去的仍然是 ASCII 字符串,不是一个真正的 uint64_t 数值。


为什么 snprintf 语义不对

后面代码是这样用结果的:

asm
16b8 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 值,不是你想要的数。


你这题里最合理的修法

方案 1:源码修复,改成 strtoulsscanf

最正统的是:

c
char s[0x100] = {0}; read(0, s, 0xff); *(unsigned long *)card = strtoul(s, NULL, 10);

或者:

c
sscanf(s, "%lu", (unsigned long *)card);

这个才和后续逻辑匹配。


方案 2:二进制补丁,直接改成 scanf("%lu", card)

如果你是修复题、只能 patch 二进制,而且当前导入表里只有 __isoc99_scanf,没有 sscanf/strtoul,那最实用的是:

把这段:

asm
memset(s, 0, 0x100) read(0, s, 0x100) snprintf(card, 0x2072, s, 8)

改成:

asm
scanf("%lu", card)

也就是直接从 stdin 读一个 unsigned longcard[0]

但要注意:

这时前面的 read(0, s, 0x100) 必须一起去掉/跳过,不然输入已经被 read 吃掉了,后面的 scanf 会等下一次输入。

所以:

  • 只改 snprintf 调用点,不够
  • 必须把“读到 s 里”这一步也一起改掉

你说的“通过 write 系统调用修改参数顺序”行不行?

分两层说:

1. 如果你是说“用 patch 的方式改机器码”

那当然可以,任何能把字节写回 ELF 的方式都行,write 只是落盘手段之一。

2. 如果你是说“逻辑上还是保留 snprintf,只是把参数排对”

不行,因为函数选错了,不是传参顺序的问题。


最后给你一个明确结论

能不能这么修?

  • 可以 patch
  • 不可以继续用 snprintf
  • 正确目标应该是 scanf / sscanf / strtoul

对这题最省事的二进制修法

如果不想加新导入:

  • 删掉 read(0, s, 0x100)
  • snprintf(...) 改成 __isoc99_scanf("%lu", card)

这是最稳的。

你要是愿意,我可以直接按你这个 ELF 给你写一版汇编级 patch 方案,把 0x1644 到 0x1698 这一段具体怎么改成字节序列给你列出来。

問題

这个snprintf本意是把s变量的前八字节输出到[rbp+var_11C]是吗

思考

Clarifying snprintf and explaining likely intent

The user’s question is clear now: no, it does not output to [rbp+var_11C]. That location is just an index. The destination is actually the card pointer from the array at that index. It’s also not the first eight bytes of ss is used as the format string. It seems the intent was likely something like snprintf(chunk, 8, "%lu", ...), but more realistically sscanf(s, "%lu", card) could be what's intended, especially with the flow involving reading the user input size, followed by comparisons. This logic suggests the goal was parsing a number from s into *(uint64_t *)card.

Explaining the calling convention and snprintf usage

The %lu at address 2072 isn't used as a format string because it's in rsi. var_11C is just an index, loaded earlier. The snprintf signature should be clarified—if it were snprintf(dst, 8, "%s", s), the first 8 bytes might make sense, but that’s not the case here. The 8 in ecx just signals one variadic argument, not the first 8 bytes of s. In fact, since the format string is user-controlled, multiple arguments could follow if s contains more specifiers. This is helpful insight.

Thought for 39s

回答

不是。[rbp+var_11C] 这里不是输出位置,它只是前面 scanf("%d", &idx) 读进来的 index

按 x86-64 SysV 调用约定,snprintf 参数是:

c
int snprintf(char *str, size_t size, const char *format, ...);

对应寄存器:

  • rdi = str
  • rsi = size
  • rdx = format
  • rcx = 第一个可变参数

你这段在 call 前实际是:

asm
1662 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

所以它真正等价于:

c
snprintf(cards[idx], 0x2072, s, 8);

也就是:

  • 输出目标cards[idx]
  • size0x2072(因为把 "%lu" 的地址误塞进了 rsi
  • formats
  • 第一个可变参数8

所以不是“把 s 的前 8 字节输出到 [rbp+var_11C]

更准确地说:

  • var_11C 只是 idx
  • s 被当成了 format string
  • 8 不是“拷贝 8 字节”的意思,而是 snprintf第一个格式化参数

例如如果 s 是:

c
"%lu"

那效果近似是:

c
snprintf(cards[idx], 0x2072, "%lu", 8);

输出字符串 "8"cards[idx]

如果 s 是:

c
"%p %p %p"

那就会把后续寄存器/栈里的值拿来格式化,形成你说的格式化字符串漏洞

如果 s 是:

c
"%n"

还会尝试写内存。


“前 8 字节”这个误解来自哪里

因为你看到了:

asm
mov ecx, 8

但这里的 8 不是长度参数。
snprintf 的长度参数在 rsi,而 rsi 现在装的是 "%lu" 的地址。

所以这里完全不是

c
snprintf(dst, 8, "%s", s);

也不是:

c
write(dst, s, 8);

这段代码更像作者本来想写什么?

结合后面逻辑:

asm
16b8 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)

很明显它想做的是:

  1. 读入一个“新长度”
  2. 写到 *(uint64_t *)cards[idx]
  3. 如果新长度大于旧长度,就恢复旧长度
  4. 然后按这个长度往 card+8 读内容

所以作者本意大概率不是输出字符串,而是想:

c
sscanf(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。

問題

292b61a5-e438-4b9e-a553-48b8a10411c1.png

这里printf可以改成write吗?怎么改

思考

Explaining the use of write@plt in binary patching

To use write@plt, after reading into the buffer, we can move the size into rdx, the buffer address into rsi, and set edi to 1 for stdout. If raw syscall is preferred, we'd set up for rax=1 and call the syscall directly. For binary patching, it's simpler to use write@plt if it's already imported; otherwise, we might need to patch it manually.

Explaining how to handle the write syscall and patch

Directly using the write syscall requires careful handling of the arguments and ensuring space for extra code. Since printf takes the buffer as rdi, all arguments need to be properly set for write. If you want a minimal patch, using printf("%.*s", n, buf) is easiest, but be careful with NUL termination.

Thought for 1m 9s

回答

可以,而且这处write 比用 printf("%s", buf) 更稳
因为这里原代码:

c
buf = malloc(0x400); read(0, buf, 0x400); printf((char *)buf);

问题有两个:

  1. printf((char *)buf)格式化字符串漏洞
  2. read 不会自动补 \0,所以就算你改成 printf("%s", buf),也还是可能继续读到堆上后面的垃圾数据

正确改法

最合理的是改成:

c
ssize_t n = read(0, buf, 0x400); if (n > 0) { write(1, buf, n); }

这样:

  • 不再把输入当 format
  • 只输出真实读到的长度
  • 不依赖 \0
  • 不会把 malloc 出来的未初始化尾部一起泄露出去

为什么不能直接改成 write(1, buf, 0x400)

可以这样写,但不推荐

c
write(1, buf, 0x400);

因为如果 read 实际只读了 20 字节,后面 0x400-20 字节还是堆上的未初始化内容。
你这样会把这些内容也输出,造成信息泄露

所以一定要用 read 的返回值当长度。


汇编上怎么改

你图里这段:

asm
mov rax, [rbp+buf] mov rdi, rax mov eax, 0 call _printf

如果前面刚执行完 read,那么:

  • rax 里原本就是 read 的返回值 n
  • 但你这句 mov rax, [rbp+buf] 把它覆盖掉了

所以要改成:

目标效果

c
write(1, buf, n);

x64 SysV 下 write 参数

c
ssize_t write(int fd, const void *buf, size_t count);

寄存器是:

  • rdi = 1
  • rsi = buf
  • rdx = n

如果程序已经导入了 write@plt

那最直接的 patch 是把 printf 那 17 字节改成:

asm
mov 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

这个逻辑就是:

c
write(1, buf, n);

一个细节:这只是“最小 patch”,还不够健壮

因为如果 read 返回:

  • 0:EOF
  • -1:失败

那你直接:

asm
mov rdx, rax

就会把 -1 变成超大长度,出事。

所以严谨版应该先判断:

asm
test rax, rax jle short skip_write mov rdx, rax mov rsi, [rbp+buf] mov edi, 1 call _write skip_write:

如果二进制里没有 write@plt

那你有两个办法:

方法 1:直接打 syscall

Linux x86_64 下:

  • rax = 1sys_write
  • rdi = 1
  • rsi = buf
  • rdx = n

即:

asm
test rax, rax jle short skip_write mov rdx, rax mov rsi, [rbp+buf] mov edi, 1 mov eax, 1 syscall skip_write:

方法 2:不用 write,改成安全字符串输出

如果你只是想修漏洞,不执着于 write,也可以改成:

c
ssize_t n = read(0, buf, 0x3ff); if (n > 0) { ((char *)buf)[n] = 0; printf("%s", (char *)buf); }

但这比 write 麻烦,因为:

  • 要改 read 长度成 0x3ff
  • 还要补 \0

结论

这里 printf 可以改成 write

而且这是个好修法

最推荐的逻辑

c
n = read(0, buf, 0x400); if (n > 0) write(1, buf, n);

最小汇编替换思路

把:

asm
mov rax, [rbp+buf] mov rdi, rax mov eax, 0 call _printf

改成:

asm
mov 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吗

思考

Thought for 39s

回答

不一定。从“真正修复”角度看,write(1, buf, 0x400) 不算合格修复。

原因很直接:

c
buf = malloc(0x400); n = read(0, buf, 0x400); write(1, buf, 0x400);

这里 read 实际可能只读到很少的数据,比如 10 字节。
这时候你却把整整 0x400 字节都打出去,后面剩下的就是:

  • 堆上未初始化数据
  • 可能造成信息泄露
  • 某些 checker 如果按“输出应等于输入”来比对,也会直接判失败

所以:

  • 如果 check 很松,只检查“格式化字符串没了,程序还能跑”,它可能过。
  • 如果 check 稍微正规一点,会看有没有新漏洞/信息泄露,那它大概率不过。

更稳的写法是:

c
ssize_t n = read(0, buf, 0x400); if (n > 0) write(1, buf, n);

这个才是标准修法。

如果你现在做的是 patch 题的自动 check,我的判断是:
write(1, buf, 0x400) 有概率过简单 check,但不应该当成正确答案。
想稳过,最好用 read 返回值当第三个参数。

問題

puts是printf的上位替代吗

思考

Thought for 6s

回答

不是。puts 不是 printf 的上位替代,只是更受限的一个字符串输出函数

区别

printf

c
printf("x=%d\n", x); printf("%s", buf);

puts

c
puts(buf);

puts 的特点是:

  1. 只能输出一个字符串 不能像 printf 那样做格式化。

  2. 会自动在末尾加一个换行 puts("abc") 实际输出是 abc\n

  3. 要求参数是以 \0 结尾的字符串 它会一直读到空字节为止。


所以它不是“更强”,而是“更窄”

大概关系更像:

  • puts(s)printf("%s\n", s)
  • 但不支持 %d%p%lx 这些格式化能力

在修格式化字符串漏洞时,能不能把 printf(buf) 改成 puts(buf)

有时能修掉 format bug,但不一定安全

例如:

c
read(0, buf, 0x400); printf(buf);

改成:

c
puts(buf);

格式化字符串漏洞确实没了,因为 puts 不解析 %n/%p/%s

但是这里还有个大问题:

c
read(0, buf, 0x400);

read 不会自动补 \0
所以 puts(buf) 可能继续往后读,直到碰到某个偶然的 \0,导致:

  • 越界读
  • 信息泄露
  • 崩溃

你这个场景里谁更合适

如果数据来自 read,优先级一般是:

最稳

c
n = read(0, buf, 0x400); if (n > 0) write(1, buf, n);

也可以

c
n = read(0, buf, 0x3ff); if (n > 0) { buf[n] = '\0'; puts(buf); }

但它会自动多输出一个换行,行为和原程序未必完全一致。


一句话总结

puts 不是 printf 的上位替代。
它更像是一个只会输出 C 字符串、并且自动补换行的简化版函数。
在你这种 read 读入原始字节的修复题里,write 往往比 puts 更合适。

分享這個問答