Even-Odd *(Cursed version)*


Introduction

I was doomscrolling on Twitter1 and found a tweet about the interesting way about determining wheter the given number is even or odd.

Most well-known example (of course, for fun) is:

def is_odd(n):
    match n:
        case     1 |  3 |  5 |  7 |  9 | 11 | 13 | 15 | 17 | 19 | \
                21 | 23 | 25 | 27 | 29 | 31 | 33 | 35 | 37 | 39 | \
                41 | 43 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | \
                61 | 63 | 65 | 67 | 69 | 71 | 73 | 75 | 77 | 79 | \
                81 | 83 | 85 | 87 | 89 | 91 | 93 | 95 | 97 | 99:
            return True
        case _:
            return False

There were several brilliant ways to determine odd or even, so I decided to commit this one I came across.

Codes

I need 2 codes (evenodd.c, evenoddstack.c) to determine wheter the given number is even or odd.

evenodd.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>

int main()
{
    char number[0x10];
    pid_t pid;
    int status;

    scanf("%s", number);

    char *argv_list[3] = {"evenoddstack.out", (char *)number, NULL};

    pid = fork();

    if (pid == 0)
    {
        execv("evenoddstack.out", argv_list);
    }
    else
    {
        if (waitpid(pid, &status, 0) > 0)
        {
            if (status != 0)
            {
                printf("Odd\n");
            }
        }
    }
    return 0;
}

evenoddstack.c

#include <stdio.h>
#include <immintrin.h>

int main(int argc, char *argv[])
{
    __uint64_t x = atoi(argv[1]);

    asm(
        "and rsp, 0xfffffffffffffff0    \n"
        "mov rcx, QWORD PTR %[val]      \n"
        "compare:                       \n"
        "test rcx, rcx                  \n"
        "jz lab                         \n"
        "push rcx                       \n"
        "dec rcx                        \n"
        "jmp compare                    \n"
        "lab:                           \n"
        "movaps xmmword ptr [rsp], xmm0 \n"
        :
        : [val] "m"(x)
        : "rcx", "xmm0");

    printf("Even\n");
    return 0;
}

This requires AVX support for the processor. Determining odd or even requries quite modern CPU architecture

Build

gcc -o evenoddstack.out evenoddstack.c -masm=intel
gcc -o evenodd.out evenodd.c

Execution

Run the evenodd.out with the number from 1 to 100. (any unsigned integer is allowed)

$ for i in `seq 100`;                                                                                                                                                                                                          02:09:42 Mar 14
do
echo -n $i ' '
./evenodd.out <<< $i
done

1  Odd
2  Even
3  Odd
4  Even
5  Odd
6  Even
7  Odd
8  Even
9  Odd
10  Even
11  Odd
12  Even
13  Odd
14  Even
15  Odd
16  Even
17  Odd
18  Even
19  Odd
20  Even
21  Odd
22  Even
23  Odd
24  Even
25  Odd
26  Even
27  Odd
28  Even
29  Odd
30  Even
31  Odd
32  Even
33  Odd
34  Even
35  Odd
36  Even
37  Odd
38  Even
39  Odd
40  Even
41  Odd
42  Even
43  Odd
44  Even
45  Odd
46  Even
47  Odd
48  Even
49  Odd
50  Even
51  Odd
52  Even
53  Odd
54  Even
55  Odd
56  Even
57  Odd
58  Even
59  Odd
60  Even
61  Odd
62  Even
63  Odd
64  Even
65  Odd
66  Even
67  Odd
68  Even
69  Odd
70  Even
71  Odd
72  Even
73  Odd
74  Even
75  Odd
76  Even
77  Odd
78  Even
79  Odd
80  Even
81  Odd
82  Even
83  Odd
84  Even
85  Odd
86  Even
87  Odd
88  Even
89  Odd
90  Even
91  Odd
92  Even
93  Odd
94  Even
95  Odd
96  Even
97  Odd
98  Even
99  Odd
100  Even

Explanation

Why do this code work? Think case-by-case.

Even case

Parent process: evenodd.out

  1. Inside of evenodd.out, the instruction execv("evenoddstack.out", argv_list); executed. evenoddstack.out with the given number as an argument.

Child process: evenoddstack.out

  1. Inside of evenoddstack.out, the given number string is parsed and stored into 64-bit unsigned integer x.
  2. Run the inline assembly code.
    • Aligns the stack pointer to 16-byte boundary.
    • Looping through the assembly, stacking the number from the given number to 1.
    • After the loop, store the xmm0 register to the stack.
  3. Print "Even" and return 0.

(Child process terminated with status 0)

  1. Parent process terminates.

Odd case

Parent process: evenodd.out

  1. Inside of evenodd.out, the instruction execv("evenoddstack.out", argv_list); executed. evenoddstack.out with the given number as an argument.

Child process: evenoddstack.out

  1. Inside of evenoddstack.out, the given number string is parsed and stored into 64-bit unsigned integer x.
  2. Run the inline assembly code.
    • Aligns the stack pointer to 16-byte boundary.
    • Looping through the assembly, stacking the number from the given number to 1.
    • After the loop, store the xmm0 register to the stack. Raises a segmentation fault!

(Child process terminated with non-zero status)

  1. Parent process prints "Odd".
  2. Parent process terminates.

Deep dive into assembly and AVX

Exploiting an x64 system with ROP (Return Oriented Programming) is simple - just chaining the gadgets to jump to the desired function. However, if the desired function is system-related something, We should align the stack pointer to 16-byte boundary. (To satisfy this, additional ret gadget for 8-byte excessive padding is sometimes put.)

Seperately run the evenoddstack.out and check the stack pointer alignment to check the stack pointer alignment.

$ ./evenoddstack.out <<< 33

$rsp = 0x7fffffffe248

Program stops at the main+0x4e, reason is SIGSEGV. AVX-extension (like movaps in this case) requires the stack pointer to be aligned to 16-byte boundary, but the odd number pushes number (2n+1)(2n + 1) times, final $rsp would be decrease (16n+8)(16n + 8) bytes. So the stack pointer would never aligned to 16-byte boundary, raising a segmentation fault.

$ ./evenoddstack.out <<< 34

$rsp = 0x7fffffffe240

To compare the case between odd and even, I set the breakpoint at the main+0x4e and the check the stack pointer alignment. Even number pushes number 2n2n times, final $rsp would be decrease 16n16n bytes. So the stack pointer would be aligned to 16-byte boundary, and the rest of the program would be executed without any error.

Conclustion

Nobody would use this code in the real world, but we can generalize it by using ymm(256-bits) and zmm(512-bits) to determine the given number is multiplier of 4 or 8.

Signal-driven programming using property of AVX was selected from the original author, and I’d mentioned from them!

In the original thread, there are more brilliant ways so I recommend to check the original tweet threads!

Footnotes

  1. Formerly Twitter - now X; who cares?