12 ms·
A primer on some C obfuscation tricks
- TimSchumann 7y agoThanks for sharing this!
- freefal 7y agoWhat does the provided example do? 5) Surprising math: int x = 0xfffe+0x0001;
- moonchild 7y agoIt looks a bit like a binary exponent, but according to clang and gcc it's erroneous: 'invalid suffix '+0x0001' on integer constant'.
- monocasa 7y agoIt doesn't compile. It's trying to parse the e as scientific notation I think. surprising_math.c: In function ‘main’: surprising_math.c:4:10: error: invalid suffix "+0x0001" on integer constant int x = 0xfffe+0x0001; ^~~~~~~~~~~~~
- inetknght 7y agoIt produces an error message. error: invalid suffix '+0x0001' on integer constant [0] https://gcc.godbolt.org/z/pR5L6v https://gcc.godbolt.org/z/pR5L6v
- rightbyte 7y agoThat is really stupid. How is that not a bug? I can't belive GCC wont compile it. What does other compilers do. Edit: Clang also gives an error. Mscv seems to compile. I wonder who follows spec. I assume not mscv ...
- _kst_ 7y agoIt's not a bug. 0xfffe+0x0001 satisfies the syntax of a "preprocessing number". In a later translation phase, preprocessing numbers are converted to numeric constants. Not all valid preprocessing numbers are valid numeric constants. The syntax of preprocessing numbers is deliberately permissive to avoid complicating the implementation of the preprocessor. I'm sure it could have been made stricter, allowing 0xfffe+0x0001 to be treated as 0xfffe + 0x0001 -- but the solution is simply to write 0xfffe + 0x0001 in the first place. The language grammar has to be consistently defined even if it leads to surprises now and then.
- deleted 7y ago[deleted]
- jackhalford 7y ago> x[index] is *(x+index) > index[x] is legal C and equivalent too Can't be unseen, I can't believe I never thought of that.
- guitmz 7y agoI find that after learning assembly language, things like this become very obvious when seen in different languages, specially C
- roywiggins 7y agoThe main reason that works is historic, I think. It could just as easily throw a compilation error to index a constant with an array rather than the other way around. I don't think this works in Rust even if the resulting machine code for array indexing is the same.
- eMSF 7y agoThe main reason is that in C, "indexing" an array is purely syntactic sugar for pointer arithmetic, which itself is commutative; that is. ((A)[B]) is equivalent to ((A)+(B)), which itself is equivalent to ((B)+(A)) (assuming one of them has an integral type and the other a pointer to complete object type). Now, of course an array type isn't a pointer type, but as "indexing" isn't one of the very few cases where an expression that has an array type isn't converted to an expression with a pointer type, you aren't really indexing an array, but a pointer to its first element.
- wahern 7y agoAnother way to look at it is that C syntax was designed to be extremely simple to parse, and C semantics to simplify code generation. Early C compilers immediately generated code as they parsed each expression, keeping minimal state. (No AST!) Also consider that in B the only data type was the machine word, so the type of the operands were irrelevant to the code you generated. In early C the biggest difference was structures, which required some minimal bookkeeping (very minimal when all members were in the same namespace), but a struct dereference is just syntactic sugar for an (address + offset) expression, so underneath the covers the compiler was still just chewing through identifiers, left to right, and emitting simple assembly for addition and multiplication, because each identifier was just a symbol for an integer. So index[array] isn't an historical accident. It might not have been deliberate, but it follows naturally from the nature of the language. Go very much follows the same discipline. Speed and simplicity of compilation constrain the syntax, most notably the lack of generics. Goroutines, channels, etc, only require minimal syntactic and compiler support. Contrast that with Rust--Rust front loads everything into the parsing phase--lifetimes, async, etc. Deep AST analysis and transformation is everything for Rust. Of course these days people abhor even the possibility of allowing something like index[array], so even a compiler like Go goes out of its way to disallow it.
- inetknght 7y agoToday I learned... _magic_ #include <cstdio> void sw(int s) { switch (s) while (0) { case 0: printf("zero\n"); continue; case 1: printf("one\n"); continue; case 2: printf("two\n"); continue; } } [0] https://gcc.godbolt.org/z/Q26LWG https://gcc.godbolt.org/z/Q26LWG
- lostmsu 7y agoHuh, what does it do?
- strbean 7y agoLooking at the link, it generates the exact same assembly (and behavior of course) as void sw(int s) noexcept { switch (s) { case 0: printf("zero\n"); break; case 1: printf("one\n"); break; case 2: printf("two\n"); break; } }
- saagarjha 7y agothatsthejoke.jpg
- strbean 7y agoI mean, parent seemed an awful lot like someone asking what it did. Damn me for missing the invisible /s. I'm sure no other reader would ever be curious!
- saagarjha 7y agoIt's not the comment that is the joke, but the example in the context of the "C obfuscation tricks" article where it's just a convoluted way to write the same thing as a simple C construct.
- moonchild 7y ago({_:&&_;}); ({}); ({;}); All of these rely on a gcc extension ('statement expressions'). ________________________________ a = '-'-'-' You can also do: a = '/'/'/' To generate a 1 instead of a 0. ________________________________ printf("%d %d\n", 0 == sizeof(count = 2, count++), count); This works because: 1. There are two forms of sizeof; sizeof(T) where T is a type, and sizeof x where x is an expression. 2. There are 'comma expressions'; if you have (x, y) where x and y are expressions, then x is executed and the expression evaluates to y. 3. Parameters to sizeof are not evaluated (which is important because otherwise the value of the 3rd argument would be undefined, since there's no sequence point between evaluation of function parameters).
- saagarjha 7y ago> Parameters to sizeof are not evaluated Oh, but they are ;) Try running sizeof on a VLA sometime.
- _kst_ 7y agoThe argument to sizeof is evaluated if and only if it's of a variable length array type. (If it's a parenthesized type name, it's unclear what it means to "evaluate" it.)
- userbinator 7y agoNumber 24 on the list shows exactly this.
- sigjuice 7y ago$ cat sizeof.c #include <stdio.h> int main(int argc, char *argv[]) { printf("%zu\n", sizeof(int[argc - 2])); } $ make -B sizeof cc sizeof.c -o sizeof $ ./sizeof foo 0 $ ./sizeof 17179869180
- saagarjha 7y agoThis can yield surprising results even without undefined behavior. For example: https://godbolt.org/z/fwhLH4 https://godbolt.org/z/fwhLH4
- faehnrich 7y agoThat index[x] was used in one of my favorite IOCCC one-liners. http://faehnri.ch/have-fun/ http://faehnri.ch/have-fun/
- saagarjha 7y agoIs it bad that I've written some of these in my normal code? In particular, I use the concepts in these when they're relevant: *(c ? &x : &y) = v; return (char *[]){"No", "Yes"}[!!x]; (Note: for the second one, I use the "selecting from a temporary array" and !! separately; if I only had two options then I'd use x ? "Yes" : "No" of course.)
- bear8642 7y ago>return (char *[]){"No", "Yes"}[!!x]; Is !!x equivalent to x here?
- saagarjha 7y agoNo, it "collapses" an integer to 0 or 1. (Essentially, it's x != 0.)
- asveikau 7y agoPersonally I prefer (x ? 1 : 0) over (x != 0) or !!x for this sort of thing. It fits semantically with "I'm using x as a boolean, but it may not be 1".
- uryga 7y agoi'm on the fence about this... i don't love type coercions, but (!!x) is nice because you don't have to stop and check - was it (x ? 1 : 0) or (x ? 0 : 1), (x !=0) or (x==0)? i think arthur whitney said something in the vein of "less characters - less room for typos" :)
- deleted 7y ago[deleted]
- gallier2 7y ago!!x is idiomatic. (bool) cast is modern. I use !=0 in only one context, if(strcmp(a,b) != 0) to check if two strings differ. Normally if an if() expression I don't check explicitly for 0 or not 0, except for strcmp class of functions, because of the ordered comparison of it (<0, 0, >0) , its "boolean" value is inverted, so somewhat counter intuitive if used with implicit boolean values.
- fortran77 7y agoI wonder when there'll be the Obfuscated Rust Programming Contest.
- BubRoss 7y agoThey might need an obsfuscated compilation contest.
- steveklabnik 7y agoWe held an “Underhanded Rust” contest, which is similar but different, a few years back.
- gallier2 7y agoNo need for that, any Rust program is obfuscated.
- m463 7y agohow about this one? int main() { fork(); printf("choo"); }
- saagarjha 7y agoWhat's the trick (besides calling functions after fork that you shouldn't be calling?)
- oddlama 7y agoI think parent wanted to write the following: int main() { printf("choo"); fork(); } Here, "choo" can be printed twice, even though we fork after printing. This is a result from line buffering when the flush happens after the process is forked. Essentially, the output buffer is copied when forking and therefore duplicated.
- deleted 7y ago[deleted]
- m463 7y agooh yes, that's what I meant! your example and mine print the same output.
- nneonneo 7y agoI publicly reverse-engineered a cute bit of C code many years ago that employs some of these tricks: https://stackoverflow.com/questions/15393441/obfuscated-c-code-contest-2006-please-explain-sykes2-c https://stackoverflow.com/questions/15393441/obfuscated-c-co... It's a digital clock (which has to be compiled and run once per second to be accurate). Of course, if you're interested in obfuscated C, you can't miss the International Obfuscated C Code Contest, which is where most of these evil tricks show up. Submissions for this year's IOCCC are still open: https://www.ioccc.org/ https://www.ioccc.org/ The IOCCC has been running since 1984, and there are some absolutely marvelous gems: https://www.ioccc.org/years.html https://www.ioccc.org/years.html. A great rabbit-hole to dive down if you're stuck at home ;)
- _kst_ 7y ago> or since int is default type Not since the 1999 ISO standard. > int main(){ return linux > unix; } A conforming compiler must diagnose "linux" and "unix" as undeclared identifiers. Many C compilers are not conforming by default.
- ben0x539 7y agoI suspect the post is about GNU C rather than ISO C.
- Nevermark 7y agoI keep my code interesting, to make work more enjoyable for future maintainers, by keeping all my code ... inline with_all_ColinIanKing_standards()
- waltpad 7y agoThat probably shows (once more) that the C language really needs an overhaul, with a stricter grammar disallowing that sort of tricks. Or maybe not.
- platinumrad 7y agoThis is a valid Rust program: fn main() { return return return return return return return } You can do silly things in any language.
- waltpad 7y agoIndeed, anyone can also write code over-abusing lambdas in a functional language, but lambdas are also quite useful in the majority of cases. On the other hand, can you point out a single situation where swapping int and typedef in a type definition brings anything good? Likewise, in your example, I doubt that this could bring anything. Is there a practical obfuscation method based on that quirk in Rust, or a reason for keeping it in the syntax? I cannot tell, but maybe you can? Overall, the problem that I have with this is not that it is silly, it is that it makes it harder to understand and maintain. In some cases, it requires active engineering to fix the issue, but the language should be designed so that most of these problems are taken care of by design. Also, many people seem to enjoy the fact the C can be bent that way. I don't mean to remove that from them, I just think that for system programming, it should be less permissive. Perhaps a 'strict mode' could be devised, not at the syntax level like in javascript (which I suppose couldn't be avoided), but as compiler flag (like the c++ people did it).
- kccqzy 7y agoOne guy's useless tricks can be another's essential flexibility. Especially when you consider code generation.
- waltpad 7y ago> Especially when you consider code generation. That's a very good point you are making here. However, as I said elsewhere, I don't see the use of being able to either write typedef int ... or int typedef ... and I don't think that proper code generation would require it.
- ramshorns 7y agoHey, I've got one! Use the exponent operator. if (2^3 == 8) puts("two cubed is eight"); if (5^2 == 25) puts("five squared is twenty-five");
- bear8642 7y agoThat's Bitwise Xor not exponentiation. 2^3 => 1 5^2 => 7
- xnhbx 7y agoFrom bulletin #4: "-2147483648 is positive. This is because 2147483648 cannot fit in the type int, so (following the ISO C rules) its data type is unsigned long int. Negating this value yields 2147483648 again." This is not true. If a decimal integer constant value cannot be represented in type "int", the next candidate type is "long int". If the value cannot fit in "long int" either, the next type to try is "long long int" in C99 and "unsigned long int" only in C89.
- dvfjsdhgfv 7y agoI like if (val && ~val) as an alternative to if(val) based on the fact that for non-zero ints ~val will also be non-zero.
- Sharlin 7y ago> return (char *[]){"No", "Yes"}[!!x]; I prefer return (!x<<2)+"Yes\0No";