This page has been machine-translated from the original page.
I participated in SECCON CTF 13 with the team 0nePadding.
Individually I solved 2 Rev challenges, finishing 34th domestically and 89th overall.
I came away a bit unsatisfied, but lately I haven’t had much time to do thorough post-mortems, so I’ll write up a quick summary for now.
packed (Rev)
Packer is one of the most common technique malwares are using.
Inspecting the challenge binary revealed that it was a UPX-packed file.
I used the upx command to unpack it and then analyzed the unpacked binary in Binary Ninja.
However, the unpacked binary was implemented to always display a Wrong error without ever validating the input, so it was not useful for identifying the correct flag.
So I tried debugging the original (still-packed) binary with gdb, and found code that checks whether the input length is 0x31 and then validates it against the correct flag.
In other words, I suspected the binary had been modified in some way so that part of the code is not included in the UPX-unpacked output.
I couldn’t immediately think of exactly what kind of modification was applied, so I decided to skip unpacking and analyze the binary dynamically as-is.
After passing the input-length check, the binary appears to XOR each input character against a hardcoded key.
The key used here can be easily retrieved with gdb.
The hardcoded byte array ultimately used for comparing against the correct flag was also easy to read out with gdb.
From there, the following solver decrypts the XOR cipher to obtain the correct flag.
key = [0xe8,0x4a,0x00,0x00,0x00,0x83,0xf9,0x49,0x75,0x44,0x53,0x57,0x48,0x8d,0x4c,0x37,0xfd,0x5e,0x56,0x5b,0xeb,0x2f,0x48,0x39,0xce,0x73,0x32,0x56,0x5e,0xac,0x3c,0x80,0x72,0x0a,0x3c,0x8f,0x77,0x06,0x80,0x7e,0xfe,0x0f,0x74,0x06,0x2c,0xe8,0x3c,0x01]
target = [0xbb,0x0f,0x43,0x43,0x4f,0xcd,0x82,0x1c,0x25,0x1c,0x0c,0x24,0x7f,0xf8,0x2e,0x68,0xcc,0x2d,0x09,0x3a,0xb4,0x48,0x78,0x56,0xaa,0x2c,0x42,0x3a,0x6a,0xcf,0x0f,0xdf,0x14,0x3a,0x4e,0xd0,0x1f,0x37,0xe4,0x17,0x90,0x39,0x2b,0x65,0x1c,0x8c,0x0f,0x7c]
for i in range(0x30):
print(chr(key[i]^target[i]),end="")
# SECCON{UPX_s7ub_1s_a_g0od_pl4c3_f0r_h1din6_c0d3}Jump (Rev)
Who would have predicted that ARM would become so popular?
※ We confirmed the binary of Jump accepts multiple flags. The SHA-1 of the correct flag is c69bc9382d04f8f3fbb92341143f2e3590a61a08 We’re sorry for your patience and inconvenience
I analyzed the ARM binary provided as the challenge binary using Binary Ninja.
The jumper function called from main checks whether command-line arguments are present, and if so, executes a RET to the execution address loaded onto the stack.
This appears to be the key characteristic of this binary: rather than making normal function calls, execution proceeds by RET-ing (ROP-style) to specific code locations within each function. (Probably an anti-analysis technique?)
When command-line arguments are present, the program jumps to the code immediately after the prologue of the target function.
Inside this function, a switch statement is defined that jumps to various processing paths based on a flag value that is updated during execution.
At first glance the implementation looks unreadable, but by using gdb to trace the actual jump targets, I was able to confirm that execution eventually reaches code inside the following checker function.
void checker(int32_t* arg1)
{
data_41203c = 1;
switch (((uint64_t)index))
{
case 0: // SECC
{
uint64_t var_30_4 = ((uint64_t)*(uint32_t*)arg1);
int64_t (* var_28_4)() = sub_400b48;
break; // SECC
}
case 4: // ON{5
{
uint64_t var_30_1 = ((uint64_t)*(uint32_t*)((char*)arg1 + ((int64_t)index)));
int64_t (* var_28_1)() = sub_400aa8;
break; // ON{5
}
case 8: // h4k3
{
uint64_t var_30_2 = ((uint64_t)*(uint32_t*)((char*)arg1 + ((int64_t)index)));
int64_t (* var_28_2)() = sub_400ae4;
break; // h4k3
}
case 0xc: // _1t_
{
uint64_t var_30_5 = ((uint64_t)*(uint32_t*)((char*)arg1 + ((int64_t)index)));
void* const var_28_5 = &data_400b84;
break; // _1t_
}
case 0x10: // up_5
{
int32_t* var_30_7 = arg1;
void* const var_28_7 = &data_400bd4;
break; // up_5
}
case 0x14: // BBBB + up_5 = 0x9d949ddd
{
int32_t* var_30_3 = arg1;
int64_t (* var_28_3)() = sub_400b14;
break; // BBBB + up_5 = 0x9d949ddd
}
case 0x18: // CCCC + BBBB = 0x9d9d6295
{
int32_t* var_30 = arg1;
int64_t (* var_28)() = sub_400a6c;
break; // CCCC + BBBB = 0x9d9d6295
}
case 0x1c: // DDDD - CCCC = 0x47cb363b
{
int32_t* var_30_6 = arg1;
int64_t (* var_28_6)() = sub_400ba4;
break; // DDDD - CCCC = 0x47cb363b
}
}
}This code is implemented to jump to a different validation routine depending on which position of the flag string (received from command-line arguments) is being checked.
However, this binary had a bug that caused flag checking to not function correctly, so I stopped the dynamic analysis here and decided to identify the correct flag through static analysis.
Analyzing the code reveals that the first 16 characters can simply be determined by XOR-ing hardcoded data with a key.
XOR-ing 4 characters at a time, I identified the first 16 characters as SECCON{5h4k3_1t.
The following 16 characters were validated by checking whether the sum or difference of the previous 4 characters’ hex values matched specific hardcoded integers.
I worked through the calculations manually from the beginning.
Ultimately, I identified SECCON{5h4k3_1t_up_5h-5h-5h5hk3} as the correct flag.
Conclusion
I was pretty frustrated while analyzing the Jump binary since it wasn’t working properly and finding the flag was taking forever, but when I actually read it carefully the flag turned out to be easily obtainable through static analysis — so that was simply a matter of my own lack of skill. My apologies.
On the bright side, if the program’s validation had been working correctly, I might have been able to grab the flag with a mindless angr run from the start, so maybe it worked out for the best.
Every time I do SECCON I hit a wall after the third challenge, so I’d really like to get to a point where I can tackle harder problems soon.