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()AL:1 <RETURN>
undefined 4]: Entry Point(*), main:00400a76(c),
help XREF[00400e60(*)
00400dd8, PUSH RBP
004009b7 MOV RBP,RSP
004009b8 MOV EDI=>s_-_nLotto_Rule_-_00400c06,s_-_nLotto_Rul = "- nLotto Rule -"
004009bb CALL puts int puts(char * __s)
004009c0 MOV EDI=>s_nlotto_is_consisted_with_6_rando_00400c = "nlotto is consisted with 6 ra
004009c5 CALL puts int puts(char * __s)
004009ca MOV EDI=>s_your_goal_is_to_match_lotto_numb_00400c = "your goal is to match lotto n
004009cf CALL puts int puts(char * __s)
004009d4 MOV EDI=>s_if_you_win_lottery_for_*1st_plac_00400c = "if you win lottery for *1st p
004009d9 CALL puts int puts(char * __s)
004009de 004009e3 MOV EDI=>s_for_more_details,_follow_the_lin_00400c = "for more details, follow the
004009e8 CALL puts int puts(char * __s)
CALL puts int puts(char * __s)
004009f2 MOV EDI=>s_mathematical_chance_to_win_this_g_00400 = "mathematical chance to win th
004009f7 CALL puts int puts(char * __s)
004009fc POP RBP
00400a01 RET 00400a02
Ok, here is literally nothing of interest. It is really just a help section that just prints out text.
PUSH RBP
00400a03 MOV RBP,RSP
00400a04 SUB RSP,0x20
00400a07 MOV dword ptr [RBP + local_1c],EDI
00400a0b MOV qword ptr [RBP + local_28],RSI
00400a0e 1]: 00400a99(j)
LAB_00400a12 XREF[MOV EDI=>s_-_Select_Menu_-_00400d77,s_-_Select_Men = "- Select Menu -"
00400a12 CALL puts int puts(char * __s)
00400a17 MOV EDI=>s_1._Play_Lotto_00400d87,s_1._Play_Lotto_ = "1. Play Lotto"
00400a1c CALL puts int puts(char * __s)
00400a21 MOV EDI=>s_2._Help_00400d95,s_2._Help_00400d95 = "2. Help"
00400a26 CALL puts int puts(char * __s)
00400a2b MOV EDI=>s_3._Exit_00400d9d,s_3._Exit_00400d9d = "3. Exit"
00400a30 CALL puts int puts(char * __s)
00400a35 MOV EAX,DAT_00400da5 = 25h %
00400a3a LEA RDX=>local_c,[RBP + -0x4]
00400a3f MOV RSI,RDX
00400a43 MOV RDI=>DAT_00400da5,RAX = 25h %
00400a46 MOV EAX,0x0
00400a49 CALL __isoc99_scanf undefined __isoc99_scanf()
00400a4e MOV EAX,dword ptr [RBP + local_c]
00400a53 CMP EAX,0x2
00400a56 JZ LAB_00400a71
00400a59 CMP EAX,0x3
00400a5b JZ LAB_00400a7d
00400a5e CMP EAX,0x1
00400a60 JNZ LAB_00400a8e
00400a63 MOV EAX,0x0
00400a65 CALL play undefined play()
00400a6a JMP LAB_00400a99
00400a6f 1]: 00400a59(j)
LAB_00400a71 XREF[MOV EAX,0x0
00400a71 CALL help undefined help()
00400a76 JMP LAB_00400a99
00400a7b 1]: 00400a5e(j)
LAB_00400a7d XREF[MOV EDI=>DAT_00400da8,DAT_00400da8 = 62h b
00400a7d CALL puts int puts(char * __s)
00400a82 MOV EAX,0x0
00400a87 LEAVE
00400a8c RET
00400a8d 1]: 00400a63(j)
LAB_00400a8e XREF[MOV EDI=>s_invalid_menu_00400dac,s_invalid_menu_00 = "invalid menu"
00400a8e CALL puts int puts(char * __s)
00400a93 NOP
00400a98 2]: 00400a6f(j), 00400a7b(j)
LAB_00400a99 XREF[JMP LAB_00400a12
00400a99 90h
00400a9e ?? 90h 00400a9f ??
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
MOV RAX,qword ptr FS:[0x28]
0040080c 00400815 MOV qword ptr [RBP + local_10],RAX
00400819 XOR EAX,EAX
MOV EAX,s_Submit_your_6_lotto_bytes_:_00400b90 = "Submit your 6 lotto bytes : "
0040081b 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, ...)
MOV RAX,qword ptr [stdout]
0040082d 00400834 MOV RDI,RAX
00400837 CALL fflush int fflush(FILE * __stream)
MOV EDX,0x6
0040083c 00400841 MOV ESI=>submit,submit
00400846 MOV EDI,0x0
MOV EAX,0x0
0040084b 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!"
CALL puts int puts(char * __s)
0040085d 00400862 MOV ESI,0x0
00400867 MOV EDI=>s_/dev/urandom_00400bba,s_/dev/urandom_00 = "/dev/urandom"
MOV EAX,0x0
0040086c 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
JNZ LAB_00400893 0040087d
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]
MOV EDX,0x6
0040089a MOV RSI,RCX
0040089f MOV EDI,EAX
004008a2 MOV EAX,0x0
004008a4 CALL read ssize_t read(int __fd, void * __
004008a9 CMP EAX,0x6
004008ae JZ LAB_004008c7 004008b1
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)
MOV dword ptr [RBP + local_2c],0x0
004008c7 JMP LAB_0040091e 004008ce
Block 3, initialize local_2c
to zero
CMP dword ptr [RBP + local_2c],0x5
0040091e 00400922 JLE LAB_004008d0
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.
MOV EAX,dword ptr [RBP + local_2c]
004008d0 CDQE
004008d3 MOVZX EDX,byte ptr [RBP + RAX*0x1 + -0x10]
004008d5 MOVZX ECX,DL
004008da MOV EAX,ECX
004008dd ADD EAX,EAX
004008df 004008e1 ADD EAX,ECX
004008e3 LEA ESI,[RAX*0x8]
ADD EAX,ESI
004008ea SHL EAX,0x2
004008ec ADD EAX,ECX
004008ef SHR AX,0x8
004008f1 MOV ECX,EDX
004008f5 SUB CL,AL
004008f7 SHR CL,1
004008f9 ADD EAX,ECX
004008fb SHR AL,0x5
004008fd 00400900 MOV ECX,0x2d
00400905 IMUL EAX,ECX
00400908 MOV ECX,EDX
SUB CL,AL
0040090a MOV EAX,ECX
0040090c LEA EDX,[RAX + 0x1]
0040090e 00400911 MOV EAX,dword ptr [RBP + local_2c]
00400914 CDQE
00400916 MOV byte ptr [RBP + RAX*0x1 + -0x10],DL
ADD dword ptr [RBP + local_2c],0x1 0040091a
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) {
0;
Y = while (Y < 6) {
if (urandom[I] == *(byte *)((long)&userInput + (long)Y)) {
1;
match = match +
}1;
Y = Y +
}1;
I = I + }
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