Flare-On 7 – Task 10

Stage 3

We know the flag is not complete… But where to go next? This is how the last part of the code being executed looks (the handle called on truncate ):

We can see the last thing executed is some callback function. I named it call_mem0, because the pointer was earlier initialized with 0. So it seemed to be again a call to MEMORY[0] – which is supposed to be handled inside the grandchild:

But it doesn’t seem correct… And indeed, after hooking strncmp I confirmed that the execution never reaches here. So, there is another overwrite somewhere… But where?

The thing that may raise some suspicion, is that a buffer that is processed in the previous part is 40000 bytes long… It turns out the buffer that is being filled here is to small to contain the data that is being copied into. As a result, the pointer (call_mem0) gets overwritten, and no longer holds the 0 value, but the pointer to the shellcode. So, the next stage is executed in an unusual way – by a buffer overflow.

To get the code that is going to be executed next, we need to dump the buffer that is passed into truncate function. I did it using again LD_PRELOAD and overwriting truncate with my own function (code available here) that just dumped the binary. Then I copied the binary string into a hexeditor.

Inside the buffer we can see the shellcode that is going to be loaded now:

We can extract this code and load it into IDA. And hooray! This time no nanomites or other obfuscation whatsoever. Just a simple code that we need to clean up a bit. At the end of the shellcode there are some strings, that are then referenced by their offsets. I filled them in the IDA view:

We can also see this shellcode will access the buffer that was filled by the original executable, by using its absolute address (yeah, the original binary is not going to be relocated). The strings are some long hexadecimal numbers. At first I thought they may be hashes, but after analyzing how they are used, it turned out that they are just long integers initialized by strings.

So, there are some operations performed on those big integers. I didn’t want to analyze those functions statically (as it was laborious), but I wanted to understand what operations do they represent. To find out, I decided to export them and call from my own wrappers. It can be done using various emulation frameworks, but since this is still x86 code and containing nothing Linux-specific, I decided to make a wrapper app on Windows. I loaded the shellcode in the executable memory, referenced functions by their offsets, and applied them on my own buffers. Calling convention of the functions was atypical, so I had to call them via wrappers with inline assembly. Example:

//_DWORD *__usercall sub_89F@<eax>(_DWORD *a1@<eax>, int a2@<edx>, int a3@<ecx>)
signed int op_set_value(void* a1, int a2, int a3)
    BYTE *func_offset = (BYTE *)(0x89F + (ULONG_PTR)g_Buf);
    __asm {
        mov eax, a1
        mov edx, a2
        mov ecx, a3
        call func_offset

It turned out to work pretty well for identification of the basic operations.

Yet, some parts were missing. I’ve got a hint to search for a big int library that was used in this code. It is the following library: https://github.com/kokke/tiny-bignum-c . Knowing this, the reconstruction of the full code turned out much easier. This is the final result that I’ve got:

// offset: 0x9c3
int __usercall sub_9C3@<eax>(bn *a1@<eax>, bn *a2@<edx>, bn *a3@<ecx>, bn *_arg_1)
bn *arg_1; // edi
bn *arg_2; // esi
int result; // eax
struct bn val_res; // [esp+0h] [ebp-284h]
struct bn val_2; // [esp+80h] [ebp-204h]
struct bn val_1; // [esp+100h] [ebp-184h]
struct bn val_0; // [esp+180h] [ebp-104h]
struct bn div_result; // [esp+200h] [ebp-84h]
bn *div_by; // [esp+280h] [ebp-4h]
arg_1 = a1;
arg_2 = a2;
div_by = a3;
bignum_from_int(_arg_1, 1, 0);
bignum_from_int(&val_2, 2, 0);
bignum_from_int(&val_1, 1, 0);
bignum_from_int(&val_0, 0, 0);
bignum_divmod(arg_2, &val_2, &div_result, &val_res);
if ( val_res.array[31] == val_1.array[31] )
bignum_assign(_arg_1, arg_1);
bignum_div((int)arg_2, (int)&val_2, val_res.array);
bignum_assign(arg_2, &val_res);
result = val_0.array[31];
if ( arg_2->array[31] == val_0.array[31] )
return result;
bignum_mul((char *)arg_1, (int)arg_1, &val_res);
bignum_divmod(&val_res, div_by, &div_result, arg_1);
while ( 1 )
bignum_divmod(arg_2, &val_2, &div_result, &val_res);
if ( val_res.array[31] != val_0.array[31] )
bignum_mul((char *)_arg_1, (int)arg_1, &val_res);
bignum_divmod(&val_res, div_by, &div_result, _arg_1);
bignum_div((int)arg_2, (int)&val_2, val_res.array);
bignum_assign(arg_2, &val_res);
result = val_0.array[31];
if ( arg_2->array[31] == val_0.array[31] )
bignum_mul((char *)arg_1, (int)arg_1, &val_res);
bignum_divmod(&val_res, div_by, &div_result, arg_1);
return result;
// offset: 0xdbe
void __cdecl __noreturn main_func(int a1, char *buffer, int buf_len)
int fd; // esi
char data; // [esp+0h] [ebp-A4Ch]
int flag_res; // [esp+18h] [ebp-A34h]
bn key_part1; // [esp+44h] [ebp-A08h]
bn key_part2; // [esp+C4h] [ebp-988h]
bn const_0; // [esp+144h] [ebp-908h]
bn const_1; // [esp+1C4h] [ebp-888h]
bn div_reminder; // [esp+244h] [ebp-808h]
bn const_3; // [esp+2C4h] [ebp-788h]
bn a2; // [esp+344h] [ebp-708h]
bn internal_buf; // [esp+3C4h] [ebp-688h]
bn out_buf0; // [esp+444h] [ebp-608h]
bn out_buf1; // [esp+4C4h] [ebp-588h]
bn r_buf; // [esp+544h] [ebp-508h]
bn div_res; // [esp+5C4h] [ebp-488h]
signed int a4; // [esp+644h] [ebp-408h]
int base; // [esp+A44h] [ebp-8h]
pid_t pid; // [esp+A48h] [ebp-4h]
base = 0;
pid = MEMORY[0x81A5280];
to_sys_ptrace(0, MEMORY[0x81A5280], 12, &data);
if ( buf_len != 32 )
flag_res = –1; // failed
to_sys_ptrace(0, pid, 13, &data);
to_sys_ptrace(0, pid, 17, 0);
convert_from_hex(&const_0, base + 0x12A6, 64);// "d1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f"
convert_from_hex(&const_1, base + 0x1224, 64);// "c10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31"
convert_from_hex(&key_part1, base + 0x11E3, 64);// "480022d87d1823880d9e4ef56090b54001d343720dd77cbc5bc5692be948236c"
convert_from_hex(&const_3, base + 0x11E3, 64);// "480022d87d1823880d9e4ef56090b54001d343720dd77cbc5bc5692be948236c"
convert_from_hex(&key_part2, base + 0x1265, 64);// "d036c5d4e7eda23afceffbad4e087a48762840ebb18e3d51e4146f48c04697eb"
qmemcpy(&internal_buf, buffer + 0x30, 24u); // get next part of the password
fd = to_sys_open(0, 0, (char *)(base + 0x11D6));// "/dev/urandom"
to_sys_read(32u, &r_buf, fd); // r_buf = random bytes
bignum_divmod(&r_buf, &const_0, &div_res, &div_reminder);
bignum_assign(&r_buf, &div_reminder);
sub_9C3(&const_1, &r_buf, &const_0, &a2);
bignum_assign(&r_buf, &div_reminder);
sub_9C3(&const_3, &r_buf, &const_0, &out_buf0);
bignum_mul((char *)&internal_buf, (int)&a2, &r_buf); // a2 = "c10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31"
bignum_divmod(&r_buf, &const_0, &div_res, &out_buf1);
//print in hex:
memset(&a4, 0, 0x400u);
bigint_to_str(&out_buf0, (int)&a4, 0x400);
memset(&a4, 0, 0x400u);
bigint_to_str(&out_buf1, (int)&a4, 0x400);
if ( bignum_cmp(&key_part1, &out_buf0) || bignum_cmp(&key_part2, &out_buf1) )// flag verification
flag_res = –1; // failed
to_sys_ptrace(0, pid, 13, &data); // PTRACE_SETREGS
to_sys_ptrace(0, pid, 17, 0);
buffer[72] = 0;
write_to_remote_buf(pid, MEMORY[0x81A57C0], (void **)buffer, (signed int)&base, 73);
flag_res = 32; // the flag is ok
to_sys_ptrace(0, pid, 13, &data); // PTRACE_SETREGS
to_sys_ptrace(0, pid, 17, 0);
view raw snippet.cpp hosted with ❤ by GitHub

At this point we can see that the next part of the flag is verified by being input to and equation. The result of this equation is then compared with the hardcoded big number.

One of the numbers seems to be dynamically generated – calculated from a random buffer. But after loading the shellcode to my wrapper and making some experiments with various random inputs, I found out that the result is unaffected by the input: “c10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31”, so we can use this number just as a constant.

The full equation is:

(input * "c10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31")
% "d1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f"        
= "d036c5d4e7eda23afceffbad4e087a48762840ebb18e3d51e4146f48c04697eb"

We are dealing here with a simple asymmetric cipher, related to modular arithmetic on co-prime numbers.

(inp * a) % b = c

The formula to reverse this equation is:

inp = inverse(a, b) * c % b

For the required calculations I used the following online toolkit: https://www.boxentriq.com/code-breaking

  1. Finding the inverse:
inverse(a, b) =
d1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f) =

2. Calculating the solution:

inp = inverse(a, b) * c % b = 7d6990db0059850d8e02937be5e2ac7b9dfe6411de316c1e462762c24d647b5c * 
d036c5d4e7eda23afceffbad4e087a48762840ebb18e3d51e4146f48c04697eb % 
d1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f =

Now we need to convert it into ASCII (I did it by just pasting to a HxD hexeditor):

The string just needs to be reversed, and we’ve got the final part of the flag!


If you finished reading this post, please share your feedback leaving a comment below!

About hasherezade

Programmer and researcher, interested in InfoSec.
This entry was posted in CrackMe and tagged , . Bookmark the permalink.

1 Response to Flare-On 7 – Task 10

  1. 0xstan says:

    Great write-up !

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s