Behind the Scenes (HTB rev)
SIGILL handlers hide the real password compare from ltrace and strace, so read it statically
The binary deliberately executes UD2 to raise SIGILL and runs the real password logic inside the signal handler, so dynamic tracing shows nothing. I skipped the dynamic angle and pulled the flag straight out of the binary with a hex editor.
the challenge
Behind the Scenes is a 64-bit reversing challenge. One file, an ELF:
behindthescenes: 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
It is a PIE and, helpfully, not stripped, so main and the handler keep their symbol names. It wants a password on the command line:
$ ./behindthescenes
./challenge <password>
The only two strings of interest in the binary are the usage line and the success format:
$ strings behindthescenes | grep -iE 'HTB|password'
./challenge <password>
> HTB{%s}
So it prints > HTB{...} once the password matches. The whole challenge is in how the comparison hides from the obvious tools.
analysis
My first instinct was to trace the comparison dynamically. Both ltrace and strace came back with nothing useful: no visible strcmp, no readable arguments, no syscall that exposed the check. That is the tell the challenge name points at. The work is happening somewhere the tracer cannot follow.
The reason is in main. The function never runs straight through. Every few instructions there is a ud2:
1261 <main>:
1261: endbr64
...
12b3: call 1130 <sigemptyset@plt>
12b8: lea rax,[rip-0x96] ; -> 1229 <segill_sigaction>
12bf: mov QWORD PTR [rbp-0xa0],rax
12c6: mov DWORD PTR [rbp-0x18],0x4 ; SIGILL = 4
...
12dc: mov edi,0x4 ; signum = SIGILL
12e1: call 10e0 <sigaction@plt>
12e6: ud2 ; <-- illegal instruction
12e8: cmp DWORD PTR [rbp-0xa4],0x2 ; argc == 2 ?
12ef: je 130b
12f1: ud2
12f3: lea rdi,[rip+0xd0a] ; "./challenge <password>"
12fa: call 10d0 <puts@plt>
12ff: ud2
ud2 is the two-byte opcode 0f 0b. On x86-64 it raises an Invalid Opcode Exception, which the kernel delivers to the process as SIGILL. There are fifteen of them scattered through main:
$ objdump -d behindthescenes | grep -c ud2
15
Before the first one runs, main registers a handler with sigaction(SIGILL, ...). The handler is segill_sigaction, and it is tiny:
1229 <segill_sigaction>:
1229: endbr64
122d: push rbp
122e: mov rbp,rsp
1231: mov DWORD PTR [rbp-0x14],edi ; signum
1234: mov QWORD PTR [rbp-0x20],rsi ; siginfo_t *
1238: mov QWORD PTR [rbp-0x28],rdx ; void *ucontext
123c: mov rax,QWORD PTR [rbp-0x28]
1240: mov QWORD PTR [rbp-0x8],rax
1244: mov rax,QWORD PTR [rbp-0x8]
1248: mov rax,QWORD PTR [rax+0xa8] ; ctx->uc_mcontext.gregs[REG_RIP]
124f: lea rdx,[rax+0x2] ; rip + 2
1253: mov rax,QWORD PTR [rbp-0x8]
1257: mov QWORD PTR [rax+0xa8],rdx ; write rip back, advanced
125e: nop
125f: pop rbp
1260: ret
The handler reads the third argument, the ucontext, fishes the saved instruction pointer out of it at offset 0xa8 (the REG_RIP slot inside uc_mcontext.gregs), adds 2, and writes it back. Two bytes is exactly the length of ud2. So every time execution trips on a ud2, the kernel diverts into segill_sigaction, which rewinds the saved rip past the faulting bytes and returns. Control resumes on the instruction after the ud2, as if nothing happened.
That is the entire anti-analysis trick. The program runs correctly because the handler papers over each fault, but the real control flow hops through the signal machinery instead of the normal instruction stream. ltrace/strace see only the sigaction setup and the faults, never the comparison, because the comparison runs between two faults under a handler that the tracer is not stepping into. Naive disassemblers also stop at each ud2: tools like Ghidra assume an illegal instruction terminates the path and refuse to disassemble past it, so the body between the ud2 markers looks like dead bytes.
But objdump linear-sweeps right through it. With the ud2 noise mentally stripped, main is a plain four-stage password check. First it confirms argc == 2 and that the password is exactly twelve bytes:
130d: mov rax,QWORD PTR [rbp-0xb0]
1314: add rax,0x8 ; argv[1]
1318: mov rax,QWORD PTR [rax]
131e: call 10f0 <strlen@plt>
1323: cmp rax,0xc ; len == 12 ?
1327: jne 1432
Then four strncmp calls, three bytes each, walking the password in three-character chunks against four rodata constants:
; chunk 0: argv[1][0..2] vs 0x201b
133d: mov edx,0x3
1342: lea rsi,[rip+0xcd2] ; 0x201b
134c: call 10c0 <strncmp@plt>
; chunk 1: argv[1]+3 vs 0x201f
1369: add rax,0x3
1372: lea rsi,[rip+0xca6] ; 0x201f
137c: call 10c0 <strncmp@plt>
; chunk 2: argv[1]+6 vs 0x2023
1399: add rax,0x6
13a2: lea rsi,[rip+0xc7a] ; 0x2023
13ac: call 10c0 <strncmp@plt>
; chunk 3: argv[1]+9 vs 0x2027
13c5: add rax,0x9
13ce: lea rsi,[rip+0xc52] ; 0x2027
13d8: call 10c0 <strncmp@plt>
; all four matched -> printf("> HTB{%s}\n", argv[1])
13f4: lea rdi,[rip+0xc30] ; "> HTB{%s}\n"
1400: call 1110 <printf@plt>
If every chunk matches, the password is reflected into > HTB{%s}. The flag is therefore the password itself.
the solve
Satisfying the obfuscated check at runtime is the slow path. The four comparison constants are plain bytes in .rodata, and the success string just echoes the password back, so the answer is already in the file. I dumped .rodata:
$ objdump -s -j .rodata behindthescenes
2000 01000200 2e2f6368 616c6c65 6e676520 ...../challenge
2010 3c706173 73776f72 643e0049 747a005f <password>.Itz._
2020 306e004c 795f0055 4432003e 20485442 0n.Ly_.UD2.> HTB
2030 7b25737d 0a00 {%s}..
Lining the four strncmp targets up with their addresses:
0x201b->Itz(49 74 7a, then00)0x201f->_0n(5f 30 6e, then00)0x2023->Ly_(4c 79 5f, then00)0x2027->UD2(55 44 32, then00)
Concatenated in chunk order, the four constants form a twelve-byte password that matches the len == 12 gate exactly. Feeding it back on the command line clears all four strncmp calls, and the binary reflects it into > HTB{%s} as the success line:
$ ./behindthescenes <recovered-password>
> HTB{...}
No need to defeat the signal trick at all. A hex editor or objdump -s reads the password straight out of the constants the comparison points at. If I had wanted the dynamic route instead, the patch is just as mechanical: replace each 0f 0b (ud2) with 90 90 (two nops) and the binary runs as a normal linear program that strncmp traces happily.
the flag
The flag is the twelve-byte password wrapped in HTB{...}, and its wording calls out that it was only UD2 doing the hiding. The takeaway held: when ltrace and strace go silent because of SIGILL and signal-redirected control flow, stop fighting the runtime and read the binary. The data the comparison references, the rodata constants and the format string, gives up the answer with no execution at all.