POSTS
MRMCDCTF2019: Misguided
Solution to Misguided (easy - medium) from MRMCDCTF 2019
Misguided is another Linux binary reversing challenge I had written for MRMCDCTF 2019. This one employs some simple obfuscation: Some entries in the symbol table are mixed up.
When opening the file in Ghidra (like we have done with Cereal or Slicer before), at first, everything seems normal.
This changes once we take at look at main, which decompiles to this:
void main(void)
{
return;
}
Well, that’s a little less functionality than expected.
There are other interesting functions, like check_password
, initialise_flag
, and read_password
.
But none of them seem to check out.
check_password
:
/* WARNING: Removing unreachable block (ram,0x00101134) */
/* WARNING: Removing unreachable block (ram,0x00101140) */
void check_password(void)
{
return;
}
initialise_flag
:
/* WARNING: Removing unreachable block (ram,0x001010f3) */
/* WARNING: Removing unreachable block (ram,0x001010ff) */
void initialise_flag(void)
{
return;
}
read_password
:
void read_password(void)
{
__gmon_start__();
return;
}
encrypt_password
:
void encrypt_password(ulong uParm1,undefined8 uParm2,undefined8 uParm3)
{
long lVar1;
read_password();
lVar1 = 0;
do {
(*(code *)(&__frame_dummy_init_array_entry)[lVar1])(uParm1 & 0xffffffff,uParm2,uParm3);
lVar1 = lVar1 + 1;
} while (lVar1 != 1);
return;
}
At least the last one is doing something, but it does not look like encryption. But the rest?
What’s going on here?
Well, the function names do not match the function.
How can this be?
Well, how does the decompiler know the names of functions in the first place?
The decompiler gets the function names from the ELF symbol table.
This table is created by the compiler and contains, among many other things, the names and addresses of the various functions of the binary.
But this information is not needed by the Linux kernel to load and run the executable.
It can be removed from the binary without affecting its functionality (see man strip
).
But since it’s not used by the kernel in any way, it can also be filled with misinformation.
And this is was has happened here:
The names of compiler-generated functions (like deregister_tm_clones
) were swapped with programmer-written functions (like check_password
).
That’s why none of the seemingly interesting functions contained anything interesting, while many of the boring functions do:
just check out register_tm_clones
, deregister_tm_clones
, __libc_csu_init
, __libc_csu_fini
and _start
.
But how do you figure this out without knowing it beforehand?
This first way is to look for interesting strings (like we did with Slicer): The program prints the “What is the password?” and “Try again” messages, and the code that references these is definitely a good place to start looking around.
The password prompt gets referenced by register_tm_clones
:
s_The_password_is_the_flag._What_i_00102008 XREF[1]: register_tm_clones:001011ab(*)
00102008 ds "The password is the flag. What is the passwor
And both the “Wrong” and “Congratulations!” text gets referenced by deregister_tm_clones
:
s__###############################_00102040 XREF[1]: deregister_tm_clones:00101397(*)
00102040 ds "\n###########################################
s_Wrong._Try_again._001020cf XREF[1]: deregister_tm_clones:001013aa(*)
001020cf ds "Wrong. Try again."
With both functions it should be obvious that you are onto something. From there you may look which functions call theses, and what do they do, and whom do they call. The ‘Function Call Graph’ (Window->Function Call Graph) from Ghidra can be quite useful here.
Another way is to look for interesting imported (library) functions: You know from the start that the binary prints something (the messages) and reads something (the password). So when you don’t know what to do, take a look at the code where this happens.
You can find the imported functions in the Symbol Tree in the upper left corner, under functions - just like the normal functions of the binary.
With ELF files they appear two times; we are only interested in the first appearance: the indirect jump that references the global offset table.
References can be displayed in two ways:
you can rightclick the function in the Symbol Tree and select ‘Show References to’.
Alternatively, you can select the function there and look at the XREFs in the central (‘Listing’) subwindow.
Looking there at references to puts
gives you register_tm_clones
and deregister_tm_clones
again:
**************************************************************
* THUNK FUNCTION *
**************************************************************
thunk int puts(char * __s)
Thunked-Function: puts
int EAX:4 <RETURN>
char * RDI:8 __s
puts XREF[3]: register_tm_clones:001011b2(c),
deregister_tm_clones:0010139e(c),
deregister_tm_clones:001013b1(c)
00101030 JMP qword ptr [->puts] int puts(char * __s)
-- Flow Override: CALL_RETURN (COMPUTED_CALL_TERMINATOR)
And fgets
is used by register_tm_clones
as well:
**************************************************************
* THUNK FUNCTION *
**************************************************************
thunk char * fgets(char * __s, int __n, FILE * __stream)
Thunked-Function: fgets
char * RAX:8 <RETURN>
char * RDI:8 __s
int ESI:4 __n
FILE * RDX:8 __stream
fgets XREF[1]: register_tm_clones:001011ca(c)
00101070 JMP qword ptr [->fgets] char * fgets(char * __s, int __n
-- Flow Override: CALL_RETURN (COMPUTED_CALL_TERMINATOR)
When beginning with the imported functions and following the call graph, you end with the same ‘interesting’ functions as with the stings method.
With this, we can start looking at the code. The centerpiece seems to be _start
:
void _start(void)
{
undefined8 uVar1;
undefined8 uVar2;
uVar1 = register_tm_clones();
__libc_csu_fini(uVar1);
uVar2 = __libc_csu_init();
deregister_tm_clones(uVar1,uVar2,uVar2);
return;
}
It first calls register_tm_clones
(which we have already noted a few times), which prints the password prompt and returns the entered password:
char * register_tm_clones(void)
{
char *__s;
__s = (char *)malloc(0x80);
puts("The password is the flag. What is the password?");
fgets(__s,0x80,stdin);
strtok(__s,"\n");
return __s;
}
The password is then passed to __libc_csu_fini
, which xor’s every character with 0x41:
void __libc_csu_fini(long lParm1)
{
int local_c;
local_c = 0;
while (local_c < 0x80) {
*(byte *)(lParm1 + (long)local_c) = *(byte *)(lParm1 + (long)local_c) ^ 0x41;
local_c = local_c + 1;
}
return;
}
_start
continues to call __libc_csu_init
.
This function allocates a new buffer, fills it with constant data, and returns it to _start
.
undefined8 * __libc_csu_init(void)
{
long lVar1;
undefined8 *__s;
long in_FS_OFFSET;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
__s = (undefined8 *)malloc(0x80);
memset(__s,0,0x80);
*__s = 0x7150205020c130c;
__s[1] = 0x242d1e352f2e253a;
__s[2] = 0x33361e2429351e35;
__s[3] = 0x2d23202d1e262f2e;
__s[4] = 0x1e2d2e2e271e3224;
*(undefined4 *)(__s + 5) = 0x3c342e38;
*(undefined *)((long)__s + 0x2c) = 0x41;
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return __s;
}
Both the xor-encrypted password and the initialised buffer are at last passed to deregister_tm_clones
:
undefined8 deregister_tm_clones(void *pvParm1,void *pvParm2)
{
int iVar1;
undefined8 uVar2;
iVar1 = memcmp(pvParm1,pvParm2,0x2d);
if (iVar1 == 0) {
puts(
"\n##############################################\n### Congratulations! This is the flag! ###\n##############################################\n"
);
uVar2 = 0;
}
else {
puts("Wrong. Try again.");
uVar2 = 0xffffffff;
}
return uVar2;
}
deregister_tm_clones
does nothing more than call memcmp
on both its parameters and prints the ‘Congratulations!’ or ‘Wrong’ message if the match or don’t match.
So one parameter to memcmp
will be the entered password, xor’ed with 0x41.
And the other one will be the correct flag, xor’ed with 0x41.
So we need to grap the parameter to memcmp
and xor it with 0x41, and the result will be the flag.
One simple way to get the parameter is to run the program in a debugger and set a breakpoint on memcmp
.
user@host:$ gdb ./misguided
[...]
69 commands loaded for GDB 8.2.91.20190405-git using Python engine 3.7
[*] 4 commands could not be loaded, run `gef missing` to know why.
Reading symbols from ./misguided...
(No debugging symbols found in ./misguided)
gef➤ b memcmp
Haltepunkt 1 at 0x1060
gef➤ r
Starting program: misguided
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
The password is the flag. What is the password?
foo
Breakpoint 1, __memcmp_sse4_1 () at ../sysdeps/x86_64/multiarch/memcmp-sse4.S:50
50 ../sysdeps/x86_64/multiarch/memcmp-sse4.S: Datei oder Verzeichnis nicht gefunden.
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────── registers ────
$rax : 0x0000555555559260 → "'..AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$rbx : 0x0
$rcx : 0x0000555555559b10 → 0x07150205020c130c
$rdx : 0x2d
$rsp : 0x00007fffffffda08 → 0x0000555555555393 → <deregister_tm_clones+40> test eax, eax
$rbp : 0x00007fffffffda20 → 0x00007fffffffda40 → 0x0000555555555410 → <encrypt_password+0> push r15
$rsi : 0x0000555555559b10 → 0x07150205020c130c
$rdi : 0x0000555555559260 → "'..AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$rip : 0x00007ffff7d29ed0 → <__memcmp_sse4_1+0> pxor xmm0, xmm0
$r8 : 0x0000555555559010 → 0x0000000000000000
$r9 : 0x00007ffff7b76b80 → 0x00007ffff7b76b80 → [loop detected]
$r10 : 0x00007ffff7d83ca0 → 0x0000555555559b90 → 0x0000000000000000
$r11 : 0x00007ffff7d83ca0 → 0x0000555555559b90 → 0x0000000000000000
$r12 : 0x00005555555550b0 → <read_password+176> xor ebp, ebp
$r13 : 0x00007fffffffdb20 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
──────────────────────────────────────────────────────────────── stack ────
0x00007fffffffda08│+0x0000: 0x0000555555555393 → <deregister_tm_clones+40> test eax, eax ← $rsp
0x00007fffffffda10│+0x0008: 0x0000555555559b10 → 0x07150205020c130c
0x00007fffffffda18│+0x0010: 0x0000555555559260 → "'..AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007fffffffda20│+0x0018: 0x00007fffffffda40 → 0x0000555555555410 → <encrypt_password+0> push r15 ← $rbp
0x00007fffffffda28│+0x0020: 0x0000555555555400 → <_start+67> leave
0x00007fffffffda30│+0x0028: 0x0000555555559260 → "'..AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007fffffffda38│+0x0030: 0x0000555555559b10 → 0x07150205020c130c
0x00007fffffffda40│+0x0038: 0x0000555555555410 → <encrypt_password+0> push r15
────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7d29ec0 <__wcsnlen_avx2+832> ja 0x7ffff7d29e85 <__wcsnlen_avx2+773>
0x7ffff7d29ec2 nop WORD PTR cs:[rax+rax*1+0x0]
0x7ffff7d29ecc nop DWORD PTR [rax+0x0]
→ 0x7ffff7d29ed0 <__memcmp_sse4_1+0> pxor xmm0, xmm0
0x7ffff7d29ed4 <__memcmp_sse4_1+4> cmp rdx, 0x4f
0x7ffff7d29ed8 <__memcmp_sse4_1+8> ja 0x7ffff7d29f10 <__memcmp_sse4_1+64>
0x7ffff7d29eda <__memcmp_sse4_1+10> cmp rdx, 0x1
0x7ffff7d29ede <__memcmp_sse4_1+14> je 0x7ffff7d29f00 <__memcmp_sse4_1+48>
0x7ffff7d29ee0 <__memcmp_sse4_1+16> add rsi, rdx
──────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7d29ed0 → __memcmp_sse4_1()
[#1] 0x555555555393 → deregister_tm_clones()
[#2] 0x555555555400 → _start()
[#3] 0x7ffff7bc5b6b → __libc_start_main(main=0x5555555553bd <_start>, argc=0x1, argv=0x7fffffffdb28, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdb18)
[#4] 0x5555555550da → read_password()
───────────────────────────────────────────────────────────────────────────
Here we run the executable with gdb (with the gef script).
We set a breakpoint on memcmp
, launch the program, enter some password (it’s not important) and let the breakpoint trigger.
Under the System V AMD64 ABI the second parameter is placed in the RSI register. So RSI contains the address of the xor-encrypted flag, and we can tell gdb to display it.
gef➤ x/50bx $rsi
0x555555559b10: 0x0c 0x13 0x0c 0x02 0x05 0x02 0x15 0x07
0x555555559b18: 0x3a 0x25 0x2e 0x2f 0x35 0x1e 0x2d 0x24
0x555555559b20: 0x35 0x1e 0x35 0x29 0x24 0x1e 0x36 0x33
0x555555559b28: 0x2e 0x2f 0x26 0x1e 0x2d 0x20 0x23 0x2d
0x555555559b30: 0x24 0x32 0x1e 0x27 0x2e 0x2e 0x2d 0x1e
0x555555559b38: 0x38 0x2e 0x34 0x3c 0x41 0x00 0x00 0x00
0x555555559b40: 0x00 0x00
gef➤
Xor’ing every singe byte with 0x41 to get the plaintext flag is left as an exercise to the reader.