Dawn of War II Unlockable Content, part 2

This post addresses a few of the questions which have arisen since part 1:

"can you put file to rapidshare" (and similar requests)
I'm here to point out flaws in Relic's security chain. I'm not here to give everyone quick and easy access to the pre-order content, so I will not be posting the modified files myself.

"Well done there, though the question of how long this will be possible for, how much data the Steam client collects about the programs it runs (DLL hashes are pretty standard, or?) and what Relic's views on this might be still remain."
Hashing of DLLs is fairly standard; for example, if you modify SimEngine.dll (perhaps to add a maphack), then you can't join online games as your hashes won't match those of the other players. However, as wincrypt.dll is not a DLL which comes with the game, it is not hashed by default. Furthermore, as it is a Windows DLL, it will (probably) change with each Windows version and/or service pack and/or patch, meaning that it cannot be reliably hashed.

"How do I find where this function is which I need to replace the first five bytes of?"
There are tools which list all exported functions of a DLL and their virtual positions, i.e. the position in virtual memory (relative to the DLL base) at which you will find the function when the DLL is loaded into memory. Converting this virtual position into a physical position, i.e. the position in the file on disk of the function, can be done by analysing the DLL's PE section table in a suitable tool.

"I don't see anything about UnlockMask in the content.bin, do you have any more specific instructions for this?"
The contents of content.bin can be viewed in two ways; calling dofile("content.bin") in a Lua interpreter and then printing the resulting DLC table, or running content.bin through luac: (lua and luac are both part of the official Lua.org package)

E:\Lua>lua
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio
> dofile "content.bin"
> print_recursive(DLC) -- implementation left to the reader

E:\Lua>luac -l content.bin

main <e:\dow2-release\assets\config\content.lua:0,0> (197 instructions, 788 bytes at 008B7AC8)
0+ params, 17 slots, 0 upvalues, 0 locals, 100 constants, 0 functions
    1  [10] NEWTABLE  0 1 0
    2  [11] NEWTABLE  1 0 3
    3  [13] SETTABLE  1 -2 -3 ; "ContentName" "Retailer Exclusive DLC"
    4  [14] SETTABLE  1 -4 -5 ; "ContentID" "FC6A678326EAC085ACA365AE99B50A121F98BE53"
    5  [15] NEWTABLE  2 6 0
    6  [16] NEWTABLE  3 0 6
    7  [18] SETTABLE  3 -7 -8 ; "OfferName" "Assault Marine Bonus Pack"
    8  [19] SETTABLE  3 -9 -10 ; "LicenseMask" "1"
    9  [21] NEWTABLE  4 3 0
   10  [23] LOADK     5 -12 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_arm_power_armour_ravens_barding_of_flight"
   11  [24] LOADK     6 -13 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp2_chainsword_blade_of_ulyus"
   12  [26] LOADK     7 -14 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_bolt_pistol_herald_of_the_coming_doom"
   13  [26] SETLIST   4 3 1 ; 1
   14  [26] SETTABLE  3 -11 4 ; "NewCampaignWargear" -
   15  [28] NEWTABLE  4 4 0
   16  [30] LOADK     5 -16 ; "Burnished Gold"
   17  [31] LOADK     6 -17 ; "Abyss Red"
   18  [32] LOADK     7 -18 ; "Metallic Green"
   19  [34] LOADK     8 -19 ; "Crystal Blue"
   20  [34] SETLIST   4 4 1 ; 1
   21  [34] SETTABLE  3 -15 4 ; "Paints" -
   22  [36] NEWTABLE  4 1 0
   23  [39] LOADK     5 -21 ; "SM_Brazen_Claws"
   24  [39] SETLIST   4 1 1 ; 1
   25  [39] SETTABLE  3 -20 4 ; "Badges" -
   26  [41] NEWTABLE  4 1 0
   27  [44] LOADK     5 -21 ; "SM_Brazen_Claws"
   28  [44] SETLIST   4 1 1 ; 1
   29  [44] SETTABLE  3 -22 4 ; "ArmyPainterTemplates" -
   30  [45] NEWTABLE  4 0 6
   31  [48] SETTABLE  4 -7 -23 ; "OfferName" "Devastator Marine Bonus Pack"
   32  [49] SETTABLE  4 -9 -24 ; "LicenseMask" "2"
   33  [51] NEWTABLE  5 3 0
   34  [53] LOADK     6 -25 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_arm_power_armour_armor_of_the_destroyer"
   35  [54] LOADK     7 -26 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_heavy_bolter_purge_of_victory_bay"
   36  [56] LOADK     8 -27 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_missile_launcher_unerring_thunderbolt"
   37  [56] SETLIST   5 3 1 ; 1
   38  [56] SETTABLE  4 -11 5 ; "NewCampaignWargear" -
   39  [58] NEWTABLE  5 4 0
   40  [60] LOADK     6 -28 ; "Dwarf Bronze"
   41  [61] LOADK     7 -29 ; "Abyss Blue"
   42  [62] LOADK     8 -30 ; "Abyss Magenta"
   43  [64] LOADK     9 -31 ; "Bubonic Brown"
   44  [64] SETLIST   5 4 1 ; 1
   45  [64] SETTABLE  4 -15 5 ; "Paints" -
   46  [66] NEWTABLE  5 1 0
   47  [69] LOADK     6 -32 ; "SM_Novamarines"
   48  [69] SETLIST   5 1 1 ; 1
   49  [69] SETTABLE  4 -20 5 ; "Badges" -
   50  [71] NEWTABLE  5 1 0
   51  [74] LOADK     6 -32 ; "SM_Novamarines"
   52  [74] SETLIST   5 1 1 ; 1
   53  [74] SETTABLE  4 -22 5 ; "ArmyPainterTemplates" -
   54  [75] NEWTABLE  5 0 6
   55  [78] SETTABLE  5 -7 -33 ; "OfferName" "Force Commander Bonus Pack"
   56  [79] SETTABLE  5 -9 -34 ; "LicenseMask" "4"
   57  [81] NEWTABLE  6 6 0
   58  [83] LOADK     7 -35 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_arm_power_armour_cuirass_of_azariah"
   59  [84] LOADK     8 -36 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_bolt_pistol_pistol_of_baal"
   60  [85] LOADK     9 -37 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp2_powerfist_gauntlet_of_blood"
   61  [86] LOADK     10 -38 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_com_iron_halo_laurels_of_hadrian"
   62  [87] LOADK     11 -39 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_plasma_gun_fearsome_light_of_faith"
   63  [89] LOADK     12 -40 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_arm_power_armour_mantle_of_the_great_father"
   64  [89] SETLIST   6 6 1 ; 1
   65  [89] SETTABLE  5 -11 6 ; "NewCampaignWargear" -
   66  [91] NEWTABLE  6 8 0
   67  [93] LOADK     7 -41 ; "Shining Gold"
   68  [94] LOADK     8 -42 ; "Mithril Silver"
   69  [95] LOADK     9 -43 ; "Abyss Purple"
   70  [96] LOADK     10 -44 ; "Insect Green"
   71  [97] LOADK     11 -45 ; "Iron"
   72  [98] LOADK     12 -46 ; "Abyss Orange"
   73  [99] LOADK     13 -47 ; "Brazen Brass"
   74 [101] LOADK     14 -48 ; "Metallic Blue"
   75 [101] SETLIST   6 8 1 ; 1
   76 [101] SETTABLE  5 -15 6 ; "Paints" -
   77 [103] NEWTABLE  6 2 0
   78 [105] LOADK     7 -49 ; "SM_Revilers"
   79 [107] LOADK     8 -50 ; "SM_Angels_Sanguine"
   80 [107] SETLIST   6 2 1 ; 1
   81 [107] SETTABLE  5 -20 6 ; "Badges" -
   82 [109] NEWTABLE  6 2 0
   83 [111] LOADK     7 -49 ; "SM_Revilers"
   84 [113] LOADK     8 -50 ; "SM_Angels_Sanguine"
   85 [113] SETLIST   6 2 1 ; 1
   86 [113] SETTABLE  5 -22 6 ; "ArmyPainterTemplates" -
   87 [114] NEWTABLE  6 0 6
   88 [117] SETTABLE  6 -7 -51 ; "OfferName" "Scout Marine Bonus Pack"
   89 [118] SETTABLE  6 -9 -52 ; "LicenseMask" "8"
   90 [120] NEWTABLE  7 3 0
   91 [122] LOADK     8 -53 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_arm_scout_armour_grim_silence"
   92 [123] LOADK     9 -54 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_combat_shotgun_initiates_lesson_of_strength"
   93 [125] LOADK     10 -55 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_sniper_rifle_deathtouch_of_the_angel"
   94 [125] SETLIST   7 3 1 ; 1
   95 [125] SETTABLE  6 -11 7 ; "NewCampaignWargear" -
   96 [127] NEWTABLE  7 4 0
   97 [129] LOADK     8 -56 ; "Abyss Yellow"
   98 [130] LOADK     9 -57 ; "Chainmail"
   99 [131] LOADK     10 -58 ; "Abyss Green"
  100 [133] LOADK     11 -59 ; "Rotting Flesh"
  101 [133] SETLIST   7 4 1 ; 1
  102 [133] SETTABLE  6 -15 7 ; "Paints" -
  103 [135] NEWTABLE  7 1 0
  104 [138] LOADK     8 -60 ; "SM_Angels_of_Redemption"
  105 [138] SETLIST   7 1 1 ; 1
  106 [138] SETTABLE  6 -20 7 ; "Badges" -
  107 [140] NEWTABLE  7 1 0
  108 [143] LOADK     8 -60 ; "SM_Angels_of_Redemption"
  109 [143] SETLIST   7 1 1 ; 1
  110 [143] SETTABLE  6 -22 7 ; "ArmyPainterTemplates" -
  111 [144] NEWTABLE  7 0 6
  112 [147] SETTABLE  7 -7 -61 ; "OfferName" "Tactical Marine Bonus Pack"
  113 [148] SETTABLE  7 -9 -62 ; "LicenseMask" "16"
  114 [150] NEWTABLE  8 3 0
  115 [152] LOADK     9 -63 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_arm_power_armour_armor_of_vandea"
  116 [153] LOADK     10 -64 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_bolter_unforgiving_truth"
  117 [155] LOADK     11 -65 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_acc_frag_grenade_pack_veterans_grenades"
  118 [155] SETLIST   8 3 1 ; 1
  119 [155] SETTABLE  7 -11 8 ; "NewCampaignWargear" -
  120 [157] NEWTABLE  8 4 0
  121 [159] LOADK     9 -66 ; "Bronze"
  122 [160] LOADK     10 -67 ; "Metallic Yellow"
  123 [161] LOADK     11 -68 ; "Metallic Orange"
  124 [163] LOADK     12 -69 ; "Spectral Blue"
  125 [163] SETLIST   8 4 1 ; 1
  126 [163] SETTABLE  7 -15 8 ; "Paints" -
  127 [165] NEWTABLE  8 1 0
  128 [168] LOADK     9 -70 ; "SM_Taurans"
  129 [168] SETLIST   8 1 1 ; 1
  130 [168] SETTABLE  7 -20 8 ; "Badges" -
  131 [170] NEWTABLE  8 1 0
  132 [173] LOADK     9 -70 ; "SM_Taurans"
  133 [173] SETLIST   8 1 1 ; 1
  134 [173] SETTABLE  7 -22 8 ; "ArmyPainterTemplates" -
  135 [174] NEWTABLE  8 0 7
  136 [177] SETTABLE  8 -7 -71 ; "OfferName" "Wargear Bonus Pack"
  137 [178] SETTABLE  8 -9 -72 ; "LicenseMask" "32"
  138 [180] NEWTABLE  9 1 0
  139 [183] LOADK     10 -74 ; "data:maps\\pvp\\2p_legishighstratum"
  140 [183] SETLIST   9 1 1 ; 1
  141 [183] SETTABLE  8 -73 9 ; "Maps" -
  142 [185] NEWTABLE  9 7 0
  143 [187] LOADK     10 -75 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_com_battle_standard_chapter_battle_standard"
  144 [188] LOADK     11 -76 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp2_chainsword_snarl_of_the_wolf"
  145 [189] LOADK     12 -77 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_plasma_gun_purifier_of_tombs"
  146 [190] LOADK     13 -78 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_sniper_rifle_honored_silence"
  147 [191] LOADK     14 -79 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_wp1_heavy_bolter_scourge_of_xenos"
  148 [192] LOADK     15 -80 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_acc_purity_seal_light_of_the_astronomicon"
  149 [194] LOADK     16 -81 ; "wargear\\wargear\\campaign\\playable\\race_marine\\sm_acc_purity_seal_parable_of_the_lion"
  150 [194] SETLIST   9 7 1 ; 1
  151 [194] SETTABLE  8 -11 9 ; "NewCampaignWargear" -
  152 [196] NEWTABLE  9 4 0
  153 [198] LOADK     10 -82 ; "Scaly Green"
  154 [199] LOADK     11 -83 ; "Platinum"
  155 [200] LOADK     12 -84 ; "Golden Purple"
  156 [202] LOADK     13 -85 ; "Metallic Red"
  157 [202] SETLIST   9 4 1 ; 1
  158 [202] SETTABLE  8 -15 9 ; "Paints" -
  159 [204] NEWTABLE  9 1 0
  160 [207] LOADK     10 -86 ; "SM_Marauders"
  161 [207] SETLIST   9 1 1 ; 1
  162 [207] SETTABLE  8 -20 9 ; "Badges" -
  163 [209] NEWTABLE  9 1 0
  164 [212] LOADK     10 -86 ; "SM_Marauders"
  165 [212] SETLIST   9 1 1 ; 1
  166 [212] SETTABLE  8 -22 9 ; "ArmyPainterTemplates" -
  167 [214] SETLIST   2 6 1 ; 1
  168 [214] SETTABLE  1 -6 2 ; "Offers" -
  169 [216] SETLIST   0 1 1 ; 1
  170 [216] SETGLOBAL 0 -1 ; DLC
  171 [218] NEWTABLE  0 7 0
  172 [219] NEWTABLE  1 0 2
  173 [221] SETTABLE  1 -88 -89 ; "map_name" "6P_Typhon_plateau"
  174 [222] SETTABLE  1 -90 -91 ; "map_mode_type" "3_vs_3"
  175 [223] NEWTABLE  2 0 2
  176 [225] SETTABLE  2 -88 -92 ; "map_name" "6p_Calderis_outskirts"
  177 [226] SETTABLE  2 -90 -91 ; "map_mode_type" "3_vs_3"
  178 [227] NEWTABLE  3 0 2
  179 [229] SETTABLE  3 -88 -93 ; "map_name" "6p_MeridianHighCity"
  180 [230] SETTABLE  3 -90 -91 ; "map_mode_type" "3_vs_3"
  181 [231] NEWTABLE  4 0 2
  182 [233] SETTABLE  4 -88 -94 ; "map_name" "6p_arena"
  183 [234] SETTABLE  4 -90 -91 ; "map_mode_type" "3_vs_3"
  184 [235] NEWTABLE  5 0 2
  185 [237] SETTABLE  5 -88 -95 ; "map_name" "2p_CalderisDunes"
  186 [238] SETTABLE  5 -90 -96 ; "map_mode_type" "1_vs_1"
  187 [239] NEWTABLE  6 0 2
  188 [241] SETTABLE  6 -88 -97 ; "map_name" "2p_TyphonSwamp"
  189 [242] SETTABLE  6 -90 -96 ; "map_mode_type" "1_vs_1"
  190 [243] NEWTABLE  7 0 2
  191 [245] SETTABLE  7 -88 -98 ; "map_name" "1v1typhonlowlands"
  192 [246] SETTABLE  7 -90 -96 ; "map_mode_type" "1_vs_1"
  193 [248] SETLIST   0 7 1 ; 1
  194 [248] SETGLOBAL 0 -87 ; QuickMatchMaps
  195 [250] LOADK     0 -100 ; "200"
  196 [250] SETGLOBAL 0 -99 ; LocalPingAutomatchAdjustment
  197 [250] RETURN    0 1

Once you've looked at the contents of the file, it is trivial to load it into a hex editor and replace the 1, 2, 4, 8, 16 and 32 strings with 0, 0, 0, 0, 00 and 00 respectively.

Dawn of War II Unlockable Content

Dawn of War II has unlockable content; that is certain maps, campaign wargear, army painter colours and so on are locked by default, and have to be unlocked using special codes. People who pre-ordered the game got a code or two, and codes are also available through other promotional sources.

This unlockable content is implemented as follows:

The standard approaches to unlocking all of the content might include:

Neither of the above methods would work, due to the cryptographic checks done on the content.bin and DOW2.exe files. The weakpoint of DoW2's system is how these checks are done. As previously stated, wintrust.dll (a Microsoft DLL which lives in C:\Windows\System32) is used to make sure that the security catalogue files are valid and successfully verify DOW2.exe and content.bin, using the WinVerifyTrust[Ex] function. Furthermore, wintrust.dll itself is not cryptographically signed. If wintrust.dll is copied from the System32 directory to the dawn of war directory and then modified so that WinVerifyTrust always returns ERROR_SUCCESS, then the cryptographic checks are sidestepped (ERROR_SUCCESS is conveniently the value zero, so this simply means replacing the first five bytes of WinVerifyTrust with xor eax, eax; retn 12; or 33 C0 C2 0C 00 in machine code).

If content.bin is now modified so that the unlock masks are all zero, then when the game is run, it'll load wintrust.dll from the game directory rather than the System32 directory (DLLs in the 'current' directory override those in the system directory by default), and when it comes to verify content.bin, the patched WinVerifyTrust returns ERROR_SUCCESS, and so the game believes that the file is still cryptographically signed. It then comes to see what content the GFWL account has unlocked by doing 'does UnlockMask bitwise-and UnlockBits equal UnlockMask' for each unlock set, and as zero bitwise-and anything does equal zero, it'll unlock all of the content.

For further reading, see part 2.

sga4to5

The Dawn of War II Beta is upon us, which brings with it a series of new file formats compared to Dawn of War 1 and Company of Heroes. The most important of these is the SGA format, which is the archive format used by Relic games for game assets. Dawn of War 1 used version 2 SGA files, Company of Heroes used version 4 and Dawn of War II uses version 5 (for completeness, I think Impossible Creatures used version 1, some unreleased project or The Outfit used version 3, and Company of Heroes Online used version 4.1).

I've already released SgaReader2 for looking inside version 5 (and 4.1, 4 and 2) SGA archives. In order to actually mod Dawn of War II properly, we need to be make to make new version 5 archives rather than just read existing ones, which is where sga4to5 comes in.

sga4to5 is a small (8 kilobyte) application which converts a version 4 SGA archive into a version 5 archive.
Download: sga4to5.exe

Combine this tool with something capable of making version 4 SGA archives (e.g. Mod Studio with a CoH mod loaded, or CoH's archive.exe) and you can create new version 5 SGA archives and start modding Dawn of War II. Note that unlike most of my tools, sga4to5 is a command line application, so a quick guide to its command line parameters is in order:

Usage: sga4to5.exe -i[in[put]] file -o[ut[put]] file [-name newname] [-q[uiet]] [-v[erbose]]
  -i, -in or -input specify the input file (version 4.0 / CoH SGA)
  -o, -out or -output specify the output file (version 5.0 / DoW2 SGA)
  -name changes the name in the file header of the output
  -q or -quiet reduces the amount written to the console
  -v or -verbose causes more than usual to be written to the console

Sample usage:
sga4to5.exe -i "E:\Valve\Steam\SteamApps\common\warhammer 40,000 dawn of war ii - beta\MahArchives\v4Attrib.sga" -o "E:\Valve\Steam\SteamApps\common\warhammer 40,000 dawn of war ii - beta\MahArchives\GameAttrib.sga" -v -name "Attributes"

Output from above command:
-- Corsix's SGA v4 to v5 Convertor --
Opened input file and output file
Input archive details:
  name: Made with Corsix's Rainman
  version: 4.0
  data header offset: 184
  data header size: 237960
  data offset: 238144
  data length: 13494961
  content MD5: 0F6155399F5200D5F1FB5F890052542F
  header  MD5: 961A968EDB37216BE0D88DB5EDE95D48
Copying file data, this may take a while...
Output archive details:
  name: Attributes
  version: 5.0
  data header offset: 13495157
  data header size: 237958
  data offset: 196
  data length: 13494961
  content MD5: C1322F3DE54A35A93D14F6A5DEF04ACD
  header  MD5: 4A6A47C63F46490A75800869CF715F75
Done

The actual conversion details are not too interesting; change the version number, add an extra field to the file header, relocate the data header to the end of the file, shrink the TOC records by 2 bytes each, update various offset and length fields, and recalculate the checksums. There are a few interesting implementation details like the initialisation vectors used for checksums and the precise definition of what data is checksummed, but the process is fairly simple conceptually.

More interesting (to me) is the process of squishing the program down to a mere 8 kilobytes. By default, a C++ program compiled using Visual Studio will either dynamically link to msvcr[t|p]90.dll (Microsoft's C/C++ runtime library) or include the required parts of the CRT within the executable itself. Using the former method, sga4to5.exe came out to around 18 kilobytes, but it referenced a several-hundred-kilobyte DLL, and using the latter method, it came to around 70 kilobytes. For a small program, the several-hundred-kilobyte runtime DLL is very excessive, and the 70 kilobyte amalgamation is still 10 times larger than it needs to be. To get any smaller, the C runtime has to be removed, which is a non-trivial process:

Note that the above list is the steps that I had to take to make sga4to5.exe CRT-less, other applications may have other requirements upon the CRT which are harder to remove (YMMV). After performing the above steps, sga4to5.exe came out to 14 kilobytes and referenced only two DLLs, both of which are core Windows DLLs: Kernel32.dll (for CloseHandle, CompareStringEx, CreateFileW, GetCommandLineW, GetStdHandle, ReadFile, SetFilePointer, VirtualAlloc, VirtualFree, WriteConsoleW and WriteFile) and Shell32.dll (for CommandLineToArgvW). The final step was to pass the executable through UPX, which squished it down a bit more to the final size of 8 kilobytes.

Lua 5.1.4 bug recap

  1. Causing the currently running function to be garbage collected
    By using bytecode manipulation, a function's upvalues can point to all instances of that function (i.e. the local in which it is stored, and the stack slot it is placed in while being called), and thus cause them all to become nil. If the function then forces a GC cycle, then it will be collected while still running, leading to a segfault:
function Evil()
  local Experimental, _2
  
  Experimental = function()
    -- Erase all references in the stack to
    -- this (currently running) function:
    Experimental = nil
    _2 = nil -- (this line only does so after bytecode manipulation)
    
    -- Do some cycles of garbage collection to free ourselves, and
    -- some allocations to try and overwrite the memory:
    for i = 1, 10 do
      collectgarbage "collect"
      alloc()
    end
    
    -- A segfault will probably now have occured
  end
  
  Experimental()
end

-- Do some bytecode manipulation of the Evil function:
Es = ('').dump(Evil)
Es = Es:gsub("(\36..."      -- OP_CLOSURE
          .. "%z%z%z%z"     -- Use local 0 as upvalue 0
          .. "%z%z)\128%z"  -- Use local 1 as upvalue 1
          ,
             "%1\0\1")      -- OP_CLOSURE, using locals 0 and 2 as
                            -- upvalues 0 and 1 (local 0 is the
                            -- Experimental function, local 2 is 
                            -- where the function is placed for the
                            -- call)
Evil = loadstring(Es)

-- Function to trash some memory:
function alloc()
  local t = {}
  for i = 1, 100 do
    t[i] = i
  end
end

-- Run the evil:
Evil()
  1. Accessing other function's locals
    The VM call instruction is traditionally done at the top of the stack. However, through bytecode manipulation, it can be done in the middle of the stack, and then after the call is complete, any locals used by the called function will be left at the top of the stack. If a C function was called, then its locals could be used to cause a segfault:
-- Define a Lua function which calls a 'complex' C function
function X()
  io.close()
end

--[[
  X currently looks something like this:
    (2 locals, 2 constants)
    GETGLOBAL "io"
    GETTABLE  "close"
    CALL register 0
    RETURN nothing   
--]]

-- Make some modifications to X
Xs = string.dump(X)
Xs = Xs:gsub("\2(\4%z%z%z)","\20%1")
  -- num locals, num instructions; 2, 4 -> 20, 4
Xs = Xs:gsub("\30%z\128%z","\30\0\0\8")
  -- return nothing -> return all locals
X = assert(loadstring(Xs))

--[[
  X now looks something like:
    (20 locals, 2 constants)
    GETGLOBAL "io"
    GETTABLE  "close"
    CALL register 0
    RETURN registers 0 through 16
    
  Calling X now returns some of what io.close
  left on the stack when it returned!!!
--]]

-- Take the io environment table, and remove the __close method
select(3, X()).__close = nil

--[[
  Call io.close again, within which, these two
  lines cause a segfault:
   lua_getfield(L, -1, "__close");
   return (lua_tocfunction(L, -1))(L);
--]]
X()
  1. Upvalue name array is assumed to be either complete, or absent
    Through bytecode manipulation, an incomplete upvalue name array can be present, which can then lead to a segfault when the interpreter tries to access an element of the array which is not present:
-- Configure these parameters for your environment
sizeof_int = 4    -- sizeof(size_t) in C
sizeof_size_t = 4 -- sizeof(int) in C
endian = "small"  -- "small" or "big"

-- do ... end block so that the locals are used if this is typed 
-- line by line into the interpreter
do
  -- define some locals to be used as upvalues
  local a, b, c
  -- define a function using upvalues
  function F()
    -- Make sure that upvalues #1 through #2 refer to a, b and c
    local _ = {a, b, c}
    -- This line will generate an error referring to upvalue #3
    return c[b][a]
  end
end

-- Convert function F to it's binary form
-- (the values of the upvalues are not dumped)
S = string.dump(F)

-- Remove the upvalue names of upvalues #2 and #3 from the 
-- debug information
if endian == "small" then
  -- We need at-least one upvalue name, or else the upvalue name 
  -- array will be of zero length and thus be NULL (lua allocator
  -- must return NULL when nsize == 0). Thus reduce the upvalue
  -- name array to a single entry.
  P = S:gsub("\3".. ("%z"):rep(sizeof_int - 1) ..
              -- Number of upvalue names (3)
             "\2".. ("%z"):rep(sizeof_size_t - 1) ..".%z"..
              -- Name of upvalue #1 (length 2, "a\0")
             "\2".. ("%z"):rep(sizeof_size_t - 1) ..".%z"
              -- Name of upvalue #2 (length 2, "b\0")
             ,
             "\1".. ("\0"):rep(sizeof_int - 1)
              -- Number of upvalue names (1)
            )
else
  -- Same as previous code, but for big-endian integers
  P = S:gsub(("%z"):rep(sizeof_int - 1) .."\3"..
             ("%z"):rep(sizeof_size_t - 1) .."\2.%z"..
             ("%z"):rep(sizeof_size_t - 1) .."\2.%z"
             ,
             ("\0"):rep(sizeof_int - 1) .. "\1"
            )
end

-- Load the modified binary
M = assert(loadstring(S))

-- Execute the modified function
-- This should cause the error "attempt to index upvalue 'c' (a 
-- nil value)". However, as the name of upvalue #3 is no longer
-- in the upvalue name array, when the VM goes to generate the
-- error message, it references past the end of the upvalue name
-- array, leading to a segfault
M()
  1. LoadString()'s return value is not always checked for NULL
    When loading a string in a binary chunk, LoadString returns NULL for zero-length strings (all string constants contain a null-terminator, and are therefore at least length 1), which in one case is not checked for and leads to a segfault:
loadstring(
  ('').dump(function()X''end)
  :gsub('\2%z%z%zX','\0\0\0')
)()

Generating Lua Inheritance Trees Quickly

Relic games store their entity/building/squad/etc. attributes in Lua files. They have alot of Lua files, all of which look something like this:

-- Some comment about copyright and editing the file by hand
GameData = Inherit([[sbps\races\chaos\chaos_squad.nil]])
GameData["squad_cap_ext"]["support_cap_usage"] = 2.00000
GameData["squad_combat_stance_ext"] = Reference([[sbpextensions\squad_combat_stance_ext.lua]])
-- more changes to GameData...

If you want to create an editor like their official Lua Attribute Editor, then you need to build an inheritance tree out of all those Inherit calls in all those files. The official editor is known for being slow, but I believe I've found a nice fast way to build it.

The first component of this is a function which is given the name of one Lua file, and returns the name of the file that it inherits from. You could load the entire file as text into memory and do search for Inherit( and extract the filename like that, however that would have the following disadvantages:

It would be alot simpler to let the official Lua library handle the parsing and lexing of the source file, and then grab the information we wanted out of the parsed state. So that is exactly what I do:

#include <lua.h>
#include <lauxlib.h>
// ...
struct read_info_t {
  IFile *pFile;
  char cBuffer[1024];
};
const char* ReaderFn(lua_State *L, read_info_t* dt, size_t *size) {
  *size = dt->pFile->readNoThrow(dt->cBuffer, 1, 1024);
  return dt->cBuffer;
}
// ...
lua_State L = luaL_newstate();
read_info_t oReadInfo;
oReadInfo.pFile = RainOpenFile(L"filename", FM_Read);
lua_load(L, (lua_Reader)ReaderFn, (void*)&oReadInfo, "lua file");

The other advantage of using the Lua API to parse the file is that you don't have to read it all in at once; lua_load takes the file a piece at a time.

Now comes the clever part; finding that Inherit filename from the lua_State. The call to Inherit would have compiled to Lua bytecodes:

GETGLOBAL "Inherit" -- Push the global `Inherit` onto the stack
LOADK "filename" -- Load the constants "filename" onto the stack
CALL 1 1 -- Call a function with one argument and one result

Thus if we look through the compiled bytecode looking for a GETGLOBAL Inherit followed by a LOADK and a CALL, we can obtain the filename from the LOADK:

#include <lobject.h>
#include <lopcodes.h>
#include <lstate.h>
// ...
Proto *pPrototype = ((Closure*)lua_topointer(L, -1))->l.p;
Instruction* I = pPrototype->code;
for(int ip = 0; ip < pPrototype->sizecode; ++ip, ++I) {
  if(GET_OPCODE(*I) == OP_GETGLOBAL
  && strcmp(svalue(pPrototype->k + GETARG_Bx(*I)), "Inherit") == 0
  && GET_OPCODE(I[1]) == OP_LOADK
  && GET_OPCODE(I[2]) == OP_CALL) {
    return svalue(pPrototype->k + GETARG_Bx(I[1]));
  }
}

Delving into the Lua internals like that is rather rude, and may be made incompatible even by minor version releases of Lua, but I'll be damned if it isn't extremely fast.

Now that we know which file is Inherit-ed by each other file, it is trivial to construct this into a tree.

page: 18 19 20 21 22