Rating: 4.0

Memory Corruption - Ace of Spades

What we have is a card game. Fooling around with it (under valgrind - why not?) reveals the following:

  • There is a shuffled deck of 52 cards.
  • We can draw as many cards as we please (even all 52).
  • We can discard drawn cards in FIFO manner.
  • When we are happy with the hand, we can "play" it - this will compute the score based on the following table:
    • Jack: 300
    • Queen: 600
    • King: 900
    • Ace: 1200
    • Ace of Spades: x2
  • Only some of the cards in the hand are taken into account.
  • After the hand is played, all the cards are shuffled into the deck and we can draw again.
  • So the theoretical maximum of points we can achieve is (1200 * 3 + 900) * 2 = 9000. Memez FTW!
  • Winning 1000 or more points results in a prize. We can keep it or we can replace it with anything else.
  • Stuff like drawing more than 52 cards and discarding from the empty hand seems to be handled correctly. No easy way to crash the program.
  • The only valgrind warning is:
==11086== Source and destination overlap in strcpy(0x10b260, 0x10b261)
==11086==    at 0x4833537: strcpy (in vgpreload_memcheck-x86-linux.so)
==11086==    by 0x108D92: ??? (in ace_of_spades)
==11086==    by 0x109236: ??? (in ace_of_spades)
==11086==    by 0x109354: main (in ace_of_spades)

Well, this is for sure a lousy non-portable coding practice, but on our particular platform this seems to work fine. Moving on.

Reversing the code in IDA raises more eyebrows than I care to count. Spoiler: most of them ended up being useless at the end:

  • The function that calculates points explicitly handles multiple Aces of Spades:
.text:00000DEB                 cmp     al, 28h ; '('
.text:00000DED                 jnz     short is_not_aos
.text:00000DEF is_aos:
.text:00000DEF                 add     [ebp+aos_count], 1
.text:00000DF3                 jmp     short next_index

...

.text:00000E48 double_points:
.text:00000E48                 shl     [ebp+points], 1
.text:00000E4B                 add     [ebp+index], 1
.text:00000E4F loop_end:
.text:00000E4F                 mov     eax, [ebp+index]
.text:00000E52                 cmp     eax, [ebp+aos_count]
.text:00000E55                 jb      short double_points

A doorway into exceeding the maximum "fair" amount of points?

  • The function that shows the hand does not honor the number of cards and instead waits for the trailing \0:
.text:0000115E                 movzx   eax, byte ptr [eax]
.text:00001161                 test    al, al
.text:00001163                 jnz     short loop_body

An opportunity for an arbitrary memory read?

  • RNG seed is not discarded, but rather stored into a global variable, which lies right after deck, hand and discard pile. Can we read it somehow?
.bss:00003220 deck            db 35h dup(?)

...

.bss:00003260 hand            db 35h dup(?)

...

.bss:000032A0 discard_pile    db 35h dup(?)

...

.bss:000032E0 seed            dd ?
  • Prize selection does not handle 11000 points and more correctly. Winning 16000 points and keeping the prize allows us to read memory pointed to by rbp. Replacing the prize allows us to write to said memory:
.text:00000FDE                 mov     eax, [ebp+points]
.text:00000FE1                 mov     edx, 10624DD3h
.text:00000FE6                 mul     edx
.text:00000FE8                 mov     eax, edx
.text:00000FEA                 shr     eax, 6
.text:00000FED                 mov     [ebp+prize_index], eax
.text:00000FF0                 mov     eax, [ebp+prize_index]
.text:00000FF3                 mov     eax, [ebp+eax*4+prizes]
.text:00000FF7                 sub     esp, 8
.text:00000FFA                 push    eax
.text:00000FFB                 lea     eax, (aYourPrizeS - 2F98h)[ebx] ; "Your prize: %s\n"
.text:00001001                 push    eax             ; format
.text:00001002                 call    _printf

But how do we win that much?!

  • Winning regular prizes and changing them to 'x' * 0x20 allows us to overwrite the trailing '\0' and see the next prize:
.text:00001087 change:
.text:00001087                 mov     eax, [ebp+prize_index]
.text:0000108A                 mov     eax, [ebp+eax*4+prizes]
.text:0000108E                 sub     esp, 4
.text:00001091                 push    20h ; ' '       ; nbytes
.text:00001093                 push    eax             ; buf
.text:00001094                 push    0               ; fd
.text:00001096                 call    _read

Any possibility to read something useful here?

  • Familiar strcpy issue:
.text:00000D7D                 lea     eax, (hand+1 - 2F98h)[ebx]
.text:00000D83                 sub     esp, 8
.text:00000D86                 push    eax             ; src
.text:00000D87                 lea     eax, (hand - 2F98h)[ebx]
.text:00000D8D                 push    eax             ; dest
.text:00000D8E                 call    _strcpy

Uh huh. I would NACK that in a code review, but this is CTF.

  • Only the first 5 cards count towards the score, even if we play an empty hand:
.text:00000E39 loop_header:
.text:00000E39                 cmp     [ebp+index], 4
.text:00000E3D                 jbe     short process_card

Can we somehow put Aces of Spades into empty slots?

  • Function that asks for a choice reads 4 characters and calls atoi() without any checking - can we read something useful this way?

  • When reading the seed from /dev/urandom the code doesn't check the return value of read(): can we somehow overwhelm the system RNG and make read() return just 1 byte, making the seed more predictable?

  • Why are we given the xinetd file? Is the environment the server runs in somehow useful?

Two pieces of the puzzle come together: duplicating Aces of Spades will make it possible to exceed the maximum score and gain the ability to read and write to the stack. The missing piece is: how to duplicate the cards?

All the issues related to predicting the RNG, even if they would somehow work, are useless: we need to actively mess with the deck, not learn stuff about it.

After hours and hours of reading the assembly and experimenting with ideas above, I come back to strcpy and wrote the following (by this time I've developed a bunch of pyexpect-based helpers to drive the game):

def strcpy_problem(p):
    for _ in range(52):
        draw(p)
    h = print_hand(p)
    for _ in range(52):
        discard(p)
        h1 = print_hand(p)
        if h[1:] != h1:
            print('^^^ BUG ^^^')
        h = h1
    fold(p)

Ouch! When working with certain string lengths (29, 44, 45), strcpy with overlapping buffers duplicates certain characters. So, let's do the following in a loop:

  • Draw 52 cards.
  • See if some combination of 5 cards gives us the desired score.
    • Discard until those 5 cards become the first ones and play the hand.
      • If in rare cases when strcpy bug messes with that, tough luck - fold and try again.
  • See if strcpy bug lets us replace a worthless card with a Jack or something better.
    • Discard until the bug is triggered, fold.
  • Fold.

Here are the scores that we want to achieve, in order:

  • Some "legal" score in range 1000-10999 in order to change the prize to the name of the file containing the flag - /home/ace_of_spades/flag\0. This actually required some guesswork (flag vs flag.txt and / vs . vs $HOME) - xinetd file allowed to learn the name of the home directory.

  • 16000-16999 in order to print the prize, which is the stack contents. prize[4:8] contains the return address, which is always main+147 - so long, ASLR!

  • 16000-16999 again in order to change the prize and overwrite the stack.

Where can we return? We have a bunch of libc functions in PLT, but, alas, no sweet exec. We have to open the flag anyway. Then we need to read it.

The first step is easy - overwrite the stack with:

  • 0 (old ebp)
  • open address
  • 0 (open return address)
  • address of path to flag
  • open flags

and leave the game in order to pass control to open.

When open returns, the flag fd will be in eax and top of the stack is going to be:

  • address of path to flag
  • flag length

Can we do anything useful with that? Turns out we can. Let's find all the read calls. One of them is in the very beginning and is responsible for reading /dev/urandom. Let's try jumping there:

  • 0 (old ebp)
  • open address
  • main + 0x46:
.text:00001308                 push    eax             ; fd
.text:00001309                 call    _read
  • address of path to flag
  • 64 which is a valid open flag and at the same time a reasonable flag length

When we are at the read call site, the stack looks like this:

  • flag fd
  • address of path to flag
  • 64

The flag will be placed into the prize slot, which used to contain the path. After that the game will be restarted, so we just play again, win the prize at this slot and get the flag!

Original writeup (https://github.com/mephi42/ctf/tree/master/2019.09.28-PwnThyBytes_CTF_2019/memory_corruption-Ace_of_Spades).