mirror of
https://github.com/ivellioscolin/pykd.git
synced 2025-04-19 11:03:23 +08:00
[0.3.x] branch : snippets/ntobj.py
git-svn-id: https://pykd.svn.codeplex.com/svn@86278 9b283d60-5439-405e-af05-b73fd8c4d996
This commit is contained in:
parent
b46b74cb71
commit
6ecf53e19d
@ -53,11 +53,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "snippets", "snippets", "{AA
|
||||
snippets\cr4.py = snippets\cr4.py
|
||||
snippets\ctlcode.py = snippets\ctlcode.py
|
||||
snippets\export.py = snippets\export.py
|
||||
snippets\findhandle.py = snippets\findhandle.py
|
||||
snippets\findtag.py = snippets\findtag.py
|
||||
snippets\gdt.py = snippets\gdt.py
|
||||
snippets\help.py = snippets\help.py
|
||||
snippets\iat.py = snippets\iat.py
|
||||
snippets\ndis.py = snippets\ndis.py
|
||||
snippets\ntobj.py = snippets\ntobj.py
|
||||
snippets\pytowiki.py = snippets\pytowiki.py
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
@ -34,6 +34,7 @@ BOOST_PYTHON_FUNCTION_OVERLOADS( startProcess_, startProcess, 1, 2 );
|
||||
BOOST_PYTHON_FUNCTION_OVERLOADS( detachProcess_, kdlib::detachProcess, 0, 1 );
|
||||
BOOST_PYTHON_FUNCTION_OVERLOADS( terminateProcess_, kdlib::terminateProcess, 0, 1 );
|
||||
BOOST_PYTHON_FUNCTION_OVERLOADS( attachKernel_, attachKernel, 0, 1 );
|
||||
BOOST_PYTHON_FUNCTION_OVERLOADS( evaluate_, evaluate, 1, 2 );
|
||||
|
||||
BOOST_PYTHON_FUNCTION_OVERLOADS( dprint_, kdlib::dprint, 1, 2 );
|
||||
BOOST_PYTHON_FUNCTION_OVERLOADS( dprintln_, kdlib::dprintln, 1, 2 );
|
||||
@ -119,8 +120,8 @@ BOOST_PYTHON_MODULE( pykd )
|
||||
|
||||
python::def( "breakin", &targetBreak,
|
||||
"Break into debugger" );
|
||||
python::def( "expr", &evaluate,
|
||||
"Evaluate windbg expression" );
|
||||
python::def( "expr", &evaluate, evaluate_( boost::python::args( "expression", "cplusplus" ),
|
||||
"Evaluate windbg expression" ) );
|
||||
python::def( "dbgCommand", &debugCommand,
|
||||
"Run a debugger's command and return it's result as a string" );
|
||||
python::def( "go", &targetGo,
|
||||
|
49
snippets/findhandle.py
Normal file
49
snippets/findhandle.py
Normal file
@ -0,0 +1,49 @@
|
||||
from pykd import *
|
||||
import ntobj
|
||||
import sys
|
||||
|
||||
nt = module("nt")
|
||||
|
||||
|
||||
def findHanle(objaddr):
|
||||
|
||||
processList = typedVarList( nt.PsActiveProcessHead, "nt!_EPROCESS", "ActiveProcessLinks" )
|
||||
|
||||
for process in processList:
|
||||
|
||||
dprintln( "search in process %x " % process.UniqueProcessId + "".join( [chr(i) for i in process.ImageFileName if i != 0] ) )
|
||||
|
||||
if process.ObjectTable == 0:
|
||||
continue
|
||||
|
||||
objects = ntobj.getListByHandleTable( process.ObjectTable )
|
||||
for obj in objects:
|
||||
if obj[0] == objaddr:
|
||||
dprintln("\tHandle: %x" % ( obj[1],) )
|
||||
|
||||
|
||||
def usage():
|
||||
dprintln("!py findhandle object_address")
|
||||
|
||||
def main():
|
||||
|
||||
if not isKernelDebugging():
|
||||
dprintln("This script for kernel debugging only")
|
||||
return
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
usage();
|
||||
return;
|
||||
|
||||
objaddr = expr(sys.argv[1])
|
||||
|
||||
objectType = ntobj.getType(objaddr)
|
||||
|
||||
dprintln("Object Type: " + ntobj.getObjectName(objectType) )
|
||||
dprintln("Object Name: "+ ntobj.getObjectName(objaddr) )
|
||||
dprintln("")
|
||||
|
||||
findHanle( objaddr )
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
439
snippets/ntobj.py
Normal file
439
snippets/ntobj.py
Normal file
@ -0,0 +1,439 @@
|
||||
"""
|
||||
Work with NT Object tree manager
|
||||
|
||||
getType(p)
|
||||
Return object type address by object address
|
||||
|
||||
getObjectNameInfo(p)
|
||||
Return typedVar of nt!_OBJECT_HEADER_NAME_INFO or None
|
||||
|
||||
getObjectName(p)
|
||||
Return string of object name. If can not get name - empty string
|
||||
|
||||
buildObjectName(p)
|
||||
Return string of full object name. If can not get name - empty string
|
||||
|
||||
getListByHandleTable(tableHandles=None, objTypeAddr=0, containHeaders=True)
|
||||
Return list of objects from table of handles
|
||||
|
||||
getListByDirectoryObject(p, objTypeAddr=0)
|
||||
Return list of objects from object directory
|
||||
|
||||
getObjectByName(name, caseSensitive=False)
|
||||
Return address of object by full name. If error (f.e. not exist) - None
|
||||
|
||||
"""
|
||||
|
||||
from pykd import *
|
||||
|
||||
nt = module("nt")
|
||||
|
||||
# optimization
|
||||
OBJECT_HEADER = nt.type("_OBJECT_HEADER")
|
||||
HANDLE_TABLE_ENTRY = nt.type("_HANDLE_TABLE_ENTRY")
|
||||
OBJECT_DIRECTORY_ENTRY = nt.type("_OBJECT_DIRECTORY_ENTRY")
|
||||
OBJECT_HEADER_NAME_INFO = nt.type("_OBJECT_HEADER_NAME_INFO")
|
||||
|
||||
|
||||
|
||||
def getTypeWin7(p):
|
||||
"""
|
||||
Get object header by object pointer
|
||||
Implementation for Win7+
|
||||
"""
|
||||
objHeader = containingRecord(p, OBJECT_HEADER, "Body")
|
||||
tableTypeIndex = nt.ObTypeIndexTable
|
||||
return ptrPtr(tableTypeIndex + (ptrSize() * objHeader.TypeIndex))
|
||||
|
||||
def getTypeLegacy(p):
|
||||
"""
|
||||
Get object header by object pointer
|
||||
Implementation for before Win7
|
||||
"""
|
||||
objHeader = containingRecord(p, OBJECT_HEADER, "Body")
|
||||
return objHeader.Type
|
||||
|
||||
# Select platform-specific function for getting object header
|
||||
# Select key body type: nt!CmpKeyObjectType or nt!CmKeyObjectType
|
||||
if (ptrWord( nt.NtBuildNumber ) >= 7600):
|
||||
getType = getTypeWin7
|
||||
# _kcbObjectType = expr("poi(nt!CmKeyObjectType)")
|
||||
else:
|
||||
getType = getTypeLegacy
|
||||
# _kcbObjectType = expr("poi(nt!CmpKeyObjectType)")
|
||||
|
||||
|
||||
def getObjectNameInfoFromHeader(p):
|
||||
"""
|
||||
Get object name information from field NameInfoOffset of object header
|
||||
"""
|
||||
objHeader = containingRecord(p, OBJECT_HEADER, "Body")
|
||||
if (0 == objHeader.NameInfoOffset):
|
||||
return None
|
||||
return typedVar(OBJECT_HEADER_NAME_INFO, objHeader.getAddress() - objHeader.NameInfoOffset)
|
||||
|
||||
def getObjectNameInfoFromInfoMask(p):
|
||||
"""
|
||||
Get object name information from field NameInfoOffset of object header
|
||||
"""
|
||||
objHeader = containingRecord(p, OBJECT_HEADER, "Body")
|
||||
if (0 == (objHeader.InfoMask & 2)):
|
||||
return None
|
||||
offsetNameInfo = ptrByte( nt.ObpInfoMaskToOffset + (objHeader.InfoMask & 3) )
|
||||
if (0 == offsetNameInfo):
|
||||
return None
|
||||
return typedVar(OBJECT_HEADER_NAME_INFO, objHeader.getAddress() - offsetNameInfo)
|
||||
|
||||
|
||||
# Select platform-specific function for getting name of object
|
||||
getObjectNameInfo = None
|
||||
try:
|
||||
OBJECT_HEADER.NameInfoOffset
|
||||
getObjectNameInfo = getObjectNameInfoFromHeader
|
||||
except TypeException:
|
||||
getObjectNameInfo = getObjectNameInfoFromInfoMask
|
||||
|
||||
|
||||
def getObjectName(p):
|
||||
"""
|
||||
Get object name by name information
|
||||
"""
|
||||
nameInfo = getObjectNameInfo(p)
|
||||
if (None == nameInfo):
|
||||
return ""
|
||||
return loadUnicodeString(nameInfo.Name.getAddress())
|
||||
|
||||
def buildObjectName(p):
|
||||
"""
|
||||
Get object name by name information (parent directory handled)
|
||||
"""
|
||||
objectFullName = ""
|
||||
|
||||
nameInfo = getObjectNameInfo(p)
|
||||
if None == nameInfo:
|
||||
return objectFullName
|
||||
namePart = loadUnicodeString(nameInfo.Name.getAddress())
|
||||
if namePart != "\\":
|
||||
objectFullName = namePart + "\\" + objectFullName
|
||||
else:
|
||||
objectFullName = "\\" + objectFullName
|
||||
|
||||
while (0 != nameInfo.Directory):
|
||||
p = nameInfo.Directory
|
||||
nameInfo = getObjectNameInfo(p)
|
||||
if (None == nameInfo):
|
||||
return "...\\" + objectFullName
|
||||
namePart = loadUnicodeString(nameInfo.Name.getAddress())
|
||||
if namePart != "\\":
|
||||
objectFullName = namePart + "\\" + objectFullName
|
||||
else:
|
||||
objectFullName = "\\" + objectFullName
|
||||
|
||||
return objectFullName
|
||||
|
||||
|
||||
HANDLE_VALUE_INC = 4
|
||||
HT_PAGE_SIZE = 4096
|
||||
HT_ENTRY_SIZE = (2 * ptrSize())
|
||||
HT_LOWLEVEL_COUNT = HT_PAGE_SIZE // HT_ENTRY_SIZE
|
||||
HT_MIDLEVEL_COUNT = HT_PAGE_SIZE // ptrSize()
|
||||
|
||||
def getListByHandleTable(tableHandles=None, objTypeAddr=0, containHeaders=True):
|
||||
"""
|
||||
Build list of objects from target handle table
|
||||
|
||||
Parameter objTypeAddr if not 0 used for getting object of specific type,
|
||||
otherwise get object of all types
|
||||
|
||||
Parameter containHeaders used to interpret table contents:
|
||||
if containHeaders=True then table contains pointers to nt!_OBJECT_HEADER,
|
||||
otherwise table contains pointers to objects
|
||||
"""
|
||||
|
||||
def getByHandleEntry(entryHandle, containHeader):
|
||||
"""
|
||||
Query object pointer by handle entry from handle table
|
||||
"""
|
||||
if (0 == entryHandle):
|
||||
return 0
|
||||
|
||||
HandleEntry = typedVar( HANDLE_TABLE_ENTRY, entryHandle)
|
||||
if (0xFFFFFFFE == HandleEntry.NextFreeTableEntry):
|
||||
return 0
|
||||
|
||||
p = ptrPtr(HandleEntry.getAddress()) & 0xFFFFFFFFFFFFFFF8
|
||||
if (0 == p):
|
||||
return 0
|
||||
|
||||
if (containHeader):
|
||||
objHeader = typedVar( OBJECT_HEADER, p)
|
||||
p = objHeader.Body.getAddress()
|
||||
return p
|
||||
|
||||
def getListByHandleTableL0(pTableContent, nMaxHandleIndex, objTypeAddr, containHeaders):
|
||||
"""
|
||||
Build list of objects from target handle table level 0
|
||||
"""
|
||||
lstObjects = list()
|
||||
nTableLevel0Count = nMaxHandleIndex // HANDLE_VALUE_INC
|
||||
for HandleEntryIndex in range(nTableLevel0Count):
|
||||
|
||||
pHandleEntry = pTableContent + (HT_ENTRY_SIZE * HandleEntryIndex)
|
||||
p = getByHandleEntry(pHandleEntry, containHeaders)
|
||||
if (0 == p):
|
||||
continue
|
||||
|
||||
if (0 == objTypeAddr):
|
||||
lstObjects.append( ( p, HandleEntryIndex*HANDLE_VALUE_INC) )
|
||||
else:
|
||||
pCurrentType = getType(p)
|
||||
if (addr64(objTypeAddr) == addr64(pCurrentType)):
|
||||
lstObjects.append( ( p, HandleEntryIndex*HANDLE_VALUE_INC) )
|
||||
|
||||
return lstObjects
|
||||
|
||||
def getListByHandleTableL1(pTableContent, nMaxHandleIndex, objTypeAddr, containHeaders):
|
||||
"""
|
||||
Build list of objects from target handle table level 1
|
||||
"""
|
||||
lstObjects = list()
|
||||
nTableLevel1Count = (nMaxHandleIndex // HANDLE_VALUE_INC) // HT_LOWLEVEL_COUNT
|
||||
for Index in range(nTableLevel1Count):
|
||||
pTableLevel0 = ptrPtr(pTableContent + (Index * ptrSize()))
|
||||
lstObjects += getListByHandleTableL0(pTableLevel0, HT_LOWLEVEL_COUNT * HANDLE_VALUE_INC, objTypeAddr, containHeaders)
|
||||
|
||||
return lstObjects
|
||||
|
||||
def getListByHandleTableL2(pTableContent, nMaxHandleIndex, objTypeAddr, containHeaders):
|
||||
"""
|
||||
Build list of objects from target handle table level 2
|
||||
"""
|
||||
lstObjects = list()
|
||||
nTableLevel2Count = ((nMaxHandleIndex // HANDLE_VALUE_INC) // HT_LOWLEVEL_COUNT) // HT_MIDLEVEL_COUNT
|
||||
for Index in range(nTableLevel2Count):
|
||||
pTableLevel1 = ptrPtr(pTableContent + (Index * ptrSize()))
|
||||
lstObjects += getListByHandleTableL1(pTableLevel1, HT_MIDLEVEL_COUNT * HT_LOWLEVEL_COUNT * HANDLE_VALUE_INC, objTypeAddr, containHeaders)
|
||||
|
||||
return lstObjects
|
||||
|
||||
if (None == tableHandles):
|
||||
currProcess = nt.typedVar("_EPROCESS", getCurrentProcess())
|
||||
if (None == currProcess):
|
||||
dprintln("Get current process failed")
|
||||
return
|
||||
tableHandles = currProcess.ObjectTable
|
||||
|
||||
tableHandles = nt.typedVar("_HANDLE_TABLE", tableHandles)
|
||||
nMaxHandleIndex = tableHandles.NextHandleNeedingPool & 0xFFFFFFFF
|
||||
nTableLevel = (tableHandles.TableCode & 3)
|
||||
pTableContent = tableHandles.TableCode - nTableLevel
|
||||
|
||||
if (0 == nTableLevel):
|
||||
return getListByHandleTableL0(pTableContent, nMaxHandleIndex, objTypeAddr, containHeaders)
|
||||
elif (1 == nTableLevel):
|
||||
return getListByHandleTableL1(pTableContent, nMaxHandleIndex, objTypeAddr, containHeaders)
|
||||
elif (2 == nTableLevel):
|
||||
return getListByHandleTableL2(pTableContent, nMaxHandleIndex, objTypeAddr, containHeaders)
|
||||
|
||||
dprintln("ERROR: Unknown handle table level: %u" % nTableLevel)
|
||||
return list()
|
||||
|
||||
|
||||
|
||||
NUMBER_HASH_BUCKETS = 37
|
||||
|
||||
def getListByDirectoryObject(p, objTypeAddr=0):
|
||||
"""
|
||||
Build list of objects from object directory
|
||||
|
||||
Parameter objTypeAddr if not 0 used for getting object of specific type,
|
||||
otherwise get object of all types
|
||||
"""
|
||||
|
||||
if getType(p) != ptrPtr( nt.ObpDirectoryObjectType ):
|
||||
return None
|
||||
|
||||
result = list()
|
||||
|
||||
for i in range(0, NUMBER_HASH_BUCKETS):
|
||||
bucket = ptrPtr( p + (i * ptrSize()) )
|
||||
while bucket:
|
||||
bucketVar = typedVar( OBJECT_DIRECTORY_ENTRY, bucket)
|
||||
if objTypeAddr and (getType(bucketVar.Object) == objTypeAddr):
|
||||
result.append(bucketVar.Object)
|
||||
elif (not objTypeAddr):
|
||||
result.append(bucketVar.Object)
|
||||
bucket = bucketVar.ChainLink
|
||||
|
||||
return result
|
||||
|
||||
def getObjectByName(name, caseSensitive=False):
|
||||
"""
|
||||
Query address of object by full name
|
||||
"""
|
||||
|
||||
def cmpCase(s1, s2): return s1 == s2
|
||||
def cmpNoCase(s1, s2): return s1.lower() == s2.lower()
|
||||
|
||||
if not len(name):
|
||||
return None
|
||||
|
||||
if name[0] != '\\':
|
||||
return None
|
||||
|
||||
object = ptrPtr( nt.ObpRootDirectoryObject )
|
||||
|
||||
cmpFunc = cmpNoCase
|
||||
if caseSensitive:
|
||||
cmpFunc = cmpCase
|
||||
|
||||
while True:
|
||||
name = name[1:]
|
||||
if not len(name):
|
||||
break
|
||||
|
||||
tok = name.find("\\")
|
||||
if -1 != tok:
|
||||
namePart = name[:tok]
|
||||
name = name[tok:]
|
||||
else:
|
||||
namePart = name
|
||||
|
||||
if 0 == len(namePart):
|
||||
return None
|
||||
|
||||
# FIXME: use name/index hash
|
||||
lstObjects = getListByDirectoryObject(object)
|
||||
if None == lstObjects:
|
||||
return None
|
||||
|
||||
found = False
|
||||
for p in lstObjects:
|
||||
objName = getObjectName(p)
|
||||
if len(objName) and cmpFunc( namePart, objName ):
|
||||
object = p
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
return None
|
||||
|
||||
if -1 == tok:
|
||||
break
|
||||
|
||||
return object
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
# Display object kd-command
|
||||
ViewObjectCommand = {
|
||||
addr64( expr("poi(nt!IoFileObjectType)") ) : "!fileobj",
|
||||
addr64( expr("poi(nt!PsProcessType)") ) : "!process",
|
||||
addr64( expr("poi(nt!PsThreadType)") ) : "!thread",
|
||||
addr64( expr("poi(nt!IoDeviceObjectType)") ) : "!devobj",
|
||||
addr64( expr("poi(nt!SeTokenObjectType)") ) : "!token",
|
||||
# addr64( _kcbObjectType ) : "!reg kcb",
|
||||
addr64( expr("poi(nt!IoDriverObjectType)") ) : "!drvobj 7"
|
||||
}
|
||||
|
||||
def main():
|
||||
"""
|
||||
Print content of object table (handle table)
|
||||
|
||||
Usage:
|
||||
!py ntobj [table= <ADDR>] [type= <ADDR>] [headers= <BOOL>]
|
||||
|
||||
When (options can be specified in any order):
|
||||
table= <ADDR> : table of handle address. F.e. address object table
|
||||
contained in field ObjectTable structure nt!_EPROCESS
|
||||
default: nt!_EPROCESS.ObjectTable of current processes
|
||||
|
||||
type= <ADDR> : address of object type. If specified (!= 0) then print
|
||||
object of target type only.
|
||||
default: 0
|
||||
|
||||
headers= <BOOL> : table of handles format: contain pointer to object or to
|
||||
object header (nt!_OBJECT_HEADER): True or Flase
|
||||
F.e. poi(nt!PspCidTable) contains objects (processes and
|
||||
threads), and nt!_EPROCESS.ObjectTable contains headers
|
||||
default: True
|
||||
Examples:
|
||||
<link cmd=\"!py ntobj\">!py ntobj</link>
|
||||
Print object table of current process
|
||||
|
||||
<link cmd=\"!py ntobj type= poi(nt!IoFileObjectType)\">!py ntobj type= poi(nt!IoFileObjectType)</link>
|
||||
Print object table (only _FILE_OBJECT *) of current process
|
||||
|
||||
<link cmd=\"!py ntobj table= poi(poi(nt!PsInitialSystemProcess)+@@C++(#FIELD_OFFSET(nt!_EPROCESS,ObjectTable)))\">!py ntobj table= poi(poi(nt!PsInitialSystemProcess)+@@C++(#FIELD_OFFSET(nt!_EPROCESS,ObjectTable)))</link>
|
||||
Print object table for SYSTEM process.
|
||||
|
||||
<link cmd=\"!py ntobj table= poi(nt!PspCidTable) headers= False type= poi(nt!PsProcessType)\">!py ntobj table= poi(nt!PspCidTable) headers= False type= poi(nt!PsProcessType)</link>
|
||||
Print all process objects from nt!PspCidTable
|
||||
"""
|
||||
|
||||
if not isWindbgExt():
|
||||
print "Script is launch out of WinDBG"
|
||||
return
|
||||
|
||||
if not isKernelDebugging():
|
||||
dprintln("This script for kernel debugging only")
|
||||
return
|
||||
|
||||
argc = len(sys.argv)
|
||||
|
||||
if (2 == argc) and ("help" == sys.argv[1] or "?" == sys.argv[1]):
|
||||
dprintln(main.__doc__, True)
|
||||
return
|
||||
|
||||
if (0 == (argc % 2)):
|
||||
dprintln("Invalid number of comand line arguments")
|
||||
dprintln(main.__doc__, True)
|
||||
return
|
||||
|
||||
tableHandles = None
|
||||
objTypeAddr = 0
|
||||
containHeaders = True
|
||||
|
||||
for i in range(1, argc, 2):
|
||||
if ("table=" == sys.argv[i]):
|
||||
tableHandles = expr(sys.argv[i + 1])
|
||||
elif ("type=" == sys.argv[i]):
|
||||
objTypeAddr = expr(sys.argv[i + 1])
|
||||
elif ("headers=" == sys.argv[i]):
|
||||
containHeaders = ("True" == sys.argv[i + 1])
|
||||
else:
|
||||
dprintln("Unknown option `" + sys.argv[i] + "'")
|
||||
dprintln(main.__doc__, True)
|
||||
return
|
||||
|
||||
ObjectHandlePairs = [ (p[0], p[1]) for p in getListByHandleTable(tableHandles, objTypeAddr, containHeaders) ]
|
||||
|
||||
dprintln("%u objects:" % len(ObjectHandlePairs))
|
||||
for objectHandle in ObjectHandlePairs:
|
||||
objectType = getType(objectHandle[0])
|
||||
if objectType in ViewObjectCommand:
|
||||
viewCommand = ViewObjectCommand[objectType]
|
||||
else:
|
||||
viewCommand = "!object"
|
||||
|
||||
dprint("\t<link cmd=\"" + viewCommand + " 0x%x\">" % objectHandle[0] + viewCommand + " 0x%x</link>" % objectHandle[0], True)
|
||||
objectName = buildObjectName(objectHandle[0])
|
||||
if len(objectName):
|
||||
dprint( ", name=`" + objectName + "'" )
|
||||
elif nt.typedVar("_OBJECT_TYPE", getType(objectHandle[0])).TypeInfo.QueryNameProcedure:
|
||||
dprint(", <i>custom</i> name", True)
|
||||
else:
|
||||
dprint(" , <_unnamed_>")
|
||||
|
||||
dprint(", <link cmd=\"!handle 0x%x\">!handle 0x%x</link>\n" % (objectHandle[1], objectHandle[1]), True)
|
||||
|
||||
dprint("\ttype is `" + getObjectName(objectType) + "' (<link cmd=\"!object 0x%x\">0x%x</link>)" % (objectType, objectType), True)
|
||||
|
||||
dprintln( "\n" )
|
||||
|
||||
if (1 == argc):
|
||||
dprintln("\n<link cmd=\"!py " + sys.argv[0] + " ?\" \">View help for ntobj</link>", True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in New Issue
Block a user