1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 """
32 SQL database storage support.
33
34 @group Crash reporting:
35 CrashDAO
36 """
37
38 __revision__ = "$Id: sql.py 1299 2013-12-20 09:30:55Z qvasimodo $"
39
40 __all__ = ['CrashDAO']
41
42 import sqlite3
43 import datetime
44 import warnings
45
46 from sqlalchemy import create_engine, Column, ForeignKey, Sequence
47 from sqlalchemy.engine.url import URL
48 from sqlalchemy.ext.compiler import compiles
49 from sqlalchemy.ext.declarative import declarative_base
50 from sqlalchemy.interfaces import PoolListener
51 from sqlalchemy.orm import sessionmaker, deferred
52 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
53 from sqlalchemy.types import Integer, BigInteger, Boolean, DateTime, String, \
54 LargeBinary, Enum, VARCHAR
55 from sqlalchemy.sql.expression import asc, desc
56
57 from crash import Crash, Marshaller, pickle, HIGHEST_PROTOCOL
58 from textio import CrashDump
59 import win32
60
61
62
63 try:
64 from decorator import decorator
65 except ImportError:
66 import functools
68 """
69 The C{decorator} module was not found. You can install it from:
70 U{http://pypi.python.org/pypi/decorator/}
71 """
72 def d(fn):
73 @functools.wraps(fn)
74 def x(*argv, **argd):
75 return w(fn, *argv, **argd)
76 return x
77 return d
78
79
80
81 @compiles(String, 'mysql')
82 @compiles(VARCHAR, 'mysql')
83 -def _compile_varchar_mysql(element, compiler, **kw):
84 """MySQL hack to avoid the "VARCHAR requires a length" error."""
85 if not element.length or element.length == 'max':
86 return "TEXT"
87 else:
88 return compiler.visit_VARCHAR(element, **kw)
89
93 """
94 Used internally by L{BaseDAO}.
95
96 After connecting to an SQLite database, ensure that the foreign keys
97 support is enabled. If not, abort the connection.
98
99 @see: U{http://sqlite.org/foreignkeys.html}
100 """
101 - def connect(dbapi_connection, connection_record):
102 """
103 Called once by SQLAlchemy for each new SQLite DB-API connection.
104
105 Here is where we issue some PRAGMA statements to configure how we're
106 going to access the SQLite database.
107
108 @param dbapi_connection:
109 A newly connected raw SQLite DB-API connection.
110
111 @param connection_record:
112 Unused by this method.
113 """
114 try:
115 cursor = dbapi_connection.cursor()
116 try:
117 cursor.execute("PRAGMA foreign_keys = ON;")
118 cursor.execute("PRAGMA foreign_keys;")
119 if cursor.fetchone()[0] != 1:
120 raise Exception()
121 finally:
122 cursor.close()
123 except Exception:
124 dbapi_connection.close()
125 raise sqlite3.Error()
126
130 """
131 Customized declarative base for SQLAlchemy.
132 """
133
134 __table_args__ = {
135
136
137 'mysql_engine': 'InnoDB',
138
139
140 'drizzle_engine': 'InnoDB',
141
142
143 'mysql_charset': 'utf8',
144
145 }
146
147 BaseDTO = declarative_base(cls = BaseDTO)
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163 -class BaseDAO (object):
164 """
165 Data Access Object base class.
166
167 @type _url: sqlalchemy.url.URL
168 @ivar _url: Database connection URL.
169
170 @type _dialect: str
171 @ivar _dialect: SQL dialect currently being used.
172
173 @type _driver: str
174 @ivar _driver: Name of the database driver currently being used.
175 To get the actual Python module use L{_url}.get_driver() instead.
176
177 @type _session: sqlalchemy.orm.Session
178 @ivar _session: Database session object.
179
180 @type _new_session: class
181 @cvar _new_session: Custom configured Session class used to create the
182 L{_session} instance variable.
183
184 @type _echo: bool
185 @cvar _echo: Set to C{True} to print all SQL queries to standard output.
186 """
187
188 _echo = False
189
190 _new_session = sessionmaker(autoflush = True,
191 autocommit = True,
192 expire_on_commit = True,
193 weak_identity_map = True)
194
195 - def __init__(self, url, creator = None):
196 """
197 Connect to the database using the given connection URL.
198
199 The current implementation uses SQLAlchemy and so it will support
200 whatever database said module supports.
201
202 @type url: str
203 @param url:
204 URL that specifies the database to connect to.
205
206 Some examples:
207 - Opening an SQLite file:
208 C{dao = CrashDAO("sqlite:///C:\\some\\path\\database.sqlite")}
209 - Connecting to a locally installed SQL Express database:
210 C{dao = CrashDAO("mssql://.\\SQLEXPRESS/Crashes?trusted_connection=yes")}
211 - Connecting to a MySQL database running locally, using the
212 C{oursql} library, authenticating as the "winappdbg" user with
213 no password:
214 C{dao = CrashDAO("mysql+oursql://winappdbg@localhost/Crashes")}
215 - Connecting to a Postgres database running locally,
216 authenticating with user and password:
217 C{dao = CrashDAO("postgresql://winappdbg:winappdbg@localhost/Crashes")}
218
219 For more information see the C{SQLAlchemy} documentation online:
220 U{http://docs.sqlalchemy.org/en/latest/core/engines.html}
221
222 Note that in all dialects except for SQLite the database
223 must already exist. The tables schema, however, is created
224 automatically when connecting for the first time.
225
226 To create the database in MSSQL, you can use the
227 U{SQLCMD<http://msdn.microsoft.com/en-us/library/ms180944.aspx>}
228 command::
229 sqlcmd -Q "CREATE DATABASE Crashes"
230
231 In MySQL you can use something like the following::
232 mysql -u root -e "CREATE DATABASE Crashes;"
233
234 And in Postgres::
235 createdb Crashes -h localhost -U winappdbg -p winappdbg -O winappdbg
236
237 Some small changes to the schema may be tolerated (for example,
238 increasing the maximum length of string columns, or adding new
239 columns with default values). Of course, it's best to test it
240 first before making changes in a live database. This all depends
241 very much on the SQLAlchemy version you're using, but it's best
242 to use the latest version always.
243
244 @type creator: callable
245 @param creator: (Optional) Callback function that creates the SQL
246 database connection.
247
248 Normally it's not necessary to use this argument. However in some
249 odd cases you may need to customize the database connection.
250 """
251
252
253 parsed_url = URL(url)
254 schema = parsed_url.drivername
255 if '+' in schema:
256 dialect, driver = schema.split('+')
257 else:
258 dialect, driver = schema, 'base'
259 dialect = dialect.strip().lower()
260 driver = driver.strip()
261
262
263 arguments = {'echo' : self._echo}
264 if dialect == 'sqlite':
265 arguments['module'] = sqlite3.dbapi2
266 arguments['listeners'] = [_SQLitePatch()]
267 if creator is not None:
268 arguments['creator'] = creator
269
270
271 engine = create_engine(url, **arguments)
272
273
274 session = self._new_session(bind = engine)
275
276
277 BaseDTO.metadata.create_all(engine)
278
279
280
281 self._url = parsed_url
282 self._driver = driver
283 self._dialect = dialect
284 self._session = session
285
287 """
288 Begins a transaction and calls the given DAO method.
289
290 If the method executes successfully the transaction is commited.
291
292 If the method fails, the transaction is rolled back.
293
294 @type method: callable
295 @param method: Bound method of this class or one of its subclasses.
296 The first argument will always be C{self}.
297
298 @return: The return value of the method call.
299
300 @raise Exception: Any exception raised by the method.
301 """
302 self._session.begin(subtransactions = True)
303 try:
304 result = method(self, *argv, **argd)
305 self._session.commit()
306 return result
307 except:
308 self._session.rollback()
309 raise
310
311
312
313 @decorator
314 -def Transactional(fn, self, *argv, **argd):
315 """
316 Decorator that wraps DAO methods to handle transactions automatically.
317
318 It may only work with subclasses of L{BaseDAO}.
319 """
320 return self._transactional(fn, *argv, **argd)
321
326 f = []
327 for a1 in ("---", "R--", "RW-", "RC-", "--X", "R-X", "RWX", "RCX", "???"):
328 for a2 in ("G", "-"):
329 for a3 in ("N", "-"):
330 for a4 in ("W", "-"):
331 f.append("%s %s%s%s" % (a1, a2, a3, a4))
332 return tuple(f)
333 _valid_access_flags = _gen_valid_access_flags()
334
335
336 n_MEM_ACCESS_ENUM = {"name" : "MEM_ACCESS_ENUM"}
337 n_MEM_ALLOC_ACCESS_ENUM = {"name" : "MEM_ALLOC_ACCESS_ENUM"}
338 MEM_ACCESS_ENUM = Enum(*_valid_access_flags,
339 **n_MEM_ACCESS_ENUM)
340 MEM_ALLOC_ACCESS_ENUM = Enum(*_valid_access_flags,
341 **n_MEM_ALLOC_ACCESS_ENUM)
342 MEM_STATE_ENUM = Enum("Reserved", "Commited", "Free", "Unknown",
343 name = "MEM_STATE_ENUM")
344 MEM_TYPE_ENUM = Enum("Image", "Mapped", "Private", "Unknown",
345 name = "MEM_TYPE_ENUM")
346
347
348 del _gen_valid_access_flags
349 del _valid_access_flags
350 del n_MEM_ACCESS_ENUM
351 del n_MEM_ALLOC_ACCESS_ENUM
356 """
357 Database mapping for memory dumps.
358 """
359
360
361 __tablename__ = 'memory'
362 id = Column(Integer, Sequence(__tablename__ + '_seq'),
363 primary_key = True, autoincrement = True)
364 crash_id = Column(Integer, ForeignKey('crashes.id',
365 ondelete = 'CASCADE',
366 onupdate = 'CASCADE'),
367 nullable = False)
368 address = Column(BigInteger, nullable = False, index = True)
369 size = Column(BigInteger, nullable = False)
370 state = Column(MEM_STATE_ENUM, nullable = False)
371 access = Column(MEM_ACCESS_ENUM)
372 type = Column(MEM_TYPE_ENUM)
373 alloc_base = Column(BigInteger)
374 alloc_access = Column(MEM_ALLOC_ACCESS_ENUM)
375 filename = Column(String)
376 content = deferred(Column(LargeBinary))
377
438
471
472 - def toMBI(self, getMemoryDump = False):
503
504 @staticmethod
514
515 @staticmethod
526
527 @staticmethod
555
559 """
560 Database mapping for crash dumps.
561 """
562
563
564 __tablename__ = "crashes"
565
566
567 id = Column(Integer, Sequence(__tablename__ + '_seq'),
568 primary_key = True, autoincrement = True)
569
570
571 timestamp = Column(DateTime, nullable = False, index = True)
572
573
574 exploitable = Column(Integer, nullable = False)
575 exploitability_rule = Column(String(32), nullable = False)
576 exploitability_rating = Column(String(32), nullable = False)
577 exploitability_desc = Column(String, nullable = False)
578
579
580 os = Column(String(32), nullable = False)
581 arch = Column(String(16), nullable = False)
582 bits = Column(Integer, nullable = False)
583
584
585 event = Column(String, nullable = False)
586 pid = Column(Integer, nullable = False)
587 tid = Column(Integer, nullable = False)
588 pc = Column(BigInteger, nullable = False)
589 sp = Column(BigInteger, nullable = False)
590 fp = Column(BigInteger, nullable = False)
591 pc_label = Column(String, nullable = False)
592
593
594 exception = Column(String(64))
595 exception_text = Column(String(64))
596 exception_address = Column(BigInteger)
597 exception_label = Column(String)
598 first_chance = Column(Boolean)
599 fault_type = Column(Integer)
600 fault_address = Column(BigInteger)
601 fault_label = Column(String)
602 fault_disasm = Column(String)
603 stack_trace = Column(String)
604
605
606 command_line = Column(String)
607 environment = Column(String)
608
609
610 debug_string = Column(String)
611
612
613 notes = Column(String)
614
615
616 signature = Column(String, nullable = False)
617
618
619 data = deferred(Column(LargeBinary, nullable = False))
620
622 """
623 @type crash: Crash
624 @param crash: L{Crash} object to store into the database.
625 """
626
627
628 self.timestamp = datetime.datetime.fromtimestamp( crash.timeStamp )
629 self.signature = pickle.dumps(crash.signature, protocol = 0)
630
631
632
633 memoryMap = crash.memoryMap
634 try:
635 crash.memoryMap = None
636 self.data = buffer( Marshaller.dumps(crash) )
637 finally:
638 crash.memoryMap = memoryMap
639
640
641 self.exploitability_rating, \
642 self.exploitability_rule, \
643 self.exploitability_desc = crash.isExploitable()
644
645
646 self.exploitable = [
647 "Not an exception",
648 "Not exploitable",
649 "Not likely exploitable",
650 "Unknown",
651 "Probably exploitable",
652 "Exploitable",
653 ].index(self.exploitability_rating)
654
655
656 self.os = crash.os
657 self.arch = crash.arch
658 self.bits = crash.bits
659
660
661 self.event = crash.eventName
662 self.pid = crash.pid
663 self.tid = crash.tid
664 self.pc = crash.pc
665 self.sp = crash.sp
666 self.fp = crash.fp
667 self.pc_label = crash.labelPC
668
669
670 self.exception = crash.exceptionName
671 self.exception_text = crash.exceptionDescription
672 self.exception_address = crash.exceptionAddress
673 self.exception_label = crash.exceptionLabel
674 self.first_chance = crash.firstChance
675 self.fault_type = crash.faultType
676 self.fault_address = crash.faultAddress
677 self.fault_label = crash.faultLabel
678 self.fault_disasm = CrashDump.dump_code( crash.faultDisasm,
679 crash.pc )
680 self.stack_trace = CrashDump.dump_stack_trace_with_labels(
681 crash.stackTracePretty )
682
683
684 self.command_line = crash.commandLine
685
686
687 if crash.environment:
688 envList = crash.environment.items()
689 envList.sort()
690 environment = ''
691 for envKey, envVal in envList:
692
693
694 environment += envKey + '=' + envVal + '\n'
695 if environment:
696 self.environment = environment
697
698
699 self.debug_string = crash.debugString
700
701
702 self.notes = crash.notesReport()
703
704 - def toCrash(self, getMemoryDump = False):
705 """
706 Returns a L{Crash} object using the data retrieved from the database.
707
708 @type getMemoryDump: bool
709 @param getMemoryDump: If C{True} retrieve the memory dump.
710 Defaults to C{False} since this may be a costly operation.
711
712 @rtype: L{Crash}
713 @return: Crash object.
714 """
715 crash = Marshaller.loads(str(self.data))
716 if not isinstance(crash, Crash):
717 raise TypeError(
718 "Expected Crash instance, got %s instead" % type(crash))
719 crash._rowid = self.id
720 if not crash.memoryMap:
721 memory = getattr(self, "memory", [])
722 if memory:
723 crash.memoryMap = [dto.toMBI(getMemoryDump) for dto in memory]
724 return crash
725
726
727
728
729
730 -class CrashDAO (BaseDAO):
731 """
732 Data Access Object to read, write and search for L{Crash} objects in a
733 database.
734 """
735
736 @Transactional
737 - def add(self, crash, allow_duplicates = True):
738 """
739 Add a new crash dump to the database, optionally filtering them by
740 signature to avoid duplicates.
741
742 @type crash: L{Crash}
743 @param crash: Crash object.
744
745 @type allow_duplicates: bool
746 @param allow_duplicates: (Optional)
747 C{True} to always add the new crash dump.
748 C{False} to only add the crash dump if no other crash with the
749 same signature is found in the database.
750
751 Sometimes, your fuzzer turns out to be I{too} good. Then you find
752 youself browsing through gigabytes of crash dumps, only to find
753 a handful of actual bugs in them. This simple heuristic filter
754 saves you the trouble by discarding crashes that seem to be similar
755 to another one you've already found.
756 """
757
758
759 if not allow_duplicates:
760 signature = pickle.dumps(crash.signature, protocol = 0)
761 if self._session.query(CrashDTO.id) \
762 .filter_by(signature = signature) \
763 .count() > 0:
764 return
765
766
767 crash_id = self.__add_crash(crash)
768
769
770 self.__add_memory(crash_id, crash.memoryMap)
771
772
773
774
775 crash._rowid = crash_id
776
777
779 session = self._session
780 r_crash = None
781 try:
782
783
784 r_crash = CrashDTO(crash)
785 session.add(r_crash)
786
787
788 session.flush()
789 crash_id = r_crash.id
790
791 finally:
792 try:
793
794
795 if r_crash is not None:
796 session.expire(r_crash)
797
798 finally:
799
800
801
802 del r_crash
803
804
805 return crash_id
806
807
809 session = self._session
810 if memoryMap:
811 for mbi in memoryMap:
812 r_mem = MemoryDTO(crash_id, mbi)
813 session.add(r_mem)
814 session.flush()
815
816 @Transactional
817 - def find(self,
818 signature = None, order = 0,
819 since = None, until = None,
820 offset = None, limit = None):
821 """
822 Retrieve all crash dumps in the database, optionally filtering them by
823 signature and timestamp, and/or sorting them by timestamp.
824
825 Results can be paged to avoid consuming too much memory if the database
826 is large.
827
828 @see: L{find_by_example}
829
830 @type signature: object
831 @param signature: (Optional) Return only through crashes matching
832 this signature. See L{Crash.signature} for more details.
833
834 @type order: int
835 @param order: (Optional) Sort by timestamp.
836 If C{== 0}, results are not sorted.
837 If C{> 0}, results are sorted from older to newer.
838 If C{< 0}, results are sorted from newer to older.
839
840 @type since: datetime
841 @param since: (Optional) Return only the crashes after and
842 including this date and time.
843
844 @type until: datetime
845 @param until: (Optional) Return only the crashes before this date
846 and time, not including it.
847
848 @type offset: int
849 @param offset: (Optional) Skip the first I{offset} results.
850
851 @type limit: int
852 @param limit: (Optional) Return at most I{limit} results.
853
854 @rtype: list(L{Crash})
855 @return: List of Crash objects.
856 """
857
858
859 if since and until and since > until:
860 warnings.warn("CrashDAO.find() got the 'since' and 'until'"
861 " arguments reversed, corrected automatically.")
862 since, until = until, since
863 if limit is not None and not limit:
864 warnings.warn("CrashDAO.find() was set a limit of 0 results,"
865 " returning without executing a query.")
866 return []
867
868
869 query = self._session.query(CrashDTO)
870 if signature is not None:
871 sig_pickled = pickle.dumps(signature, protocol = 0)
872 query = query.filter(CrashDTO.signature == sig_pickled)
873 if since:
874 query = query.filter(CrashDTO.timestamp >= since)
875 if until:
876 query = query.filter(CrashDTO.timestamp < until)
877 if order:
878 if order > 0:
879 query = query.order_by(asc(CrashDTO.timestamp))
880 else:
881 query = query.order_by(desc(CrashDTO.timestamp))
882 else:
883
884
885 query = query.order_by(asc(CrashDTO.id))
886 if offset:
887 query = query.offset(offset)
888 if limit:
889 query = query.limit(limit)
890
891
892 try:
893 return [dto.toCrash() for dto in query.all()]
894 except NoResultFound:
895 return []
896
897 @Transactional
899 """
900 Find all crash dumps that have common properties with the crash dump
901 provided.
902
903 Results can be paged to avoid consuming too much memory if the database
904 is large.
905
906 @see: L{find}
907
908 @type crash: L{Crash}
909 @param crash: Crash object to compare with. Fields set to C{None} are
910 ignored, all other fields but the signature are used in the
911 comparison.
912
913 To search for signature instead use the L{find} method.
914
915 @type offset: int
916 @param offset: (Optional) Skip the first I{offset} results.
917
918 @type limit: int
919 @param limit: (Optional) Return at most I{limit} results.
920
921 @rtype: list(L{Crash})
922 @return: List of similar crash dumps found.
923 """
924
925
926 if limit is not None and not limit:
927 warnings.warn("CrashDAO.find_by_example() was set a limit of 0"
928 " results, returning without executing a query.")
929 return []
930
931
932 query = self._session.query(CrashDTO)
933
934
935
936 query = query.asc(CrashDTO.id)
937
938
939 dto = CrashDTO(crash)
940
941
942
943 for name, column in CrashDTO.__dict__.iteritems():
944 if not name.startswith('__') and name not in ('id',
945 'signature',
946 'data'):
947 if isinstance(column, Column):
948 value = getattr(dto, name, None)
949 if value is not None:
950 query = query.filter(column == value)
951
952
953 if offset:
954 query = query.offset(offset)
955 if limit:
956 query = query.limit(limit)
957
958
959 try:
960 return [dto.toCrash() for dto in query.all()]
961 except NoResultFound:
962 return []
963
964 @Transactional
965 - def count(self, signature = None):
966 """
967 Counts how many crash dumps have been stored in this database.
968 Optionally filters the count by heuristic signature.
969
970 @type signature: object
971 @param signature: (Optional) Count only the crashes that match
972 this signature. See L{Crash.signature} for more details.
973
974 @rtype: int
975 @return: Count of crash dumps stored in this database.
976 """
977 query = self._session.query(CrashDTO.id)
978 if signature:
979 sig_pickled = pickle.dumps(signature, protocol = 0)
980 query = query.filter_by(signature = sig_pickled)
981 return query.count()
982
983 @Transactional
985 """
986 Remove the given crash dump from the database.
987
988 @type crash: L{Crash}
989 @param crash: Crash dump to remove.
990 """
991 query = self._session.query(CrashDTO).filter_by(id = crash._rowid)
992 query.delete(synchronize_session = False)
993 del crash._rowid
994