tcsteg_v2


SUBMITTED BY: Guest

DATE: Feb. 9, 2013, 11:56 p.m.

FORMAT: Python

SIZE: 17.6 kB

HITS: 2033

  1. #!/usr/bin/env python
  2. """
  3. tcsteg2 -- TrueCrypt real steganography tool
  4. version 2.0 (2012-02-18)
  5. by Vladimir Ivanov <vladimirivanov815@gmail.com>
  6. and Martin J. Fiedler <martin.fiedler@gmx.net>
  7. see: http://keyj.emphy.de/real-steganography-with-truecrypt
  8. This software is published under the terms of KeyJ's Research License,
  9. version 0.2. Usage of this software is subject to the following conditions:
  10. 0. There's no warranty whatsoever. The author(s) of this software can not
  11. be held liable for any damages that occur when using this software.
  12. 1. This software may be used freely for both non-commercial and commercial
  13. purposes.
  14. 2. This software may be redistributed freely as long as no fees are charged
  15. for the distribution and this license information is included.
  16. 3. This software may be modified freely except for this license information,
  17. which must not be changed in any way.
  18. 4. If anything other than configuration, indentation or comments have been
  19. altered in the code, the original author(s) must receive a copy of the
  20. modified code.
  21. Version history
  22. ===============
  23. 2.0 (Vladimir Ivanov, speed optimizations by Martin Fiedler)
  24. - now supports files over 4 GiB
  25. - erases duplicate encoder signature
  26. - auto-renames TrueCrypt container
  27. - supports 3gp videos
  28. - function allowing post-embed password change
  29. 1.0 (Martin Fiedler)
  30. - initial release
  31. To generate a steganographic TrueCrypt/QuickTime hybrid using the script, do the following:
  32. Find a good candidate QuickTime or MP4 file to use as a disguise, let’s say SomeVideo.mp4. The file should be encoded in a very efficient way so that an increase in filesize is believable in relation to the length and quality of the video. Estimate how much larger the file may become without becoming suspicious.
  33. Use TrueCrypt’s Volume Creating Wizard to create a new hidden(!) TrueCrypt volume.
  34. Use the name of the final hybrid file as the container file name, e.g. InnocentLookingVideo.mp4.
  35. As the outer volume size, enter the estimated maximum enlargment from step 1.
  36. Don’t bother entering a good password for the outer volume, it will be destroyed anyway.
  37. Use the maximum possible size for the hidden volume. Enter the size in KB instead of MB and do a bit of number guessing – the »Next« button in the wizard is disabled when the size is too large. Find the maximum size where the button is still clickable. (Technically, you could enter lower values, but why should you? Every byte left to the outer volume is a wasted byte!)
  38. Use your real ultra-secret password or keyfile for the hidden volume.
  39. Do not mount the outer volume! You will likely destroy the hidden volume otherwise.
  40. Use the script:
  41. python tcsteg.py SomeVideo.mp4 InnocentLookingVideo.mp4
  42. This will modify the TrueCrypt container file in-place. It might still take a while to process, since the disguise file is basically copied over into the container file.
  43. If everything worked, you will now have a file that
  44. looks like a video file in every way
  45. can be played as a video file using normal video player applications
  46. can still be mounted in TrueCrypt as a hidden volume
  47. is very hard to detect as a hybrid file
  48. """
  49. import sys, os, struct
  50. MAX_BUFFER_SIZE = 67108864 # 64 MiB
  51. TC_HEADER_SIZE = 65536 # 64 KiB
  52. MAX_INT32 = 4294967295
  53. MAX_INT64 = 18446744073709551615L
  54. class ProcessingError(RuntimeError):
  55. pass
  56. ################################################################################
  57. class Atom(object):
  58. def __init__(self, f_src, name, start, header_size, size, mother):
  59. self.f_src = f_src
  60. self.name = name
  61. self.start = start
  62. self.size = size
  63. self.header_size = header_size
  64. self.mother = mother
  65. self.childs = []
  66. self.contents = None
  67. def setBodySize(self, bodySize):
  68. oldBodySize = self.size - self.header_size
  69. bodyDiff = bodySize - oldBodySize
  70. hDiff = 0
  71. if bodySize <= MAX_INT32:
  72. if self.header_size != 8:
  73. self.header_size = 8
  74. hDiff = -8
  75. else:
  76. if self.header_size != 16:
  77. self.header_size = 16
  78. hDiff = 8
  79. self.size = self.header_size + bodySize
  80. if self.mother:
  81. oldParentBodySize = self.mother.size - self.mother.header_size
  82. self.mother.setBodySize(oldParentBodySize + hDiff + bodyDiff)
  83. def writeHeader(self, f_dest):
  84. if self.size >= MAX_INT32 and self.header_size == 8:
  85. raise ProcessingError("Atom size too large for compact header")
  86. # compact
  87. if self.size <= MAX_INT32 and self.header_size == 8:
  88. f_dest.write(struct.pack(">I4s", self.size, self.name))
  89. # extended
  90. else:
  91. f_dest.write(struct.pack(">I4sQ", 1, self.name, self.size))
  92. return self.size - self.header_size
  93. def writePayload(self, f_dest):
  94. if self.childs:
  95. for atom in self.childs:
  96. atom.write(f_dest)
  97. else:
  98. dataBuffer = None
  99. bodySize = self.size - self.header_size
  100. if self.f_src:
  101. self.f_src.seek(self.start + self.header_size)
  102. percent_i = 0
  103. percent_f = 0.0
  104. if bodySize > MAX_BUFFER_SIZE:
  105. percent_incr = 100.0 * MAX_BUFFER_SIZE / bodySize
  106. else:
  107. percent_incr = 0.0
  108. while bodySize > 0:
  109. if bodySize > MAX_BUFFER_SIZE:
  110. dataBuffer = self.f_src.read(MAX_BUFFER_SIZE)
  111. else:
  112. dataBuffer = self.f_src.read(bodySize)
  113. f_dest.write(dataBuffer)
  114. bodySize -= MAX_BUFFER_SIZE
  115. percent_f += percent_incr
  116. percent_i_new = min(100, int(percent_f))
  117. if percent_i_new > percent_i:
  118. percent_i = percent_i_new
  119. sys.stderr.write("%3d%% done\r" % percent_i)
  120. sys.stderr.flush()
  121. elif self.contents:
  122. if bodySize == len(self.contents):
  123. f_dest.write(self.contents)
  124. else:
  125. raise ProcessingError("Atom content size does not equal body size")
  126. else:
  127. if bodySize > 0:
  128. f_dest.seek(bodySize - 1, 1)
  129. byte = f_dest.read(1)
  130. if not byte:
  131. f_dest.write("\0")
  132. else:
  133. f_dest.seek(-1, 1)
  134. f_dest.write(byte)
  135. def write(self, f_dest):
  136. self.writeHeader(f_dest)
  137. self.writePayload(f_dest)
  138. ################################################################################
  139. def AnalyseFile(f):
  140. atoms = None
  141. try:
  142. atoms = parseAtoms(f, 0, os.fstat(f.fileno()).st_size, None)
  143. except Exception, e:
  144. raise ProcessingError("Parse Error: " + str(e))
  145. return atoms
  146. def parseAtoms(f, start, end, mother):
  147. offset = start
  148. atomSize = None
  149. atomHeaderSize = None
  150. comrades = []
  151. try:
  152. while offset < end:
  153. f.seek(offset)
  154. atomSize = struct.unpack(">I", f.read(4))[0]
  155. atomType = struct.unpack(">4s", f.read(4))[0]
  156. if atomSize == 1:
  157. atomSize = struct.unpack(">Q", f.read(8))[0]
  158. atomHeaderSize = 16 # Extended
  159. else:
  160. atomHeaderSize = 8 # Compact
  161. if atomSize == 0:
  162. atomSize = end - offset
  163. if start + atomSize > end:
  164. raise ProcessingError("Invalid size for atom '" + atomType + "' @ " + hex(offset))
  165. atom = Atom(f, atomType, offset, atomHeaderSize, atomSize, mother)
  166. if mother:
  167. mother.childs.append(atom)
  168. comrades.append(atom)
  169. if atomType in ["moov","trak","mdia","minf","stbl"]:
  170. atom.childs = parseAtoms(f, offset + atomHeaderSize, offset + atomSize, atom)
  171. offset = offset + atomSize
  172. except struct.error, e:
  173. raise ProcessingError("Atom header must be multiples 4 or 8 near " + hex(offset))
  174. except Exception, e:
  175. raise ProcessingError(str(e))
  176. return comrades
  177. def findAtom(atoms, name):
  178. aList = []
  179. for a in atoms:
  180. if a.name == name:
  181. aList.append(a)
  182. aList = aList + findAtom(a.childs, name)
  183. return aList
  184. def printAtoms(atoms, l=0):
  185. for a in atoms:
  186. print "%s %s %ld @ 0x%lx" % (" "*l, a.name, a.size, a.start)
  187. printAtoms(a.childs,l+1)
  188. def adjustSampleOffsets(atoms, offset):
  189. sampleAtoms = findAtom(atoms, "stco") + findAtom(atoms, "co64")
  190. if len(sampleAtoms) == 0:
  191. raise ProcessingError("Could not find any 'stco' or 'co64' atoms")
  192. for sAtom in sampleAtoms:
  193. sAtom.f_src.seek(sAtom.start + sAtom.header_size)
  194. verFlags, count = struct.unpack(">II", sAtom.f_src.read(8))
  195. if sAtom.name == "stco":
  196. sampleOffsets = struct.unpack('>' + 'I' * count, sAtom.f_src.read(count * 4))
  197. elif sAtom.name == "co64":
  198. sampleOffsets = struct.unpack('>' + 'Q' * count, sAtom.f_src.read(count * 8))
  199. sampleOffsets = [x + offset for x in sampleOffsets]
  200. # Does the atom need to support 64-bit values?
  201. if max(sampleOffsets) > MAX_INT32 and sAtom.name == "stco":
  202. sAtom.name = "co64"
  203. sAtom.contents = struct.pack(">II", verFlags, count)
  204. if sAtom.name == "stco":
  205. sAtom.contents += struct.pack('>' + 'I' * count, *sampleOffsets)
  206. elif sAtom.name == "co64":
  207. sAtom.contents += struct.pack('>' + 'Q' * count, *sampleOffsets)
  208. if (sAtom.size - sAtom.header_size) != len(sAtom.contents):
  209. sAtom.setBodySize(len(sAtom.contents))
  210. sAtom.f_src = None
  211. return min(sampleOffsets)
  212. def TCSteg_Embed(atoms, tcFile):
  213. ftyp = findAtom(atoms, "ftyp")
  214. mdat = findAtom(atoms, "mdat")
  215. moov = findAtom(atoms, "moov")
  216. if len(ftyp) != 1 or len(mdat) != 1 or len(moov) != 1:
  217. printAtoms(atoms)
  218. raise ProcessingError("One of each type required to embed: ['ftyp','mdat','moov']\nWe do not support this.")
  219. ftyp = ftyp[0]
  220. mdat = mdat[0]
  221. moov = moov[0]
  222. tcFileSize = os.fstat(tcFile.fileno()).st_size
  223. tcPreservedSize = tcFileSize - (TC_HEADER_SIZE * 3)
  224. tcStartHeaderVolBackup = tcFileSize - (TC_HEADER_SIZE * 2)
  225. mdatRealBodySize = mdat.size - mdat.header_size
  226. mdatEndMarker = tcFileSize - (TC_HEADER_SIZE * 2) + (mdatRealBodySize)
  227. mdatNewSize = mdatEndMarker - ftyp.size
  228. tcFile.seek(0)
  229. if ftyp.size + 16 > TC_HEADER_SIZE:
  230. raise ProcessingError("'ftyp' atom + 'mdat' headers too long")
  231. ftyp.write(tcFile)
  232. tempH = mdat.header_size
  233. tempL = mdat.size
  234. if mdatNewSize <= MAX_INT32:
  235. Atom(None, "free", None, 8, 8, None).write(tcFile)
  236. mdatNewSize = mdatNewSize - 8
  237. mdat.size = mdatNewSize
  238. mdat.header_size = 8
  239. mdat.writeHeader(tcFile)
  240. else:
  241. mdat.size = mdatNewSize
  242. mdat.header_size = 16
  243. mdat.writeHeader(tcFile)
  244. mdat.header_size = tempH
  245. mdat.size = tempL
  246. # re-generate first 64 KiB
  247. voidRegionSize = TC_HEADER_SIZE - tcFile.tell()
  248. mdat.f_src.seek(mdat.start + mdat.header_size)
  249. tcFile.write(mdat.f_src.read(voidRegionSize))
  250. # start header volume backups. Last 128 KiB of tc_file
  251. tcFile.seek(tcStartHeaderVolBackup)
  252. # Mark the position of the real mdat sample start
  253. mdatOffset = tcFile.tell() - (mdat.start + mdat.header_size)
  254. mdat.writePayload(tcFile)
  255. if tcFile.tell() != mdatEndMarker:
  256. raise ProcessingError("Wrote more mdat than we should have")
  257. # fix mdat shift by offsetting to each sample chunk
  258. print "Fixing up hybrid file ..."
  259. firstSample = adjustSampleOffsets(atoms, mdatOffset)
  260. # Destory duplicate encoder signature before first sample.
  261. tcFile.seek(tcStartHeaderVolBackup)
  262. tcFile.write(os.urandom(min(firstSample - tcStartHeaderVolBackup, TC_HEADER_SIZE)))
  263. tcFile.seek(mdatEndMarker)
  264. moov.write(tcFile)
  265. def Pass_Helper(video_path):
  266. f = None
  267. try:
  268. f = open(video_path, "rb+")
  269. last = AnalyseFile(f)[-1]
  270. if last.name == "skip":
  271. print "Removing padding 'skip' atom"
  272. f.truncate(last.start)
  273. print "Removal completed successfully"
  274. else:
  275. print "Preparing hybrid file for password change ... "
  276. f.seek(0, 2)
  277. Atom(None, "skip", None, 8, 8 + TC_HEADER_SIZE * 2, None).write(f)
  278. print "Complete. Now change the TrueCrypt password"
  279. except IndexError:
  280. pass
  281. except IOError:
  282. print >>sys.stderr, "Error opening file '"+video_path+"'"
  283. except Exception, e:
  284. print >>sys.stderr, str(e)
  285. if f:
  286. f.close()
  287. ################################################################################
  288. if __name__ == "__main__":
  289. supported_formats = ["mov","qt","mp4","m4v","m4a","3gp"]
  290. if len(sys.argv) < 3:
  291. pname = sys.argv[0].split(os.sep)[-1]
  292. print "too few arguments"
  293. print "Usage (1):", pname, "<MP4 Video> <TrueCrypt Container>"
  294. print "Embeds a file into a TrueCrypt container so that both are still readable."
  295. print
  296. print "<MP4 Video> is a file in one of the following formats:"
  297. print " QuickTime / ISO MPEG-4 (%s)" % (", ".join(["*." + fmt for fmt in supported_formats]))
  298. print
  299. print "<TrueCrypt Container> is a TrueCrypt hidden volume. The file will be"
  300. print "modified in-place so that it seems like a copy of the input file that can be"
  301. print "opened in an appropriate viewer/player. However, the hidden TrueCtype volume"
  302. print "will also be preserved and can be used."
  303. print
  304. print
  305. print "Usage (2):", pname, "-p <Hybrid File>"
  306. print "<Hybrid File> is a file that is both TrueCrypt container and a video."
  307. print "This file will be modified in-place to make it possible to change the TrueCrypt"
  308. print "password. After changing the password, this command should be run again to"
  309. print "remove that (detectable and hence insecure) modification!"
  310. print
  311. print
  312. sys.exit(2)
  313. if sys.argv[1] == "-p":
  314. Pass_Helper(sys.argv[2])
  315. sys.exit(0)
  316. video_path = sys.argv[1]
  317. tc_path = sys.argv[2]
  318. video_file = None
  319. tc_file = None
  320. tcSize = 0
  321. try:
  322. video_file = open(video_path, "rb")
  323. except IOError, e:
  324. print >>sys.stderr, "Error opening file '"+video_path+"'"
  325. sys.exit(1)
  326. try:
  327. tc_file = open(tc_path, "rb+")
  328. tcSize = os.path.getsize(tc_path)
  329. except IOError, e:
  330. print >>sys.stderr, "Error opening file '"+tc_path+"'"
  331. sys.exit(1)
  332. try:
  333. video_ext = os.path.splitext(video_path)[1].lstrip(".")
  334. if video_ext in supported_formats:
  335. print "Parsing video ..."
  336. atoms = AnalyseFile(video_file)
  337. print "Embedding ... be patient"
  338. TCSteg_Embed(atoms, tc_file)
  339. tc_file.close()
  340. if not tc_path.endswith("." + video_ext):
  341. if not os.path.exists(tc_path + "." + video_ext):
  342. new_tc_path = tc_path + "." + video_ext
  343. os.rename(tc_path, new_tc_path)
  344. tc_path = new_tc_path
  345. print "Hybrid file '%s' was created successfully." % tc_path
  346. print
  347. print "Everything OK. Try mounting the file in TrueCrypt and playing the video."
  348. else:
  349. print >>sys.stderr, "Error: input video format is not supported"
  350. except (ProcessingError, IOError), e:
  351. print >>sys.stderr, "ERROR:", e
  352. tc_file.truncate(tcSize)
  353. finally:
  354. video_file.close()
  355. tc_file.close()

comments powered by Disqus