#+TITLE: Writeups for Dennis Yurichev's Reverse Engineering Challenges (#2-#11) #+DATE: <2019-03-10 Sun 00:00> #+TAGS: writeup reverse-engineering arm x86 As mentioned in the (now deleted) post I wrote describing my plans for 2019, one of my goals this year is to get through at least 50 of the exercises on Dennis Yurichev's [[https://challenges.re/][challenges.re]]. I've decided to document my progress in the form of writeups for the challenges I complete, batched in sets of ten exercises. For each challenge, I'll try to explain the intuitions that brought me closer to answering the recurring question from Yurichev, "[w]hat does this code do?" * Challenge #2 In nearly all of the challenges available on the site, we're given equivalent disassembly listings of a function, =f=, as generated by different compilers on different processor architectures, and we're asked to describe what the code does. For now, I've decided to take it easy and only pay attention to the disassemblies for GCC on x86, as that's what I've done the most work with. We aren't given a target operating system, but I think it's reasonable to assume that the x86 code uses the [[https://en.wikipedia.org/wiki/X86_calling_conventions#cdecl][cdecl calling convention]]. Although I stayed within my comfort zone in terms of instruction set architecture, I refrained from my usual habit of converting the disassembly listing to AT&T syntax for once. #+BEGIN_EXPORT html
#+END_EXPORT Below is a rough translation of the disassembly listing to C. My process is relatively unchanged from the workflow I described in an [[http://jakob.space/blog/decompilation-by-hand.html][older post]]. #+BEGIN_SRC c :hl_lines 0 unsigned f(unsigned a) { // mov eax,DWORD PTR [esp+0x4] // bswap eax a = ((a & 0xff) << 24) | ((a & 0xff00) << 8) | ((a & 0xff0000) >> 8) | ((a & 0xff000000) >> 24); // mov edx,eax // and eax,0xf0f0f0f // and edx,0xf0f0f0f0 // shr edx,0x4 // shl eax,0x4 // or eax,edx a = ((a & 0xf0f0f0f) << 4) | ((a & 0xf0f0f0f0) >> 4); // mov edx,eax // and eax,0x33333333 // and edx,0xcccccccc // shr edx,0x2 // shl eax,0x2 // or eax,edx a = ((a & 0x33333333) << 2) | ((a & 0xcccccccc) >> 2); // and eax,0x55555555 // and edx,0xaaaaaaaa // add eax,eax // shr edx,1 // or eax,edx a = ((a & 0x55555555) << 1) | ((a & 0xaaaaaaaa) >> 1); // ret return a; } #+END_SRC I think it should make sense that =add eax,eax= is mathematically equivalent to =imul eax, 2=, but it takes another step to see that it's [[https://math.stackexchange.com/questions/1610667/why-shifting-left-1-bit-is-the-same-as-multiply-the-number-by-2][equivalent]] to =shl eax,1=, which is represented in the C code as =<< 1=. This isn't terribly complicated, but it's an optimization detail that some might not be familiar with. =bswap= is an instruction I was unfamiliar with, so I consulted my [[https://c9x.me/x86/html/file_module_x86_id_21.html][favorite x86 reference]]. It converts the endianness of the word in the register. If you don't know what that means, I'd suggest you read the page in the ISA reference. The code seems nonsensical at first, but we can compile it and inspect the output given some test values. #+BEGIN_SRC c :hl_lines 0 #include void main(void) { unsigned i; for (i = 0; i <= 256; i++) { printf("%010u %08x\n", i, i); printf("%010u %08x\n", f(i), f(i)); printf("\n"); } } #+END_SRC Which produces: #+BEGIN_SRC 0000000000 00000000 0000000000 00000000 0000000001 00000001 2147483648 80000000 0000000002 00000002 1073741824 40000000 0000000003 00000003 3221225472 c0000000 0000000004 00000004 0536870912 20000000 ... #+END_SRC What's happening might not be immediately obvious, but there's a pattern in the column of input/output represented in hexadecimal. Let's have a look at the binary representations of a few pairs: #+BEGIN_SRC python bin(0x00000001) # --> '0b00000000000000000000000000000001' bin(0x80000000) # --> '0b10000000000000000000000000000000' bin(0x00000003) # --> '0b00000000000000000000000000000011' bin(0xc0000000) # --> '0b11000000000000000000000000000000' # ... bin(0x0000004d) # --> '0b00000000000000000000000001001101' bin(0xb2000000) # --> '0b10110010000000000000000000000000' #+END_SRC My answer to the question is that =f= reverses the bits of the word it is given. * Challenge #3 This time, we're given an array of 64 32-bit integers and a hint that "[t]he algorithm is well-known, but I've changed [the] constant so it wouldn't be googleable." #+BEGIN_SRC c :hl_lines 0 int f(unsigned n) { unsigned a, b; // mov edx, edi // shr edx // or edx, edi // mov eax, edx a = b = (n >> 1) | n; // shr eax, 2 // or eax, edx // mov edx, eax a = b = (a >> 2) | b; // shr edx, 4 // or edx, eax // mov eax, edx a = b = (b >> 4) | a; // shr eax, 8 // or eax, edx // mov edx, eax a = b = (a >> 8) | b; // shr edx, 16 // or edx, eax b = (b >> 16) | a; // imul eax, edx, 79355661 ; 0x4badf0d // shr eax, 26 a = (b * 0x4badf0d) >> 26; // mov eax, DWORD PTR v[0+rax*4] // ret return v[a]; } #+END_SRC The first thing that stood out to me was the presence of =-1= in the array of integers. Testing from =0= to =UINT_MAX=, the only =n= that returns =-1= is =0=. Interesting. It's also worth noting that the array contains every integer from 0, 31, so this function is using /some rule/ to map the input space onto [0, 31]. If we inspect the values of =f= for test values from =0= to =UINT_MAX=: #+BEGIN_SRC :hl_lines 0 f(1) = 31 f(2) = 30 f(3) = 30 f(4) = 29 f(5) = 29 f(6) = 29 f(7) = 29 f(8) = 28 f(9) = 28 f(10) = 28 f(11) = 28 f(12) = 28 f(13) = 28 f(14) = 28 f(15) = 28 f(16) = 27 f(17) = 27 f(18) = 27 f(19) = 27 f(20) = 27 f(21) = 27 f(22) = 27 f(23) = 27 f(24) = 27 f(25) = 27 f(26) = 27 f(27) = 27 f(28) = 27 f(29) = 27 f(30) = 27 f(31) = 27 #+END_SRC There's a pattern of exponential growth here -- every result occurs twice as frequently as the previous result. Mathematically, this is \(31 - [log_2(n)]\) where the brackets represent the Greatest Integer Function (\(f(x)\) returning the largest integer less than or equal to \(x\)). This can be verified by comparing the result of =f= to the following function for some test values: #+BEGIN_SRC c :hl_lines 0 int my_f(unsigned n) { return 31 - ((int) (log(n) / log(2))); } #+END_SRC * Challenge #4 This time around we're given an additional question to answer: "Some versions have the =0x1010101= constant, some do not. Why?" I decided that I'd reverse the x86 disassembly first, and then compare it to the other architectures. #+BEGIN_SRC c :hl_lines 0 unsigned f(unsigned a) { // mov edx,edi // shr edx,1 // and edx,0x55555555 // sub edi,edx a -= ((a >> 1) & 0x55555555); // mov eax,edi // shr edi,0x2 // and eax,0x33333333 // and edi,0x33333333 // add edi,eax a = (a & 0x33333333) + ((a >> 2) & 0x33333333); // mov eax,edi // shr eax,0x4 // add eax,edi // and eax,0xf0f0f0f // imul eax,eax,0x1010101 // shr eax,0x18 // ret return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 0x18 } #+END_SRC The past few challenges have shown us that a good way of reversing these bit-twiddling functions is to test a few input values and look at the binary representations of the input and output values. #+BEGIN_SRC In: 00000000 Out: 0 In: 00000001 Out: 1 In: 00000010 Out: 1 In: 00000011 Out: 2 ... In: 00001100 Out: 2 In: 00001101 Out: 3 In: 00001110 Out: 3 In: 00001111 Out: 4 #+END_SRC It doesn't take much effort to see that the function is counting the number of bits set in the input. This was particularly interesting to me as I was asked to derive this algorithm for a past job interview (though I wasn't able to in the time given). This falls apart for numbers larger than =0xff=, however. It returns the number of bits plus some constant that changes depending on which bits in the higher bytes are set. I'll assume that =f= is only meant to be called with 8-bit integers. With that, we can move onto the second question. The disassemblies for x86, ARM64, and Thumb have the =0x1010101= constant, while the disassemblies for ARM and MIPS do not. Returning to the strategy of inspecting binary representations: #+BEGIN_SRC 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 00000001 00000001 00000001 00000001 00000000 00000000 00000000 00000010 00000010 00000010 00000010 00000010 ... 00000000 00000000 00000000 00010000 00010000 00010000 00010000 00010000 00000000 00000000 00000000 00010001 00010001 00010001 00010001 00010001 00000000 00000000 00000000 00010010 00010010 00010010 00010010 00010010 ... 00000000 00000000 00000000 11111101 11111101 11111101 11111101 11111101 00000000 00000000 00000000 11111110 11111110 11111110 11111110 11111110 00000000 00000000 00000000 11111111 11111111 11111111 11111111 11111111 #+END_SRC It would appear that multiplying a 32-bit integer by =0x1010101= propagates the low byte to the three bytes above it. This makes sense when you notice that the multiplication is paired with a =shr= of =0x18= -- moving the highest byte into the lowest byte. Though, this doesn't really answer the question. What difference is there between the architectures that use the =0x1010101= and the architectures that don't? ARM and MIPS still do a shift by =0x18=, so what's going on? Looking at the ARM example, the instructions around the shift are: #+BEGIN_SRC asm :hl_lines 0 ADD r0,r0,r0,LSL #16 ADD r0,r0,r0,LSL #8 LSR r0,r0,#24 #+END_SRC For MIPS, it looks like: #+BEGIN_SRC asm :hl_lines 0 sll $3,$2,8 addu $2,$3,$2 sll $3,$2,16 addu $2,$2,$3 j $31 srl $2,$2,24 #+END_SRC Both of these (humor me, I know the orders are different) are equivalent to: #+BEGIN_SRC c :hl_lines 0 a = (a << 8) + a; a = (a << 16) + a; a >> 24; #+END_SRC And, with some test values, we can see that this is equivalent to multiplication by =0x1010101= and shifting by 24. #+BEGIN_SRC c :hl_lines 0 unsigned a(unsigned n) { n = (n << 8) + n; n = (n << 16) + n; return n >> 24; } unsigned b(unsigned n) { return (n * 0x1010101) >> 24; } void main(void) { for (unsigned i = 0; i < UINT_MAX; i++) { if (a(i) != b(i)) { printf("%u\n", i); } } } #+END_SRC I suspect the reason it doesn't show up in the ARM or MIPS disassemblies is due to the fixed-width instruction encoding. The compiler likely decided it would be less efficient to work with the =0x1010101= constant than to break it up into a pair of shifts and additions. * Challenge #5 This is the first challenge we're given that has loops and conditionals, as indicated by the telltale labels starting with ".L". Another initial observation is that the first instruction in =f= operates on =%rsi=, and the third operates on =%rcx=, so it's very likely that this function has four parameters. Translation to C is more involved than it was with the previous challenges, but it is valuable as it makes the purpose of =f= very clear. In lieu of an analysis of inputs and outputs, I'll provide a few notes on the process of translation. First, =cmp= gave me a bit of trouble as I've been out of practice for some time and the difference between AT&T and Intel syntax threw me for a loop. Fortunately, the [[https://en.wikibooks.org/wiki/X86_Assembly/Control_Flow#Comparison_Instructions][wikibooks]] for x86 assembly covers this in detail. In AT&T syntax, the order is =cmp subtrahend, minuend=, while in Intel syntax, the order is =cmp minuend, subtrahend=. The subtrahend is subtracted from the minuend, so, in Intel syntax, =cmp rcx, rsi; ja .L10= will jump if =%rcx= is greater than =%rsi=. Looking further into the function, there is some dereferencing with =BYTE PTR=, which tipped me off that this was probably a function operating on a string. There's a curious =push rbx=, followed by a =pop rbx= before the =ret=. I ignored this initially, taking it to be register preservation. It was. An intuition of what's worth ignoring is valuable in reverse engineering. Upon reaching =.L16=, there are a lot of registers in use. It helped to look at each register in isolation and see how they were used. For example, =%r10= is used in the following instructions: =xor r10d, r10d=, =add r10, 1=, =lea rax, [rdi+r10]=, and =cmp r10, r11=. This is very typical of a for-loop counter. =%r9= on the other hand only shows up in two instructions: =mov r9d, 1=, and =cmovne r8d, r9d=. =%r9= is just used as a source of 1 for =cmovne=, since there are no encodings for =cmovne= that have an immediate source. =cmovne= was unfamiliar to me, so I did look it up in my favorite [[https://c9x.me/x86/html/file_module_x86_id_34.html][x86 reference]]. It's a conditional move. =movz= was similarly unfamiliar. It simply loads =%bl= with the source byte and zeroes out the higher portions of the register. #+BEGIN_SRC c :hl_lines 0 char *f(char *a, unsigned b, char *c, unsigned d) { // cmp rcx, rsi // ja .L10 if (d >= b) { // .L10: // xor eax, eax // ret return NULL; } // sub rsi, rcx // add rsi, 1 // mov r11, rsi b = b - d + 1; // je .L10 if (b == 0) { // .L10: // xor eax, eax // ret return NULL; } // test rcx, rcx // jne .L16 // mov rax, rdi // ret if (d == 0) { return a; } // .L16: // push rbx // xor r10d, r10d // mov r9d, 1 // ... // cmp r10, r11 // jne .L4 for (int i = 0; i != b; i++) { // xor r8d, r8d unsigned ret = 0; // .L4: // lea rax, [rdi+r10] // xor esi, esi // ... // add rsi, 1 // cmp rsi, rcx // jne .L8 for (int j = 0; j != d; j++) { // movzx ebx, BYTE PTR [rdx+rsi] // cmp BYTE PTR [rax+rsi], bl // cmovne r8d, r9d if (a[i] != c[j]) { ret = 1; } } // test r8d, r8d // je .L12 if (!ret) { // .L12: // pop rbx // ret return a + i; } } // xor eax, eax // pop rbx // ret return NULL; } #+END_SRC The variable names I chose are pretty opaque, but if you stare at this long enough, it should be pretty clear that =f= returns the offset of =c= in =a=. =b= and =d= are just the lengths of =a= and =c= respectively. * Challenge #6 An additional hint given for this exercise is that, "[t]his is one of the simplest exercises I made, but still this code can be served as useful library function and is certainly used in many modern real-world applications." I'll leave the relative addresses in my annotations of the disassembly, as it appears to be PIC. For the sake of showing the mapping between assembly instructions and C code, I'll first give a translation that uses =goto=, followed by a cleaned up version. #+BEGIN_SRC c // 0: push rbp // 1: mov rbp,rsp // 4: mov QWORD PTR [rbp-0x8],rdi // 8: mov QWORD PTR [rbp-0x10],rsi int f(char *a, char *b) { _start: // c: mov rax,QWORD PTR [rbp-0x8] // 10: movzx eax,BYTE PTR [rax] // 13: movsx dx,al // 17: mov rax,QWORD PTR [rbp-0x10] // 1b: mov WORD PTR [rax],dx *b = *a; // 1e: mov rax,QWORD PTR [rbp-0x10] // 22: movzx eax,WORD PTR [rax] // 25: test ax,ax // 28: jne 2c // 2a: jmp 38 if (*a & 0xffff != 0) { // 2c: add QWORD PTR [rbp-0x8],0x1 // 31: add QWORD PTR [rbp-0x10],0x2 // 36: jmp c a++; b++; goto _start; } // 38: pop rbp // 39: ret } #+END_SRC #+BEGIN_SRC c int f(char *a, char *b) { while (*a != '\0') { *b++ = *a++; } } #+END_SRC Cool. Yurichev wasn't lying, this is a damn simple exercise, but it is something that's used in nearly every C program. It's =strcpy=! * Challenge #7 This exercise gives the same hint as last time, and similarly uses address offsets instead of symbols. Control flow isn't as initially obvious as some of the past exercises, but the first instruction is a pretty good tell that this function takes a =char *= as a parameter, and the =test dl,dl= was a good tell that the control flow depends on the individual characters in that parameter. The =0x41= in that ==lea esi,[rdx-0x41]= instruction stood out to me, as =0x41= is 'A' in ASCII, and the =0x20= in the =add edx,0x20= was also a big clue, as ='a' - 'A'= is =0x20=. #+BEGIN_SRC c void f(char *a) { char *cur; // 0: movzx edx,BYTE PTR [rdi] // 3: mov rax,rdi // 6: mov rcx,rdi // 9: test dl,dl // b: je 29 // 29: repz ret if (*a == '\0') return; // 6: mov rcx,rdi cur = a; // 25: test dl,dl // 27: jne 10 while (*cur != '\0') { // 10: lea esi,[rdx-0x41] // 13: cmp sil,0x19 // 17: ja 1e // 19: add edx,0x20 // 1c: mov BYTE PTR [rcx],dl if (*cur - 0x41 <= 0x19) *cur += 0x20; // 1e: add rcx,0x1 // 22: movzx edx,BYTE PTR [rcx] cur++; } // 29: repz ret } #+END_SRC Just from the tells outlined in the previous paragraph, I don't even need to run =f= to know that it converts =a= to lowercase, albeit only capable of transforming capital ASCII characters (producing garbage for, say, a space character). * Challenge #8 The hint we're given this time is, "[t]his is one of the busiest algorithms under the hood, though, usually hidden from programmers. It implements one of the most popular algorithms in computer science. It features recursion and a callback function." In preparation for an exercise that's would likely be more difficult than the past few, I did a couple quick perusals to get a basic idea of the control flow, the parameters, and the return values. The =mov rbp,rdx= early on indicates that there are at least three parameters. There's a =push rbp= instruction, but [[https://en.wikipedia.org/wiki/Function_prologue][explicit creation of a stack frame]]. There are also =push r12= and =push rbx= instructions. These all occur at the beginning of the function, so we see some register preservation and an indication that these are the registers that are going to be used in the code. I find that a lot of reverse engineering involves getting good footing, so this is the information you want when starting out. What I normally try to find out next is whether the parameters and return type are integers or pointers: =mov rsi,QWORD PTR [rbx]=, after =%rsi= was moved into =%rbx= is a good tell that the second parameter is a pointer, likely to an array of pointer as it's dereferenced as =QWORD PTR=, and the =call r12= tells me that the first parameter is the callback that was mentioned in the hint. The =js 40= after testing the callback's return value tells me that its return value is signed -- probably an int, not a pointer -- and the pair of =mov rsi,QWORD PTR [rbx]= and =mov rdi,rbp= before the call indicate that it takes two parameters. #+BEGIN_SRC c void *f(int (*a)(void *, int), void **b, int c) { int ret; // 0: push r12 // 2: test rsi,rsi // ... // 10: je 32 if (b == 0) { // 32: pop rbx // 33: pop rbp // 34: xor eax,eax // 36: pop r12 // 38: ret return NULL; } // r12 <- a // rbx <- b // rbp <- c while (1) { // (This code path is also duplicated at 49-54. The branch that // contains the duplicated code has been omitted, as the same // effect arises from this loop continuing to iterate. // // 18: mov rsi,QWORD PTR [rbx] // 1b: mov rdi,rbp // 1e: call r12 ret = a(*b, c); // 21: test eax,eax // 23: je 56 if (ret == 0) { // 56: mov rax,rbx // 59: pop rbx // 5a: pop rbp // 5b: pop r12 // 5d: ret return b; } // 25: js 40 else if (ret < 0) { // 40: mov rbx,QWORD PTR [rbx+0x10] b = b[4]; // 44: test rbx,rbx // 47: je 32 if (b == NULL) { // 32: pop rbx // 33: pop rbp // 34: xor eax,eax // 36: pop r12 // 38: ret return NULL; } } else { // 27: mov rbx,QWORD PTR [rbx+0x18] b = b[6]; // 2b: test rbx,rbx // 30: jne 18 if (b == NULL) { // 32: pop rbx // 33: pop rbp // 34: xor eax,eax // 36: pop r12 // 38: ret return NULL; } } } } #+END_SRC In deriving meaning from this, I have a bit of an advantage; I've just recently implemented this exact algorithm for my university's computer systems principle course. This is the search function for a binary search tree, which takes an arbitrary comparison function, =a=,, and returns the first node for which =a= returns 0. The function returns =NULL= if the item is not in the tree. =c= is some sort of "data" parameter for the callback function, hence why it isn't used in the algorithm. =b= is probably a pointer to a struct looking something like the following: #+BEGIN_SRC c struct tree_node { char data[0x10]; struct tree_node *left; struct tree_node *right; } #+END_SRC as =QWORD PTR [rbx+0x10]= is followed when =a= returns something less than 0 (represented in the struct as =left=), and =QWORD PTR [rbx+0x18]= is followed when =a= returns something greater than 0 - (represented in the struct as =right=). This exercise is a little unusual. The hint mentions recursion, but this algorithm is entirely iterative. Perhaps it was implemented recursively in C, and the compiler performed some sort of tail-call optimization? I honestly have no idea. * Challenge #9 The hint we're given this time is, "[n]ow that's easy." I certainly hope it is. This is the first challenge we're given that uses libc. It's also the first challenge in which we see the compiler using [[https://en.wikipedia.org/wiki/Switch_statement#Compilation][binary search]] to optimize a conditional with more than one branch. I tend to write these out as =switch= statements whenever I see them, but it's perfectly reasonable for a compiler to optimize an =if= in the same way. #+BEGIN_SRC c #include #include int f(char a) { // sub rsp, 8 // movzx eax, BYTE PTR [rdi] switch (a) { // cmp al, 89 // je .L3 case 'Y': // cmp al, 121 // jne .L2 case 'y': // .L3: // mov eax, 1 // add rsp, 8 // ret return 1; // jle .L21 // ... // .L21: // cmp al, 78 // je .L6 case 'N': // ... // cmp al, 110 // je .L6 case 'n': // .L6: // xor eax, eax // add rsp, 8 // ret return 0; default: // .L2: // mov edi, OFFSET FLAT:.LC0 // call puts // xor edi, edi // call exit puts("error!"); exit(0); } } #+END_SRC Yurichev wasn't lying, this was an easy challenge. In fact, if I were reverse engineering a binary and came across something like this, I probably wouldn't bother translating the assembly to equivalent C. It's a function that converts a character to a boolean (in the sense of a prompt that asks the user for 'Y' or 'N' -- "Yes" or "No") and exits prematurely if the character wouldn't make sense in that context. * Challenge #10 The hint time is "[t]his code snippet is short, but tricky. What does it do? It's used heavily in low-level programming and is well-known to many low-level programmers. There are several ways to calculate it, and this is the one of them." The snippet really is short, clocking in at only four instructions, but I still felt the need to break out [[https://godbolt.org/][Compiler Explorer]] for this one. The part about being "used heavily in low-level programming" threw me off a bit, since I saw =neg= and thought that perhaps that'd correspond to the =~= operator in C, which I've only seen used in very low-level bit shifting code. This initial assumption would've led me astray, however, and I'm glad I took the extra minute to verify. #+BEGIN_SRC c int f(int a) { return -a; } #+END_SRC #+BEGIN_SRC asm f(int): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov eax, DWORD PTR [rbp-4] neg eax pop rbp ret #+END_SRC #+BEGIN_SRC c int f(int a) { return ~a; } #+END_SRC #+BEGIN_SRC asm f(int): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov eax, DWORD PTR [rbp-4] not eax pop rbp ret #+END_SRC =not= corresponds to =~=, and =neg= corresponds to =-= We're dealing with =neg= here. The equivalent C code for the snippet is given. Because I had Compiler Explorer open already, I decided to throw this in there for kicks and giggles. x86-64 gcc 8.3 with =-O2= spits out the exact same series of instructions as the challenge. I love the predictability of C compilers. #+BEGIN_SRC c int f(int a, int b) { return (a + b - 1) & -b; } #+END_SRC This doesn't answer our question, though. What does this do? We can test a few values of =a= and =b= with the following snippet, replacing =2<<0= with various constants. #+BEGIN_SRC c int main(void) { int i, j; j = 2 << 0; for (i = 0; i < 256; i++) { printf("%-8x %-8x %-8x\n", i, j, f(i, j)); } } #+END_SRC #+BEGIN_SRC 0 2 0 1 2 2 2 2 2 3 2 4 4 2 4 5 2 6 6 2 6 7 2 8 8 2 8 9 2 a a 2 a b 2 c c 2 c d 2 e e 2 e f 2 10 ... 0 8 0 1 8 8 2 8 8 3 8 8 4 8 8 5 8 8 6 8 8 7 8 8 8 8 8 9 8 10 a 8 10 b 8 10 c 8 10 d 8 10 e 8 10 f 8 10 10 8 10 11 8 18 12 8 18 #+END_SRC It would seem that this is some sort of "least multiple of \(b\) such that \(b \gt a\) given that \(b\) is a power of two", but I feel as though I'm grasping at straws here. As a Gentoo user, I have the Linux source tree checked out at =/usr/src/linux=, and because the hint mentions low-level programming, I decided to create a regular expression for the C I came up with and let =ag= have a go at it. =ag "\\(.*-[^>].*\\).*&.*\\-" /usr/src/linux= yielded quite a few results. Before I ran the command, I wasn't expecting much, thinking that my regex was too permissive, but the first result I saw looked remarkably like the C expression I had come up with -- right at the beginning of =sysv_readdir= in =fs/sysv/dir.c=: #+BEGIN_SRC c static int sysv_readdir(struct file *file, struct dir_context *ctx) { unsigned long pos = ctx->pos; struct inode *inode = file_inode(file); struct super_block *sb = inode->i_sb; unsigned long npages = dir_pages(inode); unsigned offset; unsigned long n; ctx->pos = pos = (pos + SYSV_DIRSIZE-1) & ~(SYSV_DIRSIZE-1); if (pos >= inode->i_size) return 0; #+END_SRC Hm. Remember how I mentioned that I expected =neg= to correspond to a =~=? Well, jumping back to Compiler Explorer: #+BEGIN_SRC c int f(int a) { return ~a; } #+END_SRC #+BEGIN_SRC asm f(int): mov eax, edi not eax ret #+END_SRC #+BEGIN_SRC c int f(int a) { return ~(a - 1); } #+END_SRC #+BEGIN_SRC asm f(int): mov eax, edi neg eax ret #+END_SRC Modifying our search slightly to =ag "\\(.*-[^>].*\\).*&.*\\~.*\\-.*1"= yields a massive number of results, some of which are named macros. Here's one of them, in =include/uapi/linux/if_packet.h=: #+BEGIN_SRC c #define TPACKET_ALIGN(x) (((x)+TPACKET_ALIGNMENT-1)&~(TPACKET_ALIGNMENT-1)) #+END_SRC Cool. That makes me feel much more confident in my answer. * Challenge #11 The hint for this exercise is: "[t]his is a somewhat large function (in contrast to the other exercises in this blog), but heavily used nowadays in various software. As it can be clearly seen, it uses standard C/C++ functions including strlen() and sscanf(). Some other helper function is also used. I intentionally gave it this name to conceal its real function. So what does the whole code snippet do?" I'd like to apologize in advance for the sloppiness of the code that follows. Also, I've renamed =helper= to =is_hex_digit=, as it makes the code for =f= clearer. #+BEGIN_SRC c #include #include int is_hex_digit(char a) { // lea edx, [rdi-48] // mov eax, 1 // cmp edx, 9 // jbe .L2 if (a <= '9') { // .L2: // ret return 1; } // and edi, -33 // xor eax, eax // sub edi, 65 // cmp edi, 5 // setbe al // .L2: // ret return (a & -33) <= 'F' ? 1 : 0; } int f(char *a, char *b) { int len; int local_12; char *cur; char *end; char *dst; char *next; // push r15 // xor eax, eax // or rcx, -1 // push r14 // push r13 // push r12 // mov r12, rsi // push rbp // mov rbp, rsi // push rbx // mov rbx, rdi // sub rsp, 24 // repnz scasb // not rcx dst = b; cur = a; len = strlen(a); // lea r14, [rbx-1+rcx] // .L6: // cmp rbx, r14 // ja .L24 while (cur <= end) { // movsx eax, BYTE PTR [rbx] // ... // mov DWORD PTR [rsp+12], eax local_12 = (int) *cur; // lea r13, [rbx+1] next = cur + 1; // mov r15, r13 // cmp eax, 43 // jne .L7 if (*cur == '+') { // mov DWORD PTR [rsp+12], 32 local_12 = ' '; // jmp .L8 } else { // .L7: // cmp eax, 37 // jne .L8 // movsx edi, BYTE PTR [rbx+1] // call helper // test eax, eax // jne .L9 if (*cur == '%' && is_hex_digit(*(cur + 1))) { // .L9: // movsx edi, BYTE PTR [rbx+2] // lea r13, [rbx+3] next = cur + 3; // call helper // test eax, eax // je .L11 if (!is_hex_digit(*(cur + 2))) { // .L11: // or eax, -1 // jmp .L10 // .L10: // add rsp, 24 // pop rbx // pop rbp // pop r12 // pop r13 // pop r14 // pop r15 // ret return -1; } // lea rdx, [rsp+12] // xor eax, eax // mov esi, OFFSET FLAT:.LC0 // mov rdi, r15 // call __isoc99_sscanf // test eax, eax // je .L11 if (!sscanf(cur + 1, "%2X", &local_12)) { // .L11: // or eax, -1 // jmp .L10 // .L10: // add rsp, 24 // pop rbx // pop rbp // pop r12 // pop r13 // pop r14 // pop r15 // ret return -1; } } } // .L8: // test r12, r12 // je .L12 if (b != NULL) { // mov eax, DWORD PTR [rsp+12] // mov BYTE PTR [rbp+0], al *dst = local_12; } // .L12: // inc rbp // mov rbx, r13 // jmp .L6 dst++; cur = next; } // .L24: // mov eax, ebp // sub eax, r12d // .L10: // add rsp, 24 // pop rbx // pop rbp // pop r12 // pop r13 // pop r14 // pop r15 // ret return dst - b; } #+END_SRC This could very well be cleaned up. In fact, I'm not even sure that my translation is completely correct, but I got to the point where I felt it was "good enough" and I could explain that =f= is a function for decoding a [[https://en.wikipedia.org/wiki/Percent-encoding][percent-encoded]] string, where =a= is the encoded string and =b= is a destination to decode to. If not for the telltale ='+'= corresponding to a =' '= and use of a ='%'= character, I probably would have spent more time cleaning up my translation and making sense of it. But I've seen code like this many times in my life, it really is "heavily used nowadays in various software." I began this challenge by reversing =helper=, which I think was a good move as it gave me some footing. I didn't even notice '%' or '+' in =f= at first, but the realization that =helper= worked with hexadecimal digits got me started on ideas for what =f= might do. On the topic of =helper=, the reason I was able to pick out that it's checking for hexadecimal digits was realizing that \(a - 48 \leq 9\) is equivalent to \(a \leq 49 + 9\). The comparison is otherwise pretty unclear. And I suspect that the =-33= is related to how ASCII is encoded. The control flow for =f= is pretty intimidating with its 8 labels. When it came time to look at =f=, I drew out a rudimentary control flow graph on paper -- scribbling down the label names and drawing arrows between the different labels. I found this to be very useful in identifying which jumps are loops (cycles in the graph), which are conditionals (branches), and which labels are related (linear relationships). #+BEGIN_EXPORT html #+END_EXPORT #+BEGIN_EXPORT html #+END_EXPORT