这道逆向题我已经解出flag了,根据我的动态调试,程序并不检查1-5以及最后一字节内容,只检查6-2...

創建於:2026年3月24日

使用 GPT-5.4 Pro 回答 Chat01

問題

nssctf_round2_findxenny.exe

这道逆向题我已经解出flag了,根据我的动态调试,程序并不检查1-5以及最后一字节内容,只检查6-27字节
v13 = (_QWORD *)(sub_7FF7699815EB((__int64)flag) + 5);这一行就是把flag的第六字节指针赋值给v13
我输入flag{oh_you_found_our_x3nny1是可以通过检查的,这很奇怪,我看代码,对第28字节是有检查的呀:
debug034:000001D23D75A980 sub_1D23D75A980 proc near
debug034:000001D23D75A980
debug034:000001D23D75A980 var_20 = qword ptr -20h
debug034:000001D23D75A980
debug034:000001D23D75A980 sub rsp, 20h
debug034:000001D23D75A984 mov [rsp+20h+var_20], rcx
debug034:000001D23D75A988 mov r10d, 7C072E27h
debug034:000001D23D75A98E xor r10d, 12345678h
debug034:000001D23D75A995 mov edi, dword ptr [rsp+20h+var_20]
debug034:000001D23D75A998 cmp edi, r10d
debug034:000001D23D75A99B jnz short loc_1D23D75A9BB
debug034:000001D23D75A99D mov edi, dword ptr [rsp+20h+var_20+4]
debug034:000001D23D75A9A1 mov r10d, 87653A4Fh
debug034:000001D23D75A9A7 xor r10d, 87654321h
debug034:000001D23D75A9AE cmp edi, r10d
debug034:000001D23D75A9B1 jnz short loc_1D23D75A9BB
debug034:000001D23D75A9B3 xor rax, rax
debug034:000001D23D75A9B6 add rsp, 20h
debug034:000001D23D75A9BA retn
debug034:000001D23D75A9BB ; ---------------------------------------------------------------------------
debug034:000001D23D75A9BB
debug034:000001D23D75A9BB loc_1D23D75A9BB: ; CODE XREF: sub_1D23D75A980+1B↑j
debug034:000001D23D75A9BB ; sub_1D23D75A980+31↑j
debug034:000001D23D75A9BB add rsp, 20h
debug034:000001D23D75A9BF xor rax, rax
debug034:000001D23D75A9C2 sub rax, 1
debug034:000001D23D75A9C6 retn
debug034:000001D23D75A9C6 sub_1D23D75A980 endp
解释一下

思考

In this passage, the user's request involves examining an executable file, particularly analyzing assembly code and addressing byte manipulation.

Inspecting and preparing

Interested in using API tool skills, inspecting files via container, and analyzing the uploaded exe with objdump for binary code analysis.

Assessing skills

I'm looking at the skill creator here. Wasn't clear initially, but inspecting the binary now with file, rabin2, and objdump. Progress is steady and promising.

Combining objdump directives

I'm considering objdump -d -Mintel to disassemble addresses and compute RVAs. Mapping to the file might involve llvm-objdump. Inspecting sections and symbols seems essential.

Locating function call
I'm trying to pinpoint the function call and its constants by examining the RVA and using objdump near the specified offset.

Investigating address representation

I'm exploring if objdump doesn't display addresses in hexadecimal. Grepping the output might help clarify things.

Breaking down references

I’m listing out components to disassemble, including chanter and drones. This helps ensure everything is included for a comprehensive analysis.

Investigating further

Examining the executable's behavior after self-modification involves looking into the constant 7C072E27 and xor operation with 12345678, possibly searching for bytes or disassembly.

Decrypting code logic

I'm examining dynamically decrypted code, possibly involving runtime stub or packer. Parsing the snippet to identify function operations and logic, particularly around sub_1D23D75A980 call and pointer offset.

Investigating function behavior

I’m thinking through a function's use of pointers and integers, considering stack variable confusion and potential decompiler errors. Trying to understand how _QWORD *v13 is utilized.

Examining byte concatenation

I'm considering byte grouping 0x0000796E to 6E 79 00 00 in little endian format and its effect on memory comparisons.

Tracing byte alignment

I’m noting the alignment of bytes and pointers, examining shifts in data chunks and null characters.

Evaluating function start

I examined function starts at bytes 22, 21, and 23, but none matched ny\0\0. Continuing to analyze the actual checker structure in the binary.

Analyzing code segments

I’m examining the code segments, possibly identified as flag_1 or similar. Searching through strings and functions for references to these segments, I’ll consider tools like objdump -s, ghidra, llvm-pdbutil, strings, and rabin2 -zz.

Evaluating options

I’m exploring Ghidra and radare2 for reverse engineering tasks, assessing their feasibility and compatibility.

Evaluating capabilities

I’m gathering info on using LLVM-objdump with xrefs or Python with pefile and capstone for analysis.

Reflecting on installation ease

Thinking installation might be easier with objdump and Python parsing. Considering the need for a reason from a user snippet.

Identifying functions

I’m pulling together functions that reference specific strings. Using objdump to get details on address-related references.

Evaluating tools and methods

I'm piecing together the use of llvm-objdump -d --print-imm-hex and running the binary under Wine, weighing the feasibility and installation requirements.

Disasm scanning input

OK, I’m checking the disassembly for input forms consistently.

Checking string patterns
I’m gathering info on LLVM objdump for detailed string patterns around main. Also, considering strings -td for inspecting .rdata strings and interesting ASCII sequences.

Tracking address details

I’m mapping out the address 80432 in the file, working through decimal to VA conversion, and using objdump to explore section offsets for the VA around 0x1400250.

Analyzing disassembly

I’m examining the disassembly input, focusing on location 0x140025230 within the .rdata section, and exploring any references to this address.

Disassembling components

Unpacking code details

I'm digging into how dynamic code stores function pointers in .data, loading 64-bit values for comparisons, and specifically zeroing out the last input character, leading to potential data leakage.

Assigning values

I’m setting up values for chunk pointers based on input length and byte pattern. Crunching bytes 22-27, the 28th byte gets zeroed, matching the constants.

Fixing the lines

OK, let me see: patching incorrect lines in 'lib/entry_pg.c' using grep to ensure disassembly starts at 0x140011514.

Decoding assembly

Gathering insights around instruction at address 18420. Noticing patterns in code execution and flow.

Investigating functions

I’m examining the function at a different location and searching for a new range to continue the assessment.

Examining key functions

OK, let me see. I’m digging into VirtualProtect and function pointer writes to understand their roles and implications. This suggests a deep dive into system-level functionality.

Clarifying function details

I’m curious about how the function arguments are loaded and split. Also, examining the key instruction that zeroes the last user-supplied character, ensuring specific bytes are checked.

Looking at string validation

Hm, I'm thinking about the program's string length requirement and the importance of zeroing the last character. Strings longer than 28 should be carefully checked.

Piecing together functions

I’m gathering info on dynamically disarming byte arrays to reconstruct runtime functions, using tools like objdump or direct binary parsing.

Breaking down the code

I’m digging into the decryption logic to understand how the final bytes get overwritten, causing a crucial shift in memory handling.

Investigating buffer handling

I’m analyzing the buffer and calling convention, considering byte positions, and examining the assembly code and pseudocode for consistency and correct value extraction.

Examining performance issues

I'm piecing together subtle details in memory access and string termination, ensuring "0 n y 0" aligns correctly by analyzing qword positions and string storage nuances.

Noticing dynamic allocation

The function sub_1D23D75A980 is dynamically generated and heap-allocated, explaining differences from the static file's .text segment.

Breaking down the process

I’m examining three key points: main flow decoding, clearing the last character of flag, and comparing three qwords from flagged byte chunks.

Comfortable calculations

OK, let me see. Calculating constants now. Processing the third qword's bytes, ensuring the valid input length is 28.

Overwriting memory

OK, let me see. The highest byte of the overwritten QWORD is zero, which aligns with the expected behavior after the instruction.

Confirming details

Piecing together the dynamic address and prefix check for the main logic, verifying candidate length for input validation.

Verifying string lengths

Checking string lengths to ensure 28-character input works, considering varied lengths and byte positions, and addressing potential buffer issues.

Highlighting key concepts

While iterating through the analysis, I clarify dynamic function parameters as 8-byte values, showcasing static assembly, pseudocode, and testing input constraints for nuanced understanding.

Ensuring clarity

I’m working through the explanation of a code snippet, including file manipulation and detailed logic. Considering the user's input, I’m addressing potential discrepancies and focusing on effective refinement.

Piecing together insights

I'm mapping out the reliability of a string buffer length and examining the dynamics of a function's runtime decryption and heap constructs. Progress is steady.

Reading buffer positions

Consulting the buffer's dynamic function, the 28th byte, _x3nny\0\0, aligns perfectly with decoding rules.

Mapping out constraints

I'm piecing together the word length and character restrictions to ensure clarity and precision.

Thought for 25m 8s

回答

你这个现象其实一点都不奇怪,关键有两个坑:

第一,这个函数不是在比较 rcx 指向的内存内容,而是在比较传进来的 rcx 这个 64 位参数本身
第二,主逻辑在调用它之前,先把你输入的最后一个字符改成了 0,所以你看到“第 28 字节好像被检查了”,但你输入的那个 1 其实已经被抹掉了。

你贴的这段汇编,真正含义是:

asm
sub rsp, 20h mov [rsp], rcx ; 先把 rcx 保存到栈上 mov r10d, 7C072E27h xor r10d, 12345678h ; r10d = 6E33785Fh mov edi, dword ptr [rsp] cmp edi, r10d ; 比较 rcx 的低 4 字节 jnz fail mov edi, dword ptr [rsp+4] mov r10d, 87653A4Fh xor r10d, 87654321h ; r10d = 0000796Eh cmp edi, r10d ; 比较 rcx 的高 4 字节 jnz fail

也就是说它等价于:

c
int check(uint64_t x) { if ((uint32_t)x != 0x6E33785F) return -1; if ((uint32_t)(x >> 32) != 0x0000796E) return -1; return 0; }

把这 8 个字节按小端展开,就是:

c
0x0000796E6E33785F -> 5F 78 33 6E 6E 79 00 00 -> _ x 3 n n y \0 \0

所以这个检查函数要求的其实是:

text
"_x3nny\0\0"

不是 "_x3nny1"


更关键的是,我看了主逻辑,里面有这么一段:

asm
140018cec: addq $0x5, %rax 140018cf0: movq %rax, 0x48(%rbp) ; p = flag + 5 140018cf4: ... ; rax = strlen(flag) 140018cfd: movq 0x48(%rbp), %rcx ; rcx = p = flag + 5 140018d01: movb $0x0, -0x6(%rcx,%rax)

这一句:

asm
movb $0x0, -0x6(%rcx,%rax)

代进去就是:

c
p = flag + 5; p[len - 6] = 0;

因为 p = flag + 5,所以这其实等价于:

c
flag[len - 1] = 0;

也就是:

程序会把输入串的最后一个字符直接改成 \0


所以你输入:

text
flag{oh_you_found_our_x3nny1

长度是 28。程序在检查前会把第 28 个字符 '1' 改成 0,内存里变成:

text
flag{oh_you_found_our_x3nny\0

再加上字符串本来结尾还有一个自然的 \0,最后那 8 字节就正好是:

text
_x3nny\0\0

完全符合这个函数的比较条件。


再结合主函数的调用方式,你的理解也能彻底串起来:

asm
movq (%rax), %rcx call check1 movq (%rax), %rcx call check2 movq (%rax), %rcx call check3

这里是先把 v13 指向的位置当成 _QWORD* 分三段取值:

  • v13[0] -> 第 6 到 13 字节
  • v13[1] -> 第 14 到 21 字节
  • v13[2] -> 第 22 到 29 字节

所以程序确实是在按 8 字节块检查 6~29 这个范围,但第 28 字节在检查前已经被程序自己清零了,第 29 字节本来就是字符串终止符 \0
因此从“你可控的输入字符”角度说,确实只有 第 6 到 27 字节是实质参与校验的。

结论就是:

  • 你动态调试得到的“只检查 6-27”这个结论,本质上是对的
  • 静态上看到“检查到第 28 字节”,也没错
  • 真相是:第 28 字节会先被改成 0,然后再参与比较
  • 所以你输入的最后一个字符是什么,其实都无所谓,只要总长度对上,它都会被抹掉

换句话说,这题的 check 其实是个“多解”校验,不是唯一 flag。

分享這個問答