Zj_W1nd's BLOG

DefCon33 Qual

2025/04/14

跟着nu1l的大佬们打了defcon预选。这是一周之后的记录了。队伍里没啥人来,和nu1l的大跌们和做了一把(虽然只是简单题)。

关于题目风格

首先,defcon很明显的,题目的种类很多而且很新。很多东西都很小众变态。

而且常规题目是不提供类型的,除了web以外剩下的多多少少都很综合(观察频道中的师傅们交流得出的结论)。我自己做的两道题目感觉非常misc,俄罗斯方块那道题很好玩。

关于我参与的题目

Nu1l的师傅们很厉害而且人巨多,各个还都是48h连轴转的精力怪物,很多题开题没有跟着做也就没必要讨论了()。我看的两道题恰好还都是misc,有一个出的很好玩。

tetrx

题不难但是很有意思,逆向分析可以发现这玩意实现了一个终端俄罗斯方块,同时用一块0x30的,mmap出来的bitmap存储当前的地形状态信息。

很简单的测试跑完就会发现报错segfault,跟进一下就可以知道,这个bitmap给了可执行权限。在失败之后会直接去执行这块地址!换句话说,只需要一个精简且合适的shellcode,然后通过玩俄罗斯方块的形式,就能直接执行sc,甚至这个程序还给了后门。

但是大家一尝试就发现了,平常的shellcode(比如精简版的pwntools提供的那种)随随便便就会超长度,也就是上方根本没空间给我们做操作,所以需要比较巧妙的sc。硬编码的字符串肯定就不要想了。而且0也要尽可能地少。

调试过程中,本来想使用寄存器+sub/add调整到后门直接jmp,但是没有合适的寄存器。而且add和sub立即数指令会导致0太多,不好构造。

然后思路就转向了重新写,即想办法调read,自己控制这片rwx内存。同时使用pop从栈上拿当前位置的地址,然后xor rdi。

没想到Nu1l里面还有俄罗斯方块大神()给地形就能构造这一块。

1
2
3
4
5
6
7
pop rdi
pop rdx
pop rdi
pop rdi
pop rsi
xor edi,edi
syscall

用类似这样的asm格式构造俄罗斯方块就行。

im-pio-ssible

首先,附件就直接且只提供了源代码,是一个我没听说过的语言swift,是mac上用的。直接AI读源码,发现这玩意实现了两个PIO通信,PIO这玩意全名是可编程IO,是树莓派上的一个专用模块,类似dma,是可以基于时钟独立控制的IO模块。

这里是手册,这玩意谁tm会,不过还好它的汇编指令就9条,现学+AI给翻译脚本,很快也能掌握。

远程有三个json文件,每一个都描述了一个发送数据的目标PIO,通过一个就会进下一个。题目会为每个发送方随机化7个16位数据放在PIO的FIFO存储中,我们需要通过分析配置和时钟匹配,来创建自己的接收方PIO,并且以json格式传进去。发送方的输出FIFO和我们接收方的输入FIFO是一个,只要FIFO内成功接收了这些数据,通过了校验就会给flag。

二进制文件太捞了,源码中实现了DEBUG的信息输出但是没定义开启,如果有一个debug环境就会好很多。另一个师傅找到了一个swift的docker,在docker里编译,开debug,就能本地观察到每条指令执行和PIO的情况了。

一个小插曲是,出题人的程序写错了,将FIFO写成了LIFO。但是最后的结果检查还是用的错误的API,结果就导致顺序其实也没错。刚开始另一个师傅在调试信息发现顺序是烂的之后放弃了,结果有队交了这个题,仔细看了看让我审出来接着做了。

三个关卡的内容方向也有点不一样。

第一个是简单引脚读,开了共享引脚,将数据循环按1位从一个引脚输出。逆完指令集之后做时钟同步从引脚读就行。

1
2
3
4
5
6
7
8
9
program = [
PIOAssembler.set(2,7, sideset=28), # set y = 7
PIOAssembler.set(1,15, sideset=22), # set x = 15
PIOAssembler.set(1,15, sideset=23), # set x = 15
PIOAssembler.in_op(0, 1, 30),
PIOAssembler.jmp(2, 3, sideset=17),
PIOAssembler.jmp(4, 1, sideset=16), # Y--
PIOAssembler.wait(1, 1, 0)
]

第二关开始上难度了。它会先拉取两次数据,然后拿这两个数据做双层循环的计数变量。了解了下swift语法并且逆了下具体的指令实现,发现X和Y寄存器虽然是Uint但是没有判断,jmp x–这种指令会有uint的整数环绕,因此无论如何都会push y-x进去。所以最后理论上拿到x和y-x可以还原y。但是负数太大会爆,因此算两个数x-y和~(x-y)这种就能做。

第三关则是最抽象的,时钟侧信道。

1
2
3
4
5
6
7
8
9
{'type': 'PULL', 'if_empty': 0, 'block': 1, 'sideset': 'do sideset count: 0x0 value: 0x0 mask: 0x0 sideset: 0'}
{'type': 'OUT', 'dest': 'PINS', 'bitcount': 32, 'sideset': 'do sideset count: 0x0 value: 0x0 mask: 0x0 sideset: 0'}
{'type': 'IN', 'src': 'PINS', 'bitcount': 2, 'sideset': 'do sideset count: 0x0 value: 0x0 mask: 0x0 sideset: 0'}
{'type': 'MOV', 'dest': 'Y', 'op': '', 'src': 'ISR', 'sideset': 'do sideset count: 0x0 value: 0x0 mask: 0x0 sideset: 0'}
{'type': 'JMP', 'cond': 'Y--', 'addr': 4, 'sideset': 'do sideset count: 0x0 value: 0x0 mask: 0x0 delay: 5 sideset: 5'}
{'type': 'SET', 'dest': 'PINS', 'data': 0, 'sideset': 'do sideset count: 0x0 value: 0x0 mask: 0x0 sideset: 0'}
{'type': 'IN', 'src': 'PINS', 'bitcount': 32, 'sideset': 'do sideset count: 0x0 value: 0x0 mask: 0x0 sideset: 0'}
{'type': 'MOV', 'dest': 'PINS', 'op': '', 'src': 'NULL', 'sideset': 'do sideset count: 0x0 value: 0x0 mask: 0x0 sideset: 0'}
{'type': 'PUSH', 'if_full': 0, 'block': 1, 'sideset': 'do sideset count: 0x0 value: 0x0 mask: 0x0 delay: 10 sideset: 10'}

它将数据读取到32个引脚之后,用set抹去了两位(置为0了)。然后这两位会做为变量去jmp循环。所以我们只要从时钟对齐拿到循环次数就能推算出这缺的两位是多少。

问题是怎么去标记这个起止时间。最后的想法是利用autopull机制,自动拉取数据(不用手动拉取,并且省时钟),然后用jmp判断OSR寄存器是否为空。精确控制时钟之后

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
36
37
program = [
asm.set(DstReg.Y, 7), # set Y = 3
asm.set(DstReg.X, 11),
asm.label("start"),
asm.jmp(Cond.X_DEC_NZ, "start"), # jmp x--
# 原始状态机中第 9条指令执行后即push 数据
# autopull 需要在第9条指令后第10条指令前才能自动获取数据
# ins count: 10
asm.jmp(Cond.OSRE, "n_0"), # osr 非空则跳转, n=0
asm.set(DstReg.X, 0, sideset=5),
asm.jmp(Cond.OSRE, "n_1"), # osr 非空则跳转, n=1
asm.set(DstReg.X, 0, sideset=5),
asm.jmp(Cond.OSRE, "n_2"), # osr 非空则跳转, n=2
asm.set(DstReg.X, 0, sideset=5),
# n=3
asm.out(DstReg.PINS, 32),
asm.set(DstReg.PINS, 3),
asm.jmp(Cond.ALWAYS, "n_end"),
# n=2
asm.label("n_2"),
asm.out(DstReg.PINS, 32),
asm.set(DstReg.PINS, 2),
asm.jmp(Cond.ALWAYS, "n_end"),
# n=1
asm.label("n_1"),
asm.out(DstReg.PINS, 32),
asm.set(DstReg.PINS, 1),
asm.jmp(Cond.ALWAYS, "n_end"),
# n=0
asm.label("n_0"),
asm.out(DstReg.PINS, 32),
asm.label("n_end"),
asm.mov(DstReg.ISR, MovOp.NONE, SrcReg.PINS),
asm.push(),
asm.mov(DstReg.PINS, MovOp.NONE, SrcReg.NULL),
asm.wait(1, 1, 0),
]

一些总结

48h连轴转首先肯定不是专门打或者有一个人很多的team肯定遭不住,打高分肯定是要连轴转的。另外这些师傅是真的nb。

defcon的题目风格和国内差的挺多的,

CATALOG
  1. 1. 关于题目风格
  2. 2. 关于我参与的题目
    1. 2.1. tetrx
    2. 2.2. im-pio-ssible
  3. 3. 一些总结