This is an obfuscated flag checker that validates our input and return whether it is correct or not. At a higher level, the checking logic is a combination of decision statement (if-statement) for each bit of the flag body.
The function checks the “R3CTF{…}” flag format, extracts the 32-byte body inside the curly braces, and passes it into sub_214326
So sub_214326 is the main validation process, however it is insanely obfuscated something like this
By looking at the decompilation, we can figure out that the binary virtualize the initial program by using multiple indirect calls. From that hides the CFG, the execution flow and the main process.
Alright after searching several internal strings, I find two functions used for printing Correct and Fail which is sub_21EB44 and sub_21EB6D respectively. So the key is these two functions
At this point, there is two possible targets. Firstly we can try to deobfuscate the binary, recover the initial program and write a solve script for that. Secondly, we can directly solve the challenge by witnessing how input is parsing and transfering during the execution phase. I chose the latter method because I don’t know whether the former one would produces any undefined behaviour or not, like too many patterns or some kinds..
To begin with, there is a hypothesis that these obfuscation layers hide the main checking logic toward our input, so we need to find the exact pivot where the program access our flag and used it to control something or to calculate something we don’t know.
The fact that if our flag is specified, the program execution flow would be absolutely linear which means we could use angr to detect the splitting branch. From that we could clarify where is the first code stub uses our input
#!/usr/bin/env python3importangrimportclaripyproj=angr.Project("./chall",auto_load_libs=False)base_addr=proj.loader.main_object.mapped_baseprint(f"Image base {base_addr:#x}")state:angr.SimState=proj.factory.blank_state(addr=base_addr+0x214326,add_options={angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,})# fake statestate.regs.rbp=0x7fffffffdc70state.regs.rsp=0x7fffffffda28# argument setupbuffer_addr=0x7fffffffda30state.regs.rdi=0state.regs.rsi=buffer_addrbuf=[claripy.BVS(f"inp{i}",8,explicit_name=True)foriinrange(32)]fori,binenumerate(buf):state.memory.store(buffer_addr+i,b)# flag = "aaaabaaacaaadaaaeaaafaaagaaahaaa"# for i, b in enumerate(flag):# state.solver.add(buf[i] == ord(b))foriinrange(1):state.solver.add(claripy.Extract((i%8),(i%8),buf[i//8])==(i&1))# first i need to see somethingfail=0x21EB87success=0x21EB54ins_counter=0# while len(s.active) == 1:# s.step()# ins_counter += 1s=proj.factory.simgr(state)counter=0defcallback(sm):globalcountercounter+=1ifcounter%200==0:print(f"Ran {counter}")returnlen(sm.active)!=1s.run(until=callback)print(f"Ran {ins_counter} instructions, Splitted to ")forstateins.active:print(hex(state.addr-base_addr),end=" ")print()one=s.active[0]fori,frameinenumerate(one.callstack):print(f"Frame #{i}: {frame.func_addr-base_addr:#x}, called by: {frame.call_site_addr-base_addr:#x}")expr=claripy.simplify(one.regs.rax)print(expr)
It produces something like
0x21ea85 0x21ea7c
Frame #0: 0x21ea41, called by: 0xf81b8
Frame #1: 0xf81b8, called by: 0xf860e
Frame #2: 0xf8324, called by: 0xf92d8
Frame #3: 0xf890e, called by: 0x21c74a
Frame #4: -0x400000, called by: -0x400000
<BV64 0x0 .. inp0[1:1]>
The first bit of the first input byte is not be transformed (it is still inp0[1:1]). So it might be mathematically transformed into an intermediate buffer or it might just be used to compare at this address (and this will strengthen my following analyzing stuff). But we would’t care it, the key of this log is a function at 0x21ea41.
I tried to set a concrete value for the first few bits, and they always stop at this function. Through my script, the second argument is a current input bit, and the first argument is some heap address might be come from calloc somewhere else. Let’s watch the caller stub
The function shifts the current input byte value by one before consuming the LSB. This strongly suggests that the program is trying to extract each bit of each flag byte. In summary, those aforemention stubs are preparing 256 structs (corresponding with 256 bits). If a current bit is odd sub_21E99D is selected, else sub_21E9FE.
These functions generate a callback function. After doing a few research on google and ChatGPT, I have known this is some kind of closure function where local variables are memorized to be used outside the function scope. An example on the internet is javascript, because C does not have this feature
For example
The function memorizes the count variable. In order to simulate this is in C, we can create a struct like this
1
2
3
4
structClosure{uint64_tcallbacks;// our function
void*env;// used to register local variable
};
Then we can use it like this
1
2
3
4
5
6
7
8
9
10
uint64_tcreating_closure(){structClosure*closure=calloc(1,sizeof(structClosure));closure->env=calloc(1,8);// this is used to store count
*(uint64_t*)closure->env=2007;// we act like this is a local variable
returnclosure;}structClosure*cls=creating_closure();printf("%lld\n",*(uint64_t*)cls->env);// it prints 2007
So it could be the main obfuscation type of this challenge. Go back to sub_21E99D and sub_21E9FE. These functions are used to generate a closure function
So both of them always initialize a closure with pre-setup argument, then trigger that function with another argument. This is a mess while solving because it will take an expensive effort just to find those arguments.
The former returns the captured argument while the latter returns a new passed argument. Let’s call it first argument and second argument generally
So it could be something like this
If bit_i == 1 -> create a func(A, B) -> return A - let's call a True-like function
If bit_i == 0 -> create a func(A, B) -> return B - let's call a False-like function
After doing a few research and of course asking GPT. I have known that this is equivalent to Boolean-encoding Church (you can read more here https://en.wikipedia.org/wiki/Church_encoding).
In short, each bit looks like the condition for the selector: true chooses the first argument, and false chooses the second argument.
It is just a random example it is not the program internal logic.
So the Boolean expression is used to virtualize this logic and hide the real if-statement structure, the order and many other things. In order to solve this challenge, we have to lift these whole virtualization layers into a visualized structure first.
Back to the challenge, after the preparation process, I still don’t know what to do with these collect data. So I decided to trace the program instructions using unicorn to try to find some crucial hints
#!/usr/bin/env python3fromunicornimport*fromunicorn.x86_constimport*fromcapstoneimport*fromcapstone.x86_constimport*fromstructimport*uc=Uc(UC_ARCH_X86,UC_MODE_64)withopen("./chall","rb")asf:blob=f.read()# disassembly cachecache={}md=Cs(CS_ARCH_X86,CS_MODE_64)forinsinmd.disasm(blob[0x1100:0x21ED4D],0x1100):cache[ins.address]=f"{ins.mnemonic}{ins.op_str}"puts_addr=0x1050calloc_addr=0x10a0text_addr=0x1100map_start=0x1000map_size=0x21f000uc.mem_map(map_start,map_size)uc.mem_write(text_addr,blob[0x1100:0x21ED4D])# Initialize stackstack_size=0x100000000stack_base=0x7ffff0000000uc.mem_map(stack_base,stack_size)uc.reg_write(UC_X86_REG_RSP,0x7fffffffda28)uc.reg_write(UC_X86_REG_RBP,0x7fffffffdc70)buffer_addr=0x7fffffffda30uc.reg_write(UC_X86_REG_RDI,0)uc.reg_write(UC_X86_REG_RSI,buffer_addr)# Fill flagflag=b'aaaabaaacaaadaaaeaaafaaagaaahaaa'uc.mem_write(0x7fffffffda30,b'aaaabaaacaaadaaaeaaafaaagaaahaaa')bit=[]forcinflag:x=cfor_inrange(8):bit.append(x&1)x>>=1defread_mem(uc:Uc,addr,sz=8):returnint.from_bytes(uc.mem_read(addr,sz),'little')# Initialize heaphp_size=0x5000000hp_addr=0x90000000uc.mem_map(hp_addr,hp_size)# Simulate Libc's callocheap_top=hp_addrdeflibc_calloc(uc,addr,size,user_data):code=uc.mem_read(addr,size)if(size!=5)or(code[0]!=232):returncall_addr=(addr+unpack("<i",code[1:5])[0]+5)&0xffffffffifcall_addr!=0x10a0:returnglobalheap_top,heap_size,hp_addr,hp_sizearg0=uc.reg_read(UC_X86_REG_RDI)arg1=uc.reg_read(UC_X86_REG_RSI)heap_size=arg0*arg1ifheap_size+heap_top>hp_addr+hp_size:print("Too much calloc!")exit(0)uc.mem_write(heap_top,b'\x00'*heap_size)uc.reg_write(UC_X86_REG_RAX,heap_top)heap_top+=heap_sizeuc.reg_write(UC_X86_REG_RIP,addr+5)returnuc.hook_add(UC_HOOK_CODE,callback=libc_calloc)defputs_libc(uc,addr,size,user_data):code=uc.mem_read(addr,size)if(size!=5)or(code[0]!=232):returncall_addr=(addr+unpack("<i",code[1:5])[0]+5)&0xffffffffifcall_addr!=0x1050:returnprint("Calling puts...")uc.reg_write(UC_X86_REG_RIP,addr+5)passuc.hook_add(UC_HOOK_CODE,callback=puts_libc)counter=0stream=open("trace.log","a")deftracer(uc,addr,size,user_data):globalstreamstream.write(f"{addr:#x}{cache.get(addr,'unknown')}\n")uc.hook_add(UC_HOOK_CODE,callback=tracer)definvalid_mem(uc,access,address,size,value,user_data):rip=uc.reg_read(UC_X86_REG_RIP)print(f"Invalid memory access {address:#x} at {rip}")returnFalseuc.hook_add(UC_HOOK_MEM_INVALID,callback=invalid_mem)uc.emu_start(begin=0x214326,until=0x21E986)
However things goes evil after this decision. It ruined my entire workspace because of huge quantity of assembly instructions (over 500M running in about 30 minutes, could be more but I stopped tracing).
Manually inspecting thousand of millions of instruction is not a clever option, although there might be a pattern and I can use script to extract pattern, clean log and lift to readable structure. It is still too risky because how I am supposed to know that I have covered enough cases? Additionally, it can be inconvenient because I could not examine the whole logs as well as running the script also a waste of time.
Moreover, there is actually two type. The first type is triggering a callback with a memorized variable and new argument.The second one is running a normal functions. A signal for the normal function is the high bit set. We can see that whenever the program calls a normal function it will unset the high bit and call it. For example (func & 0x7FFFFFFFFFFFFFFFLL)(0, something)
That is all the challenge does, by arranging this type of indirect calls into a multiple layers it makes static analysis much harder, at least my skill issue prevented me :sob:
To be honest, in order to deobfuscate this challenge we have to intensively examine the assembly instructions, witness pattern, group instruction, lift them into a readable IR. I rarely do any challenges similar to this. I had done some obfuscation challenges with different vibes like VM, Packer, Shellcode, JIT, etc. I don’t mean it is easier or harder, it is just purely different or just my feelings?
Enough talkings, let’s go back to the challenge. As I mentioned above, I didn’t use unicorn in the final solution. But I did try it to deobfuscate the binary a little bit before giving up… So I wanna take a short discussion about that here also
First of all, the script is based on what I send above you can take a look there. So in order to witness how program processes with our Boolean expressions of the input, we need to save the returned heap address from 0x21ea41 and save it. We can do that by simply using HOOK in unicorn hehe
After the 256 bit objects is created, the program is no longer needs to read our original inputs anymore. Instead, it uses the heap-allocated address from our input. That is the reason why I store the heap address. To continue with, how could we able to watch where our expressions is used? Well by looking at assembly level we always see this pattern of closure function
The program load the closure into stack address then loading the first 8 byte to retrieve the callback function and call them. In unicorn, we can use UC_HOOK_MEMORY_READ to detect mov rcx, [rax] easily. After that we just need to check whether the loaded memory is one of our saved value in rev_bit_map or not
1
2
3
4
5
6
defhook_load(uc:Uc,access,addr,size,value,ud):ifaddrnotinrev_bit_map:returnrip=uc.reg_read(UC_X86_REG_RIP)print(f"Loading function at {addr:#x} bit_{rev_bit_map[addr]} = {bit[rev_bit_map[addr]]} at {rip:#x}")passuc.hook_add(UC_HOOK_MEM_READ,callback=hook_load)
The output will look like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Loadingfunctionat0x9001c5d8bit_135=0at0x36174Loadingfunctionat0x9001c548bit_134=1at0x36174Loadingfunctionat0x9001c4c8bit_133=1at0x36174Loadingfunctionat0x9001c458bit_132=0at0x36174Loadingfunctionat0x9001c3f8bit_131=0at0x36174Loadingfunctionat0x9001c3a8bit_130=1at0x36174Loadingfunctionat0x9001c368bit_129=0at0x36174Loadingfunctionat0x9001c338bit_128=1at0x36174Loadingfunctionat0x9001de20bit_143=0at0x2d5b7Loadingfunctionat0x9001dd90bit_142=1at0x2d5b7Loadingfunctionat0x9001dd10bit_141=1at0x2d5b7Loadingfunctionat0x9001dca0bit_140=0at0x2d5b7Loadingfunctionat0x9001dc40bit_139=0at0x2d5b7// truncated 99,999999% of the content lol
Alright after running for around 5 mins, only two address is recorded which is 0x36174 and 0x2d5b7. Oh nice sound promising only 2 address hehehe…
So the address 0x36174 is located in this function
__int64__fastcallsub_3603A(__int64a1,__int64a2){__int64v4;// [rsp+28h] [rbp-18h]
__int64v5;// [rsp+30h] [rbp-10h]
__int64v6;// [rsp+38h] [rbp-8h]
if(a2>=0)v6=(*a2)(a2,sub_35FB3+0x8000000000000000LL);elsev6=((a2&0x7FFFFFFFFFFFFFFFLL))(0,sub_35FB3+0x8000000000000000LL);if(v6>=0)v5=(*v6)(v6,sub_35FEB+0x8000000000000000LL);elsev5=((v6&0x7FFFFFFFFFFFFFFFLL))(0,sub_35FEB+0x8000000000000000LL);if(*(a1+8)>=0)v4=(**(a1+8))(*(a1+8),v5);// <<<<<------ THIS IS WHERE 0x36174 SHOW US
elsev4=((*(a1+8)&0x7FFFFFFFFFFFFFFFLL))(0,v5);if(v4>=0)return(*v4)(v4,a2);elsereturn((v4&0x7FFFFFFFFFFFFFFFLL))(0,a2);}
So the pattern is pretty clear now, v4 is used to creating closure and initialize first argument which is v5. And then the program triggers the closure immediately with second argument a2
So I need a script to automatically trace these two argument, so we can use UC_HOOK_CODE to watch the value. It is equivalently for 0x2d5b7 but it is harder a little bit to see but you would able to figure out it, not at that hard
EVERYTHING… uh sorry for caplocking. Everything seems to be normal JUST UNTIL NOW.
For 0x36174, the examined logs show that its argument is usually an internal function (like 0x35feb) in the binary related to True/False Boolean expression. However for 0x2DBA7, the arguments are heap-allocated address/object instead.
This is where unicorn feels impotent. If an argument is not a binary hardcode function/address but heap address, it would take more efforts to understanding the context. We must look at where it is called from, what is the related argument around that. For example it is a closure function? It is statically like everytime the binary runs with different input, it stores the same value etc
Moreover, there is a critical point, unicorn can not handle dynamic object. If we receive an address or a pointer, we don’t know whether it is program related object or it is the selected object from one of our input Boolean expression. We can not mask an ID to an object, structure or a heap-allocated address that it is input dependent or program independent object. So tough right? So I gave up using unicorn here
The only solution I think about is building a handmade unicorn (written in C for speed) to handle those case by myself, this is seemed to be the most intuitive approach but would be painful like finishing minecraft hardcore world with half heart, full of cursed of binding of unbreakable leather armor and infinite blind effect.
Before reading, sorry if my explanation is confused, because I’m trying to describe about what I’m thinking in my brain while solving instead of providing a comprehensive writeup for the challenge
Alright back from the challenge, this method is inspired by one of malware technique that I have learnt it is DLL injection. So basically the function will call our program function instead of the binary one leading to over control the execution.
So I decided to create a dummy Object that is the alternative for binary Boolean expression. So that we can easily trace how my object travel around the binary, how my object is used and is combined with others stuff. It also easier for me to distinguish from program internal object
#define _GNU_SOURCE
#include<stdio.h>#include<dlfcn.h>#include<stdint.h>#include<stdbool.h>#include<unistd.h>#include<link.h>#include<sys/mman.h>#include<string.h>#define logging(fmt, ...) fprintf(stream, fmt, ##__VA_ARGS__)
#define ull uint64_t
#define HIGH 0x8000000000000000ULL
#define MASK 0x7fffffffffffffffULL
unsignedlonglongcounter=0;staticvoid*(*real_calloc)(size_ta,size_tb)=NULL;staticvoid*(*real_exit)(intcode)=NULL;staticullfn_func_symbolic;staticullfn_partial_symbolic;staticullfn_choice_object;staticullfn_run_intermediate;staticullfn_run_intermediate_yes;staticullfn_run_intermediate_no;staticullfn_decoy=0;staticulldecoy(){printf("Why can it access this?");}staticullbase_address=0;FILE*stream=NULL;void*calloc(size_tx,size_ty){if(real_calloc==NULL)real_calloc=dlsym(RTLD_NEXT,"calloc");returnreal_calloc(x,y);}ullexpr_cnt=0;/*
func is the callback function
type 0 = bit constructor
type 1 = yes argument
type 2 = no argument and decide
*/typedefstruct{ullfunc;intbit_id;ullexpr;ullyes,no;}Object;staticboolisHigh(ullx){return(x&HIGH)!=0;}staticboolisTarget(ulladdr){if(!addr)returnfalse;addr&=MASK;return((addr-base_address)==0x21EB44)||((addr-base_address)==0x21EB6D);}staticboolis_in_text(ulladdr){addr&=MASK;returnbase_address<=addr&&addr<=base_address+0x21ED4D;}staticullconvertAddr(ulladdr){addr&=MASK;returnis_in_text(addr)?addr-base_address:addr;}staticboolis_object(ulladdress){if(isHigh(address)||!address)return0;ullfunc=*(ull*)address;if(func==fn_func_symbolic||func==fn_partial_symbolic||func==fn_choice_object||func==fn_run_intermediate||func==fn_run_intermediate_yes||func==fn_run_intermediate_no||func==decoy){return1;}return0;}staticObject*create_object(ullfunction){Object*x=calloc(1,sizeof(Object));x->func=function;returnx;}staticintfunc_type(ulladdress){address&=MASK;uint8_t*array=(uint8_t*)address;if(array[0]!=0x55||array[1]!=0x48||array[2]!=0x89||array[3]!=0xE5){return-1;}intcounter=1;for(;((array[counter-1]!=0xC3)||(array[counter-1]==0xC3&&(array[counter-2]!=0x5D&&array[counter-2]!=0xC9)))&&counter<=101;counter++);if(counter==79){return1;// true-like
}if((counter==34)||(counter==67)){return0;// false-like
}return-1;}staticintfunc_type_log(ulladdress){address&=MASK;intcounter=1;printf("%#llx\n",address,address-base_address);for(;(*(uint8_t*)address!=0xC3)&&counter<=100;address++,counter++){printf("%x ",*(uint8_t*)address);}printf("%d \n",counter);if(counter==79){return1;// true-like
}if((counter==34)||(counter==67)){return0;// false-like
}return-1;}staticObject*choice_object(Object*self){ullyes=self->yes;ullno=self->no;{boola=isTarget(yes);boolb=isTarget(no);if((a&&!b)||(!a&&b)){printf("Not good");_exit(123);}if(a&&b){// This is Correct/Fail function
// if (convertAddr(yes) != 0x21EB44)
logging("expr_%d = Choice(expr_%d, %#llx, %#llx)\n",expr_cnt+1,self->expr,convertAddr(yes),convertAddr(no));_exit(0);}}{// Here wee need to treat other functions as a boolean type
// So it could be either TRUE/FALSE or Symbolic
// Is it easier to treat True/False as Symbolic ?
// To be easier lets assume there is no weird function apart from this type
// So yes_func and no_func is Boolean
// if yes_func or no_func is Object it would be different
boola=is_object(yes);boolb=is_object(no);if(a&&!b){Object*cur=create_object(fn_func_symbolic);intbtype=func_type(no);// logging("%d\n", btype);
if(btype==-1){// so here btype = -1 which means it could be non-boolean function we need to handler this case
// logging("expr_%d %#llx %#llx\n", self->expr, yes, convertAddr(*(ull *)no));
// we can still recursive until no is some case in this choice_object function
// logging("%#llx\n", convertAddr(*(ull*)no));
// cur->func = fn_run_intermediate_no;
// cur->expr = self->expr;
// cur->no = self->no;
// cur->yes = self->yes;
// return cur;
}else{Object*x=(Object*)yes;cur->expr=expr_cnt++;// logging("[Object-Like] expr_%lld = Choice(expr_%lld, expr_%lld, %s)\n",
// cur->expr, self->expr, x->expr, (btype ? "true" : "false")
// );
logging("expr_%lld = Choice(expr_%lld, expr_%lld, %s)\n",cur->expr,self->expr,x->expr,(btype?"true":"false"));returncur;}}if(!a&&b){Object*cur=create_object(fn_func_symbolic);intatype=func_type(yes);if(atype==-1){// cur->func = fn_run_intermediate_yes;
// cur->expr = self->expr;
// cur->yes = self->yes;
// cur->no = self->no;
// return cur;
}else{Object*y=(Object*)no;cur->expr=expr_cnt++;logging("expr_%lld = Choice(expr_%lld, %s, expr_%lld)\n",cur->expr,self->expr,(atype?"true":"false"),y->expr);returncur;}}// if ((a && !b) || (!a && b)) {
// printf("Weird here\n");
// printf("%#llx %#llx", convertAddr(yes), convertAddr(no));
// _exit(0);
// }
if(a&&b){Object*cur=create_object(fn_func_symbolic);// it is Choice(self->expr, yes->expr, no->expr)
Object*x=(Object*)yes;Object*y=(Object*)no;cur->expr=expr_cnt++;// logging("[Object-Like] expr_%lld = Choice(expr_%lld, expr_%lld, expr_%lld)\n",
// cur->expr, self->expr, x->expr, y->expr
// );
logging("expr_%lld = Choice(expr_%lld, expr_%lld, expr_%lld)\n",cur->expr,self->expr,x->expr,y->expr);returncur;}// now A/B is either TRUE/FALSE (because we assumed there is no non-closure function)
//
{Object*cur=create_object(fn_func_symbolic);cur->expr=expr_cnt++;intatype=func_type(yes);intbtype=func_type(no);// logging("expr_%d %d %d\n", self->expr, atype, btype);
// printf("%d %d %#llx %#llx\n", atype, btype, convertAddr(yes), convertAddr(no));
if((atype!=-1)&&(btype!=-1)){// logging(
// "[Func-Like] expr_%lld = Choice(expr_%lld, %s, %s)\n", cur->expr,
// self->expr, (atype ? "true" : "false"), (btype ? "true" : "false")
// );
logging("expr_%lld = Choice(expr_%lld, %s, %s)\n",cur->expr,self->expr,(atype?"true":"false"),(btype?"true":"false"));returncur;}}// time for undefined function for example 0x3706A
// fprintf(stream, "%#llx %#llx\n", convertAddr(*(ull*)(yes & MASK)), convertAddr(*(ull*)(no & MASK)));
// the pattern is likely
/*
Yes/No here non-boolean function. Lets have "yes" as an example
It will call a function for example selector(yes, false/true-like)
Then using true/false-like function to select whether yes[0] or yes[1]
Then yes[0] or yes[1] is again a selector until there is a boolean-expression appear
The solution is pretty cheap here, because selector(yes, no) receive the same true/false-like argument
So we just need to identity whether it is true-like or false-like function
So run_intermediate is a key here
it clarify the type of filter function
then map new argument for the boolean expression
for example
Expression(yes, no, false) -> Expression(yes[1], no[1], filter)...
*/{// invalid case fallback here includes half object
/*
pattern here
Choice(express, func_a, func_b)
func_a[0] = selector
func_a[1] = first
func_a[2] = second
filter = choose first or second
func_a[0](func_a, filter) -> func_a[1]/func_a[2]
// so we need to keep recursive util func_a/func_b is not a selector anymore
// How could we do that? Idk either
*/// logging("expr_%d yes=%#llx no=%#llx\n", self->expr, convertAddr(self->yes), convertAddr(self->no));
Object*cur=create_object(fn_run_intermediate);cur->expr=self->expr;// we will not creating a new expression for this we will reuse
cur->yes=self->yes;cur->no=self->no;// logging("yes=%#llx no=%#llx yes[1] = %#llx no[1] = %#llx\n",
// cur->yes, cur->no, *(ull*)(cur->yes + 8), *(ull*)(cur->no + 8)
// );
/*
yes = *(ull*)(yes + 16);
logging("yes[1][0] %#llx, yes[1][1] = %#llx yes[1][2] = %#llx, object %d %d\n", convertAddr(*(ull*)yes), *(ull*)(yes+8), *(ull*)(yes + 16), is_object(*(ull*)(yes+8)), is_object(*(ull*)(yes + 16)));
yes = *(ull*)(yes + 8);
logging("yes[1][0] %#llx, yes[1][1] = %#llx yes[1][2] = %#llx, object %d %d\n", convertAddr(*(ull*)yes), *(ull*)(yes+8), *(ull*)(yes + 16), is_object(*(ull*)(yes+8)), is_object(*(ull*)(yes + 16)));
no = *(ull*)(no + 8);
logging("no[1][0] %#llx, no[1][1] = %#llx no[1][2] = %#llx, object %d %d\n", convertAddr(*(ull*)no), *(ull*)(no+8), *(ull*)(no + 16), is_object(*(ull*)(no+8)), is_object(*(ull*)(no + 16)));
no = *(ull*)(no + 8);
logging("no[1][0] %#llx, no[1][1] = %#llx no[1][2] = %#llx, object %d %d\n", convertAddr(*(ull*)no), *(ull*)(no+8), *(ull*)(no + 16), is_object(*(ull*)(no+8)), is_object(*(ull*)(no + 16)));
*/// printf("mercy"); exit(0);
returncur;}}printf("An error occurred: %#llx %#llx\n",convertAddr(yes),convertAddr(no));_exit(-1);}staticObject*run_intermediate(Object*self,ullfilter){// can we just run this?
if(!isHigh(filter)){logging("Latter expr_%d",self->expr);_exit(0);}boolvalid_a=is_object(self->yes)||(func_type(self->yes)!=-1);boolvalid_b=is_object(self->no)||(func_type(self->no)!=-1);if((valid_a&&!valid_b)||(!valid_a&&valid_b)){logging("We will see about this");_exit(0);}if(valid_a&&valid_b){Object*cur=create_object(fn_decoy);cur->yes=self->yes;cur->no=self->no;cur->expr=self->expr;returnchoice_object(cur);}booltype=func_type(filter);if(type==0)// false-like
{Object*cur=create_object(fn_decoy);cur->yes=*(ull*)(self->yes+16);cur->no=*(ull*)(self->no+16);cur->expr=self->expr;returnchoice_object(cur);}else// true-like
{Object*cur=create_object(fn_decoy);cur->yes=*(ull*)(self->yes+8);cur->no=*(ull*)(self->no+8);cur->expr=self->expr;returnchoice_object(cur);}logging("Uh oh\n");_exit(0);}staticObject*func_partial_symbolic(Object*self,ullno){// fprintf(stream, "[Bit] bit_%d expr_%lld Second argument %#llx\n", self->bit_id, self->expr, convertAddr(no));
self->no=no;returnchoice_object(self);}intbit_counter=0;staticObject*func_symbolic(Object*self,ullyes){Object*cur=create_object(fn_partial_symbolic);cur->yes=yes;cur->expr=self->expr;cur->bit_id=self->bit_id;// fprintf(stream, "[Bit] bit_%d expr_%lld First argument %#llx\n", cur->bit_id, cur->expr, convertAddr(yes));
returncur;}ullgenerate_bit(ullx,ullarg){Object*cur=create_object(fn_func_symbolic);cur->expr=expr_cnt++;cur->bit_id=bit_counter++;logging("expr_%d = bit_%d\n",counter++,cur->expr);// fprintf(stream,"[Bit] bit_%d concrete=%d\n", cur->bit_id, (int)(arg & 1));
return(ull)cur;}intfind_base_address(structdl_phdr_info*info,size_tsz,void*base){if(info->dlpi_name==NULL||info->dlpi_name[0]=='\0'){ullbase_addr=(ull)info->dlpi_addr;*(ull*)base=base_addr;return1;}return0;}voidmap_memory(ullstart,ullend){intpage_size=sysconf(_SC_PAGESIZE);ullmask=~(page_size-1);start=start&mask;end=(end+page_size-1)&mask;size_tlength=end-start;if(mprotect((void*)start,length,PROT_EXEC|PROT_READ|PROT_WRITE)!=0){perror("mprotect");_exit(123);}return;}boolhook_function(ulladdress){/*
the hook idea is pretty simple
mov rax, address
jmp rax
*/ulladdr=address&~0xfffULL;charshellcode[12]={0x48,0xb8};*(ull*)&shellcode[2]=(ull)generate_bit;shellcode[10]=0xff;shellcode[11]=0xe0;if(mprotect((void*)addr,0x1000,PROT_EXEC|PROT_READ|PROT_WRITE)!=0){perror("mprotect");_exit(123);}memcpy((void*)address,(void*)shellcode,sizeof(shellcode));__builtin___clear_cache((char*)address,(char*)address+sizeof(shellcode));returntrue;}__attribute__((constructor))staticvoidinit_array(void){if(real_calloc==NULL)real_calloc=dlsym(RTLD_NEXT,"calloc");dl_iterate_phdr(find_base_address,&base_address);remove("formula.txt");stream=fopen("formula.txt","a");if(stream==NULL){perror("stream null");}setvbuf(stream,NULL,_IONBF,0);setvbuf(stdout,NULL,_IONBF,0);fn_func_symbolic=(ull)func_symbolic;fn_partial_symbolic=(ull)func_partial_symbolic;fn_choice_object=(ull)choice_object;fn_run_intermediate=(ull)run_intermediate;fn_decoy=(ull)decoy;printf("Main address = %#llx\n",base_address);if(hook_function(base_address+0x21EA41)){printf("Hooked successfully\n");}map_memory(base_address+0x1100,base_address+0x21ED4D);return;}__attribute__((destructor))staticvoidfinish_array(void){printf("\nBye\n");return;}
Misunderstanding is my best friend while writing this script but unfortunately he left me when I solved this challenge. Why he so means T_T
Because It is so long I will explain it slowly latter.
First of all I used the LD_PRELOAD trick here to force the ld load my shared object first. Then I interrupt the program calloc to my own calloc instead of libc. It is seemed to be useless, so true because it not one of a part in my script I was just use it to test some stuff and forgot to remove…
Alright enough for joking, this is what I used for hooking the 0x21ea41 function
boolhook_function(ulladdress){/*
the hook idea is pretty simple
mov rax, address
jmp rax
*/ulladdr=address&~0xfffULL;charshellcode[12]={0x48,0xb8};*(ull*)&shellcode[2]=(ull)generate_bit;// <-- this function is mentioned below
shellcode[10]=0xff;shellcode[11]=0xe0;if(mprotect((void*)addr,0x1000,PROT_EXEC|PROT_READ|PROT_WRITE)!=0){perror("mprotect");_exit(123);}memcpy((void*)address,(void*)shellcode,sizeof(shellcode));__builtin___clear_cache((char*)address,(char*)address+sizeof(shellcode));returntrue;}
I change a very first few bytes of a function to jump instruction. If you often work with malware binary, this could be familar. Remember to use __builtin___clear_cache because in modern CPU, there is a D-Cache for data caching and I-Cache for Instruction caching. The instruction will be cached in ram for faster dispatching. If we don’t clear the cache, the old instruction still be executed.
The replacement for binary’s boolean expression:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedefstruct{ullfunc;// function used for callback
intbit_id;// as it is named
ullexpr;// the current expression of the boolean expression
ullyes,no;// the first and second argument
}Object;staticObject*create_object(ullfunction){Object*x=calloc(1,sizeof(Object));x->func=function;returnx;}
Talk more about expr, it is used to identify the current object and identify the nested decision. For example if the binary use bit_0 to select A and B, then use bit_1 to select the result with C it could be something like this.
See? if we can lift the whole program into this format. Z3 can easily solve the constraint hehehe.
The trick here is instead of running program function for example 0x36174 or 0x2d5b7 as I mentioned above, we could redirect it to our own function. From that we could collect data, parsing expression and understand many things
The alternative for 0x21ae41, we used to logging initialization for each bit
Record our argument and setting up some middle steps then redirect it for fn_partial_symbolic which takes our second argument and performing handling data to deobfuscate
staticObject*choice_object(Object*self){ullyes=self->yes;ullno=self->no;{boola=isTarget(yes);boolb=isTarget(no);if((a&&!b)||(!a&&b)){printf("Not good");_exit(123);}if(a&&b){logging("expr_%d = Choice(expr_%d, %#llx, %#llx)\n",expr_cnt+1,self->expr,convertAddr(yes),convertAddr(no));_exit(0);}}{boola=is_object(yes);boolb=is_object(no);if(a&&!b){Object*cur=create_object(fn_func_symbolic);intbtype=func_type(no);if(btype==-1){// fall back to fn_run_intermediate at the end of the function
}else{Object*x=(Object*)yes;cur->expr=expr_cnt++;logging("expr_%lld = Choice(expr_%lld, expr_%lld, %s)\n",cur->expr,self->expr,x->expr,(btype?"true":"false"));returncur;}}if(!a&&b){Object*cur=create_object(fn_func_symbolic);intatype=func_type(yes);if(atype==-1){// fall back to fn_run_intermediate at the end of the function
}else{Object*y=(Object*)no;cur->expr=expr_cnt++;logging("expr_%lld = Choice(expr_%lld, %s, expr_%lld)\n",cur->expr,self->expr,(atype?"true":"false"),y->expr);returncur;}}if(a&&b){Object*cur=create_object(fn_func_symbolic);Object*x=(Object*)yes;Object*y=(Object*)no;cur->expr=expr_cnt++;logging("expr_%lld = Choice(expr_%lld, expr_%lld, expr_%lld)\n",cur->expr,self->expr,x->expr,y->expr);returncur;}{Object*cur=create_object(fn_func_symbolic);cur->expr=expr_cnt++;intatype=func_type(yes);intbtype=func_type(no);if((atype!=-1)&&(btype!=-1)){logging("expr_%lld = Choice(expr_%lld, %s, %s)\n",cur->expr,self->expr,(atype?"true":"false"),(btype?"true":"false"));returncur;}}{Object*cur=create_object(fn_run_intermediate);cur->expr=self->expr;// we will not creating a new expression for this we will reuse
cur->yes=self->yes;cur->no=self->no;returncur;}}printf("An error occurred: %#llx %#llx\n",convertAddr(yes),convertAddr(no));_exit(-1);}
I will explain some helper function
isTarget: checking if the address is Correct or Fail function
convertAddr: get the rva of the address or its value if it is a heap-allocated address
is_in_text, isHigh, logging: like what it is named
is_object: checking if it is our created object or not (simply comparing obj->func)
func_type: checking if it is True-like or False-Like function by using really stupid-but-work method
Talk more about func_type, I used many heuristic pattern to recognize the function, eventhough it could be incorrect in general but it worked in this binary, that is all what we need
Sorry I would not put those helper functions here but the blog will be too repeative, you can see in the full script
I separated each case into 4 different block. The first block is checking if we reach Correct/Fail or not.
This third block is checking if our input is both True or False. Something like expr_15 = Choice(expr_12, true, false). True/False here is a concrete Boolean expression. I mentioned in the Initial Analysis. After selecting True or False, the object will be pushed into queue for generating closure so fn_func_symbolic is the best option here.
boola=is_object(yes);boolb=is_object(no);if(a&&!b){Object*cur=create_object(fn_func_symbolic);intbtype=func_type(no);if(btype==-1){// fall back to fn_run_intermediate at the end of the function
}else{Object*x=(Object*)yes;cur->expr=expr_cnt++;logging("expr_%lld = Choice(expr_%lld, expr_%lld, %s)\n",cur->expr,self->expr,x->expr,(btype?"true":"false"));returncur;}}if(!a&&b){Object*cur=create_object(fn_func_symbolic);intatype=func_type(yes);if(atype==-1){// fall back to fn_run_intermediate at the end of the function
}else{Object*y=(Object*)no;cur->expr=expr_cnt++;logging("expr_%lld = Choice(expr_%lld, %s, expr_%lld)\n",cur->expr,self->expr,(atype?"true":"false"),y->expr);returncur;}}if(a&&b){Object*cur=create_object(fn_func_symbolic);Object*x=(Object*)yes;Object*y=(Object*)no;cur->expr=expr_cnt++;logging("expr_%lld = Choice(expr_%lld, expr_%lld, expr_%lld)\n",cur->expr,self->expr,x->expr,y->expr);returncur;}
This handler only processes cases when at least one of the two arguments is our object, the other could be either object or True/False-like function. The logic is pretty simple and could be understand easily through this example
1
2
3
4
1.expr_1=Choice(expr_2,expr_3,true/false)2.expr_1=Choice(expr_2,true/false,expr_3)3.expr_1=Choice(expr_2,expr_3,expr_4)// only these three cases
Then the last stub handle a leftover case
1
2
3
Object*cur=create_object(fn_run_intermediate);cur->expr=self->expr;// we will not creating a new expression for this we will reuse
cur->yes=self->yes;cur->no=self->no;
Let’s analyze this. Genuinelly, This case is the first argument is a closure, and the second argument is a selector which is either True-like or False-like function. But it hide that pattern using multiple intermediate internal object then using unpacker to unpack respectively. The unpacker is
The first argument is program internal object, and the second one is fixed selector which is either true-like or false-like function
Let’s look what called sub_3706A
It generates a closure with two registered variables. The behavior of this could be describe like this
First it will initialize two real Boolean expression, then it will wrap those expressions into an intermediate closure and using unpacker with fixed selector function to release initial expression
In the end, temp is our valid object, so we can manually write a recursive unpacker to retrieve the core expression. If the current state is not one of the case that choice_object can describe an expression, it will be this case and transfer control for run_intermediate
staticObject*run_intermediate(Object*self,ullfilter){// can we just run this?
if(!isHigh(filter)){logging("Latter expr_%d",self->expr);_exit(0);}boolvalid_a=is_object(self->yes)||(func_type(self->yes)!=-1);boolvalid_b=is_object(self->no)||(func_type(self->no)!=-1);if((valid_a&&!valid_b)||(!valid_a&&valid_b)){logging("We will see about this");_exit(0);}if(valid_a&&valid_b){// valid object
Object*cur=create_object(fn_decoy);cur->yes=self->yes;cur->no=self->no;cur->expr=self->expr;returnchoice_object(cur);}booltype=func_type(filter);if(type==0)// false-like
{Object*cur=create_object(fn_decoy);cur->yes=*(ull*)(self->yes+16);cur->no=*(ull*)(self->no+16);cur->expr=self->expr;returnchoice_object(cur);}else// true-like
{Object*cur=create_object(fn_decoy);cur->yes=*(ull*)(self->yes+8);cur->no=*(ull*)(self->no+8);cur->expr=self->expr;returnchoice_object(cur);}logging("Uh oh\n");_exit(0);}
The filter is a selector function which is a true-like or false-like function, because it high bit is never set (I tested in the script).
That is all about my script and by running it we could have these expressions in Here. In the end of the script is this line
1
expr_103888=Choice(expr_103886,0x21eb44,0x21eb6d)
It is exactly what I have expected, 0x21eb44 is the Correct function and 0x21eb6d is the Fail function. So after this we could use Z3 to solve the constant that leading to the Correct option. We can have the flag
If you have any questions, you could DM me. I’m not a good teacher to be honest so some part of the writeup would be confused. Final words, happy reversing >:D