< Home

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: HELP

                     **************************************************************
                     *                          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.

Function: MAIN

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.

Function: PLAY

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.

Graph Diagram of the function play

Block 1: Intro

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)

Block 2: just a read

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, 4 and 5: A while loop!

004008c7   MOV        dword ptr [RBP + local_2c],0x0
004008ce   JMP        LAB_0040091e

Block 3, initialize local_2c to zero

0040091e   CMP        dword ptr [RBP + local_2c],0x5
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.

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.

  1. First we move local_2c (or our i) to EAX
  2. CDQE -> We make our EAX a RAX
  3. /dev/urandom was stored in RBP-0x10 (local_18), here we see we are accessing a byte of this with our local_2c ( urandom[i] ) and storing it in EDX
  4. We move the lower part of the previous operation in ECX, but we only moved a byte to EDX so we actually move the whole letter to ECX
  5. Now we move ECX into EAX
  6. We multiply it ( urandom[i] * 2 )
  7. We add ECX (which was urandom[i] before the previous operation) to it so it seems this is actually urandom[i] * 3
  8. We do ( urandom[i] * 3 ) * 8 and move it into esi
  9. Now we add this to EAX (so urandom[i] * 3 + (urandom[i] * 3) * 8)
  10. Now we do a SHL 2 so… (urandom[i] * 3 + (urandom[i] * 3) * 8) * 4
  11. Now we re-add ECX to it which was (urandom[i] * 3 + (urandom[i] * 3) * 8) * 4 + urandom[i]
  12. Ok we do a SHR 0x8 of AX so… AX is 16 bytes, so we remove the first 8 bytes of that. so if we had 0xFF FF it becomes 0x00 FF but everything before AX is kept the same. What the fuck is happening here.
  13. We move urandom[i] into ECX
  14. We now subtract AL from CL (lower byte of EAX from lower byte ECX)
  15. After that we shift right CL
  16. Now we add ECX to EAX
  17. We shift right the lower byte of EAX 5 times
  18. We move 0x2D into ECX
  19. We multiple EAX with ECX
  20. We move EDX into ECX
  21. … I just completely lost it what is even happening here?

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)

Blocks after: Two for loops with a counter

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