Initial lifting POC

This commit is contained in:
Duncan Ogilvie
2026-05-09 15:09:52 +02:00
parent d0c719bc98
commit beec01d84e
7 changed files with 381 additions and 14 deletions
+6
View File
@@ -1 +1,7 @@
__pycache__/
*.id0
*.id1
*.id2
*.nam
*.til
*.i64
+21
View File
@@ -0,0 +1,21 @@
# binaryshield-lifter
## Requirements
- [uv](https://astral.sh/uv)
- [CMake](https://cmake.org)
- LLVM 21+
## Building
You need LLVM 21 or higher. Before running the first build, allow CMake to find LLVM:
```bash
export LLVM_ROOT=$(brew --prefix llvm)
```
Set up the virtual environment:
```bash
uv sync
```
+20 -13
View File
@@ -5,7 +5,13 @@ from pathlib import Path
import icicle
import pefile
from capstone import CS_ARCH_X86, CS_MODE_64, Cs
from capstone import (
CS_ARCH_X86,
CS_MODE_64,
CS_MODE_32,
Cs,
CsInsn,
)
FUNCTION_ADDRESS = 0x00000001400016D0
@@ -45,7 +51,7 @@ class PEmulator:
self.stack_size = 0x10000
self.stack_base = self.allocate(self.stack_size)
self.md = Cs(CS_ARCH_X86, CS_MODE_64)
self.md = Cs(CS_ARCH_X86, CS_MODE_64 if arch_name == "x86_64" else CS_MODE_32)
def page_align(self, size: int) -> int:
return (size + 0xFFF) & ~0xFFF
@@ -161,24 +167,25 @@ class PEmulator:
self.ic.reg_write("RCX", rcx)
self.ic.reg_write("RIP", callee)
def cs_disasm(self, address: int, code: bytes) -> CsInsn:
for insn in self.md.disasm(code, address, count=1): # ty: ignore[missing-argument, invalid-argument-type]
return insn
raise ValueError(f"Failed to disassemble {code.hex()}@{hex(address)}")
def disassemble_one(self, address: int) -> tuple[str, str, int]:
# x86-64 instructions are at most 15 bytes. Try shorter reads if close to
# an unmapped/protected page boundary.
last_error = None
for size in range(15, 0, -1):
try:
code = self.ic.mem_read(address, size)
except Exception as exc: # icicle.MemoryException in normal failure cases
last_error = exc
except Exception: # icicle.MemoryException in normal failure cases
continue
insns = list(self.md.disasm(code, address, count=1))
if insns:
insn = insns[0]
text = insn.mnemonic
if insn.op_str:
text += f" {insn.op_str}"
return insn.mnemonic, text, insn.size
raise RuntimeError(f"Could not disassemble at {address:#x}: {last_error}")
insn = self.cs_disasm(address, code)
text = insn.mnemonic
if insn.op_str:
text += f" {insn.op_str}"
return insn.mnemonic, text, insn.size
raise RuntimeError("disassembly failed")
def trace_call(
self,
+222
View File
@@ -0,0 +1,222 @@
from contextlib import contextmanager
from capstone import (
CS_ARCH_X86,
CS_MODE_64,
CS_OP_REG,
CS_OP_MEM,
CS_OP_IMM,
Cs,
CsInsn,
)
from capstone.x86 import X86Op
from capstone.x86_const import (
X86_INS_ADD,
X86_INS_AND,
X86_INS_CMOVNE,
X86_INS_INC,
X86_INS_JMP,
X86_INS_LEA,
X86_INS_MOV,
X86_INS_NOP,
X86_INS_OR,
X86_INS_POP,
X86_INS_POPFQ,
X86_INS_PUSH,
X86_INS_PUSHFQ,
X86_INS_RET,
X86_INS_SUB,
X86_INS_XOR,
)
import llvm
class Lifter:
def __init__(self, module: llvm.Module):
self.cs = Cs(CS_ARCH_X86, CS_MODE_64)
self.cs.detail = True
self.module = module
self.context = module.context
types = self.context.types
self.types = types
self.reg_sizes = {
"rax": 64,
"rbx": 64,
"rcx": 64,
"rdx": 64,
"rsi": 64,
"rdi": 64,
"rsp": 64,
"rbp": 64,
"r8": 64,
"r8": 64,
"r10": 64,
"r11": 64,
"r12": 64,
"r13": 64,
"r14": 64,
"r15": 64,
"rip": 64,
"cf": 8,
"zf": 8,
"sf": 8,
"of": 8,
"pf": 8,
"af": 8,
}
self.reg_types = {
name: self.types.int_n(size) for name, size in self.reg_sizes.items()
}
self.reg_ptrs: dict[str, llvm.Value] = {}
self.state_ty = types.struct(self.reg_types.values(), name="State")
self.function: llvm.Function | None = None
self.lifted_ty = types.function(types.void, [types.ptr, types.ptr])
@staticmethod
@contextmanager
def create(module_name="lifted"):
with llvm.create_context() as context:
with context.create_module(module_name) as module:
yield Lifter(module)
def cs_disasm(self, address: int, code: bytes) -> CsInsn:
for insn in self.cs.disasm(code, address, count=1): # ty: ignore[missing-argument, invalid-argument-type]
return insn
raise ValueError(f"Failed to disassemble {code.hex()}@{hex(address)}")
def switch_function(self, name: str):
fn = self.module.get_function(name)
if fn is None:
fn = self.module.add_function(name, self.lifted_ty)
fn.linkage = llvm.Linkage.Internal
memory, state = fn.params
memory.name = "memory"
state.name = "state"
entry = fn.append_basic_block("initialize")
assert fn.last_basic_block == entry
with entry.create_builder() as builder:
for i, name in enumerate(self.reg_sizes.keys()):
reg_ptr = builder.struct_gep(self.state_ty, state, i, name)
self.reg_ptrs[name] = reg_ptr
else:
self.reg_ptrs = {}
entry = fn.entry_block
assert fn.last_basic_block == entry
assert entry.name == "initialize", (
"unexpected basic block for lifted function"
)
for insn in entry.instructions:
if (
insn.opcode == llvm.Opcode.GetElementPtr
and insn.gep_source_element_type == self.state_ty
):
assert insn.name in self.reg_types, "unexpected GEP"
self.reg_ptrs[insn.name] = insn
self.function = fn
def _reg_read(self, builder: llvm.Builder, name: str):
reg_ptr = self.reg_ptrs[name]
return builder.load(self.reg_types[name], reg_ptr)
def _reg_write(self, builder: llvm.Builder, name: str, value: llvm.Value):
reg_ptr = self.reg_ptrs[name]
# TODO: zero extend/cast?
builder.store(value, reg_ptr)
def _operand_read(
self, builder: llvm.Builder, insn: CsInsn, index: int
) -> llvm.Value:
op: X86Op = insn.operands[index]
if op.type == CS_OP_REG:
name: str = insn.reg_name(op.reg) # pyright: ignore[reportAssignmentType]
return self._reg_read(builder, name)
if op.type == CS_OP_IMM:
# TODO: is the sign handled correctly?
return self.types.int_n(op.size * 8).constant(op.imm)
if op.type == CS_OP_MEM:
raise NotImplementedError("CS_OP_MEM")
assert False, "unreachable"
def _operand_write(
self, builder: llvm.Builder, insn: CsInsn, index: int, value: llvm.Value
):
op: X86Op = insn.operands[index]
if op.type == CS_OP_REG:
name: str = insn.reg_name(op.reg) # pyright: ignore[reportAssignmentType]
self._reg_write(builder, name, value)
elif op.type == CS_OP_IMM:
raise ValueError("Cannot write to CS_OP_IMM")
elif op.type == CS_OP_MEM:
raise NotImplementedError("CS_OP_MEM")
def _lift_add(self, builder: llvm.Builder, insn: CsInsn):
op2 = self._operand_read(builder, insn, 1)
op1 = self._operand_read(builder, insn, 0)
result = builder.add(op1, op2)
self._operand_write(builder, insn, 0, result)
# TODO: flags
def _mem_write(self, builder: llvm.Builder, addr: llvm.Value, value: llvm.Value):
assert self.function, "call switch first"
memory = self.function.get_param(0)
ptr = builder.gep(self.types.i8, memory, [addr])
builder.store(value, ptr)
def _lift_push(self, builder: llvm.Builder, insn: CsInsn):
value = self._operand_read(builder, insn, 0)
rsp = self._reg_read(builder, "rsp")
rsp_sub = builder.sub(rsp, self.types.i64.constant(8))
self._reg_write(builder, "rsp", rsp_sub)
self._mem_write(builder, rsp_sub, value)
def lift_insn(self, address: int, code: bytes):
assert self.function, f"You need to call switch_function first!"
# Create a new block to lift the instruction
# TODO: how to handle conditional jumps?
last_block = self.function.last_basic_block
insn_block = self.function.append_basic_block(f"lifted_{hex(address)}")
with last_block.create_builder() as builder:
builder.br(insn_block)
insn = self.cs_disasm(address, code)
print(insn.mnemonic, insn.op_str)
with insn_block.create_builder() as builder:
self._reg_write(builder, "rip", self.types.i64.constant(address))
if insn.id == X86_INS_ADD:
self._lift_add(builder, insn)
elif insn.id == X86_INS_PUSH:
self._lift_push(builder, insn)
else:
raise NotImplementedError(
f"Instruction not implemented: {insn.mnemonic}"
)
print(insn_block)
def lift_end(self):
assert self.function
with self.function.last_basic_block.create_builder() as builder:
builder.ret_void()
def main():
with Lifter.create() as lifter:
lifter.switch_function("handler1")
lifter.switch_function("handler1")
lifter.lift_insn(0x14001603E, bytes.fromhex("4D 01 F5"))
lifter.lift_insn(0x140017A41, bytes.fromhex("68 8F 67 01 00"))
lifter.lift_end()
lifter.module.verify_or_raise()
print(lifter.module)
pass
if __name__ == "__main__":
main()
+10
View File
@@ -5,5 +5,15 @@ requires-python = ">=3.12"
dependencies = [
"capstone>=5.0.7",
"icicle-emu>=0.0.11",
"llvm-nanobind",
"pefile>=2024.8.26",
]
[tool.uv.sources]
llvm-nanobind = { git = "https://github.com/LLVMParty/llvm-nanobind" }
[dependency-groups]
dev = [
"ruff>=0.15.12",
"ty>=0.0.34",
]
+33
View File
@@ -0,0 +1,33 @@
.binshld:0000000140017A41 sub_140017A41 proc near ; CODE XREF: sub_1400016D0↑j
.binshld:0000000140017A41 68 8F 67 01 00 push 1678Fh
.binshld:0000000140017A46 E9 B5 E5 FF FF jmp sub_140016000
.binshld:0000000140017A46 sub_140017A41 endp
.binshld:0000000140016000 41 57 push r15
.binshld:0000000140016002 41 56 push r14
.binshld:0000000140016004 41 55 push r13
.binshld:0000000140016006 41 54 push r12
.binshld:0000000140016008 41 53 push r11
.binshld:000000014001600A 41 52 push r10
.binshld:000000014001600C 41 51 push r9
.binshld:000000014001600E 41 50 push r8
.binshld:0000000140016010 55 push rbp
.binshld:0000000140016011 57 push rdi
.binshld:0000000140016012 56 push rsi
.binshld:0000000140016013 52 push rdx
.binshld:0000000140016014 51 push rcx
.binshld:0000000140016015 53 push rbx
.binshld:0000000140016016 50 push rax
.binshld:0000000140016017 9C pushfq
.binshld:0000000140016018 49 89 E7 mov r15, rsp
.binshld:000000014001601B 48 81 EC 00 01 00 00 sub rsp, 100h
.binshld:0000000140016022 65 4C 8B 34 25 60 00 00 mov r14, gs:60h
.binshld:0000000140016022 00
.binshld:000000014001602B 4D 8B 76 10 mov r14, [r14+10h]
.binshld:000000014001602F 4C 89 B4 24 88 00 00 00 mov [rsp+180h+var_F8], r14
.binshld:0000000140016037 45 8B AF 80 00 00 00 mov r13d, [r15+80h]
.binshld:000000014001603E 4D 01 F5 add r13, r14
.binshld:0000000140016041 41 8B 45 00 mov eax, [r13+0]
.binshld:0000000140016045 49 83 C5 04 add r13, 4
.binshld:0000000140016049 4C 01 F0 add rax, r14
.binshld:000000014001604C FF E0 jmp rax
Generated
+69 -1
View File
@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.12"
[[package]]
@@ -9,16 +9,30 @@ source = { virtual = "." }
dependencies = [
{ name = "capstone" },
{ name = "icicle-emu" },
{ name = "llvm-nanobind" },
{ name = "pefile" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
{ name = "ty" },
]
[package.metadata]
requires-dist = [
{ name = "capstone", specifier = ">=5.0.7" },
{ name = "icicle-emu", specifier = ">=0.0.11" },
{ name = "llvm-nanobind", git = "https://github.com/LLVMParty/llvm-nanobind" },
{ name = "pefile", specifier = ">=2024.8.26" },
]
[package.metadata.requires-dev]
dev = [
{ name = "ruff", specifier = ">=0.15.12" },
{ name = "ty", specifier = ">=0.0.34" },
]
[[package]]
name = "capstone"
version = "5.0.7"
@@ -50,6 +64,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/3d/98030b959250f5223d697964d987dfa39c65fe4cd4671bb4c00d72738af6/icicle_emu-0.0.11-cp38-abi3-win_amd64.whl", hash = "sha256:94e0da6502ae19ba89497ced98fe0f9e881b13deaf070caaca9a4a552e4e9aa6", size = 3643928, upload-time = "2025-11-06T15:12:57.104Z" },
]
[[package]]
name = "llvm-nanobind"
version = "0.0.1"
source = { git = "https://github.com/LLVMParty/llvm-nanobind#a681b3656e868fc085ce377fbe395007ff991716" }
[[package]]
name = "pefile"
version = "2024.8.26"
@@ -58,3 +77,52 @@ sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
]
[[package]]
name = "ruff"
version = "0.15.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" },
{ url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" },
{ url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" },
{ url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" },
{ url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" },
{ url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" },
{ url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" },
{ url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" },
{ url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" },
{ url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" },
{ url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" },
{ url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" },
{ url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" },
{ url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" },
{ url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" },
]
[[package]]
name = "ty"
version = "0.0.34"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/69/e24eefe2c35c0fdbdec9b60e162727af669bb76d64d993d982eb67b24c38/ty-0.0.34.tar.gz", hash = "sha256:a6efe66b0f13c03a65e6c72ec9abfe2792e2fd063c74fa67e2c4930e29d661be", size = 5585933, upload-time = "2026-05-01T23:06:46.388Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/7b/8b85003d6639ef17a97dcbb31f4511cfe78f1c81a964470db100c8c883e7/ty-0.0.34-py3-none-linux_armv6l.whl", hash = "sha256:9ecc3d14f07a95a6ceb88e07f8e62358dbd37325d3d5bd56da7217ff1fef7fb8", size = 11067094, upload-time = "2026-05-01T23:06:21.133Z" },
{ url = "https://files.pythonhosted.org/packages/d7/25/b0098f65b020b015c40567c763fc66fffbec88b2ba6f584bca1e92f05ebb/ty-0.0.34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0dccffd8a9d02321cd2dee3249df205e26d62694e741f4eeca36b157fd8b419f", size = 10840909, upload-time = "2026-05-01T23:06:18.409Z" },
{ url = "https://files.pythonhosted.org/packages/e4/55/5e4adcf7d2a1006b844903b27cb81244a9b748d850433a46a6c21776c401/ty-0.0.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0ea47a2998e167ab3b21d2f4b5309a9cf33c297809f6d7e3e753252223174d0", size = 10279378, upload-time = "2026-05-01T23:06:37.962Z" },
{ url = "https://files.pythonhosted.org/packages/4d/91/f537dca0db8fe2558e8ab04d8941d687b384fcc1df5eb9023b2db75ac26c/ty-0.0.34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37da00b41a118a459ae56d8947e70651073fb33ebfbceb820e4a10b22d5023", size = 10817423, upload-time = "2026-05-01T23:06:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c4/55a3ad1da2815af1009bdc1b8c90dc11a364cd314e4b48c5128ba9d38859/ty-0.0.34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81cbbb93c2342fe3de43e625d3a9eb149633e9f485e816ebf6395d08685355d8", size = 10851826, upload-time = "2026-05-01T23:06:24.198Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8c/9c7606af22d73fb43ea4369472d9c66ece11231be73b0efe8e3c61655559/ty-0.0.34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5b4dea1594a021289e172582df9cde7089dce14b276fc650e7b212b1772e12", size = 11356318, upload-time = "2026-05-01T23:06:51.139Z" },
{ url = "https://files.pythonhosted.org/packages/20/54/bb423f663721ab4138b216425c6b55eaefd3a068243b24d6d8fe988f4e13/ty-0.0.34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030fb00aa2d2a5b5ae9d9183d574e0c82dae80566700a7490c43669d8ece40cd", size = 11902968, upload-time = "2026-05-01T23:06:35.82Z" },
{ url = "https://files.pythonhosted.org/packages/b6/22/01122b21ab6b534a2f618c6bbe5f1f7f49fd56f4b2ec8887cd6d40d08fb3/ty-0.0.34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae9555e24e36c63a8218e037a5a63f15579eb6aa94f41017e57cd41d335cfb5", size = 11548860, upload-time = "2026-05-01T23:06:42.155Z" },
{ url = "https://files.pythonhosted.org/packages/d1/50/86008b1392ec64bed1957bbcc7aaa43b466b50dfc91bb131841c21d7c5c3/ty-0.0.34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99eb23df9ed129fc26d1ab00d6f0b8dfe5253b09c2ac6abdb11523fa70d67f10", size = 11457097, upload-time = "2026-05-01T23:06:53.477Z" },
{ url = "https://files.pythonhosted.org/packages/92/3e/4558b2296963ba99c58d8409c57d7db4f3061b656c3613cb21c02c1ef4c2/ty-0.0.34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85de45382016eceae69e104815eb2cfa200787df104002e262a86cbd43ed2c02", size = 10798192, upload-time = "2026-05-01T23:06:40.004Z" },
{ url = "https://files.pythonhosted.org/packages/76/bf/650d24402be2ef678528d60caac1d9477a40fc37e3792ecef07834fd7a4a/ty-0.0.34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:14cb575fb8fa5131f5129d100cfe23c1575d23faf5dfc5158432749a3e38c9b5", size = 10890390, upload-time = "2026-05-01T23:06:33.076Z" },
{ url = "https://files.pythonhosted.org/packages/5c/ef/ccd2ca13906079f7935fd7e067661b24233017f57d987d51d6a121d85bb5/ty-0.0.34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c6fc0b69d8450e6910ba9db34572b959b81329a97ae273c391f70e9fb6c1aade", size = 11031564, upload-time = "2026-05-01T23:06:55.812Z" },
{ url = "https://files.pythonhosted.org/packages/ba/2d/d27b72005b6f43599e3bcabab0d7135ac0c230b7a307bb99f9eea02c1cda/ty-0.0.34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:30dfcec2f0fde3993f4f912ed0e057dcbebc8615299f610a4c2ddb7b5a3e1e06", size = 11553430, upload-time = "2026-05-01T23:06:31.096Z" },
{ url = "https://files.pythonhosted.org/packages/a7/12/20812e1ad930b8d4af70eebf19ad23cff6e31efcfa613ef884531fcdbaa1/ty-0.0.34-py3-none-win32.whl", hash = "sha256:97b77ddf007271b812a313a8f0a14929bc5590958433e1fb83ef585676f53342", size = 10436048, upload-time = "2026-05-01T23:06:49.108Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6a/afa095c5987868fbda27c0f731146ac8e3d07b357adfa83daccaee5b1a16/ty-0.0.34-py3-none-win_amd64.whl", hash = "sha256:1f543968accb952705134028d1fda8656882787dbbc667ad4d6c3ba23791d604", size = 11462526, upload-time = "2026-05-01T23:06:28.514Z" },
{ url = "https://files.pythonhosted.org/packages/63/8f/bf041a06260d77662c0605e56dacfe90b786bf824cbe1aed238d15fe5e84/ty-0.0.34-py3-none-win_arm64.whl", hash = "sha256:ea09108cbcb16b6b06d7596312b433bf49681e78d30e4dc7fb3c1b248a95e09a", size = 10846945, upload-time = "2026-05-01T23:06:44.428Z" },
]