멀웨어 파일을 실행하면 위 실행파일이 하나 더 생긴다 해당 파일명을 OSINT 해보았다.

anyrun 분석 보고서를 하나 찾았다. 악성코드는 각각을 식별하기 위해 바이너리 내용으로 암호 해쉬값을 생성한다. MD5, SHA1, SHA256이 작성되어 있는 걸 알 수 있다.

실행하는 즉시 시작되는 malicious 파일을 나열한 거 같다.

probably downloading payload..를 보면 iexplore.exe임을 알 수 있다. 보안 정책/설정 등은 winRAR.exe로 읽고, 이외에 악성 행위들은 모두 MariyelsTherapy.exe에서 했음을 알 수 있다.
iexplore.exe는 인터넷 익스플로러 브라우저를 여는 파일이다.
실행 파일 처음 열었을 때이다.
상단바에 모듈을 확인하면 ntdll.dll이다. 메인 함수가 있는 (파일명).exe로 가야 한다.

보통 디버거에서 자동으로 중단점 설정을 해주는데 이때 메인 코드가 있는 곳에 중단점이 설정되어 있을 확률이 높다.
stackframe_practice.exe인 중단점 아무거나 더블 클릭하면 모듈이 stackframe_practice.exe로 넘어간다.

스터디 시간에 배운 것처럼
push ebp
mov ebp, esp의 형태를 띄는 걸 알 수 있다.

무슨 함수 호출을 볼까 하다가 malloc 함수 call을 자세히 보기로 했다.

malloc으로 들어가기 전 레지스터 상태이다.

f7을 눌러 malloc 안으로 들어가도록 했다. eip 옆 주석을 보면 msvcrt.malloc임을 알 수 있다. msvcrt는 일종의 라이브러리, malloc 함수가 위치한 모듈 정도로 이해했다.

이 함수에 들어오면 역시나
push ebp
mov ebp, esp
가 반긴다.

push ebp를 실행하면

ebp에 있던 값이

스택 상단에 배치된 걸 확인할 수 있다. 현재 스택 프레임은 다음과 같다.
| 스택 주소 |
배치된 값 |
주석 |
| 0061FED8 |
0061FF68 |
|
| 0061FEDC |
004012E7 |
stackframe_practice로 가는 ret |

esp는 ebp의 값을 넣어준 주소가 된다.

다음 mov ebp, esp를 해주면

ebp가 esp가 있는 곳까지 올라오면서 동일해진다. 이제 malloc 함수의 데이터 범위가 지정된 것이다. ebp를 malloc의 땅바닥이라 생각하고 스택을 쌓아올리면 된다.
malloc의 함수 종료 부분도 살펴보자

pop esi
pop ebp
ret

pop ebp를 하면 해당 스택이 줄어들고

값이 ebp에 넣어진다. esp는 스택이 줄어들어서 0061FEDC임을 알 수 있다.

0061FEDC를 보면 return address임을 알 수 있다.

ret를 실행하면

위에 0061FEDC에 저장되었던 값 004012E7로 이동했음을 알 수 있다.

EBP와 ESP를 보면 malloc으로 들어가기 전 EBP, ESP 상태와 같다.

일단 imageifo가 오류난다

소문자로 해줘야 했다

ip니까 netscan을 써야 할 거 같았다

왼쪽 PID가 제대로 인식을 못하는 거 같아서 의심스러운 부분이지만 확실한 근거가 없어서 일단 넘겼다

cmdline을 검색했다

해당 문서 이름이 수상하다

dumpfiles를 이렇게 하면 안되는 거 같다

올바르게 입력해줬다 근데 이건 notepad.exe 프로세스 덤프라 버렸다

그냥 filescan을 하면 너무 많이 나와서 파이프로 findstr txt검색했더니
SecreetDocume7.txt의 오프셋이 나온다

오프셋을 이용해서 파일을 덤프해준다. 확장자가 txt가 아니라서 어떻게 하나 했는데 strings.exe라는 툴을 설치하면 된다

위처럼 txt 변환해준다

Key가 올바르게 들어있다!
2번,3번은 풀었는데 1번 김장군 PC의 IP주소가 맞는지 의심만 가고 확실한 근거가 없어서 라업을 참고하기로 했다.

netscan을 돌려보면 나오는 ip 주소는 아래 3개이다.
127.0.0.1
0.0.0.0
192.168.197.138
이중에서 1번째는 localhost이고, 2번째 0은 지정하는 ip없이 모든 ip를 의미하므로 김장군 IP주소는 192.168.197.138이라고 한다.
192.168.197.138SecreetDocume7.txt4rmy_4irforce_N4vy

여기서 소문자로 바꿔줘서 c152e3fb5a6882563231b00f21a8ed5f이 답이다
드림핵에서 수강한 내용에 추가해서 문서화했다.
- 메모리 구조
메인 메모리(RAM)과 보조 저장장치 등등이 있다.
보조 저장장치에 있던 프로그램 -> 실행 -> 메인 메모리에 위치한 프로세스가 됨

실제 메모리는 여기저기 정렬되어 있지 않지만 virtual address로 프로세스에서는 규칙을 가짐
Memory Layout
메모리 레이아웃: 가상 메모리의 구성
섹션
데이터가 모여있는 영역
윈도우의 PE 파일: PE 헤더 + 1개 이상의 섹션
PE 헤더에 담긴 섹션 데이터: 이름, 크기, 오프셋, 속성 및 권한
이 헤더 정보를 참조하여 가상 메모리의 적절한 세그먼트에 매핑
1. .text
실행 가능한 기계 코드
읽기, 실행 권한 부여, 쓰기 권한은 거의 대부분 제거
int main() { return 31337; }
2. .data
컴파일 시점 값이 정해진 전역 변수 위치
읽기, 쓰기 권한
int data_num = 31337;
char data_rwstr[] = "writable_data"; //data
3. .rdata
컴파일 시점 값이 정해진 전역 상수와 참조할 DLL 및 외부 함수들 정보
읽기 권한
const char data_rostr[] = "readonly_data";
char *str_ptr = "readonly"; //str_ptr은 .data, 문자열("readonly")은 .rdata
섹션 외 메모리
1. 스택
각 스레드 본인만의 스택 존재
지역 변수, 함수의 리턴 주소 저장 (ex. 매개변수, 지역변수, return address…)
읽기, 쓰기 권한
아래로 확장: 낮은 주소로 업데이트
2. 힙
모든 종류 데이터 저장 가능
비교적 스택보다 큰 데이터 저장 가능
전역적 접근 가능, 동적 할당
int main() {
int *heap_data_ptr = malloc(sizeof(*heap_data_ptr)); //동적 할당한 힙 영역 주소를 가리킴
*heap_data_ptr = 31337; //힙 영역에 값 작성
return 0;
}
heap_data_ptr은 스택, malloc으로 할당받은 힙 영역을 가리킴
- 스택 프레임
특정 함수가 가지는 공간 구조를 말한다.
함수가 접근할 수 있는 데이터 범위 지정
ESP, EBP 같은 포인터 레지스터와 함께 한다.
함수 호출 규약: 함수의 호출 및 반환에 대한 약속
한 함수에서 다른 함수 호출 -> 실행 흐름 옮기기 -> 호출한 함수 반환 -> 실행 흐름 옮기기
이 과정에서 Caller의 stack frame과 return address 저장 필요 <- 이래야 반환 후 실행 흐름을 옮길 수 있음
호출 시 Callee가 요구하는 인자 전달 필요, 반환 시 Caller에게 반환값 전달 필요
해당 호출 규약은 x86 아키텍처에서 쓰이는 cdecl
스택을 통해 인자 전달
인자 전달을 위해 caller가 스택 정리
스택이 아래로 자라므로 마지막 인자부터 거꾸로 push
cdecl 함수 호출 규약 실습
// Name: cdecl.c
// Compile: gcc -fno-asynchronous-unwind-tables -nostdlib -masm=intel \
// -fomit-frame-pointer -S cdecl.c -w -m32 -fno-pic -O0
void __attribute__((cdecl)) callee(int a1, int a2) { // cdecl로 호출
}
void caller() {
callee(1, 2);
}
; Name: cdecl.s
.file "cdecl.c"
.intel_syntax noprefix
.text
.globl callee
.type callee, @function
callee:
nop
ret ; 스택을 정리하지 않고 리턴합니다.
.size callee, .-callee
.globl caller
.type caller, @function
caller:
push 2 ; 2를 스택에 저장하여 callee의 인자로 전달합니다.
push 1 ; 1을 스택에 저장하여 callee의 인자로 전달합니다.
call callee
add esp, 8 ; 스택을 정리합니다. (push를 2번하였기 때문에 8byte만큼 esp가 증가되어 있습니다.)
nop
ret
.size caller, .-caller
.ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0"
.section .note.GNU-stack,"",@progbits
위 어셈블리를 보면, push 2, push 1로 callee의 인자값 오른쪽부터 push해주고 있다.
스택 상태가 위와 같아진다. 이제 call callee를 해주면 스택에 ret가 push된다.
ebp의 값을 push해주고, mov ebp,esp로 ebp를 끌어올려 함수의 데이터 범위를 재설정한다.
아까 코드에 없던 이유는 아마 해당 작업들이 callee에서 실행되기 때문이라고 생각한다.
지역변수를 넣어준다(없다면 생략)
변수를 넣어주면 esp는 올라가면서 esp, ebp의 범위가 다시 벌어질 텐데 해당 범위가 callee 함수가 쓸 수 있는 데이터 영역이다.
이제 함수를 종료해볼 차례이다. mov esp, ebp로 이번엔 esp에 ebp값을 옮겨서 esp를 내려준다.
*아까 위 코드 블럭에서는 8바이트만큼 할당해서 add esp, 8로 mov를 대체했다.
이후 pop ebp를 해준다. 이렇게 하면 main의 ebp값이 pop되면서 ebp가 main의 스택 프레임 범위로 복귀한다.
위에서 이제 RETN을 실행하면 esp가 가리키는 ret로 흐름이 바뀌면서 caller로 돌아오게 된다.
callee의 반환값은 레지스터(eax)에 저장된다.
함수가 시작할 때 보통
push ebp
mov ebp, esp
가 실행된다.
드림핵에서 수강한 내용에 추가해서 문서화했다.
1. 컴퓨터 구조
컴퓨터 기능에 대한 설계(폰 노이만 구조, 하버드 구조, 수정된 하버드 구조)+(CPU가 처리해야 하는)명령어 집합 구조(ARM, MIPS, x86, x86-64)+ 마이크로 아키텍처(CPU의 하드웨어적 설계)+기타 하드웨어 및 컴퓨팅 방법 설계(직접 메모리 접근)
연산+제어+저장
연산과 제어는 CPU: 산술논리장치+제어장치+레지스터
저장은 기억장치(memory)가: 주기억장치(RAM) 혹은 보조기억장치(하드 드라이브)
데이터 및 신호 교환은 버스(bus): 데이터 버스, 주소 버스, 제어 버스(읽쓰 제어)

CPU의 구성
- ALU: 연산장치
- CU: 메모리에서 기계어 코드 읽고 실행
- 레지스터: CPU의 데이터 저장소
CPU의 동작
- CU에서 명령어 처리
- ALU에서 연산
- 데이터는 레지스터에 저장
- 외부 통신은 bus 사용
2. 레지스터
범용 레지스터, 세그먼트 레지스터, 명령어 포인터 레지스터, 플래그 레지스터 존재
레지스터 간에는 위와 같이 호환이 가능하다. x64의 경우에는 8 바이트, x32의 경우에는 4 바이트이다(그림에 ax는 2 바이트, ah al는 1바이트)
1. 범용 레지스터
8바이트까지 저장, unsigned 정수 2^64-1
| x64 |
x32 |
|
| rax (accumulator) |
eax |
함수의 반환값 |
| rbx (base) |
ebx |
|
| rcx (counter) |
ecx |
반복문의 반복 횟수, 연산 시행 횟수 |
| rdx (data) |
edx |
|
| rsi (source index) |
esi |
데이터 옮길 때 원본 |
| rdi (destination index) |
edi |
데이터 옮길 때 목적지 |
| rsp (stack pointer) |
esp |
사용중인 스택 위치 |
| rbp (stack base pointer) |
ebp |
스택의 바닥 |
2. 세그먼트 레지스터
cs, ss, ds, es, fs, gs 각각 16바이트 크기
cs(코드), ds(데이터), ss(스택)
3. 명령어 포인터 레지스터
CPU가 실행할 부분을 가리키는 레지스터: rip
8바이트 크기
4. 플래그 레지스터
프로세서 현재 상태 저장하는 레지스터
x64 아키텍처에서 RFLAGS라는 64비트 레지스터 존재 (32비트는 EFLAGS)
| 플래그 |
|
| CF (Carry) |
부호 없는 수의 연산 결과 > 비트 범위 |
| ZF (Zero) |
연산 결과 0 |
| SF (Sign) |
연산 결과 음수 |
| OF (Overflow) |
부호 있는 수 연산 결과 > 비트 범위 |
3. 어셈블리어
x64 어셈블리 언어
기본 구조: 명령어(opcode) 연산자(operand)
연산자로 올 수 있는 것
- 상수
- 레지스터
- 메모리
크기 지정자+[] 형식: []의 데이터를 크기 지정자만큼 참조
참고: [rbx+8]이면 rbx부터 8바이트가 아니고 rbs+8부터 참조
명령어로 올 수 있는 것
1. 데이터 이동(Data Transfer)
mov dst, src : src 값을 dst에 대입
lea dst, src : src의 유효 주소를 dst에 저장
2. 산술 연산(Arithmetic)
add dst, src : dst에 src 더함
sub dst, src : dst에 src 뺌
inc op : op의 값 1 증가
dec op : op의 값 1 감소
mul dst, src:
div dst, src
3. 논리 연산(Logical)
and dst, src : and 연산, f는 그대로 0은 0
or dst, src : or 연산, f는 f 0은 그대로
xor dst, src : xor 연산
not op : 비트 반전, neg로도 쓰임
4. 비교 (Comparison)
피연산자 값 비교 후 플래그 설정
cmp op1, op2 : 두 피연산자를 빼서 비교
| 비교 |
플래그 레지스터.. |
| op1이 더 크다면 |
AF=1 |
| op2와 같다면 |
PF=1, AF=0, ZF=1 |
| op2가 더 크다면 |
CF=1, PF=0, AF=1, ZF=0, SF=1 |
test op1, op2 : 두 피연산자를 AND연산해서 비교
5. 분기(Branch)
rip 이동시킴, 다양함
jmp addr : addr로 점프
je addr : 직전 비교한 두 피연산자 같으면 점프 jump if equal
jg addr : 직전 비교한 두 연산자 중 전자가 더 크면 점프 jump if greater
ja addr : CF=0 AND ZF=0이면 분기
jz addr: ZF=1이면 분기
jb addr: CF=1이면 분기
6. 스택(Stack)
운영체제 핵심 자료구조
push val : val을 스택 최상단에 쌓음
pop reg : 스택 최상단 값 꺼내서 reg에 대입
7. 프로시져(Procedure)
C언어 함수에 대응
반복되는 연산 -> 프로시저 호출로 대체 가능
프로시저를 호출하고 반환, call 다음 명령어 주소를 스택에 저장하고 rip로 이동시킴
call addr : addr에 위치한 프로시저 호출, push return_address(call 다음 주소임) -> jmp addr
여기까지 하면 스택 rsp에 리턴 주소(call 다음 명령어 주소) 저장됨
기존 스택 프레임 저장을 위해 rbp를 push함
그리고 새로운 스택 프레임 생성… rbp를 rsp로 옮김, 공간 확장을 위해 rsp를 빼줌(아래로 자라니까)
이제 새로운 스택 프레임이 생성되었으니 지역변수 할당하고 연산 처리
leave : 스택프레임 정리, mov rsp, rbp -> pop rbp
rbp를 꺼내서 원래의 스택 프레임으로 돌아감
ret : return address로 반환, pop rip
반환 주소 꺼내서 원래의 실행 코드로 돌아감
8. 반복

스터디에서 배운 이미지를 가져왔다.
1: eax에 10 넣기
2: INC로 10+1 해주기
3: CMP로 비교.. 20이 더 큼 op2가 더 크면 CF=1
4: CF=1이면 jmp하므로 00401015로 점프
5: EAX가 20이 될 때까지 반복될 것
4. 디버거 사용법

x32dbg를 처음 열면 위와 같은 화면이 뜬다. 좌측 상단 큰 화면은 어셈블리 코드가 실행되는 주화면이다. 해당 부분에 코드가 적혀있으므로 메인창으로 사용한다.
이제 어떤 함수의 매개변수나 반환값은 호출규약에 의해 레지스터에 저장된다. 레지스터는 우측에서 확인할 수 있다. 해당 화면 스크롤을 내려보면 범용 레지스터뿐 아니라 플래그 등 모든 레지스터를 확인할 수 있다.
덤프 아래 화면은 hex 코드를 보여준다. 레지스터나 메모리 등 덤프1,2,3,4,5에서 확인할 수 있다.
우측 하단 작은 화면은 stack 메모리를 보여준다.
단축키는 아래와 같다.
| 단축키 |
|
| F9 |
프로그램 실행 (중단점이 있다면 중단점까지) |
| Ctrl + F9 |
함수 내로 들어가서 그 함수 ret 이전까지 코드 실행 |
| F2 |
중단점 설정 |
| Ctrl + F2 |
프로그램 재시작 |
| F7 |
call된 함수로 들어가서 한 줄 실행 |
| F8 |
call된 함수로 들어가지 않고 한 줄 실행 |
| Enter |
코드 미리 보기 (dst로 이동) |
| Ctrl + G |
사용자가 입력한 주소로 이동 |
| Ctrl + E |
사용자가 지정한 위치에 있는 데이터 수정 |
| ; |
주석 |
| -, + |
명령어 위치(커서) 이동, 실제로 실행하지는 않음 |