1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
|
#+TITLE: Writeups for Dennis Yurichev's Reverse Engineering Challenges (#36-#74)
#+DATE: <2019-12-29 Sun 19:55>
#+TAGS: writeup reverse-engineering x86
This is the fourth and final set of for my self-imposed challenge of completing
at least fifty of the exercises on Dennis Yurichev's [[https://challenges.re][challenges.re]] by the end of
the year. The previous set is available [[http:///jakob.space/challenges-re-writeups-3.html][here]].
We'll actually be covering twenty challenges in this one. I'd been so busy with
school that I forgot to make a post when I hit forty.
* Challenge #36
The description this time describes that this is "[a] well-known algorithm
again. What does it do? Also, take notice that the code for x86 uses FPU, but
SIMD instructions are used instead in the x64 code. That's OK."
#+BEGIN_SRC c
long state = 0x12345678;
float f1(void)
{
state = state * 0x19660d + 0x3c6ef35f;
return ((float) ((state & 0x7fffff) | 0x40000000)) - 3.0f;
}
void f(void)
{
int i;
int count;
float a;
float b;
for (i = 0, count = 0; i < 1000000; i++) {
a = f1();
b = f1();
if (a * a + b * b > 1.0f) {
count++
}
}
((float) (((double) count) * 2.25) / 10.9073486328125);
}
#+END_SRC
I thought this was a lame challenge. The floating point operations of =f1= have
been optimized to the point that it's unrecognizable, so if you aren't familiar
with the standard bit-twiddling tricks that GCC uses to speed up floating-point
operations, you aren't going to be able to come up with anything meaningful --
especially since neither function take parameters. My response? =f= returns the
constant value =206282.937500=.
I thought this might be the [[https://en.wikipedia.org/wiki/Fast_inverse_square_root][fast inverse square root]], but I don't believe it is.
* Challenge #37
Ah, another challenge for which the description is that it is a "[w]ell-known
function" and only x86 disassembly given is from MSVC. Fortunately, this one is
not too difficult.
#+BEGIN_SRC c
int f(int a, int b)
{
int i;
int n;
if (a == 0) {
return b + 1;
}
n = b;
i = a;
do {
if (n == 0) {
n = 1;
} else {
n = f(i, n - 1);
}
} while (--i != 0);
return n + 1;
}
#+END_SRC
This is the Ackermann function, albeit using a loop rather than a direct
translation of the Ackermann–Péter function to code.
To answer Yurichev's additional questions, a stack overflow occurs if 4 and 2
are supplied as input because those are [[https://www.wolframalpha.com/input/?i=Ackermann(4,2)][absurd parameters for this function]], and
this function bears the error of not enforcing the constraints given in the
definition of the Ackermann–Péter function.
* Challenge #38
Fun. Another challenge provided as a binary.
#+BEGIN_SRC prog
jakob@Upsilon ~ $ sha256sum 17
8f73f329e0988968a9fa40f61da906e83b46817bcb5c0e93f7e95aa74c30e8e0 17
jakob@Upsilon ~ $ file 17
17: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.18, BuildID[sha1]=bdeac54f2d98db38d7a63a43f1c41857432686c4, stripped
#+END_SRC
I stopped a bit early because, for once, the question wasn't "[w]hat does this
code do?", but was instead, "[t]his program prints some information to stdout,
each time different. What is that?"
#+BEGIN_SRC c
#include <stdlib.h>
#include <time.h>
static time_t current_time;
int fcn.004006c4(void)
{
return current_time = current_time * 0x19660d * 0x3c6ef35f;
}
int main(int argc, char **argv)
{
char *s2;
void **s1;
int var_1ch;
int var_18h;
int var_11h;
s2 = malloc(230);
s1 = malloc(80);
var_1ch = 0;
while (var_1ch <= 9) {
s1[var_1ch++] = calloc(230, 1);
}
current_time = time(NULL);
var_1ch = 0;
while (var_1ch <= 230) {
var_11h = fcn.00400b60(fcn.004006c4());
s2[var_1ch++] = var_11h & 1;
}
var_1ch = 0;
while (1) {
fcn.00400970(s2, 230);
fcn.0040072a(s2, 230, 110);
var_18h = 0;
while (var_18h <= 8) {
if (!memcmp(s1[var_18h++], s2, 230)) {
exit(0);
}
}
var_18h = 0;
while (var_18h <= 8) {
memcpy(s1[var_18h], s1[++var_18h], 230);
}
memcpy(s1[9], s2, 230);
var_1ch++;
}
}
#+END_SRC
The only nondeterminism I saw in the disassembly was from =time=. The actual
output of the program is incomprehensible -- appearing to be some sort of
ASCII-art fractal. For this reason, I'm concluding that the information printed
to =stdout= is the current time.
* Challenge #39
The description for this challenge got me excited. "This program requires a
password. Find it."
#+BEGIN_SRC prog
jakob@Upsilon ~ $ sha256sum password2
8c8365f316de896c453511c5f484755600208b87ad0f1595a2900cbf5a36db24 password2
#+END_SRC
=main= is simple enough that I feel I can omit the: it reads in a password with
=scanf=, and then checks it with the following snippet.
#+BEGIN_SRC prog
│ 0x0804853e e87affffff calll fcn.080484bd
│ 0x08048543 3df8010000 cmpl $0x1f8, %eax ; 504
#+END_SRC
We want to find some =password= such that =fcn.080484bd(password) = 0x1f8=.
Peeking into =fcn.080484bd=, I was a little disappointed.
#+BEGIN_SRC prog
┌ (fcn) fcn.080484bd 46
│ fcn.080484bd (int32_t arg_8h);
│ ; var int32_t var_4h @ ebp-0x4
│ ; arg int32_t arg_8h @ ebp+0x8
│ ; CALL XREF from main @ 0x804853e
│ 0x080484bd 55 pushl %ebp
│ 0x080484be 89e5 movl %esp, %ebp
│ 0x080484c0 83ec10 subl $0x10, %esp
│ 0x080484c3 c745fc000000. movl $0, var_4h
│ ┌─< 0x080484ca eb10 jmp 0x80484dc
│ │ ; CODE XREF from fcn.080484bd @ 0x80484e4
│ ┌──> 0x080484cc 8b4508 movl arg_8h, %eax ; [0x8:4]=-1 ; 8 ; edx
│ ╎│ 0x080484cf 0fb600 movzbl 0(%eax), %eax
│ ╎│ 0x080484d2 0fbec0 movsbl %al, %eax
│ ╎│ 0x080484d5 0145fc addl %eax, var_4h
│ ╎│ 0x080484d8 83450801 addl $1, arg_8h ; [0x8:4]=-1 ; 1
│ ╎│ ; CODE XREF from fcn.080484bd @ 0x80484ca
│ ╎└─> 0x080484dc 8b4508 movl arg_8h, %eax ; [0x8:4]=-1 ; 8 ; edx
│ ╎ 0x080484df 0fb600 movzbl 0(%eax), %eax
│ ╎ 0x080484e2 84c0 testb %al, %al
│ └──< 0x080484e4 75e6 jne 0x80484cc ; likely
│ 0x080484e6 8b45fc movl var_4h, %eax ; edx
│ 0x080484e9 c9 leave ; edx
└ 0x080484ea c3 retl ; edx
#+END_SRC
Do I even need to provide a C translation? The disassembly should be glaringly
obvious: this "check" function just returns the sum of the string argument's
individual bytes. Coming up with a valid password is trivial.
#+BEGIN_SRC prog
jakob@Upsilon ~ $ ./password2
enter password:
AAAAAAA1
password is correct
#+END_SRC
The problem also suggests that I "try to change the password by patching the
executable file," but this doesn't invokve anything more than changing the word
at =0x08048544=.
* Challenge #41
The question this time is: "[t]his program prints some numbers to stdout. What
is it?"
#+BEGIN_SRC prog
jakob@Upsilon ~ $ file problem
problem: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=a89ecf1ae2f2474548d09ca3ebccd7db4162fa1e, stripped
jakob@Upsilon ~ $ sha256sum problem
ab3864e8fceeffe4b166cb7481332e88a1fe90b6a406e635c6921119c91a00fd problem
#+END_SRC
I wrote a C translation without running the program. In retrospect, this wasn't
a bad idea. The calculation is a function of some integer, but the binary spits
out subsequent numbers with no sort of delimitation. Having the C translation
means that I could add a =printf("\n");= after the call to =fcn_00400536(var_4h++);=
and get output similar to the following:
#+BEGIN_SRC prog
jakob@Upsilon ~ $ /tmp/test
1
2
1
3
10
5
16
8
4
2
1
4
2
1
5
16
8
4
2
1
#+END_SRC
Here's the C translation.
#+BEGIN_SRC c
void fcn_00400536(int a)
{
printf("%d\n", a);
while (a != 1) {
if (a & 1 != 0) {
a = a * 3 + 1;
} else {
a >>= 1;
}
printf("%d\n", a);
}
}
int main(int argc, char **argv)
{
int var_4h;
var_4h = 1;
while (var_4h <= 15) {
fcn_00400536(var_4h++);
}
return var_4h;
}
#+END_SRC
We can pick any interesting sequence and plug it into [[https://oeis.org/search?q=3%2C10%2C5%2C16%2C8%2C4%2C2%2C1&language=english&go=Search][OEIS]], which identifies
=fcn_00400536= as "A070165: Irregular triangle read by rows giving trajectory of n
in Collatz problem." Ah, yes. This is looking familiar now. This is the famously
unsolved problem in mathematics, the [[https://en.wikipedia.org/wiki/Collatz_conjecture][Collatz conjecture]].
* Challenge #43
#+BEGIN_SRC prog
jakob@Upsilon ~ $ file unknown_utility_2_3
unknown_utility_2_3: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=cb74037dd37694879f6250bfb5623c273ef68ca6, stripped
jakob@Upsilon ~ $ sha256sum unknown_utility_2_3
9d3df3be78f21971059ba2d9973a1358865ccbe2f47f78fc5800779d6f6083fe unknown_utility_2_3
#+END_SRC
I really wasn't feeling it today, so I took the easy way out and just ran the
binary provided on a test file. It spits out a floating point number, which
seems to decrease as the file becomes less interesting. Just a hunch, but maybe
it's binary entropy?
#+BEGIN_SRC prog
jakob@Upsilon ~ $ python -c "print('a' * 256)" > test.txt
jakob@Upsilon ~ $ ./unknown_utility_2_3 test.txt
0.036753
jakob@Upsilon ~ $ rahash2 -a entropy test.txt
test.txt: 0x00000000-0x00000100 entropy: 0.03675295
jakob@Upsilon ~ $ dd bs=256 count=1 if=/dev/urandom > test.txt
1+0 records in
1+0 records out
256 bytes copied, 7.0438e-05 s, 3.6 MB/s
jakob@Upsilon ~ $ ./unknown_utility_2_3 test.txt
7.069718
jakob@Upsilon ~ $ rahash2 -a entropy test.txt
test.txt: 0x00000000-0x000000ff entropy: 7.06971784
#+END_SRC
Well, that's an answer I'm certainly happy with.
* Challenge #48
It looks like we're starting to get into the realm of =win32=. The question for
this challenge is, "[w]hat does this win32-function do?"
#+BEGIN_SRC asm
main:
push 0xFFFFFFFF
call MessageBeep
xor eax,eax
retn
#+END_SRC
This is pretty simple. It's a wrapper for =MessageBeep=. According to [[https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebeep][MSDN]], the
=0xFFFFFFFF= parameter produces "[a] simple beep. If the sound card is not
available, the sound is generated using the speaker."
* Challenge #49
Another rather simple one. The disassembly for this challenge is given in AT&T
syntax, which is my preferred way of reading x86 assembly.
#+BEGIN_SRC asm
main:
pushq %rbp
movq %rsp, %rbp
movl $2, %edi
call sleep
popq %rbp
ret
#+END_SRC
A wrapper around =sleep=, presumably the only provided by =unistd.h=, calling it
with an argument of two seconds.
* Challenge #52
Another simple disassembly:
#+BEGIN_SRC asm
$SG3103 DB '%d', 0aH, 00H
_main PROC
push 0
call DWORD PTR __imp___time64
push edx
push eax
push OFFSET $SG3103 ; '%d'
call DWORD PTR __imp__printf
add esp, 16
xor eax, eax
ret 0
_main ENDP
#+END_SRC
To copy straight from [[https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/time-time32-time64?view=vs-2019][MSDN]], this prints the number of "seconds elapsed since
midnight (00:00:00), January 1, 1970, Coordinated Universal Time (UTC)."
MSDN also answers the follow-up question: "=time= is a wrapper for =_time64= and
=time_t= is, by default, equivalent to =__time64_t=. If you need to force the
compiler to interpret =time_t= as the old 32-bit =time_t=, you can define
=_USE_32BIT_TIME_T=. This is not recommended because your application may fail
after January 18, 2038; the use of this macro is not allowed on 64-bit
platforms."
* Challenge #53
I thought this was an interesting challenge. "This code, compiled in Linux
x86-64 using GCC is crashing while execution (segmentation fault). It's also
crashed if compiled by MinGW for win32. However, it works in Windows environment
if compiled by MSVC 2010 x86. Why?"
#+BEGIN_SRC c
#include <string.h>
#include <stdio.h>
void alter_string(char *s)
{
strcpy (s, "Goodbye!");
printf ("Result: %s\n", s);
};
int main()
{
alter_string ("Hello, world!\n");
};
#+END_SRC
The code is modifying a string constant, which GCC tends to put in a read-only
memory segment (=.rodata=) in the resultant executable. Writing to a read-only
memory segment will cause a segmentation fault. I haven't tested it, but the
question statement makes me think that MSVC puts string constants in a writable
segment, so this would work just fine.
* Challenge #54
No disassembly is given for this challenge. The only thing on the page is "[w]hy
isn't the x86 LOOP instruction used by modern compilers anymore?" Some searching
yields [[https://stackoverflow.com/questions/35742570/why-is-the-loop-instruction-slow-couldnt-intel-have-implemented-it-efficiently/35743699#35743699][this Stack Overflow answer]]. Basically, =loop= is from the time before x86
became horribly complex, and so on modern processors, it's slow.
* Challenge #56
I decided to skip challenge #55, as it would really just be a walkthrough of
which r2 commands I used. Challenge #56 is not particularly difficult. I went
along with the disassembly from MSVC.
#+BEGIN_SRC c
#include <stdio.h>
int main(void)
{
int n;
n = 100;
do {
printf("%d", n);
} while (n-- != 0);
return 0;
}
#+END_SRC
The code prints the integers from 100 to 0.
* Challenge #57
This is almost the same disassembly as last time.
#+BEGIN_SRC c
#include <stdio.h>
int main(void)
{
int n;
n = 1;
do {
printf("%d", n);
n += 3;
} while (n < 100);
return 0;
}
#+END_SRC
* Challenge #58
This time, we're fortunate enough to be given a disassembly from GCC 4.8.1,
albeit with =-O3=.
#+BEGIN_SRC c
int f(char *a)
{
int count;
count = 0;
while (*a != '\0') {
if (*a++ == ' ') {
count++;
}
}
return count;
}
#+END_SRC
=f= counts the number of spaces in a given string. As an aside, when I was first
learning to read assembly, I recall someone describing =-O3= as "unintelligible to
humans." The more reverse engineering I've done, the more I've realized that the
optimizations at that level tend to not be as absurd as people make them out to
be. I considered this to be an easy challenge.
* Challenge #59
This one was /really/ easy.
#+BEGIN_SRC asm
_a$ = 8
_f PROC
mov ecx, DWORD PTR _a$[esp-4]
lea eax, DWORD PTR [ecx*8]
sub eax, ecx
ret 0
_f ENDP
#+END_SRC
The function just returns =a * 7=. I suspect the multiplication followed by
subtraction was an optimization, since multiplication by a power of two can be
implemented as a left shift.
* Challenge #61
Perhaps the most difficult part of this challenge was going out my way to [[https://float.exposed/0x4014000000000000][ensure
that the constant really was 5.0]].
#+BEGIN_SRC c
double f(double a, double b, double c, double d, double e)
{
return (a + b + c + d + e) / 5;
}
#+END_SRC
=f= simply averages five numbers.
* Challenge #62
The challenge notes that the compiler was optimizing for space, which may
explain the pointless nested loop.
#+BEGIN_SRC c
void f(float *a, float *b, float *c)
{
int i;
int j;
long coffee;
long cake;
coffee = a - b;
cake = c - b;
for (i = 200; i > 0; i--) {
for (j = 100; j > 0; j--) {
b[cake] = b[0] + b[coffee];
b += 8;
}
}
}
#+END_SRC
=f= adds 20000 elements from =a= and =b=, storing their sums in =c=.
* Challenge #64
I was swamped with preparing for finals this weekend, so I decided to skip
challenge #63 in favor of something less arduous. The question for this one is,
"[a]n array of array[x][y] form is accessed here. Try to determine the
dimensions of the array, at least partially, by finding y."
#+BEGIN_SRC c
double f(double *array, int x, int y)
{
return array[y + x * 15];
}
#+END_SRC
The array has some number of rows each containing 15 elements.
* Challenge #65
The question here is the same as the previous challenge.
#+BEGIN_SRC c
int f(int *array, int x, int y, int z)
{
return array[z + 5 * 16 * (y + 4 * 15 * x)];
}
#+END_SRC
Assuming an array of integers, the dimensions of the array are 15 x 20 x ...
* Challenge #74
I skipped way ahead this time because I was done with finals and knew that this
was the last of the challenges I'd be doing this year. So I looked through what
remained in search of something difficult, but interesting, and settled on this
one.
We're given a binary,
#+BEGIN_SRC prog
jakob@Epsilon ~ $ sha256sum challenge74
6d2ac11d1e6200d6a2cca988189764b6acdb7811d24619e8e66f1796c8c27394 challenge74
jakob@Epsilon ~ $ file challenge74
challenge74: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=7fa3bd4aa738bced5aaccb161090818646e07704, stripped
#+END_SRC
as well as a few valid keys.
#+BEGIN_SRC prog
2Z7A7-EK270-TMHR4-BHC71-CEB52-HELL0-HELL0-EONP9
2Z7A7-6I7R9-MZGO9-FDQJ3-JN0Q6-HELL0-HELL0-72KJ9
#+END_SRC
I took this as an opportunity to try out the [[https://github.com/radareorg/r2ghidra-dec][r2ghidra-dec]] plugin for Radare.
Let's see how it does.
#+BEGIN_SRC c
// WARNING: Could not reconcile some variable overlaps
// WARNING: [r2ghidra] Detected overlap for variable var_20h// WARNING: [r2ghidra] Failed to match type signed int64_t for variable var_10h to Decompiler type: Unknown type
// identifier signed
// WARNING: [r2ghidra] Detected overlap for variable var_ch
// WARNING: [r2ghidra] Failed to match type signed int64_t for variable var_8h to Decompiler type: Unknown type
// identifier signed
// WARNING: [r2ghidra] Detected overlap for variable var_8h
// WARNING: [r2ghidra] Failed to match type signed int for variable var_4h to Decompiler type: Unknown type identifier
// signed
// WARNING: [r2ghidra] Detected overlap for variable var_4h
// WARNING: [r2ghidra] Detected overlap for variable var_bh
undefined8 main(uint32_t argc, char **argv)
{
int64_t iVar1;
char cVar2;
int32_t iVar3;
int64_t in_FS_OFFSET;
int64_t var_30h;
int64_t var_24h;
int64_t var_8h;
iVar1 = *(int64_t *)(in_FS_OFFSET + 0x28);
var_24h._0_4_ = argc;
sym.imp.puts("Crackme/keygenme by Dennis Yurichev, http://challenges.re/74");
sym.imp.putchar(10);
if ((uint32_t)var_24h == 1) {
sym.imp.puts("Command line: <serial number>");
// WARNING: Subroutine does not return
sym.imp.exit(0);
}
iVar3 = sym.imp.memcmp(argv[1] + 0x1e, "HELL0-HELL0", 0xb);
if (iVar3 != 0) {
sym.imp.puts("SN format is incorrect");
// WARNING: Subroutine does not return
sym.imp.exit(0);
}
iVar3 = fcn.00400bb5((int64_t)argv[1], (int64_t)&var_24h + 4);
if (iVar3 == -1) {
sym.imp.puts("SN format is incorrect");
// WARNING: Subroutine does not return
sym.imp.exit(0);
}
cVar2 = fcn.0040085e((void *)((int64_t)&var_24h + 4));
if (cVar2 == '\0') {
sym.imp.puts("SN is not valid");
} else {
sym.imp.puts("SN valid");
}
if (iVar1 != *(int64_t *)(in_FS_OFFSET + 0x28)) {
// WARNING: Subroutine does not return
sym.imp.__stack_chk_fail();
}
return 0;
}
#+END_SRC
When I saw this, I was blown away. Damn. The NSA did a great job with this.
Here's my cleaned up version.
#+BEGIN_SRC c
#include <stdint.h>
int main(int argc, char **argv)
{
char buf[24];
printf("Crackme/keygenme by Dennis Yurichev, http://challenges.re/74\n\n");
if (argc == 1) {
puts("Command line: <serial number>");
exit(0);
}
if (memcmp(argv[1] + 0x1e, "HELL0-HELL0", 0xb)) {
puts("SN format is incorrect");
exit(0);
}
if (fcn_00400bb5(argv[1], buf) == -1) {
puts("SN format is incorrect");
exit(0);
}
if (fcn_0040085e(buf) == '\0') {
puts("SN is not valid");
} else {
puts("SN valid");
}
return 0;
}
#+END_SRC
Aside from getting rid of temporary variables, I removed =iVar1= as it's no more
than a stack canary, and I fixed up a few "errors" that the decompiler made. As
an example, notice that strange assignment to =var_24h._0_4_=? Let's see what the
disassembly says.
#+BEGIN_SRC asm
pushq %rbp
movq %rsp, %rbp
subq $0x30, %rsp
movl %edi, var_24h ; argc
movq %rsi, var_30h ; argv
#+END_SRC
This is the only write to =var_24h=, so the line should have been =var_24h = argv=.
For some reason, the decompiler saw this as assignment to a =struct= field. I
ended up removing it anyway. Using 'argc' is clearer.
There's also that odd =(void *)((int64_t)&var_24h + 4)=, but if we look at the
disassembly,
#+BEGIN_SRC asm
leaq var_20h, %rdx
movq %rdx, %rsi
movq %rax, %rdi
callq fcn.00400bb5
...
leaq var_20h, %rax
movq %rax, %rdi
callq fcn.0040085e
#+END_SRC
So that should've just been =var_20h= in the decompilation. Regardless, I'm
impressed. And I have to say, as a plugin, r2ghidra is really good. =pdg= takes a
few seconds, but if you rename some variables with =afvn= and run it again, it
spits out the updated version instantly, which makes me think that it's probably
doing some sort of caching and quick substitution.
Anyway, back to the challenge. We can tell from the decompilation already that
the sixth and seventh components must be "HELL0-HELL0". It also has to contain
eight components, delimited by '-', as we can see from =fcn_00400bb5=:
#+BEGIN_SRC c
var_10h._0_4_ = 0;
while ((int32_t)var_10h < 7) {
if (*(char *)(arg1 + (int64_t)((int32_t)var_10h * 6) + 5) != '-') {
return 0xffffffff;
}
var_10h._0_4_ = (int32_t)var_10h + 1;
}
#+END_SRC
Here's the gist of the key verification algorithm:
- =fcn_00400bb5= parses the key into a buffer (I renamed this to =parse_key=)
- Calls out to a =charcode= function which maps numerals to their numeric values
('0' becomes 0) and letters 'A' through 'Z' to 10-35.
- The buffer is written with 3 bytes per component. I thought this was a
decompiler mistake at first, but I checked the disassembly and it really is
3 bytes per component. 24 bytes total.
- =fcn_0040085e= does further verification and enables features based on the
parsed key (I renamed this to =enable_features=).
- The resultant parsed buffer needs to start with 0xdeadbabe.
- The 4th and 5th bytes give a numerical year, the 6th a numerical month, and
the 7th a numerical day.
- There's a sanity checks to ensure that the day is between 1 and 31, that
the month is between 1 and 12, and that the year is between 2016 and 2101.
- The 7th bit of byte 8 enables feature A
- The 1st bit of byte 9 enables feature B
- The 2nd bit of byte 10 enables feature C
- The 4th bit of byte 11 enables feature D
- The 1st bit of byte 12 enables feature E
- There's one final check of the last 8 bytes against a =checksum= function.
#+BEGIN_SRC c
#include <stdint.h>
uint64_t checksum(int64_t init, char *parsed, int64_t length)
{
uint64_t ret;
char *cur;
int i;
int j;
cur = parsed;
ret = ~init;
i = length;
while (i != 0) {
ret = ret ^ (uint64_t) *cur;
j = 0;
while (i--, cur++, j < 8) {
if ((ret & 1) == 0) {
ret = ret >> 1;
} else {
ret = ret >> 1 ^ 0x42f0e1eb0badbad0;
}
j++;
}
}
return ~ret;
}
#+END_SRC
I actually didn't realize that last part until I'd already hacked together a
quick key verifier.
#+BEGIN_SRC common-lisp
(use-package :cl-utilities)
(defun charcode (c)
(let ((value (char-code c)))
(cond ((<= (char-code #\0) value (char-code #\9)) (- value #x30))
((<= (char-code #\A) value (char-code #\Z)) (- value #x37)))))
(defun hash-component (component)
(let* ((characters (coerce component 'list))
(values (mapcar #'charcode characters)))
(unless (or (/= 5 (length values)) (find nil values))
(+ (* #x000001 (nth 0 values))
(* #x000024 (nth 1 values))
(* #x000510 (nth 2 values))
(* #x00b640 (nth 3 values))
(* #x19a100 (nth 4 values))))))
(defun parse-key (key)
(reduce #'append
(mapcar #'(lambda (component)
(let ((hash (hash-component component)))
(list (logand hash #xff)
(logand (ash hash -8) #xff)
(logand (ash hash -16) #xff))))
(split-sequence #\- key))))
(defun key-valid-p (key)
;; Must begin with 0xdeadbabe, and have HELL0 for components 6 and 7.
(and (equal (subseq key 0 4) '(222 173 186 190))
(equal (subseq key 15 21) '(153 95 15 153 95 15))))
(defun key-attributes (key)
(let ((parsed (parse-key key)))
(list :valid (key-valid-p parsed)
:expiry-year (logior (ash (nth 4 parsed) 8)
(nth 5 parsed))
:expiry-month (nth 6 parsed)
:expiry-day (nth 7 parsed)
:feature-a (plusp (logand (nth 8 parsed) (ash 1 6)))
:feature-b (plusp (logand (nth 9 parsed) (ash 1 0)))
:feature-c (plusp (logand (nth 10 parsed) (ash 1 1)))
:feature-d (plusp (logand (nth 11 parsed) (ash 1 2)))
:feature-e (plusp (logand (nth 12 parsed) (ash 1 0))))))
(key-attributes "2Z7A7-EK270-TMHR4-BHC71-CEB52-HELL0-HELL0-EONP9")
;; (:VALID T :EXPIRY-YEAR 2053 :EXPIRY-MONTH 5 :EXPIRY-DAY 22 :FEATURE-A T
;; :FEATURE-B T :FEATURE-C T :FEATURE-D T :FEATURE-E NIL)
(key-attributes "2Z7A7-6I7R9-MZGO9-FDQJ3-JN0Q6-HELL0-HELL0-72KJ9")
;; (:VALID T :EXPIRY-YEAR 2042 :EXPIRY-MONTH 2 :EXPIRY-DAY 21 :FEATURE-A T
;; :FEATURE-B T :FEATURE-C T :FEATURE-D T :FEATURE-E T)
#+END_SRC
We can verify our results.
#+BEGIN_SRC prog
jakob@Epsilon ~ $ ./challenge74 "2Z7A7-EK270-TMHR4-BHC71-CEB52-HELL0-HELL0-EONP9"
Crackme/keygenme by Dennis Yurichev, http://challenges.re/74
Expiration date: 2053-05-22
Feature A: ON
Feature B: ON
Feature C: ON
Feature D: ON
Feature E: OFF
SN valid
jakob@Epsilon ~ $ ./challenge74 "2Z7A7-6I7R9-MZGO9-FDQJ3-JN0Q6-HELL0-HELL0-72KJ9"
Crackme/keygenme by Dennis Yurichev, http://challenges.re/74
Expiration date: 2042-02-21
Feature A: ON
Feature B: ON
Feature C: ON
Feature D: ON
Feature E: ON
SN valid
#+END_SRC
But, as I mentioned, I'd missed the checksum, so we'll need to deal with that in
developing a keygen. What makes this so difficult is that the bytes of the
checksum are incorporated in the checksum value. So, I thought this might be an
opportunity to add something else to my toolbox: the [[https://en.wikipedia.org/wiki/Z3_Theorem_Prover][Z3 Theorem Prover]].
I'd never used it before, but it seems to show up in CTF writeups quite
frequently. I did a bit of reading ([[https://jomo.tv/security/mrmcd-ctf-writeup-flag-checker][this]], [[https://ericpony.github.io/z3py-tutorial/guide-examples.htm][this]] and [[https://stackoverflow.com/questions/53726998/using-z3-where-constraint-depends-on-output-of-function][this]]) and put together this:
#+BEGIN_SRC python
from z3 import *
s = Solver()
def checksum(init, key):
result = BitVecVal(~init, 64)
for byte in key:
result ^= ZeroExt(56, byte)
for i in range(8):
result = (result >> 1 & 0x7fffffffffffffff) ^ (0x42f0e1eb0badbad0 * (result & 1))
result = ~result
return result
def unpack(word):
result = BitVecVal(0, 64)
result |= ZeroExt(56, word[0])
result |= ZeroExt(56, word[1]) << 8
result |= ZeroExt(56, word[2]) << 16
result |= ZeroExt(56, word[3]) << 24
result |= ZeroExt(56, word[4]) << 32
result |= ZeroExt(56, word[5]) << 40
result |= ZeroExt(56, word[6]) << 48
result |= ZeroExt(56, word[7]) << 56
return result
key = [BitVec("bv{}".format(i), 8) for i in range(24)]
FEATURE_A = False
FEATURE_B = False
FEATURE_C = False
FEATURE_D = False
FEATURE_E = False
s.add(key[0] == 222)
s.add(key[1] == 173)
s.add(key[2] == 186)
s.add(key[3] == 190)
s.add(key[4] == ((2019 & 0xff00) >> 8))
s.add(key[5] == 2019 & 0x00ff)
s.add(key[6] == 12)
s.add(key[7] == 25)
s.add(key[8] & 0b100000 == (1 if FEATURE_A else 0))
s.add(key[9] & 0b000001 == (1 if FEATURE_B else 0))
s.add(key[10] & 0b000010 == (1 if FEATURE_C else 0))
s.add(key[11] & 0b001000 == (1 if FEATURE_D else 0))
s.add(key[12] & 0b000001 == (1 if FEATURE_E else 0))
s.add(key[15] == 153)
s.add(key[16] == 95)
s.add(key[17] == 15)
s.add(key[18] == 153)
s.add(key[19] == 95)
s.add(key[20] == 15)
s.add(unpack(key[16:]) == checksum(0, key))
s.check()
print(s.model())
#+END_SRC
Accurately translating the checksum function was a pain in the tuckus. The right
shift was giving me a hard time since the Z3 right shift doesn't prepend with
zeroes. The =& 0x7fffffffffffffff= is my attempt at dealing with that.
As an aside, I just want to say that GDB's =call= functionality is godsend. It
made verifying my translation so much easier.
#+BEGIN_SRC prog
(gdb) p (unsigned long long) $checksum(0, &{'\xff', '\xff', '\xff'}, 3)
$14 = 18446742974197923840
#+END_SRC
So, I let this run overnight, which brought me back to when I was more active
with CTF and would let my half-complete solutions run while I slept.
#+BEGIN_SRC prog
jakob@Epsilon ~ $ python solver.py
[bv22 = 196,
bv13 = 18,
bv21 = 216,
bv23 = 130,
bv14 = 209,
bv8 = 130,
bv10 = 108,
bv9 = 170,
bv11 = 208,
bv12 = 240,
bv20 = 15,
bv19 = 95,
bv18 = 153,
bv17 = 15,
bv16 = 95,
bv15 = 153,
bv7 = 24,
bv6 = 12,
bv5 = 227,
bv4 = 0,
bv3 = 190,
bv2 = 186,
bv1 = 173,
bv0 = 222]
#+END_SRC
This was waiting for me when I got back from the gym the next morning.
#+BEGIN_SRC common-lisp
(string-join
(mapcar #'ahash-component-inverse
(mapcar #'triplet-to-number '((222 173 186)
(190 0 27)
(12 24 13)
(170 108 208)
(240 18 20)
(153 95 15)
(153 95 15)
(216 196 130))))
"-")
#+END_SRC
#+BEGIN_SRC prog
CL-USER> (string-join
(mapcar #'ahash-component-inverse
(mapcar #'triplet-to-number '((222 173 186)
(190 0 27)
(12 24 13)
(170 108 208)
(240 18 20)
(153 95 15)
(153 95 15)
(216 196 130))))
"-")
"2Z7A7-AHX11-S4EI0-6LR48-K37S0-HELL0-HELL0-KPO35"
CL-USER> (key-attributes "2Z7A7-AHX11-S4EI0-6LR48-K37S0-HELL0-HELL0-KPO35")
(:VALID NIL :EXPIRY-YEAR 27 :EXPIRY-MONTH 12 :EXPIRY-DAY 24 :FEATURE-A NIL
:FEATURE-B NIL :FEATURE-C NIL :FEATURE-D NIL :FEATURE-E NIL)
#+END_SRC
Oh no...
#+BEGIN_SRC python
...
s.add(key[4] == 2019 & 0xff00)
...
#+END_SRC
That should've been =s.add(key[4] == ((2019 & 0xff00) >> 8))=...
;-;
Let's try this again.
#+BEGIN_SRC prog
jakob@Epsilon ~ $ python solver.py
[bv22 = 80,
bv13 = 39,
bv21 = 204,
bv23 = 133,
bv14 = 124,
bv8 = 140,
bv10 = 12,
bv9 = 168,
bv11 = 183,
bv12 = 184,
bv20 = 15,
bv19 = 95,
bv18 = 153,
bv17 = 15,
bv16 = 95,
bv15 = 153,
bv7 = 25,
bv6 = 12,
bv5 = 227,
bv4 = 7,
bv3 = 190,
bv2 = 186,
bv1 = 173,
bv0 = 222]
#+END_SRC
This time it actually ran for a whole two days.
#+BEGIN_SRC prog
CL-USER> (mapcar #'hash-component-inverse
(mapcar #'triplet-to-number
'((222 173 186)
(190 7 227)
(12 25 140)
(168 12 183)
(184 39 124)
(153 95 15)
(153 95 15)
(204 80 133))))
("2Z7A7" "YFWU8" "CGSG5" "CF457" "K9EU4" "HELL0" "HELL0" "OH975")
CL-USER> (key-attributes "2Z7A7-YFWU8-CGSG5-CF457-K9EU4-HELL0-HELL0-OH975")
(:VALID T :EXPIRY-YEAR 2019 :EXPIRY-MONTH 12 :EXPIRY-DAY 25 :FEATURE-A NIL
:FEATURE-B NIL :FEATURE-C NIL :FEATURE-D NIL :FEATURE-E NIL)
#+END_SRC
#+BEGIN_SRC prog
jakob@Epsilon ~ $ ./challenge74 "2Z7A7-YFWU8-CGSG5-CF457-K9EU4-HELL0-HELL0-OH975"
Crackme/keygenme by Dennis Yurichev, http://challenges.re/74
Expiration date: 2019-12-25
Feature A: OFF
Feature B: OFF
Feature C: OFF
Feature D: OFF
Feature E: OFF
SN valid
#+END_SRC
There we go. A working keygen! (Provided you're willing to wait).
* An End-of-Year Reflection
This was fun, but I think in planning this out, I should have preferred depth
over breadth, like getting through some of the challenges on [[http://reversing.kr/index.php][reversing.kr]]. The
challenges I got the most out of were the ones I had to spend more than a day
reversing. Another thing that made regret the choice of Dennis Yurichev's
challenges is the significance of context in reverse engineering. Most of these
challenges give little more than a disassembly. There are exceptions --
challenge #33, for example, was one I was able to solve because the description
said that it was a cryptographic function. But for the most part, I think being
able to see the "big picture" would have been a more realistic way to practice
my reverse engineering chops.
One idea I've been toying with is putting out a crackme on a monthly basis.
Infrequent enough that it wouldn't be overwhelming, and I'd be able to make it a
sizeable challenge. I'd be able to give out hints every week, and post the
solution at the end of the month. Actually, I may do this through the [[https://ctf.cs.umass.edu/][wargames]]
site we're putting together at university. Stay tuned!
#+BEGIN_EXPORT html
<nav class="series-nav" aria-label="Series navigation">
<a href="/blog/challenges-re-writeups-3.html" class="series-box single-left next">
<span class="series-label">« Previous in series</span>
<span class="series-title">Writeups for Dennis Yurichev's Reverse Engineering Challenges (#23-#35)</span>
</a>
</nav>
#+END_EXPORT
|