I’m finally proud to write this blog post after some weeks of study and test about how i’ve created my first debugger, built on top of Frida.
As usual, let’s spend a couple of word to let the folks understand what was the goal. People following me through twitter or github already know that I recently came out with a new tool called frick, which is a Frida cli that sleep the target thread once the hook is hit giving a context with commands to play with. However, sleeping a single thread is not very efficent when we are dealing with multithread targets that can alter the memory layout while our hook thread is sleeping, or spawn more threads that will maybe invoke the hook target one more time, TLDR; we need to sleep everything except the threads involved with frida.
Basically, we are looking for debugger breakpoints, which let me say, was the harderst thing I’ve ever did in term of researches and tests (even if in the end, we are speaking about 50/100 lines of code).
Let me say once again that I’m really proud of this, I’ve tested this code against 2 popular games (for instance – Clash of Clans – protected with Arxan (more researches to prevent main thread to detect hooks modifications) and Jurassic World Alive (which literally spawn 200 threads a second).
The code style is terrible, the logic can be fixed but I was so excited to write about this so I’ve decided to post streight the original code that reach the goal (will be improved and fixed for frick). I’ve added notes on the code which will help understand the logic i’ve used and why.
// retrieve gettid native function var gettid = nf(getnf('gettid', 'libc.so', 'int', [])); // getting main process tid var main_tid = gettid(); // map 8 bytes of trash. We will use frida replace api to use js code var handler = Memory.alloc(8); Memory.protect(handler, 8, 'rwx'); Interceptor.replace(handler, new NativeCallback(function (sig) { console.log('handle -> ' + gettid()); while (sleep) { Thread.sleep(1); } return sig; }, 'int', ['int'])); // following code will attach to our main target and sleep all the threads function att(off, pt) { if (base === 0) { return; } targets['' + pt] = Interceptor.attach(pt, function() { sleep = true; // retrieve signal and tkill native functions var signal = nf(getnf('signal', 'libc.so', 'int', ['int', 'pointer'])); var tkill = nf(getnf('tkill', 'libc.so', 'int', ['int', 'int'])); // setup an handler for sigusr2 signal(12, handler); var t_hooks = {}; // get all the threads the nerd way var ts = readThreads(); for (var to in ts) { var t = ts[to]; // prevent frida stuffs if (t[0].indexOf('gum') >= 0 || t[0].indexOf('gdbus') >= 0) { continue; } // if the thread is running, we use the signal handler // otherwise, we can know the current PC of the thread, using signal // on sleeping threads caused bad behaviors, so just attach to //the arbitrary instruction and sleep in case something else resume it if (t[2] === 'R') { if (parseInt(t[0]) !== main_tid) { try { tkill(parseInt(t[0]), 12); console.log('signaling -> ' + t[1]); } catch (e) {} } } else { var tpc = ptr(t[29]); if (t_hooks[t[29]] !== null && typeof t_hooks[t[29]] !== 'undefined') { continue; } console.log('attaching -> ' + tpc); t_hooks[t[29]] = Interceptor.attach(tpc, function () { console.log('sleep -> ' + this.context.pc); while (sleep) { Thread.sleep(1); } }); } } while(sleep) { Thread.sleep(1); } for (var k in t_hooks) { t_hooks[k].detach(); } }); } // if you read all the notes before this, // there is no need of notes to understand this method function readThreads() { var m_alloc = Memory.alloc(1024); Memory.protect(m_alloc, 1024, 'rwx'); var opendir = nf(getnf('opendir', 'libc.so', 'pointer', ['pointer'])); var readdir = nf(getnf('readdir', 'libc.so', 'pointer', ['pointer'])); var fopen = nf(getnf('fopen', 'libc.so', 'pointer', ['pointer', 'pointer'])); var fgets = nf(getnf('fgets', 'libc.so', 'pointer', ['pointer', 'int', 'pointer'])); Memory.writeUtf8String(m_alloc, '/proc/self/task'); var proc_dir = opendir(m_alloc); var entry; var res = []; while ((entry = readdir(proc_dir)) > 0) { var line = Memory.readUtf8String(entry.add(19)); if (line.indexOf('.') >= 0) { continue; } Memory.writeUtf8String(m_alloc, '/proc/' + pid + '/task/' + line + '/stat'); Memory.writeUtf8String(m_alloc.add(64), 'r'); try { var fp = fopen(m_alloc, m_alloc.add(64)); line = Memory.readUtf8String(fgets(m_alloc, 1024, fp)); var name = line.substring(line.indexOf('('), 1 + line.indexOf(')')); line = line.replace(' ' + name, ''); var proc = line.split(' '); proc.splice(1, 0, name.replace('(', '').replace(')', '')); res.push(proc); } catch (e) { } } return res; }
The log result from Clash of Clans
s l -> 2 commands added to init callback -> frida attached -> script injected -> target arch: arm -> pointer size: 4 -> leaked target base at 0xd1a46000 -> attached to 0xf1e2e695 -> 0xf1e2e695 added to target offsets -> thread started: 14776 target: 0xd1ca67a1 (0x2607a1) -> thread started: 14790 target: 0xd1e95e7f (0x44fe7f) -> thread started: 14791 target: 0xd1e95e7f (0x44fe7f) ----[ 0xf1e2e695 ]------------------------------------------------------------------------------------------ -------------------------------------------------------------------------------------------[ registers ]---- R0 : 0x58 R1 : 0xceb21f58 -> 0x642f0001 R2 : 0x6e R3 : 0xf1e3d16a -> 0x2b720000 -> 0x0 R4 : 0x58 R5 : 0xceb21f58 -> 0x642f0001 R6 : 0xceb21f5a -> 0x7665642f -> 0x0 R7 : 0x1 R8 : 0x0 R9 : 0x0 R10 : 0xceb22010 -> 0x400 R11 : 0xceb220d8 -> 0x400 R12 : 0xf1ea3b2c -> 0xf1e2e695 -> 0x1f000f8 SP : 0xceb21f50 -> 0x4 PC : 0xf1e3d109 -> 0xebd1051c -> 0x0 LR : 0xf1e3d109 -> 0xebd1051c -> 0x0 attaching -> 0xf1e5c880 attaching -> 0xf1e2c050 attaching -> 0xf1e5cbb0 attaching -> 0xf1e5ca58 attaching -> 0xd855b154 attaching -> 0xf1e5c9c0 attaching -> 0xf1e5d9a0 signaling -> ll.clashofclans sleep -> 0xf1e5bb6b signaling -> GLThread 5366 sleep -> 0xf1e5bb6b attaching -> 0xf1e5d760 sleep -> 0xf0cb9319 sleep -> 0xf1e31403 sleep -> 0xf1e31403 sleep -> 0xf016c7c1 sleep -> 0xf016c7c1 sleep -> 0xf1e2e985 sleep -> 0xf1e31403 run sleep -> 0xf016b553 sleep -> 0xf1e31403 sleep -> 0xf1e31403 sleep -> 0xf016b553 sleep -> 0xf016b553 sleep -> 0xf1e31403 sleep -> 0xf1e31403 sleep -> 0xf1e31403 sleep -> 0xf1e2e985 sleep -> 0xf1e2e985 sleep -> 0xf1e31403 handle -> 14777 handle -> 14769 ----[ 0xf1e2e695 ]------------------------------------------------------------------------------------------ -------------------------------------------------------------------------------------------[ registers ]---- R0 : 0x58 R1 : 0xceb21f78 -> 0xbb01000a R2 : 0x1c R3 : 0x0 R4 : 0x58 R5 : 0xceb21f64 -> 0x0 R6 : 0x131028b0 -> 0x6fd5c568 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 R7 : 0xceb21f78 -> 0xbb01000a R8 : 0x0 R9 : 0xd0591200 -> 0x590000 R10 : 0xceb22040 -> 0x131028b0 -> 0x6fd5c568 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 -> 0x6fdb0f08 R11 : 0xe5d0b410 -> 0xf0515b74 -> 0x0 R12 : 0xea570c68 -> 0xf1e2e695 -> 0x1f000f8 SP : 0xceb21f58 -> 0xceb21f60 -> 0x1c PC : 0xea55bf51 -> 0x26f7ef46 -> 0x0 LR : 0xea55bf51 -> 0x26f7ef46 -> 0x0 attaching -> 0xf1e5c880 attaching -> 0xf1e2c050 attaching -> 0xf1e5cbb0 attaching -> 0xf1e5ca58 attaching -> 0xf1e5d760 attaching -> 0xf1e5d9a0 attaching -> 0xd855b154 signaling -> ll.clashofclans handle -> 14769 signaling -> GLThread 5366 handle -> 14777