mirror of
https://github.com/mandiant/capa.git
synced 2026-04-28 11:53:20 -07:00
fix: correct off-by-one in get_dotnet_table_row so row_index=1 is not rejected
`get_dotnet_table_row` used `if row_index - 1 <= 0` to guard against invalid indices. Because .NET metadata tables are 1-indexed, row_index=1 is the first valid row, but the condition is equivalent to `row_index <= 1`, silently rejecting it and making the first row of every table unreachable. Changed to `if row_index <= 0`, which correctly rejects only the zero/null token and leaves all valid rows accessible. Added four unit tests against the real dd9098ff91717f4906afe9dafdfa2f52.exe_ sample to verify the guard boundary: row_index=1 returns the first row, row_index=0 returns None, all row indices 1..N succeed, and an out-of-bounds index returns None. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
-
|
-
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
- fix: correct off-by-one in get_dotnet_table_row so row_index=1 (first valid .NET metadata row) is no longer rejected @williballenthin
|
||||||
- fix: add missing import for assert_never in cape extractor.py to avoid NameError when call argument has unexpected type @williballenthin
|
- fix: add missing import for assert_never in cape extractor.py to avoid NameError when call argument has unexpected type @williballenthin
|
||||||
- fix: stop mutating call.api in cape thread.get_calls; yield one CallHandle per call so the original API name is preserved for all handles @williballenthin
|
- fix: stop mutating call.api in cape thread.get_calls; yield one CallHandle per call so the original API name is preserved for all handles @williballenthin
|
||||||
- fix: use instruction_indices in is_security_cookie to handle single-instruction basic blocks where end_index is omitted, preventing KeyError on -1 @williballenthin
|
- fix: use instruction_indices in is_security_cookie to handle single-instruction basic blocks where end_index is omitted, preventing KeyError on -1 @williballenthin
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ def get_dotnet_table_row(pe: dnfile.dnPE, table_index: int, row_index: int) -> O
|
|||||||
assert pe.net is not None
|
assert pe.net is not None
|
||||||
assert pe.net.mdtables is not None
|
assert pe.net.mdtables is not None
|
||||||
|
|
||||||
if row_index - 1 <= 0:
|
if row_index <= 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(table_index)
|
table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(table_index)
|
||||||
|
|||||||
@@ -12,8 +12,15 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import dnfile
|
||||||
import fixtures
|
import fixtures
|
||||||
|
|
||||||
|
from capa.features.extractors.dnfile.helpers import get_dotnet_table_row
|
||||||
|
|
||||||
|
DOTNET_DIR = Path(__file__).resolve().parent / "data" / "dotnet"
|
||||||
|
|
||||||
|
|
||||||
@fixtures.parametrize(
|
@fixtures.parametrize(
|
||||||
"sample,scope,feature,expected",
|
"sample,scope,feature,expected",
|
||||||
@@ -31,3 +38,38 @@ def test_dnfile_features(sample, scope, feature, expected):
|
|||||||
)
|
)
|
||||||
def test_dnfile_feature_counts(sample, scope, feature, expected):
|
def test_dnfile_feature_counts(sample, scope, feature, expected):
|
||||||
fixtures.do_test_feature_count(fixtures.get_dnfile_extractor, sample, scope, feature, expected)
|
fixtures.do_test_feature_count(fixtures.get_dnfile_extractor, sample, scope, feature, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_dotnet_table_row_first_row():
|
||||||
|
"""row_index=1 is the first valid .NET metadata row; it must not be rejected."""
|
||||||
|
pe = dnfile.dnPE(DOTNET_DIR / "dd9098ff91717f4906afe9dafdfa2f52.exe_")
|
||||||
|
row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, 1)
|
||||||
|
assert row is not None
|
||||||
|
assert str(row.TypeName) == "<Module>"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_dotnet_table_row_invalid_zero():
|
||||||
|
"""row_index=0 is the null token; the function must return None."""
|
||||||
|
pe = dnfile.dnPE(DOTNET_DIR / "dd9098ff91717f4906afe9dafdfa2f52.exe_")
|
||||||
|
assert get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, 0) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_dotnet_table_row_valid_rows():
|
||||||
|
"""All valid row indices 1..N return a row from the real PE."""
|
||||||
|
pe = dnfile.dnPE(DOTNET_DIR / "dd9098ff91717f4906afe9dafdfa2f52.exe_")
|
||||||
|
assert pe.net is not None
|
||||||
|
assert pe.net.mdtables is not None
|
||||||
|
table = pe.net.mdtables.tables.get(dnfile.mdtable.TypeDef.number)
|
||||||
|
assert table is not None
|
||||||
|
for row_index in range(1, len(table.rows) + 1):
|
||||||
|
assert get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, row_index) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_dotnet_table_row_out_of_bounds():
|
||||||
|
"""row_index beyond the table size returns None."""
|
||||||
|
pe = dnfile.dnPE(DOTNET_DIR / "dd9098ff91717f4906afe9dafdfa2f52.exe_")
|
||||||
|
assert pe.net is not None
|
||||||
|
assert pe.net.mdtables is not None
|
||||||
|
table = pe.net.mdtables.tables.get(dnfile.mdtable.TypeDef.number)
|
||||||
|
assert table is not None
|
||||||
|
assert get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, len(table.rows) + 1) is None
|
||||||
|
|||||||
Reference in New Issue
Block a user