Improved AMI UCP > NAL unpacking

Fix potential illegal path traversals
This commit is contained in:
platomav 2022-04-15 18:17:58 +03:00
parent 672b4b2321
commit 40686d5edf
13 changed files with 102 additions and 43 deletions

View file

@ -7,7 +7,7 @@ AMI BIOS Guard Extractor
Copyright (C) 2018-2022 Plato Mavropoulos Copyright (C) 2018-2022 Plato Mavropoulos
""" """
title = 'AMI BIOS Guard Extractor v4.0_a5' title = 'AMI BIOS Guard Extractor v4.0_a6'
import os import os
import re import re
@ -20,7 +20,7 @@ sys.dont_write_bytecode = True
from common.externals import get_bgs_tool from common.externals import get_bgs_tool
from common.num_ops import get_ordinal from common.num_ops import get_ordinal
from common.path_ops import safe_name from common.path_ops import get_safe_name
from common.patterns import PAT_AMI_PFAT from common.patterns import PAT_AMI_PFAT
from common.struct_ops import get_struct, char, uint8_t, uint16_t, uint32_t from common.struct_ops import get_struct, char, uint8_t, uint16_t, uint32_t
from common.system import script_init, argparse_init, printer from common.system import script_init, argparse_init, printer
@ -139,7 +139,7 @@ def get_ami_pfat(input_buffer):
return match, buffer return match, buffer
def get_file_name(index, title): def get_file_name(index, title):
return safe_name('%0.2d -- %s' % (index, title)) return get_safe_name('%0.2d -- %s' % (index, title))
def parse_bg_script(script_data, padding): def parse_bg_script(script_data, padding):
is_opcode_div = len(script_data) % 8 == 0 is_opcode_div = len(script_data) % 8 == 0
@ -224,7 +224,7 @@ def parse_pfat_file(buffer, output_path, padding):
extract_name = os.path.basename(output_path) extract_name = os.path.basename(output_path)
extract_path = os.path.join(output_path + '_extracted', '') extract_path = os.path.join(output_path + '_extracted')
if os.path.isdir(extract_path): shutil.rmtree(extract_path) if os.path.isdir(extract_path): shutil.rmtree(extract_path)

View file

@ -7,7 +7,7 @@ AMI UCP BIOS Extractor
Copyright (C) 2021-2022 Plato Mavropoulos Copyright (C) 2021-2022 Plato Mavropoulos
""" """
title = 'AMI UCP BIOS Extractor v2.0_a6' title = 'AMI UCP BIOS Extractor v2.0_a7'
import os import os
import re import re
@ -16,6 +16,7 @@ import shutil
import struct import struct
import ctypes import ctypes
import contextlib import contextlib
from pathlib import Path, PurePath
# Stop __pycache__ generation # Stop __pycache__ generation
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
@ -23,10 +24,11 @@ sys.dont_write_bytecode = True
from common.a7z_comp import a7z_decompress, is_7z_supported from common.a7z_comp import a7z_decompress, is_7z_supported
from common.checksums import get_chk_16 from common.checksums import get_chk_16
from common.efi_comp import efi_decompress, is_efi_compressed from common.efi_comp import efi_decompress, is_efi_compressed
from common.path_ops import safe_name from common.path_ops import get_safe_name, get_safe_path
from common.patterns import PAT_AMI_UCP, PAT_INTEL_ENG from common.patterns import PAT_AMI_UCP, PAT_INTEL_ENG
from common.struct_ops import get_struct, char, uint8_t, uint16_t, uint32_t from common.struct_ops import get_struct, char, uint8_t, uint16_t, uint32_t
from common.system import script_init, argparse_init, printer from common.system import script_init, argparse_init, printer
from common.text_ops import to_string
from AMI_PFAT_Extract import get_ami_pfat, parse_pfat_file from AMI_PFAT_Extract import get_ami_pfat, parse_pfat_file
@ -211,7 +213,7 @@ def ucp_extract(buffer, out_path, ucp_tag='@UAF', padding=0, is_checksum=False):
printer('Utility Configuration Program', padding) printer('Utility Configuration Program', padding)
extract_path = os.path.join(out_path + '_extracted', '') extract_path = os.path.join(out_path + '_extracted')
if os.path.isdir(extract_path): shutil.rmtree(extract_path) if os.path.isdir(extract_path): shutil.rmtree(extract_path)
@ -259,7 +261,7 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na
is_comp = uaf_mod.CompressSize != uaf_mod.OriginalSize # Detect @UAF|@HPU Module EFI Compression is_comp = uaf_mod.CompressSize != uaf_mod.OriginalSize # Detect @UAF|@HPU Module EFI Compression
if uaf_tag in nal_dict: uaf_name = nal_dict[uaf_tag] # Always prefer @NAL naming first if uaf_tag in nal_dict: uaf_name = nal_dict[uaf_tag][1] # Always prefer @NAL naming first
elif uaf_tag in UAF_TAG_DICT: uaf_name = UAF_TAG_DICT[uaf_tag][0] # Otherwise use built-in naming elif uaf_tag in UAF_TAG_DICT: uaf_name = UAF_TAG_DICT[uaf_tag][0] # Otherwise use built-in naming
elif uaf_tag == '@ROM': uaf_name = 'BIOS.bin' # BIOS/PFAT Firmware (w/o Signature) elif uaf_tag == '@ROM': uaf_name = 'BIOS.bin' # BIOS/PFAT Firmware (w/o Signature)
elif uaf_tag.startswith('@R0'): uaf_name = 'BIOS_0%s.bin' % uaf_tag[3:] # BIOS/PFAT Firmware elif uaf_tag.startswith('@R0'): uaf_name = 'BIOS_0%s.bin' % uaf_tag[3:] # BIOS/PFAT Firmware
@ -278,10 +280,16 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na
# Check if unknown @UAF|@HPU Module Tag is present in @NAL but not in built-in dictionary # Check if unknown @UAF|@HPU Module Tag is present in @NAL but not in built-in dictionary
if uaf_tag in nal_dict and uaf_tag not in UAF_TAG_DICT and not uaf_tag.startswith(('@ROM','@R0','@S0','@DR','@DS')): if uaf_tag in nal_dict and uaf_tag not in UAF_TAG_DICT and not uaf_tag.startswith(('@ROM','@R0','@S0','@DR','@DS')):
printer('Note: Detected new AMI UCP Module %s (%s) in @NAL!' % (uaf_tag, nal_dict[uaf_tag]), padding + 4, pause=True) printer('Note: Detected new AMI UCP Module %s (%s) in @NAL!' % (uaf_tag, nal_dict[uaf_tag][1]), padding + 4, pause=True)
# Generate @UAF|@HPU Module File name, depending on whether decompression will be required # Generate @UAF|@HPU Module File name, depending on whether decompression will be required
uaf_fname = os.path.join(extract_path, safe_name(uaf_name + ('.temp' if is_comp else uaf_fext))) uaf_sname = get_safe_name(uaf_name + ('.temp' if is_comp else uaf_fext))
if uaf_tag in nal_dict:
uaf_npath = get_safe_path(extract_path, nal_dict[uaf_tag][0])
Path.mkdir(Path(uaf_npath), parents=True, exist_ok=True)
uaf_fname = get_safe_path(uaf_npath, uaf_sname)
else:
uaf_fname = get_safe_path(extract_path, uaf_sname)
if is_checksum: chk16_validate(uaf_data_all, uaf_tag, padding + 4) if is_checksum: chk16_validate(uaf_data_all, uaf_tag, padding + 4)
@ -375,15 +383,19 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na
# Parse all @NAL Module Entries # Parse all @NAL Module Entries
for info in nal_info: for info in nal_info:
info_tag,info_val = info.split(':',1) info_tag,info_value = info.split(':',1)
printer(info_tag + ' : ' + info_val, padding + 8, False) # Print @NAL Module Tag-Path Info printer(info_tag + ' : ' + info_value, padding + 8, False) # Print @NAL Module Tag-Path Info
nal_dict[info_tag] = os.path.basename(info_val) # Assign a file name (w/o path) to each Tag info_part = PurePath(info_value.replace('\\', os.sep)).parts # Split path in parts
info_path = to_string(info_part[1:-1], os.sep) # Get path without drive/root or file
info_name = info_part[-1] # Get file from last path part
nal_dict[info_tag] = (info_path,info_name) # Assign a file path & name to each Tag
# Parse Insyde BIOS @UAF|@HPU Module (@INS) # Parse Insyde BIOS @UAF|@HPU Module (@INS)
if uaf_tag == '@INS' and is_7z_supported(uaf_fname): if uaf_tag == '@INS' and is_7z_supported(uaf_fname):
ins_dir = os.path.join(extract_path, safe_name(uaf_tag + '_nested-SFX')) # Generate extraction directory ins_dir = os.path.join(extract_path, get_safe_name(uaf_tag + '_nested-SFX')) # Generate extraction directory
printer('Insyde BIOS 7z SFX Archive:', padding + 4) printer('Insyde BIOS 7z SFX Archive:', padding + 4)
@ -394,7 +406,7 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na
pfat_match,pfat_buffer = get_ami_pfat(uaf_data_raw) pfat_match,pfat_buffer = get_ami_pfat(uaf_data_raw)
if pfat_match: if pfat_match:
pfat_dir = os.path.join(extract_path, safe_name(uaf_name)) pfat_dir = os.path.join(extract_path, get_safe_name(uaf_name))
parse_pfat_file(pfat_buffer, pfat_dir, padding + 4) parse_pfat_file(pfat_buffer, pfat_dir, padding + 4)
@ -410,7 +422,7 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na
# Parse Nested AMI UCP Structure # Parse Nested AMI UCP Structure
if nested_uaf_off: if nested_uaf_off:
uaf_dir = os.path.join(extract_path, safe_name(uaf_tag + '_nested-UCP')) # Generate extraction directory uaf_dir = os.path.join(extract_path, get_safe_name(uaf_tag + '_nested-UCP')) # Generate extraction directory
ucp_extract(nested_uaf_bin, uaf_dir, nested_uaf_tag, padding + 4, is_checksum) # Call recursively ucp_extract(nested_uaf_bin, uaf_dir, nested_uaf_tag, padding + 4, is_checksum) # Call recursively

View file

@ -7,7 +7,7 @@ Dell PFS Update Extractor
Copyright (C) 2018-2022 Plato Mavropoulos Copyright (C) 2018-2022 Plato Mavropoulos
""" """
title = 'Dell PFS Update Extractor v6.0_a4' title = 'Dell PFS Update Extractor v6.0_a5'
import os import os
import io import io
@ -22,7 +22,7 @@ import contextlib
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
from common.checksums import get_chk_8_xor from common.checksums import get_chk_8_xor
from common.path_ops import safe_name from common.path_ops import get_safe_name
from common.patterns import PAT_DELL_HDR, PAT_DELL_FTR, PAT_DELL_PKG from common.patterns import PAT_DELL_HDR, PAT_DELL_FTR, PAT_DELL_PKG
from common.struct_ops import get_struct, char, uint8_t, uint16_t, uint32_t, uint64_t from common.struct_ops import get_struct, char, uint8_t, uint16_t, uint32_t, uint64_t
from common.system import script_init, argparse_init, printer from common.system import script_init, argparse_init, printer
@ -243,7 +243,7 @@ def pfs_section_parse(zlib_data, zlib_start, output_path, pfs_name, pfs_index, p
printer('Extracting Dell PFS %d >%s > %s' % (pfs_index, pfs_name, section_name), padding) printer('Extracting Dell PFS %d >%s > %s' % (pfs_index, pfs_name, section_name), padding)
# Set PFS ZLIB Section extraction sub-directory path # Set PFS ZLIB Section extraction sub-directory path
section_path = os.path.join(output_path, section_name) section_path = os.path.join(output_path, get_safe_name(section_name))
# Delete existing extraction sub-directory (not in recursions) # Delete existing extraction sub-directory (not in recursions)
if os.path.isdir(section_path) and not is_rec: shutil.rmtree(section_path) if os.path.isdir(section_path) and not is_rec: shutil.rmtree(section_path)
@ -406,7 +406,7 @@ def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, output_path, pfs_padd, i
name_start = info_start + PFS_INFO_LEN + PFS_NAME_LEN # PFS Entry's FileName start offset name_start = info_start + PFS_INFO_LEN + PFS_NAME_LEN # PFS Entry's FileName start offset
name_size = entry_info_mod.CharacterCount * 2 # PFS Entry's FileName buffer total size name_size = entry_info_mod.CharacterCount * 2 # PFS Entry's FileName buffer total size
name_data = filename_info[name_start:name_start + name_size] # PFS Entry's FileName buffer name_data = filename_info[name_start:name_start + name_size] # PFS Entry's FileName buffer
entry_name = safe_name(name_data.decode('utf-16').strip()) # PFS Entry's FileName value entry_name = get_safe_name(name_data.decode('utf-16').strip()) # PFS Entry's FileName value
# Show PFS FileName Structure info # Show PFS FileName Structure info
if is_structure: if is_structure:
@ -443,7 +443,7 @@ def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, output_path, pfs_padd, i
# As Nested PFS Entry Name, we'll use the actual PFS File Name # As Nested PFS Entry Name, we'll use the actual PFS File Name
# Replace common Windows reserved/illegal filename characters # Replace common Windows reserved/illegal filename characters
entry_name = safe_name(entry_info.FileName.decode('utf-8').strip('.exe')) entry_name = get_safe_name(entry_info.FileName.decode('utf-8').strip('.exe'))
# As Nested PFS Entry Version, we'll use the actual PFS File Version # As Nested PFS Entry Version, we'll use the actual PFS File Version
entry_version = entry_info.FileVersion.decode('utf-8') entry_version = entry_info.FileVersion.decode('utf-8')
@ -537,7 +537,7 @@ def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, output_path, pfs_padd, i
sub_pfs_name = ' %s v%s' % (info_all[pfs_count - 2][1], info_all[pfs_count - 2][2]) if info_all else ' UNKNOWN' sub_pfs_name = ' %s v%s' % (info_all[pfs_count - 2][1], info_all[pfs_count - 2][2]) if info_all else ' UNKNOWN'
# Set the sub-PFS output path (create sub-folders for each sub-PFS and its ZLIB sections) # Set the sub-PFS output path (create sub-folders for each sub-PFS and its ZLIB sections)
sub_pfs_path = os.path.join(output_path, str(pfs_count) + sub_pfs_name) sub_pfs_path = os.path.join(output_path, str(pfs_count) + get_safe_name(sub_pfs_name))
# Recursively call the PFS ZLIB Section Parser function for the sub-PFS Volume (pfs_index = pfs_count) # Recursively call the PFS ZLIB Section Parser function for the sub-PFS Volume (pfs_index = pfs_count)
pfs_section_parse(entry_data, offset, sub_pfs_path, sub_pfs_name, pfs_count, pfs_count, True, pfs_padd + 4, is_structure, is_advanced) pfs_section_parse(entry_data, offset, sub_pfs_path, sub_pfs_name, pfs_count, pfs_count, True, pfs_padd + 4, is_structure, is_advanced)
@ -844,7 +844,7 @@ def chk_pfs_ftr(footer_buffer, data_buffer, data_size, text, padding, is_structu
def pfs_file_write(bin_buff, bin_name, bin_type, full_name, out_path, padding, is_structure=True, is_advanced=True): def pfs_file_write(bin_buff, bin_name, bin_type, full_name, out_path, padding, is_structure=True, is_advanced=True):
# Store Data/Metadata Signature (advanced users only) # Store Data/Metadata Signature (advanced users only)
if bin_name.startswith('sign'): if bin_name.startswith('sign'):
final_name = '%s.%s.sig' % (safe_name(full_name), bin_name.split('_')[1]) final_name = '%s.%s.sig' % (get_safe_name(full_name), bin_name.split('_')[1])
final_path = os.path.join(out_path, final_name) final_path = os.path.join(out_path, final_name)
with open(final_path, 'wb') as pfs_out: pfs_out.write(bin_buff) # Write final Data/Metadata Signature with open(final_path, 'wb') as pfs_out: pfs_out.write(bin_buff) # Write final Data/Metadata Signature
@ -857,7 +857,7 @@ def pfs_file_write(bin_buff, bin_name, bin_type, full_name, out_path, padding, i
# Some Data may be Text or XML files with useful information for non-advanced users # Some Data may be Text or XML files with useful information for non-advanced users
is_text,final_data,file_ext,write_mode = bin_is_text(bin_buff, bin_type, bin_name == 'meta', padding, is_structure, is_advanced) is_text,final_data,file_ext,write_mode = bin_is_text(bin_buff, bin_type, bin_name == 'meta', padding, is_structure, is_advanced)
final_name = '%s%s' % (safe_name(full_name), bin_ext[:-4] + file_ext if is_text else bin_ext) final_name = '%s%s' % (get_safe_name(full_name), bin_ext[:-4] + file_ext if is_text else bin_ext)
final_path = os.path.join(out_path, final_name) final_path = os.path.join(out_path, final_name)
with open(final_path, write_mode) as pfs_out: pfs_out.write(final_data) # Write final Data/Metadata Payload with open(final_path, write_mode) as pfs_out: pfs_out.write(final_data) # Write final Data/Metadata Payload

View file

@ -42,4 +42,4 @@ def a7z_decompress(in_path, out_path, in_name, padding, static=False):
printer('Succesfull %s decompression via 7-Zip!' % in_name, padding) printer('Succesfull %s decompression via 7-Zip!' % in_name, padding)
return 0 return 0

View file

@ -18,4 +18,4 @@ def get_chk_8_xor(data, value=0):
value ^= 0x0 value ^= 0x0
return value return value

View file

@ -48,4 +48,4 @@ def efi_decompress(in_path, out_path, padding, comp_type='--uefi'):
printer('Succesfull EFI/Tiano decompression via TianoCompress!', padding) printer('Succesfull EFI/Tiano decompression via TianoCompress!', padding)
return 0 return 0

View file

@ -8,4 +8,4 @@ def get_bgs_tool():
except: except:
BigScript = None BigScript = None
return BigScript return BigScript

View file

@ -8,4 +8,4 @@ def get_ordinal(number):
v = number % 100 v = number % 100
return f'{number}{s[v % 10]}' if v > 13 else f'{number}{s[v]}' return f'{number}{s[v % 10]}' if v > 13 else f'{number}{s[v]}'

View file

@ -7,14 +7,55 @@ import sys
import inspect import inspect
from pathlib import Path from pathlib import Path
from common.text_ops import to_string
# Fix illegal/reserved Windows characters # Fix illegal/reserved Windows characters
def safe_name(in_name): def get_safe_name(in_name):
raw_name = repr(in_name).strip("'") raw_name = repr(in_name).strip("'")
fix_name = re.sub(r'[\\/*?:"<>|]', '_', raw_name) fix_name = re.sub(r'[\\/*?:"<>|]', '_', raw_name)
return fix_name return fix_name
# Check and attempt to fix illegal/unsafe OS path traversals
def get_safe_path(base_path, user_paths, follow_symlinks=False):
# Convert user path(s) to string w/ OS separators
user_path = to_string(user_paths, os.sep)
# Create target path from base + requested user path
target_path = get_norm_path(base_path, user_path)
# Check if target path is OS illegal/unsafe
if is_safe_path(base_path, target_path, follow_symlinks):
return target_path
# Re-create target path from base + leveled/safe illegal "path" (now file)
nuked_path = get_norm_path(base_path, get_safe_name(user_path))
# Check if illegal path leveling worked
if is_safe_path(base_path, nuked_path, follow_symlinks):
return nuked_path
# Still illegal, create fallback base path + placeholder file
failed_path = get_norm_path(base_path, 'illegal_path_traversal')
return failed_path
# Check for illegal/unsafe OS path traversal
def is_safe_path(base_path, target_path, follow_symlinks=True):
if follow_symlinks:
actual_path = os.path.realpath(target_path)
else:
actual_path = os.path.abspath(target_path)
common_path = os.path.commonpath((base_path, actual_path))
return base_path == common_path
# Create normalized base path + OS separator + user path
def get_norm_path(base_path, user_path):
return os.path.normpath(base_path + os.sep + user_path)
# Walk path to get all files # Walk path to get all files
def get_path_files(in_path): def get_path_files(in_path):
path_files = [] path_files = []
@ -76,4 +117,4 @@ def get_script_dir(follow_symlinks=True):
if follow_symlinks: if follow_symlinks:
path = os.path.realpath(path) path = os.path.realpath(path)
return os.path.dirname(path) return os.path.dirname(path)

View file

@ -5,7 +5,7 @@ import re
PAT_AMI_PFAT = re.compile(b'_AMIPFAT.AMI_BIOS_GUARD_FLASH_CONFIGURATIONS', re.DOTALL) PAT_AMI_PFAT = re.compile(b'_AMIPFAT.AMI_BIOS_GUARD_FLASH_CONFIGURATIONS', re.DOTALL)
PAT_AMI_UCP = re.compile(br'@(UAF|HPU).{12}@', re.DOTALL) PAT_AMI_UCP = re.compile(br'@(UAF|HPU).{12}@', re.DOTALL)
PAT_DELL_PKG = re.compile(br'\x72\x13\x55\x00.{45}7zXZ', re.DOTALL)
PAT_DELL_HDR = re.compile(br'\xEE\xAA\x76\x1B\xEC\xBB\x20\xF1\xE6\x51.\x78\x9C', re.DOTALL)
PAT_DELL_FTR = re.compile(br'\xEE\xAA\xEE\x8F\x49\x1B\xE8\xAE\x14\x37\x90') PAT_DELL_FTR = re.compile(br'\xEE\xAA\xEE\x8F\x49\x1B\xE8\xAE\x14\x37\x90')
PAT_INTEL_ENG = re.compile(br'\x04\x00{3}[\xA1\xE1]\x00{3}.{8}\x86\x80.{9}\x00\$((MN2)|(MAN))', re.DOTALL) PAT_DELL_HDR = re.compile(br'\xEE\xAA\x76\x1B\xEC\xBB\x20\xF1\xE6\x51.\x78\x9C', re.DOTALL)
PAT_DELL_PKG = re.compile(br'\x72\x13\x55\x00.{45}7zXZ', re.DOTALL)
PAT_INTEL_ENG = re.compile(br'\x04\x00{3}[\xA1\xE1]\x00{3}.{8}\x86\x80.{9}\x00\$((MN2)|(MAN))', re.DOTALL)

View file

@ -21,4 +21,4 @@ def get_struct(buffer, start_offset, class_name, param_list=None):
ctypes.memmove(ctypes.addressof(structure), struct_data, fit_len) ctypes.memmove(ctypes.addressof(structure), struct_data, fit_len)
return structure return structure

View file

@ -6,7 +6,7 @@ import ctypes
import argparse import argparse
import traceback import traceback
from common.text_ops import padder from common.text_ops import padder, to_string
from common.path_ops import process_input_files from common.path_ops import process_input_files
# Get Python Version (tuple) # Get Python Version (tuple)
@ -106,11 +106,8 @@ def nice_exc_handler(exc_type, exc_value, tb):
sys.exit(3) sys.exit(3)
# Show message(s) while controlling padding, newline, pausing & separator # Show message(s) while controlling padding, newline, pausing & separator
def printer(in_message='', padd_count=0, new_line=True, pause=False, sep_char=' '): def printer(in_message='', padd_count=0, new_line=True, pause=False, sep_char=' '):
if type(in_message).__name__ in ('list','tuple'): message = to_string(in_message, sep_char)
message = sep_char.join(map(str, in_message))
else:
message = str(in_message)
padding = padder(padd_count) padding = padder(padd_count)
@ -118,4 +115,4 @@ def printer(in_message='', padd_count=0, new_line=True, pause=False, sep_char='
output = newline + padding + message output = newline + padding + message
(input if pause and not is_auto_exit() else print)(output) (input if pause and not is_auto_exit() else print)(output)

View file

@ -3,4 +3,13 @@
# Generate padding (spaces or tabs) # Generate padding (spaces or tabs)
def padder(padd_count, tab=False): def padder(padd_count, tab=False):
return ('\t' if tab else ' ') * padd_count return ('\t' if tab else ' ') * padd_count
# Get String from given input object
def to_string(input_object, sep_char=''):
if type(input_object).__name__ in ('list','tuple'):
output_string = sep_char.join(map(str, input_object))
else:
output_string = str(input_object)
return output_string