Login onto server
lotto@pwnable:~$ ls -ls
total 24
4 -r--r----- 1 lotto_pwn root 55 Feb 18 2015 flag
16 -r-sr-x--- 1 lotto_pwn lotto 13081 Feb 18 2015 lotto
4 -r--r--r-- 1 root root 1713 Feb 18 2015 lotto.c
Copy lotto to our local computer for Ghidra and execute Lotto to analyse what is wants.
lotto@pwnable:~$ ./lotto
- Select Menu -
1. Play Lotto
2. Help
3. Exit
- nLotto Rule -
nlotto is consisted with 6 random natural numbers less than 46
your goal is to match lotto numbers as many as you can
if you win lottery for *1st place*, you will get reward
for more details, follow the link below
http://www.nlotto.co.kr/counsel.do?method=playerGuide
mathematical chance to win this game is known to be 1/8145060.
The link doesn’t work anymore, let’s check archive.org. Seems like the website is just down and a real website was available in the past. Fun part is that it is in Korean. But I don’t think we need to really use the website.
When opening it in Ghidra we can see already it has multiple functions
Let us do a quick check at main and help but I think that play will
be the most interesting one or the FUN_00400660
.
**************************************************************
* FUNCTION *
**************************************************************
undefined help()
undefined AL:1 <RETURN>
help XREF[4]: Entry Point(*), main:00400a76(c),
00400dd8, 00400e60(*)
004009b7 PUSH RBP
004009b8 MOV RBP,RSP
004009bb MOV EDI=>s_-_nLotto_Rule_-_00400c06,s_-_nLotto_Rul = "- nLotto Rule -"
004009c0 CALL puts int puts(char * __s)
004009c5 MOV EDI=>s_nlotto_is_consisted_with_6_rando_00400c = "nlotto is consisted with 6 ra
004009ca CALL puts int puts(char * __s)
004009cf MOV EDI=>s_your_goal_is_to_match_lotto_numb_00400c = "your goal is to match lotto n
004009d4 CALL puts int puts(char * __s)
004009d9 MOV EDI=>s_if_you_win_lottery_for_*1st_plac_00400c = "if you win lottery for *1st p
004009de CALL puts int puts(char * __s)
004009e3 MOV EDI=>s_for_more_details,_follow_the_lin_00400c = "for more details, follow the
004009e8 CALL puts int puts(char * __s)
004009f2 CALL puts int puts(char * __s)
004009f7 MOV EDI=>s_mathematical_chance_to_win_this_g_00400 = "mathematical chance to win th
004009fc CALL puts int puts(char * __s)
00400a01 POP RBP
00400a02 RET
Ok, here is literally nothing of interest. It is really just a help section that just prints out text.
00400a03 PUSH RBP
00400a04 MOV RBP,RSP
00400a07 SUB RSP,0x20
00400a0b MOV dword ptr [RBP + local_1c],EDI
00400a0e MOV qword ptr [RBP + local_28],RSI
LAB_00400a12 XREF[1]: 00400a99(j)
00400a12 MOV EDI=>s_-_Select_Menu_-_00400d77,s_-_Select_Men = "- Select Menu -"
00400a17 CALL puts int puts(char * __s)
00400a1c MOV EDI=>s_1._Play_Lotto_00400d87,s_1._Play_Lotto_ = "1. Play Lotto"
00400a21 CALL puts int puts(char * __s)
00400a26 MOV EDI=>s_2._Help_00400d95,s_2._Help_00400d95 = "2. Help"
00400a2b CALL puts int puts(char * __s)
00400a30 MOV EDI=>s_3._Exit_00400d9d,s_3._Exit_00400d9d = "3. Exit"
00400a35 CALL puts int puts(char * __s)
00400a3a MOV EAX,DAT_00400da5 = 25h %
00400a3f LEA RDX=>local_c,[RBP + -0x4]
00400a43 MOV RSI,RDX
00400a46 MOV RDI=>DAT_00400da5,RAX = 25h %
00400a49 MOV EAX,0x0
00400a4e CALL __isoc99_scanf undefined __isoc99_scanf()
00400a53 MOV EAX,dword ptr [RBP + local_c]
00400a56 CMP EAX,0x2
00400a59 JZ LAB_00400a71
00400a5b CMP EAX,0x3
00400a5e JZ LAB_00400a7d
00400a60 CMP EAX,0x1
00400a63 JNZ LAB_00400a8e
00400a65 MOV EAX,0x0
00400a6a CALL play undefined play()
00400a6f JMP LAB_00400a99
LAB_00400a71 XREF[1]: 00400a59(j)
00400a71 MOV EAX,0x0
00400a76 CALL help undefined help()
00400a7b JMP LAB_00400a99
LAB_00400a7d XREF[1]: 00400a5e(j)
00400a7d MOV EDI=>DAT_00400da8,DAT_00400da8 = 62h b
00400a82 CALL puts int puts(char * __s)
00400a87 MOV EAX,0x0
00400a8c LEAVE
00400a8d RET
LAB_00400a8e XREF[1]: 00400a63(j)
00400a8e MOV EDI=>s_invalid_menu_00400dac,s_invalid_menu_00 = "invalid menu"
00400a93 CALL puts int puts(char * __s)
00400a98 NOP
LAB_00400a99 XREF[2]: 00400a6f(j), 00400a7b(j)
00400a99 JMP LAB_00400a12
00400a9e ?? 90h
00400a9f ?? 90h
Main is also quite boring by looking at it diagonally. It just watches for player input and goes to the player choice. It has an invalid choice handler but we can skip main.
Alright, here is the part we need. First this seems like a lot of code with some flows in it so lets view the graph of this function.
00400804 PUSH RBP
00400805 MOV RBP,RSP
00400808 SUB RSP,0x30
0040080c MOV RAX,qword ptr FS:[0x28]
00400815 MOV qword ptr [RBP + local_10],RAX
00400819 XOR EAX,EAX
0040081b MOV EAX,s_Submit_your_6_lotto_bytes_:_00400b90 = "Submit your 6 lotto bytes : "
00400820 MOV RDI=>s_Submit_your_6_lotto_bytes_:_00400b90,RAX = "Submit your 6 lotto bytes : "
00400823 MOV EAX,0x0
00400828 CALL printf int printf(char * __format, ...)
0040082d MOV RAX,qword ptr [stdout]
00400834 MOV RDI,RAX
00400837 CALL fflush int fflush(FILE * __stream)
0040083c MOV EDX,0x6
00400841 MOV ESI=>submit,submit
00400846 MOV EDI,0x0
0040084b MOV EAX,0x0
00400850 CALL read ssize_t read(int __fd, void * __
00400855 MOV dword ptr [RBP + local_20],EAX
00400858 MOV EDI=>s_Lotto_Start!_00400bad,s_Lotto_Start!_00 = "Lotto Start!"
0040085d CALL puts int puts(char * __s)
00400862 MOV ESI,0x0
00400867 MOV EDI=>s_/dev/urandom_00400bba,s_/dev/urandom_00 = "/dev/urandom"
0040086c MOV EAX,0x0
00400871 CALL open int open(char * __file, int __of
00400876 MOV dword ptr [RBP + local_1c],EAX
00400879 CMP dword ptr [RBP + local_1c],-0x1
0040087d JNZ LAB_00400893
Ok, the first block in the list is the introduction, it will ask for your numbers you want to input and then a fflush() happens which will force the stdout to be printed. After that we call read which expects 6 characters.
Then it saves our result in local_20 and outputs a message that the lotto has started and opens /dev/urandom (which is a never blocking random generator. It uses the devices of the computer for entropy so it is not like we can easily guess what the seed is)
00400893 LEA RCX=>local_18,[RBP + -0x10]
00400897 MOV EAX,dword ptr [RBP + local_1c]
0040089a MOV EDX,0x6
0040089f MOV RSI,RCX
004008a2 MOV EDI,EAX
004008a4 MOV EAX,0x0
004008a9 CALL read ssize_t read(int __fd, void * __
004008ae CMP EAX,0x6
004008b1 JZ LAB_004008c7
Here is the code that reads 6 bytes from urandom. Read returns the total of bytes it has read and we go to the next part if succesful (we’ll skip the error part as it is just a puts and exit)
Block 3, initialize local_2c
to zero
Block 4, compare if local_2c
is equal to five. We know
we read 6 bytes so it seems we will be looping over all of them.
004008d0 MOV EAX,dword ptr [RBP + local_2c]
004008d3 CDQE
004008d5 MOVZX EDX,byte ptr [RBP + RAX*0x1 + -0x10]
004008da MOVZX ECX,DL
004008dd MOV EAX,ECX
004008df ADD EAX,EAX
004008e1 ADD EAX,ECX
004008e3 LEA ESI,[RAX*0x8]
004008ea ADD EAX,ESI
004008ec SHL EAX,0x2
004008ef ADD EAX,ECX
004008f1 SHR AX,0x8
004008f5 MOV ECX,EDX
004008f7 SUB CL,AL
004008f9 SHR CL,1
004008fb ADD EAX,ECX
004008fd SHR AL,0x5
00400900 MOV ECX,0x2d
00400905 IMUL EAX,ECX
00400908 MOV ECX,EDX
0040090a SUB CL,AL
0040090c MOV EAX,ECX
0040090e LEA EDX,[RAX + 0x1]
00400911 MOV EAX,dword ptr [RBP + local_2c]
00400914 CDQE
00400916 MOV byte ptr [RBP + RAX*0x1 + -0x10],DL
0040091a ADD dword ptr [RBP + local_2c],0x1
Block 5, seems like a lot of actions we do on our random string. I’m gonna take a guess these actions will make it much less random.
As I was getting a bit discouraged I looked into the source code file (we call that part of the training! Now when I’ll see an ugly block of code I’ll try to live analyse it), apparantly this whole section is urandom[i] % 45 + 1. You could see it as making sure the value is always between 1-45. Which won’t help us a lot so the bug/exploit is hopefully in the next part.
Now, as I was in a hurry I just started to use the decompiled section of Ghidra (and energy was quite drained after that modulo)
while (I < 6) {
Y = 0;
while (Y < 6) {
if (urandom[I] == *(byte *)((long)&userInput + (long)Y)) {
match = match + 1;
}
Y = Y + 1;
}
I = I + 1;
}
Ok, what I see here is that userInput is also just a byte array. So
this was probably a for-loop that goes over both urandom and the bytes
the user had inputted (or char’s). Now what the problem here seems to be
is that it always loops over all. So if in urandom[i]
we
once have the byte 0x41
(or A) we can have 6 matches if our
userinput is purely AAAAAA
.
urandom = "A!B#C$"
userinput = "AAAAAA"
Iteration 1:
i = 1
y = 1
urandom[1] == userInput[1] --> Ok matches+1
Iteration 2:
i = 1
y = 2
urandom[1] == userInput[2] --> Ok matches+1
... and this will happen 6 times. A break would have solved this.
I used A in my example but A is actually already too high. You can choose a sign under 46 decimal from the ASCIITABLE.
This challenge felt a bit too hard, the energy was drained out of me because of the very weird modulo 45 code. I did learn for next times that you should sometimes leave behind a block and see if the next blocks make more sense. Compiler optimizations can become very complex and in-line functions can make it more unreadable.
< Home