Skip to content

add srop function#148

Merged
Kyle-Kyle merged 14 commits intoangr:masterfrom
lbr77:master
Jan 20, 2026
Merged

add srop function#148
Kyle-Kyle merged 14 commits intoangr:masterfrom
lbr77:master

Conversation

@lbr77
Copy link
Contributor

@lbr77 lbr77 commented Jan 18, 2026

usage:

chain = rop.sigreturn(..regs)
# or 
chain = rop.sigreturn_syscall(0x3b, [next(elf.search(b"/bin/sh\x00")), 0, 0])

Copilot AI review requested due to automatic review settings January 18, 2026 09:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds Sigreturn-Oriented Programming (SROP) functionality to the angrop library, enabling users to build ROP chains that leverage the sigreturn system call to set arbitrary register values. The PR introduces two new files implementing the SROP framework and integrates them into the existing chain builder infrastructure.

Changes:

  • Added SigreturnFrame class to serialize sigreturn frames for multiple architectures (i386, amd64, ARM, MIPS, AARCH64)
  • Added SigreturnBuilder class with sigreturn() and sigreturn_syscall() methods for building SROP chains
  • Extended architecture definitions with sigreturn syscall numbers for i386 and amd64
  • Modified do_syscall() to support a stack_recover parameter for SROP-specific behavior
  • Added test coverage for amd64 SROP chains

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
angrop/sigreturn.py New file implementing SigreturnFrame class with architecture-specific register layouts and serialization
angrop/chain_builder/sigreturn.py New file implementing SigreturnBuilder with methods to construct SROP chains
angrop/arch.py Added sigreturn_num attribute to base class and set values for X86 (0x77) and AMD64 (0xf)
angrop/chain_builder/init.py Integrated SigreturnBuilder and exposed sigreturn/sigreturn_syscall methods, modified do_syscall signature
angrop/chain_builder/func_caller.py Added stack_recover parameter to _func_call to support non-stack-recovering gadgets
angrop/chain_builder/sys_caller.py Propagated stack_recover parameter through do_syscall method
tests/test_ropchain.py Added test_sigreturn_chain_amd64 test case and imported SigreturnFrame
docs/pythonapi.md Added documentation example for sigreturn usage
uv.lock Added uv lock file with Python version requirement

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

chain._values = chain._values[:offset_words]
chain.payload_len = offset_words * self.project.arch.bytes
elif offset_words < 0: # drop values to fit offset.
l.warning("Negative offset, %d frame values would be dropped.",-offset_words)
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message uses an incorrect format. The code has l.warning("Negative offset, %d frame values would be dropped.",-offset_words) but the comma should come after the closing quote and there should be a space after the comma. The correct format should be: l.warning("Negative offset, %d frame values would be dropped.", -offset_words).

Suggested change
l.warning("Negative offset, %d frame values would be dropped.",-offset_words)
l.warning("Negative offset, %d frame values would be dropped.", -offset_words)

Copilot uses AI. Check for mistakes.
self._registers = _REGISTERS[arch_name]
self._values = {reg: 0 for reg in self._registers.values()}
self._values.update(_DEFAULTS.get(arch_name, {}))
self._word_size = 8 if arch_name == "amd64" else 4
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word size determination only handles amd64 as 8 bytes, defaulting all other architectures to 4 bytes. However, aarch64 also uses 8-byte word size. This will cause incorrect frame serialization for aarch64. Consider using a more comprehensive check or a mapping dictionary for word sizes per architecture.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

word_size is just arch_bytes. Can you use that instead? right now it is wrong for other arches like aarch64

"i386": {"cs": 0x73, "ss": 0x7b},
"i386_on_amd64": {"cs": 0x23, "ss": 0x2b},
"arm": {"trap_no": 0x6, "cpsr": 0x40000010, "VFPU-magic": 0x56465001, "VFPU-size": 0x120},
"mips": {},
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing default values for mipsel architecture. The _DEFAULTS dictionary has an entry for "mips" (empty dict), but not for "mipsel", which is defined in _REGISTERS and _ARCH_NAME_MAP. This could cause issues when creating sigreturn frames for MIPSEL architecture if default values are needed.

Suggested change
"mips": {},
"mips": {},
"mipsel": {},

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make sure all the references are cleanly aligned?

Comment on lines +116 to +124
if 0 < offset_words < len(chain._values): # should pad to offset(rsp at syscall)
chain._values = chain._values[:offset_words]
chain.payload_len = offset_words * self.project.arch.bytes
elif offset_words < 0: # drop values to fit offset.
l.warning("Negative offset, %d frame values would be dropped.",-offset_words)
frame_words = frame_words[-offset_words:]
elif offset_words > len(chain._values):
for _ in range(offset_words - len(chain._values)):
chain.add_value(filler)
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential logic error: when offset_words equals 0, the frame_words are appended directly without any padding or truncation. However, this case is not explicitly handled in the if-elif chain. While the code will still work (falling through to the final loop), it would be clearer to explicitly handle the offset_words == 0 case or add a comment explaining that no adjustment is needed in this case.

Copilot uses AI. Check for mistakes.
frame.update(**registers)

syscall_num = self.arch.sigreturn_num # syscall(sigreturn)
chain = self.chain_builder.do_syscall(syscall_num, [],stack_recover=False, needs_return=False) # dummy args
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after comma in function call. The code has [] followed immediately by stack_recover without a space after the comma.

Suggested change
chain = self.chain_builder.do_syscall(syscall_num, [],stack_recover=False, needs_return=False) # dummy args
chain = self.chain_builder.do_syscall(syscall_num, [], stack_recover=False, needs_return=False) # dummy args

Copilot uses AI. Check for mistakes.
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@angr angr deleted a comment from Copilot AI Jan 18, 2026
@Kyle-Kyle
Copy link
Collaborator

Kyle-Kyle commented Jan 18, 2026

This srop module looks amazing! I would love to merge it!

But I have a few comments:

  1. what is the semantic meaning of stack_recover here? It feels like what it means is that we are not going to append more gadgets after this one. So it feels its equivalent to needs_return. Since it is not exposed to users anyway. Let's just reuse needs_return.
  2. shall we allow a sp argument to sigreturn_syscall so that if users want to invoke read syscalls (in case there is no /bin/sh in the binary`), they can still ROP.
  3. can you add a i386 testcase? because sigreturn is quite different in 32-bit because all the offsets are different
  4. can you add how to sigreturn_syscall to pythonapi.md?
  5. maybe we can even have a sigreturn_execve variant. (and then we can use it in rop.execve) But you don't have to do it now. You can just add it as a TODO in the code.

@Kyle-Kyle
Copy link
Collaborator

Kyle-Kyle commented Jan 18, 2026

also, maybe we want to print sigreturn payload a bit differently in .pp or it will be too much data. But it can be marked as TODO.

@lbr77
Copy link
Contributor Author

lbr77 commented Jan 19, 2026

  1. what is the semantic meaning of stack_recover here? It feels like what it means is that we are not going to append more gadgets after this one. So it feels its equivalent to needs_return. Since it is not exposed to users anyway. Let's just reuse needs_return.

the usage for stack_recover is to avoid more gadgets appended after syscall gadget (to recover rsp) but its unnecessary to recover after sigreturn(since every register is recovered from frame. ) I think this behavior conflicts with needs_return so added a more argument here. If it means the same, I can merge them together.

@Kyle-Kyle
Copy link
Collaborator

Kyle-Kyle commented Jan 19, 2026

Ah. I see what's going on!
indeed needs_return and stack_recover have the same semantic meaning. But there is a bug in the existing code:

        for delta in range(func_gadget.stack_change//arch_bytes):
            if func_gadget.pc_offset is None or delta != func_gadget.pc_offset:
                chain.add_value(self._get_fill_val())
            else:
                chain.add_value(claripy.BVS("next_pc", self.project.arch.bits))

        # we are done here if we don't need to return
        if not needs_return:
            return chain
        # we are done here if we don't need to return
        if not needs_return:
            return chain

this should be placed before the chain add_value logic. Because if we don't need to return, then why do we specify a next_pc value on the stack?

@lbr77
Copy link
Contributor Author

lbr77 commented Jan 19, 2026

Ah. I see what's going on! indeed needs_return and stack_recover have the same semantic meaning. But there is a bug in the existing code:

        for delta in range(func_gadget.stack_change//arch_bytes):
            if func_gadget.pc_offset is None or delta != func_gadget.pc_offset:
                chain.add_value(self._get_fill_val())
            else:
                chain.add_value(claripy.BVS("next_pc", self.project.arch.bits))

        # we are done here if we don't need to return
        if not needs_return:
            return chain
        # we are done here if we don't need to return
        if not needs_return:
            return chain

this should be placed before the chain add_value logic. Because if we don't need to return, then why do we specify a next_pc value on the stack?

Ok, I'll merge the needs_return and stack_recover together and fix this.

@lbr77
Copy link
Contributor Author

lbr77 commented Jan 19, 2026

also, maybe we want to print sigreturn payload a bit differently in .pp or it will be too much data. But it can be marked as TODO.

I'm implementing this so please merge later (?

@Kyle-Kyle
Copy link
Collaborator

I'm implementing this so please merge later (?

sure. let me know when you are ready :D

@lbr77
Copy link
Contributor Author

lbr77 commented Jan 19, 2026

I'm implementing this so please merge later (?

sure. let me know when you are ready :D

Done. Now:

>>> chain = rop.sigreturn_syscall(0x3b, [next(elf.search(b"/bin/sh\x00")), 0, 0])
>>> chain.pp()
0x000000000040017b: push rax; push rbp; mov rbp, rsp; mov eax, 0xf; syscall ; nop ; pop rbp; ret 
                    <SigreturnFrame for amd64>
                    rdi             : 0x00000000004001ca
                    rax             : 0x000000000000003b
                    rip             : 0x0000000000400185
                    csgsfs          : 0x0000000000000033

Oh, sth went wrong

@Kyle-Kyle
Copy link
Collaborator

Your testcase failure is caused by the badbyte tests. The badbyte write scheduling algorithm is not optimal, which caused it to exhaustively check usable gadgets. And eventually find a weird gadget that triggers a bug in angrop.
I think rebasing it will fix fix the CI issue now.
But the actual issue is in #144

@lbr77
Copy link
Contributor Author

lbr77 commented Jan 20, 2026

Sometime this testcase could run successfully but sometime it fails. It's a weird one.

@Kyle-Kyle
Copy link
Collaborator

@lbr77 I already root caused it and fixed the bug in #150
But the underlying issue is the badbyte write bypass algorithm in #144
It is somehow nondeterministic and can be extremely slow sometimes. (sometimes it is caused by another angrop bug that I'm trying to fix now)

@lbr77
Copy link
Contributor Author

lbr77 commented Jan 20, 2026

I've rebased the commit now.

@lbr77
Copy link
Contributor Author

lbr77 commented Jan 20, 2026

oops, failed again :(

@Kyle-Kyle Kyle-Kyle merged commit 93866cf into angr:master Jan 20, 2026
29 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments