Skip to content

Commit

Permalink
[EFR][HOTFIX] QA Request (#2306)
Browse files Browse the repository at this point in the history
* Scan independent library file (.so, .dylib, Framework dylib) from APK/IPA Static Analysis Report
* Library analysis refactored relative path helper for Django template.
* Re-introduced RELRO checks for Android, added Dart binary check to avoid Flutter false positives.
* Improved stripped debug symbol check for ELF and MachO using native OS tools such as nm and objdump when available.
* Merge iOS Framework and Dylib Analysis.
  • Loading branch information
ajinabraham authored Dec 16, 2023
1 parent 4d81e09 commit 78e9563
Show file tree
Hide file tree
Showing 18 changed files with 307 additions and 151 deletions.
2 changes: 1 addition & 1 deletion mobsf/MobSF/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

logger = logging.getLogger(__name__)

VERSION = '3.8.6'
VERSION = '3.8.7'
BANNER = """
__ __ _ ____ _____ _____ ___
| \/ | ___ | |__/ ___|| ___|_ _|___ / ( _ )
Expand Down
5 changes: 4 additions & 1 deletion mobsf/MobSF/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@
# App Compare
re_path(r'^compare/(?P<hash1>[0-9a-f]{32})/(?P<hash2>[0-9a-f]{32})/$',
shared_func.compare_apps),

# Relative Shared & Dynamic Library scan
re_path(r'^scan_library/(?P<checksum>[0-9a-f]{32})$',
shared_func.scan_library,
name='scan_library'),
# Dynamic Analysis
re_path(r'^android/dynamic_analysis/$',
dz.android_dynamic_analysis,
Expand Down
18 changes: 18 additions & 0 deletions mobsf/MobSF/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import sqlite3
import unicodedata
import threading
from pathlib import Path
from distutils.version import StrictVersion

import distro
Expand Down Expand Up @@ -491,6 +492,8 @@ def update_local_db(db_name, url, local_file):
else:
logger.info('%s Database is up-to-date', db_name)
return update
except requests.exceptions.ReadTimeout:
logger.warning('Failed to download %s DB.', db_name)
except Exception:
logger.exception('[ERROR] %s DB Update', db_name)
return update
Expand Down Expand Up @@ -758,6 +761,21 @@ def replace(value, arg):
return value.replace(what, to)


def relative_path(value):
"""Show relative path to two parents."""
sep = None
if '/' in value:
sep = '/'
elif '\\\\' in value:
sep = '\\\\'
elif '\\' in value:
sep = '\\'
if not sep or value.count(sep) < 2:
return value
path = Path(value)
return path.relative_to(path.parent.parent).as_posix()


def pretty_json(value):
"""Pretty print JSON."""
try:
Expand Down
5 changes: 4 additions & 1 deletion mobsf/StaticAnalyzer/views/android/jar_aar.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ def common_analysis(request, app_dic, rescan, api, analysis_type):
'certificate_summary': {},
}
app_dic['real_name'] = ''
elf_dict = library_analysis(app_dic['app_dir'], 'elf')
elf_dict = library_analysis(
app_dic['app_dir'],
app_dic['md5'],
'elf')
tracker = Trackers.Trackers(
app_dic['app_dir'],
app_dic['tools_dir'])
Expand Down
5 changes: 4 additions & 1 deletion mobsf/StaticAnalyzer/views/android/so.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ def so_analysis(request, app_dic, rescan, api):
'certificate_summary': {},
}
app_dic['real_name'] = ''
elf_dict = library_analysis(app_dic['app_dir'], 'elf')
elf_dict = library_analysis(
app_dic['app_dir'],
app_dic['md5'],
'elf')
# File Analysis is used to store symbols from so
app_dic['certz'] = get_symbols(
elf_dict['elf_symbols'])
Expand Down
7 changes: 6 additions & 1 deletion mobsf/StaticAnalyzer/views/android/static_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
is_md5,
key,
print_n_send_error_response,
relative_path,
)
from mobsf.StaticAnalyzer.models import (
RecentScansDB,
Expand Down Expand Up @@ -92,6 +93,7 @@

register.filter('key', key)
register.filter('android_component', android_component)
register.filter('relative_path', relative_path)


def static_analyzer(request, checksum, api=False):
Expand Down Expand Up @@ -211,7 +213,10 @@ def static_analyzer(request, checksum, api=False):
# apktool should run before this
get_icon_apk(apk, app_dic)

elf_dict = library_analysis(app_dic['app_dir'], 'elf')
elf_dict = library_analysis(
app_dic['app_dir'],
app_dic['md5'],
'elf')
cert_dic = cert_info(
apk,
app_dic,
Expand Down
5 changes: 4 additions & 1 deletion mobsf/StaticAnalyzer/views/common/a.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ def a_analysis(request, app_dict, rescan, api):
'framework_analysis': {},
}
# Analyze static library
slib = library_analysis(app_dict['bin_dir'], 'ar')
slib = library_analysis(
app_dict['bin_dir'],
app_dict['md5_hash'],
'ar')
bin_dict['bin_info']['arch'] = slib['ar_a']
bin_dict['dylib_analysis'] = slib['ar_analysis']
# Store Symbols in File Analysis
Expand Down
125 changes: 108 additions & 17 deletions mobsf/StaticAnalyzer/views/common/binary/elf.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
# !/usr/bin/python
# coding=utf-8
import shutil
import subprocess

import lief

from mobsf.StaticAnalyzer.views.common.binary.strings import (
strings_on_binary,
)


NA = 'Not Applicable'
NO_RELRO = 'No RELRO'
PARTIAL_RELRO = 'Partial RELRO'
FULL_RELRO = 'Full RELRO'
INFO = 'info'
WARNING = 'warning'
HIGH = 'high'


def nm_is_debug_symbol_stripped(elf_file):
"""Check if debug symbols are stripped using OS utility."""
# https://linux.die.net/man/1/nm
out = subprocess.check_output(
[shutil.which('nm'), '--debug-syms', elf_file],
stderr=subprocess.STDOUT)
return b'no debug symbols' in out


class ELFChecksec:
def __init__(self, elf_file, so_rel):
self.elf_path = elf_file.as_posix()
Expand All @@ -20,13 +41,13 @@ def checksec(self):
return
is_nx = self.is_nx()
if is_nx:
severity = 'info'
severity = INFO
desc = (
'The binary has NX bit set. This marks a '
'memory page non-executable making attacker '
'injected shellcode non-executable.')
else:
severity = 'high'
severity = HIGH
desc = (
'The binary does not have NX bit set. NX bit '
'offer protection against exploitation of memory corruption '
Expand All @@ -40,15 +61,15 @@ def checksec(self):
}
has_canary = self.has_canary()
if has_canary:
severity = 'info'
severity = INFO
desc = (
'This binary has a stack canary value '
'added to the stack so that it will be overwritten by '
'a stack buffer that overflows the return address. '
'This allows detection of overflows by verifying the '
'integrity of the canary before function return.')
else:
severity = 'high'
severity = HIGH
desc = (
'This binary does not have a stack '
'canary value added to the stack. Stack canaries '
Expand All @@ -62,9 +83,47 @@ def checksec(self):
'severity': severity,
'description': desc,
}
relro = self.relro()
if relro == NA:
severity = INFO
desc = ('RELRO checks are not applicable for '
'Flutter/Dart binaries')
elif relro == FULL_RELRO:
severity = INFO
desc = (
'This shared object has full RELRO '
'enabled. RELRO ensures that the GOT cannot be '
'overwritten in vulnerable ELF binaries. '
'In Full RELRO, the entire GOT (.got and '
'.got.plt both) is marked as read-only.')
elif relro == PARTIAL_RELRO:
severity = WARNING
desc = (
'This shared object has partial RELRO '
'enabled. RELRO ensures that the GOT cannot be '
'overwritten in vulnerable ELF binaries. '
'In partial RELRO, the non-PLT part of the GOT '
'section is read only but .got.plt is still '
'writeable. Use the option -z,relro,-z,now to '
'enable full RELRO.')
else:
severity = HIGH
desc = (
'This shared object does not have RELRO '
'enabled. The entire GOT (.got and '
'.got.plt both) are writable. Without this compiler '
'flag, buffer overflows on a global variable can '
'overwrite GOT entries. Use the option '
'-z,relro,-z,now to enable full RELRO and only '
'-z,relro to enable partial RELRO.')
elf_dict['relocation_readonly'] = {
'relro': relro,
'severity': severity,
'description': desc,
}
rpath = self.rpath()
if rpath:
severity = 'high'
severity = HIGH
desc = (
'The binary has RPATH set. In certain cases, '
'an attacker can abuse this feature to run arbitrary '
Expand All @@ -75,7 +134,7 @@ def checksec(self):
'compiler option -rpath to remove RPATH.')
rpt = rpath.rpath
else:
severity = 'info'
severity = INFO
desc = (
'The binary does not have run-time search path '
'or RPATH set.')
Expand All @@ -87,7 +146,7 @@ def checksec(self):
}
runpath = self.runpath()
if runpath:
severity = 'high'
severity = HIGH
desc = (
'The binary has RUNPATH set. In certain cases, '
'an attacker can abuse this feature and or modify '
Expand All @@ -99,7 +158,7 @@ def checksec(self):
'option --enable-new-dtags,-rpath to remove RUNPATH.')
rnp = runpath.runpath
else:
severity = 'info'
severity = INFO
desc = (
'The binary does not have RUNPATH set.')
rnp = runpath
Expand All @@ -110,14 +169,14 @@ def checksec(self):
}
fortified_functions = self.fortify()
if fortified_functions:
severity = 'info'
severity = INFO
desc = ('The binary has the '
f'following fortified functions: {fortified_functions}')
else:
if self.is_dart():
severity = 'info'
severity = INFO
else:
severity = 'warning'
severity = WARNING
desc = ('The binary does not have any '
'fortified functions. Fortified functions '
'provides buffer overflow checks against '
Expand All @@ -133,10 +192,10 @@ def checksec(self):
}
is_stripped = self.is_symbols_stripped()
if is_stripped:
severity = 'info'
severity = INFO
desc = 'Symbols are stripped.'
else:
severity = 'warning'
severity = WARNING
desc = 'Symbols are available.'
elf_dict['symbol'] = {
'is_stripped': is_stripped,
Expand Down Expand Up @@ -176,6 +235,34 @@ def has_canary(self):
pass
return False

def relro(self):
try:
gnu_relro = lief.ELF.SEGMENT_TYPES.GNU_RELRO
bind_now_flag = lief.ELF.DYNAMIC_FLAGS.BIND_NOW
flags_tag = lief.ELF.DYNAMIC_TAGS.FLAGS
flags1_tag = lief.ELF.DYNAMIC_TAGS.FLAGS_1
now_flag = lief.ELF.DYNAMIC_FLAGS_1.NOW

if self.is_dart():
return NA

if not self.elf.get(gnu_relro):
return NO_RELRO

flags = self.elf.get(flags_tag)
bind_now = flags and bind_now_flag in flags

flags1 = self.elf.get(flags1_tag)
now = flags1 and now_flag in flags1

if bind_now or now:
return FULL_RELRO
else:
return PARTIAL_RELRO
except lief.not_found:
pass
return NO_RELRO

def rpath(self):
try:
rpath = lief.ELF.DYNAMIC_TAGS.RPATH
Expand All @@ -191,10 +278,14 @@ def runpath(self):
return False

def is_symbols_stripped(self):
for i in self.elf.static_symbols:
if i:
return False
return True
try:
return nm_is_debug_symbol_stripped(
self.elf_path)
except Exception:
for i in self.elf.static_symbols:
if i:
return False
return True

def fortify(self):
fortified_funcs = []
Expand Down
Loading

0 comments on commit 78e9563

Please sign in to comment.