NOThing to C Here
Move along, NOThing to see here, no flags in sight…
Running file
against the executable:
└─$ file NothingToC
NothingToC: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d1f89a3ac713a435ad3b35e178b2d49548640c86, for GNU/Linux 4.4.0, stripped
The binary provided with the challenge is a 64-bit, dynamically linked, PIE-enabled, stripped of debug info.
Executing the binary:
└─$ ./NothingToC
=== NOTing To C Flag Checker ===
Ready to check your flag? Let's see what you've got!
Enter the flag to check: AAAA
Oops! Your flag is 4 characters long, but I'm looking for exactly 28 characters.
Maybe count your characters next time?
Hmm... that does NOT look right...
It appears that the executable expects the flag as input and requires it to be 28 characters long.
Load it into IDA for static analysis. Press F5 to generate pseudocode.
Now open the main function from the Funtions list and let’s take a closer look at the generated pseudocode.
__int64 __fastcall main(int a1, char **a2, char **a3)
{
unsigned int v3; // eax
char *v4; // rsi
char *v5; // rdi
size_t v6; // rax
char *v7; // rdx
_BYTE *v8; // rcx
__int64 result; // rax
void (*v10)(void); // rdx
__m128i v11[2]; // [rsp+0h] [rbp-138h] BYREF
char s[28]; // [rsp+20h] [rbp-118h] BYREF
char v13[236]; // [rsp+3Ch] [rbp-FCh] BYREF
unsigned __int64 v14; // [rsp+128h] [rbp-10h]
v14 = __readfsqword(0x28u);
v11[0] = _mm_load_si128((const __m128i *)&xmmword_2240);
*(__m128i *)((char *)v11 + 12) = _mm_load_si128((const __m128i *)&xmmword_2250);
v3 = time(0LL);
srand(v3);
puts(" === NOTing To C Flag Checker === ");
puts("Ready to check your flag? Let's see what you've got!\n");
printf("Enter the flag to check: ");
v4 = (char *)&qword_100;
if ( fgets(s, 256, stdin) )
{
v5 = s;
s[strcspn(s, "\n")] = 0;
v6 = strlen(s);
if ( v6 == 28 )
{
v7 = s;
v8 = (_BYTE *)v11;
v4 = v13;
while ( *v8 == ~*v7 )
{
++v7;
++v8;
if ( v7 == v13 )
{
puts("\nCONGRATULATIONS!");
v4 = s;
v5 = "The flag is: %s\n";
printf("The flag is: %s\n", s);
goto LABEL_10;
}
}
}
else
{
v4 = (char *)v6;
printf("Oops! Your flag is %zu characters long, but I'm looking for exactly %d characters.\n", v6, 28LL);
v5 = "Maybe count your characters next time?";
puts("Maybe count your characters next time?");
}
switch ( rand() % 6 )
{
case 0:
v5 = "Nice try, but that's NOT it!";
puts("Nice try, but that's NOT it!");
break;
case 1:
v5 = "Hmm... that does NOT look right...";
puts("Hmm... that does NOT look right...");
break;
case 2:
v5 = "NOT quite it! Keep trying!";
puts("NOT quite it! Keep trying!");
break;
case 3:
v5 = "Incorrect! But do NOT give up!";
puts("Incorrect! But do NOT give up!");
break;
case 4:
v5 = "That's NOT the flag I'm looking for!";
puts("That's NOT the flag I'm looking for!");
break;
case 5:
v5 = "NOPE! This flag is NOT what I expected.";
puts("NOPE! This flag is NOT what I expected.");
break;
default:
break;
}
LABEL_10:
result = 0LL;
}
else
{
v5 = "Hmm, seems like you're having trouble typing...";
puts("Hmm, seems like you're having trouble typing...");
result = 1LL;
}
v10 = (void (*)(void))(v14 - __readfsqword(0x28u));
if ( v10 )
start((__int64)v5, (__int64)v4, v10);
return result;
}
The variable s
is the input buffer where the flag is stored, it’s declared as char s[28]
and populated by the fgets(s, 256, stdin)
call.
Here’s what the program does:
- Input: Reads the input into buffer
s
- Length check: Verifies the input is exactly 28 characters (0x1C)
- Character comparison: Compares each character of the input against a stored pattern using Bitwise NOT
The important logic is in the loop:
while ( *v8 == ~*v7 )
This compares each byte of the stored pattern v11 (v8)
with the Bitwise NOT of each character from the input s
. If the stored byte is X, the input character needs to be ~X (Bitwise complement).
Now let’s go back to the beginning of the code.
The correct flag characters are the Bitwise NOT of the bytes stored in v11
. The pattern is loaded from xmmword_2240
and xmmword_2250
.
v11[0] = _mm_load_si128((const __m128i *)&xmmword_2240);
*(__m128i *)((char *)v11 + 12) = _mm_load_si128((const __m128i *)&xmmword_2250);
First line: Loads 16 bytes from xmmword_2240
into the start of buffer v11
Second line: Loads 16 bytes from xmmword_2250
into v11
starting at offset 12. (overwrites the last 4 bytes of xmmword_2240)
The memory layout should result something like this:
v11: [----16 bytes from 2240----][----16 bytes from 2250----]
[0.................11][12................27]
Bytes 0-11: From xmmword_2240
Bytes 12-27: From xmmword_2250
Total: 28 bytes (the flag length)
If you click on any of them, IDA will take you to their location.
.rodata:0000000000002240 xmmword_2240 xmmword 0A0CA96A0CA9697C884B9ABBC9E8B9AB2h
.rodata:0000000000002240 ; DATA XREF: main+A↑r
.rodata:0000000000002250 xmmword_2250 xmmword 8286CB88A0CC978BA08BCF91A0CA96A0h
v11
memory layout (28 bytes total):
Offset: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
[B2 9A 8B 9E BC AB B9 84 C8 97 96 CA][A0 96 CA A0 91 CF 8B A0 8B 97 CC A0 88 CB 86 82]
|----------from xmmword_2240----------|----------from xmmword_2250----------------|
Applying the bitwise NOT operation reveals the flag.
CyberChef recipe for Bitwise NOT